"""Entry point — run crawl scenarios or start the admin panel.""" from __future__ import annotations import argparse import asyncio import sys import time import uvicorn from admin.models import get_active_flow_config, init_db, insert_run from config import config def _load_active_flow() -> dict[str, object]: cfg = asyncio.run(get_active_flow_config()) if cfg is None: print("[WARN] No active flow config — running without dynamic flow steps.", file=sys.stderr) return cfg or {} def _record( scenario: str, identifier: str, flow_result: object | None, exc: Exception | None, t0: float, ) -> None: from crawler.dynamic_flow import DynamicFlowResult total_ms = int((time.monotonic() - t0) * 1000) if exc is not None: asyncio.run(insert_run(scenario, identifier, False, str(exc), [], total_ms)) print(f"[FAIL] {identifier}: {exc}", file=sys.stderr) return if isinstance(flow_result, DynamicFlowResult): steps_data = [ {"name": s.name, "success": s.success, "error": s.error, "duration_ms": s.duration_ms} for s in flow_result.steps ] asyncio.run(insert_run( scenario, identifier, flow_result.success, flow_result.failure_reason, steps_data, total_ms, )) status = "OK" if flow_result.success else f"FAIL ({flow_result.failure_reason})" else: asyncio.run(insert_run(scenario, identifier, True, None, [], total_ms)) status = "OK (no flow config)" print(f"[{status}] {identifier} — {total_ms} ms") def run_otp(phone: str) -> None: from crawler.driver import driver_session from crawler.dynamic_flow import DynamicFlow from crawler.scenarios.otp_login import OTPLoginScenario def otp_resolver(p: str) -> str: # TODO: wire up your SMS gateway / webhook here. raise NotImplementedError("Implement otp_resolver to fetch OTP from your SMS gateway") flow_cfg = _load_active_flow() t0 = time.monotonic() try: with driver_session() as driver: OTPLoginScenario(driver, phone, otp_resolver).run() result = DynamicFlow(driver, flow_cfg).run() if flow_cfg else None except Exception as exc: _record("otp", phone, None, exc, t0) return _record("otp", phone, result, None, t0) def run_fresh() -> None: from crawler.driver import driver_session from crawler.dynamic_flow import DynamicFlow from crawler.scenarios.fresh_session import FreshSessionScenario flow_cfg = _load_active_flow() t0 = time.monotonic() try: with driver_session() as driver: FreshSessionScenario(driver).run() result = DynamicFlow(driver, flow_cfg).run() if flow_cfg else None except Exception as exc: _record("fresh", "fresh", None, exc, t0) return _record("fresh", "fresh", result, None, t0) def run_admin() -> None: init_db() from admin.main import app uvicorn.run(app, host=config.admin_host, port=config.admin_port) def main() -> None: parser = argparse.ArgumentParser(description="Seed crawler & admin") sub = parser.add_subparsers(dest="cmd", required=True) sub.add_parser("admin", help="Start the admin panel") otp_p = sub.add_parser("otp", help="Run Scenario 1 (SMS-OTP login)") otp_p.add_argument("--phone", help="Single phone number (default: all from config)") sub.add_parser("fresh", help="Run Scenario 2 (fresh session)") args = parser.parse_args() if args.cmd == "admin": run_admin() elif args.cmd == "otp": phones = [args.phone] if args.phone else config.phone_numbers if not phones: print("No phone numbers configured. Set PHONE_NUMBERS in .env or pass --phone", file=sys.stderr) sys.exit(1) for phone in phones: run_otp(phone) elif args.cmd == "fresh": run_fresh() if __name__ == "__main__": main()