""" Main FastAPI application """ import os from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import RedirectResponse, HTMLResponse from app.core.config import settings from app.api.v1.api import api_router from app.core.database import engine, Base from app.middleware.error_logger import ErrorLoggingMiddleware from app.models import error_log, request_log, fred_data, filing, finra_short_volume, alpaca_price # Import to register models from app.models import overlay_registry, overlay_raw_event, overlay_feature # Overlay models # Create database tables @asynccontextmanager async def lifespan(app: FastAPI): # Startup - ensure tables exist async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) # Start overlay batch scheduler (graceful no-op if apscheduler not installed) try: from app.services.overlay.scheduler import start_scheduler start_scheduler() except Exception: pass yield # Shutdown try: from app.services.overlay.scheduler import stop_scheduler stop_scheduler() except Exception: pass await engine.dispose() # Create FastAPI app app = FastAPI( title=settings.APP_NAME, version=settings.APP_VERSION, openapi_url=f"{settings.API_PREFIX}/openapi.json", docs_url=f"{settings.API_PREFIX}/docs", redoc_url=f"{settings.API_PREFIX}/redoc", lifespan=lifespan ) # Add error logging middleware app.add_middleware(ErrorLoggingMiddleware) # Set up CORS - use configured origins if available, otherwise allow all cors_origins = [str(o) for o in settings.BACKEND_CORS_ORIGINS] if settings.BACKEND_CORS_ORIGINS else ["*"] app.add_middleware( CORSMiddleware, allow_origins=cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Include API router app.include_router(api_router, prefix=settings.API_PREFIX) # Root documentation endpoint โ auto-generated from OpenAPI schema @app.get("/", response_class=HTMLResponse, include_in_schema=False) async def root_documentation(request: Request): try: schema = request.app.openapi() paths = schema.get("paths", {}) api_prefix = settings.API_PREFIX # Collect all tags defined in schema (preserves order) tag_order = [t["name"] for t in schema.get("tags", [])] # Group routes by first tag from collections import defaultdict tag_routes: dict = defaultdict(list) untagged: list = [] for path, methods in paths.items(): for method, op in methods.items(): if method.upper() not in ("GET", "POST", "PUT", "DELETE", "PATCH"): continue tags = op.get("tags", []) entry = { "method": method.upper(), "path": path, "summary": op.get("summary", ""), "deprecated": op.get("deprecated", False), } if tags: tag_routes[tags[0]].append(entry) else: untagged.append(entry) # Build tag sections โ respect schema tag order, then alphabetical remainder all_tags = tag_order + sorted(t for t in tag_routes if t not in tag_order) if untagged: all_tags.append("other") tag_routes["other"] = untagged method_colors = { "GET": "#61affe", "POST": "#49cc90", "PUT": "#fca130", "DELETE": "#f93e3e", "PATCH": "#50e3c2", } sections_html = "" for tag in all_tags: routes = tag_routes.get(tag, []) if not routes: continue rows = "" for r in routes: color = method_colors.get(r["method"], "#999") deprecated = " style='opacity:0.5;text-decoration:line-through'" if r["deprecated"] else "" display_path = r["path"].removeprefix(api_prefix) rows += ( f"
{display_path}