Metadata-Version: 2.4
Name: saperly
Version: 3.0.0
Summary: Python SDK for the Saperly v2 API — autogenerated from the OpenAPI contract.
Project-URL: Homepage, https://saperly.com
Author: Saperly
License: MIT
License-File: LICENSE
Keywords: api,saperly,sdk,sms,telephony,voice
Classifier: Development Status :: 5 - Production/Stable
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.9
Requires-Dist: attrs>=22.2.0
Requires-Dist: httpx<0.29.0,>=0.23.0
Requires-Dist: python-dateutil>=2.8.0
Description-Content-Type: text/markdown

# saperly

Python SDK for the **Saperly v2 API** — the camelCase data plane (numbers,
connections, messaging, voice, usage, consent, pricing, keys).

Autogenerated from the OpenAPI contract.

## Install

```bash
pip install saperly        # or: uv add saperly
```

Requires Python 3.9+. The async API needs no extra install (uses `httpx`).

## Usage

```python
from saperly import create_client
from saperly.api.numbers import numbers_list, numbers_provision
from saperly.api.voice import voice_place

client = create_client(api_key="sap_sk_live_...")

provisioned = numbers_provision.sync(client=client, body=...)
calls = numbers_list.sync(client=client)
```

Each operation module exposes four entry points:

- `sync(...)` — returns the parsed body (or `None`).
- `sync_detailed(...)` — returns a `Response` with `status_code`, `headers`, `parsed`.
- `asyncio(...)` / `asyncio_detailed(...)` — the `async` equivalents.

### Convenience wrapper

`Saperly` holds a configured client so you don't thread `base_url` / `token`
through every call:

```python
from saperly import Saperly
from saperly.api.usage import usage_summary

with Saperly(api_key="sap_sk_live_...") as saperly:
    summary = usage_summary.sync(client=saperly.client)
```

### Retries

`create_client()` / `Saperly(...)` retry **idempotent** requests
(GET/HEAD/OPTIONS/DELETE) once on 5xx + connection errors. `POST`/`PATCH` are
never retried. Tune or disable with `retries`:

```python
client = create_client(api_key="sap_sk_live_...", retries=3)  # or retries=0
```

## Webhooks

Verify the signature on inbound Saperly webhooks. Saperly signs each delivery
`x-saperly-signature: v1=<hex>` (HMAC-SHA256 over `f"{timestamp}.{body}"`) with
`x-saperly-timestamp`; `verify_webhook` checks the signature constant-time, then
the timestamp window (default 5 min).

```python
from saperly import verify_webhook

result = verify_webhook(raw_body, secret, request.headers)
if not result.valid:
    return Response(status_code=400)
# Dedup result.delivery_id for >= the tolerance window to defeat replays.
```

## Agent brain (manual mode)

In **manual mode** a connection's "brain" drives a live phone call: Saperly POSTs a
signed `{"event": ...}` to the connection's webhook per call event and reads back
exactly ONE directive. `saperly.agent` turns that into a typed handler surface —
you register per-event handlers, the framework owns signature verification (`401` on
a bad signature), parsing (`400` on a malformed event), dispatch, and the fail-safe
rule (past the signature gate it **always** returns `200` with a graceful directive,
since a `5xx` degrades a live call). Stdlib only (no pydantic, no extra dependency).

```python
from fastapi import FastAPI
from saperly.agent import AgentBrain, accept, Reject

brain = AgentBrain(secret=SAPERLY_MANUAL_SECRET)  # the connection's manualSecret

@brain.on_inbound_call
def open_call(event):
    return accept(greeting=f"Hi, you've reached {event.to}. How can I help?")

@brain.on_turn
def turn(event):
    return f"You said: {event.user_text}"  # a str → Speak(...)

app = FastAPI()
app.mount("/manual", brain.asgi())  # or: brain.handler(raw_body, headers) -> (status, body)
```

A handler returns a directive (`Speak`, `Reject`, `Transfer`, …), a `str` (shorthand
for `Speak`), or `None` (a safe default). `brain.asgi()` mounts on FastAPI/Starlette
with no extra dependency; `brain.handler(raw_body, headers)` is the sync
framework-agnostic entry (`brain.handle_async(...)` from async code).

## Auth

Scoped Saperly API key (`sap_sk_live_…`), sent as
`Authorization: Bearer <key>` (handled by `AuthenticatedClient`). Mint keys in
the dashboard.

## Development

This package is **generated** — do not edit `saperly/client.py`,
`saperly/api/`, or `saperly/models/`. The hand-written facade is
`saperly/__init__.py` + `saperly/_saperly.py`, preserved across
regeneration.

```bash
bash scripts/generate.sh          # regenerate from ../openapi.v2.json
uv run --no-project --with httpx --with attrs --with python-dateutil --with pytest \
  python -m pytest tests/          # smoke tests
```

The generator is
[`openapi-python-client`](https://github.com/openapi-generators/openapi-python-client)
(run via `uvx`); config in `openapi-python-client.yaml`. The contract source of
truth is the `SaperlyApi` Effect HttpApi in `packages/api`.
