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,95 @@
|
|
|
1
|
+
"""Wait operation processor for handling WAIT operation updates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from datetime import UTC, datetime, timedelta
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from aws_durable_execution_sdk_python.lambda_service import (
|
|
11
|
+
Operation,
|
|
12
|
+
OperationAction,
|
|
13
|
+
OperationStatus,
|
|
14
|
+
OperationUpdate,
|
|
15
|
+
WaitDetails,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.processors.base import (
|
|
19
|
+
OperationProcessor,
|
|
20
|
+
)
|
|
21
|
+
from aws_durable_execution_sdk_python_testing.exceptions import (
|
|
22
|
+
InvalidParameterValueException,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from aws_durable_execution_sdk_python_testing.observer import ExecutionNotifier
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WaitProcessor(OperationProcessor):
|
|
31
|
+
"""Processes WAIT operation updates with timer scheduling."""
|
|
32
|
+
|
|
33
|
+
def process(
|
|
34
|
+
self,
|
|
35
|
+
update: OperationUpdate,
|
|
36
|
+
current_op: Operation | None,
|
|
37
|
+
notifier: ExecutionNotifier,
|
|
38
|
+
execution_arn: str,
|
|
39
|
+
) -> Operation:
|
|
40
|
+
"""Process WAIT operation update with scheduler integration for timers."""
|
|
41
|
+
match update.action:
|
|
42
|
+
case OperationAction.START:
|
|
43
|
+
wait_seconds = (
|
|
44
|
+
update.wait_options.wait_seconds if update.wait_options else 0
|
|
45
|
+
)
|
|
46
|
+
time_scale = float(os.getenv("DURABLE_EXECUTION_TIME_SCALE", "1.0"))
|
|
47
|
+
logging.info("Using DURABLE_EXECUTION_TIME_SCALE: %f", time_scale)
|
|
48
|
+
scaled_wait_seconds = wait_seconds * time_scale
|
|
49
|
+
|
|
50
|
+
scheduled_end_timestamp = datetime.now(UTC) + timedelta(
|
|
51
|
+
seconds=scaled_wait_seconds
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Create WaitDetails with scheduled timestamp
|
|
55
|
+
wait_details = WaitDetails(
|
|
56
|
+
scheduled_end_timestamp=scheduled_end_timestamp
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Create new operation with wait details
|
|
60
|
+
wait_operation = Operation(
|
|
61
|
+
operation_id=update.operation_id,
|
|
62
|
+
operation_type=update.operation_type,
|
|
63
|
+
status=OperationStatus.STARTED,
|
|
64
|
+
parent_id=update.parent_id,
|
|
65
|
+
name=update.name,
|
|
66
|
+
start_timestamp=datetime.now(UTC),
|
|
67
|
+
end_timestamp=None,
|
|
68
|
+
sub_type=update.sub_type,
|
|
69
|
+
execution_details=None,
|
|
70
|
+
context_details=None,
|
|
71
|
+
step_details=None,
|
|
72
|
+
wait_details=wait_details,
|
|
73
|
+
callback_details=None,
|
|
74
|
+
chained_invoke_details=None,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Schedule wait timer to complete after delay
|
|
78
|
+
notifier.notify_wait_timer_scheduled(
|
|
79
|
+
execution_arn=execution_arn,
|
|
80
|
+
operation_id=update.operation_id,
|
|
81
|
+
delay=scaled_wait_seconds,
|
|
82
|
+
)
|
|
83
|
+
return wait_operation
|
|
84
|
+
case OperationAction.CANCEL:
|
|
85
|
+
# TODO: need to cancel the WAIT in the executor
|
|
86
|
+
# TODO: increase sequence id
|
|
87
|
+
return self._translate_update_to_operation(
|
|
88
|
+
update=update,
|
|
89
|
+
current_operation=current_op,
|
|
90
|
+
status=OperationStatus.CANCELLED,
|
|
91
|
+
)
|
|
92
|
+
case _:
|
|
93
|
+
msg: str = "Invalid action for WAIT operation."
|
|
94
|
+
|
|
95
|
+
raise InvalidParameterValueException(msg)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Operation transformer for converting OperationUpdates to Operations."""
|
|
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
|
+
OperationType,
|
|
10
|
+
OperationUpdate,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.processors.callback import (
|
|
14
|
+
CallbackProcessor,
|
|
15
|
+
)
|
|
16
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.processors.context import (
|
|
17
|
+
ContextProcessor,
|
|
18
|
+
)
|
|
19
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.processors.execution import (
|
|
20
|
+
ExecutionProcessor,
|
|
21
|
+
)
|
|
22
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.processors.step import (
|
|
23
|
+
StepProcessor,
|
|
24
|
+
)
|
|
25
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.processors.wait import (
|
|
26
|
+
WaitProcessor,
|
|
27
|
+
)
|
|
28
|
+
from aws_durable_execution_sdk_python_testing.exceptions import (
|
|
29
|
+
InvalidParameterValueException,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from collections.abc import MutableMapping
|
|
35
|
+
|
|
36
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.processors.base import (
|
|
37
|
+
OperationProcessor,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
from typing import ClassVar
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class OperationTransformer:
|
|
44
|
+
"""Transforms OperationUpdates to Operations while maintaining order and triggering scheduler actions."""
|
|
45
|
+
|
|
46
|
+
_DEFAULT_PROCESSORS: ClassVar[dict[OperationType, OperationProcessor]] = {
|
|
47
|
+
OperationType.STEP: StepProcessor(),
|
|
48
|
+
OperationType.WAIT: WaitProcessor(),
|
|
49
|
+
OperationType.CONTEXT: ContextProcessor(),
|
|
50
|
+
OperationType.CALLBACK: CallbackProcessor(),
|
|
51
|
+
OperationType.EXECUTION: ExecutionProcessor(),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
processors: MutableMapping[OperationType, OperationProcessor] | None = None,
|
|
57
|
+
):
|
|
58
|
+
self.processors = processors if processors else self._DEFAULT_PROCESSORS
|
|
59
|
+
|
|
60
|
+
def process_updates(
|
|
61
|
+
self,
|
|
62
|
+
updates: list[OperationUpdate],
|
|
63
|
+
current_operations: list[Operation],
|
|
64
|
+
notifier,
|
|
65
|
+
execution_arn: str,
|
|
66
|
+
) -> tuple[list[Operation], list[OperationUpdate]]:
|
|
67
|
+
"""Transform updates maintaining operation order and return (operations, updates)."""
|
|
68
|
+
op_map = {op.operation_id: op for op in current_operations}
|
|
69
|
+
|
|
70
|
+
# Start with copy of current operations list
|
|
71
|
+
result_operations = current_operations.copy()
|
|
72
|
+
|
|
73
|
+
for update in updates:
|
|
74
|
+
processor = self.processors.get(update.operation_type)
|
|
75
|
+
if processor:
|
|
76
|
+
current_op = op_map.get(update.operation_id)
|
|
77
|
+
updated_op = processor.process(
|
|
78
|
+
update=update,
|
|
79
|
+
current_op=current_op,
|
|
80
|
+
notifier=notifier,
|
|
81
|
+
execution_arn=execution_arn,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if updated_op is not None:
|
|
85
|
+
if update.operation_id in op_map:
|
|
86
|
+
# Update existing operation in-place
|
|
87
|
+
for i, op in enumerate(result_operations): # pragma: no branch
|
|
88
|
+
# no branch coverage because result_operation empty not reachable here
|
|
89
|
+
if op.operation_id == update.operation_id:
|
|
90
|
+
result_operations[i] = updated_op
|
|
91
|
+
break
|
|
92
|
+
else:
|
|
93
|
+
# Append new operation to end
|
|
94
|
+
result_operations.append(updated_op)
|
|
95
|
+
|
|
96
|
+
# Update map for future lookups
|
|
97
|
+
op_map[update.operation_id] = updated_op
|
|
98
|
+
else:
|
|
99
|
+
msg: str = (
|
|
100
|
+
f"Checkpoint for {update.operation_type} is not implemented yet."
|
|
101
|
+
)
|
|
102
|
+
raise InvalidParameterValueException(msg)
|
|
103
|
+
|
|
104
|
+
return result_operations, updates
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Checkpoint validation module."""
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Main checkpoint input validator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from aws_durable_execution_sdk_python.lambda_service import (
|
|
9
|
+
OperationAction,
|
|
10
|
+
OperationType,
|
|
11
|
+
OperationUpdate,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.validators.operations.callback import (
|
|
15
|
+
CallbackOperationValidator,
|
|
16
|
+
)
|
|
17
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.validators.operations.context import (
|
|
18
|
+
ContextOperationValidator,
|
|
19
|
+
)
|
|
20
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.validators.operations.execution import (
|
|
21
|
+
ExecutionOperationValidator,
|
|
22
|
+
)
|
|
23
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.validators.operations.invoke import (
|
|
24
|
+
ChainedInvokeOperationValidator,
|
|
25
|
+
)
|
|
26
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.validators.operations.step import (
|
|
27
|
+
StepOperationValidator,
|
|
28
|
+
)
|
|
29
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.validators.operations.wait import (
|
|
30
|
+
WaitOperationValidator,
|
|
31
|
+
)
|
|
32
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.validators.transitions import (
|
|
33
|
+
ValidActionsByOperationTypeValidator,
|
|
34
|
+
)
|
|
35
|
+
from aws_durable_execution_sdk_python_testing.exceptions import (
|
|
36
|
+
InvalidParameterValueException,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from collections.abc import MutableMapping
|
|
42
|
+
|
|
43
|
+
from aws_durable_execution_sdk_python_testing.execution import Execution
|
|
44
|
+
|
|
45
|
+
MAX_ERROR_PAYLOAD_SIZE_BYTES = 32768
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CheckpointValidator:
|
|
49
|
+
"""Validates checkpoint input based on current state."""
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def validate_input(updates: list[OperationUpdate], execution: Execution) -> None:
|
|
53
|
+
"""Perform validation on the given input based on the current state."""
|
|
54
|
+
if not updates:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
CheckpointValidator._validate_conflicting_execution_update(updates)
|
|
58
|
+
CheckpointValidator._validate_parent_id_and_duplicate_id(updates, execution)
|
|
59
|
+
|
|
60
|
+
for update in updates:
|
|
61
|
+
CheckpointValidator._validate_operation_update(update, execution)
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def _validate_conflicting_execution_update(updates: list[OperationUpdate]) -> None:
|
|
65
|
+
"""Validate that there are no conflicting execution updates."""
|
|
66
|
+
execution_updates = [
|
|
67
|
+
update
|
|
68
|
+
for update in updates
|
|
69
|
+
if update.operation_type == OperationType.EXECUTION
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
if len(execution_updates) > 1:
|
|
73
|
+
msg_multiple_exec: str = "Cannot checkpoint multiple EXECUTION updates."
|
|
74
|
+
|
|
75
|
+
raise InvalidParameterValueException(msg_multiple_exec)
|
|
76
|
+
|
|
77
|
+
if execution_updates and updates[-1].operation_type != OperationType.EXECUTION:
|
|
78
|
+
msg_exec_last: str = "EXECUTION checkpoint must be the last update."
|
|
79
|
+
|
|
80
|
+
raise InvalidParameterValueException(msg_exec_last)
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def _validate_operation_update(
|
|
84
|
+
update: OperationUpdate, execution: Execution
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Validate a single operation update."""
|
|
87
|
+
CheckpointValidator._validate_inconsistent_operation_metadata(update, execution)
|
|
88
|
+
CheckpointValidator._validate_payload_sizes(update)
|
|
89
|
+
ValidActionsByOperationTypeValidator.validate(
|
|
90
|
+
update.operation_type, update.action
|
|
91
|
+
)
|
|
92
|
+
CheckpointValidator._validate_operation_status_transition(update, execution)
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def _validate_payload_sizes(update: OperationUpdate) -> None:
|
|
96
|
+
"""Validate that operation payload sizes are not too large."""
|
|
97
|
+
if update.error is not None:
|
|
98
|
+
payload = json.dumps(update.error.to_dict())
|
|
99
|
+
if len(payload) > MAX_ERROR_PAYLOAD_SIZE_BYTES:
|
|
100
|
+
msg: str = f"Error object size must be less than {MAX_ERROR_PAYLOAD_SIZE_BYTES} bytes."
|
|
101
|
+
raise InvalidParameterValueException(msg)
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def _validate_operation_status_transition(
|
|
105
|
+
update: OperationUpdate, execution: Execution
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Validate that the operation status transition is valid."""
|
|
108
|
+
current_state = None
|
|
109
|
+
for operation in execution.operations:
|
|
110
|
+
if operation.operation_id == update.operation_id:
|
|
111
|
+
current_state = operation
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
match update.operation_type:
|
|
115
|
+
case OperationType.STEP:
|
|
116
|
+
StepOperationValidator.validate(current_state, update)
|
|
117
|
+
case OperationType.CONTEXT:
|
|
118
|
+
ContextOperationValidator.validate(current_state, update)
|
|
119
|
+
case OperationType.WAIT:
|
|
120
|
+
WaitOperationValidator.validate(current_state, update)
|
|
121
|
+
case OperationType.CALLBACK:
|
|
122
|
+
CallbackOperationValidator.validate(current_state, update)
|
|
123
|
+
case OperationType.CHAINED_INVOKE:
|
|
124
|
+
ChainedInvokeOperationValidator.validate(current_state, update)
|
|
125
|
+
case OperationType.EXECUTION:
|
|
126
|
+
ExecutionOperationValidator.validate(update)
|
|
127
|
+
case _: # pragma: no cover
|
|
128
|
+
msg: str = "Invalid operation type."
|
|
129
|
+
|
|
130
|
+
raise InvalidParameterValueException(msg)
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def _validate_inconsistent_operation_metadata(
|
|
134
|
+
update: OperationUpdate, execution: Execution
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Validate that operation metadata is consistent with existing operation."""
|
|
137
|
+
current_state = None
|
|
138
|
+
for operation in execution.operations:
|
|
139
|
+
if operation.operation_id == update.operation_id:
|
|
140
|
+
current_state = operation
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
if current_state is not None:
|
|
144
|
+
if (
|
|
145
|
+
update.operation_type is not None
|
|
146
|
+
and update.operation_type != current_state.operation_type
|
|
147
|
+
):
|
|
148
|
+
msg: str = "Inconsistent operation type."
|
|
149
|
+
raise InvalidParameterValueException(msg)
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
update.sub_type is not None
|
|
153
|
+
and update.sub_type != current_state.sub_type
|
|
154
|
+
):
|
|
155
|
+
msg_subtype: str = "Inconsistent operation subtype."
|
|
156
|
+
raise InvalidParameterValueException(msg_subtype)
|
|
157
|
+
|
|
158
|
+
if update.name is not None and update.name != current_state.name:
|
|
159
|
+
msg_name: str = "Inconsistent operation name."
|
|
160
|
+
raise InvalidParameterValueException(msg_name)
|
|
161
|
+
|
|
162
|
+
if (
|
|
163
|
+
update.parent_id is not None
|
|
164
|
+
and update.parent_id != current_state.parent_id
|
|
165
|
+
):
|
|
166
|
+
msg_parent: str = "Inconsistent parent operation id."
|
|
167
|
+
raise InvalidParameterValueException(msg_parent)
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def _validate_parent_id_and_duplicate_id(
|
|
171
|
+
updates: list[OperationUpdate], execution: Execution
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Validate parent IDs and check for duplicate operation IDs.
|
|
174
|
+
|
|
175
|
+
Validate that any provided parentId is valid, and also validate no duplicate operation is being
|
|
176
|
+
updated at the same time (unless it is a STEP/CONTEXT starting + performing one more non-START action).
|
|
177
|
+
"""
|
|
178
|
+
operations_started: MutableMapping[str, OperationUpdate] = {}
|
|
179
|
+
last_updates_seen: MutableMapping[str, OperationUpdate] = {}
|
|
180
|
+
|
|
181
|
+
for update in updates:
|
|
182
|
+
if CheckpointValidator._is_invalid_duplicate_update(
|
|
183
|
+
update, last_updates_seen
|
|
184
|
+
):
|
|
185
|
+
msg_duplicate: str = (
|
|
186
|
+
"Cannot checkpoint multiple operations with the same ID."
|
|
187
|
+
)
|
|
188
|
+
raise InvalidParameterValueException(msg_duplicate)
|
|
189
|
+
|
|
190
|
+
if not CheckpointValidator._is_valid_parent_for_update(
|
|
191
|
+
execution, update, operations_started
|
|
192
|
+
):
|
|
193
|
+
msg_parent: str = "Invalid parent operation id."
|
|
194
|
+
raise InvalidParameterValueException(msg_parent)
|
|
195
|
+
|
|
196
|
+
if update.action == OperationAction.START:
|
|
197
|
+
operations_started[update.operation_id] = update
|
|
198
|
+
|
|
199
|
+
last_updates_seen[update.operation_id] = update
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def _is_invalid_duplicate_update(
|
|
203
|
+
update: OperationUpdate, last_updates_seen: MutableMapping[str, OperationUpdate]
|
|
204
|
+
) -> bool:
|
|
205
|
+
"""Check if this is an invalid duplicate update."""
|
|
206
|
+
last_update = last_updates_seen.get(update.operation_id)
|
|
207
|
+
if last_update is None:
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
if last_update.operation_type in (OperationType.STEP, OperationType.CONTEXT):
|
|
211
|
+
# Allow duplicate for STEP/CONTEXT if last was START and current is not START
|
|
212
|
+
allow_duplicate = (
|
|
213
|
+
last_update.action == OperationAction.START
|
|
214
|
+
and update.action != OperationAction.START
|
|
215
|
+
)
|
|
216
|
+
return not allow_duplicate
|
|
217
|
+
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def _is_valid_parent_for_update(
|
|
222
|
+
execution: Execution,
|
|
223
|
+
update: OperationUpdate,
|
|
224
|
+
operations_started: MutableMapping[str, OperationUpdate],
|
|
225
|
+
) -> bool:
|
|
226
|
+
"""Check if the parent ID is valid for the update."""
|
|
227
|
+
parent_id = update.parent_id
|
|
228
|
+
|
|
229
|
+
if parent_id is None:
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
# Check if parent is in operations started in this batch
|
|
233
|
+
if parent_id in operations_started:
|
|
234
|
+
parent_update = operations_started[parent_id]
|
|
235
|
+
return parent_update.operation_type == OperationType.CONTEXT
|
|
236
|
+
|
|
237
|
+
# Check if parent exists in current execution state
|
|
238
|
+
for operation in execution.operations:
|
|
239
|
+
if operation.operation_id == parent_id:
|
|
240
|
+
return operation.operation_type == OperationType.CONTEXT
|
|
241
|
+
|
|
242
|
+
return False
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Operation-specific validators."""
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Callback operation validator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from aws_durable_execution_sdk_python.lambda_service import (
|
|
6
|
+
Operation,
|
|
7
|
+
OperationAction,
|
|
8
|
+
OperationStatus,
|
|
9
|
+
OperationUpdate,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from aws_durable_execution_sdk_python_testing.exceptions import (
|
|
13
|
+
InvalidParameterValueException,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
VALID_ACTIONS_FOR_CALLBACK = frozenset(
|
|
18
|
+
[
|
|
19
|
+
OperationAction.START,
|
|
20
|
+
]
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CallbackOperationValidator:
|
|
25
|
+
"""Validates CALLBACK operation transitions."""
|
|
26
|
+
|
|
27
|
+
_ALLOWED_STATUS_TO_CANCEL = frozenset(
|
|
28
|
+
[
|
|
29
|
+
OperationStatus.STARTED,
|
|
30
|
+
]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def validate(current_state: Operation | None, update: OperationUpdate) -> None:
|
|
35
|
+
"""Validate CALLBACK operation update."""
|
|
36
|
+
match update.action:
|
|
37
|
+
case OperationAction.START:
|
|
38
|
+
if current_state is not None:
|
|
39
|
+
msg_callback_exists: str = (
|
|
40
|
+
"Cannot start a CALLBACK that already exist."
|
|
41
|
+
)
|
|
42
|
+
raise InvalidParameterValueException(msg_callback_exists)
|
|
43
|
+
case _:
|
|
44
|
+
msg: str = "Invalid action for CALLBACK operation."
|
|
45
|
+
raise InvalidParameterValueException(msg)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Context operation validator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from aws_durable_execution_sdk_python.lambda_service import (
|
|
6
|
+
Operation,
|
|
7
|
+
OperationAction,
|
|
8
|
+
OperationStatus,
|
|
9
|
+
OperationUpdate,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from aws_durable_execution_sdk_python_testing.exceptions import (
|
|
13
|
+
InvalidParameterValueException,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
VALID_ACTIONS_FOR_CONTEXT = frozenset(
|
|
18
|
+
[
|
|
19
|
+
OperationAction.START,
|
|
20
|
+
OperationAction.FAIL,
|
|
21
|
+
OperationAction.SUCCEED,
|
|
22
|
+
]
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ContextOperationValidator:
|
|
27
|
+
"""Validates CONTEXT operation transitions."""
|
|
28
|
+
|
|
29
|
+
_ALLOWED_STATUS_TO_CLOSE = frozenset(
|
|
30
|
+
[
|
|
31
|
+
OperationStatus.STARTED,
|
|
32
|
+
]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def validate(current_state: Operation | None, update: OperationUpdate) -> None:
|
|
37
|
+
"""Validate CONTEXT operation update."""
|
|
38
|
+
match update.action:
|
|
39
|
+
case OperationAction.START:
|
|
40
|
+
if current_state is not None:
|
|
41
|
+
msg_context_exists: str = (
|
|
42
|
+
"Cannot start a CONTEXT that already exist."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
raise InvalidParameterValueException(msg_context_exists)
|
|
46
|
+
case OperationAction.FAIL | OperationAction.SUCCEED:
|
|
47
|
+
if (
|
|
48
|
+
current_state is not None
|
|
49
|
+
and current_state.status
|
|
50
|
+
not in ContextOperationValidator._ALLOWED_STATUS_TO_CLOSE
|
|
51
|
+
):
|
|
52
|
+
msg_context_close: str = "Invalid current CONTEXT state to close."
|
|
53
|
+
|
|
54
|
+
raise InvalidParameterValueException(msg_context_close)
|
|
55
|
+
if update.action == OperationAction.FAIL and update.payload is not None:
|
|
56
|
+
msg_context_fail_payload: str = (
|
|
57
|
+
"Cannot provide a Payload for FAIL action."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
raise InvalidParameterValueException(msg_context_fail_payload)
|
|
61
|
+
if (
|
|
62
|
+
update.action == OperationAction.SUCCEED
|
|
63
|
+
and update.error is not None
|
|
64
|
+
):
|
|
65
|
+
msg_context_succeed_error: str = (
|
|
66
|
+
"Cannot provide an Error for SUCCEED action."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
raise InvalidParameterValueException(msg_context_succeed_error)
|
|
70
|
+
case _:
|
|
71
|
+
msg_context_invalid: str = "Invalid CONTEXT action."
|
|
72
|
+
|
|
73
|
+
raise InvalidParameterValueException(msg_context_invalid)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Execution operation validator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from aws_durable_execution_sdk_python.lambda_service import (
|
|
6
|
+
OperationAction,
|
|
7
|
+
OperationUpdate,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from aws_durable_execution_sdk_python_testing.exceptions import (
|
|
11
|
+
InvalidParameterValueException,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
VALID_ACTIONS_FOR_EXECUTION = frozenset(
|
|
16
|
+
[
|
|
17
|
+
OperationAction.SUCCEED,
|
|
18
|
+
OperationAction.FAIL,
|
|
19
|
+
]
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ExecutionOperationValidator:
|
|
24
|
+
"""Validates EXECUTION operation transitions."""
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def validate(update: OperationUpdate) -> None:
|
|
28
|
+
"""Validate EXECUTION operation update."""
|
|
29
|
+
match update.action:
|
|
30
|
+
case OperationAction.SUCCEED:
|
|
31
|
+
if update.error is not None:
|
|
32
|
+
msg_exec_succeed_error: str = (
|
|
33
|
+
"Cannot provide an Error for SUCCEED action."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
raise InvalidParameterValueException(msg_exec_succeed_error)
|
|
37
|
+
case OperationAction.FAIL:
|
|
38
|
+
if update.payload is not None:
|
|
39
|
+
msg_exec_fail_payload: str = (
|
|
40
|
+
"Cannot provide a Payload for FAIL action."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
raise InvalidParameterValueException(msg_exec_fail_payload)
|
|
44
|
+
case _:
|
|
45
|
+
msg_exec_invalid: str = "Invalid EXECUTION action."
|
|
46
|
+
|
|
47
|
+
raise InvalidParameterValueException(msg_exec_invalid)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Invoke operation validator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from aws_durable_execution_sdk_python.lambda_service import (
|
|
6
|
+
Operation,
|
|
7
|
+
OperationAction,
|
|
8
|
+
OperationStatus,
|
|
9
|
+
OperationUpdate,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from aws_durable_execution_sdk_python_testing.exceptions import (
|
|
13
|
+
InvalidParameterValueException,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
VALID_ACTIONS_FOR_INVOKE = frozenset(
|
|
18
|
+
[
|
|
19
|
+
OperationAction.START,
|
|
20
|
+
OperationAction.CANCEL,
|
|
21
|
+
]
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ChainedInvokeOperationValidator:
|
|
26
|
+
"""Validates INVOKE operation transitions."""
|
|
27
|
+
|
|
28
|
+
_ALLOWED_STATUS_TO_CANCEL = frozenset(
|
|
29
|
+
[
|
|
30
|
+
OperationStatus.STARTED,
|
|
31
|
+
]
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def validate(current_state: Operation | None, update: OperationUpdate) -> None:
|
|
36
|
+
"""Validate INVOKE operation update."""
|
|
37
|
+
match update.action:
|
|
38
|
+
case OperationAction.START:
|
|
39
|
+
if current_state is not None:
|
|
40
|
+
msg_invoke_exists: str = (
|
|
41
|
+
"Cannot start an INVOKE that already exist."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
raise InvalidParameterValueException(msg_invoke_exists)
|
|
45
|
+
case OperationAction.CANCEL:
|
|
46
|
+
if (
|
|
47
|
+
current_state is None
|
|
48
|
+
or current_state.status
|
|
49
|
+
not in ChainedInvokeOperationValidator._ALLOWED_STATUS_TO_CANCEL
|
|
50
|
+
):
|
|
51
|
+
msg_invoke_cancel: str = "Cannot cancel an INVOKE that does not exist or has already completed."
|
|
52
|
+
raise InvalidParameterValueException(msg_invoke_cancel)
|
|
53
|
+
case _:
|
|
54
|
+
msg_invoke_invalid: str = "Invalid INVOKE action."
|
|
55
|
+
|
|
56
|
+
raise InvalidParameterValueException(msg_invoke_invalid)
|