Skip to main content

truegrain (Python)

The Python client for truegrain: governed metrics for agents, notebooks and applications.

There is no method that sends SQL, because there is no endpoint that accepts it. The only expressible request is a semantic one.

pip install truegrain            # no dependencies
pip install truegrain[pandas]    # adds to_dataframe()

The client uses only the standard library. That is deliberate: this package goes into agent environments where a dependency conflict is a real cost, and thirteen endpoints do not justify dragging one in.

Use it

import os
from truegrain import Client, filters

client = Client.from_env()          # SEMANTIC_URL, SEMANTIC_TOKEN

for metric in client.metrics(search="revenue"):
    print(metric.name, "-", metric.description)

result = client.query(
    metrics=["sales.order_revenue", "marketing.campaign_spend"],
    dimensions=["sales.orders.order_date"],
    grain="month",
    filters=[filters.in_("sales.orders.status", "shipped", "delivered")],
    order_by=[{"field": "order_date"}],
)

result.to_dataframe()

Those two metrics live in different namespaces and aggregate different facts. The engine computes each separately and joins them on the shared month, so both numbers are correct rather than one being inflated by the join.

Every result carries the SQL that produced it and the exact version of the definitions used:

frame = result.to_dataframe()
frame.attrs["compiled_sql"]
frame.attrs["model_version"]      # sha256:eebdad0d19bb

That is not a debug feature. It is how a disagreement about a number gets settled, which is why it survives into the frame rather than being dropped at the client boundary.

Refusals are answers

The engine declines questions it cannot answer correctly, and says what to do instead. Catch Refused and branch on the retry class rather than on the text.

from truegrain import Refused

try:
    client.query(metrics=["sales.order_revenue"],
                 dimensions=["sales.products.category"])
except Refused as refusal:
    if refusal.should_modify():
        print("try instead:", refusal.hint)
    elif refusal.should_wait():
        ...        # the same request may work shortly
    elif refusal.is_final():
        ...        # stop; say so rather than substituting another metric

That example refuses because an order contains several products, so revenue per category has no answer without an allocation rule. The hint names the metrics defined at the grain where the question is well defined:

fan_out_would_inflate: metric "order_revenue" aggregates SUM(...) over orders,
but this query repeats orders rows
  hint: ... Metrics defined at the order_lines grain answer this correctly:
        line_revenue, units_sold.
  retry: modify

The three classes matter most to an agent. Without them it either gives up on a typo or loops forever on a denial.

retry Meaning
modify Answerable, but not as written. Change the arguments.
later Nothing is wrong with the request. Something outside it failed.
never No version of this will succeed for you. Stop.

Wiring into an agent framework

Most frameworks accept a list of function schemas and call back with a name and arguments. tools produces that list and dispatches those calls, which covers LangChain, CrewAI, the OpenAI Agents SDK, Pydantic AI and anything hand-rolled without this library growing an adapter per framework.

from truegrain import Client, tools

client = Client.from_env()
specs = tools.tool_specs()          # hand these to the framework

# when the model calls back:
result = tools.dispatch(client, name, arguments)

dispatch returns refusals rather than raising them, because a framework feeding the result back to a model wants the text and the retry class, not a traceback.

If your client speaks the Model Context Protocol, skip this and point it at the engine's MCP server instead. Same four tools, same guarantees, no code.

claude mcp add truegrain -- /path/to/truegrain serve mcp

Dry runs

compile returns the SQL a request would produce without running it, which is useful before committing an agent to an expensive query.

compiled = client.compile(metrics=["sales.order_revenue"],
                          dimensions=["sales.customers.region"])
print(compiled.compiled_sql)
print(compiled.parts)      # >1 means facts were aggregated separately and joined

A dry run is still governed. A request you may not run returns a refusal and no SQL, and the inspection is recorded in the audit log as compiled rather than allowed. It shows you what a query would do; it is not a way around the gate.

Know what you are trusting

health = client.health()
health.governs_columns          # is column-level access control configured at all
health.enforcement_notes        # plain-language list of what this deployment does NOT enforce

A deployment that enforces nothing says so here. Overstating a governance guarantee is worse than not offering one.

Reading the decisions

audit returns the decisions the engine recently made, newest first. It is the only place a refusal survives the request that caused it, which makes it the answer to "why did that agent give up".

for refusal in client.audit(decision="refused").refusals():
    print(refusal.time, refusal.code, "->", refusal.hint)

refused and denied stay separate. Denied is access, meaning this caller may not read something. Refused is correctness, meaning nobody can be told this accurately and hint names a question that can be. Counting them together would report every fan-out as a security incident.

This call fails for reasons that have nothing to do with the request, so handle both: an engine started without -audit-readers answers 404, because the record carries every caller's activity and the capability is absent rather than withheld, and a caller who is authenticated but not named gets 403. Both arrive as a final Refused.

It is a bounded window rather than the whole record. An engine writing an audit file keeps everything there.

Errors

Exception When
Refused The engine understood and declined. Carries code, reason, hint, retry.
Unauthorized Credentials missing or not recognised.
TransportError The engine could not be reached, or answered with something unparseable. Never a statement about the request.
SemanticError Base class for all of the above.

Development

pip install -e ".[dev]"
pytest

tests/test_covers_spec.py checks this client against the vendored contract; see below for what that guarantees.

How this stays in step with the engine

The engine's OpenAPI contract is vendored at spec/openapi.yaml, pinned to an engine commit in spec/PINNED_AT.

tests/test_covers_spec.py checks this client against it in both directions: an operation the engine exposes with no method here fails the build, and a method claiming an operation the spec does not define fails it too. A scheduled job compares the pin against the engine daily and opens a pull request when the contract moves, so a change arrives as a reviewable diff with the contract tests already run against it.

The client is hand-written rather than generated. A generator produces a transport and a flat error type; the parts worth having are the ones it cannot produce: Refused.should_modify(), and run() submitting a job and polling it so no caller writes that loop.

Licence

Apache 2.0, matching the engine.

Metadata

Release files for truegrain 1.1.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 truegrain 1.1.0
File Size Uploaded
truegrain-1.1.0.tar.gz 51.5 kB Details

Built distribution (wheel)

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

Total release size: 82.3 kB

Release files / truegrain-1.1.0.tar.gz

Download URL truegrain-1.1.0.tar.gz
Size 51.5 kB
Tags Source
SHA-256 checksum
How to use checksums
36045af9e38f0f47764e37b2293096e978514cab511654140c32e81cb3646edd
BLAKE2b-256 checksum
How to use checksums
52bacedd5bc721ca3fda6fb74f9ff2c836a25850ca71a37846b7090bdc19bbc4
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 20, 2026.

Transparency log

Release files / truegrain-1.1.0-py3-none-any.whl

Download URL truegrain-1.1.0-py3-none-any.whl
Size 30.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0244bcc9ed30faad4b739aa4d67568aefc892b2affce7fc8877ea507b24d044d
BLAKE2b-256 checksum
How to use checksums
06536e00abafbd2f032e3bd4e0e44d6e54d912f1078e984c57c4f691b9c82866
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 20, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 release files

1.0.0

2 release files

0.2.0

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