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.
- dashboard/backend/app/__init__.py +1 -0
- dashboard/backend/app/config.py +32 -0
- dashboard/backend/app/controllers/__init__.py +6 -0
- dashboard/backend/app/controllers/run_controller.py +86 -0
- dashboard/backend/app/controllers/workflow_controller.py +33 -0
- dashboard/backend/app/dependencies/__init__.py +5 -0
- dashboard/backend/app/dependencies/storage.py +50 -0
- dashboard/backend/app/repositories/__init__.py +6 -0
- dashboard/backend/app/repositories/run_repository.py +80 -0
- dashboard/backend/app/repositories/workflow_repository.py +27 -0
- dashboard/backend/app/rest/__init__.py +8 -0
- dashboard/backend/app/rest/v1/__init__.py +12 -0
- dashboard/backend/app/rest/v1/health.py +33 -0
- dashboard/backend/app/rest/v1/runs.py +133 -0
- dashboard/backend/app/rest/v1/workflows.py +41 -0
- dashboard/backend/app/schemas/__init__.py +23 -0
- dashboard/backend/app/schemas/common.py +16 -0
- dashboard/backend/app/schemas/event.py +24 -0
- dashboard/backend/app/schemas/hook.py +25 -0
- dashboard/backend/app/schemas/run.py +54 -0
- dashboard/backend/app/schemas/step.py +28 -0
- dashboard/backend/app/schemas/workflow.py +31 -0
- dashboard/backend/app/server.py +87 -0
- dashboard/backend/app/services/__init__.py +6 -0
- dashboard/backend/app/services/run_service.py +240 -0
- dashboard/backend/app/services/workflow_service.py +155 -0
- dashboard/backend/main.py +18 -0
- docs/concepts/cancellation.mdx +362 -0
- docs/concepts/continue-as-new.mdx +434 -0
- docs/concepts/events.mdx +266 -0
- docs/concepts/fault-tolerance.mdx +370 -0
- docs/concepts/hooks.mdx +552 -0
- docs/concepts/limitations.mdx +167 -0
- docs/concepts/schedules.mdx +775 -0
- docs/concepts/sleep.mdx +312 -0
- docs/concepts/steps.mdx +301 -0
- docs/concepts/workflows.mdx +255 -0
- docs/guides/cli.mdx +942 -0
- docs/guides/configuration.mdx +560 -0
- docs/introduction.mdx +155 -0
- docs/quickstart.mdx +279 -0
- examples/__init__.py +1 -0
- examples/celery/__init__.py +1 -0
- examples/celery/durable/docker-compose.yml +55 -0
- examples/celery/durable/pyworkflow.config.yaml +12 -0
- examples/celery/durable/workflows/__init__.py +122 -0
- examples/celery/durable/workflows/basic.py +87 -0
- examples/celery/durable/workflows/batch_processing.py +102 -0
- examples/celery/durable/workflows/cancellation.py +273 -0
- examples/celery/durable/workflows/child_workflow_patterns.py +240 -0
- examples/celery/durable/workflows/child_workflows.py +202 -0
- examples/celery/durable/workflows/continue_as_new.py +260 -0
- examples/celery/durable/workflows/fault_tolerance.py +210 -0
- examples/celery/durable/workflows/hooks.py +211 -0
- examples/celery/durable/workflows/idempotency.py +112 -0
- examples/celery/durable/workflows/long_running.py +99 -0
- examples/celery/durable/workflows/retries.py +101 -0
- examples/celery/durable/workflows/schedules.py +209 -0
- examples/celery/transient/01_basic_workflow.py +91 -0
- examples/celery/transient/02_fault_tolerance.py +257 -0
- examples/celery/transient/__init__.py +20 -0
- examples/celery/transient/pyworkflow.config.yaml +25 -0
- examples/local/__init__.py +1 -0
- examples/local/durable/01_basic_workflow.py +94 -0
- examples/local/durable/02_file_storage.py +132 -0
- examples/local/durable/03_retries.py +169 -0
- examples/local/durable/04_long_running.py +119 -0
- examples/local/durable/05_event_log.py +145 -0
- examples/local/durable/06_idempotency.py +148 -0
- examples/local/durable/07_hooks.py +334 -0
- examples/local/durable/08_cancellation.py +233 -0
- examples/local/durable/09_child_workflows.py +198 -0
- examples/local/durable/10_child_workflow_patterns.py +265 -0
- examples/local/durable/11_continue_as_new.py +249 -0
- examples/local/durable/12_schedules.py +198 -0
- examples/local/durable/__init__.py +1 -0
- examples/local/transient/01_quick_tasks.py +87 -0
- examples/local/transient/02_retries.py +130 -0
- examples/local/transient/03_sleep.py +141 -0
- examples/local/transient/__init__.py +1 -0
- pyworkflow/__init__.py +256 -0
- pyworkflow/aws/__init__.py +68 -0
- pyworkflow/aws/context.py +234 -0
- pyworkflow/aws/handler.py +184 -0
- pyworkflow/aws/testing.py +310 -0
- pyworkflow/celery/__init__.py +41 -0
- pyworkflow/celery/app.py +198 -0
- pyworkflow/celery/scheduler.py +315 -0
- pyworkflow/celery/tasks.py +1746 -0
- pyworkflow/cli/__init__.py +132 -0
- pyworkflow/cli/__main__.py +6 -0
- pyworkflow/cli/commands/__init__.py +1 -0
- pyworkflow/cli/commands/hooks.py +640 -0
- pyworkflow/cli/commands/quickstart.py +495 -0
- pyworkflow/cli/commands/runs.py +773 -0
- pyworkflow/cli/commands/scheduler.py +130 -0
- pyworkflow/cli/commands/schedules.py +794 -0
- pyworkflow/cli/commands/setup.py +703 -0
- pyworkflow/cli/commands/worker.py +413 -0
- pyworkflow/cli/commands/workflows.py +1257 -0
- pyworkflow/cli/output/__init__.py +1 -0
- pyworkflow/cli/output/formatters.py +321 -0
- pyworkflow/cli/output/styles.py +121 -0
- pyworkflow/cli/utils/__init__.py +1 -0
- pyworkflow/cli/utils/async_helpers.py +30 -0
- pyworkflow/cli/utils/config.py +130 -0
- pyworkflow/cli/utils/config_generator.py +344 -0
- pyworkflow/cli/utils/discovery.py +53 -0
- pyworkflow/cli/utils/docker_manager.py +651 -0
- pyworkflow/cli/utils/interactive.py +364 -0
- pyworkflow/cli/utils/storage.py +115 -0
- pyworkflow/config.py +329 -0
- pyworkflow/context/__init__.py +63 -0
- pyworkflow/context/aws.py +230 -0
- pyworkflow/context/base.py +416 -0
- pyworkflow/context/local.py +930 -0
- pyworkflow/context/mock.py +381 -0
- pyworkflow/core/__init__.py +0 -0
- pyworkflow/core/exceptions.py +353 -0
- pyworkflow/core/registry.py +313 -0
- pyworkflow/core/scheduled.py +328 -0
- pyworkflow/core/step.py +494 -0
- pyworkflow/core/workflow.py +294 -0
- pyworkflow/discovery.py +248 -0
- pyworkflow/engine/__init__.py +0 -0
- pyworkflow/engine/events.py +879 -0
- pyworkflow/engine/executor.py +682 -0
- pyworkflow/engine/replay.py +273 -0
- pyworkflow/observability/__init__.py +19 -0
- pyworkflow/observability/logging.py +234 -0
- pyworkflow/primitives/__init__.py +33 -0
- pyworkflow/primitives/child_handle.py +174 -0
- pyworkflow/primitives/child_workflow.py +372 -0
- pyworkflow/primitives/continue_as_new.py +101 -0
- pyworkflow/primitives/define_hook.py +150 -0
- pyworkflow/primitives/hooks.py +97 -0
- pyworkflow/primitives/resume_hook.py +210 -0
- pyworkflow/primitives/schedule.py +545 -0
- pyworkflow/primitives/shield.py +96 -0
- pyworkflow/primitives/sleep.py +100 -0
- pyworkflow/runtime/__init__.py +21 -0
- pyworkflow/runtime/base.py +179 -0
- pyworkflow/runtime/celery.py +310 -0
- pyworkflow/runtime/factory.py +101 -0
- pyworkflow/runtime/local.py +706 -0
- pyworkflow/scheduler/__init__.py +9 -0
- pyworkflow/scheduler/local.py +248 -0
- pyworkflow/serialization/__init__.py +0 -0
- pyworkflow/serialization/decoder.py +146 -0
- pyworkflow/serialization/encoder.py +162 -0
- pyworkflow/storage/__init__.py +54 -0
- pyworkflow/storage/base.py +612 -0
- pyworkflow/storage/config.py +185 -0
- pyworkflow/storage/dynamodb.py +1315 -0
- pyworkflow/storage/file.py +827 -0
- pyworkflow/storage/memory.py +549 -0
- pyworkflow/storage/postgres.py +1161 -0
- pyworkflow/storage/schemas.py +486 -0
- pyworkflow/storage/sqlite.py +1136 -0
- pyworkflow/utils/__init__.py +0 -0
- pyworkflow/utils/duration.py +177 -0
- pyworkflow/utils/schedule.py +391 -0
- pyworkflow_engine-0.1.7.dist-info/METADATA +687 -0
- pyworkflow_engine-0.1.7.dist-info/RECORD +196 -0
- pyworkflow_engine-0.1.7.dist-info/WHEEL +5 -0
- pyworkflow_engine-0.1.7.dist-info/entry_points.txt +2 -0
- pyworkflow_engine-0.1.7.dist-info/licenses/LICENSE +21 -0
- pyworkflow_engine-0.1.7.dist-info/top_level.txt +5 -0
- tests/examples/__init__.py +0 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_cancellation.py +330 -0
- tests/integration/test_child_workflows.py +439 -0
- tests/integration/test_continue_as_new.py +428 -0
- tests/integration/test_dynamodb_storage.py +1146 -0
- tests/integration/test_fault_tolerance.py +369 -0
- tests/integration/test_schedule_storage.py +484 -0
- tests/unit/__init__.py +0 -0
- tests/unit/backends/__init__.py +1 -0
- tests/unit/backends/test_dynamodb_storage.py +1554 -0
- tests/unit/backends/test_postgres_storage.py +1281 -0
- tests/unit/backends/test_sqlite_storage.py +1460 -0
- tests/unit/conftest.py +41 -0
- tests/unit/test_cancellation.py +364 -0
- tests/unit/test_child_workflows.py +680 -0
- tests/unit/test_continue_as_new.py +441 -0
- tests/unit/test_event_limits.py +316 -0
- tests/unit/test_executor.py +320 -0
- tests/unit/test_fault_tolerance.py +334 -0
- tests/unit/test_hooks.py +495 -0
- tests/unit/test_registry.py +261 -0
- tests/unit/test_replay.py +420 -0
- tests/unit/test_schedule_schemas.py +285 -0
- tests/unit/test_schedule_utils.py +286 -0
- tests/unit/test_scheduled_workflow.py +274 -0
- tests/unit/test_step.py +353 -0
- 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
|
+
]
|