Skip to main content

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

CI Python 3.11+ License

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?

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-tollkeeper package 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

Apache License 2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tollkeeper-0.1.0b1.tar.gz (470.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tollkeeper-0.1.0b1-py3-none-any.whl (23.6 kB view details)

Uploaded Python 3

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

Hashes for tollkeeper-0.1.0b1.tar.gz
Algorithm Hash digest
SHA256 23983d41a8928e5e666ca7da84cf9721d6d5132ec843fa66ed4d9560cdf082d7
MD5 9a6beff23c61b0ac03d7647a2ed069bf
BLAKE2b-256 295fd150e2416e4b60b65089bcafcd4774bf0fc2820e3f7cdf3159e0e5ab9742

See more details on using hashes here.

Provenance

The following attestation bundles were made for tollkeeper-0.1.0b1.tar.gz:

Publisher: release.yml on srchilukoori/tollkeeper

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for tollkeeper-0.1.0b1-py3-none-any.whl
Algorithm Hash digest
SHA256 79564d74db9fa5f5ae37c18ce913f6e61c4370f159bc9c950d1068a885328fb2
MD5 4cc9f032a285d260d84c490b787ef5c8
BLAKE2b-256 b05c62deec672121454be7401ff31f057b8b8f96f1718ec73043ddf482ee8006

See more details on using hashes here.

Provenance

The following attestation bundles were made for tollkeeper-0.1.0b1-py3-none-any.whl:

Publisher: release.yml on srchilukoori/tollkeeper

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page