"""Operator API for the bounded Enrollment & Provisioning Plane v1.""" from __future__ import annotations from typing import Annotated, Literal from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request from pydantic import BaseModel, ConfigDict from sqlmodel import Session from ..core.security import require_operator_request from ..db.session import get_session from ..schemas.enrollment import EnrollmentPlanV1, EnrollmentTrustConfigV1 from ..services.enrollment_plane import EnrollmentError, EnrollmentPlane from ..services.repopulse_adoption_journal import ( RepoPulseAdoptionJournalError, get_repopulse_adoption_status, ) def _require_operator(request: Request) -> str: return require_operator_request(request) router = APIRouter( prefix="/api/enrollments", tags=["enrollments"], dependencies=[Depends(_require_operator)], ) class EnrollmentRequestV1(BaseModel): model_config = ConfigDict(extra="forbid") plan: EnrollmentPlanV1 operation: Literal["preflight", "enroll"] = "preflight" EnrollmentSession = Annotated[Session, Depends(get_session)] EnrollmentIdempotencyKey = Annotated[str | None, Header(alias="Idempotency-Key")] EnrollmentLimit = Annotated[int, Query(ge=1, le=250)] def _idempotency(value: str | None) -> str: if not value: raise HTTPException(status_code=400, detail="Idempotency-Key is required for enrollment mutations.") return value.strip() @router.get("/capabilities") def enrollment_capabilities(session: EnrollmentSession): return EnrollmentPlane().capabilities(session) @router.get("/trust") def enrollment_trust_status(session: EnrollmentSession): return EnrollmentPlane.trust_status(session) @router.put("/trust") def update_enrollment_trust(payload: EnrollmentTrustConfigV1, session: EnrollmentSession): return EnrollmentPlane.set_trust_config(session, payload) @router.post("") def request_enrollment( payload: EnrollmentRequestV1, session: EnrollmentSession, idempotency_key: EnrollmentIdempotencyKey = None, ): plane = EnrollmentPlane() try: if payload.operation == "preflight": return plane.preflight(session, payload.plan) return plane.enroll(session, payload.plan, idempotency_key=_idempotency(idempotency_key)) except EnrollmentError as exc: raise HTTPException(status_code=409, detail=str(exc)) from exc @router.get("") def list_enrollments(session: EnrollmentSession, limit: EnrollmentLimit = 100): return {"items": EnrollmentPlane().list_enrollments(session, limit=limit)} @router.get("/repopulse/adoption") def repopulse_adoption_status(session: EnrollmentSession): """Read the sole fixed migration journal through the operator boundary.""" try: return get_repopulse_adoption_status(session) except RepoPulseAdoptionJournalError as exc: raise HTTPException(status_code=503, detail=exc.code) from None @router.get("/{enrollment_id}") def get_enrollment(enrollment_id: str, session: EnrollmentSession): try: return EnrollmentPlane().get_enrollment(session, enrollment_id) except EnrollmentError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc