pyworkflow-engine 0.1.7__py3-none-any.whl → 0.1.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. pyworkflow/__init__.py +10 -1
  2. pyworkflow/celery/tasks.py +272 -24
  3. pyworkflow/cli/__init__.py +4 -1
  4. pyworkflow/cli/commands/runs.py +4 -4
  5. pyworkflow/cli/commands/setup.py +203 -4
  6. pyworkflow/cli/utils/config_generator.py +76 -3
  7. pyworkflow/cli/utils/docker_manager.py +232 -0
  8. pyworkflow/config.py +94 -17
  9. pyworkflow/context/__init__.py +13 -0
  10. pyworkflow/context/base.py +26 -0
  11. pyworkflow/context/local.py +80 -0
  12. pyworkflow/context/step_context.py +295 -0
  13. pyworkflow/core/registry.py +6 -1
  14. pyworkflow/core/step.py +141 -0
  15. pyworkflow/core/workflow.py +56 -0
  16. pyworkflow/engine/events.py +30 -0
  17. pyworkflow/engine/replay.py +39 -0
  18. pyworkflow/primitives/child_workflow.py +1 -1
  19. pyworkflow/runtime/local.py +1 -1
  20. pyworkflow/storage/__init__.py +14 -0
  21. pyworkflow/storage/base.py +35 -0
  22. pyworkflow/storage/cassandra.py +1747 -0
  23. pyworkflow/storage/config.py +69 -0
  24. pyworkflow/storage/dynamodb.py +31 -2
  25. pyworkflow/storage/file.py +28 -0
  26. pyworkflow/storage/memory.py +18 -0
  27. pyworkflow/storage/mysql.py +1159 -0
  28. pyworkflow/storage/postgres.py +27 -2
  29. pyworkflow/storage/schemas.py +4 -3
  30. pyworkflow/storage/sqlite.py +25 -2
  31. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/METADATA +7 -4
  32. pyworkflow_engine-0.1.10.dist-info/RECORD +91 -0
  33. pyworkflow_engine-0.1.10.dist-info/top_level.txt +1 -0
  34. dashboard/backend/app/__init__.py +0 -1
  35. dashboard/backend/app/config.py +0 -32
  36. dashboard/backend/app/controllers/__init__.py +0 -6
  37. dashboard/backend/app/controllers/run_controller.py +0 -86
  38. dashboard/backend/app/controllers/workflow_controller.py +0 -33
  39. dashboard/backend/app/dependencies/__init__.py +0 -5
  40. dashboard/backend/app/dependencies/storage.py +0 -50
  41. dashboard/backend/app/repositories/__init__.py +0 -6
  42. dashboard/backend/app/repositories/run_repository.py +0 -80
  43. dashboard/backend/app/repositories/workflow_repository.py +0 -27
  44. dashboard/backend/app/rest/__init__.py +0 -8
  45. dashboard/backend/app/rest/v1/__init__.py +0 -12
  46. dashboard/backend/app/rest/v1/health.py +0 -33
  47. dashboard/backend/app/rest/v1/runs.py +0 -133
  48. dashboard/backend/app/rest/v1/workflows.py +0 -41
  49. dashboard/backend/app/schemas/__init__.py +0 -23
  50. dashboard/backend/app/schemas/common.py +0 -16
  51. dashboard/backend/app/schemas/event.py +0 -24
  52. dashboard/backend/app/schemas/hook.py +0 -25
  53. dashboard/backend/app/schemas/run.py +0 -54
  54. dashboard/backend/app/schemas/step.py +0 -28
  55. dashboard/backend/app/schemas/workflow.py +0 -31
  56. dashboard/backend/app/server.py +0 -87
  57. dashboard/backend/app/services/__init__.py +0 -6
  58. dashboard/backend/app/services/run_service.py +0 -240
  59. dashboard/backend/app/services/workflow_service.py +0 -155
  60. dashboard/backend/main.py +0 -18
  61. docs/concepts/cancellation.mdx +0 -362
  62. docs/concepts/continue-as-new.mdx +0 -434
  63. docs/concepts/events.mdx +0 -266
  64. docs/concepts/fault-tolerance.mdx +0 -370
  65. docs/concepts/hooks.mdx +0 -552
  66. docs/concepts/limitations.mdx +0 -167
  67. docs/concepts/schedules.mdx +0 -775
  68. docs/concepts/sleep.mdx +0 -312
  69. docs/concepts/steps.mdx +0 -301
  70. docs/concepts/workflows.mdx +0 -255
  71. docs/guides/cli.mdx +0 -942
  72. docs/guides/configuration.mdx +0 -560
  73. docs/introduction.mdx +0 -155
  74. docs/quickstart.mdx +0 -279
  75. examples/__init__.py +0 -1
  76. examples/celery/__init__.py +0 -1
  77. examples/celery/durable/docker-compose.yml +0 -55
  78. examples/celery/durable/pyworkflow.config.yaml +0 -12
  79. examples/celery/durable/workflows/__init__.py +0 -122
  80. examples/celery/durable/workflows/basic.py +0 -87
  81. examples/celery/durable/workflows/batch_processing.py +0 -102
  82. examples/celery/durable/workflows/cancellation.py +0 -273
  83. examples/celery/durable/workflows/child_workflow_patterns.py +0 -240
  84. examples/celery/durable/workflows/child_workflows.py +0 -202
  85. examples/celery/durable/workflows/continue_as_new.py +0 -260
  86. examples/celery/durable/workflows/fault_tolerance.py +0 -210
  87. examples/celery/durable/workflows/hooks.py +0 -211
  88. examples/celery/durable/workflows/idempotency.py +0 -112
  89. examples/celery/durable/workflows/long_running.py +0 -99
  90. examples/celery/durable/workflows/retries.py +0 -101
  91. examples/celery/durable/workflows/schedules.py +0 -209
  92. examples/celery/transient/01_basic_workflow.py +0 -91
  93. examples/celery/transient/02_fault_tolerance.py +0 -257
  94. examples/celery/transient/__init__.py +0 -20
  95. examples/celery/transient/pyworkflow.config.yaml +0 -25
  96. examples/local/__init__.py +0 -1
  97. examples/local/durable/01_basic_workflow.py +0 -94
  98. examples/local/durable/02_file_storage.py +0 -132
  99. examples/local/durable/03_retries.py +0 -169
  100. examples/local/durable/04_long_running.py +0 -119
  101. examples/local/durable/05_event_log.py +0 -145
  102. examples/local/durable/06_idempotency.py +0 -148
  103. examples/local/durable/07_hooks.py +0 -334
  104. examples/local/durable/08_cancellation.py +0 -233
  105. examples/local/durable/09_child_workflows.py +0 -198
  106. examples/local/durable/10_child_workflow_patterns.py +0 -265
  107. examples/local/durable/11_continue_as_new.py +0 -249
  108. examples/local/durable/12_schedules.py +0 -198
  109. examples/local/durable/__init__.py +0 -1
  110. examples/local/transient/01_quick_tasks.py +0 -87
  111. examples/local/transient/02_retries.py +0 -130
  112. examples/local/transient/03_sleep.py +0 -141
  113. examples/local/transient/__init__.py +0 -1
  114. pyworkflow_engine-0.1.7.dist-info/RECORD +0 -196
  115. pyworkflow_engine-0.1.7.dist-info/top_level.txt +0 -5
  116. tests/examples/__init__.py +0 -0
  117. tests/integration/__init__.py +0 -0
  118. tests/integration/test_cancellation.py +0 -330
  119. tests/integration/test_child_workflows.py +0 -439
  120. tests/integration/test_continue_as_new.py +0 -428
  121. tests/integration/test_dynamodb_storage.py +0 -1146
  122. tests/integration/test_fault_tolerance.py +0 -369
  123. tests/integration/test_schedule_storage.py +0 -484
  124. tests/unit/__init__.py +0 -0
  125. tests/unit/backends/__init__.py +0 -1
  126. tests/unit/backends/test_dynamodb_storage.py +0 -1554
  127. tests/unit/backends/test_postgres_storage.py +0 -1281
  128. tests/unit/backends/test_sqlite_storage.py +0 -1460
  129. tests/unit/conftest.py +0 -41
  130. tests/unit/test_cancellation.py +0 -364
  131. tests/unit/test_child_workflows.py +0 -680
  132. tests/unit/test_continue_as_new.py +0 -441
  133. tests/unit/test_event_limits.py +0 -316
  134. tests/unit/test_executor.py +0 -320
  135. tests/unit/test_fault_tolerance.py +0 -334
  136. tests/unit/test_hooks.py +0 -495
  137. tests/unit/test_registry.py +0 -261
  138. tests/unit/test_replay.py +0 -420
  139. tests/unit/test_schedule_schemas.py +0 -285
  140. tests/unit/test_schedule_utils.py +0 -286
  141. tests/unit/test_scheduled_workflow.py +0 -274
  142. tests/unit/test_step.py +0 -353
  143. tests/unit/test_workflow.py +0 -243
  144. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/WHEEL +0 -0
  145. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/entry_points.txt +0 -0
  146. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/licenses/LICENSE +0 -0
@@ -1,261 +0,0 @@
1
- """
2
- Unit tests for workflow and step registry.
3
- """
4
-
5
- import pytest
6
-
7
- from pyworkflow.core.registry import (
8
- WorkflowRegistry,
9
- get_workflow,
10
- get_workflow_by_func,
11
- register_step,
12
- register_workflow,
13
- )
14
-
15
-
16
- class TestWorkflowRegistry:
17
- """Test the workflow registry functionality."""
18
-
19
- def test_register_and_get_workflow(self):
20
- """Test registering and retrieving a workflow."""
21
- registry = WorkflowRegistry()
22
-
23
- # Define a simple workflow function
24
- async def my_workflow():
25
- pass
26
-
27
- # Register workflow
28
- registry.register_workflow(
29
- name="test_workflow",
30
- func=my_workflow,
31
- original_func=my_workflow,
32
- max_duration="1h",
33
- tags=["backend", "critical"],
34
- )
35
-
36
- # Retrieve workflow
37
- workflow_meta = registry.get_workflow("test_workflow")
38
-
39
- assert workflow_meta is not None
40
- assert workflow_meta.name == "test_workflow"
41
- assert workflow_meta.func == my_workflow
42
- assert workflow_meta.max_duration == "1h"
43
- assert workflow_meta.tags == ["backend", "critical"]
44
-
45
- def test_get_workflow_by_func(self):
46
- """Test retrieving a workflow by its function reference."""
47
- registry = WorkflowRegistry()
48
-
49
- async def my_workflow():
50
- pass
51
-
52
- # Register workflow
53
- registry.register_workflow(
54
- name="test_workflow",
55
- func=my_workflow,
56
- original_func=my_workflow,
57
- )
58
-
59
- # Retrieve by function
60
- workflow_meta = registry.get_workflow_by_func(my_workflow)
61
-
62
- assert workflow_meta is not None
63
- assert workflow_meta.name == "test_workflow"
64
-
65
- def test_get_nonexistent_workflow(self):
66
- """Test retrieving a workflow that doesn't exist."""
67
- registry = WorkflowRegistry()
68
- workflow_meta = registry.get_workflow("nonexistent")
69
- assert workflow_meta is None
70
-
71
- def test_register_duplicate_workflow(self):
72
- """Test that registering the same workflow name raises an error."""
73
- registry = WorkflowRegistry()
74
-
75
- async def workflow1():
76
- pass
77
-
78
- async def workflow2():
79
- pass
80
-
81
- # Register first workflow
82
- registry.register_workflow(
83
- name="duplicate",
84
- func=workflow1,
85
- original_func=workflow1,
86
- )
87
-
88
- # Registering again should raise ValueError
89
- with pytest.raises(ValueError, match="already registered"):
90
- registry.register_workflow(
91
- name="duplicate",
92
- func=workflow2,
93
- original_func=workflow2,
94
- )
95
-
96
- def test_list_workflows(self):
97
- """Test listing all registered workflows."""
98
- registry = WorkflowRegistry()
99
-
100
- async def workflow1():
101
- pass
102
-
103
- async def workflow2():
104
- pass
105
-
106
- # Register multiple workflows
107
- registry.register_workflow(
108
- name="workflow_a",
109
- func=workflow1,
110
- original_func=workflow1,
111
- )
112
- registry.register_workflow(
113
- name="workflow_b",
114
- func=workflow2,
115
- original_func=workflow2,
116
- )
117
-
118
- workflows = registry.list_workflows()
119
- assert len(workflows) == 2
120
- assert "workflow_a" in workflows
121
- assert "workflow_b" in workflows
122
-
123
-
124
- class TestStepRegistry:
125
- """Test the step registry functionality."""
126
-
127
- def test_register_and_get_step(self):
128
- """Test registering and retrieving a step."""
129
- registry = WorkflowRegistry()
130
-
131
- async def my_step():
132
- pass
133
-
134
- # Register step
135
- registry.register_step(
136
- name="test_step",
137
- func=my_step,
138
- original_func=my_step,
139
- max_retries=5,
140
- retry_delay="10",
141
- timeout=30,
142
- metadata={"type": "api_call"},
143
- )
144
-
145
- # Retrieve step
146
- step_meta = registry.get_step("test_step")
147
-
148
- assert step_meta is not None
149
- assert step_meta.name == "test_step"
150
- assert step_meta.func == my_step
151
- assert step_meta.max_retries == 5
152
- assert step_meta.retry_delay == "10"
153
- assert step_meta.timeout == 30
154
- assert step_meta.metadata == {"type": "api_call"}
155
-
156
- def test_get_nonexistent_step(self):
157
- """Test retrieving a step that doesn't exist."""
158
- registry = WorkflowRegistry()
159
- step_meta = registry.get_step("nonexistent")
160
- assert step_meta is None
161
-
162
- def test_register_duplicate_step(self):
163
- """Test that registering the same step name raises an error."""
164
- registry = WorkflowRegistry()
165
-
166
- async def step1():
167
- pass
168
-
169
- async def step2():
170
- pass
171
-
172
- # Register first step
173
- registry.register_step(
174
- name="duplicate",
175
- func=step1,
176
- original_func=step1,
177
- )
178
-
179
- # Registering again should raise ValueError
180
- with pytest.raises(ValueError, match="already registered"):
181
- registry.register_step(
182
- name="duplicate",
183
- func=step2,
184
- original_func=step2,
185
- )
186
-
187
- def test_list_steps(self):
188
- """Test listing all registered steps."""
189
- registry = WorkflowRegistry()
190
-
191
- async def step1():
192
- pass
193
-
194
- async def step2():
195
- pass
196
-
197
- # Register multiple steps
198
- registry.register_step(name="step_a", func=step1, original_func=step1)
199
- registry.register_step(name="step_b", func=step2, original_func=step2)
200
-
201
- steps = registry.list_steps()
202
- assert len(steps) == 2
203
- assert "step_a" in steps
204
- assert "step_b" in steps
205
-
206
-
207
- class TestGlobalRegistry:
208
- """Test the global registry functions."""
209
-
210
- def test_register_workflow_globally(self):
211
- """Test the global register_workflow function."""
212
-
213
- async def global_workflow():
214
- pass
215
-
216
- register_workflow(
217
- name="global_test_workflow",
218
- func=global_workflow,
219
- original_func=global_workflow,
220
- )
221
-
222
- # Should be able to retrieve it
223
- workflow_meta = get_workflow("global_test_workflow")
224
- assert workflow_meta is not None
225
- assert workflow_meta.name == "global_test_workflow"
226
-
227
- def test_register_step_globally(self):
228
- """Test the global register_step function."""
229
-
230
- async def global_step():
231
- pass
232
-
233
- register_step(
234
- name="global_test_step",
235
- func=global_step,
236
- original_func=global_step,
237
- )
238
-
239
- # Should be able to retrieve it from global registry
240
- from pyworkflow.core.registry import _registry
241
-
242
- step_meta = _registry.get_step("global_test_step")
243
- assert step_meta is not None
244
- assert step_meta.name == "global_test_step"
245
-
246
- def test_get_workflow_by_func_globally(self):
247
- """Test retrieving workflow by function from global registry."""
248
-
249
- async def another_workflow():
250
- pass
251
-
252
- register_workflow(
253
- name="func_lookup_test",
254
- func=another_workflow,
255
- original_func=another_workflow,
256
- )
257
-
258
- # Should be able to retrieve by function
259
- workflow_meta = get_workflow_by_func(another_workflow)
260
- assert workflow_meta is not None
261
- assert workflow_meta.name == "func_lookup_test"
tests/unit/test_replay.py DELETED
@@ -1,420 +0,0 @@
1
- """
2
- Unit tests for event replay engine.
3
- """
4
-
5
- from datetime import UTC, datetime
6
-
7
- import pytest
8
-
9
- from pyworkflow.context import LocalContext
10
- from pyworkflow.engine.events import (
11
- Event,
12
- EventType,
13
- create_hook_created_event,
14
- create_hook_expired_event,
15
- create_hook_received_event,
16
- create_sleep_completed_event,
17
- create_sleep_started_event,
18
- create_step_completed_event,
19
- create_step_started_event,
20
- )
21
- from pyworkflow.engine.replay import EventReplayer, replay_events
22
- from pyworkflow.serialization.encoder import serialize
23
- from pyworkflow.storage.file import FileStorageBackend
24
-
25
-
26
- class TestEventReplayer:
27
- """Test the EventReplayer class."""
28
-
29
- @pytest.mark.asyncio
30
- async def test_replay_empty_events(self, tmp_path):
31
- """Test replaying with no events."""
32
- storage = FileStorageBackend(base_path=str(tmp_path))
33
- ctx = LocalContext(
34
- run_id="test_run",
35
- workflow_name="test_workflow",
36
- storage=storage,
37
- )
38
-
39
- replayer = EventReplayer()
40
- await replayer.replay(ctx, [])
41
-
42
- # Context should remain empty
43
- assert len(ctx.step_results) == 0
44
- assert len(ctx.completed_sleeps) == 0
45
- assert len(ctx.hook_results) == 0
46
-
47
- @pytest.mark.asyncio
48
- async def test_replay_step_completed(self, tmp_path):
49
- """Test replaying step.completed events."""
50
- storage = FileStorageBackend(base_path=str(tmp_path))
51
- ctx = LocalContext(
52
- run_id="test_run",
53
- workflow_name="test_workflow",
54
- storage=storage,
55
- )
56
-
57
- # Create step completed events
58
- events = [
59
- create_step_started_event(
60
- run_id="test_run",
61
- step_id="step_1",
62
- step_name="test_step",
63
- args="[]",
64
- kwargs="{}",
65
- attempt=1,
66
- ),
67
- create_step_completed_event(
68
- run_id="test_run",
69
- step_id="step_1",
70
- result=serialize("result_1"),
71
- step_name="test_step",
72
- ),
73
- ]
74
-
75
- # Assign sequence numbers
76
- for i, event in enumerate(events, 1):
77
- event.sequence = i
78
-
79
- replayer = EventReplayer()
80
- await replayer.replay(ctx, events)
81
-
82
- # Step result should be cached
83
- assert "step_1" in ctx.step_results
84
- assert ctx.step_results["step_1"] == "result_1"
85
-
86
- @pytest.mark.asyncio
87
- async def test_replay_multiple_steps(self, tmp_path):
88
- """Test replaying multiple step events."""
89
- storage = FileStorageBackend(base_path=str(tmp_path))
90
- ctx = LocalContext(
91
- run_id="test_run",
92
- workflow_name="test_workflow",
93
- storage=storage,
94
- )
95
-
96
- events = [
97
- create_step_completed_event(
98
- run_id="test_run",
99
- step_id="step_1",
100
- result=serialize("result_1"),
101
- step_name="test_step",
102
- ),
103
- create_step_completed_event(
104
- run_id="test_run",
105
- step_id="step_2",
106
- result=serialize("result_2"),
107
- step_name="test_step",
108
- ),
109
- create_step_completed_event(
110
- run_id="test_run",
111
- step_id="step_3",
112
- result=serialize("result_3"),
113
- step_name="test_step",
114
- ),
115
- ]
116
-
117
- for i, event in enumerate(events, 1):
118
- event.sequence = i
119
-
120
- replayer = EventReplayer()
121
- await replayer.replay(ctx, events)
122
-
123
- # All results should be cached
124
- assert len(ctx.step_results) == 3
125
- assert ctx.step_results["step_1"] == "result_1"
126
- assert ctx.step_results["step_2"] == "result_2"
127
- assert ctx.step_results["step_3"] == "result_3"
128
-
129
- @pytest.mark.asyncio
130
- async def test_replay_sleep_events(self, tmp_path):
131
- """Test replaying sleep events."""
132
- storage = FileStorageBackend(base_path=str(tmp_path))
133
- ctx = LocalContext(
134
- run_id="test_run",
135
- workflow_name="test_workflow",
136
- storage=storage,
137
- )
138
-
139
- resume_at = datetime.now(UTC)
140
- events = [
141
- create_sleep_started_event(
142
- run_id="test_run",
143
- sleep_id="sleep_1",
144
- duration_seconds=60,
145
- resume_at=resume_at,
146
- name="test_sleep",
147
- ),
148
- create_sleep_completed_event(run_id="test_run", sleep_id="sleep_1"),
149
- ]
150
-
151
- for i, event in enumerate(events, 1):
152
- event.sequence = i
153
-
154
- replayer = EventReplayer()
155
- await replayer.replay(ctx, events)
156
-
157
- # Sleep should be marked as completed
158
- assert ctx.is_sleep_completed("sleep_1")
159
- assert "sleep_1" in ctx.completed_sleeps
160
-
161
- @pytest.mark.asyncio
162
- async def test_replay_pending_sleep(self, tmp_path):
163
- """Test replaying sleep that hasn't completed."""
164
- storage = FileStorageBackend(base_path=str(tmp_path))
165
- ctx = LocalContext(
166
- run_id="test_run",
167
- workflow_name="test_workflow",
168
- storage=storage,
169
- )
170
-
171
- resume_at = datetime.now(UTC)
172
- events = [
173
- create_sleep_started_event(
174
- run_id="test_run",
175
- sleep_id="sleep_pending",
176
- duration_seconds=60,
177
- resume_at=resume_at,
178
- name="pending_sleep",
179
- ),
180
- ]
181
-
182
- for i, event in enumerate(events, 1):
183
- event.sequence = i
184
-
185
- replayer = EventReplayer()
186
- await replayer.replay(ctx, events)
187
-
188
- # Sleep should be pending (not completed)
189
- assert not ctx.is_sleep_completed("sleep_pending")
190
- assert "sleep_pending" in ctx.pending_sleeps
191
-
192
- @pytest.mark.asyncio
193
- async def test_replay_hook_events(self, tmp_path):
194
- """Test replaying hook events."""
195
- storage = FileStorageBackend(base_path=str(tmp_path))
196
- ctx = LocalContext(
197
- run_id="test_run",
198
- workflow_name="test_workflow",
199
- storage=storage,
200
- )
201
-
202
- events = [
203
- create_hook_created_event(
204
- run_id="test_run",
205
- hook_id="hook_1",
206
- url="https://example.com/hook",
207
- token="secret_token",
208
- name="test_hook",
209
- ),
210
- create_hook_received_event(
211
- run_id="test_run",
212
- hook_id="hook_1",
213
- payload={"data": "test_payload"},
214
- ),
215
- ]
216
-
217
- for i, event in enumerate(events, 1):
218
- event.sequence = i
219
-
220
- replayer = EventReplayer()
221
- await replayer.replay(ctx, events)
222
-
223
- # Hook result should be cached
224
- assert ctx.has_hook_result("hook_1")
225
- assert ctx.get_hook_result("hook_1") == {"data": "test_payload"}
226
-
227
- @pytest.mark.asyncio
228
- async def test_replay_expired_hook(self, tmp_path):
229
- """Test replaying an expired hook."""
230
- storage = FileStorageBackend(base_path=str(tmp_path))
231
- ctx = LocalContext(
232
- run_id="test_run",
233
- workflow_name="test_workflow",
234
- storage=storage,
235
- )
236
-
237
- events = [
238
- create_hook_created_event(
239
- run_id="test_run",
240
- hook_id="hook_expired",
241
- url="https://example.com/hook",
242
- token="token",
243
- ),
244
- create_hook_expired_event(
245
- run_id="test_run",
246
- hook_id="hook_expired",
247
- ),
248
- ]
249
-
250
- for i, event in enumerate(events, 1):
251
- event.sequence = i
252
-
253
- replayer = EventReplayer()
254
- await replayer.replay(ctx, events)
255
-
256
- # Hook should not be in pending hooks
257
- assert "hook_expired" not in ctx.pending_hooks
258
- # Hook should not have a result
259
- assert not ctx.has_hook_result("hook_expired")
260
-
261
- @pytest.mark.asyncio
262
- async def test_replay_events_in_order(self, tmp_path):
263
- """Test that events are replayed in sequence order."""
264
- storage = FileStorageBackend(base_path=str(tmp_path))
265
- ctx = LocalContext(
266
- run_id="test_run",
267
- workflow_name="test_workflow",
268
- storage=storage,
269
- )
270
-
271
- # Create events out of order
272
- event1 = create_step_completed_event(
273
- run_id="test_run", step_id="step_1", result=serialize("result_1"), step_name="test_step"
274
- )
275
- event1.sequence = 2
276
-
277
- event2 = create_step_completed_event(
278
- run_id="test_run", step_id="step_2", result=serialize("result_2"), step_name="test_step"
279
- )
280
- event2.sequence = 1
281
-
282
- # Provide in wrong order
283
- events = [event1, event2]
284
-
285
- replayer = EventReplayer()
286
- await replayer.replay(ctx, events)
287
-
288
- # Both should be replayed correctly regardless of input order
289
- assert len(ctx.step_results) == 2
290
-
291
- @pytest.mark.asyncio
292
- async def test_replay_sets_replaying_flag(self, tmp_path):
293
- """Test that replay sets and clears is_replaying flag."""
294
- storage = FileStorageBackend(base_path=str(tmp_path))
295
- ctx = LocalContext(
296
- run_id="test_run",
297
- workflow_name="test_workflow",
298
- storage=storage,
299
- )
300
-
301
- events = [
302
- create_step_completed_event(
303
- run_id="test_run",
304
- step_id="step_1",
305
- result=serialize("result"),
306
- step_name="test_step",
307
- ),
308
- ]
309
- events[0].sequence = 1
310
-
311
- assert ctx.is_replaying is False
312
-
313
- replayer = EventReplayer()
314
- await replayer.replay(ctx, events)
315
-
316
- # After replay, flag should be cleared
317
- assert ctx.is_replaying is False
318
-
319
-
320
- class TestReplayPublicAPI:
321
- """Test the public replay API."""
322
-
323
- @pytest.mark.asyncio
324
- async def test_replay_events_function(self, tmp_path):
325
- """Test the replay_events public function."""
326
- storage = FileStorageBackend(base_path=str(tmp_path))
327
- ctx = LocalContext(
328
- run_id="test_run",
329
- workflow_name="test_workflow",
330
- storage=storage,
331
- )
332
-
333
- events = [
334
- create_step_completed_event(
335
- run_id="test_run",
336
- step_id="step_1",
337
- result=serialize("test_result"),
338
- step_name="test_step",
339
- ),
340
- ]
341
- events[0].sequence = 1
342
-
343
- # Use public API
344
- await replay_events(ctx, events)
345
-
346
- # Verify replay worked
347
- assert "step_1" in ctx.step_results
348
- assert ctx.step_results["step_1"] == "test_result"
349
-
350
-
351
- class TestReplayIntegration:
352
- """Integration tests for replay with full workflows."""
353
-
354
- @pytest.mark.asyncio
355
- async def test_replay_full_workflow(self, tmp_path):
356
- """Test replaying a complete workflow event log."""
357
- storage = FileStorageBackend(base_path=str(tmp_path))
358
- ctx = LocalContext(
359
- run_id="test_run",
360
- workflow_name="test_workflow",
361
- storage=storage,
362
- )
363
-
364
- # Simulate a workflow with steps and sleep
365
- resume_at = datetime.now(UTC)
366
- events = [
367
- Event(
368
- run_id="test_run",
369
- type=EventType.WORKFLOW_STARTED,
370
- sequence=1,
371
- data={"workflow_name": "test_workflow"},
372
- ),
373
- create_step_started_event(
374
- run_id="test_run",
375
- step_id="step_1",
376
- step_name="first_step",
377
- args="[]",
378
- kwargs="{}",
379
- attempt=1,
380
- ),
381
- create_step_completed_event(
382
- run_id="test_run",
383
- step_id="step_1",
384
- result=serialize("step_1_result"),
385
- step_name="first_step",
386
- ),
387
- create_sleep_started_event(
388
- run_id="test_run",
389
- sleep_id="sleep_1",
390
- duration_seconds=10,
391
- resume_at=resume_at,
392
- ),
393
- create_sleep_completed_event(run_id="test_run", sleep_id="sleep_1"),
394
- create_step_started_event(
395
- run_id="test_run",
396
- step_id="step_2",
397
- step_name="second_step",
398
- args="[]",
399
- kwargs="{}",
400
- attempt=1,
401
- ),
402
- create_step_completed_event(
403
- run_id="test_run",
404
- step_id="step_2",
405
- result=serialize("step_2_result"),
406
- step_name="second_step",
407
- ),
408
- ]
409
-
410
- # Assign sequences
411
- for i, event in enumerate(events, 1):
412
- event.sequence = i
413
-
414
- await replay_events(ctx, events)
415
-
416
- # Verify all state was restored
417
- assert len(ctx.step_results) == 2
418
- assert ctx.step_results["step_1"] == "step_1_result"
419
- assert ctx.step_results["step_2"] == "step_2_result"
420
- assert ctx.is_sleep_completed("sleep_1")