Skip to content

Python API

The public API. Everything here is importable directly from consentml.

Tracking

consentml.track

The @track decorator: lineage capture around a training function.

track(*, model_name, source, hash_subject_ids=True, db_path=None)

Record training-data lineage for the decorated training function.

The source is loaded first and its payload passed to the decorated function as the first positional argument -- the caller does not supply training data. Loading first means a bad source fails immediately rather than after training has already run.

The model is hashed (SHA-256 of its pickle) and the lineage record is written only after training completes, so a training run that raises leaves nothing behind.

Sources

consentml.sources.base.Source

Bases: Protocol

consentml.sources.base.SourceResult dataclass

payload instance-attribute

Handed to the training function untouched. ConsentML never inspects it, so a pandas DataFrame, a Spark DataFrame, or anything else works.

subject_ids instance-attribute

The contract every Source.load() must uphold for this field:

  • Distinct: one entry per subject, even if the underlying rows repeat the same subject (e.g. a join or a UNION ALL). Sources dedupe with .unique() for exactly this reason -- an undeduped list would inflate n_subjects with rows that are not additional coverage.
  • Non-null: a null subject ID cannot be revoked -- there is no value a later revocation request could ever match. Worse, stringifying a null (see below) turns it into a distinct phantom subject ("nan", "None", or "" depending on pandas version and dtype), silently inflating n_subjects with coverage that was never real. Sources must reject nulls before they reach this list, not stringify them into it.
  • Stringified: every element is already str, regardless of the underlying column's type. A source that skips this (e.g. a Postgres uuid column, or any non-text pandas dtype) hands the store a value its SQL layer cannot bind, which fails after the training function has already run -- exactly the split-observation failure this interface exists to prevent (see the module docstring).

Note: frozen=True stops attribute reassignment (result.subject_ids = ..) but the list object itself is still mutable in place. Sources return a fresh list each call, so this hasn't bitten anyone -- flagged here so a future caller who wants to hold onto a SourceResult doesn't assume more protection than the dataclass actually gives.

provenance = field(default_factory=dict) class-attribute instance-attribute

JSON-serializable record of where the data came from. Discriminated by a "kind" key; every other field is that kind's business.

consentml.sources.DataFrameSource

Track a DataFrame the caller already has in memory.

label is caller-asserted and unverifiable -- ConsentML has no way to check where an in-memory frame came from. It is recorded under kind="dataframe" precisely so a reader can tell it apart from a connector-verified record.

consentml.sources.postgres.PostgresSource

Track a training set read from Postgres with arbitrary SELECT SQL.

Revocation

consentml.revoke

The revoke() API: consent-revocation reporting.

revoke() never modifies training data or models. It reports which models a subject's data reached and records that the revocation request was processed.

revoke(*, subject_id, db_path=None, dry_run=False)

Report every model trained on this subject's data.

Matches both hashed and raw stored subject values, so it works whether training used hash_subject_ids=True or False. Unless dry_run, appends a revocation event to the audit log (payload holds the hashed key only).

consentml.AffectedModel dataclass

consentml.AffectedModelsReport dataclass

Verification

consentml.verify_audit_log(*, db_path=None, expected_head=None)

Verify the audit log's hash chain and its agreement with the tables.

A hash chain alone cannot detect a wholesale rewrite from genesis. Pass expected_head with a previously recorded head_hash -- anchored somewhere outside this database -- to check that the anchor is still present somewhere in the current chain. An entry's hash transitively depends on every entry before it, so finding the anchor proves everything up to that point is byte-for-byte intact; new entries appended after it are a legitimate extension, not a mismatch.

This proves history up to the anchor point only. It says nothing about entries appended after it -- a sophisticated attacker can append validly-chained forged entries past the anchor, and no anchor taken before those entries can detect that.

consentml.VerificationReport dataclass

consentml.VerificationFinding dataclass

Export

consentml.build_dossier(*, subject_id, db_path=None)

Assemble the dossier for one subject. Never writes to the database.

Verification runs FIRST, and a missing or foreign database returns here before revoke() is ever called. That ordering is load-bearing, not stylistic: revoke() constructs a LineageStore, and LineageStore.init creates parent directories and runs the schema script against any path that lacks one. Calling revoke() first against a typoed --db would silently create an empty database, find zero affected models, verify the empty log as clean, and emit an official-looking dossier stating that no models were trained on this person's data. That false clean is the worst available bug in this feature, and it is the same hazard verify.py was hardened against; composing revoke() into a new caller re-introduces it unless this order is preserved.

consentml.Dossier dataclass

consentml.render_html(dossier)

A self-contained HTML dossier. No external assets, prints to PDF.

consentml.render_json(dossier)

The dossier as indented JSON.

consentml.render_pdf(dossier)

The dossier as a PDF. Requires the optional [pdf] extra.

reportlab is imported here rather than at module scope so that importing consentml -- or rendering HTML -- never requires the extra. The ImportError is translated into a ConsentMLError naming the exact install command, because a raw traceback mentioning 'reportlab' does not tell an operator what to do about it. This function is the only entry point, so catching it here also covers the imports inside _pdf_story().

Migration

consentml.migrate_database(*, db_path=None, allow_unverified=False)

Migrate a lineage database onto schema v2.

Verifies before and after. Refuses to migrate a database that fails verification unless allow_unverified is set.

This function's never-raise contract covers hostile database contents -- like verify_audit_log(), a corrupt or tampered lineage database is always reported as a MigrationResult, never a traceback. It does NOT cover I/O failures while opening the database: a directory at the path or a permission-denied file was never read at all, which is a different operator problem (fix the path / fix permissions) from "this is a readable database with a problem in it" -- so sqlite3.Error and OSError raised while opening the original database are deliberately let through here, exactly as verify_audit_log() lets them through, so the CLI can report that class of failure distinctly (exit 2, vs. exit 1 for a reported finding). Widening this except to swallow them would make exit 2 unreachable and silently mislabel "permission denied" as "database problem". _migrate_database does the real work; every step that touches the original database is already individually guarded, so this outer catch is a last-resort net for whatever that per-step analysis missed, not a substitute for it.

consentml.MigrationResult dataclass

Errors

consentml.ConsentMLError

Bases: Exception

Raised for ConsentML usage errors (bad arguments, missing data).