effaced-django
Django ORM adapter for effaced.
Author PII declarations on Django models with effaced_model,
pii, and subject_link; translate them into the effaced
manifest and a foreign-key-resolved subject graph; and wire every engine with
DjangoEffacedStack. Execution reuses the SQLAlchemy executors on a
MetaData derived from Model._meta (ADR 0006), so erasure/export/audit
semantics are identical to the SQLAlchemy adapter.
AnnotationRegistry
Section titled “AnnotationRegistry”class AnnotationRegistry: def __init__() -> NoneAn append-only collection of ModelAnnotation records.
Registration is explicit (never auto-discovered) — the registry is the auditable “where is my PII” declaration, mirroring the resolver registry.
Fields:
- annotations (
tuple[ModelAnnotation, ...]): The registered annotations, in registration order.
AnnotationRegistry.clear
Section titled “AnnotationRegistry.clear”def clear() -> NoneDrop every registration (used to isolate test modules).
AnnotationRegistry.register
Section titled “AnnotationRegistry.register”def register(annotation: ModelAnnotation) -> NoneAdd one model’s declarations to the registry.
build_metadata
Section titled “build_metadata”def build_metadata(annotations: Iterable[ModelAnnotation], *, metadata: MetaData | None = None) -> MetaDataBuild an effaced-annotated SQLAlchemy MetaData from Django models.
Each annotation’s model becomes a Table carrying the subject link in
its info dict and its PII specs on the matching columns, with foreign
keys translated so effaced.resolve_subject_graph_from_fk can walk
them.
Args:
- annotations (
Iterable[ModelAnnotation]): The model annotations to translate (seeeffaced_django.AnnotationRegistry). - metadata (
MetaData | None): An existingMetaDatato populate; a fresh one is created when omitted.
Returns:
MetaData— The populatedMetaData.
Raises:
EffacedDjangoError— If a field’s type has no SQLAlchemy equivalent.
collect_django_data_map
Section titled “collect_django_data_map”def collect_django_data_map(annotations: Iterable[ModelAnnotation] | None = None) -> DataMapCollect the manifest from annotated Django models.
Translates the models to SQLAlchemy metadata and reuses the core collector, so the manifest is identical to a natively-authored SQLAlchemy schema.
Args:
- annotations (
Iterable[ModelAnnotation] | None): The model annotations to collect; defaults to the process-wideeffaced_django.default_registry.
Returns:
DataMap— The collectedDataMap.
Raises:
EffacedDjangoError— If a field type cannot be mapped.ManifestError— If the annotations are invalid (propagated fromeffaced.collect_data_map).
default_registry
Section titled “default_registry”default_registry = AnnotationRegistry()The process-wide registry that bare effaced_model writes into.
DjangoEffacedStack
Section titled “DjangoEffacedStack”class DjangoEffacedStack: def __init__(metadata: MetaData, data_map: DataMap, graph: SubjectGraph, tables: EffacedTables, session_factory: sessionmaker, registry: ResolverRegistry, audit_sink: AuditSink, outbox: Outbox, exporter: Exporter, planner: ErasurePlanner, rectifier: Rectifier, consent: ConsentLedger, restriction: RestrictionLedger, sweeper: RetentionSweeper, saga_runner: SagaRunner) -> NoneEvery effaced engine, wired once from annotated Django models.
The Django counterpart of effaced.EffacedStack: the same named
handles, governed by the same contracts, built by from_models.
Construction executes no SQL — the owned tables are mounted onto the
derived MetaData (use metadata.create_all or a caller migration
to materialize them; native Django migrations for the owned tables are a
planned follow-up).
Fields:
- audit_sink (
AuditSink): The append-only trail every engine records into. - consent (
ConsentLedger): The Art. 7 consent ledger. - data_map (
DataMap): The manifest collected from the models. - exporter (
Exporter): The Art. 15 export engine. - graph (
SubjectGraph): The subject graph resolved from foreign-key constraints. - metadata (
MetaData): The SQLAlchemyMetaDataderived from the Django models. - outbox (
Outbox): The durable queue for external erasure/rectification calls. - planner (
ErasurePlanner): The Art. 17 erasure engine, execution-ready. - rectifier (
Rectifier): The Art. 16 rectification engine, execution-ready. - registry (
ResolverRegistry): The resolver registry routing external refs. - restriction (
RestrictionLedger): The Art. 18 restriction-of-processing ledger. - saga_runner (
SagaRunner): The outbox drainer — drive it from a worker, never on a serving event loop (ADR 0006). - session_factory (
sessionmaker): The application’s session factory, as provided. - sweeper (
RetentionSweeper): The Art. 5(1)(e) retention sweeper (report-only). - tables (
EffacedTables): Handles to the four effaced-owned tables.
DjangoEffacedStack.from_models
Section titled “DjangoEffacedStack.from_models”def from_models(session_factory: sessionmaker, *, annotations: Iterable[ModelAnnotation] | None = None, resolvers: Sequence[Resolver] = (), registry: ResolverRegistry | None = None, audit_sink: AuditSink | None = None) -> DjangoEffacedStackWire the full stack from annotated Django models.
Translates the models to metadata, collects the data map, resolves the subject graph from foreign keys, mounts the owned tables, and constructs every engine with the SQLAlchemy executors. Resolver registration stays explicit (never discovered).
Args:
- session_factory (
sessionmaker): Factory producing sessions on the application database, used by the components that operate outside a caller transaction (audit sink, outbox claims). Bind it to the same database Django uses, and run effaced inside atransaction.atomic()block so the outbox enqueue shares the transaction with the local erasure (ADR 0010). - annotations (
Iterable[ModelAnnotation] | None): Model annotations to wire; defaults to the process-wideeffaced_django.default_registry. - resolvers (
Sequence[Resolver]): External-system resolvers to register, by instance. - registry (
ResolverRegistry | None): A prebuilt registry, mutually exclusive withresolvers. - audit_sink (
AuditSink | None): Trail override; defaults to aeffaced.DatabaseAuditSinkon the mountedeffaced_audit_eventstable.
Returns:
DjangoEffacedStack— The wired stack.
Raises:
ConfigurationError— If bothresolversandregistryare given.EffacedDjangoError— If a model field cannot be translated.ManifestError— If the annotations are invalid.SubjectResolutionError— If the subject graph cannot be resolved.
effaced_model
Section titled “effaced_model”def effaced_model(link: SubjectLink | None = None, *, registry: AnnotationRegistry | None = None) -> Callable[[type[M]], type[M]]Register a Django model’s effaced declarations, returning it unchanged.
Reads the model’s nested Effaced class for per-field
PiiSpec declarations and records them together with the
subject link.
Args:
- link (
SubjectLink | None): How this model reaches the subject (seeeffaced_django.subject_link);Nonefor a model that declares no reachability. - registry (
AnnotationRegistry | None): Registry to record into; defaults to the process-widedefault_registry.
Returns:
Callable[[type[M]], type[M]]— A decorator that records the model and returns it unmodified.
EffacedDjangoError
Section titled “EffacedDjangoError”class EffacedDjangoError(Exception): ...A Django model could not be translated into effaced metadata.
Raised loudly (never swallowed) when a field type has no SQLAlchemy equivalent the engine can scope or anonymize — guessing would risk erasing the wrong data.
ModelAnnotation
Section titled “ModelAnnotation”class ModelAnnotation: def __init__(model: type[Model], subject_link: SubjectLink | None, column_specs: Mapping[str, PiiSpec]) -> NoneOne Django model’s effaced declarations.
Fields:
- column_specs (
Mapping[str, PiiSpec]): PII specs keyed by Django field name (not the DB column name — the introspector maps field names to columns). - model (
type[Model]): The declared Django model class. - subject_link (
SubjectLink | None): How the model reaches the subject, orNoneif the model carries only PII columns and no declared link (an error the resolver raises loudly).
def pii(category: PiiCategory, *, erasure: ErasureStrategy = ErasureStrategy.DELETE, retention: RetentionPolicy | None = None, legal_basis: LegalBasis | None = None, purpose: str | None = None, description: str | None = None) -> PiiSpecDeclare a Django model field as personal data.
Assign the result to an attribute named after the field on the model’s
nested Effaced class. The signature mirrors
effaced.pii; only the return type differs (a bare
PiiSpec, since Django has no info dict to wrap).
Args:
- category (
PiiCategory): What kind of personal data the field holds. - erasure (
ErasureStrategy): Erasure behaviour; defaults to deletion. - retention (
RetentionPolicy | None): Legal retention duty; required forRETAIN. - legal_basis (
LegalBasis | None): Lawful basis, surfaced in Art. 15 exports. - purpose (
str | None): Processing purpose, surfaced in Art. 15 exports. - description (
str | None): Free-text note for audits.
Returns:
PiiSpec— The column’sPiiSpec.
subject_link
Section titled “subject_link”def subject_link(path: str, *, subject_id_columns: tuple[str, ...] | str = 'id') -> SubjectLinkDeclare how a Django model reaches the data subject.
Pass the result to effaced_model. The subject
model declares subject_link(""); every other model names the dotted
chain of target tables (Model._meta.db_table values) by which a
foreign key reaches the subject — e.g. a comment two hops away declares
subject_link("blog_post.auth_user"). This differs from the
SQLAlchemy adapter (which names ORM relationship attributes) because the
Django adapter resolves the graph from foreign-key constraints, not ORM
mappers (effaced.resolve_subject_graph_from_fk).
Args:
- path (
str): Dotted chain of target table names to the subject table. - subject_id_columns (
tuple[str, ...] | str): Ordered identifier column(s) on the subject table. A barestris the single-column case (the default); a tuple declares a composite subject key (ADR 0025).
Returns:
SubjectLink— class:~effaced.SubjectLink.