spb/qwhpi Public
QHPI — Quebec Housing Price Index: quality-adjusted, hierarchically pooled housing price indexes.
Python 63.9%
TypeScript 25.4%
CSS 5.5%
TeX 3.5%
SQL 0.8%
Makefile 0.5%
Dockerfile 0.5%
1# =============================================================================2# QWHPI — Quebec Weekly Housing Price Index3# Author : Simon-Pierre Boucher4# Contact : contact@spboucher.ai5# File : api/app/routers/reports.py6# Purpose : Stats + automatic PDF report endpoints.7# =============================================================================8"""Derived statistics and report-generation endpoints."""910from __future__ import annotations1112from fastapi import APIRouter, HTTPException, Query, Request, Response1314from app.deps import etag_for, validate_series15from app.services import data, report, stats1617router = APIRouter()181920@router.get("/stats")21def series_statistics(request: Request, response: Response,22 geography: str, type: str = "all"):23 """Rich derived metrics for one series (peak, drawdown, momentum, ...)."""24 validate_series(geography, type)25 st = stats.series_stats(geography, type)26 if not st:27 raise HTTPException(404, "series too short for statistics")28 if etag_for(request, response, geography, type, "stats"):29 return Response(status_code=304)30 return {"geography": geography, "property_type": type, **st,31 "author": "Simon-Pierre Boucher", "contact": "contact@spboucher.ai"}323334@router.get("/stats/overview")35def statistics_overview(request: Request, response: Response,36 level: str = Query("region", pattern="^(region|municipality)$"),37 type: str = "all"):38 """One rich row per geography — powers heatmap tables."""39 if etag_for(request, response, level, type, "overview"):40 return Response(status_code=304)41 return {**stats.overview_rows(level, type),42 "author": "Simon-Pierre Boucher", "contact": "contact@spboucher.ai"}434445def _parse_specs(series: str) -> list[tuple[str, str]]:46 specs = []47 for p in [s.strip() for s in series.split(",") if s.strip()]:48 try:49 g, t = p.split(":")50 except ValueError:51 raise HTTPException(422, f"bad series spec '{p}' (want geography:type)")52 validate_series(g, t)53 specs.append((g, t))54 if not 1 <= len(specs) <= 5:55 raise HTTPException(422, "provide 1 to 5 series")56 return specs575859@router.get("/report")60def series_report(61 series: str = Query(..., description="comma list of geography:type (1-5)"),62 format: str = Query("pdf", pattern="^(pdf|json)$"),63):64 """Automatic report: one page per series (+ comparison page if several)."""65 specs = _parse_specs(series)66 if format == "json":67 return {f"{g}:{t}": stats.series_stats(g, t) for g, t in specs}68 pdf = report.build_series_report(specs)69 fname = "qhpi_report_" + "_".join(f"{g}-{t}" for g, t in specs) + ".pdf"70 return Response(71 content=pdf, media_type="application/pdf",72 headers={73 "Content-Disposition": f'inline; filename="{fname}"',74 "Cache-Control": "public, max-age=300",75 "X-Author": "Simon-Pierre Boucher <contact@spboucher.ai>",76 })777879@router.get("/report/market")80def market_report(type: str = Query("all", pattern="^(all|unifamilial|condo|plex)$")):81 """Full market report: province, 17-region pulse table, property types."""82 pdf = report.build_market_report(type)83 return Response(84 content=pdf, media_type="application/pdf",85 headers={86 "Content-Disposition": f'inline; filename="qhpi_market_report_{type}.pdf"',87 "Cache-Control": "public, max-age=300",88 "X-Author": "Simon-Pierre Boucher <contact@spboucher.ai>",89 })90