Skip to main content

ConsentML

PyPI version CI

Data-lineage tracking and consent-revocation reporting for production ML pipelines. Add one decorator to your training function; when a user revokes consent, ConsentML tells you which deployed models were trained on their data and produces a tamper-evident audit trail.

Documentation

This README covers the basics. The full site has a Quickstart, the CLI and API reference, and why ConsentML is scoped the way it is.

Tracking a training run

ConsentML loads the training data, so the lineage it records cannot disagree with what the model actually trained on:

from consentml import track
from consentml.sources.postgres import PostgresSource

@track(
    model_name="readmission-risk",
    source=PostgresSource(
        dsn="postgresql://user:pw@db.internal/clinic",
        query="""
            SELECT p.patient_id, p.age, l.ldl, p.outcome
            FROM patients p JOIN labs l USING (patient_id)
        """,
        subject_id_col="patient_id",
    ),
)
def train(df):
    return LogisticRegression().fit(df[["age", "ldl"]], df["outcome"])

model = train()    # no argument: ConsentML supplies the data

source= is evaluated at decoration time, not at call time: @track cannot decorate a function that is later called against different data, since the source (and therefore the data it loads) is fixed when the decorator runs.

Requires pip install 'consentml[postgres]'. Queries run in a read-only transaction; ConsentML never writes to the database it reads from. Credentials are never recorded — the stored provenance keeps host, port and database only.

SnowflakeSource works the same way, against Snowflake instead of Postgres:

from consentml.sources.snowflake import SnowflakeSource

@track(
    model_name="readmission-risk",
    source=SnowflakeSource(
        connection={"account": "xy12345", "user": "svc_ml", "password": "...",
                    "database": "CLINIC", "schema": "PUBLIC", "warehouse": "WH_ML"},
        query="SELECT patient_id, age, ldl, outcome FROM patients",
        subject_id_col="patient_id",
    ),
)
def train(df): ...

Requires pip install 'consentml[snowflake]'. Unlike PostgresSource, ConsentML does not enforce read-only here — Snowflake exposes no connection-level read-only flag, so the connection you pass in must use a role with read-only grants.

For data already in memory:

from consentml.sources import DataFrameSource

@track(model_name="m", source=DataFrameSource(df, subject_id_col="patient_id",
                                              label="clinic.patients"))
def train(df): ...

label is caller-asserted and recorded as such: ConsentML cannot verify where an in-memory frame came from, and the stored record says so.

A null subject ID is refused by both sources. A null cannot be revoked, so recording one would inflate the run's subject count with a subject no revocation could ever match.

Where the lineage is stored

By default the hash-chained lineage lands in a local SQLite database. To keep it in Snowflake instead, pass store= a connection dict — @track writes the audit log there rather than to a file:

from consentml import track
from consentml.sources.snowflake import SnowflakeSource

connection = {"account": "xy12345", "user": "svc_ml", "password": "...",
              "database": "CLINIC", "schema": "PUBLIC", "warehouse": "WH_ML"}

@track(model_name="readmission-risk", store=connection,
       source=SnowflakeSource(connection=connection, query="...",
                              subject_id_col="patient_id"))
def train(df): ...

store= and db_path= are mutually exclusive. The Snowflake audit log uses the same row shape and entry_hash formula as SQLite, so the same code verifies either backend — pass the same target to verification:

from consentml import verify_audit_log

report = verify_audit_log(store=connection)

Or drive the store directly with open_store(connection) when you record runs without @track. This is a Python-API feature; the consentml CLI is SQLite-only. Requires pip install 'consentml[snowflake]'. See the Snowflake guide for the store schema, the single-writer caveat, and key-pair auth.

What provenance records

Postgres runs are recorded with the exact query text and its SHA-256, plus the tables the query plan touched:

{
    "kind": "postgres",
    "host": "db.internal",
    "port": 5432,
    "database": "clinic",
    "query": "SELECT p.patient_id, p.age, l.ldl, p.outcome FROM patients p JOIN labs l USING (patient_id)",
    "query_sha256": "ac80473d33489424...",
    "referenced_tables": ["public.labs", "public.patients"],
    "referenced_tables_source": "explain",
    "n_rows": 3
}

The query text is authoritative; referenced_tables is advisory — it comes from EXPLAIN, so a table the planner optimizes away will not appear. referenced_tables_source says which mechanism produced the list, or "unavailable" if EXPLAIN could not run.

The SHA-256 of the whole provenance record goes into the hash-chained audit log, so editing provenance in the database is detected as provenance_modified.

More on tracking runs: https://consentml.lokeshkaranam.me/getting-started/tracking/

Verifying the audit trail

consentml verify --db lineage.db

Verification checks three things: that every audit entry hashes to its recorded value, that the chain links correctly, and that the log still agrees with the live tables. That last check is the one that matters most — it catches a subject_index row deleted to hide that someone was in a training set, or a training_runs.provenance edited after the fact (reported as provenance_modified), either of which the hash chain alone cannot see. Runs recorded before provenance was hash-protected are counted separately as n_legacy_runs rather than silently treated as checked.

Exit codes, so it can gate CI:

Code Meaning
0 Clean
1 Read the database and found problems (including: no database there)
2 Could not read the database at all

verify and migrate share this contract: migrate exits 1 when it refuses to migrate (including onto a missing database) and 0 once migrated or already current. revoke does not use it — it always reports what it found and exits 0, whether or not the subject affected any models or the database exists yet; it only exits 2 if the database cannot be read at all.

Verification never writes. It records no audit event of its own, and it will not create a database that isn't there — a mistyped --db reports a missing database rather than silently reporting a clean, empty one.

Export a dossier

When a data subject exercises their right to erasure, export produces the document you file: which models learned from their data, what you recommended for each, when the request was processed, and whether the audit log backing it is intact.

consentml export --subject-id user@example.com --db lineage.db

Writes consentml-dossier-<key>.html — self-contained, opens in any browser, prints to PDF. --format json emits the same content machine-readably; --format pdf writes a PDF directly and needs the optional extra:

pip install consentml[pdf]

Export never writes to the database, so it is safe to run against a copy of a production lineage store. Exit codes match verify: 0 clean; 1 problems found — the dossier is still written and reports them, unless there was no lineage database at the path, in which case nothing is written; 2 the database could not be read, or --format pdf was used without the extra installed.

The dossier covers one subject. The audit log is a single global chain, so exporting all of it to answer one subject's request would disclose every other subject's activity.

More on exporting dossiers: https://consentml.lokeshkaranam.me/getting-started/first-dossier/

Anchoring

A hash chain cannot detect an attacker who rewrites the whole log from genesis and recomputes every hash. Record the head_hash that verify reports, somewhere outside the database, and pass it back:

consentml verify --db lineage.db --expected-head <previously recorded hash>

The anchor is looked up across the whole chain rather than compared to the current head, so a log that has legitimately grown since you anchored still verifies; only a rewrite or truncation of history reports head_mismatch.

This proves history up to the anchor point. It says nothing about entries appended after it — re-anchor regularly to narrow that window.

More on anchoring: https://consentml.lokeshkaranam.me/guides/anchoring/

Upgrading an existing database

Databases created on either of the two prior schemas need a one-time upgrade to the current one — the interned-storage layout, plus the provenance column described above:

consentml migrate --db lineage.db

Migration verifies the audit log before it starts and refuses to run if verification fails — rewriting a tampered database would launder the tampering. The new database is built alongside the original and only swapped in once it verifies clean, so a failure leaves the original untouched. The original is kept as <name>.pre-migration.bak; delete it once you are satisfied. Migration temporarily needs room for two copies of the database.

Until a database is migrated it can be read but not written to, so @track and a recording revoke() will raise. consentml verify and revoke(dry_run=True) keep working.

Migration backfills provenance from the old data_source string as {"kind": "legacy", ...} and does not touch the audit log — those entries were hashed over payloads containing data_source, and rewriting them would invalidate every entry hash. Runs migrated this way keep legacy guarantees: their provenance is not hash-protected, and consentml verify reports how many such runs it did not check rather than implying it did.

More on migrating: https://consentml.lokeshkaranam.me/guides/migrating/

Status: beta (v0.x — the API may change before 1.0). MIT license.

Metadata

Release files for consentml 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for consentml 0.2.0
File Size Uploaded
consentml-0.2.0.tar.gz 49.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for consentml 0.2.0
File Interpreter ABI Platform
consentml-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 99.5 kB

Release files / consentml-0.2.0.tar.gz

Download URL consentml-0.2.0.tar.gz
Size 49.1 kB
Tags Source
SHA-256 checksum
How to use checksums
20cce99f1bda91271f49eb2f52e975a07451b14779c29798eee10d268a486763
BLAKE2b-256 checksum
How to use checksums
6c7dc86898bbab7fb3dfd00f6dde0808ee688bccb1e82d93b9b57c501a8ad97f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 19, 2026.

Transparency log

Release files / consentml-0.2.0-py3-none-any.whl

Download URL consentml-0.2.0-py3-none-any.whl
Size 50.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
268f3c5945107208f6c4d44cc1b9bb0523fef6484b7fd96c8a4dc4e108076daf
BLAKE2b-256 checksum
How to use checksums
3443ca7dccd8b3abcb46a0fb560125a8826d529dc605e26365416f743ece0b88
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 19, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page