"""Tiny stdlib HTTP server for the demo client page. - Serves `index.html` (and other sibling files) from this directory. - Exposes `GET /labels.json`, which parses the repo's `app/resources/consumable_classifier_labels.yaml` and returns its label list so the page can prefill the candidate-consumables input. Run: python scripts/demo_client/server.py # 127.0.0.1:38081 python scripts/demo_client/server.py -p 9000 # custom port """ from __future__ import annotations import argparse import json import re import sys from http import HTTPStatus from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from typing import Any SCRIPT_DIR = Path(__file__).resolve().parent REPO_ROOT = SCRIPT_DIR.parents[1] LABELS_YAML = REPO_ROOT / "app" / "resources" / "consumable_classifier_labels.yaml" def _load_labels_with_pyyaml(path: Path) -> list[str] | None: try: import yaml # type: ignore[import-untyped] except ImportError: return None try: data: Any = yaml.safe_load(path.read_text(encoding="utf-8")) except (OSError, yaml.YAMLError): return None if not isinstance(data, dict): return None names = data.get("names") if isinstance(names, dict): try: items = sorted(names.items(), key=lambda kv: int(kv[0])) except (TypeError, ValueError): items = list(names.items()) return [str(v) for _, v in items] if isinstance(names, list): return [str(v) for v in names] return None def _load_labels_fallback(path: Path) -> list[str]: """Minimal parser for the known labels.yaml shape: `: ` under `names:`.""" labels: list[tuple[int, str]] = [] in_names = False pattern = re.compile(r"^\s+(\d+)\s*:\s*(.+?)\s*$") try: text = path.read_text(encoding="utf-8") except OSError: return [] for line in text.splitlines(): stripped = line.strip() if not stripped or stripped.startswith("#"): continue if stripped.startswith("names:"): in_names = True continue if in_names and not line.startswith((" ", "\t")): in_names = False if not in_names: continue match = pattern.match(line) if match: labels.append((int(match.group(1)), match.group(2))) labels.sort(key=lambda kv: kv[0]) return [name for _, name in labels] def load_labels() -> list[str]: if not LABELS_YAML.is_file(): return [] labels = _load_labels_with_pyyaml(LABELS_YAML) if labels is None: labels = _load_labels_fallback(LABELS_YAML) return labels class DemoHandler(SimpleHTTPRequestHandler): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, directory=str(SCRIPT_DIR), **kwargs) def do_GET(self) -> None: # noqa: N802 (stdlib override) if self.path.split("?", 1)[0] == "/labels.json": self._send_labels() return super().do_GET() def _send_labels(self) -> None: body = json.dumps({"labels": load_labels()}, ensure_ascii=False).encode("utf-8") self.send_response(HTTPStatus.OK) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.send_header("Cache-Control", "no-store") self.end_headers() self.wfile.write(body) def log_message(self, format: str, *args: Any) -> None: # noqa: A002 sys.stderr.write( "[demo-client] %s - %s\n" % (self.address_string(), format % args) ) def main() -> None: parser = argparse.ArgumentParser(description="Operation room demo client server") parser.add_argument("--host", default="127.0.0.1") parser.add_argument("-p", "--port", type=int, default=38081) args = parser.parse_args() server = ThreadingHTTPServer((args.host, args.port), DemoHandler) url = f"http://{args.host}:{args.port}/" print(f"Demo client serving at {url}") print(f" static dir : {SCRIPT_DIR}") print(f" labels yaml: {LABELS_YAML}") print("Press Ctrl+C to stop.") try: server.serve_forever() except KeyboardInterrupt: print("\nShutting down.") finally: server.server_close() if __name__ == "__main__": main()