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.
Files changed (93) hide show
  1. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/.gitignore +1 -1
  2. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/PKG-INFO +33 -24
  3. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/README.md +28 -22
  4. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/VERSION.py +1 -1
  5. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/pyproject.toml +8 -4
  6. async_durable_execution-2.0.0a2/src/async_durable_execution/__init__.py +132 -0
  7. async_durable_execution-2.0.0a2/src/async_durable_execution/async_tools.py +95 -0
  8. async_durable_execution-2.0.0a2/src/async_durable_execution/client.py +94 -0
  9. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/config.py +252 -65
  10. async_durable_execution-2.0.0a2/src/async_durable_execution/context.py +68 -0
  11. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/exceptions.py +5 -27
  12. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/execution.py +82 -83
  13. async_durable_execution-2.0.0a2/src/async_durable_execution/logger.py +139 -0
  14. async_durable_execution-2.0.0a2/src/async_durable_execution/models.py +1478 -0
  15. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/base.py +35 -8
  16. async_durable_execution-2.0.0a2/src/async_durable_execution/operation/callback.py +397 -0
  17. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/child.py +175 -26
  18. async_durable_execution-2.0.0a2/src/async_durable_execution/operation/concurrency.py +382 -0
  19. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/invoke.py +50 -15
  20. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/map.py +74 -26
  21. async_durable_execution-2.0.0a2/src/async_durable_execution/operation/parallel.py +277 -0
  22. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/step.py +117 -41
  23. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/wait.py +45 -9
  24. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/wait_for_condition.py +88 -43
  25. async_durable_execution-2.0.0a2/src/async_durable_execution/operation/with_retry.py +61 -0
  26. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/plugin.py +109 -79
  27. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/serdes.py +5 -8
  28. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/state.py +267 -242
  29. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/suspend.py +4 -4
  30. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/types.py +51 -101
  31. async_durable_execution-2.0.0a2/tests_async_durable_execution/__init__.py +1 -0
  32. async_durable_execution-2.0.0a2/tests_async_durable_execution/async_tools_test.py +60 -0
  33. async_durable_execution-2.0.0a2/tests_async_durable_execution/client_test.py +486 -0
  34. 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
  35. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/config_test.py +5 -38
  36. 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
  37. async_durable_execution-2.0.0a2/tests_async_durable_execution/config_with_retry_test.py +37 -0
  38. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/context_test.py +828 -648
  39. 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
  40. {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
  41. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/e2e/execution_int_test.py +176 -90
  42. async_durable_execution-2.0.0a2/tests_async_durable_execution/e2e/map_with_concurrent_waits_int_test.py +202 -0
  43. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/exceptions_test.py +0 -17
  44. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/execution_test.py +385 -290
  45. async_durable_execution-2.0.0a2/tests_async_durable_execution/logger_test.py +319 -0
  46. async_durable_execution-2.0.0a2/tests_async_durable_execution/models_retry_test.py +19 -0
  47. 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
  48. async_durable_execution-2.0.0a2/tests_async_durable_execution/models_wait_test.py +37 -0
  49. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/base_test.py +42 -33
  50. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/callback_test.py +413 -306
  51. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/child_test.py +114 -89
  52. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution/operation}/concurrency_test.py +548 -618
  53. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/invoke_test.py +137 -121
  54. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/map_test.py +346 -205
  55. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/parallel_test.py +370 -203
  56. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/step_test.py +203 -161
  57. {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
  58. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/operation/wait_test.py +42 -37
  59. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/plugin_test.py +100 -132
  60. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/serdes_test.py +15 -59
  61. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/state_test.py +726 -1150
  62. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/suspend_test.py +4 -2
  63. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution}/test_helpers.py +9 -4
  64. async_durable_execution-2.0.0a2/tests_async_durable_execution/types_test.py +217 -0
  65. 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
  66. async_durable_execution-2.0.0a1/src/async_durable_execution/__init__.py +0 -50
  67. async_durable_execution-2.0.0a1/src/async_durable_execution/async_tools.py +0 -49
  68. async_durable_execution-2.0.0a1/src/async_durable_execution/concurrency/executor.py +0 -489
  69. async_durable_execution-2.0.0a1/src/async_durable_execution/concurrency/models.py +0 -540
  70. async_durable_execution-2.0.0a1/src/async_durable_execution/context.py +0 -765
  71. async_durable_execution-2.0.0a1/src/async_durable_execution/identifier.py +0 -24
  72. async_durable_execution-2.0.0a1/src/async_durable_execution/lambda_service.py +0 -1219
  73. async_durable_execution-2.0.0a1/src/async_durable_execution/logger.py +0 -131
  74. async_durable_execution-2.0.0a1/src/async_durable_execution/operation/callback.py +0 -182
  75. async_durable_execution-2.0.0a1/src/async_durable_execution/operation/parallel.py +0 -144
  76. async_durable_execution-2.0.0a1/src/async_durable_execution/retries.py +0 -279
  77. async_durable_execution-2.0.0a1/src/async_durable_execution/threading.py +0 -222
  78. async_durable_execution-2.0.0a1/src/async_durable_execution/waits.py +0 -130
  79. async_durable_execution-2.0.0a1/tests/async_tools_test.py +0 -22
  80. async_durable_execution-2.0.0a1/tests/e2e/__init__.py +0 -0
  81. async_durable_execution-2.0.0a1/tests/e2e/map_with_concurrent_waits_int_test.py +0 -202
  82. async_durable_execution-2.0.0a1/tests/logger_test.py +0 -424
  83. async_durable_execution-2.0.0a1/tests/operation/__init__.py +0 -0
  84. async_durable_execution-2.0.0a1/tests/threading_test.py +0 -954
  85. async_durable_execution-2.0.0a1/tests/types_test.py +0 -198
  86. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/LICENSE +0 -0
  87. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/NOTICE +0 -0
  88. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/.gitignore +0 -0
  89. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/__about__.py +0 -0
  90. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/operation/__init__.py +0 -0
  91. {async_durable_execution-2.0.0a1 → async_durable_execution-2.0.0a2}/src/async_durable_execution/py.typed +0 -0
  92. {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
  93. {async_durable_execution-2.0.0a1/tests → async_durable_execution-2.0.0a2/tests_async_durable_execution/operation}/__init__.py +0 -0
@@ -21,7 +21,7 @@ __pycache__/
21
21
  /build
22
22
 
23
23
  .venv
24
- .venv/
24
+ .venv*/
25
25
 
26
26
  .attach_*
27
27
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: async-durable-execution
3
- Version: 2.0.0a1
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.11
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 the examples, helpers, and package direction here prioritize `async def` handlers, steps, child contexts, callback submitters, and condition checks.
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-first fork** - This fork prioritizes making `async def` workflows feel natural with durable functions
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 `context.logger` for structured, de-duplicated logs
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 recommends writing new durable workflows with async callables by default.
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
- from async_durable_execution.config import Duration
85
+
86
+ logger = logging.getLogger(__name__)
80
87
 
81
88
 
82
89
  @durable_step
83
- async def validate_order(step_ctx: StepContext, order_id: str) -> dict:
90
+ async def validate_order(order_id: str) -> dict:
84
91
  await asyncio.sleep(0)
85
- step_ctx.logger.info("Validating order", extra={"order_id": order_id})
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, context: DurableContext) -> dict:
97
+ async def handler(event: dict) -> dict:
91
98
  order_id = event["order_id"]
92
- context.logger.info("Starting workflow", extra={"order_id": order_id})
99
+ logger.info("Starting workflow", extra={"order_id": order_id})
93
100
 
94
- validation = context.step(validate_order(order_id), name="validate_order")
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
- context.wait(duration=Duration.from_seconds(5), name="await_confirmation")
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 supported anywhere the SDK accepts user code, including `map()` item functions, `parallel()` branches, child contexts, callback submitters, and wait-for-condition checks. The public Durable APIs stay synchronous, so async work is awaited transparently for you:
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(step_ctx: StepContext, order_id: str) -> dict:
127
+ async def fetch_order(order_id: str) -> dict:
119
128
  await asyncio.sleep(0)
120
- step_ctx.logger.info("Fetched order", extra={"order_id": order_id})
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, context: DurableContext) -> dict:
126
- order = context.step(fetch_order(event["order_id"]), name="fetch_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 the examples, helpers, and package direction here prioritize `async def` handlers, steps, child contexts, callback submitters, and condition checks.
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-first fork** - This fork prioritizes making `async def` workflows feel natural with durable functions
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 `context.logger` for structured, de-duplicated logs
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 recommends writing new durable workflows with async callables by default.
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
- from async_durable_execution.config import Duration
61
+
62
+ logger = logging.getLogger(__name__)
59
63
 
60
64
 
61
65
  @durable_step
62
- async def validate_order(step_ctx: StepContext, order_id: str) -> dict:
66
+ async def validate_order(order_id: str) -> dict:
63
67
  await asyncio.sleep(0)
64
- step_ctx.logger.info("Validating order", extra={"order_id": order_id})
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, context: DurableContext) -> dict:
73
+ async def handler(event: dict) -> dict:
70
74
  order_id = event["order_id"]
71
- context.logger.info("Starting workflow", extra={"order_id": order_id})
75
+ logger.info("Starting workflow", extra={"order_id": order_id})
72
76
 
73
- validation = context.step(validate_order(order_id), name="validate_order")
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
- context.wait(duration=Duration.from_seconds(5), name="await_confirmation")
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 supported anywhere the SDK accepts user code, including `map()` item functions, `parallel()` branches, child contexts, callback submitters, and wait-for-condition checks. The public Durable APIs stay synchronous, so async work is awaited transparently for you:
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(step_ctx: StepContext, order_id: str) -> dict:
103
+ async def fetch_order(order_id: str) -> dict:
98
104
  await asyncio.sleep(0)
99
- step_ctx.logger.info("Fetched order", extra={"order_id": order_id})
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, context: DurableContext) -> dict:
105
- order = context.step(fetch_order(event["order_id"]), name="fetch_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
 
@@ -2,4 +2,4 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
  # Modified for the async-durable-execution fork; shared version source for all packages.
5
- __version__ = "2.0.0a1"
5
+ __version__ = "2.0.0a2"
@@ -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"
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 = "py311"
69
+ target-version = "py310"
66
70
 
67
71
  [tool.ruff.lint]
68
72
  preview = true
69
- select = ["TID252"] # Enforce absolute imports (ban relative imports)
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
- "tests/**" = [
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"]