Skip to content

Commit 35e6f22

Browse files
committed
docs: update README.md
1 parent f7ddfea commit 35e6f22

File tree

1 file changed

+57
-34
lines changed

1 file changed

+57
-34
lines changed

README.md

Lines changed: 57 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
[![Python Versions](https://img.shields.io/pypi/pyversions/duron)](https://pypi.org/project/duron)
66
[![License](https://img.shields.io/github/license/brian14708/duron.svg)](https://github.com/brian14708/duron/blob/main/LICENSE)
77

8-
Duron is a lightweight durable execution runtime for Python async workflows. It provides replayable execution primitives that work standalone or as building blocks for complex workflow engines.
8+
**Durable workflows for modern Python.** Build resilient async applications with native support for streaming and interruption.
99

10-
- 🪶 **Zero extra deps**Lightweight library that layers on top of asyncio; add Duron without bloating your stack.
11-
- 🧩 **Pluggable architecture**Bring your own storage or infra components and swap them without changing orchestration code.
12-
- 🔄 **Streams & signals**Model long-running conversations, live data feeds, and feedback loops with built-in primitives.
13-
- 🐍 **Python-native & typed**Type hints make replay serialization predictable, and everything feels like idiomatic Python.
14-
- 🔭 **Built-in tracing**Detailed logs help you inspect replays and surface observability data wherever you need it.
10+
- 💬 **Interactive workflows**AI agents, chatbots, and human-in-the-loop automation with bidirectional streaming
11+
- **Crash recovery**Deterministic replay from append-only logs means workflows survive restarts
12+
- 🎯 **Graceful interruption**Cancel or redirect operations mid-execution with signals
13+
- 🔌 **Zero dependencies**Pure Python built on asyncio, fully typed
14+
- 🧩 **Pluggable storage**Bring your own database or filesystem backend
1515

1616
## Install
1717

@@ -23,52 +23,75 @@ uv pip install duron
2323

2424
## Quickstart
2525

26-
Duron wraps async orchestration (`@duron.durable`) and effectful steps (`@duron.effect`) so complex workflows stay deterministic—even when they touch the outside world.
27-
2826
```python
27+
# /// script
28+
# dependencies = ["duron"]
29+
# ///
30+
2931
import asyncio
30-
import random
3132
from pathlib import Path
32-
3333
import duron
3434
from duron.contrib.storage import FileLogStorage
3535

3636

37-
# Effects encapsulate side effects (I/O, randomness, API calls)
3837
@duron.effect
39-
async def work(name: str) -> str:
40-
print("⚡ Preparing to greet...")
41-
await asyncio.sleep(2)
42-
print("⚡ Greeting...")
43-
return f"Hello, {name}!"
38+
async def check_fraud(amount: float, recipient: str) -> float:
39+
print("Executing risk check...")
40+
await asyncio.sleep(0.5)
41+
return 0.85 # High risk score
4442

4543

4644
@duron.effect
47-
async def generate_lucky_number() -> int:
48-
print("⚡ Generating lucky number...")
45+
async def execute_transfer(amount: float, recipient: str) -> str:
46+
print("Executing transfer...")
4947
await asyncio.sleep(1)
50-
return random.randint(1, 100)
48+
return f"Transferred ${amount} to {recipient}"
5149

5250

53-
# Durable functions orchestrate workflow logic via ctx.run()
54-
# They're deterministically replayed from logs on resume
5551
@duron.durable
56-
async def greeting_flow(ctx: duron.Context, name: str) -> str:
57-
# Run effects concurrently - results are logged for replay
58-
message, lucky_number = await asyncio.gather(
59-
ctx.run(work, name),
60-
ctx.run(generate_lucky_number),
61-
)
62-
return f"{message} Your lucky number is {lucky_number}."
52+
async def transfer_workflow(
53+
ctx: duron.Context,
54+
amount: float,
55+
recipient: str,
56+
events: duron.StreamWriter[list[str]] = duron.Provided,
57+
) -> str:
58+
async with events:
59+
# report progress through events stream
60+
await events.send(["log", f"Checking transfer: ${amount}{recipient}"])
61+
# durable function calls
62+
risk = await ctx.run(check_fraud, amount, recipient)
63+
64+
if risk > 0.8:
65+
approval_id, approval = await ctx.create_future(bool)
66+
await events.send(["log", "⚠️ High risk - approval required"])
67+
await events.send(["approval", approval_id])
68+
69+
if not await approval:
70+
await events.send(["log", "❌ Transfer rejected by manager"])
71+
return "Transfer rejected"
72+
73+
result = await ctx.run(execute_transfer, amount, recipient)
74+
await events.send(["log", f"{result}"])
75+
return result
6376

6477

6578
async def main():
66-
# Session manages execution and log storage
67-
async with duron.Session(FileLogStorage(Path("log.jsonl"))) as session:
68-
# Starts new workflow or resumes from existing log
69-
task = await session.start(greeting_flow, "Alice")
70-
result = await task.result()
71-
print(result)
79+
async with duron.Session(FileLogStorage(Path("transfer.jsonl"))) as session:
80+
task = await session.start(transfer_workflow, 10000.0, "suspicious-account")
81+
stream = await task.open_stream("events", "r")
82+
83+
async def handle_events():
84+
async for event_type, data in stream:
85+
if event_type == "log":
86+
print(data)
87+
elif event_type == "approval":
88+
if task.is_future_pending(data): # if not pending means it was a rerun
89+
decision = await asyncio.to_thread(input, "Approve? (y/n): ")
90+
await task.complete_future(
91+
data, result=(decision.lower() == "y")
92+
)
93+
94+
await asyncio.gather(task.result(), handle_events())
7295

7396

7497
if __name__ == "__main__":

0 commit comments

Comments
 (0)