"""/api/tour — guided-tour CRUD + runtime control (P4, package-local). The TourStore + TourRuntime singletons are resolved lazily from the Project.Sanad.main shim (app_p4 sets tour_store / tour_runtime). Kept Py-3.8. """ from __future__ import annotations from typing import Any, List, Optional from fastapi import APIRouter, HTTPException from pydantic import BaseModel router = APIRouter() def _store(): try: from Project.Sanad.main import tour_store except Exception: tour_store = None if tour_store is None: raise HTTPException(503, "tour store unavailable") return tour_store def _runtime(): try: from Project.Sanad.main import tour_runtime except Exception: tour_runtime = None if tour_runtime is None: raise HTTPException(503, "tour runtime unavailable") return tour_runtime class TourStop(BaseModel): place: Optional[str] = "" narration: Optional[str] = "" expression: Optional[str] = "" gesture: Optional[str] = "" greet: Optional[bool] = False dwell_sec: Optional[float] = 8 class TourSave(BaseModel): name: str stops: List[TourStop] = [] id: Optional[str] = None class StartBody(BaseModel): tour_id: str # ── tour definitions (builder) ── @router.get("/") async def list_tours(): return {"ok": True, "tours": _store().list()} @router.post("/") async def save_tour(payload: TourSave): if not (payload.name or "").strip(): raise HTTPException(400, "name is required") stops = [s.dict() for s in (payload.stops or [])] return {"ok": True, "tour": _store().save(payload.name.strip(), stops, tid=payload.id)} # ── runtime (must precede /{tid}) ── @router.get("/status") async def status(): return _runtime().status() @router.post("/start") async def start(body: StartBody): return _runtime().start(body.tour_id) @router.post("/stop") async def stop(): return _runtime().stop() @router.post("/pause") async def pause(): return _runtime().pause() @router.post("/resume") async def resume(): return _runtime().resume() @router.post("/skip") async def skip(): return _runtime().skip() @router.get("/{tid}") async def get_tour(tid: str): t = _store().get(tid) if t is None: raise HTTPException(404, "no tour %s" % tid) return {"ok": True, "tour": t} @router.delete("/{tid}") async def delete_tour(tid: str): if not _store().delete(tid): raise HTTPException(404, "no tour %s" % tid) return {"ok": True, "deleted": tid}