aws-durable-execution-sdk-python 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 (36) hide show
  1. aws_durable_execution_sdk_python/.gitignore +0 -0
  2. aws_durable_execution_sdk_python/__about__.py +4 -0
  3. aws_durable_execution_sdk_python/__init__.py +34 -0
  4. aws_durable_execution_sdk_python/botocore/data/lambdainternal/2015-03-31/service-2.json +7864 -0
  5. aws_durable_execution_sdk_python/concurrency/__init__.py +0 -0
  6. aws_durable_execution_sdk_python/concurrency/executor.py +436 -0
  7. aws_durable_execution_sdk_python/concurrency/models.py +469 -0
  8. aws_durable_execution_sdk_python/config.py +499 -0
  9. aws_durable_execution_sdk_python/context.py +551 -0
  10. aws_durable_execution_sdk_python/exceptions.py +374 -0
  11. aws_durable_execution_sdk_python/execution.py +428 -0
  12. aws_durable_execution_sdk_python/identifier.py +14 -0
  13. aws_durable_execution_sdk_python/lambda_service.py +1034 -0
  14. aws_durable_execution_sdk_python/logger.py +131 -0
  15. aws_durable_execution_sdk_python/operation/__init__.py +1 -0
  16. aws_durable_execution_sdk_python/operation/callback.py +123 -0
  17. aws_durable_execution_sdk_python/operation/child.py +162 -0
  18. aws_durable_execution_sdk_python/operation/invoke.py +119 -0
  19. aws_durable_execution_sdk_python/operation/map.py +137 -0
  20. aws_durable_execution_sdk_python/operation/parallel.py +122 -0
  21. aws_durable_execution_sdk_python/operation/step.py +269 -0
  22. aws_durable_execution_sdk_python/operation/wait.py +53 -0
  23. aws_durable_execution_sdk_python/operation/wait_for_condition.py +235 -0
  24. aws_durable_execution_sdk_python/py.typed +1 -0
  25. aws_durable_execution_sdk_python/retries.py +174 -0
  26. aws_durable_execution_sdk_python/serdes.py +502 -0
  27. aws_durable_execution_sdk_python/state.py +790 -0
  28. aws_durable_execution_sdk_python/suspend.py +84 -0
  29. aws_durable_execution_sdk_python/threading.py +222 -0
  30. aws_durable_execution_sdk_python/types.py +180 -0
  31. aws_durable_execution_sdk_python/waits.py +130 -0
  32. aws_durable_execution_sdk_python-1.0.0.dist-info/METADATA +679 -0
  33. aws_durable_execution_sdk_python-1.0.0.dist-info/RECORD +36 -0
  34. aws_durable_execution_sdk_python-1.0.0.dist-info/WHEEL +4 -0
  35. aws_durable_execution_sdk_python-1.0.0.dist-info/licenses/LICENSE +175 -0
  36. aws_durable_execution_sdk_python-1.0.0.dist-info/licenses/NOTICE +1 -0
@@ -0,0 +1,374 @@
1
+ """Exceptions for the Durable Executions SDK.
2
+
3
+ Avoid any non-stdlib references in this module, it is at the bottom of the dependency chain.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import time
9
+ from dataclasses import dataclass
10
+ from enum import Enum
11
+ from typing import TYPE_CHECKING, Self, TypedDict
12
+
13
+ BAD_REQUEST_ERROR: int = 400
14
+ TOO_MANY_REQUESTS_ERROR: int = 429
15
+ SERVICE_ERROR: int = 500
16
+
17
+ if TYPE_CHECKING:
18
+ import datetime
19
+
20
+
21
+ class AwsErrorObj(TypedDict):
22
+ Code: str | None
23
+ Message: str | None
24
+
25
+
26
+ class AwsErrorMetadata(TypedDict):
27
+ RequestId: str | None
28
+ HostId: str | None
29
+ HTTPStatusCode: int | None
30
+ HTTPHeaders: str | None
31
+ RetryAttempts: str | None
32
+
33
+
34
+ class TerminationReason(Enum):
35
+ """Reasons why a durable execution terminated."""
36
+
37
+ UNHANDLED_ERROR = "UNHANDLED_ERROR"
38
+ INVOCATION_ERROR = "INVOCATION_ERROR"
39
+ EXECUTION_ERROR = "EXECUTION_ERROR"
40
+ CHECKPOINT_FAILED = "CHECKPOINT_FAILED"
41
+ NON_DETERMINISTIC_EXECUTION = "NON_DETERMINISTIC_EXECUTION"
42
+ STEP_INTERRUPTED = "STEP_INTERRUPTED"
43
+ CALLBACK_ERROR = "CALLBACK_ERROR"
44
+ SERIALIZATION_ERROR = "SERIALIZATION_ERROR"
45
+
46
+
47
+ class DurableExecutionsError(Exception):
48
+ """Base class for Durable Executions exceptions"""
49
+
50
+
51
+ class UnrecoverableError(DurableExecutionsError):
52
+ """Base class for errors that terminate execution."""
53
+
54
+ def __init__(self, message: str, termination_reason: TerminationReason):
55
+ super().__init__(message)
56
+ self.termination_reason = termination_reason
57
+
58
+
59
+ class ExecutionError(UnrecoverableError):
60
+ """Error that returns FAILED status without retry."""
61
+
62
+ def __init__(
63
+ self,
64
+ message: str,
65
+ termination_reason: TerminationReason = TerminationReason.EXECUTION_ERROR,
66
+ ):
67
+ super().__init__(message, termination_reason)
68
+
69
+
70
+ class InvocationError(UnrecoverableError):
71
+ """Error that should cause Lambda retry by throwing from handler."""
72
+
73
+ def __init__(
74
+ self,
75
+ message: str,
76
+ termination_reason: TerminationReason = TerminationReason.INVOCATION_ERROR,
77
+ ):
78
+ super().__init__(message, termination_reason)
79
+
80
+
81
+ class CallbackError(ExecutionError):
82
+ """Error in callback handling."""
83
+
84
+ def __init__(self, message: str, callback_id: str | None = None):
85
+ super().__init__(message, TerminationReason.CALLBACK_ERROR)
86
+ self.callback_id = callback_id
87
+
88
+
89
+ class BotoClientError(InvocationError):
90
+ def __init__(
91
+ self,
92
+ message: str,
93
+ error: AwsErrorObj | None = None,
94
+ response_metadata: AwsErrorMetadata | None = None,
95
+ termination_reason=TerminationReason.INVOCATION_ERROR,
96
+ ):
97
+ super().__init__(message=message, termination_reason=termination_reason)
98
+ self.error: AwsErrorObj | None = error
99
+ self.response_metadata: AwsErrorMetadata | None = response_metadata
100
+
101
+ @classmethod
102
+ def from_exception(cls, exception: Exception) -> Self:
103
+ response = getattr(exception, "response", {})
104
+ response_metadata = response.get("ResponseMetadata")
105
+ error = response.get("Error")
106
+ return cls(
107
+ message=str(exception), error=error, response_metadata=response_metadata
108
+ )
109
+
110
+ def build_logger_extras(self) -> dict:
111
+ extras: dict = {}
112
+ # preserve PascalCase to be consistent with other langauges
113
+ if error := self.error:
114
+ extras["Error"] = error
115
+ if response_metadata := self.response_metadata:
116
+ extras["ResponseMetadata"] = response_metadata
117
+ return extras
118
+
119
+
120
+ class NonDeterministicExecutionError(ExecutionError):
121
+ """Error when execution is non-deterministic."""
122
+
123
+ def __init__(self, message: str, step_id: str | None = None):
124
+ super().__init__(message, TerminationReason.NON_DETERMINISTIC_EXECUTION)
125
+ self.step_id = step_id
126
+
127
+
128
+ class CheckpointErrorCategory(Enum):
129
+ INVOCATION = "INVOCATION"
130
+ EXECUTION = "EXECUTION"
131
+
132
+
133
+ class CheckpointError(BotoClientError):
134
+ """Failure to checkpoint. Will terminate the lambda."""
135
+
136
+ def __init__(
137
+ self,
138
+ message: str,
139
+ error_category: CheckpointErrorCategory,
140
+ error: AwsErrorObj | None = None,
141
+ response_metadata: AwsErrorMetadata | None = None,
142
+ ):
143
+ super().__init__(
144
+ message,
145
+ error,
146
+ response_metadata,
147
+ termination_reason=TerminationReason.CHECKPOINT_FAILED,
148
+ )
149
+ self.error_category: CheckpointErrorCategory = error_category
150
+
151
+ @classmethod
152
+ def from_exception(cls, exception: Exception) -> CheckpointError:
153
+ base = BotoClientError.from_exception(exception)
154
+ metadata: AwsErrorMetadata | None = base.response_metadata
155
+ error: AwsErrorObj | None = base.error
156
+ error_category: CheckpointErrorCategory = CheckpointErrorCategory.INVOCATION
157
+
158
+ # InvalidParameterValueException and error message starts with "Invalid Checkpoint Token" is an InvocationError
159
+ # all other 4xx errors are Execution Errors and should be retried
160
+ # all 5xx errors are Invocation Errors
161
+ status_code: int | None = (metadata and metadata.get("HTTPStatusCode")) or None
162
+ if (
163
+ status_code
164
+ # if we are in 4xx range (except 429) and is not an InvalidParameterValueException with Invalid Checkpoint Token
165
+ # then it's an execution error
166
+ and status_code < SERVICE_ERROR
167
+ and status_code >= BAD_REQUEST_ERROR
168
+ and status_code != TOO_MANY_REQUESTS_ERROR
169
+ and error
170
+ and (
171
+ # is not InvalidParam => Execution
172
+ (error.get("Code", "") or "") != "InvalidParameterValueException"
173
+ # is not Invalid Token => Execution
174
+ or not (error.get("Message") or "").startswith(
175
+ "Invalid Checkpoint Token"
176
+ )
177
+ )
178
+ ):
179
+ error_category = CheckpointErrorCategory.EXECUTION
180
+ return CheckpointError(str(exception), error_category, error, metadata)
181
+
182
+ def is_retriable(self):
183
+ return self.error_category == CheckpointErrorCategory.EXECUTION
184
+
185
+
186
+ class ValidationError(DurableExecutionsError):
187
+ """Incorrect arguments to a Durable Function operation."""
188
+
189
+
190
+ class GetExecutionStateError(BotoClientError):
191
+ """Raised when failing to retrieve execution state"""
192
+
193
+ def __init__(
194
+ self,
195
+ message: str,
196
+ error: AwsErrorObj | None = None,
197
+ response_metadata: AwsErrorMetadata | None = None,
198
+ ):
199
+ super().__init__(
200
+ message,
201
+ error,
202
+ response_metadata,
203
+ termination_reason=TerminationReason.INVOCATION_ERROR,
204
+ )
205
+
206
+
207
+ class InvalidStateError(DurableExecutionsError):
208
+ """Raised when an operation is attempted on an object in an invalid state."""
209
+
210
+
211
+ class UserlandError(DurableExecutionsError):
212
+ """Failure in user-land - i.e code passed into durable executions from the caller."""
213
+
214
+
215
+ class CallableRuntimeError(UserlandError):
216
+ """This error wraps any failure from inside the callable code that you pass to a Durable Function operation."""
217
+
218
+ def __init__(
219
+ self,
220
+ message: str | None,
221
+ error_type: str | None,
222
+ data: str | None,
223
+ stack_trace: list[str] | None,
224
+ ) -> None:
225
+ super().__init__(message)
226
+ self.message = message
227
+ self.error_type = error_type
228
+ self.data = data
229
+ self.stack_trace = stack_trace
230
+
231
+
232
+ class StepInterruptedError(InvocationError):
233
+ """Raised when a step is interrupted before it checkpointed at the end."""
234
+
235
+ def __init__(self, message: str, step_id: str | None = None):
236
+ super().__init__(message, TerminationReason.STEP_INTERRUPTED)
237
+ self.step_id = step_id
238
+
239
+
240
+ class BackgroundThreadError(BaseException):
241
+ """Critical error from background checkpoint thread.
242
+
243
+ Derives from BaseException to bypass normal exception handlers.
244
+ Similar to KeyboardInterrupt or SystemExit - this is a system-level
245
+ error that should terminate execution immediately without attempting
246
+ to checkpoint or process the error.
247
+
248
+ This exception is raised in the user thread when the background
249
+ checkpoint processing thread encounters a fatal error. It propagates
250
+ through CompletionEvent.wait() to interrupt blocked user code.
251
+
252
+ Attributes:
253
+ source_exception: The original exception from the background thread
254
+ """
255
+
256
+ def __init__(self, message: str, source_exception: Exception):
257
+ super().__init__(message)
258
+ self.source_exception = source_exception
259
+
260
+
261
+ class SuspendExecution(BaseException):
262
+ """Raise this exception to suspend the current execution by returning PENDING to DAR.
263
+
264
+ Note this derives from BaseException - in keeping with system-exiting exceptions like
265
+ KeyboardInterrupt or SystemExit.
266
+ """
267
+
268
+ def __init__(self, message: str):
269
+ super().__init__(message)
270
+
271
+
272
+ class TimedSuspendExecution(SuspendExecution):
273
+ """Suspend execution until a specific timestamp.
274
+
275
+ This is a specialized form of SuspendExecution that includes a scheduled resume time.
276
+
277
+ Attributes:
278
+ scheduled_timestamp (float): Unix timestamp in seconds at which to resume.
279
+ """
280
+
281
+ def __init__(self, message: str, scheduled_timestamp: float):
282
+ super().__init__(message)
283
+ self.scheduled_timestamp = scheduled_timestamp
284
+
285
+ @classmethod
286
+ def from_delay(cls, message: str, delay_seconds: int) -> TimedSuspendExecution:
287
+ """Create a timed suspension with the delay calculated from now.
288
+
289
+ Args:
290
+ message: Descriptive message for the suspension
291
+ delay_seconds: Duration to suspend in seconds from current time
292
+
293
+ Returns:
294
+ TimedSuspendExecution: Instance with calculated resume time
295
+
296
+ Example:
297
+ >>> exception = TimedSuspendExecution.from_delay("Waiting for callback", 30)
298
+ >>> # Will suspend for 30 seconds from now
299
+ """
300
+ resume_time = time.time() + delay_seconds
301
+ return cls(message, scheduled_timestamp=resume_time)
302
+
303
+ @classmethod
304
+ def from_datetime(
305
+ cls, message: str, datetime_timestamp: datetime.datetime
306
+ ) -> TimedSuspendExecution:
307
+ """Create a timed suspension with the delay calculated from now.
308
+
309
+ Args:
310
+ message: Descriptive message for the suspension
311
+ datetime_timestamp: Unix datetime timestamp in seconds at which to resume
312
+
313
+ Returns:
314
+ TimedSuspendExecution: Instance with calculated resume time
315
+ """
316
+ return cls(message, scheduled_timestamp=datetime_timestamp.timestamp())
317
+
318
+
319
+ class OrderedLockError(DurableExecutionsError):
320
+ """An error from OrderedLock.
321
+
322
+ Typically raised when a previous lock in the sequentially ordered chain of lock acquire requests failed.
323
+
324
+ Because of the order guarantee of OrderedLock, subsequent queued up lock acquire requests cannot proceed,
325
+ and will get this error instead.
326
+
327
+ Attributes:
328
+ source_exception (Exception): The exception that caused the lock to break.
329
+ """
330
+
331
+ def __init__(self, message: str, source_exception: Exception | None = None) -> None:
332
+ """Initialize with the message and the exception source"""
333
+ msg = (
334
+ f"{message} {type(source_exception).__name__}: {source_exception}"
335
+ if source_exception
336
+ else message
337
+ )
338
+ super().__init__(msg)
339
+ self.source_exception: Exception | None = source_exception
340
+
341
+
342
+ @dataclass(frozen=True)
343
+ class CallableRuntimeErrorSerializableDetails:
344
+ """Serializable error details."""
345
+
346
+ type: str
347
+ message: str
348
+
349
+ @classmethod
350
+ def from_exception(
351
+ cls, exception: Exception
352
+ ) -> CallableRuntimeErrorSerializableDetails:
353
+ """Create an instance from an Exception, using its type and message.
354
+
355
+ Args:
356
+ exception: An Exception instance
357
+
358
+ Returns:
359
+ A CallableRuntimeErrorDetails instance with the exception's type name and message
360
+ """
361
+ return cls(type=exception.__class__.__name__, message=str(exception))
362
+
363
+ def __str__(self) -> str:
364
+ """
365
+ Return a string representation of the object.
366
+
367
+ Returns:
368
+ A string in the format "type: message"
369
+ """
370
+ return f"{self.type}: {self.message}"
371
+
372
+
373
+ class SerDesError(DurableExecutionsError):
374
+ """Raised when serialization fails."""