"""Shared helpers for operator API endpoints (SLICE-08). Common response models + the recent-aggregates query used by all 3 cohort view endpoints (cohort, mastery, failure-patterns). Kept here to avoid duplicating the Pydantic models + pool query across 3 files. """ from __future__ import annotations import datetime as _dt from typing import Any from fastapi import HTTPException, Request, status from pydantic import BaseModel class Cell(BaseModel): metric: str window_start: _dt.date | None = None window_end: _dt.date | None = None value: float | None = None cell_count: int = 0 cell_suppressed: bool = False updated_at: _dt.datetime | None = None class PathView(BaseModel): path: str metrics: list[Cell] class ViewResponse(BaseModel): views: list[PathView] last_updated: _dt.datetime | None = None async def require_pg_store(request: Request): pg_store = getattr(request.app.state, "pg_store", None) if pg_store is None: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="operator tier unavailable (no Postgres)", ) return pg_store async def all_recent_aggregates(pg_store, since: _dt.date) -> list[dict[str, Any]]: async with pg_store.pool.acquire() as conn: rows = await conn.fetch( "SELECT path, metric, window_start, window_end, value, " "cell_count, cell_suppressed, updated_at " "FROM cohort_aggregates WHERE window_start >= $1 " "ORDER BY path, metric, window_start", since, ) return [dict(r) for r in rows] def cell_from_row(row: dict[str, Any]) -> Cell: return Cell( metric=row.get("metric", ""), window_start=row.get("window_start"), window_end=row.get("window_end"), value=float(row["value"]) if row.get("value") is not None else None, cell_count=int(row.get("cell_count") or 0), cell_suppressed=bool(row.get("cell_suppressed") or False), updated_at=row.get("updated_at"), ) def group_by_path( rows: list[dict[str, Any]], metric_filter: set[str] | None = None, ) -> tuple[list[PathView], _dt.datetime | None]: by_path: dict[str, list[dict[str, Any]]] = {} last_updated: _dt.datetime | None = None for r in rows: if metric_filter is not None and r.get("metric") not in metric_filter: continue by_path.setdefault(r["path"], []).append(r) ua = r.get("updated_at") if isinstance(ua, _dt.datetime) and (last_updated is None or ua > last_updated): last_updated = ua views = [PathView(path=p, metrics=[cell_from_row(c) for c in cells]) for p, cells in by_path.items()] return views, last_updated __all__ = [ "Cell", "PathView", "ViewResponse", "require_pg_store", "all_recent_aggregates", "cell_from_row", "group_by_path", ]