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.
Files changed (107) hide show
  1. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/.gitignore +1 -0
  2. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/PKG-INFO +91 -41
  3. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/README.md +90 -40
  4. {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
  5. {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
  6. {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
  7. {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
  8. {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
  9. {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
  10. {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
  11. {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
  12. {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
  13. {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
  14. {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
  15. aws_durable_execution_sdk_python-3.0.0/src/aws_durable_execution_sdk_python/plugin.py +2035 -0
  16. aws_durable_execution_sdk_python-3.0.0/src/aws_durable_execution_sdk_python/plugin_discovery.py +517 -0
  17. {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
  18. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/concurrency_test.py +192 -20
  19. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/config_test.py +65 -3
  20. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/context_test.py +123 -26
  21. aws_durable_execution_sdk_python-3.0.0/tests/e2e/batch_result_none_replay_test.py +83 -0
  22. {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
  23. aws_durable_execution_sdk_python-3.0.0/tests/e2e/custom_subtype_int_test.py +165 -0
  24. aws_durable_execution_sdk_python-3.0.0/tests/e2e/factory_registration_cleanup_int_test.py +93 -0
  25. aws_durable_execution_sdk_python-3.0.0/tests/e2e/factory_registration_int_test.py +96 -0
  26. aws_durable_execution_sdk_python-3.0.0/tests/e2e/handler_context_propagation_int_test.py +296 -0
  27. {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
  28. {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
  29. aws_durable_execution_sdk_python-3.0.0/tests/e2e/plugin_hook_contract_int_test.py +383 -0
  30. {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
  31. {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
  32. {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
  33. {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
  34. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/execution_test.py +579 -25
  35. aws_durable_execution_sdk_python-3.0.0/tests/factory_registration_test.py +475 -0
  36. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/handler_worker_context_test.py +21 -19
  37. aws_durable_execution_sdk_python-3.0.0/tests/identifier_test.py +134 -0
  38. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/lambda_service_test.py +172 -67
  39. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/child_test.py +32 -35
  40. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/invoke_propagation_test.py +6 -5
  41. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/step_test.py +9 -9
  42. aws_durable_execution_sdk_python-3.0.0/tests/plugin_discovery_test.py +844 -0
  43. aws_durable_execution_sdk_python-3.0.0/tests/plugin_hook_registry_test.py +1704 -0
  44. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/plugin_test.py +919 -92
  45. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/propagation_test.py +20 -35
  46. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/state_test.py +58 -37
  47. aws_durable_execution_sdk_python-3.0.0/tests/test_helpers.py +79 -0
  48. aws_durable_execution_sdk_python-2.1.0/src/aws_durable_execution_sdk_python/plugin.py +0 -997
  49. aws_durable_execution_sdk_python-2.1.0/src/aws_durable_execution_sdk_python/plugin_discovery.py +0 -353
  50. aws_durable_execution_sdk_python-2.1.0/tests/identifier_test.py +0 -56
  51. aws_durable_execution_sdk_python-2.1.0/tests/plugin_discovery_test.py +0 -1002
  52. aws_durable_execution_sdk_python-2.1.0/tests/test_helpers.py +0 -20
  53. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/LICENSE +0 -0
  54. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/NOTICE +0 -0
  55. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/pyproject.toml +0 -0
  56. {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
  57. {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
  58. {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
  59. {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
  60. {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
  61. {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
  62. {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
  63. {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
  64. {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
  65. {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
  66. {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
  67. {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
  68. {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
  69. {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
  70. {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
  71. {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
  72. {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
  73. {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
  74. {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
  75. {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
  76. {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
  77. {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
  78. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/__init__.py +0 -0
  79. {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
  80. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/e2e/__init__.py +0 -0
  81. {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
  82. {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
  83. {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
  84. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/e2e/execution_int_test.py +0 -0
  85. {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
  86. {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
  87. {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
  88. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/exceptions_test.py +0 -0
  89. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/filesystem_serdes_test.py +0 -0
  90. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/invoke_wire_propagation_test.py +0 -0
  91. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/logger_test.py +0 -0
  92. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/__init__.py +0 -0
  93. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/base_test.py +0 -0
  94. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/callback_test.py +0 -0
  95. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/invoke_test.py +0 -0
  96. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/map_test.py +0 -0
  97. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/parallel_test.py +0 -0
  98. {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
  99. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/operation/wait_test.py +0 -0
  100. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/preview_test.py +0 -0
  101. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/retries_test.py +0 -0
  102. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/serdes_test.py +0 -0
  103. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/suspend_test.py +0 -0
  104. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/threading_test.py +0 -0
  105. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/types_test.py +0 -0
  106. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/waits_test.py +0 -0
  107. {aws_durable_execution_sdk_python-2.1.0 → aws_durable_execution_sdk_python-3.0.0}/tests/with_retry_test.py +0 -0
@@ -34,6 +34,7 @@ dist/
34
34
  **/build/
35
35
  **/*.zip
36
36
  .aws-sam/
37
+ .microvm-image-build/
37
38
  **/template.generated.json
38
39
 
39
40
  .env
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: aws-durable-execution-sdk-python
3
- Version: 2.1.0
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 plugins run first and take precedence when a
65
- dynamic provider creates the same concrete plugin type.
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 versioned factory:
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
- DurableInstrumentationPluginProvider,
120
+ InvocationStartInfo,
73
121
  )
74
122
 
75
123
 
@@ -77,40 +125,40 @@ class AuditPlugin(DurableInstrumentationPlugin):
77
125
  pass
78
126
 
79
127
 
80
- AUDIT_PLUGIN_PROVIDER = DurableInstrumentationPluginProvider(
81
- plugin_type=AuditPlugin,
82
- factory=AuditPlugin,
83
- plugin_api_version=1,
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 provider in the package's `pyproject.toml`:
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:AUDIT_PLUGIN_PROVIDER"
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, incompatible, or invalid providers raise `PluginLoadError` during
99
- handler initialization with the provider and distribution details.
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 `InvocationOtelPlugin` and `ExecutionOtelPlugin`. This
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 2.1+ with OTel 1.1+ rejects both at cold start with `PluginLoadError` naming the
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 plugins can run alongside either view. OTel 1.1 requires core 2.1 or later for the invocation-worker lifecycle as well
110
- as view-exclusivity validation. Release core 2.1 before releasing OTel 1.1.
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 plugin class. That class and its subclasses
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 plugin registration even if several ancestors declare it.
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 views therefore retain the view group when they
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 plugin. Plugins with no marker anywhere
131
- in their hierarchy retain their existing attributes/helpers. The generic plugin base defines
132
- neither attribute nor hook, and provider API version 1 and existing plugin
133
- lifecycle order are unchanged.
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. Successful Start bindings flow into later Start hooks,
148
- the handler, serialization and resource cleanup. A later successful Start may
149
- replace an earlier binding. If a Start hook raises, its new bindings are discarded
150
- for subsequent work; its End still runs in the original Context so its tokens can
151
- be reset. End hooks retain forward registration order, not reverse stack order,
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
- These lifecycle guarantees require core 2.1.0 or later. The OTel 1.1 plugin
164
- requires that core version and uses the existing Start/End hooks. Upgrading the
165
- core alone with OTel 1.0 isolates worker bindings, but does not add the newer
166
- Invocation-view fallback to that older plugin; upgrade both packages for it.
167
- Provider API version 1 and the independent registration API are unchanged.
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
- Release coordination still requires the compatible core/OTel minor versions.
202
- Existing valid core/plugin combinations retain prior tracing behavior; older
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 plugins run first and take precedence when a
44
- dynamic provider creates the same concrete plugin type.
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 versioned factory:
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
- DurableInstrumentationPluginProvider,
99
+ InvocationStartInfo,
52
100
  )
53
101
 
54
102
 
@@ -56,40 +104,40 @@ class AuditPlugin(DurableInstrumentationPlugin):
56
104
  pass
57
105
 
58
106
 
59
- AUDIT_PLUGIN_PROVIDER = DurableInstrumentationPluginProvider(
60
- plugin_type=AuditPlugin,
61
- factory=AuditPlugin,
62
- plugin_api_version=1,
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 provider in the package's `pyproject.toml`:
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:AUDIT_PLUGIN_PROVIDER"
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, incompatible, or invalid providers raise `PluginLoadError` during
78
- handler initialization with the provider and distribution details.
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 `InvocationOtelPlugin` and `ExecutionOtelPlugin`. This
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 2.1+ with OTel 1.1+ rejects both at cold start with `PluginLoadError` naming the
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 plugins can run alongside either view. OTel 1.1 requires core 2.1 or later for the invocation-worker lifecycle as well
89
- as view-exclusivity validation. Release core 2.1 before releasing OTel 1.1.
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 plugin class. That class and its subclasses
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 plugin registration even if several ancestors declare it.
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 views therefore retain the view group when they
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 plugin. Plugins with no marker anywhere
110
- in their hierarchy retain their existing attributes/helpers. The generic plugin base defines
111
- neither attribute nor hook, and provider API version 1 and existing plugin
112
- lifecycle order are unchanged.
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. Successful Start bindings flow into later Start hooks,
127
- the handler, serialization and resource cleanup. A later successful Start may
128
- replace an earlier binding. If a Start hook raises, its new bindings are discarded
129
- for subsequent work; its End still runs in the original Context so its tokens can
130
- be reset. End hooks retain forward registration order, not reverse stack order,
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
- These lifecycle guarantees require core 2.1.0 or later. The OTel 1.1 plugin
143
- requires that core version and uses the existing Start/End hooks. Upgrading the
144
- core alone with OTel 1.0 isolates worker bindings, but does not add the newer
145
- Invocation-view fallback to that older plugin; upgrade both packages for it.
146
- Provider API version 1 and the independent registration API are unchanged.
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
- Release coordination still requires the compatible core/OTel minor versions.
181
- Existing valid core/plugin combinations retain prior tracing behavior; older
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
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2025-present Amazon.com, Inc. or its affiliates.
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
- __version__ = "2.1.0"
4
+ __version__ = "3.0.0"
@@ -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: OperationSubType,
133
- sub_type_iteration: OperationSubType,
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
- first_error = next(
522
- (item.error for item in self.all if item.status is BatchItemStatus.FAILED),
519
+ first_failed = next(
520
+ (item for item in self.all if item.status is BatchItemStatus.FAILED),
523
521
  None,
524
522
  )
525
- if first_error is not None:
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 error.
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 and item.result is not None
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