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.
- aws_durable_execution_sdk_python/.gitignore +0 -0
- aws_durable_execution_sdk_python/__about__.py +4 -0
- aws_durable_execution_sdk_python/__init__.py +34 -0
- aws_durable_execution_sdk_python/botocore/data/lambdainternal/2015-03-31/service-2.json +7864 -0
- aws_durable_execution_sdk_python/concurrency/__init__.py +0 -0
- aws_durable_execution_sdk_python/concurrency/executor.py +436 -0
- aws_durable_execution_sdk_python/concurrency/models.py +469 -0
- aws_durable_execution_sdk_python/config.py +499 -0
- aws_durable_execution_sdk_python/context.py +551 -0
- aws_durable_execution_sdk_python/exceptions.py +374 -0
- aws_durable_execution_sdk_python/execution.py +428 -0
- aws_durable_execution_sdk_python/identifier.py +14 -0
- aws_durable_execution_sdk_python/lambda_service.py +1034 -0
- aws_durable_execution_sdk_python/logger.py +131 -0
- aws_durable_execution_sdk_python/operation/__init__.py +1 -0
- aws_durable_execution_sdk_python/operation/callback.py +123 -0
- aws_durable_execution_sdk_python/operation/child.py +162 -0
- aws_durable_execution_sdk_python/operation/invoke.py +119 -0
- aws_durable_execution_sdk_python/operation/map.py +137 -0
- aws_durable_execution_sdk_python/operation/parallel.py +122 -0
- aws_durable_execution_sdk_python/operation/step.py +269 -0
- aws_durable_execution_sdk_python/operation/wait.py +53 -0
- aws_durable_execution_sdk_python/operation/wait_for_condition.py +235 -0
- aws_durable_execution_sdk_python/py.typed +1 -0
- aws_durable_execution_sdk_python/retries.py +174 -0
- aws_durable_execution_sdk_python/serdes.py +502 -0
- aws_durable_execution_sdk_python/state.py +790 -0
- aws_durable_execution_sdk_python/suspend.py +84 -0
- aws_durable_execution_sdk_python/threading.py +222 -0
- aws_durable_execution_sdk_python/types.py +180 -0
- aws_durable_execution_sdk_python/waits.py +130 -0
- aws_durable_execution_sdk_python-1.0.0.dist-info/METADATA +679 -0
- aws_durable_execution_sdk_python-1.0.0.dist-info/RECORD +36 -0
- aws_durable_execution_sdk_python-1.0.0.dist-info/WHEEL +4 -0
- aws_durable_execution_sdk_python-1.0.0.dist-info/licenses/LICENSE +175 -0
- aws_durable_execution_sdk_python-1.0.0.dist-info/licenses/NOTICE +1 -0
|
@@ -0,0 +1,1034 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Protocol, TypeAlias
|
|
10
|
+
|
|
11
|
+
import boto3 # type: ignore
|
|
12
|
+
from botocore.config import Config # type: ignore
|
|
13
|
+
|
|
14
|
+
from aws_durable_execution_sdk_python.exceptions import (
|
|
15
|
+
CallableRuntimeError,
|
|
16
|
+
CheckpointError,
|
|
17
|
+
GetExecutionStateError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import MutableMapping
|
|
22
|
+
|
|
23
|
+
from aws_durable_execution_sdk_python.identifier import OperationIdentifier
|
|
24
|
+
|
|
25
|
+
# Replace with `type` it when dropping support to Python 3.11
|
|
26
|
+
ReplayChildren: TypeAlias = bool
|
|
27
|
+
OperationPayload: TypeAlias = str
|
|
28
|
+
TimeoutSeconds: TypeAlias = int
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# region model
|
|
34
|
+
class OperationAction(Enum):
|
|
35
|
+
START = "START"
|
|
36
|
+
SUCCEED = "SUCCEED"
|
|
37
|
+
FAIL = "FAIL"
|
|
38
|
+
RETRY = "RETRY"
|
|
39
|
+
CANCEL = "CANCEL"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class OperationStatus(Enum):
|
|
43
|
+
STARTED = "STARTED"
|
|
44
|
+
PENDING = "PENDING"
|
|
45
|
+
READY = "READY"
|
|
46
|
+
SUCCEEDED = "SUCCEEDED"
|
|
47
|
+
FAILED = "FAILED"
|
|
48
|
+
CANCELLED = "CANCELLED"
|
|
49
|
+
TIMED_OUT = "TIMED_OUT"
|
|
50
|
+
STOPPED = "STOPPED"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class OperationType(Enum):
|
|
54
|
+
EXECUTION = "EXECUTION"
|
|
55
|
+
CONTEXT = "CONTEXT"
|
|
56
|
+
STEP = "STEP"
|
|
57
|
+
WAIT = "WAIT"
|
|
58
|
+
CALLBACK = "CALLBACK"
|
|
59
|
+
CHAINED_INVOKE = "CHAINED_INVOKE"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class CallbackTimeoutType(Enum):
|
|
63
|
+
TIMEOUT = "Callback.Timeout"
|
|
64
|
+
HEARTBEAT = "Callback.Heartbeat"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ChainedInvokeFailedToStartType(Enum):
|
|
68
|
+
FAILED_TO_START = "ChainedInvoke.FailedToStart"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ChainedInvokeTimeoutType(Enum):
|
|
72
|
+
TIMEOUT = "ChainedInvoke.Timeout"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ChainedInvokeStopType(Enum):
|
|
76
|
+
STOPPED = "ChainedInvoke.Stopped"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class OperationSubType(Enum):
|
|
80
|
+
STEP = "Step"
|
|
81
|
+
WAIT = "Wait"
|
|
82
|
+
CALLBACK = "Callback"
|
|
83
|
+
RUN_IN_CHILD_CONTEXT = "RunInChildContext"
|
|
84
|
+
MAP = "Map"
|
|
85
|
+
MAP_ITERATION = "MapIteration"
|
|
86
|
+
PARALLEL = "Parallel"
|
|
87
|
+
PARALLEL_BRANCH = "ParallelBranch"
|
|
88
|
+
WAIT_FOR_CALLBACK = "WaitForCallback"
|
|
89
|
+
WAIT_FOR_CONDITION = "WaitForCondition"
|
|
90
|
+
CHAINED_INVOKE = "ChainedInvoke"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class ExecutionDetails:
|
|
95
|
+
input_payload: str | None = None
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def from_dict(cls, data: MutableMapping[str, Any]) -> ExecutionDetails:
|
|
99
|
+
return cls(input_payload=data.get("InputPayload"))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class ContextDetails:
|
|
104
|
+
replay_children: ReplayChildren = False
|
|
105
|
+
result: OperationPayload | None = None
|
|
106
|
+
error: ErrorObject | None = None
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def from_dict(cls, data: MutableMapping[str, Any]) -> ContextDetails:
|
|
110
|
+
error_raw = data.get("Error")
|
|
111
|
+
return cls(
|
|
112
|
+
replay_children=data.get("ReplayChildren", False),
|
|
113
|
+
result=data.get("Result"),
|
|
114
|
+
error=ErrorObject.from_dict(error_raw) if error_raw else None,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True)
|
|
119
|
+
class ErrorObject:
|
|
120
|
+
message: str | None
|
|
121
|
+
type: str | None
|
|
122
|
+
data: str | None
|
|
123
|
+
stack_trace: list[str] | None
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def from_dict(cls, data: MutableMapping[str, Any]) -> ErrorObject:
|
|
127
|
+
return cls(
|
|
128
|
+
message=data.get("ErrorMessage"),
|
|
129
|
+
type=data.get("ErrorType"),
|
|
130
|
+
data=data.get("ErrorData"),
|
|
131
|
+
stack_trace=data.get("StackTrace"),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def from_exception(cls, exception: Exception) -> ErrorObject:
|
|
136
|
+
return cls(
|
|
137
|
+
message=str(exception),
|
|
138
|
+
type=type(exception).__name__,
|
|
139
|
+
data=None,
|
|
140
|
+
stack_trace=None,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def from_message(cls, message: str) -> ErrorObject:
|
|
145
|
+
return cls(
|
|
146
|
+
message=message,
|
|
147
|
+
type=None,
|
|
148
|
+
data=None,
|
|
149
|
+
stack_trace=None,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def to_dict(self) -> MutableMapping[str, Any]:
|
|
153
|
+
result: MutableMapping[str, Any] = {}
|
|
154
|
+
if self.message is not None:
|
|
155
|
+
result["ErrorMessage"] = self.message
|
|
156
|
+
if self.type is not None:
|
|
157
|
+
result["ErrorType"] = self.type
|
|
158
|
+
if self.data is not None:
|
|
159
|
+
result["ErrorData"] = self.data
|
|
160
|
+
if self.stack_trace is not None:
|
|
161
|
+
result["StackTrace"] = self.stack_trace
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
def to_callable_runtime_error(self) -> CallableRuntimeError:
|
|
165
|
+
return CallableRuntimeError(
|
|
166
|
+
message=self.message,
|
|
167
|
+
error_type=self.type,
|
|
168
|
+
data=self.data,
|
|
169
|
+
stack_trace=self.stack_trace,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass(frozen=True)
|
|
174
|
+
class StepDetails:
|
|
175
|
+
attempt: int = 0
|
|
176
|
+
next_attempt_timestamp: datetime.datetime | None = None
|
|
177
|
+
result: OperationPayload | None = None
|
|
178
|
+
error: ErrorObject | None = None
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def from_dict(cls, data: MutableMapping[str, Any]) -> StepDetails:
|
|
182
|
+
error_raw = data.get("Error")
|
|
183
|
+
return cls(
|
|
184
|
+
attempt=data.get("Attempt", 0),
|
|
185
|
+
next_attempt_timestamp=data.get("NextAttemptTimestamp"),
|
|
186
|
+
result=data.get("Result"),
|
|
187
|
+
error=ErrorObject.from_dict(error_raw) if error_raw else None,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass(frozen=True)
|
|
192
|
+
class WaitDetails:
|
|
193
|
+
scheduled_end_timestamp: datetime.datetime | None = None
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def from_dict(cls, data: MutableMapping[str, Any]) -> WaitDetails:
|
|
197
|
+
return cls(scheduled_end_timestamp=data.get("ScheduledEndTimestamp"))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@dataclass(frozen=True)
|
|
201
|
+
class CallbackDetails:
|
|
202
|
+
callback_id: str
|
|
203
|
+
result: str | None = None
|
|
204
|
+
error: ErrorObject | None = None
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def from_dict(cls, data: MutableMapping[str, Any]) -> CallbackDetails:
|
|
208
|
+
error_raw = data.get("Error")
|
|
209
|
+
return cls(
|
|
210
|
+
callback_id=data["CallbackId"],
|
|
211
|
+
result=data.get("Result"),
|
|
212
|
+
error=ErrorObject.from_dict(error_raw) if error_raw else None,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@dataclass(frozen=True)
|
|
217
|
+
class ChainedInvokeDetails:
|
|
218
|
+
result: str | None = None
|
|
219
|
+
error: ErrorObject | None = None
|
|
220
|
+
|
|
221
|
+
@classmethod
|
|
222
|
+
def from_dict(cls, data: MutableMapping[str, Any]) -> ChainedInvokeDetails:
|
|
223
|
+
error_raw = data.get("Error")
|
|
224
|
+
return cls(
|
|
225
|
+
result=data.get("Result"),
|
|
226
|
+
error=ErrorObject.from_dict(error_raw) if error_raw else None,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@dataclass(frozen=True)
|
|
231
|
+
class StepOptions:
|
|
232
|
+
next_attempt_delay_seconds: int = 0
|
|
233
|
+
|
|
234
|
+
@classmethod
|
|
235
|
+
def from_dict(cls, data: MutableMapping[str, Any]) -> StepOptions:
|
|
236
|
+
return cls(next_attempt_delay_seconds=data.get("NextAttemptDelaySeconds", 0))
|
|
237
|
+
|
|
238
|
+
def to_dict(self) -> MutableMapping[str, Any]:
|
|
239
|
+
return {
|
|
240
|
+
"NextAttemptDelaySeconds": self.next_attempt_delay_seconds,
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@dataclass(frozen=True)
|
|
245
|
+
class WaitOptions:
|
|
246
|
+
"""
|
|
247
|
+
Wait Options provides details regarding suspension.
|
|
248
|
+
|
|
249
|
+
As of 2025/10/27:
|
|
250
|
+
|
|
251
|
+
- `wait_seconds` accepts values between 1, and 31622400
|
|
252
|
+
- When wait_second seconds does not exist,then we default to 1
|
|
253
|
+
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
wait_seconds: int = 1
|
|
257
|
+
|
|
258
|
+
@classmethod
|
|
259
|
+
def from_dict(cls, data: MutableMapping[str, Any]) -> WaitOptions:
|
|
260
|
+
return cls(wait_seconds=data.get("WaitSeconds", 1))
|
|
261
|
+
|
|
262
|
+
def to_dict(self) -> MutableMapping[str, Any]:
|
|
263
|
+
return {"WaitSeconds": self.wait_seconds}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@dataclass(frozen=True)
|
|
267
|
+
class CallbackOptions:
|
|
268
|
+
"""
|
|
269
|
+
Callback options provides details about the callback, wrt timeout
|
|
270
|
+
and heartbeat checks.
|
|
271
|
+
|
|
272
|
+
As of 2025/10/27:
|
|
273
|
+
- When timeout_seconds == 0, then the callback has no timeout
|
|
274
|
+
- When heartbeat_timeout_seconds == 0, then the callback has no timeout
|
|
275
|
+
|
|
276
|
+
- When timeout_seconds is not present, then default is 0
|
|
277
|
+
- When heartbeat_timeout_seconds, then default is 0
|
|
278
|
+
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
timeout_seconds: TimeoutSeconds = 0
|
|
282
|
+
heartbeat_timeout_seconds: int = 0
|
|
283
|
+
|
|
284
|
+
@classmethod
|
|
285
|
+
def from_dict(cls, data: MutableMapping[str, Any]) -> CallbackOptions:
|
|
286
|
+
return cls(
|
|
287
|
+
timeout_seconds=data.get("TimeoutSeconds", 0),
|
|
288
|
+
heartbeat_timeout_seconds=data.get("HeartbeatTimeoutSeconds", 0),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
def to_dict(self) -> MutableMapping[str, Any]:
|
|
292
|
+
return {
|
|
293
|
+
"TimeoutSeconds": self.timeout_seconds,
|
|
294
|
+
"HeartbeatTimeoutSeconds": self.heartbeat_timeout_seconds,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@dataclass(frozen=True)
|
|
299
|
+
class ChainedInvokeOptions:
|
|
300
|
+
"""
|
|
301
|
+
As of 2025/10/27:
|
|
302
|
+
- Chained invoke options only contains a function name
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
function_name: str
|
|
306
|
+
tenant_id: str | None = None
|
|
307
|
+
|
|
308
|
+
@classmethod
|
|
309
|
+
def from_dict(cls, data: MutableMapping[str, Any]) -> ChainedInvokeOptions:
|
|
310
|
+
return cls(
|
|
311
|
+
function_name=data["FunctionName"],
|
|
312
|
+
tenant_id=data.get("TenantId"),
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def to_dict(self) -> MutableMapping[str, Any]:
|
|
316
|
+
result: MutableMapping[str, Any] = {
|
|
317
|
+
"FunctionName": self.function_name,
|
|
318
|
+
}
|
|
319
|
+
if self.tenant_id is not None:
|
|
320
|
+
result["TenantId"] = self.tenant_id
|
|
321
|
+
|
|
322
|
+
return result
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@dataclass(frozen=True)
|
|
326
|
+
class ContextOptions:
|
|
327
|
+
replay_children: ReplayChildren = False
|
|
328
|
+
|
|
329
|
+
@classmethod
|
|
330
|
+
def from_dict(cls, data: MutableMapping[str, Any]) -> ContextOptions:
|
|
331
|
+
return cls(replay_children=data.get("ReplayChildren", False))
|
|
332
|
+
|
|
333
|
+
def to_dict(self) -> MutableMapping[str, Any]:
|
|
334
|
+
return {"ReplayChildren": self.replay_children}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@dataclass(frozen=True)
|
|
338
|
+
class OperationUpdate:
|
|
339
|
+
"""Update an Operation. Use this to create a checkpoint.
|
|
340
|
+
|
|
341
|
+
See the various create_ factory class methods to instantiate me.
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
operation_id: str
|
|
345
|
+
operation_type: OperationType
|
|
346
|
+
action: OperationAction
|
|
347
|
+
parent_id: str | None = None
|
|
348
|
+
name: str | None = None
|
|
349
|
+
sub_type: OperationSubType | None = None
|
|
350
|
+
payload: str | None = None
|
|
351
|
+
error: ErrorObject | None = None
|
|
352
|
+
context_options: ContextOptions | None = None
|
|
353
|
+
step_options: StepOptions | None = None
|
|
354
|
+
wait_options: WaitOptions | None = None
|
|
355
|
+
callback_options: CallbackOptions | None = None
|
|
356
|
+
chained_invoke_options: ChainedInvokeOptions | None = None
|
|
357
|
+
|
|
358
|
+
def to_dict(self) -> MutableMapping[str, Any]:
|
|
359
|
+
result: MutableMapping[str, Any] = {
|
|
360
|
+
"Id": self.operation_id,
|
|
361
|
+
"Type": self.operation_type.value,
|
|
362
|
+
"Action": self.action.value,
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if self.parent_id:
|
|
366
|
+
result["ParentId"] = self.parent_id
|
|
367
|
+
if self.name:
|
|
368
|
+
result["Name"] = self.name
|
|
369
|
+
if self.sub_type:
|
|
370
|
+
result["SubType"] = self.sub_type.value
|
|
371
|
+
if self.payload:
|
|
372
|
+
result["Payload"] = self.payload
|
|
373
|
+
if self.error:
|
|
374
|
+
result["Error"] = self.error.to_dict()
|
|
375
|
+
if self.context_options:
|
|
376
|
+
result["ContextOptions"] = self.context_options.to_dict()
|
|
377
|
+
if self.step_options:
|
|
378
|
+
result["StepOptions"] = self.step_options.to_dict()
|
|
379
|
+
if self.wait_options:
|
|
380
|
+
result["WaitOptions"] = self.wait_options.to_dict()
|
|
381
|
+
if self.callback_options:
|
|
382
|
+
result["CallbackOptions"] = self.callback_options.to_dict()
|
|
383
|
+
if self.chained_invoke_options:
|
|
384
|
+
result["ChainedInvokeOptions"] = self.chained_invoke_options.to_dict()
|
|
385
|
+
|
|
386
|
+
return result
|
|
387
|
+
|
|
388
|
+
@classmethod
|
|
389
|
+
def from_dict(cls, data: MutableMapping[str, Any]) -> OperationUpdate:
|
|
390
|
+
"""Create OperationUpdate from dictionary data."""
|
|
391
|
+
error = ErrorObject.from_dict(data["Error"]) if data.get("Error") else None
|
|
392
|
+
|
|
393
|
+
context_options = None
|
|
394
|
+
if context_data := data.get("ContextOptions"):
|
|
395
|
+
context_options = ContextOptions.from_dict(context_data)
|
|
396
|
+
|
|
397
|
+
step_options = None
|
|
398
|
+
if step_data := data.get("StepOptions"):
|
|
399
|
+
step_options = StepOptions.from_dict(step_data)
|
|
400
|
+
|
|
401
|
+
wait_options = None
|
|
402
|
+
if wait_data := data.get("WaitOptions"):
|
|
403
|
+
wait_options = WaitOptions.from_dict(wait_data)
|
|
404
|
+
|
|
405
|
+
callback_options = None
|
|
406
|
+
if callback_data := data.get("CallbackOptions"):
|
|
407
|
+
callback_options = CallbackOptions.from_dict(callback_data)
|
|
408
|
+
|
|
409
|
+
chained_invoke_options = None
|
|
410
|
+
if invoke_data := data.get("ChainedInvokeOptions"):
|
|
411
|
+
chained_invoke_options = ChainedInvokeOptions.from_dict(invoke_data)
|
|
412
|
+
|
|
413
|
+
return cls(
|
|
414
|
+
operation_id=data["Id"],
|
|
415
|
+
operation_type=OperationType(data["Type"]),
|
|
416
|
+
action=OperationAction(data["Action"]),
|
|
417
|
+
parent_id=data.get("ParentId"),
|
|
418
|
+
name=data.get("Name"),
|
|
419
|
+
sub_type=OperationSubType(data["SubType"]) if data.get("SubType") else None,
|
|
420
|
+
payload=data.get("Payload"),
|
|
421
|
+
error=error,
|
|
422
|
+
context_options=context_options,
|
|
423
|
+
step_options=step_options,
|
|
424
|
+
wait_options=wait_options,
|
|
425
|
+
callback_options=callback_options,
|
|
426
|
+
chained_invoke_options=chained_invoke_options,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
@classmethod
|
|
430
|
+
def create_callback(
|
|
431
|
+
cls, identifier: OperationIdentifier, callback_options: CallbackOptions
|
|
432
|
+
) -> OperationUpdate:
|
|
433
|
+
"""Create an instance of OperationUpdate for type:CALLBACK, action:START"""
|
|
434
|
+
return cls(
|
|
435
|
+
operation_id=identifier.operation_id,
|
|
436
|
+
parent_id=identifier.parent_id,
|
|
437
|
+
operation_type=OperationType.CALLBACK,
|
|
438
|
+
sub_type=OperationSubType.CALLBACK,
|
|
439
|
+
action=OperationAction.START,
|
|
440
|
+
name=identifier.name,
|
|
441
|
+
callback_options=callback_options,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# region context
|
|
445
|
+
@classmethod
|
|
446
|
+
def create_context_start(
|
|
447
|
+
cls, identifier: OperationIdentifier, sub_type: OperationSubType
|
|
448
|
+
) -> OperationUpdate:
|
|
449
|
+
"""Create an instance of OperationUpdate for type: CONTEXT, action: START."""
|
|
450
|
+
return cls(
|
|
451
|
+
operation_id=identifier.operation_id,
|
|
452
|
+
parent_id=identifier.parent_id,
|
|
453
|
+
operation_type=OperationType.CONTEXT,
|
|
454
|
+
sub_type=sub_type,
|
|
455
|
+
action=OperationAction.START,
|
|
456
|
+
name=identifier.name,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
@classmethod
|
|
460
|
+
def create_context_succeed(
|
|
461
|
+
cls,
|
|
462
|
+
identifier: OperationIdentifier,
|
|
463
|
+
payload: str,
|
|
464
|
+
sub_type: OperationSubType,
|
|
465
|
+
context_options: ContextOptions | None = None,
|
|
466
|
+
) -> OperationUpdate:
|
|
467
|
+
"""Create an instance of OperationUpdate for type: CONTEXT, action: SUCCEED."""
|
|
468
|
+
return cls(
|
|
469
|
+
operation_id=identifier.operation_id,
|
|
470
|
+
parent_id=identifier.parent_id,
|
|
471
|
+
operation_type=OperationType.CONTEXT,
|
|
472
|
+
sub_type=sub_type,
|
|
473
|
+
action=OperationAction.SUCCEED,
|
|
474
|
+
name=identifier.name,
|
|
475
|
+
payload=payload,
|
|
476
|
+
context_options=context_options,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
@classmethod
|
|
480
|
+
def create_context_fail(
|
|
481
|
+
cls,
|
|
482
|
+
identifier: OperationIdentifier,
|
|
483
|
+
error: ErrorObject,
|
|
484
|
+
sub_type: OperationSubType,
|
|
485
|
+
) -> OperationUpdate:
|
|
486
|
+
"""Create an instance of OperationUpdate for type: CONTEXT, action: FAIL."""
|
|
487
|
+
return cls(
|
|
488
|
+
operation_id=identifier.operation_id,
|
|
489
|
+
parent_id=identifier.parent_id,
|
|
490
|
+
operation_type=OperationType.CONTEXT,
|
|
491
|
+
sub_type=sub_type,
|
|
492
|
+
action=OperationAction.FAIL,
|
|
493
|
+
name=identifier.name,
|
|
494
|
+
error=error,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
# endregion context
|
|
498
|
+
|
|
499
|
+
# region execution
|
|
500
|
+
@classmethod
|
|
501
|
+
def create_execution_succeed(cls, payload: str) -> OperationUpdate:
|
|
502
|
+
"""Create an instance of OperationUpdate for type: EXECUTION, action: SUCCEED."""
|
|
503
|
+
return cls(
|
|
504
|
+
operation_id=f"execution-result-{int(datetime.datetime.now(tz=datetime.UTC).timestamp() * 1000)}",
|
|
505
|
+
operation_type=OperationType.EXECUTION,
|
|
506
|
+
action=OperationAction.SUCCEED,
|
|
507
|
+
payload=payload,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
@classmethod
|
|
511
|
+
def create_execution_fail(cls, error: ErrorObject) -> OperationUpdate:
|
|
512
|
+
"""Create an instance of OperationUpdate for type: EXECUTION, action: FAIL."""
|
|
513
|
+
return cls(
|
|
514
|
+
operation_id=f"execution-result-{int(datetime.datetime.now(tz=datetime.UTC).timestamp() * 1000)}",
|
|
515
|
+
operation_type=OperationType.EXECUTION,
|
|
516
|
+
action=OperationAction.FAIL,
|
|
517
|
+
error=error,
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
# endregion execution
|
|
521
|
+
|
|
522
|
+
# region step
|
|
523
|
+
@classmethod
|
|
524
|
+
def create_step_succeed(
|
|
525
|
+
cls, identifier: OperationIdentifier, payload: str
|
|
526
|
+
) -> OperationUpdate:
|
|
527
|
+
"""Create an instance of OperationUpdate for type: STEP, action: SUCCEED."""
|
|
528
|
+
return cls(
|
|
529
|
+
operation_id=identifier.operation_id,
|
|
530
|
+
parent_id=identifier.parent_id,
|
|
531
|
+
operation_type=OperationType.STEP,
|
|
532
|
+
sub_type=OperationSubType.STEP,
|
|
533
|
+
action=OperationAction.SUCCEED,
|
|
534
|
+
name=identifier.name,
|
|
535
|
+
payload=payload,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
@classmethod
|
|
539
|
+
def create_step_fail(
|
|
540
|
+
cls, identifier: OperationIdentifier, error: ErrorObject
|
|
541
|
+
) -> OperationUpdate:
|
|
542
|
+
"""Create an instance of OperationUpdate for type: STEP, action: FAIL."""
|
|
543
|
+
return cls(
|
|
544
|
+
operation_id=identifier.operation_id,
|
|
545
|
+
parent_id=identifier.parent_id,
|
|
546
|
+
operation_type=OperationType.STEP,
|
|
547
|
+
sub_type=OperationSubType.STEP,
|
|
548
|
+
action=OperationAction.FAIL,
|
|
549
|
+
name=identifier.name,
|
|
550
|
+
error=error,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
@classmethod
|
|
554
|
+
def create_step_start(cls, identifier: OperationIdentifier) -> OperationUpdate:
|
|
555
|
+
"""Create an instance of OperationUpdate for type: STEP, action: START."""
|
|
556
|
+
return cls(
|
|
557
|
+
operation_id=identifier.operation_id,
|
|
558
|
+
parent_id=identifier.parent_id,
|
|
559
|
+
operation_type=OperationType.STEP,
|
|
560
|
+
sub_type=OperationSubType.STEP,
|
|
561
|
+
action=OperationAction.START,
|
|
562
|
+
name=identifier.name,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
@classmethod
|
|
566
|
+
def create_step_retry(
|
|
567
|
+
cls,
|
|
568
|
+
identifier: OperationIdentifier,
|
|
569
|
+
error: ErrorObject,
|
|
570
|
+
next_attempt_delay_seconds: int,
|
|
571
|
+
) -> OperationUpdate:
|
|
572
|
+
"""Create an instance of OperationUpdate for type: STEP, action: RETRY."""
|
|
573
|
+
return cls(
|
|
574
|
+
operation_id=identifier.operation_id,
|
|
575
|
+
parent_id=identifier.parent_id,
|
|
576
|
+
operation_type=OperationType.STEP,
|
|
577
|
+
sub_type=OperationSubType.STEP,
|
|
578
|
+
action=OperationAction.RETRY,
|
|
579
|
+
name=identifier.name,
|
|
580
|
+
error=error,
|
|
581
|
+
step_options=StepOptions(
|
|
582
|
+
next_attempt_delay_seconds=next_attempt_delay_seconds
|
|
583
|
+
),
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
# endregion step
|
|
587
|
+
|
|
588
|
+
# region invoke
|
|
589
|
+
@classmethod
|
|
590
|
+
def create_invoke_start(
|
|
591
|
+
cls,
|
|
592
|
+
identifier: OperationIdentifier,
|
|
593
|
+
payload: str,
|
|
594
|
+
chained_invoke_options: ChainedInvokeOptions,
|
|
595
|
+
) -> OperationUpdate:
|
|
596
|
+
"""Create an instance of OperationUpdate for type: INVOKE, action: START."""
|
|
597
|
+
return cls(
|
|
598
|
+
operation_id=identifier.operation_id,
|
|
599
|
+
parent_id=identifier.parent_id,
|
|
600
|
+
operation_type=OperationType.CHAINED_INVOKE,
|
|
601
|
+
sub_type=OperationSubType.CHAINED_INVOKE,
|
|
602
|
+
action=OperationAction.START,
|
|
603
|
+
name=identifier.name,
|
|
604
|
+
payload=payload,
|
|
605
|
+
chained_invoke_options=chained_invoke_options,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
# endregion invoke
|
|
609
|
+
|
|
610
|
+
# region wait for condition
|
|
611
|
+
@classmethod
|
|
612
|
+
def create_wait_for_condition_start(
|
|
613
|
+
cls, identifier: OperationIdentifier
|
|
614
|
+
) -> OperationUpdate:
|
|
615
|
+
"""Create an instance of OperationUpdate for type: STEP, action: START."""
|
|
616
|
+
return cls(
|
|
617
|
+
operation_id=identifier.operation_id,
|
|
618
|
+
parent_id=identifier.parent_id,
|
|
619
|
+
operation_type=OperationType.STEP,
|
|
620
|
+
sub_type=OperationSubType.WAIT_FOR_CONDITION,
|
|
621
|
+
action=OperationAction.START,
|
|
622
|
+
name=identifier.name,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
@classmethod
|
|
626
|
+
def create_wait_for_condition_succeed(
|
|
627
|
+
cls, identifier: OperationIdentifier, payload: str
|
|
628
|
+
) -> OperationUpdate:
|
|
629
|
+
"""Create an instance of OperationUpdate for type: STEP, action: SUCCEED."""
|
|
630
|
+
return cls(
|
|
631
|
+
operation_id=identifier.operation_id,
|
|
632
|
+
parent_id=identifier.parent_id,
|
|
633
|
+
operation_type=OperationType.STEP,
|
|
634
|
+
sub_type=OperationSubType.WAIT_FOR_CONDITION,
|
|
635
|
+
action=OperationAction.SUCCEED,
|
|
636
|
+
name=identifier.name,
|
|
637
|
+
payload=payload,
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
@classmethod
|
|
641
|
+
def create_wait_for_condition_retry(
|
|
642
|
+
cls,
|
|
643
|
+
identifier: OperationIdentifier,
|
|
644
|
+
payload: str,
|
|
645
|
+
next_attempt_delay_seconds: int,
|
|
646
|
+
) -> OperationUpdate:
|
|
647
|
+
"""Create an instance of OperationUpdate for type: STEP, action: RETRY."""
|
|
648
|
+
return cls(
|
|
649
|
+
operation_id=identifier.operation_id,
|
|
650
|
+
parent_id=identifier.parent_id,
|
|
651
|
+
operation_type=OperationType.STEP,
|
|
652
|
+
sub_type=OperationSubType.WAIT_FOR_CONDITION,
|
|
653
|
+
action=OperationAction.RETRY,
|
|
654
|
+
name=identifier.name,
|
|
655
|
+
payload=payload,
|
|
656
|
+
step_options=StepOptions(
|
|
657
|
+
next_attempt_delay_seconds=next_attempt_delay_seconds
|
|
658
|
+
),
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
@classmethod
|
|
662
|
+
def create_wait_for_condition_fail(
|
|
663
|
+
cls, identifier: OperationIdentifier, error: ErrorObject
|
|
664
|
+
) -> OperationUpdate:
|
|
665
|
+
"""Create an instance of OperationUpdate for type: STEP, action: FAIL."""
|
|
666
|
+
return cls(
|
|
667
|
+
operation_id=identifier.operation_id,
|
|
668
|
+
parent_id=identifier.parent_id,
|
|
669
|
+
operation_type=OperationType.STEP,
|
|
670
|
+
sub_type=OperationSubType.WAIT_FOR_CONDITION,
|
|
671
|
+
action=OperationAction.FAIL,
|
|
672
|
+
name=identifier.name,
|
|
673
|
+
error=error,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
# endregion wait for condition
|
|
677
|
+
|
|
678
|
+
# region wait
|
|
679
|
+
@classmethod
|
|
680
|
+
def create_wait_start(
|
|
681
|
+
cls, identifier: OperationIdentifier, wait_options: WaitOptions
|
|
682
|
+
) -> OperationUpdate:
|
|
683
|
+
"""Create an instance of OperationUpdate for type: WAIT, action: START."""
|
|
684
|
+
return cls(
|
|
685
|
+
operation_id=identifier.operation_id,
|
|
686
|
+
parent_id=identifier.parent_id,
|
|
687
|
+
operation_type=OperationType.WAIT,
|
|
688
|
+
sub_type=OperationSubType.WAIT,
|
|
689
|
+
action=OperationAction.START,
|
|
690
|
+
name=identifier.name,
|
|
691
|
+
wait_options=wait_options,
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
# endregion wait
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
@dataclass(frozen=True)
|
|
698
|
+
class Operation:
|
|
699
|
+
"""Represent the Operation type for GetDurableExecutionState and CheckpointDurableExecution."""
|
|
700
|
+
|
|
701
|
+
operation_id: str
|
|
702
|
+
operation_type: OperationType
|
|
703
|
+
status: OperationStatus
|
|
704
|
+
parent_id: str | None = None
|
|
705
|
+
name: str | None = None
|
|
706
|
+
start_timestamp: datetime.datetime | None = None
|
|
707
|
+
end_timestamp: datetime.datetime | None = None
|
|
708
|
+
sub_type: OperationSubType | None = None
|
|
709
|
+
execution_details: ExecutionDetails | None = None
|
|
710
|
+
context_details: ContextDetails | None = None
|
|
711
|
+
step_details: StepDetails | None = None
|
|
712
|
+
wait_details: WaitDetails | None = None
|
|
713
|
+
callback_details: CallbackDetails | None = None
|
|
714
|
+
chained_invoke_details: ChainedInvokeDetails | None = None
|
|
715
|
+
|
|
716
|
+
@classmethod
|
|
717
|
+
def from_dict(cls, data: MutableMapping[str, Any]) -> Operation:
|
|
718
|
+
"""Create an Operation instance from a dictionary with the original Smithy model field names.
|
|
719
|
+
|
|
720
|
+
Args:
|
|
721
|
+
data: Dictionary with camelCase keys matching the Smithy model
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
An Operation instance with snake_case attributes
|
|
725
|
+
"""
|
|
726
|
+
operation_type = OperationType(data.get("Type"))
|
|
727
|
+
operation_status = OperationStatus(data.get("Status"))
|
|
728
|
+
|
|
729
|
+
sub_type = None
|
|
730
|
+
if sub_type_input := data.get("SubType"):
|
|
731
|
+
sub_type = OperationSubType(sub_type_input)
|
|
732
|
+
|
|
733
|
+
execution_details = None
|
|
734
|
+
if execution_details_input := data.get("ExecutionDetails"):
|
|
735
|
+
execution_details = ExecutionDetails.from_dict(execution_details_input)
|
|
736
|
+
|
|
737
|
+
context_details = None
|
|
738
|
+
if context_details_input := data.get("ContextDetails"):
|
|
739
|
+
context_details = ContextDetails.from_dict(context_details_input)
|
|
740
|
+
|
|
741
|
+
step_details = None
|
|
742
|
+
if step_details_input := data.get("StepDetails"):
|
|
743
|
+
step_details = StepDetails.from_dict(step_details_input)
|
|
744
|
+
|
|
745
|
+
wait_details = None
|
|
746
|
+
if wait_details_input := data.get("WaitDetails"):
|
|
747
|
+
wait_details = WaitDetails.from_dict(wait_details_input)
|
|
748
|
+
|
|
749
|
+
callback_details = None
|
|
750
|
+
if callback_details_input := data.get("CallbackDetails"):
|
|
751
|
+
callback_details = CallbackDetails.from_dict(callback_details_input)
|
|
752
|
+
|
|
753
|
+
chained_invoke_details = None
|
|
754
|
+
if chained_invoke_details := data.get("chained_invoke_details"):
|
|
755
|
+
chained_invoke_details = ChainedInvokeDetails.from_dict(
|
|
756
|
+
chained_invoke_details
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
return cls(
|
|
760
|
+
operation_id=data["Id"],
|
|
761
|
+
operation_type=operation_type,
|
|
762
|
+
status=operation_status,
|
|
763
|
+
parent_id=data.get("ParentId"),
|
|
764
|
+
name=data.get("Name"),
|
|
765
|
+
start_timestamp=data.get("StartTimestamp"),
|
|
766
|
+
end_timestamp=data.get("EndTimestamp"),
|
|
767
|
+
sub_type=sub_type,
|
|
768
|
+
execution_details=execution_details,
|
|
769
|
+
context_details=context_details,
|
|
770
|
+
step_details=step_details,
|
|
771
|
+
wait_details=wait_details,
|
|
772
|
+
callback_details=callback_details,
|
|
773
|
+
chained_invoke_details=chained_invoke_details,
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
def to_dict(self) -> MutableMapping[str, Any]:
|
|
777
|
+
result: MutableMapping[str, Any] = {
|
|
778
|
+
"Id": self.operation_id,
|
|
779
|
+
"Type": self.operation_type.value,
|
|
780
|
+
"Status": self.status.value,
|
|
781
|
+
}
|
|
782
|
+
if self.parent_id:
|
|
783
|
+
result["ParentId"] = self.parent_id
|
|
784
|
+
if self.name:
|
|
785
|
+
result["Name"] = self.name
|
|
786
|
+
if self.start_timestamp:
|
|
787
|
+
result["StartTimestamp"] = self.start_timestamp
|
|
788
|
+
if self.end_timestamp:
|
|
789
|
+
result["EndTimestamp"] = self.end_timestamp
|
|
790
|
+
if self.sub_type:
|
|
791
|
+
result["SubType"] = self.sub_type.value
|
|
792
|
+
if self.execution_details:
|
|
793
|
+
result["ExecutionDetails"] = {
|
|
794
|
+
"InputPayload": self.execution_details.input_payload
|
|
795
|
+
}
|
|
796
|
+
if self.context_details:
|
|
797
|
+
result["ContextDetails"] = {"Result": self.context_details.result}
|
|
798
|
+
if self.step_details:
|
|
799
|
+
step_dict: MutableMapping[str, Any] = {"Attempt": self.step_details.attempt}
|
|
800
|
+
if self.step_details.next_attempt_timestamp:
|
|
801
|
+
step_dict["NextAttemptTimestamp"] = (
|
|
802
|
+
self.step_details.next_attempt_timestamp
|
|
803
|
+
)
|
|
804
|
+
if self.step_details.result:
|
|
805
|
+
step_dict["Result"] = self.step_details.result
|
|
806
|
+
if self.step_details.error:
|
|
807
|
+
step_dict["Error"] = self.step_details.error.to_dict()
|
|
808
|
+
result["StepDetails"] = step_dict
|
|
809
|
+
if self.wait_details:
|
|
810
|
+
result["WaitDetails"] = {
|
|
811
|
+
"ScheduledEndTimestamp": self.wait_details.scheduled_end_timestamp
|
|
812
|
+
}
|
|
813
|
+
if self.callback_details:
|
|
814
|
+
callback_dict: MutableMapping[str, Any] = {
|
|
815
|
+
"CallbackId": self.callback_details.callback_id
|
|
816
|
+
}
|
|
817
|
+
if self.callback_details.result:
|
|
818
|
+
callback_dict["Result"] = self.callback_details.result
|
|
819
|
+
if self.callback_details.error:
|
|
820
|
+
callback_dict["Error"] = self.callback_details.error.to_dict()
|
|
821
|
+
result["CallbackDetails"] = callback_dict
|
|
822
|
+
if self.chained_invoke_details:
|
|
823
|
+
invoke_dict: MutableMapping[str, Any] = {}
|
|
824
|
+
if self.chained_invoke_details.result:
|
|
825
|
+
invoke_dict["Result"] = self.chained_invoke_details.result
|
|
826
|
+
if self.chained_invoke_details.error:
|
|
827
|
+
invoke_dict["Error"] = self.chained_invoke_details.error.to_dict()
|
|
828
|
+
result["ChainedInvokeDetails"] = invoke_dict
|
|
829
|
+
return result
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
@dataclass(frozen=True)
|
|
833
|
+
class CheckpointUpdatedExecutionState:
|
|
834
|
+
"""Representation of the CheckpointUpdatedExecutionState structure of the DEX API."""
|
|
835
|
+
|
|
836
|
+
operations: list[Operation] = field(default_factory=list)
|
|
837
|
+
next_marker: str | None = None
|
|
838
|
+
|
|
839
|
+
@classmethod
|
|
840
|
+
def from_dict(
|
|
841
|
+
cls, data: MutableMapping[str, Any]
|
|
842
|
+
) -> CheckpointUpdatedExecutionState:
|
|
843
|
+
"""Create an instance from a dictionary with the original Smithy model field names.
|
|
844
|
+
|
|
845
|
+
Args:
|
|
846
|
+
data: Dictionary with camelCase keys matching the Smithy model
|
|
847
|
+
|
|
848
|
+
Returns:
|
|
849
|
+
Instance of the current class.
|
|
850
|
+
"""
|
|
851
|
+
operations = []
|
|
852
|
+
if input_operations := data.get("Operations"):
|
|
853
|
+
operations = [Operation.from_dict(op) for op in input_operations]
|
|
854
|
+
|
|
855
|
+
return cls(operations=operations, next_marker=data.get("NextMarker"))
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
@dataclass(frozen=True)
|
|
859
|
+
class CheckpointOutput:
|
|
860
|
+
"""Representation of the CheckpointDurableExecutionOutput structure of the DEX CheckpointDurableExecution API."""
|
|
861
|
+
|
|
862
|
+
checkpoint_token: str
|
|
863
|
+
new_execution_state: CheckpointUpdatedExecutionState
|
|
864
|
+
|
|
865
|
+
@classmethod
|
|
866
|
+
def from_dict(cls, data: MutableMapping[str, Any]) -> CheckpointOutput:
|
|
867
|
+
"""Create an instance from a dictionary with the original Smithy model field names.
|
|
868
|
+
|
|
869
|
+
Args:
|
|
870
|
+
data: Dictionary with camelCase keys matching the Smithy model
|
|
871
|
+
|
|
872
|
+
Returns:
|
|
873
|
+
A CheckpointDurableExecutionOutput instance.
|
|
874
|
+
"""
|
|
875
|
+
new_execution_state = None
|
|
876
|
+
if input_execution_state := data.get("NewExecutionState"):
|
|
877
|
+
new_execution_state = CheckpointUpdatedExecutionState.from_dict(
|
|
878
|
+
input_execution_state
|
|
879
|
+
)
|
|
880
|
+
else:
|
|
881
|
+
# Provide an empty default if not present
|
|
882
|
+
new_execution_state = CheckpointUpdatedExecutionState()
|
|
883
|
+
|
|
884
|
+
return cls(
|
|
885
|
+
# TODO: maybe should throw if empty?
|
|
886
|
+
checkpoint_token=data.get("CheckpointToken", ""),
|
|
887
|
+
new_execution_state=new_execution_state,
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
@dataclass(frozen=True)
|
|
892
|
+
class StateOutput:
|
|
893
|
+
"""Representation of the GetDurableExecutionStateOutput structure of the DEX GetDurableExecutionState API."""
|
|
894
|
+
|
|
895
|
+
operations: list[Operation] = field(default_factory=list)
|
|
896
|
+
next_marker: str | None = None
|
|
897
|
+
|
|
898
|
+
@classmethod
|
|
899
|
+
def from_dict(cls, data: MutableMapping[str, Any]) -> StateOutput:
|
|
900
|
+
"""Create a GetDurableExecutionStateOutput instance from a dictionary with the original Smithy model field names.
|
|
901
|
+
|
|
902
|
+
Args:
|
|
903
|
+
data: Dictionary with camelCase keys matching the Smithy model
|
|
904
|
+
|
|
905
|
+
Returns:
|
|
906
|
+
A GetDurableExecutionStateOutput instance.
|
|
907
|
+
"""
|
|
908
|
+
operations = []
|
|
909
|
+
if input_operations := data.get("Operations"):
|
|
910
|
+
operations = [Operation.from_dict(op) for op in input_operations]
|
|
911
|
+
|
|
912
|
+
return cls(operations=operations, next_marker=data.get("NextMarker"))
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
# endregion model
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
# region client
|
|
919
|
+
class DurableServiceClient(Protocol):
|
|
920
|
+
"""Durable Service clients must implement this interface."""
|
|
921
|
+
|
|
922
|
+
def checkpoint(
|
|
923
|
+
self,
|
|
924
|
+
durable_execution_arn: str,
|
|
925
|
+
checkpoint_token: str,
|
|
926
|
+
updates: list[OperationUpdate],
|
|
927
|
+
client_token: str | None,
|
|
928
|
+
) -> CheckpointOutput: ... # pragma: no cover
|
|
929
|
+
|
|
930
|
+
def get_execution_state(
|
|
931
|
+
self,
|
|
932
|
+
durable_execution_arn: str,
|
|
933
|
+
checkpoint_token: str,
|
|
934
|
+
next_marker: str,
|
|
935
|
+
max_items: int = 1000,
|
|
936
|
+
) -> StateOutput: ... # pragma: no cover
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
class LambdaClient(DurableServiceClient):
|
|
940
|
+
"""Persist durable operations to the Lambda Durable Function APIs."""
|
|
941
|
+
|
|
942
|
+
def __init__(self, client: Any) -> None:
|
|
943
|
+
self.client = client
|
|
944
|
+
|
|
945
|
+
@staticmethod
|
|
946
|
+
def load_preview_botocore_models() -> None:
|
|
947
|
+
"""
|
|
948
|
+
Load boto3 models from the Python path for custom preview client.
|
|
949
|
+
"""
|
|
950
|
+
os.environ["AWS_DATA_PATH"] = str(
|
|
951
|
+
Path(__file__).parent.joinpath("botocore", "data")
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
@staticmethod
|
|
955
|
+
def initialize_from_env() -> LambdaClient:
|
|
956
|
+
LambdaClient.load_preview_botocore_models()
|
|
957
|
+
|
|
958
|
+
"""
|
|
959
|
+
TODO - we can remove this when were using the actual lambda client,
|
|
960
|
+
but we need this with the preview model because boto won't match against lambdainternal.
|
|
961
|
+
"""
|
|
962
|
+
endpoint_url = os.getenv("AWS_ENDPOINT_URL_LAMBDA", None)
|
|
963
|
+
if not endpoint_url:
|
|
964
|
+
client = boto3.client(
|
|
965
|
+
"lambdainternal",
|
|
966
|
+
config=Config(
|
|
967
|
+
connect_timeout=5,
|
|
968
|
+
read_timeout=50,
|
|
969
|
+
),
|
|
970
|
+
)
|
|
971
|
+
else:
|
|
972
|
+
client = boto3.client(
|
|
973
|
+
"lambdainternal",
|
|
974
|
+
endpoint_url=endpoint_url,
|
|
975
|
+
config=Config(
|
|
976
|
+
connect_timeout=5,
|
|
977
|
+
read_timeout=50,
|
|
978
|
+
),
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
return LambdaClient(client=client)
|
|
982
|
+
|
|
983
|
+
def checkpoint(
|
|
984
|
+
self,
|
|
985
|
+
durable_execution_arn: str,
|
|
986
|
+
checkpoint_token: str,
|
|
987
|
+
updates: list[OperationUpdate],
|
|
988
|
+
client_token: str | None,
|
|
989
|
+
) -> CheckpointOutput:
|
|
990
|
+
try:
|
|
991
|
+
params = {
|
|
992
|
+
"DurableExecutionArn": durable_execution_arn,
|
|
993
|
+
"CheckpointToken": checkpoint_token,
|
|
994
|
+
"Updates": [o.to_dict() for o in updates],
|
|
995
|
+
}
|
|
996
|
+
if client_token is not None:
|
|
997
|
+
params["ClientToken"] = client_token
|
|
998
|
+
|
|
999
|
+
result: MutableMapping[str, Any] = self.client.checkpoint_durable_execution(
|
|
1000
|
+
**params
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
return CheckpointOutput.from_dict(result)
|
|
1004
|
+
except Exception as e:
|
|
1005
|
+
checkpoint_error = CheckpointError.from_exception(e)
|
|
1006
|
+
logger.exception(
|
|
1007
|
+
"Failed to checkpoint.", extra=checkpoint_error.build_logger_extras()
|
|
1008
|
+
)
|
|
1009
|
+
raise checkpoint_error from None
|
|
1010
|
+
|
|
1011
|
+
def get_execution_state(
|
|
1012
|
+
self,
|
|
1013
|
+
durable_execution_arn: str,
|
|
1014
|
+
checkpoint_token: str,
|
|
1015
|
+
next_marker: str,
|
|
1016
|
+
max_items: int = 1000,
|
|
1017
|
+
) -> StateOutput:
|
|
1018
|
+
try:
|
|
1019
|
+
result: MutableMapping[str, Any] = self.client.get_durable_execution_state(
|
|
1020
|
+
DurableExecutionArn=durable_execution_arn,
|
|
1021
|
+
CheckpointToken=checkpoint_token,
|
|
1022
|
+
Marker=next_marker,
|
|
1023
|
+
MaxItems=max_items,
|
|
1024
|
+
)
|
|
1025
|
+
return StateOutput.from_dict(result)
|
|
1026
|
+
except Exception as e:
|
|
1027
|
+
error = GetExecutionStateError.from_exception(e)
|
|
1028
|
+
logger.exception(
|
|
1029
|
+
"Failed to get execution state.", extra=error.build_logger_extras()
|
|
1030
|
+
)
|
|
1031
|
+
raise error from None
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
# endregion client
|