Spaces:
Sleeping
Sleeping
| """FastAPI application entry point.""" | |
| from __future__ import annotations | |
| import os | |
| from contextlib import asynccontextmanager | |
| from pathlib import Path | |
| from fastapi import FastAPI, Request | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import FileResponse, JSONResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from slowapi import _rate_limit_exceeded_handler | |
| from slowapi.errors import RateLimitExceeded | |
| from app.api import analysis, export, health, webhooks | |
| from app.core.config import settings | |
| from app.core.logging import get_logger, setup_logging | |
| from app.core.middleware import register_middleware | |
| from app.core.security import limiter | |
| from app.core.telemetry import setup_telemetry | |
| from app.services.redis_client import close_redis | |
| setup_logging(settings.log_level, settings.log_format) | |
| logger = get_logger(__name__) | |
| STATIC_DIR = Path(__file__).parent.parent / "static" | |
| async def lifespan(app: FastAPI): | |
| logger.info( | |
| "application_starting", | |
| app_name=settings.app_name, | |
| env=settings.app_env, | |
| ) | |
| settings.upload_path # Ensure upload directory exists | |
| yield | |
| logger.info("application_shutting_down") | |
| await close_redis() | |
| app = FastAPI( | |
| title=settings.app_name, | |
| description="Sentiment & Topic Analysis Dashboard API", | |
| version="1.0.0", | |
| lifespan=lifespan, | |
| docs_url="/docs" if not settings.is_production else None, | |
| redoc_url="/redoc" if not settings.is_production else None, | |
| ) | |
| # Middleware | |
| app.state.limiter = limiter | |
| app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=settings.cors_origins, | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| register_middleware(app) | |
| setup_telemetry(app) | |
| # Routes | |
| app.include_router(health.router) | |
| app.include_router(analysis.router) | |
| app.include_router(export.router) | |
| app.include_router(webhooks.router) | |
| async def global_exception_handler(request: Request, exc: Exception): | |
| from app.core.logging import get_correlation_id | |
| logger.error( | |
| "unhandled_exception", | |
| path=request.url.path, | |
| error=str(exc), | |
| exc_info=True, | |
| ) | |
| return JSONResponse( | |
| status_code=500, | |
| content={ | |
| "detail": "Internal server error", | |
| "correlation_id": get_correlation_id(), | |
| }, | |
| ) | |
| # Serve frontend static files in production (when static/ dir exists) | |
| if STATIC_DIR.is_dir(): | |
| app.mount("/assets", StaticFiles(directory=str(STATIC_DIR / "assets")), name="static-assets") | |
| async def serve_spa(full_path: str): | |
| """Serve the SPA index.html for all non-API routes.""" | |
| # Don't intercept API, docs, health, or metrics paths | |
| if full_path.startswith(("api/", "docs", "redoc", "openapi.json", "health", "metrics")): | |
| return JSONResponse(status_code=404, content={"detail": "Not found"}) | |
| file_path = STATIC_DIR / full_path | |
| if file_path.is_file(): | |
| return FileResponse(str(file_path)) | |
| return FileResponse(str(STATIC_DIR / "index.html")) | |