async-durable-execution 2.2.0__tar.gz → 2.3.0__tar.gz
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.
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/PKG-INFO +5 -2
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/README.md +2 -0
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/__about__.py +1 -1
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/__init__.py +28 -14
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_core/__init__.py +4 -3
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_core/client.py +6 -10
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_core/config.py +1 -1
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_core/context.py +162 -21
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_core/exceptions.py +13 -13
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_core/execution.py +4 -4
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_core/logger.py +4 -1
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_core/models.py +251 -283
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_core/serdes.py +7 -16
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_core/state.py +7 -7
- async_durable_execution-2.3.0/async_durable_execution/_extension/__init__.py +23 -0
- async_durable_execution-2.3.0/async_durable_execution/_operation/__init__.py +1 -0
- async_durable_execution-2.3.0/async_durable_execution/_operation/callback.py +41 -0
- async_durable_execution-2.3.0/async_durable_execution/_operation/child.py +47 -0
- {async_durable_execution-2.2.0/async_durable_execution/_extension → async_durable_execution-2.3.0/async_durable_execution/_operation}/flow.py +34 -8
- async_durable_execution-2.3.0/async_durable_execution/_operation/invoke.py +50 -0
- {async_durable_execution-2.2.0/async_durable_execution/_extension → async_durable_execution-2.3.0/async_durable_execution/_operation}/map.py +28 -7
- {async_durable_execution-2.2.0/async_durable_execution/_extension → async_durable_execution-2.3.0/async_durable_execution/_operation}/parallel.py +244 -47
- {async_durable_execution-2.2.0/async_durable_execution/_extension → async_durable_execution-2.3.0/async_durable_execution/_operation}/recurse.py +13 -8
- {async_durable_execution-2.2.0/async_durable_execution/_extension → async_durable_execution-2.3.0/async_durable_execution/_operation}/replay_safe.py +1 -1
- async_durable_execution-2.3.0/async_durable_execution/_operation/step.py +56 -0
- async_durable_execution-2.3.0/async_durable_execution/_operation/wait.py +40 -0
- {async_durable_execution-2.2.0/async_durable_execution/_extension → async_durable_execution-2.3.0/async_durable_execution/_operation}/wait_for_callback.py +60 -4
- {async_durable_execution-2.2.0/async_durable_execution/_extension → async_durable_execution-2.3.0/async_durable_execution/_operation}/wait_for_condition.py +72 -38
- {async_durable_execution-2.2.0/async_durable_execution/_extension → async_durable_execution-2.3.0/async_durable_execution/_operation}/with_retry.py +41 -3
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_primitive/base.py +32 -2
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_primitive/callback.py +13 -36
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_primitive/child.py +10 -10
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_primitive/invoke.py +11 -35
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_primitive/step.py +301 -34
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_primitive/wait.py +4 -33
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_runner/cloud/__init__.py +2 -20
- async_durable_execution-2.3.0/async_durable_execution/_runner/exceptions.py +55 -0
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_runner/local/__init__.py +7 -8
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_runner/local/execution.py +1 -100
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_runner/local/executor.py +6 -76
- async_durable_execution-2.3.0/async_durable_execution/_runner/local/model.py +205 -0
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_runner/local/processor.py +1 -1
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_runner/local/processors/base.py +11 -2
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_runner/local/processors/step.py +25 -10
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_runner/local/scheduler.py +9 -35
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_runner/model.py +199 -885
- async_durable_execution-2.3.0/async_durable_execution/extension.py +701 -0
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/pyproject.toml +9 -3
- async_durable_execution-2.2.0/async_durable_execution/_extension/__init__.py +0 -1
- async_durable_execution-2.2.0/async_durable_execution/_runner/exceptions.py +0 -276
- async_durable_execution-2.2.0/async_durable_execution/_runner/local/model.py +0 -275
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/.gitignore +0 -0
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/LICENSE +0 -0
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/NOTICE +0 -0
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/.gitignore +0 -0
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_core/task.py +0 -0
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_primitive/__init__.py +0 -0
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_runner/__init__.py +0 -0
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_runner/local/processors/__init__.py +0 -0
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_runner/local/processors/callback.py +0 -0
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_runner/local/processors/context.py +0 -0
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_runner/local/processors/execution.py +0 -0
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_runner/local/processors/invoke.py +0 -0
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_runner/local/processors/wait.py +0 -0
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_runner/local/time_scale.py +0 -0
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/_runner/py.typed +0 -0
- {async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: async-durable-execution
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Community-maintained durable execution SDK for AWS Lambda in Python
|
|
5
5
|
Project-URL: Documentation, https://github.com/zhongkechen/async-durable-execution#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/zhongkechen/async-durable-execution/issues
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
20
21
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
21
22
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
22
23
|
Requires-Python: >=3.10
|
|
@@ -72,6 +73,7 @@ composition, and APIs designed for modern Python applications.
|
|
|
72
73
|
|
|
73
74
|
- **[Async-first durable code](https://zhongkechen.github.io/async-durable-execution/official-python-sdk-comparison.html#programming-model)** - Compared with the official AWS SDK, user-provided durable handlers, steps, child contexts, `flow` nodes, callback submitters, map item functions, parallel branches, and wait-for-condition checks are written with `async def`.
|
|
74
75
|
- **[Operations not available in the official SDK](https://zhongkechen.github.io/async-durable-execution/api/extension/replay_safe.html)** - This SDK adds [replay-safe helpers](https://zhongkechen.github.io/async-durable-execution/api/extension/replay_safe.html) (`random()`, `now()`, `timestamp()`, and `uuid()`) and [durable self-invocation](https://zhongkechen.github.io/async-durable-execution/api/extension/recurse.html) (`recurse()`).
|
|
76
|
+
- **[Stable custom operation SPI](https://zhongkechen.github.io/async-durable-execution/custom-operations.html)** - Third-party packages can reserve opaque deterministic primitive identities, use custom subtypes, and build stateful replay-safe operations without importing SDK internals.
|
|
75
77
|
- **[Declarative DAG workflows](https://zhongkechen.github.io/async-durable-execution/api/extension/flow.html#quick-start)** - Define acyclic workflows with typed node inputs, inferred or conditional dependencies, failure routes, and durable operations inside each node. The SDK validates the graph before execution and skips nodes that are not required by the selected outputs.
|
|
76
78
|
- **[Background operation tasks](https://zhongkechen.github.io/async-durable-execution/advanced-usage.html#background-operation-tasks)** - Durable operations such as `step(...)`, `wait(...)`, `invoke(...)`, `recurse(...)`, `run_in_child_context(...)`, and `flow(...)` return `asyncio.Task` objects, so independent operations can run in the background and be awaited together with `asyncio.gather` without using `parallel()` or `map()`.
|
|
77
79
|
- **[Pythonic operation parameters](https://zhongkechen.github.io/async-durable-execution/migrating-from-official-python-sdk.html#api-mapping)** - Operations use direct keyword arguments, standard Python types such as `datetime.timedelta`, and keyword-only names instead of configuration wrapper objects.
|
|
@@ -247,6 +249,7 @@ For the developer workflow to run or deploy example integration tests, see the [
|
|
|
247
249
|
- **[Deploy and Invoke](https://zhongkechen.github.io/async-durable-execution/deployment.html)** - Configure IAM, qualified function identifiers, invocations, CloudFormation, and SAM
|
|
248
250
|
- **[Using Synchronous Code](https://zhongkechen.github.io/async-durable-execution/using-synchronous-code.html)** - Wrap existing synchronous business logic and blocking clients safely
|
|
249
251
|
- **[Advanced Usage](https://zhongkechen.github.io/async-durable-execution/advanced-usage.html)** - Explore background operation tasks, batch completion conditions, Lambda clients, and Lambda layers
|
|
252
|
+
- **[Custom Durable Operations](https://zhongkechen.github.io/async-durable-execution/custom-operations.html)** - Build third-party durable operation libraries on the stable extension-author interface
|
|
250
253
|
- **[Runner Architecture](https://zhongkechen.github.io/async-durable-execution/runner-architecture.html)** - Local and cloud runner execution flow, components, and diagrams
|
|
251
254
|
- **[Contributing Guide](https://github.com/zhongkechen/async-durable-execution/blob/main/CONTRIBUTING.md)** - Development workflow, Hatch commands, testing, and pull request guidance
|
|
252
255
|
|
|
@@ -43,6 +43,7 @@ composition, and APIs designed for modern Python applications.
|
|
|
43
43
|
|
|
44
44
|
- **[Async-first durable code](https://zhongkechen.github.io/async-durable-execution/official-python-sdk-comparison.html#programming-model)** - Compared with the official AWS SDK, user-provided durable handlers, steps, child contexts, `flow` nodes, callback submitters, map item functions, parallel branches, and wait-for-condition checks are written with `async def`.
|
|
45
45
|
- **[Operations not available in the official SDK](https://zhongkechen.github.io/async-durable-execution/api/extension/replay_safe.html)** - This SDK adds [replay-safe helpers](https://zhongkechen.github.io/async-durable-execution/api/extension/replay_safe.html) (`random()`, `now()`, `timestamp()`, and `uuid()`) and [durable self-invocation](https://zhongkechen.github.io/async-durable-execution/api/extension/recurse.html) (`recurse()`).
|
|
46
|
+
- **[Stable custom operation SPI](https://zhongkechen.github.io/async-durable-execution/custom-operations.html)** - Third-party packages can reserve opaque deterministic primitive identities, use custom subtypes, and build stateful replay-safe operations without importing SDK internals.
|
|
46
47
|
- **[Declarative DAG workflows](https://zhongkechen.github.io/async-durable-execution/api/extension/flow.html#quick-start)** - Define acyclic workflows with typed node inputs, inferred or conditional dependencies, failure routes, and durable operations inside each node. The SDK validates the graph before execution and skips nodes that are not required by the selected outputs.
|
|
47
48
|
- **[Background operation tasks](https://zhongkechen.github.io/async-durable-execution/advanced-usage.html#background-operation-tasks)** - Durable operations such as `step(...)`, `wait(...)`, `invoke(...)`, `recurse(...)`, `run_in_child_context(...)`, and `flow(...)` return `asyncio.Task` objects, so independent operations can run in the background and be awaited together with `asyncio.gather` without using `parallel()` or `map()`.
|
|
48
49
|
- **[Pythonic operation parameters](https://zhongkechen.github.io/async-durable-execution/migrating-from-official-python-sdk.html#api-mapping)** - Operations use direct keyword arguments, standard Python types such as `datetime.timedelta`, and keyword-only names instead of configuration wrapper objects.
|
|
@@ -218,6 +219,7 @@ For the developer workflow to run or deploy example integration tests, see the [
|
|
|
218
219
|
- **[Deploy and Invoke](https://zhongkechen.github.io/async-durable-execution/deployment.html)** - Configure IAM, qualified function identifiers, invocations, CloudFormation, and SAM
|
|
219
220
|
- **[Using Synchronous Code](https://zhongkechen.github.io/async-durable-execution/using-synchronous-code.html)** - Wrap existing synchronous business logic and blocking clients safely
|
|
220
221
|
- **[Advanced Usage](https://zhongkechen.github.io/async-durable-execution/advanced-usage.html)** - Explore background operation tasks, batch completion conditions, Lambda clients, and Lambda layers
|
|
222
|
+
- **[Custom Durable Operations](https://zhongkechen.github.io/async-durable-execution/custom-operations.html)** - Build third-party durable operation libraries on the stable extension-author interface
|
|
221
223
|
- **[Runner Architecture](https://zhongkechen.github.io/async-durable-execution/runner-architecture.html)** - Local and cloud runner execution flow, components, and diagrams
|
|
222
224
|
- **[Contributing Guide](https://github.com/zhongkechen/async-durable-execution/blob/main/CONTRIBUTING.md)** - Development workflow, Hatch commands, testing, and pull request guidance
|
|
223
225
|
|
{async_durable_execution-2.2.0 → async_durable_execution-2.3.0}/async_durable_execution/__init__.py
RENAMED
|
@@ -36,13 +36,13 @@ from ._core import (
|
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
# Durable operations
|
|
39
|
-
from .
|
|
39
|
+
from ._operation.with_retry import (
|
|
40
40
|
WithRetryContext,
|
|
41
41
|
get_with_retry_context,
|
|
42
42
|
with_retry,
|
|
43
43
|
)
|
|
44
|
-
from .
|
|
45
|
-
from .
|
|
44
|
+
from ._operation.map import MapItemContext, get_map_item_context, map
|
|
45
|
+
from ._operation.flow import (
|
|
46
46
|
FlowDefinitionError,
|
|
47
47
|
FlowExecutionError,
|
|
48
48
|
FlowNode,
|
|
@@ -56,7 +56,7 @@ from ._extension.flow import (
|
|
|
56
56
|
get_node_context,
|
|
57
57
|
node,
|
|
58
58
|
)
|
|
59
|
-
from .
|
|
59
|
+
from ._operation.parallel import (
|
|
60
60
|
BatchItem,
|
|
61
61
|
BatchItemStatus,
|
|
62
62
|
BatchResult,
|
|
@@ -66,38 +66,46 @@ from ._extension.parallel import (
|
|
|
66
66
|
CompletionStatus,
|
|
67
67
|
NestingType,
|
|
68
68
|
)
|
|
69
|
-
from .
|
|
69
|
+
from ._operation.wait_for_condition import (
|
|
70
70
|
PollingStrategy,
|
|
71
71
|
WaitForConditionCheckContext,
|
|
72
72
|
WaitForConditionError,
|
|
73
73
|
get_wait_for_condition_check_context,
|
|
74
74
|
wait_for_condition,
|
|
75
75
|
)
|
|
76
|
-
from .
|
|
77
|
-
from .
|
|
78
|
-
from .
|
|
76
|
+
from ._operation.invoke import invoke
|
|
77
|
+
from ._operation.recurse import recurse
|
|
78
|
+
from ._operation.parallel import (
|
|
79
79
|
parallel,
|
|
80
80
|
)
|
|
81
|
-
from .
|
|
81
|
+
from ._operation.callback import (
|
|
82
82
|
Callback,
|
|
83
83
|
CallbackError,
|
|
84
84
|
create_callback,
|
|
85
85
|
)
|
|
86
|
-
from .
|
|
86
|
+
from ._operation.wait_for_callback import (
|
|
87
87
|
WaitForCallbackContext,
|
|
88
88
|
get_wait_for_callback_context,
|
|
89
89
|
wait_for_callback,
|
|
90
90
|
)
|
|
91
|
-
from .
|
|
92
|
-
from .
|
|
91
|
+
from ._operation.child import SummaryGenerator, run_in_child_context
|
|
92
|
+
from ._operation.step import (
|
|
93
93
|
StepContext,
|
|
94
94
|
StepInterruptedError,
|
|
95
95
|
StepSemantics,
|
|
96
96
|
get_step_context,
|
|
97
97
|
step,
|
|
98
98
|
)
|
|
99
|
-
from .
|
|
100
|
-
from .
|
|
99
|
+
from ._operation.replay_safe import now, random, timestamp, uuid
|
|
100
|
+
from ._operation.wait import wait
|
|
101
|
+
from .extension import (
|
|
102
|
+
ExtensionContext,
|
|
103
|
+
ExtensionOperation,
|
|
104
|
+
ExtensionStepFunction,
|
|
105
|
+
ExtensionStepResult,
|
|
106
|
+
ExtensionStepRetryStrategy,
|
|
107
|
+
get_extension_context,
|
|
108
|
+
)
|
|
101
109
|
from ._runner import (
|
|
102
110
|
DurableFunctionCloudTestRunner,
|
|
103
111
|
DurableFunctionLocalTestRunner,
|
|
@@ -119,6 +127,7 @@ __all__ = [
|
|
|
119
127
|
"flow",
|
|
120
128
|
"get_current_context",
|
|
121
129
|
"get_durable_context",
|
|
130
|
+
"get_extension_context",
|
|
122
131
|
"get_map_item_context",
|
|
123
132
|
"get_node_context",
|
|
124
133
|
"get_serdes_context",
|
|
@@ -159,6 +168,11 @@ __all__ = [
|
|
|
159
168
|
"DurableExecutionsError",
|
|
160
169
|
"ErrorObject",
|
|
161
170
|
"ExecutionError",
|
|
171
|
+
"ExtensionContext",
|
|
172
|
+
"ExtensionOperation",
|
|
173
|
+
"ExtensionStepFunction",
|
|
174
|
+
"ExtensionStepResult",
|
|
175
|
+
"ExtensionStepRetryStrategy",
|
|
162
176
|
"ExtendedTypeSerDes",
|
|
163
177
|
"FlowDefinitionError",
|
|
164
178
|
"FlowExecutionError",
|
|
@@ -15,6 +15,7 @@ from .config import (
|
|
|
15
15
|
from .context import (
|
|
16
16
|
DurableContext,
|
|
17
17
|
OperationContext,
|
|
18
|
+
SerDesContext,
|
|
18
19
|
bind_current_context,
|
|
19
20
|
bind_durable_definition,
|
|
20
21
|
ensure_durable_operations_allowed,
|
|
@@ -50,6 +51,7 @@ from .execution import (
|
|
|
50
51
|
durable_execution,
|
|
51
52
|
)
|
|
52
53
|
from .models import (
|
|
54
|
+
BotoSerializableModel,
|
|
53
55
|
CallbackDetails,
|
|
54
56
|
CallbackOptions,
|
|
55
57
|
CallbackTimeoutType,
|
|
@@ -64,21 +66,21 @@ from .models import (
|
|
|
64
66
|
ExecutionDetails,
|
|
65
67
|
InvocationStatus,
|
|
66
68
|
LambdaContext,
|
|
69
|
+
MappingModel,
|
|
67
70
|
Operation,
|
|
68
71
|
OperationAction,
|
|
69
72
|
OperationIdentifier,
|
|
70
73
|
OperationPayload,
|
|
71
74
|
OperationStatus,
|
|
72
75
|
OperationSubType,
|
|
76
|
+
OperationSubTypeValue,
|
|
73
77
|
OperationType,
|
|
74
78
|
OperationUpdate,
|
|
75
|
-
SerializableModel,
|
|
76
79
|
StateOutput,
|
|
77
80
|
StepDetails,
|
|
78
81
|
TimestampConverter,
|
|
79
82
|
WaitDetails,
|
|
80
83
|
WaitOptions,
|
|
81
|
-
_metadata,
|
|
82
84
|
)
|
|
83
85
|
from .serdes import (
|
|
84
86
|
DEFAULT_JSON_SERDES,
|
|
@@ -87,7 +89,6 @@ from .serdes import (
|
|
|
87
89
|
JsonSerDes,
|
|
88
90
|
PassThroughSerDes,
|
|
89
91
|
SerDes,
|
|
90
|
-
SerDesContext,
|
|
91
92
|
TypeCodecExtension,
|
|
92
93
|
TypeTag,
|
|
93
94
|
deserialize,
|
|
@@ -25,13 +25,9 @@ logger = logging.getLogger(__name__)
|
|
|
25
25
|
class LambdaApiClient(Protocol):
|
|
26
26
|
"""Minimal Lambda client surface needed by durable execution."""
|
|
27
27
|
|
|
28
|
-
def checkpoint_durable_execution(
|
|
29
|
-
self, **kwargs: Any
|
|
30
|
-
) -> Mapping[str, Any]: ... # pragma: no cover
|
|
28
|
+
def checkpoint_durable_execution(self, **kwargs: Any) -> Mapping[str, Any]: ...
|
|
31
29
|
|
|
32
|
-
def get_durable_execution_state(
|
|
33
|
-
self, **kwargs: Any
|
|
34
|
-
) -> Mapping[str, Any]: ... # pragma: no cover
|
|
30
|
+
def get_durable_execution_state(self, **kwargs: Any) -> Mapping[str, Any]: ...
|
|
35
31
|
|
|
36
32
|
|
|
37
33
|
class AsyncLambdaApiClient(Protocol):
|
|
@@ -39,11 +35,11 @@ class AsyncLambdaApiClient(Protocol):
|
|
|
39
35
|
|
|
40
36
|
def checkpoint_durable_execution(
|
|
41
37
|
self, **kwargs: Any
|
|
42
|
-
) -> Awaitable[Mapping[str, Any]]: ...
|
|
38
|
+
) -> Awaitable[Mapping[str, Any]]: ...
|
|
43
39
|
|
|
44
40
|
def get_durable_execution_state(
|
|
45
41
|
self, **kwargs: Any
|
|
46
|
-
) -> Awaitable[Mapping[str, Any]]: ...
|
|
42
|
+
) -> Awaitable[Mapping[str, Any]]: ...
|
|
47
43
|
|
|
48
44
|
|
|
49
45
|
class DurableServiceClient(Protocol):
|
|
@@ -55,7 +51,7 @@ class DurableServiceClient(Protocol):
|
|
|
55
51
|
checkpoint_token: str,
|
|
56
52
|
updates: list[OperationUpdate],
|
|
57
53
|
client_token: str | None,
|
|
58
|
-
) -> CheckpointOutput: ...
|
|
54
|
+
) -> CheckpointOutput: ...
|
|
59
55
|
|
|
60
56
|
async def get_execution_state(
|
|
61
57
|
self,
|
|
@@ -63,7 +59,7 @@ class DurableServiceClient(Protocol):
|
|
|
63
59
|
checkpoint_token: str,
|
|
64
60
|
next_marker: str,
|
|
65
61
|
max_items: int = 1000,
|
|
66
|
-
) -> StateOutput: ...
|
|
62
|
+
) -> StateOutput: ...
|
|
67
63
|
|
|
68
64
|
|
|
69
65
|
def _create_client_config() -> Config:
|
|
@@ -85,7 +85,7 @@ class _DelayStrategy:
|
|
|
85
85
|
jitter_strategy: JitterStrategy = field(default=JitterStrategy.FULL)
|
|
86
86
|
increment: Duration | None = None
|
|
87
87
|
|
|
88
|
-
def __post_init__(self):
|
|
88
|
+
def __post_init__(self) -> None:
|
|
89
89
|
self.initial_delay = duration_to_seconds(self.initial_delay, "initial_delay")
|
|
90
90
|
self.max_delay = duration_to_seconds(self.max_delay, "max_delay")
|
|
91
91
|
if self.increment is not None:
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import functools
|
|
4
4
|
import hashlib
|
|
5
5
|
import logging
|
|
6
|
+
from collections.abc import Iterator
|
|
6
7
|
from contextlib import contextmanager
|
|
7
8
|
from contextvars import ContextVar, Token
|
|
8
9
|
from dataclasses import dataclass
|
|
@@ -18,19 +19,32 @@ from .models import (
|
|
|
18
19
|
|
|
19
20
|
if TYPE_CHECKING:
|
|
20
21
|
from .models import LambdaContext
|
|
21
|
-
from .serdes import SerDesContext
|
|
22
22
|
from .state import ExecutionState
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
logger = logging.getLogger(__name__)
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class SerDesContext:
|
|
30
|
+
"""Context for serialization operations."""
|
|
31
|
+
|
|
32
|
+
operation_id: str = ""
|
|
33
|
+
durable_execution_arn: str = ""
|
|
34
|
+
recursive_level: int = 0
|
|
35
|
+
|
|
36
|
+
|
|
28
37
|
class OperationIdGenerator:
|
|
29
38
|
"""Generate deterministic operation ids within a durable execution scope."""
|
|
30
39
|
|
|
31
40
|
def __init__(self, prefix: str | None) -> None:
|
|
32
41
|
self._prefix = prefix
|
|
33
42
|
self._counter = 0
|
|
43
|
+
self._claimed_local_ids: set[str] = set()
|
|
44
|
+
self._unconsumed_reservations: dict[str, bool] = {}
|
|
45
|
+
self._unconsumed_checkpoint_count = 0
|
|
46
|
+
self._reservation_selection_started = False
|
|
47
|
+
self._replay_frontier_pending = False
|
|
34
48
|
|
|
35
49
|
def increment(self) -> int:
|
|
36
50
|
self._counter += 1
|
|
@@ -39,16 +53,89 @@ class OperationIdGenerator:
|
|
|
39
53
|
def get_current(self) -> int:
|
|
40
54
|
return self._counter
|
|
41
55
|
|
|
42
|
-
def
|
|
43
|
-
"""
|
|
56
|
+
def _create_id(self, value: str) -> str:
|
|
57
|
+
"""Hash one context-local identity value."""
|
|
44
58
|
prefix = self._prefix
|
|
45
|
-
step_id = f"{prefix}-{
|
|
59
|
+
step_id = f"{prefix}-{value}" if prefix else value
|
|
46
60
|
return hashlib.blake2b(step_id.encode()).hexdigest()[:64]
|
|
47
61
|
|
|
62
|
+
def _create_id_for_local_id(self, local_id: str) -> str:
|
|
63
|
+
"""Generate an id in the caller-provided local-id namespace."""
|
|
64
|
+
return self._create_id(f"local:{local_id}")
|
|
65
|
+
|
|
66
|
+
def _create_step_id_for_logical_step(self, step: int) -> str:
|
|
67
|
+
"""Generate the stable operation id for a logical step."""
|
|
68
|
+
return self._create_id(str(step))
|
|
69
|
+
|
|
48
70
|
def create_step_id(self) -> str:
|
|
49
71
|
"""Generate an operation id and advance the logical step counter."""
|
|
50
72
|
return self._create_step_id_for_logical_step(self.increment())
|
|
51
73
|
|
|
74
|
+
def create_step_id_for_local_id(self, local_id: str) -> str:
|
|
75
|
+
"""Generate an operation id from a stable caller-provided local id."""
|
|
76
|
+
if not isinstance(local_id, str):
|
|
77
|
+
msg = "local_operation_id must be a string"
|
|
78
|
+
raise TypeError(msg)
|
|
79
|
+
if not local_id.strip():
|
|
80
|
+
msg = "local_operation_id must not be blank"
|
|
81
|
+
raise ValueError(msg)
|
|
82
|
+
if self._reservation_selection_started:
|
|
83
|
+
msg = (
|
|
84
|
+
"local_operation_id reservations must be created before any "
|
|
85
|
+
"reserved operation is selected"
|
|
86
|
+
)
|
|
87
|
+
raise RuntimeError(msg)
|
|
88
|
+
if local_id in self._claimed_local_ids:
|
|
89
|
+
msg = f"local_operation_id is already reserved: {local_id}"
|
|
90
|
+
raise ValueError(msg)
|
|
91
|
+
|
|
92
|
+
self._claimed_local_ids.add(local_id)
|
|
93
|
+
return self._create_id_for_local_id(local_id)
|
|
94
|
+
|
|
95
|
+
def _register_reservation(
|
|
96
|
+
self,
|
|
97
|
+
operation_id: str,
|
|
98
|
+
*,
|
|
99
|
+
has_checkpoint: bool,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Track an allocated reservation until workflow code selects it."""
|
|
102
|
+
previous = self._unconsumed_reservations.get(operation_id)
|
|
103
|
+
if operation_id in self._unconsumed_reservations:
|
|
104
|
+
if previous == has_checkpoint:
|
|
105
|
+
return
|
|
106
|
+
if previous:
|
|
107
|
+
self._unconsumed_checkpoint_count -= 1
|
|
108
|
+
self._unconsumed_reservations[operation_id] = has_checkpoint
|
|
109
|
+
if has_checkpoint:
|
|
110
|
+
self._unconsumed_checkpoint_count += 1
|
|
111
|
+
|
|
112
|
+
def _consume_reservation(self, operation_id: str) -> None:
|
|
113
|
+
"""Discard a reservation after workflow code selects it."""
|
|
114
|
+
self._mark_reservation_selected()
|
|
115
|
+
has_checkpoint = self._unconsumed_reservations.pop(operation_id, False)
|
|
116
|
+
if has_checkpoint:
|
|
117
|
+
self._unconsumed_checkpoint_count -= 1
|
|
118
|
+
|
|
119
|
+
def _mark_reservation_selected(self) -> None:
|
|
120
|
+
"""Prevent explicit local ids from being registered after selection."""
|
|
121
|
+
self._reservation_selection_started = True
|
|
122
|
+
|
|
123
|
+
def _has_unconsumed_checkpoint(self) -> bool:
|
|
124
|
+
"""Return whether any allocated reservation still has replay history."""
|
|
125
|
+
return self._unconsumed_checkpoint_count > 0
|
|
126
|
+
|
|
127
|
+
def _mark_replay_frontier(self) -> None:
|
|
128
|
+
"""Remember that replay ended immediately before a virtual scope."""
|
|
129
|
+
self._replay_frontier_pending = True
|
|
130
|
+
|
|
131
|
+
def _clear_replay_frontier(self) -> None:
|
|
132
|
+
"""Clear a replay frontier after the next operation boundary is known."""
|
|
133
|
+
self._replay_frontier_pending = False
|
|
134
|
+
|
|
135
|
+
def _is_replay_frontier_pending(self) -> bool:
|
|
136
|
+
"""Return whether a virtual scope may still contain flattened history."""
|
|
137
|
+
return self._replay_frontier_pending
|
|
138
|
+
|
|
52
139
|
|
|
53
140
|
@dataclass(frozen=True)
|
|
54
141
|
class OperationContext:
|
|
@@ -113,7 +200,11 @@ class DurableContext(OperationContext):
|
|
|
113
200
|
)
|
|
114
201
|
|
|
115
202
|
def create_child_context(
|
|
116
|
-
self,
|
|
203
|
+
self,
|
|
204
|
+
operation_id: str,
|
|
205
|
+
*,
|
|
206
|
+
is_virtual: bool = False,
|
|
207
|
+
replaying: bool | None = None,
|
|
117
208
|
) -> DurableContext:
|
|
118
209
|
"""Create a child context for the given operation."""
|
|
119
210
|
child_parent_id = self.parent_id if is_virtual else operation_id
|
|
@@ -130,7 +221,7 @@ class DurableContext(OperationContext):
|
|
|
130
221
|
parent_id=child_parent_id,
|
|
131
222
|
),
|
|
132
223
|
step_id_prefix=operation_id,
|
|
133
|
-
replaying=self.is_replaying(),
|
|
224
|
+
replaying=self.is_replaying() if replaying is None else replaying,
|
|
134
225
|
)
|
|
135
226
|
|
|
136
227
|
def is_replaying(self) -> bool:
|
|
@@ -139,6 +230,18 @@ class DurableContext(OperationContext):
|
|
|
139
230
|
|
|
140
231
|
def _set_replay_status_new(self) -> None:
|
|
141
232
|
object.__setattr__(self, "replaying", False)
|
|
233
|
+
self.step_counter._clear_replay_frontier() # noqa: SLF001
|
|
234
|
+
|
|
235
|
+
def _set_replay_status_frontier(self) -> None:
|
|
236
|
+
"""End parent replay while retaining a snapshot for a virtual child."""
|
|
237
|
+
object.__setattr__(self, "replaying", False)
|
|
238
|
+
self.step_counter._mark_replay_frontier() # noqa: SLF001
|
|
239
|
+
|
|
240
|
+
def _virtual_child_replay_snapshot(self) -> bool:
|
|
241
|
+
"""Return replay state including flattened history past the frontier."""
|
|
242
|
+
return (
|
|
243
|
+
self.is_replaying() or self.step_counter._is_replay_frontier_pending() # noqa: SLF001
|
|
244
|
+
)
|
|
142
245
|
|
|
143
246
|
def _peek_next_operation_id(self) -> str:
|
|
144
247
|
return self.step_counter._create_step_id_for_logical_step( # noqa: SLF001
|
|
@@ -146,13 +249,24 @@ class DurableContext(OperationContext):
|
|
|
146
249
|
)
|
|
147
250
|
|
|
148
251
|
def _next_operation_result(self) -> Operation | None:
|
|
149
|
-
return self.
|
|
252
|
+
return self._operation_result(self._peek_next_operation_id())
|
|
253
|
+
|
|
254
|
+
def _operation_result(self, operation_id: str) -> Operation | None:
|
|
255
|
+
return self.execution_state.operations.get(operation_id)
|
|
150
256
|
|
|
151
257
|
def _next_operation_exists(self) -> bool:
|
|
152
258
|
return self._next_operation_result() is not None
|
|
153
259
|
|
|
260
|
+
def _next_reserved_or_sequential_operation_exists(self) -> bool:
|
|
261
|
+
if self.step_counter._has_unconsumed_checkpoint(): # noqa: SLF001
|
|
262
|
+
return True
|
|
263
|
+
return self._next_operation_exists()
|
|
264
|
+
|
|
154
265
|
def _next_operation_is_terminal_checkpoint(self) -> bool:
|
|
155
|
-
|
|
266
|
+
return self._operation_is_terminal_checkpoint(self._peek_next_operation_id())
|
|
267
|
+
|
|
268
|
+
def _operation_is_terminal_checkpoint(self, operation_id: str) -> bool:
|
|
269
|
+
operation = self._operation_result(operation_id)
|
|
156
270
|
if operation is None:
|
|
157
271
|
return False
|
|
158
272
|
return operation.status in {
|
|
@@ -164,20 +278,39 @@ class DurableContext(OperationContext):
|
|
|
164
278
|
}
|
|
165
279
|
|
|
166
280
|
@contextmanager
|
|
167
|
-
def _replay_aware(
|
|
168
|
-
|
|
281
|
+
def _replay_aware(
|
|
282
|
+
self,
|
|
283
|
+
*,
|
|
284
|
+
operation_id: str | None = None,
|
|
285
|
+
executes_user_code: bool = False,
|
|
286
|
+
consume_reservation: bool = True,
|
|
287
|
+
) -> Iterator[None]:
|
|
288
|
+
"""Update replay status around one durable operation.
|
|
289
|
+
|
|
290
|
+
`operation_id` identifies an operation that was allocated before entering
|
|
291
|
+
this scope. Pre-allocated reservations are tracked independently from the
|
|
292
|
+
sequential counter so launch order does not end replay prematurely.
|
|
293
|
+
"""
|
|
169
294
|
was_replaying = self.is_replaying()
|
|
170
|
-
|
|
171
|
-
|
|
295
|
+
self.step_counter._clear_replay_frontier() # noqa: SLF001
|
|
296
|
+
current_operation_id = operation_id or self._peek_next_operation_id()
|
|
297
|
+
if operation_id is not None and consume_reservation:
|
|
298
|
+
self.step_counter._consume_reservation(operation_id) # noqa: SLF001
|
|
299
|
+
current_exists = was_replaying and (
|
|
300
|
+
self._operation_result(current_operation_id) is not None
|
|
301
|
+
)
|
|
302
|
+
current_terminal = was_replaying and self._operation_is_terminal_checkpoint(
|
|
303
|
+
current_operation_id
|
|
304
|
+
)
|
|
172
305
|
flip_after = (
|
|
173
306
|
was_replaying
|
|
174
307
|
and not executes_user_code
|
|
175
|
-
and
|
|
176
|
-
and not
|
|
308
|
+
and current_exists
|
|
309
|
+
and not current_terminal
|
|
177
310
|
)
|
|
178
311
|
|
|
179
312
|
if was_replaying and (
|
|
180
|
-
not
|
|
313
|
+
not current_exists or (executes_user_code and not current_terminal)
|
|
181
314
|
):
|
|
182
315
|
self._set_replay_status_new()
|
|
183
316
|
|
|
@@ -185,9 +318,15 @@ class DurableContext(OperationContext):
|
|
|
185
318
|
yield
|
|
186
319
|
finally:
|
|
187
320
|
if flip_after:
|
|
188
|
-
self.
|
|
189
|
-
elif self.is_replaying()
|
|
190
|
-
|
|
321
|
+
self._set_replay_status_frontier()
|
|
322
|
+
elif self.is_replaying():
|
|
323
|
+
next_operation_exists = (
|
|
324
|
+
self._next_reserved_or_sequential_operation_exists()
|
|
325
|
+
if operation_id is not None
|
|
326
|
+
else self._next_operation_exists()
|
|
327
|
+
)
|
|
328
|
+
if not next_operation_exists:
|
|
329
|
+
self._set_replay_status_frontier()
|
|
191
330
|
|
|
192
331
|
|
|
193
332
|
_current_context: ContextVar = ContextVar(
|
|
@@ -215,7 +354,7 @@ def ensure_durable_operations_allowed(operation_name: str) -> None:
|
|
|
215
354
|
|
|
216
355
|
|
|
217
356
|
@contextmanager
|
|
218
|
-
def bind_durable_definition(operation_name: str):
|
|
357
|
+
def bind_durable_definition(operation_name: str) -> Iterator[None]:
|
|
219
358
|
"""Mark a synchronous definition phase in the current context."""
|
|
220
359
|
token = _durable_definition_operation.set(operation_name)
|
|
221
360
|
try:
|
|
@@ -234,7 +373,7 @@ def reset_current_context(token: Token) -> None:
|
|
|
234
373
|
_current_context.reset(token)
|
|
235
374
|
|
|
236
375
|
|
|
237
|
-
def get_current_context():
|
|
376
|
+
def get_current_context() -> OperationContext | SerDesContext:
|
|
238
377
|
"""Return the currently active durable execution context.
|
|
239
378
|
|
|
240
379
|
Raises:
|
|
@@ -271,7 +410,9 @@ def get_durable_context() -> DurableContext:
|
|
|
271
410
|
|
|
272
411
|
|
|
273
412
|
@contextmanager
|
|
274
|
-
def bind_current_context(
|
|
413
|
+
def bind_current_context(
|
|
414
|
+
context: OperationContext | SerDesContext,
|
|
415
|
+
) -> Iterator[None]:
|
|
275
416
|
"""Temporarily bind the supplied durable context while invoking user code."""
|
|
276
417
|
token = set_current_context(context)
|
|
277
418
|
try:
|
|
@@ -238,7 +238,7 @@ class DurableExecutionsError(Exception):
|
|
|
238
238
|
class UnrecoverableError(DurableExecutionsError):
|
|
239
239
|
"""Base class for errors that terminate execution."""
|
|
240
240
|
|
|
241
|
-
def __init__(self, message: str, termination_reason: TerminationReason):
|
|
241
|
+
def __init__(self, message: str, termination_reason: TerminationReason) -> None:
|
|
242
242
|
super().__init__(message)
|
|
243
243
|
self.termination_reason = termination_reason
|
|
244
244
|
|
|
@@ -250,7 +250,7 @@ class ExecutionError(UnrecoverableError):
|
|
|
250
250
|
self,
|
|
251
251
|
message: str,
|
|
252
252
|
termination_reason: TerminationReason = TerminationReason.EXECUTION_ERROR,
|
|
253
|
-
):
|
|
253
|
+
) -> None:
|
|
254
254
|
super().__init__(message, termination_reason)
|
|
255
255
|
|
|
256
256
|
|
|
@@ -263,7 +263,7 @@ class _RestoredExecutionError(ExecutionError):
|
|
|
263
263
|
*,
|
|
264
264
|
original_error_type: str,
|
|
265
265
|
termination_reason: TerminationReason,
|
|
266
|
-
):
|
|
266
|
+
) -> None:
|
|
267
267
|
super().__init__(message, termination_reason)
|
|
268
268
|
self.original_error_type = original_error_type
|
|
269
269
|
|
|
@@ -275,7 +275,7 @@ class InvocationError(UnrecoverableError):
|
|
|
275
275
|
self,
|
|
276
276
|
message: str,
|
|
277
277
|
termination_reason: TerminationReason = TerminationReason.INVOCATION_ERROR,
|
|
278
|
-
):
|
|
278
|
+
) -> None:
|
|
279
279
|
super().__init__(message, termination_reason)
|
|
280
280
|
|
|
281
281
|
def is_retryable(self) -> bool:
|
|
@@ -318,7 +318,7 @@ class BotoClientError(InvocationError):
|
|
|
318
318
|
error: AwsErrorObj | None = None,
|
|
319
319
|
response_metadata: AwsErrorMetadata | None = None,
|
|
320
320
|
termination_reason=TerminationReason.INVOCATION_ERROR,
|
|
321
|
-
):
|
|
321
|
+
) -> None:
|
|
322
322
|
super().__init__(message=message, termination_reason=termination_reason)
|
|
323
323
|
self.error: AwsErrorObj | None = error
|
|
324
324
|
self.response_metadata: AwsErrorMetadata | None = response_metadata
|
|
@@ -409,7 +409,7 @@ class _RestoredInvocationError(InvocationError):
|
|
|
409
409
|
error_category: DurableApiErrorCategory | None = None,
|
|
410
410
|
error: AwsErrorObj | None = None,
|
|
411
411
|
response_metadata: AwsErrorMetadata | None = None,
|
|
412
|
-
):
|
|
412
|
+
) -> None:
|
|
413
413
|
super().__init__(message, termination_reason)
|
|
414
414
|
self.original_error_type = original_error_type
|
|
415
415
|
self.retryable = retryable
|
|
@@ -643,7 +643,7 @@ def _restore_sdk_control_error(
|
|
|
643
643
|
class NonDeterministicExecutionError(ExecutionError):
|
|
644
644
|
"""Error when execution is non-deterministic."""
|
|
645
645
|
|
|
646
|
-
def __init__(self, message: str, step_id: str | None = None):
|
|
646
|
+
def __init__(self, message: str, step_id: str | None = None) -> None:
|
|
647
647
|
super().__init__(message, TerminationReason.NON_DETERMINISTIC_EXECUTION)
|
|
648
648
|
self.step_id = step_id
|
|
649
649
|
|
|
@@ -657,7 +657,7 @@ class CheckpointError(BotoClientError):
|
|
|
657
657
|
error_category: DurableApiErrorCategory = DurableApiErrorCategory.INVOCATION,
|
|
658
658
|
error: AwsErrorObj | None = None,
|
|
659
659
|
response_metadata: AwsErrorMetadata | None = None,
|
|
660
|
-
):
|
|
660
|
+
) -> None:
|
|
661
661
|
super().__init__(
|
|
662
662
|
message,
|
|
663
663
|
error_category,
|
|
@@ -680,7 +680,7 @@ class GetExecutionStateError(BotoClientError):
|
|
|
680
680
|
error_category: DurableApiErrorCategory = DurableApiErrorCategory.INVOCATION,
|
|
681
681
|
error: AwsErrorObj | None = None,
|
|
682
682
|
response_metadata: AwsErrorMetadata | None = None,
|
|
683
|
-
):
|
|
683
|
+
) -> None:
|
|
684
684
|
super().__init__(
|
|
685
685
|
message,
|
|
686
686
|
error_category,
|
|
@@ -740,7 +740,7 @@ class BackgroundThreadError(BaseException):
|
|
|
740
740
|
source_exception: The original exception from the background thread
|
|
741
741
|
"""
|
|
742
742
|
|
|
743
|
-
def __init__(self, message: str, source_exception: Exception):
|
|
743
|
+
def __init__(self, message: str, source_exception: Exception) -> None:
|
|
744
744
|
super().__init__(message)
|
|
745
745
|
self.source_exception = source_exception
|
|
746
746
|
|
|
@@ -752,7 +752,7 @@ class OrphanedChildException(BaseException):
|
|
|
752
752
|
with broad exception handlers like ``except Exception``.
|
|
753
753
|
"""
|
|
754
754
|
|
|
755
|
-
def __init__(self, message: str, operation_id: str):
|
|
755
|
+
def __init__(self, message: str, operation_id: str) -> None:
|
|
756
756
|
super().__init__(message)
|
|
757
757
|
self.operation_id = operation_id
|
|
758
758
|
|
|
@@ -764,7 +764,7 @@ class SuspendExecution(BaseException):
|
|
|
764
764
|
KeyboardInterrupt or SystemExit.
|
|
765
765
|
"""
|
|
766
766
|
|
|
767
|
-
def __init__(self, message: str):
|
|
767
|
+
def __init__(self, message: str) -> None:
|
|
768
768
|
super().__init__(message)
|
|
769
769
|
|
|
770
770
|
|
|
@@ -777,7 +777,7 @@ class TimedSuspendExecution(SuspendExecution):
|
|
|
777
777
|
scheduled_timestamp (float): Unix timestamp in seconds at which to resume.
|
|
778
778
|
"""
|
|
779
779
|
|
|
780
|
-
def __init__(self, message: str, scheduled_timestamp: float):
|
|
780
|
+
def __init__(self, message: str, scheduled_timestamp: float) -> None:
|
|
781
781
|
super().__init__(message)
|
|
782
782
|
self.scheduled_timestamp = scheduled_timestamp
|
|
783
783
|
|