warmhub
The Python SDK for WarmHub — create repos, commit and query data, and compound knowledge with your AI agents.
Install
pip install warmhub
Requires Python 3.10 or later. It pulls one runtime dependency, httpx, plus
typing-extensions below Python 3.13.
Add the re2 extra only if you validate shape pattern constraints in the
client. It ships no wheel for Alpine and builds from source there:
pip install "warmhub[re2]"
Versions
Every release carries a real version, assigned by the release pipeline and
never edited by hand — so importlib.metadata.version("warmhub") and a
pip freeze line identify exactly which build you have. That number is
independent of the TypeScript SDK's: a Python-only fix does not wait on a
TypeScript release, and the two are not expected to match.
An installed copy reporting 0.0.0.dev0 is not a release. That is the
placeholder the source tree carries, never published under that number, and it
is how you tell a real install apart from a build made from a checkout.
Upgrading
CHANGELOG.md ships inside the wheel and the sdist. It is migration-guide
style: every breaking change is written as what it was, what it is now, and the
exact edit with before/after code. Read it before a version bump.
from importlib.resources import files
print(files("warmhub").joinpath("CHANGELOG.md").read_text(encoding="utf-8"))
Release notes are also published at https://docs.warmhub.ai/sdk/python/changelog/.
Quickstart
from warmhub import WarmHubClient
with WarmHubClient.from_env() as client: # reads WH_TOKEN
repo = client.repository("acme/sensors")
page = repo.things.head(shape="Reading", limit=5)
for item in page.items:
print(item.wref, item.version)
WarmHubClient(access_token=...) is the explicit form. The default constructor
reads no environment variable — only from_env() does, and it says so in the
name.
Writing
batch = repo.batch(message="seed readings")
batch.add("probe-1", shapes=["Reading"], data={"temp_celsius": 21.4})
result = batch.commit() # the only line that issues a request
Submission is explicit. Nothing here performs network I/O on scope exit.
A write names a Thing by its full name (taken literally, / included) and
states its Shapes in exactly one way, or it is refused with a
WriteOperationError (VALIDATION_ERROR, with operation_index) before any
request:
from warmhub import Add, Rename, Retract, Revise, shape_name
repo.apply(
"seed",
[
Add(name="acme", shapes=["Deal"], data={"company": "Acme"}),
Add(name="scratch", shapeless=True, data={"note": "free"}),
Revise(name="acme", keep_shapes=True, data={"company": "Acme 2"}),
Add(name="r1", shapes=["Rating"], data={"about": "acme", "score": 5}),
Add(name="crew", shapes=["Set"], data={"members": ["acme"]}),
Rename(name="scratch", new_name="scratch-2"),
Retract(name="crew", reason="disbanded"),
# A Shape is the Thing hub/shapes/X, written with shapes=[].
Add(name=shape_name("Deal"), shapes=[], data={"fields": {"company": "string"}}),
],
)
shapes=["Deal", "Contactable"]: bare Shape names. A pinnedDeal@v2is refused: a write always certifies under each Shape's head version.shapeless=True: no Shape; the data is stored but not validated, and the operation result carrieswarnings.shapeless.keep_shapes=True(Reviseonly): the SDK reads the Thing's current Shapes and version and sends both, the version asexpectedVersion(a suppliedexpected_versionwins). The Shapes are sent bare, so the revise re-certifies under each Shape's head version. A Thing revised earlier in the same submission carries that revise's Shapes and no version; one added, or renamed to or from, earlier in the same submission is refused before anything is sent (submit it in a later request, or passshapes). keep_shapes pins the version it read; a cross-process resubmit of the same submission_id can conflict; explicit shapes is the retry-safe form.
The v1 keyword arguments (kind, about, type, members, wref) are gone;
passing one raises a TypeError naming the v2 spelling, and a v1-shaped mapping
is refused with its v2 form in the message.
Preview the same operation array with the real server commit evaluator before submitting it:
preview = repo.validate(
[
{
"operation": "add",
"name": "probe-2",
"shapes": ["Reading"],
"data": {"temp_celsius": 19.8},
}
],
message="seed probe-2",
include_would_be_body=True,
)
if not preview.can_commit:
for operation in preview.operations:
if operation.status == "error":
print(operation.errors)
repo.validate(...) is the repo-bound spelling of
client.commit.validate(org_name, repo_name, operations, ...); the async client
has the same method and awaits it. Validation sends one bounded request (at most
10,000 operations and 4 MiB encoded), creates no durable repository state or
receipt, and returns ordered would_apply, noop, or error results. It is a
snapshot rather than a reservation: concurrent writes, write-time admission, and
asynchronous actions are not projected.
A revise takes expected_version to make the write conditional on the version
you read. The commit fails rather than clobbering a concurrent write:
repo.batch(message="correct probe-1").revise(
"probe-1",
shapes=["Reading"],
data={"temp_celsius": 21.7},
expected_version=3,
).commit()
Assertions
An assertion is a thing that makes a shape-validated claim about another
thing: a Thing one of whose declared Shapes composes Assertion, with its
target in data["about"]. Writing one requires the server's
composite-Shapes write setting to be on. It is the write that makes a
repository a knowledge graph rather than a table, and it is queryable
like any other thing:
repo.batch(message="flag the outlier").add(
"probe-1-spike",
shapes=["Suspect"],
data={
"about": "probe-1",
"confidence": 0.8,
"reason": "exceeds calibrated range",
},
).commit()
data["about"] accepts any wref — a specific thing (probe-1) or a whole
shape (hub/shapes/Reading).
Ask the other direction with thing.about, called on the subject's
repository. The assertions may live somewhere else entirely, and nothing in the
call names where:
filed = client.thing.about("acme", "sensors", "probe-1", limit=25)
if filed.target is not None: # the subject itself, optional
print(filed.target.wref, filed.target.version)
for claim in filed.assertions:
print(claim.declared_shapes, claim.wref, "->", claim.about_wref)
One request returns both halves, so "fetch it, then fetch what people said
about it" is not two round trips. Anyone can assert about your records without
your repository knowing they exist; this is how you find out. .next_cursor
pages exactly like head does.
Querying
head reads current state, filtered. where builds predicates through
operator overloading, and a dotted path reaches nested fields:
from warmhub import where
page = repo.things.head(
shape="Reading",
where=[where("temp_celsius") > 30, where("sensor.county") == "Marin"],
limit=100,
)
Results are paginated. head_iter walks the pages for you, and head_all
collects them under an explicit ceiling:
for item in repo.things.head_iter(shape="Reading"):
print(item.wref, item.version)
everything = repo.things.head_all(shape="Reading", max_items=10_000)
Typed reads
data is your shape's payload, so the SDK types it JsonValue — it genuinely
does not know your shape. When you do, say so with decode_as and get a page
of frozen dataclasses, statically as well as at runtime:
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Reading:
temp_celsius: float
probeId: str
page = repo.things.head(shape="Reading", decode_as=Reading)
page.items[0].data.temp_celsius # a Reading, not a dict
No key is transformed, in either direction. Field names are looked up verbatim, so a camelCase wire field needs a camelCase attribute. That reads oddly in Python and it is the right trade: a client that guessed at case conversion would be a client that can silently corrupt a repository. Declaring a subset is fine — extra keys are ignored, and only the fields you declare must be present.
Async
Every surface has an async twin with an identical signature:
from warmhub import AsyncWarmHubClient
async with AsyncWarmHubClient.from_env() as client:
repo = client.repository("acme/sensors")
page = await repo.things.head(shape="Reading", limit=5)
The sync and async clients emit byte-identical HTTP requests for the same call, and a test asserts it.
Two rules worth knowing up front
Everything returned is a frozen object with snake_case attributes. Unknown
fields a newer backend adds land in .extra rather than being dropped. .data
is a plain mapping, because those keys are your own shape fields and the client
never transforms them.
Omitted is not null. Optional arguments default to UNSET, not None.
Passing None means "set this to null" on the wire. bool(UNSET) raises, so
if limit: cannot silently conflate "not provided" with 0.
Documentation
Metadata
Release files for warmhub 0.27.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| warmhub-0.27.0.tar.gz | 383.9 kB | With provenance | Details for warmhub-0.27.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| warmhub-0.27.0-py3-none-any.whl | Python 3 | none | any | Details for warmhub-0.27.0-py3-none-any.whl |
Total release size: 849.2 kB
Release files / warmhub-0.27.0.tar.gz
| Download URL | warmhub-0.27.0.tar.gz |
|---|---|
| Size | 383.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4ca099c493fa420cf5da5e2f62c8d15be510060885190f33507bd28e3c7a4072
|
|
BLAKE2b-256 checksum How to use checksums |
36c1e2a818a5a9bf23b1bd9f3a26b4d771bbc5066d14e21221a1aefcb71f01d0
|
| 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 Oct 19, 2026.
Transparency logRelease files / warmhub-0.27.0-py3-none-any.whl
| Download URL | warmhub-0.27.0-py3-none-any.whl |
|---|---|
| Size | 465.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e173da87e6543ac6466262f31b8aa1111652ca5e9410abe6c32a727709e8b81a
|
|
BLAKE2b-256 checksum How to use checksums |
bf953e8768813b22824869ad4a6666b976f526ad01897d9b32fdc722d111155f
|
| 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 Oct 19, 2026.
Transparency logSecurity for warmhub 0.27.0
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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for warmhub-0.27.0.tar.gz
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 Oct 19, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.