Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

iii-sdk

Python SDK for the iii engine.

PyPI Python License

Install

pip install iii-sdk

Hello World

from iii import register_worker

iii = register_worker("ws://localhost:49134")

def greet(data):
    return {"message": f"Hello, {data['name']}!"}

iii.register_function("hello::greet", greet)

result = iii.call({"function_id": "hello::greet", "payload": {"name": "world"}})
print(result)  # {"message": "Hello, world!"}

API

Operation Signature Description
Initialize register_worker(url, options?) Create an SDK instance and auto-connect
Register function iii.register_function(id, handler, **options) Register a function that can be called by name
Call (await result) iii.call({"function_id": id, "payload": data}) Call a function and wait for the result
Call (fire-and-forget) iii.call({"function_id": id, ..., "action": CallAction.Void()}) Fire-and-forget
Call (enqueue) iii.call({"function_id": id, ..., "action": CallAction.Enqueue(queue="name")}) Route the call through a named queue
Call through an observer iii.call({"observer": obs, "payload": data}) Run an observer's target from a deliver hook
Observe iii.observe("fn::id", target, options?) Run target when the function runs
Observer hooks iii.on_observer_added, on_observer_removed, on_observer_match, on_observer_deliver Decide what observing a function this worker hosts means
Current invocation iii.current_invocation() The invocation the running handler serves
Shutdown iii.shutdown() Disconnect and stop background thread

register_worker() creates the SDK instance and auto-connects to the engine. trigger and trigger_async are deprecated aliases of call and call_async.

Registering Functions

def create_order(data):
    return {"status_code": 201, "body": {"id": "123", "item": data["body"]["item"]}}

iii.register_function("orders::create", create_order)

The handler's return value is what the function's observers observe. Options: description, metadata, request_format, response_format (extracted from type hints when omitted), args_format (a JSON Schema or Pydantic model of the observers' args), private=True (only this worker may call it; its own calls pass the input and result by reference), observable=False (nobody may observe it), standby=True (take the function over when the worker that hosts it leaves) and local_dispatch.

Calling Functions

result = iii.call({"function_id": "orders::create", "payload": {"body": {"item": "widget"}}})

call() blocks the calling thread; await iii.call_async(...) works from any event loop. A call to a function this worker registered itself runs in-process once the engine confirmed it (no frames on the wire unless someone observes it): a sync handler on a thread of its own, an async one on the calling loop. Pass "dispatch": "remote" to go through the engine anyway, or register the function with local_dispatch=False.

Observing Functions

When orders::create runs, alerts::big-order runs:

iii.observe("orders::create", "alerts::big-order")

The target is a registered function; it receives the Observed envelope (input, output or error, caller, ...). The full form takes args (read by the worker that hosts the function), filters, on, capture and deliver, and a policy:

def big_order(envelope):
    notify(envelope["input"], envelope["output"])

iii.register_function("alerts::big-order", big_order)

observer = iii.observe(
    {"function_id": "orders::create", "filters": {"input.body.total": {"$gte": 500}}},
    "alerts::big-order",
    {"max_in_flight": 4},
)
observer.ready()            # or: await observer.ready_async()
# later
observer.dispose()

Hosting Observable Functions

The worker that hosts a function decides what observing it means with four hooks. Each one overrides something iii does by default:

Hook What iii does by default
on_observer_added(obs) accepts the observer: it becomes active and receives the function's calls. Raise ObserveRejection to reject it.
on_observer_removed(obs) stops delivering to the observer and forgets it
on_observer_match every observer wants every call
on_observer_deliver(call, observers) runs the target of every matching observer whose on and filters match the call, and gives the caller the handler's return value

on_observer_added and on_observer_removed can be set for one function or for every function the worker hosts; both levels run, the worker's first when an observer is added and last when it is removed. on_observer_match and on_observer_deliver are set per function.

The methods work as decorators and as plain calls, on the client by function id or on the FunctionRef that register_function returns. Set hooks by id before registering the function so they run for every observer.

from iii import Call, Observer, ObserveRejection

@iii.on_observer_added("cron::on-tick")
def start(obs: Observer) -> None:
    if not scheduler.is_valid(obs.args["expression"]):
        raise ObserveRejection("INVALID_CRON_EXPRESSION", "invalid expression")
    scheduler.retain(obs.args["expression"])

@iii.on_observer_removed("cron::on-tick")
def stop(obs: Observer) -> None:
    scheduler.release(obs.args["expression"])

# an observer of expression X gets the ticks of expression X
@iii.on_observer_match("cron::on-tick")
def same_expression(obs: Observer, call: Call) -> bool:
    return obs.args["expression"] == call.input["expression"]

iii.register_function(
    "cron::on-tick",
    lambda tick: tick,  # an event source returns the event it is called with
    args_format={"type": "object", "required": ["expression"],
                 "properties": {"expression": {"type": "string"}}},
    private=True,
)

# a tick is a call of the function
iii.call({"function_id": "cron::on-tick", "payload": {"expression": "*/5 * * * * *", "scheduled_time": now}})

on_observer_match takes a predicate (obs, call) -> bool, asked once per active observer per call, or observer= and call= key functions for a function with many observers. An observer wants a call when they share a key; the SDK computes an observer's keys once, when it is accepted, and keeps an index, so a call costs one lookup per key:

iii.on_observer_match(
    "database::on-row-changed",
    observer=lambda obs: f"{obs.args['table']}|{obs.args['op']}",  # what this observer listens for
    call=lambda call: f"{call.input['table']}|{call.input['op']}",  # what this call is
)

iii's delivery applies the on and filters of the observers that matched. The engine evaluates them for every call it routes: it holds the call's input, caller and metadata, and takes its output or error from this worker's answer. For those calls this worker runs only the match and deliver hooks, and its answer names the observers the match hook matched. The engine never sees the calls this worker makes to its own functions, so the SDK evaluates on and filters for those, through an index of the filters' $eq and $in conditions: with 1,000 observers that each filter on their own input.tenant, a local call evaluates one filter. obs.matches(call) evaluates one observer's on and filters in this worker.

iii.is_observed(function_id, call) tells a source whether a call would reach anyone (on a function matched by keys, by looking up the call's keys), and iii.observers(function_id) lists the active observers.

A function whose observers' answers matter, or whose calls each go to one observer, delivers its calls itself. Its on_observer_deliver receives the Call and the observers that matched it, calls through an observer, and returns what the caller gets back. iii delivers nothing for that function:

@iii.on_observer_deliver("http::on-request")
async def route(call, observers):
    obs = routes.match(call.input)
    if obs is None:
        return {"status_code": 404}
    if not obs.matches(call):
        return {"status_code": 422}
    return await iii.call_async({"observer": obs, "payload": call.input})

To send one observer something of its own, such as its initial state, call the function with "metadata": {"for_observer": obs.id} once the observer is accepted, and let the match hook give that call to that observer alone. iii delivers it like every call, so the target receives it in the shape it receives every call of the function:

import asyncio

from iii import CallAction, ObserveRejection


async def send_state(obs):
    try:
        await obs.accepted_async()
    except ObserveRejection:
        return  # a rejected observer gets nothing
    await iii.call_async({
        "function_id": "tmux::on-changed",
        "payload": state_for(obs.args),
        "metadata": {"for_observer": obs.id},
        "action": CallAction.Void(),
    })


@iii.on_observer_added("tmux::on-changed")
async def on_added(obs):
    asyncio.get_running_loop().create_task(send_state(obs))


@iii.on_observer_match("tmux::on-changed")
def wants(obs, call):
    for_observer = (call.metadata or {}).get("for_observer")
    if for_observer:
        return for_observer == obs.id
    return obs.args["session"] == call.input["session"]

With keys, file each observer under f"for_observer:{obs.id}" as well, and give such a call that one key.

Inside any handler, iii.current_invocation() returns the running invocation (function_id, invocation_id, caller, activation, observe_depth). Sync handlers and hooks run on a thread of their own started in a copy of the caller's context, so the current invocation and the OpenTelemetry context follow them.

A function that sets args_format, private=True, observable=False, an on_observer_match hook or an on_observer_deliver hook needs an engine that advertises observe.v2; against an older engine it stays unregistered, and the SDK logs UNSUPPORTED once for it.

Development

Install in development mode

pip install -e .

Type checking

mypy src

Linting

ruff check src

Resources

Metadata

Release files for iii-sdk 0.26.0rc1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for iii-sdk 0.26.0rc1
File Size Uploaded Provenance Actions
iii_sdk-0.26.0rc1.tar.gz 395.2 kB Without provenance Details for iii_sdk-0.26.0rc1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for iii-sdk 0.26.0rc1
File Interpreter ABI Platform Actions
iii_sdk-0.26.0rc1-py3-none-any.whl Python 3 none any Details for iii_sdk-0.26.0rc1-py3-none-any.whl

Total release size: 519.4 kB

Release files / iii_sdk-0.26.0rc1.tar.gz

Download URL iii_sdk-0.26.0rc1.tar.gz
Size 395.2 kB
Tags Source
SHA-256 checksum
How to use checksums
d20057a6c6bd357a06c9f345de0d7e4010d19479db6719d0c423e8f49404e0d7
BLAKE2b-256 checksum
How to use checksums
6f3a93c06902ddf978ec8b41789c679a4b60bfa7111edc2eca5f5e6ef4877750
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / iii_sdk-0.26.0rc1-py3-none-any.whl

Download URL iii_sdk-0.26.0rc1-py3-none-any.whl
Size 124.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2d886329f4946ff4c1c695c329385a23a91464a06ba3dda976bf28e36c2542c6
BLAKE2b-256 checksum
How to use checksums
5bf77052079f03fdb75e0a39685ac140c719cb3281aaf255eaa4f64ee2bf1c0b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Security for iii-sdk 0.26.0rc1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page