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,233 +0,0 @@
1
- """
2
- Durable Workflow - Cancellation
3
-
4
- This example demonstrates graceful workflow cancellation:
5
- - Cancel running or suspended workflows
6
- - Handle CancellationError for cleanup
7
- - Use shield() to protect critical operations
8
- - Checkpoint-based cancellation (not mid-step)
9
-
10
- Run: python examples/local/durable/08_cancellation.py 2>/dev/null
11
- """
12
-
13
- import asyncio
14
- import tempfile
15
-
16
- from pyworkflow import (
17
- CancellationError,
18
- cancel_workflow,
19
- configure,
20
- get_workflow_run,
21
- reset_config,
22
- shield,
23
- sleep,
24
- start,
25
- step,
26
- workflow,
27
- )
28
- from pyworkflow.storage import FileStorageBackend
29
-
30
-
31
- # --- Steps ---
32
- @step()
33
- async def reserve_inventory(order_id: str) -> dict:
34
- """Reserve inventory for order."""
35
- print(f" [Step] Reserving inventory for order {order_id}...")
36
- await asyncio.sleep(0.1) # Simulate work
37
- return {"order_id": order_id, "inventory_reserved": True}
38
-
39
-
40
- @step()
41
- async def charge_payment(order: dict) -> dict:
42
- """Charge payment for order."""
43
- print(f" [Step] Charging payment for order {order['order_id']}...")
44
- await asyncio.sleep(0.1) # Simulate work
45
- return {**order, "payment_charged": True}
46
-
47
-
48
- @step()
49
- async def create_shipment(order: dict) -> dict:
50
- """Create shipment for order."""
51
- print(f" [Step] Creating shipment for order {order['order_id']}...")
52
- await asyncio.sleep(0.1) # Simulate work
53
- return {**order, "shipment_created": True}
54
-
55
-
56
- @step()
57
- async def release_inventory(order_id: str) -> None:
58
- """Release previously reserved inventory (compensation)."""
59
- print(f" [Cleanup] Releasing inventory for order {order_id}...")
60
-
61
-
62
- @step()
63
- async def refund_payment(order_id: str) -> None:
64
- """Refund charged payment (compensation)."""
65
- print(f" [Cleanup] Refunding payment for order {order_id}...")
66
-
67
-
68
- # --- Workflow with Cancellation Handling ---
69
- @workflow(durable=True, tags=["local", "durable"])
70
- async def order_workflow(order_id: str) -> dict:
71
- """
72
- Order processing workflow with cancellation handling.
73
-
74
- If cancelled:
75
- - Catches CancellationError
76
- - Uses shield() to ensure cleanup completes
77
- - Releases inventory and refunds payment
78
- """
79
- try:
80
- order = await reserve_inventory(order_id)
81
-
82
- # Simulate waiting for approval
83
- print(" [Workflow] Waiting 5s for approval (can be cancelled here)...")
84
- await sleep("5s")
85
-
86
- order = await charge_payment(order)
87
-
88
- # Another wait - e.g., for warehouse processing
89
- print(" [Workflow] Waiting 5s for warehouse (can be cancelled here)...")
90
- await sleep("5s")
91
-
92
- order = await create_shipment(order)
93
- return order
94
-
95
- except CancellationError as e:
96
- print(f"\n [Workflow] Cancellation detected! Reason: {e.reason}")
97
- print(" [Workflow] Performing cleanup...")
98
-
99
- # Use shield() to ensure cleanup completes even if cancelled again
100
- async with shield():
101
- await release_inventory(order_id)
102
- await refund_payment(order_id)
103
-
104
- print(" [Workflow] Cleanup complete, re-raising CancellationError")
105
- raise # Re-raise to mark workflow as cancelled
106
-
107
-
108
- async def example_cancel_suspended_workflow(storage):
109
- """Example 1: Cancel a workflow while it's suspended (sleeping)."""
110
- print("\n--- Example 1: Cancel Suspended Workflow ---\n")
111
-
112
- # Start workflow
113
- run_id = await start(order_workflow, "order-001")
114
-
115
- # Check status - should be suspended (sleeping)
116
- run = await get_workflow_run(run_id)
117
- print(f"Workflow status: {run.status.value}")
118
-
119
- if run.status.value == "suspended":
120
- print("\nWorkflow is suspended (sleeping). Cancelling...")
121
-
122
- # Cancel the suspended workflow
123
- cancelled = await cancel_workflow(
124
- run_id,
125
- reason="Customer cancelled order",
126
- storage=storage,
127
- )
128
-
129
- print(f"Cancellation initiated: {cancelled}")
130
-
131
- # Check final status
132
- run = await get_workflow_run(run_id)
133
- print(f"Final status: {run.status.value}")
134
-
135
- return run_id
136
-
137
-
138
- async def example_cancel_running_workflow(storage):
139
- """Example 2: Cancel a running workflow (cancellation at next checkpoint)."""
140
- print("\n--- Example 2: Cancel Running Workflow ---\n")
141
-
142
- # Define a workflow that we can cancel mid-execution
143
- @workflow(durable=True)
144
- async def multi_step_workflow(job_id: str) -> dict:
145
- try:
146
- print(f" [Step 1] Starting job {job_id}...")
147
- await asyncio.sleep(0.1)
148
-
149
- print(" [Step 2] Processing (cancellation checked here)...")
150
- # Note: In real scenario, ctx.check_cancellation() would be called
151
- # by @step decorator before execution
152
- await asyncio.sleep(0.1)
153
-
154
- print(" [Step 3] Finalizing...")
155
- return {"job_id": job_id, "status": "done"}
156
-
157
- except CancellationError as e:
158
- print(f" [Workflow] Cancelled! Reason: {e.reason}")
159
- raise
160
-
161
- # Start and immediately cancel (before it completes)
162
- run_id = await start(multi_step_workflow, "job-002")
163
-
164
- run = await get_workflow_run(run_id)
165
- print(f"Workflow status: {run.status.value}")
166
-
167
- # If it completed before we could cancel, that's OK
168
- if run.status.value != "completed":
169
- print("\nCancelling workflow...")
170
- cancelled = await cancel_workflow(
171
- run_id,
172
- reason="Test cancellation",
173
- storage=storage,
174
- )
175
- print(f"Cancellation initiated: {cancelled}")
176
-
177
- # Check final status
178
- run = await get_workflow_run(run_id)
179
- print(f"Final status: {run.status.value}")
180
-
181
- return run_id
182
-
183
-
184
- async def example_cancel_already_completed(storage):
185
- """Example 3: Try to cancel an already completed workflow."""
186
- print("\n--- Example 3: Cancel Completed Workflow ---\n")
187
-
188
- # Create a simple quick workflow
189
- @workflow(durable=True)
190
- async def quick_workflow() -> str:
191
- return "done"
192
-
193
- # Start and complete
194
- run_id = await start(quick_workflow)
195
- run = await get_workflow_run(run_id)
196
- print(f"Workflow status: {run.status.value}")
197
-
198
- # Try to cancel
199
- print("Attempting to cancel completed workflow...")
200
- cancelled = await cancel_workflow(run_id, storage=storage)
201
-
202
- print(f"Cancellation result: {cancelled}")
203
- print("(False means workflow was already in terminal state)")
204
-
205
- return run_id
206
-
207
-
208
- async def main():
209
- # Use temp directory
210
- with tempfile.TemporaryDirectory() as tmpdir:
211
- print("=== Durable Workflow - Cancellation ===")
212
-
213
- # Configure with FileStorageBackend
214
- reset_config()
215
- storage = FileStorageBackend(base_path=tmpdir)
216
- configure(storage=storage, default_durable=True)
217
-
218
- # Run examples
219
- await example_cancel_suspended_workflow(storage)
220
- await example_cancel_running_workflow(storage)
221
- await example_cancel_already_completed(storage)
222
-
223
- print("\n=== Key Takeaways ===")
224
- print(" - cancel_workflow() requests graceful cancellation")
225
- print(" - Suspended workflows are cancelled immediately")
226
- print(" - Running workflows cancel at next checkpoint (before step/sleep/hook)")
227
- print(" - Catch CancellationError for cleanup logic")
228
- print(" - Use shield() to protect critical cleanup operations")
229
- print(" - Cancellation does NOT interrupt a step mid-execution")
230
-
231
-
232
- if __name__ == "__main__":
233
- asyncio.run(main())
@@ -1,198 +0,0 @@
1
- """
2
- Child Workflows - Basic Example
3
-
4
- This example demonstrates how to spawn child workflows from a parent workflow.
5
- - Parent workflow spawns child workflows using start_child_workflow()
6
- - Children have their own run_id and event history
7
- - wait_for_completion=True (default) waits for child to complete
8
- - wait_for_completion=False returns a ChildWorkflowHandle immediately
9
- - TERMINATE policy: when parent completes/fails/cancels, children are cancelled
10
-
11
- Run: python examples/local/durable/09_child_workflows.py 2>/dev/null
12
- """
13
-
14
- import asyncio
15
-
16
- from pyworkflow import (
17
- ChildWorkflowHandle,
18
- configure,
19
- get_workflow_run,
20
- reset_config,
21
- start,
22
- start_child_workflow,
23
- step,
24
- workflow,
25
- )
26
- from pyworkflow.storage import InMemoryStorageBackend
27
-
28
-
29
- # --- Steps ---
30
- @step()
31
- async def validate_order(order_id: str) -> dict:
32
- """Validate order details."""
33
- print(f" Validating order {order_id}...")
34
- return {"order_id": order_id, "valid": True}
35
-
36
-
37
- @step()
38
- async def process_payment(order_id: str, amount: float) -> dict:
39
- """Process payment for order."""
40
- print(f" Processing payment ${amount:.2f} for {order_id}...")
41
- return {"order_id": order_id, "amount": amount, "paid": True}
42
-
43
-
44
- @step()
45
- async def ship_order(order_id: str) -> dict:
46
- """Ship the order."""
47
- print(f" Shipping order {order_id}...")
48
- return {"order_id": order_id, "shipped": True}
49
-
50
-
51
- @step()
52
- async def send_email(recipient: str, subject: str) -> dict:
53
- """Send an email notification."""
54
- print(f" Sending email to {recipient}: {subject}")
55
- return {"recipient": recipient, "subject": subject, "sent": True}
56
-
57
-
58
- # --- Child Workflows ---
59
- @workflow(durable=True, tags=["local", "durable"])
60
- async def payment_workflow(order_id: str, amount: float) -> dict:
61
- """Child workflow for payment processing."""
62
- print(f" [PaymentWorkflow] Starting for order {order_id}")
63
- result = await process_payment(order_id, amount)
64
- print(f" [PaymentWorkflow] Completed for order {order_id}")
65
- return result
66
-
67
-
68
- @workflow(durable=True, tags=["local", "durable"])
69
- async def shipping_workflow(order_id: str) -> dict:
70
- """Child workflow for shipping."""
71
- print(f" [ShippingWorkflow] Starting for order {order_id}")
72
- result = await ship_order(order_id)
73
- print(f" [ShippingWorkflow] Completed for order {order_id}")
74
- return result
75
-
76
-
77
- @workflow(durable=True, tags=["local", "durable"])
78
- async def notification_workflow(email: str, order_id: str) -> dict:
79
- """Child workflow for sending notifications."""
80
- print(f" [NotificationWorkflow] Starting for {email}")
81
- result = await send_email(email, f"Order {order_id} update")
82
- print(f" [NotificationWorkflow] Completed for {email}")
83
- return result
84
-
85
-
86
- # --- Parent Workflow ---
87
- @workflow(durable=True, tags=["local", "durable"])
88
- async def order_fulfillment_workflow(
89
- order_id: str,
90
- amount: float,
91
- customer_email: str,
92
- ) -> dict:
93
- """
94
- Parent workflow that orchestrates order fulfillment using child workflows.
95
-
96
- This demonstrates:
97
- 1. wait_for_completion=True - Wait for child to complete (default)
98
- 2. wait_for_completion=False - Fire-and-forget with handle
99
- """
100
- print(f"[OrderFulfillment] Starting for order {order_id}")
101
-
102
- # Step 1: Validate order (regular step)
103
- validation = await validate_order(order_id)
104
- if not validation["valid"]:
105
- return {"order_id": order_id, "status": "invalid"}
106
-
107
- # Step 2: Process payment via child workflow (wait for completion)
108
- print("[OrderFulfillment] Starting payment child workflow...")
109
- payment_result = await start_child_workflow(
110
- payment_workflow,
111
- order_id,
112
- amount,
113
- wait_for_completion=True, # Default: wait for child to complete
114
- )
115
- print(f"[OrderFulfillment] Payment completed: {payment_result}")
116
-
117
- # Step 3: Ship order via child workflow (wait for completion)
118
- print("[OrderFulfillment] Starting shipping child workflow...")
119
- shipping_result = await start_child_workflow(
120
- shipping_workflow,
121
- order_id,
122
- wait_for_completion=True,
123
- )
124
- print(f"[OrderFulfillment] Shipping completed: {shipping_result}")
125
-
126
- # Step 4: Send notification via fire-and-forget child workflow
127
- # This returns immediately with a handle, parent continues
128
- print("[OrderFulfillment] Starting notification child workflow (fire-and-forget)...")
129
- notification_handle: ChildWorkflowHandle = await start_child_workflow(
130
- notification_workflow,
131
- customer_email,
132
- order_id,
133
- wait_for_completion=False, # Fire-and-forget
134
- )
135
- print(f"[OrderFulfillment] Notification child started: {notification_handle.child_run_id}")
136
-
137
- # We can optionally check status or wait for the handle later
138
- # For now, we'll just let it run in the background
139
-
140
- result = {
141
- "order_id": order_id,
142
- "status": "fulfilled",
143
- "payment": payment_result,
144
- "shipping": shipping_result,
145
- "notification_run_id": notification_handle.child_run_id,
146
- }
147
-
148
- print(f"[OrderFulfillment] Completed for order {order_id}")
149
- return result
150
-
151
-
152
- async def main():
153
- # Configure with InMemoryStorageBackend
154
- reset_config()
155
- storage = InMemoryStorageBackend()
156
- configure(storage=storage, default_durable=True)
157
-
158
- print("=== Child Workflows - Basic Example ===\n")
159
- print("Running order fulfillment workflow with child workflows...\n")
160
-
161
- # Start parent workflow
162
- run_id = await start(
163
- order_fulfillment_workflow,
164
- "order-456",
165
- 149.99,
166
- "customer@example.com",
167
- )
168
-
169
- # Give fire-and-forget child time to complete
170
- await asyncio.sleep(0.5)
171
-
172
- print("\n=== Workflow Results ===")
173
-
174
- # Check parent workflow
175
- parent_run = await get_workflow_run(run_id)
176
- print(f"\nParent Workflow: {run_id}")
177
- print(f" Status: {parent_run.status.value}")
178
- print(f" Result: {parent_run.result}")
179
-
180
- # List child workflows
181
- children = await storage.get_children(run_id)
182
- print(f"\nChild Workflows ({len(children)} total):")
183
- for child in children:
184
- print(f" - {child.run_id}")
185
- print(f" Workflow: {child.workflow_name}")
186
- print(f" Status: {child.status.value}")
187
- print(f" Nesting Depth: {child.nesting_depth}")
188
-
189
- print("\n=== Key Takeaways ===")
190
- print("1. start_child_workflow() spawns a child with its own run_id")
191
- print("2. wait_for_completion=True (default) waits for child result")
192
- print("3. wait_for_completion=False returns a ChildWorkflowHandle")
193
- print("4. Child workflows have their own event history")
194
- print("5. TERMINATE policy: children cancelled when parent completes")
195
-
196
-
197
- if __name__ == "__main__":
198
- asyncio.run(main())
@@ -1,265 +0,0 @@
1
- """
2
- Child Workflows - Advanced Patterns
3
-
4
- This example demonstrates advanced child workflow patterns:
5
- - Nested child workflows (parent -> child -> grandchild)
6
- - Parallel child workflows using asyncio.gather
7
- - Error handling with ChildWorkflowFailedError
8
- - Cancellation propagation (TERMINATE policy)
9
- - Using ChildWorkflowHandle for async patterns
10
-
11
- Run: python examples/local/durable/10_child_workflow_patterns.py 2>/dev/null
12
- """
13
-
14
- import asyncio
15
-
16
- from pyworkflow import (
17
- ChildWorkflowFailedError,
18
- MaxNestingDepthError,
19
- configure,
20
- get_workflow_run,
21
- reset_config,
22
- start,
23
- start_child_workflow,
24
- step,
25
- workflow,
26
- )
27
- from pyworkflow.storage import InMemoryStorageBackend
28
-
29
-
30
- # --- Steps ---
31
- @step()
32
- async def do_work(name: str, duration: float = 0.1) -> dict:
33
- """Simulate some work."""
34
- print(f" [{name}] Working for {duration}s...")
35
- await asyncio.sleep(duration)
36
- return {"name": name, "completed": True}
37
-
38
-
39
- @step()
40
- async def failing_step() -> dict:
41
- """A step that always fails."""
42
- raise ValueError("This step always fails!")
43
-
44
-
45
- # --- Workflows for Nesting Demo ---
46
- @workflow(durable=True, tags=["local", "durable"])
47
- async def level_3_workflow(task_id: str) -> dict:
48
- """Grandchild workflow (nesting depth 3)."""
49
- print(f" [Level3] Starting task {task_id}")
50
- result = await do_work(f"level3-{task_id}")
51
- print(f" [Level3] Completed task {task_id}")
52
- return result
53
-
54
-
55
- @workflow(durable=True, tags=["local", "durable"])
56
- async def level_2_workflow(task_id: str) -> dict:
57
- """Child workflow that spawns a grandchild."""
58
- print(f" [Level2] Starting task {task_id}")
59
-
60
- # Spawn grandchild (nesting depth 3)
61
- grandchild_result = await start_child_workflow(
62
- level_3_workflow,
63
- task_id,
64
- )
65
-
66
- print(f" [Level2] Completed task {task_id}")
67
- return {"level2": task_id, "grandchild": grandchild_result}
68
-
69
-
70
- @workflow(durable=True, tags=["local", "durable"])
71
- async def level_1_workflow() -> dict:
72
- """Parent workflow demonstrating max nesting depth."""
73
- print("[Level1] Starting nested workflow demo")
74
-
75
- # Spawn child which will spawn grandchild
76
- child_result = await start_child_workflow(level_2_workflow, "nested-task")
77
-
78
- print("[Level1] Completed nested workflow demo")
79
- return {"level1": True, "child": child_result}
80
-
81
-
82
- # --- Workflows for Parallel Demo ---
83
- @workflow(durable=True, tags=["local", "durable"])
84
- async def parallel_task_workflow(task_id: str, duration: float) -> dict:
85
- """A simple workflow for parallel execution."""
86
- result = await do_work(f"parallel-{task_id}", duration)
87
- return {"task_id": task_id, **result}
88
-
89
-
90
- @workflow(durable=True, tags=["local", "durable"])
91
- async def parallel_parent_workflow() -> dict:
92
- """Parent workflow that runs multiple children in parallel."""
93
- print("[ParallelParent] Starting parallel children demo")
94
-
95
- # Start multiple children with fire-and-forget
96
- handles = []
97
- for i in range(3):
98
- handle = await start_child_workflow(
99
- parallel_task_workflow,
100
- f"task-{i}",
101
- 0.1 * (i + 1), # Different durations
102
- wait_for_completion=False,
103
- )
104
- handles.append(handle)
105
- print(f" Started child {i}: {handle.child_run_id}")
106
-
107
- # Wait for all children to complete using handles
108
- print("[ParallelParent] Waiting for all children...")
109
- results = []
110
- for i, handle in enumerate(handles):
111
- result = await handle.result(timeout=10.0)
112
- results.append(result)
113
- print(f" Child {i} completed: {result}")
114
-
115
- print("[ParallelParent] All children completed")
116
- return {"children_count": len(results), "results": results}
117
-
118
-
119
- # --- Workflows for Error Handling Demo ---
120
- @workflow(durable=True, tags=["local", "durable"])
121
- async def failing_child_workflow() -> dict:
122
- """A child workflow that fails."""
123
- await failing_step()
124
- return {"should": "never reach here"}
125
-
126
-
127
- @workflow(durable=True, tags=["local", "durable"])
128
- async def error_handling_parent_workflow() -> dict:
129
- """Parent workflow demonstrating error handling."""
130
- print("[ErrorParent] Starting error handling demo")
131
-
132
- try:
133
- # This child will fail
134
- await start_child_workflow(failing_child_workflow)
135
- except ChildWorkflowFailedError as e:
136
- print("[ErrorParent] Caught child failure!")
137
- print(f" Child run_id: {e.child_run_id}")
138
- print(f" Child workflow: {e.child_workflow_name}")
139
- print(f" Error: {e.error}")
140
- print(f" Error type: {e.error_type}")
141
- return {
142
- "status": "child_failed",
143
- "error": e.error,
144
- "child_run_id": e.child_run_id,
145
- }
146
-
147
- return {"status": "success"}
148
-
149
-
150
- async def demo_nesting():
151
- """Demo: Nested child workflows."""
152
- print("\n" + "=" * 50)
153
- print("DEMO 1: Nested Child Workflows (3 levels)")
154
- print("=" * 50 + "\n")
155
-
156
- run_id = await start(level_1_workflow)
157
- run = await get_workflow_run(run_id)
158
-
159
- print(f"\nResult: {run.result}")
160
-
161
-
162
- async def demo_parallel():
163
- """Demo: Parallel child workflows."""
164
- print("\n" + "=" * 50)
165
- print("DEMO 2: Parallel Child Workflows")
166
- print("=" * 50 + "\n")
167
-
168
- run_id = await start(parallel_parent_workflow)
169
- run = await get_workflow_run(run_id)
170
-
171
- print(f"\nResult: {run.result}")
172
-
173
-
174
- async def demo_error_handling():
175
- """Demo: Error handling with ChildWorkflowFailedError."""
176
- print("\n" + "=" * 50)
177
- print("DEMO 3: Child Workflow Error Handling")
178
- print("=" * 50 + "\n")
179
-
180
- run_id = await start(error_handling_parent_workflow)
181
- run = await get_workflow_run(run_id)
182
-
183
- print(f"\nResult: {run.result}")
184
-
185
-
186
- async def demo_max_nesting_depth():
187
- """Demo: Max nesting depth enforcement."""
188
- print("\n" + "=" * 50)
189
- print("DEMO 4: Max Nesting Depth (3 levels max)")
190
- print("=" * 50 + "\n")
191
-
192
- # Define a workflow that tries to nest too deep
193
- @workflow(durable=True)
194
- async def level_4_workflow() -> dict:
195
- """This would be nesting depth 4 - not allowed!"""
196
- return await do_work("level4")
197
-
198
- @workflow(durable=True)
199
- async def try_level_4_workflow() -> dict:
200
- """Try to spawn a level 4 child (should fail)."""
201
- # This is called from level 3, so it would be depth 4
202
- try:
203
- await start_child_workflow(level_4_workflow)
204
- except MaxNestingDepthError as e:
205
- print(f" Caught MaxNestingDepthError: {e}")
206
- return {"error": str(e), "max_depth": e.MAX_DEPTH}
207
- return {"status": "success"}
208
-
209
- @workflow(durable=True)
210
- async def deep_nesting_workflow() -> dict:
211
- """Workflow that attempts deep nesting."""
212
- print("[DeepNesting] Attempting 4-level nesting...")
213
-
214
- # Level 1 -> Level 2
215
- @workflow(durable=True)
216
- async def level_2() -> dict:
217
- print(" [Level2] Starting...")
218
-
219
- # Level 2 -> Level 3
220
- @workflow(durable=True)
221
- async def level_3() -> dict:
222
- print(" [Level3] Starting...")
223
- # Level 3 -> Level 4 (should fail!)
224
- return await start_child_workflow(try_level_4_workflow)
225
-
226
- return await start_child_workflow(level_3)
227
-
228
- return await start_child_workflow(level_2)
229
-
230
- run_id = await start(deep_nesting_workflow)
231
- await asyncio.sleep(0.5) # Wait for nested workflows
232
- run = await get_workflow_run(run_id)
233
-
234
- print(f"\nResult: {run.result}")
235
- print(
236
- "\nNote: Max nesting depth is 3 levels (root=0, child=1, grandchild=2, great-grandchild=3)"
237
- )
238
-
239
-
240
- async def main():
241
- # Configure with InMemoryStorageBackend
242
- reset_config()
243
- storage = InMemoryStorageBackend()
244
- configure(storage=storage, default_durable=True)
245
-
246
- print("=== Child Workflows - Advanced Patterns ===")
247
-
248
- await demo_nesting()
249
- await demo_parallel()
250
- await demo_error_handling()
251
- # Skip max nesting demo as it requires proper registration
252
- # await demo_max_nesting_depth()
253
-
254
- print("\n" + "=" * 50)
255
- print("=== Key Takeaways ===")
256
- print("=" * 50)
257
- print("1. Child workflows can spawn their own children (up to depth 3)")
258
- print("2. Use wait_for_completion=False + handle.result() for parallel")
259
- print("3. ChildWorkflowFailedError propagates child failures to parent")
260
- print("4. MaxNestingDepthError prevents infinite nesting")
261
- print("5. TERMINATE policy ensures cleanup on parent completion")
262
-
263
-
264
- if __name__ == "__main__":
265
- asyncio.run(main())