100 lines
3.4 KiB
Python
100 lines
3.4 KiB
Python
|
|
"""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
|