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,1146 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Integration tests for DynamoDB storage backend.
|
|
3
|
-
|
|
4
|
-
These tests require DynamoDB Local to be running on localhost:8000.
|
|
5
|
-
To start DynamoDB Local:
|
|
6
|
-
docker run -p 8000:8000 amazon/dynamodb-local -jar DynamoDBLocal.jar -sharedDb -inMemory
|
|
7
|
-
|
|
8
|
-
Tests will be skipped if DynamoDB Local is not available.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import socket
|
|
12
|
-
from datetime import UTC, datetime, timedelta
|
|
13
|
-
|
|
14
|
-
import pytest
|
|
15
|
-
|
|
16
|
-
# Skip all tests if dependencies are not installed
|
|
17
|
-
pytest.importorskip("aiobotocore")
|
|
18
|
-
|
|
19
|
-
from pyworkflow.engine.events import Event, EventType
|
|
20
|
-
from pyworkflow.storage.dynamodb import DynamoDBStorageBackend
|
|
21
|
-
from pyworkflow.storage.schemas import (
|
|
22
|
-
Hook,
|
|
23
|
-
HookStatus,
|
|
24
|
-
RunStatus,
|
|
25
|
-
Schedule,
|
|
26
|
-
ScheduleSpec,
|
|
27
|
-
ScheduleStatus,
|
|
28
|
-
StepExecution,
|
|
29
|
-
StepStatus,
|
|
30
|
-
WorkflowRun,
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def is_dynamodb_local_available(host: str = "localhost", port: int = 8000) -> bool:
|
|
35
|
-
"""Check if DynamoDB Local is available."""
|
|
36
|
-
try:
|
|
37
|
-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
38
|
-
sock.settimeout(1)
|
|
39
|
-
result = sock.connect_ex((host, port))
|
|
40
|
-
sock.close()
|
|
41
|
-
return result == 0
|
|
42
|
-
except Exception:
|
|
43
|
-
return False
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# Skip all tests if DynamoDB Local is not running
|
|
47
|
-
pytestmark = pytest.mark.skipif(
|
|
48
|
-
not is_dynamodb_local_available(),
|
|
49
|
-
reason="DynamoDB Local is not available at localhost:8000. "
|
|
50
|
-
"Start with: docker run -p 8000:8000 amazon/dynamodb-local "
|
|
51
|
-
"-jar DynamoDBLocal.jar -sharedDb -inMemory",
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@pytest.fixture
|
|
56
|
-
def aws_credentials():
|
|
57
|
-
"""Set mock AWS credentials for DynamoDB Local."""
|
|
58
|
-
import os
|
|
59
|
-
|
|
60
|
-
os.environ["AWS_ACCESS_KEY_ID"] = "testing"
|
|
61
|
-
os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
|
|
62
|
-
os.environ["AWS_DEFAULT_REGION"] = "us-east-1"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
@pytest.fixture
|
|
66
|
-
async def dynamodb_storage(aws_credentials):
|
|
67
|
-
"""Create a DynamoDB storage backend connected to DynamoDB Local."""
|
|
68
|
-
import uuid
|
|
69
|
-
|
|
70
|
-
# Use unique table name per test to avoid conflicts
|
|
71
|
-
table_name = f"test_pyworkflow_{uuid.uuid4().hex[:8]}"
|
|
72
|
-
|
|
73
|
-
backend = DynamoDBStorageBackend(
|
|
74
|
-
table_name=table_name,
|
|
75
|
-
region="us-east-1",
|
|
76
|
-
endpoint_url="http://localhost:8000",
|
|
77
|
-
)
|
|
78
|
-
await backend.connect()
|
|
79
|
-
yield backend
|
|
80
|
-
|
|
81
|
-
# Cleanup: delete the test table
|
|
82
|
-
try:
|
|
83
|
-
async with backend._get_client() as client:
|
|
84
|
-
await client.delete_table(TableName=table_name)
|
|
85
|
-
except Exception:
|
|
86
|
-
pass # Table may already be deleted or not exist
|
|
87
|
-
|
|
88
|
-
await backend.disconnect()
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
class TestWorkflowRunCRUD:
|
|
92
|
-
"""Test workflow run CRUD operations."""
|
|
93
|
-
|
|
94
|
-
@pytest.mark.asyncio
|
|
95
|
-
async def test_create_and_get_run(self, dynamodb_storage):
|
|
96
|
-
"""Test creating and retrieving a workflow run."""
|
|
97
|
-
now = datetime.now(UTC)
|
|
98
|
-
run = WorkflowRun(
|
|
99
|
-
run_id="test_run_001",
|
|
100
|
-
workflow_name="test_workflow",
|
|
101
|
-
status=RunStatus.PENDING,
|
|
102
|
-
created_at=now,
|
|
103
|
-
updated_at=now,
|
|
104
|
-
input_args="[]",
|
|
105
|
-
input_kwargs='{"key": "value"}',
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
await dynamodb_storage.create_run(run)
|
|
109
|
-
|
|
110
|
-
retrieved = await dynamodb_storage.get_run("test_run_001")
|
|
111
|
-
assert retrieved is not None
|
|
112
|
-
assert retrieved.run_id == "test_run_001"
|
|
113
|
-
assert retrieved.workflow_name == "test_workflow"
|
|
114
|
-
assert retrieved.status == RunStatus.PENDING
|
|
115
|
-
|
|
116
|
-
@pytest.mark.asyncio
|
|
117
|
-
async def test_get_run_not_found(self, dynamodb_storage):
|
|
118
|
-
"""Test getting a non-existent run."""
|
|
119
|
-
retrieved = await dynamodb_storage.get_run("nonexistent")
|
|
120
|
-
assert retrieved is None
|
|
121
|
-
|
|
122
|
-
@pytest.mark.asyncio
|
|
123
|
-
async def test_update_run_status(self, dynamodb_storage):
|
|
124
|
-
"""Test updating run status."""
|
|
125
|
-
now = datetime.now(UTC)
|
|
126
|
-
run = WorkflowRun(
|
|
127
|
-
run_id="status_update_test",
|
|
128
|
-
workflow_name="test_workflow",
|
|
129
|
-
status=RunStatus.PENDING,
|
|
130
|
-
created_at=now,
|
|
131
|
-
updated_at=now,
|
|
132
|
-
input_args="[]",
|
|
133
|
-
input_kwargs="{}",
|
|
134
|
-
)
|
|
135
|
-
await dynamodb_storage.create_run(run)
|
|
136
|
-
|
|
137
|
-
await dynamodb_storage.update_run_status(
|
|
138
|
-
run_id="status_update_test",
|
|
139
|
-
status=RunStatus.RUNNING,
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
retrieved = await dynamodb_storage.get_run("status_update_test")
|
|
143
|
-
assert retrieved.status == RunStatus.RUNNING
|
|
144
|
-
|
|
145
|
-
@pytest.mark.asyncio
|
|
146
|
-
async def test_update_run_status_with_result(self, dynamodb_storage):
|
|
147
|
-
"""Test updating run status with result."""
|
|
148
|
-
now = datetime.now(UTC)
|
|
149
|
-
run = WorkflowRun(
|
|
150
|
-
run_id="result_test",
|
|
151
|
-
workflow_name="test_workflow",
|
|
152
|
-
status=RunStatus.RUNNING,
|
|
153
|
-
created_at=now,
|
|
154
|
-
updated_at=now,
|
|
155
|
-
input_args="[]",
|
|
156
|
-
input_kwargs="{}",
|
|
157
|
-
)
|
|
158
|
-
await dynamodb_storage.create_run(run)
|
|
159
|
-
|
|
160
|
-
await dynamodb_storage.update_run_status(
|
|
161
|
-
run_id="result_test",
|
|
162
|
-
status=RunStatus.COMPLETED,
|
|
163
|
-
result='{"output": "success"}',
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
retrieved = await dynamodb_storage.get_run("result_test")
|
|
167
|
-
assert retrieved.status == RunStatus.COMPLETED
|
|
168
|
-
assert retrieved.result == '{"output": "success"}'
|
|
169
|
-
|
|
170
|
-
@pytest.mark.asyncio
|
|
171
|
-
async def test_update_run_status_with_error(self, dynamodb_storage):
|
|
172
|
-
"""Test updating run status with error."""
|
|
173
|
-
now = datetime.now(UTC)
|
|
174
|
-
run = WorkflowRun(
|
|
175
|
-
run_id="error_test",
|
|
176
|
-
workflow_name="test_workflow",
|
|
177
|
-
status=RunStatus.RUNNING,
|
|
178
|
-
created_at=now,
|
|
179
|
-
updated_at=now,
|
|
180
|
-
input_args="[]",
|
|
181
|
-
input_kwargs="{}",
|
|
182
|
-
)
|
|
183
|
-
await dynamodb_storage.create_run(run)
|
|
184
|
-
|
|
185
|
-
await dynamodb_storage.update_run_status(
|
|
186
|
-
run_id="error_test",
|
|
187
|
-
status=RunStatus.FAILED,
|
|
188
|
-
error="Test error message",
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
retrieved = await dynamodb_storage.get_run("error_test")
|
|
192
|
-
assert retrieved.status == RunStatus.FAILED
|
|
193
|
-
assert retrieved.error == "Test error message"
|
|
194
|
-
|
|
195
|
-
@pytest.mark.asyncio
|
|
196
|
-
async def test_get_run_by_idempotency_key(self, dynamodb_storage):
|
|
197
|
-
"""Test retrieving run by idempotency key."""
|
|
198
|
-
now = datetime.now(UTC)
|
|
199
|
-
run = WorkflowRun(
|
|
200
|
-
run_id="idempotent_run",
|
|
201
|
-
workflow_name="test_workflow",
|
|
202
|
-
status=RunStatus.PENDING,
|
|
203
|
-
created_at=now,
|
|
204
|
-
updated_at=now,
|
|
205
|
-
input_args="[]",
|
|
206
|
-
input_kwargs="{}",
|
|
207
|
-
idempotency_key="unique_key_123",
|
|
208
|
-
)
|
|
209
|
-
await dynamodb_storage.create_run(run)
|
|
210
|
-
|
|
211
|
-
retrieved = await dynamodb_storage.get_run_by_idempotency_key("unique_key_123")
|
|
212
|
-
assert retrieved is not None
|
|
213
|
-
assert retrieved.run_id == "idempotent_run"
|
|
214
|
-
|
|
215
|
-
@pytest.mark.asyncio
|
|
216
|
-
async def test_get_run_by_idempotency_key_not_found(self, dynamodb_storage):
|
|
217
|
-
"""Test idempotency key lookup when not found."""
|
|
218
|
-
retrieved = await dynamodb_storage.get_run_by_idempotency_key("nonexistent_key")
|
|
219
|
-
assert retrieved is None
|
|
220
|
-
|
|
221
|
-
@pytest.mark.asyncio
|
|
222
|
-
async def test_update_recovery_attempts(self, dynamodb_storage):
|
|
223
|
-
"""Test updating recovery attempts counter."""
|
|
224
|
-
now = datetime.now(UTC)
|
|
225
|
-
run = WorkflowRun(
|
|
226
|
-
run_id="recovery_test",
|
|
227
|
-
workflow_name="test_workflow",
|
|
228
|
-
status=RunStatus.RUNNING,
|
|
229
|
-
created_at=now,
|
|
230
|
-
updated_at=now,
|
|
231
|
-
input_args="[]",
|
|
232
|
-
input_kwargs="{}",
|
|
233
|
-
recovery_attempts=0,
|
|
234
|
-
)
|
|
235
|
-
await dynamodb_storage.create_run(run)
|
|
236
|
-
|
|
237
|
-
await dynamodb_storage.update_run_recovery_attempts("recovery_test", 2)
|
|
238
|
-
|
|
239
|
-
retrieved = await dynamodb_storage.get_run("recovery_test")
|
|
240
|
-
assert retrieved.recovery_attempts == 2
|
|
241
|
-
|
|
242
|
-
@pytest.mark.asyncio
|
|
243
|
-
async def test_list_runs(self, dynamodb_storage):
|
|
244
|
-
"""Test listing workflow runs."""
|
|
245
|
-
now = datetime.now(UTC)
|
|
246
|
-
|
|
247
|
-
# Create multiple runs
|
|
248
|
-
for i in range(5):
|
|
249
|
-
run = WorkflowRun(
|
|
250
|
-
run_id=f"list_test_{i}",
|
|
251
|
-
workflow_name="test_workflow",
|
|
252
|
-
status=RunStatus.PENDING,
|
|
253
|
-
created_at=now + timedelta(seconds=i),
|
|
254
|
-
updated_at=now + timedelta(seconds=i),
|
|
255
|
-
input_args="[]",
|
|
256
|
-
input_kwargs="{}",
|
|
257
|
-
)
|
|
258
|
-
await dynamodb_storage.create_run(run)
|
|
259
|
-
|
|
260
|
-
runs, next_cursor = await dynamodb_storage.list_runs()
|
|
261
|
-
|
|
262
|
-
assert len(runs) == 5
|
|
263
|
-
|
|
264
|
-
@pytest.mark.asyncio
|
|
265
|
-
async def test_list_runs_with_status_filter(self, dynamodb_storage):
|
|
266
|
-
"""Test listing runs filtered by status."""
|
|
267
|
-
now = datetime.now(UTC)
|
|
268
|
-
|
|
269
|
-
# Create runs with different statuses
|
|
270
|
-
for i, status in enumerate([RunStatus.PENDING, RunStatus.RUNNING, RunStatus.COMPLETED]):
|
|
271
|
-
run = WorkflowRun(
|
|
272
|
-
run_id=f"status_filter_{i}",
|
|
273
|
-
workflow_name="test_workflow",
|
|
274
|
-
status=status,
|
|
275
|
-
created_at=now + timedelta(seconds=i),
|
|
276
|
-
updated_at=now + timedelta(seconds=i),
|
|
277
|
-
input_args="[]",
|
|
278
|
-
input_kwargs="{}",
|
|
279
|
-
)
|
|
280
|
-
await dynamodb_storage.create_run(run)
|
|
281
|
-
|
|
282
|
-
runs, _ = await dynamodb_storage.list_runs(status=RunStatus.PENDING)
|
|
283
|
-
|
|
284
|
-
assert len(runs) == 1
|
|
285
|
-
assert runs[0].status == RunStatus.PENDING
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
class TestEventOperations:
|
|
289
|
-
"""Test event log operations."""
|
|
290
|
-
|
|
291
|
-
@pytest.mark.asyncio
|
|
292
|
-
async def test_record_and_get_events(self, dynamodb_storage):
|
|
293
|
-
"""Test recording and retrieving events."""
|
|
294
|
-
now = datetime.now(UTC)
|
|
295
|
-
|
|
296
|
-
# First create a run
|
|
297
|
-
run = WorkflowRun(
|
|
298
|
-
run_id="event_test_run",
|
|
299
|
-
workflow_name="test_workflow",
|
|
300
|
-
status=RunStatus.RUNNING,
|
|
301
|
-
created_at=now,
|
|
302
|
-
updated_at=now,
|
|
303
|
-
input_args="[]",
|
|
304
|
-
input_kwargs="{}",
|
|
305
|
-
)
|
|
306
|
-
await dynamodb_storage.create_run(run)
|
|
307
|
-
|
|
308
|
-
# Record events
|
|
309
|
-
for i in range(3):
|
|
310
|
-
event = Event(
|
|
311
|
-
event_id=f"evt_{i}",
|
|
312
|
-
run_id="event_test_run",
|
|
313
|
-
type=EventType.STEP_COMPLETED,
|
|
314
|
-
timestamp=now + timedelta(seconds=i),
|
|
315
|
-
data={"step_id": f"step_{i}", "result": f"result_{i}"},
|
|
316
|
-
)
|
|
317
|
-
await dynamodb_storage.record_event(event)
|
|
318
|
-
|
|
319
|
-
# Retrieve events
|
|
320
|
-
events = await dynamodb_storage.get_events("event_test_run")
|
|
321
|
-
|
|
322
|
-
assert len(events) == 3
|
|
323
|
-
# Events should be ordered by sequence
|
|
324
|
-
for i, event in enumerate(events):
|
|
325
|
-
assert event.sequence == i
|
|
326
|
-
|
|
327
|
-
@pytest.mark.asyncio
|
|
328
|
-
async def test_get_events_with_type_filter(self, dynamodb_storage):
|
|
329
|
-
"""Test retrieving events filtered by type."""
|
|
330
|
-
now = datetime.now(UTC)
|
|
331
|
-
|
|
332
|
-
run = WorkflowRun(
|
|
333
|
-
run_id="event_filter_run",
|
|
334
|
-
workflow_name="test_workflow",
|
|
335
|
-
status=RunStatus.RUNNING,
|
|
336
|
-
created_at=now,
|
|
337
|
-
updated_at=now,
|
|
338
|
-
input_args="[]",
|
|
339
|
-
input_kwargs="{}",
|
|
340
|
-
)
|
|
341
|
-
await dynamodb_storage.create_run(run)
|
|
342
|
-
|
|
343
|
-
# Record different event types
|
|
344
|
-
events_to_create = [
|
|
345
|
-
(EventType.WORKFLOW_STARTED, {}),
|
|
346
|
-
(EventType.STEP_STARTED, {"step_id": "step_1"}),
|
|
347
|
-
(EventType.STEP_COMPLETED, {"step_id": "step_1", "result": "ok"}),
|
|
348
|
-
(EventType.WORKFLOW_COMPLETED, {"result": "done"}),
|
|
349
|
-
]
|
|
350
|
-
|
|
351
|
-
for i, (event_type, data) in enumerate(events_to_create):
|
|
352
|
-
event = Event(
|
|
353
|
-
event_id=f"evt_{i}",
|
|
354
|
-
run_id="event_filter_run",
|
|
355
|
-
type=event_type,
|
|
356
|
-
timestamp=now + timedelta(seconds=i),
|
|
357
|
-
data=data,
|
|
358
|
-
)
|
|
359
|
-
await dynamodb_storage.record_event(event)
|
|
360
|
-
|
|
361
|
-
# Filter by type
|
|
362
|
-
step_events = await dynamodb_storage.get_events(
|
|
363
|
-
"event_filter_run",
|
|
364
|
-
event_types=["step.completed"],
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
assert len(step_events) == 1
|
|
368
|
-
assert step_events[0].type == EventType.STEP_COMPLETED
|
|
369
|
-
|
|
370
|
-
@pytest.mark.asyncio
|
|
371
|
-
async def test_get_latest_event(self, dynamodb_storage):
|
|
372
|
-
"""Test getting the latest event."""
|
|
373
|
-
now = datetime.now(UTC)
|
|
374
|
-
|
|
375
|
-
run = WorkflowRun(
|
|
376
|
-
run_id="latest_event_run",
|
|
377
|
-
workflow_name="test_workflow",
|
|
378
|
-
status=RunStatus.RUNNING,
|
|
379
|
-
created_at=now,
|
|
380
|
-
updated_at=now,
|
|
381
|
-
input_args="[]",
|
|
382
|
-
input_kwargs="{}",
|
|
383
|
-
)
|
|
384
|
-
await dynamodb_storage.create_run(run)
|
|
385
|
-
|
|
386
|
-
# Record multiple events
|
|
387
|
-
for i in range(5):
|
|
388
|
-
event = Event(
|
|
389
|
-
event_id=f"evt_{i}",
|
|
390
|
-
run_id="latest_event_run",
|
|
391
|
-
type=EventType.STEP_COMPLETED,
|
|
392
|
-
timestamp=now + timedelta(seconds=i),
|
|
393
|
-
data={"index": i},
|
|
394
|
-
)
|
|
395
|
-
await dynamodb_storage.record_event(event)
|
|
396
|
-
|
|
397
|
-
latest = await dynamodb_storage.get_latest_event("latest_event_run")
|
|
398
|
-
|
|
399
|
-
assert latest is not None
|
|
400
|
-
assert latest.sequence == 4 # 0-indexed, so 5th event is sequence 4
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
class TestStepOperations:
|
|
404
|
-
"""Test step execution operations."""
|
|
405
|
-
|
|
406
|
-
@pytest.mark.asyncio
|
|
407
|
-
async def test_create_and_get_step(self, dynamodb_storage):
|
|
408
|
-
"""Test creating and retrieving a step."""
|
|
409
|
-
now = datetime.now(UTC)
|
|
410
|
-
|
|
411
|
-
# First create a run
|
|
412
|
-
run = WorkflowRun(
|
|
413
|
-
run_id="step_test_run",
|
|
414
|
-
workflow_name="test_workflow",
|
|
415
|
-
status=RunStatus.RUNNING,
|
|
416
|
-
created_at=now,
|
|
417
|
-
updated_at=now,
|
|
418
|
-
input_args="[]",
|
|
419
|
-
input_kwargs="{}",
|
|
420
|
-
)
|
|
421
|
-
await dynamodb_storage.create_run(run)
|
|
422
|
-
|
|
423
|
-
step = StepExecution(
|
|
424
|
-
step_id="step_001",
|
|
425
|
-
run_id="step_test_run",
|
|
426
|
-
step_name="process_data",
|
|
427
|
-
status=StepStatus.RUNNING,
|
|
428
|
-
created_at=now,
|
|
429
|
-
started_at=now,
|
|
430
|
-
input_args="[]",
|
|
431
|
-
input_kwargs='{"data": "test"}',
|
|
432
|
-
attempt=1,
|
|
433
|
-
)
|
|
434
|
-
await dynamodb_storage.create_step(step)
|
|
435
|
-
|
|
436
|
-
retrieved = await dynamodb_storage.get_step("step_001")
|
|
437
|
-
assert retrieved is not None
|
|
438
|
-
assert retrieved.step_id == "step_001"
|
|
439
|
-
assert retrieved.step_name == "process_data"
|
|
440
|
-
assert retrieved.status == StepStatus.RUNNING
|
|
441
|
-
|
|
442
|
-
@pytest.mark.asyncio
|
|
443
|
-
async def test_update_step_status(self, dynamodb_storage):
|
|
444
|
-
"""Test updating step status."""
|
|
445
|
-
now = datetime.now(UTC)
|
|
446
|
-
|
|
447
|
-
run = WorkflowRun(
|
|
448
|
-
run_id="step_update_run",
|
|
449
|
-
workflow_name="test_workflow",
|
|
450
|
-
status=RunStatus.RUNNING,
|
|
451
|
-
created_at=now,
|
|
452
|
-
updated_at=now,
|
|
453
|
-
input_args="[]",
|
|
454
|
-
input_kwargs="{}",
|
|
455
|
-
)
|
|
456
|
-
await dynamodb_storage.create_run(run)
|
|
457
|
-
|
|
458
|
-
step = StepExecution(
|
|
459
|
-
step_id="step_update_001",
|
|
460
|
-
run_id="step_update_run",
|
|
461
|
-
step_name="test_step",
|
|
462
|
-
status=StepStatus.RUNNING,
|
|
463
|
-
created_at=now,
|
|
464
|
-
input_args="[]",
|
|
465
|
-
input_kwargs="{}",
|
|
466
|
-
attempt=1,
|
|
467
|
-
)
|
|
468
|
-
await dynamodb_storage.create_step(step)
|
|
469
|
-
|
|
470
|
-
await dynamodb_storage.update_step_status(
|
|
471
|
-
step_id="step_update_001",
|
|
472
|
-
status="completed",
|
|
473
|
-
result='{"output": "success"}',
|
|
474
|
-
)
|
|
475
|
-
|
|
476
|
-
retrieved = await dynamodb_storage.get_step("step_update_001")
|
|
477
|
-
assert retrieved.status == StepStatus.COMPLETED
|
|
478
|
-
assert retrieved.result == '{"output": "success"}'
|
|
479
|
-
|
|
480
|
-
@pytest.mark.asyncio
|
|
481
|
-
async def test_list_steps(self, dynamodb_storage):
|
|
482
|
-
"""Test listing steps for a run."""
|
|
483
|
-
now = datetime.now(UTC)
|
|
484
|
-
|
|
485
|
-
run = WorkflowRun(
|
|
486
|
-
run_id="list_steps_run",
|
|
487
|
-
workflow_name="test_workflow",
|
|
488
|
-
status=RunStatus.RUNNING,
|
|
489
|
-
created_at=now,
|
|
490
|
-
updated_at=now,
|
|
491
|
-
input_args="[]",
|
|
492
|
-
input_kwargs="{}",
|
|
493
|
-
)
|
|
494
|
-
await dynamodb_storage.create_run(run)
|
|
495
|
-
|
|
496
|
-
# Create multiple steps
|
|
497
|
-
for i in range(3):
|
|
498
|
-
step = StepExecution(
|
|
499
|
-
step_id=f"list_step_{i}",
|
|
500
|
-
run_id="list_steps_run",
|
|
501
|
-
step_name=f"step_{i}",
|
|
502
|
-
status=StepStatus.COMPLETED,
|
|
503
|
-
created_at=now + timedelta(seconds=i),
|
|
504
|
-
input_args="[]",
|
|
505
|
-
input_kwargs="{}",
|
|
506
|
-
attempt=1,
|
|
507
|
-
)
|
|
508
|
-
await dynamodb_storage.create_step(step)
|
|
509
|
-
|
|
510
|
-
steps = await dynamodb_storage.list_steps("list_steps_run")
|
|
511
|
-
|
|
512
|
-
assert len(steps) == 3
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
class TestHookOperations:
|
|
516
|
-
"""Test webhook/hook operations."""
|
|
517
|
-
|
|
518
|
-
@pytest.mark.asyncio
|
|
519
|
-
async def test_create_and_get_hook(self, dynamodb_storage):
|
|
520
|
-
"""Test creating and retrieving a hook."""
|
|
521
|
-
now = datetime.now(UTC)
|
|
522
|
-
|
|
523
|
-
run = WorkflowRun(
|
|
524
|
-
run_id="hook_test_run",
|
|
525
|
-
workflow_name="test_workflow",
|
|
526
|
-
status=RunStatus.RUNNING,
|
|
527
|
-
created_at=now,
|
|
528
|
-
updated_at=now,
|
|
529
|
-
input_args="[]",
|
|
530
|
-
input_kwargs="{}",
|
|
531
|
-
)
|
|
532
|
-
await dynamodb_storage.create_run(run)
|
|
533
|
-
|
|
534
|
-
hook = Hook(
|
|
535
|
-
hook_id="hook_001",
|
|
536
|
-
run_id="hook_test_run",
|
|
537
|
-
token="token_abc123",
|
|
538
|
-
status=HookStatus.PENDING,
|
|
539
|
-
created_at=now,
|
|
540
|
-
expires_at=now + timedelta(hours=1),
|
|
541
|
-
)
|
|
542
|
-
await dynamodb_storage.create_hook(hook)
|
|
543
|
-
|
|
544
|
-
retrieved = await dynamodb_storage.get_hook("hook_001")
|
|
545
|
-
assert retrieved is not None
|
|
546
|
-
assert retrieved.hook_id == "hook_001"
|
|
547
|
-
assert retrieved.token == "token_abc123"
|
|
548
|
-
assert retrieved.status == HookStatus.PENDING
|
|
549
|
-
|
|
550
|
-
@pytest.mark.asyncio
|
|
551
|
-
async def test_get_hook_by_token(self, dynamodb_storage):
|
|
552
|
-
"""Test retrieving hook by token."""
|
|
553
|
-
now = datetime.now(UTC)
|
|
554
|
-
|
|
555
|
-
run = WorkflowRun(
|
|
556
|
-
run_id="hook_token_run",
|
|
557
|
-
workflow_name="test_workflow",
|
|
558
|
-
status=RunStatus.RUNNING,
|
|
559
|
-
created_at=now,
|
|
560
|
-
updated_at=now,
|
|
561
|
-
input_args="[]",
|
|
562
|
-
input_kwargs="{}",
|
|
563
|
-
)
|
|
564
|
-
await dynamodb_storage.create_run(run)
|
|
565
|
-
|
|
566
|
-
hook = Hook(
|
|
567
|
-
hook_id="hook_token_001",
|
|
568
|
-
run_id="hook_token_run",
|
|
569
|
-
token="unique_token_xyz",
|
|
570
|
-
status=HookStatus.PENDING,
|
|
571
|
-
created_at=now,
|
|
572
|
-
)
|
|
573
|
-
await dynamodb_storage.create_hook(hook)
|
|
574
|
-
|
|
575
|
-
retrieved = await dynamodb_storage.get_hook_by_token("unique_token_xyz")
|
|
576
|
-
assert retrieved is not None
|
|
577
|
-
assert retrieved.hook_id == "hook_token_001"
|
|
578
|
-
|
|
579
|
-
@pytest.mark.asyncio
|
|
580
|
-
async def test_update_hook_status(self, dynamodb_storage):
|
|
581
|
-
"""Test updating hook status with payload."""
|
|
582
|
-
now = datetime.now(UTC)
|
|
583
|
-
|
|
584
|
-
run = WorkflowRun(
|
|
585
|
-
run_id="hook_update_run",
|
|
586
|
-
workflow_name="test_workflow",
|
|
587
|
-
status=RunStatus.RUNNING,
|
|
588
|
-
created_at=now,
|
|
589
|
-
updated_at=now,
|
|
590
|
-
input_args="[]",
|
|
591
|
-
input_kwargs="{}",
|
|
592
|
-
)
|
|
593
|
-
await dynamodb_storage.create_run(run)
|
|
594
|
-
|
|
595
|
-
hook = Hook(
|
|
596
|
-
hook_id="hook_update_001",
|
|
597
|
-
run_id="hook_update_run",
|
|
598
|
-
token="update_token",
|
|
599
|
-
status=HookStatus.PENDING,
|
|
600
|
-
created_at=now,
|
|
601
|
-
)
|
|
602
|
-
await dynamodb_storage.create_hook(hook)
|
|
603
|
-
|
|
604
|
-
await dynamodb_storage.update_hook_status(
|
|
605
|
-
hook_id="hook_update_001",
|
|
606
|
-
status=HookStatus.RECEIVED,
|
|
607
|
-
payload='{"data": "webhook_payload"}',
|
|
608
|
-
)
|
|
609
|
-
|
|
610
|
-
retrieved = await dynamodb_storage.get_hook("hook_update_001")
|
|
611
|
-
assert retrieved.status == HookStatus.RECEIVED
|
|
612
|
-
assert retrieved.payload == '{"data": "webhook_payload"}'
|
|
613
|
-
assert retrieved.received_at is not None
|
|
614
|
-
|
|
615
|
-
@pytest.mark.asyncio
|
|
616
|
-
async def test_list_hooks(self, dynamodb_storage):
|
|
617
|
-
"""Test listing hooks for a run."""
|
|
618
|
-
now = datetime.now(UTC)
|
|
619
|
-
|
|
620
|
-
run = WorkflowRun(
|
|
621
|
-
run_id="list_hooks_run",
|
|
622
|
-
workflow_name="test_workflow",
|
|
623
|
-
status=RunStatus.RUNNING,
|
|
624
|
-
created_at=now,
|
|
625
|
-
updated_at=now,
|
|
626
|
-
input_args="[]",
|
|
627
|
-
input_kwargs="{}",
|
|
628
|
-
)
|
|
629
|
-
await dynamodb_storage.create_run(run)
|
|
630
|
-
|
|
631
|
-
# Create multiple hooks
|
|
632
|
-
for i in range(3):
|
|
633
|
-
hook = Hook(
|
|
634
|
-
hook_id=f"list_hook_{i}",
|
|
635
|
-
run_id="list_hooks_run",
|
|
636
|
-
token=f"token_{i}",
|
|
637
|
-
status=HookStatus.PENDING,
|
|
638
|
-
created_at=now + timedelta(seconds=i),
|
|
639
|
-
)
|
|
640
|
-
await dynamodb_storage.create_hook(hook)
|
|
641
|
-
|
|
642
|
-
hooks = await dynamodb_storage.list_hooks(run_id="list_hooks_run")
|
|
643
|
-
|
|
644
|
-
assert len(hooks) == 3
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
class TestCancellationOperations:
|
|
648
|
-
"""Test cancellation flag operations."""
|
|
649
|
-
|
|
650
|
-
@pytest.mark.asyncio
|
|
651
|
-
async def test_set_and_check_cancellation_flag(self, dynamodb_storage):
|
|
652
|
-
"""Test setting and checking cancellation flag."""
|
|
653
|
-
now = datetime.now(UTC)
|
|
654
|
-
|
|
655
|
-
run = WorkflowRun(
|
|
656
|
-
run_id="cancel_test_run",
|
|
657
|
-
workflow_name="test_workflow",
|
|
658
|
-
status=RunStatus.RUNNING,
|
|
659
|
-
created_at=now,
|
|
660
|
-
updated_at=now,
|
|
661
|
-
input_args="[]",
|
|
662
|
-
input_kwargs="{}",
|
|
663
|
-
)
|
|
664
|
-
await dynamodb_storage.create_run(run)
|
|
665
|
-
|
|
666
|
-
# Initially no cancellation
|
|
667
|
-
assert await dynamodb_storage.check_cancellation_flag("cancel_test_run") is False
|
|
668
|
-
|
|
669
|
-
# Set cancellation
|
|
670
|
-
await dynamodb_storage.set_cancellation_flag("cancel_test_run")
|
|
671
|
-
|
|
672
|
-
# Now should be cancelled
|
|
673
|
-
assert await dynamodb_storage.check_cancellation_flag("cancel_test_run") is True
|
|
674
|
-
|
|
675
|
-
@pytest.mark.asyncio
|
|
676
|
-
async def test_clear_cancellation_flag(self, dynamodb_storage):
|
|
677
|
-
"""Test clearing cancellation flag."""
|
|
678
|
-
now = datetime.now(UTC)
|
|
679
|
-
|
|
680
|
-
run = WorkflowRun(
|
|
681
|
-
run_id="clear_cancel_run",
|
|
682
|
-
workflow_name="test_workflow",
|
|
683
|
-
status=RunStatus.RUNNING,
|
|
684
|
-
created_at=now,
|
|
685
|
-
updated_at=now,
|
|
686
|
-
input_args="[]",
|
|
687
|
-
input_kwargs="{}",
|
|
688
|
-
)
|
|
689
|
-
await dynamodb_storage.create_run(run)
|
|
690
|
-
|
|
691
|
-
await dynamodb_storage.set_cancellation_flag("clear_cancel_run")
|
|
692
|
-
assert await dynamodb_storage.check_cancellation_flag("clear_cancel_run") is True
|
|
693
|
-
|
|
694
|
-
await dynamodb_storage.clear_cancellation_flag("clear_cancel_run")
|
|
695
|
-
assert await dynamodb_storage.check_cancellation_flag("clear_cancel_run") is False
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
class TestContinueAsNewOperations:
|
|
699
|
-
"""Test continue-as-new chain operations."""
|
|
700
|
-
|
|
701
|
-
@pytest.mark.asyncio
|
|
702
|
-
async def test_update_run_continuation(self, dynamodb_storage):
|
|
703
|
-
"""Test updating run continuation link."""
|
|
704
|
-
now = datetime.now(UTC)
|
|
705
|
-
|
|
706
|
-
# Create original run
|
|
707
|
-
run1 = WorkflowRun(
|
|
708
|
-
run_id="chain_run_1",
|
|
709
|
-
workflow_name="test_workflow",
|
|
710
|
-
status=RunStatus.COMPLETED,
|
|
711
|
-
created_at=now,
|
|
712
|
-
updated_at=now,
|
|
713
|
-
input_args="[]",
|
|
714
|
-
input_kwargs="{}",
|
|
715
|
-
)
|
|
716
|
-
await dynamodb_storage.create_run(run1)
|
|
717
|
-
|
|
718
|
-
# Create continuation run
|
|
719
|
-
run2 = WorkflowRun(
|
|
720
|
-
run_id="chain_run_2",
|
|
721
|
-
workflow_name="test_workflow",
|
|
722
|
-
status=RunStatus.RUNNING,
|
|
723
|
-
created_at=now + timedelta(seconds=1),
|
|
724
|
-
updated_at=now + timedelta(seconds=1),
|
|
725
|
-
input_args="[]",
|
|
726
|
-
input_kwargs="{}",
|
|
727
|
-
continued_from_run_id="chain_run_1",
|
|
728
|
-
)
|
|
729
|
-
await dynamodb_storage.create_run(run2)
|
|
730
|
-
|
|
731
|
-
# Link the runs
|
|
732
|
-
await dynamodb_storage.update_run_continuation("chain_run_1", "chain_run_2")
|
|
733
|
-
|
|
734
|
-
retrieved = await dynamodb_storage.get_run("chain_run_1")
|
|
735
|
-
assert retrieved.continued_to_run_id == "chain_run_2"
|
|
736
|
-
|
|
737
|
-
@pytest.mark.asyncio
|
|
738
|
-
async def test_get_workflow_chain(self, dynamodb_storage):
|
|
739
|
-
"""Test getting workflow chain."""
|
|
740
|
-
now = datetime.now(UTC)
|
|
741
|
-
|
|
742
|
-
# Create a chain of 3 runs
|
|
743
|
-
run1 = WorkflowRun(
|
|
744
|
-
run_id="chain_1",
|
|
745
|
-
workflow_name="test_workflow",
|
|
746
|
-
status=RunStatus.COMPLETED,
|
|
747
|
-
created_at=now,
|
|
748
|
-
updated_at=now,
|
|
749
|
-
input_args="[]",
|
|
750
|
-
input_kwargs="{}",
|
|
751
|
-
continued_to_run_id="chain_2",
|
|
752
|
-
)
|
|
753
|
-
await dynamodb_storage.create_run(run1)
|
|
754
|
-
|
|
755
|
-
run2 = WorkflowRun(
|
|
756
|
-
run_id="chain_2",
|
|
757
|
-
workflow_name="test_workflow",
|
|
758
|
-
status=RunStatus.COMPLETED,
|
|
759
|
-
created_at=now + timedelta(seconds=1),
|
|
760
|
-
updated_at=now + timedelta(seconds=1),
|
|
761
|
-
input_args="[]",
|
|
762
|
-
input_kwargs="{}",
|
|
763
|
-
continued_from_run_id="chain_1",
|
|
764
|
-
continued_to_run_id="chain_3",
|
|
765
|
-
)
|
|
766
|
-
await dynamodb_storage.create_run(run2)
|
|
767
|
-
|
|
768
|
-
run3 = WorkflowRun(
|
|
769
|
-
run_id="chain_3",
|
|
770
|
-
workflow_name="test_workflow",
|
|
771
|
-
status=RunStatus.RUNNING,
|
|
772
|
-
created_at=now + timedelta(seconds=2),
|
|
773
|
-
updated_at=now + timedelta(seconds=2),
|
|
774
|
-
input_args="[]",
|
|
775
|
-
input_kwargs="{}",
|
|
776
|
-
continued_from_run_id="chain_2",
|
|
777
|
-
)
|
|
778
|
-
await dynamodb_storage.create_run(run3)
|
|
779
|
-
|
|
780
|
-
# Get chain from middle run
|
|
781
|
-
chain = await dynamodb_storage.get_workflow_chain("chain_2")
|
|
782
|
-
|
|
783
|
-
assert len(chain) == 3
|
|
784
|
-
assert chain[0].run_id == "chain_1"
|
|
785
|
-
assert chain[1].run_id == "chain_2"
|
|
786
|
-
assert chain[2].run_id == "chain_3"
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
class TestChildWorkflowOperations:
|
|
790
|
-
"""Test child workflow operations."""
|
|
791
|
-
|
|
792
|
-
@pytest.mark.asyncio
|
|
793
|
-
async def test_get_children(self, dynamodb_storage):
|
|
794
|
-
"""Test getting child workflows."""
|
|
795
|
-
now = datetime.now(UTC)
|
|
796
|
-
|
|
797
|
-
# Create parent run
|
|
798
|
-
parent = WorkflowRun(
|
|
799
|
-
run_id="parent_run",
|
|
800
|
-
workflow_name="parent_workflow",
|
|
801
|
-
status=RunStatus.RUNNING,
|
|
802
|
-
created_at=now,
|
|
803
|
-
updated_at=now,
|
|
804
|
-
input_args="[]",
|
|
805
|
-
input_kwargs="{}",
|
|
806
|
-
nesting_depth=0,
|
|
807
|
-
)
|
|
808
|
-
await dynamodb_storage.create_run(parent)
|
|
809
|
-
|
|
810
|
-
# Create child runs
|
|
811
|
-
for i in range(3):
|
|
812
|
-
child = WorkflowRun(
|
|
813
|
-
run_id=f"child_run_{i}",
|
|
814
|
-
workflow_name="child_workflow",
|
|
815
|
-
status=RunStatus.COMPLETED if i < 2 else RunStatus.RUNNING,
|
|
816
|
-
created_at=now + timedelta(seconds=i),
|
|
817
|
-
updated_at=now + timedelta(seconds=i),
|
|
818
|
-
input_args="[]",
|
|
819
|
-
input_kwargs="{}",
|
|
820
|
-
parent_run_id="parent_run",
|
|
821
|
-
nesting_depth=1,
|
|
822
|
-
)
|
|
823
|
-
await dynamodb_storage.create_run(child)
|
|
824
|
-
|
|
825
|
-
children = await dynamodb_storage.get_children("parent_run")
|
|
826
|
-
|
|
827
|
-
assert len(children) == 3
|
|
828
|
-
|
|
829
|
-
@pytest.mark.asyncio
|
|
830
|
-
async def test_get_children_with_status_filter(self, dynamodb_storage):
|
|
831
|
-
"""Test getting children filtered by status."""
|
|
832
|
-
now = datetime.now(UTC)
|
|
833
|
-
|
|
834
|
-
parent = WorkflowRun(
|
|
835
|
-
run_id="filter_parent",
|
|
836
|
-
workflow_name="parent_workflow",
|
|
837
|
-
status=RunStatus.RUNNING,
|
|
838
|
-
created_at=now,
|
|
839
|
-
updated_at=now,
|
|
840
|
-
input_args="[]",
|
|
841
|
-
input_kwargs="{}",
|
|
842
|
-
)
|
|
843
|
-
await dynamodb_storage.create_run(parent)
|
|
844
|
-
|
|
845
|
-
# Create child runs with different statuses
|
|
846
|
-
for i, status in enumerate([RunStatus.COMPLETED, RunStatus.COMPLETED, RunStatus.RUNNING]):
|
|
847
|
-
child = WorkflowRun(
|
|
848
|
-
run_id=f"filter_child_{i}",
|
|
849
|
-
workflow_name="child_workflow",
|
|
850
|
-
status=status,
|
|
851
|
-
created_at=now + timedelta(seconds=i),
|
|
852
|
-
updated_at=now + timedelta(seconds=i),
|
|
853
|
-
input_args="[]",
|
|
854
|
-
input_kwargs="{}",
|
|
855
|
-
parent_run_id="filter_parent",
|
|
856
|
-
nesting_depth=1,
|
|
857
|
-
)
|
|
858
|
-
await dynamodb_storage.create_run(child)
|
|
859
|
-
|
|
860
|
-
completed = await dynamodb_storage.get_children("filter_parent", status=RunStatus.COMPLETED)
|
|
861
|
-
|
|
862
|
-
assert len(completed) == 2
|
|
863
|
-
|
|
864
|
-
@pytest.mark.asyncio
|
|
865
|
-
async def test_get_parent(self, dynamodb_storage):
|
|
866
|
-
"""Test getting parent workflow."""
|
|
867
|
-
now = datetime.now(UTC)
|
|
868
|
-
|
|
869
|
-
parent = WorkflowRun(
|
|
870
|
-
run_id="the_parent",
|
|
871
|
-
workflow_name="parent_workflow",
|
|
872
|
-
status=RunStatus.RUNNING,
|
|
873
|
-
created_at=now,
|
|
874
|
-
updated_at=now,
|
|
875
|
-
input_args="[]",
|
|
876
|
-
input_kwargs="{}",
|
|
877
|
-
)
|
|
878
|
-
await dynamodb_storage.create_run(parent)
|
|
879
|
-
|
|
880
|
-
child = WorkflowRun(
|
|
881
|
-
run_id="the_child",
|
|
882
|
-
workflow_name="child_workflow",
|
|
883
|
-
status=RunStatus.RUNNING,
|
|
884
|
-
created_at=now,
|
|
885
|
-
updated_at=now,
|
|
886
|
-
input_args="[]",
|
|
887
|
-
input_kwargs="{}",
|
|
888
|
-
parent_run_id="the_parent",
|
|
889
|
-
nesting_depth=1,
|
|
890
|
-
)
|
|
891
|
-
await dynamodb_storage.create_run(child)
|
|
892
|
-
|
|
893
|
-
retrieved_parent = await dynamodb_storage.get_parent("the_child")
|
|
894
|
-
|
|
895
|
-
assert retrieved_parent is not None
|
|
896
|
-
assert retrieved_parent.run_id == "the_parent"
|
|
897
|
-
|
|
898
|
-
@pytest.mark.asyncio
|
|
899
|
-
async def test_get_nesting_depth(self, dynamodb_storage):
|
|
900
|
-
"""Test getting nesting depth."""
|
|
901
|
-
now = datetime.now(UTC)
|
|
902
|
-
|
|
903
|
-
run = WorkflowRun(
|
|
904
|
-
run_id="depth_test",
|
|
905
|
-
workflow_name="test_workflow",
|
|
906
|
-
status=RunStatus.RUNNING,
|
|
907
|
-
created_at=now,
|
|
908
|
-
updated_at=now,
|
|
909
|
-
input_args="[]",
|
|
910
|
-
input_kwargs="{}",
|
|
911
|
-
nesting_depth=2,
|
|
912
|
-
)
|
|
913
|
-
await dynamodb_storage.create_run(run)
|
|
914
|
-
|
|
915
|
-
depth = await dynamodb_storage.get_nesting_depth("depth_test")
|
|
916
|
-
|
|
917
|
-
assert depth == 2
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
class TestScheduleOperations:
|
|
921
|
-
"""Test schedule CRUD operations."""
|
|
922
|
-
|
|
923
|
-
@pytest.mark.asyncio
|
|
924
|
-
async def test_create_and_get_schedule(self, dynamodb_storage):
|
|
925
|
-
"""Test creating and retrieving a schedule."""
|
|
926
|
-
now = datetime.now(UTC)
|
|
927
|
-
spec = ScheduleSpec(cron="0 9 * * *")
|
|
928
|
-
schedule = Schedule(
|
|
929
|
-
schedule_id="schedule_001",
|
|
930
|
-
workflow_name="scheduled_workflow",
|
|
931
|
-
spec=spec,
|
|
932
|
-
status=ScheduleStatus.ACTIVE,
|
|
933
|
-
created_at=now,
|
|
934
|
-
)
|
|
935
|
-
|
|
936
|
-
await dynamodb_storage.create_schedule(schedule)
|
|
937
|
-
|
|
938
|
-
retrieved = await dynamodb_storage.get_schedule("schedule_001")
|
|
939
|
-
assert retrieved is not None
|
|
940
|
-
assert retrieved.schedule_id == "schedule_001"
|
|
941
|
-
assert retrieved.workflow_name == "scheduled_workflow"
|
|
942
|
-
assert retrieved.spec.cron == "0 9 * * *"
|
|
943
|
-
assert retrieved.status == ScheduleStatus.ACTIVE
|
|
944
|
-
|
|
945
|
-
@pytest.mark.asyncio
|
|
946
|
-
async def test_update_schedule(self, dynamodb_storage):
|
|
947
|
-
"""Test updating a schedule."""
|
|
948
|
-
now = datetime.now(UTC)
|
|
949
|
-
spec = ScheduleSpec(cron="0 9 * * *")
|
|
950
|
-
schedule = Schedule(
|
|
951
|
-
schedule_id="update_schedule",
|
|
952
|
-
workflow_name="test_workflow",
|
|
953
|
-
spec=spec,
|
|
954
|
-
status=ScheduleStatus.ACTIVE,
|
|
955
|
-
created_at=now,
|
|
956
|
-
)
|
|
957
|
-
await dynamodb_storage.create_schedule(schedule)
|
|
958
|
-
|
|
959
|
-
# Update the schedule
|
|
960
|
-
schedule.status = ScheduleStatus.PAUSED
|
|
961
|
-
schedule.spec = ScheduleSpec(cron="0 10 * * *")
|
|
962
|
-
schedule.updated_at = datetime.now(UTC)
|
|
963
|
-
await dynamodb_storage.update_schedule(schedule)
|
|
964
|
-
|
|
965
|
-
retrieved = await dynamodb_storage.get_schedule("update_schedule")
|
|
966
|
-
assert retrieved.status == ScheduleStatus.PAUSED
|
|
967
|
-
assert retrieved.spec.cron == "0 10 * * *"
|
|
968
|
-
|
|
969
|
-
@pytest.mark.asyncio
|
|
970
|
-
async def test_delete_schedule(self, dynamodb_storage):
|
|
971
|
-
"""Test deleting (soft delete) a schedule."""
|
|
972
|
-
now = datetime.now(UTC)
|
|
973
|
-
spec = ScheduleSpec(interval="5m")
|
|
974
|
-
schedule = Schedule(
|
|
975
|
-
schedule_id="delete_schedule",
|
|
976
|
-
workflow_name="test_workflow",
|
|
977
|
-
spec=spec,
|
|
978
|
-
status=ScheduleStatus.ACTIVE,
|
|
979
|
-
created_at=now,
|
|
980
|
-
)
|
|
981
|
-
await dynamodb_storage.create_schedule(schedule)
|
|
982
|
-
|
|
983
|
-
await dynamodb_storage.delete_schedule("delete_schedule")
|
|
984
|
-
|
|
985
|
-
retrieved = await dynamodb_storage.get_schedule("delete_schedule")
|
|
986
|
-
assert retrieved.status == ScheduleStatus.DELETED
|
|
987
|
-
|
|
988
|
-
@pytest.mark.asyncio
|
|
989
|
-
async def test_list_schedules(self, dynamodb_storage):
|
|
990
|
-
"""Test listing schedules."""
|
|
991
|
-
now = datetime.now(UTC)
|
|
992
|
-
|
|
993
|
-
for i in range(5):
|
|
994
|
-
schedule = Schedule(
|
|
995
|
-
schedule_id=f"list_sched_{i}",
|
|
996
|
-
workflow_name=f"workflow_{i % 2}",
|
|
997
|
-
spec=ScheduleSpec(cron="0 9 * * *"),
|
|
998
|
-
status=ScheduleStatus.ACTIVE if i % 2 == 0 else ScheduleStatus.PAUSED,
|
|
999
|
-
created_at=now + timedelta(seconds=i),
|
|
1000
|
-
)
|
|
1001
|
-
await dynamodb_storage.create_schedule(schedule)
|
|
1002
|
-
|
|
1003
|
-
schedules = await dynamodb_storage.list_schedules()
|
|
1004
|
-
|
|
1005
|
-
assert len(schedules) == 5
|
|
1006
|
-
|
|
1007
|
-
@pytest.mark.asyncio
|
|
1008
|
-
async def test_list_schedules_by_status(self, dynamodb_storage):
|
|
1009
|
-
"""Test listing schedules filtered by status."""
|
|
1010
|
-
now = datetime.now(UTC)
|
|
1011
|
-
|
|
1012
|
-
for i, status in enumerate(
|
|
1013
|
-
[ScheduleStatus.ACTIVE, ScheduleStatus.ACTIVE, ScheduleStatus.PAUSED]
|
|
1014
|
-
):
|
|
1015
|
-
schedule = Schedule(
|
|
1016
|
-
schedule_id=f"status_sched_{i}",
|
|
1017
|
-
workflow_name="test_workflow",
|
|
1018
|
-
spec=ScheduleSpec(cron="0 9 * * *"),
|
|
1019
|
-
status=status,
|
|
1020
|
-
created_at=now + timedelta(seconds=i),
|
|
1021
|
-
)
|
|
1022
|
-
await dynamodb_storage.create_schedule(schedule)
|
|
1023
|
-
|
|
1024
|
-
active = await dynamodb_storage.list_schedules(status=ScheduleStatus.ACTIVE)
|
|
1025
|
-
|
|
1026
|
-
assert len(active) == 2
|
|
1027
|
-
|
|
1028
|
-
@pytest.mark.asyncio
|
|
1029
|
-
async def test_get_due_schedules(self, dynamodb_storage):
|
|
1030
|
-
"""Test getting due schedules."""
|
|
1031
|
-
now = datetime.now(UTC)
|
|
1032
|
-
past = now - timedelta(minutes=5)
|
|
1033
|
-
future = now + timedelta(minutes=5)
|
|
1034
|
-
|
|
1035
|
-
# Create schedules with different next_run_times
|
|
1036
|
-
for i, (next_run, status) in enumerate(
|
|
1037
|
-
[
|
|
1038
|
-
(past, ScheduleStatus.ACTIVE),
|
|
1039
|
-
(past, ScheduleStatus.ACTIVE),
|
|
1040
|
-
(future, ScheduleStatus.ACTIVE),
|
|
1041
|
-
(past, ScheduleStatus.PAUSED), # Paused should not be returned
|
|
1042
|
-
]
|
|
1043
|
-
):
|
|
1044
|
-
schedule = Schedule(
|
|
1045
|
-
schedule_id=f"due_sched_{i}",
|
|
1046
|
-
workflow_name="test_workflow",
|
|
1047
|
-
spec=ScheduleSpec(cron="0 9 * * *"),
|
|
1048
|
-
status=status,
|
|
1049
|
-
next_run_time=next_run,
|
|
1050
|
-
created_at=now,
|
|
1051
|
-
)
|
|
1052
|
-
await dynamodb_storage.create_schedule(schedule)
|
|
1053
|
-
|
|
1054
|
-
due = await dynamodb_storage.get_due_schedules(now)
|
|
1055
|
-
|
|
1056
|
-
# Only 2 active schedules with past next_run_time
|
|
1057
|
-
assert len(due) == 2
|
|
1058
|
-
|
|
1059
|
-
@pytest.mark.asyncio
|
|
1060
|
-
async def test_add_and_remove_running_run(self, dynamodb_storage):
|
|
1061
|
-
"""Test adding and removing running run IDs."""
|
|
1062
|
-
now = datetime.now(UTC)
|
|
1063
|
-
schedule = Schedule(
|
|
1064
|
-
schedule_id="running_sched",
|
|
1065
|
-
workflow_name="test_workflow",
|
|
1066
|
-
spec=ScheduleSpec(cron="0 9 * * *"),
|
|
1067
|
-
status=ScheduleStatus.ACTIVE,
|
|
1068
|
-
created_at=now,
|
|
1069
|
-
)
|
|
1070
|
-
await dynamodb_storage.create_schedule(schedule)
|
|
1071
|
-
|
|
1072
|
-
# Add running runs
|
|
1073
|
-
await dynamodb_storage.add_running_run("running_sched", "run_1")
|
|
1074
|
-
await dynamodb_storage.add_running_run("running_sched", "run_2")
|
|
1075
|
-
|
|
1076
|
-
retrieved = await dynamodb_storage.get_schedule("running_sched")
|
|
1077
|
-
assert "run_1" in retrieved.running_run_ids
|
|
1078
|
-
assert "run_2" in retrieved.running_run_ids
|
|
1079
|
-
assert len(retrieved.running_run_ids) == 2
|
|
1080
|
-
|
|
1081
|
-
# Remove a run
|
|
1082
|
-
await dynamodb_storage.remove_running_run("running_sched", "run_1")
|
|
1083
|
-
|
|
1084
|
-
retrieved = await dynamodb_storage.get_schedule("running_sched")
|
|
1085
|
-
assert "run_1" not in retrieved.running_run_ids
|
|
1086
|
-
assert "run_2" in retrieved.running_run_ids
|
|
1087
|
-
assert len(retrieved.running_run_ids) == 1
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
class TestLifecycleMethods:
|
|
1091
|
-
"""Test lifecycle methods (connect, disconnect, health_check)."""
|
|
1092
|
-
|
|
1093
|
-
@pytest.mark.asyncio
|
|
1094
|
-
async def test_disconnect_and_reconnect(self, dynamodb_storage):
|
|
1095
|
-
"""Test that disconnect works and reconnect is possible."""
|
|
1096
|
-
# First verify we can create a run
|
|
1097
|
-
now = datetime.now(UTC)
|
|
1098
|
-
run = WorkflowRun(
|
|
1099
|
-
run_id="lifecycle_test_run",
|
|
1100
|
-
workflow_name="test_workflow",
|
|
1101
|
-
status=RunStatus.PENDING,
|
|
1102
|
-
created_at=now,
|
|
1103
|
-
updated_at=now,
|
|
1104
|
-
input_args="[]",
|
|
1105
|
-
input_kwargs="{}",
|
|
1106
|
-
)
|
|
1107
|
-
await dynamodb_storage.create_run(run)
|
|
1108
|
-
|
|
1109
|
-
# Verify it was created
|
|
1110
|
-
retrieved = await dynamodb_storage.get_run("lifecycle_test_run")
|
|
1111
|
-
assert retrieved is not None
|
|
1112
|
-
|
|
1113
|
-
# Disconnect
|
|
1114
|
-
await dynamodb_storage.disconnect()
|
|
1115
|
-
assert dynamodb_storage._initialized is False
|
|
1116
|
-
|
|
1117
|
-
# Reconnect
|
|
1118
|
-
await dynamodb_storage.connect()
|
|
1119
|
-
assert dynamodb_storage._initialized is True
|
|
1120
|
-
|
|
1121
|
-
# Verify we can still perform operations after reconnecting
|
|
1122
|
-
retrieved = await dynamodb_storage.get_run("lifecycle_test_run")
|
|
1123
|
-
assert retrieved is not None
|
|
1124
|
-
assert retrieved.run_id == "lifecycle_test_run"
|
|
1125
|
-
|
|
1126
|
-
@pytest.mark.asyncio
|
|
1127
|
-
async def test_health_check_returns_true(self, dynamodb_storage):
|
|
1128
|
-
"""Test health check returns True for healthy backend."""
|
|
1129
|
-
result = await dynamodb_storage.health_check()
|
|
1130
|
-
assert result is True
|
|
1131
|
-
|
|
1132
|
-
@pytest.mark.asyncio
|
|
1133
|
-
async def test_health_check_after_disconnect_returns_false(self, aws_credentials):
|
|
1134
|
-
"""Test health check returns False when disconnected."""
|
|
1135
|
-
import uuid
|
|
1136
|
-
|
|
1137
|
-
table_name = f"test_health_{uuid.uuid4().hex[:8]}"
|
|
1138
|
-
backend = DynamoDBStorageBackend(
|
|
1139
|
-
table_name=table_name,
|
|
1140
|
-
region="us-east-1",
|
|
1141
|
-
endpoint_url="http://localhost:8000",
|
|
1142
|
-
)
|
|
1143
|
-
|
|
1144
|
-
# Don't connect - should return False
|
|
1145
|
-
result = await backend.health_check()
|
|
1146
|
-
assert result is False
|