pyworkflow-engine 0.1.7__py3-none-any.whl → 0.1.9__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 (145) 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/context/__init__.py +13 -0
  9. pyworkflow/context/base.py +26 -0
  10. pyworkflow/context/local.py +80 -0
  11. pyworkflow/context/step_context.py +295 -0
  12. pyworkflow/core/registry.py +6 -1
  13. pyworkflow/core/step.py +141 -0
  14. pyworkflow/core/workflow.py +56 -0
  15. pyworkflow/engine/events.py +30 -0
  16. pyworkflow/engine/replay.py +39 -0
  17. pyworkflow/primitives/child_workflow.py +1 -1
  18. pyworkflow/runtime/local.py +1 -1
  19. pyworkflow/storage/__init__.py +14 -0
  20. pyworkflow/storage/base.py +35 -0
  21. pyworkflow/storage/cassandra.py +1747 -0
  22. pyworkflow/storage/config.py +69 -0
  23. pyworkflow/storage/dynamodb.py +31 -2
  24. pyworkflow/storage/file.py +28 -0
  25. pyworkflow/storage/memory.py +18 -0
  26. pyworkflow/storage/mysql.py +1159 -0
  27. pyworkflow/storage/postgres.py +27 -2
  28. pyworkflow/storage/schemas.py +4 -3
  29. pyworkflow/storage/sqlite.py +25 -2
  30. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.9.dist-info}/METADATA +7 -4
  31. pyworkflow_engine-0.1.9.dist-info/RECORD +91 -0
  32. pyworkflow_engine-0.1.9.dist-info/top_level.txt +1 -0
  33. dashboard/backend/app/__init__.py +0 -1
  34. dashboard/backend/app/config.py +0 -32
  35. dashboard/backend/app/controllers/__init__.py +0 -6
  36. dashboard/backend/app/controllers/run_controller.py +0 -86
  37. dashboard/backend/app/controllers/workflow_controller.py +0 -33
  38. dashboard/backend/app/dependencies/__init__.py +0 -5
  39. dashboard/backend/app/dependencies/storage.py +0 -50
  40. dashboard/backend/app/repositories/__init__.py +0 -6
  41. dashboard/backend/app/repositories/run_repository.py +0 -80
  42. dashboard/backend/app/repositories/workflow_repository.py +0 -27
  43. dashboard/backend/app/rest/__init__.py +0 -8
  44. dashboard/backend/app/rest/v1/__init__.py +0 -12
  45. dashboard/backend/app/rest/v1/health.py +0 -33
  46. dashboard/backend/app/rest/v1/runs.py +0 -133
  47. dashboard/backend/app/rest/v1/workflows.py +0 -41
  48. dashboard/backend/app/schemas/__init__.py +0 -23
  49. dashboard/backend/app/schemas/common.py +0 -16
  50. dashboard/backend/app/schemas/event.py +0 -24
  51. dashboard/backend/app/schemas/hook.py +0 -25
  52. dashboard/backend/app/schemas/run.py +0 -54
  53. dashboard/backend/app/schemas/step.py +0 -28
  54. dashboard/backend/app/schemas/workflow.py +0 -31
  55. dashboard/backend/app/server.py +0 -87
  56. dashboard/backend/app/services/__init__.py +0 -6
  57. dashboard/backend/app/services/run_service.py +0 -240
  58. dashboard/backend/app/services/workflow_service.py +0 -155
  59. dashboard/backend/main.py +0 -18
  60. docs/concepts/cancellation.mdx +0 -362
  61. docs/concepts/continue-as-new.mdx +0 -434
  62. docs/concepts/events.mdx +0 -266
  63. docs/concepts/fault-tolerance.mdx +0 -370
  64. docs/concepts/hooks.mdx +0 -552
  65. docs/concepts/limitations.mdx +0 -167
  66. docs/concepts/schedules.mdx +0 -775
  67. docs/concepts/sleep.mdx +0 -312
  68. docs/concepts/steps.mdx +0 -301
  69. docs/concepts/workflows.mdx +0 -255
  70. docs/guides/cli.mdx +0 -942
  71. docs/guides/configuration.mdx +0 -560
  72. docs/introduction.mdx +0 -155
  73. docs/quickstart.mdx +0 -279
  74. examples/__init__.py +0 -1
  75. examples/celery/__init__.py +0 -1
  76. examples/celery/durable/docker-compose.yml +0 -55
  77. examples/celery/durable/pyworkflow.config.yaml +0 -12
  78. examples/celery/durable/workflows/__init__.py +0 -122
  79. examples/celery/durable/workflows/basic.py +0 -87
  80. examples/celery/durable/workflows/batch_processing.py +0 -102
  81. examples/celery/durable/workflows/cancellation.py +0 -273
  82. examples/celery/durable/workflows/child_workflow_patterns.py +0 -240
  83. examples/celery/durable/workflows/child_workflows.py +0 -202
  84. examples/celery/durable/workflows/continue_as_new.py +0 -260
  85. examples/celery/durable/workflows/fault_tolerance.py +0 -210
  86. examples/celery/durable/workflows/hooks.py +0 -211
  87. examples/celery/durable/workflows/idempotency.py +0 -112
  88. examples/celery/durable/workflows/long_running.py +0 -99
  89. examples/celery/durable/workflows/retries.py +0 -101
  90. examples/celery/durable/workflows/schedules.py +0 -209
  91. examples/celery/transient/01_basic_workflow.py +0 -91
  92. examples/celery/transient/02_fault_tolerance.py +0 -257
  93. examples/celery/transient/__init__.py +0 -20
  94. examples/celery/transient/pyworkflow.config.yaml +0 -25
  95. examples/local/__init__.py +0 -1
  96. examples/local/durable/01_basic_workflow.py +0 -94
  97. examples/local/durable/02_file_storage.py +0 -132
  98. examples/local/durable/03_retries.py +0 -169
  99. examples/local/durable/04_long_running.py +0 -119
  100. examples/local/durable/05_event_log.py +0 -145
  101. examples/local/durable/06_idempotency.py +0 -148
  102. examples/local/durable/07_hooks.py +0 -334
  103. examples/local/durable/08_cancellation.py +0 -233
  104. examples/local/durable/09_child_workflows.py +0 -198
  105. examples/local/durable/10_child_workflow_patterns.py +0 -265
  106. examples/local/durable/11_continue_as_new.py +0 -249
  107. examples/local/durable/12_schedules.py +0 -198
  108. examples/local/durable/__init__.py +0 -1
  109. examples/local/transient/01_quick_tasks.py +0 -87
  110. examples/local/transient/02_retries.py +0 -130
  111. examples/local/transient/03_sleep.py +0 -141
  112. examples/local/transient/__init__.py +0 -1
  113. pyworkflow_engine-0.1.7.dist-info/RECORD +0 -196
  114. pyworkflow_engine-0.1.7.dist-info/top_level.txt +0 -5
  115. tests/examples/__init__.py +0 -0
  116. tests/integration/__init__.py +0 -0
  117. tests/integration/test_cancellation.py +0 -330
  118. tests/integration/test_child_workflows.py +0 -439
  119. tests/integration/test_continue_as_new.py +0 -428
  120. tests/integration/test_dynamodb_storage.py +0 -1146
  121. tests/integration/test_fault_tolerance.py +0 -369
  122. tests/integration/test_schedule_storage.py +0 -484
  123. tests/unit/__init__.py +0 -0
  124. tests/unit/backends/__init__.py +0 -1
  125. tests/unit/backends/test_dynamodb_storage.py +0 -1554
  126. tests/unit/backends/test_postgres_storage.py +0 -1281
  127. tests/unit/backends/test_sqlite_storage.py +0 -1460
  128. tests/unit/conftest.py +0 -41
  129. tests/unit/test_cancellation.py +0 -364
  130. tests/unit/test_child_workflows.py +0 -680
  131. tests/unit/test_continue_as_new.py +0 -441
  132. tests/unit/test_event_limits.py +0 -316
  133. tests/unit/test_executor.py +0 -320
  134. tests/unit/test_fault_tolerance.py +0 -334
  135. tests/unit/test_hooks.py +0 -495
  136. tests/unit/test_registry.py +0 -261
  137. tests/unit/test_replay.py +0 -420
  138. tests/unit/test_schedule_schemas.py +0 -285
  139. tests/unit/test_schedule_utils.py +0 -286
  140. tests/unit/test_scheduled_workflow.py +0 -274
  141. tests/unit/test_step.py +0 -353
  142. tests/unit/test_workflow.py +0 -243
  143. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.9.dist-info}/WHEEL +0 -0
  144. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.9.dist-info}/entry_points.txt +0 -0
  145. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.9.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())