Metadata-Version: 2.5
Name: ironflow-py
Version: 0.33.1
Summary: Ironflow Python SDK — event-driven backend platform client
Project-URL: Homepage, https://ironflow.run
Project-URL: Documentation, https://docs.ironflow.run
Project-URL: Repository, https://github.com/sahina/ironflow-py
Project-URL: Issues, https://github.com/sahina/ironflow-py/issues
License-Expression: LicenseRef-Ironflow-EULA
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
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: connectrpc<0.12,>=0.11.1
Requires-Dist: pyqwest<0.11,>=0.10
Provides-Extra: dev
Requires-Dist: build>=1.0; extra == 'dev'
Requires-Dist: mypy>=1.0; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.1; extra == 'dev'
Requires-Dist: uvicorn>=0.30; extra == 'dev'
Provides-Extra: gen
Requires-Dist: connectrpc==0.11.1; extra == 'gen'
Requires-Dist: protobuf-py==0.1.1; extra == 'gen'
Requires-Dist: protoc-gen-connectrpc==0.11.1; extra == 'gen'
Requires-Dist: protoc-gen-py==0.1.1; extra == 'gen'
Description-Content-Type: text/markdown

# Ironflow Python SDK

Python client for [Ironflow](https://ironflow.run) — the Continuous History platform for backend systems.

## Installation

```bash
pip install ironflow-py
```

```python
import ironflow
```

> **Install `ironflow-py`, not `ironflow`.** The distribution name is `ironflow-py`; the
> import name is `ironflow`. The bare name `ironflow` on PyPI belongs to an unrelated
> third-party project (a materials-science tool from the pyiron group), which also
> installs a top-level `ironflow` module — a virtualenv holding both has two claimants on
> that name and the install order silently decides which wins. Keep them in separate
> environments.

Requires Python 3.10+.

Available from v0.33.0. Versions track the Ironflow engine release, so `ironflow-py`
0.33.0 is the client for engine 0.33.0.

## Status

Experimental and client-only. It can emit events and query runs, projections,
KV, and config, but it ships **no worker runtime** — there is no `step.run`, no
`step.sleep`, and no push or pull mode.

**Two clients, two protocols, neither a superset of the other.** `IronflowClient`
speaks REST and retries idempotent methods. `IronflowRPC` / `AsyncIronflowRPC`
speak ConnectRPC and reach 46 capabilities REST does not serve — webhook
management, agent tools, time travel, pub/sub consumer groups, function
versioning, and environment lookup/key rotation — including four server
streams. `IronflowRPC` retries only the unary methods the protos annotate
side-effect-free, and reconnects a subscription only when you position it (see
[Retries](#retries-and-what-can-still-go-wrong)).

Routes the server's manifest annotates with schemas get a typed `models.*` `TypedDict`
return and keyword-only query parameters; routes it does not annotate still return `Any`
and take no query kwargs. `TypedDict` is a type-checker construct only — there is **no
runtime validation**. Headers declared by a route, including `If-Match`,
`If-None-Match`, and `Idempotency-Key`, are generated as typed keyword arguments.
`BaseClient.request()` remains the escape hatch for undeclared headers. WebSocket and
watch endpoints (`/ws`, config watch, KV watch) have no generated methods at all and
`request()` cannot reach them either; poll the corresponding read method, use a
ConnectRPC subscription, or use the Go or JavaScript SDK for a live watch.

For durable step execution, use the Go or JavaScript SDK.

## Quick Start

```python
from ironflow import IronflowClient

client = IronflowClient(
    server_url="http://localhost:9123",
    api_key="ifkey_...",
)

# Emit an event
client.events_create(body={
    "name": "user.created",
    "data": {"user_id": "123", "email": "user@example.com"},
})

# List runs
runs = client.runs_list()

# Get a specific run
run = client.runs_get("run_abc123")

# List projections
projections = client.projections_list()

# Public server inspection
health = client.health()
readiness = client.ready()
capabilities = client.capabilities()
```

## ConnectRPC client

For the capabilities REST does not serve. Request and response types come from
`ironflow.rpc.v1`; `ironflow._gen` is private and its layout may change.

```python
from ironflow import IronflowRPC
from ironflow.rpc.v1 import CreateWebhookSourceRequest, SubscribeRequest

with IronflowRPC(server_url="http://localhost:9123", api_key="ifkey_...") as rpc:
    source = rpc.webhooks.create_source(
        CreateWebhookSourceRequest(name="Stripe", event_prefix="stripe.")
    )

    # Server streams are ordinary iterators; breaking out cancels.
    for event in rpc.pubsub.subscribe(SubscribeRequest(pattern="topic:orders.*")):
        print(event.event_id)
        break
```

`AsyncIronflowRPC` mirrors it method for method — `await` the unary calls,
`async for` the streams, and `await rpc.aclose()` instead of `close()`.

Failures raise `IronflowRPCError`, which subclasses `IronflowError`, so
`except IronflowError` still catches every failure from either client. Full
reference, including timeouts, `NO_TIMEOUT`, and stream lifetime:
[docs.ironflow.run/reference/api/python-sdk](https://docs.ironflow.run/reference/api/python-sdk).

### Retries, and what can still go wrong

A unary call that fails with `unavailable` — a refused connection, a socket
dropped mid-response, a server shedding load — is sent again, up to 3 attempts
with exponential backoff. Nothing else is retried: every other Connect code is a
decision the server will reach again identically.

**Only side-effect-free methods are retried.** The protobuf definitions annotate
them `idempotency_level = NO_SIDE_EFFECTS`, and the client reads that annotation
rather than any list of its own. A method without it — every create, update,
delete, rotate and emit — is sent exactly once and its failure is raised to you
immediately, because a transport error cannot tell you whether the server
committed the write before the connection dropped.

Two things this does **not** promise:

- **A retried read may execute more than once on the server.** A response lost
  on the way back is indistinguishable from a request that never arrived, so the
  retry re-runs the method. That is harmless for a read by definition, and it is
  the reason the annotation gates the behaviour.
- **A write is never retried for you.** If you need one repeated safely, repeat
  it yourself with an idempotency key — `EmitRequest` and `PublishRequest` both
  carry `idempotency_key` — and treat a failed write as *unknown*, not *failed*.

`timeout=` still bounds the whole call including every retry and backoff, not
each attempt. Pass `max_attempts=1` to the constructor to switch retries off.

**`subscribe` reconnects only if you positioned it.** Set
`options.start_after_sequence` to the last `event.sequence` you processed, and a
dropped connection is retried from there. That field is both the cursor and the
opt-in: without it there is no honest place to resume from, so the stream raises
and re-subscribing is yours.

```python
from ironflow.rpc.v1 import SubscribeRequest, SubscribeOptions

cursor = 0
for event in rpc.pubsub.subscribe(SubscribeRequest(
    pattern="topic:orders.*",
    options=SubscribeOptions(start_after_sequence=cursor),
)):
    handle(event)
    cursor = event.sequence   # persist this if you need to resume across restarts
```

A resumed stream is **at-least-once**: the frame in flight when the connection
dropped may arrive twice, because the server sending it is not you having
processed it. Make `handle` idempotent, or dedupe on `event.sequence`.

**The other three streams are not reconnected, and do not need to be.**
`stream_events`, `wait_catchup_stream` and `join_consumer_group` are positioned
by the server on a durable consumer, so simply calling them again resumes where
they left off. A client-side cursor there would move a position other readers
share.

## API Coverage

This SDK is auto-generated from the Ironflow server's route manifest. Run `make sdk-health`
for the current method count — it changes on every regeneration, so it is not reproduced
here. Route coverage is not the same as usable coverage; see the Status section above.

See the [SDK Comparison Matrix](https://docs.ironflow.run/reference/sdk-comparison) for
full coverage details.

## Dependencies

Two direct runtime dependencies, `connectrpc` and `pyqwest`, which resolve to six
packages — two of them compiled:

```text
connectrpc ─┬─ protobuf-py ── protobuf-py-ext   (native, CPython only)
            ├─ pyqwest                          (Rust)
            │    └─ opentelemetry-api
            └─ typing_extensions
```

They are required, not an extra. The ConnectRPC client is part of the default
contract, so putting it behind a flag would turn a heavier install into a runtime
`ImportError` — see ADR 0062 (engine repo, internal).

`IronflowClient` itself still uses only the standard library (`urllib`, `json`),
and `import ironflow` does not load the ConnectRPC stack — that cost is paid on
first use of `IronflowRPC`.

## Where this package is published from

The Ironflow engine is closed source. This SDK's source is mirrored to
[`sahina/ironflow-py`](https://github.com/sahina/ironflow-py) at each release, and PyPI
uploads happen from there over [Trusted Publishing](https://docs.pypi.org/trusted-publishers/).
No PyPI API token exists for this project, so every release carries an attestation
binding the artifact to a public commit you can inspect.

The mirror is **read-only**. Pull requests against it are closed without review; source
changes land in the engine repo and appear at the next release.

## Bugs and security

Issues are disabled on the mirror. Everything goes to one tracker:

- Bugs and feature requests → [sahina/ironflow-issues](https://github.com/sahina/ironflow-issues/issues/new/choose), component **Python SDK**. Include your Python version, platform, and a minimal repro.
- Security issues → [private advisory](https://github.com/sahina/ironflow-issues/security/advisories/new). Do **not** open a public issue.
- Commercial licensing → the contact in [LICENSE](LICENSE).

## License

See [LICENSE](LICENSE) — SPDX `LicenseRef-Ironflow-EULA`. Not an OSI-approved open source
licence; read it before deploying commercially.
