Coverage for src/secchi/web.py: 97%
39 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 23:28 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 23:28 +0000
1"""Browser dashboard launcher backed by Textual's local web server."""
3from __future__ import annotations
5import shlex
6import shutil
7import subprocess
8import sys
9from dataclasses import dataclass
10from pathlib import Path
12from secchi.workflows.dashboard import DashboardRequest
15class TextualServeUnavailable(RuntimeError):
16 """Raised when the Textual CLI is not installed or not on PATH."""
19@dataclass(frozen=True)
20class WebDashboardLaunch:
21 """Details for a generated Textual serve launch."""
23 command: list[str]
26def build_dashboard_command(
27 request: DashboardRequest,
28 *,
29 package: str | None = None,
30 registry: str | None = None,
31 project_name: str | None = None,
32 refresh: bool = False,
33 security_refresh: bool = False,
34 verbose: bool = False,
35 log_file: Path | None = None,
36) -> list[str]:
37 """Build the dashboard command Textual serve should run."""
39 command = [sys.executable, "-m", "secchi", "dashboard"]
40 if package:
41 command.append(package)
42 if registry:
43 command.extend(["--registry", registry])
44 if request.config_path.exists():
45 command.extend(["--config", str(request.config_path)])
46 if project_name:
47 command.extend(["--project", project_name])
48 if refresh:
49 command.append("--no-cache")
50 elif security_refresh:
51 command.append("--security-no-cache")
52 if verbose:
53 command.append("--verbose")
54 if log_file:
55 command.extend(["--log-file", str(log_file)])
56 return command
59def prepare_launch(
60 request: DashboardRequest,
61 *,
62 package: str | None = None,
63 registry: str | None = None,
64 project_name: str | None = None,
65 refresh: bool = False,
66 security_refresh: bool = False,
67 verbose: bool = False,
68 log_file: Path | None = None,
69) -> WebDashboardLaunch:
70 """Prepare a local Textual serve launch without starting a subprocess."""
72 command = build_dashboard_command(
73 request,
74 package=package,
75 registry=registry,
76 project_name=project_name,
77 refresh=refresh,
78 security_refresh=security_refresh,
79 verbose=verbose,
80 log_file=log_file,
81 )
82 return WebDashboardLaunch(command=command)
85def run_textual_serve(command: list[str], *, port: int = 8000) -> None:
86 """Run the dashboard through Textual's local browser server."""
88 textual = shutil.which("textual")
89 if textual is None:
90 raise TextualServeUnavailable(
91 "Browser dashboard support requires the Textual CLI. "
92 "Run `uv sync` and use `uv run secchi web`, or install Secchi "
93 "with a Textual version that provides `textual serve`."
94 )
95 subprocess.run(
96 [
97 textual,
98 "serve",
99 "--title",
100 "Secchi",
101 "--port",
102 str(port),
103 "--command",
104 shlex.join(command),
105 ],
106 check=True,
107 )