pyworkflow-engine 0.1.7__py3-none-any.whl → 0.1.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyworkflow/__init__.py +10 -1
- pyworkflow/celery/tasks.py +272 -24
- pyworkflow/cli/__init__.py +4 -1
- pyworkflow/cli/commands/runs.py +4 -4
- pyworkflow/cli/commands/setup.py +203 -4
- pyworkflow/cli/utils/config_generator.py +76 -3
- pyworkflow/cli/utils/docker_manager.py +232 -0
- pyworkflow/config.py +94 -17
- pyworkflow/context/__init__.py +13 -0
- pyworkflow/context/base.py +26 -0
- pyworkflow/context/local.py +80 -0
- pyworkflow/context/step_context.py +295 -0
- pyworkflow/core/registry.py +6 -1
- pyworkflow/core/step.py +141 -0
- pyworkflow/core/workflow.py +56 -0
- pyworkflow/engine/events.py +30 -0
- pyworkflow/engine/replay.py +39 -0
- pyworkflow/primitives/child_workflow.py +1 -1
- pyworkflow/runtime/local.py +1 -1
- pyworkflow/storage/__init__.py +14 -0
- pyworkflow/storage/base.py +35 -0
- pyworkflow/storage/cassandra.py +1747 -0
- pyworkflow/storage/config.py +69 -0
- pyworkflow/storage/dynamodb.py +31 -2
- pyworkflow/storage/file.py +28 -0
- pyworkflow/storage/memory.py +18 -0
- pyworkflow/storage/mysql.py +1159 -0
- pyworkflow/storage/postgres.py +27 -2
- pyworkflow/storage/schemas.py +4 -3
- pyworkflow/storage/sqlite.py +25 -2
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/METADATA +7 -4
- pyworkflow_engine-0.1.10.dist-info/RECORD +91 -0
- pyworkflow_engine-0.1.10.dist-info/top_level.txt +1 -0
- dashboard/backend/app/__init__.py +0 -1
- dashboard/backend/app/config.py +0 -32
- dashboard/backend/app/controllers/__init__.py +0 -6
- dashboard/backend/app/controllers/run_controller.py +0 -86
- dashboard/backend/app/controllers/workflow_controller.py +0 -33
- dashboard/backend/app/dependencies/__init__.py +0 -5
- dashboard/backend/app/dependencies/storage.py +0 -50
- dashboard/backend/app/repositories/__init__.py +0 -6
- dashboard/backend/app/repositories/run_repository.py +0 -80
- dashboard/backend/app/repositories/workflow_repository.py +0 -27
- dashboard/backend/app/rest/__init__.py +0 -8
- dashboard/backend/app/rest/v1/__init__.py +0 -12
- dashboard/backend/app/rest/v1/health.py +0 -33
- dashboard/backend/app/rest/v1/runs.py +0 -133
- dashboard/backend/app/rest/v1/workflows.py +0 -41
- dashboard/backend/app/schemas/__init__.py +0 -23
- dashboard/backend/app/schemas/common.py +0 -16
- dashboard/backend/app/schemas/event.py +0 -24
- dashboard/backend/app/schemas/hook.py +0 -25
- dashboard/backend/app/schemas/run.py +0 -54
- dashboard/backend/app/schemas/step.py +0 -28
- dashboard/backend/app/schemas/workflow.py +0 -31
- dashboard/backend/app/server.py +0 -87
- dashboard/backend/app/services/__init__.py +0 -6
- dashboard/backend/app/services/run_service.py +0 -240
- dashboard/backend/app/services/workflow_service.py +0 -155
- dashboard/backend/main.py +0 -18
- docs/concepts/cancellation.mdx +0 -362
- docs/concepts/continue-as-new.mdx +0 -434
- docs/concepts/events.mdx +0 -266
- docs/concepts/fault-tolerance.mdx +0 -370
- docs/concepts/hooks.mdx +0 -552
- docs/concepts/limitations.mdx +0 -167
- docs/concepts/schedules.mdx +0 -775
- docs/concepts/sleep.mdx +0 -312
- docs/concepts/steps.mdx +0 -301
- docs/concepts/workflows.mdx +0 -255
- docs/guides/cli.mdx +0 -942
- docs/guides/configuration.mdx +0 -560
- docs/introduction.mdx +0 -155
- docs/quickstart.mdx +0 -279
- examples/__init__.py +0 -1
- examples/celery/__init__.py +0 -1
- examples/celery/durable/docker-compose.yml +0 -55
- examples/celery/durable/pyworkflow.config.yaml +0 -12
- examples/celery/durable/workflows/__init__.py +0 -122
- examples/celery/durable/workflows/basic.py +0 -87
- examples/celery/durable/workflows/batch_processing.py +0 -102
- examples/celery/durable/workflows/cancellation.py +0 -273
- examples/celery/durable/workflows/child_workflow_patterns.py +0 -240
- examples/celery/durable/workflows/child_workflows.py +0 -202
- examples/celery/durable/workflows/continue_as_new.py +0 -260
- examples/celery/durable/workflows/fault_tolerance.py +0 -210
- examples/celery/durable/workflows/hooks.py +0 -211
- examples/celery/durable/workflows/idempotency.py +0 -112
- examples/celery/durable/workflows/long_running.py +0 -99
- examples/celery/durable/workflows/retries.py +0 -101
- examples/celery/durable/workflows/schedules.py +0 -209
- examples/celery/transient/01_basic_workflow.py +0 -91
- examples/celery/transient/02_fault_tolerance.py +0 -257
- examples/celery/transient/__init__.py +0 -20
- examples/celery/transient/pyworkflow.config.yaml +0 -25
- examples/local/__init__.py +0 -1
- examples/local/durable/01_basic_workflow.py +0 -94
- examples/local/durable/02_file_storage.py +0 -132
- examples/local/durable/03_retries.py +0 -169
- examples/local/durable/04_long_running.py +0 -119
- examples/local/durable/05_event_log.py +0 -145
- examples/local/durable/06_idempotency.py +0 -148
- examples/local/durable/07_hooks.py +0 -334
- examples/local/durable/08_cancellation.py +0 -233
- examples/local/durable/09_child_workflows.py +0 -198
- examples/local/durable/10_child_workflow_patterns.py +0 -265
- examples/local/durable/11_continue_as_new.py +0 -249
- examples/local/durable/12_schedules.py +0 -198
- examples/local/durable/__init__.py +0 -1
- examples/local/transient/01_quick_tasks.py +0 -87
- examples/local/transient/02_retries.py +0 -130
- examples/local/transient/03_sleep.py +0 -141
- examples/local/transient/__init__.py +0 -1
- pyworkflow_engine-0.1.7.dist-info/RECORD +0 -196
- pyworkflow_engine-0.1.7.dist-info/top_level.txt +0 -5
- tests/examples/__init__.py +0 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_cancellation.py +0 -330
- tests/integration/test_child_workflows.py +0 -439
- tests/integration/test_continue_as_new.py +0 -428
- tests/integration/test_dynamodb_storage.py +0 -1146
- tests/integration/test_fault_tolerance.py +0 -369
- tests/integration/test_schedule_storage.py +0 -484
- tests/unit/__init__.py +0 -0
- tests/unit/backends/__init__.py +0 -1
- tests/unit/backends/test_dynamodb_storage.py +0 -1554
- tests/unit/backends/test_postgres_storage.py +0 -1281
- tests/unit/backends/test_sqlite_storage.py +0 -1460
- tests/unit/conftest.py +0 -41
- tests/unit/test_cancellation.py +0 -364
- tests/unit/test_child_workflows.py +0 -680
- tests/unit/test_continue_as_new.py +0 -441
- tests/unit/test_event_limits.py +0 -316
- tests/unit/test_executor.py +0 -320
- tests/unit/test_fault_tolerance.py +0 -334
- tests/unit/test_hooks.py +0 -495
- tests/unit/test_registry.py +0 -261
- tests/unit/test_replay.py +0 -420
- tests/unit/test_schedule_schemas.py +0 -285
- tests/unit/test_schedule_utils.py +0 -286
- tests/unit/test_scheduled_workflow.py +0 -274
- tests/unit/test_step.py +0 -353
- tests/unit/test_workflow.py +0 -243
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/WHEEL +0 -0
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/entry_points.txt +0 -0
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -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()
|