Skip to content

Repository files navigation

Duron

CIPyPI - VersionPython VersionsLicense

Durable workflows for modern Python. Build resilient async applications with native support for streaming and interruption.

  • 💬 Interactive workflows — AI agents, chatbots, and human-in-the-loop automation with bidirectional streaming
  • Crash recovery — Deterministic replay from append-only logs means workflows survive restarts
  • 🎯 Graceful interruption — Cancel or redirect operations mid-execution with signals
  • 🔌 Zero dependencies — Pure Python built on asyncio, fully typed
  • 🧩 Pluggable storage — Bring your own database or filesystem backend

Install

Duron requires Python 3.10+.

uv pip install duron

Quickstart

# /// script# dependencies = ["duron"]# ///importasynciofrompathlibimportPathfromtypingimportOptional, TypedDictimportduronfromduron.contrib.storageimportFileLogStorageclassEvent(TypedDict):
""" Event type for communicating workflow progress and approvals. Fields: message: Log or status message for the user. approval_id: Durable future ID to request approval (None for normal logs). """message: strapproval_id: Optional[str]
# -----------------------# Effect definitions# -----------------------@duron.effectasyncdefcheck_fraud(amount: float, recipient: str) ->float:
"""Simulate a risk engine returning a fraud probability."""print("Executing risk check...")
awaitasyncio.sleep(0.5)
return0.85@duron.effectasyncdefexecute_transfer(amount: float, recipient: str) ->str:
"""Simulate a real transfer execution."""print("Executing transfer...")
awaitasyncio.sleep(1)
returnf"Transferred ${amount} to {recipient}"# -----------------------# Durable workflow# -----------------------@duron.durableasyncdeftransfer_workflow(
ctx: duron.Context,
amount: float,
recipient: str,
events: duron.StreamWriter[Event] =duron.Provided,
) ->str:
""" Durable workflow to execute a transfer with fraud detection and optional manager approval. """asyncwithevents:
# Log start of transferawaitevents.send({
"message": f"Checking transfer: ${amount}{recipient}",
"approval_id": None,
})
# Step 1: Fraud checkrisk=awaitctx.run(check_fraud, amount, recipient)
# Step 2: Approval required if high riskifrisk>0.8:
approval_id, approval=awaitctx.create_future(bool)
awaitevents.send({
"message": "⚠️ High risk - approval required",
"approval_id": approval_id,
})
ifnotawaitapproval:
awaitevents.send({
"message": "❌ Transfer rejected by manager",
"approval_id": None,
})
return"Transfer rejected"# Step 3: Execute transferresult=awaitctx.run(execute_transfer, amount, recipient)
awaitevents.send({"message": f"✓ {result}", "approval_id": None})
returnresult# -----------------------# Host process# -----------------------asyncdefmain():
""" Run the workflow locally with file-based state storage. """asyncwithduron.Session(FileLogStorage(Path("transfer.jsonl"))) assession:
task=awaitsession.start(transfer_workflow, 10000.0, "suspicious-account")
stream=awaittask.open_stream("events", "r")
asyncdefhandle_events():
asyncforeventinstream:
# Always print messageprint(event["message"])
# If approval_id is present, prompt for manager decision# If the future is not pending, it means it was already resolved (e.g., workflow resumed)ifevent["approval_id"] andtask.is_future_pending(
event["approval_id"]
):
decision=awaitasyncio.to_thread(input, "Approve? (y/n): ")
awaittask.complete_future(
event["approval_id"], result=(decision.lower() =="y")
)
awaitasyncio.gather(task.result(), handle_events())
if__name__=="__main__":
asyncio.run(main())

Next steps

About

🌀 Durable async runtime for Python

Topics

Resources

Stars

11 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages