pyworkflow-engine 0.1.7__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.
- dashboard/backend/app/__init__.py +1 -0
- dashboard/backend/app/config.py +32 -0
- dashboard/backend/app/controllers/__init__.py +6 -0
- dashboard/backend/app/controllers/run_controller.py +86 -0
- dashboard/backend/app/controllers/workflow_controller.py +33 -0
- dashboard/backend/app/dependencies/__init__.py +5 -0
- dashboard/backend/app/dependencies/storage.py +50 -0
- dashboard/backend/app/repositories/__init__.py +6 -0
- dashboard/backend/app/repositories/run_repository.py +80 -0
- dashboard/backend/app/repositories/workflow_repository.py +27 -0
- dashboard/backend/app/rest/__init__.py +8 -0
- dashboard/backend/app/rest/v1/__init__.py +12 -0
- dashboard/backend/app/rest/v1/health.py +33 -0
- dashboard/backend/app/rest/v1/runs.py +133 -0
- dashboard/backend/app/rest/v1/workflows.py +41 -0
- dashboard/backend/app/schemas/__init__.py +23 -0
- dashboard/backend/app/schemas/common.py +16 -0
- dashboard/backend/app/schemas/event.py +24 -0
- dashboard/backend/app/schemas/hook.py +25 -0
- dashboard/backend/app/schemas/run.py +54 -0
- dashboard/backend/app/schemas/step.py +28 -0
- dashboard/backend/app/schemas/workflow.py +31 -0
- dashboard/backend/app/server.py +87 -0
- dashboard/backend/app/services/__init__.py +6 -0
- dashboard/backend/app/services/run_service.py +240 -0
- dashboard/backend/app/services/workflow_service.py +155 -0
- dashboard/backend/main.py +18 -0
- docs/concepts/cancellation.mdx +362 -0
- docs/concepts/continue-as-new.mdx +434 -0
- docs/concepts/events.mdx +266 -0
- docs/concepts/fault-tolerance.mdx +370 -0
- docs/concepts/hooks.mdx +552 -0
- docs/concepts/limitations.mdx +167 -0
- docs/concepts/schedules.mdx +775 -0
- docs/concepts/sleep.mdx +312 -0
- docs/concepts/steps.mdx +301 -0
- docs/concepts/workflows.mdx +255 -0
- docs/guides/cli.mdx +942 -0
- docs/guides/configuration.mdx +560 -0
- docs/introduction.mdx +155 -0
- docs/quickstart.mdx +279 -0
- examples/__init__.py +1 -0
- examples/celery/__init__.py +1 -0
- examples/celery/durable/docker-compose.yml +55 -0
- examples/celery/durable/pyworkflow.config.yaml +12 -0
- examples/celery/durable/workflows/__init__.py +122 -0
- examples/celery/durable/workflows/basic.py +87 -0
- examples/celery/durable/workflows/batch_processing.py +102 -0
- examples/celery/durable/workflows/cancellation.py +273 -0
- examples/celery/durable/workflows/child_workflow_patterns.py +240 -0
- examples/celery/durable/workflows/child_workflows.py +202 -0
- examples/celery/durable/workflows/continue_as_new.py +260 -0
- examples/celery/durable/workflows/fault_tolerance.py +210 -0
- examples/celery/durable/workflows/hooks.py +211 -0
- examples/celery/durable/workflows/idempotency.py +112 -0
- examples/celery/durable/workflows/long_running.py +99 -0
- examples/celery/durable/workflows/retries.py +101 -0
- examples/celery/durable/workflows/schedules.py +209 -0
- examples/celery/transient/01_basic_workflow.py +91 -0
- examples/celery/transient/02_fault_tolerance.py +257 -0
- examples/celery/transient/__init__.py +20 -0
- examples/celery/transient/pyworkflow.config.yaml +25 -0
- examples/local/__init__.py +1 -0
- examples/local/durable/01_basic_workflow.py +94 -0
- examples/local/durable/02_file_storage.py +132 -0
- examples/local/durable/03_retries.py +169 -0
- examples/local/durable/04_long_running.py +119 -0
- examples/local/durable/05_event_log.py +145 -0
- examples/local/durable/06_idempotency.py +148 -0
- examples/local/durable/07_hooks.py +334 -0
- examples/local/durable/08_cancellation.py +233 -0
- examples/local/durable/09_child_workflows.py +198 -0
- examples/local/durable/10_child_workflow_patterns.py +265 -0
- examples/local/durable/11_continue_as_new.py +249 -0
- examples/local/durable/12_schedules.py +198 -0
- examples/local/durable/__init__.py +1 -0
- examples/local/transient/01_quick_tasks.py +87 -0
- examples/local/transient/02_retries.py +130 -0
- examples/local/transient/03_sleep.py +141 -0
- examples/local/transient/__init__.py +1 -0
- pyworkflow/__init__.py +256 -0
- pyworkflow/aws/__init__.py +68 -0
- pyworkflow/aws/context.py +234 -0
- pyworkflow/aws/handler.py +184 -0
- pyworkflow/aws/testing.py +310 -0
- pyworkflow/celery/__init__.py +41 -0
- pyworkflow/celery/app.py +198 -0
- pyworkflow/celery/scheduler.py +315 -0
- pyworkflow/celery/tasks.py +1746 -0
- pyworkflow/cli/__init__.py +132 -0
- pyworkflow/cli/__main__.py +6 -0
- pyworkflow/cli/commands/__init__.py +1 -0
- pyworkflow/cli/commands/hooks.py +640 -0
- pyworkflow/cli/commands/quickstart.py +495 -0
- pyworkflow/cli/commands/runs.py +773 -0
- pyworkflow/cli/commands/scheduler.py +130 -0
- pyworkflow/cli/commands/schedules.py +794 -0
- pyworkflow/cli/commands/setup.py +703 -0
- pyworkflow/cli/commands/worker.py +413 -0
- pyworkflow/cli/commands/workflows.py +1257 -0
- pyworkflow/cli/output/__init__.py +1 -0
- pyworkflow/cli/output/formatters.py +321 -0
- pyworkflow/cli/output/styles.py +121 -0
- pyworkflow/cli/utils/__init__.py +1 -0
- pyworkflow/cli/utils/async_helpers.py +30 -0
- pyworkflow/cli/utils/config.py +130 -0
- pyworkflow/cli/utils/config_generator.py +344 -0
- pyworkflow/cli/utils/discovery.py +53 -0
- pyworkflow/cli/utils/docker_manager.py +651 -0
- pyworkflow/cli/utils/interactive.py +364 -0
- pyworkflow/cli/utils/storage.py +115 -0
- pyworkflow/config.py +329 -0
- pyworkflow/context/__init__.py +63 -0
- pyworkflow/context/aws.py +230 -0
- pyworkflow/context/base.py +416 -0
- pyworkflow/context/local.py +930 -0
- pyworkflow/context/mock.py +381 -0
- pyworkflow/core/__init__.py +0 -0
- pyworkflow/core/exceptions.py +353 -0
- pyworkflow/core/registry.py +313 -0
- pyworkflow/core/scheduled.py +328 -0
- pyworkflow/core/step.py +494 -0
- pyworkflow/core/workflow.py +294 -0
- pyworkflow/discovery.py +248 -0
- pyworkflow/engine/__init__.py +0 -0
- pyworkflow/engine/events.py +879 -0
- pyworkflow/engine/executor.py +682 -0
- pyworkflow/engine/replay.py +273 -0
- pyworkflow/observability/__init__.py +19 -0
- pyworkflow/observability/logging.py +234 -0
- pyworkflow/primitives/__init__.py +33 -0
- pyworkflow/primitives/child_handle.py +174 -0
- pyworkflow/primitives/child_workflow.py +372 -0
- pyworkflow/primitives/continue_as_new.py +101 -0
- pyworkflow/primitives/define_hook.py +150 -0
- pyworkflow/primitives/hooks.py +97 -0
- pyworkflow/primitives/resume_hook.py +210 -0
- pyworkflow/primitives/schedule.py +545 -0
- pyworkflow/primitives/shield.py +96 -0
- pyworkflow/primitives/sleep.py +100 -0
- pyworkflow/runtime/__init__.py +21 -0
- pyworkflow/runtime/base.py +179 -0
- pyworkflow/runtime/celery.py +310 -0
- pyworkflow/runtime/factory.py +101 -0
- pyworkflow/runtime/local.py +706 -0
- pyworkflow/scheduler/__init__.py +9 -0
- pyworkflow/scheduler/local.py +248 -0
- pyworkflow/serialization/__init__.py +0 -0
- pyworkflow/serialization/decoder.py +146 -0
- pyworkflow/serialization/encoder.py +162 -0
- pyworkflow/storage/__init__.py +54 -0
- pyworkflow/storage/base.py +612 -0
- pyworkflow/storage/config.py +185 -0
- pyworkflow/storage/dynamodb.py +1315 -0
- pyworkflow/storage/file.py +827 -0
- pyworkflow/storage/memory.py +549 -0
- pyworkflow/storage/postgres.py +1161 -0
- pyworkflow/storage/schemas.py +486 -0
- pyworkflow/storage/sqlite.py +1136 -0
- pyworkflow/utils/__init__.py +0 -0
- pyworkflow/utils/duration.py +177 -0
- pyworkflow/utils/schedule.py +391 -0
- pyworkflow_engine-0.1.7.dist-info/METADATA +687 -0
- pyworkflow_engine-0.1.7.dist-info/RECORD +196 -0
- pyworkflow_engine-0.1.7.dist-info/WHEEL +5 -0
- pyworkflow_engine-0.1.7.dist-info/entry_points.txt +2 -0
- pyworkflow_engine-0.1.7.dist-info/licenses/LICENSE +21 -0
- pyworkflow_engine-0.1.7.dist-info/top_level.txt +5 -0
- tests/examples/__init__.py +0 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_cancellation.py +330 -0
- tests/integration/test_child_workflows.py +439 -0
- tests/integration/test_continue_as_new.py +428 -0
- tests/integration/test_dynamodb_storage.py +1146 -0
- tests/integration/test_fault_tolerance.py +369 -0
- tests/integration/test_schedule_storage.py +484 -0
- tests/unit/__init__.py +0 -0
- tests/unit/backends/__init__.py +1 -0
- tests/unit/backends/test_dynamodb_storage.py +1554 -0
- tests/unit/backends/test_postgres_storage.py +1281 -0
- tests/unit/backends/test_sqlite_storage.py +1460 -0
- tests/unit/conftest.py +41 -0
- tests/unit/test_cancellation.py +364 -0
- tests/unit/test_child_workflows.py +680 -0
- tests/unit/test_continue_as_new.py +441 -0
- tests/unit/test_event_limits.py +316 -0
- tests/unit/test_executor.py +320 -0
- tests/unit/test_fault_tolerance.py +334 -0
- tests/unit/test_hooks.py +495 -0
- tests/unit/test_registry.py +261 -0
- tests/unit/test_replay.py +420 -0
- tests/unit/test_schedule_schemas.py +285 -0
- tests/unit/test_schedule_utils.py +286 -0
- tests/unit/test_scheduled_workflow.py +274 -0
- tests/unit/test_step.py +353 -0
- tests/unit/test_workflow.py +243 -0
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyworkflow-engine
|
|
3
|
+
Version: 0.1.7
|
|
4
|
+
Summary: A Python implementation of durable, event-sourced workflows inspired by Vercel Workflow
|
|
5
|
+
Author: PyWorkflow Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://docs.pyworkflow.dev
|
|
8
|
+
Project-URL: Documentation, https://docs.pyworkflow.dev
|
|
9
|
+
Project-URL: Repository, https://github.com/QualityUnit/pyworkflow
|
|
10
|
+
Project-URL: Issues, https://github.com/QualityUnit/pyworkflow/issues
|
|
11
|
+
Keywords: workflow,durable,event-sourcing,celery,async,orchestration
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
21
|
+
Classifier: Framework :: Celery
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: celery<6.0.0,>=5.3.0
|
|
26
|
+
Requires-Dist: cloudpickle>=3.0.0
|
|
27
|
+
Requires-Dist: pydantic<3.0.0,>=2.0.0
|
|
28
|
+
Requires-Dist: loguru>=0.7.0
|
|
29
|
+
Requires-Dist: click>=8.0.0
|
|
30
|
+
Requires-Dist: inquirerpy>=0.3.4; python_version < "4.0"
|
|
31
|
+
Requires-Dist: httpx>=0.25.0
|
|
32
|
+
Requires-Dist: python-dateutil>=2.8.0
|
|
33
|
+
Requires-Dist: filelock>=3.12.0
|
|
34
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
35
|
+
Requires-Dist: croniter>=2.0.0
|
|
36
|
+
Provides-Extra: redis
|
|
37
|
+
Requires-Dist: redis>=5.0.0; extra == "redis"
|
|
38
|
+
Requires-Dist: celery[redis]<6.0.0,>=5.3.0; extra == "redis"
|
|
39
|
+
Provides-Extra: sqlite
|
|
40
|
+
Requires-Dist: aiosqlite>=0.19.0; extra == "sqlite"
|
|
41
|
+
Provides-Extra: postgres
|
|
42
|
+
Requires-Dist: asyncpg>=0.29.0; extra == "postgres"
|
|
43
|
+
Provides-Extra: aws
|
|
44
|
+
Requires-Dist: aws-durable-execution-sdk-python>=0.1.0; extra == "aws"
|
|
45
|
+
Provides-Extra: dynamodb
|
|
46
|
+
Requires-Dist: aiobotocore>=2.5.0; extra == "dynamodb"
|
|
47
|
+
Provides-Extra: all
|
|
48
|
+
Requires-Dist: redis>=5.0.0; extra == "all"
|
|
49
|
+
Requires-Dist: celery[redis]<6.0.0,>=5.3.0; extra == "all"
|
|
50
|
+
Requires-Dist: aiosqlite>=0.19.0; extra == "all"
|
|
51
|
+
Requires-Dist: asyncpg>=0.29.0; extra == "all"
|
|
52
|
+
Requires-Dist: aws-durable-execution-sdk-python>=0.1.0; extra == "all"
|
|
53
|
+
Provides-Extra: dev
|
|
54
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
55
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
56
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
57
|
+
Requires-Dist: pytest-celery>=0.0.0; extra == "dev"
|
|
58
|
+
Requires-Dist: pytest-mock>=3.12.0; extra == "dev"
|
|
59
|
+
Requires-Dist: moto[dynamodb]>=5.0.0; extra == "dev"
|
|
60
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
61
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
62
|
+
Requires-Dist: mypy>=1.7.0; extra == "dev"
|
|
63
|
+
Requires-Dist: pre-commit>=3.5.0; extra == "dev"
|
|
64
|
+
Requires-Dist: types-redis>=4.6.0; extra == "dev"
|
|
65
|
+
Requires-Dist: types-python-dateutil>=2.8.0; extra == "dev"
|
|
66
|
+
Requires-Dist: types-PyYAML>=6.0.0; extra == "dev"
|
|
67
|
+
Requires-Dist: flower>=2.0.0; extra == "dev"
|
|
68
|
+
Requires-Dist: redis>=5.0.0; extra == "dev"
|
|
69
|
+
Requires-Dist: celery[redis]<6.0.0,>=5.3.0; extra == "dev"
|
|
70
|
+
Requires-Dist: aiosqlite>=0.19.0; extra == "dev"
|
|
71
|
+
Requires-Dist: asyncpg>=0.29.0; extra == "dev"
|
|
72
|
+
Dynamic: license-file
|
|
73
|
+
|
|
74
|
+
# PyWorkflow
|
|
75
|
+
|
|
76
|
+
**Distributed, durable workflow orchestration for Python**
|
|
77
|
+
|
|
78
|
+
Build long-running, fault-tolerant workflows with automatic retry, sleep/delay capabilities, and complete observability. PyWorkflow uses event sourcing and Celery for production-grade distributed execution.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## What is PyWorkflow?
|
|
83
|
+
|
|
84
|
+
PyWorkflow is a workflow orchestration framework that enables you to build complex, long-running business processes as simple Python code. It handles the hard parts of distributed systems: fault tolerance, automatic retries, state management, and horizontal scaling.
|
|
85
|
+
|
|
86
|
+
### Key Features
|
|
87
|
+
|
|
88
|
+
- **Distributed by Default**: All workflows execute across Celery workers for horizontal scaling
|
|
89
|
+
- **Durable Execution**: Event sourcing ensures workflows can recover from any failure
|
|
90
|
+
- **Auto Recovery**: Automatic workflow resumption after worker crashes with event replay
|
|
91
|
+
- **Time Travel**: Sleep for minutes, hours, or days with automatic resumption
|
|
92
|
+
- **Fault Tolerant**: Automatic retries with configurable backoff strategies
|
|
93
|
+
- **Zero-Resource Suspension**: Workflows suspend without holding resources during sleep
|
|
94
|
+
- **Production Ready**: Built on battle-tested Celery and Redis
|
|
95
|
+
- **Fully Typed**: Complete type hints and Pydantic validation
|
|
96
|
+
- **Observable**: Structured logging with workflow context
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Quick Start
|
|
101
|
+
|
|
102
|
+
### Installation
|
|
103
|
+
|
|
104
|
+
**Basic installation** (File and Memory storage backends):
|
|
105
|
+
```bash
|
|
106
|
+
pip install pyworkflow-engine
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**With optional storage backends:**
|
|
110
|
+
```bash
|
|
111
|
+
# Redis backend (includes Redis as Celery broker)
|
|
112
|
+
pip install pyworkflow-engine[redis]
|
|
113
|
+
|
|
114
|
+
# SQLite backend
|
|
115
|
+
pip install pyworkflow-engine[sqlite]
|
|
116
|
+
|
|
117
|
+
# PostgreSQL backend
|
|
118
|
+
pip install pyworkflow-engine[postgres]
|
|
119
|
+
|
|
120
|
+
# All storage backends
|
|
121
|
+
pip install pyworkflow-engine[all]
|
|
122
|
+
|
|
123
|
+
# Development (includes all backends + dev tools)
|
|
124
|
+
pip install pyworkflow-engine[dev]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Prerequisites
|
|
128
|
+
|
|
129
|
+
**For distributed execution** (recommended for production):
|
|
130
|
+
|
|
131
|
+
PyWorkflow uses Celery for distributed task execution. You need a message broker:
|
|
132
|
+
|
|
133
|
+
**Option 1: Redis (recommended)**
|
|
134
|
+
```bash
|
|
135
|
+
# Install Redis support
|
|
136
|
+
pip install pyworkflow-engine[redis]
|
|
137
|
+
|
|
138
|
+
# Start Redis
|
|
139
|
+
docker run -d -p 6379:6379 redis:7-alpine
|
|
140
|
+
|
|
141
|
+
# Start Celery worker(s)
|
|
142
|
+
celery -A pyworkflow.celery.app worker --loglevel=info
|
|
143
|
+
|
|
144
|
+
# Start Celery Beat (for automatic sleep resumption)
|
|
145
|
+
celery -A pyworkflow.celery.app beat --loglevel=info
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Or use the CLI to set up Docker infrastructure:
|
|
149
|
+
```bash
|
|
150
|
+
pyworkflow setup
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Option 2: Other brokers** (RabbitMQ, etc.)
|
|
154
|
+
```bash
|
|
155
|
+
# Celery supports multiple brokers
|
|
156
|
+
# Configure via environment: CELERY_BROKER_URL=amqp://localhost
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**For local development/testing:**
|
|
160
|
+
```bash
|
|
161
|
+
# No broker needed - use in-process execution
|
|
162
|
+
pyworkflow configure --runtime local
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
See [DISTRIBUTED.md](DISTRIBUTED.md) for complete deployment guide.
|
|
166
|
+
|
|
167
|
+
### Your First Workflow
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from pyworkflow import workflow, step, start, sleep
|
|
171
|
+
|
|
172
|
+
@step()
|
|
173
|
+
async def send_welcome_email(user_id: str):
|
|
174
|
+
# This runs on any available Celery worker
|
|
175
|
+
print(f"Sending welcome email to user {user_id}")
|
|
176
|
+
return f"Email sent to {user_id}"
|
|
177
|
+
|
|
178
|
+
@step()
|
|
179
|
+
async def send_tips_email(user_id: str):
|
|
180
|
+
print(f"Sending tips email to user {user_id}")
|
|
181
|
+
return f"Tips sent to {user_id}"
|
|
182
|
+
|
|
183
|
+
@workflow()
|
|
184
|
+
async def onboarding_workflow(user_id: str):
|
|
185
|
+
# Send welcome email immediately
|
|
186
|
+
await send_welcome_email(user_id)
|
|
187
|
+
|
|
188
|
+
# Sleep for 1 day - workflow suspends, zero resources used
|
|
189
|
+
await sleep("1d")
|
|
190
|
+
|
|
191
|
+
# Automatically resumes after 1 day!
|
|
192
|
+
await send_tips_email(user_id)
|
|
193
|
+
|
|
194
|
+
return "Onboarding complete"
|
|
195
|
+
|
|
196
|
+
# Start workflow - executes across Celery workers
|
|
197
|
+
run_id = start(onboarding_workflow, user_id="user_123")
|
|
198
|
+
print(f"Workflow started: {run_id}")
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**What happens:**
|
|
202
|
+
1. Workflow starts on a Celery worker
|
|
203
|
+
2. Welcome email is sent
|
|
204
|
+
3. Workflow suspends after calling `sleep("1d")`
|
|
205
|
+
4. Worker is freed to handle other tasks
|
|
206
|
+
5. After 1 day, Celery Beat automatically schedules resumption
|
|
207
|
+
6. Workflow resumes on any available worker
|
|
208
|
+
7. Tips email is sent
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Core Concepts
|
|
213
|
+
|
|
214
|
+
### Workflows
|
|
215
|
+
|
|
216
|
+
Workflows are the top-level orchestration functions. They coordinate steps, handle business logic, and can sleep for extended periods.
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from pyworkflow import workflow, start
|
|
220
|
+
|
|
221
|
+
@workflow(name="process_order", max_duration="1h")
|
|
222
|
+
async def process_order(order_id: str):
|
|
223
|
+
"""
|
|
224
|
+
Process a customer order.
|
|
225
|
+
|
|
226
|
+
This workflow:
|
|
227
|
+
- Validates the order
|
|
228
|
+
- Processes payment
|
|
229
|
+
- Creates shipment
|
|
230
|
+
- Sends confirmation
|
|
231
|
+
"""
|
|
232
|
+
order = await validate_order(order_id)
|
|
233
|
+
payment = await process_payment(order)
|
|
234
|
+
shipment = await create_shipment(order)
|
|
235
|
+
await send_confirmation(order)
|
|
236
|
+
|
|
237
|
+
return {"order_id": order_id, "status": "completed"}
|
|
238
|
+
|
|
239
|
+
# Start the workflow
|
|
240
|
+
run_id = start(process_order, order_id="ORD-123")
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Steps
|
|
244
|
+
|
|
245
|
+
Steps are the building blocks of workflows. Each step is an isolated, retryable unit of work that runs on Celery workers.
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from pyworkflow import step, RetryableError, FatalError
|
|
249
|
+
|
|
250
|
+
@step(max_retries=5, retry_delay="exponential")
|
|
251
|
+
async def call_external_api(url: str):
|
|
252
|
+
"""
|
|
253
|
+
Call external API with automatic retry.
|
|
254
|
+
|
|
255
|
+
Retries up to 5 times with exponential backoff if it fails.
|
|
256
|
+
"""
|
|
257
|
+
try:
|
|
258
|
+
response = await httpx.get(url)
|
|
259
|
+
|
|
260
|
+
if response.status_code == 404:
|
|
261
|
+
# Don't retry - resource doesn't exist
|
|
262
|
+
raise FatalError("Resource not found")
|
|
263
|
+
|
|
264
|
+
if response.status_code >= 500:
|
|
265
|
+
# Retry - server error
|
|
266
|
+
raise RetryableError("Server error", retry_after="30s")
|
|
267
|
+
|
|
268
|
+
return response.json()
|
|
269
|
+
except httpx.NetworkError:
|
|
270
|
+
# Retry with exponential backoff
|
|
271
|
+
raise RetryableError("Network error")
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Sleep and Delays
|
|
275
|
+
|
|
276
|
+
Workflows can sleep for any duration. During sleep, the workflow suspends and consumes zero resources.
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
from pyworkflow import workflow, sleep
|
|
280
|
+
|
|
281
|
+
@workflow()
|
|
282
|
+
async def scheduled_reminder(user_id: str):
|
|
283
|
+
# Send immediate reminder
|
|
284
|
+
await send_reminder(user_id, "immediate")
|
|
285
|
+
|
|
286
|
+
# Sleep for 1 hour
|
|
287
|
+
await sleep("1h")
|
|
288
|
+
await send_reminder(user_id, "1 hour later")
|
|
289
|
+
|
|
290
|
+
# Sleep for 1 day
|
|
291
|
+
await sleep("1d")
|
|
292
|
+
await send_reminder(user_id, "1 day later")
|
|
293
|
+
|
|
294
|
+
# Sleep for 1 week
|
|
295
|
+
await sleep("7d")
|
|
296
|
+
await send_reminder(user_id, "1 week later")
|
|
297
|
+
|
|
298
|
+
return "All reminders sent"
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Supported formats:**
|
|
302
|
+
- Duration strings: `"5s"`, `"10m"`, `"2h"`, `"3d"`
|
|
303
|
+
- Timedelta: `timedelta(hours=2, minutes=30)`
|
|
304
|
+
- Datetime: `datetime(2025, 12, 25, 9, 0, 0)`
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Architecture
|
|
309
|
+
|
|
310
|
+
### Event-Sourced Execution
|
|
311
|
+
|
|
312
|
+
PyWorkflow uses event sourcing to achieve durable, fault-tolerant execution:
|
|
313
|
+
|
|
314
|
+
1. **All state changes are recorded as events** in an append-only log
|
|
315
|
+
2. **Deterministic replay** enables workflow resumption from any point
|
|
316
|
+
3. **Complete audit trail** of everything that happened in the workflow
|
|
317
|
+
|
|
318
|
+
**Event Types** (16 total):
|
|
319
|
+
- Workflow: `started`, `completed`, `failed`, `suspended`, `resumed`
|
|
320
|
+
- Step: `started`, `completed`, `failed`, `retrying`
|
|
321
|
+
- Sleep: `created`, `completed`
|
|
322
|
+
- Logging: `info`, `warning`, `error`, `debug`
|
|
323
|
+
|
|
324
|
+
### Distributed Execution
|
|
325
|
+
|
|
326
|
+
```
|
|
327
|
+
┌─────────────────────────────────────────────────────┐
|
|
328
|
+
│ Your Application │
|
|
329
|
+
│ │
|
|
330
|
+
│ start(my_workflow, args) │
|
|
331
|
+
│ │ │
|
|
332
|
+
└─────────┼───────────────────────────────────────────┘
|
|
333
|
+
│
|
|
334
|
+
▼
|
|
335
|
+
┌─────────┐
|
|
336
|
+
│ Redis │ ◄──── Message Broker
|
|
337
|
+
└─────────┘
|
|
338
|
+
│
|
|
339
|
+
├──────┬──────┬──────┐
|
|
340
|
+
▼ ▼ ▼ ▼
|
|
341
|
+
┌──────┐ ┌──────┐ ┌──────┐
|
|
342
|
+
│Worker│ │Worker│ │Worker│ ◄──── Horizontal Scaling
|
|
343
|
+
└──────┘ └──────┘ └──────┘
|
|
344
|
+
│ │ │
|
|
345
|
+
└──────┴──────┘
|
|
346
|
+
│
|
|
347
|
+
▼
|
|
348
|
+
┌──────────┐
|
|
349
|
+
│ Storage │ ◄──── Event Log (File/Redis/PostgreSQL)
|
|
350
|
+
└──────────┘
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Storage Backends
|
|
354
|
+
|
|
355
|
+
PyWorkflow supports pluggable storage backends:
|
|
356
|
+
|
|
357
|
+
| Backend | Status | Installation | Use Case |
|
|
358
|
+
|---------|--------|--------------|----------|
|
|
359
|
+
| **File** | ✅ Complete | Included | Development, single-machine |
|
|
360
|
+
| **Memory** | ✅ Complete | Included | Testing, ephemeral workflows |
|
|
361
|
+
| **SQLite** | ✅ Complete | `pip install pyworkflow-engine[sqlite]` | Embedded, local persistence |
|
|
362
|
+
| **PostgreSQL** | ✅ Complete | `pip install pyworkflow-engine[postgres]` | Production, enterprise |
|
|
363
|
+
| **Redis** | 📋 Planned | `pip install pyworkflow-engine[redis]` | High-performance, distributed |
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## Advanced Features
|
|
368
|
+
|
|
369
|
+
### Parallel Execution
|
|
370
|
+
|
|
371
|
+
Use Python's native `asyncio.gather()` for parallel step execution:
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
import asyncio
|
|
375
|
+
from pyworkflow import workflow, step
|
|
376
|
+
|
|
377
|
+
@step()
|
|
378
|
+
async def fetch_user(user_id: str):
|
|
379
|
+
# Fetch user data
|
|
380
|
+
return {"id": user_id, "name": "Alice"}
|
|
381
|
+
|
|
382
|
+
@step()
|
|
383
|
+
async def fetch_orders(user_id: str):
|
|
384
|
+
# Fetch user orders
|
|
385
|
+
return [{"id": "ORD-1"}, {"id": "ORD-2"}]
|
|
386
|
+
|
|
387
|
+
@step()
|
|
388
|
+
async def fetch_recommendations(user_id: str):
|
|
389
|
+
# Fetch recommendations
|
|
390
|
+
return ["Product A", "Product B"]
|
|
391
|
+
|
|
392
|
+
@workflow()
|
|
393
|
+
async def dashboard_data(user_id: str):
|
|
394
|
+
# Fetch all data in parallel
|
|
395
|
+
user, orders, recommendations = await asyncio.gather(
|
|
396
|
+
fetch_user(user_id),
|
|
397
|
+
fetch_orders(user_id),
|
|
398
|
+
fetch_recommendations(user_id)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
"user": user,
|
|
403
|
+
"orders": orders,
|
|
404
|
+
"recommendations": recommendations
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Error Handling
|
|
409
|
+
|
|
410
|
+
PyWorkflow distinguishes between retriable and fatal errors:
|
|
411
|
+
|
|
412
|
+
```python
|
|
413
|
+
from pyworkflow import FatalError, RetryableError, step
|
|
414
|
+
|
|
415
|
+
@step(max_retries=3, retry_delay="exponential")
|
|
416
|
+
async def process_payment(amount: float):
|
|
417
|
+
try:
|
|
418
|
+
# Attempt payment
|
|
419
|
+
result = await payment_gateway.charge(amount)
|
|
420
|
+
return result
|
|
421
|
+
except InsufficientFundsError:
|
|
422
|
+
# Don't retry - user doesn't have enough money
|
|
423
|
+
raise FatalError("Insufficient funds")
|
|
424
|
+
except PaymentGatewayTimeoutError:
|
|
425
|
+
# Retry - temporary issue
|
|
426
|
+
raise RetryableError("Gateway timeout", retry_after="10s")
|
|
427
|
+
except Exception as e:
|
|
428
|
+
# Unknown error - retry with backoff
|
|
429
|
+
raise RetryableError(f"Unknown error: {e}")
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**Retry strategies:**
|
|
433
|
+
- `retry_delay="fixed"` - Fixed delay between retries (default: 60s)
|
|
434
|
+
- `retry_delay="exponential"` - Exponential backoff (1s, 2s, 4s, 8s, ...)
|
|
435
|
+
- `retry_delay="5s"` - Custom fixed delay
|
|
436
|
+
|
|
437
|
+
### Auto Recovery
|
|
438
|
+
|
|
439
|
+
Workflows automatically recover from worker crashes:
|
|
440
|
+
|
|
441
|
+
```python
|
|
442
|
+
from pyworkflow import workflow, step, sleep
|
|
443
|
+
|
|
444
|
+
@workflow(
|
|
445
|
+
recover_on_worker_loss=True, # Enable recovery (default for durable)
|
|
446
|
+
max_recovery_attempts=5, # Max recovery attempts
|
|
447
|
+
)
|
|
448
|
+
async def resilient_workflow(data_id: str):
|
|
449
|
+
data = await fetch_data(data_id) # Completed steps are skipped on recovery
|
|
450
|
+
await sleep("10m") # Sleep state is preserved
|
|
451
|
+
return await process_data(data) # Continues from here after crash
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
**What happens on worker crash:**
|
|
455
|
+
1. Celery detects worker loss, requeues task
|
|
456
|
+
2. New worker picks up the task
|
|
457
|
+
3. Events are replayed to restore state
|
|
458
|
+
4. Workflow resumes from last checkpoint
|
|
459
|
+
|
|
460
|
+
Configure globally:
|
|
461
|
+
```python
|
|
462
|
+
import pyworkflow
|
|
463
|
+
|
|
464
|
+
pyworkflow.configure(
|
|
465
|
+
default_recover_on_worker_loss=True,
|
|
466
|
+
default_max_recovery_attempts=3,
|
|
467
|
+
)
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Or via config file:
|
|
471
|
+
```yaml
|
|
472
|
+
# pyworkflow.config.yaml
|
|
473
|
+
recovery:
|
|
474
|
+
recover_on_worker_loss: true
|
|
475
|
+
max_recovery_attempts: 3
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Idempotency
|
|
479
|
+
|
|
480
|
+
Prevent duplicate workflow executions with idempotency keys:
|
|
481
|
+
|
|
482
|
+
```python
|
|
483
|
+
from pyworkflow import start
|
|
484
|
+
|
|
485
|
+
# Same idempotency key = same workflow
|
|
486
|
+
run_id_1 = start(
|
|
487
|
+
process_order,
|
|
488
|
+
order_id="ORD-123",
|
|
489
|
+
idempotency_key="order-ORD-123"
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# This will return the same run_id, not start a new workflow
|
|
493
|
+
run_id_2 = start(
|
|
494
|
+
process_order,
|
|
495
|
+
order_id="ORD-123",
|
|
496
|
+
idempotency_key="order-ORD-123"
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
assert run_id_1 == run_id_2 # True!
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Observability
|
|
503
|
+
|
|
504
|
+
PyWorkflow includes structured logging with automatic context:
|
|
505
|
+
|
|
506
|
+
```python
|
|
507
|
+
from pyworkflow import configure_logging
|
|
508
|
+
|
|
509
|
+
# Configure logging
|
|
510
|
+
configure_logging(
|
|
511
|
+
level="INFO",
|
|
512
|
+
log_file="workflow.log",
|
|
513
|
+
json_logs=True, # JSON format for production
|
|
514
|
+
show_context=True # Include run_id, step_id, etc.
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
# Logs automatically include:
|
|
518
|
+
# - run_id: Workflow execution ID
|
|
519
|
+
# - workflow_name: Name of the workflow
|
|
520
|
+
# - step_id: Current step ID
|
|
521
|
+
# - step_name: Name of the step
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## Testing
|
|
527
|
+
|
|
528
|
+
PyWorkflow uses a unified API for testing with local execution:
|
|
529
|
+
|
|
530
|
+
```python
|
|
531
|
+
import pytest
|
|
532
|
+
from pyworkflow import workflow, step, start, configure, reset_config
|
|
533
|
+
from pyworkflow.storage.memory import InMemoryStorageBackend
|
|
534
|
+
|
|
535
|
+
@step()
|
|
536
|
+
async def my_step(x: int):
|
|
537
|
+
return x * 2
|
|
538
|
+
|
|
539
|
+
@workflow()
|
|
540
|
+
async def my_workflow(x: int):
|
|
541
|
+
result = await my_step(x)
|
|
542
|
+
return result + 1
|
|
543
|
+
|
|
544
|
+
@pytest.fixture(autouse=True)
|
|
545
|
+
def setup_storage():
|
|
546
|
+
reset_config()
|
|
547
|
+
storage = InMemoryStorageBackend()
|
|
548
|
+
configure(storage=storage, default_durable=True)
|
|
549
|
+
yield storage
|
|
550
|
+
reset_config()
|
|
551
|
+
|
|
552
|
+
@pytest.mark.asyncio
|
|
553
|
+
async def test_my_workflow(setup_storage):
|
|
554
|
+
storage = setup_storage
|
|
555
|
+
run_id = await start(my_workflow, 5)
|
|
556
|
+
|
|
557
|
+
# Get workflow result
|
|
558
|
+
run = await storage.get_run(run_id)
|
|
559
|
+
assert run.status.value == "completed"
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## Production Deployment
|
|
565
|
+
|
|
566
|
+
### Docker Compose
|
|
567
|
+
|
|
568
|
+
```yaml
|
|
569
|
+
version: '3.8'
|
|
570
|
+
|
|
571
|
+
services:
|
|
572
|
+
redis:
|
|
573
|
+
image: redis:7-alpine
|
|
574
|
+
ports:
|
|
575
|
+
- "6379:6379"
|
|
576
|
+
|
|
577
|
+
worker:
|
|
578
|
+
build: .
|
|
579
|
+
command: celery -A pyworkflow.celery.app worker --loglevel=info
|
|
580
|
+
depends_on:
|
|
581
|
+
- redis
|
|
582
|
+
deploy:
|
|
583
|
+
replicas: 3 # Run 3 workers
|
|
584
|
+
|
|
585
|
+
beat:
|
|
586
|
+
build: .
|
|
587
|
+
command: celery -A pyworkflow.celery.app beat --loglevel=info
|
|
588
|
+
depends_on:
|
|
589
|
+
- redis
|
|
590
|
+
|
|
591
|
+
flower:
|
|
592
|
+
build: .
|
|
593
|
+
command: celery -A pyworkflow.celery.app flower --port=5555
|
|
594
|
+
ports:
|
|
595
|
+
- "5555:5555"
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
Start everything using the CLI:
|
|
599
|
+
```bash
|
|
600
|
+
pyworkflow setup
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
See [DISTRIBUTED.md](DISTRIBUTED.md) for complete deployment guide with Kubernetes.
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
607
|
+
## Examples
|
|
608
|
+
|
|
609
|
+
Check out the [examples/](examples/) directory for complete working examples:
|
|
610
|
+
|
|
611
|
+
- **[basic_workflow.py](examples/functional/basic_workflow.py)** - Complete example with retries, errors, and sleep
|
|
612
|
+
- **[distributed_example.py](examples/functional/distributed_example.py)** - Multi-worker distributed execution example
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
## Project Status
|
|
617
|
+
|
|
618
|
+
✅ **Status**: Production Ready (v1.0)
|
|
619
|
+
|
|
620
|
+
**Completed Features**:
|
|
621
|
+
- ✅ Core workflow and step execution
|
|
622
|
+
- ✅ Event sourcing with 16 event types
|
|
623
|
+
- ✅ Distributed execution via Celery
|
|
624
|
+
- ✅ Sleep primitive with automatic resumption
|
|
625
|
+
- ✅ Error handling and retry strategies
|
|
626
|
+
- ✅ File storage backend
|
|
627
|
+
- ✅ Structured logging
|
|
628
|
+
- ✅ Comprehensive test coverage (68 tests)
|
|
629
|
+
- ✅ Docker Compose deployment
|
|
630
|
+
- ✅ Idempotency support
|
|
631
|
+
|
|
632
|
+
**Next Milestones**:
|
|
633
|
+
- 📋 Redis storage backend
|
|
634
|
+
- 📋 PostgreSQL storage backend
|
|
635
|
+
- 📋 Webhook integration
|
|
636
|
+
- 📋 Web UI for monitoring
|
|
637
|
+
- 📋 CLI management tools
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## Contributing
|
|
642
|
+
|
|
643
|
+
Contributions are welcome!
|
|
644
|
+
|
|
645
|
+
### Development Setup
|
|
646
|
+
|
|
647
|
+
```bash
|
|
648
|
+
# Clone repository
|
|
649
|
+
git clone https://github.com/QualityUnit/pyworkflow
|
|
650
|
+
cd pyworkflow
|
|
651
|
+
|
|
652
|
+
# Install with Poetry
|
|
653
|
+
poetry install
|
|
654
|
+
|
|
655
|
+
# Run tests
|
|
656
|
+
poetry run pytest
|
|
657
|
+
|
|
658
|
+
# Format code
|
|
659
|
+
poetry run black pyworkflow tests
|
|
660
|
+
poetry run ruff check pyworkflow tests
|
|
661
|
+
|
|
662
|
+
# Type checking
|
|
663
|
+
poetry run mypy pyworkflow
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
## Documentation
|
|
669
|
+
|
|
670
|
+
- **[Distributed Deployment Guide](DISTRIBUTED.md)** - Production deployment with Docker Compose and Kubernetes
|
|
671
|
+
- [Examples](examples/) - Working examples and patterns
|
|
672
|
+
- [API Reference](docs/api-reference.md) (Coming soon)
|
|
673
|
+
- [Architecture Guide](docs/architecture.md) (Coming soon)
|
|
674
|
+
|
|
675
|
+
---
|
|
676
|
+
|
|
677
|
+
## License
|
|
678
|
+
|
|
679
|
+
Apache License 2.0 - See [LICENSE](LICENSE) file for details.
|
|
680
|
+
|
|
681
|
+
---
|
|
682
|
+
|
|
683
|
+
## Links
|
|
684
|
+
|
|
685
|
+
- **Documentation**: https://docs.pyworkflow.dev
|
|
686
|
+
- **GitHub**: https://github.com/QualityUnit/pyworkflow
|
|
687
|
+
- **Issues**: https://github.com/QualityUnit/pyworkflow/issues
|