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,327 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from threading import Lock
6
+ from typing import TYPE_CHECKING, Any, Protocol
7
+ from uuid import uuid4
8
+
9
+ import boto3 # type: ignore
10
+ from aws_durable_execution_sdk_python.execution import (
11
+ DurableExecutionInvocationInput,
12
+ DurableExecutionInvocationInputWithClient,
13
+ DurableExecutionInvocationOutput,
14
+ InitialExecutionState,
15
+ InvocationStatus,
16
+ )
17
+
18
+ from aws_durable_execution_sdk_python_testing.exceptions import (
19
+ DurableFunctionsTestError,
20
+ ServiceException,
21
+ )
22
+ from aws_durable_execution_sdk_python_testing.model import LambdaContext
23
+
24
+ if TYPE_CHECKING:
25
+ from collections.abc import Callable
26
+
27
+ from aws_durable_execution_sdk_python_testing.client import InMemoryServiceClient
28
+ from aws_durable_execution_sdk_python_testing.execution import Execution
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class InvokeResponse:
33
+ """Response from invoking a durable function."""
34
+
35
+ invocation_output: DurableExecutionInvocationOutput
36
+ request_id: str
37
+
38
+
39
+ def create_test_lambda_context() -> LambdaContext:
40
+ # Create client context as a dictionary, not as objects
41
+ # LambdaContext.__init__ expects dictionaries and will create the objects internally
42
+ client_context_dict = {
43
+ "custom": {"test_key": "test_value"},
44
+ "env": {"platform": "test", "make": "test", "model": "test"},
45
+ "client": {
46
+ "installation_id": "test-installation-123",
47
+ "app_title": "TestApp",
48
+ "app_version_name": "1.0.0",
49
+ "app_version_code": "100",
50
+ "app_package_name": "com.test.app",
51
+ },
52
+ }
53
+
54
+ cognito_identity_dict = {
55
+ "cognitoIdentityId": "test-cognito-identity-123",
56
+ "cognitoIdentityPoolId": "us-west-2:test-pool-456",
57
+ }
58
+
59
+ return LambdaContext(
60
+ aws_request_id="test-invoke-12345",
61
+ client_context=client_context_dict,
62
+ identity=cognito_identity_dict,
63
+ invoked_function_arn="arn:aws:lambda:us-west-2:123456789012:function:test-function",
64
+ tenant_id="test-tenant-789",
65
+ )
66
+
67
+
68
+ class Invoker(Protocol):
69
+ def create_invocation_input(
70
+ self, execution: Execution
71
+ ) -> DurableExecutionInvocationInput: ... # pragma: no cover
72
+
73
+ def invoke(
74
+ self,
75
+ function_name: str,
76
+ input: DurableExecutionInvocationInput,
77
+ endpoint_url: str | None = None,
78
+ ) -> InvokeResponse: ... # pragma: no cover
79
+
80
+ def update_endpoint(
81
+ self, endpoint_url: str, region_name: str
82
+ ) -> None: ... # pragma: no cover
83
+
84
+
85
+ class InProcessInvoker(Invoker):
86
+ def __init__(self, handler: Callable, service_client: InMemoryServiceClient):
87
+ self.handler = handler
88
+ self.service_client = service_client
89
+
90
+ def create_invocation_input(
91
+ self, execution: Execution
92
+ ) -> DurableExecutionInvocationInput:
93
+ return DurableExecutionInvocationInputWithClient(
94
+ durable_execution_arn=execution.durable_execution_arn,
95
+ # TODO: this needs better logic - use existing if not used yet, vs create new
96
+ checkpoint_token=execution.get_new_checkpoint_token(),
97
+ initial_execution_state=InitialExecutionState(
98
+ operations=execution.operations,
99
+ next_marker="",
100
+ ),
101
+ service_client=self.service_client,
102
+ )
103
+
104
+ def invoke(
105
+ self,
106
+ function_name: str, # noqa: ARG002
107
+ input: DurableExecutionInvocationInput,
108
+ endpoint_url: str | None = None, # noqa: ARG002
109
+ ) -> InvokeResponse:
110
+ # TODO: reasses if function_name will be used in future
111
+ input_with_client = DurableExecutionInvocationInputWithClient.from_durable_execution_invocation_input(
112
+ input, self.service_client
113
+ )
114
+ context = create_test_lambda_context()
115
+ response_dict = self.handler(input_with_client, context)
116
+ output = DurableExecutionInvocationOutput.from_dict(response_dict)
117
+ return InvokeResponse(
118
+ invocation_output=output, request_id=context.aws_request_id
119
+ )
120
+
121
+ def update_endpoint(self, endpoint_url: str, region_name: str) -> None:
122
+ """No-op for in-process invoker."""
123
+
124
+
125
+ class LambdaInvoker(Invoker):
126
+ def __init__(self, lambda_client: Any) -> None:
127
+ self.lambda_client = lambda_client
128
+ # Maps execution_arn -> endpoint for that execution
129
+ # Maps endpoint -> client to reuse clients across executions
130
+ self._execution_endpoints: dict[str, str] = {}
131
+ self._endpoint_clients: dict[str, Any] = {}
132
+ self._current_endpoint: str = "" # Track current endpoint for new executions
133
+ self._lock = Lock()
134
+
135
+ @staticmethod
136
+ def create(endpoint_url: str, region_name: str) -> LambdaInvoker:
137
+ """Create with the boto lambda client."""
138
+ invoker = LambdaInvoker(
139
+ boto3.client(
140
+ "lambdainternal", endpoint_url=endpoint_url, region_name=region_name
141
+ )
142
+ )
143
+ invoker._current_endpoint = endpoint_url
144
+ invoker._endpoint_clients[endpoint_url] = invoker.lambda_client
145
+ return invoker
146
+
147
+ def update_endpoint(self, endpoint_url: str, region_name: str) -> None:
148
+ """Update the Lambda client endpoint."""
149
+ # Cache client by endpoint to reuse across executions
150
+ with self._lock:
151
+ if endpoint_url not in self._endpoint_clients:
152
+ self._endpoint_clients[endpoint_url] = boto3.client(
153
+ "lambdainternal", endpoint_url=endpoint_url, region_name=region_name
154
+ )
155
+ self.lambda_client = self._endpoint_clients[endpoint_url]
156
+ self._current_endpoint = endpoint_url
157
+
158
+ def _get_client_for_execution(
159
+ self,
160
+ durable_execution_arn: str,
161
+ lambda_endpoint: str | None = None,
162
+ region_name: str | None = None,
163
+ ) -> Any:
164
+ """Get the appropriate client for this execution."""
165
+ # Use provided endpoint or fall back to cached endpoint for this execution
166
+ if lambda_endpoint:
167
+ if lambda_endpoint not in self._endpoint_clients:
168
+ self._endpoint_clients[lambda_endpoint] = boto3.client(
169
+ "lambdainternal",
170
+ endpoint_url=lambda_endpoint,
171
+ region_name=region_name or "us-east-1",
172
+ )
173
+ return self._endpoint_clients[lambda_endpoint]
174
+
175
+ # Fallback to cached endpoint
176
+ if durable_execution_arn not in self._execution_endpoints:
177
+ with self._lock:
178
+ if durable_execution_arn not in self._execution_endpoints:
179
+ self._execution_endpoints[durable_execution_arn] = (
180
+ self._current_endpoint
181
+ )
182
+
183
+ endpoint = self._execution_endpoints[durable_execution_arn]
184
+
185
+ # If no endpoint configured, fall back to default client
186
+ if not endpoint:
187
+ return self.lambda_client
188
+
189
+ return self._endpoint_clients[endpoint]
190
+
191
+ def create_invocation_input(
192
+ self, execution: Execution
193
+ ) -> DurableExecutionInvocationInput:
194
+ return DurableExecutionInvocationInput(
195
+ durable_execution_arn=execution.durable_execution_arn,
196
+ checkpoint_token=execution.get_new_checkpoint_token(),
197
+ initial_execution_state=InitialExecutionState(
198
+ operations=execution.operations,
199
+ next_marker="",
200
+ ),
201
+ )
202
+
203
+ def invoke(
204
+ self,
205
+ function_name: str,
206
+ input: DurableExecutionInvocationInput,
207
+ endpoint_url: str | None = None,
208
+ ) -> InvokeResponse:
209
+ """Invoke AWS Lambda function and return durable execution result.
210
+
211
+ Args:
212
+ function_name: Name of the Lambda function to invoke
213
+ input: Durable execution invocation input
214
+ endpoint_url: Lambda endpoint url
215
+
216
+ Returns:
217
+ InvokeResponse: Response containing invocation output and request ID
218
+
219
+ Raises:
220
+ ResourceNotFoundException: If function does not exist
221
+ InvalidParameterValueException: If parameters are invalid
222
+ DurableFunctionsTestError: For other invocation failures
223
+ """
224
+ from aws_durable_execution_sdk_python_testing.exceptions import (
225
+ ResourceNotFoundException,
226
+ InvalidParameterValueException,
227
+ )
228
+
229
+ # Parameter validation
230
+ if not function_name or not function_name.strip():
231
+ msg = "Function name is required"
232
+ raise InvalidParameterValueException(msg)
233
+
234
+ # Get the client for this execution
235
+ client = self._get_client_for_execution(
236
+ input.durable_execution_arn, endpoint_url
237
+ )
238
+
239
+ try:
240
+ # Invoke AWS Lambda function using standard invoke method
241
+ response = client.invoke(
242
+ FunctionName=function_name,
243
+ InvocationType="RequestResponse", # Synchronous invocation
244
+ Payload=json.dumps(input.to_dict(), default=str),
245
+ )
246
+
247
+ # Check HTTP status code
248
+ status_code = response.get("StatusCode")
249
+ if status_code not in (200, 202, 204):
250
+ msg = f"Lambda invocation failed with status code: {status_code}"
251
+ raise DurableFunctionsTestError(msg)
252
+
253
+ # Check for function errors
254
+ if "FunctionError" in response:
255
+ error_payload = response["Payload"].read().decode("utf-8")
256
+ msg = f"Lambda invocation failed with status {status_code}: {error_payload}"
257
+ raise DurableFunctionsTestError(msg)
258
+
259
+ # Parse response payload
260
+ response_payload = response["Payload"].read().decode("utf-8")
261
+ response_dict = json.loads(response_payload)
262
+
263
+ # Extract request ID from response headers (x-amzn-RequestId or x-amzn-request-id)
264
+ headers = response.get("ResponseMetadata", {}).get("HTTPHeaders", {})
265
+ request_id = (
266
+ headers.get("x-amzn-RequestId")
267
+ or headers.get("x-amzn-request-id")
268
+ or f"local-{uuid4()}"
269
+ )
270
+
271
+ # Convert to DurableExecutionInvocationOutput
272
+ output = DurableExecutionInvocationOutput.from_dict(response_dict)
273
+ return InvokeResponse(invocation_output=output, request_id=request_id)
274
+
275
+ except client.exceptions.ResourceNotFoundException as e:
276
+ msg = f"Function not found: {function_name}"
277
+ raise ResourceNotFoundException(msg) from e
278
+ except client.exceptions.InvalidParameterValueException as e:
279
+ msg = f"Invalid parameter: {e}"
280
+ raise InvalidParameterValueException(msg) from e
281
+ except (
282
+ client.exceptions.TooManyRequestsException,
283
+ client.exceptions.ServiceException,
284
+ client.exceptions.ResourceConflictException,
285
+ client.exceptions.InvalidRequestContentException,
286
+ client.exceptions.RequestTooLargeException,
287
+ client.exceptions.UnsupportedMediaTypeException,
288
+ client.exceptions.InvalidRuntimeException,
289
+ client.exceptions.InvalidZipFileException,
290
+ client.exceptions.ResourceNotReadyException,
291
+ client.exceptions.SnapStartTimeoutException,
292
+ client.exceptions.SnapStartNotReadyException,
293
+ client.exceptions.SnapStartException,
294
+ client.exceptions.RecursiveInvocationException,
295
+ ) as e:
296
+ msg = f"Lambda invocation failed: {e}"
297
+ raise DurableFunctionsTestError(msg) from e
298
+ except (
299
+ client.exceptions.InvalidSecurityGroupIDException,
300
+ client.exceptions.EC2ThrottledException,
301
+ client.exceptions.EFSMountConnectivityException,
302
+ client.exceptions.SubnetIPAddressLimitReachedException,
303
+ client.exceptions.EC2UnexpectedException,
304
+ client.exceptions.InvalidSubnetIDException,
305
+ client.exceptions.EC2AccessDeniedException,
306
+ client.exceptions.EFSIOException,
307
+ client.exceptions.ENILimitReachedException,
308
+ client.exceptions.EFSMountTimeoutException,
309
+ client.exceptions.EFSMountFailureException,
310
+ ) as e:
311
+ msg = f"Lambda infrastructure error: {e}"
312
+ raise DurableFunctionsTestError(msg) from e
313
+ except (
314
+ client.exceptions.KMSAccessDeniedException,
315
+ client.exceptions.KMSDisabledException,
316
+ client.exceptions.KMSNotFoundException,
317
+ client.exceptions.KMSInvalidStateException,
318
+ ) as e:
319
+ msg = f"Lambda KMS error: {e}"
320
+ raise DurableFunctionsTestError(msg) from e
321
+ except Exception as e:
322
+ # Handle any remaining exceptions, including custom ones like DurableExecutionAlreadyStartedException
323
+ if "DurableExecutionAlreadyStartedException" in str(type(e)):
324
+ msg = f"Durable execution already started: {e}"
325
+ raise DurableFunctionsTestError(msg) from e
326
+ msg = f"Unexpected error during Lambda invocation: {e}"
327
+ raise DurableFunctionsTestError(msg) from e