feat(slice-1): FastAPI factory + /healthz; self-migrate at startup (INV-1/§6.4)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 08:38:42 -07:00
parent 9462423642
commit ef385340e8
3 changed files with 90 additions and 2 deletions
+48 -2
View File
@@ -1,5 +1,51 @@
"""ecomm backend — FastAPI app factory + the REST BFF.
Stub in SLICE-1 Task 1 so the top layer of the import-linter contract exists from the
start; the FastAPI factory and /healthz are added in Task 5.
SLICE-1 mounts only /healthz; the auth and storefront endpoints (§6.4) grow here in
later slices. create_app() opens the connection pool and self-migrates the database
at startup (INV-1, INV-7): the app boots against empty persistence and applies its
own schema — there is no seed step.
"""
from __future__ import annotations
from contextlib import asynccontextmanager
import psycopg
from fastapi import Depends, FastAPI, Response
from app.platform import config, db
from app.platform.deps import get_conn
def create_app(database_url: str | None = None) -> FastAPI:
dsn = database_url or config.database_url()
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.pool = db.open_pool(dsn)
with app.state.pool.connection() as conn:
db.migrate(conn) # self-migrate at startup (INV-1, INV-7)
try:
yield
finally:
app.state.pool.close()
app = FastAPI(title="ecomm", version="0.1", lifespan=lifespan)
@app.get("/healthz")
def healthz(response: Response, conn: psycopg.Connection = Depends(get_conn)):
"""Liveness + readiness: process up, DB reachable, migrations current (§6.4)."""
try:
conn.execute("SELECT 1")
pending = db.pending_migrations(conn)
except Exception:
response.status_code = 503
return {"status": "unavailable", "reason": "database_unreachable"}
if pending:
response.status_code = 503
return {"status": "unavailable", "reason": "migrations_pending"}
return {"status": "ok"}
return app
app = create_app()