Metadata-Version: 2.4
Name: sesame-sdk
Version: 0.1.1
Summary: Python SDK for the Sesame human-in-the-loop approval API
Project-URL: Homepage, https://github.com/lavanpuri1999/sesame
Project-URL: Repository, https://github.com/lavanpuri1999/sesame
Project-URL: Issues, https://github.com/lavanpuri1999/sesame/issues
Author: Sesame
License: MIT
License-File: LICENSE
Keywords: agents,approval,authorization,human-in-the-loop,sesame,webhook
Classifier: Development Status :: 4 - Beta
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.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.6
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# sesame-sdk

Python SDK for the [Sesame](https://getsesame.dev) human-in-the-loop approval API.

Drop an approval gate in front of any sensitive operation in your backend. Sesame
triggers a request that a human approves (via Telegram + push), and your code blocks
until the decision lands — or reacts asynchronously via a webhook.

```bash
pip install sesame-sdk
```

Requires Python 3.12+. Runtime deps: `httpx`, `pydantic`.

## Configuration

The client reads credentials from the environment, or you can pass them explicitly:

| Env var             | Default                  | Purpose                          |
| ------------------- | ------------------------ | -------------------------------- |
| `SESAME_API_KEY`    | _(required)_             | `sk_live_...` API key            |
| `SESAME_BROKER_URL` | `https://getsesame.dev`  | Broker base URL — override only to self-host or point at local dev |

```python
from sesame_sdk import Sesame

client = Sesame()                                  # hosted broker; reads SESAME_API_KEY
client = Sesame(api_key="sk_live_...")             # default base_url is https://getsesame.dev
client = Sesame(api_key="sk_live_...", base_url="http://localhost:8000")  # self-host / local
```

## Gate a function with `@require_approval`

The wrapped function only runs if a human approves. Denied/expired raises
`ApprovalDenied`; no decision before `timeout` raises `ApprovalTimeout`.

```python
from sesame_sdk import require_approval, ApprovalDenied

@require_approval(
    "payments.refund",
    summary=lambda amount, **_: f"Refund ${amount/100:.2f} to customer",
    timeout=300,
)
def issue_refund(amount: int, customer_id: str) -> None:
    stripe.Refund.create(amount=amount, customer=customer_id)

try:
    issue_refund(4200, customer_id="cus_123")
except ApprovalDenied:
    log.warning("refund rejected by operator")
```

`summary` may be a static string or a callable receiving the wrapped function's
arguments. Omit it for a sensible default built from the action and function name.
Pass `client=Sesame(...)` to use a specific client; otherwise a module-level default
is built lazily from the environment.

## Trigger and wait explicitly

```python
from sesame_sdk import Sesame

client = Sesame()
approval = client.approvals.trigger(
    action="db.delete",
    summary="Delete 1,204 rows from prod.orders",
    reason="GDPR erasure request #882",
    context={"table": "orders", "rows": 1204},
)

approval.wait(timeout=300)   # blocks on the broker's long-poll until decided
if approval.approved:
    run_deletion()
else:
    print("decision:", approval.status)  # "denied" or "expired"
```

## Receive decisions via webhook

When you pass a `callback_url`, the broker POSTs to it on every terminal decision.
Verify the signature before trusting the body — `verify_webhook` recomputes the
HMAC-SHA256 over the *raw* request body and rejects stale timestamps.

### Flask

```python
from flask import Flask, request, abort
from sesame_sdk import verify_webhook, WebhookVerificationError

app = Flask(__name__)
WEBHOOK_SECRET = os.environ["SESAME_WEBHOOK_SECRET"]

@app.post("/sesame/webhook")
def sesame_webhook():
    try:
        payload = verify_webhook(request.headers, request.get_data(), WEBHOOK_SECRET)
    except WebhookVerificationError:
        abort(400)
    # payload: {"approval_id", "action", "status", "decided_at", "requester_label", "dedup_key"?}
    handle_decision(payload["approval_id"], payload["status"])
    return "", 204
```

### FastAPI

```python
from fastapi import FastAPI, Request, Response, HTTPException
from sesame_sdk import verify_webhook, WebhookVerificationError

app = FastAPI()

@app.post("/sesame/webhook")
async def sesame_webhook(request: Request):
    raw = await request.body()  # must be the exact bytes the broker signed
    try:
        payload = verify_webhook(request.headers, raw, WEBHOOK_SECRET)
    except WebhookVerificationError:
        raise HTTPException(status_code=400, detail="bad signature")
    handle_decision(payload["approval_id"], payload["status"])
    return Response(status_code=204)
```

## Exceptions

All inherit from `ApprovalError`:

- `ApprovalDenied` — terminal `denied`/`expired` (carries `.approval_id`, `.status`)
- `ApprovalTimeout` — no decision before the caller's timeout
- `SesameAuthError` — broker rejected the API key (HTTP 401)
- `NotFoundError` — unknown approval id (HTTP 404)
- `WebhookVerificationError` — bad/missing signature, stale timestamp, or bad JSON

## Notes

This SDK is synchronous for v1. An async client may be added later; until then, run
`approval.wait()` in a worker thread if you need to avoid blocking an event loop.
