pyworkflow-engine 0.1.7__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 (196) hide show
  1. dashboard/backend/app/__init__.py +1 -0
  2. dashboard/backend/app/config.py +32 -0
  3. dashboard/backend/app/controllers/__init__.py +6 -0
  4. dashboard/backend/app/controllers/run_controller.py +86 -0
  5. dashboard/backend/app/controllers/workflow_controller.py +33 -0
  6. dashboard/backend/app/dependencies/__init__.py +5 -0
  7. dashboard/backend/app/dependencies/storage.py +50 -0
  8. dashboard/backend/app/repositories/__init__.py +6 -0
  9. dashboard/backend/app/repositories/run_repository.py +80 -0
  10. dashboard/backend/app/repositories/workflow_repository.py +27 -0
  11. dashboard/backend/app/rest/__init__.py +8 -0
  12. dashboard/backend/app/rest/v1/__init__.py +12 -0
  13. dashboard/backend/app/rest/v1/health.py +33 -0
  14. dashboard/backend/app/rest/v1/runs.py +133 -0
  15. dashboard/backend/app/rest/v1/workflows.py +41 -0
  16. dashboard/backend/app/schemas/__init__.py +23 -0
  17. dashboard/backend/app/schemas/common.py +16 -0
  18. dashboard/backend/app/schemas/event.py +24 -0
  19. dashboard/backend/app/schemas/hook.py +25 -0
  20. dashboard/backend/app/schemas/run.py +54 -0
  21. dashboard/backend/app/schemas/step.py +28 -0
  22. dashboard/backend/app/schemas/workflow.py +31 -0
  23. dashboard/backend/app/server.py +87 -0
  24. dashboard/backend/app/services/__init__.py +6 -0
  25. dashboard/backend/app/services/run_service.py +240 -0
  26. dashboard/backend/app/services/workflow_service.py +155 -0
  27. dashboard/backend/main.py +18 -0
  28. docs/concepts/cancellation.mdx +362 -0
  29. docs/concepts/continue-as-new.mdx +434 -0
  30. docs/concepts/events.mdx +266 -0
  31. docs/concepts/fault-tolerance.mdx +370 -0
  32. docs/concepts/hooks.mdx +552 -0
  33. docs/concepts/limitations.mdx +167 -0
  34. docs/concepts/schedules.mdx +775 -0
  35. docs/concepts/sleep.mdx +312 -0
  36. docs/concepts/steps.mdx +301 -0
  37. docs/concepts/workflows.mdx +255 -0
  38. docs/guides/cli.mdx +942 -0
  39. docs/guides/configuration.mdx +560 -0
  40. docs/introduction.mdx +155 -0
  41. docs/quickstart.mdx +279 -0
  42. examples/__init__.py +1 -0
  43. examples/celery/__init__.py +1 -0
  44. examples/celery/durable/docker-compose.yml +55 -0
  45. examples/celery/durable/pyworkflow.config.yaml +12 -0
  46. examples/celery/durable/workflows/__init__.py +122 -0
  47. examples/celery/durable/workflows/basic.py +87 -0
  48. examples/celery/durable/workflows/batch_processing.py +102 -0
  49. examples/celery/durable/workflows/cancellation.py +273 -0
  50. examples/celery/durable/workflows/child_workflow_patterns.py +240 -0
  51. examples/celery/durable/workflows/child_workflows.py +202 -0
  52. examples/celery/durable/workflows/continue_as_new.py +260 -0
  53. examples/celery/durable/workflows/fault_tolerance.py +210 -0
  54. examples/celery/durable/workflows/hooks.py +211 -0
  55. examples/celery/durable/workflows/idempotency.py +112 -0
  56. examples/celery/durable/workflows/long_running.py +99 -0
  57. examples/celery/durable/workflows/retries.py +101 -0
  58. examples/celery/durable/workflows/schedules.py +209 -0
  59. examples/celery/transient/01_basic_workflow.py +91 -0
  60. examples/celery/transient/02_fault_tolerance.py +257 -0
  61. examples/celery/transient/__init__.py +20 -0
  62. examples/celery/transient/pyworkflow.config.yaml +25 -0
  63. examples/local/__init__.py +1 -0
  64. examples/local/durable/01_basic_workflow.py +94 -0
  65. examples/local/durable/02_file_storage.py +132 -0
  66. examples/local/durable/03_retries.py +169 -0
  67. examples/local/durable/04_long_running.py +119 -0
  68. examples/local/durable/05_event_log.py +145 -0
  69. examples/local/durable/06_idempotency.py +148 -0
  70. examples/local/durable/07_hooks.py +334 -0
  71. examples/local/durable/08_cancellation.py +233 -0
  72. examples/local/durable/09_child_workflows.py +198 -0
  73. examples/local/durable/10_child_workflow_patterns.py +265 -0
  74. examples/local/durable/11_continue_as_new.py +249 -0
  75. examples/local/durable/12_schedules.py +198 -0
  76. examples/local/durable/__init__.py +1 -0
  77. examples/local/transient/01_quick_tasks.py +87 -0
  78. examples/local/transient/02_retries.py +130 -0
  79. examples/local/transient/03_sleep.py +141 -0
  80. examples/local/transient/__init__.py +1 -0
  81. pyworkflow/__init__.py +256 -0
  82. pyworkflow/aws/__init__.py +68 -0
  83. pyworkflow/aws/context.py +234 -0
  84. pyworkflow/aws/handler.py +184 -0
  85. pyworkflow/aws/testing.py +310 -0
  86. pyworkflow/celery/__init__.py +41 -0
  87. pyworkflow/celery/app.py +198 -0
  88. pyworkflow/celery/scheduler.py +315 -0
  89. pyworkflow/celery/tasks.py +1746 -0
  90. pyworkflow/cli/__init__.py +132 -0
  91. pyworkflow/cli/__main__.py +6 -0
  92. pyworkflow/cli/commands/__init__.py +1 -0
  93. pyworkflow/cli/commands/hooks.py +640 -0
  94. pyworkflow/cli/commands/quickstart.py +495 -0
  95. pyworkflow/cli/commands/runs.py +773 -0
  96. pyworkflow/cli/commands/scheduler.py +130 -0
  97. pyworkflow/cli/commands/schedules.py +794 -0
  98. pyworkflow/cli/commands/setup.py +703 -0
  99. pyworkflow/cli/commands/worker.py +413 -0
  100. pyworkflow/cli/commands/workflows.py +1257 -0
  101. pyworkflow/cli/output/__init__.py +1 -0
  102. pyworkflow/cli/output/formatters.py +321 -0
  103. pyworkflow/cli/output/styles.py +121 -0
  104. pyworkflow/cli/utils/__init__.py +1 -0
  105. pyworkflow/cli/utils/async_helpers.py +30 -0
  106. pyworkflow/cli/utils/config.py +130 -0
  107. pyworkflow/cli/utils/config_generator.py +344 -0
  108. pyworkflow/cli/utils/discovery.py +53 -0
  109. pyworkflow/cli/utils/docker_manager.py +651 -0
  110. pyworkflow/cli/utils/interactive.py +364 -0
  111. pyworkflow/cli/utils/storage.py +115 -0
  112. pyworkflow/config.py +329 -0
  113. pyworkflow/context/__init__.py +63 -0
  114. pyworkflow/context/aws.py +230 -0
  115. pyworkflow/context/base.py +416 -0
  116. pyworkflow/context/local.py +930 -0
  117. pyworkflow/context/mock.py +381 -0
  118. pyworkflow/core/__init__.py +0 -0
  119. pyworkflow/core/exceptions.py +353 -0
  120. pyworkflow/core/registry.py +313 -0
  121. pyworkflow/core/scheduled.py +328 -0
  122. pyworkflow/core/step.py +494 -0
  123. pyworkflow/core/workflow.py +294 -0
  124. pyworkflow/discovery.py +248 -0
  125. pyworkflow/engine/__init__.py +0 -0
  126. pyworkflow/engine/events.py +879 -0
  127. pyworkflow/engine/executor.py +682 -0
  128. pyworkflow/engine/replay.py +273 -0
  129. pyworkflow/observability/__init__.py +19 -0
  130. pyworkflow/observability/logging.py +234 -0
  131. pyworkflow/primitives/__init__.py +33 -0
  132. pyworkflow/primitives/child_handle.py +174 -0
  133. pyworkflow/primitives/child_workflow.py +372 -0
  134. pyworkflow/primitives/continue_as_new.py +101 -0
  135. pyworkflow/primitives/define_hook.py +150 -0
  136. pyworkflow/primitives/hooks.py +97 -0
  137. pyworkflow/primitives/resume_hook.py +210 -0
  138. pyworkflow/primitives/schedule.py +545 -0
  139. pyworkflow/primitives/shield.py +96 -0
  140. pyworkflow/primitives/sleep.py +100 -0
  141. pyworkflow/runtime/__init__.py +21 -0
  142. pyworkflow/runtime/base.py +179 -0
  143. pyworkflow/runtime/celery.py +310 -0
  144. pyworkflow/runtime/factory.py +101 -0
  145. pyworkflow/runtime/local.py +706 -0
  146. pyworkflow/scheduler/__init__.py +9 -0
  147. pyworkflow/scheduler/local.py +248 -0
  148. pyworkflow/serialization/__init__.py +0 -0
  149. pyworkflow/serialization/decoder.py +146 -0
  150. pyworkflow/serialization/encoder.py +162 -0
  151. pyworkflow/storage/__init__.py +54 -0
  152. pyworkflow/storage/base.py +612 -0
  153. pyworkflow/storage/config.py +185 -0
  154. pyworkflow/storage/dynamodb.py +1315 -0
  155. pyworkflow/storage/file.py +827 -0
  156. pyworkflow/storage/memory.py +549 -0
  157. pyworkflow/storage/postgres.py +1161 -0
  158. pyworkflow/storage/schemas.py +486 -0
  159. pyworkflow/storage/sqlite.py +1136 -0
  160. pyworkflow/utils/__init__.py +0 -0
  161. pyworkflow/utils/duration.py +177 -0
  162. pyworkflow/utils/schedule.py +391 -0
  163. pyworkflow_engine-0.1.7.dist-info/METADATA +687 -0
  164. pyworkflow_engine-0.1.7.dist-info/RECORD +196 -0
  165. pyworkflow_engine-0.1.7.dist-info/WHEEL +5 -0
  166. pyworkflow_engine-0.1.7.dist-info/entry_points.txt +2 -0
  167. pyworkflow_engine-0.1.7.dist-info/licenses/LICENSE +21 -0
  168. pyworkflow_engine-0.1.7.dist-info/top_level.txt +5 -0
  169. tests/examples/__init__.py +0 -0
  170. tests/integration/__init__.py +0 -0
  171. tests/integration/test_cancellation.py +330 -0
  172. tests/integration/test_child_workflows.py +439 -0
  173. tests/integration/test_continue_as_new.py +428 -0
  174. tests/integration/test_dynamodb_storage.py +1146 -0
  175. tests/integration/test_fault_tolerance.py +369 -0
  176. tests/integration/test_schedule_storage.py +484 -0
  177. tests/unit/__init__.py +0 -0
  178. tests/unit/backends/__init__.py +1 -0
  179. tests/unit/backends/test_dynamodb_storage.py +1554 -0
  180. tests/unit/backends/test_postgres_storage.py +1281 -0
  181. tests/unit/backends/test_sqlite_storage.py +1460 -0
  182. tests/unit/conftest.py +41 -0
  183. tests/unit/test_cancellation.py +364 -0
  184. tests/unit/test_child_workflows.py +680 -0
  185. tests/unit/test_continue_as_new.py +441 -0
  186. tests/unit/test_event_limits.py +316 -0
  187. tests/unit/test_executor.py +320 -0
  188. tests/unit/test_fault_tolerance.py +334 -0
  189. tests/unit/test_hooks.py +495 -0
  190. tests/unit/test_registry.py +261 -0
  191. tests/unit/test_replay.py +420 -0
  192. tests/unit/test_schedule_schemas.py +285 -0
  193. tests/unit/test_schedule_utils.py +286 -0
  194. tests/unit/test_scheduled_workflow.py +274 -0
  195. tests/unit/test_step.py +353 -0
  196. tests/unit/test_workflow.py +243 -0
@@ -0,0 +1,243 @@
1
+ """
2
+ Unit tests for @workflow decorator and workflow execution.
3
+ """
4
+
5
+ import pytest
6
+
7
+ from pyworkflow.context import LocalContext, get_context, has_context, set_context
8
+ from pyworkflow.core.workflow import execute_workflow_with_context, workflow
9
+ from pyworkflow.storage.file import FileStorageBackend
10
+
11
+
12
+ class TestWorkflowDecorator:
13
+ """Test the @workflow decorator."""
14
+
15
+ def test_workflow_decorator_basic(self):
16
+ """Test basic workflow decoration."""
17
+
18
+ @workflow()
19
+ async def simple_workflow():
20
+ return "success"
21
+
22
+ # Check that workflow attributes are set
23
+ assert hasattr(simple_workflow, "__workflow__")
24
+ assert simple_workflow.__workflow__ is True
25
+ assert simple_workflow.__workflow_name__ == "simple_workflow"
26
+
27
+ def test_workflow_decorator_with_name(self):
28
+ """Test workflow decorator with custom name."""
29
+
30
+ @workflow(name="custom_name")
31
+ async def my_workflow():
32
+ return "success"
33
+
34
+ assert my_workflow.__workflow_name__ == "custom_name"
35
+
36
+ def test_workflow_decorator_with_max_duration(self):
37
+ """Test workflow decorator with max_duration."""
38
+
39
+ @workflow(max_duration="2h")
40
+ async def timed_workflow():
41
+ return "success"
42
+
43
+ assert timed_workflow.__workflow_max_duration__ == "2h"
44
+
45
+ def test_workflow_decorator_with_tags(self):
46
+ """Test workflow decorator with tags."""
47
+ tags = ["backend", "critical"]
48
+
49
+ @workflow(tags=tags)
50
+ async def tagged_workflow():
51
+ return "success"
52
+
53
+ assert tagged_workflow.__workflow_tags__ == tags
54
+
55
+ def test_workflow_decorator_with_too_many_tags(self):
56
+ """Test workflow decorator rejects more than 3 tags."""
57
+ with pytest.raises(ValueError, match="at most 3 tags"):
58
+
59
+ @workflow(tags=["a", "b", "c", "d"])
60
+ async def too_many_tags_workflow():
61
+ return "success"
62
+
63
+ @pytest.mark.asyncio
64
+ async def test_workflow_execution(self):
65
+ """Test basic workflow execution."""
66
+
67
+ @workflow()
68
+ async def test_workflow(x: int):
69
+ return x * 2
70
+
71
+ result = await test_workflow(5)
72
+ assert result == 10
73
+
74
+ def test_workflow_registration(self):
75
+ """Test that workflow is registered in global registry."""
76
+ from pyworkflow.core.registry import get_workflow
77
+
78
+ @workflow(name="registered_workflow")
79
+ async def my_workflow():
80
+ return "success"
81
+
82
+ # Check that it's registered
83
+ workflow_meta = get_workflow("registered_workflow")
84
+ assert workflow_meta is not None
85
+ assert workflow_meta.name == "registered_workflow"
86
+
87
+ @pytest.mark.asyncio
88
+ async def test_workflow_with_args_and_kwargs(self):
89
+ """Test workflow with various argument types."""
90
+
91
+ @workflow()
92
+ async def args_workflow(a: int, b: int, c: int = 10):
93
+ return a + b + c
94
+
95
+ result = await args_workflow(1, 2, c=3)
96
+ assert result == 6
97
+
98
+
99
+ class TestWorkflowExecution:
100
+ """Test workflow execution with context."""
101
+
102
+ @pytest.mark.asyncio
103
+ async def test_execute_workflow_with_context(self, tmp_path):
104
+ """Test executing a workflow with proper context setup."""
105
+
106
+ @workflow()
107
+ async def context_workflow(value: str):
108
+ ctx = get_context()
109
+ assert ctx.run_id == "test_run_123"
110
+ assert ctx.workflow_name == "test_workflow"
111
+ return f"processed: {value}"
112
+
113
+ # Create storage backend
114
+ storage = FileStorageBackend(base_path=str(tmp_path))
115
+
116
+ # Execute with context
117
+ result = await execute_workflow_with_context(
118
+ workflow_func=context_workflow,
119
+ run_id="test_run_123",
120
+ workflow_name="test_workflow",
121
+ storage=storage,
122
+ args=("test_value",),
123
+ kwargs={},
124
+ )
125
+
126
+ assert result == "processed: test_value"
127
+
128
+ @pytest.mark.asyncio
129
+ async def test_context_cleared_after_execution(self, tmp_path):
130
+ """Test that context is cleared after workflow execution."""
131
+
132
+ @workflow()
133
+ async def cleanup_workflow():
134
+ return "done"
135
+
136
+ storage = FileStorageBackend(base_path=str(tmp_path))
137
+
138
+ await execute_workflow_with_context(
139
+ workflow_func=cleanup_workflow,
140
+ run_id="test_run",
141
+ workflow_name="cleanup_test",
142
+ storage=storage,
143
+ args=(),
144
+ kwargs={},
145
+ )
146
+
147
+ # Context should be cleared
148
+ assert not has_context()
149
+
150
+ @pytest.mark.asyncio
151
+ async def test_workflow_exception_handling(self, tmp_path):
152
+ """Test that workflow exceptions are properly handled."""
153
+
154
+ @workflow()
155
+ async def failing_workflow():
156
+ raise ValueError("Test error")
157
+
158
+ storage = FileStorageBackend(base_path=str(tmp_path))
159
+
160
+ with pytest.raises(ValueError, match="Test error"):
161
+ await execute_workflow_with_context(
162
+ workflow_func=failing_workflow,
163
+ run_id="test_run",
164
+ workflow_name="failing_test",
165
+ storage=storage,
166
+ args=(),
167
+ kwargs={},
168
+ )
169
+
170
+ # Context should still be cleared after exception
171
+ assert not has_context()
172
+
173
+ @pytest.mark.asyncio
174
+ async def test_workflow_event_recording(self, tmp_path):
175
+ """Test that workflow execution records events."""
176
+
177
+ @workflow()
178
+ async def event_workflow():
179
+ return "completed"
180
+
181
+ storage = FileStorageBackend(base_path=str(tmp_path))
182
+ run_id = "test_run_events"
183
+
184
+ await execute_workflow_with_context(
185
+ workflow_func=event_workflow,
186
+ run_id=run_id,
187
+ workflow_name="event_test",
188
+ storage=storage,
189
+ args=(),
190
+ kwargs={},
191
+ )
192
+
193
+ # Check that events were recorded
194
+ events = await storage.get_events(run_id)
195
+ assert len(events) >= 1
196
+
197
+ # Should have workflow.completed event
198
+ event_types = [e.type.value for e in events]
199
+ assert "workflow.completed" in event_types
200
+
201
+ @pytest.mark.asyncio
202
+ async def test_workflow_with_nested_context(self, tmp_path):
203
+ """Test workflow execution restores previous context."""
204
+ from pyworkflow.context import reset_context
205
+
206
+ # Set up an initial context
207
+ initial_storage = FileStorageBackend(base_path=str(tmp_path / "initial"))
208
+ initial_ctx = LocalContext(
209
+ run_id="initial_run",
210
+ workflow_name="initial_workflow",
211
+ storage=initial_storage,
212
+ )
213
+ initial_token = set_context(initial_ctx)
214
+
215
+ try:
216
+
217
+ @workflow()
218
+ async def nested_workflow():
219
+ ctx = get_context()
220
+ # This should be the new context
221
+ assert ctx.run_id == "nested_run"
222
+ return "nested"
223
+
224
+ storage = FileStorageBackend(base_path=str(tmp_path / "nested"))
225
+
226
+ result = await execute_workflow_with_context(
227
+ workflow_func=nested_workflow,
228
+ run_id="nested_run",
229
+ workflow_name="nested_test",
230
+ storage=storage,
231
+ args=(),
232
+ kwargs={},
233
+ )
234
+
235
+ assert result == "nested"
236
+
237
+ # After execution, context should be restored to initial
238
+ # (token-based reset restores previous context)
239
+ assert has_context()
240
+ ctx = get_context()
241
+ assert ctx.run_id == "initial_run"
242
+ finally:
243
+ reset_context(initial_token)