"""FastAPI surface for the hosted Phase Registry and Extension Registry (WP-0006-T03). Only registration and read endpoints live here — the Target Ledger append API (T04), Metrics (T05), and Attestation (T06) are separate components per `specs/TechnicalSpecificationDocument.md` §4.1 and are not implemented in this module. Every route delegates to `target_revenue.registry`; this file's only job is HTTP framing (status codes, request/response shape) and reading the bearer token, not conformance logic. """ from __future__ import annotations import os from typing import Any from fastapi import Depends, FastAPI, HTTPException, Request from psycopg import Connection from psycopg_pool import ConnectionPool from .. import registry app = FastAPI(title="Target Revenue Trust Service — Registries", version="0.1.0") _DATABASE_URL_ENV = "TRF_DATABASE_URL" def get_pool() -> ConnectionPool: if not hasattr(app.state, "pool"): dsn = os.environ.get(_DATABASE_URL_ENV) if not dsn: raise RuntimeError(f"{_DATABASE_URL_ENV} is not set") app.state.pool = ConnectionPool(dsn, min_size=1, max_size=5, open=True) return app.state.pool def get_connection(): pool = get_pool() with pool.connection() as conn: yield conn def get_licensor(request: Request, conn: Connection = Depends(get_connection)) -> registry.Licensor: auth = request.headers.get("authorization", "") if not auth.lower().startswith("bearer "): raise HTTPException(status_code=401, detail="missing bearer token") token = auth.split(" ", 1)[1].strip() try: return registry.authenticate(conn, token) except registry.RegistrationError as exc: raise HTTPException(status_code=401, detail=str(exc)) from exc @app.post("/phases", status_code=201) def register_phase( manifest: dict[str, Any], licensor: registry.Licensor = Depends(get_licensor), conn: Connection = Depends(get_connection), ) -> dict[str, str]: try: registry.register_phase_manifest(conn, licensor, manifest) except registry.RegistrationError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc return {"phase_id": manifest["phase"]["id"], "status": "registered"} @app.get("/phases/{phase_id:path}") def read_phase(phase_id: str, conn: Connection = Depends(get_connection)) -> dict[str, Any]: manifest = registry.get_phase_manifest(conn, phase_id) if manifest is None: raise HTTPException(status_code=404, detail="phase not found") return manifest @app.post("/extensions", status_code=201) def register_extension( extension: dict[str, Any], licensor: registry.Licensor = Depends(get_licensor), conn: Connection = Depends(get_connection), ) -> dict[str, str]: try: registry.register_extension(conn, licensor, extension) except registry.RegistrationError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc return { "extension_id": extension["id"], "version": extension["version"], "status": "registered", } @app.get("/extensions/{extension_id}/{version}") def read_extension( extension_id: str, version: str, conn: Connection = Depends(get_connection) ) -> dict[str, Any]: result = registry.get_extension(conn, extension_id, version) if result is None: raise HTTPException(status_code=404, detail="extension not found") return result