# ============================================================================= # QWHPI — Quebec Weekly Housing Price Index # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # File : api/app/main.py # Purpose : FastAPI application — routers, CORS, rate limiting, OpenAPI. # ============================================================================= """QWHPI API service entry point. Run locally: uvicorn app.main:app --reload --port 8080 (from api/) Docs: /docs (OpenAPI). Every response credits the author and exposes uncertainty (CIs, reliability grades) — the API never hides it. """ from __future__ import annotations import time from collections import defaultdict, deque from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from app.routers import geographies, index, liquidity, maps, meta, reports RATE_LIMIT = 120 # requests RATE_WINDOW = 60.0 # seconds _hits: dict[str, deque] = defaultdict(deque) app = FastAPI( title="QWHPI API", version="0.1.0", description=( "Quebec Weekly Housing Price Index — quality-adjusted, hierarchically " "pooled weekly indexes for Quebec geographies and property types.\n\n" "**Author: Simon-Pierre Boucher — contact@spboucher.ai**" ), contact={"name": "Simon-Pierre Boucher", "email": "contact@spboucher.ai"}, ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["GET"], allow_headers=["*"], ) @app.middleware("http") async def rate_limiter(request: Request, call_next): ip = request.client.host if request.client else "anon" now = time.monotonic() q = _hits[ip] while q and now - q[0] > RATE_WINDOW: q.popleft() if len(q) >= RATE_LIMIT: return JSONResponse({"detail": "rate limit exceeded"}, status_code=429) q.append(now) return await call_next(request) app.include_router(index.router, prefix="/v1", tags=["index"]) app.include_router(reports.router, prefix="/v1", tags=["stats & reports"]) app.include_router(geographies.router, prefix="/v1", tags=["geographies"]) app.include_router(liquidity.router, prefix="/v1", tags=["liquidity"]) app.include_router(maps.router, prefix="/v1", tags=["maps"]) app.include_router(meta.router, tags=["meta"])