Metadata-Version: 2.3
Name: duraton
Version: 0.1.1
Summary: Duraton Python SDK: durable-workflow runner over the duraton.connect.v0 WebSocket protocol
Author: Duraton
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Dist: httpx>=0.28.1
Requires-Dist: websockets>=16.1
Requires-Dist: anthropic>=0.117.0 ; extra == 'anthropic'
Requires-Python: >=3.11
Project-URL: Documentation, https://docs.duraton.dev
Project-URL: Homepage, https://duraton.dev
Provides-Extra: anthropic
Description-Content-Type: text/markdown

# duraton (Python SDK)

The Python runner SDK for [Duraton](https://duraton.dev). It speaks the frozen
`duraton.connect.v0` WebSocket protocol: a runner dials the engine, registers its workflows, and
the engine drives each run one replay pass at a time. Durable steps memoize across passes, so a
handler resumes exactly where it left off after a crash, deploy, sleep, or human approval.

This is the **core** transport + runtime. See `DECISIONS.md` for the design, the evidence behind
every library/tooling choice, and the spec-conformance notes.

Requires Python >= 3.11.

## Quickstart

```python
import asyncio
from duraton import Runner, define_workflow
from duraton.context import StepContext


async def fulfill(ctx: StepContext) -> object:
    charge = await ctx.step.run("charge", lambda: {"charge_id": "ch_1"})
    await ctx.step.sleep("cooldown", "30s")
    await ctx.step.emit("notify", name="order.fulfilled", data=charge)
    return {"ok": True}


workflows = [define_workflow("fulfill", fulfill, triggers=[{"event": "order.created"}])]


async def main() -> None:
    # url / app / api_key fall back to DURATON_URL / DURATON_APP / DURATON_API_KEY.
    await Runner(workflows).run()  # blocks, reconnecting with backoff, until cancelled


asyncio.run(main())
```

`Runner(...).run()` is the blocking entrypoint. Inside an existing loop, `async with Runner(...)`
runs it in the background, or `connect(workflows, ...)` returns a handle you can `aclose()`.

## The `ctx` surface (10 structural ops)

`ctx.step.run` / `sleep` / `sleep_until` / `wait_for_event` / `run_workflow` / `emit`,
`ctx.webhook.send`, `ctx.step.approval`, `ctx.step.ai.infer`, `ctx.score`. Plus `ctx.log`
(structured, per-run), `ctx.asserts` (eval checks), and run metadata (`ctx.run_id`, `ctx.attempt`,
`ctx.app`, `ctx.event`).

## Configuration

| Knob | Option | Env | Default |
|---|---|---|---|
| Engine URL | `url` | `DURATON_URL` | `http://localhost:6770` |
| App | `app` | `DURATON_APP` | `default` |
| API key (Bearer) | `api_key` | `DURATON_API_KEY` | (dev: none) |
| App-ping interval | `ping_interval_ms` | `DURATON_CONNECT_PING_MS` | 25000 |
| Pong watchdog | `pong_timeout_ms` | `DURATON_CONNECT_PONG_TIMEOUT_MS` | 10000 |
| Reconnect initial | `reconnect_initial_ms` | `DURATON_CONNECT_RECONNECT_INITIAL_MS` | 500 |
| Reconnect max | `reconnect_max_ms` | `DURATON_CONNECT_RECONNECT_MAX_MS` | 30000 |

## Development

```
uv sync                         # install
uv run pytest -q                # offline conformance + unit tests
uv run mypy .                   # strict type gate
uv run pyright                  # second type checker (src)
uv run ruff check . && uv run ruff format --check .
uv build                        # wheel + sdist
```

The offline conformance tests reproduce the shared golden vectors
(`tests/fixtures/golden-vectors.json`) byte-for-byte: step-id hashing (incl. Unicode /
no-normalization), `:n` dedup, version parsing, and envelope round-trips.
