Source code for phenotypic.gui.tune._app

"""Dash app factory for the ``/tune/`` co-pilot.

Mirrors :mod:`phenotypic.gui.results_viewer._app` and
:mod:`phenotypic.gui.analysis._app`: builds a configured :class:`dash.Dash`
whose layout is the tune page for a bound :class:`~phenotypic.gui.tune.TuneRunRoot`,
or the pick-a-run empty state when no run is bound.

The factory is deliberately **optuna-free** at import / build time: the live
study is opened only inside the Monitor poll callback (gated on the ``tune``
extra being importable), never here. Importing this module — and therefore
:mod:`phenotypic.gui.tune` — must not pull ``optuna`` into ``sys.modules``.
"""
from __future__ import annotations

import logging
from pathlib import Path
from typing import Optional

import dash
import dash_bootstrap_components as dbc  # type: ignore[import-untyped]

from phenotypic.gui._config import (
    CFG_RUNNER,
    CFG_RUN_REGISTRY,
    CFG_SANDBOX_ROOT,
    MOUNT_HOME,
    TITLE_TUNE,
)
from phenotypic.gui._design import inject_design_tokens
from phenotypic.gui._shared import register_shared_static
from phenotypic.gui.tune._callbacks import register_callbacks
from phenotypic.gui.tune._layout import build_layout
from phenotypic.gui.tune._run_root import TuneRunRoot

if False:  # TYPE_CHECKING guard kept lightweight (avoid eager sandbox import cost)
    from phenotypic.gui.shell._sandbox import SandboxRoot

logger = logging.getLogger(__name__)


[docs] def create_app( root: Optional[TuneRunRoot] = None, *, url_prefix: str = MOUNT_HOME, sandbox: "Optional[SandboxRoot]" = None, registry: object | None = None, runner: object | None = None, ) -> dash.Dash: """Build a configured Dash instance for the tune co-pilot. Args: root: Validated tune output handle (see :meth:`phenotypic.gui.tune.TuneRunRoot.discover`). ``None`` mounts the empty state: the page renders the pick-a-run prompt and the run picker, and the user binds a run at runtime (the bind callback writes ``TUNE_RUN_ROOT_STORE`` and swaps in the loaded views). A non-``None`` ``root`` pre-binds the run (standalone launch / the loaded standalone capture). url_prefix: Mount-point prefix passed to ``dash.Dash`` as ``requests_pathname_prefix``; ``routes_pathname_prefix`` stays ``MOUNT_HOME`` so the DispatcherMiddleware-stripped path matches. Standalone launches collapse to ``MOUNT_HOME`` ("/"); :func:`phenotypic.gui.shell._app.compose_hub` passes ``MOUNT_TUNE``. sandbox: The frozen-at-launch sandbox root. Both the runtime run picker (Chunk C) and the Curate view's Image Source picker (Chunk B-ii) need it to bound directory selection / plate loads on a shared SSH tunnel; the hub composer passes it. When ``None`` both pickers degrade to a note (the standalone-without-sandbox path); a pre-bound ``root`` still renders its views. Returns: A configured :class:`dash.Dash`; ``app.run(...)`` is the caller's responsibility. Notes: Callbacks are registered **unconditionally** (the factory always calls :func:`~phenotypic.gui.tune._callbacks.register_callbacks`), even for the empty-state mount. ``suppress_callback_exceptions=True`` makes that safe: the loaded views' components only appear after a run is bound at runtime, but their callbacks must already be wired so they fire once the bind callback swaps the body in. The bind + view callbacks all key off ``TUNE_RUN_ROOT_STORE``, so runtime binding makes them render. """ assets_folder = str(Path(__file__).parent / "_assets") app = dash.Dash( __name__, external_stylesheets=[dbc.themes.BOOTSTRAP], suppress_callback_exceptions=True, assets_folder=assets_folder, title=TITLE_TUNE, # See results_viewer/analysis _app.py: the dispatcher strips the mount # prefix before Dash routes, so requests build URLs with the prefix but # routing answers at "/". requests_pathname_prefix=url_prefix, routes_pathname_prefix=MOUNT_HOME, ) inject_design_tokens(app) register_shared_static(app.server) if sandbox is not None: app.server.config[CFG_SANDBOX_ROOT] = str(sandbox.root) if registry is not None: app.server.config[CFG_RUN_REGISTRY] = registry if runner is not None: app.server.config[CFG_RUNNER] = runner app.layout = build_layout(root, sandbox=sandbox) register_callbacks(app, sandbox=sandbox) if root is None: logger.debug( "Tune co-pilot built in empty-state mode (url_prefix=%s); " "run binds at runtime via the picker.", url_prefix, ) else: logger.info("Tune co-pilot ready: run=%s", root.path) return app
__all__ = ["create_app"]