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/hooks.mdx
DELETED
|
@@ -1,552 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 'Hooks'
|
|
3
|
-
description: 'Pause workflows and wait for external events like approvals, webhooks, or user actions'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## What are Hooks?
|
|
7
|
-
|
|
8
|
-
Hooks allow workflows to **suspend execution and wait for external events**. Unlike `sleep()` which resumes after a time duration, hooks wait for an external system or user to provide data before continuing.
|
|
9
|
-
|
|
10
|
-
```python
|
|
11
|
-
from pydantic import BaseModel
|
|
12
|
-
from pyworkflow import define_hook, workflow
|
|
13
|
-
|
|
14
|
-
class ApprovalPayload(BaseModel):
|
|
15
|
-
approved: bool
|
|
16
|
-
reviewer: str
|
|
17
|
-
comments: str | None = None
|
|
18
|
-
|
|
19
|
-
# Define a typed hook with Pydantic validation
|
|
20
|
-
approval_hook = define_hook("manager_approval", ApprovalPayload)
|
|
21
|
-
|
|
22
|
-
@workflow()
|
|
23
|
-
async def approval_workflow(order_id: str):
|
|
24
|
-
order = await prepare_order(order_id)
|
|
25
|
-
|
|
26
|
-
# Workflow suspends here - waits for external approval
|
|
27
|
-
approval: ApprovalPayload = await approval_hook(timeout="7d")
|
|
28
|
-
|
|
29
|
-
if approval.approved:
|
|
30
|
-
return await fulfill_order(order)
|
|
31
|
-
else:
|
|
32
|
-
return await cancel_order(order, approval.comments)
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## How It Works
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
Workflow Execution
|
|
39
|
-
│
|
|
40
|
-
▼
|
|
41
|
-
┌───────────────┐
|
|
42
|
-
│ Execute Steps │
|
|
43
|
-
└───────┬───────┘
|
|
44
|
-
│
|
|
45
|
-
▼
|
|
46
|
-
┌───────────────────┐
|
|
47
|
-
│ await hook(...) │
|
|
48
|
-
└───────┬───────────┘
|
|
49
|
-
│
|
|
50
|
-
├─── 1. Generate token (run_id:hook_id)
|
|
51
|
-
│
|
|
52
|
-
├─── 2. Record hook_created event
|
|
53
|
-
│
|
|
54
|
-
├─── 3. Store hook with schema (for CLI)
|
|
55
|
-
│
|
|
56
|
-
├─── 4. Call on_created callback with token
|
|
57
|
-
│
|
|
58
|
-
├─── 5. Raise SuspensionSignal
|
|
59
|
-
│
|
|
60
|
-
└─── 6. Worker is freed
|
|
61
|
-
│
|
|
62
|
-
│ ... waiting for external event ...
|
|
63
|
-
│
|
|
64
|
-
▼
|
|
65
|
-
┌─────────────────────┐
|
|
66
|
-
│ External system │
|
|
67
|
-
│ calls resume_hook() │
|
|
68
|
-
│ with token + payload│
|
|
69
|
-
└────────┬────────────┘
|
|
70
|
-
│
|
|
71
|
-
├─── 7. Validate payload (if typed)
|
|
72
|
-
│
|
|
73
|
-
├─── 8. Record hook_received event
|
|
74
|
-
│
|
|
75
|
-
└─── 9. Schedule workflow resumption
|
|
76
|
-
│
|
|
77
|
-
▼
|
|
78
|
-
┌───────────────┐
|
|
79
|
-
│ Replay events │
|
|
80
|
-
│ Resume work │
|
|
81
|
-
└───────────────┘
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
## Choosing a Hook Type
|
|
85
|
-
|
|
86
|
-
PyWorkflow offers two ways to define hooks:
|
|
87
|
-
|
|
88
|
-
| Feature | TypedHook (Recommended) | Simple Hook |
|
|
89
|
-
|---------|-------------------------|-------------|
|
|
90
|
-
| Type safety | Full Pydantic validation | Dict with any keys |
|
|
91
|
-
| CLI prompts | Interactive field-by-field | JSON payload only |
|
|
92
|
-
| IDE support | Autocomplete, type hints | None |
|
|
93
|
-
| Schema stored | Yes (enables CLI features) | No |
|
|
94
|
-
| Best for | Production workflows | Quick prototypes |
|
|
95
|
-
|
|
96
|
-
<Note>
|
|
97
|
-
We recommend **TypedHook** for all production workflows. The Pydantic schema enables CLI interactive prompts, payload validation, and better IDE support.
|
|
98
|
-
</Note>
|
|
99
|
-
|
|
100
|
-
## TypedHook with Pydantic (Recommended)
|
|
101
|
-
|
|
102
|
-
TypedHook combines hook suspension with Pydantic validation for type-safe payloads.
|
|
103
|
-
|
|
104
|
-
### Defining a Typed Hook
|
|
105
|
-
|
|
106
|
-
```python
|
|
107
|
-
from pydantic import BaseModel
|
|
108
|
-
from typing import Optional
|
|
109
|
-
from pyworkflow import define_hook
|
|
110
|
-
|
|
111
|
-
# 1. Define the payload schema
|
|
112
|
-
class ApprovalPayload(BaseModel):
|
|
113
|
-
approved: bool
|
|
114
|
-
reviewer: str
|
|
115
|
-
comments: Optional[str] = None
|
|
116
|
-
|
|
117
|
-
# 2. Create the typed hook
|
|
118
|
-
approval_hook = define_hook("manager_approval", ApprovalPayload)
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### Using in a Workflow
|
|
122
|
-
|
|
123
|
-
```python
|
|
124
|
-
from pyworkflow import workflow
|
|
125
|
-
|
|
126
|
-
@workflow()
|
|
127
|
-
async def order_approval(order_id: str):
|
|
128
|
-
order = await prepare_order(order_id)
|
|
129
|
-
|
|
130
|
-
async def on_hook_created(token: str):
|
|
131
|
-
print(f"Approval needed! Token: {token}")
|
|
132
|
-
# Send token to external system, email, Slack, etc.
|
|
133
|
-
|
|
134
|
-
# Wait for approval - returns validated ApprovalPayload
|
|
135
|
-
approval: ApprovalPayload = await approval_hook(
|
|
136
|
-
timeout="7d",
|
|
137
|
-
on_created=on_hook_created,
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
# Type-safe access to payload fields
|
|
141
|
-
if approval.approved:
|
|
142
|
-
print(f"Approved by {approval.reviewer}")
|
|
143
|
-
return await fulfill_order(order)
|
|
144
|
-
else:
|
|
145
|
-
return await cancel_order(order, approval.comments or "Rejected")
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### CLI Interactive Resume
|
|
149
|
-
|
|
150
|
-
When you run `pyworkflow hooks resume`, the CLI uses the stored Pydantic schema to prompt for each field:
|
|
151
|
-
|
|
152
|
-
```bash
|
|
153
|
-
$ pyworkflow hooks resume
|
|
154
|
-
|
|
155
|
-
? Select a pending hook to resume:
|
|
156
|
-
> manager_approval (run_abc123:hook_manager_approval_1)
|
|
157
|
-
|
|
158
|
-
? approved (bool): yes
|
|
159
|
-
? reviewer (str): admin@example.com
|
|
160
|
-
? comments (str, optional): Looks good!
|
|
161
|
-
|
|
162
|
-
Hook resumed successfully.
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
## Simple Hook
|
|
166
|
-
|
|
167
|
-
For quick prototyping or when you don't need typed payloads, use the `hook()` function directly.
|
|
168
|
-
|
|
169
|
-
```python
|
|
170
|
-
from pyworkflow import hook, workflow
|
|
171
|
-
|
|
172
|
-
@workflow()
|
|
173
|
-
async def simple_approval(order_id: str):
|
|
174
|
-
order = await prepare_order(order_id)
|
|
175
|
-
|
|
176
|
-
async def on_hook_created(token: str):
|
|
177
|
-
print(f"Resume with: pyworkflow hooks resume {token}")
|
|
178
|
-
|
|
179
|
-
# Wait for any payload (untyped dict)
|
|
180
|
-
approval = await hook(
|
|
181
|
-
"approval",
|
|
182
|
-
timeout="24h",
|
|
183
|
-
on_created=on_hook_created,
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
# Payload is a dict - no type safety
|
|
187
|
-
if approval.get("approved"):
|
|
188
|
-
return await fulfill_order(order)
|
|
189
|
-
else:
|
|
190
|
-
return await cancel_order(order, approval.get("reason", "Rejected"))
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
<Warning>
|
|
194
|
-
Simple hooks don't enable CLI interactive prompts. You must provide the full JSON payload when resuming.
|
|
195
|
-
</Warning>
|
|
196
|
-
|
|
197
|
-
## Resuming Hooks
|
|
198
|
-
|
|
199
|
-
### Using the CLI
|
|
200
|
-
|
|
201
|
-
```bash
|
|
202
|
-
# List all pending hooks
|
|
203
|
-
pyworkflow hooks list --status pending
|
|
204
|
-
|
|
205
|
-
# View hook details
|
|
206
|
-
pyworkflow hooks info <token>
|
|
207
|
-
|
|
208
|
-
# Resume interactively (TypedHook only - prompts for fields)
|
|
209
|
-
pyworkflow hooks resume
|
|
210
|
-
|
|
211
|
-
# Resume with explicit payload
|
|
212
|
-
pyworkflow hooks resume <token> --payload '{"approved": true, "reviewer": "admin"}'
|
|
213
|
-
|
|
214
|
-
# Resume with payload from file
|
|
215
|
-
pyworkflow hooks resume <token> --payload-file approval.json
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
### Programmatically
|
|
219
|
-
|
|
220
|
-
```python
|
|
221
|
-
from pyworkflow import resume_hook
|
|
222
|
-
|
|
223
|
-
# Resume a hook with payload
|
|
224
|
-
result = await resume_hook(
|
|
225
|
-
token="run_abc123:hook_manager_approval_1",
|
|
226
|
-
payload={"approved": True, "reviewer": "admin@example.com"},
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
if result.success:
|
|
230
|
-
print(f"Hook resumed, workflow continuing")
|
|
231
|
-
else:
|
|
232
|
-
print(f"Failed: {result.error}")
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### Token Format
|
|
236
|
-
|
|
237
|
-
Tokens are composite identifiers in the format `run_id:hook_id`:
|
|
238
|
-
|
|
239
|
-
```
|
|
240
|
-
run_abc123:hook_manager_approval_1
|
|
241
|
-
└────┬────┘ └─────────┬──────────┘
|
|
242
|
-
run_id hook_id
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
This self-describing format allows the system to route the resumption to the correct workflow run.
|
|
246
|
-
|
|
247
|
-
## Configuration Options
|
|
248
|
-
|
|
249
|
-
| Option | Type | Description |
|
|
250
|
-
|--------|------|-------------|
|
|
251
|
-
| `timeout` | `str \| int` | Maximum wait time before hook expires. String format (`"24h"`, `"7d"`) or seconds. |
|
|
252
|
-
| `on_created` | `Callable[[str], Awaitable[None]]` | Async callback invoked with token when hook is created. |
|
|
253
|
-
| `payload_schema` | `Type[BaseModel]` | Pydantic model for payload validation (used by simple `hook()` only). |
|
|
254
|
-
|
|
255
|
-
### Timeout Examples
|
|
256
|
-
|
|
257
|
-
```python
|
|
258
|
-
# String format (recommended)
|
|
259
|
-
await approval_hook(timeout="24h") # 24 hours
|
|
260
|
-
await approval_hook(timeout="7d") # 7 days
|
|
261
|
-
await approval_hook(timeout="1h30m") # 1 hour 30 minutes
|
|
262
|
-
|
|
263
|
-
# Integer (seconds)
|
|
264
|
-
await approval_hook(timeout=3600) # 1 hour
|
|
265
|
-
|
|
266
|
-
# No timeout (waits indefinitely)
|
|
267
|
-
await approval_hook()
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
### on_created Callback
|
|
271
|
-
|
|
272
|
-
The `on_created` callback is called with the hook token when the hook is created. Use it to notify external systems:
|
|
273
|
-
|
|
274
|
-
```python
|
|
275
|
-
async def notify_approver(token: str):
|
|
276
|
-
await send_slack_message(
|
|
277
|
-
channel="#approvals",
|
|
278
|
-
text=f"Approval needed! Resume: `pyworkflow hooks resume {token}`"
|
|
279
|
-
)
|
|
280
|
-
await send_email(
|
|
281
|
-
to="manager@example.com",
|
|
282
|
-
subject="Approval Required",
|
|
283
|
-
body=f"Click to approve: https://app.example.com/approve?token={token}"
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
approval = await approval_hook(
|
|
287
|
-
timeout="7d",
|
|
288
|
-
on_created=notify_approver,
|
|
289
|
-
)
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
## Use Cases
|
|
293
|
-
|
|
294
|
-
### Human Approval Workflows
|
|
295
|
-
|
|
296
|
-
```python
|
|
297
|
-
@workflow()
|
|
298
|
-
async def expense_approval(expense_id: str, amount: float):
|
|
299
|
-
expense = await prepare_expense(expense_id)
|
|
300
|
-
|
|
301
|
-
if amount > 1000:
|
|
302
|
-
# High-value expenses need manager approval
|
|
303
|
-
approval = await manager_approval_hook(timeout="48h")
|
|
304
|
-
if not approval.approved:
|
|
305
|
-
return await reject_expense(expense, approval.reason)
|
|
306
|
-
|
|
307
|
-
return await process_expense(expense)
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
### Multi-Level Approval
|
|
311
|
-
|
|
312
|
-
```python
|
|
313
|
-
@workflow()
|
|
314
|
-
async def contract_approval(contract_id: str):
|
|
315
|
-
contract = await prepare_contract(contract_id)
|
|
316
|
-
|
|
317
|
-
# Level 1: Manager approval
|
|
318
|
-
manager = await manager_hook(timeout="24h")
|
|
319
|
-
if not manager.approved:
|
|
320
|
-
return await reject_contract(contract, "Manager rejected")
|
|
321
|
-
|
|
322
|
-
# Level 2: Legal review
|
|
323
|
-
legal = await legal_hook(timeout="72h")
|
|
324
|
-
if not legal.approved:
|
|
325
|
-
return await reject_contract(contract, "Legal rejected")
|
|
326
|
-
|
|
327
|
-
# Level 3: Executive sign-off
|
|
328
|
-
executive = await executive_hook(timeout="7d")
|
|
329
|
-
if not executive.approved:
|
|
330
|
-
return await reject_contract(contract, "Executive rejected")
|
|
331
|
-
|
|
332
|
-
return await execute_contract(contract)
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
### Webhook Integration
|
|
336
|
-
|
|
337
|
-
```python
|
|
338
|
-
@workflow()
|
|
339
|
-
async def payment_processing(order_id: str):
|
|
340
|
-
order = await create_order(order_id)
|
|
341
|
-
|
|
342
|
-
async def setup_webhook(token: str):
|
|
343
|
-
# Register token with payment provider
|
|
344
|
-
await payment_provider.register_webhook(
|
|
345
|
-
callback_url=f"https://api.example.com/hooks/{token}",
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
# Wait for payment confirmation from external provider
|
|
349
|
-
payment = await payment_confirmation_hook(
|
|
350
|
-
timeout="1h",
|
|
351
|
-
on_created=setup_webhook,
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
if payment.status == "completed":
|
|
355
|
-
return await fulfill_order(order)
|
|
356
|
-
else:
|
|
357
|
-
return await cancel_order(order, payment.error)
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
### User Confirmation
|
|
361
|
-
|
|
362
|
-
```python
|
|
363
|
-
@workflow()
|
|
364
|
-
async def account_deletion(user_id: str):
|
|
365
|
-
user = await get_user(user_id)
|
|
366
|
-
|
|
367
|
-
async def send_confirmation_email(token: str):
|
|
368
|
-
await send_email(
|
|
369
|
-
to=user.email,
|
|
370
|
-
subject="Confirm Account Deletion",
|
|
371
|
-
body=f"Click to confirm: https://app.example.com/confirm?token={token}"
|
|
372
|
-
)
|
|
373
|
-
|
|
374
|
-
# Wait for user to confirm deletion
|
|
375
|
-
confirmation = await confirmation_hook(
|
|
376
|
-
timeout="24h",
|
|
377
|
-
on_created=send_confirmation_email,
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
if confirmation.confirmed:
|
|
381
|
-
return await delete_user(user_id)
|
|
382
|
-
else:
|
|
383
|
-
return {"status": "cancelled", "user_id": user_id}
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
## Best Practices
|
|
387
|
-
|
|
388
|
-
<AccordionGroup>
|
|
389
|
-
<Accordion title="Use TypedHook for production workflows">
|
|
390
|
-
TypedHook provides validation, IDE support, and CLI interactive prompts:
|
|
391
|
-
|
|
392
|
-
```python
|
|
393
|
-
# Good: Typed hook with Pydantic
|
|
394
|
-
class ApprovalPayload(BaseModel):
|
|
395
|
-
approved: bool
|
|
396
|
-
reviewer: str
|
|
397
|
-
|
|
398
|
-
approval_hook = define_hook("approval", ApprovalPayload)
|
|
399
|
-
|
|
400
|
-
# Avoid in production: Untyped hook
|
|
401
|
-
approval = await hook("approval")
|
|
402
|
-
```
|
|
403
|
-
</Accordion>
|
|
404
|
-
|
|
405
|
-
<Accordion title="Set appropriate timeouts">
|
|
406
|
-
Always set timeouts to prevent workflows from waiting indefinitely:
|
|
407
|
-
|
|
408
|
-
```python
|
|
409
|
-
# Good: Set reasonable timeout
|
|
410
|
-
await approval_hook(timeout="7d")
|
|
411
|
-
|
|
412
|
-
# Avoid: No timeout (waits forever)
|
|
413
|
-
await approval_hook()
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
Handle expiration gracefully in your workflow logic.
|
|
417
|
-
</Accordion>
|
|
418
|
-
|
|
419
|
-
<Accordion title="Use descriptive hook names">
|
|
420
|
-
Hook names should clearly indicate their purpose:
|
|
421
|
-
|
|
422
|
-
```python
|
|
423
|
-
# Good: Clear purpose
|
|
424
|
-
manager_approval_hook = define_hook("manager_approval", ApprovalPayload)
|
|
425
|
-
payment_confirmation_hook = define_hook("payment_confirmation", PaymentPayload)
|
|
426
|
-
|
|
427
|
-
# Avoid: Vague names
|
|
428
|
-
hook1 = define_hook("hook1", Payload)
|
|
429
|
-
```
|
|
430
|
-
</Accordion>
|
|
431
|
-
|
|
432
|
-
<Accordion title="Notify external systems via on_created">
|
|
433
|
-
Use the `on_created` callback to trigger notifications:
|
|
434
|
-
|
|
435
|
-
```python
|
|
436
|
-
async def notify_systems(token: str):
|
|
437
|
-
await send_slack_notification(token)
|
|
438
|
-
await send_email_notification(token)
|
|
439
|
-
await register_webhook(token)
|
|
440
|
-
|
|
441
|
-
approval = await approval_hook(
|
|
442
|
-
timeout="24h",
|
|
443
|
-
on_created=notify_systems,
|
|
444
|
-
)
|
|
445
|
-
```
|
|
446
|
-
</Accordion>
|
|
447
|
-
|
|
448
|
-
<Accordion title="Handle hook errors gracefully">
|
|
449
|
-
Catch and handle hook-related exceptions:
|
|
450
|
-
|
|
451
|
-
```python
|
|
452
|
-
from pyworkflow import HookExpiredError, HookNotFoundError
|
|
453
|
-
|
|
454
|
-
try:
|
|
455
|
-
result = await resume_hook(token, payload)
|
|
456
|
-
except HookExpiredError:
|
|
457
|
-
print("Hook has expired")
|
|
458
|
-
except HookNotFoundError:
|
|
459
|
-
print("Hook not found")
|
|
460
|
-
```
|
|
461
|
-
</Accordion>
|
|
462
|
-
</AccordionGroup>
|
|
463
|
-
|
|
464
|
-
## Testing Hooks
|
|
465
|
-
|
|
466
|
-
Use `MockContext` to test workflows with hooks without actual suspension:
|
|
467
|
-
|
|
468
|
-
```python
|
|
469
|
-
import asyncio
|
|
470
|
-
from pyworkflow import MockContext, set_context
|
|
471
|
-
|
|
472
|
-
def test_approval_workflow():
|
|
473
|
-
# Create mock context with predefined hook responses
|
|
474
|
-
ctx = MockContext(
|
|
475
|
-
run_id="test_run",
|
|
476
|
-
workflow_name="approval_workflow",
|
|
477
|
-
mock_hooks={
|
|
478
|
-
"manager_approval": {
|
|
479
|
-
"approved": True,
|
|
480
|
-
"reviewer": "test@example.com",
|
|
481
|
-
"comments": "Approved in test",
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
)
|
|
485
|
-
set_context(ctx)
|
|
486
|
-
|
|
487
|
-
try:
|
|
488
|
-
# Run workflow - hook returns mock response immediately
|
|
489
|
-
result = asyncio.run(approval_workflow("order-123"))
|
|
490
|
-
|
|
491
|
-
# Verify workflow completed correctly
|
|
492
|
-
assert result["status"] == "fulfilled"
|
|
493
|
-
assert ctx.hook_count == 1
|
|
494
|
-
finally:
|
|
495
|
-
set_context(None)
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
### Testing Multiple Hooks
|
|
499
|
-
|
|
500
|
-
```python
|
|
501
|
-
def test_multi_approval_workflow():
|
|
502
|
-
ctx = MockContext(
|
|
503
|
-
run_id="test_run",
|
|
504
|
-
workflow_name="multi_approval",
|
|
505
|
-
mock_hooks={
|
|
506
|
-
"manager_approval": {"approved": True, "approver": "manager"},
|
|
507
|
-
"finance_approval": {"approved": True, "approver": "finance"},
|
|
508
|
-
}
|
|
509
|
-
)
|
|
510
|
-
set_context(ctx)
|
|
511
|
-
|
|
512
|
-
try:
|
|
513
|
-
result = asyncio.run(multi_approval_workflow("order-123"))
|
|
514
|
-
assert result["status"] == "fulfilled"
|
|
515
|
-
assert ctx.hook_count == 2
|
|
516
|
-
finally:
|
|
517
|
-
set_context(None)
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
## Hook Events
|
|
521
|
-
|
|
522
|
-
Hooks generate events that are stored in the event log:
|
|
523
|
-
|
|
524
|
-
| Event Type | When | Data |
|
|
525
|
-
|------------|------|------|
|
|
526
|
-
| `hook.created` | Hook is awaited | hook_id, token, name, expires_at, schema |
|
|
527
|
-
| `hook.received` | Hook is resumed | hook_id, payload |
|
|
528
|
-
| `hook.expired` | Timeout reached | hook_id |
|
|
529
|
-
| `hook.disposed` | Hook cleaned up | hook_id |
|
|
530
|
-
|
|
531
|
-
View hook events for a run:
|
|
532
|
-
|
|
533
|
-
```bash
|
|
534
|
-
pyworkflow runs logs <run_id> --type hook
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
## Next Steps
|
|
538
|
-
|
|
539
|
-
<CardGroup cols={2}>
|
|
540
|
-
<Card title="Sleep" icon="clock" href="/concepts/sleep">
|
|
541
|
-
Pause workflows for time durations.
|
|
542
|
-
</Card>
|
|
543
|
-
<Card title="Workflows" icon="diagram-project" href="/concepts/workflows">
|
|
544
|
-
Learn about workflow orchestration.
|
|
545
|
-
</Card>
|
|
546
|
-
<Card title="CLI Guide" icon="terminal" href="/guides/cli">
|
|
547
|
-
Manage hooks with CLI commands.
|
|
548
|
-
</Card>
|
|
549
|
-
<Card title="Fault Tolerance" icon="shield" href="/concepts/fault-tolerance">
|
|
550
|
-
Auto-recovery from worker crashes.
|
|
551
|
-
</Card>
|
|
552
|
-
</CardGroup>
|
docs/concepts/limitations.mdx
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 'Limitations'
|
|
3
|
-
description: 'Built-in safeguards and limits to ensure stable workflow execution'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## Overview
|
|
7
|
-
|
|
8
|
-
PyWorkflow includes built-in safeguards to prevent runaway workflows from consuming excessive resources. These limits help ensure system stability and predictable behavior.
|
|
9
|
-
|
|
10
|
-
## Event History Limits
|
|
11
|
-
|
|
12
|
-
Since PyWorkflow uses event sourcing, every workflow action is recorded as an event. To prevent unbounded growth and memory issues, there are limits on the number of events a workflow can generate.
|
|
13
|
-
|
|
14
|
-
### Soft Limit (Warning)
|
|
15
|
-
|
|
16
|
-
| Setting | Default | Description |
|
|
17
|
-
|---------|---------|-------------|
|
|
18
|
-
| `event_soft_limit` | 10,000 | Logs a warning when reached |
|
|
19
|
-
| `event_warning_interval` | 100 | Logs warning every N events after soft limit |
|
|
20
|
-
|
|
21
|
-
When a workflow reaches 10,000 events, PyWorkflow logs a warning:
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
WARNING - Workflow approaching event limit: 10000/50000
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
After the soft limit, warnings continue every 100 events (10,100, 10,200, etc.) to alert you that the workflow is growing large.
|
|
28
|
-
|
|
29
|
-
### Hard Limit (Failure)
|
|
30
|
-
|
|
31
|
-
| Setting | Default | Description |
|
|
32
|
-
|---------|---------|-------------|
|
|
33
|
-
| `event_hard_limit` | 50,000 | Terminates workflow with failure |
|
|
34
|
-
|
|
35
|
-
When a workflow reaches 50,000 events, it is terminated with an `EventLimitExceededError`:
|
|
36
|
-
|
|
37
|
-
```python
|
|
38
|
-
from pyworkflow.core.exceptions import EventLimitExceededError
|
|
39
|
-
|
|
40
|
-
# This error is raised when hard limit is reached
|
|
41
|
-
# EventLimitExceededError: Workflow run_abc123 exceeded maximum event limit: 50000 >= 50000
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
<Warning>
|
|
45
|
-
The hard limit is a safety mechanism. If your workflow is hitting this limit, it likely indicates a design issue such as an infinite loop or processing too many items in a single workflow run.
|
|
46
|
-
</Warning>
|
|
47
|
-
|
|
48
|
-
## Why These Limits Exist
|
|
49
|
-
|
|
50
|
-
1. **Memory Protection**: Each event consumes memory. Unbounded event growth can exhaust system resources.
|
|
51
|
-
|
|
52
|
-
2. **Replay Performance**: When workflows resume, all events are replayed. Large event logs slow down resumption.
|
|
53
|
-
|
|
54
|
-
3. **Storage Costs**: Events are persisted to storage. Excessive events increase storage requirements.
|
|
55
|
-
|
|
56
|
-
4. **Bug Detection**: Hitting limits often indicates bugs like infinite loops or improper workflow design.
|
|
57
|
-
|
|
58
|
-
## Best Practices
|
|
59
|
-
|
|
60
|
-
<Tip>
|
|
61
|
-
**Design for bounded event counts:**
|
|
62
|
-
|
|
63
|
-
```python
|
|
64
|
-
# BAD: Processing millions of items in one workflow
|
|
65
|
-
@workflow()
|
|
66
|
-
async def process_all_orders():
|
|
67
|
-
orders = await get_all_orders() # Could be millions!
|
|
68
|
-
for order in orders:
|
|
69
|
-
await process_order(order) # Each creates events
|
|
70
|
-
|
|
71
|
-
# GOOD: Process in batches with separate workflow runs
|
|
72
|
-
@workflow()
|
|
73
|
-
async def process_order_batch(batch_ids: list[str]):
|
|
74
|
-
for order_id in batch_ids[:100]: # Bounded batch size
|
|
75
|
-
await process_order(order_id)
|
|
76
|
-
|
|
77
|
-
# Orchestrate batches externally
|
|
78
|
-
for batch in chunk_list(all_order_ids, 100):
|
|
79
|
-
await start(process_order_batch, batch)
|
|
80
|
-
```
|
|
81
|
-
</Tip>
|
|
82
|
-
|
|
83
|
-
## Configuring Limits
|
|
84
|
-
|
|
85
|
-
<Warning>
|
|
86
|
-
Modifying event limits is **not recommended**. The defaults are carefully chosen to balance flexibility with safety. Only change these if you fully understand the implications.
|
|
87
|
-
</Warning>
|
|
88
|
-
|
|
89
|
-
If you must change the limits:
|
|
90
|
-
|
|
91
|
-
```python
|
|
92
|
-
import pyworkflow
|
|
93
|
-
|
|
94
|
-
# This will emit a UserWarning - proceed with caution
|
|
95
|
-
pyworkflow.configure(
|
|
96
|
-
event_soft_limit=20_000, # Warning at 20K events
|
|
97
|
-
event_hard_limit=100_000, # Fail at 100K events
|
|
98
|
-
event_warning_interval=200, # Warn every 200 events after soft limit
|
|
99
|
-
)
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Configuration Options
|
|
103
|
-
|
|
104
|
-
| Option | Type | Default | Description |
|
|
105
|
-
|--------|------|---------|-------------|
|
|
106
|
-
| `event_soft_limit` | `int` | 10,000 | Event count to start logging warnings |
|
|
107
|
-
| `event_hard_limit` | `int` | 50,000 | Event count to terminate workflow |
|
|
108
|
-
| `event_warning_interval` | `int` | 100 | Events between warnings after soft limit |
|
|
109
|
-
|
|
110
|
-
## Transient Mode
|
|
111
|
-
|
|
112
|
-
Event limits only apply to **durable** workflows. Transient workflows (with `durable=False`) do not record events and are not subject to these limits.
|
|
113
|
-
|
|
114
|
-
```python
|
|
115
|
-
@workflow(durable=False)
|
|
116
|
-
async def quick_task():
|
|
117
|
-
# No event limits - events aren't recorded
|
|
118
|
-
for i in range(100_000):
|
|
119
|
-
await some_step(i)
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
<Note>
|
|
123
|
-
Transient workflows sacrifice durability for performance. They cannot be resumed after crashes or restarts.
|
|
124
|
-
</Note>
|
|
125
|
-
|
|
126
|
-
## Monitoring Event Counts
|
|
127
|
-
|
|
128
|
-
You can monitor event counts using the storage backend:
|
|
129
|
-
|
|
130
|
-
```python
|
|
131
|
-
from pyworkflow import get_storage
|
|
132
|
-
|
|
133
|
-
storage = get_storage()
|
|
134
|
-
events = await storage.get_events(run_id)
|
|
135
|
-
|
|
136
|
-
print(f"Event count: {len(events)}")
|
|
137
|
-
|
|
138
|
-
# Check if approaching limits
|
|
139
|
-
if len(events) > 8000:
|
|
140
|
-
print("Warning: Workflow has many events, consider redesigning")
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
## Handling Limit Errors
|
|
144
|
-
|
|
145
|
-
When the hard limit is reached, an `EventLimitExceededError` is raised. This error inherits from `FatalError`, meaning it will **not** be retried.
|
|
146
|
-
|
|
147
|
-
```python
|
|
148
|
-
from pyworkflow.core.exceptions import EventLimitExceededError, FatalError
|
|
149
|
-
|
|
150
|
-
try:
|
|
151
|
-
await start(my_workflow, args)
|
|
152
|
-
except EventLimitExceededError as e:
|
|
153
|
-
print(f"Workflow {e.run_id} exceeded {e.limit} events")
|
|
154
|
-
print(f"Actual count: {e.event_count}")
|
|
155
|
-
# Consider splitting the work into smaller workflows
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
## Next Steps
|
|
159
|
-
|
|
160
|
-
<CardGroup cols={2}>
|
|
161
|
-
<Card title="Events" icon="scroll" href="/concepts/events">
|
|
162
|
-
Learn how event sourcing works in PyWorkflow.
|
|
163
|
-
</Card>
|
|
164
|
-
<Card title="Configuration" icon="gear" href="/guides/configuration">
|
|
165
|
-
See all available configuration options.
|
|
166
|
-
</Card>
|
|
167
|
-
</CardGroup>
|