Skip to content

Export

This content is for 0.1. Switch to the latest version for up-to-date documentation.

Art. 15 export — collect a subject’s data into a structured bundle.

class ExportBundle(BaseModel):
generated_at: datetime
incomplete_sources: tuple[str, ...] = ()
records: tuple[ExportRecord, ...] = ()
schema_version: int = MANIFEST_SCHEMA_VERSION
subject_id: str = Field(min_length=1)

Everything held on one subject, with the required Art. 15 metadata.

Fields:

  • generated_at (datetime): When the bundle was assembled (UTC).
  • incomplete_sources (tuple[str, ...]): Sources that failed during collection — surfaced loudly rather than silently omitted.
  • records (tuple[ExportRecord, ...]): Every exported value, grouped by consumers as they wish.
  • schema_version (int): Manifest schema version the bundle was built under.
  • subject_id (str): The identifier the export was requested for.
class Exporter:
def __init__(data_map: DataMap, graph: SubjectGraph, metadata: MetaData, audit_sink: AuditSink, registry: ResolverRegistry | None = None) -> None

Collects all of a subject’s personal data into one structured bundle.

Walks the data map for local data and fans out to registered resolvers for external systems. Resolver failures never silently shrink the bundle — they are recorded in incomplete_sources.

def export_subject(session: Session, subject_id: str, *, refs: tuple[SubjectRef, ...] = ()) -> ExportBundle

Collect everything held on one subject (Art. 15).

Each ref is routed to the resolver whose name equals the ref’s kind (ADR 0008). 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_COMPLETED payload’s skipped_resolvers, not in incomplete_sources. A resolver call that fails puts the resolver’s name in incomplete_sources instead of failing the export. A local database failure propagates after EXPORT_REQUESTED was appended — a requested-but-never- completed trail is the abandonment marker. Input validation (subject id coercion, ref-kind matching) raises before EXPORT_REQUESTED is appended: a malformed call never became a data-subject request, so it deliberately leaves no audit trace.

Blocking call; resolver fan-out runs on an internal event loop, so it must not be invoked on a running event-loop thread — in async web apps dispatch via a threadpool (e.g. FastAPI’s run_in_threadpool). See ADR 0006.

Args:

  • session (Session): An open database session; reads only, never writes.
  • subject_id (str): Identifier on the subject table (see SubjectLink).
  • refs (tuple[SubjectRef, ...]): External-system references for resolver fan-out.

Returns:

  • ExportBundle — The structured bundle including Art. 15 metadata (purposes,
  • ExportBundle — legal bases, retention reasons).

Raises:

  • SubjectResolutionError — If subject_id cannot be coerced to the subject id column’s type.
  • ResolverError — If a ref’s kind matches no registered resolver — a typo must not silently drop an external source from the answer.
class ExportRecord(BaseModel):
category: PiiCategory
field: str = Field(min_length=1)
legal_basis: LegalBasis | None = None
purpose: str | None = None
retention_reason: str | None = None
source: str = Field(min_length=1)
value: object = None

One exported value with its Art. 15 metadata.

Fields:

  • category (PiiCategory): PII category of the value.
  • field (str): The field within the source.
  • legal_basis (LegalBasis | None): Why the data is held, if declared.
  • purpose (str | None): Processing purpose, if declared.
  • retention_reason (str | None): The legal duty keeping the value, if any.
  • source (str): Where the value came from — a table name or resolver name.
  • value (object): The value itself, JSON-encoded.