effaced-fastapi
effaced-fastapi — mount effaced’s data-subject endpoints on a FastAPI app.
Wires the full effaced.EffacedStack from your annotated
declarative base and exposes the trigger points — consent (Art. 7),
export (Art. 15), erasure (Art. 17), and optionally restriction
(Art. 18) — as one fastapi.APIRouter. Your auth stays yours: a
dependency you provide resolves the request’s Subject.
The endpoints call the same engines, under the same audit and idempotency contracts, that you could wire by hand — this package only removes the mechanical wiring.
ConsentRequest
Section titled “ConsentRequest”class ConsentRequest(BaseModel): granted: bool policy_version: str = Field(min_length=1, max_length=255) purpose: str = Field(min_length=1, max_length=255)What the caller decides about consent — nothing more.
The server fills in everything the caller must not control: the
subject id comes from the authenticated Subject,
the timestamp from the server clock, and the source is fixed to
"api". The same body records a grant (granted=True) and a
withdrawal (granted=False) — one call, per Art. 7(3)‘s “as easy to
withdraw as to give”.
Fields:
- granted (
bool):Truefor a grant,Falsefor a withdrawal. - policy_version (
str): Version of the policy text the subject saw. - purpose (
str): The processing purpose consented to (e.g."newsletter").
EffacedFastAPI
Section titled “EffacedFastAPI”class EffacedFastAPI: def __init__(base: type[DeclarativeBase] | None = None, session_factory: sessionmaker | None = None, *, resolvers: Sequence[Resolver] = (), stack: EffacedStack | None = None) -> NoneMounts effaced’s data-subject endpoints on a FastAPI application.
Wires the full effaced.EffacedStack from your declarative
base (or accepts one you prewired) and exposes the trigger points as a
router. Every route is a plain def: FastAPI runs it on its
threadpool, so the sync engines (ADR 0006) never block the event loop
— and your subject provider may still be async.
The integration is deliberately thin: requests are served by the same engines, with the same audit, idempotency, and retention contracts, that you could call by hand. Route paths and response shapes are public API — the responses are the engines’ own result models, so what an endpoint returns changes only when the underlying engine’s behaviour does (widened SemVer).
Fields:
- session_dependency (
Callable[[], Iterator[Session]]): The default per-request transaction dependency (with session_factory.begin()). Built once, so it can be overridden by identity viaapp.dependency_overrides. - stack (
EffacedStack): The wired engines, for direct calls beyond the endpoints.
EffacedFastAPI.lifespan
Section titled “EffacedFastAPI.lifespan”def lifespan(*, poll_interval: float = 5.0) -> Callable[[FastAPI], AbstractAsyncContextManager[None]]Build a FastAPI lifespan that drains the outbox in the background.
Starts a SagaWorker (a daemon thread —
never a task on the serving loop, ADR 0006) on startup and stops
it on shutdown. FastAPI accepts a single lifespan: if your app
already has one, construct the worker inside it instead.
Args:
- poll_interval (
float): Seconds the worker sleeps when the outbox comes back empty.
Returns:
Callable[[FastAPI], AbstractAsyncContextManager[None]]— A lifespan context manager forFastAPI(lifespan=...).
EffacedFastAPI.router
Section titled “EffacedFastAPI.router”def router(subject: SubjectProvider, *, session: Callable[..., Iterator[Session]] | None = None, restriction: bool = False, tags: Sequence[str] = ('gdpr',)) -> APIRouterBuild the data-subject router around your subject provider.
Include it with a prefix that scopes it to the authenticated user,
e.g. app.include_router(gdpr.router(subject=...), prefix="/me")
— the erasure route lives at the prefix root (DELETE /me).
Default endpoints: POST /consent (record grant/withdrawal),
GET /consent/{purpose} (current status), GET /export
(Art. 15 bundle), and DELETE at the prefix root (Art. 17
erasure). Rectification ships no endpoint: which corrections a
subject may self-serve is an authorization decision your
application owns — call Rectifier.rectify_subject
from your own route.
.. warning::
The router authorizes nothing — your subject dependency
is the only access control. It authenticates no one and never
checks that the caller may act on the resolved subject (ADR
0020): it exports or erases exactly the
Subject the dependency returns. If
that dependency reads an identifier the caller supplies
(a header, path, query, or body field) without proving the
authenticated caller is that subject — or is authorized to
act on it — any caller can export or erase any subject (an
insecure direct object reference). Resolve the subject from a
verified session or token, and reject a request whose claimed
subject does not match it.
Args:
- subject (
SubjectProvider): Dependency resolving the request’sSubject— your auth decides who the subject is and which external refs they carry. It is the trust boundary: it must prove the authenticated caller is (or may act on) the subject it returns (see the warning above). - session (
Callable[..., Iterator[Session]] | None): Per-router override ofsession_dependency. - restriction (
bool): Also exposePOST /restrictionandGET /restriction(Art. 18 flag-keeping, ADR 0014). - tags (
Sequence[str]): OpenAPI tags applied to every route.
Returns:
APIRouter— The router, ready forapp.include_router.
RestrictionRequest
Section titled “RestrictionRequest”class RestrictionRequest(BaseModel): purpose: str | None = Field(default=None, min_length=1, max_length=255) reason: str | None = Field(default=None, max_length=255) restricted: boolWhat the caller decides about a restriction of processing (Art. 18).
The server fills in the subject id (from the authenticated
Subject), the timestamp, and the "api"
source. Recording is flag-keeping, never enforcement — nothing in
effaced consults the flag before processing (ADR 0014).
Fields:
- purpose (
str | None): The processing purpose restricted;Nonemeans all processing for the subject. - reason (
str | None): Free-text grounds (e.g. the Art. 18(1) basis claimed). Kept in the ledger’s history, never mirrored into audit payloads. - restricted (
bool):Trueplaces a restriction,Falselifts one.
SagaWorker
Section titled “SagaWorker”class SagaWorker: def __init__(runner: SagaRunner, *, poll_interval: float = 5.0) -> NoneDrives SagaRunner.run_once on a daemon thread.
run_once makes blocking database calls between awaits, so it must
never run on a serving event loop (ADR 0006). This worker packages the
sanctioned alternative: a daemon thread running its own private
asyncio loop, claiming batches until stopped and polling gently
while the queue is empty.
A failing batch (database briefly down, resolver dependency
unreachable) is logged and retried after the poll interval — the
worker never dies silently, because a stalled drain loop means
data-subject requests stop completing. Running several workers at
once is safe: claiming uses FOR UPDATE SKIP LOCKED and crashed
claims are re-claimed after the lease expires (ADR 0010).
SagaWorker.start
Section titled “SagaWorker.start”def start() -> NoneStart the drain thread; a no-op if it is already running.
SagaWorker.stop
Section titled “SagaWorker.stop”def stop(*, timeout: float = 10.0) -> NoneSignal the drain loop to exit and wait for the thread to finish.
The loop notices the signal at its next batch boundary or sleep wake-up, so stopping can take up to one poll interval.
Args:
- timeout (
float): Seconds to wait for the thread to join; the thread is a daemon, so a missed join never blocks interpreter exit. On a timed-out join the worker keeps its thread handle, so a subsequentstartstays a no-op instead of spawning a second concurrent drain loop.
Subject
Section titled “Subject”class Subject(BaseModel): refs: tuple[SubjectRef, ...] = () subject_id: ValidatedSubjectIdWho the request is about — resolved by your auth, never by effaced.
The router never authenticates and never guesses where a subject lives outside the database: both are application knowledge. Your subject provider (any FastAPI dependency returning this model) supplies the subject id your models are keyed by and the external refs — e.g. the Stripe customer id you stored at signup — that route the subject’s export and erasure to registered resolvers.
Fields:
- refs (
tuple[SubjectRef, ...]): Where the subject lives in external systems; each ref’skindnames the resolver that handles it (ADR 0008). - subject_id (
ValidatedSubjectId): The id the annotated models key the subject by — a single-columnstror a compositeCompositeSubjectId(ADR 0025), passed through to the engines unchanged.
SubjectProvider
Section titled “SubjectProvider”SubjectProvider: TypeAlias = Callable[..., Subject] | Callable[..., Awaitable[Subject]]A FastAPI dependency resolving the request’s Subject.
Sync or async, and free to depend on your own auth dependencies — a plain
def route can depend on an async def provider; FastAPI resolves
the dependency tree on the event loop either way.