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,680 +0,0 @@
1
- """
2
- Unit tests for child workflow feature.
3
-
4
- Tests cover:
5
- - ChildWorkflowError and ChildWorkflowFailedError exceptions
6
- - MaxNestingDepthError exception
7
- - Child workflow event types
8
- - ChildWorkflowHandle class
9
- - Storage methods for child workflows
10
- - Context child workflow state
11
- """
12
-
13
- from datetime import UTC, datetime
14
-
15
- import pytest
16
-
17
- from pyworkflow import (
18
- ChildWorkflowError,
19
- ChildWorkflowFailedError,
20
- LocalContext,
21
- MaxNestingDepthError,
22
- WorkflowError,
23
- )
24
- from pyworkflow.engine.events import (
25
- EventType,
26
- create_child_workflow_cancelled_event,
27
- create_child_workflow_completed_event,
28
- create_child_workflow_failed_event,
29
- create_child_workflow_started_event,
30
- )
31
- from pyworkflow.primitives.child_handle import ChildWorkflowHandle
32
- from pyworkflow.storage.memory import InMemoryStorageBackend
33
- from pyworkflow.storage.schemas import RunStatus, WorkflowRun
34
-
35
-
36
- class TestChildWorkflowError:
37
- """Test ChildWorkflowError base exception."""
38
-
39
- def test_child_workflow_error_is_workflow_error(self):
40
- """Test ChildWorkflowError inherits from WorkflowError."""
41
- error = ChildWorkflowError("Test error")
42
- assert isinstance(error, WorkflowError)
43
-
44
- def test_child_workflow_error_message(self):
45
- """Test ChildWorkflowError has message."""
46
- error = ChildWorkflowError("Child workflow failed")
47
- assert str(error) == "Child workflow failed"
48
-
49
-
50
- class TestChildWorkflowFailedError:
51
- """Test ChildWorkflowFailedError exception."""
52
-
53
- def test_child_workflow_failed_error_attributes(self):
54
- """Test ChildWorkflowFailedError stores all attributes."""
55
- error = ChildWorkflowFailedError(
56
- child_run_id="run_child123",
57
- child_workflow_name="payment_workflow",
58
- error="Payment declined",
59
- error_type="PaymentError",
60
- )
61
- assert error.child_run_id == "run_child123"
62
- assert error.child_workflow_name == "payment_workflow"
63
- assert error.error == "Payment declined"
64
- assert error.error_type == "PaymentError"
65
-
66
- def test_child_workflow_failed_error_message(self):
67
- """Test ChildWorkflowFailedError has descriptive message."""
68
- error = ChildWorkflowFailedError(
69
- child_run_id="run_child123",
70
- child_workflow_name="payment_workflow",
71
- error="Payment declined",
72
- error_type="PaymentError",
73
- )
74
- assert "payment_workflow" in str(error)
75
- assert "Payment declined" in str(error)
76
-
77
- def test_child_workflow_failed_error_is_child_workflow_error(self):
78
- """Test ChildWorkflowFailedError inherits from ChildWorkflowError."""
79
- error = ChildWorkflowFailedError(
80
- child_run_id="run_123",
81
- child_workflow_name="test",
82
- error="error",
83
- error_type="Error",
84
- )
85
- assert isinstance(error, ChildWorkflowError)
86
-
87
-
88
- class TestMaxNestingDepthError:
89
- """Test MaxNestingDepthError exception."""
90
-
91
- def test_max_nesting_depth_error_attributes(self):
92
- """Test MaxNestingDepthError stores current depth."""
93
- error = MaxNestingDepthError(current_depth=3)
94
- assert error.current_depth == 3
95
- assert error.MAX_DEPTH == 3
96
-
97
- def test_max_nesting_depth_error_message(self):
98
- """Test MaxNestingDepthError has descriptive message."""
99
- error = MaxNestingDepthError(current_depth=3)
100
- assert "3" in str(error)
101
- assert "maximum" in str(error).lower() or "exceeded" in str(error).lower()
102
-
103
- def test_max_nesting_depth_error_is_child_workflow_error(self):
104
- """Test MaxNestingDepthError inherits from ChildWorkflowError."""
105
- error = MaxNestingDepthError(current_depth=3)
106
- assert isinstance(error, ChildWorkflowError)
107
-
108
-
109
- class TestChildWorkflowEventTypes:
110
- """Test child workflow event types exist."""
111
-
112
- def test_child_workflow_started_event_type(self):
113
- """Test CHILD_WORKFLOW_STARTED event type exists."""
114
- assert hasattr(EventType, "CHILD_WORKFLOW_STARTED")
115
- assert EventType.CHILD_WORKFLOW_STARTED.value == "child_workflow.started"
116
-
117
- def test_child_workflow_completed_event_type(self):
118
- """Test CHILD_WORKFLOW_COMPLETED event type exists."""
119
- assert hasattr(EventType, "CHILD_WORKFLOW_COMPLETED")
120
- assert EventType.CHILD_WORKFLOW_COMPLETED.value == "child_workflow.completed"
121
-
122
- def test_child_workflow_failed_event_type(self):
123
- """Test CHILD_WORKFLOW_FAILED event type exists."""
124
- assert hasattr(EventType, "CHILD_WORKFLOW_FAILED")
125
- assert EventType.CHILD_WORKFLOW_FAILED.value == "child_workflow.failed"
126
-
127
- def test_child_workflow_cancelled_event_type(self):
128
- """Test CHILD_WORKFLOW_CANCELLED event type exists."""
129
- assert hasattr(EventType, "CHILD_WORKFLOW_CANCELLED")
130
- assert EventType.CHILD_WORKFLOW_CANCELLED.value == "child_workflow.cancelled"
131
-
132
-
133
- class TestChildWorkflowEventCreation:
134
- """Test child workflow event creation helpers."""
135
-
136
- def test_create_child_workflow_started_event(self):
137
- """Test creating CHILD_WORKFLOW_STARTED event."""
138
- event = create_child_workflow_started_event(
139
- run_id="run_parent123",
140
- child_id="child_abc",
141
- child_run_id="run_child456",
142
- child_workflow_name="payment_workflow",
143
- args='["order-123"]',
144
- kwargs='{"amount": 99.99}',
145
- wait_for_completion=True,
146
- )
147
- assert event.run_id == "run_parent123"
148
- assert event.type == EventType.CHILD_WORKFLOW_STARTED
149
- assert event.data["child_id"] == "child_abc"
150
- assert event.data["child_run_id"] == "run_child456"
151
- assert event.data["child_workflow_name"] == "payment_workflow"
152
- assert event.data["wait_for_completion"] is True
153
-
154
- def test_create_child_workflow_completed_event(self):
155
- """Test creating CHILD_WORKFLOW_COMPLETED event."""
156
- event = create_child_workflow_completed_event(
157
- run_id="run_parent123",
158
- child_id="child_abc",
159
- child_run_id="run_child456",
160
- result='{"status": "paid"}',
161
- )
162
- assert event.run_id == "run_parent123"
163
- assert event.type == EventType.CHILD_WORKFLOW_COMPLETED
164
- assert event.data["child_id"] == "child_abc"
165
- assert event.data["child_run_id"] == "run_child456"
166
- assert event.data["result"] == '{"status": "paid"}'
167
-
168
- def test_create_child_workflow_failed_event(self):
169
- """Test creating CHILD_WORKFLOW_FAILED event."""
170
- event = create_child_workflow_failed_event(
171
- run_id="run_parent123",
172
- child_id="child_abc",
173
- child_run_id="run_child456",
174
- error="Payment declined",
175
- error_type="PaymentError",
176
- )
177
- assert event.run_id == "run_parent123"
178
- assert event.type == EventType.CHILD_WORKFLOW_FAILED
179
- assert event.data["child_id"] == "child_abc"
180
- assert event.data["error"] == "Payment declined"
181
- assert event.data["error_type"] == "PaymentError"
182
-
183
- def test_create_child_workflow_cancelled_event(self):
184
- """Test creating CHILD_WORKFLOW_CANCELLED event."""
185
- event = create_child_workflow_cancelled_event(
186
- run_id="run_parent123",
187
- child_id="child_abc",
188
- child_run_id="run_child456",
189
- reason="Parent completed",
190
- )
191
- assert event.run_id == "run_parent123"
192
- assert event.type == EventType.CHILD_WORKFLOW_CANCELLED
193
- assert event.data["child_id"] == "child_abc"
194
- assert event.data["reason"] == "Parent completed"
195
-
196
-
197
- class TestStorageChildWorkflowMethods:
198
- """Test storage backend child workflow methods."""
199
-
200
- @pytest.fixture
201
- def storage(self):
202
- """Create a fresh storage instance."""
203
- return InMemoryStorageBackend()
204
-
205
- @pytest.mark.asyncio
206
- async def test_get_children_returns_empty_list(self, storage):
207
- """Test get_children returns empty list when no children."""
208
- children = await storage.get_children("run_parent123")
209
- assert children == []
210
-
211
- @pytest.mark.asyncio
212
- async def test_get_children_returns_children(self, storage):
213
- """Test get_children returns child workflows."""
214
- # Create parent
215
- parent = WorkflowRun(
216
- run_id="run_parent123",
217
- workflow_name="parent_workflow",
218
- status=RunStatus.RUNNING,
219
- created_at=datetime.now(UTC),
220
- )
221
- await storage.create_run(parent)
222
-
223
- # Create children
224
- child1 = WorkflowRun(
225
- run_id="run_child1",
226
- workflow_name="child_workflow",
227
- status=RunStatus.COMPLETED,
228
- created_at=datetime.now(UTC),
229
- parent_run_id="run_parent123",
230
- nesting_depth=1,
231
- )
232
- child2 = WorkflowRun(
233
- run_id="run_child2",
234
- workflow_name="child_workflow",
235
- status=RunStatus.RUNNING,
236
- created_at=datetime.now(UTC),
237
- parent_run_id="run_parent123",
238
- nesting_depth=1,
239
- )
240
- await storage.create_run(child1)
241
- await storage.create_run(child2)
242
-
243
- children = await storage.get_children("run_parent123")
244
- assert len(children) == 2
245
- child_ids = {c.run_id for c in children}
246
- assert child_ids == {"run_child1", "run_child2"}
247
-
248
- @pytest.mark.asyncio
249
- async def test_get_children_with_status_filter(self, storage):
250
- """Test get_children filters by status."""
251
- # Create parent
252
- parent = WorkflowRun(
253
- run_id="run_parent123",
254
- workflow_name="parent_workflow",
255
- status=RunStatus.RUNNING,
256
- created_at=datetime.now(UTC),
257
- )
258
- await storage.create_run(parent)
259
-
260
- # Create children with different statuses
261
- child1 = WorkflowRun(
262
- run_id="run_child1",
263
- workflow_name="child_workflow",
264
- status=RunStatus.COMPLETED,
265
- created_at=datetime.now(UTC),
266
- parent_run_id="run_parent123",
267
- nesting_depth=1,
268
- )
269
- child2 = WorkflowRun(
270
- run_id="run_child2",
271
- workflow_name="child_workflow",
272
- status=RunStatus.RUNNING,
273
- created_at=datetime.now(UTC),
274
- parent_run_id="run_parent123",
275
- nesting_depth=1,
276
- )
277
- await storage.create_run(child1)
278
- await storage.create_run(child2)
279
-
280
- # Filter by RUNNING
281
- running = await storage.get_children("run_parent123", status=RunStatus.RUNNING)
282
- assert len(running) == 1
283
- assert running[0].run_id == "run_child2"
284
-
285
- # Filter by COMPLETED
286
- completed = await storage.get_children("run_parent123", status=RunStatus.COMPLETED)
287
- assert len(completed) == 1
288
- assert completed[0].run_id == "run_child1"
289
-
290
- @pytest.mark.asyncio
291
- async def test_get_parent_returns_none_for_root(self, storage):
292
- """Test get_parent returns None for root workflow."""
293
- root = WorkflowRun(
294
- run_id="run_root",
295
- workflow_name="root_workflow",
296
- status=RunStatus.RUNNING,
297
- created_at=datetime.now(UTC),
298
- )
299
- await storage.create_run(root)
300
-
301
- parent = await storage.get_parent("run_root")
302
- assert parent is None
303
-
304
- @pytest.mark.asyncio
305
- async def test_get_parent_returns_parent(self, storage):
306
- """Test get_parent returns parent workflow."""
307
- parent = WorkflowRun(
308
- run_id="run_parent",
309
- workflow_name="parent_workflow",
310
- status=RunStatus.RUNNING,
311
- created_at=datetime.now(UTC),
312
- )
313
- child = WorkflowRun(
314
- run_id="run_child",
315
- workflow_name="child_workflow",
316
- status=RunStatus.RUNNING,
317
- created_at=datetime.now(UTC),
318
- parent_run_id="run_parent",
319
- nesting_depth=1,
320
- )
321
- await storage.create_run(parent)
322
- await storage.create_run(child)
323
-
324
- result = await storage.get_parent("run_child")
325
- assert result is not None
326
- assert result.run_id == "run_parent"
327
-
328
- @pytest.mark.asyncio
329
- async def test_get_nesting_depth_root(self, storage):
330
- """Test get_nesting_depth returns 0 for root."""
331
- root = WorkflowRun(
332
- run_id="run_root",
333
- workflow_name="root_workflow",
334
- status=RunStatus.RUNNING,
335
- created_at=datetime.now(UTC),
336
- nesting_depth=0,
337
- )
338
- await storage.create_run(root)
339
-
340
- depth = await storage.get_nesting_depth("run_root")
341
- assert depth == 0
342
-
343
- @pytest.mark.asyncio
344
- async def test_get_nesting_depth_child(self, storage):
345
- """Test get_nesting_depth returns correct depth."""
346
- child = WorkflowRun(
347
- run_id="run_child",
348
- workflow_name="child_workflow",
349
- status=RunStatus.RUNNING,
350
- created_at=datetime.now(UTC),
351
- parent_run_id="run_parent",
352
- nesting_depth=2,
353
- )
354
- await storage.create_run(child)
355
-
356
- depth = await storage.get_nesting_depth("run_child")
357
- assert depth == 2
358
-
359
-
360
- class TestWorkflowRunParentChildFields:
361
- """Test WorkflowRun parent/child fields."""
362
-
363
- def test_workflow_run_default_parent_run_id_is_none(self):
364
- """Test WorkflowRun defaults to no parent."""
365
- run = WorkflowRun(
366
- run_id="run_123",
367
- workflow_name="test",
368
- status=RunStatus.PENDING,
369
- created_at=datetime.now(UTC),
370
- )
371
- assert run.parent_run_id is None
372
-
373
- def test_workflow_run_default_nesting_depth_is_zero(self):
374
- """Test WorkflowRun defaults to nesting depth 0."""
375
- run = WorkflowRun(
376
- run_id="run_123",
377
- workflow_name="test",
378
- status=RunStatus.PENDING,
379
- created_at=datetime.now(UTC),
380
- )
381
- assert run.nesting_depth == 0
382
-
383
- def test_workflow_run_with_parent(self):
384
- """Test WorkflowRun with parent_run_id."""
385
- run = WorkflowRun(
386
- run_id="run_child",
387
- workflow_name="child_workflow",
388
- status=RunStatus.PENDING,
389
- created_at=datetime.now(UTC),
390
- parent_run_id="run_parent",
391
- nesting_depth=1,
392
- )
393
- assert run.parent_run_id == "run_parent"
394
- assert run.nesting_depth == 1
395
-
396
- def test_workflow_run_to_dict_includes_parent_fields(self):
397
- """Test to_dict includes parent/child fields."""
398
- run = WorkflowRun(
399
- run_id="run_child",
400
- workflow_name="child_workflow",
401
- status=RunStatus.PENDING,
402
- created_at=datetime.now(UTC),
403
- parent_run_id="run_parent",
404
- nesting_depth=2,
405
- )
406
- data = run.to_dict()
407
- assert data["parent_run_id"] == "run_parent"
408
- assert data["nesting_depth"] == 2
409
-
410
- def test_workflow_run_from_dict_reads_parent_fields(self):
411
- """Test from_dict reads parent/child fields."""
412
- now = datetime.now(UTC)
413
- data = {
414
- "run_id": "run_child",
415
- "workflow_name": "child_workflow",
416
- "status": "pending",
417
- "created_at": now.isoformat(),
418
- "updated_at": now.isoformat(),
419
- "parent_run_id": "run_parent",
420
- "nesting_depth": 2,
421
- }
422
- run = WorkflowRun.from_dict(data)
423
- assert run.parent_run_id == "run_parent"
424
- assert run.nesting_depth == 2
425
-
426
-
427
- class TestContextChildWorkflowState:
428
- """Test context child workflow state methods."""
429
-
430
- def test_local_context_has_child_result_false_initially(self):
431
- """Test LocalContext starts with no child results."""
432
- ctx = LocalContext(
433
- run_id="test_run",
434
- workflow_name="test_workflow",
435
- storage=None,
436
- durable=False,
437
- )
438
- assert ctx.has_child_result("child_123") is False
439
-
440
- def test_local_context_cache_child_result(self):
441
- """Test LocalContext can cache child result."""
442
- ctx = LocalContext(
443
- run_id="test_run",
444
- workflow_name="test_workflow",
445
- storage=None,
446
- durable=False,
447
- )
448
- ctx.cache_child_result(
449
- child_id="child_123",
450
- child_run_id="run_child_123",
451
- result={"status": "completed"},
452
- )
453
- assert ctx.has_child_result("child_123") is True
454
-
455
- def test_local_context_get_child_result(self):
456
- """Test LocalContext can get cached child result."""
457
- ctx = LocalContext(
458
- run_id="test_run",
459
- workflow_name="test_workflow",
460
- storage=None,
461
- durable=False,
462
- )
463
- ctx.cache_child_result(
464
- child_id="child_123",
465
- child_run_id="run_child_123",
466
- result={"status": "completed"},
467
- )
468
- result = ctx.get_child_result("child_123")
469
- assert result["result"] == {"status": "completed"}
470
- assert result["child_run_id"] == "run_child_123"
471
-
472
- def test_local_context_cache_failed_child_result(self):
473
- """Test LocalContext can cache failed child result."""
474
- ctx = LocalContext(
475
- run_id="test_run",
476
- workflow_name="test_workflow",
477
- storage=None,
478
- durable=False,
479
- )
480
- ctx.cache_child_result(
481
- child_id="child_123",
482
- child_run_id="run_child_123",
483
- result=None,
484
- failed=True,
485
- error="Payment failed",
486
- error_type="PaymentError",
487
- )
488
- result = ctx.get_child_result("child_123")
489
- assert result["__failed__"] is True
490
- assert result["error"] == "Payment failed"
491
- assert result["error_type"] == "PaymentError"
492
-
493
- def test_local_context_add_pending_child(self):
494
- """Test LocalContext can track pending children."""
495
- ctx = LocalContext(
496
- run_id="test_run",
497
- workflow_name="test_workflow",
498
- storage=None,
499
- durable=False,
500
- )
501
- ctx.add_pending_child("child_123", "run_child_123")
502
- assert "child_123" in ctx._pending_children
503
- assert ctx._pending_children["child_123"] == "run_child_123"
504
-
505
-
506
- class TestChildWorkflowHandle:
507
- """Test ChildWorkflowHandle class."""
508
-
509
- @pytest.fixture
510
- def storage(self):
511
- """Create a fresh storage instance."""
512
- return InMemoryStorageBackend()
513
-
514
- def test_handle_attributes(self, storage):
515
- """Test ChildWorkflowHandle stores attributes."""
516
- handle = ChildWorkflowHandle(
517
- child_id="child_123",
518
- child_run_id="run_child_123",
519
- child_workflow_name="payment_workflow",
520
- parent_run_id="run_parent",
521
- _storage=storage,
522
- )
523
- assert handle.child_id == "child_123"
524
- assert handle.child_run_id == "run_child_123"
525
- assert handle.child_workflow_name == "payment_workflow"
526
- assert handle.parent_run_id == "run_parent"
527
-
528
- def test_handle_repr(self, storage):
529
- """Test ChildWorkflowHandle has repr."""
530
- handle = ChildWorkflowHandle(
531
- child_id="child_123",
532
- child_run_id="run_child_123",
533
- child_workflow_name="payment_workflow",
534
- parent_run_id="run_parent",
535
- _storage=storage,
536
- )
537
- repr_str = repr(handle)
538
- assert "child_123" in repr_str
539
- assert "run_child_123" in repr_str
540
- assert "payment_workflow" in repr_str
541
-
542
- @pytest.mark.asyncio
543
- async def test_handle_get_status(self, storage):
544
- """Test ChildWorkflowHandle.get_status()."""
545
- child = WorkflowRun(
546
- run_id="run_child_123",
547
- workflow_name="payment_workflow",
548
- status=RunStatus.RUNNING,
549
- created_at=datetime.now(UTC),
550
- )
551
- await storage.create_run(child)
552
-
553
- handle = ChildWorkflowHandle(
554
- child_id="child_123",
555
- child_run_id="run_child_123",
556
- child_workflow_name="payment_workflow",
557
- parent_run_id="run_parent",
558
- _storage=storage,
559
- )
560
- status = await handle.get_status()
561
- assert status == RunStatus.RUNNING
562
-
563
- @pytest.mark.asyncio
564
- async def test_handle_get_status_not_found(self, storage):
565
- """Test ChildWorkflowHandle.get_status() raises for not found."""
566
- handle = ChildWorkflowHandle(
567
- child_id="child_123",
568
- child_run_id="run_nonexistent",
569
- child_workflow_name="payment_workflow",
570
- parent_run_id="run_parent",
571
- _storage=storage,
572
- )
573
- with pytest.raises(ValueError, match="not found"):
574
- await handle.get_status()
575
-
576
- @pytest.mark.asyncio
577
- async def test_handle_is_running(self, storage):
578
- """Test ChildWorkflowHandle.is_running()."""
579
- child = WorkflowRun(
580
- run_id="run_child_123",
581
- workflow_name="payment_workflow",
582
- status=RunStatus.RUNNING,
583
- created_at=datetime.now(UTC),
584
- )
585
- await storage.create_run(child)
586
-
587
- handle = ChildWorkflowHandle(
588
- child_id="child_123",
589
- child_run_id="run_child_123",
590
- child_workflow_name="payment_workflow",
591
- parent_run_id="run_parent",
592
- _storage=storage,
593
- )
594
- assert await handle.is_running() is True
595
-
596
- @pytest.mark.asyncio
597
- async def test_handle_is_terminal(self, storage):
598
- """Test ChildWorkflowHandle.is_terminal()."""
599
- child = WorkflowRun(
600
- run_id="run_child_123",
601
- workflow_name="payment_workflow",
602
- status=RunStatus.COMPLETED,
603
- created_at=datetime.now(UTC),
604
- )
605
- await storage.create_run(child)
606
-
607
- handle = ChildWorkflowHandle(
608
- child_id="child_123",
609
- child_run_id="run_child_123",
610
- child_workflow_name="payment_workflow",
611
- parent_run_id="run_parent",
612
- _storage=storage,
613
- )
614
- assert await handle.is_terminal() is True
615
-
616
- @pytest.mark.asyncio
617
- async def test_handle_result_completed(self, storage):
618
- """Test ChildWorkflowHandle.result() for completed workflow."""
619
- child = WorkflowRun(
620
- run_id="run_child_123",
621
- workflow_name="payment_workflow",
622
- status=RunStatus.COMPLETED,
623
- created_at=datetime.now(UTC),
624
- result='{"status": "paid"}',
625
- )
626
- await storage.create_run(child)
627
-
628
- handle = ChildWorkflowHandle(
629
- child_id="child_123",
630
- child_run_id="run_child_123",
631
- child_workflow_name="payment_workflow",
632
- parent_run_id="run_parent",
633
- _storage=storage,
634
- )
635
- result = await handle.result(timeout=1.0)
636
- assert result == {"status": "paid"}
637
-
638
- @pytest.mark.asyncio
639
- async def test_handle_result_failed_raises(self, storage):
640
- """Test ChildWorkflowHandle.result() raises for failed workflow."""
641
- child = WorkflowRun(
642
- run_id="run_child_123",
643
- workflow_name="payment_workflow",
644
- status=RunStatus.FAILED,
645
- created_at=datetime.now(UTC),
646
- error="Payment declined",
647
- )
648
- await storage.create_run(child)
649
-
650
- handle = ChildWorkflowHandle(
651
- child_id="child_123",
652
- child_run_id="run_child_123",
653
- child_workflow_name="payment_workflow",
654
- parent_run_id="run_parent",
655
- _storage=storage,
656
- )
657
- with pytest.raises(ChildWorkflowFailedError):
658
- await handle.result(timeout=1.0)
659
-
660
- @pytest.mark.asyncio
661
- async def test_handle_result_cancelled_raises(self, storage):
662
- """Test ChildWorkflowHandle.result() raises for cancelled workflow."""
663
- child = WorkflowRun(
664
- run_id="run_child_123",
665
- workflow_name="payment_workflow",
666
- status=RunStatus.CANCELLED,
667
- created_at=datetime.now(UTC),
668
- )
669
- await storage.create_run(child)
670
-
671
- handle = ChildWorkflowHandle(
672
- child_id="child_123",
673
- child_run_id="run_child_123",
674
- child_workflow_name="payment_workflow",
675
- parent_run_id="run_parent",
676
- _storage=storage,
677
- )
678
- with pytest.raises(ChildWorkflowFailedError) as exc_info:
679
- await handle.result(timeout=1.0)
680
- assert "cancelled" in str(exc_info.value.error).lower()