Metadata-Version: 2.4
Name: warmhub
Version: 0.23.0
Summary: WarmHub Python client
Project-URL: Homepage, https://warmhub.ai
Project-URL: Documentation, https://docs.warmhub.ai/sdk/python/
Project-URL: Changelog, https://docs.warmhub.ai/sdk/python/changelog/
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: griffe-sphinx==0.2.1; extra == 'dev'
Requires-Dist: griffe==2.1.0; extra == 'dev'
Requires-Dist: hatchling<1.32,>=1.31; 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.

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

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

```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("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:

```python
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 pinned `Deal@v2` is
  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 carries `warnings.shapeless`.
- `keep_shapes=True` (`Revise` only): the SDK reads the Thing's current Shapes
  and version and sends both, the version as `expectedVersion` (a supplied
  `expected_version` wins). 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 pass `shapes`). **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:

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

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

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

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

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

- [Python SDK guide](https://docs.warmhub.ai/sdk/python/)
- [Python API reference](https://docs.warmhub.ai/python-reference/)
