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,807 @@
|
|
|
1
|
+
"""HTTP endpoint handlers for AWS Lambda Durable Functions operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
10
|
+
|
|
11
|
+
from aws_durable_execution_sdk_python_testing.exceptions import (
|
|
12
|
+
AwsApiException,
|
|
13
|
+
ExecutionAlreadyStartedException,
|
|
14
|
+
ExecutionConflictException,
|
|
15
|
+
IllegalStateException,
|
|
16
|
+
InvalidParameterValueException,
|
|
17
|
+
ServiceException,
|
|
18
|
+
)
|
|
19
|
+
from aws_durable_execution_sdk_python_testing.model import (
|
|
20
|
+
CheckpointDurableExecutionRequest,
|
|
21
|
+
CheckpointDurableExecutionResponse,
|
|
22
|
+
GetDurableExecutionHistoryResponse,
|
|
23
|
+
GetDurableExecutionStateResponse,
|
|
24
|
+
ListDurableExecutionsByFunctionRequest,
|
|
25
|
+
ListDurableExecutionsRequest,
|
|
26
|
+
ListDurableExecutionsResponse,
|
|
27
|
+
SendDurableExecutionCallbackFailureRequest,
|
|
28
|
+
SendDurableExecutionCallbackFailureResponse,
|
|
29
|
+
SendDurableExecutionCallbackHeartbeatRequest,
|
|
30
|
+
SendDurableExecutionCallbackHeartbeatResponse,
|
|
31
|
+
SendDurableExecutionCallbackSuccessResponse,
|
|
32
|
+
StartDurableExecutionInput,
|
|
33
|
+
StartDurableExecutionOutput,
|
|
34
|
+
StopDurableExecutionRequest,
|
|
35
|
+
StopDurableExecutionResponse,
|
|
36
|
+
)
|
|
37
|
+
from aws_durable_execution_sdk_python_testing.web.models import (
|
|
38
|
+
HTTPRequest,
|
|
39
|
+
HTTPResponse,
|
|
40
|
+
)
|
|
41
|
+
from aws_durable_execution_sdk_python_testing.web.routes import (
|
|
42
|
+
CallbackFailureRoute,
|
|
43
|
+
CallbackHeartbeatRoute,
|
|
44
|
+
CallbackSuccessRoute,
|
|
45
|
+
CheckpointDurableExecutionRoute,
|
|
46
|
+
GetDurableExecutionHistoryRoute,
|
|
47
|
+
GetDurableExecutionRoute,
|
|
48
|
+
GetDurableExecutionStateRoute,
|
|
49
|
+
ListDurableExecutionsByFunctionRoute,
|
|
50
|
+
StopDurableExecutionRoute,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if TYPE_CHECKING:
|
|
55
|
+
from aws_durable_execution_sdk_python_testing.executor import Executor
|
|
56
|
+
from aws_durable_execution_sdk_python_testing.web.routes import Route
|
|
57
|
+
|
|
58
|
+
logger = logging.getLogger(__name__)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class EndpointHandler(ABC):
|
|
62
|
+
"""Abstract base class for HTTP endpoint handlers."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, executor: Executor) -> None:
|
|
65
|
+
"""Initialize the handler with an executor.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
executor: The executor instance for handling operations
|
|
69
|
+
"""
|
|
70
|
+
self.executor = executor
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse:
|
|
74
|
+
"""Handle an HTTP request and return an HTTP response.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
parsed_route: The strongly-typed route object
|
|
78
|
+
request: The HTTP request data
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
HTTPResponse: The HTTP response to send to the client
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def _parse_json_body(self, request: HTTPRequest) -> dict[str, Any]:
|
|
85
|
+
"""Parse JSON body from HTTP request with validation.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
request: The HTTP request containing the JSON body
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
dict: The parsed JSON data
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
InvalidParameterValueException: If the request body is empty
|
|
95
|
+
"""
|
|
96
|
+
if not request.body:
|
|
97
|
+
msg = "Request body is required"
|
|
98
|
+
raise InvalidParameterValueException(msg)
|
|
99
|
+
return self._parse_json_body_optional(request)
|
|
100
|
+
|
|
101
|
+
def _parse_json_body_optional(self, request: HTTPRequest) -> dict[str, Any]:
|
|
102
|
+
"""Parse JSON body from HTTP request with validation.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
request: The HTTP request containing the JSON body
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
dict: The parsed JSON data
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
InvalidParameterValueException: If the request body is invalid JSON
|
|
112
|
+
"""
|
|
113
|
+
if not request.body:
|
|
114
|
+
return {}
|
|
115
|
+
|
|
116
|
+
# Handle both dict and bytes body types
|
|
117
|
+
if isinstance(request.body, dict):
|
|
118
|
+
return request.body
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
return json.loads(request.body.decode("utf-8"))
|
|
122
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
123
|
+
msg = f"Invalid JSON in request body: {e}"
|
|
124
|
+
raise InvalidParameterValueException(msg) from e
|
|
125
|
+
|
|
126
|
+
def _json_response(
|
|
127
|
+
self,
|
|
128
|
+
status_code: int,
|
|
129
|
+
data: dict[str, Any],
|
|
130
|
+
additional_headers: dict[str, str] | None = None,
|
|
131
|
+
) -> HTTPResponse:
|
|
132
|
+
"""Create a JSON HTTP response.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
status_code: HTTP status code
|
|
136
|
+
data: Data to serialize as JSON
|
|
137
|
+
additional_headers: Optional additional headers to include
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
HTTPResponse: The HTTP response with JSON body
|
|
141
|
+
"""
|
|
142
|
+
return HTTPResponse.create_json(status_code, data, additional_headers)
|
|
143
|
+
|
|
144
|
+
def _success_response(
|
|
145
|
+
self, data: dict[str, Any], additional_headers: dict[str, str] | None = None
|
|
146
|
+
) -> HTTPResponse:
|
|
147
|
+
"""Create a successful JSON response (200 OK).
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
data: Data to serialize as JSON
|
|
151
|
+
additional_headers: Optional additional headers to include
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
HTTPResponse: The HTTP response with JSON body
|
|
155
|
+
"""
|
|
156
|
+
return self._json_response(200, data, additional_headers)
|
|
157
|
+
|
|
158
|
+
def _created_response(
|
|
159
|
+
self, data: dict[str, Any], additional_headers: dict[str, str] | None = None
|
|
160
|
+
) -> HTTPResponse:
|
|
161
|
+
"""Create a created JSON response (201 Created).
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
data: Data to serialize as JSON
|
|
165
|
+
additional_headers: Optional additional headers to include
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
HTTPResponse: The HTTP response with JSON body
|
|
169
|
+
"""
|
|
170
|
+
return self._json_response(201, data, additional_headers)
|
|
171
|
+
|
|
172
|
+
def _no_content_response(
|
|
173
|
+
self, additional_headers: dict[str, str] | None = None
|
|
174
|
+
) -> HTTPResponse:
|
|
175
|
+
"""Create a no content response (204 No Content).
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
additional_headers: Optional additional headers to include
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
HTTPResponse: The HTTP response with empty body
|
|
182
|
+
"""
|
|
183
|
+
return HTTPResponse.create_empty(204, additional_headers)
|
|
184
|
+
|
|
185
|
+
# Removed deprecated _error_response method - use AWS exceptions directly
|
|
186
|
+
|
|
187
|
+
def _parse_callback_result_payload(self, request: HTTPRequest) -> bytes:
|
|
188
|
+
"""Parse callback result payload from request body.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
request: The HTTP request containing the binary payload
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
bytes: The result payload
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
InvalidParameterValueException: If payload parsing fails
|
|
198
|
+
"""
|
|
199
|
+
if not request.body or not isinstance(request.body, bytes):
|
|
200
|
+
return b""
|
|
201
|
+
|
|
202
|
+
return request.body
|
|
203
|
+
|
|
204
|
+
def _parse_query_param(self, request: HTTPRequest, param_name: str) -> str | None:
|
|
205
|
+
"""Parse a single query parameter from the request.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
request: The HTTP request
|
|
209
|
+
param_name: Name of the query parameter
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
str | None: The parameter value or None if not present
|
|
213
|
+
"""
|
|
214
|
+
param_values = request.query_params.get(param_name)
|
|
215
|
+
return param_values[0] if param_values else None
|
|
216
|
+
|
|
217
|
+
def _parse_query_param_list(
|
|
218
|
+
self, request: HTTPRequest, param_name: str
|
|
219
|
+
) -> list[str]:
|
|
220
|
+
"""Parse a query parameter that can have multiple values.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
request: The HTTP request
|
|
224
|
+
param_name: Name of the query parameter
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
list[str]: List of parameter values (empty if not present)
|
|
228
|
+
"""
|
|
229
|
+
return request.query_params.get(param_name, [])
|
|
230
|
+
|
|
231
|
+
def _validate_required_fields(
|
|
232
|
+
self, data: dict[str, Any], required_fields: list[str]
|
|
233
|
+
) -> None:
|
|
234
|
+
"""Validate that required fields are present in the data.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
data: The data dictionary to validate
|
|
238
|
+
required_fields: List of required field names
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
ValueError: If any required field is missing
|
|
242
|
+
"""
|
|
243
|
+
missing_fields = [field for field in required_fields if field not in data]
|
|
244
|
+
if missing_fields:
|
|
245
|
+
msg = f"Missing required fields: {', '.join(missing_fields)}"
|
|
246
|
+
raise InvalidParameterValueException(msg)
|
|
247
|
+
|
|
248
|
+
def _handle_aws_exception(self, exception: AwsApiException) -> HTTPResponse:
|
|
249
|
+
"""Handle AWS API exceptions directly.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
exception: The AWS API exception
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
HTTPResponse: AWS-compliant error response
|
|
256
|
+
"""
|
|
257
|
+
# Log server errors
|
|
258
|
+
if exception.http_status_code >= 500: # noqa: PLR2004
|
|
259
|
+
logger.exception("Server error: %s", exception)
|
|
260
|
+
return HTTPResponse.create_error_from_exception(exception)
|
|
261
|
+
|
|
262
|
+
def _handle_framework_exception(self, exception: Exception) -> HTTPResponse:
|
|
263
|
+
"""Handle framework exceptions by mapping to AWS exceptions.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
exception: The framework exception
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
HTTPResponse: AWS-compliant error response
|
|
270
|
+
"""
|
|
271
|
+
if isinstance(exception, (ValueError | KeyError)):
|
|
272
|
+
return HTTPResponse.create_error_from_exception(
|
|
273
|
+
InvalidParameterValueException(str(exception))
|
|
274
|
+
)
|
|
275
|
+
logger.exception("Unexpected error: %s", exception)
|
|
276
|
+
return HTTPResponse.create_error_from_exception(
|
|
277
|
+
ServiceException(str(exception))
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class StartExecutionHandler(EndpointHandler):
|
|
282
|
+
"""Handler for POST /start-durable-execution."""
|
|
283
|
+
|
|
284
|
+
def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse: # noqa: ARG002
|
|
285
|
+
"""Handle start execution request.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
parsed_route: The strongly-typed route object
|
|
289
|
+
request: The HTTP request data
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
HTTPResponse: The HTTP response to send to the client
|
|
293
|
+
"""
|
|
294
|
+
try:
|
|
295
|
+
body_data: dict[str, Any] = self._parse_json_body(request)
|
|
296
|
+
|
|
297
|
+
start_input: StartDurableExecutionInput = (
|
|
298
|
+
StartDurableExecutionInput.from_dict(body_data)
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
start_output: StartDurableExecutionOutput = self.executor.start_execution(
|
|
302
|
+
start_input
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
response_data: dict[str, Any] = start_output.to_dict()
|
|
306
|
+
|
|
307
|
+
# Return HTTP 201 Created response
|
|
308
|
+
return self._created_response(response_data)
|
|
309
|
+
|
|
310
|
+
except IllegalStateException as e:
|
|
311
|
+
# For StartExecution operations, map to ExecutionAlreadyStartedException
|
|
312
|
+
aws_exception = ExecutionAlreadyStartedException(
|
|
313
|
+
str(e),
|
|
314
|
+
"arn:aws:lambda:us-east-1:123456789012:function:test",
|
|
315
|
+
)
|
|
316
|
+
return HTTPResponse.create_error_from_exception(aws_exception)
|
|
317
|
+
except AwsApiException as e:
|
|
318
|
+
return self._handle_aws_exception(e)
|
|
319
|
+
except Exception as e: # noqa: BLE001
|
|
320
|
+
return self._handle_framework_exception(e)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class GetDurableExecutionHandler(EndpointHandler):
|
|
324
|
+
"""Handler for GET /2025-12-01/durable-executions/{arn}."""
|
|
325
|
+
|
|
326
|
+
def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse: # noqa: ARG002
|
|
327
|
+
"""Handle get durable execution request.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
parsed_route: The strongly-typed route object
|
|
331
|
+
request: The HTTP request data
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
HTTPResponse: The HTTP response to send to the client
|
|
335
|
+
"""
|
|
336
|
+
try:
|
|
337
|
+
route = cast(GetDurableExecutionRoute, parsed_route)
|
|
338
|
+
|
|
339
|
+
execution_response = self.executor.get_execution_details(route.arn)
|
|
340
|
+
|
|
341
|
+
response_data: dict[str, Any] = execution_response.to_dict()
|
|
342
|
+
|
|
343
|
+
# HTTP 200 OK response
|
|
344
|
+
return self._success_response(response_data)
|
|
345
|
+
|
|
346
|
+
except AwsApiException as e:
|
|
347
|
+
return self._handle_aws_exception(e)
|
|
348
|
+
except Exception as e: # noqa: BLE001
|
|
349
|
+
return self._handle_framework_exception(e)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class CheckpointDurableExecutionHandler(EndpointHandler):
|
|
353
|
+
"""Handler for POST /2025-12-01/durable-executions/{arn}/checkpoint."""
|
|
354
|
+
|
|
355
|
+
def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse:
|
|
356
|
+
"""Handle checkpoint durable execution request.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
parsed_route: The strongly-typed route object
|
|
360
|
+
request: The HTTP request data
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
HTTPResponse: The HTTP response to send to the client
|
|
364
|
+
"""
|
|
365
|
+
try:
|
|
366
|
+
body_data: dict[str, Any] = self._parse_json_body(request)
|
|
367
|
+
|
|
368
|
+
checkpoint_route = cast(CheckpointDurableExecutionRoute, parsed_route)
|
|
369
|
+
execution_arn: str = checkpoint_route.arn
|
|
370
|
+
|
|
371
|
+
checkpoint_request: CheckpointDurableExecutionRequest = (
|
|
372
|
+
CheckpointDurableExecutionRequest.from_dict(body_data, execution_arn)
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
checkpoint_response: CheckpointDurableExecutionResponse = (
|
|
376
|
+
self.executor.checkpoint_execution(
|
|
377
|
+
execution_arn,
|
|
378
|
+
checkpoint_request.checkpoint_token,
|
|
379
|
+
checkpoint_request.updates,
|
|
380
|
+
checkpoint_request.client_token,
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
response_data: dict[str, Any] = checkpoint_response.to_dict()
|
|
385
|
+
|
|
386
|
+
return self._success_response(response_data)
|
|
387
|
+
|
|
388
|
+
except AwsApiException as e:
|
|
389
|
+
return self._handle_aws_exception(e)
|
|
390
|
+
except Exception as e: # noqa: BLE001
|
|
391
|
+
return self._handle_framework_exception(e)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class StopDurableExecutionHandler(EndpointHandler):
|
|
395
|
+
"""Handler for POST /2025-12-01/durable-executions/{arn}/stop."""
|
|
396
|
+
|
|
397
|
+
def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse:
|
|
398
|
+
"""Handle stop durable execution request.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
parsed_route: The strongly-typed route object
|
|
402
|
+
request: The HTTP request data
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
HTTPResponse: The HTTP response to send to the client
|
|
406
|
+
"""
|
|
407
|
+
try:
|
|
408
|
+
body_data: dict[str, Any] = self._parse_json_body_optional(request)
|
|
409
|
+
|
|
410
|
+
stop_route = cast(StopDurableExecutionRoute, parsed_route)
|
|
411
|
+
execution_arn: str = stop_route.arn
|
|
412
|
+
|
|
413
|
+
body_data["DurableExecutionArn"] = execution_arn
|
|
414
|
+
stop_request: StopDurableExecutionRequest = (
|
|
415
|
+
StopDurableExecutionRequest.from_dict(body_data)
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
stop_response: StopDurableExecutionResponse = self.executor.stop_execution(
|
|
419
|
+
execution_arn, stop_request.error
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
response_data: dict[str, Any] = stop_response.to_dict()
|
|
423
|
+
|
|
424
|
+
return self._success_response(response_data)
|
|
425
|
+
|
|
426
|
+
except AwsApiException as e:
|
|
427
|
+
return self._handle_aws_exception(e)
|
|
428
|
+
except Exception as e: # noqa: BLE001
|
|
429
|
+
return self._handle_framework_exception(e)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class GetDurableExecutionStateHandler(EndpointHandler):
|
|
433
|
+
"""Handler for GET /2025-12-01/durable-executions/{arn}/state."""
|
|
434
|
+
|
|
435
|
+
def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse: # noqa: ARG002
|
|
436
|
+
"""Handle get durable execution state request.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
parsed_route: The strongly-typed route object
|
|
440
|
+
request: The HTTP request data
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
HTTPResponse: The HTTP response to send to the client
|
|
444
|
+
"""
|
|
445
|
+
try:
|
|
446
|
+
state_route = cast(GetDurableExecutionStateRoute, parsed_route)
|
|
447
|
+
execution_arn: str = state_route.arn
|
|
448
|
+
|
|
449
|
+
state_response: GetDurableExecutionStateResponse = (
|
|
450
|
+
self.executor.get_execution_state(execution_arn)
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
response_data: dict[str, Any] = state_response.to_dict()
|
|
454
|
+
|
|
455
|
+
return self._success_response(response_data)
|
|
456
|
+
|
|
457
|
+
except AwsApiException as e:
|
|
458
|
+
return self._handle_aws_exception(e)
|
|
459
|
+
except Exception as e: # noqa: BLE001
|
|
460
|
+
return self._handle_framework_exception(e)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
class GetDurableExecutionHistoryHandler(EndpointHandler):
|
|
464
|
+
"""Handler for GET /2025-12-01/durable-executions/{arn}/history."""
|
|
465
|
+
|
|
466
|
+
def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse:
|
|
467
|
+
"""Handle get durable execution history request.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
parsed_route: The strongly-typed route object
|
|
471
|
+
request: The HTTP request data
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
HTTPResponse: The HTTP response to send to the client
|
|
475
|
+
"""
|
|
476
|
+
try:
|
|
477
|
+
history_route = cast(GetDurableExecutionHistoryRoute, parsed_route)
|
|
478
|
+
execution_arn: str = history_route.arn
|
|
479
|
+
|
|
480
|
+
max_items: str | None = self._parse_query_param(request, "MaxItems")
|
|
481
|
+
marker: str | None = self._parse_query_param(request, "Marker")
|
|
482
|
+
include_execution_data_str: str | None = self._parse_query_param(
|
|
483
|
+
request, "IncludeExecutionData"
|
|
484
|
+
)
|
|
485
|
+
include_execution_data: bool = (
|
|
486
|
+
include_execution_data_str == "true"
|
|
487
|
+
if include_execution_data_str
|
|
488
|
+
else False
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
history_response: GetDurableExecutionHistoryResponse = (
|
|
492
|
+
self.executor.get_execution_history(
|
|
493
|
+
execution_arn,
|
|
494
|
+
include_execution_data=include_execution_data,
|
|
495
|
+
reverse_order=False,
|
|
496
|
+
marker=marker,
|
|
497
|
+
max_items=int(max_items) if max_items else None,
|
|
498
|
+
)
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
response_data: dict[str, Any] = history_response.to_dict()
|
|
502
|
+
|
|
503
|
+
return self._success_response(response_data)
|
|
504
|
+
|
|
505
|
+
except AwsApiException as e:
|
|
506
|
+
return self._handle_aws_exception(e)
|
|
507
|
+
except Exception as e: # noqa: BLE001
|
|
508
|
+
return self._handle_framework_exception(e)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
class ListDurableExecutionsHandler(EndpointHandler):
|
|
512
|
+
"""Handler for GET /2025-12-01/durable-executions."""
|
|
513
|
+
|
|
514
|
+
def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse: # noqa: ARG002
|
|
515
|
+
"""Handle list durable executions request.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
parsed_route: The strongly-typed route object
|
|
519
|
+
request: The HTTP request data
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
HTTPResponse: The HTTP response to send to the client
|
|
523
|
+
"""
|
|
524
|
+
try:
|
|
525
|
+
list_request: ListDurableExecutionsRequest = (
|
|
526
|
+
ListDurableExecutionsRequest.from_dict(request.query_params)
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# Call executor method with correct attribute mapping
|
|
530
|
+
list_response: ListDurableExecutionsResponse = self.executor.list_executions(
|
|
531
|
+
function_name=list_request.function_name,
|
|
532
|
+
function_version=list_request.function_version,
|
|
533
|
+
execution_name=list_request.durable_execution_name, # Map to executor parameter
|
|
534
|
+
status_filter=list_request.status_filter[0]
|
|
535
|
+
if list_request.status_filter
|
|
536
|
+
else None, # Executor expects single string
|
|
537
|
+
started_after=list_request.started_after,
|
|
538
|
+
started_before=list_request.started_before,
|
|
539
|
+
marker=list_request.marker,
|
|
540
|
+
max_items=list_request.max_items
|
|
541
|
+
if list_request.max_items > 0
|
|
542
|
+
else None,
|
|
543
|
+
reverse_order=list_request.reverse_order or False,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
# Serialize response
|
|
547
|
+
response_data: dict[str, Any] = list_response.to_dict()
|
|
548
|
+
|
|
549
|
+
return self._success_response(response_data)
|
|
550
|
+
|
|
551
|
+
except AwsApiException as e:
|
|
552
|
+
return self._handle_aws_exception(e)
|
|
553
|
+
except Exception as e: # noqa: BLE001
|
|
554
|
+
return self._handle_framework_exception(e)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
class ListDurableExecutionsByFunctionHandler(EndpointHandler):
|
|
558
|
+
"""Handler for GET /2025-12-01/functions/{function_name}/durable-executions."""
|
|
559
|
+
|
|
560
|
+
@staticmethod
|
|
561
|
+
def _validate_function_name(function_name: str) -> None:
|
|
562
|
+
"""Validate function name parameter."""
|
|
563
|
+
if not function_name or not function_name.strip():
|
|
564
|
+
msg = "Function name is required"
|
|
565
|
+
raise InvalidParameterValueException(msg)
|
|
566
|
+
|
|
567
|
+
def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse:
|
|
568
|
+
"""Handle list durable executions by function request.
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
parsed_route: The strongly-typed route object
|
|
572
|
+
request: The HTTP request data
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
HTTPResponse: The HTTP response to send to the client
|
|
576
|
+
"""
|
|
577
|
+
function_route = cast(ListDurableExecutionsByFunctionRoute, parsed_route)
|
|
578
|
+
function_name: str = function_route.function_name
|
|
579
|
+
|
|
580
|
+
# Validate function name before processing
|
|
581
|
+
self._validate_function_name(function_name)
|
|
582
|
+
|
|
583
|
+
try:
|
|
584
|
+
# Add function name from route to query params
|
|
585
|
+
query_params = dict(request.query_params)
|
|
586
|
+
query_params["FunctionName"] = [function_name]
|
|
587
|
+
list_request = ListDurableExecutionsByFunctionRequest.from_dict(
|
|
588
|
+
query_params
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
list_response = self.executor.list_executions_by_function(
|
|
592
|
+
function_name=list_request.function_name,
|
|
593
|
+
qualifier=list_request.qualifier,
|
|
594
|
+
execution_name=list_request.durable_execution_name,
|
|
595
|
+
status_filter=list_request.status_filter[0]
|
|
596
|
+
if list_request.status_filter
|
|
597
|
+
else None,
|
|
598
|
+
started_after=list_request.started_after,
|
|
599
|
+
started_before=list_request.started_before,
|
|
600
|
+
marker=list_request.marker,
|
|
601
|
+
max_items=list_request.max_items
|
|
602
|
+
if list_request.max_items > 0
|
|
603
|
+
else None,
|
|
604
|
+
reverse_order=list_request.reverse_order or False,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
return self._success_response(list_response.to_dict())
|
|
608
|
+
|
|
609
|
+
except AwsApiException as e:
|
|
610
|
+
return self._handle_aws_exception(e)
|
|
611
|
+
except Exception as e: # noqa: BLE001
|
|
612
|
+
return self._handle_framework_exception(e)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
class SendDurableExecutionCallbackSuccessHandler(EndpointHandler):
|
|
616
|
+
"""Handler for POST /2025-12-01/durable-execution-callbacks/{callback_id}/succeed."""
|
|
617
|
+
|
|
618
|
+
def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse:
|
|
619
|
+
"""Handle send durable execution callback success request.
|
|
620
|
+
|
|
621
|
+
Args:
|
|
622
|
+
parsed_route: The strongly-typed route object
|
|
623
|
+
request: The HTTP request data
|
|
624
|
+
|
|
625
|
+
Returns:
|
|
626
|
+
HTTPResponse: The HTTP response to send to the client
|
|
627
|
+
"""
|
|
628
|
+
try:
|
|
629
|
+
callback_route = cast(CallbackSuccessRoute, parsed_route)
|
|
630
|
+
callback_id: str = callback_route.callback_id
|
|
631
|
+
|
|
632
|
+
result_bytes: bytes = self._parse_callback_result_payload(request)
|
|
633
|
+
|
|
634
|
+
callback_response: SendDurableExecutionCallbackSuccessResponse = ( # noqa: F841
|
|
635
|
+
self.executor.send_callback_success(
|
|
636
|
+
callback_id=callback_id, result=result_bytes
|
|
637
|
+
)
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
logger.debug(
|
|
641
|
+
"Callback %s succeeded with result: %s",
|
|
642
|
+
callback_id,
|
|
643
|
+
result_bytes.decode("utf-8", errors="replace"),
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
# Callback success response is empty
|
|
647
|
+
return self._success_response({})
|
|
648
|
+
|
|
649
|
+
except IllegalStateException as e:
|
|
650
|
+
# For callback operations, map to ExecutionConflictException
|
|
651
|
+
aws_exception = ExecutionConflictException(str(e))
|
|
652
|
+
return HTTPResponse.create_error_from_exception(aws_exception)
|
|
653
|
+
except AwsApiException as e:
|
|
654
|
+
return self._handle_aws_exception(e)
|
|
655
|
+
except Exception as e: # noqa: BLE001
|
|
656
|
+
return self._handle_framework_exception(e)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
class SendDurableExecutionCallbackFailureHandler(EndpointHandler):
|
|
660
|
+
"""Handler for POST /2025-12-01/durable-execution-callbacks/{callback_id}/fail."""
|
|
661
|
+
|
|
662
|
+
def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse:
|
|
663
|
+
"""Handle send durable execution callback failure request.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
parsed_route: The strongly-typed route object
|
|
667
|
+
request: The HTTP request data
|
|
668
|
+
|
|
669
|
+
Returns:
|
|
670
|
+
HTTPResponse: The HTTP response to send to the client
|
|
671
|
+
"""
|
|
672
|
+
try:
|
|
673
|
+
callback_route = cast(CallbackFailureRoute, parsed_route)
|
|
674
|
+
callback_id: str = callback_route.callback_id
|
|
675
|
+
|
|
676
|
+
body_data: dict[str, Any] = self._parse_json_body_optional(request)
|
|
677
|
+
callback_request: SendDurableExecutionCallbackFailureRequest = (
|
|
678
|
+
SendDurableExecutionCallbackFailureRequest.from_dict(
|
|
679
|
+
body_data, callback_id
|
|
680
|
+
)
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
callback_response: SendDurableExecutionCallbackFailureResponse = ( # noqa: F841
|
|
684
|
+
self.executor.send_callback_failure(
|
|
685
|
+
callback_id=callback_id, error=callback_request.error
|
|
686
|
+
)
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
logger.debug(
|
|
690
|
+
"Callback %s failed with error: %s", callback_id, callback_request.error
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
# Callback failure response is empty
|
|
694
|
+
return self._success_response({})
|
|
695
|
+
|
|
696
|
+
except IllegalStateException as e:
|
|
697
|
+
# For callback operations, map to ExecutionConflictException
|
|
698
|
+
aws_exception = ExecutionConflictException(str(e))
|
|
699
|
+
return HTTPResponse.create_error_from_exception(aws_exception)
|
|
700
|
+
except AwsApiException as e:
|
|
701
|
+
return self._handle_aws_exception(e)
|
|
702
|
+
except Exception as e: # noqa: BLE001
|
|
703
|
+
return self._handle_framework_exception(e)
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
class SendDurableExecutionCallbackHeartbeatHandler(EndpointHandler):
|
|
707
|
+
"""Handler for POST /2025-12-01/durable-execution-callbacks/{callback_id}/heartbeat."""
|
|
708
|
+
|
|
709
|
+
def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse:
|
|
710
|
+
"""Handle send durable execution callback heartbeat request.
|
|
711
|
+
|
|
712
|
+
Args:
|
|
713
|
+
parsed_route: The strongly-typed route object
|
|
714
|
+
request: The HTTP request data
|
|
715
|
+
|
|
716
|
+
Returns:
|
|
717
|
+
HTTPResponse: The HTTP response to send to the client
|
|
718
|
+
"""
|
|
719
|
+
try:
|
|
720
|
+
# Heartbeat requests don't have a body, only callback_id from URL
|
|
721
|
+
callback_route = cast(CallbackHeartbeatRoute, parsed_route)
|
|
722
|
+
callback_id: str = callback_route.callback_id
|
|
723
|
+
|
|
724
|
+
callback_response: SendDurableExecutionCallbackHeartbeatResponse = ( # noqa: F841
|
|
725
|
+
self.executor.send_callback_heartbeat(callback_id=callback_id)
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
# Callback heartbeat response is empty
|
|
729
|
+
return self._success_response({})
|
|
730
|
+
|
|
731
|
+
except IllegalStateException as e:
|
|
732
|
+
# For callback operations, map to ExecutionConflictException
|
|
733
|
+
aws_exception = ExecutionConflictException(str(e))
|
|
734
|
+
return HTTPResponse.create_error_from_exception(aws_exception)
|
|
735
|
+
except AwsApiException as e:
|
|
736
|
+
return self._handle_aws_exception(e)
|
|
737
|
+
except Exception as e: # noqa: BLE001
|
|
738
|
+
return self._handle_framework_exception(e)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
# TODO: should this be /ping instead?
|
|
742
|
+
class HealthHandler(EndpointHandler):
|
|
743
|
+
"""Handler for GET /health."""
|
|
744
|
+
|
|
745
|
+
def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse: # noqa: ARG002
|
|
746
|
+
"""Handle health check request.
|
|
747
|
+
|
|
748
|
+
Args:
|
|
749
|
+
parsed_route: The strongly-typed route object
|
|
750
|
+
request: The HTTP request data
|
|
751
|
+
|
|
752
|
+
Returns:
|
|
753
|
+
HTTPResponse: The HTTP response to send to the client
|
|
754
|
+
"""
|
|
755
|
+
return self._success_response({"status": "healthy"})
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
class MetricsHandler(EndpointHandler):
|
|
759
|
+
"""Handler for GET /metrics."""
|
|
760
|
+
|
|
761
|
+
def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse: # noqa: ARG002
|
|
762
|
+
"""Handle metrics request.
|
|
763
|
+
|
|
764
|
+
Args:
|
|
765
|
+
parsed_route: The strongly-typed route object
|
|
766
|
+
request: The HTTP request data
|
|
767
|
+
|
|
768
|
+
Returns:
|
|
769
|
+
HTTPResponse: The HTTP response to send to the client
|
|
770
|
+
"""
|
|
771
|
+
# TODO: Implement metrics collection logic
|
|
772
|
+
return self._success_response({"metrics": {}})
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
class UpdateLambdaEndpointHandler(EndpointHandler):
|
|
776
|
+
"""Handler for PUT /lambda-endpoint."""
|
|
777
|
+
|
|
778
|
+
def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse: # noqa: ARG002
|
|
779
|
+
"""Handle update Lambda endpoint request.
|
|
780
|
+
|
|
781
|
+
Args:
|
|
782
|
+
parsed_route: The strongly-typed route object
|
|
783
|
+
request: The HTTP request data
|
|
784
|
+
|
|
785
|
+
Returns:
|
|
786
|
+
HTTPResponse: The HTTP response to send to the client
|
|
787
|
+
"""
|
|
788
|
+
try:
|
|
789
|
+
body = self._parse_json_body(request)
|
|
790
|
+
endpoint_url = body.get("EndpointUrl")
|
|
791
|
+
region_name = body.get("RegionName", "us-east-1")
|
|
792
|
+
|
|
793
|
+
if not endpoint_url:
|
|
794
|
+
return self._handle_aws_exception(
|
|
795
|
+
InvalidParameterValueException("EndpointUrl is required")
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
# Update the invoker's Lambda endpoint
|
|
799
|
+
invoker = self.executor._invoker # noqa: SLF001
|
|
800
|
+
logger.info("Updating lambda endpoint to %s", endpoint_url)
|
|
801
|
+
invoker.update_endpoint(endpoint_url, region_name)
|
|
802
|
+
return self._success_response(
|
|
803
|
+
{"message": "Lambda endpoint updated successfully"}
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
except Exception as e: # noqa: BLE001
|
|
807
|
+
return self._handle_framework_exception(e)
|