"""FastAPI-приложение внутреннего API рекомендаций.""" import asyncio import hmac import logging from collections.abc import Awaitable, Callable from typing import Annotated from fastapi import Depends, FastAPI, Header, HTTPException, status from fastapi.responses import JSONResponse from pipu_rec.settings import Settings Check = Callable[[], Awaitable[None]] log = logging.getLogger(__name__) CHECK_TIMEOUT_S = 2.0 def create_app(settings: Settings, checks: dict[str, Check] | None = None) -> FastAPI: app = FastAPI(title="pipu-recommender", version=settings.version, docs_url=None, redoc_url=None) checks = checks or {} def require_token(authorization: Annotated[str, Header()] = "") -> None: scheme, _, token = authorization.partition(" ") expected = settings.internal_token if scheme != "Bearer" or not expected or not hmac.compare_digest(token, expected): raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail="auth.invalid_internal_token") @app.get("/healthz") async def healthz() -> dict[str, str]: return {"status": "ok"} async def run_check(name: str, check: Check) -> str: try: await asyncio.wait_for(check(), CHECK_TIMEOUT_S) except Exception as exc: log.warning("readiness check %s failed: %r", name, exc) return "fail" return "ok" @app.get("/readyz") async def readyz() -> JSONResponse: # Проверки параллельные, каждая ограничена таймаутом; причину ошибки пишем только в лог. names = list(checks) outcomes = await asyncio.gather(*(run_check(n, checks[n]) for n in names)) results = dict(zip(names, outcomes, strict=True)) healthy = all(v == "ok" for v in results.values()) return JSONResponse( {"status": "ok" if healthy else "degraded", "checks": results}, status_code=status.HTTP_200_OK if healthy else status.HTTP_503_SERVICE_UNAVAILABLE, ) @app.get("/internal/status", dependencies=[Depends(require_token)]) async def internal_status() -> dict[str, object]: return {"version": settings.version, "models": [], "ranker": "heuristic-v1"} return app