aws-durable-execution-sdk-python-testing 1.0.0__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.
- aws_durable_execution_sdk_python_testing/__about__.py +4 -0
- aws_durable_execution_sdk_python_testing/__init__.py +20 -0
- aws_durable_execution_sdk_python_testing/checkpoint/__init__.py +1 -0
- aws_durable_execution_sdk_python_testing/checkpoint/processor.py +101 -0
- aws_durable_execution_sdk_python_testing/checkpoint/processors/__init__.py +1 -0
- aws_durable_execution_sdk_python_testing/checkpoint/processors/base.py +199 -0
- aws_durable_execution_sdk_python_testing/checkpoint/processors/callback.py +89 -0
- aws_durable_execution_sdk_python_testing/checkpoint/processors/context.py +59 -0
- aws_durable_execution_sdk_python_testing/checkpoint/processors/execution.py +52 -0
- aws_durable_execution_sdk_python_testing/checkpoint/processors/step.py +124 -0
- aws_durable_execution_sdk_python_testing/checkpoint/processors/wait.py +95 -0
- aws_durable_execution_sdk_python_testing/checkpoint/transformer.py +104 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/__init__.py +1 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/checkpoint.py +242 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/__init__.py +1 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/callback.py +45 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/context.py +73 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/execution.py +47 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/invoke.py +56 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/step.py +106 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/wait.py +54 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/transitions.py +66 -0
- aws_durable_execution_sdk_python_testing/cli.py +501 -0
- aws_durable_execution_sdk_python_testing/client.py +50 -0
- aws_durable_execution_sdk_python_testing/exceptions.py +288 -0
- aws_durable_execution_sdk_python_testing/execution.py +440 -0
- aws_durable_execution_sdk_python_testing/executor.py +1214 -0
- aws_durable_execution_sdk_python_testing/invoker.py +327 -0
- aws_durable_execution_sdk_python_testing/model.py +3267 -0
- aws_durable_execution_sdk_python_testing/observer.py +144 -0
- aws_durable_execution_sdk_python_testing/py.typed +1 -0
- aws_durable_execution_sdk_python_testing/runner.py +1185 -0
- aws_durable_execution_sdk_python_testing/scheduler.py +246 -0
- aws_durable_execution_sdk_python_testing/stores/__init__.py +1 -0
- aws_durable_execution_sdk_python_testing/stores/base.py +147 -0
- aws_durable_execution_sdk_python_testing/stores/filesystem.py +104 -0
- aws_durable_execution_sdk_python_testing/stores/memory.py +38 -0
- aws_durable_execution_sdk_python_testing/stores/sqlite.py +283 -0
- aws_durable_execution_sdk_python_testing/token.py +49 -0
- aws_durable_execution_sdk_python_testing/web/__init__.py +1 -0
- aws_durable_execution_sdk_python_testing/web/errors.py +8 -0
- aws_durable_execution_sdk_python_testing/web/handlers.py +807 -0
- aws_durable_execution_sdk_python_testing/web/models.py +266 -0
- aws_durable_execution_sdk_python_testing/web/routes.py +684 -0
- aws_durable_execution_sdk_python_testing/web/serialization.py +245 -0
- aws_durable_execution_sdk_python_testing/web/server.py +243 -0
- aws_durable_execution_sdk_python_testing-1.0.0.dist-info/METADATA +221 -0
- aws_durable_execution_sdk_python_testing-1.0.0.dist-info/RECORD +52 -0
- aws_durable_execution_sdk_python_testing-1.0.0.dist-info/WHEEL +4 -0
- aws_durable_execution_sdk_python_testing-1.0.0.dist-info/entry_points.txt +2 -0
- aws_durable_execution_sdk_python_testing-1.0.0.dist-info/licenses/LICENSE +175 -0
- aws_durable_execution_sdk_python_testing-1.0.0.dist-info/licenses/NOTICE +1 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""DurableExecutionsPythonTestingLibrary module."""
|
|
2
|
+
|
|
3
|
+
from aws_durable_execution_sdk_python_testing.runner import (
|
|
4
|
+
DurableChildContextTestRunner,
|
|
5
|
+
DurableFunctionCloudTestRunner,
|
|
6
|
+
DurableFunctionTestResult,
|
|
7
|
+
DurableFunctionTestRunner,
|
|
8
|
+
WebRunner,
|
|
9
|
+
WebRunnerConfig,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"DurableChildContextTestRunner",
|
|
15
|
+
"DurableFunctionCloudTestRunner",
|
|
16
|
+
"DurableFunctionTestResult",
|
|
17
|
+
"DurableFunctionTestRunner",
|
|
18
|
+
"WebRunner",
|
|
19
|
+
"WebRunnerConfig",
|
|
20
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Checkpoint processing module for handling OperationUpdate transformations."""
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Main checkpoint processor that orchestrates operation transformations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from aws_durable_execution_sdk_python.lambda_service import (
|
|
8
|
+
CheckpointOutput,
|
|
9
|
+
CheckpointUpdatedExecutionState,
|
|
10
|
+
OperationUpdate,
|
|
11
|
+
StateOutput,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.transformer import (
|
|
15
|
+
OperationTransformer,
|
|
16
|
+
)
|
|
17
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.validators.checkpoint import (
|
|
18
|
+
CheckpointValidator,
|
|
19
|
+
)
|
|
20
|
+
from aws_durable_execution_sdk_python_testing.exceptions import (
|
|
21
|
+
InvalidParameterValueException,
|
|
22
|
+
)
|
|
23
|
+
from aws_durable_execution_sdk_python_testing.observer import ExecutionNotifier
|
|
24
|
+
from aws_durable_execution_sdk_python_testing.token import CheckpointToken
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from aws_durable_execution_sdk_python_testing.execution import Execution
|
|
29
|
+
from aws_durable_execution_sdk_python_testing.scheduler import Scheduler
|
|
30
|
+
from aws_durable_execution_sdk_python_testing.stores.base import ExecutionStore
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CheckpointProcessor:
|
|
34
|
+
"""Handle OperationUpdate transformations and execution state updates."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, store: ExecutionStore, scheduler: Scheduler):
|
|
37
|
+
self._store = store
|
|
38
|
+
self._scheduler = scheduler
|
|
39
|
+
self._notifier = ExecutionNotifier()
|
|
40
|
+
self._transformer = OperationTransformer()
|
|
41
|
+
|
|
42
|
+
def add_execution_observer(self, observer) -> None:
|
|
43
|
+
"""Add observer for execution events."""
|
|
44
|
+
self._notifier.add_observer(observer)
|
|
45
|
+
|
|
46
|
+
def process_checkpoint(
|
|
47
|
+
self,
|
|
48
|
+
checkpoint_token: str,
|
|
49
|
+
updates: list[OperationUpdate],
|
|
50
|
+
client_token: str | None, # noqa: ARG002
|
|
51
|
+
) -> CheckpointOutput:
|
|
52
|
+
"""Process checkpoint updates and return result with updated execution state."""
|
|
53
|
+
# 1. Get current execution state
|
|
54
|
+
token: CheckpointToken = CheckpointToken.from_str(checkpoint_token)
|
|
55
|
+
execution: Execution = self._store.load(token.execution_arn)
|
|
56
|
+
|
|
57
|
+
# 2. Validate checkpoint token
|
|
58
|
+
if execution.is_complete or token.token_sequence != execution.token_sequence:
|
|
59
|
+
msg: str = "Invalid checkpoint token"
|
|
60
|
+
|
|
61
|
+
raise InvalidParameterValueException(msg)
|
|
62
|
+
|
|
63
|
+
# 3. Validate all updates, state transitions are valid, sizes etc.
|
|
64
|
+
CheckpointValidator.validate_input(updates, execution)
|
|
65
|
+
|
|
66
|
+
# 4. Transform OperationUpdate -> Operation and schedule future replays
|
|
67
|
+
updated_operations, all_updates = self._transformer.process_updates(
|
|
68
|
+
updates=updates,
|
|
69
|
+
current_operations=execution.operations,
|
|
70
|
+
notifier=self._notifier,
|
|
71
|
+
execution_arn=token.execution_arn,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# 5. Generate a new checkpoint token and save updated operations
|
|
75
|
+
new_checkpoint_token = execution.get_new_checkpoint_token()
|
|
76
|
+
execution.operations = updated_operations
|
|
77
|
+
execution.updates.extend(all_updates)
|
|
78
|
+
self._store.update(execution)
|
|
79
|
+
|
|
80
|
+
# 6. Return checkpoint result
|
|
81
|
+
return CheckpointOutput(
|
|
82
|
+
checkpoint_token=new_checkpoint_token,
|
|
83
|
+
new_execution_state=CheckpointUpdatedExecutionState(
|
|
84
|
+
operations=execution.get_navigable_operations(), next_marker=None
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def get_execution_state(
|
|
89
|
+
self,
|
|
90
|
+
checkpoint_token: str,
|
|
91
|
+
next_marker: str, # noqa: ARG002
|
|
92
|
+
max_items: int = 1000, # noqa: ARG002
|
|
93
|
+
) -> StateOutput:
|
|
94
|
+
"""Get current execution state."""
|
|
95
|
+
token: CheckpointToken = CheckpointToken.from_str(checkpoint_token)
|
|
96
|
+
execution: Execution = self._store.load(token.execution_arn)
|
|
97
|
+
|
|
98
|
+
# TODO: paging when size or max
|
|
99
|
+
return StateOutput(
|
|
100
|
+
operations=execution.get_navigable_operations(), next_marker=None
|
|
101
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Checkpoint processors module."""
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Base processor class for operation transformations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from aws_durable_execution_sdk_python.lambda_service import (
|
|
10
|
+
CallbackDetails,
|
|
11
|
+
ChainedInvokeDetails,
|
|
12
|
+
ContextDetails,
|
|
13
|
+
ExecutionDetails,
|
|
14
|
+
Operation,
|
|
15
|
+
OperationAction,
|
|
16
|
+
OperationStatus,
|
|
17
|
+
OperationType,
|
|
18
|
+
OperationUpdate,
|
|
19
|
+
StepDetails,
|
|
20
|
+
WaitDetails,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from aws_durable_execution_sdk_python_testing.observer import ExecutionNotifier
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OperationProcessor:
|
|
29
|
+
"""Base class for processing OperationUpdate to Operation transformations."""
|
|
30
|
+
|
|
31
|
+
def process(
|
|
32
|
+
self,
|
|
33
|
+
update: OperationUpdate,
|
|
34
|
+
current_op: Operation | None,
|
|
35
|
+
notifier: ExecutionNotifier,
|
|
36
|
+
execution_arn: str,
|
|
37
|
+
) -> Operation | None:
|
|
38
|
+
"""Process an operation update and return the transformed operation."""
|
|
39
|
+
raise NotImplementedError
|
|
40
|
+
|
|
41
|
+
def _get_start_time(
|
|
42
|
+
self, current_operation: Operation | None
|
|
43
|
+
) -> datetime.datetime | None:
|
|
44
|
+
start_time: datetime.datetime | None = (
|
|
45
|
+
current_operation.start_timestamp
|
|
46
|
+
if current_operation
|
|
47
|
+
else datetime.datetime.now(tz=datetime.UTC)
|
|
48
|
+
)
|
|
49
|
+
return start_time
|
|
50
|
+
|
|
51
|
+
def _get_end_time(
|
|
52
|
+
self, current_operation: Operation | None, status: OperationStatus
|
|
53
|
+
) -> datetime.datetime | None:
|
|
54
|
+
"""Get end timestamp for operation based on current state and status."""
|
|
55
|
+
if current_operation and current_operation.end_timestamp:
|
|
56
|
+
return current_operation.end_timestamp
|
|
57
|
+
if status in {
|
|
58
|
+
OperationStatus.SUCCEEDED,
|
|
59
|
+
OperationStatus.FAILED,
|
|
60
|
+
OperationStatus.CANCELLED,
|
|
61
|
+
OperationStatus.TIMED_OUT,
|
|
62
|
+
OperationStatus.STOPPED,
|
|
63
|
+
}:
|
|
64
|
+
return datetime.datetime.now(tz=datetime.UTC)
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
def _create_execution_details(
|
|
68
|
+
self, update: OperationUpdate
|
|
69
|
+
) -> ExecutionDetails | None:
|
|
70
|
+
"""Create ExecutionDetails from OperationUpdate."""
|
|
71
|
+
return (
|
|
72
|
+
ExecutionDetails(input_payload=update.payload)
|
|
73
|
+
if update.operation_type == OperationType.EXECUTION
|
|
74
|
+
else None
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def _create_context_details(self, update: OperationUpdate) -> ContextDetails | None:
|
|
78
|
+
"""Create ContextDetails from OperationUpdate."""
|
|
79
|
+
return (
|
|
80
|
+
ContextDetails(
|
|
81
|
+
result=update.payload,
|
|
82
|
+
error=update.error,
|
|
83
|
+
replay_children=update.context_options.replay_children
|
|
84
|
+
if update.context_options
|
|
85
|
+
else False,
|
|
86
|
+
)
|
|
87
|
+
if update.operation_type == OperationType.CONTEXT
|
|
88
|
+
else None
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def _create_step_details(
|
|
92
|
+
self,
|
|
93
|
+
update: OperationUpdate,
|
|
94
|
+
current_operation: Operation | None = None,
|
|
95
|
+
) -> StepDetails | None:
|
|
96
|
+
"""Create StepDetails from OperationUpdate.
|
|
97
|
+
|
|
98
|
+
Automatically increments attempt count for RETRY, SUCCEED, and FAIL actions.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
attempt: int = 0
|
|
102
|
+
next_attempt_timestamp: datetime.datetime | None = None
|
|
103
|
+
|
|
104
|
+
if update.operation_type is OperationType.STEP:
|
|
105
|
+
if current_operation and current_operation.step_details:
|
|
106
|
+
attempt = current_operation.step_details.attempt
|
|
107
|
+
next_attempt_timestamp = (
|
|
108
|
+
current_operation.step_details.next_attempt_timestamp
|
|
109
|
+
)
|
|
110
|
+
# Increment attempt for RETRY, SUCCEED, and FAIL actions
|
|
111
|
+
if update.action in {
|
|
112
|
+
OperationAction.RETRY,
|
|
113
|
+
OperationAction.SUCCEED,
|
|
114
|
+
OperationAction.FAIL,
|
|
115
|
+
}:
|
|
116
|
+
attempt += 1
|
|
117
|
+
return StepDetails(
|
|
118
|
+
attempt=attempt,
|
|
119
|
+
next_attempt_timestamp=next_attempt_timestamp,
|
|
120
|
+
result=update.payload,
|
|
121
|
+
error=update.error,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
def _create_callback_details(
|
|
127
|
+
self, update: OperationUpdate
|
|
128
|
+
) -> CallbackDetails | None:
|
|
129
|
+
"""Create CallbackDetails from OperationUpdate."""
|
|
130
|
+
return (
|
|
131
|
+
CallbackDetails(
|
|
132
|
+
callback_id="placeholder", result=update.payload, error=update.error
|
|
133
|
+
)
|
|
134
|
+
if update.operation_type == OperationType.CALLBACK
|
|
135
|
+
else None
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def _create_invoke_details(
|
|
139
|
+
self, update: OperationUpdate
|
|
140
|
+
) -> ChainedInvokeDetails | None:
|
|
141
|
+
"""Create ChainedInvokeDetails from OperationUpdate."""
|
|
142
|
+
if (
|
|
143
|
+
update.operation_type == OperationType.CHAINED_INVOKE
|
|
144
|
+
and update.chained_invoke_options
|
|
145
|
+
):
|
|
146
|
+
return ChainedInvokeDetails(result=update.payload, error=update.error)
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
def _translate_update_to_operation(
|
|
150
|
+
self,
|
|
151
|
+
update: OperationUpdate,
|
|
152
|
+
current_operation: Operation | None,
|
|
153
|
+
status: OperationStatus,
|
|
154
|
+
) -> Operation:
|
|
155
|
+
"""Transform OperationUpdate to Operation, always creating new Operation."""
|
|
156
|
+
start_time: datetime.datetime | None = self._get_start_time(current_operation)
|
|
157
|
+
end_time: datetime.datetime | None = self._get_end_time(
|
|
158
|
+
current_operation, status
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
execution_details = self._create_execution_details(update)
|
|
162
|
+
context_details = self._create_context_details(update)
|
|
163
|
+
step_details = self._create_step_details(update, current_operation)
|
|
164
|
+
callback_details = self._create_callback_details(update)
|
|
165
|
+
invoke_details = self._create_invoke_details(update)
|
|
166
|
+
wait_details = self._create_wait_details(update, current_operation)
|
|
167
|
+
|
|
168
|
+
return Operation(
|
|
169
|
+
operation_id=update.operation_id,
|
|
170
|
+
parent_id=update.parent_id,
|
|
171
|
+
name=update.name,
|
|
172
|
+
start_timestamp=start_time,
|
|
173
|
+
end_timestamp=end_time,
|
|
174
|
+
operation_type=update.operation_type,
|
|
175
|
+
status=status,
|
|
176
|
+
sub_type=update.sub_type,
|
|
177
|
+
execution_details=execution_details,
|
|
178
|
+
context_details=context_details,
|
|
179
|
+
step_details=step_details,
|
|
180
|
+
callback_details=callback_details,
|
|
181
|
+
chained_invoke_details=invoke_details,
|
|
182
|
+
wait_details=wait_details,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def _create_wait_details(
|
|
186
|
+
self, update: OperationUpdate, current_operation: Operation | None
|
|
187
|
+
) -> WaitDetails | None:
|
|
188
|
+
"""Create WaitDetails from OperationUpdate."""
|
|
189
|
+
if update.operation_type == OperationType.WAIT and update.wait_options:
|
|
190
|
+
if current_operation and current_operation.wait_details:
|
|
191
|
+
scheduled_end_timestamp = (
|
|
192
|
+
current_operation.wait_details.scheduled_end_timestamp
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
scheduled_end_timestamp = datetime.datetime.now(
|
|
196
|
+
tz=datetime.UTC
|
|
197
|
+
) + timedelta(seconds=update.wait_options.wait_seconds)
|
|
198
|
+
return WaitDetails(scheduled_end_timestamp=scheduled_end_timestamp)
|
|
199
|
+
return None
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Callback operation processor for handling CALLBACK operation updates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from aws_durable_execution_sdk_python.lambda_service import (
|
|
9
|
+
Operation,
|
|
10
|
+
OperationAction,
|
|
11
|
+
OperationStatus,
|
|
12
|
+
OperationUpdate,
|
|
13
|
+
CallbackDetails,
|
|
14
|
+
OperationType,
|
|
15
|
+
CallbackOptions,
|
|
16
|
+
)
|
|
17
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.processors.base import (
|
|
18
|
+
OperationProcessor,
|
|
19
|
+
)
|
|
20
|
+
from aws_durable_execution_sdk_python_testing.exceptions import (
|
|
21
|
+
InvalidParameterValueException,
|
|
22
|
+
)
|
|
23
|
+
from aws_durable_execution_sdk_python_testing.token import CallbackToken
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from aws_durable_execution_sdk_python_testing.observer import ExecutionNotifier
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CallbackProcessor(OperationProcessor):
|
|
30
|
+
"""Processes CALLBACK operation updates with activity scheduling."""
|
|
31
|
+
|
|
32
|
+
def process(
|
|
33
|
+
self,
|
|
34
|
+
update: OperationUpdate,
|
|
35
|
+
current_op: Operation | None,
|
|
36
|
+
notifier: ExecutionNotifier, # noqa: ARG002
|
|
37
|
+
execution_arn: str, # noqa: ARG002
|
|
38
|
+
) -> Operation:
|
|
39
|
+
"""Process CALLBACK operation update with scheduler integration for activities."""
|
|
40
|
+
match update.action:
|
|
41
|
+
case OperationAction.START:
|
|
42
|
+
callback_token: CallbackToken = CallbackToken(
|
|
43
|
+
execution_arn=execution_arn,
|
|
44
|
+
operation_id=update.operation_id,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
callback_id: str = callback_token.to_str()
|
|
48
|
+
|
|
49
|
+
callback_details: CallbackDetails | None = (
|
|
50
|
+
CallbackDetails(
|
|
51
|
+
callback_id=callback_id,
|
|
52
|
+
result=update.payload,
|
|
53
|
+
error=update.error,
|
|
54
|
+
)
|
|
55
|
+
if update.operation_type == OperationType.CALLBACK
|
|
56
|
+
else None
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
status: OperationStatus = OperationStatus.STARTED
|
|
60
|
+
|
|
61
|
+
start_time: datetime.datetime | None = self._get_start_time(current_op)
|
|
62
|
+
|
|
63
|
+
end_time: datetime.datetime | None = self._get_end_time(
|
|
64
|
+
current_op, status
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
operation: Operation = Operation(
|
|
68
|
+
operation_id=update.operation_id,
|
|
69
|
+
parent_id=update.parent_id,
|
|
70
|
+
name=update.name,
|
|
71
|
+
start_timestamp=start_time,
|
|
72
|
+
end_timestamp=end_time,
|
|
73
|
+
operation_type=update.operation_type,
|
|
74
|
+
status=status,
|
|
75
|
+
sub_type=update.sub_type,
|
|
76
|
+
callback_details=callback_details,
|
|
77
|
+
)
|
|
78
|
+
callback_options: CallbackOptions | None = update.callback_options
|
|
79
|
+
|
|
80
|
+
notifier.notify_callback_created(
|
|
81
|
+
execution_arn=execution_arn,
|
|
82
|
+
operation_id=update.operation_id,
|
|
83
|
+
callback_options=callback_options,
|
|
84
|
+
callback_token=callback_token,
|
|
85
|
+
)
|
|
86
|
+
return operation
|
|
87
|
+
case _:
|
|
88
|
+
msg: str = "Invalid action for CALLBACK operation."
|
|
89
|
+
raise InvalidParameterValueException(msg)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Context operation processor for handling CONTEXT operation updates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from aws_durable_execution_sdk_python.lambda_service import (
|
|
8
|
+
Operation,
|
|
9
|
+
OperationAction,
|
|
10
|
+
OperationStatus,
|
|
11
|
+
OperationUpdate,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.processors.base import (
|
|
15
|
+
OperationProcessor,
|
|
16
|
+
)
|
|
17
|
+
from aws_durable_execution_sdk_python_testing.exceptions import (
|
|
18
|
+
InvalidParameterValueException,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from aws_durable_execution_sdk_python_testing.observer import ExecutionNotifier
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ContextProcessor(OperationProcessor):
|
|
27
|
+
"""Processes CONTEXT operation updates for execution context management."""
|
|
28
|
+
|
|
29
|
+
def process(
|
|
30
|
+
self,
|
|
31
|
+
update: OperationUpdate,
|
|
32
|
+
current_op: Operation | None,
|
|
33
|
+
notifier: ExecutionNotifier, # noqa: ARG002
|
|
34
|
+
execution_arn: str, # noqa: ARG002
|
|
35
|
+
) -> Operation:
|
|
36
|
+
"""Process CONTEXT operation update for context state transitions."""
|
|
37
|
+
match update.action:
|
|
38
|
+
case OperationAction.START:
|
|
39
|
+
# TODO: check for "Cannot start a CONTEXT operation that already exists."
|
|
40
|
+
return self._translate_update_to_operation(
|
|
41
|
+
update=update,
|
|
42
|
+
current_operation=current_op,
|
|
43
|
+
status=OperationStatus.STARTED,
|
|
44
|
+
)
|
|
45
|
+
case OperationAction.SUCCEED:
|
|
46
|
+
return self._translate_update_to_operation(
|
|
47
|
+
update=update,
|
|
48
|
+
current_operation=current_op,
|
|
49
|
+
status=OperationStatus.SUCCEEDED,
|
|
50
|
+
)
|
|
51
|
+
case OperationAction.FAIL:
|
|
52
|
+
return self._translate_update_to_operation(
|
|
53
|
+
update=update,
|
|
54
|
+
current_operation=current_op,
|
|
55
|
+
status=OperationStatus.FAILED,
|
|
56
|
+
)
|
|
57
|
+
case _:
|
|
58
|
+
msg: str = "Invalid action for CONTEXT operation."
|
|
59
|
+
raise InvalidParameterValueException(msg)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Execution operation processor for handling EXECUTION operation updates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from aws_durable_execution_sdk_python.lambda_service import (
|
|
8
|
+
ErrorObject,
|
|
9
|
+
Operation,
|
|
10
|
+
OperationAction,
|
|
11
|
+
OperationUpdate,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.processors.base import (
|
|
15
|
+
OperationProcessor,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from aws_durable_execution_sdk_python_testing.observer import ExecutionNotifier
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ExecutionProcessor(OperationProcessor):
|
|
24
|
+
"""Processes EXECUTION operation updates for workflow completion."""
|
|
25
|
+
|
|
26
|
+
def process(
|
|
27
|
+
self,
|
|
28
|
+
update: OperationUpdate,
|
|
29
|
+
current_op: Operation | None, # noqa: ARG002
|
|
30
|
+
notifier: ExecutionNotifier,
|
|
31
|
+
execution_arn: str,
|
|
32
|
+
) -> Operation | None:
|
|
33
|
+
"""Process EXECUTION operation update for workflow completion/failure."""
|
|
34
|
+
match update.action:
|
|
35
|
+
case OperationAction.SUCCEED:
|
|
36
|
+
notifier.notify_completed(
|
|
37
|
+
execution_arn=execution_arn, result=update.payload
|
|
38
|
+
)
|
|
39
|
+
case _:
|
|
40
|
+
# intentional. actual service will fail any EXECUTION update that is not SUCCEED.
|
|
41
|
+
error = (
|
|
42
|
+
update.error
|
|
43
|
+
if update.error
|
|
44
|
+
else ErrorObject.from_message(
|
|
45
|
+
"There is no error details but EXECUTION checkpoint action is not SUCCEED."
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
# All EXECUTION failures go through normal fail path
|
|
49
|
+
# Timeout/Stop status is set by executor based on the operation that caused it
|
|
50
|
+
notifier.notify_failed(execution_arn=execution_arn, error=error)
|
|
51
|
+
# TODO: Svc doesn't actually create checkpoint for EXECUTION. might have to for localrunner though.
|
|
52
|
+
return None
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Step operation processor for handling STEP operation updates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime, timedelta
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from aws_durable_execution_sdk_python.lambda_service import (
|
|
9
|
+
Operation,
|
|
10
|
+
OperationAction,
|
|
11
|
+
OperationStatus,
|
|
12
|
+
OperationUpdate,
|
|
13
|
+
StepDetails,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.processors.base import (
|
|
17
|
+
OperationProcessor,
|
|
18
|
+
)
|
|
19
|
+
from aws_durable_execution_sdk_python_testing.exceptions import (
|
|
20
|
+
InvalidParameterValueException,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from aws_durable_execution_sdk_python_testing.observer import ExecutionNotifier
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StepProcessor(OperationProcessor):
|
|
29
|
+
"""Processes STEP operation updates with retry scheduling."""
|
|
30
|
+
|
|
31
|
+
def process(
|
|
32
|
+
self,
|
|
33
|
+
update: OperationUpdate,
|
|
34
|
+
current_op: Operation | None,
|
|
35
|
+
notifier: ExecutionNotifier,
|
|
36
|
+
execution_arn: str,
|
|
37
|
+
) -> Operation:
|
|
38
|
+
"""Process STEP operation update with scheduler integration for retries."""
|
|
39
|
+
match update.action:
|
|
40
|
+
case OperationAction.START:
|
|
41
|
+
return self._translate_update_to_operation(
|
|
42
|
+
update=update,
|
|
43
|
+
current_operation=current_op,
|
|
44
|
+
status=OperationStatus.STARTED,
|
|
45
|
+
)
|
|
46
|
+
case OperationAction.RETRY:
|
|
47
|
+
# set Status=PENDING, next attempt time, attempt count + 1
|
|
48
|
+
delay = (
|
|
49
|
+
update.step_options.next_attempt_delay_seconds
|
|
50
|
+
if update.step_options
|
|
51
|
+
else 0
|
|
52
|
+
)
|
|
53
|
+
next_attempt_time = datetime.now(UTC) + timedelta(seconds=delay)
|
|
54
|
+
|
|
55
|
+
# Build new step_details with incremented attempt
|
|
56
|
+
current_attempt = (
|
|
57
|
+
current_op.step_details.attempt
|
|
58
|
+
if current_op and current_op.step_details
|
|
59
|
+
else 0
|
|
60
|
+
)
|
|
61
|
+
new_step_details = StepDetails(
|
|
62
|
+
attempt=current_attempt + 1,
|
|
63
|
+
next_attempt_timestamp=next_attempt_time,
|
|
64
|
+
result=(
|
|
65
|
+
current_op.step_details.result
|
|
66
|
+
if current_op and current_op.step_details
|
|
67
|
+
else None
|
|
68
|
+
),
|
|
69
|
+
error=(
|
|
70
|
+
current_op.step_details.error
|
|
71
|
+
if current_op and current_op.step_details
|
|
72
|
+
else None
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Create new operation with updated step_details
|
|
77
|
+
retry_operation = Operation(
|
|
78
|
+
operation_id=update.operation_id,
|
|
79
|
+
operation_type=update.operation_type,
|
|
80
|
+
status=OperationStatus.PENDING,
|
|
81
|
+
parent_id=update.parent_id,
|
|
82
|
+
name=update.name,
|
|
83
|
+
start_timestamp=(
|
|
84
|
+
current_op.start_timestamp if current_op else datetime.now(UTC)
|
|
85
|
+
),
|
|
86
|
+
end_timestamp=None,
|
|
87
|
+
sub_type=update.sub_type,
|
|
88
|
+
execution_details=current_op.execution_details
|
|
89
|
+
if current_op
|
|
90
|
+
else None,
|
|
91
|
+
context_details=current_op.context_details if current_op else None,
|
|
92
|
+
step_details=new_step_details,
|
|
93
|
+
wait_details=current_op.wait_details if current_op else None,
|
|
94
|
+
callback_details=current_op.callback_details
|
|
95
|
+
if current_op
|
|
96
|
+
else None,
|
|
97
|
+
chained_invoke_details=current_op.chained_invoke_details
|
|
98
|
+
if current_op
|
|
99
|
+
else None,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Schedule step retry timer to fire after delay
|
|
103
|
+
notifier.notify_step_retry_scheduled(
|
|
104
|
+
execution_arn=execution_arn,
|
|
105
|
+
operation_id=update.operation_id,
|
|
106
|
+
delay=delay,
|
|
107
|
+
)
|
|
108
|
+
return retry_operation
|
|
109
|
+
case OperationAction.SUCCEED:
|
|
110
|
+
return self._translate_update_to_operation(
|
|
111
|
+
update=update,
|
|
112
|
+
current_operation=current_op,
|
|
113
|
+
status=OperationStatus.SUCCEEDED,
|
|
114
|
+
)
|
|
115
|
+
case OperationAction.FAIL:
|
|
116
|
+
return self._translate_update_to_operation(
|
|
117
|
+
update=update,
|
|
118
|
+
current_operation=current_op,
|
|
119
|
+
status=OperationStatus.FAILED,
|
|
120
|
+
)
|
|
121
|
+
case _:
|
|
122
|
+
msg: str = "Invalid action for STEP operation."
|
|
123
|
+
|
|
124
|
+
raise InvalidParameterValueException(msg)
|