"""FastAPI application: /api/v1 (OpenAPI at /api/v1/openapi.json, docs at /api/docs), /metrics, static console.""" from __future__ import annotations import time import uuid from pathlib import Path from fastapi import Depends, FastAPI, Request from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse, Response from fastapi.staticfiles import StaticFiles from findata_core.config import get_settings from findata_core.logging import configure_logging, get_logger from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.middleware.base import BaseHTTPMiddleware from findata_api import auth as A from findata_api import metrics as M from findata_api.ratelimit import bucket from findata_api.routers import ( admin, assistant, auth, catalog, coverage, exports, fundamentals, health, instruments, market, quality, query, ) API_PREFIX = "/api/v1" class Observability(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): rid = request.headers.get("x-request-id") or uuid.uuid4().hex[:16] request.state.request_id = rid t0 = time.monotonic() route = request.url.path try: response = await call_next(request) except Exception: M.REQUESTS.labels(request.method, route, "500").inc() raise dt = time.monotonic() - t0 if route.startswith(API_PREFIX): M.REQUESTS.labels(request.method, _route_label(route), str(response.status_code)).inc() M.LATENCY.labels(_route_label(route)).observe(dt) response.headers["X-Request-ID"] = rid response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Frame-Options"] = "DENY" response.headers["Referrer-Policy"] = "no-referrer" response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()" if get_settings().is_production: response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" if not route.startswith("/api/docs"): response.headers["Content-Security-Policy"] = ("default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; " "script-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'") return response def _route_label(path: str) -> str: parts = path.split("/") return "/".join(p if not _looks_id(p) else "{id}" for p in parts[:6]) def _looks_id(p: str) -> bool: return len(p) >= 16 and all(c in "0123456789abcdef-" for c in p) class RateLimit(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): if request.url.path.startswith(API_PREFIX) and not request.url.path.endswith("/health"): auth_h = request.headers.get("authorization", "") key = auth_h[-24:] if auth_h else (request.cookies.get(A.SESSION_COOKIE, "")[-24:] or (request.client.host if request.client else "anon")) ok, remaining = bucket.allow(f"rl:{key}", get_settings().rate_limit_per_minute) if not ok: return JSONResponse({"error": {"code": "rate_limited", "message": "Too many requests", "request_id": getattr(request.state, "request_id", None)}}, status_code=429, headers={"Retry-After": "5"}) return await call_next(request) def create_app() -> FastAPI: s = get_settings() configure_logging() log = get_logger("api") app = FastAPI(title="FinData OS API", version="1.0.0", openapi_url=f"{API_PREFIX}/openapi.json", docs_url="/api/docs", redoc_url=None, description="Private, research-grade financial data lakehouse. All market data © FirstRate Data — no redistribution. " "Timestamps are UTC ISO-8601; bars are stamped at bar **open**.") app.add_middleware(Observability) app.add_middleware(RateLimit) @app.exception_handler(StarletteHTTPException) async def http_exc(request: Request, exc: StarletteHTTPException): detail = exc.detail if isinstance(exc.detail, dict) else {"code": "http_error", "message": str(exc.detail)} detail["request_id"] = getattr(request.state, "request_id", None) return JSONResponse({"error": detail}, status_code=exc.status_code, headers=getattr(exc, "headers", None)) @app.exception_handler(RequestValidationError) async def val_exc(request: Request, exc: RequestValidationError): return JSONResponse({"error": {"code": "validation_error", "message": "Invalid request", "details": exc.errors(), "request_id": getattr(request.state, "request_id", None)}}, status_code=422) @app.exception_handler(Exception) async def any_exc(request: Request, exc: Exception): from findata_core.redact import redact log.error("unhandled", error=redact(repr(exc), s.secret_values()), path=request.url.path) return JSONResponse({"error": {"code": "internal_error", "message": "Internal error", "request_id": getattr(request.state, "request_id", None)}}, status_code=500) for r in (auth.router, catalog.router, instruments.router, market.router, fundamentals.router, query.router, exports.router, coverage.router, quality.router, health.router, admin.router, assistant.router): app.include_router(r, prefix=API_PREFIX) @app.get("/metrics", include_in_schema=False) def metrics(request: Request, p: A.Principal = Depends(A.get_principal)): host = request.client.host if request.client else "" private = host in ("127.0.0.1", "::1") or host.startswith(("192.168.", "10.67.", "10.", "172.16.")) if not private and not p.has("admin"): return JSONResponse({"error": {"code": "forbidden", "message": "metrics require admin or a private network"}}, status_code=403) from findata_api.health import evaluate evaluate() body, ctype = M.render() return Response(body, media_type=ctype) web = s.web_dist or Path(s.root) / "apps" / "web" / "out" if web.exists(): app.mount("/", StaticFiles(directory=str(web), html=True), name="web") log.info("web.mounted", path=str(web)) else: @app.get("/", include_in_schema=False) def root(): return {"service": "FinData OS", "docs": "/api/docs", "note": "web console not built"} return app app = create_app()