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.
Files changed (146) hide show
  1. pyworkflow/__init__.py +10 -1
  2. pyworkflow/celery/tasks.py +272 -24
  3. pyworkflow/cli/__init__.py +4 -1
  4. pyworkflow/cli/commands/runs.py +4 -4
  5. pyworkflow/cli/commands/setup.py +203 -4
  6. pyworkflow/cli/utils/config_generator.py +76 -3
  7. pyworkflow/cli/utils/docker_manager.py +232 -0
  8. pyworkflow/config.py +94 -17
  9. pyworkflow/context/__init__.py +13 -0
  10. pyworkflow/context/base.py +26 -0
  11. pyworkflow/context/local.py +80 -0
  12. pyworkflow/context/step_context.py +295 -0
  13. pyworkflow/core/registry.py +6 -1
  14. pyworkflow/core/step.py +141 -0
  15. pyworkflow/core/workflow.py +56 -0
  16. pyworkflow/engine/events.py +30 -0
  17. pyworkflow/engine/replay.py +39 -0
  18. pyworkflow/primitives/child_workflow.py +1 -1
  19. pyworkflow/runtime/local.py +1 -1
  20. pyworkflow/storage/__init__.py +14 -0
  21. pyworkflow/storage/base.py +35 -0
  22. pyworkflow/storage/cassandra.py +1747 -0
  23. pyworkflow/storage/config.py +69 -0
  24. pyworkflow/storage/dynamodb.py +31 -2
  25. pyworkflow/storage/file.py +28 -0
  26. pyworkflow/storage/memory.py +18 -0
  27. pyworkflow/storage/mysql.py +1159 -0
  28. pyworkflow/storage/postgres.py +27 -2
  29. pyworkflow/storage/schemas.py +4 -3
  30. pyworkflow/storage/sqlite.py +25 -2
  31. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/METADATA +7 -4
  32. pyworkflow_engine-0.1.10.dist-info/RECORD +91 -0
  33. pyworkflow_engine-0.1.10.dist-info/top_level.txt +1 -0
  34. dashboard/backend/app/__init__.py +0 -1
  35. dashboard/backend/app/config.py +0 -32
  36. dashboard/backend/app/controllers/__init__.py +0 -6
  37. dashboard/backend/app/controllers/run_controller.py +0 -86
  38. dashboard/backend/app/controllers/workflow_controller.py +0 -33
  39. dashboard/backend/app/dependencies/__init__.py +0 -5
  40. dashboard/backend/app/dependencies/storage.py +0 -50
  41. dashboard/backend/app/repositories/__init__.py +0 -6
  42. dashboard/backend/app/repositories/run_repository.py +0 -80
  43. dashboard/backend/app/repositories/workflow_repository.py +0 -27
  44. dashboard/backend/app/rest/__init__.py +0 -8
  45. dashboard/backend/app/rest/v1/__init__.py +0 -12
  46. dashboard/backend/app/rest/v1/health.py +0 -33
  47. dashboard/backend/app/rest/v1/runs.py +0 -133
  48. dashboard/backend/app/rest/v1/workflows.py +0 -41
  49. dashboard/backend/app/schemas/__init__.py +0 -23
  50. dashboard/backend/app/schemas/common.py +0 -16
  51. dashboard/backend/app/schemas/event.py +0 -24
  52. dashboard/backend/app/schemas/hook.py +0 -25
  53. dashboard/backend/app/schemas/run.py +0 -54
  54. dashboard/backend/app/schemas/step.py +0 -28
  55. dashboard/backend/app/schemas/workflow.py +0 -31
  56. dashboard/backend/app/server.py +0 -87
  57. dashboard/backend/app/services/__init__.py +0 -6
  58. dashboard/backend/app/services/run_service.py +0 -240
  59. dashboard/backend/app/services/workflow_service.py +0 -155
  60. dashboard/backend/main.py +0 -18
  61. docs/concepts/cancellation.mdx +0 -362
  62. docs/concepts/continue-as-new.mdx +0 -434
  63. docs/concepts/events.mdx +0 -266
  64. docs/concepts/fault-tolerance.mdx +0 -370
  65. docs/concepts/hooks.mdx +0 -552
  66. docs/concepts/limitations.mdx +0 -167
  67. docs/concepts/schedules.mdx +0 -775
  68. docs/concepts/sleep.mdx +0 -312
  69. docs/concepts/steps.mdx +0 -301
  70. docs/concepts/workflows.mdx +0 -255
  71. docs/guides/cli.mdx +0 -942
  72. docs/guides/configuration.mdx +0 -560
  73. docs/introduction.mdx +0 -155
  74. docs/quickstart.mdx +0 -279
  75. examples/__init__.py +0 -1
  76. examples/celery/__init__.py +0 -1
  77. examples/celery/durable/docker-compose.yml +0 -55
  78. examples/celery/durable/pyworkflow.config.yaml +0 -12
  79. examples/celery/durable/workflows/__init__.py +0 -122
  80. examples/celery/durable/workflows/basic.py +0 -87
  81. examples/celery/durable/workflows/batch_processing.py +0 -102
  82. examples/celery/durable/workflows/cancellation.py +0 -273
  83. examples/celery/durable/workflows/child_workflow_patterns.py +0 -240
  84. examples/celery/durable/workflows/child_workflows.py +0 -202
  85. examples/celery/durable/workflows/continue_as_new.py +0 -260
  86. examples/celery/durable/workflows/fault_tolerance.py +0 -210
  87. examples/celery/durable/workflows/hooks.py +0 -211
  88. examples/celery/durable/workflows/idempotency.py +0 -112
  89. examples/celery/durable/workflows/long_running.py +0 -99
  90. examples/celery/durable/workflows/retries.py +0 -101
  91. examples/celery/durable/workflows/schedules.py +0 -209
  92. examples/celery/transient/01_basic_workflow.py +0 -91
  93. examples/celery/transient/02_fault_tolerance.py +0 -257
  94. examples/celery/transient/__init__.py +0 -20
  95. examples/celery/transient/pyworkflow.config.yaml +0 -25
  96. examples/local/__init__.py +0 -1
  97. examples/local/durable/01_basic_workflow.py +0 -94
  98. examples/local/durable/02_file_storage.py +0 -132
  99. examples/local/durable/03_retries.py +0 -169
  100. examples/local/durable/04_long_running.py +0 -119
  101. examples/local/durable/05_event_log.py +0 -145
  102. examples/local/durable/06_idempotency.py +0 -148
  103. examples/local/durable/07_hooks.py +0 -334
  104. examples/local/durable/08_cancellation.py +0 -233
  105. examples/local/durable/09_child_workflows.py +0 -198
  106. examples/local/durable/10_child_workflow_patterns.py +0 -265
  107. examples/local/durable/11_continue_as_new.py +0 -249
  108. examples/local/durable/12_schedules.py +0 -198
  109. examples/local/durable/__init__.py +0 -1
  110. examples/local/transient/01_quick_tasks.py +0 -87
  111. examples/local/transient/02_retries.py +0 -130
  112. examples/local/transient/03_sleep.py +0 -141
  113. examples/local/transient/__init__.py +0 -1
  114. pyworkflow_engine-0.1.7.dist-info/RECORD +0 -196
  115. pyworkflow_engine-0.1.7.dist-info/top_level.txt +0 -5
  116. tests/examples/__init__.py +0 -0
  117. tests/integration/__init__.py +0 -0
  118. tests/integration/test_cancellation.py +0 -330
  119. tests/integration/test_child_workflows.py +0 -439
  120. tests/integration/test_continue_as_new.py +0 -428
  121. tests/integration/test_dynamodb_storage.py +0 -1146
  122. tests/integration/test_fault_tolerance.py +0 -369
  123. tests/integration/test_schedule_storage.py +0 -484
  124. tests/unit/__init__.py +0 -0
  125. tests/unit/backends/__init__.py +0 -1
  126. tests/unit/backends/test_dynamodb_storage.py +0 -1554
  127. tests/unit/backends/test_postgres_storage.py +0 -1281
  128. tests/unit/backends/test_sqlite_storage.py +0 -1460
  129. tests/unit/conftest.py +0 -41
  130. tests/unit/test_cancellation.py +0 -364
  131. tests/unit/test_child_workflows.py +0 -680
  132. tests/unit/test_continue_as_new.py +0 -441
  133. tests/unit/test_event_limits.py +0 -316
  134. tests/unit/test_executor.py +0 -320
  135. tests/unit/test_fault_tolerance.py +0 -334
  136. tests/unit/test_hooks.py +0 -495
  137. tests/unit/test_registry.py +0 -261
  138. tests/unit/test_replay.py +0 -420
  139. tests/unit/test_schedule_schemas.py +0 -285
  140. tests/unit/test_schedule_utils.py +0 -286
  141. tests/unit/test_scheduled_workflow.py +0 -274
  142. tests/unit/test_step.py +0 -353
  143. tests/unit/test_workflow.py +0 -243
  144. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/WHEEL +0 -0
  145. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/entry_points.txt +0 -0
  146. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/licenses/LICENSE +0 -0
@@ -1,148 +0,0 @@
1
- """
2
- Durable Workflow - Idempotency
3
-
4
- This example demonstrates idempotency key usage to prevent duplicate workflow execution.
5
- - Same workflow called twice with same idempotency_key
6
- - Second call returns same run_id without re-execution
7
- - Prevents duplicate orders, payments, etc.
8
- - Uses FileStorageBackend for persistence
9
-
10
- Run: python examples/local/durable/06_idempotency.py 2>/dev/null
11
- """
12
-
13
- import asyncio
14
- import tempfile
15
-
16
- from pyworkflow import (
17
- configure,
18
- get_workflow_run,
19
- reset_config,
20
- start,
21
- step,
22
- workflow,
23
- )
24
- from pyworkflow.storage import FileStorageBackend
25
-
26
- # Execution counter to track step calls
27
- execution_count = 0
28
-
29
-
30
- # --- Steps ---
31
- @step()
32
- async def create_order(order_id: str, amount: float) -> dict:
33
- """Create a new order."""
34
- global execution_count
35
- execution_count += 1
36
-
37
- print(f" Creating order {order_id} for ${amount:.2f}...")
38
- print(f" (Execution count: {execution_count})")
39
-
40
- return {"order_id": order_id, "amount": amount, "status": "created"}
41
-
42
-
43
- @step()
44
- async def charge_customer(order: dict) -> dict:
45
- """Charge the customer."""
46
- print(f" Charging customer ${order['amount']:.2f}...")
47
- return {**order, "charged": True}
48
-
49
-
50
- @step()
51
- async def send_confirmation(order: dict) -> dict:
52
- """Send order confirmation."""
53
- print(f" Sending confirmation for order {order['order_id']}...")
54
- return {**order, "confirmed": True}
55
-
56
-
57
- # --- Workflow ---
58
- @workflow(durable=True, tags=["local", "durable"])
59
- async def order_workflow(order_id: str, amount: float) -> dict:
60
- """Complete order workflow (must be idempotent)."""
61
- order = await create_order(order_id, amount)
62
- order = await charge_customer(order)
63
- order = await send_confirmation(order)
64
- return order
65
-
66
-
67
- async def main():
68
- global execution_count
69
-
70
- # Use temp directory (use real path for production)
71
- with tempfile.TemporaryDirectory() as tmpdir:
72
- print("=== Durable Workflow - Idempotency ===\n")
73
-
74
- # Configure with FileStorageBackend (for persistence)
75
- reset_config()
76
- storage = FileStorageBackend(base_path=tmpdir)
77
- configure(storage=storage, default_durable=True)
78
-
79
- # Reset execution count
80
- execution_count = 0
81
-
82
- # First call with idempotency key
83
- print("First call: Creating order with idempotency_key='order-unique-123'...\n")
84
- run_id_1 = await start(
85
- order_workflow, "order-unique-123", 99.99, idempotency_key="order-unique-123"
86
- )
87
-
88
- run_1 = await get_workflow_run(run_id_1)
89
- print("\nFirst call completed:")
90
- print(f" Run ID: {run_id_1}")
91
- print(f" Status: {run_1.status.value}")
92
- print(f" Result: {run_1.result}")
93
- print(f" Execution count: {execution_count}")
94
-
95
- # Second call with SAME idempotency key
96
- print("\n" + "=" * 60)
97
- print("\nSecond call: Same idempotency_key='order-unique-123'...\n")
98
- run_id_2 = await start(
99
- order_workflow, "order-unique-123", 99.99, idempotency_key="order-unique-123"
100
- )
101
-
102
- run_2 = await get_workflow_run(run_id_2)
103
- print("\nSecond call result:")
104
- print(f" Run ID: {run_id_2}")
105
- print(f" Status: {run_2.status.value}")
106
- print(f" Result: {run_2.result}")
107
- print(f" Execution count: {execution_count} (not incremented!)")
108
-
109
- # Verify they're the same
110
- print("\n" + "=" * 60)
111
- print("\n=== Verification ===")
112
- print(f"run_id_1 == run_id_2: {run_id_1 == run_id_2}")
113
- print(f"Workflow re-executed: {execution_count > 1}")
114
-
115
- if run_id_1 == run_id_2:
116
- print("\n✓ SUCCESS: Same run_id returned, workflow NOT re-executed!")
117
- else:
118
- print("\n✗ UNEXPECTED: Different run_id, workflow was re-executed!")
119
-
120
- # Third call with DIFFERENT idempotency key
121
- print("\n" + "=" * 60)
122
- print("\nThird call: Different idempotency_key='order-unique-456'...\n")
123
- run_id_3 = await start(
124
- order_workflow, "order-unique-456", 149.99, idempotency_key="order-unique-456"
125
- )
126
-
127
- run_3 = await get_workflow_run(run_id_3)
128
- print("\nThird call result:")
129
- print(f" Run ID: {run_id_3}")
130
- print(f" Status: {run_3.status.value}")
131
- print(f" Execution count: {execution_count} (incremented!)")
132
-
133
- print("\n=== Use Cases ===")
134
- print("✓ Prevent duplicate orders from retry logic")
135
- print("✓ Ensure exactly-once payment processing")
136
- print("✓ Handle duplicate webhook deliveries")
137
- print("✓ Guarantee idempotent API endpoints")
138
-
139
- print("\n=== Key Takeaways ===")
140
- print("✓ Same idempotency_key returns same run_id")
141
- print("✓ Workflow NOT re-executed on duplicate key")
142
- print("✓ Different keys create new workflow executions")
143
- print("✓ Critical for financial transactions and critical workflows")
144
- print("✓ Works across process restarts (persisted to storage)")
145
-
146
-
147
- if __name__ == "__main__":
148
- asyncio.run(main())
@@ -1,334 +0,0 @@
1
- """
2
- Durable Workflow - Hooks Example
3
-
4
- This example demonstrates the hooks feature for waiting on external events:
5
- - Using hook() to suspend workflow and wait for external input
6
- - Using define_hook() for typed hooks with Pydantic validation
7
- - Using resume_hook() to deliver payloads from external systems
8
- - Composite tokens (run_id:hook_id) for self-describing tokens
9
- - on_created callback for receiving the generated token
10
-
11
- Run: python examples/local/durable/07_hooks.py 2>/dev/null
12
- """
13
-
14
- import asyncio
15
-
16
- from pydantic import BaseModel
17
-
18
- from pyworkflow import (
19
- configure,
20
- define_hook,
21
- get_workflow_events,
22
- get_workflow_run,
23
- hook,
24
- reset_config,
25
- resume,
26
- resume_hook,
27
- start,
28
- step,
29
- workflow,
30
- )
31
- from pyworkflow.storage import InMemoryStorageBackend
32
-
33
- # Global storage reference for resumption
34
- _storage = None
35
-
36
- # Store tokens received from on_created callback
37
- _captured_tokens = {}
38
-
39
-
40
- # --- Pydantic models for typed hooks ---
41
- class ApprovalPayload(BaseModel):
42
- """Typed payload for approval hook."""
43
-
44
- approved: bool
45
- reviewer: str
46
- comments: str | None = None
47
-
48
-
49
- # Create typed hook
50
- approval_hook = define_hook("approval", ApprovalPayload)
51
-
52
-
53
- # --- Steps ---
54
- @step()
55
- async def prepare_order(order_id: str) -> dict:
56
- """Prepare the order for review."""
57
- print(f" Preparing order {order_id}...")
58
- return {"order_id": order_id, "status": "pending_approval"}
59
-
60
-
61
- @step()
62
- async def fulfill_order(order: dict) -> dict:
63
- """Fulfill the approved order."""
64
- print(f" Fulfilling order {order['order_id']}...")
65
- return {**order, "status": "fulfilled"}
66
-
67
-
68
- @step()
69
- async def cancel_order(order: dict, reason: str) -> dict:
70
- """Cancel the rejected order."""
71
- print(f" Cancelling order {order['order_id']}: {reason}")
72
- return {**order, "status": "cancelled", "reason": reason}
73
-
74
-
75
- # --- Workflow with simple hook ---
76
- @workflow(durable=True, name="simple_hook_workflow", tags=["local", "durable"])
77
- async def simple_hook_workflow(order_id: str) -> dict:
78
- """
79
- Workflow using simple hook() with untyped payload.
80
-
81
- Demonstrates basic hook usage with auto-generated composite token.
82
- Token format: run_id:hook_id (e.g., "run_abc123:hook_simple_approval_1")
83
- """
84
- order = await prepare_order(order_id)
85
-
86
- async def capture_token(token: str):
87
- """Capture the generated token for later use."""
88
- _captured_tokens[order_id] = token
89
- print(f" Hook created with token: {token}")
90
-
91
- # Wait for external approval using simple hook
92
- # Token is auto-generated in composite format: run_id:hook_id
93
- approval = await hook(
94
- "simple_approval",
95
- timeout="24h", # Expire after 24 hours
96
- on_created=capture_token,
97
- )
98
-
99
- if approval.get("approved"):
100
- return await fulfill_order(order)
101
- else:
102
- return await cancel_order(order, approval.get("reason", "Rejected"))
103
-
104
-
105
- # --- Workflow with typed hook ---
106
- @workflow(durable=True, name="typed_hook_workflow", tags=["local", "durable"])
107
- async def typed_hook_workflow(order_id: str) -> dict:
108
- """
109
- Workflow using define_hook() for type-safe payloads.
110
-
111
- Demonstrates typed hooks with Pydantic validation.
112
- """
113
- order = await prepare_order(order_id)
114
-
115
- async def capture_typed_token(token: str):
116
- """Capture the generated token for later use."""
117
- _captured_tokens[f"typed:{order_id}"] = token
118
- print(f" Typed hook created with token: {token}")
119
-
120
- # Wait for typed approval - payload is validated against ApprovalPayload
121
- approval: ApprovalPayload = await approval_hook(
122
- on_created=capture_typed_token,
123
- )
124
-
125
- print(f" Received: approved={approval.approved}, reviewer={approval.reviewer}")
126
-
127
- if approval.approved:
128
- return await fulfill_order(order)
129
- else:
130
- return await cancel_order(order, approval.comments or "No reason given")
131
-
132
-
133
- # --- Workflow with on_created callback ---
134
- @workflow(durable=True, name="callback_hook_workflow", tags=["local", "durable"])
135
- async def callback_hook_workflow(order_id: str) -> dict:
136
- """
137
- Workflow demonstrating on_created callback.
138
-
139
- The callback is invoked when the hook is created,
140
- allowing you to notify external systems with the token.
141
- Token format is composite: run_id:hook_id
142
- """
143
- order = await prepare_order(order_id)
144
-
145
- async def on_hook_created(token: str):
146
- # In real scenarios, you would notify external systems here
147
- # e.g., send email, update database, register webhook URL
148
- _captured_tokens[f"callback:{order_id}"] = token
149
- print(f" Hook created! Token: {token}")
150
- print(f" External system can POST to: /webhook/{token}")
151
-
152
- # Wait for approval with on_created callback
153
- approval = await hook(
154
- "callback_approval",
155
- on_created=on_hook_created,
156
- )
157
-
158
- if approval.get("approved"):
159
- return await fulfill_order(order)
160
- else:
161
- return await cancel_order(order, approval.get("reason", "Rejected"))
162
-
163
-
164
- async def demo_simple_hook():
165
- """Demo simple hook workflow."""
166
- print("\n" + "=" * 50)
167
- print("Demo 1: Simple Hook with Composite Token")
168
- print("=" * 50)
169
-
170
- run_id = await start(simple_hook_workflow, "ORDER-001")
171
- print(f"\nWorkflow started: {run_id}")
172
-
173
- # Check status - should be suspended
174
- run = await get_workflow_run(run_id)
175
- print(f"Status: {run.status.value}")
176
-
177
- # Get the token that was captured via on_created callback
178
- token = _captured_tokens.get("ORDER-001")
179
- print(f"\nCaptured token: {token}")
180
-
181
- # Simulate external system calling resume_hook with the composite token
182
- print("\n[External System] Sending approval...")
183
- result = await resume_hook(
184
- token=token,
185
- payload={"approved": True, "approver": "manager@example.com"},
186
- storage=_storage,
187
- )
188
- print(f"Resume result: {result.status}")
189
-
190
- # In local mode without Celery, manually resume the workflow
191
- await resume(run_id, storage=_storage)
192
-
193
- # Check final status
194
- run = await get_workflow_run(run_id)
195
- print(f"\nFinal status: {run.status.value}")
196
- print(f"Result: {run.result}")
197
-
198
-
199
- async def demo_typed_hook():
200
- """Demo typed hook workflow."""
201
- print("\n" + "=" * 50)
202
- print("Demo 2: Typed Hook with Pydantic")
203
- print("=" * 50)
204
-
205
- run_id = await start(typed_hook_workflow, "ORDER-002")
206
- print(f"\nWorkflow started: {run_id}")
207
-
208
- # Check status - should be suspended
209
- run = await get_workflow_run(run_id)
210
- print(f"Status: {run.status.value}")
211
-
212
- # Get the token that was captured via on_created callback
213
- token = _captured_tokens.get("typed:ORDER-002")
214
- print(f"\nCaptured token: {token}")
215
-
216
- # Simulate external system calling resume_hook with typed payload
217
- print("\n[External System] Sending typed approval...")
218
- result = await resume_hook(
219
- token=token,
220
- payload={
221
- "approved": True,
222
- "reviewer": "jane.doe@example.com",
223
- "comments": "Looks good!",
224
- },
225
- storage=_storage,
226
- )
227
- print(f"Resume result: {result.status}")
228
-
229
- # In local mode without Celery, manually resume the workflow
230
- await resume(run_id, storage=_storage)
231
-
232
- # Check final status
233
- run = await get_workflow_run(run_id)
234
- print(f"\nFinal status: {run.status.value}")
235
- print(f"Result: {run.result}")
236
-
237
-
238
- async def demo_rejection():
239
- """Demo hook rejection flow."""
240
- print("\n" + "=" * 50)
241
- print("Demo 3: Rejection Flow")
242
- print("=" * 50)
243
-
244
- run_id = await start(simple_hook_workflow, "ORDER-003")
245
- print(f"\nWorkflow started: {run_id}")
246
-
247
- # Get the token that was captured via on_created callback
248
- token = _captured_tokens.get("ORDER-003")
249
-
250
- # Simulate rejection
251
- print("\n[External System] Sending rejection...")
252
- result = await resume_hook(
253
- token=token,
254
- payload={"approved": False, "reason": "Insufficient inventory"},
255
- storage=_storage,
256
- )
257
- print(f"Resume result: {result.status}")
258
-
259
- # In local mode without Celery, manually resume the workflow
260
- await resume(run_id, storage=_storage)
261
-
262
- # Check final status
263
- run = await get_workflow_run(run_id)
264
- print(f"\nFinal status: {run.status.value}")
265
- print(f"Result: {run.result}")
266
-
267
-
268
- async def demo_event_log():
269
- """Show the event log for a hook-based workflow."""
270
- print("\n" + "=" * 50)
271
- print("Demo 4: Event Log Inspection")
272
- print("=" * 50)
273
-
274
- run_id = await start(simple_hook_workflow, "ORDER-004")
275
-
276
- # Get the token that was captured via on_created callback
277
- token = _captured_tokens.get("ORDER-004")
278
-
279
- # Resume hook using captured token
280
- await resume_hook(
281
- token=token,
282
- payload={"approved": True},
283
- storage=_storage,
284
- )
285
-
286
- # Resume workflow in local mode
287
- await resume(run_id, storage=_storage)
288
-
289
- # Inspect event log
290
- events = await get_workflow_events(run_id)
291
- print(f"\nEvent Log ({len(events)} events):")
292
- for event in events:
293
- print(f" {event.sequence}: {event.type.value}")
294
- if "hook" in event.type.value:
295
- if "hook_id" in event.data:
296
- print(f" hook_id: {event.data.get('hook_id', 'N/A')}")
297
- if "token" in event.data:
298
- print(f" token: {event.data.get('token', 'N/A')[:40]}...")
299
-
300
-
301
- async def main():
302
- global _storage
303
-
304
- # Configure with InMemoryStorageBackend
305
- reset_config()
306
- _storage = InMemoryStorageBackend()
307
- configure(storage=_storage, default_durable=True)
308
-
309
- print("=== Durable Workflow - Hooks Example ===")
310
- print("""
311
- This example demonstrates hooks for external event integration:
312
- - hook(): Wait for external events
313
- - define_hook(): Create typed hooks with validation
314
- - resume_hook(): Deliver payloads from external systems
315
- """)
316
-
317
- await demo_simple_hook()
318
- await demo_typed_hook()
319
- await demo_rejection()
320
- await demo_event_log()
321
-
322
- print("\n" + "=" * 50)
323
- print("Key Takeaways")
324
- print("=" * 50)
325
- print("- hook() suspends workflow until resume_hook() is called")
326
- print("- Tokens are auto-generated in composite format: run_id:hook_id")
327
- print("- Use on_created callback to capture the token for external systems")
328
- print("- define_hook() provides type-safe payloads with Pydantic")
329
- print("- Composite tokens are self-describing (contain run_id)")
330
- print("- Events record hook creation and reception")
331
-
332
-
333
- if __name__ == "__main__":
334
- asyncio.run(main())