SPB Git

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%
3.5 KB · 90 lines python
Raw Blame History
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