import json from typing import Annotated, Any from fastapi import APIRouter, Depends, HTTPException, Query, Request from psycopg.types.json import Jsonb from app.auth import AuthAccount, require_scope from app.db import get_pool from app.models.views import LiveViewResponse from app.rate_limit import limiter router = APIRouter(prefix="/api/views", tags=["views"]) _FILTERS_DESC = "JSON object: cost_centre, assigned_city, vehicle_numbers[]" def _parse_filters(filters_q: str | None) -> dict[str, Any]: if not filters_q: return {} try: parsed = json.loads(filters_q) except json.JSONDecodeError as exc: raise HTTPException(status_code=400, detail=f"invalid filters json: {exc}") from exc if not isinstance(parsed, dict): raise HTTPException(status_code=400, detail="filters must be a JSON object") return parsed @router.get("/live", response_model=LiveViewResponse) @limiter.limit("60/minute") async def live_view( request: Request, _account: Annotated[AuthAccount, Depends(require_scope("read:fleet"))], filters: Annotated[str | None, Query(description=_FILTERS_DESC)] = None, ) -> LiveViewResponse: _ = request filters_dict = _parse_filters(filters) pool = await get_pool() async with pool.connection() as conn, conn.cursor() as cur: await cur.execute("SELECT serve.fn_live_view(%s)", (Jsonb(filters_dict),)) row = await cur.fetchone() if row is None or row[0] is None: raise HTTPException(status_code=500, detail="serve.fn_live_view returned NULL") return LiveViewResponse.model_validate(row[0])