# ============================================================================= # QWHPI — Quebec Weekly Housing Price Index # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # File : api/app/routers/reports.py # Purpose : Stats + automatic PDF report endpoints. # ============================================================================= """Derived statistics and report-generation endpoints.""" from __future__ import annotations from fastapi import APIRouter, HTTPException, Query, Request, Response from app.deps import etag_for, validate_series from app.services import data, report, stats router = APIRouter() @router.get("/stats") def series_statistics(request: Request, response: Response, geography: str, type: str = "all"): """Rich derived metrics for one series (peak, drawdown, momentum, ...).""" validate_series(geography, type) st = stats.series_stats(geography, type) if not st: raise HTTPException(404, "series too short for statistics") if etag_for(request, response, geography, type, "stats"): return Response(status_code=304) return {"geography": geography, "property_type": type, **st, "author": "Simon-Pierre Boucher", "contact": "contact@spboucher.ai"} @router.get("/stats/overview") def statistics_overview(request: Request, response: Response, level: str = Query("region", pattern="^(region|municipality)$"), type: str = "all"): """One rich row per geography — powers heatmap tables.""" if etag_for(request, response, level, type, "overview"): return Response(status_code=304) return {**stats.overview_rows(level, type), "author": "Simon-Pierre Boucher", "contact": "contact@spboucher.ai"} def _parse_specs(series: str) -> list[tuple[str, str]]: specs = [] for p in [s.strip() for s in series.split(",") if s.strip()]: try: g, t = p.split(":") except ValueError: raise HTTPException(422, f"bad series spec '{p}' (want geography:type)") validate_series(g, t) specs.append((g, t)) if not 1 <= len(specs) <= 5: raise HTTPException(422, "provide 1 to 5 series") return specs @router.get("/report") def series_report( series: str = Query(..., description="comma list of geography:type (1-5)"), format: str = Query("pdf", pattern="^(pdf|json)$"), ): """Automatic report: one page per series (+ comparison page if several).""" specs = _parse_specs(series) if format == "json": return {f"{g}:{t}": stats.series_stats(g, t) for g, t in specs} pdf = report.build_series_report(specs) fname = "qhpi_report_" + "_".join(f"{g}-{t}" for g, t in specs) + ".pdf" return Response( content=pdf, media_type="application/pdf", headers={ "Content-Disposition": f'inline; filename="{fname}"', "Cache-Control": "public, max-age=300", "X-Author": "Simon-Pierre Boucher ", }) @router.get("/report/market") def market_report(type: str = Query("all", pattern="^(all|unifamilial|condo|plex)$")): """Full market report: province, 17-region pulse table, property types.""" pdf = report.build_market_report(type) return Response( content=pdf, media_type="application/pdf", headers={ "Content-Disposition": f'inline; filename="qhpi_market_report_{type}.pdf"', "Cache-Control": "public, max-age=300", "X-Author": "Simon-Pierre Boucher ", })