Metadata-Version: 2.4
Name: warmhub
Version: 0.4.0
Summary: WarmHub Python client
Project-URL: Homepage, https://warmhub.ai
Project-URL: Documentation, https://docs.warmhub.ai/sdk/overview/
Project-URL: Changelog, https://docs.warmhub.ai/releases/overview/
Project-URL: Issues, https://docs.warmhub.ai/support/
Author: WarmHub
License: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: typing-extensions>=4.7; python_version < '3.13'
Provides-Extra: dev
Requires-Dist: build<2,>=1.2; extra == 'dev'
Requires-Dist: google-re2>=1.1; extra == 'dev'
Requires-Dist: mypy<3,>=2.3; extra == 'dev'
Requires-Dist: packaging>=24; extra == 'dev'
Requires-Dist: pyright==1.1.411; extra == 'dev'
Requires-Dist: pytest-asyncio<2,>=1.4; extra == 'dev'
Requires-Dist: pytest-cov<8,>=7; extra == 'dev'
Requires-Dist: pytest<10,>=9.1; extra == 'dev'
Requires-Dist: ruff<0.17,>=0.16; extra == 'dev'
Requires-Dist: twine<8,>=7; extra == 'dev'
Provides-Extra: re2
Requires-Dist: google-re2>=1.1; extra == 're2'
Description-Content-Type: text/markdown

# warmhub

The Python SDK for WarmHub — create repos, commit and query data, and compound
knowledge with your AI agents.

## Install

```bash
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:

```bash
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.

## Quickstart

```python
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

```python
batch = repo.batch(message="seed readings")
batch.add(name="Reading/probe-1", 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 revise takes `expected_version` to make the write conditional on the version
you read. The commit fails rather than clobbering a concurrent write:

```python
repo.batch(message="correct probe-1").revise(
    name="Reading/probe-1",
    data={"temp_celsius": 21.7},
    expected_version=3,
).commit()
```

## Assertions

An assertion is a thing that makes a shape-validated claim *about* another
thing. It is the write that makes a repository a knowledge graph rather than a
table, and it is queryable like any other thing:

```python
repo.batch(message="flag the outlier").add(
    name="Suspect/probe-1-spike",
    kind="assertion",
    about="Reading/probe-1",
    data={"confidence": 0.8, "reason": "exceeds calibrated range"},
).commit()
```

`about` accepts any wref — a specific thing (`Reading/probe-1`) or a whole
shape (`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:

```python
filed = client.thing.about("acme", "sensors", "Reading/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.shape_name, 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:

```python
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:

```python
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:

```python
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:

```python
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

- [WarmHub docs](https://docs.warmhub.ai)
