Python library for the Write-Audit-Publish pattern. Stage writes, run data quality checks, publish only when checks pass. Pluggable backends, SQL lineage parsing, and Airflow integration.
Project description
tollkeeper
A Python library that implements the Write-Audit-Publish pattern for data pipelines. Stage writes in isolation, run data quality checks, and promote to production only when checks pass.
Table of Contents
- Why Tollkeeper?
- Features
- Installation
- Quick Start
- How It Works
- Data Quality Checks
- Backends
- Signal Store
- SQL Lineage Parser
- Airflow Integration
- API Reference
- Development
- Contributing
- License
Why Tollkeeper?
Data pipelines that write directly to production tables are fragile. A bad upstream transformation can corrupt production data before anyone notices. The Write-Audit-Publish pattern solves this:
┌─── DQ pass ───► Publish to production
Raw data ──► Staging ──┤
└─── DQ fail ───► Rollback (production untouched)
tollkeeper codifies this into a Python library with a fluent API, pluggable backends, and optional Airflow integration.
Features
- Zero runtime dependencies in core, optional extras for Polars, Iceberg, sqlglot
- Fluent API with method chaining and context manager support
- Pluggable backends: CsvBackend, IcebergBackend, or implement your own
- Pluggable DQ engine: 5 built-in Polars checks, or bring your own (Pandas, Presto, etc.)
- Hard and soft failure modes: halt-and-rollback or publish-with-notification
- Signal store: cross-pipeline coordination via SQLite or any DB-API 2.0 database
- SQL lineage parser: automatic source/sink extraction from SQL using sqlglot
- Airflow integration:
airflow-tollkeeperpackage with TollkeeperOperator, TollkeeperSensor, DQ operators, strategy registry
Installation
pip install tollkeeper # core only (zero deps)
pip install "tollkeeper[polars]" # + Polars DQ checks
pip install "tollkeeper[iceberg]" # + PyIceberg backend
pip install "tollkeeper[sql]" # + sqlglot lineage parser
pip install "tollkeeper[all]" # everything
For Airflow integration:
pip install airflow-tollkeeper
Requires Python 3.11+.
Quick Start
import shutil
from pathlib import Path
from tollkeeper import Tollkeeper, CsvBackend, NullCheck, RowCountCheck
backend = CsvBackend(staging_dir=Path("/tmp/tollkeeper"), publish_dir=Path("/data/output"))
(Tollkeeper(backend)
.table("sales")
.write(lambda ref: shutil.copy("upstream_output.csv", ref))
.audit([NullCheck("id"), RowCountCheck(min_rows=100)])
.publish())
If any check fails, the staged file is rolled back automatically. Production is never touched.
Context manager
with Tollkeeper(backend).table("sales") as session:
session.write(lambda ref: shutil.copy("upstream_output.csv", ref))
session.audit([NullCheck("id")])
session.publish()
# Auto-rollback on exception or if publish() was never called
How It Works
┌──────────────────────────────────────────────────────────────────┐
│ Tollkeeper Lifecycle │
│ │
│ 1. WRITE Your callback writes to an isolated staging ref │
│ 2. AUDIT DQ checks run against staged data │
│ 3. PUBLISH On pass: staging promoted to production │
│ 4. ROLLBACK On fail: staging discarded, production untouched │
└──────────────────────────────────────────────────────────────────┘
The lifecycle is backend-agnostic. For CSV files, "staging" is a temp file and "publish" is a rename. For Iceberg, "staging" is a branch and "publish" is a fast-forward merge.
Data Quality Checks
Five built-in checks using Polars (install with [polars] extra):
| Check | Constructor | Passes when |
|---|---|---|
NullCheck |
NullCheck("column") |
No nulls in column |
RowCountCheck |
RowCountCheck(min_rows=100) |
Row count >= threshold |
ExpressionCheck |
ExpressionCheck("name", pl.col("age") > 0) |
All rows match the Polars expression |
SqlCheck |
SqlCheck("name", "age > 0 AND score >= 0") |
All rows match the SQL WHERE condition |
UniqueCheck |
UniqueCheck(["region", "date"]) |
No duplicate groups on given columns |
Custom checks
Subclass BaseCheck to create checks with any engine:
from tollkeeper import BaseCheck, CheckResult
class SchemaCheck(BaseCheck):
def __init__(self, expected_columns: list[str]) -> None:
self._expected = expected_columns
def run(self, version_ref: str, *, conn=None) -> CheckResult:
import polars as pl
df = pl.read_csv(version_ref)
missing = set(self._expected) - set(df.columns)
return CheckResult(
check_name=self.name,
passed=len(missing) == 0,
details=f"missing columns: {missing}" if missing else "all columns present",
)
Soft failures
Publish despite failed checks with an optional notification callback:
session.audit(
[RowCountCheck(min_rows=1000)],
on_failure="continue",
on_notify=lambda table, ref, failed: log.warning(f"{table}@{ref}: {failed}"),
)
Backends
| Backend | Extra | Description |
|---|---|---|
CsvBackend |
none | Local CSV files with staging/publish directories |
IcebergBackend |
[iceberg] |
Branch-based versioning with pointer-swap publish via PyIceberg |
| Custom | none | Implement the Backend ABC: create_version, publish_version, rollback_version |
Signal Store
Coordinate across pipelines by tracking table readiness:
from tollkeeper import Tollkeeper, SqliteSignalStore
signal_store = SqliteSignalStore("/tmp/signals.db")
tk = Tollkeeper(backend, signal_store=signal_store)
# After successful audit+publish, a signal is emitted automatically.
# Downstream pipelines can check:
signal = signal_store.check("sales", {"ds": "2026-01-15"})
| Store | Description |
|---|---|
SqliteSignalStore |
File-based, zero-config, good for single-machine pipelines |
DbApiSignalStore |
Any DB-API 2.0 connection (Postgres, MySQL, etc.) |
SQL Lineage Parser
Automatically extract source and sink tables from SQL (install with [sql] extra):
from tollkeeper import extract_lineage
result = extract_lineage(
"INSERT INTO warehouse.fact_orders SELECT * FROM staging.raw_orders JOIN dim_date USING (dt)",
dialect="trino",
)
print(result.sources) # {'staging.raw_orders', 'dim_date'}
print(result.sinks) # {'warehouse.fact_orders'}
Handles CTEs (excluded from sources), schema/catalog-qualified names, INSERT/CTAS/MERGE, multi-statement SQL, and subqueries. Supports Spark, Trino, and Snowflake dialects. Rejects Jinja-templated SQL with a clear error.
Airflow Integration
The airflow-tollkeeper package wraps any Airflow operator in a Write-Audit-Publish lifecycle:
from airflow_tollkeeper import TollkeeperOperator
tk_task = TollkeeperOperator(
task_id="tk_load_orders",
operator=sql_operator, # any BaseOperator
table="warehouse.fact_orders",
backend=iceberg_backend,
checks=[NullCheck("order_id"), RowCountCheck(min_rows=1)],
engine="local",
)
- TollkeeperOperator: wraps any operator with write-audit-publish lifecycle
- TollkeeperSensor: gates downstream tasks on upstream Tollkeeper signal completion
- Strategy registry: maps operator types to Tollkeeper redirect logic; unknown operators pass through unchanged
Requires apache-airflow>=2.9.
API Reference
Tollkeeper(backend, signal_store=None)
Entry point. Takes a Backend and optional SignalStore.
.table(name) -> TollkeeperSession: creates an isolated staging version
TollkeeperSession
Returned by .table(). Supports fluent chaining and context manager:
| Method | Description |
|---|---|
.write(fn) |
Calls fn(version_ref) to write to the staged version |
.audit(checks, *, on_failure, on_notify, execution_ctx, conn) |
Run DQ checks against staged data |
.publish() |
Promote staged version to production |
.rollback() |
Discard staged version |
.ref |
The version reference string |
.report |
CheckReport with .passed, .failed, .results |
Backend (ABC)
| Method | Purpose |
|---|---|
create_version(table) -> str |
Create isolated staging, return a reference |
publish_version(table, ref) |
Promote to production |
rollback_version(table, ref) |
Discard staging |
BaseCheck (ABC)
| Method | Purpose |
|---|---|
run(version_ref, *, conn=None) -> CheckResult |
Execute a data quality check |
Development
git clone https://github.com/srchilukoori/tollkeeper.git
cd tollkeeper
uv sync --all-extras --group dev --group docs
# Run tests
uv run pytest tests/ -v # core tests
cd packages/airflow-tollkeeper && uv run pytest tests/ -v # airflow tests
# Lint and type check
uv run ruff check src/ tests/
uv run ruff format --check src/ tests/
uv run ty check src/
# Docs
uv run mkdocs serve # http://127.0.0.1:8000
# Docker (integration tests with Airflow)
docker compose run test
Contributing
Contributions are welcome. See CONTRIBUTING.md for guidelines.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tollkeeper-0.1.0b1.tar.gz.
File metadata
- Download URL: tollkeeper-0.1.0b1.tar.gz
- Upload date:
- Size: 470.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
23983d41a8928e5e666ca7da84cf9721d6d5132ec843fa66ed4d9560cdf082d7
|
|
| MD5 |
9a6beff23c61b0ac03d7647a2ed069bf
|
|
| BLAKE2b-256 |
295fd150e2416e4b60b65089bcafcd4774bf0fc2820e3f7cdf3159e0e5ab9742
|
Provenance
The following attestation bundles were made for tollkeeper-0.1.0b1.tar.gz:
Publisher:
release.yml on srchilukoori/tollkeeper
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tollkeeper-0.1.0b1.tar.gz -
Subject digest:
23983d41a8928e5e666ca7da84cf9721d6d5132ec843fa66ed4d9560cdf082d7 - Sigstore transparency entry: 2164575196
- Sigstore integration time:
-
Permalink:
srchilukoori/tollkeeper@bef1f6ae7f0f211fceb087e2aea443fd8f32bbdd -
Branch / Tag:
refs/tags/v0.1.0b1 - Owner: https://github.com/srchilukoori
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@bef1f6ae7f0f211fceb087e2aea443fd8f32bbdd -
Trigger Event:
release
-
Statement type:
File details
Details for the file tollkeeper-0.1.0b1-py3-none-any.whl.
File metadata
- Download URL: tollkeeper-0.1.0b1-py3-none-any.whl
- Upload date:
- Size: 23.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
79564d74db9fa5f5ae37c18ce913f6e61c4370f159bc9c950d1068a885328fb2
|
|
| MD5 |
4cc9f032a285d260d84c490b787ef5c8
|
|
| BLAKE2b-256 |
b05c62deec672121454be7401ff31f057b8b8f96f1718ec73043ddf482ee8006
|
Provenance
The following attestation bundles were made for tollkeeper-0.1.0b1-py3-none-any.whl:
Publisher:
release.yml on srchilukoori/tollkeeper
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tollkeeper-0.1.0b1-py3-none-any.whl -
Subject digest:
79564d74db9fa5f5ae37c18ce913f6e61c4370f159bc9c950d1068a885328fb2 - Sigstore transparency entry: 2164575224
- Sigstore integration time:
-
Permalink:
srchilukoori/tollkeeper@bef1f6ae7f0f211fceb087e2aea443fd8f32bbdd -
Branch / Tag:
refs/tags/v0.1.0b1 - Owner: https://github.com/srchilukoori
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@bef1f6ae7f0f211fceb087e2aea443fd8f32bbdd -
Trigger Event:
release
-
Statement type: