pyworkflow-engine 0.1.7__py3-none-any.whl → 0.1.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyworkflow/__init__.py +10 -1
- pyworkflow/celery/tasks.py +272 -24
- pyworkflow/cli/__init__.py +4 -1
- pyworkflow/cli/commands/runs.py +4 -4
- pyworkflow/cli/commands/setup.py +203 -4
- pyworkflow/cli/utils/config_generator.py +76 -3
- pyworkflow/cli/utils/docker_manager.py +232 -0
- pyworkflow/config.py +94 -17
- pyworkflow/context/__init__.py +13 -0
- pyworkflow/context/base.py +26 -0
- pyworkflow/context/local.py +80 -0
- pyworkflow/context/step_context.py +295 -0
- pyworkflow/core/registry.py +6 -1
- pyworkflow/core/step.py +141 -0
- pyworkflow/core/workflow.py +56 -0
- pyworkflow/engine/events.py +30 -0
- pyworkflow/engine/replay.py +39 -0
- pyworkflow/primitives/child_workflow.py +1 -1
- pyworkflow/runtime/local.py +1 -1
- pyworkflow/storage/__init__.py +14 -0
- pyworkflow/storage/base.py +35 -0
- pyworkflow/storage/cassandra.py +1747 -0
- pyworkflow/storage/config.py +69 -0
- pyworkflow/storage/dynamodb.py +31 -2
- pyworkflow/storage/file.py +28 -0
- pyworkflow/storage/memory.py +18 -0
- pyworkflow/storage/mysql.py +1159 -0
- pyworkflow/storage/postgres.py +27 -2
- pyworkflow/storage/schemas.py +4 -3
- pyworkflow/storage/sqlite.py +25 -2
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/METADATA +7 -4
- pyworkflow_engine-0.1.10.dist-info/RECORD +91 -0
- pyworkflow_engine-0.1.10.dist-info/top_level.txt +1 -0
- dashboard/backend/app/__init__.py +0 -1
- dashboard/backend/app/config.py +0 -32
- dashboard/backend/app/controllers/__init__.py +0 -6
- dashboard/backend/app/controllers/run_controller.py +0 -86
- dashboard/backend/app/controllers/workflow_controller.py +0 -33
- dashboard/backend/app/dependencies/__init__.py +0 -5
- dashboard/backend/app/dependencies/storage.py +0 -50
- dashboard/backend/app/repositories/__init__.py +0 -6
- dashboard/backend/app/repositories/run_repository.py +0 -80
- dashboard/backend/app/repositories/workflow_repository.py +0 -27
- dashboard/backend/app/rest/__init__.py +0 -8
- dashboard/backend/app/rest/v1/__init__.py +0 -12
- dashboard/backend/app/rest/v1/health.py +0 -33
- dashboard/backend/app/rest/v1/runs.py +0 -133
- dashboard/backend/app/rest/v1/workflows.py +0 -41
- dashboard/backend/app/schemas/__init__.py +0 -23
- dashboard/backend/app/schemas/common.py +0 -16
- dashboard/backend/app/schemas/event.py +0 -24
- dashboard/backend/app/schemas/hook.py +0 -25
- dashboard/backend/app/schemas/run.py +0 -54
- dashboard/backend/app/schemas/step.py +0 -28
- dashboard/backend/app/schemas/workflow.py +0 -31
- dashboard/backend/app/server.py +0 -87
- dashboard/backend/app/services/__init__.py +0 -6
- dashboard/backend/app/services/run_service.py +0 -240
- dashboard/backend/app/services/workflow_service.py +0 -155
- dashboard/backend/main.py +0 -18
- docs/concepts/cancellation.mdx +0 -362
- docs/concepts/continue-as-new.mdx +0 -434
- docs/concepts/events.mdx +0 -266
- docs/concepts/fault-tolerance.mdx +0 -370
- docs/concepts/hooks.mdx +0 -552
- docs/concepts/limitations.mdx +0 -167
- docs/concepts/schedules.mdx +0 -775
- docs/concepts/sleep.mdx +0 -312
- docs/concepts/steps.mdx +0 -301
- docs/concepts/workflows.mdx +0 -255
- docs/guides/cli.mdx +0 -942
- docs/guides/configuration.mdx +0 -560
- docs/introduction.mdx +0 -155
- docs/quickstart.mdx +0 -279
- examples/__init__.py +0 -1
- examples/celery/__init__.py +0 -1
- examples/celery/durable/docker-compose.yml +0 -55
- examples/celery/durable/pyworkflow.config.yaml +0 -12
- examples/celery/durable/workflows/__init__.py +0 -122
- examples/celery/durable/workflows/basic.py +0 -87
- examples/celery/durable/workflows/batch_processing.py +0 -102
- examples/celery/durable/workflows/cancellation.py +0 -273
- examples/celery/durable/workflows/child_workflow_patterns.py +0 -240
- examples/celery/durable/workflows/child_workflows.py +0 -202
- examples/celery/durable/workflows/continue_as_new.py +0 -260
- examples/celery/durable/workflows/fault_tolerance.py +0 -210
- examples/celery/durable/workflows/hooks.py +0 -211
- examples/celery/durable/workflows/idempotency.py +0 -112
- examples/celery/durable/workflows/long_running.py +0 -99
- examples/celery/durable/workflows/retries.py +0 -101
- examples/celery/durable/workflows/schedules.py +0 -209
- examples/celery/transient/01_basic_workflow.py +0 -91
- examples/celery/transient/02_fault_tolerance.py +0 -257
- examples/celery/transient/__init__.py +0 -20
- examples/celery/transient/pyworkflow.config.yaml +0 -25
- examples/local/__init__.py +0 -1
- examples/local/durable/01_basic_workflow.py +0 -94
- examples/local/durable/02_file_storage.py +0 -132
- examples/local/durable/03_retries.py +0 -169
- examples/local/durable/04_long_running.py +0 -119
- examples/local/durable/05_event_log.py +0 -145
- examples/local/durable/06_idempotency.py +0 -148
- examples/local/durable/07_hooks.py +0 -334
- examples/local/durable/08_cancellation.py +0 -233
- examples/local/durable/09_child_workflows.py +0 -198
- examples/local/durable/10_child_workflow_patterns.py +0 -265
- examples/local/durable/11_continue_as_new.py +0 -249
- examples/local/durable/12_schedules.py +0 -198
- examples/local/durable/__init__.py +0 -1
- examples/local/transient/01_quick_tasks.py +0 -87
- examples/local/transient/02_retries.py +0 -130
- examples/local/transient/03_sleep.py +0 -141
- examples/local/transient/__init__.py +0 -1
- pyworkflow_engine-0.1.7.dist-info/RECORD +0 -196
- pyworkflow_engine-0.1.7.dist-info/top_level.txt +0 -5
- tests/examples/__init__.py +0 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_cancellation.py +0 -330
- tests/integration/test_child_workflows.py +0 -439
- tests/integration/test_continue_as_new.py +0 -428
- tests/integration/test_dynamodb_storage.py +0 -1146
- tests/integration/test_fault_tolerance.py +0 -369
- tests/integration/test_schedule_storage.py +0 -484
- tests/unit/__init__.py +0 -0
- tests/unit/backends/__init__.py +0 -1
- tests/unit/backends/test_dynamodb_storage.py +0 -1554
- tests/unit/backends/test_postgres_storage.py +0 -1281
- tests/unit/backends/test_sqlite_storage.py +0 -1460
- tests/unit/conftest.py +0 -41
- tests/unit/test_cancellation.py +0 -364
- tests/unit/test_child_workflows.py +0 -680
- tests/unit/test_continue_as_new.py +0 -441
- tests/unit/test_event_limits.py +0 -316
- tests/unit/test_executor.py +0 -320
- tests/unit/test_fault_tolerance.py +0 -334
- tests/unit/test_hooks.py +0 -495
- tests/unit/test_registry.py +0 -261
- tests/unit/test_replay.py +0 -420
- tests/unit/test_schedule_schemas.py +0 -285
- tests/unit/test_schedule_utils.py +0 -286
- tests/unit/test_scheduled_workflow.py +0 -274
- tests/unit/test_step.py +0 -353
- tests/unit/test_workflow.py +0 -243
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/WHEEL +0 -0
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/entry_points.txt +0 -0
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/licenses/LICENSE +0 -0
docs/concepts/sleep.mdx
DELETED
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 'Sleep'
|
|
3
|
-
description: 'Pause workflows for any duration without consuming resources'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## What is Sleep?
|
|
7
|
-
|
|
8
|
-
The `sleep()` primitive pauses a workflow for a specified duration. Unlike traditional sleep that blocks a thread, PyWorkflow's sleep **suspends** the workflow completely - no resources are consumed during the sleep period.
|
|
9
|
-
|
|
10
|
-
```python
|
|
11
|
-
from pyworkflow import workflow, sleep
|
|
12
|
-
|
|
13
|
-
@workflow()
|
|
14
|
-
async def reminder_sequence(user_id: str):
|
|
15
|
-
await send_reminder(user_id, "First reminder")
|
|
16
|
-
|
|
17
|
-
# Workflow suspends here - zero resources used
|
|
18
|
-
await sleep("1d")
|
|
19
|
-
|
|
20
|
-
# Resumes automatically after 1 day
|
|
21
|
-
await send_reminder(user_id, "Second reminder")
|
|
22
|
-
|
|
23
|
-
await sleep("7d")
|
|
24
|
-
|
|
25
|
-
await send_reminder(user_id, "Final reminder")
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## How It Works
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
Workflow Execution
|
|
32
|
-
│
|
|
33
|
-
▼
|
|
34
|
-
┌───────────────┐
|
|
35
|
-
│ Execute Steps │
|
|
36
|
-
└───────┬───────┘
|
|
37
|
-
│
|
|
38
|
-
▼
|
|
39
|
-
┌───────────────┐
|
|
40
|
-
│ sleep("1d") │
|
|
41
|
-
└───────┬───────┘
|
|
42
|
-
│
|
|
43
|
-
├─── 1. Record sleep_started event
|
|
44
|
-
│
|
|
45
|
-
├─── 2. Schedule Celery Beat task for wake time
|
|
46
|
-
│
|
|
47
|
-
├─── 3. Raise SuspensionSignal
|
|
48
|
-
│
|
|
49
|
-
└─── 4. Worker is freed
|
|
50
|
-
│
|
|
51
|
-
│ ... 1 day passes ...
|
|
52
|
-
│
|
|
53
|
-
▼
|
|
54
|
-
┌─────────────────┐
|
|
55
|
-
│ Celery Beat │
|
|
56
|
-
│ triggers resume │
|
|
57
|
-
└────────┬────────┘
|
|
58
|
-
│
|
|
59
|
-
▼
|
|
60
|
-
┌───────────────┐
|
|
61
|
-
│ Replay events │
|
|
62
|
-
│ Resume work │
|
|
63
|
-
└───────────────┘
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Duration Formats
|
|
67
|
-
|
|
68
|
-
### String Format (Recommended)
|
|
69
|
-
|
|
70
|
-
```python
|
|
71
|
-
await sleep("30s") # 30 seconds
|
|
72
|
-
await sleep("5m") # 5 minutes
|
|
73
|
-
await sleep("2h") # 2 hours
|
|
74
|
-
await sleep("1d") # 1 day
|
|
75
|
-
await sleep("1w") # 1 week
|
|
76
|
-
|
|
77
|
-
# Combined
|
|
78
|
-
await sleep("1h30m") # 1 hour 30 minutes
|
|
79
|
-
await sleep("2d12h") # 2 days 12 hours
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### Timedelta
|
|
83
|
-
|
|
84
|
-
```python
|
|
85
|
-
from datetime import timedelta
|
|
86
|
-
|
|
87
|
-
await sleep(timedelta(hours=2, minutes=30))
|
|
88
|
-
await sleep(timedelta(days=7))
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### Until Specific Time
|
|
92
|
-
|
|
93
|
-
```python
|
|
94
|
-
from datetime import datetime
|
|
95
|
-
|
|
96
|
-
# Sleep until a specific datetime
|
|
97
|
-
await sleep(datetime(2025, 12, 25, 9, 0, 0))
|
|
98
|
-
|
|
99
|
-
# Sleep until next Monday at 9 AM
|
|
100
|
-
next_monday = get_next_monday()
|
|
101
|
-
await sleep(next_monday.replace(hour=9, minute=0))
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Integer (Seconds)
|
|
105
|
-
|
|
106
|
-
```python
|
|
107
|
-
await sleep(300) # 300 seconds (5 minutes)
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## Zero-Resource Suspension
|
|
111
|
-
|
|
112
|
-
Traditional async sleep blocks a worker:
|
|
113
|
-
|
|
114
|
-
```python
|
|
115
|
-
# BAD: This holds a worker for 24 hours!
|
|
116
|
-
import asyncio
|
|
117
|
-
|
|
118
|
-
async def traditional_sleep():
|
|
119
|
-
await do_something()
|
|
120
|
-
await asyncio.sleep(86400) # Blocks worker for 24h
|
|
121
|
-
await do_something_else()
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
PyWorkflow's sleep releases the worker:
|
|
125
|
-
|
|
126
|
-
```python
|
|
127
|
-
# GOOD: Worker is freed during sleep
|
|
128
|
-
from pyworkflow import sleep
|
|
129
|
-
|
|
130
|
-
@workflow()
|
|
131
|
-
async def efficient_sleep():
|
|
132
|
-
await do_something()
|
|
133
|
-
await sleep("24h") # Worker freed, resumes later
|
|
134
|
-
await do_something_else()
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
<Note>
|
|
138
|
-
With 100 workflows each sleeping for 1 day, traditional sleep would need 100 workers blocked for 24 hours. PyWorkflow needs 0 workers during the sleep period.
|
|
139
|
-
</Note>
|
|
140
|
-
|
|
141
|
-
## Use Cases
|
|
142
|
-
|
|
143
|
-
### Scheduled Reminders
|
|
144
|
-
|
|
145
|
-
```python
|
|
146
|
-
@workflow()
|
|
147
|
-
async def onboarding_drip(user_id: str):
|
|
148
|
-
await send_email(user_id, "Welcome!")
|
|
149
|
-
|
|
150
|
-
await sleep("1d")
|
|
151
|
-
await send_email(user_id, "Getting started tips")
|
|
152
|
-
|
|
153
|
-
await sleep("3d")
|
|
154
|
-
await send_email(user_id, "Advanced features")
|
|
155
|
-
|
|
156
|
-
await sleep("7d")
|
|
157
|
-
await send_email(user_id, "How's it going?")
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
### Delayed Processing
|
|
161
|
-
|
|
162
|
-
```python
|
|
163
|
-
@workflow()
|
|
164
|
-
async def process_refund(order_id: str):
|
|
165
|
-
await validate_refund_request(order_id)
|
|
166
|
-
|
|
167
|
-
# Wait for potential fraud review
|
|
168
|
-
await sleep("24h")
|
|
169
|
-
|
|
170
|
-
# If not flagged, process refund
|
|
171
|
-
await execute_refund(order_id)
|
|
172
|
-
await notify_customer(order_id)
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### Rate Limiting
|
|
176
|
-
|
|
177
|
-
```python
|
|
178
|
-
@workflow()
|
|
179
|
-
async def batch_api_calls(items: list):
|
|
180
|
-
for i, item in enumerate(items):
|
|
181
|
-
await call_api(item)
|
|
182
|
-
|
|
183
|
-
# Rate limit: 10 calls per minute
|
|
184
|
-
if (i + 1) % 10 == 0:
|
|
185
|
-
await sleep("1m")
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
### Retry with Backoff
|
|
189
|
-
|
|
190
|
-
```python
|
|
191
|
-
@workflow()
|
|
192
|
-
async def resilient_operation():
|
|
193
|
-
for attempt in range(5):
|
|
194
|
-
try:
|
|
195
|
-
result = await risky_step()
|
|
196
|
-
return result
|
|
197
|
-
except TemporaryError:
|
|
198
|
-
if attempt < 4:
|
|
199
|
-
# Exponential backoff: 1m, 2m, 4m, 8m
|
|
200
|
-
delay = f"{2 ** attempt}m"
|
|
201
|
-
await sleep(delay)
|
|
202
|
-
|
|
203
|
-
raise FatalError("All retries exhausted")
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
## Sleep vs Step Timeout
|
|
207
|
-
|
|
208
|
-
Sleep and timeouts serve different purposes:
|
|
209
|
-
|
|
210
|
-
| Feature | Sleep | Timeout |
|
|
211
|
-
|---------|-------|---------|
|
|
212
|
-
| Purpose | Intentional delay | Maximum execution time |
|
|
213
|
-
| Resources | Zero during sleep | Worker is active |
|
|
214
|
-
| Failure | Never fails | Fails if exceeded |
|
|
215
|
-
|
|
216
|
-
```python
|
|
217
|
-
@step(timeout="30s") # Fails if step takes > 30s
|
|
218
|
-
async def quick_step():
|
|
219
|
-
pass
|
|
220
|
-
|
|
221
|
-
@workflow()
|
|
222
|
-
async def my_workflow():
|
|
223
|
-
await quick_step()
|
|
224
|
-
await sleep("1h") # Intentional 1-hour pause
|
|
225
|
-
await quick_step()
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
## Celery Beat Requirement
|
|
229
|
-
|
|
230
|
-
Sleep resumption requires Celery Beat to be running:
|
|
231
|
-
|
|
232
|
-
```bash
|
|
233
|
-
# Start Celery Beat for scheduled task execution
|
|
234
|
-
celery -A pyworkflow.celery.app beat --loglevel=info
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
<Warning>
|
|
238
|
-
Without Celery Beat, workflows will suspend but never resume automatically. Make sure Beat is running in production.
|
|
239
|
-
</Warning>
|
|
240
|
-
|
|
241
|
-
### Docker Compose Setup
|
|
242
|
-
|
|
243
|
-
```yaml
|
|
244
|
-
services:
|
|
245
|
-
worker:
|
|
246
|
-
command: celery -A pyworkflow.celery.app worker --loglevel=info
|
|
247
|
-
|
|
248
|
-
beat:
|
|
249
|
-
command: celery -A pyworkflow.celery.app beat --loglevel=info
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
## Best Practices
|
|
253
|
-
|
|
254
|
-
<AccordionGroup>
|
|
255
|
-
<Accordion title="Use sleep for intentional delays only">
|
|
256
|
-
Don't use sleep as a retry mechanism. Use step retry configuration instead:
|
|
257
|
-
|
|
258
|
-
```python
|
|
259
|
-
# Good: Use retry config
|
|
260
|
-
@step(max_retries=3, retry_delay="exponential")
|
|
261
|
-
async def my_step():
|
|
262
|
-
pass
|
|
263
|
-
|
|
264
|
-
# Avoid: Manual retry with sleep
|
|
265
|
-
@workflow()
|
|
266
|
-
async def my_workflow():
|
|
267
|
-
for i in range(3):
|
|
268
|
-
try:
|
|
269
|
-
await my_step()
|
|
270
|
-
break
|
|
271
|
-
except:
|
|
272
|
-
await sleep(f"{2**i}m")
|
|
273
|
-
```
|
|
274
|
-
</Accordion>
|
|
275
|
-
|
|
276
|
-
<Accordion title="Consider timezone implications">
|
|
277
|
-
When sleeping until a specific time, be aware of timezones:
|
|
278
|
-
|
|
279
|
-
```python
|
|
280
|
-
from datetime import datetime
|
|
281
|
-
import pytz
|
|
282
|
-
|
|
283
|
-
# Explicit timezone
|
|
284
|
-
eastern = pytz.timezone("US/Eastern")
|
|
285
|
-
wake_time = eastern.localize(datetime(2025, 1, 15, 9, 0))
|
|
286
|
-
await sleep(wake_time)
|
|
287
|
-
```
|
|
288
|
-
</Accordion>
|
|
289
|
-
|
|
290
|
-
<Accordion title="Keep sleep durations reasonable">
|
|
291
|
-
Very long sleeps (months, years) work but consider if a different approach is better:
|
|
292
|
-
|
|
293
|
-
```python
|
|
294
|
-
# Works, but consider alternatives
|
|
295
|
-
await sleep("365d")
|
|
296
|
-
|
|
297
|
-
# Alternative: Schedule a new workflow
|
|
298
|
-
schedule_workflow(annual_review, run_at="2026-01-01")
|
|
299
|
-
```
|
|
300
|
-
</Accordion>
|
|
301
|
-
</AccordionGroup>
|
|
302
|
-
|
|
303
|
-
## Next Steps
|
|
304
|
-
|
|
305
|
-
<CardGroup cols={2}>
|
|
306
|
-
<Card title="Workflows" icon="diagram-project" href="/concepts/workflows">
|
|
307
|
-
Learn about workflow orchestration.
|
|
308
|
-
</Card>
|
|
309
|
-
<Card title="Deployment" icon="rocket" href="/guides/deployment">
|
|
310
|
-
Set up Celery Beat in production.
|
|
311
|
-
</Card>
|
|
312
|
-
</CardGroup>
|
docs/concepts/steps.mdx
DELETED
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 'Steps'
|
|
3
|
-
description: 'Isolated, retryable units of work that form the building blocks of workflows'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## What is a Step?
|
|
7
|
-
|
|
8
|
-
A step is an isolated, retryable unit of work within a workflow. Steps are where the actual business logic executes - calling APIs, processing data, sending emails, etc. Each step runs on a Celery worker and can be retried independently if it fails.
|
|
9
|
-
|
|
10
|
-
```python
|
|
11
|
-
from pyworkflow import step
|
|
12
|
-
|
|
13
|
-
@step()
|
|
14
|
-
async def send_email(to: str, subject: str, body: str):
|
|
15
|
-
"""Send an email - retries automatically on failure."""
|
|
16
|
-
async with EmailClient() as client:
|
|
17
|
-
await client.send(to=to, subject=subject, body=body)
|
|
18
|
-
return {"sent": True, "to": to}
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Key Characteristics
|
|
22
|
-
|
|
23
|
-
<CardGroup cols={2}>
|
|
24
|
-
<Card title="Isolated" icon="box">
|
|
25
|
-
Each step runs independently with its own retry policy and timeout.
|
|
26
|
-
</Card>
|
|
27
|
-
<Card title="Retryable" icon="rotate">
|
|
28
|
-
Failed steps automatically retry with configurable backoff strategies.
|
|
29
|
-
</Card>
|
|
30
|
-
<Card title="Cached" icon="database">
|
|
31
|
-
Completed step results are cached and replayed during workflow resumption.
|
|
32
|
-
</Card>
|
|
33
|
-
<Card title="Distributed" icon="server">
|
|
34
|
-
Steps execute on Celery workers, distributing load across your cluster.
|
|
35
|
-
</Card>
|
|
36
|
-
</CardGroup>
|
|
37
|
-
|
|
38
|
-
## Creating Steps
|
|
39
|
-
|
|
40
|
-
<Tabs>
|
|
41
|
-
<Tab title="Decorator">
|
|
42
|
-
```python
|
|
43
|
-
from pyworkflow import step
|
|
44
|
-
|
|
45
|
-
@step()
|
|
46
|
-
async def fetch_user(user_id: str):
|
|
47
|
-
async with httpx.AsyncClient() as client:
|
|
48
|
-
response = await client.get(f"/api/users/{user_id}")
|
|
49
|
-
return response.json()
|
|
50
|
-
```
|
|
51
|
-
</Tab>
|
|
52
|
-
<Tab title="Class">
|
|
53
|
-
```python
|
|
54
|
-
from pyworkflow import Step
|
|
55
|
-
|
|
56
|
-
class FetchUserStep(Step):
|
|
57
|
-
async def execute(self, user_id: str):
|
|
58
|
-
async with httpx.AsyncClient() as client:
|
|
59
|
-
response = await client.get(f"/api/users/{user_id}")
|
|
60
|
-
return response.json()
|
|
61
|
-
|
|
62
|
-
# Usage in workflow
|
|
63
|
-
user = await FetchUserStep()(user_id)
|
|
64
|
-
```
|
|
65
|
-
</Tab>
|
|
66
|
-
</Tabs>
|
|
67
|
-
|
|
68
|
-
### Configuration Options
|
|
69
|
-
|
|
70
|
-
<Tabs>
|
|
71
|
-
<Tab title="Decorator">
|
|
72
|
-
```python
|
|
73
|
-
@step(
|
|
74
|
-
name="fetch_user_data", # Custom step name
|
|
75
|
-
max_retries=5, # Retry up to 5 times
|
|
76
|
-
retry_delay="exponential", # Exponential backoff
|
|
77
|
-
timeout="30s" # Step timeout
|
|
78
|
-
)
|
|
79
|
-
async def fetch_user(user_id: str):
|
|
80
|
-
pass
|
|
81
|
-
```
|
|
82
|
-
</Tab>
|
|
83
|
-
<Tab title="Class">
|
|
84
|
-
```python
|
|
85
|
-
class FetchUserStep(Step):
|
|
86
|
-
name = "fetch_user_data"
|
|
87
|
-
max_retries = 5
|
|
88
|
-
retry_delay = "exponential"
|
|
89
|
-
timeout = "30s"
|
|
90
|
-
|
|
91
|
-
async def execute(self, user_id: str):
|
|
92
|
-
pass
|
|
93
|
-
```
|
|
94
|
-
</Tab>
|
|
95
|
-
</Tabs>
|
|
96
|
-
|
|
97
|
-
| Option | Type | Default | Description |
|
|
98
|
-
|--------|------|---------|-------------|
|
|
99
|
-
| `name` | `str` | Function/class name | Unique identifier for the step |
|
|
100
|
-
| `max_retries` | `int` | `3` | Maximum retry attempts |
|
|
101
|
-
| `retry_delay` | `str` | `"fixed"` | Retry strategy: `"fixed"`, `"exponential"`, or duration |
|
|
102
|
-
| `timeout` | `str` | `"60s"` | Maximum step execution time |
|
|
103
|
-
|
|
104
|
-
## Retry Strategies
|
|
105
|
-
|
|
106
|
-
### Fixed Delay
|
|
107
|
-
|
|
108
|
-
Retry with a constant delay between attempts:
|
|
109
|
-
|
|
110
|
-
```python
|
|
111
|
-
@step(max_retries=3, retry_delay="30s")
|
|
112
|
-
async def call_api():
|
|
113
|
-
# Retries at: 30s, 60s, 90s
|
|
114
|
-
pass
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### Exponential Backoff
|
|
118
|
-
|
|
119
|
-
Retry with increasing delays (recommended for external APIs):
|
|
120
|
-
|
|
121
|
-
```python
|
|
122
|
-
@step(max_retries=5, retry_delay="exponential")
|
|
123
|
-
async def call_external_api():
|
|
124
|
-
# Retries at: 1s, 2s, 4s, 8s, 16s
|
|
125
|
-
pass
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### Custom Delay in Error
|
|
129
|
-
|
|
130
|
-
Specify retry delay when raising an error:
|
|
131
|
-
|
|
132
|
-
```python
|
|
133
|
-
from pyworkflow import step, RetryableError
|
|
134
|
-
|
|
135
|
-
@step(max_retries=3)
|
|
136
|
-
async def rate_limited_api():
|
|
137
|
-
response = await call_api()
|
|
138
|
-
|
|
139
|
-
if response.status_code == 429:
|
|
140
|
-
retry_after = response.headers.get("Retry-After", "60")
|
|
141
|
-
raise RetryableError(
|
|
142
|
-
"Rate limited",
|
|
143
|
-
retry_after=f"{retry_after}s"
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
return response.json()
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
## Error Handling
|
|
150
|
-
|
|
151
|
-
### RetryableError
|
|
152
|
-
|
|
153
|
-
Use for transient failures that should be retried:
|
|
154
|
-
|
|
155
|
-
```python
|
|
156
|
-
from pyworkflow import step, RetryableError
|
|
157
|
-
|
|
158
|
-
@step(max_retries=3)
|
|
159
|
-
async def fetch_data():
|
|
160
|
-
try:
|
|
161
|
-
return await external_api.get_data()
|
|
162
|
-
except ConnectionError:
|
|
163
|
-
raise RetryableError("Connection failed")
|
|
164
|
-
except TimeoutError:
|
|
165
|
-
raise RetryableError("Request timed out", retry_after="10s")
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### FatalError
|
|
169
|
-
|
|
170
|
-
Use for permanent failures that should stop the workflow:
|
|
171
|
-
|
|
172
|
-
```python
|
|
173
|
-
from pyworkflow import step, FatalError
|
|
174
|
-
|
|
175
|
-
@step()
|
|
176
|
-
async def validate_input(data: dict):
|
|
177
|
-
if "email" not in data:
|
|
178
|
-
raise FatalError("Email is required")
|
|
179
|
-
|
|
180
|
-
if not is_valid_email(data["email"]):
|
|
181
|
-
raise FatalError(f"Invalid email: {data['email']}")
|
|
182
|
-
|
|
183
|
-
return data
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### Error Flow
|
|
187
|
-
|
|
188
|
-
```
|
|
189
|
-
Step Execution
|
|
190
|
-
│
|
|
191
|
-
├─── Success ───────────────────────> Return Result
|
|
192
|
-
│
|
|
193
|
-
└─── Exception
|
|
194
|
-
│
|
|
195
|
-
├─── FatalError ────────────> Workflow Failed
|
|
196
|
-
│
|
|
197
|
-
└─── RetryableError / Other
|
|
198
|
-
│
|
|
199
|
-
├─── Retries Left ──> Wait & Retry
|
|
200
|
-
│
|
|
201
|
-
└─── No Retries ────> Workflow Failed
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
## Step Results and Caching
|
|
205
|
-
|
|
206
|
-
When a workflow resumes after suspension, completed steps are not re-executed. Instead, their cached results are returned:
|
|
207
|
-
|
|
208
|
-
```python
|
|
209
|
-
@workflow()
|
|
210
|
-
async def my_workflow():
|
|
211
|
-
# First run: executes the step
|
|
212
|
-
# After resume: returns cached result
|
|
213
|
-
user = await fetch_user("user_123")
|
|
214
|
-
|
|
215
|
-
await sleep("1h")
|
|
216
|
-
|
|
217
|
-
# After 1 hour, workflow resumes
|
|
218
|
-
# fetch_user is NOT called again - cached result is used
|
|
219
|
-
await send_email(user["email"], "Hello!")
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
<Warning>
|
|
223
|
-
Step results must be serializable. PyWorkflow supports common Python types (dict, list, str, int, datetime, etc.) and uses cloudpickle for complex objects.
|
|
224
|
-
</Warning>
|
|
225
|
-
|
|
226
|
-
## Parallel Step Execution
|
|
227
|
-
|
|
228
|
-
Execute multiple steps concurrently using `asyncio.gather()`:
|
|
229
|
-
|
|
230
|
-
```python
|
|
231
|
-
import asyncio
|
|
232
|
-
from pyworkflow import workflow, step
|
|
233
|
-
|
|
234
|
-
@step()
|
|
235
|
-
async def fetch_user(user_id: str):
|
|
236
|
-
return await api.get_user(user_id)
|
|
237
|
-
|
|
238
|
-
@step()
|
|
239
|
-
async def fetch_orders(user_id: str):
|
|
240
|
-
return await api.get_orders(user_id)
|
|
241
|
-
|
|
242
|
-
@step()
|
|
243
|
-
async def fetch_preferences(user_id: str):
|
|
244
|
-
return await api.get_preferences(user_id)
|
|
245
|
-
|
|
246
|
-
@workflow()
|
|
247
|
-
async def load_dashboard(user_id: str):
|
|
248
|
-
# All three steps run in parallel
|
|
249
|
-
user, orders, preferences = await asyncio.gather(
|
|
250
|
-
fetch_user(user_id),
|
|
251
|
-
fetch_orders(user_id),
|
|
252
|
-
fetch_preferences(user_id)
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
return {
|
|
256
|
-
"user": user,
|
|
257
|
-
"orders": orders,
|
|
258
|
-
"preferences": preferences
|
|
259
|
-
}
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
## Best Practices
|
|
263
|
-
|
|
264
|
-
<AccordionGroup>
|
|
265
|
-
<Accordion title="Keep steps small and focused">
|
|
266
|
-
Each step should do one thing well. This makes retries more efficient - if a step fails, only that specific operation is retried.
|
|
267
|
-
</Accordion>
|
|
268
|
-
|
|
269
|
-
<Accordion title="Make steps idempotent">
|
|
270
|
-
Steps may be retried, so ensure they can be safely re-executed. Use idempotency keys when calling external APIs.
|
|
271
|
-
|
|
272
|
-
```python
|
|
273
|
-
@step()
|
|
274
|
-
async def charge_payment(order_id: str, amount: float):
|
|
275
|
-
# Use order_id as idempotency key
|
|
276
|
-
return await stripe.charges.create(
|
|
277
|
-
amount=amount,
|
|
278
|
-
idempotency_key=f"charge-{order_id}"
|
|
279
|
-
)
|
|
280
|
-
```
|
|
281
|
-
</Accordion>
|
|
282
|
-
|
|
283
|
-
<Accordion title="Use appropriate timeouts">
|
|
284
|
-
Set timeouts based on expected execution time. External API calls should have shorter timeouts than data processing steps.
|
|
285
|
-
</Accordion>
|
|
286
|
-
|
|
287
|
-
<Accordion title="Handle errors explicitly">
|
|
288
|
-
Distinguish between retryable and fatal errors. Don't retry errors that will never succeed.
|
|
289
|
-
</Accordion>
|
|
290
|
-
</AccordionGroup>
|
|
291
|
-
|
|
292
|
-
## Next Steps
|
|
293
|
-
|
|
294
|
-
<CardGroup cols={2}>
|
|
295
|
-
<Card title="Events" icon="timeline" href="/concepts/events">
|
|
296
|
-
Learn how event sourcing enables durability and replay.
|
|
297
|
-
</Card>
|
|
298
|
-
<Card title="Error Handling" icon="shield-check" href="/guides/error-handling">
|
|
299
|
-
Deep dive into retry strategies and error handling.
|
|
300
|
-
</Card>
|
|
301
|
-
</CardGroup>
|