Skip to main content

telmai

Python client for the Telmai data quality platform, covering both halves of the job. Configuration: connections, projects, assets and their attributes, monitors, scans and jobs, alerts, incidents, DQ score and data binning — enough to provision a tenant end to end and keep it in sync, from code. Gating: three ready-made ways to turn a scan result into a decision an Airflow or Databricks job can act on.

So if you are here to find out whether this can configure a tenant: yes. Ten namespaces cover the platform's configuration surface — see the namespaces — and the gate is a feature built on top of them, not the whole product.

from telmai import Telmai

tm = Telmai.from_env()  # TELMAI_HOST / TELMAI_TENANT / TELMAI_API_KEY

tm.connections.create({...})  # connect a warehouse
tm.assets.create(project_id, {...})  # onboard a table
tm.monitors.create(ORDERS, {...})  # say what good looks like
tm.scans.run(ORDERS, wait=True)  # and go and check

Published. telmai is on PyPI — pip install telmai works. Current version is 0.3.1. Developing the SDK itself, rather than just using it? See Getting set up for installing from source.

Why the gate is here too

Telmai finds quality problems. It cannot act on them, because the moment that matters is inside someone else's pipeline: the step between writing a batch and publishing it. No API call can fail a customer's Databricks task or skip their Airflow step. That has to be code running in their job.

Customers who want that today write their own HTTP client. Asking "did this batch pass?" is four calls, a state machine, and five failure modes that all have to resolve the same way. Getting it wrong is easy and the consequences are asymmetric, so this ships it once, correctly.

from telmai import CircuitBreakerTripped, Severity, Telmai

tm = Telmai.from_env()  # TELMAI_HOST / TELMAI_TENANT / TELMAI_API_KEY
tm.circuit_breaker("a1b2c3d4e5f6")  # raises if the batch is not clean

Getting set up

Just using the SDK in a pipeline? pip install telmai is all you need — into a virtual environment, because macOS system Python (and Debian's) refuses to install into itself and fails with externally-managed-environment, PEP 668:

python3 -m venv .venv
./.venv/bin/pip install telmai

The steps below are for developing the SDK itself, so they install from source in editable mode with the dev extras:

git clone https://github.com/Telmai/telmai-python.git && cd telmai-python
python3 -m venv .venv && ./.venv/bin/pip install -e ".[dev]"

Every command below uses ./.venv/bin/... explicitly, so nothing depends on whether the venv is activated.

Three environment variables, and nothing else:

TELMAI_HOST bare hostname, no scheme — your-tenant.telm.ai
TELMAI_TENANT the tenant id, the opaque string in your Telmai URL
TELMAI_API_KEY from your secret store, never from a file in a repo

.env.example documents these three plus TELMAI_LIVE, and is the fastest local setup. Nothing auto-loads it — the SDK reads the environment, not a file — so source it yourself:

cp .env.example .env        # then fill in TELMAI_API_KEY
set -a && source .env && set +a

.env is gitignored. .env.example deliberately leaves TELMAI_API_KEY commented out rather than empty, so sourcing a half-filled file cannot export an empty string over a key that was already working in your shell.

Python 3.9 or newer. CI runs 3.9 through 3.13. The floor is 3.9 rather than something more modern because Databricks and Airflow runtimes lag, and those are the two places this code actually runs.

Assets are addressed by id

Always ids, never table names. Two tables in one project can both be called orders; only the id is unique, and it survives a rename in Telmai while your pipeline code stays put. Passing a name where an id belongs is rejected before any scan is triggered.

Look the id up once, out of band, and put it in your job config:

python -m telmai resolve gold.orders    # prints the id, then exits

There is a tm.resolve(name) for scripts that must do it at runtime, but a pipeline that runs every day should not be paying for a lookup — or risking an ambiguous one — on every run.

Three ways to act on a scan

One scan underneath, three responses to its result.

Circuit Breaker. Stop the pipeline. Wire it between the write step and the promote step, so a failure skips everything downstream.

tm.circuit_breaker(ORDERS)
tm.circuit_breaker([ORDERS, CLAIMS], min_severity=Severity.MEDIUM)

Quarantine. Keep running, route the bad records aside. Needs Data Binning configured on the asset — see tm.binning.

for r in tm.quarantine([ORDERS, CLAIMS]):
    if not r.clean:
        if r.fully_isolated:
            move_flagged_rows(r.location.incorrect_data_path)
        else:
            hold_entire_batch(r.label)  # binning covers only some monitors

Live Pass Through. Get told, without the gate deciding for you. For pipelines that must run regardless of what the scan finds.

tm.live_pass_through(ORDERS, on_result=lambda v: notify(v) if not v.passed else None)

It waits for the scan to finish, same as the other two, then calls back with the verdict — it does not run alongside your pipeline. What it gives you over circuit_breaker is that it never raises for a quality result, so nothing downstream is skipped.

Fail closed, by contract

Every path that cannot confirm a clean result blocks: a scan that fails or times out, a severity we cannot read, an alert type meaning the scan never read the table, an asset with no monitors. A gate that reports success on bad data is worse than no gate, because the pipeline then certifies the batch.

Four consequences worth knowing before reading the code:

  • A typo raises, it does not become a verdict. An unresolvable or ambiguous asset id fails the whole batch before anything is scanned. A typo should not look like a data problem.
  • CircuitBreakerTripped is not a TelmaiError. A blocked batch is a correct decision, not an API failure, so except TelmaiError: retry cannot silently retry past it.
  • A severity we cannot read blocks. If Telmai adds a tier above HIGH tomorrow, an SDK that predates it treats that alert as unrankable and stops the pipeline, rather than reading an unfamiliar value as harmless.
  • "Blocked" and "bad data" are different things. A scan that could not read the table blocks too, and says so separately. passed decides whether the pipeline continues; blocked_by_quality decides what you tell someone. The CLI splits them as exit 2 versus exit 1.
try:
    tm.circuit_breaker([ORDERS, CLAIMS])
except CircuitBreakerTripped as e:
    for v in e.failed:  # every failure, not just the first
        log.error("%s: %s", v.label, [a.policy_name for a in v.blocking_alerts])
    raise

The namespaces

Ten of them, mirroring what you are working on. This is the configuration surface, and it is most of the SDK:

tm.projects.list()  # project ids, the first argument to most writes

tm.connections.create({...})  # connect a warehouse
tm.connections.test(conn_id, {...})  # check it can still reach the source

tm.assets.create(project_id, {...})
tm.assets.detect_columns(ORDERS)  # async; poll with tm.jobs.wait()
tm.assets.set_monitored_columns(ORDERS, {...})

tm.monitors.create(ORDERS, {...})
tm.monitors.set_enabled(ORDERS, monitor_id, False)
tm.monitors.export(ORDERS)  # monitors as code, with import_()

run = tm.scans.run(ORDERS, wait=True)  # run.job_id, run.status
tm.scans.cancel(ORDERS, run.job_id)  # abandoning a scan does not stop it

tm.alerts.for_scan(ORDERS, job_id)
tm.jobs.wait(ORDERS, job_id)
tm.incidents.list()
tm.dq_score.get(ORDERS)
tm.binning.set(ORDERS, {...})  # where quarantine routes bad rows

Those namespaces are the surface. There is a generated layer underneath, one method per route in Telmai's OpenAPI spec, but it is internal and the client does not expose it: which endpoint the SDK calls for you is ours to change, and its method names come from the platform's operationIds, which are not names anyone should be asked to call (get_connection_4 is the POST that creates a connection). If something you need is not covered above, ask us for a wrapper — that is the supported path, and it lands on the stable surface. See docs/versioning.md for the full stability policy.

Runnable examples

Nine scripts under examples/, covering all twenty features, with a table of which ones cost compute. Start with the read-only one, which prints the asset ids the rest need:

./.venv/bin/python examples/01_find_your_assets.py

Anything that costs compute asks first. Anything that writes configuration is a dry run until you pass --commit.

Developing

./.venv/bin/pytest -q                          # offline, no network, under a second
./.venv/bin/ruff check . && ./.venv/bin/ruff format --check .
./.venv/bin/mypy                               # strict
./.venv/bin/python tools/generate.py --check   # generated layer is current

mypy takes no argument on purpose: it is configured to check the package. mypy . would also walk the tests and the Airflow recipe, which imports a package the SDK does not depend on, and it reports several hundred errors that are not defects. Widening strict typing to the test suite is real work, not a config flag.

Never hand-edit telmai/_generated/. Fix tools/generate.py and regenerate. Any tool that generates code here runs the real linter and formatter on its own output rather than reimplementing their rules — that bit us twice.

The live suite

tests/live/ creates and deletes real objects on a real tenant. It is excluded from the default run and opts in by naming the tenant twice, which is what stops a stray environment variable pointing destructive tests somewhere unintended:

export TELMAI_LIVE=$TELMAI_TENANT
./.venv/bin/pytest tests/live -m "not costly"   # free
./.venv/bin/pytest tests/live -m costly         # triggers real scans, spends compute

Nothing pre-existing is ever deleted: cleanup only removes ids the run itself recorded. Read tests/live/conftest.py before changing it.

It earns its keep. Two bugs no offline test could have found: iter_detailed looped forever on a real first page, and alerts.counts() could never have worked because a required query parameter was generated as optional.

Where it stands

Published on PyPI as 0.3.1. All three gate modes are built and validated end to end against a live tenant, not just against fakes. Real alert payloads captured from that tenant are checked against our enums in tests/test_contract.py, which found defects no offline test could — including a severity tier the platform does not emit, and an alert type we were reporting as bad data when it actually meant the scan could not run.

The offline suite runs in under a second and is the specification, not a safety net: tests/test_fail_closed.py encodes decisions that look like over-engineering and are not. mypy --strict and ruff are clean, and the wheel ships py.typed, verified reaching a consumer's own type checker.

Known gaps and open questions are in TODO.md. If you are picking this up, start with HANDOFF.md.

More

Release files for telmai 0.3.2

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

Source distribution (sdist)

Source distribution for telmai 0.3.2
File Size Uploaded
telmai-0.3.2.tar.gz 98.2 kB Details

Built distribution (wheel)

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

Total release size: 183.9 kB

Release files / telmai-0.3.2.tar.gz

Download URL telmai-0.3.2.tar.gz
Size 98.2 kB
Tags Source
SHA-256 checksum
How to use checksums
53151d25b83e3b65166b13f8a969a5cf837f8d86aa3693f307e0ab55607d7c28
BLAKE2b-256 checksum
How to use checksums
e08ee89e5265d34b263eebd9601aa13d9a2a829fbefd1184d32e995b4669e004
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 Sep 17, 2026.

Transparency log

Release files / telmai-0.3.2-py3-none-any.whl

Download URL telmai-0.3.2-py3-none-any.whl
Size 85.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5bea359bcb1c49cb0d8b9e3638e27b39cea90634062fa53cc7b36c04e5fbf2b6
BLAKE2b-256 checksum
How to use checksums
3829b662af99c9e634e4d3cef8ca4fd3618ff9b1a7a39cad06eb7d5ebaacdc41
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 Sep 17, 2026.

Transparency log

Release history Release notifications | RSS feed

0.3.5

2 release files

0.3.4

2 release files

0.3.3

2 release files

This release

0.3.2 This release

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.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