GitHub Actions
Auto-deploy from github.com/AdithaBuwaneka/DataX-DataStorm-7.0@029dcac
221080b
Raw History Blame Contribute Delete
4.46 kB
"""FastAPI entry-point for the Outlet Intelligence web app.
Run locally:
cd webapp/backend
pip install -r requirements.txt
cp .env.example .env # set GEMINI_API_KEY
python -m uvicorn main:app --reload --port 8000
The app loads gold parquets / audit CSVs into memory once at startup and
exposes the data through JSON endpoints. CORS is open in local dev so the
Next.js frontend on :3000 can call it directly.
"""
from __future__ import annotations
import sys
from contextlib import asynccontextmanager
from pathlib import Path
# Ensure routers/ and services/ are importable as top-level packages
# whether the app is launched as `main:app` from webapp/backend/ or as
# `backend.main:app` from webapp/.
_BACKEND_DIR = Path(__file__).resolve().parent
if str(_BACKEND_DIR) not in sys.path:
sys.path.insert(0, str(_BACKEND_DIR))
from dotenv import load_dotenv # noqa: E402
from fastapi import FastAPI # noqa: E402
from fastapi.middleware.cors import CORSMiddleware # noqa: E402
# Always load the .env that lives next to this file, regardless of CWD.
load_dotenv(_BACKEND_DIR / ".env")
from routers import outlets as outlets_router # noqa: E402
from routers import dashboards as dash_router # noqa: E402
from routers import xai as xai_router # noqa: E402
from services.data_loader import get_cache # noqa: E402
@asynccontextmanager
async def lifespan(app: FastAPI):
import os
cache = get_cache()
print(f"[startup] Repo root: {cache.repo_root}")
print(f"[startup] outlets table: {cache.outlets_table.shape}")
print(f"[startup] predictions: {cache.predictions.shape}")
print(f"[startup] dormancy: {cache.dormancy.shape}")
key = os.environ.get("GEMINI_API_KEY")
if key:
print(f"[startup] GEMINI_API_KEY loaded ({len(key)} chars) "
f"using model {os.environ.get('GEMINI_MODEL', 'gemini-2.5-flash')}")
else:
print("[startup] WARN: GEMINI_API_KEY not set — /api/xai endpoints will 503")
yield
app = FastAPI(
title="Outlet Intelligence API",
version="0.1.0",
description="Beverage outlet potential, budget allocation, and XAI narratives.",
lifespan=lifespan,
)
# CORS — local dev (localhost / 127.0.0.1 any port) plus deployed origins:
# Vercel previews/production (*.vercel.app), the HF Space's own origin so
# the FastAPI Swagger UI works in-place, and any extra origins listed in
# the CORS_EXTRA_ORIGINS env var (comma-separated). The regex is intentionally
# strict so we don't accept arbitrary HTTP origins from the internet.
import os as _os # noqa: E402
_extra = [o.strip() for o in _os.environ.get("CORS_EXTRA_ORIGINS", "").split(",") if o.strip()]
app.add_middleware(
CORSMiddleware,
allow_origin_regex=(
r"https?://(localhost|127\.0\.0\.1)(:\d+)?|"
r"https://[a-zA-Z0-9-]+\.vercel\.app|"
r"https://[a-zA-Z0-9-]+-[a-zA-Z0-9-]+\.vercel\.app|"
r"https://huggingface\.co|"
r"https://[a-zA-Z0-9-]+\.hf\.space"
),
allow_origins=_extra,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(outlets_router.router)
app.include_router(dash_router.router)
app.include_router(xai_router.router)
@app.get("/")
def root():
return {
"service": "Outlet Intelligence API",
"endpoints": [
"GET /health",
"GET /api/summary",
"GET /api/outlets",
"GET /api/outlets/filters",
"GET /api/outlets/{outlet_id}",
"POST /api/xai/explain/{outlet_id}",
"GET /api/budget/distributors",
"GET /api/budget/channels",
"GET /api/budget/outlets",
"GET /api/cooler-roi/top100",
"GET /api/cooler-roi/summary",
"GET /api/dormancy/top",
"GET /api/dormancy/bands",
"GET /api/scorecard",
"GET /api/territories",
"GET /api/forensics",
"GET /api/shap/global",
],
}
@app.get("/health")
def health():
"""Liveness probe used by the frontend on first paint."""
import os
cache = get_cache()
return {
"status": "ok",
"outlets_loaded": int(len(cache.outlets_table)),
"predictions_loaded": int(len(cache.predictions)),
"gemini_configured": bool(os.environ.get("GEMINI_API_KEY")),
}