async-durable-execution 2.0.0a1__tar.gz → 2.0.0a2__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.0.0a1 → async_durable_execution-2.0.0a2}/.gitignore +1 -1
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/PKG-INFO +33 -24
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/README.md +28 -22
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/VERSION.py +1 -1
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/pyproject.toml +8 -4
- async_durable_execution-2.0.0a2/src/async_durable_execution/__init__.py +132 -0
- async_durable_execution-2.0.0a2/src/async_durable_execution/async_tools.py +95 -0
- async_durable_execution-2.0.0a2/src/async_durable_execution/client.py +94 -0
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/config.py +252 -65
- async_durable_execution-2.0.0a2/src/async_durable_execution/context.py +68 -0
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/exceptions.py +5 -27
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/execution.py +82 -83
- async_durable_execution-2.0.0a2/src/async_durable_execution/logger.py +139 -0
- async_durable_execution-2.0.0a2/src/async_durable_execution/models.py +1478 -0
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/base.py +35 -8
- async_durable_execution-2.0.0a2/src/async_durable_execution/operation/callback.py +397 -0
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/child.py +175 -26
- async_durable_execution-2.0.0a2/src/async_durable_execution/operation/concurrency.py +382 -0
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/invoke.py +50 -15
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/map.py +74 -26
- async_durable_execution-2.0.0a2/src/async_durable_execution/operation/parallel.py +277 -0
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/step.py +117 -41
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/wait.py +45 -9
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/wait_for_condition.py +88 -43
- async_durable_execution-2.0.0a2/src/async_durable_execution/operation/with_retry.py +61 -0
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/plugin.py +109 -79
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/serdes.py +5 -8
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/state.py +267 -242
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/suspend.py +4 -4
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/types.py +51 -101
- async_durable_execution-2.0.0a2/tests_async_durable_execution/__init__.py +1 -0
- async_durable_execution-2.0.0a2/tests_async_durable_execution/async_tools_test.py +60 -0
- async_durable_execution-2.0.0a2/tests_async_durable_execution/client_test.py +486 -0
- async_durable_execution-2.0.0a1/tests/retries_test.py → async_durable_execution-2.0.0a2/tests_async_durable_execution/config_retry_test.py +73 -172
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/config_test.py +5 -38
- async_durable_execution-2.0.0a1/tests/waits_test.py → async_durable_execution-2.0.0a2/tests_async_durable_execution/config_wait_test.py +66 -114
- async_durable_execution-2.0.0a2/tests_async_durable_execution/config_with_retry_test.py +37 -0
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/context_test.py +828 -648
- async_durable_execution-2.0.0a1/tests/with_retry_test.py → async_durable_execution-2.0.0a2/tests_async_durable_execution/context_with_retry_test.py +125 -114
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/e2e/checkpoint_response_int_test.py +116 -85
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/e2e/execution_int_test.py +176 -90
- async_durable_execution-2.0.0a2/tests_async_durable_execution/e2e/map_with_concurrent_waits_int_test.py +202 -0
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/exceptions_test.py +0 -17
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/execution_test.py +385 -290
- async_durable_execution-2.0.0a2/tests_async_durable_execution/logger_test.py +319 -0
- async_durable_execution-2.0.0a2/tests_async_durable_execution/models_retry_test.py +19 -0
- async_durable_execution-2.0.0a1/tests/lambda_service_test.py → async_durable_execution-2.0.0a2/tests_async_durable_execution/models_test.py +238 -764
- async_durable_execution-2.0.0a2/tests_async_durable_execution/models_wait_test.py +37 -0
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/base_test.py +42 -33
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/callback_test.py +413 -306
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/child_test.py +114 -89
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution/operation}/concurrency_test.py +548 -618
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/invoke_test.py +137 -121
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/map_test.py +346 -205
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/parallel_test.py +370 -203
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/step_test.py +203 -161
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/wait_for_condition_test.py +207 -235
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/wait_test.py +42 -37
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/plugin_test.py +100 -132
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/serdes_test.py +15 -59
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/state_test.py +726 -1150
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/suspend_test.py +4 -2
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/test_helpers.py +9 -4
- async_durable_execution-2.0.0a2/tests_async_durable_execution/types_test.py +217 -0
- async_durable_execution-2.0.0a1/tests/durable_executions_python_language_sdk_test.py → async_durable_execution-2.0.0a2/tests_async_durable_execution/version_test.py +0 -5
- async_durable_execution-2.0.0a1/src/async_durable_execution/__init__.py +0 -50
- async_durable_execution-2.0.0a1/src/async_durable_execution/async_tools.py +0 -49
- async_durable_execution-2.0.0a1/src/async_durable_execution/concurrency/executor.py +0 -489
- async_durable_execution-2.0.0a1/src/async_durable_execution/concurrency/models.py +0 -540
- async_durable_execution-2.0.0a1/src/async_durable_execution/context.py +0 -765
- async_durable_execution-2.0.0a1/src/async_durable_execution/identifier.py +0 -24
- async_durable_execution-2.0.0a1/src/async_durable_execution/lambda_service.py +0 -1219
- async_durable_execution-2.0.0a1/src/async_durable_execution/logger.py +0 -131
- async_durable_execution-2.0.0a1/src/async_durable_execution/operation/callback.py +0 -182
- async_durable_execution-2.0.0a1/src/async_durable_execution/operation/parallel.py +0 -144
- async_durable_execution-2.0.0a1/src/async_durable_execution/retries.py +0 -279
- async_durable_execution-2.0.0a1/src/async_durable_execution/threading.py +0 -222
- async_durable_execution-2.0.0a1/src/async_durable_execution/waits.py +0 -130
- async_durable_execution-2.0.0a1/tests/async_tools_test.py +0 -22
- async_durable_execution-2.0.0a1/tests/e2e/__init__.py +0 -0
- async_durable_execution-2.0.0a1/tests/e2e/map_with_concurrent_waits_int_test.py +0 -202
- async_durable_execution-2.0.0a1/tests/logger_test.py +0 -424
- async_durable_execution-2.0.0a1/tests/operation/__init__.py +0 -0
- async_durable_execution-2.0.0a1/tests/threading_test.py +0 -954
- async_durable_execution-2.0.0a1/tests/types_test.py +0 -198
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/LICENSE +0 -0
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/NOTICE +0 -0
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/.gitignore +0 -0
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/__about__.py +0 -0
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/__init__.py +0 -0
- {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/py.typed +0 -0
- {async_durable_execution-2.0.0a1/src/async_durable_execution/concurrency → async_durable_execution-2.0.0a2/tests_async_durable_execution/e2e}/__init__.py +0 -0
- {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution/operation}/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: async-durable-execution
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.0a2
|
|
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
|
|
@@ -9,14 +9,17 @@ Author: Zhongke Chen
|
|
|
9
9
|
License-Expression: Apache-2.0
|
|
10
10
|
Classifier: Development Status :: 4 - Beta
|
|
11
11
|
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
16
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
17
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
17
18
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
18
|
-
Requires-Python: >=3.
|
|
19
|
+
Requires-Python: >=3.10
|
|
19
20
|
Requires-Dist: boto3>=1.42.1
|
|
21
|
+
Provides-Extra: typing
|
|
22
|
+
Requires-Dist: boto3-stubs[lambda]>=1.42.1; extra == 'typing'
|
|
20
23
|
Description-Content-Type: text/markdown
|
|
21
24
|
|
|
22
25
|
# Async Durable Execution for Python
|
|
@@ -33,17 +36,17 @@ Build reliable, long-running AWS Lambda workflows with checkpointed steps, waits
|
|
|
33
36
|
|
|
34
37
|
This package is distributed from a community-maintained fork of the original Apache-2.0 licensed AWS project and continues under Apache License 2.0 with upstream notices preserved.
|
|
35
38
|
|
|
36
|
-
This fork is specifically focused on making async Python work naturally with durable functions. The public API remains synchronous at the durable operation boundary, but
|
|
39
|
+
This fork is specifically focused on making async Python work naturally with durable functions. The public API remains synchronous at the durable operation boundary, but user-provided durable callables must now use `async def` for handlers, steps, child contexts, callback submitters, and condition checks.
|
|
37
40
|
|
|
38
41
|
## ✨ Key Features
|
|
39
42
|
|
|
40
|
-
- **Async-
|
|
43
|
+
- **Async-only user callables** - Durable handlers and user-provided durable callbacks must use `async def`
|
|
41
44
|
- **Automatic checkpointing** - Resume execution after Lambda pauses or restarts
|
|
42
45
|
- **Durable steps** - Run work with retry strategies and deterministic replay
|
|
43
46
|
- **Waits and callbacks** - Pause for time or external signals without blocking Lambda
|
|
44
47
|
- **Parallel and map operations** - Fan out work with configurable completion criteria
|
|
45
48
|
- **Child contexts** - Structure complex workflows into isolated subflows
|
|
46
|
-
- **Replay-safe logging** - Use `
|
|
49
|
+
- **Replay-safe logging** - Use standard `logging` loggers enriched by the durable context filter
|
|
47
50
|
- **Local and cloud testing** - Validate workflows with the testing SDK
|
|
48
51
|
- **Async Python support** - Use `async def` for handlers, steps, child contexts, callback submitters, and wait-for-condition checks
|
|
49
52
|
|
|
@@ -57,7 +60,8 @@ This fork is specifically focused on making async Python work naturally with dur
|
|
|
57
60
|
|
|
58
61
|
## 🚀 Quick Start
|
|
59
62
|
|
|
60
|
-
This fork
|
|
63
|
+
This fork now requires async callables for all user-provided durable code.
|
|
64
|
+
Requires Python 3.10 or newer.
|
|
61
65
|
|
|
62
66
|
Install the execution SDK:
|
|
63
67
|
|
|
@@ -69,61 +73,66 @@ Create a durable Lambda handler:
|
|
|
69
73
|
|
|
70
74
|
```python
|
|
71
75
|
import asyncio
|
|
76
|
+
import logging
|
|
77
|
+
from datetime import timedelta
|
|
72
78
|
|
|
73
79
|
from async_durable_execution import (
|
|
74
|
-
DurableContext,
|
|
75
|
-
StepContext,
|
|
76
|
-
durable_execution,
|
|
77
80
|
durable_step,
|
|
81
|
+
durable_execution,
|
|
82
|
+
step,
|
|
83
|
+
wait,
|
|
78
84
|
)
|
|
79
|
-
|
|
85
|
+
|
|
86
|
+
logger = logging.getLogger(__name__)
|
|
80
87
|
|
|
81
88
|
|
|
82
89
|
@durable_step
|
|
83
|
-
async def validate_order(
|
|
90
|
+
async def validate_order(order_id: str) -> dict:
|
|
84
91
|
await asyncio.sleep(0)
|
|
85
|
-
|
|
92
|
+
logger.info("Validating order", extra={"order_id": order_id})
|
|
86
93
|
return {"order_id": order_id, "valid": True}
|
|
87
94
|
|
|
88
95
|
|
|
89
96
|
@durable_execution
|
|
90
|
-
async def handler(event: dict
|
|
97
|
+
async def handler(event: dict) -> dict:
|
|
91
98
|
order_id = event["order_id"]
|
|
92
|
-
|
|
99
|
+
logger.info("Starting workflow", extra={"order_id": order_id})
|
|
93
100
|
|
|
94
|
-
validation =
|
|
101
|
+
validation = await step(validate_order(order_id), name="validate_order")
|
|
95
102
|
if not validation["valid"]:
|
|
96
103
|
return {"status": "rejected", "order_id": order_id}
|
|
97
104
|
|
|
98
105
|
# simulate approval (real world: use wait_for_callback)
|
|
99
|
-
|
|
106
|
+
await wait(duration=timedelta(seconds=5), name="await_confirmation")
|
|
100
107
|
|
|
101
108
|
return {"status": "approved", "order_id": order_id}
|
|
102
109
|
```
|
|
103
110
|
|
|
104
|
-
Async callables are
|
|
111
|
+
Async callables are required anywhere the SDK accepts user code, including `map()` item functions, `parallel()` branches, child contexts, callback submitters, and wait-for-condition checks. Durable context operations are awaitable and run on the same event loop as your handler:
|
|
105
112
|
|
|
106
113
|
```python
|
|
107
114
|
import asyncio
|
|
115
|
+
import logging
|
|
108
116
|
|
|
109
117
|
from async_durable_execution import (
|
|
110
|
-
DurableContext,
|
|
111
|
-
StepContext,
|
|
112
|
-
durable_execution,
|
|
113
118
|
durable_step,
|
|
119
|
+
durable_execution,
|
|
120
|
+
step,
|
|
114
121
|
)
|
|
115
122
|
|
|
123
|
+
logger = logging.getLogger(__name__)
|
|
124
|
+
|
|
116
125
|
|
|
117
126
|
@durable_step
|
|
118
|
-
async def fetch_order(
|
|
127
|
+
async def fetch_order(order_id: str) -> dict:
|
|
119
128
|
await asyncio.sleep(0)
|
|
120
|
-
|
|
129
|
+
logger.info("Fetched order", extra={"order_id": order_id})
|
|
121
130
|
return {"order_id": order_id, "status": "ready"}
|
|
122
131
|
|
|
123
132
|
|
|
124
133
|
@durable_execution
|
|
125
|
-
async def handler(event: dict
|
|
126
|
-
order =
|
|
134
|
+
async def handler(event: dict) -> dict:
|
|
135
|
+
order = await step(fetch_order(event["order_id"]), name="fetch_order")
|
|
127
136
|
return {"order": order}
|
|
128
137
|
```
|
|
129
138
|
|
|
@@ -12,17 +12,17 @@ Build reliable, long-running AWS Lambda workflows with checkpointed steps, waits
|
|
|
12
12
|
|
|
13
13
|
This package is distributed from a community-maintained fork of the original Apache-2.0 licensed AWS project and continues under Apache License 2.0 with upstream notices preserved.
|
|
14
14
|
|
|
15
|
-
This fork is specifically focused on making async Python work naturally with durable functions. The public API remains synchronous at the durable operation boundary, but
|
|
15
|
+
This fork is specifically focused on making async Python work naturally with durable functions. The public API remains synchronous at the durable operation boundary, but user-provided durable callables must now use `async def` for handlers, steps, child contexts, callback submitters, and condition checks.
|
|
16
16
|
|
|
17
17
|
## ✨ Key Features
|
|
18
18
|
|
|
19
|
-
- **Async-
|
|
19
|
+
- **Async-only user callables** - Durable handlers and user-provided durable callbacks must use `async def`
|
|
20
20
|
- **Automatic checkpointing** - Resume execution after Lambda pauses or restarts
|
|
21
21
|
- **Durable steps** - Run work with retry strategies and deterministic replay
|
|
22
22
|
- **Waits and callbacks** - Pause for time or external signals without blocking Lambda
|
|
23
23
|
- **Parallel and map operations** - Fan out work with configurable completion criteria
|
|
24
24
|
- **Child contexts** - Structure complex workflows into isolated subflows
|
|
25
|
-
- **Replay-safe logging** - Use `
|
|
25
|
+
- **Replay-safe logging** - Use standard `logging` loggers enriched by the durable context filter
|
|
26
26
|
- **Local and cloud testing** - Validate workflows with the testing SDK
|
|
27
27
|
- **Async Python support** - Use `async def` for handlers, steps, child contexts, callback submitters, and wait-for-condition checks
|
|
28
28
|
|
|
@@ -36,7 +36,8 @@ This fork is specifically focused on making async Python work naturally with dur
|
|
|
36
36
|
|
|
37
37
|
## 🚀 Quick Start
|
|
38
38
|
|
|
39
|
-
This fork
|
|
39
|
+
This fork now requires async callables for all user-provided durable code.
|
|
40
|
+
Requires Python 3.10 or newer.
|
|
40
41
|
|
|
41
42
|
Install the execution SDK:
|
|
42
43
|
|
|
@@ -48,61 +49,66 @@ Create a durable Lambda handler:
|
|
|
48
49
|
|
|
49
50
|
```python
|
|
50
51
|
import asyncio
|
|
52
|
+
import logging
|
|
53
|
+
from datetime import timedelta
|
|
51
54
|
|
|
52
55
|
from async_durable_execution import (
|
|
53
|
-
DurableContext,
|
|
54
|
-
StepContext,
|
|
55
|
-
durable_execution,
|
|
56
56
|
durable_step,
|
|
57
|
+
durable_execution,
|
|
58
|
+
step,
|
|
59
|
+
wait,
|
|
57
60
|
)
|
|
58
|
-
|
|
61
|
+
|
|
62
|
+
logger = logging.getLogger(__name__)
|
|
59
63
|
|
|
60
64
|
|
|
61
65
|
@durable_step
|
|
62
|
-
async def validate_order(
|
|
66
|
+
async def validate_order(order_id: str) -> dict:
|
|
63
67
|
await asyncio.sleep(0)
|
|
64
|
-
|
|
68
|
+
logger.info("Validating order", extra={"order_id": order_id})
|
|
65
69
|
return {"order_id": order_id, "valid": True}
|
|
66
70
|
|
|
67
71
|
|
|
68
72
|
@durable_execution
|
|
69
|
-
async def handler(event: dict
|
|
73
|
+
async def handler(event: dict) -> dict:
|
|
70
74
|
order_id = event["order_id"]
|
|
71
|
-
|
|
75
|
+
logger.info("Starting workflow", extra={"order_id": order_id})
|
|
72
76
|
|
|
73
|
-
validation =
|
|
77
|
+
validation = await step(validate_order(order_id), name="validate_order")
|
|
74
78
|
if not validation["valid"]:
|
|
75
79
|
return {"status": "rejected", "order_id": order_id}
|
|
76
80
|
|
|
77
81
|
# simulate approval (real world: use wait_for_callback)
|
|
78
|
-
|
|
82
|
+
await wait(duration=timedelta(seconds=5), name="await_confirmation")
|
|
79
83
|
|
|
80
84
|
return {"status": "approved", "order_id": order_id}
|
|
81
85
|
```
|
|
82
86
|
|
|
83
|
-
Async callables are
|
|
87
|
+
Async callables are required anywhere the SDK accepts user code, including `map()` item functions, `parallel()` branches, child contexts, callback submitters, and wait-for-condition checks. Durable context operations are awaitable and run on the same event loop as your handler:
|
|
84
88
|
|
|
85
89
|
```python
|
|
86
90
|
import asyncio
|
|
91
|
+
import logging
|
|
87
92
|
|
|
88
93
|
from async_durable_execution import (
|
|
89
|
-
DurableContext,
|
|
90
|
-
StepContext,
|
|
91
|
-
durable_execution,
|
|
92
94
|
durable_step,
|
|
95
|
+
durable_execution,
|
|
96
|
+
step,
|
|
93
97
|
)
|
|
94
98
|
|
|
99
|
+
logger = logging.getLogger(__name__)
|
|
100
|
+
|
|
95
101
|
|
|
96
102
|
@durable_step
|
|
97
|
-
async def fetch_order(
|
|
103
|
+
async def fetch_order(order_id: str) -> dict:
|
|
98
104
|
await asyncio.sleep(0)
|
|
99
|
-
|
|
105
|
+
logger.info("Fetched order", extra={"order_id": order_id})
|
|
100
106
|
return {"order_id": order_id, "status": "ready"}
|
|
101
107
|
|
|
102
108
|
|
|
103
109
|
@durable_execution
|
|
104
|
-
async def handler(event: dict
|
|
105
|
-
order =
|
|
110
|
+
async def handler(event: dict) -> dict:
|
|
111
|
+
order = await step(fetch_order(event["order_id"]), name="fetch_order")
|
|
106
112
|
return {"order": order}
|
|
107
113
|
```
|
|
108
114
|
|
|
@@ -8,13 +8,14 @@ name = "async-durable-execution"
|
|
|
8
8
|
dynamic = ["version"]
|
|
9
9
|
description = "Community-maintained durable execution SDK for AWS Lambda in Python"
|
|
10
10
|
readme = "README.md"
|
|
11
|
-
requires-python = ">=3.
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
12
|
license = "Apache-2.0"
|
|
13
13
|
keywords = []
|
|
14
14
|
authors = [{ name = "Zhongke Chen" }]
|
|
15
15
|
classifiers = [
|
|
16
16
|
"Development Status :: 4 - Beta",
|
|
17
17
|
"Programming Language :: Python",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
18
19
|
"Programming Language :: Python :: 3.11",
|
|
19
20
|
"Programming Language :: Python :: 3.12",
|
|
20
21
|
"Programming Language :: Python :: 3.13",
|
|
@@ -24,6 +25,9 @@ classifiers = [
|
|
|
24
25
|
]
|
|
25
26
|
dependencies = ["boto3>=1.42.1"]
|
|
26
27
|
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
typing = ["boto3-stubs[lambda]>=1.42.1"]
|
|
30
|
+
|
|
27
31
|
[project.urls]
|
|
28
32
|
Documentation = "https://github.com/zhongkechen/async-durable-execution#readme"
|
|
29
33
|
Issues = "https://github.com/zhongkechen/async-durable-execution/issues"
|
|
@@ -62,11 +66,11 @@ exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"]
|
|
|
62
66
|
|
|
63
67
|
[tool.ruff]
|
|
64
68
|
line-length = 88
|
|
65
|
-
target-version = "
|
|
69
|
+
target-version = "py310"
|
|
66
70
|
|
|
67
71
|
[tool.ruff.lint]
|
|
68
72
|
preview = true
|
|
69
|
-
select = [
|
|
73
|
+
select = []
|
|
70
74
|
|
|
71
75
|
[tool.ruff.lint.isort]
|
|
72
76
|
known-first-party = ["async_durable_execution"]
|
|
@@ -74,7 +78,7 @@ force-single-line = false
|
|
|
74
78
|
lines-after-imports = 2
|
|
75
79
|
|
|
76
80
|
[tool.ruff.lint.per-file-ignores]
|
|
77
|
-
"
|
|
81
|
+
"tests_async_durable_execution/**" = [
|
|
78
82
|
"ARG001",
|
|
79
83
|
"ARG002",
|
|
80
84
|
"ARG005",
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""AWS Lambda Durable Executions Python SDK."""
|
|
2
|
+
|
|
3
|
+
# Package metadata
|
|
4
|
+
from .__about__ import __version__
|
|
5
|
+
|
|
6
|
+
# Main context - used in every durable function
|
|
7
|
+
# Helper decorators - commonly used for step functions
|
|
8
|
+
# Concurrency
|
|
9
|
+
from .models import (
|
|
10
|
+
BatchItem,
|
|
11
|
+
BatchItemStatus,
|
|
12
|
+
BatchResult,
|
|
13
|
+
CompletionReason,
|
|
14
|
+
InvocationStatus,
|
|
15
|
+
OperationType,
|
|
16
|
+
OperationStatus,
|
|
17
|
+
)
|
|
18
|
+
from .config import (
|
|
19
|
+
CallbackConfig,
|
|
20
|
+
CompletionConfig,
|
|
21
|
+
MapConfig,
|
|
22
|
+
NestingType,
|
|
23
|
+
ParallelBranch,
|
|
24
|
+
ParallelConfig,
|
|
25
|
+
RetryPresets,
|
|
26
|
+
RetryStrategyBuilder,
|
|
27
|
+
StepConfig,
|
|
28
|
+
StepSemantics,
|
|
29
|
+
WaitStrategyBuilder,
|
|
30
|
+
WaitForCallbackConfig,
|
|
31
|
+
WaitForConditionConfig,
|
|
32
|
+
WithRetryConfig,
|
|
33
|
+
)
|
|
34
|
+
from .context import (
|
|
35
|
+
get_current_context,
|
|
36
|
+
)
|
|
37
|
+
from .operation.with_retry import with_retry
|
|
38
|
+
from .operation.map import map
|
|
39
|
+
from .operation.wait_for_condition import (
|
|
40
|
+
wait_for_condition,
|
|
41
|
+
WaitForConditionCheckContext,
|
|
42
|
+
)
|
|
43
|
+
from .operation.invoke import invoke
|
|
44
|
+
from .operation.parallel import parallel, durable_parallel_branch
|
|
45
|
+
from .operation.callback import (
|
|
46
|
+
create_callback,
|
|
47
|
+
durable_wait_for_callback,
|
|
48
|
+
wait_for_callback,
|
|
49
|
+
WaitForCallbackContext,
|
|
50
|
+
Callback,
|
|
51
|
+
)
|
|
52
|
+
from .operation.child import durable_child_context, run_in_child_context, DurableContext
|
|
53
|
+
from .operation.step import step, durable_step, StepContext, get_attempt
|
|
54
|
+
from .models import (
|
|
55
|
+
ErrorObject,
|
|
56
|
+
OperationIdentifier,
|
|
57
|
+
RetryDecision,
|
|
58
|
+
WaitDecision,
|
|
59
|
+
WaitForConditionDecision,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Most common exceptions - users need to handle these exceptions
|
|
63
|
+
from .exceptions import (
|
|
64
|
+
DurableExecutionsError,
|
|
65
|
+
InvocationError,
|
|
66
|
+
ValidationError,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Core decorator - used in every durable function
|
|
70
|
+
from .execution import durable_execution
|
|
71
|
+
from .operation.wait import wait
|
|
72
|
+
from .plugin import DurableInstrumentationPlugin
|
|
73
|
+
from .serdes import JsonSerDes, SerDes, SerDesContext
|
|
74
|
+
|
|
75
|
+
__all__ = [
|
|
76
|
+
"BatchItem",
|
|
77
|
+
"BatchItemStatus",
|
|
78
|
+
"BatchResult",
|
|
79
|
+
"Callback",
|
|
80
|
+
"CallbackConfig",
|
|
81
|
+
"CompletionConfig",
|
|
82
|
+
"CompletionReason",
|
|
83
|
+
"DurableContext",
|
|
84
|
+
"DurableInstrumentationPlugin",
|
|
85
|
+
"DurableExecutionsError",
|
|
86
|
+
"ErrorObject",
|
|
87
|
+
"InvocationError",
|
|
88
|
+
"JsonSerDes",
|
|
89
|
+
"MapConfig",
|
|
90
|
+
"NestingType",
|
|
91
|
+
"OperationIdentifier",
|
|
92
|
+
"ParallelBranch",
|
|
93
|
+
"ParallelConfig",
|
|
94
|
+
"RetryDecision",
|
|
95
|
+
"RetryPresets",
|
|
96
|
+
"RetryStrategyBuilder",
|
|
97
|
+
"SerDes",
|
|
98
|
+
"SerDesContext",
|
|
99
|
+
"StepConfig",
|
|
100
|
+
"StepContext",
|
|
101
|
+
"StepSemantics",
|
|
102
|
+
"ValidationError",
|
|
103
|
+
"WaitForCallbackConfig",
|
|
104
|
+
"WaitForCallbackContext",
|
|
105
|
+
"WaitDecision",
|
|
106
|
+
"WaitForConditionCheckContext",
|
|
107
|
+
"WaitForConditionConfig",
|
|
108
|
+
"WaitForConditionDecision",
|
|
109
|
+
"WaitStrategyBuilder",
|
|
110
|
+
"WithRetryConfig",
|
|
111
|
+
"__version__",
|
|
112
|
+
"create_callback",
|
|
113
|
+
"durable_execution",
|
|
114
|
+
"durable_parallel_branch",
|
|
115
|
+
"durable_step",
|
|
116
|
+
"durable_wait_for_callback",
|
|
117
|
+
"get_attempt",
|
|
118
|
+
"get_current_context",
|
|
119
|
+
"invoke",
|
|
120
|
+
"map",
|
|
121
|
+
"parallel",
|
|
122
|
+
"run_in_child_context",
|
|
123
|
+
"step",
|
|
124
|
+
"wait",
|
|
125
|
+
"wait_for_callback",
|
|
126
|
+
"wait_for_condition",
|
|
127
|
+
"with_retry",
|
|
128
|
+
"InvocationStatus",
|
|
129
|
+
"OperationType",
|
|
130
|
+
"OperationStatus",
|
|
131
|
+
"durable_child_context",
|
|
132
|
+
]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import inspect
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from typing import TYPE_CHECKING, TypeVar, cast
|
|
7
|
+
|
|
8
|
+
from .context import reset_current_context, set_current_context
|
|
9
|
+
from .exceptions import ValidationError
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from . import DurableContext
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T")
|
|
15
|
+
_CONTEXT_PARAM_NAMES = {
|
|
16
|
+
"context",
|
|
17
|
+
"ctx",
|
|
18
|
+
"child_context",
|
|
19
|
+
"child_ctx",
|
|
20
|
+
"durable_context",
|
|
21
|
+
"durable_ctx",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_async_callable(func: Callable[..., object]) -> bool:
|
|
26
|
+
if inspect.iscoroutinefunction(func):
|
|
27
|
+
return True
|
|
28
|
+
if isinstance(func, functools.partial):
|
|
29
|
+
return is_async_callable(func.func)
|
|
30
|
+
|
|
31
|
+
call = getattr(func, "__call__", None)
|
|
32
|
+
return call is not None and inspect.iscoroutinefunction(call)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_callable_name(
|
|
36
|
+
func: Callable[..., object],
|
|
37
|
+
*,
|
|
38
|
+
include_original_name: bool = True,
|
|
39
|
+
) -> str | None:
|
|
40
|
+
if isinstance(func, functools.partial):
|
|
41
|
+
return get_callable_name(
|
|
42
|
+
func.func,
|
|
43
|
+
include_original_name=include_original_name,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if include_original_name:
|
|
47
|
+
original_name = getattr(func, "_original_name", None)
|
|
48
|
+
if original_name is not None:
|
|
49
|
+
return original_name
|
|
50
|
+
|
|
51
|
+
if inspect.isfunction(func) or inspect.ismethod(func):
|
|
52
|
+
return getattr(func, "__name__", None)
|
|
53
|
+
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def assert_async_callable(
|
|
58
|
+
func: Callable[..., object],
|
|
59
|
+
*,
|
|
60
|
+
label: str = "func",
|
|
61
|
+
) -> None:
|
|
62
|
+
if is_async_callable(func):
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
name = get_callable_name(func)
|
|
66
|
+
if name is None:
|
|
67
|
+
name = type(func).__name__
|
|
68
|
+
|
|
69
|
+
msg = (
|
|
70
|
+
f"`{label}` must be an async function. "
|
|
71
|
+
f"Non-async callables are no longer supported: {name}."
|
|
72
|
+
)
|
|
73
|
+
raise ValidationError(msg)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def invoke_callable(func: Callable[..., Awaitable[T]], *args, **kwargs) -> T:
|
|
77
|
+
assert_async_callable(func)
|
|
78
|
+
return await func(*args, **kwargs)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def invoke_user_callable(
|
|
82
|
+
context: DurableContext,
|
|
83
|
+
func: Callable[..., Awaitable[T]],
|
|
84
|
+
*args,
|
|
85
|
+
**kwargs,
|
|
86
|
+
) -> T:
|
|
87
|
+
token = set_current_context(context)
|
|
88
|
+
try:
|
|
89
|
+
return await invoke_callable(
|
|
90
|
+
func,
|
|
91
|
+
*args,
|
|
92
|
+
**kwargs,
|
|
93
|
+
)
|
|
94
|
+
finally:
|
|
95
|
+
reset_current_context(token)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
import boto3
|
|
8
|
+
from botocore.config import Config
|
|
9
|
+
|
|
10
|
+
from .__about__ import __version__
|
|
11
|
+
from .exceptions import CheckpointError, GetExecutionStateError
|
|
12
|
+
from .models import (
|
|
13
|
+
CheckpointOutput,
|
|
14
|
+
OperationUpdate,
|
|
15
|
+
StateOutput,
|
|
16
|
+
)
|
|
17
|
+
from .types import DurableServiceClient, LambdaApiClient
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create_default_client():
|
|
23
|
+
user_agent = f"async-durable-execution/{__version__}-async"
|
|
24
|
+
return boto3.client(
|
|
25
|
+
"lambda",
|
|
26
|
+
config=Config(
|
|
27
|
+
connect_timeout=5,
|
|
28
|
+
read_timeout=50,
|
|
29
|
+
user_agent_extra=user_agent,
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ThreadedSyncLambdaClient(DurableServiceClient):
|
|
35
|
+
"""Adapt the sync boto3 Lambda client to the async service interface."""
|
|
36
|
+
|
|
37
|
+
_cached_boto_client: LambdaApiClient | None = None
|
|
38
|
+
|
|
39
|
+
def __init__(self, client: LambdaApiClient | None) -> None:
|
|
40
|
+
self.client = client or create_default_client()
|
|
41
|
+
|
|
42
|
+
async def checkpoint(
|
|
43
|
+
self,
|
|
44
|
+
durable_execution_arn: str,
|
|
45
|
+
checkpoint_token: str,
|
|
46
|
+
updates: list[OperationUpdate],
|
|
47
|
+
client_token: str | None,
|
|
48
|
+
) -> CheckpointOutput:
|
|
49
|
+
try:
|
|
50
|
+
optional_params: dict[str, str] = {}
|
|
51
|
+
if client_token is not None:
|
|
52
|
+
optional_params["ClientToken"] = client_token
|
|
53
|
+
|
|
54
|
+
result = await asyncio.to_thread(
|
|
55
|
+
self.client.checkpoint_durable_execution,
|
|
56
|
+
DurableExecutionArn=durable_execution_arn,
|
|
57
|
+
CheckpointToken=checkpoint_token,
|
|
58
|
+
Updates=cast("Any", [o.to_dict() for o in updates]),
|
|
59
|
+
**optional_params, # type: ignore[arg-type]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return CheckpointOutput.from_dict(result)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
checkpoint_error = CheckpointError.from_exception(e)
|
|
65
|
+
logger.exception(
|
|
66
|
+
"Failed to checkpoint.", extra=checkpoint_error.build_logger_extras()
|
|
67
|
+
)
|
|
68
|
+
raise checkpoint_error from None
|
|
69
|
+
|
|
70
|
+
async def get_execution_state(
|
|
71
|
+
self,
|
|
72
|
+
durable_execution_arn: str,
|
|
73
|
+
checkpoint_token: str,
|
|
74
|
+
next_marker: str,
|
|
75
|
+
max_items: int = 1000,
|
|
76
|
+
) -> StateOutput:
|
|
77
|
+
try:
|
|
78
|
+
result = await asyncio.to_thread(
|
|
79
|
+
self.client.get_durable_execution_state,
|
|
80
|
+
DurableExecutionArn=durable_execution_arn,
|
|
81
|
+
CheckpointToken=checkpoint_token,
|
|
82
|
+
Marker=next_marker,
|
|
83
|
+
MaxItems=max_items,
|
|
84
|
+
)
|
|
85
|
+
return StateOutput.from_dict(result)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
error = GetExecutionStateError.from_exception(e)
|
|
88
|
+
logger.exception(
|
|
89
|
+
"Failed to get execution state.", extra=error.build_logger_extras()
|
|
90
|
+
)
|
|
91
|
+
raise error from None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
__all__ = ["ThreadedSyncLambdaClient"]
|