diff --git a/server/__main__.py b/server/__main__.py index 133122c..ae0a2d9 100644 --- a/server/__main__.py +++ b/server/__main__.py @@ -14,6 +14,7 @@ no audio/no tokens at runtime, not a crash. from __future__ import annotations import os +from contextlib import asynccontextmanager from typing import Any from loguru import logger @@ -32,6 +33,8 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from pipecat.transports.smallwebrtc.connection import SmallWebRTCConnection +from db.pg_migrate import apply_pg_migrations +from db.pg_store import PgStore from db.store import PraxisStore from server.pipeline import build_pipeline from server.vc.verification import verify_credential @@ -47,6 +50,54 @@ HOST = _env("PRAXIS_HOST", "0.0.0.0") PORT = int(_env("PRAXIS_PORT", "8789")) +@asynccontextmanager +async def lifespan(app: FastAPI): + """v0.4 — create the asyncpg Postgres pool on startup, close on shutdown. + + Graceful degradation (D-050, REQ-NFR-MT-01): if PRAXIS_PG_DSN is unset, + the server starts without Postgres — the learner voice loop (SQLite) + is unaffected. app.state.pg_pool / app.state.pg_store are None in that + case and auth/operator routes return 503. + """ + dsn = os.environ.get("PRAXIS_PG_DSN", "").strip() + if not dsn: + logger.warning( + "PRAXIS_PG_DSN not set — starting without Postgres (dev/no-pool mode). " + "Operator auth + cohort endpoints will be unavailable (503). " + "Learner voice loop (SQLite) is unaffected." + ) + app.state.pg_pool = None + app.state.pg_store = None + try: + yield + finally: + return + import asyncpg + + logger.info("Creating asyncpg Postgres pool (min=1, max=10, D-050)") + pool = await asyncpg.create_pool( + dsn=dsn, + min_size=1, + max_size=10, + command_timeout=10, + ) + app.state.pg_pool = pool + app.state.pg_store = PgStore(pool) + try: + applied = await apply_pg_migrations(pool) + if applied: + logger.info(f"Postgres migrations applied: {applied}") + else: + logger.info("Postgres migrations up to date") + try: + yield + finally: + pass + finally: + await pool.close() + logger.info("Postgres pool closed") + + class WebRTCOffer(BaseModel): """Client→server WebRTC offer (SDP + type).""" @@ -54,7 +105,7 @@ class WebRTCOffer(BaseModel): type: str = "offer" -app = FastAPI(title="Praxis v0.1 voice server", version="0.1.0") +app = FastAPI(title="Praxis v0.1 voice server", version="0.1.0", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], # dev — the client is a separate Vite origin