f2a12f9fed
v0.4 (Operator Tier — Cohort Dashboard + Auth + Postgres) milestone complete. Phases: ✓ P0 pre-execution (planning) → v0.1.6 ✓ P1 operator foundation (Postgres+auth+VC migration) → v0.1.7 ✓ P2 cohort dashboard + aggregation → v0.1.8 ✓ P3 final review + ship → v0.1.9 (= v0.4 milestone release) Requirements covered (8/8): REQ-MT-01 (Postgres store), REQ-MT-02 (aggregation pipeline), REQ-AUTH-01 (operator auth), REQ-DASH-01 (cohort dashboard), REQ-NFR-AUTH-01 (auth NFRs), REQ-NFR-MT-01 (Postgres-in-LXC), REQ-NFR-DASH-01 (k-anonymity ≥10), REQ-NFR-DASH-02 (freshness ≤24h) Grill MUSTs honored (6/6): G-008, G-011, G-027, G-031, G-038, G-041 Tests: 317 pytest pass, 36 skip (Postgres-requiring), 0 fail; 17/17 vitest pass Review: APPROVE_WITH_NOTES (6/6 personas, 0 P0, 8 P1+ carry-forward) Audit: HEALTHY (reconstruction PASS, 8/8 REQ, 6/6 grill) ---ci--- project: praxis phase: 3 milestone: v0.4 status: complete phase_role: final milestone_complete: true milestone_merged_to_main: true tag: v0.1.9 requirements: covered: [REQ-MT-01, REQ-MT-02, REQ-AUTH-01, REQ-DASH-01, REQ-NFR-AUTH-01, REQ-NFR-MT-01, REQ-NFR-DASH-01, REQ-NFR-DASH-02] partial: [] ---/ci---
93 lines
2.9 KiB
Python
93 lines
2.9 KiB
Python
"""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",
|
|
] |