Files
pipu-music/services/recommender/pipu_rec/api/app.py
T
mrleo1nidandClaude Opus 5.5 4fa1add80d
ci / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 48s
ci / api (push) Successful in 51s
ci / recommender (push) Canceled after 2s
ci / ts (packages + web) (push) Canceled after 0s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (apps/web/Dockerfile, web, ) (push) Canceled after 0s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (services/api/Dockerfile, api, api) (push) Canceled after 0s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (services/api/Dockerfile, storage-agent, storage-agent) (push) Canceled after 0s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (services/recommender/Dockerfile, recommender, ) (push) Canceled after 0s
ci / mobile (Metro-бандл Android) (push) Canceled after 0s
fix: address project review findings
Security and robustness:
- storage-agent rejects an empty INTERNAL_TOKEN (config notEmpty + middleware)
- api takes the client IP only from X-Real-IP (chi RealIP trusted True-Client-IP)
- /readyz reports ok/fail and logs the cause; checks run in parallel with a timeout
- storage-agent shuts down gracefully; shared httpx.NewServer/Serve with IdleTimeout
- goose migrations take a Postgres advisory lock
- recommender readyz: per-check timeout, redis socket timeouts

Clients:
- Android release allows cleartext HTTP for LAN servers (expo-build-properties)
- checkServer times out after 10s; connect button is disabled while checking

Deploy and CI:
- install.sh hands cache/uploads/updates/models to the non-root service users
- update.sh refuses downgrades unless PIPU_ALLOW_DOWNGRADE=1
- recommender/analyzer get only the env they need; required secrets fail fast
- nginx security headers and CSP; strip True-Client-IP
- dev compose binds ports to 127.0.0.1; postgres/redis images pinned
- version.mjs fails on a malformed explicit tag instead of releasing 0.0.0-dev
- contract check catches untracked generated files
- final releases fail without a release keystore; changelog skips rc tags

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 00:46:46 +03:00

60 lines
2.3 KiB
Python

"""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