from __future__ import annotations import argparse import json import os import sys from datetime import datetime, timezone from pathlib import Path from typing import Any from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from backend.sql_catalog import server from scripts.import_clinic_health_report import build_findings from scripts.seed_demo_from_clinic_report import ( REPORT_PATH, demo_intake_template, derived_finding, prepare_demo, ) API_BASE = os.environ.get("SQL_CATALOG_API_BASE", "http://127.0.0.1:8765").rstrip("/") APP_BASE = os.environ.get("SQL_CATALOG_APP_BASE", "http://127.0.0.1:5173").rstrip("/") def read_report() -> dict[str, Any]: return json.loads(REPORT_PATH.read_text(encoding="utf-8")) def post_finding(payload: dict[str, Any]) -> dict[str, Any]: request = Request( f"{API_BASE}/api/v1/findings", data=json.dumps(payload).encode("utf-8"), headers={"content-type": "application/json"}, method="POST", ) try: with urlopen(request, timeout=30) as response: return json.loads(response.read().decode("utf-8")) except HTTPError as exc: body = exc.read().decode("utf-8") raise RuntimeError(f"Finding API returned HTTP {exc.code}: {body}") from exc except URLError as exc: raise RuntimeError(f"Cannot reach SQL Catalog API at {API_BASE}: {exc.reason}") from exc def event_suffix(value: str | None) -> str: if value: return value return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") def live_payload( template: dict[str, Any], report: dict[str, Any], *, scenario: str, suffix: str, ) -> dict[str, Any]: observed_at = datetime.now(timezone.utc).replace(microsecond=0) run_id = f"{report['strategyUuid']}:demo:{scenario}:{suffix}" payload = derived_finding( template, report, {"cluster_id": str(report["clusterId"]), "profile": "live-demo"}, run_id, observed_at, exact_report=False, metric_scale=1.03, ) payload["created_by"] = "clinic-health-check-demo" payload["source_event_id"] = ( f"clinic_health_check:{run_id}:{payload['detector_code']}:{payload['sql_digest']}" ) derivation = payload.setdefault("dimensions", {}).setdefault("fixture_derivation", {}) derivation["demo_scenario"] = scenario return payload def completed_catalog_item() -> dict[str, Any]: with server.get_conn() as conn: row = conn.execute( """ SELECT ci.*, t.id AS treatment_id FROM catalog_items ci JOIN relations relation ON relation.from_type = 'treatment' AND relation.relation_type = 'treatment_for' AND relation.to_type = 'catalog_item' AND relation.to_id = ci.id JOIN treatments t ON t.id = relation.from_id WHERE t.state = 'completed' ORDER BY t.completed_at DESC, t.id DESC LIMIT 1 """ ).fetchone() if not row: raise RuntimeError("No completed Treatment is available. Run the prepare command first.") return dict(row) def template_for_digest(templates: list[dict[str, Any]], digest: str) -> dict[str, Any]: candidates = [ template for template in templates if str(template.get("sql_digest")) == digest and template.get("detector_code") == "clinic.slowlog_top_digest" ] if not candidates: candidates = [ template for template in templates if str(template.get("sql_digest")) == digest ] if not candidates: raise RuntimeError(f"The source report does not contain digest {digest}") return candidates[0] def response_summary(result: dict[str, Any], scenario: str) -> dict[str, Any]: finding = result["finding"] item = result["catalog_item"] return { "scenario": scenario, "finding_created": result["created"], "catalog_created": finding["is_new_digest"], "severity": finding["severity"], "symptoms": [symptom["code"] for symptom in finding["symptoms"]], "finding_id": finding["id"], "catalog_item_id": item["id"], "finding_url": f"{APP_BASE}/findings/{finding['id']}", "catalog_url": f"{APP_BASE}/catalog/{item['id']}", "workbench_url": APP_BASE, } def command_prepare() -> dict[str, Any]: result = prepare_demo() completed = completed_catalog_item() return { **result, "next_command": "python3 scripts/demo_flow.py new", "workbench_url": APP_BASE, "historical_catalog_url": f"{APP_BASE}/catalog/{completed['id']}", } def command_new(suffix: str | None) -> dict[str, Any]: report = read_report() template = demo_intake_template(build_findings(report)) payload = live_payload( template, report, scenario="new-catalog", suffix=event_suffix(suffix), ) return response_summary(post_finding(payload), "new-catalog") def command_reappear(suffix: str | None) -> dict[str, Any]: report = read_report() templates = build_findings(report) item = completed_catalog_item() payload = live_payload( template_for_digest(templates, str(item["sql_digest"])), report, scenario="reappeared-after-treatment", suffix=event_suffix(suffix), ) return response_summary(post_finding(payload), "reappeared-after-treatment") def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Prepare and trigger the SQL Catalog demo flow.") subparsers = parser.add_subparsers(dest="command", required=True) subparsers.add_parser("prepare", help="Reset and seed the Clinic-derived baseline.") new_parser = subparsers.add_parser("new", help="Post the reserved Critical Finding through the API.") new_parser.add_argument("--event-suffix", help="Stable suffix for idempotency rehearsal.") reappear_parser = subparsers.add_parser( "reappear", help="Post a Finding for a digest with a completed Treatment." ) reappear_parser.add_argument("--event-suffix", help="Stable suffix for idempotency rehearsal.") return parser.parse_args() def main() -> None: args = parse_args() if args.command == "prepare": result = command_prepare() elif args.command == "new": result = command_new(args.event_suffix) else: result = command_reappear(args.event_suffix) print(json.dumps(result, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()