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,551 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import logging
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Concatenate, Generic, ParamSpec, TypeVar
|
|
6
|
+
|
|
7
|
+
from aws_durable_execution_sdk_python.config import (
|
|
8
|
+
BatchedInput,
|
|
9
|
+
CallbackConfig,
|
|
10
|
+
ChildConfig,
|
|
11
|
+
Duration,
|
|
12
|
+
InvokeConfig,
|
|
13
|
+
MapConfig,
|
|
14
|
+
ParallelConfig,
|
|
15
|
+
StepConfig,
|
|
16
|
+
WaitForCallbackConfig,
|
|
17
|
+
)
|
|
18
|
+
from aws_durable_execution_sdk_python.exceptions import (
|
|
19
|
+
CallbackError,
|
|
20
|
+
SuspendExecution,
|
|
21
|
+
ValidationError,
|
|
22
|
+
)
|
|
23
|
+
from aws_durable_execution_sdk_python.identifier import OperationIdentifier
|
|
24
|
+
from aws_durable_execution_sdk_python.lambda_service import OperationSubType
|
|
25
|
+
from aws_durable_execution_sdk_python.logger import Logger, LogInfo
|
|
26
|
+
from aws_durable_execution_sdk_python.operation.callback import (
|
|
27
|
+
create_callback_handler,
|
|
28
|
+
wait_for_callback_handler,
|
|
29
|
+
)
|
|
30
|
+
from aws_durable_execution_sdk_python.operation.child import child_handler
|
|
31
|
+
from aws_durable_execution_sdk_python.operation.invoke import invoke_handler
|
|
32
|
+
from aws_durable_execution_sdk_python.operation.map import map_handler
|
|
33
|
+
from aws_durable_execution_sdk_python.operation.parallel import parallel_handler
|
|
34
|
+
from aws_durable_execution_sdk_python.operation.step import step_handler
|
|
35
|
+
from aws_durable_execution_sdk_python.operation.wait import wait_handler
|
|
36
|
+
from aws_durable_execution_sdk_python.operation.wait_for_condition import (
|
|
37
|
+
wait_for_condition_handler,
|
|
38
|
+
)
|
|
39
|
+
from aws_durable_execution_sdk_python.serdes import (
|
|
40
|
+
PassThroughSerDes,
|
|
41
|
+
SerDes,
|
|
42
|
+
deserialize,
|
|
43
|
+
)
|
|
44
|
+
from aws_durable_execution_sdk_python.state import ExecutionState # noqa: TCH001
|
|
45
|
+
from aws_durable_execution_sdk_python.threading import OrderedCounter
|
|
46
|
+
from aws_durable_execution_sdk_python.types import (
|
|
47
|
+
BatchResult,
|
|
48
|
+
LoggerInterface,
|
|
49
|
+
StepContext,
|
|
50
|
+
WaitForCallbackContext,
|
|
51
|
+
WaitForConditionCheckContext,
|
|
52
|
+
)
|
|
53
|
+
from aws_durable_execution_sdk_python.types import Callback as CallbackProtocol
|
|
54
|
+
from aws_durable_execution_sdk_python.types import (
|
|
55
|
+
DurableContext as DurableContextProtocol,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if TYPE_CHECKING:
|
|
59
|
+
from collections.abc import Callable, Sequence
|
|
60
|
+
|
|
61
|
+
from aws_durable_execution_sdk_python.state import CheckpointedResult
|
|
62
|
+
from aws_durable_execution_sdk_python.types import LambdaContext
|
|
63
|
+
from aws_durable_execution_sdk_python.waits import WaitForConditionConfig
|
|
64
|
+
|
|
65
|
+
P = TypeVar("P") # Payload type
|
|
66
|
+
R = TypeVar("R") # Result type
|
|
67
|
+
T = TypeVar("T")
|
|
68
|
+
U = TypeVar("U")
|
|
69
|
+
Params = ParamSpec("Params")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
logger = logging.getLogger(__name__)
|
|
73
|
+
|
|
74
|
+
PASS_THROUGH_SERDES: SerDes[Any] = PassThroughSerDes()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def durable_step(
|
|
78
|
+
func: Callable[Concatenate[StepContext, Params], T],
|
|
79
|
+
) -> Callable[Params, Callable[[StepContext], T]]:
|
|
80
|
+
"""Wrap your callable into a named function that a Durable step can run."""
|
|
81
|
+
|
|
82
|
+
def wrapper(*args, **kwargs):
|
|
83
|
+
def function_with_arguments(context: StepContext):
|
|
84
|
+
return func(context, *args, **kwargs)
|
|
85
|
+
|
|
86
|
+
function_with_arguments._original_name = func.__name__ # noqa: SLF001
|
|
87
|
+
return function_with_arguments
|
|
88
|
+
|
|
89
|
+
return wrapper
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def durable_with_child_context(
|
|
93
|
+
func: Callable[Concatenate[DurableContext, Params], T],
|
|
94
|
+
) -> Callable[Params, Callable[[DurableContext], T]]:
|
|
95
|
+
"""Wrap your callable into a Durable child context."""
|
|
96
|
+
|
|
97
|
+
def wrapper(*args, **kwargs):
|
|
98
|
+
def function_with_arguments(child_context: DurableContext):
|
|
99
|
+
return func(child_context, *args, **kwargs)
|
|
100
|
+
|
|
101
|
+
function_with_arguments._original_name = func.__name__ # noqa: SLF001
|
|
102
|
+
return function_with_arguments
|
|
103
|
+
|
|
104
|
+
return wrapper
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Callback(Generic[T], CallbackProtocol[T]): # noqa: PYI059
|
|
108
|
+
"""A future that will block on result() until callback_id returns."""
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
callback_id: str,
|
|
113
|
+
operation_id: str,
|
|
114
|
+
state: ExecutionState,
|
|
115
|
+
serdes: SerDes[T] | None = None,
|
|
116
|
+
):
|
|
117
|
+
self.callback_id: str = callback_id
|
|
118
|
+
self.operation_id: str = operation_id
|
|
119
|
+
self.state: ExecutionState = state
|
|
120
|
+
self.serdes: SerDes[T] | None = serdes
|
|
121
|
+
|
|
122
|
+
def result(self) -> T | None:
|
|
123
|
+
"""Return the result of the future. Will block until result is available.
|
|
124
|
+
|
|
125
|
+
This will suspend the current execution while waiting for the result to
|
|
126
|
+
become available. Durable Functions will replay the execution once the
|
|
127
|
+
result is ready, and proceed when it reaches the .result() call.
|
|
128
|
+
|
|
129
|
+
Use the callback id with the following APIs to send back the result, error or
|
|
130
|
+
heartbeats: SendDurableExecutionCallbackSuccess, SendDurableExecutionCallbackFailure
|
|
131
|
+
and SendDurableExecutionCallbackHeartbeat.
|
|
132
|
+
"""
|
|
133
|
+
checkpointed_result: CheckpointedResult = self.state.get_checkpoint_result(
|
|
134
|
+
self.operation_id
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if not checkpointed_result.is_existent():
|
|
138
|
+
msg = "Callback operation must exist"
|
|
139
|
+
raise CallbackError(msg)
|
|
140
|
+
|
|
141
|
+
if (
|
|
142
|
+
checkpointed_result.is_failed()
|
|
143
|
+
or checkpointed_result.is_cancelled()
|
|
144
|
+
or checkpointed_result.is_timed_out()
|
|
145
|
+
or checkpointed_result.is_stopped()
|
|
146
|
+
):
|
|
147
|
+
checkpointed_result.raise_callable_error()
|
|
148
|
+
|
|
149
|
+
if checkpointed_result.is_succeeded():
|
|
150
|
+
if checkpointed_result.result is None:
|
|
151
|
+
return None # type: ignore
|
|
152
|
+
|
|
153
|
+
return deserialize(
|
|
154
|
+
serdes=self.serdes if self.serdes is not None else PASS_THROUGH_SERDES,
|
|
155
|
+
data=checkpointed_result.result,
|
|
156
|
+
operation_id=self.operation_id,
|
|
157
|
+
durable_execution_arn=self.state.durable_execution_arn,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# operation exists; it has not terminated (successfully or otherwise)
|
|
161
|
+
# therefore we should wait
|
|
162
|
+
msg = "Callback result not received yet. Suspending execution while waiting for result."
|
|
163
|
+
raise SuspendExecution(msg)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class DurableContext(DurableContextProtocol):
|
|
167
|
+
def __init__(
|
|
168
|
+
self,
|
|
169
|
+
state: ExecutionState,
|
|
170
|
+
lambda_context: LambdaContext | None = None,
|
|
171
|
+
parent_id: str | None = None,
|
|
172
|
+
logger: Logger | None = None,
|
|
173
|
+
) -> None:
|
|
174
|
+
self.state: ExecutionState = state
|
|
175
|
+
self.lambda_context = lambda_context
|
|
176
|
+
self._parent_id: str | None = parent_id
|
|
177
|
+
self._step_counter: OrderedCounter = OrderedCounter()
|
|
178
|
+
|
|
179
|
+
log_info = LogInfo(
|
|
180
|
+
execution_state=state,
|
|
181
|
+
parent_id=parent_id,
|
|
182
|
+
)
|
|
183
|
+
self._log_info = log_info
|
|
184
|
+
self.logger: Logger = logger or Logger.from_log_info(
|
|
185
|
+
logger=logging.getLogger(),
|
|
186
|
+
info=log_info,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# region factories
|
|
190
|
+
@staticmethod
|
|
191
|
+
def from_lambda_context(
|
|
192
|
+
state: ExecutionState,
|
|
193
|
+
lambda_context: LambdaContext,
|
|
194
|
+
):
|
|
195
|
+
return DurableContext(
|
|
196
|
+
state=state,
|
|
197
|
+
lambda_context=lambda_context,
|
|
198
|
+
parent_id=None,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def create_child_context(self, parent_id: str) -> DurableContext:
|
|
202
|
+
"""Create a child context from the given parent."""
|
|
203
|
+
logger.debug("Creating child context for parent %s", parent_id)
|
|
204
|
+
return DurableContext(
|
|
205
|
+
state=self.state,
|
|
206
|
+
lambda_context=self.lambda_context,
|
|
207
|
+
parent_id=parent_id,
|
|
208
|
+
logger=self.logger.with_log_info(
|
|
209
|
+
LogInfo(
|
|
210
|
+
execution_state=self.state,
|
|
211
|
+
parent_id=parent_id,
|
|
212
|
+
)
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# endregion factories
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def _resolve_step_name(name: str | None, func: Callable) -> str | None:
|
|
220
|
+
"""Resolve the step name.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
str | None: The provided name, and if that doesn't exist the callable function's name if it has one.
|
|
224
|
+
"""
|
|
225
|
+
# callable's name will override name if name is falsy ('' or None)
|
|
226
|
+
return name or getattr(func, "_original_name", None)
|
|
227
|
+
|
|
228
|
+
def set_logger(self, new_logger: LoggerInterface):
|
|
229
|
+
"""Set the logger for the current context."""
|
|
230
|
+
self.logger = Logger.from_log_info(
|
|
231
|
+
logger=new_logger,
|
|
232
|
+
info=self._log_info,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def _create_step_id_for_logical_step(self, step: int) -> str:
|
|
236
|
+
"""
|
|
237
|
+
Generate a step_id based on the given logical step.
|
|
238
|
+
This allows us to recover operation ids or even look
|
|
239
|
+
forward without changing the internal state of this context.
|
|
240
|
+
"""
|
|
241
|
+
step_id = f"{self._parent_id}-{step}" if self._parent_id else str(step)
|
|
242
|
+
return hashlib.blake2b(step_id.encode()).hexdigest()[:64]
|
|
243
|
+
|
|
244
|
+
def _create_step_id(self) -> str:
|
|
245
|
+
"""Generate a thread-safe step id, incrementing in order of invocation.
|
|
246
|
+
|
|
247
|
+
This method is an internal implementation detail. Do not rely the exact format of
|
|
248
|
+
the id generated by this method. It is subject to change without notice.
|
|
249
|
+
"""
|
|
250
|
+
new_counter: int = self._step_counter.increment()
|
|
251
|
+
return self._create_step_id_for_logical_step(new_counter)
|
|
252
|
+
|
|
253
|
+
# region Operations
|
|
254
|
+
|
|
255
|
+
def create_callback(
|
|
256
|
+
self, name: str | None = None, config: CallbackConfig | None = None
|
|
257
|
+
) -> Callback:
|
|
258
|
+
"""Create a callback.
|
|
259
|
+
|
|
260
|
+
This generates a future with a callback id. External systems can signal
|
|
261
|
+
your Durable Function to proceed by using this callback id with the
|
|
262
|
+
SendDurableExecutionCallbackSuccess, SendDurableExecutionCallbackFailure and
|
|
263
|
+
SendDurableExecutionCallbackHeartbeat APIs.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
name (str): Optional name for the operation.
|
|
267
|
+
config (CallbackConfig): Configuration for the callback.
|
|
268
|
+
|
|
269
|
+
Return:
|
|
270
|
+
Callback future. Use result() on this future to wait for the callback resuilt.
|
|
271
|
+
"""
|
|
272
|
+
if not config:
|
|
273
|
+
config = CallbackConfig()
|
|
274
|
+
operation_id: str = self._create_step_id()
|
|
275
|
+
callback_id: str = create_callback_handler(
|
|
276
|
+
state=self.state,
|
|
277
|
+
operation_identifier=OperationIdentifier(
|
|
278
|
+
operation_id=operation_id, parent_id=self._parent_id, name=name
|
|
279
|
+
),
|
|
280
|
+
config=config,
|
|
281
|
+
)
|
|
282
|
+
result: Callback = Callback(
|
|
283
|
+
callback_id=callback_id,
|
|
284
|
+
operation_id=operation_id,
|
|
285
|
+
state=self.state,
|
|
286
|
+
serdes=config.serdes,
|
|
287
|
+
)
|
|
288
|
+
self.state.track_replay(operation_id=operation_id)
|
|
289
|
+
return result
|
|
290
|
+
|
|
291
|
+
def invoke(
|
|
292
|
+
self,
|
|
293
|
+
function_name: str,
|
|
294
|
+
payload: P,
|
|
295
|
+
name: str | None = None,
|
|
296
|
+
config: InvokeConfig[P, R] | None = None,
|
|
297
|
+
) -> R:
|
|
298
|
+
"""Invoke another Durable Function.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
function_name: Name of the function to invoke
|
|
302
|
+
payload: Input payload to send to the function
|
|
303
|
+
name: Optional name for the operation
|
|
304
|
+
config: Optional configuration for the invoke operation
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
The result of the invoked function
|
|
308
|
+
"""
|
|
309
|
+
operation_id = self._create_step_id()
|
|
310
|
+
result: R = invoke_handler(
|
|
311
|
+
function_name=function_name,
|
|
312
|
+
payload=payload,
|
|
313
|
+
state=self.state,
|
|
314
|
+
operation_identifier=OperationIdentifier(
|
|
315
|
+
operation_id=operation_id,
|
|
316
|
+
parent_id=self._parent_id,
|
|
317
|
+
name=name,
|
|
318
|
+
),
|
|
319
|
+
config=config,
|
|
320
|
+
)
|
|
321
|
+
self.state.track_replay(operation_id=operation_id)
|
|
322
|
+
return result
|
|
323
|
+
|
|
324
|
+
def map(
|
|
325
|
+
self,
|
|
326
|
+
inputs: Sequence[U],
|
|
327
|
+
func: Callable[[DurableContext, U | BatchedInput[Any, U], int, Sequence[U]], T],
|
|
328
|
+
name: str | None = None,
|
|
329
|
+
config: MapConfig | None = None,
|
|
330
|
+
) -> BatchResult[R]:
|
|
331
|
+
"""Execute a callable for each item in parallel."""
|
|
332
|
+
map_name: str | None = self._resolve_step_name(name, func)
|
|
333
|
+
|
|
334
|
+
operation_id = self._create_step_id()
|
|
335
|
+
operation_identifier = OperationIdentifier(
|
|
336
|
+
operation_id=operation_id, parent_id=self._parent_id, name=map_name
|
|
337
|
+
)
|
|
338
|
+
map_context = self.create_child_context(parent_id=operation_id)
|
|
339
|
+
|
|
340
|
+
def map_in_child_context() -> BatchResult[R]:
|
|
341
|
+
# map_context is a child_context of the context upon which `.map`
|
|
342
|
+
# was called. We are calling it `map_context` to make it explicit
|
|
343
|
+
# that any operations happening from hereon are done on the context
|
|
344
|
+
# that owns the branches
|
|
345
|
+
return map_handler(
|
|
346
|
+
items=inputs,
|
|
347
|
+
func=func,
|
|
348
|
+
config=config,
|
|
349
|
+
execution_state=self.state,
|
|
350
|
+
map_context=map_context,
|
|
351
|
+
operation_identifier=operation_identifier,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
result: BatchResult[R] = child_handler(
|
|
355
|
+
func=map_in_child_context,
|
|
356
|
+
state=self.state,
|
|
357
|
+
operation_identifier=operation_identifier,
|
|
358
|
+
config=ChildConfig(
|
|
359
|
+
sub_type=OperationSubType.MAP,
|
|
360
|
+
serdes=getattr(config, "serdes", None),
|
|
361
|
+
# child_handler should only know the serdes of the parent serdes,
|
|
362
|
+
# the item serdes will be passed when we are actually executing
|
|
363
|
+
# the branch within its own child_handler.
|
|
364
|
+
item_serdes=None,
|
|
365
|
+
),
|
|
366
|
+
)
|
|
367
|
+
self.state.track_replay(operation_id=operation_id)
|
|
368
|
+
return result
|
|
369
|
+
|
|
370
|
+
def parallel(
|
|
371
|
+
self,
|
|
372
|
+
functions: Sequence[Callable[[DurableContext], T]],
|
|
373
|
+
name: str | None = None,
|
|
374
|
+
config: ParallelConfig | None = None,
|
|
375
|
+
) -> BatchResult[T]:
|
|
376
|
+
"""Execute multiple callables in parallel."""
|
|
377
|
+
# _create_step_id() is thread-safe. rest of method is safe, since using local copy of parent id
|
|
378
|
+
operation_id = self._create_step_id()
|
|
379
|
+
parallel_context = self.create_child_context(parent_id=operation_id)
|
|
380
|
+
operation_identifier = OperationIdentifier(
|
|
381
|
+
operation_id=operation_id, parent_id=self._parent_id, name=name
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
def parallel_in_child_context() -> BatchResult[T]:
|
|
385
|
+
# parallel_context is a child_context of the context upon which `.map`
|
|
386
|
+
# was called. We are calling it `parallel_context` to make it explicit
|
|
387
|
+
# that any operations happening from hereon are done on the context
|
|
388
|
+
# that owns the branches
|
|
389
|
+
return parallel_handler(
|
|
390
|
+
callables=functions,
|
|
391
|
+
config=config,
|
|
392
|
+
execution_state=self.state,
|
|
393
|
+
parallel_context=parallel_context,
|
|
394
|
+
operation_identifier=operation_identifier,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
result: BatchResult[T] = child_handler(
|
|
398
|
+
func=parallel_in_child_context,
|
|
399
|
+
state=self.state,
|
|
400
|
+
operation_identifier=operation_identifier,
|
|
401
|
+
config=ChildConfig(
|
|
402
|
+
sub_type=OperationSubType.PARALLEL,
|
|
403
|
+
serdes=getattr(config, "serdes", None),
|
|
404
|
+
# child_handler should only know the serdes of the parent serdes,
|
|
405
|
+
# the item serdes will be passed when we are actually executing
|
|
406
|
+
# the branch within its own child_handler.
|
|
407
|
+
item_serdes=None,
|
|
408
|
+
),
|
|
409
|
+
)
|
|
410
|
+
self.state.track_replay(operation_id=operation_id)
|
|
411
|
+
return result
|
|
412
|
+
|
|
413
|
+
def run_in_child_context(
|
|
414
|
+
self,
|
|
415
|
+
func: Callable[[DurableContext], T],
|
|
416
|
+
name: str | None = None,
|
|
417
|
+
config: ChildConfig | None = None,
|
|
418
|
+
) -> T:
|
|
419
|
+
"""Run the callable and pass a child context to it.
|
|
420
|
+
|
|
421
|
+
Use this to nest and group operations.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
callable (Callable[[DurableContext], T]): Run this callable and pass the child context as the argument to it.
|
|
425
|
+
name (str | None): name for the operation.
|
|
426
|
+
config (ChildConfig | None = None): c
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
T: The result of the callable.
|
|
430
|
+
"""
|
|
431
|
+
step_name: str | None = self._resolve_step_name(name, func)
|
|
432
|
+
# _create_step_id() is thread-safe. rest of method is safe, since using local copy of parent id
|
|
433
|
+
operation_id = self._create_step_id()
|
|
434
|
+
|
|
435
|
+
def callable_with_child_context():
|
|
436
|
+
return func(self.create_child_context(parent_id=operation_id))
|
|
437
|
+
|
|
438
|
+
result: T = child_handler(
|
|
439
|
+
func=callable_with_child_context,
|
|
440
|
+
state=self.state,
|
|
441
|
+
operation_identifier=OperationIdentifier(
|
|
442
|
+
operation_id=operation_id, parent_id=self._parent_id, name=step_name
|
|
443
|
+
),
|
|
444
|
+
config=config,
|
|
445
|
+
)
|
|
446
|
+
self.state.track_replay(operation_id=operation_id)
|
|
447
|
+
return result
|
|
448
|
+
|
|
449
|
+
def step(
|
|
450
|
+
self,
|
|
451
|
+
func: Callable[[StepContext], T],
|
|
452
|
+
name: str | None = None,
|
|
453
|
+
config: StepConfig | None = None,
|
|
454
|
+
) -> T:
|
|
455
|
+
step_name = self._resolve_step_name(name, func)
|
|
456
|
+
logger.debug("Step name: %s", step_name)
|
|
457
|
+
operation_id = self._create_step_id()
|
|
458
|
+
result: T = step_handler(
|
|
459
|
+
func=func,
|
|
460
|
+
config=config,
|
|
461
|
+
state=self.state,
|
|
462
|
+
operation_identifier=OperationIdentifier(
|
|
463
|
+
operation_id=operation_id,
|
|
464
|
+
parent_id=self._parent_id,
|
|
465
|
+
name=step_name,
|
|
466
|
+
),
|
|
467
|
+
context_logger=self.logger,
|
|
468
|
+
)
|
|
469
|
+
self.state.track_replay(operation_id=operation_id)
|
|
470
|
+
return result
|
|
471
|
+
|
|
472
|
+
def wait(self, duration: Duration, name: str | None = None) -> None:
|
|
473
|
+
"""Wait for a specified amount of time.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
duration: Duration to wait
|
|
477
|
+
name: Optional name for the wait step
|
|
478
|
+
"""
|
|
479
|
+
seconds = duration.to_seconds()
|
|
480
|
+
if seconds < 1:
|
|
481
|
+
msg = "duration must be at least 1 second"
|
|
482
|
+
raise ValidationError(msg)
|
|
483
|
+
operation_id = self._create_step_id()
|
|
484
|
+
wait_handler(
|
|
485
|
+
seconds=seconds,
|
|
486
|
+
state=self.state,
|
|
487
|
+
operation_identifier=OperationIdentifier(
|
|
488
|
+
operation_id=operation_id,
|
|
489
|
+
parent_id=self._parent_id,
|
|
490
|
+
name=name,
|
|
491
|
+
),
|
|
492
|
+
)
|
|
493
|
+
self.state.track_replay(operation_id=operation_id)
|
|
494
|
+
|
|
495
|
+
def wait_for_callback(
|
|
496
|
+
self,
|
|
497
|
+
submitter: Callable[[str, WaitForCallbackContext], None],
|
|
498
|
+
name: str | None = None,
|
|
499
|
+
config: WaitForCallbackConfig | None = None,
|
|
500
|
+
) -> Any:
|
|
501
|
+
step_name: str | None = self._resolve_step_name(name, submitter)
|
|
502
|
+
logger.debug("wait_for_callback name: %s", step_name)
|
|
503
|
+
|
|
504
|
+
def wait_in_child_context(context: DurableContext):
|
|
505
|
+
return wait_for_callback_handler(context, submitter, step_name, config)
|
|
506
|
+
|
|
507
|
+
return self.run_in_child_context(
|
|
508
|
+
wait_in_child_context,
|
|
509
|
+
step_name,
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
def wait_for_condition(
|
|
513
|
+
self,
|
|
514
|
+
check: Callable[[T, WaitForConditionCheckContext], T],
|
|
515
|
+
config: WaitForConditionConfig[T],
|
|
516
|
+
name: str | None = None,
|
|
517
|
+
) -> T:
|
|
518
|
+
"""Wait for a condition to be met by polling.
|
|
519
|
+
|
|
520
|
+
Args:
|
|
521
|
+
check (Callable[[T, WaitForConditionCheckContext], T]): Function that checks the condition and returns updated state
|
|
522
|
+
config (WaitForConditionConfig[T]): Configuration including wait strategy and initial state
|
|
523
|
+
name (str | None): Optional name for the operation
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
The final state when condition is met.
|
|
527
|
+
"""
|
|
528
|
+
if check is None:
|
|
529
|
+
msg = "`check` is required for wait_for_condition"
|
|
530
|
+
raise ValidationError(msg)
|
|
531
|
+
if not config:
|
|
532
|
+
msg = "`config` is required for wait_for_condition"
|
|
533
|
+
raise ValidationError(msg)
|
|
534
|
+
|
|
535
|
+
operation_id = self._create_step_id()
|
|
536
|
+
result: T = wait_for_condition_handler(
|
|
537
|
+
check=check,
|
|
538
|
+
config=config,
|
|
539
|
+
state=self.state,
|
|
540
|
+
operation_identifier=OperationIdentifier(
|
|
541
|
+
operation_id=operation_id,
|
|
542
|
+
parent_id=self._parent_id,
|
|
543
|
+
name=name,
|
|
544
|
+
),
|
|
545
|
+
context_logger=self.logger,
|
|
546
|
+
)
|
|
547
|
+
self.state.track_replay(operation_id=operation_id)
|
|
548
|
+
return result
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
# endregion Operations
|