Export
An Art. 15 answer has to cover everything you hold on a subject — local
tables and the external systems that hold PII on your behalf. The
Exporter walks the data map for local data and fans out
to registered resolvers for the rest:
exporter = Exporter(data_map, graph, Base.metadata, audit, registry)bundle = exporter.export_subject(session, "42", refs=(stripe_ref,))export_subject only reads — it never writes through your session. It is
a blocking call that drives async resolvers on an internal event loop, so
in async web apps dispatch it via a threadpool (FastAPI:
run_in_threadpool, or a plain def route).
The bundle
Section titled “The bundle”ExportBundle is a frozen pydantic model:
subject_idandgenerated_at(UTC).schema_version— the manifest schema version the bundle was built under, so downstream tooling can interpret old bundles correctly.records— oneExportRecordper exported value, local and external alike:source(table or resolver name),field,category, thevalueitself, and the Art. 15 metadata declared in the annotations —legal_basis,purpose,retention_reason. Recording why data is held is required Art. 15(1)(a) metadata; effaced surfaces exactly what you declared. Records from retention-only resolvers also carryexpires_at— the instant the value is guaranteed to expire at its source, where on-demand erasure is unavailable (ADR 0022); it staysNoneeverywhere else.incomplete_sources— see below.
Group and render records however your product needs;
bundle.model_dump(mode="json") gives a JSON-ready payload.
Art. 20 portability
Section titled “Art. 20 portability”Art. 20 entitles the subject to receive their data “in a structured,
commonly used and machine-readable format”. The bundle already is that
format: frozen pydantic, schema-versioned, JSON-serializable, with every
record carrying its source, field, and category. There is no separate
portability mechanism — hand the subject the same bundle (or its
model_dump(mode="json") payload) your Art. 15 flow produces.
The determinations stay yours: whether Art. 20 applies to a given request (it covers data processed by automated means on consent or contract, which the subject provided) and whether the bundle’s scope matches that subset is a legal call effaced does not make. The mechanism is the format and the collection; the applicability is not.
The incomplete_sources contract
Section titled “The incomplete_sources contract”A resolver call that fails does not fail the export and does not
silently shrink the bundle — the resolver’s name lands in
incomplete_sources. A bundle with a non-empty incomplete_sources is an
honest partial answer: you know exactly which system is missing and can
retry or escalate. Silent omission would be the worst outcome — a subject
(or a regulator) reading a bundle as complete when it isn’t.
The boundary cases are deliberate:
- A ref whose
kindmatches no registered resolver raisesResolverErrorbefore any work or audit event — a typo’d kind must never silently drop an external source from the answer. - A registered resolver with no matching ref is skipped: “the
subject has no identity in that system” is a complete answer, recorded
in the
EXPORT_COMPLETEDaudit payload’sskipped_resolvers, never inincomplete_sources. - A local database failure propagates as an exception after
EXPORT_REQUESTEDwas appended — a requested-but-never-completed trail is the abandonment marker.
Audit trail
Section titled “Audit trail”Every export appends EXPORT_REQUESTED before collection and
EXPORT_COMPLETED after, with record counts, incomplete sources, and
skipped resolvers in the payload — counts and names only, never the
exported values. Input validation failures raise before any event: a
malformed call never became a data-subject request.
Full signatures: API reference.