Gridlock / app /backend /main.py
parvmittal07's picture
Improved Map UI and minor fixes
2c64d54
Raw
History Blame Contribute Delete
4.39 kB
"""FastAPI app: serves the prediction API and the built React frontend from a
single origin (one container, one port - easy to deploy on Render).
Data tabs (options/areas/map/metrics) are served from precomputed JSON, so the
heavy ML service is loaded lazily on the first /api/predict call only.
"""
from __future__ import annotations
import json
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from .schemas import PredictRequest
ROOT = Path(__file__).resolve().parents[2]
GENERATED_DIR = Path(__file__).resolve().parent / "generated"
FIGURES_DIR = ROOT / "reports" / "figures"
FRONTEND_DIST = ROOT / "app" / "frontend" / "dist"
app = FastAPI(title="Gridlock Traffic Intelligence", version="1.0.0")
# Allow the Vite dev server (localhost:5173) to call the API during development.
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
allow_methods=["*"],
allow_headers=["*"],
)
# Gzip large JSON payloads (map.json is ~540 KB -> ~100 KB on the wire), which
# noticeably speeds up the first Map-tab load. Starlette ships this middleware,
# so it needs no extra dependency (works as-is on Hugging Face / Render).
app.add_middleware(GZipMiddleware, minimum_size=1024)
_JSON_CACHE: dict[str, object] = {}
def _serve_generated(name: str):
if name in _JSON_CACHE:
return _JSON_CACHE[name]
path = GENERATED_DIR / name
if not path.exists():
raise HTTPException(
status_code=503,
detail=f"{name} not found. Run `python -m app.backend.precompute` to generate it.",
)
data = json.loads(path.read_text(encoding="utf-8"))
_JSON_CACHE[name] = data
return data
# --------------------------------------------------------------------------- #
# API
# --------------------------------------------------------------------------- #
@app.get("/api/health")
def health():
return {"status": "ok"}
@app.get("/api/options")
def options():
return _serve_generated("options.json")
@app.get("/api/areas")
def areas():
return _serve_generated("areas.json")
@app.get("/api/map")
def map_data():
return _serve_generated("map.json")
@app.get("/api/metrics")
def metrics():
return _serve_generated("metrics.json")
@app.post("/api/predict")
def predict(req: PredictRequest):
from .inference import get_service
try:
service = get_service()
return service.predict(req.to_payload())
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc))
except Exception as exc: # pragma: no cover
raise HTTPException(status_code=500, detail=f"Prediction failed: {exc}")
@app.get("/api/figures/{name}")
def figure(name: str):
# Prevent path traversal; only serve known PNGs from the figures folder.
safe = Path(name).name
path = FIGURES_DIR / safe
if path.suffix.lower() != ".png" or not path.exists():
raise HTTPException(status_code=404, detail="figure not found")
return FileResponse(path, media_type="image/png")
# --------------------------------------------------------------------------- #
# Static frontend (single-origin). Mounted last so /api/* always wins.
# --------------------------------------------------------------------------- #
if FRONTEND_DIST.exists():
assets_dir = FRONTEND_DIST / "assets"
if assets_dir.exists():
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
@app.get("/")
def index():
return FileResponse(FRONTEND_DIST / "index.html")
@app.get("/{full_path:path}")
def spa_fallback(full_path: str):
if full_path.startswith("api/"):
raise HTTPException(status_code=404, detail="not found")
candidate = FRONTEND_DIST / full_path
if candidate.is_file():
return FileResponse(candidate)
return FileResponse(FRONTEND_DIST / "index.html")
else: # pragma: no cover - dev mode before the frontend is built
@app.get("/")
def index_dev():
return JSONResponse(
{"message": "Gridlock API running. Build the frontend (npm run build) or use the Vite dev server."}
)