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,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()
|