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,245 @@
1
+ """Serialization interfaces and AWS boto integration for HTTP request/response models.
2
+
3
+ This module provides Protocol interfaces for serialization and deserialization,
4
+ along with AWS-compatible implementations using boto's rest-json serializers.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ from typing import Any, Protocol
12
+ from datetime import datetime
13
+
14
+ import aws_durable_execution_sdk_python
15
+ import botocore.loaders # type: ignore
16
+ from botocore.model import ServiceModel # type: ignore
17
+ from botocore.parsers import create_parser # type: ignore
18
+ from botocore.serialize import create_serializer # type: ignore
19
+
20
+ from aws_durable_execution_sdk_python_testing.exceptions import (
21
+ InvalidParameterValueException,
22
+ )
23
+
24
+
25
+ class Serializer(Protocol):
26
+ """Interface for serializing data to bytes."""
27
+
28
+ def to_bytes(self, data: Any) -> bytes:
29
+ """Serialize data to bytes.
30
+
31
+ Args:
32
+ data: The data to serialize
33
+
34
+ Returns:
35
+ bytes: The serialized data
36
+
37
+ Raises:
38
+ InvalidParameterValueException: If serialization fails
39
+ """
40
+ ... # pragma: no cover
41
+
42
+
43
+ class Deserializer(Protocol):
44
+ """Interface for deserializing bytes to data."""
45
+
46
+ def from_bytes(self, data: bytes) -> dict[str, Any]:
47
+ """Deserialize bytes to dictionary.
48
+
49
+ Args:
50
+ data: The bytes to deserialize
51
+
52
+ Returns:
53
+ dict: The deserialized data
54
+
55
+ Raises:
56
+ InvalidParameterValueException: If deserialization fails
57
+ """
58
+ ... # pragma: no cover
59
+
60
+
61
+ class JSONSerializer:
62
+ """JSON serializer with datetime support."""
63
+
64
+ def to_bytes(self, data: Any) -> bytes:
65
+ """Serialize data to JSON bytes."""
66
+ try:
67
+ json_string = json.dumps(
68
+ data, separators=(",", ":"), default=self._default_handler
69
+ )
70
+ return json_string.encode("utf-8")
71
+ except (TypeError, ValueError) as e:
72
+ raise InvalidParameterValueException(
73
+ f"Failed to serialize data to JSON: {str(e)}"
74
+ )
75
+
76
+ def _default_handler(self, obj: Any) -> float:
77
+ """Handle non-permitive objects."""
78
+ if isinstance(obj, datetime):
79
+ return obj.timestamp()
80
+ # Raise TypeError for unsupported types
81
+ raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
82
+
83
+
84
+ class AwsRestJsonSerializer:
85
+ """AWS rest-json serializer using boto."""
86
+
87
+ def __init__(self, operation_name: str, serializer: Any, operation_model: Any):
88
+ """Initialize the AWS rest-json serializer.
89
+
90
+ Args:
91
+ operation_name: Name of the AWS operation
92
+ serializer: Boto serializer instance
93
+ operation_model: Boto operation model
94
+ """
95
+ self._operation_name = operation_name
96
+ self._serializer = serializer
97
+ self._operation_model = operation_model
98
+
99
+ @classmethod
100
+ def create(cls, operation_name: str) -> AwsRestJsonSerializer:
101
+ """Create serializer with boto components.
102
+
103
+ Args:
104
+ operation_name: Name of the AWS operation
105
+
106
+ Returns:
107
+ AwsRestJsonSerializer: Configured serializer instance
108
+
109
+ Raises:
110
+ InvalidParameterValueException: If serializer creation fails
111
+ """
112
+ try:
113
+ package_path = os.path.dirname(aws_durable_execution_sdk_python.__file__)
114
+ data_path = f"{package_path}/botocore/data"
115
+
116
+ # Load service model
117
+ os.environ["AWS_DATA_PATH"] = data_path
118
+ loader = botocore.loaders.Loader()
119
+ loader.search_paths.append(data_path)
120
+
121
+ raw_model = loader.load_service_model("lambdainternal", "service-2")
122
+ service_model = ServiceModel(raw_model)
123
+
124
+ # Create serializer (rest-json protocol)
125
+ serializer = create_serializer("rest-json", include_validation=True)
126
+ operation_model = service_model.operation_model(operation_name)
127
+
128
+ return cls(operation_name, serializer, operation_model)
129
+ except Exception as e:
130
+ msg = f"Failed to create serializer for {operation_name}: {e}"
131
+ raise InvalidParameterValueException(msg) from e
132
+
133
+ def to_bytes(self, data: dict[str, Any]) -> bytes:
134
+ """Serialize data using boto rest-json serializer.
135
+
136
+ Args:
137
+ data: Dictionary data to serialize
138
+
139
+ Returns:
140
+ bytes: Serialized data
141
+
142
+ Raises:
143
+ InvalidParameterValueException: If serialization fails
144
+ """
145
+ if not self._serializer or not self._operation_model:
146
+ msg = f"Serializer not initialized for {self._operation_name}"
147
+ raise InvalidParameterValueException(msg)
148
+
149
+ try:
150
+ serialized = self._serializer.serialize_to_request(
151
+ data, self._operation_model
152
+ )
153
+ body = serialized.get("body", b"")
154
+
155
+ if isinstance(body, str):
156
+ return body.encode("utf-8")
157
+
158
+ return body # noqa: TRY300
159
+ except Exception as e:
160
+ msg = f"Failed to serialize data for {self._operation_name}: {e}"
161
+ raise InvalidParameterValueException(msg) from e
162
+
163
+
164
+ class AwsRestJsonDeserializer:
165
+ """AWS rest-json deserializer using boto."""
166
+
167
+ def __init__(self, operation_name: str, parser: Any, operation_model: Any):
168
+ """Initialize the AWS rest-json deserializer.
169
+
170
+ Args:
171
+ operation_name: Name of the AWS operation
172
+ parser: Boto parser instance
173
+ operation_model: Boto operation model
174
+ """
175
+ self._operation_name = operation_name
176
+ self._parser = parser
177
+ self._operation_model = operation_model
178
+
179
+ @classmethod
180
+ def create(cls, operation_name: str) -> AwsRestJsonDeserializer:
181
+ """Create deserializer with boto components.
182
+
183
+ Args:
184
+ operation_name: Name of the AWS operation
185
+
186
+ Returns:
187
+ AwsRestJsonDeserializer: Configured deserializer instance
188
+
189
+ Raises:
190
+ InvalidParameterValueException: If deserializer creation fails
191
+ """
192
+ try:
193
+ package_path = os.path.dirname(aws_durable_execution_sdk_python.__file__)
194
+ data_path = f"{package_path}/botocore/data"
195
+
196
+ # Load service model
197
+ os.environ["AWS_DATA_PATH"] = data_path
198
+ loader = botocore.loaders.Loader()
199
+ loader.search_paths.append(data_path)
200
+
201
+ raw_model = loader.load_service_model("lambdainternal", "service-2")
202
+ service_model = ServiceModel(raw_model)
203
+
204
+ # Create parser (rest-json protocol)
205
+ parser = create_parser("rest-json")
206
+ operation_model = service_model.operation_model(operation_name)
207
+
208
+ return cls(operation_name, parser, operation_model)
209
+ except Exception as e:
210
+ msg = f"Failed to create deserializer for {operation_name}: {e}"
211
+ raise InvalidParameterValueException(msg) from e
212
+
213
+ def from_bytes(self, data: bytes) -> dict[str, Any]:
214
+ """Deserialize bytes using boto rest-json parser.
215
+
216
+ Args:
217
+ data: Bytes to deserialize
218
+
219
+ Returns:
220
+ dict: Deserialized data
221
+
222
+ Raises:
223
+ InvalidParameterValueException: If deserialization fails
224
+ """
225
+ if not self._parser or not self._operation_model:
226
+ msg = f"Parser not initialized for {self._operation_name}"
227
+ raise InvalidParameterValueException(msg)
228
+
229
+ try:
230
+ if self._operation_model.output_shape:
231
+ # Create response dict for boto parser
232
+ response_dict = {
233
+ "body": data,
234
+ "headers": {"content-type": "application/json"},
235
+ "status_code": 200,
236
+ }
237
+ return self._parser.parse(
238
+ response_dict, self._operation_model.output_shape
239
+ )
240
+
241
+ # If no output shape, just parse as JSON
242
+ return json.loads(data.decode("utf-8"))
243
+ except Exception as e:
244
+ msg = f"Failed to deserialize data for {self._operation_name}: {e}"
245
+ raise InvalidParameterValueException(msg) from e
@@ -0,0 +1,243 @@
1
+ """Local testing web server for AWS Lambda Durable Functions that mimics the actual Lambda backend services."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass
7
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
8
+ from typing import TYPE_CHECKING, Self
9
+ from urllib.parse import parse_qs, urlparse
10
+
11
+ from aws_durable_execution_sdk_python_testing.exceptions import (
12
+ AwsApiException,
13
+ ServiceException,
14
+ UnknownRouteError,
15
+ )
16
+
17
+
18
+ if TYPE_CHECKING:
19
+ from aws_durable_execution_sdk_python_testing.executor import Executor
20
+
21
+
22
+ # Removed deprecated imports from web.errors
23
+ from aws_durable_execution_sdk_python_testing.web.handlers import (
24
+ CheckpointDurableExecutionHandler,
25
+ EndpointHandler,
26
+ GetDurableExecutionHandler,
27
+ GetDurableExecutionHistoryHandler,
28
+ GetDurableExecutionStateHandler,
29
+ HealthHandler,
30
+ ListDurableExecutionsByFunctionHandler,
31
+ ListDurableExecutionsHandler,
32
+ MetricsHandler,
33
+ SendDurableExecutionCallbackFailureHandler,
34
+ SendDurableExecutionCallbackHeartbeatHandler,
35
+ SendDurableExecutionCallbackSuccessHandler,
36
+ StartExecutionHandler,
37
+ StopDurableExecutionHandler,
38
+ UpdateLambdaEndpointHandler,
39
+ )
40
+ from aws_durable_execution_sdk_python_testing.web.models import (
41
+ HTTPRequest,
42
+ HTTPResponse,
43
+ )
44
+ from aws_durable_execution_sdk_python_testing.web.routes import (
45
+ BytesPayloadRoute,
46
+ CallbackFailureRoute,
47
+ CallbackHeartbeatRoute,
48
+ CallbackSuccessRoute,
49
+ CheckpointDurableExecutionRoute,
50
+ GetDurableExecutionHistoryRoute,
51
+ GetDurableExecutionRoute,
52
+ GetDurableExecutionStateRoute,
53
+ HealthRoute,
54
+ ListDurableExecutionsByFunctionRoute,
55
+ ListDurableExecutionsRoute,
56
+ MetricsRoute,
57
+ Route,
58
+ Router,
59
+ StartExecutionRoute,
60
+ StopDurableExecutionRoute,
61
+ UpdateLambdaEndpointRoute,
62
+ )
63
+
64
+
65
+ logger = logging.getLogger(__name__)
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class WebServiceConfig:
70
+ """Configuration for the web service."""
71
+
72
+ host: str = "localhost"
73
+ port: int = 5000
74
+ log_level: int = logging.INFO
75
+ max_request_size: int = 10 * 1024 * 1024 # 10MB
76
+
77
+
78
+ class RequestHandler(BaseHTTPRequestHandler):
79
+ """HTTP request handler for durable execution operations."""
80
+
81
+ def __init__(self, request, client_address, server) -> None:
82
+ self.executor: Executor = server.executor
83
+ self.router: Router = server.router # Access shared router
84
+ self.endpoint_handlers: dict[type[Route], EndpointHandler] = (
85
+ server.endpoint_handlers
86
+ ) # Access shared handlers
87
+ super().__init__(request, client_address, server)
88
+
89
+ def do_GET(self) -> None: # noqa: N802
90
+ """Handle GET requests."""
91
+ self._handle_request("GET")
92
+
93
+ def do_POST(self) -> None: # noqa: N802
94
+ """Handle POST requests."""
95
+ self._handle_request("POST")
96
+
97
+ def do_PUT(self) -> None: # noqa: N802
98
+ """Handle PUT requests."""
99
+ self._handle_request("PUT")
100
+
101
+ def _handle_request(self, method: str) -> None:
102
+ """Handle HTTP request with strongly-typed routing."""
103
+ try:
104
+ # Parse URL path and method into strongly-typed Route object using shared router
105
+ url_path: str = self.path.split("?")[0]
106
+ parsed_route: Route = self.router.find_route(url_path, method)
107
+
108
+ # Find handler for this route type
109
+ handler: EndpointHandler | None = self.endpoint_handlers.get(
110
+ type(parsed_route)
111
+ )
112
+
113
+ if not handler:
114
+ raise UnknownRouteError(method, url_path) # noqa: TRY301
115
+
116
+ # Parse query parameters and request body
117
+ parsed_url = urlparse(self.path)
118
+ query_params: dict[str, list[str]] = parse_qs(parsed_url.query)
119
+ content_length: int = int(self.headers.get("Content-Length", 0))
120
+ body_bytes: bytes = (
121
+ self.rfile.read(content_length) if content_length > 0 else b""
122
+ )
123
+
124
+ # For callback operations, use raw bytes directly
125
+ if isinstance(parsed_route, BytesPayloadRoute):
126
+ request = HTTPRequest.from_raw_bytes(
127
+ body_bytes=body_bytes,
128
+ method=method,
129
+ path=parsed_route,
130
+ headers=dict(self.headers),
131
+ query_params=query_params,
132
+ )
133
+ else:
134
+ # Create strongly-typed HTTP request object with pre-parsed body
135
+ request = HTTPRequest.from_bytes(
136
+ body_bytes=body_bytes,
137
+ operation_name=None,
138
+ method=method,
139
+ path=parsed_route,
140
+ headers=dict(self.headers),
141
+ query_params=query_params,
142
+ )
143
+
144
+ # Handle request with appropriate handler
145
+ response: HTTPResponse = handler.handle(parsed_route, request)
146
+
147
+ # Send HTTP response
148
+ self._send_response(response)
149
+
150
+ except Exception as e:
151
+ logger.exception("Request handling failed")
152
+
153
+ aws_exception: AwsApiException = (
154
+ e if isinstance(e, AwsApiException) else ServiceException(str(e))
155
+ )
156
+
157
+ http_response = HTTPResponse.create_error_from_exception(aws_exception)
158
+ self._send_response(http_response)
159
+
160
+ def _send_response(self, response: HTTPResponse) -> None:
161
+ """Send HTTP response to client."""
162
+ self.send_response(response.status_code)
163
+ for header_name, header_value in response.headers.items():
164
+ self.send_header(header_name, header_value)
165
+ self.end_headers()
166
+
167
+ # Convert response body to bytes for transmission
168
+ if response.body:
169
+ self.wfile.write(response.body_to_bytes())
170
+
171
+ def log_message(self, format_string: str, *args) -> None:
172
+ """Override to use Python logging instead of stderr."""
173
+ logger.info("%s - %s", self.address_string(), format_string % args)
174
+
175
+
176
+ class WebServer(ThreadingHTTPServer):
177
+ """Multi-threaded HTTP server for durable execution operations."""
178
+
179
+ def __init__(self, config: WebServiceConfig, executor: Executor) -> None:
180
+ """Initialize the web server.
181
+
182
+ Args:
183
+ config: Server configuration
184
+ executor: Executor instance for handling operations
185
+ """
186
+ self.config = config
187
+ self.executor = executor
188
+
189
+ # Configure logging
190
+ logging.basicConfig(level=config.log_level)
191
+ logging.getLogger("botocore").setLevel(logging.WARNING)
192
+
193
+ # Create shared router and endpoint handlers
194
+ self.router = Router() # Shared across all request handlers
195
+ self.endpoint_handlers = (
196
+ self._create_endpoint_handlers()
197
+ ) # Shared handler registry
198
+
199
+ # Initialize the HTTP server
200
+ super().__init__((config.host, config.port), RequestHandler)
201
+
202
+ logger.info("Web server initialized on %s:%s", config.host, config.port)
203
+
204
+ def _create_endpoint_handlers(self) -> dict[type[Route], EndpointHandler]:
205
+ """Create endpoint handlers registry - called once during server initialization."""
206
+ return {
207
+ StartExecutionRoute: StartExecutionHandler(self.executor),
208
+ GetDurableExecutionRoute: GetDurableExecutionHandler(self.executor),
209
+ CheckpointDurableExecutionRoute: CheckpointDurableExecutionHandler(
210
+ self.executor
211
+ ),
212
+ StopDurableExecutionRoute: StopDurableExecutionHandler(self.executor),
213
+ GetDurableExecutionStateRoute: GetDurableExecutionStateHandler(
214
+ self.executor
215
+ ),
216
+ GetDurableExecutionHistoryRoute: GetDurableExecutionHistoryHandler(
217
+ self.executor
218
+ ),
219
+ ListDurableExecutionsRoute: ListDurableExecutionsHandler(self.executor),
220
+ ListDurableExecutionsByFunctionRoute: ListDurableExecutionsByFunctionHandler(
221
+ self.executor
222
+ ),
223
+ CallbackSuccessRoute: SendDurableExecutionCallbackSuccessHandler(
224
+ self.executor
225
+ ),
226
+ CallbackFailureRoute: SendDurableExecutionCallbackFailureHandler(
227
+ self.executor
228
+ ),
229
+ CallbackHeartbeatRoute: SendDurableExecutionCallbackHeartbeatHandler(
230
+ self.executor
231
+ ),
232
+ HealthRoute: HealthHandler(self.executor),
233
+ UpdateLambdaEndpointRoute: UpdateLambdaEndpointHandler(self.executor),
234
+ MetricsRoute: MetricsHandler(self.executor),
235
+ }
236
+
237
+ def __enter__(self) -> Self:
238
+ """Context manager entry."""
239
+ return self
240
+
241
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
242
+ """Context manager exit - cleanup server resources."""
243
+ self.server_close()