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,184 @@
1
+ """
2
+ AWS Lambda handler wrapper for PyWorkflow workflows.
3
+
4
+ This module provides a decorator to create AWS Lambda handlers from
5
+ PyWorkflow workflow functions.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import functools
12
+ from collections.abc import Callable
13
+ from typing import TYPE_CHECKING, Any, TypeVar
14
+
15
+ from loguru import logger
16
+
17
+ from pyworkflow.context import reset_context, set_context
18
+
19
+ from .context import AWSWorkflowContext
20
+
21
+ if TYPE_CHECKING:
22
+ from aws_durable_execution_sdk_python import DurableContext
23
+
24
+ # Type variable for the workflow function
25
+ F = TypeVar("F", bound=Callable[..., Any])
26
+
27
+
28
+ def aws_workflow_handler(workflow_fn: F) -> Callable[[dict[str, Any], Any], Any]:
29
+ """
30
+ Decorator to create AWS Lambda handler from a PyWorkflow workflow.
31
+
32
+ This decorator wraps a PyWorkflow workflow function and creates an AWS
33
+ Lambda handler that uses AWS Durable Execution SDK for checkpointing
34
+ and durability.
35
+
36
+ The decorated function receives the Lambda event as keyword arguments,
37
+ and has access to an AWSWorkflowContext for step execution and sleeping.
38
+
39
+ Note: The AWS SDK is imported lazily - you can define workflows locally
40
+ without the SDK installed, and test them with MockDurableContext.
41
+ The SDK is only required when actually running on AWS Lambda.
42
+
43
+ Usage:
44
+ ```python
45
+ from pyworkflow import workflow, step
46
+ from pyworkflow.aws import aws_workflow_handler
47
+
48
+ @step
49
+ async def process_data(data: str) -> dict:
50
+ return {"processed": data}
51
+
52
+ @aws_workflow_handler
53
+ @workflow
54
+ async def my_workflow(ctx: AWSWorkflowContext, data: str):
55
+ result = await process_data(data)
56
+ ctx.sleep(300) # Wait 5 minutes
57
+ return result
58
+
59
+ # Export as Lambda handler
60
+ handler = my_workflow
61
+ ```
62
+
63
+ Args:
64
+ workflow_fn: A PyWorkflow workflow function (sync or async)
65
+
66
+ Returns:
67
+ An AWS Lambda handler function (decorated with @durable_execution when SDK available)
68
+ """
69
+ # Get workflow name for logging
70
+ workflow_name = getattr(workflow_fn, "__name__", "unknown_workflow")
71
+
72
+ # Try to import AWS SDK - if available, use real decorator
73
+ # If not available, create a wrapper that fails at runtime
74
+ try:
75
+ from aws_durable_execution_sdk_python import durable_execution
76
+
77
+ _has_aws_sdk = True
78
+ except ImportError:
79
+ _has_aws_sdk = False
80
+
81
+ def durable_execution(f):
82
+ return f # no-op decorator
83
+
84
+ @durable_execution
85
+ @functools.wraps(workflow_fn)
86
+ def lambda_handler(event: dict[str, Any], context: Any) -> Any:
87
+ """
88
+ AWS Lambda handler that executes the PyWorkflow workflow.
89
+
90
+ Args:
91
+ event: Lambda event payload (passed as kwargs to workflow)
92
+ context: AWS DurableContext for checkpointing
93
+
94
+ Returns:
95
+ The result of the workflow execution
96
+ """
97
+ # Check if SDK is available when actually executing
98
+ if not _has_aws_sdk:
99
+ raise ImportError(
100
+ "aws-durable-execution-sdk-python is required for AWS runtime. "
101
+ "Install it with: pip install pyworkflow[aws]\n"
102
+ "For local testing, use create_test_handler() from pyworkflow.aws.testing"
103
+ )
104
+
105
+ logger.info(f"Starting AWS workflow: {workflow_name}", event=event)
106
+
107
+ # Create PyWorkflow AWS context adapter
108
+ aws_ctx = AWSWorkflowContext(context)
109
+
110
+ # Set the implicit context
111
+ token = set_context(aws_ctx)
112
+
113
+ try:
114
+ # Execute the workflow (no longer pass ctx explicitly)
115
+ if asyncio.iscoroutinefunction(workflow_fn):
116
+ # Async workflow - run in event loop
117
+ try:
118
+ loop = asyncio.get_running_loop()
119
+ except RuntimeError:
120
+ loop = None
121
+
122
+ if loop is not None:
123
+ # Running in async context (unusual for Lambda)
124
+ import concurrent.futures
125
+
126
+ with concurrent.futures.ThreadPoolExecutor() as executor:
127
+ future = executor.submit(asyncio.run, workflow_fn(**event))
128
+ result = future.result()
129
+ else:
130
+ # Normal Lambda execution
131
+ result = asyncio.run(workflow_fn(**event))
132
+ else:
133
+ # Sync workflow - execute directly
134
+ result = workflow_fn(**event)
135
+
136
+ logger.info(f"AWS workflow completed: {workflow_name}")
137
+ return result
138
+
139
+ except Exception as e:
140
+ logger.error(
141
+ f"AWS workflow failed: {workflow_name}",
142
+ error=str(e),
143
+ exc_info=True,
144
+ )
145
+ raise
146
+
147
+ finally:
148
+ # Reset the implicit context
149
+ reset_context(token)
150
+ # Clean up AWS-specific context
151
+ aws_ctx.cleanup()
152
+
153
+ # Preserve original function metadata
154
+ lambda_handler.__pyworkflow_workflow__ = workflow_fn
155
+ lambda_handler.__pyworkflow_workflow_name__ = workflow_name
156
+
157
+ return lambda_handler
158
+
159
+
160
+ def create_lambda_handler(
161
+ workflow_fn: Callable[..., Any],
162
+ ) -> Callable[[dict[str, Any], DurableContext], Any]:
163
+ """
164
+ Alternative function-based API to create Lambda handler.
165
+
166
+ This is an alternative to the decorator approach for cases where
167
+ decoration at definition time isn't convenient.
168
+
169
+ Usage:
170
+ ```python
171
+ @workflow
172
+ async def my_workflow(ctx, data: str):
173
+ return {"result": data}
174
+
175
+ handler = create_lambda_handler(my_workflow)
176
+ ```
177
+
178
+ Args:
179
+ workflow_fn: A PyWorkflow workflow function
180
+
181
+ Returns:
182
+ An AWS Lambda handler function
183
+ """
184
+ return aws_workflow_handler(workflow_fn)
@@ -0,0 +1,310 @@
1
+ """
2
+ Testing utilities for AWS Durable Lambda Functions.
3
+
4
+ This module provides mock implementations of AWS SDK components
5
+ for local testing without deploying to AWS.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ from collections.abc import Callable
12
+ from typing import Any
13
+
14
+ from loguru import logger
15
+
16
+
17
+ class MockDuration:
18
+ """Mock implementation of AWS Duration for testing."""
19
+
20
+ def __init__(self, seconds: int) -> None:
21
+ self._seconds = seconds
22
+
23
+ @classmethod
24
+ def from_seconds(cls, seconds: int) -> MockDuration:
25
+ return cls(seconds)
26
+
27
+ @classmethod
28
+ def from_minutes(cls, minutes: int) -> MockDuration:
29
+ return cls(minutes * 60)
30
+
31
+ @classmethod
32
+ def from_hours(cls, hours: int) -> MockDuration:
33
+ return cls(hours * 3600)
34
+
35
+ @property
36
+ def seconds(self) -> int:
37
+ return self._seconds
38
+
39
+
40
+ class MockDurableContext:
41
+ """
42
+ Mock implementation of AWS DurableContext for local testing.
43
+
44
+ This class simulates the behavior of AWS Durable Execution SDK's
45
+ DurableContext, allowing you to test workflows locally without
46
+ deploying to AWS.
47
+
48
+ The mock supports:
49
+ - Step execution with optional checkpointing simulation
50
+ - Wait/sleep (skipped in tests by default)
51
+ - Checkpoint tracking for verification
52
+
53
+ Usage:
54
+ ```python
55
+ from pyworkflow.aws.testing import MockDurableContext
56
+ from pyworkflow.aws import AWSWorkflowContext
57
+
58
+ def test_my_workflow():
59
+ # Create mock context
60
+ mock_ctx = MockDurableContext()
61
+ aws_ctx = AWSWorkflowContext(mock_ctx)
62
+
63
+ # Run workflow
64
+ result = my_workflow(aws_ctx, order_id="123")
65
+
66
+ # Verify checkpoints
67
+ assert "validate_order" in mock_ctx.checkpoints
68
+ assert mock_ctx.wait_count > 0
69
+ ```
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ skip_waits: bool = True,
75
+ simulate_replay: bool = False,
76
+ checkpoint_data: dict[str, Any] | None = None,
77
+ ) -> None:
78
+ """
79
+ Initialize the mock context.
80
+
81
+ Args:
82
+ skip_waits: If True, wait() calls return immediately (default: True)
83
+ simulate_replay: If True, use checkpoint_data for replaying steps
84
+ checkpoint_data: Pre-populated checkpoint data for replay simulation
85
+ """
86
+ self.skip_waits = skip_waits
87
+ self.simulate_replay = simulate_replay
88
+
89
+ # Tracking data
90
+ self._checkpoints: dict[str, Any] = checkpoint_data or {}
91
+ self._step_calls: list[dict[str, Any]] = []
92
+ self._wait_calls: list[int] = []
93
+
94
+ @property
95
+ def checkpoints(self) -> dict[str, Any]:
96
+ """Get all recorded checkpoints."""
97
+ return self._checkpoints.copy()
98
+
99
+ @property
100
+ def step_calls(self) -> list[dict[str, Any]]:
101
+ """Get list of all step() calls made."""
102
+ return self._step_calls.copy()
103
+
104
+ @property
105
+ def wait_calls(self) -> list[int]:
106
+ """Get list of all wait() durations (in seconds)."""
107
+ return self._wait_calls.copy()
108
+
109
+ @property
110
+ def wait_count(self) -> int:
111
+ """Get total number of wait() calls."""
112
+ return len(self._wait_calls)
113
+
114
+ def step(
115
+ self,
116
+ fn: Callable[[Any], Any],
117
+ name: str | None = None,
118
+ ) -> Any:
119
+ """
120
+ Execute a step function with checkpointing.
121
+
122
+ In replay mode, returns cached result if available.
123
+ Otherwise, executes the function and caches the result.
124
+
125
+ Args:
126
+ fn: The step function to execute
127
+ name: Optional step name
128
+
129
+ Returns:
130
+ The result of the step function
131
+ """
132
+ step_name = name or f"step_{len(self._step_calls)}"
133
+
134
+ logger.debug(f"Mock step: {step_name}")
135
+
136
+ # Record the call
137
+ call_info = {"name": step_name, "fn": fn}
138
+ self._step_calls.append(call_info)
139
+
140
+ # Check for replay
141
+ if self.simulate_replay and step_name in self._checkpoints:
142
+ logger.debug(f"Mock step replay: {step_name}")
143
+ return self._checkpoints[step_name]
144
+
145
+ # Execute the function
146
+ result = fn(None)
147
+
148
+ # Store checkpoint
149
+ self._checkpoints[step_name] = result
150
+
151
+ return result
152
+
153
+ def wait(self, duration: MockDuration | int) -> None:
154
+ """
155
+ Wait for specified duration.
156
+
157
+ Args:
158
+ duration: MockDuration or seconds to wait
159
+ """
160
+ seconds = duration.seconds if isinstance(duration, MockDuration) else int(duration)
161
+
162
+ logger.debug(f"Mock wait: {seconds} seconds")
163
+
164
+ # Record the call
165
+ self._wait_calls.append(seconds)
166
+
167
+ # Optionally skip the actual wait
168
+ if not self.skip_waits:
169
+ import time
170
+
171
+ time.sleep(seconds)
172
+
173
+ def create_callback(
174
+ self,
175
+ name: str | None = None,
176
+ config: Any | None = None,
177
+ ) -> MockCallback:
178
+ """
179
+ Create a callback for external input (webhook/approval).
180
+
181
+ Args:
182
+ name: Optional callback name
183
+ config: Optional callback configuration
184
+
185
+ Returns:
186
+ MockCallback object
187
+ """
188
+ callback_name = name or f"callback_{len(self._step_calls)}"
189
+ logger.debug(f"Mock create_callback: {callback_name}")
190
+ return MockCallback(callback_name)
191
+
192
+ def wait_for_callback(
193
+ self,
194
+ fn: Callable[[str], Any],
195
+ name: str | None = None,
196
+ config: Any | None = None,
197
+ ) -> Any:
198
+ """
199
+ Wait for a callback (combines create_callback + result).
200
+
201
+ Args:
202
+ fn: Function that takes callback_id and triggers external process
203
+ name: Optional callback name
204
+ config: Optional callback configuration
205
+
206
+ Returns:
207
+ The callback result
208
+ """
209
+ callback_name = name or f"callback_{len(self._step_calls)}"
210
+ logger.debug(f"Mock wait_for_callback: {callback_name}")
211
+
212
+ # Execute the function with a mock callback ID
213
+ callback_id = f"mock_callback_{callback_name}"
214
+ fn(callback_id)
215
+
216
+ # Return mock result
217
+ return {"callback_id": callback_id, "received": True}
218
+
219
+ def parallel(self, *tasks: Callable[[MockDurableContext], Any]) -> list[Any]:
220
+ """
221
+ Execute multiple tasks in parallel.
222
+
223
+ Args:
224
+ *tasks: Task functions that take context as argument
225
+
226
+ Returns:
227
+ List of results from all tasks
228
+ """
229
+ logger.debug(f"Mock parallel: {len(tasks)} tasks")
230
+ results = []
231
+ for task in tasks:
232
+ result = task(self)
233
+ results.append(result)
234
+ return results
235
+
236
+ def reset(self) -> None:
237
+ """Reset all tracking data for a fresh test run."""
238
+ self._checkpoints.clear()
239
+ self._step_calls.clear()
240
+ self._wait_calls.clear()
241
+
242
+
243
+ class MockCallback:
244
+ """Mock callback for testing webhook/approval patterns."""
245
+
246
+ def __init__(self, name: str) -> None:
247
+ self.name = name
248
+ self.callback_id = f"mock_callback_{name}"
249
+ self._result: Any | None = None
250
+ self._completed = False
251
+
252
+ def complete(self, payload: Any) -> None:
253
+ """Complete the callback with a payload."""
254
+ self._result = payload
255
+ self._completed = True
256
+
257
+ def result(self) -> Any:
258
+ """
259
+ Get the callback result.
260
+
261
+ In tests, you typically call complete() before result().
262
+ """
263
+ if not self._completed:
264
+ # Return mock result for testing
265
+ return {"callback_id": self.callback_id, "mock": True}
266
+ return self._result
267
+
268
+
269
+ def create_test_handler(
270
+ workflow_fn: Callable[..., Any],
271
+ mock_ctx: MockDurableContext | None = None,
272
+ ) -> Callable[[dict[str, Any]], Any]:
273
+ """
274
+ Create a test handler for a PyWorkflow workflow.
275
+
276
+ This function creates a handler that can be used in tests without
277
+ the AWS SDK dependency.
278
+
279
+ Usage:
280
+ ```python
281
+ @workflow
282
+ async def my_workflow(ctx, data: str):
283
+ return {"result": data}
284
+
285
+ handler = create_test_handler(my_workflow)
286
+ result = handler({"data": "test"})
287
+ assert result == {"result": "test"}
288
+ ```
289
+
290
+ Args:
291
+ workflow_fn: A PyWorkflow workflow function
292
+ mock_ctx: Optional MockDurableContext (creates one if not provided)
293
+
294
+ Returns:
295
+ A test handler function
296
+ """
297
+ from .context import AWSWorkflowContext
298
+
299
+ def test_handler(event: dict[str, Any]) -> Any:
300
+ ctx = mock_ctx or MockDurableContext()
301
+ aws_ctx = AWSWorkflowContext(ctx)
302
+
303
+ try:
304
+ if asyncio.iscoroutinefunction(workflow_fn):
305
+ return asyncio.run(workflow_fn(aws_ctx, **event))
306
+ return workflow_fn(aws_ctx, **event)
307
+ finally:
308
+ aws_ctx.cleanup()
309
+
310
+ return test_handler
@@ -0,0 +1,41 @@
1
+ """
2
+ Celery integration for distributed workflow execution.
3
+
4
+ This module provides Celery-based distributed execution for PyWorkflow,
5
+ enabling horizontal scaling across multiple workers.
6
+
7
+ Usage:
8
+ # Start Celery worker
9
+ celery -A pyworkflow.celery.app worker --loglevel=info
10
+
11
+ # Start Celery beat (for scheduled tasks)
12
+ celery -A pyworkflow.celery.app beat --loglevel=info
13
+
14
+ # Use in code
15
+ from pyworkflow.celery import celery_app, start_workflow_task
16
+
17
+ # Start workflow in distributed mode
18
+ result = start_workflow_task.delay("my_workflow", args_json, kwargs_json)
19
+ """
20
+
21
+ from pyworkflow.celery.app import celery_app, create_celery_app, get_celery_app
22
+ from pyworkflow.celery.scheduler import PyWorkflowScheduler
23
+ from pyworkflow.celery.tasks import (
24
+ execute_scheduled_workflow_task,
25
+ execute_step_task,
26
+ resume_workflow_task,
27
+ schedule_workflow_resumption,
28
+ start_workflow_task,
29
+ )
30
+
31
+ __all__ = [
32
+ "celery_app",
33
+ "create_celery_app",
34
+ "get_celery_app",
35
+ "execute_step_task",
36
+ "start_workflow_task",
37
+ "resume_workflow_task",
38
+ "schedule_workflow_resumption",
39
+ "execute_scheduled_workflow_task",
40
+ "PyWorkflowScheduler",
41
+ ]