Skip to content

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.

class AnnotationRegistry:
def __init__() -> None

An 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.
def clear() -> None

Drop every registration (used to isolate test modules).

def register(annotation: ModelAnnotation) -> None

Add one model’s declarations to the registry.

def build_metadata(annotations: Iterable[ModelAnnotation], *, metadata: MetaData | None = None) -> MetaData

Build 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 (see effaced_django.AnnotationRegistry).
  • metadata (MetaData | None): An existing MetaData to populate; a fresh one is created when omitted.

Returns:

  • MetaData — The populated MetaData.

Raises:

  • EffacedDjangoError — If a field’s type has no SQLAlchemy equivalent.
def collect_django_data_map(annotations: Iterable[ModelAnnotation] | None = None) -> DataMap

Collect 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:

Returns:

  • DataMap — The collected DataMap.

Raises:

  • EffacedDjangoError — If a field type cannot be mapped.
  • ManifestError — If the annotations are invalid (propagated from effaced.collect_data_map).
default_registry = AnnotationRegistry()

The process-wide registry that bare effaced_model writes into.

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) -> None

Every 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 SQLAlchemy MetaData derived 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.
def from_models(session_factory: sessionmaker, *, annotations: Iterable[ModelAnnotation] | None = None, resolvers: Sequence[Resolver] = (), registry: ResolverRegistry | None = None, audit_sink: AuditSink | None = None) -> DjangoEffacedStack

Wire 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 a transaction.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-wide effaced_django.default_registry.
  • resolvers (Sequence[Resolver]): External-system resolvers to register, by instance.
  • registry (ResolverRegistry | None): A prebuilt registry, mutually exclusive with resolvers.
  • audit_sink (AuditSink | None): Trail override; defaults to a effaced.DatabaseAuditSink on the mounted effaced_audit_events table.

Returns:

  • DjangoEffacedStack — The wired stack.

Raises:

  • ConfigurationError — If both resolvers and registry are given.
  • EffacedDjangoError — If a model field cannot be translated.
  • ManifestError — If the annotations are invalid.
  • SubjectResolutionError — If the subject graph cannot be resolved.
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 (see effaced_django.subject_link); None for a model that declares no reachability.
  • registry (AnnotationRegistry | None): Registry to record into; defaults to the process-wide default_registry.

Returns:

  • Callable[[type[M]], type[M]] — A decorator that records the model and returns it unmodified.
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.

class ModelAnnotation:
def __init__(model: type[Model], subject_link: SubjectLink | None, column_specs: Mapping[str, PiiSpec]) -> None

One 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, or None if 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) -> PiiSpec

Declare 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 for RETAIN.
  • 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’s PiiSpec.
def subject_link(path: str, *, subject_id_columns: tuple[str, ...] | str = 'id') -> SubjectLink

Declare 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 bare str is the single-column case (the default); a tuple declares a composite subject key (ADR 0025).

Returns:

  • SubjectLink — class:~effaced.SubjectLink.