Wiggle — Python client
An idiomatic Python client and worker for the Wiggle workflow engine. It speaks the same gRPC control plane as the Java client, so Python and Java workers interoperate on the same server: define a workflow in either language, and any worker that registers the matching handlers can run its steps.
- Control client — register workflows, start and track instances, deliver signals, manage schedules.
- Worker — pull tasks you have capacity for, run handlers, report results; automatic lease heartbeats and retries.
- Fluent DSL — build a workflow as a chain of
step/gate/sleep/await_signal, with optional per-stepretryandqueue.
Install
pip install wiggle-client
# or from a checkout:
pip install -e .
Requires Python 3.9+ and a running Wiggle server — see the engine repo
(docker run … hadielmougy/wiggle, or ./gradlew :dist:run there).
Quick start
from wiggle import Workflow, Retry, WiggleClient, Worker
# 1. Define a workflow. The context is a plain dict; a step returns the whole context.
wf = (Workflow("order")
.step("validate", lambda o: {**o, "status": "VALIDATED"})
.gate("in-stock", lambda o: o["quantity"] > 0) # false -> ends as gated:in-stock
.step("charge", charge, queue="payments", retry=Retry.exponential(5, 0.1))
.sleep("cool-off", seconds=1)
.effect("notify", lambda o: print("shipped", o["orderId"]))
.build())
with WiggleClient("localhost:8080") as client:
client.register(wf)
worker = Worker(client, "worker-1").register(wf).start() # background threads
try:
iid = client.start(wf, {"orderId": "A-1", "quantity": 3})
view = client.await_completion(iid, timeout_s=30)
print(view.status, view.context) # COMPLETED {...}
finally:
worker.stop()
Run the bundled example against a server on :8080:
python examples/order.py # after `pip install -e .` (or: PYTHONPATH=. python examples/order.py)
The DSL
| Operator | Meaning |
|---|---|
step(name, fn, *, queue=None, retry=None) |
run fn(ctx) -> ctx on a worker; the returned context is merged back |
then(name, fn, ...) |
alias for step, reads well when sequencing |
effect(name, fn, ...) |
run fn(ctx) for a side effect; context unchanged |
gate(name, test, ...) |
continue only while test(ctx) is true; false ends the instance as gated:<name> |
fork(Branch.of(name, body), …) |
run branches in parallel, then wait for all of them (join) |
fork_each(name, items_key, item_key, body) |
runtime fan-out: one parallel branch per element of the list at items_key |
choose(Case.when(name, guard, body), …, Case.otherwise(name, body)) |
exclusive choice: the first matching guard's branch runs |
do_while(name, cond, body) |
run body, then repeat while cond(ctx) holds (body runs at least once) |
sub_workflow(name, child) |
run another workflow (a Blueprint, Workflow, or name) as a child; its result merges back |
sleep(name, *, seconds=, millis=) |
server-side timer; no worker is held |
await_signal(name, *, timeout_s=0, escalation=None) |
wait for a signal delivered via client.signal(...); on timeout, fail — or run the escalation branch and rejoin |
default_queue(q) |
queue for every step that doesn't set its own (defaults to the workflow name) |
build() |
produce a Blueprint to register and serve |
A branch/case body is a function that receives a nested builder and chains onto it:
wf = (Workflow("order")
.step("validate", validate)
.fork( # parallel, joined
Branch.of("payment", lambda b: b.step("charge", charge)),
Branch.of("shipping", lambda b: b.step("reserve", reserve).step("label", label)))
.choose( # exactly one arm runs
Case.when("vip", lambda o: o.get("vip"), lambda b: b.step("concierge", concierge)),
Case.otherwise("standard", lambda b: b.step("thanks", thanks)))
.step("notify", notify)
.build())
Branches touching different fields merge cleanly; if two write the same key, the later write wins.
A gate inside a branch short-circuits to that fork's join (not the whole instance).
Runtime fan-out spawns one branch per list element, each seeing its element (and index):
wf = (Workflow("charge")
.fork_each("charge-items", "items", "item", lambda b: b
.step("price", lambda o: {**o, f"priced-{o['itemIndex']}": o["item"] * 10}))
.step("summarise", summarise)
.build())
# start(wf, {"items": [1, 2, 3]}) -> priced-0..2 ; an empty/missing list skips straight through
Branches share one context, so put per-element results under per-element keys (use the index).
Retry.exponential(attempts, initial_s), Retry.fixed(attempts, backoff_s), Retry.none(),
Retry.forever(). Raise wiggle.PermanentError from a handler to fail a step without retrying.
The builder covers the full operator set —
step/gate/fork/fork_each/choose/do_while/sub_workflow/sleep/await_signal — matching
the Java DSL. A sub_workflow's child must be registered separately (client.register(child)), and a
worker must serve the child's handlers too (Worker(...).register(child).register(parent)). Because
handlers are keyed by activity name ("<workflow>#<step>"), Python and Java workers interoperate:
either can run the other's steps.
Binding handlers by name (polyglot)
You don't have to re-declare a workflow just to implement one of its steps in Python. If the graph is
already registered (by any client, Java or Python), bind handlers by name — no Workflow, no
register:
# implement just `charge` on a flow whose topology was authored elsewhere (e.g. in Java)
worker = Worker(client, "payments").handle(
"order-fulfilment", "charge",
lambda o: {**o, "paymentRef": f"auth-{o['orderId']}"})
worker.start()
| Method | Binds a… |
|---|---|
handle(workflow, step, fn) |
task — fn(ctx) -> ctx, only the changed keys are sent back |
handle_gate(workflow, step, test) |
predicate — test(ctx) -> bool (a gate / choose guard / do-while condition) |
handle_effect(workflow, step, fn) |
side effect — fn(ctx) runs, the context is unchanged |
On start() the worker reconciles every binding against the registered graph: it checks each
step exists and is the right kind (a typo or a task bound as a gate fails fast, listing the real step
names), and it discovers which queue each step polls — so a name-only worker needs no queue
config. The graph must be registered before the worker starts; pass await_registration_s=… to
ride out a startup race instead of failing fast. See
examples/polyglot_worker.py for a Java-authored flow served from
Python.
Client API
client.register(blueprint) -> int # version
client.get_workflow(name) -> dict # the registered graph (steps, kinds, queues)
client.start(blueprint_or_name, context, *, version=None, correlation_id=None) -> instance_id
client.instance(id) -> InstanceView # .status .context .termination_reason .error
client.await_completion(id, timeout_s=30) -> InstanceView
client.list_instances(workflow=None, status=None, limit=100)
client.cancel(id, reason="cancelled")
client.signal(id, name, payload=None)
client.create_schedule(workflow, every_s=..|cron=.., context=None) / list_schedules() / delete_schedule(id)
client.health() / client.cluster()
Versioning
By default a workflow's version is a content hash of its structure — node kinds, names, activities, queues, retries, and the edge topology — independent of internal node-id numbering. So the same structure always yields the same version: re-registering is idempotent, the server de-duplicates, and changing the graph mints a new version (in-flight instances keep running on the old one). This is the safe, content-addressed default; you never set a number.
Pin an explicit version when you want a stable, human-meaningful one (or to match another client):
wf = Workflow("order", version=3).step("validate", validate)....build()
With an explicit version you own bumping it when the graph changes — the server overwrites the
stored graph for a reused name:version, which affects instances already running on it. Leave it
unset unless you have a specific reason.
Note:
client.start("name", …)uses the latest registered version. Don't register the same workflow name from two clients/definitions with different graphs; give them distinct names, or define the workflow in one place. (The Python content hash is its own — it does not equal the Java client's number for the "same" workflow; cross-language interop is by activity name, not version.)
Tests
Offline tests (no server needed) cover the DSL graph shapes and the wire conversions:
pip install -e '.[dev]'
pytest -q
Notes & limits
- Execution mode: Python-defined workflows run in
SERVERmode. The worker does not implement theLOCAL_SYNC/LOCAL_ASYNC(client-side chaining) protocol, so serve those with a Java worker. - Numbers: context travels as protobuf
Value(doubles). Whole numbers come back asint; fractional values asfloat— the same JSON-number reality as the rest of the system. - TLS: pass
WiggleClient(target, credentials=grpc.ssl_channel_credentials(...)). - Regenerating stubs:
pip install grpcio-tools && ./codegen.shafter changing the proto.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file wiggle_client-0.1.1.tar.gz.
File metadata
- Download URL: wiggle_client-0.1.1.tar.gz
- Upload date:
- Size: 38.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7d6dca10c78a7ca34b660f02b4730e448cbf0d08851614afd32788d3d35748c9
|
|
| MD5 |
7839531c79105fadaaff0e6138cc6ad9
|
|
| BLAKE2b-256 |
dbc891101a9c60b7d5e3a037e17210749bf58e8cfb553b5e9311d14810d5b71c
|
File details
Details for the file wiggle_client-0.1.1-py3-none-any.whl.
File metadata
- Download URL: wiggle_client-0.1.1-py3-none-any.whl
- Upload date:
- Size: 31.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f1b21694ba3cb74fed1a8c4f54f98ef8cd0e413019c8915ba47853f1a192cfd8
|
|
| MD5 |
1b45190febba1bab9d7a8bb2af26736d
|
|
| BLAKE2b-256 |
3b9d2004b6a4d1e4dc637216c985decc21a83c8bca14e93cc726587cfcdcfaa4
|