Metadata-Version: 2.4
Name: band-sdk-core
Version: 0.6.0
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Typing :: Typed
Summary: Shared Band event-payload validation
Keywords: band,sdk,validation,events
Author: band.ai
License-Expression: MIT
Requires-Python: >=3.11
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Changelog, https://github.com/band-ai/band-sdk-core/blob/main/crates/py/CHANGELOG.md
Project-URL: Homepage, https://github.com/band-ai/band-sdk-core
Project-URL: Issues, https://github.com/band-ai/band-sdk-core/issues
Project-URL: Repository, https://github.com/band-ai/band-sdk-core

# band-sdk-core

Shared Band event-payload validation, memory-taxonomy, and inbound-delivery
runtime state, compiled from Rust into a native Python extension
(`import band_sdk_core`). Every function is synchronous, pure computation —
no network, filesystem, or logging.

```
band-sdk-core-core (Rust)  -->  band-sdk-core (this package, PyPI)  -->  band-sdk-python
```

If you're using [`band-sdk-python`](https://github.com/band-ai/band-sdk-python),
it already depends on this package — you don't need to install it
yourself. Install it directly only if you're calling into it without that
SDK. Full picture: [repository architecture](https://github.com/band-ai/band-sdk-core#architecture).

## Install

```bash
pip install band-sdk-core
```

Wheels are prebuilt (`abi3`, Python >= 3.11) for Linux (manylinux/musllinux,
x86_64/aarch64), macOS (arm64/x86_64), and Windows (amd64/arm64) — no Rust
toolchain needed to install.

## Quickstart

```python
import band_sdk_core

try:
    payload = band_sdk_core.validate_event_payload("room_deleted", {"id": "room-1"})
except ValueError as e:
    for path, code, message in e.issues:
        print(f"{path}: {code} - {message}")
```

`validate_event_payload` is the platform's inbound WebSocket payload
policy: normalize a well-formed payload, or reject a malformed one with
every violation reported at once, never just the first.

## Public surface

### `validate_event_payload(event_type, raw, trace_context=None)`

`event_type` is a platform name (`"message_created"`, `"agent.control"`, …)
or an `EventType`. `raw` is a JSON-shaped Python value. Success returns the
normalized payload (`event_created` returns the input unchanged). Failure
raises `ValueError` with `.args == (message,)` (`str(e)` is ordinary),
`.issues` (a tuple of `(path, code, message)`), and `.trace_context`.
There is no custom exception class.

### `EventType`

The closed set of inbound event names.

### Delivery-state runtime classes

`ClaimRegistry`, `RetryTracker`, `ParticipantRoster`, and `SubscriptionTracker` are the
inbound-delivery runtime state classes — lifecycle and design decisions:
[`runtime-state-policy.md`](https://github.com/band-ai/band-sdk-core/blob/main/crates/core/docs/runtime-state-policy.md).

A participant is any mapping; `add`/`set_all` read `id`, `name`, `type`,
`handle`, `description` from it and `list()` returns dicts with exactly
those five keys. `id` is required and must be a string; the other four are
each a string or `None`. A non-mapping value, a missing/non-string `id`, or
another field that is not a string or `None` raises `TypeError`. `set_all`
takes an optional `trace_context` and raises `ValueError` — leaving the
roster unchanged, with `.issues` and `.trace_context` attached like
`validate_event_payload`'s error — if its snapshot names the same `id`
twice. `RetryTracker`'s `max_tracked` must be at least `1`; `0` raises
`ValueError`. `ClaimRegistry`'s `max_completed` must be at least `1` too —
`0` also raises `ValueError`.

`SubscriptionTracker` provides synchronous, transport-independent decisions for
agent-topic joins and the two-topic room subscription transaction. It returns
opaque integer tickets; callers supply the matching ticket when recording each
completion. Failed rollbacks and failed or unknown leaves require explicit
reconciliation before a fresh claim is allowed.

### `SubscriptionTracker` lifecycle

```python
from band_sdk_core import LeaveOutcome, RoomStatus, RoomSubscribeResult, SubscriptionTracker

tracker = SubscriptionTracker()
ticket = tracker.begin_room_subscribe("room-1")
if ticket is None:
    raise RuntimeError("room is not claimable")

result = tracker.record_room_participants_join_failed("room-1", ticket, False)
if result is RoomSubscribeResult.RollbackFailed:
    assert tracker.room_status("room-1") is RoomStatus.NeedsReconciliation
    assert tracker.acknowledge_room_reconciled("room-1") is True
    ticket = tracker.begin_room_subscribe("room-1")
    assert ticket is not None
    assert tracker.record_both_room_topics_joined("room-1", ticket) is RoomSubscribeResult.Subscribed
    leave_ticket = tracker.unsubscribe_room("room-1")
    assert leave_ticket is not None
    assert tracker.mark_room_leave_complete("room-1", leave_ticket, LeaveOutcome.Left) is True
else:
    match result:
        case (
            RoomSubscribeResult.Subscribed
            | RoomSubscribeResult.JoinFailed
            | RoomSubscribeResult.RolledBack
            | RoomSubscribeResult.Stale
        ):
            pass
        case _:
            raise AssertionError(f"unhandled subscription result: {result}")
```

### `Session` — WebSocket reconnect state machine

`Session`/`SessionPolicy` are a sans-io session state machine plus
reconnect backoff/jitter policy; `classify_close`/`classify_upgrade`
classify a WebSocket close code or HTTP upgrade-rejection status.
`Session` never sleeps, connects, or closes a socket itself — the caller
drives its own transport and reports what happened through `on_connected`/
`on_socket_close`/`on_upgrade_rejected`/`on_supersede`. Confirmed
decisions:
[`runtime-state-policy.md`](https://github.com/band-ai/band-sdk-core/blob/main/crates/core/docs/runtime-state-policy.md)'s
`## Session` section.

### `Session` lifecycle

```python
from band_sdk_core import Session, SessionPolicy, SessionState

session = Session(SessionPolicy.default())
epoch = session.begin_attempt(0.0)
assert epoch is not None

connected = session.on_connected(epoch, 0.0)
assert connected.state is SessionState.Up

disconnected = session.on_socket_close(epoch, 5.0, 1006, 0.5)
assert disconnected.state is SessionState.Reconnecting
assert disconnected.retry_after_s is not None
```

### Memory taxonomy

`MemorySystem`, `MemoryType`, `MemorySegment`, `MemoryStoreScope`,
`MemoryListScope`, `MemoryStatus` are the canonical memory taxonomy —
design decisions:
[`memory-taxonomy-policy.md`](https://github.com/band-ai/band-sdk-core/blob/main/crates/core/docs/memory-taxonomy-policy.md).
Each is a closed set with a `wire_name` property and a `from_wire_name`
static method (`None` for an unrecognized string), mirroring `EventType`.

### `validate_memory_type_for_system(system, memory_type, trace_context=None)`

`system`/`memory_type` are each a wire-name string or the matching
`MemorySystem`/`MemoryType` instance (mirroring `validate_event_payload`'s
acceptance of either an event name or an `EventType`), and it returns
`None` on success. Failure raises `ValueError` with the same
`.issues`/`.trace_context` contract as `validate_event_payload` — an
unrecognized `system` and an unrecognized `memory_type` are independent
issues, both reported when both strings are invalid.

## Values across the language boundary

JSON objects become Python `dict`s. JSON `null` becomes `None`. An
explicit null stays distinct from an absent key — a distinction the
payload validator uses. Values that cannot be converted raise
`TypeError`, which `except Exception` catches.

## Build / test

`just check` type-checks and lints this crate. Linking the extension
happens through `uv` / maturin (`just test-py`, `just build-py`), not
`cargo test`.

```bash
just test-py     # uv sync, pytest, isolated wheel install
just build-py    # uv build (wheel only)
```

Wheels are platform-specific. Recipes use [`uv`](https://docs.astral.sh/uv/)
and honor `UV_PYTHON`, or `PYTHON` / `PYTHON_BIN`. Windows builds need the
MSVC toolchain rustup's default host target uses.

The wheel is `abi3` for Python >= 3.11. Typed stubs are `band_sdk_core.pyi` plus
an empty `py.typed`. `just test-py` checks they match the runtime package
and that they ship in the wheel.

## License

MIT — see [LICENSE](https://github.com/band-ai/band-sdk-core/blob/main/LICENSE).

