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/main.py6# Purpose : FastAPI application — routers, CORS, rate limiting, OpenAPI.7# =============================================================================8"""QWHPI API service entry point.910Run locally: uvicorn app.main:app --reload --port 8080 (from api/)11Docs: /docs (OpenAPI). Every response credits the author and exposes12uncertainty (CIs, reliability grades) — the API never hides it.13"""1415from __future__ import annotations1617import time18from collections import defaultdict, deque1920from fastapi import FastAPI, Request21from fastapi.middleware.cors import CORSMiddleware22from fastapi.responses import JSONResponse2324from app.routers import geographies, index, liquidity, maps, meta, reports2526RATE_LIMIT = 120 # requests27RATE_WINDOW = 60.0 # seconds28_hits: dict[str, deque] = defaultdict(deque)2930app = FastAPI(31 title="QWHPI API",32 version="0.1.0",33 description=(34 "Quebec Weekly Housing Price Index — quality-adjusted, hierarchically "35 "pooled weekly indexes for Quebec geographies and property types.\n\n"36 "**Author: Simon-Pierre Boucher — contact@spboucher.ai**"37 ),38 contact={"name": "Simon-Pierre Boucher", "email": "contact@spboucher.ai"},39)4041app.add_middleware(42 CORSMiddleware,43 allow_origins=["*"],44 allow_methods=["GET"],45 allow_headers=["*"],46)474849@app.middleware("http")50async def rate_limiter(request: Request, call_next):51 ip = request.client.host if request.client else "anon"52 now = time.monotonic()53 q = _hits[ip]54 while q and now - q[0] > RATE_WINDOW:55 q.popleft()56 if len(q) >= RATE_LIMIT:57 return JSONResponse({"detail": "rate limit exceeded"}, status_code=429)58 q.append(now)59 return await call_next(request)606162app.include_router(index.router, prefix="/v1", tags=["index"])63app.include_router(reports.router, prefix="/v1", tags=["stats & reports"])64app.include_router(geographies.router, prefix="/v1", tags=["geographies"])65app.include_router(liquidity.router, prefix="/v1", tags=["liquidity"])66app.include_router(maps.router, prefix="/v1", tags=["maps"])67app.include_router(meta.router, tags=["meta"])68