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,441 +0,0 @@
1
- """
2
- Unit tests for continue-as-new feature.
3
-
4
- Tests cover:
5
- - ContinueAsNewSignal exception
6
- - continue_as_new() primitive function
7
- - CONTINUED_AS_NEW status and event types
8
- - Storage chain tracking methods
9
- """
10
-
11
- import pytest
12
-
13
- from pyworkflow import (
14
- CancellationError,
15
- LocalContext,
16
- MockContext,
17
- RunStatus,
18
- set_context,
19
- )
20
- from pyworkflow.core.exceptions import ContinueAsNewSignal
21
- from pyworkflow.engine.events import (
22
- EventType,
23
- create_workflow_continued_as_new_event,
24
- )
25
- from pyworkflow.primitives.continue_as_new import continue_as_new
26
- from pyworkflow.storage.memory import InMemoryStorageBackend
27
- from pyworkflow.storage.schemas import WorkflowRun
28
-
29
-
30
- class TestContinueAsNewSignal:
31
- """Test ContinueAsNewSignal exception."""
32
-
33
- def test_signal_default_message(self):
34
- """Test ContinueAsNewSignal has correct default message."""
35
- signal = ContinueAsNewSignal()
36
- assert str(signal) == "Workflow continuing as new execution"
37
-
38
- def test_signal_stores_args(self):
39
- """Test ContinueAsNewSignal stores positional args."""
40
- signal = ContinueAsNewSignal(workflow_args=("arg1", "arg2"))
41
- assert signal.workflow_args == ("arg1", "arg2")
42
-
43
- def test_signal_stores_kwargs(self):
44
- """Test ContinueAsNewSignal stores keyword args."""
45
- signal = ContinueAsNewSignal(workflow_kwargs={"key": "value"})
46
- assert signal.workflow_kwargs == {"key": "value"}
47
-
48
- def test_signal_stores_both_args_and_kwargs(self):
49
- """Test ContinueAsNewSignal stores both args and kwargs."""
50
- signal = ContinueAsNewSignal(workflow_args=("a", "b"), workflow_kwargs={"x": 1, "y": 2})
51
- assert signal.workflow_args == ("a", "b")
52
- assert signal.workflow_kwargs == {"x": 1, "y": 2}
53
-
54
- def test_signal_defaults_to_empty(self):
55
- """Test ContinueAsNewSignal defaults to empty args/kwargs."""
56
- signal = ContinueAsNewSignal()
57
- assert signal.workflow_args == ()
58
- assert signal.workflow_kwargs == {}
59
-
60
- def test_signal_none_kwargs_becomes_empty_dict(self):
61
- """Test None kwargs becomes empty dict."""
62
- signal = ContinueAsNewSignal(workflow_kwargs=None)
63
- assert signal.workflow_kwargs == {}
64
-
65
-
66
- class TestContinueAsNewPrimitive:
67
- """Test continue_as_new() primitive function."""
68
-
69
- def test_raises_runtime_error_outside_context(self):
70
- """Test continue_as_new raises RuntimeError outside context."""
71
- set_context(None)
72
-
73
- with pytest.raises(RuntimeError) as exc_info:
74
- continue_as_new("arg1")
75
-
76
- assert "must be called within a workflow context" in str(exc_info.value)
77
-
78
- def test_raises_value_error_without_args(self):
79
- """Test continue_as_new raises ValueError without args."""
80
- ctx = LocalContext(
81
- run_id="test_run",
82
- workflow_name="test_workflow",
83
- storage=None,
84
- durable=False,
85
- )
86
- set_context(ctx)
87
-
88
- try:
89
- with pytest.raises(ValueError) as exc_info:
90
- continue_as_new()
91
-
92
- assert "requires at least one argument" in str(exc_info.value)
93
- finally:
94
- set_context(None)
95
-
96
- def test_raises_signal_with_positional_args(self):
97
- """Test continue_as_new raises signal with positional args."""
98
- ctx = LocalContext(
99
- run_id="test_run",
100
- workflow_name="test_workflow",
101
- storage=None,
102
- durable=False,
103
- )
104
- set_context(ctx)
105
-
106
- try:
107
- with pytest.raises(ContinueAsNewSignal) as exc_info:
108
- continue_as_new("arg1", "arg2")
109
-
110
- assert exc_info.value.workflow_args == ("arg1", "arg2")
111
- assert exc_info.value.workflow_kwargs == {}
112
- finally:
113
- set_context(None)
114
-
115
- def test_raises_signal_with_keyword_args(self):
116
- """Test continue_as_new raises signal with keyword args."""
117
- ctx = LocalContext(
118
- run_id="test_run",
119
- workflow_name="test_workflow",
120
- storage=None,
121
- durable=False,
122
- )
123
- set_context(ctx)
124
-
125
- try:
126
- with pytest.raises(ContinueAsNewSignal) as exc_info:
127
- continue_as_new(cursor="abc123")
128
-
129
- assert exc_info.value.workflow_args == ()
130
- assert exc_info.value.workflow_kwargs == {"cursor": "abc123"}
131
- finally:
132
- set_context(None)
133
-
134
- def test_raises_signal_with_mixed_args(self):
135
- """Test continue_as_new raises signal with mixed args."""
136
- ctx = LocalContext(
137
- run_id="test_run",
138
- workflow_name="test_workflow",
139
- storage=None,
140
- durable=False,
141
- )
142
- set_context(ctx)
143
-
144
- try:
145
- with pytest.raises(ContinueAsNewSignal) as exc_info:
146
- continue_as_new("pos_arg", key1="val1", key2="val2")
147
-
148
- assert exc_info.value.workflow_args == ("pos_arg",)
149
- assert exc_info.value.workflow_kwargs == {"key1": "val1", "key2": "val2"}
150
- finally:
151
- set_context(None)
152
-
153
- def test_checks_cancellation_before_raising_signal(self):
154
- """Test continue_as_new checks cancellation first."""
155
- ctx = LocalContext(
156
- run_id="test_run",
157
- workflow_name="test_workflow",
158
- storage=None,
159
- durable=False,
160
- )
161
- ctx.request_cancellation(reason="User cancelled")
162
- set_context(ctx)
163
-
164
- try:
165
- with pytest.raises(CancellationError):
166
- continue_as_new("arg1")
167
- finally:
168
- set_context(None)
169
-
170
-
171
- class TestContinuedAsNewStatus:
172
- """Test CONTINUED_AS_NEW status."""
173
-
174
- def test_status_exists_in_enum(self):
175
- """Test CONTINUED_AS_NEW exists in RunStatus enum."""
176
- assert hasattr(RunStatus, "CONTINUED_AS_NEW")
177
- assert RunStatus.CONTINUED_AS_NEW.value == "continued_as_new"
178
-
179
- def test_status_is_distinct_from_completed(self):
180
- """Test CONTINUED_AS_NEW is distinct from COMPLETED."""
181
- assert RunStatus.CONTINUED_AS_NEW != RunStatus.COMPLETED
182
-
183
-
184
- class TestContinuedAsNewEvent:
185
- """Test WORKFLOW_CONTINUED_AS_NEW event type."""
186
-
187
- def test_event_type_exists(self):
188
- """Test WORKFLOW_CONTINUED_AS_NEW exists in EventType enum."""
189
- assert hasattr(EventType, "WORKFLOW_CONTINUED_AS_NEW")
190
- assert EventType.WORKFLOW_CONTINUED_AS_NEW.value == "workflow.continued_as_new"
191
-
192
- def test_create_event(self):
193
- """Test create_workflow_continued_as_new_event."""
194
- event = create_workflow_continued_as_new_event(
195
- run_id="run_123",
196
- new_run_id="run_456",
197
- args='["arg1", "arg2"]',
198
- kwargs='{"key": "value"}',
199
- reason="Event limit reached",
200
- )
201
-
202
- assert event.run_id == "run_123"
203
- assert event.type == EventType.WORKFLOW_CONTINUED_AS_NEW
204
- assert event.data["new_run_id"] == "run_456"
205
- assert event.data["args"] == '["arg1", "arg2"]'
206
- assert event.data["kwargs"] == '{"key": "value"}'
207
- assert event.data["reason"] == "Event limit reached"
208
-
209
- def test_create_event_minimal(self):
210
- """Test create_workflow_continued_as_new_event with minimal params."""
211
- event = create_workflow_continued_as_new_event(
212
- run_id="run_123",
213
- new_run_id="run_456",
214
- args="[]",
215
- kwargs="{}",
216
- )
217
-
218
- assert event.run_id == "run_123"
219
- assert event.type == EventType.WORKFLOW_CONTINUED_AS_NEW
220
- assert event.data.get("reason") is None
221
-
222
-
223
- class TestWorkflowRunContinuationFields:
224
- """Test WorkflowRun continuation tracking fields."""
225
-
226
- def test_workflow_run_has_continuation_fields(self):
227
- """Test WorkflowRun has continued_from and continued_to fields."""
228
- run = WorkflowRun(
229
- run_id="run_123",
230
- workflow_name="test_workflow",
231
- status=RunStatus.PENDING,
232
- continued_from_run_id="run_100",
233
- continued_to_run_id="run_200",
234
- )
235
-
236
- assert run.continued_from_run_id == "run_100"
237
- assert run.continued_to_run_id == "run_200"
238
-
239
- def test_workflow_run_defaults_to_none(self):
240
- """Test continuation fields default to None."""
241
- run = WorkflowRun(
242
- run_id="run_123",
243
- workflow_name="test_workflow",
244
- status=RunStatus.PENDING,
245
- )
246
-
247
- assert run.continued_from_run_id is None
248
- assert run.continued_to_run_id is None
249
-
250
- def test_workflow_run_to_dict_includes_continuation_fields(self):
251
- """Test to_dict includes continuation fields."""
252
- run = WorkflowRun(
253
- run_id="run_123",
254
- workflow_name="test_workflow",
255
- status=RunStatus.CONTINUED_AS_NEW,
256
- continued_from_run_id="run_100",
257
- continued_to_run_id="run_200",
258
- )
259
-
260
- data = run.to_dict()
261
-
262
- assert data["continued_from_run_id"] == "run_100"
263
- assert data["continued_to_run_id"] == "run_200"
264
-
265
- def test_workflow_run_from_dict_parses_continuation_fields(self):
266
- """Test from_dict parses continuation fields."""
267
- from datetime import UTC, datetime
268
-
269
- now = datetime.now(UTC)
270
- data = {
271
- "run_id": "run_123",
272
- "workflow_name": "test_workflow",
273
- "status": "continued_as_new",
274
- "created_at": now.isoformat(),
275
- "updated_at": now.isoformat(),
276
- "continued_from_run_id": "run_100",
277
- "continued_to_run_id": "run_200",
278
- }
279
-
280
- run = WorkflowRun.from_dict(data)
281
-
282
- assert run.continued_from_run_id == "run_100"
283
- assert run.continued_to_run_id == "run_200"
284
-
285
-
286
- class TestStorageChainMethods:
287
- """Test storage backend chain tracking methods."""
288
-
289
- @pytest.mark.asyncio
290
- async def test_update_run_continuation(self):
291
- """Test update_run_continuation sets continued_to_run_id."""
292
- storage = InMemoryStorageBackend()
293
-
294
- run = WorkflowRun(
295
- run_id="run_1",
296
- workflow_name="test_workflow",
297
- status=RunStatus.PENDING,
298
- )
299
- await storage.create_run(run)
300
-
301
- await storage.update_run_continuation("run_1", "run_2")
302
-
303
- updated_run = await storage.get_run("run_1")
304
- assert updated_run.continued_to_run_id == "run_2"
305
-
306
- @pytest.mark.asyncio
307
- async def test_update_run_continuation_nonexistent(self):
308
- """Test update_run_continuation for non-existent run does not raise."""
309
- storage = InMemoryStorageBackend()
310
-
311
- # Should not raise
312
- await storage.update_run_continuation("nonexistent_run", "run_2")
313
-
314
- @pytest.mark.asyncio
315
- async def test_get_workflow_chain_single_run(self):
316
- """Test get_workflow_chain returns single run for no chain."""
317
- storage = InMemoryStorageBackend()
318
-
319
- run = WorkflowRun(
320
- run_id="run_1",
321
- workflow_name="test_workflow",
322
- status=RunStatus.PENDING,
323
- )
324
- await storage.create_run(run)
325
-
326
- chain = await storage.get_workflow_chain("run_1")
327
-
328
- assert len(chain) == 1
329
- assert chain[0].run_id == "run_1"
330
-
331
- @pytest.mark.asyncio
332
- async def test_get_workflow_chain_two_runs(self):
333
- """Test get_workflow_chain returns ordered chain of two runs."""
334
- storage = InMemoryStorageBackend()
335
-
336
- # Create first run
337
- run1 = WorkflowRun(
338
- run_id="run_1",
339
- workflow_name="test_workflow",
340
- status=RunStatus.CONTINUED_AS_NEW,
341
- )
342
- await storage.create_run(run1)
343
-
344
- # Create second run with continued_from
345
- run2 = WorkflowRun(
346
- run_id="run_2",
347
- workflow_name="test_workflow",
348
- status=RunStatus.RUNNING,
349
- continued_from_run_id="run_1",
350
- )
351
- await storage.create_run(run2)
352
-
353
- # Link first run to second
354
- await storage.update_run_continuation("run_1", "run_2")
355
-
356
- # Query from either run should give same chain
357
- chain_from_1 = await storage.get_workflow_chain("run_1")
358
- chain_from_2 = await storage.get_workflow_chain("run_2")
359
-
360
- assert len(chain_from_1) == 2
361
- assert len(chain_from_2) == 2
362
- assert chain_from_1[0].run_id == "run_1"
363
- assert chain_from_1[1].run_id == "run_2"
364
- assert chain_from_2[0].run_id == "run_1"
365
- assert chain_from_2[1].run_id == "run_2"
366
-
367
- @pytest.mark.asyncio
368
- async def test_get_workflow_chain_three_runs(self):
369
- """Test get_workflow_chain returns ordered chain of three runs."""
370
- storage = InMemoryStorageBackend()
371
-
372
- # Create chain: run_1 -> run_2 -> run_3
373
- run1 = WorkflowRun(
374
- run_id="run_1",
375
- workflow_name="test_workflow",
376
- status=RunStatus.CONTINUED_AS_NEW,
377
- )
378
- await storage.create_run(run1)
379
-
380
- run2 = WorkflowRun(
381
- run_id="run_2",
382
- workflow_name="test_workflow",
383
- status=RunStatus.CONTINUED_AS_NEW,
384
- continued_from_run_id="run_1",
385
- )
386
- await storage.create_run(run2)
387
-
388
- run3 = WorkflowRun(
389
- run_id="run_3",
390
- workflow_name="test_workflow",
391
- status=RunStatus.RUNNING,
392
- continued_from_run_id="run_2",
393
- )
394
- await storage.create_run(run3)
395
-
396
- await storage.update_run_continuation("run_1", "run_2")
397
- await storage.update_run_continuation("run_2", "run_3")
398
-
399
- # Query from middle run
400
- chain = await storage.get_workflow_chain("run_2")
401
-
402
- assert len(chain) == 3
403
- assert [r.run_id for r in chain] == ["run_1", "run_2", "run_3"]
404
-
405
- @pytest.mark.asyncio
406
- async def test_get_workflow_chain_nonexistent_run(self):
407
- """Test get_workflow_chain returns empty list for nonexistent run."""
408
- storage = InMemoryStorageBackend()
409
-
410
- chain = await storage.get_workflow_chain("nonexistent")
411
-
412
- assert chain == []
413
-
414
-
415
- class TestMockContextContinueAsNew:
416
- """Test MockContext with continue_as_new."""
417
-
418
- def test_mock_context_allows_continue_as_new(self):
419
- """Test continue_as_new works with MockContext."""
420
- ctx = MockContext(run_id="test", workflow_name="test")
421
- set_context(ctx)
422
-
423
- try:
424
- with pytest.raises(ContinueAsNewSignal) as exc_info:
425
- continue_as_new("arg1")
426
-
427
- assert exc_info.value.workflow_args == ("arg1",)
428
- finally:
429
- set_context(None)
430
-
431
- def test_mock_context_cancellation_prevents_continue_as_new(self):
432
- """Test MockContext cancellation prevents continue_as_new."""
433
- ctx = MockContext(run_id="test", workflow_name="test")
434
- ctx.request_cancellation()
435
- set_context(ctx)
436
-
437
- try:
438
- with pytest.raises(CancellationError):
439
- continue_as_new("arg1")
440
- finally:
441
- set_context(None)