Metadata-Version: 2.4
Name: slothbox
Version: 0.1.0
Summary: Official Python SDK for the Slothbox API
Project-URL: Homepage, https://github.com/sloth-box/sdk-python
Project-URL: Repository, https://github.com/sloth-box/sdk-python
Project-URL: Changelog, https://github.com/sloth-box/sdk-python/blob/main/CHANGELOG.md
Author: Slothbox
License-Expression: MIT
License-File: LICENSE
Keywords: api,dev-environments,sdk,slothbox
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
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: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: attrs>=22.2.0
Requires-Dist: httpx<1,>=0.23.0
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: mypy==2.1.0; extra == 'dev'
Requires-Dist: openapi-python-client==0.29.0; (python_version >= '3.11') and extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff==0.15.16; extra == 'dev'
Description-Content-Type: text/markdown

# slothbox (Python SDK)

Official Python SDK for the [Slothbox](https://slothbox.dev) API — typed,
sync **and** async, with retries, cursor pagination, lifecycle waiters, and a
webhook verification toolkit. Feature parity with
[`@slothbox/sdk`](https://github.com/sloth-box/sdk-typescript) is tracked in
[PARITY_CHECKLIST.md](PARITY_CHECKLIST.md).

```sh
pip install slothbox
```

Requires Python 3.10+ and an org service-account API key (`sk_…`, API plan)
from the Slothbox dashboard — the SDK identifies itself on every request and
the API rejects SDK traffic on other credentials (see
[SDK identification](#sdk-identification)).

## Quickstart

```python
from slothbox import Slothbox

client = Slothbox()  # reads SLOTHBOX_API_KEY; or Slothbox(api_key="sk_…")

orgs = client.organizations.list()
envs = client.environments.list(org_id)

# Safe launch: always sends an Idempotency-Key, then polls until `running`.
box = client.environments.launch_and_wait(org_id, {"templateId": template_id})
print(box.env_id, box.status)
```

Async is the same surface with `await`:

```python
from slothbox import AsyncSlothbox

async with AsyncSlothbox() as client:
    box = await client.environments.launch_and_wait(org_id, {"templateId": template_id})
```

Request bodies accept either the typed models (`slothbox.models`) or plain
dicts keyed by the wire (JSON) names, as above.

## SDK identification

Every request carries an always-on identification header:

```http
x-slothbox-sdk: slothbox-sdk-python/<version>
```

The API uses it to identify SDK traffic, which must authenticate with an org
service-account key (API plan). SDK-identified requests on any other
credential are rejected `403` with code `sdk_requires_service_key`, raised as
`PermissionDeniedError`. Identification wins: the header is set as a
client-level default (so it also covers direct calls through the
`client.generated` escape hatch) **and** re-asserted on every request the
wrapper sends, so it cannot be removed or overridden — not via
`RequestOptions(headers=…)`, not via `httpx_args`, and not via the generated
`with_headers()`.

## Errors

Every non-2xx response raises a typed exception with the API's
machine-readable `code`, the gateway request id, and validation `issues`:

```python
from slothbox import ConflictError, RateLimitError

try:
    client.environments.launch(org_id, {"templateId": template_id})
except ConflictError as err:
    if err.code == "template_not_baked":
        client.templates.wait_until_baked(org_id, template_id)
except RateLimitError as err:
    print("retry after", err.retry_after, "s — request", err.request_id)
```

## Retries

GET/HEAD/PUT/DELETE are retried on 429/5xx/network errors (3 retries by
default) with capped full-jitter backoff; `Retry-After` is honored. POST and
PATCH are **never** blind-retried — a duplicated launch provisions a second
EC2 box — unless the request carries an `Idempotency-Key`
(`environments.launch(..., idempotency_key=…)`; `launch_and_wait` always
sends one). Tune with `Slothbox(max_retries=…)` or per request via
`RequestOptions(max_retries=…)`.

## Pagination

The audit and webhook-delivery lists are cursor-paginated; iterating a page
walks every page:

```python
for event in client.audit.list_org_events(org_id, limit=100):
    ...

page = client.webhooks.list_deliveries(org_id, endpoint_id)
while page.has_next_page():
    page = page.get_next_page()
```

(`async for` on the async client.)

## Webhooks

Verify deliveries with the exact raw bytes you received:

```python
from slothbox import parse_webhook_event, WebhookVerificationError

try:
    event = parse_webhook_event(raw_body, request_headers, endpoint_secret)
except WebhookVerificationError as err:
    return 400, err.code

if event["type"] == "environment.stopped":
    ...
```

The verifier implements Standard Webhooks v1 (HMAC-SHA256, secret rotation,
±300s replay window) and is tested against the same production-signed vector
file as the API's signer and the TS SDK.

## Architecture

Per [ADR 0001](docs/adr/0001-generator-choice.md): a pinned
[`openapi-python-client`](https://github.com/openapi-generators/openapi-python-client)
generates the typed core (`slothbox._generated` — committed, httpx + attrs,
regenerated deterministically by `scripts/generate.sh` and diff-checked in
CI), and the hand-written `slothbox` package on top is the public
compatibility surface. `client.generated` exposes the generated
`AuthenticatedClient` as an escape hatch — note that the generated
`set_httpx_client()` bypasses auth; pass transports via
`Slothbox(httpx_args=…)` instead.

## Development

```sh
python -m venv .venv && .venv/bin/pip install -e '.[dev]'
ruff check . && ruff format --check .   # lint
mypy                                     # strict on the wrapper
pytest                                   # 3.10+ supported
scripts/generate.sh                      # regen the core from openapi.json
```

Releases are automated — see [RELEASING.md](RELEASING.md).
