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.
Files changed (52) hide show
  1. aws_durable_execution_sdk_python_testing/__about__.py +4 -0
  2. aws_durable_execution_sdk_python_testing/__init__.py +20 -0
  3. aws_durable_execution_sdk_python_testing/checkpoint/__init__.py +1 -0
  4. aws_durable_execution_sdk_python_testing/checkpoint/processor.py +101 -0
  5. aws_durable_execution_sdk_python_testing/checkpoint/processors/__init__.py +1 -0
  6. aws_durable_execution_sdk_python_testing/checkpoint/processors/base.py +199 -0
  7. aws_durable_execution_sdk_python_testing/checkpoint/processors/callback.py +89 -0
  8. aws_durable_execution_sdk_python_testing/checkpoint/processors/context.py +59 -0
  9. aws_durable_execution_sdk_python_testing/checkpoint/processors/execution.py +52 -0
  10. aws_durable_execution_sdk_python_testing/checkpoint/processors/step.py +124 -0
  11. aws_durable_execution_sdk_python_testing/checkpoint/processors/wait.py +95 -0
  12. aws_durable_execution_sdk_python_testing/checkpoint/transformer.py +104 -0
  13. aws_durable_execution_sdk_python_testing/checkpoint/validators/__init__.py +1 -0
  14. aws_durable_execution_sdk_python_testing/checkpoint/validators/checkpoint.py +242 -0
  15. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/__init__.py +1 -0
  16. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/callback.py +45 -0
  17. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/context.py +73 -0
  18. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/execution.py +47 -0
  19. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/invoke.py +56 -0
  20. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/step.py +106 -0
  21. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/wait.py +54 -0
  22. aws_durable_execution_sdk_python_testing/checkpoint/validators/transitions.py +66 -0
  23. aws_durable_execution_sdk_python_testing/cli.py +501 -0
  24. aws_durable_execution_sdk_python_testing/client.py +50 -0
  25. aws_durable_execution_sdk_python_testing/exceptions.py +288 -0
  26. aws_durable_execution_sdk_python_testing/execution.py +440 -0
  27. aws_durable_execution_sdk_python_testing/executor.py +1214 -0
  28. aws_durable_execution_sdk_python_testing/invoker.py +327 -0
  29. aws_durable_execution_sdk_python_testing/model.py +3267 -0
  30. aws_durable_execution_sdk_python_testing/observer.py +144 -0
  31. aws_durable_execution_sdk_python_testing/py.typed +1 -0
  32. aws_durable_execution_sdk_python_testing/runner.py +1185 -0
  33. aws_durable_execution_sdk_python_testing/scheduler.py +246 -0
  34. aws_durable_execution_sdk_python_testing/stores/__init__.py +1 -0
  35. aws_durable_execution_sdk_python_testing/stores/base.py +147 -0
  36. aws_durable_execution_sdk_python_testing/stores/filesystem.py +104 -0
  37. aws_durable_execution_sdk_python_testing/stores/memory.py +38 -0
  38. aws_durable_execution_sdk_python_testing/stores/sqlite.py +283 -0
  39. aws_durable_execution_sdk_python_testing/token.py +49 -0
  40. aws_durable_execution_sdk_python_testing/web/__init__.py +1 -0
  41. aws_durable_execution_sdk_python_testing/web/errors.py +8 -0
  42. aws_durable_execution_sdk_python_testing/web/handlers.py +807 -0
  43. aws_durable_execution_sdk_python_testing/web/models.py +266 -0
  44. aws_durable_execution_sdk_python_testing/web/routes.py +684 -0
  45. aws_durable_execution_sdk_python_testing/web/serialization.py +245 -0
  46. aws_durable_execution_sdk_python_testing/web/server.py +243 -0
  47. aws_durable_execution_sdk_python_testing-1.0.0.dist-info/METADATA +221 -0
  48. aws_durable_execution_sdk_python_testing-1.0.0.dist-info/RECORD +52 -0
  49. aws_durable_execution_sdk_python_testing-1.0.0.dist-info/WHEEL +4 -0
  50. aws_durable_execution_sdk_python_testing-1.0.0.dist-info/entry_points.txt +2 -0
  51. aws_durable_execution_sdk_python_testing-1.0.0.dist-info/licenses/LICENSE +175 -0
  52. 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)