async-durable-execution 0.1.0__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-0.1.0 → async_durable_execution-2.0.0a2}/.gitignore +11 -2
- async_durable_execution-2.0.0a2/NOTICE +8 -0
- async_durable_execution-2.0.0a2/PKG-INFO +155 -0
- async_durable_execution-2.0.0a2/README.md +131 -0
- async_durable_execution-0.1.0/src/async_durable_execution/__about__.py → async_durable_execution-2.0.0a2/VERSION.py +2 -1
- async_durable_execution-2.0.0a2/pyproject.toml +90 -0
- async_durable_execution-2.0.0a2/src/async_durable_execution/__about__.py +33 -0
- 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-0.1.0 → async_durable_execution-2.0.0a2}/src/async_durable_execution/config.py +352 -71
- async_durable_execution-2.0.0a2/src/async_durable_execution/context.py +68 -0
- {async_durable_execution-0.1.0 → async_durable_execution-2.0.0a2}/src/async_durable_execution/exceptions.py +105 -69
- {async_durable_execution-0.1.0 → async_durable_execution-2.0.0a2}/src/async_durable_execution/execution.py +158 -165
- 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-0.1.0 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/base.py +34 -7
- async_durable_execution-2.0.0a2/src/async_durable_execution/operation/callback.py +397 -0
- {async_durable_execution-0.1.0 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/child.py +218 -49
- async_durable_execution-2.0.0a2/src/async_durable_execution/operation/concurrency.py +382 -0
- {async_durable_execution-0.1.0 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/invoke.py +46 -11
- {async_durable_execution-0.1.0 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/map.py +85 -21
- async_durable_execution-2.0.0a2/src/async_durable_execution/operation/parallel.py +277 -0
- {async_durable_execution-0.1.0 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/step.py +121 -35
- {async_durable_execution-0.1.0 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/wait.py +43 -7
- {async_durable_execution-0.1.0 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/wait_for_condition.py +90 -37
- async_durable_execution-2.0.0a2/src/async_durable_execution/operation/with_retry.py +61 -0
- async_durable_execution-2.0.0a2/src/async_durable_execution/plugin.py +439 -0
- {async_durable_execution-0.1.0 → async_durable_execution-2.0.0a2}/src/async_durable_execution/serdes.py +5 -12
- {async_durable_execution-0.1.0 → async_durable_execution-2.0.0a2}/src/async_durable_execution/state.py +415 -231
- {async_durable_execution-0.1.0 → async_durable_execution-2.0.0a2}/src/async_durable_execution/suspend.py +3 -3
- {async_durable_execution-0.1.0 → async_durable_execution-2.0.0a2}/src/async_durable_execution/types.py +52 -95
- 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-0.1.0/tests/retries_test.py → async_durable_execution-2.0.0a2/tests_async_durable_execution/config_retry_test.py +73 -172
- {async_durable_execution-0.1.0/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/config_test.py +5 -49
- async_durable_execution-0.1.0/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.0a2/tests_async_durable_execution/context_test.py +2511 -0
- async_durable_execution-2.0.0a2/tests_async_durable_execution/context_with_retry_test.py +421 -0
- {async_durable_execution-0.1.0/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/e2e/checkpoint_response_int_test.py +106 -95
- {async_durable_execution-0.1.0/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/e2e/execution_int_test.py +172 -98
- async_durable_execution-2.0.0a2/tests_async_durable_execution/e2e/map_with_concurrent_waits_int_test.py +202 -0
- {async_durable_execution-0.1.0/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/exceptions_test.py +145 -51
- {async_durable_execution-0.1.0/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/execution_test.py +913 -346
- 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-0.1.0/tests/lambda_service_test.py → async_durable_execution-2.0.0a2/tests_async_durable_execution/models_test.py +285 -704
- async_durable_execution-2.0.0a2/tests_async_durable_execution/models_wait_test.py +37 -0
- {async_durable_execution-0.1.0/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/base_test.py +42 -33
- {async_durable_execution-0.1.0/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/callback_test.py +505 -340
- {async_durable_execution-0.1.0/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/child_test.py +439 -92
- {async_durable_execution-0.1.0/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution/operation}/concurrency_test.py +773 -580
- {async_durable_execution-0.1.0/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/invoke_test.py +238 -158
- async_durable_execution-2.0.0a2/tests_async_durable_execution/operation/map_test.py +1512 -0
- async_durable_execution-2.0.0a2/tests_async_durable_execution/operation/parallel_test.py +1480 -0
- {async_durable_execution-0.1.0/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/step_test.py +307 -179
- {async_durable_execution-0.1.0/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/wait_for_condition_test.py +353 -272
- {async_durable_execution-0.1.0/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/wait_test.py +71 -48
- async_durable_execution-2.0.0a2/tests_async_durable_execution/plugin_test.py +751 -0
- {async_durable_execution-0.1.0/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/serdes_test.py +15 -59
- {async_durable_execution-0.1.0/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/state_test.py +1511 -1016
- {async_durable_execution-0.1.0/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/suspend_test.py +4 -2
- {async_durable_execution-0.1.0/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-0.1.0/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-0.1.0/.github/workflows/pypi.yml +0 -70
- async_durable_execution-0.1.0/PKG-INFO +0 -80
- async_durable_execution-0.1.0/README.md +0 -70
- async_durable_execution-0.1.0/pyproject.toml +0 -44
- async_durable_execution-0.1.0/src/async_durable_execution/__init__.py +0 -42
- async_durable_execution-0.1.0/src/async_durable_execution/concurrency/executor.py +0 -461
- async_durable_execution-0.1.0/src/async_durable_execution/concurrency/models.py +0 -540
- async_durable_execution-0.1.0/src/async_durable_execution/context.py +0 -635
- async_durable_execution-0.1.0/src/async_durable_execution/identifier.py +0 -14
- async_durable_execution-0.1.0/src/async_durable_execution/lambda_service.py +0 -1120
- async_durable_execution-0.1.0/src/async_durable_execution/logger.py +0 -131
- async_durable_execution-0.1.0/src/async_durable_execution/operation/callback.py +0 -182
- async_durable_execution-0.1.0/src/async_durable_execution/operation/parallel.py +0 -122
- async_durable_execution-0.1.0/src/async_durable_execution/retries.py +0 -174
- async_durable_execution-0.1.0/src/async_durable_execution/threading.py +0 -222
- async_durable_execution-0.1.0/src/async_durable_execution/waits.py +0 -130
- async_durable_execution-0.1.0/tests/context_test.py +0 -1961
- async_durable_execution-0.1.0/tests/e2e/__init__.py +0 -0
- async_durable_execution-0.1.0/tests/logger_test.py +0 -418
- async_durable_execution-0.1.0/tests/operation/__init__.py +0 -0
- async_durable_execution-0.1.0/tests/operation/map_test.py +0 -1112
- async_durable_execution-0.1.0/tests/operation/parallel_test.py +0 -1104
- async_durable_execution-0.1.0/tests/threading_test.py +0 -954
- async_durable_execution-0.1.0/tests/types_test.py +0 -198
- {async_durable_execution-0.1.0 → async_durable_execution-2.0.0a2}/LICENSE +0 -0
- {async_durable_execution-0.1.0 → async_durable_execution-2.0.0a2}/src/async_durable_execution/.gitignore +0 -0
- {async_durable_execution-0.1.0 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/__init__.py +0 -0
- {async_durable_execution-0.1.0 → async_durable_execution-2.0.0a2}/src/async_durable_execution/py.typed +0 -0
- {async_durable_execution-0.1.0/src/async_durable_execution/concurrency → async_durable_execution-2.0.0a2/tests_async_durable_execution/e2e}/__init__.py +0 -0
- {async_durable_execution-0.1.0/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution/operation}/__init__.py +0 -0
|
@@ -9,6 +9,7 @@ __pycache__/
|
|
|
9
9
|
*$py.class
|
|
10
10
|
*.egg-info/
|
|
11
11
|
|
|
12
|
+
*.coverage
|
|
12
13
|
/.coverage
|
|
13
14
|
/.coverage.*
|
|
14
15
|
/.cache
|
|
@@ -20,7 +21,7 @@ __pycache__/
|
|
|
20
21
|
/build
|
|
21
22
|
|
|
22
23
|
.venv
|
|
23
|
-
.venv
|
|
24
|
+
.venv*/
|
|
24
25
|
|
|
25
26
|
.attach_*
|
|
26
27
|
|
|
@@ -28,4 +29,12 @@ dist/
|
|
|
28
29
|
|
|
29
30
|
.idea
|
|
30
31
|
|
|
31
|
-
.kiro/
|
|
32
|
+
.kiro/
|
|
33
|
+
|
|
34
|
+
**/build/
|
|
35
|
+
**/*.zip
|
|
36
|
+
packages/async-durable-execution-examples/template.yaml
|
|
37
|
+
**/template.generated.json
|
|
38
|
+
|
|
39
|
+
.env
|
|
40
|
+
*.egg-info
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
|
|
3
|
+
This repository is a modified fork of software originally released by
|
|
4
|
+
Amazon.com, Inc. or its affiliates under the Apache License 2.0.
|
|
5
|
+
|
|
6
|
+
Fork modifications and continued maintenance:
|
|
7
|
+
Copyright 2026 Zhongke Chen
|
|
8
|
+
Repository: https://github.com/zhongkechen/async-durable-execution
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: async-durable-execution
|
|
3
|
+
Version: 2.0.0a2
|
|
4
|
+
Summary: Community-maintained durable execution SDK for AWS Lambda in Python
|
|
5
|
+
Project-URL: Documentation, https://github.com/zhongkechen/async-durable-execution#readme
|
|
6
|
+
Project-URL: Issues, https://github.com/zhongkechen/async-durable-execution/issues
|
|
7
|
+
Project-URL: Source, https://github.com/zhongkechen/async-durable-execution
|
|
8
|
+
Author: Zhongke Chen
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
18
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: boto3>=1.42.1
|
|
21
|
+
Provides-Extra: typing
|
|
22
|
+
Requires-Dist: boto3-stubs[lambda]>=1.42.1; extra == 'typing'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Async Durable Execution for Python
|
|
26
|
+
|
|
27
|
+
[](https://github.com/zhongkechen/async-durable-execution/actions/workflows/build.yml)
|
|
28
|
+
[](https://pypi.org/project/async-durable-execution)
|
|
29
|
+
[](https://pypi.org/project/async-durable-execution)
|
|
30
|
+
[](https://scorecard.dev/viewer/?uri=github.com/zhongkechen/async-durable-execution)
|
|
31
|
+
[](../../LICENSE)
|
|
32
|
+
|
|
33
|
+
-----
|
|
34
|
+
|
|
35
|
+
Build reliable, long-running AWS Lambda workflows with checkpointed steps, waits, callbacks, and parallel execution.
|
|
36
|
+
|
|
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.
|
|
38
|
+
|
|
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.
|
|
40
|
+
|
|
41
|
+
## ✨ Key Features
|
|
42
|
+
|
|
43
|
+
- **Async-only user callables** - Durable handlers and user-provided durable callbacks must use `async def`
|
|
44
|
+
- **Automatic checkpointing** - Resume execution after Lambda pauses or restarts
|
|
45
|
+
- **Durable steps** - Run work with retry strategies and deterministic replay
|
|
46
|
+
- **Waits and callbacks** - Pause for time or external signals without blocking Lambda
|
|
47
|
+
- **Parallel and map operations** - Fan out work with configurable completion criteria
|
|
48
|
+
- **Child contexts** - Structure complex workflows into isolated subflows
|
|
49
|
+
- **Replay-safe logging** - Use standard `logging` loggers enriched by the durable context filter
|
|
50
|
+
- **Local and cloud testing** - Validate workflows with the testing SDK
|
|
51
|
+
- **Async Python support** - Use `async def` for handlers, steps, child contexts, callback submitters, and wait-for-condition checks
|
|
52
|
+
|
|
53
|
+
## 📦 Packages
|
|
54
|
+
|
|
55
|
+
| Package | Description | Version |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| `async-durable-execution` | Execution SDK for Lambda durable functions | [](https://pypi.org/project/async-durable-execution) |
|
|
58
|
+
| `async-durable-execution-runner` | Local/cloud test runner and pytest helpers | [](https://pypi.org/project/async-durable-execution-runner) |
|
|
59
|
+
| `async-durable-execution-examples` | Example durable functions and integration tests for local and cloud workflows | Shared repo version |
|
|
60
|
+
|
|
61
|
+
## 🚀 Quick Start
|
|
62
|
+
|
|
63
|
+
This fork now requires async callables for all user-provided durable code.
|
|
64
|
+
Requires Python 3.10 or newer.
|
|
65
|
+
|
|
66
|
+
Install the execution SDK:
|
|
67
|
+
|
|
68
|
+
```console
|
|
69
|
+
pip install async-durable-execution
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Create a durable Lambda handler:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import asyncio
|
|
76
|
+
import logging
|
|
77
|
+
from datetime import timedelta
|
|
78
|
+
|
|
79
|
+
from async_durable_execution import (
|
|
80
|
+
durable_step,
|
|
81
|
+
durable_execution,
|
|
82
|
+
step,
|
|
83
|
+
wait,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
logger = logging.getLogger(__name__)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@durable_step
|
|
90
|
+
async def validate_order(order_id: str) -> dict:
|
|
91
|
+
await asyncio.sleep(0)
|
|
92
|
+
logger.info("Validating order", extra={"order_id": order_id})
|
|
93
|
+
return {"order_id": order_id, "valid": True}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@durable_execution
|
|
97
|
+
async def handler(event: dict) -> dict:
|
|
98
|
+
order_id = event["order_id"]
|
|
99
|
+
logger.info("Starting workflow", extra={"order_id": order_id})
|
|
100
|
+
|
|
101
|
+
validation = await step(validate_order(order_id), name="validate_order")
|
|
102
|
+
if not validation["valid"]:
|
|
103
|
+
return {"status": "rejected", "order_id": order_id}
|
|
104
|
+
|
|
105
|
+
# simulate approval (real world: use wait_for_callback)
|
|
106
|
+
await wait(duration=timedelta(seconds=5), name="await_confirmation")
|
|
107
|
+
|
|
108
|
+
return {"status": "approved", "order_id": order_id}
|
|
109
|
+
```
|
|
110
|
+
|
|
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:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
import asyncio
|
|
115
|
+
import logging
|
|
116
|
+
|
|
117
|
+
from async_durable_execution import (
|
|
118
|
+
durable_step,
|
|
119
|
+
durable_execution,
|
|
120
|
+
step,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
logger = logging.getLogger(__name__)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@durable_step
|
|
127
|
+
async def fetch_order(order_id: str) -> dict:
|
|
128
|
+
await asyncio.sleep(0)
|
|
129
|
+
logger.info("Fetched order", extra={"order_id": order_id})
|
|
130
|
+
return {"order_id": order_id, "status": "ready"}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@durable_execution
|
|
134
|
+
async def handler(event: dict) -> dict:
|
|
135
|
+
order = await step(fetch_order(event["order_id"]), name="fetch_order")
|
|
136
|
+
return {"order": order}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## 📚 Documentation
|
|
140
|
+
|
|
141
|
+
The complete documentation for the AWS Durable Execution SDK for Python lives on the AWS Documentation site:
|
|
142
|
+
|
|
143
|
+
- **[AWS Durable Execution Documentation](https://docs.aws.amazon.com/durable-execution/)** - Concepts, getting started, core operations, advanced topics, and API reference
|
|
144
|
+
- **[AWS Lambda Durable Functions Guide](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html)** - How durable functions work on Lambda
|
|
145
|
+
|
|
146
|
+
## 💬 Feedback & Support
|
|
147
|
+
|
|
148
|
+
- [Bug report](https://github.com/zhongkechen/async-durable-execution/issues/new?template=bug_report.yml)
|
|
149
|
+
- [Feature request](https://github.com/zhongkechen/async-durable-execution/issues/new?template=feature_request.yml)
|
|
150
|
+
- [Documentation feedback](https://github.com/zhongkechen/async-durable-execution/issues/new?template=documentation.yml)
|
|
151
|
+
- [Contributing guide](../../CONTRIBUTING.md)
|
|
152
|
+
|
|
153
|
+
## 📄 License
|
|
154
|
+
|
|
155
|
+
See the [LICENSE](../../LICENSE) file for our project's licensing.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Async Durable Execution for Python
|
|
2
|
+
|
|
3
|
+
[](https://github.com/zhongkechen/async-durable-execution/actions/workflows/build.yml)
|
|
4
|
+
[](https://pypi.org/project/async-durable-execution)
|
|
5
|
+
[](https://pypi.org/project/async-durable-execution)
|
|
6
|
+
[](https://scorecard.dev/viewer/?uri=github.com/zhongkechen/async-durable-execution)
|
|
7
|
+
[](../../LICENSE)
|
|
8
|
+
|
|
9
|
+
-----
|
|
10
|
+
|
|
11
|
+
Build reliable, long-running AWS Lambda workflows with checkpointed steps, waits, callbacks, and parallel execution.
|
|
12
|
+
|
|
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
|
+
|
|
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
|
+
|
|
17
|
+
## ✨ Key Features
|
|
18
|
+
|
|
19
|
+
- **Async-only user callables** - Durable handlers and user-provided durable callbacks must use `async def`
|
|
20
|
+
- **Automatic checkpointing** - Resume execution after Lambda pauses or restarts
|
|
21
|
+
- **Durable steps** - Run work with retry strategies and deterministic replay
|
|
22
|
+
- **Waits and callbacks** - Pause for time or external signals without blocking Lambda
|
|
23
|
+
- **Parallel and map operations** - Fan out work with configurable completion criteria
|
|
24
|
+
- **Child contexts** - Structure complex workflows into isolated subflows
|
|
25
|
+
- **Replay-safe logging** - Use standard `logging` loggers enriched by the durable context filter
|
|
26
|
+
- **Local and cloud testing** - Validate workflows with the testing SDK
|
|
27
|
+
- **Async Python support** - Use `async def` for handlers, steps, child contexts, callback submitters, and wait-for-condition checks
|
|
28
|
+
|
|
29
|
+
## 📦 Packages
|
|
30
|
+
|
|
31
|
+
| Package | Description | Version |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| `async-durable-execution` | Execution SDK for Lambda durable functions | [](https://pypi.org/project/async-durable-execution) |
|
|
34
|
+
| `async-durable-execution-runner` | Local/cloud test runner and pytest helpers | [](https://pypi.org/project/async-durable-execution-runner) |
|
|
35
|
+
| `async-durable-execution-examples` | Example durable functions and integration tests for local and cloud workflows | Shared repo version |
|
|
36
|
+
|
|
37
|
+
## 🚀 Quick Start
|
|
38
|
+
|
|
39
|
+
This fork now requires async callables for all user-provided durable code.
|
|
40
|
+
Requires Python 3.10 or newer.
|
|
41
|
+
|
|
42
|
+
Install the execution SDK:
|
|
43
|
+
|
|
44
|
+
```console
|
|
45
|
+
pip install async-durable-execution
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Create a durable Lambda handler:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import asyncio
|
|
52
|
+
import logging
|
|
53
|
+
from datetime import timedelta
|
|
54
|
+
|
|
55
|
+
from async_durable_execution import (
|
|
56
|
+
durable_step,
|
|
57
|
+
durable_execution,
|
|
58
|
+
step,
|
|
59
|
+
wait,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
logger = logging.getLogger(__name__)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@durable_step
|
|
66
|
+
async def validate_order(order_id: str) -> dict:
|
|
67
|
+
await asyncio.sleep(0)
|
|
68
|
+
logger.info("Validating order", extra={"order_id": order_id})
|
|
69
|
+
return {"order_id": order_id, "valid": True}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@durable_execution
|
|
73
|
+
async def handler(event: dict) -> dict:
|
|
74
|
+
order_id = event["order_id"]
|
|
75
|
+
logger.info("Starting workflow", extra={"order_id": order_id})
|
|
76
|
+
|
|
77
|
+
validation = await step(validate_order(order_id), name="validate_order")
|
|
78
|
+
if not validation["valid"]:
|
|
79
|
+
return {"status": "rejected", "order_id": order_id}
|
|
80
|
+
|
|
81
|
+
# simulate approval (real world: use wait_for_callback)
|
|
82
|
+
await wait(duration=timedelta(seconds=5), name="await_confirmation")
|
|
83
|
+
|
|
84
|
+
return {"status": "approved", "order_id": order_id}
|
|
85
|
+
```
|
|
86
|
+
|
|
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:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import asyncio
|
|
91
|
+
import logging
|
|
92
|
+
|
|
93
|
+
from async_durable_execution import (
|
|
94
|
+
durable_step,
|
|
95
|
+
durable_execution,
|
|
96
|
+
step,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
logger = logging.getLogger(__name__)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@durable_step
|
|
103
|
+
async def fetch_order(order_id: str) -> dict:
|
|
104
|
+
await asyncio.sleep(0)
|
|
105
|
+
logger.info("Fetched order", extra={"order_id": order_id})
|
|
106
|
+
return {"order_id": order_id, "status": "ready"}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@durable_execution
|
|
110
|
+
async def handler(event: dict) -> dict:
|
|
111
|
+
order = await step(fetch_order(event["order_id"]), name="fetch_order")
|
|
112
|
+
return {"order": order}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 📚 Documentation
|
|
116
|
+
|
|
117
|
+
The complete documentation for the AWS Durable Execution SDK for Python lives on the AWS Documentation site:
|
|
118
|
+
|
|
119
|
+
- **[AWS Durable Execution Documentation](https://docs.aws.amazon.com/durable-execution/)** - Concepts, getting started, core operations, advanced topics, and API reference
|
|
120
|
+
- **[AWS Lambda Durable Functions Guide](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html)** - How durable functions work on Lambda
|
|
121
|
+
|
|
122
|
+
## 💬 Feedback & Support
|
|
123
|
+
|
|
124
|
+
- [Bug report](https://github.com/zhongkechen/async-durable-execution/issues/new?template=bug_report.yml)
|
|
125
|
+
- [Feature request](https://github.com/zhongkechen/async-durable-execution/issues/new?template=feature_request.yml)
|
|
126
|
+
- [Documentation feedback](https://github.com/zhongkechen/async-durable-execution/issues/new?template=documentation.yml)
|
|
127
|
+
- [Contributing guide](../../CONTRIBUTING.md)
|
|
128
|
+
|
|
129
|
+
## 📄 License
|
|
130
|
+
|
|
131
|
+
See the [LICENSE](../../LICENSE) file for our project's licensing.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
# Modified fork metadata for async-durable-execution; upstream Apache-2.0 notices are preserved in the repository LICENSE and NOTICE files.
|
|
6
|
+
[project]
|
|
7
|
+
name = "async-durable-execution"
|
|
8
|
+
dynamic = ["version"]
|
|
9
|
+
description = "Community-maintained durable execution SDK for AWS Lambda in Python"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
license = "Apache-2.0"
|
|
13
|
+
keywords = []
|
|
14
|
+
authors = [{ name = "Zhongke Chen" }]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Programming Language :: Python",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
24
|
+
"Programming Language :: Python :: Implementation :: PyPy",
|
|
25
|
+
]
|
|
26
|
+
dependencies = ["boto3>=1.42.1"]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
typing = ["boto3-stubs[lambda]>=1.42.1"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Documentation = "https://github.com/zhongkechen/async-durable-execution#readme"
|
|
33
|
+
Issues = "https://github.com/zhongkechen/async-durable-execution/issues"
|
|
34
|
+
Source = "https://github.com/zhongkechen/async-durable-execution"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.sdist.force-include]
|
|
37
|
+
"../../LICENSE" = "LICENSE"
|
|
38
|
+
"../../NOTICE" = "NOTICE"
|
|
39
|
+
"../../VERSION.py" = "VERSION.py"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["src/async_durable_execution"]
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
45
|
+
"../../LICENSE" = "LICENSE"
|
|
46
|
+
"../../NOTICE" = "NOTICE"
|
|
47
|
+
"../../VERSION.py" = "VERSION.py"
|
|
48
|
+
|
|
49
|
+
[tool.hatch.version]
|
|
50
|
+
path = "../../VERSION.py"
|
|
51
|
+
|
|
52
|
+
[tool.coverage.run]
|
|
53
|
+
source_pkgs = ["async_durable_execution"]
|
|
54
|
+
branch = true
|
|
55
|
+
parallel = true
|
|
56
|
+
omit = ["src/async_durable_execution/__about__.py"]
|
|
57
|
+
|
|
58
|
+
[tool.coverage.paths]
|
|
59
|
+
async_durable_execution = [
|
|
60
|
+
"src/async_durable_execution",
|
|
61
|
+
"*/async-durable-execution/src/async_durable_execution",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.coverage.report]
|
|
65
|
+
exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"]
|
|
66
|
+
|
|
67
|
+
[tool.ruff]
|
|
68
|
+
line-length = 88
|
|
69
|
+
target-version = "py310"
|
|
70
|
+
|
|
71
|
+
[tool.ruff.lint]
|
|
72
|
+
preview = true
|
|
73
|
+
select = []
|
|
74
|
+
|
|
75
|
+
[tool.ruff.lint.isort]
|
|
76
|
+
known-first-party = ["async_durable_execution"]
|
|
77
|
+
force-single-line = false
|
|
78
|
+
lines-after-imports = 2
|
|
79
|
+
|
|
80
|
+
[tool.ruff.lint.per-file-ignores]
|
|
81
|
+
"tests_async_durable_execution/**" = [
|
|
82
|
+
"ARG001",
|
|
83
|
+
"ARG002",
|
|
84
|
+
"ARG005",
|
|
85
|
+
"S101",
|
|
86
|
+
"PLR2004",
|
|
87
|
+
"PLR6301",
|
|
88
|
+
"SIM117",
|
|
89
|
+
"TRY301",
|
|
90
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-present Amazon.com, Inc. or its affiliates.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_DIST_NAME = "async-durable-execution"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _read_repo_version() -> str:
|
|
12
|
+
version_file = _find_version_file()
|
|
13
|
+
if version_file is None:
|
|
14
|
+
return "0.0.0"
|
|
15
|
+
|
|
16
|
+
namespace: dict[str, str] = {}
|
|
17
|
+
exec(version_file.read_text(encoding="utf-8"), namespace)
|
|
18
|
+
return namespace["__version__"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _find_version_file() -> Path | None:
|
|
22
|
+
current = Path(__file__).resolve()
|
|
23
|
+
for parent in current.parents:
|
|
24
|
+
candidate = parent / "VERSION.py"
|
|
25
|
+
if candidate.exists():
|
|
26
|
+
return candidate
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
__version__ = version(_DIST_NAME)
|
|
32
|
+
except PackageNotFoundError:
|
|
33
|
+
__version__ = _read_repo_version()
|
|
@@ -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)
|