Source code for phenotypic.gui.results_viewer.__main__
"""CLI launcher for the PhenoTypic results viewer.
Boots a Dash server hosting an OpenSeadragon-backed interactive viewer
over a CLI output directory produced by ``python -m phenotypic``. The
typical workflow is to run this on a cluster login or compute node and
SSH-tunnel the chosen port back to a workstation::
ssh -L 8050:localhost:8050 user@cluster
# on the cluster:
cd <output-root>
uv run python -m phenotypic.gui.results_viewer
Then point a local browser at ``http://localhost:8050/``.
Examples:
Default port, current working directory as the output root::
uv run python -m phenotypic.gui.results_viewer
Explicit output root + port + Dash debug mode::
uv run python -m phenotypic.gui.results_viewer \\
--output-root /scratch/$USER/run-2026-01 \\
--port 9000 \\
--debug
"""
from __future__ import annotations
import argparse
import logging
from pathlib import Path
from typing import Optional, Sequence
from phenotypic.gui._config import (
DEFAULT_HOST,
DEFAULT_PORT,
DEFAULT_URL_PREFIX,
SSH_TUNNEL_HINT,
TITLE_VIEWER,
VIEWER_CACHE_DIRNAME,
add_launcher_args,
configure_launcher_logging,
print_launcher_banner,
)
from phenotypic.gui.results_viewer._app import create_app
from phenotypic.gui.results_viewer._output_root import OutputRoot
logger = logging.getLogger(__name__)
[docs]
def launch_results_viewer(
output_root: Path | str = Path.cwd(),
host: str = DEFAULT_HOST,
port: int = DEFAULT_PORT,
debug: bool = False,
url_prefix: str = DEFAULT_URL_PREFIX,
) -> None:
"""Boot the Dash results viewer against an output root.
Validates the directory layout via
:meth:`OutputRoot.discover`, builds the Dash app via
:func:`phenotypic.gui.results_viewer._app.create_app`, prints a
one-shot startup banner with SSH-tunnel and cache-nuke hints, and
finally hands control to ``app.run`` (which blocks until the user
interrupts).
Args:
output_root: Path to a CLI output directory (the one containing
``master_measurements.parquet`` and ``results/``). Strings
are accepted for ergonomics; both are resolved to an
absolute :class:`pathlib.Path`. Defaults to the current
working directory.
host: Interface to bind. :data:`DEFAULT_HOST` keeps the server
loopback-only -- pair with SSH port forwarding for remote
access. ``0.0.0.0`` exposes the app on the network.
port: TCP port to bind. Defaults to :data:`DEFAULT_PORT`.
debug: Run Dash in debug mode (auto-reload + verbose
tracebacks). Defaults to ``False``.
url_prefix: Browser-visible path prefix for path-stripping
reverse proxies such as Open OnDemand. Defaults to ``"/"``.
Raises:
FileNotFoundError: If *output_root* does not contain a valid CLI
output layout.
ValueError: If the master measurements parquet is missing
required ``Metadata_Dataset`` / ``Metadata_ImageFile``
columns.
"""
root = Path(output_root).resolve()
output = OutputRoot.discover(root)
app = create_app(output, url_prefix=url_prefix)
cache_dir = root / VIEWER_CACHE_DIRNAME
print_launcher_banner(
title=TITLE_VIEWER,
host=host,
port=port,
root=root,
url_prefix=url_prefix,
extra_lines=(f"Clear tile cache : rm -rf {cache_dir}",),
)
app.run(host=host, port=port, debug=debug)
def _build_parser() -> argparse.ArgumentParser:
"""Build the argparse parser used by :func:`main`.
Returns:
A configured :class:`argparse.ArgumentParser` with
``--output-root``, ``--host``, ``--port``, and ``--debug``
flags. Defaults match :func:`launch_results_viewer`.
"""
parser = argparse.ArgumentParser(
prog="python -m phenotypic.gui.results_viewer",
description=(
"Launch the PhenoTypic results viewer (Dash + OpenSeadragon) "
"over a CLI output directory. Browse, filter, and pixel-zoom "
"per-image overlays produced by `python -m phenotypic`. "
f"Reach the UI by SSH-tunneling the chosen port (`{SSH_TUNNEL_HINT}`)."
),
)
parser.add_argument(
"--output-root",
type=Path,
default=Path.cwd(),
help=(
"Path to the CLI output directory containing "
"`master_measurements.parquet` and `results/<dataset>/overlays/`. "
"Defaults to the current working directory."
),
)
add_launcher_args(parser)
return parser
def main(argv: Optional[Sequence[str]] = None) -> None:
"""Module-level entry point invoked by ``python -m``.
Args:
argv: Optional argument vector for testing. ``None`` (the
default) lets argparse read from ``sys.argv[1:]``.
"""
args = _build_parser().parse_args(argv)
configure_launcher_logging(debug=args.debug)
launch_results_viewer(
output_root=args.output_root,
host=args.host,
port=args.port,
debug=args.debug,
url_prefix=args.url_prefix,
)
if __name__ == "__main__":
main()