aws-durable-execution-sdk-python 1.3.0__py3-none-any.whl → 1.5.0__py3-none-any.whl

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 (22) hide show
  1. aws_durable_execution_sdk_python/__about__.py +1 -1
  2. aws_durable_execution_sdk_python/__init__.py +9 -0
  3. aws_durable_execution_sdk_python/concurrency/executor.py +40 -13
  4. aws_durable_execution_sdk_python/concurrency/models.py +88 -17
  5. aws_durable_execution_sdk_python/config.py +108 -14
  6. aws_durable_execution_sdk_python/context.py +125 -18
  7. aws_durable_execution_sdk_python/exceptions.py +100 -42
  8. aws_durable_execution_sdk_python/execution.py +57 -51
  9. aws_durable_execution_sdk_python/lambda_service.py +12 -0
  10. aws_durable_execution_sdk_python/operation/child.py +43 -29
  11. aws_durable_execution_sdk_python/operation/map.py +15 -2
  12. aws_durable_execution_sdk_python/operation/parallel.py +25 -4
  13. aws_durable_execution_sdk_python/operation/step.py +2 -2
  14. aws_durable_execution_sdk_python/serdes.py +1 -5
  15. aws_durable_execution_sdk_python/state.py +146 -39
  16. aws_durable_execution_sdk_python/types.py +3 -1
  17. {aws_durable_execution_sdk_python-1.3.0.dist-info → aws_durable_execution_sdk_python-1.5.0.dist-info}/METADATA +6 -29
  18. aws_durable_execution_sdk_python-1.5.0.dist-info/RECORD +36 -0
  19. {aws_durable_execution_sdk_python-1.3.0.dist-info → aws_durable_execution_sdk_python-1.5.0.dist-info}/WHEEL +1 -1
  20. aws_durable_execution_sdk_python-1.3.0.dist-info/RECORD +0 -36
  21. {aws_durable_execution_sdk_python-1.3.0.dist-info → aws_durable_execution_sdk_python-1.5.0.dist-info}/licenses/LICENSE +0 -0
  22. {aws_durable_execution_sdk_python-1.3.0.dist-info → aws_durable_execution_sdk_python-1.5.0.dist-info}/licenses/NOTICE +0 -0
@@ -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__ = "1.3.0"
4
+ __version__ = "1.5.0"
@@ -1,11 +1,16 @@
1
1
  """AWS Lambda Durable Executions Python SDK."""
2
2
 
3
+ # Package metadata
4
+ from aws_durable_execution_sdk_python.__about__ import __version__
5
+
3
6
  # Main context - used in every durable function
4
7
  # Helper decorators - commonly used for step functions
5
8
  # Concurrency
6
9
  from aws_durable_execution_sdk_python.concurrency.models import BatchResult
10
+ from aws_durable_execution_sdk_python.config import ParallelBranch
7
11
  from aws_durable_execution_sdk_python.context import (
8
12
  DurableContext,
13
+ durable_parallel_branch,
9
14
  durable_step,
10
15
  durable_wait_for_callback,
11
16
  durable_with_child_context,
@@ -24,14 +29,18 @@ from aws_durable_execution_sdk_python.execution import durable_execution
24
29
  # Essential context types - passed to user functions
25
30
  from aws_durable_execution_sdk_python.types import StepContext
26
31
 
32
+
27
33
  __all__ = [
28
34
  "BatchResult",
29
35
  "DurableContext",
30
36
  "DurableExecutionsError",
31
37
  "InvocationError",
38
+ "ParallelBranch",
32
39
  "StepContext",
33
40
  "ValidationError",
41
+ "__version__",
34
42
  "durable_execution",
43
+ "durable_parallel_branch",
35
44
  "durable_step",
36
45
  "durable_wait_for_callback",
37
46
  "durable_with_child_context",
@@ -20,7 +20,10 @@ from aws_durable_execution_sdk_python.concurrency.models import (
20
20
  ExecutionCounters,
21
21
  SuspendResult,
22
22
  )
23
- from aws_durable_execution_sdk_python.config import ChildConfig
23
+ from aws_durable_execution_sdk_python.config import (
24
+ ChildConfig,
25
+ NestingType,
26
+ )
24
27
  from aws_durable_execution_sdk_python.exceptions import (
25
28
  OrphanedChildException,
26
29
  SuspendExecution,
@@ -30,6 +33,7 @@ from aws_durable_execution_sdk_python.identifier import OperationIdentifier
30
33
  from aws_durable_execution_sdk_python.lambda_service import ErrorObject
31
34
  from aws_durable_execution_sdk_python.operation.child import child_handler
32
35
 
36
+
33
37
  if TYPE_CHECKING:
34
38
  from collections.abc import Callable
35
39
 
@@ -142,6 +146,7 @@ class ConcurrentExecutor(ABC, Generic[CallableType, ResultType]):
142
146
  serdes: SerDes | None,
143
147
  item_serdes: SerDes | None = None,
144
148
  summary_generator: SummaryGenerator | None = None,
149
+ nesting_type: NestingType = NestingType.NESTED,
145
150
  ):
146
151
  """Initialize ConcurrentExecutor.
147
152
 
@@ -159,6 +164,7 @@ class ConcurrentExecutor(ABC, Generic[CallableType, ResultType]):
159
164
  self.sub_type_iteration = sub_type_iteration
160
165
  self.name_prefix = name_prefix
161
166
  self.summary_generator = summary_generator
167
+ self.nesting_type = nesting_type
162
168
 
163
169
  # Event-driven state tracking for when the executor is done
164
170
  self._completion_event = threading.Event()
@@ -188,6 +194,14 @@ class ConcurrentExecutor(ABC, Generic[CallableType, ResultType]):
188
194
  """Execute a single executable in a child context and return the result."""
189
195
  raise NotImplementedError
190
196
 
197
+ def get_iteration_name(self, index: int) -> str:
198
+ """Get the display name for an iteration/branch at the given index.
199
+
200
+ Subclasses can override this to provide custom naming (e.g., from item_namer
201
+ or branch names). The default returns "{name_prefix}{index}".
202
+ """
203
+ return f"{self.name_prefix}{index}"
204
+
191
205
  def execute(
192
206
  self, execution_state: ExecutionState, executor_context: DurableContext
193
207
  ) -> BatchResult[ResultType]:
@@ -196,6 +210,11 @@ class ConcurrentExecutor(ABC, Generic[CallableType, ResultType]):
196
210
  "▶️ Executing concurrent operation, items: %d", len(self.executables)
197
211
  )
198
212
 
213
+ # Early return for empty executables
214
+ if not self.executables:
215
+ logger.debug("No items to execute, returning empty result")
216
+ return self._create_result()
217
+
199
218
  max_workers = self.max_concurrency or len(self.executables)
200
219
 
201
220
  self.executables_with_state = [
@@ -385,29 +404,36 @@ class ConcurrentExecutor(ABC, Generic[CallableType, ResultType]):
385
404
  """
386
405
  Execute a single item in a derived child context.
387
406
 
388
- instead of relying on `executor_context.run_in_child_context`
389
- we generate an operation_id for the child, and then call `child_handler`
390
- directly. This avoids the hidden mutation of the context's internal counter.
391
- we can do this because we explicitly control the generation of step_id and do it
392
- using executable.index.
393
-
407
+ Instead of relying on `executor_context.run_in_child_context` we
408
+ generate an operation_id for the child, then call `child_handler`
409
+ directly. This avoids the hidden mutation of the context's
410
+ internal counter. We explicitly derive the child's operation_id
411
+ from `executable.index` so that the same input always produces
412
+ the same id regardless of the order branches actually run in.
394
413
 
395
- invariant: `operation_id` for a given executable is deterministic,
396
- and execution order invariant.
414
+ Invariant: `operation_id` for a given executable is deterministic
415
+ and execution-order invariant.
397
416
  """
398
417
 
399
- operation_id = executor_context._create_step_id_for_logical_step( # noqa: SLF001
418
+ operation_id: str = executor_context._create_step_id_for_logical_step( # noqa: SLF001
400
419
  executable.index
401
420
  )
402
- name = f"{self.name_prefix}{executable.index}"
403
- child_context = executor_context.create_child_context(operation_id)
421
+ name: str = self.get_iteration_name(executable.index)
422
+ is_virtual: bool = self.nesting_type is NestingType.FLAT
423
+
424
+ child_context: DurableContext = executor_context.create_child_context(
425
+ operation_id, is_virtual=is_virtual
426
+ )
427
+ # For NESTED this is for branch's START/SUCCEED/FAIL checkpoints (not the children of the branch).
428
+ # For FLAT `child_handler` skips checkpoints, so not used.
429
+ # Construct it unconditionally to keep the call simple.
404
430
  operation_identifier = OperationIdentifier(
405
431
  operation_id,
406
432
  executor_context._parent_id, # noqa: SLF001
407
433
  name,
408
434
  )
409
435
 
410
- def run_in_child_handler():
436
+ def run_in_child_handler() -> ResultType:
411
437
  return self.execute_item(child_context, executable)
412
438
 
413
439
  result: ResultType = child_handler(
@@ -418,6 +444,7 @@ class ConcurrentExecutor(ABC, Generic[CallableType, ResultType]):
418
444
  serdes=self.item_serdes or self.serdes,
419
445
  sub_type=self.sub_type_iteration,
420
446
  summary_generator=self.summary_generator,
447
+ is_virtual=is_virtual,
421
448
  ),
422
449
  )
423
450
  child_context.state.track_replay(operation_id=operation_id)
@@ -114,6 +114,85 @@ class BatchResult(Generic[R], BatchResultProtocol[R]): # noqa: PYI059
114
114
  completion_reason = CompletionReason(completion_reason_value)
115
115
  return cls(batch_items, completion_reason)
116
116
 
117
+ @staticmethod
118
+ def _get_completion_reason(
119
+ failure_count: int,
120
+ success_count: int,
121
+ completed_count: int,
122
+ total_count: int,
123
+ completion_config: CompletionConfig | None,
124
+ ) -> CompletionReason:
125
+ """
126
+ Determine completion reason based on completion counts.
127
+
128
+ Logic order:
129
+ 1. Check failure tolerance FIRST (before checking if all completed)
130
+ 2. Check if all completed
131
+ 3. Check if minimum successful reached
132
+ 4. Default to ALL_COMPLETED
133
+
134
+ Args:
135
+ failure_count: Number of failed items
136
+ success_count: Number of succeeded items
137
+ completed_count: Total completed (succeeded + failed)
138
+ total_count: Total number of items
139
+ completion_config: Optional completion configuration
140
+
141
+ Returns:
142
+ CompletionReason enum value
143
+ """
144
+ # STEP 1: Check tolerance first, before checking if all completed
145
+
146
+ # Handle fail-fast behavior (no completion config or empty completion config)
147
+ if completion_config is None:
148
+ if failure_count > 0:
149
+ return CompletionReason.FAILURE_TOLERANCE_EXCEEDED
150
+ else:
151
+ # Check if completion config has any criteria set
152
+ has_any_completion_criteria = (
153
+ completion_config.min_successful is not None
154
+ or completion_config.tolerated_failure_count is not None
155
+ or completion_config.tolerated_failure_percentage is not None
156
+ )
157
+
158
+ if not has_any_completion_criteria:
159
+ # Empty completion config - fail fast on any failure
160
+ if failure_count > 0:
161
+ return CompletionReason.FAILURE_TOLERANCE_EXCEEDED
162
+ else:
163
+ # Check specific tolerance thresholds
164
+ if (
165
+ completion_config.tolerated_failure_count is not None
166
+ and failure_count > completion_config.tolerated_failure_count
167
+ ):
168
+ return CompletionReason.FAILURE_TOLERANCE_EXCEEDED
169
+
170
+ if (
171
+ completion_config.tolerated_failure_percentage is not None
172
+ and total_count > 0
173
+ ):
174
+ failure_percentage = (failure_count / total_count) * 100
175
+ if (
176
+ failure_percentage
177
+ > completion_config.tolerated_failure_percentage
178
+ ):
179
+ return CompletionReason.FAILURE_TOLERANCE_EXCEEDED
180
+
181
+ # STEP 2: Check if all completed
182
+ if completed_count == total_count:
183
+ return CompletionReason.ALL_COMPLETED
184
+
185
+ # STEP 3: Check if minimum successful reached
186
+ if (
187
+ completion_config is not None
188
+ and completion_config.min_successful is not None
189
+ and success_count >= completion_config.min_successful
190
+ ):
191
+ return CompletionReason.MIN_SUCCESSFUL_REACHED
192
+
193
+ # STEP 4: Default
194
+ return CompletionReason.ALL_COMPLETED
195
+
117
196
  @classmethod
118
197
  def from_items(
119
198
  cls,
@@ -123,12 +202,8 @@ class BatchResult(Generic[R], BatchResultProtocol[R]): # noqa: PYI059
123
202
  """
124
203
  Infer completion reason based on batch item statuses and completion config.
125
204
 
126
- This follows the same logic as the TypeScript implementation:
127
- - If all items completed: ALL_COMPLETED
128
- - If minSuccessful threshold met and not all completed: MIN_SUCCESSFUL_REACHED
129
- - Otherwise: FAILURE_TOLERANCE_EXCEEDED
205
+ This follows the same logic as the TypeScript implementation.
130
206
  """
131
-
132
207
  statuses = (item.status for item in items)
133
208
  counts = Counter(statuses)
134
209
  succeeded_count = counts.get(BatchItemStatus.SUCCEEDED, 0)
@@ -138,18 +213,14 @@ class BatchResult(Generic[R], BatchResultProtocol[R]): # noqa: PYI059
138
213
  completed_count = succeeded_count + failed_count
139
214
  total_count = started_count + completed_count
140
215
 
141
- # If all items completed (no started items), it's ALL_COMPLETED
142
- if completed_count == total_count:
143
- completion_reason = CompletionReason.ALL_COMPLETED
144
- elif ( # If we have completion config and minSuccessful threshold is met
145
- completion_config
146
- and (min_successful := completion_config.min_successful) is not None
147
- and succeeded_count >= min_successful
148
- ):
149
- completion_reason = CompletionReason.MIN_SUCCESSFUL_REACHED
150
- else:
151
- # Otherwise, assume failure tolerance was exceeded
152
- completion_reason = CompletionReason.FAILURE_TOLERANCE_EXCEEDED
216
+ # Determine completion reason using the same logic as JavaScript SDK
217
+ completion_reason = cls._get_completion_reason(
218
+ failure_count=failed_count,
219
+ success_count=succeeded_count,
220
+ completed_count=completed_count,
221
+ total_count=total_count,
222
+ completion_config=completion_config,
223
+ )
153
224
 
154
225
  return cls(items, completion_reason)
155
226
 
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Generic, TypeVar
9
9
 
10
10
  from aws_durable_execution_sdk_python.exceptions import ValidationError
11
11
 
12
+
12
13
  P = TypeVar("P") # Payload type
13
14
  R = TypeVar("R") # Result type
14
15
  T = TypeVar("T")
@@ -76,6 +77,42 @@ class TerminationMode(Enum):
76
77
  ABANDON = "ABANDON"
77
78
 
78
79
 
80
+ class NestingType(Enum):
81
+ """Control how child contexts are created for batch operations.
82
+
83
+ Applies to `map` and `parallel`. Each branch or iteration runs inside a
84
+ child context.
85
+
86
+ - NESTED: full checkpointed context
87
+ - FLAT: a virtual context that skips checkpoints for the branch/iteration.
88
+
89
+ """
90
+
91
+ NESTED = "NESTED"
92
+ """Create CONTEXT operations for each branch/iteration with full checkpointing.
93
+
94
+ Operations within each branch/iteration are wrapped in their own context.
95
+
96
+ - Observability: high — each branch/iteration appears as a separate
97
+ operation in execution history.
98
+ - Cost: higher — consumes more operations due to CONTEXT creation
99
+ overhead.
100
+ - Scale: lower maximum iterations due to operation limits.
101
+ """
102
+
103
+ FLAT = "FLAT"
104
+ """Skip CONTEXT operations for branches/iterations using virtual contexts.
105
+
106
+ Operations execute directly without individual context wrapping.
107
+
108
+ - Observability: lower — branches/iterations don't appear as separate
109
+ operations in execution history.
110
+ - Cost: ~30% lower — reduces operation consumption by skipping CONTEXT
111
+ overhead.
112
+ - Scale: higher maximum iterations possible within operation limits.
113
+ """
114
+
115
+
79
116
  @dataclass(frozen=True)
80
117
  class CompletionConfig:
81
118
  """Configuration for determining when parallel/map operations complete.
@@ -187,6 +224,10 @@ class ParallelConfig:
187
224
  Used internally by map/parallel operations to handle large BatchResult payloads.
188
225
  Signature: (result: T) -> str
189
226
 
227
+ nesting_type: How child operations should inherit context from their parent.
228
+ - NESTED: Each branch runs in its own isolated context (default)
229
+ - FLAT: All branches share the same parent context
230
+
190
231
  Example:
191
232
  # Run at most 3 branches concurrently, succeed if any one succeeds
192
233
  config = ParallelConfig(
@@ -202,6 +243,42 @@ class ParallelConfig:
202
243
  serdes: SerDes | None = None
203
244
  item_serdes: SerDes | None = None
204
245
  summary_generator: SummaryGenerator | None = None
246
+ nesting_type: NestingType = NestingType.NESTED
247
+
248
+
249
+ @dataclass(frozen=True)
250
+ class ParallelBranch(Generic[T]):
251
+ """A named branch for parallel execution.
252
+
253
+ Use this to provide custom names for parallel branches, improving
254
+ observability in execution history.
255
+
256
+ Type Parameters:
257
+ T: The return type of the branch function.
258
+
259
+ Args:
260
+ func: The callable to execute in this branch. Receives a DurableContext.
261
+ name: Optional custom name for this branch. When provided, replaces
262
+ the default "parallel-branch-{index}" naming in execution history.
263
+ This affects observability but not replay determinism.
264
+
265
+ Example:
266
+ context.parallel(
267
+ functions=[
268
+ ParallelBranch(func=lambda ctx: fetch_user(ctx), name="fetch-user-data"),
269
+ ParallelBranch(func=lambda ctx: fetch_orders(ctx), name="fetch-order-history"),
270
+ ],
271
+ name="load-data",
272
+ config=ParallelConfig(max_concurrency=2),
273
+ )
274
+ """
275
+
276
+ func: Callable
277
+ name: str | None = None
278
+
279
+ def __call__(self, *args, **kwargs):
280
+ """Delegate to the wrapped function, making ParallelBranch itself callable."""
281
+ return self.func(*args, **kwargs)
205
282
 
206
283
 
207
284
  class StepSemantics(Enum):
@@ -218,12 +295,6 @@ class StepConfig:
218
295
  serdes: SerDes | None = None
219
296
 
220
297
 
221
- class CheckpointMode(Enum):
222
- NO_CHECKPOINT = ("NO_CHECKPOINT",)
223
- CHECKPOINT_AT_FINISH = ("CHECKPOINT_AT_FINISH",)
224
- CHECKPOINT_AT_START_AND_FINISH = "CHECKPOINT_AT_START_AND_FINISH"
225
-
226
-
227
298
  @dataclass(frozen=True)
228
299
  class ChildConfig(Generic[T]):
229
300
  """Configuration options for child context operations.
@@ -259,21 +330,23 @@ class ChildConfig(Generic[T]):
259
330
 
260
331
  Used internally by map/parallel operations to handle large BatchResult payloads.
261
332
  Signature: (result: T) -> str
262
- Note:
263
- checkpoint_mode field is commented out as it's not currently implemented.
264
- When implemented, it will control when checkpoints are created:
265
- - CHECKPOINT_AT_START_AND_FINISH: Checkpoint at both start and completion (default)
266
- - CHECKPOINT_AT_FINISH: Only checkpoint when operation completes
267
- - NO_CHECKPOINT: No automatic checkpointing
333
+
334
+ is_virtual: When True, skip all checkpoints (START, SUCCEED,
335
+ FAIL) for this child context and propagate the caller's reporting
336
+ parent id through to operations created inside the child. The
337
+ branch is a logical scope for step-id prefixing but does not
338
+ appear in the execution history. Used internally by
339
+ NestingType.FLAT branches. Use this to group operations without
340
+ adding a CONTEXT entry to the execution history.
268
341
 
269
342
  See TypeScript reference: aws-durable-execution-sdk-js/src/types/index.ts
270
343
  """
271
344
 
272
- # checkpoint_mode: CheckpointMode = CheckpointMode.CHECKPOINT_AT_START_AND_FINISH
273
345
  serdes: SerDes | None = None
274
346
  item_serdes: SerDes | None = None
275
347
  sub_type: OperationSubType | None = None
276
348
  summary_generator: SummaryGenerator | None = None
349
+ is_virtual: bool = False
277
350
 
278
351
 
279
352
  class ItemsPerBatchUnit(Enum):
@@ -317,12 +390,15 @@ class ItemBatcher(Generic[T]):
317
390
 
318
391
 
319
392
  @dataclass(frozen=True)
320
- class MapConfig:
393
+ class MapConfig(Generic[T]):
321
394
  """Configuration options for map operations over collections.
322
395
 
323
396
  This class configures how map operations process collections of items,
324
397
  including concurrency, batching, completion criteria, and serialization.
325
398
 
399
+ Type Parameters:
400
+ T: The type of items being processed in the map operation.
401
+
326
402
  Args:
327
403
  max_concurrency: Maximum number of items to process concurrently.
328
404
  If None, no limit is imposed and all items are processed concurrently.
@@ -361,6 +437,16 @@ class MapConfig:
361
437
  Used internally by map/parallel operations to handle large BatchResult payloads.
362
438
  Signature: (result: T) -> str
363
439
 
440
+ nesting_type: How child operations should inherit context from their parent.
441
+ - NESTED: Each item runs in its own isolated context (default)
442
+ - FLAT: All items share the same parent context
443
+
444
+ item_namer: Optional callable to generate custom names for each map iteration.
445
+ When provided, replaces the default "map-item-{index}" naming scheme.
446
+ Receives the item and its index, and returns a string name for that iteration.
447
+ This affects observability (execution history names) but not replay determinism.
448
+ If None, uses the default naming: "map-item-{index}".
449
+
364
450
  Example:
365
451
  # Process 5 items at a time, batch by count, require all to succeed
366
452
  config = MapConfig(
@@ -368,6 +454,12 @@ class MapConfig:
368
454
  item_batcher=ItemBatcher(max_items_per_batch=10),
369
455
  completion_config=CompletionConfig.all_successful()
370
456
  )
457
+
458
+ # With custom iteration names
459
+ config = MapConfig(
460
+ max_concurrency=5,
461
+ item_namer=lambda item, index: f"process-order-{item.id}"
462
+ )
371
463
  """
372
464
 
373
465
  max_concurrency: int | None = None
@@ -376,6 +468,8 @@ class MapConfig:
376
468
  serdes: SerDes | None = None
377
469
  item_serdes: SerDes | None = None
378
470
  summary_generator: SummaryGenerator | None = None
471
+ nesting_type: NestingType = NestingType.NESTED
472
+ item_namer: Callable[[T, int], str] | None = None
379
473
 
380
474
 
381
475
  @dataclass(frozen=True)