Spaces:
Running
Running
| """Fin-DataPilot FastAPI application entrypoint. | |
| Triggered by the user adding HF_SSH_PRIVATE_KEY to GitHub Secrets. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from contextlib import asynccontextmanager | |
| from typing import AsyncIterator | |
| from fastapi import FastAPI, Request | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import ORJSONResponse | |
| from slowapi import Limiter | |
| from slowapi.util import get_remote_address | |
| from app import __version__ | |
| from app.api import agent, health, sessions, skills | |
| from app.config import get_settings | |
| from app.db_init import init_db | |
| from app.skills import registry as _skills_registry # noqa: F401 — trigger registration | |
| from app.skills.user_uploads import load_uploaded_skills_at_startup | |
| from app.utils.trace import setup_logging | |
| logger = logging.getLogger(__name__) | |
| async def lifespan(_app: FastAPI) -> AsyncIterator[None]: | |
| settings = get_settings() | |
| setup_logging(settings.log_level) | |
| await init_db() | |
| # After the 4 built-in skills have registered, re-import any | |
| # previously uploaded skills so they survive container restarts. | |
| n_loaded = load_uploaded_skills_at_startup() | |
| if n_loaded: | |
| logger.info( | |
| "Loaded %d uploaded skill(s) from %s", | |
| n_loaded, | |
| settings.user_skills_dir, | |
| ) | |
| yield | |
| def create_app() -> FastAPI: | |
| settings = get_settings() | |
| app = FastAPI( | |
| title="Fin-DataPilot", | |
| version=__version__, | |
| description="Natural-language financial data agent platform", | |
| default_response_class=ORJSONResponse, | |
| lifespan=lifespan, | |
| ) | |
| # CORS | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=settings.cors_origins_list, | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| expose_headers=["X-Trace-Id"], | |
| ) | |
| # Rate limiting (per remote IP) | |
| limiter = Limiter(key_func=get_remote_address) | |
| app.state.limiter = limiter | |
| # Optional API key enforcement | |
| if settings.api_key: | |
| async def api_key_guard(request: Request, call_next): | |
| if request.url.path in {"/api/health", "/docs", "/openapi.json", "/redoc"}: | |
| return await call_next(request) | |
| provided = request.headers.get("X-API-Key", "") | |
| if provided != settings.api_key: | |
| return ORJSONResponse({"error": "unauthorized"}, status_code=401) | |
| return await call_next(request) | |
| # Routers | |
| app.include_router(health.router, prefix="/api", tags=["health"]) | |
| app.include_router(skills.router, prefix="/api", tags=["skills"]) | |
| app.include_router(sessions.router, prefix="/api", tags=["sessions"]) | |
| app.include_router(agent.router, prefix="/api/agent", tags=["agent"]) | |
| return app | |
| app = create_app() | |