aws-durable-execution-sdk-python 2.1.0__tar.gz → 3.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/.gitignore +1 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/PKG-INFO +91 -41
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/README.md +90 -40
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/__about__.py +1 -1
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/concurrency/executor.py +2 -4
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/concurrency/models.py +31 -21
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/config.py +109 -7
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/context.py +63 -22
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/execution.py +30 -20
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/identifier.py +9 -8
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/lambda_service.py +55 -36
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/operation/child.py +0 -5
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/operation/map.py +4 -4
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/operation/parallel.py +4 -4
- aws_durable_execution_sdk_python-3.0.0/src/aws_durable_execution_sdk_python/plugin.py +2035 -0
- aws_durable_execution_sdk_python-3.0.0/src/aws_durable_execution_sdk_python/plugin_discovery.py +517 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/state.py +101 -7
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/concurrency_test.py +192 -20
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/config_test.py +65 -3
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/context_test.py +123 -26
- aws_durable_execution_sdk_python-3.0.0/tests/e2e/batch_result_none_replay_test.py +83 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/e2e/branch_worker_context_test.py +16 -7
- aws_durable_execution_sdk_python-3.0.0/tests/e2e/custom_subtype_int_test.py +165 -0
- aws_durable_execution_sdk_python-3.0.0/tests/e2e/factory_registration_cleanup_int_test.py +93 -0
- aws_durable_execution_sdk_python-3.0.0/tests/e2e/factory_registration_int_test.py +96 -0
- aws_durable_execution_sdk_python-3.0.0/tests/e2e/handler_context_propagation_int_test.py +296 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/e2e/handler_invocation_context_int_test.py +4 -1
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/e2e/invoke_propagation_int_test.py +22 -8
- aws_durable_execution_sdk_python-3.0.0/tests/e2e/plugin_hook_contract_int_test.py +383 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/e2e/plugin_invocation_operations_int_test.py +36 -17
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/e2e/plugin_invocation_payload_int_test.py +5 -5
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/e2e/plugin_operation_type_int_test.py +9 -6
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/e2e/plugin_user_function_lifecycle_int_test.py +2 -1
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/execution_test.py +579 -25
- aws_durable_execution_sdk_python-3.0.0/tests/factory_registration_test.py +475 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/handler_worker_context_test.py +21 -19
- aws_durable_execution_sdk_python-3.0.0/tests/identifier_test.py +134 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/lambda_service_test.py +172 -67
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/child_test.py +32 -35
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/invoke_propagation_test.py +6 -5
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/step_test.py +9 -9
- aws_durable_execution_sdk_python-3.0.0/tests/plugin_discovery_test.py +844 -0
- aws_durable_execution_sdk_python-3.0.0/tests/plugin_hook_registry_test.py +1704 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/plugin_test.py +919 -92
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/propagation_test.py +20 -35
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/state_test.py +58 -37
- aws_durable_execution_sdk_python-3.0.0/tests/test_helpers.py +79 -0
- aws_durable_execution_sdk_python-2.1.0/src/aws_durable_execution_sdk_python/plugin.py +0 -997
- aws_durable_execution_sdk_python-2.1.0/src/aws_durable_execution_sdk_python/plugin_discovery.py +0 -353
- aws_durable_execution_sdk_python-2.1.0/tests/identifier_test.py +0 -56
- aws_durable_execution_sdk_python-2.1.0/tests/plugin_discovery_test.py +0 -1002
- aws_durable_execution_sdk_python-2.1.0/tests/test_helpers.py +0 -20
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/LICENSE +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/NOTICE +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/pyproject.toml +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/.gitignore +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/__init__.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/concurrency/__init__.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/constants.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/exceptions.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/filesystem_serdes.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/logger.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/operation/__init__.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/operation/base.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/operation/callback.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/operation/invoke.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/operation/step.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/operation/wait.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/operation/wait_for_condition.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/preview.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/py.typed +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/retries.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/serdes.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/suspend.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/threading.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/types.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/src/aws_durable_execution_sdk_python/waits.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/__init__.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/durable_executions_python_language_sdk_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/e2e/__init__.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/e2e/checkpoint_response_int_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/e2e/custom_serdes_roundtrip_int_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/e2e/error_hierarchy_int_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/e2e/execution_int_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/e2e/filesystem_serdes_int_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/e2e/map_with_concurrent_waits_int_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/e2e/should_complete_suspend_resume_int_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/exceptions_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/filesystem_serdes_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/invoke_wire_propagation_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/logger_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/__init__.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/base_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/callback_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/invoke_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/map_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/parallel_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/wait_for_condition_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/wait_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/preview_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/retries_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/serdes_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/suspend_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/threading_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/types_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/waits_test.py +0 -0
- {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/with_retry_test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: aws-durable-execution-sdk-python
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: AWS Durable Execution SDK for Python
|
|
5
5
|
Project-URL: Documentation, https://github.com/aws/aws-durable-execution-sdk-python#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/aws/aws-durable-execution-sdk-python/issues
|
|
@@ -50,6 +50,9 @@ Build reliable, long-running AWS Lambda workflows with checkpointed steps, waits
|
|
|
50
50
|
|
|
51
51
|
## Dynamic instrumentation plugins
|
|
52
52
|
|
|
53
|
+
Upgrading an existing application? Follow the [2.x to 3.0 migration guide](https://github.com/aws/aws-durable-execution-sdk-python/blob/main/docs/migration-2.x-to-3.0.md)
|
|
54
|
+
for dependency versions, before/after examples, provider packaging, and validation.
|
|
55
|
+
|
|
53
56
|
Instrumentation plugins can be selected at Lambda cold start without importing
|
|
54
57
|
them in the function artifact. Install a provider package in the function or a
|
|
55
58
|
Lambda layer, then set an ordered allow-list:
|
|
@@ -61,15 +64,60 @@ DURABLE_EXECUTION_PLUGINS=otel-invocation,example_audit
|
|
|
61
64
|
The SDK resolves those names from the `aws_durable_execution.plugins` Python
|
|
62
65
|
entry-point group when the decorated handler is initialized. An unset or blank
|
|
63
66
|
variable preserves the existing behavior. The decorator's `plugins` argument
|
|
64
|
-
remains supported; explicit
|
|
65
|
-
|
|
67
|
+
remains supported; explicit factories run first, and a factory passed to the
|
|
68
|
+
decorator is not registered a second time through the environment.
|
|
69
|
+
|
|
70
|
+
A plugin is registered as a *factory*, not as an instance. A factory is an object
|
|
71
|
+
with a `create_plugin(info)` method taking the invocation's `InvocationStartInfo`
|
|
72
|
+
and returning a `DurableInstrumentationPlugin`; the SDK calls that method once per
|
|
73
|
+
invocation, so the instance it returns serves that one invocation only and can
|
|
74
|
+
hold **per-invocation** state in ordinary attributes.
|
|
75
|
+
|
|
76
|
+
Per-invocation is narrower than per-execution, and the difference matters. A
|
|
77
|
+
durable execution spans as many invocations as it waits, retries or resumes, and
|
|
78
|
+
the instance is dropped when each of those returns — so anything a plugin keeps in
|
|
79
|
+
its attributes is gone by the next invocation of the same execution. State that
|
|
80
|
+
has to survive that has two honest homes: rebuild it from the operation map the
|
|
81
|
+
invocation hooks carry (`InvocationStartInfo.operations` is a full snapshot,
|
|
82
|
+
which is how the bundled Telemetry exporter reports operations that completed in an
|
|
83
|
+
earlier invocation), or put it on the factory, which outlives every invocation —
|
|
84
|
+
keyed by execution ARN, and pruned by the owner, because the SDK will not tell the
|
|
85
|
+
factory when an execution ends for good.
|
|
86
|
+
|
|
87
|
+
A factory is an object with a method rather than a plain callable so the
|
|
88
|
+
registration type can grow a second, optional member later -- a process-level
|
|
89
|
+
flush on execution-environment shutdown, for example -- without a second breaking
|
|
90
|
+
change to this surface.
|
|
91
|
+
|
|
92
|
+
Write a small factory class and construct the plugin in its `create_plugin`. The
|
|
93
|
+
factory holds what outlives an invocation, such as an exporter or a resolved
|
|
94
|
+
configuration, and setup work that can fail belongs in the factory's own
|
|
95
|
+
constructor rather than the plugin's:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
class AuditPluginFactory:
|
|
99
|
+
def __init__(self, sink):
|
|
100
|
+
self._sink = sink
|
|
101
|
+
|
|
102
|
+
def create_plugin(self, info):
|
|
103
|
+
return AuditPlugin(self._sink)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
plugins=[AuditPluginFactory(sink)]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
A plugin class is not a factory, and neither is a bare callable. `plugins=[MyPlugin]`
|
|
110
|
+
and `plugins=[lambda info: MyPlugin(sink)]` raise `PluginLoadError` during handler
|
|
111
|
+
initialization, because neither carries `create_plugin`. A class that declares
|
|
112
|
+
`create_plugin` as a `@classmethod` is accepted, since the requirement is the
|
|
113
|
+
member and not the kind of object.
|
|
66
114
|
|
|
67
|
-
Provider packages expose a
|
|
115
|
+
Provider packages expose such a factory:
|
|
68
116
|
|
|
69
117
|
```python
|
|
70
118
|
from aws_durable_execution_sdk_python.plugin import (
|
|
71
119
|
DurableInstrumentationPlugin,
|
|
72
|
-
|
|
120
|
+
InvocationStartInfo,
|
|
73
121
|
)
|
|
74
122
|
|
|
75
123
|
|
|
@@ -77,40 +125,40 @@ class AuditPlugin(DurableInstrumentationPlugin):
|
|
|
77
125
|
pass
|
|
78
126
|
|
|
79
127
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
128
|
+
class AuditPluginFactory:
|
|
129
|
+
def create_plugin(self, info: InvocationStartInfo) -> AuditPlugin:
|
|
130
|
+
return AuditPlugin()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
AUDIT_PLUGIN_FACTORY = AuditPluginFactory()
|
|
85
134
|
```
|
|
86
135
|
|
|
87
|
-
Register the
|
|
136
|
+
Register the factory instance in the package's `pyproject.toml`:
|
|
88
137
|
|
|
89
138
|
```toml
|
|
90
139
|
[project.entry-points."aws_durable_execution.plugins"]
|
|
91
|
-
example_audit = "example_audit:
|
|
140
|
+
example_audit = "example_audit:AUDIT_PLUGIN_FACTORY"
|
|
92
141
|
```
|
|
93
142
|
|
|
94
|
-
Set `plugin_api_version` to the literal API version the provider implements.
|
|
95
|
-
Update it only after verifying the provider against that API version.
|
|
96
|
-
|
|
97
143
|
Provider names must be unique across installed distributions. Missing,
|
|
98
|
-
ambiguous,
|
|
99
|
-
|
|
144
|
+
ambiguous, or wrongly shaped providers raise `PluginLoadError` during handler
|
|
145
|
+
initialization with the provider and distribution details.
|
|
100
146
|
|
|
101
147
|
### Selecting an OpenTelemetry view
|
|
102
148
|
|
|
103
|
-
Enable at most one of `
|
|
149
|
+
Enable at most one of `InvocationOtelPluginFactory` and
|
|
150
|
+
`ExecutionOtelPluginFactory`. This
|
|
104
151
|
constraint applies to the combined `plugins=[...]` argument and
|
|
105
152
|
`DURABLE_EXECUTION_PLUGINS=otel-invocation` or `otel-execution` selection.
|
|
106
|
-
Core
|
|
153
|
+
Core 3 with OTel 2 rejects both at cold start with `PluginLoadError` naming the
|
|
107
154
|
conflicting views; keep only one. Choose Invocation for work within each Lambda
|
|
108
155
|
invocation or Execution for logical operations across the durable execution.
|
|
109
|
-
Unrelated instrumentation
|
|
110
|
-
|
|
156
|
+
Unrelated instrumentation plugin factories can run alongside either view.
|
|
157
|
+
OTel 2 requires core 3.x and the per-invocation factory contract. Release the
|
|
158
|
+
coordinated core version before releasing OTel 2.
|
|
111
159
|
|
|
112
160
|
Plugin authors explicitly opt in by declaring
|
|
113
|
-
`__durable_registration_api__ = 1` on a
|
|
161
|
+
`__durable_registration_api__ = 1` on a factory class. That class and its subclasses
|
|
114
162
|
have their optional `exclusive_group` metadata validated and their
|
|
115
163
|
optional `on_registration_result(registered)` callback invoked. A missing group
|
|
116
164
|
or `None` adds no exclusivity constraint. The callback can release rejected
|
|
@@ -123,14 +171,14 @@ Once enabled, every class in the MRO explicitly declaring the integer `1`
|
|
|
123
171
|
contributes its resolved `exclusive_group`. These constraints accumulate:
|
|
124
172
|
repeating the marker with a new group adds to inherited groups rather than
|
|
125
173
|
replacing them, and `None` does not erase an inherited constraint. A group is
|
|
126
|
-
counted once per
|
|
174
|
+
counted once per factory registration even if several ancestors declare it.
|
|
127
175
|
Unmarked classes' coincidental same-named legacy fields/helpers remain inert.
|
|
128
|
-
Subclasses of the bundled OTel
|
|
176
|
+
Subclasses of the bundled OTel factories therefore retain the view group when they
|
|
129
177
|
add their own group. Repeat the marker to add a group or customize the callback;
|
|
130
|
-
the callback is still notified once per
|
|
131
|
-
in their hierarchy retain their existing attributes/helpers. The generic plugin
|
|
132
|
-
neither attribute nor hook
|
|
133
|
-
|
|
178
|
+
the callback is still notified once per factory. Factories with no marker anywhere
|
|
179
|
+
in their hierarchy retain their existing attributes/helpers. The generic plugin
|
|
180
|
+
base defines neither attribute nor hook. The registration API remains independent
|
|
181
|
+
of the removed provider API version 1 contract.
|
|
134
182
|
|
|
135
183
|
### Invocation worker lifecycle
|
|
136
184
|
|
|
@@ -144,11 +192,14 @@ End before returning the outcome. The caller shuts down the handler executor.
|
|
|
144
192
|
There is no separate handler-context hook or context-manager plugin API.
|
|
145
193
|
|
|
146
194
|
When plugins are registered, the worker begins with a copy of the caller's
|
|
147
|
-
context-variable bindings.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
195
|
+
context-variable bindings. Start hooks, the handler, serialization, resource
|
|
196
|
+
cleanup and End hooks use that same Context. Changes made by a Start hook remain
|
|
197
|
+
visible to later hooks and handler code even if that hook raises; the SDK contains
|
|
198
|
+
the exception and retains the plugin without rolling back its bindings. A later
|
|
199
|
+
Start hook may replace an earlier binding. Plugins are responsible for restoring
|
|
200
|
+
partial bindings themselves when needed, either before raising or during End.
|
|
201
|
+
End runs in the same Context as Start, so context-variable tokens retain their
|
|
202
|
+
owner. End hooks retain forward registration order, not reverse stack order,
|
|
152
203
|
so they must not rely on observing a stack-like unwind of other plugins' contexts.
|
|
153
204
|
Each SDK-managed `map` or `parallel` branch admission, including an in-process
|
|
154
205
|
resume, receives a fresh copy of the coordinator's bindings when plugins are
|
|
@@ -160,11 +211,11 @@ cleanup failures, leaving the host's bindings unchanged. This isolates bindings,
|
|
|
160
211
|
not mutations to shared objects or external side effects. Without plugins, the
|
|
161
212
|
handler retains its existing fresh-worker context behavior.
|
|
162
213
|
|
|
163
|
-
|
|
164
|
-
requires
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
214
|
+
Core 3 creates a fresh plugin instance per invocation through each registered
|
|
215
|
+
factory. OTel 2 requires core 3.x and uses the existing Start/End hooks on those
|
|
216
|
+
instances. Upgrade both packages together; core 3 rejects OTel 1.x registrations
|
|
217
|
+
and the removed provider API version 1 contract. The independent registration
|
|
218
|
+
API remains unchanged.
|
|
168
219
|
|
|
169
220
|
### Chained-invoke propagation
|
|
170
221
|
|
|
@@ -198,9 +249,8 @@ Python has no `DistributedMapOptions` wrapper
|
|
|
198
249
|
or distributed-map START API: its existing map/parallel operations use CONTEXT.
|
|
199
250
|
The design's corresponding distributed-map field awaits that future modeled path.
|
|
200
251
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
cores without this optional contract contribute no new propagation metadata.
|
|
252
|
+
This release pairs core 3 with OTel 2 and its per-invocation factories. Custom
|
|
253
|
+
plugins that omit the optional hook retain their existing tracing behavior.
|
|
204
254
|
Backend rollout and deployed downstream trace-topology validation remain pending.
|
|
205
255
|
|
|
206
256
|
## 🚀 Quick Start
|
|
@@ -29,6 +29,9 @@ Build reliable, long-running AWS Lambda workflows with checkpointed steps, waits
|
|
|
29
29
|
|
|
30
30
|
## Dynamic instrumentation plugins
|
|
31
31
|
|
|
32
|
+
Upgrading an existing application? Follow the [2.x to 3.0 migration guide](https://github.com/aws/aws-durable-execution-sdk-python/blob/main/docs/migration-2.x-to-3.0.md)
|
|
33
|
+
for dependency versions, before/after examples, provider packaging, and validation.
|
|
34
|
+
|
|
32
35
|
Instrumentation plugins can be selected at Lambda cold start without importing
|
|
33
36
|
them in the function artifact. Install a provider package in the function or a
|
|
34
37
|
Lambda layer, then set an ordered allow-list:
|
|
@@ -40,15 +43,60 @@ DURABLE_EXECUTION_PLUGINS=otel-invocation,example_audit
|
|
|
40
43
|
The SDK resolves those names from the `aws_durable_execution.plugins` Python
|
|
41
44
|
entry-point group when the decorated handler is initialized. An unset or blank
|
|
42
45
|
variable preserves the existing behavior. The decorator's `plugins` argument
|
|
43
|
-
remains supported; explicit
|
|
44
|
-
|
|
46
|
+
remains supported; explicit factories run first, and a factory passed to the
|
|
47
|
+
decorator is not registered a second time through the environment.
|
|
48
|
+
|
|
49
|
+
A plugin is registered as a *factory*, not as an instance. A factory is an object
|
|
50
|
+
with a `create_plugin(info)` method taking the invocation's `InvocationStartInfo`
|
|
51
|
+
and returning a `DurableInstrumentationPlugin`; the SDK calls that method once per
|
|
52
|
+
invocation, so the instance it returns serves that one invocation only and can
|
|
53
|
+
hold **per-invocation** state in ordinary attributes.
|
|
54
|
+
|
|
55
|
+
Per-invocation is narrower than per-execution, and the difference matters. A
|
|
56
|
+
durable execution spans as many invocations as it waits, retries or resumes, and
|
|
57
|
+
the instance is dropped when each of those returns — so anything a plugin keeps in
|
|
58
|
+
its attributes is gone by the next invocation of the same execution. State that
|
|
59
|
+
has to survive that has two honest homes: rebuild it from the operation map the
|
|
60
|
+
invocation hooks carry (`InvocationStartInfo.operations` is a full snapshot,
|
|
61
|
+
which is how the bundled Telemetry exporter reports operations that completed in an
|
|
62
|
+
earlier invocation), or put it on the factory, which outlives every invocation —
|
|
63
|
+
keyed by execution ARN, and pruned by the owner, because the SDK will not tell the
|
|
64
|
+
factory when an execution ends for good.
|
|
65
|
+
|
|
66
|
+
A factory is an object with a method rather than a plain callable so the
|
|
67
|
+
registration type can grow a second, optional member later -- a process-level
|
|
68
|
+
flush on execution-environment shutdown, for example -- without a second breaking
|
|
69
|
+
change to this surface.
|
|
70
|
+
|
|
71
|
+
Write a small factory class and construct the plugin in its `create_plugin`. The
|
|
72
|
+
factory holds what outlives an invocation, such as an exporter or a resolved
|
|
73
|
+
configuration, and setup work that can fail belongs in the factory's own
|
|
74
|
+
constructor rather than the plugin's:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
class AuditPluginFactory:
|
|
78
|
+
def __init__(self, sink):
|
|
79
|
+
self._sink = sink
|
|
80
|
+
|
|
81
|
+
def create_plugin(self, info):
|
|
82
|
+
return AuditPlugin(self._sink)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
plugins=[AuditPluginFactory(sink)]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
A plugin class is not a factory, and neither is a bare callable. `plugins=[MyPlugin]`
|
|
89
|
+
and `plugins=[lambda info: MyPlugin(sink)]` raise `PluginLoadError` during handler
|
|
90
|
+
initialization, because neither carries `create_plugin`. A class that declares
|
|
91
|
+
`create_plugin` as a `@classmethod` is accepted, since the requirement is the
|
|
92
|
+
member and not the kind of object.
|
|
45
93
|
|
|
46
|
-
Provider packages expose a
|
|
94
|
+
Provider packages expose such a factory:
|
|
47
95
|
|
|
48
96
|
```python
|
|
49
97
|
from aws_durable_execution_sdk_python.plugin import (
|
|
50
98
|
DurableInstrumentationPlugin,
|
|
51
|
-
|
|
99
|
+
InvocationStartInfo,
|
|
52
100
|
)
|
|
53
101
|
|
|
54
102
|
|
|
@@ -56,40 +104,40 @@ class AuditPlugin(DurableInstrumentationPlugin):
|
|
|
56
104
|
pass
|
|
57
105
|
|
|
58
106
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
107
|
+
class AuditPluginFactory:
|
|
108
|
+
def create_plugin(self, info: InvocationStartInfo) -> AuditPlugin:
|
|
109
|
+
return AuditPlugin()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
AUDIT_PLUGIN_FACTORY = AuditPluginFactory()
|
|
64
113
|
```
|
|
65
114
|
|
|
66
|
-
Register the
|
|
115
|
+
Register the factory instance in the package's `pyproject.toml`:
|
|
67
116
|
|
|
68
117
|
```toml
|
|
69
118
|
[project.entry-points."aws_durable_execution.plugins"]
|
|
70
|
-
example_audit = "example_audit:
|
|
119
|
+
example_audit = "example_audit:AUDIT_PLUGIN_FACTORY"
|
|
71
120
|
```
|
|
72
121
|
|
|
73
|
-
Set `plugin_api_version` to the literal API version the provider implements.
|
|
74
|
-
Update it only after verifying the provider against that API version.
|
|
75
|
-
|
|
76
122
|
Provider names must be unique across installed distributions. Missing,
|
|
77
|
-
ambiguous,
|
|
78
|
-
|
|
123
|
+
ambiguous, or wrongly shaped providers raise `PluginLoadError` during handler
|
|
124
|
+
initialization with the provider and distribution details.
|
|
79
125
|
|
|
80
126
|
### Selecting an OpenTelemetry view
|
|
81
127
|
|
|
82
|
-
Enable at most one of `
|
|
128
|
+
Enable at most one of `InvocationOtelPluginFactory` and
|
|
129
|
+
`ExecutionOtelPluginFactory`. This
|
|
83
130
|
constraint applies to the combined `plugins=[...]` argument and
|
|
84
131
|
`DURABLE_EXECUTION_PLUGINS=otel-invocation` or `otel-execution` selection.
|
|
85
|
-
Core
|
|
132
|
+
Core 3 with OTel 2 rejects both at cold start with `PluginLoadError` naming the
|
|
86
133
|
conflicting views; keep only one. Choose Invocation for work within each Lambda
|
|
87
134
|
invocation or Execution for logical operations across the durable execution.
|
|
88
|
-
Unrelated instrumentation
|
|
89
|
-
|
|
135
|
+
Unrelated instrumentation plugin factories can run alongside either view.
|
|
136
|
+
OTel 2 requires core 3.x and the per-invocation factory contract. Release the
|
|
137
|
+
coordinated core version before releasing OTel 2.
|
|
90
138
|
|
|
91
139
|
Plugin authors explicitly opt in by declaring
|
|
92
|
-
`__durable_registration_api__ = 1` on a
|
|
140
|
+
`__durable_registration_api__ = 1` on a factory class. That class and its subclasses
|
|
93
141
|
have their optional `exclusive_group` metadata validated and their
|
|
94
142
|
optional `on_registration_result(registered)` callback invoked. A missing group
|
|
95
143
|
or `None` adds no exclusivity constraint. The callback can release rejected
|
|
@@ -102,14 +150,14 @@ Once enabled, every class in the MRO explicitly declaring the integer `1`
|
|
|
102
150
|
contributes its resolved `exclusive_group`. These constraints accumulate:
|
|
103
151
|
repeating the marker with a new group adds to inherited groups rather than
|
|
104
152
|
replacing them, and `None` does not erase an inherited constraint. A group is
|
|
105
|
-
counted once per
|
|
153
|
+
counted once per factory registration even if several ancestors declare it.
|
|
106
154
|
Unmarked classes' coincidental same-named legacy fields/helpers remain inert.
|
|
107
|
-
Subclasses of the bundled OTel
|
|
155
|
+
Subclasses of the bundled OTel factories therefore retain the view group when they
|
|
108
156
|
add their own group. Repeat the marker to add a group or customize the callback;
|
|
109
|
-
the callback is still notified once per
|
|
110
|
-
in their hierarchy retain their existing attributes/helpers. The generic plugin
|
|
111
|
-
neither attribute nor hook
|
|
112
|
-
|
|
157
|
+
the callback is still notified once per factory. Factories with no marker anywhere
|
|
158
|
+
in their hierarchy retain their existing attributes/helpers. The generic plugin
|
|
159
|
+
base defines neither attribute nor hook. The registration API remains independent
|
|
160
|
+
of the removed provider API version 1 contract.
|
|
113
161
|
|
|
114
162
|
### Invocation worker lifecycle
|
|
115
163
|
|
|
@@ -123,11 +171,14 @@ End before returning the outcome. The caller shuts down the handler executor.
|
|
|
123
171
|
There is no separate handler-context hook or context-manager plugin API.
|
|
124
172
|
|
|
125
173
|
When plugins are registered, the worker begins with a copy of the caller's
|
|
126
|
-
context-variable bindings.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
174
|
+
context-variable bindings. Start hooks, the handler, serialization, resource
|
|
175
|
+
cleanup and End hooks use that same Context. Changes made by a Start hook remain
|
|
176
|
+
visible to later hooks and handler code even if that hook raises; the SDK contains
|
|
177
|
+
the exception and retains the plugin without rolling back its bindings. A later
|
|
178
|
+
Start hook may replace an earlier binding. Plugins are responsible for restoring
|
|
179
|
+
partial bindings themselves when needed, either before raising or during End.
|
|
180
|
+
End runs in the same Context as Start, so context-variable tokens retain their
|
|
181
|
+
owner. End hooks retain forward registration order, not reverse stack order,
|
|
131
182
|
so they must not rely on observing a stack-like unwind of other plugins' contexts.
|
|
132
183
|
Each SDK-managed `map` or `parallel` branch admission, including an in-process
|
|
133
184
|
resume, receives a fresh copy of the coordinator's bindings when plugins are
|
|
@@ -139,11 +190,11 @@ cleanup failures, leaving the host's bindings unchanged. This isolates bindings,
|
|
|
139
190
|
not mutations to shared objects or external side effects. Without plugins, the
|
|
140
191
|
handler retains its existing fresh-worker context behavior.
|
|
141
192
|
|
|
142
|
-
|
|
143
|
-
requires
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
193
|
+
Core 3 creates a fresh plugin instance per invocation through each registered
|
|
194
|
+
factory. OTel 2 requires core 3.x and uses the existing Start/End hooks on those
|
|
195
|
+
instances. Upgrade both packages together; core 3 rejects OTel 1.x registrations
|
|
196
|
+
and the removed provider API version 1 contract. The independent registration
|
|
197
|
+
API remains unchanged.
|
|
147
198
|
|
|
148
199
|
### Chained-invoke propagation
|
|
149
200
|
|
|
@@ -177,9 +228,8 @@ Python has no `DistributedMapOptions` wrapper
|
|
|
177
228
|
or distributed-map START API: its existing map/parallel operations use CONTEXT.
|
|
178
229
|
The design's corresponding distributed-map field awaits that future modeled path.
|
|
179
230
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
cores without this optional contract contribute no new propagation metadata.
|
|
231
|
+
This release pairs core 3 with OTel 2 and its per-invocation factories. Custom
|
|
232
|
+
plugins that omit the optional hook retain their existing tracing behavior.
|
|
183
233
|
Backend rollout and deployed downstream trace-topology validation remain pending.
|
|
184
234
|
|
|
185
235
|
## 🚀 Quick Start
|
|
@@ -53,7 +53,6 @@ if TYPE_CHECKING:
|
|
|
53
53
|
from collections.abc import Callable
|
|
54
54
|
from aws_durable_execution_sdk_python.config import CompletionConfig
|
|
55
55
|
from aws_durable_execution_sdk_python.context import DurableContext
|
|
56
|
-
from aws_durable_execution_sdk_python.lambda_service import OperationSubType
|
|
57
56
|
from aws_durable_execution_sdk_python.serdes import SerDes
|
|
58
57
|
from aws_durable_execution_sdk_python.state import (
|
|
59
58
|
CheckpointedResult,
|
|
@@ -129,8 +128,8 @@ class ConcurrentExecutor(Generic[CallableType, ResultType]):
|
|
|
129
128
|
executables: list[Executable[CallableType]],
|
|
130
129
|
max_concurrency: int | None,
|
|
131
130
|
completion_config: CompletionConfig,
|
|
132
|
-
sub_type_top:
|
|
133
|
-
sub_type_iteration:
|
|
131
|
+
sub_type_top: str,
|
|
132
|
+
sub_type_iteration: str,
|
|
134
133
|
name_prefix: str,
|
|
135
134
|
serdes: SerDes | None,
|
|
136
135
|
operation_id_namespace: OperationIdNamespace,
|
|
@@ -702,7 +701,6 @@ class ConcurrentExecutor(Generic[CallableType, ResultType]):
|
|
|
702
701
|
operation_identifier=operation_identifier,
|
|
703
702
|
config=ChildConfig(
|
|
704
703
|
serdes=self.item_serdes or self.serdes,
|
|
705
|
-
sub_type=self.sub_type_iteration,
|
|
706
704
|
is_virtual=is_virtual,
|
|
707
705
|
),
|
|
708
706
|
)
|
|
@@ -7,7 +7,7 @@ import logging
|
|
|
7
7
|
from collections import Counter
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
from enum import Enum
|
|
10
|
-
from typing import TYPE_CHECKING, Generic, TypeVar
|
|
10
|
+
from typing import TYPE_CHECKING, Generic, TypeVar, cast
|
|
11
11
|
|
|
12
12
|
from aws_durable_execution_sdk_python.exceptions import (
|
|
13
13
|
ChildContextError,
|
|
@@ -396,6 +396,12 @@ def envelope_summary_generator(
|
|
|
396
396
|
|
|
397
397
|
@dataclass(frozen=True)
|
|
398
398
|
class BatchItem(Generic[R]):
|
|
399
|
+
"""One branch of a map or parallel batch.
|
|
400
|
+
|
|
401
|
+
``error`` may be None on a FAILED item when its source contained no
|
|
402
|
+
ErrorObject. ``BatchResult.throw_if_error`` still raises for it.
|
|
403
|
+
"""
|
|
404
|
+
|
|
399
405
|
index: int
|
|
400
406
|
status: BatchItemStatus
|
|
401
407
|
result: R | None = None
|
|
@@ -489,18 +495,10 @@ class BatchResult(Generic[R], BatchResultProtocol[R]): # noqa: PYI059
|
|
|
489
495
|
}
|
|
490
496
|
|
|
491
497
|
def succeeded(self) -> list[BatchItem[R]]:
|
|
492
|
-
return [
|
|
493
|
-
item
|
|
494
|
-
for item in self.all
|
|
495
|
-
if item.status is BatchItemStatus.SUCCEEDED and item.result is not None
|
|
496
|
-
]
|
|
498
|
+
return [item for item in self.all if item.status is BatchItemStatus.SUCCEEDED]
|
|
497
499
|
|
|
498
500
|
def failed(self) -> list[BatchItem[R]]:
|
|
499
|
-
return [
|
|
500
|
-
item
|
|
501
|
-
for item in self.all
|
|
502
|
-
if item.status is BatchItemStatus.FAILED and item.error is not None
|
|
503
|
-
]
|
|
501
|
+
return [item for item in self.all if item.status is BatchItemStatus.FAILED]
|
|
504
502
|
|
|
505
503
|
def started(self) -> list[BatchItem[R]]:
|
|
506
504
|
return [item for item in self.all if item.status is BatchItemStatus.STARTED]
|
|
@@ -518,32 +516,44 @@ class BatchResult(Generic[R], BatchResultProtocol[R]): # noqa: PYI059
|
|
|
518
516
|
return any(item.status is BatchItemStatus.FAILED for item in self.all)
|
|
519
517
|
|
|
520
518
|
def throw_if_error(self) -> None:
|
|
521
|
-
|
|
522
|
-
(item
|
|
519
|
+
first_failed = next(
|
|
520
|
+
(item for item in self.all if item.status is BatchItemStatus.FAILED),
|
|
523
521
|
None,
|
|
524
522
|
)
|
|
525
|
-
if
|
|
523
|
+
if first_failed is not None:
|
|
524
|
+
first_error = first_failed.error
|
|
525
|
+
if first_error is None:
|
|
526
|
+
raise ChildContextError(
|
|
527
|
+
message=(
|
|
528
|
+
f"Batch item {first_failed.index} failed "
|
|
529
|
+
"without an ErrorObject."
|
|
530
|
+
)
|
|
531
|
+
)
|
|
532
|
+
|
|
526
533
|
# Each item runs in a child context, so a failed item surfaces as a
|
|
527
534
|
# ChildContextError wrapping the escaping error (reconstructed as its
|
|
528
535
|
# typed form where possible, with __cause__ set). Remaining errors
|
|
529
536
|
# stay in get_errors().
|
|
530
537
|
first_error.raise_as_operation_error(ChildContextError)
|
|
531
538
|
if self.completion_reason is CompletionReason.CUSTOM_COMPLETION_FAILED:
|
|
532
|
-
# A custom decision marked the batch failed with no item
|
|
539
|
+
# A custom decision marked the batch failed with no failed item.
|
|
533
540
|
raise BatchCompletionError(self.completion_reason)
|
|
534
541
|
|
|
535
542
|
def get_results(self) -> list[R]:
|
|
543
|
+
# None is a valid result for a branch that returns nothing.
|
|
536
544
|
return [
|
|
537
|
-
item.result
|
|
545
|
+
cast(R, item.result)
|
|
538
546
|
for item in self.all
|
|
539
|
-
if item.status is BatchItemStatus.SUCCEEDED
|
|
547
|
+
if item.status is BatchItemStatus.SUCCEEDED
|
|
540
548
|
]
|
|
541
549
|
|
|
542
|
-
def get_errors(self) -> list[ErrorObject]:
|
|
550
|
+
def get_errors(self) -> list[ErrorObject | None]:
|
|
551
|
+
"""Return one entry per FAILED item, in branch order.
|
|
552
|
+
|
|
553
|
+
An entry is None when the corresponding item has no ErrorObject.
|
|
554
|
+
"""
|
|
543
555
|
return [
|
|
544
|
-
item.error
|
|
545
|
-
for item in self.all
|
|
546
|
-
if item.status is BatchItemStatus.FAILED and item.error is not None
|
|
556
|
+
item.error for item in self.all if item.status is BatchItemStatus.FAILED
|
|
547
557
|
]
|
|
548
558
|
|
|
549
559
|
@property
|