aws-durable-execution-sdk-python 1.4.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.
@@ -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.4.0"
4
+ __version__ = "1.5.0"
@@ -7,8 +7,10 @@ from aws_durable_execution_sdk_python.__about__ import __version__
7
7
  # Helper decorators - commonly used for step functions
8
8
  # Concurrency
9
9
  from aws_durable_execution_sdk_python.concurrency.models import BatchResult
10
+ from aws_durable_execution_sdk_python.config import ParallelBranch
10
11
  from aws_durable_execution_sdk_python.context import (
11
12
  DurableContext,
13
+ durable_parallel_branch,
12
14
  durable_step,
13
15
  durable_wait_for_callback,
14
16
  durable_with_child_context,
@@ -27,15 +29,18 @@ from aws_durable_execution_sdk_python.execution import durable_execution
27
29
  # Essential context types - passed to user functions
28
30
  from aws_durable_execution_sdk_python.types import StepContext
29
31
 
32
+
30
33
  __all__ = [
31
34
  "BatchResult",
32
35
  "DurableContext",
33
36
  "DurableExecutionsError",
34
37
  "InvocationError",
38
+ "ParallelBranch",
35
39
  "StepContext",
36
40
  "ValidationError",
37
41
  "__version__",
38
42
  "durable_execution",
43
+ "durable_parallel_branch",
39
44
  "durable_step",
40
45
  "durable_wait_for_callback",
41
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,
@@ -143,6 +146,7 @@ class ConcurrentExecutor(ABC, Generic[CallableType, ResultType]):
143
146
  serdes: SerDes | None,
144
147
  item_serdes: SerDes | None = None,
145
148
  summary_generator: SummaryGenerator | None = None,
149
+ nesting_type: NestingType = NestingType.NESTED,
146
150
  ):
147
151
  """Initialize ConcurrentExecutor.
148
152
 
@@ -160,6 +164,7 @@ class ConcurrentExecutor(ABC, Generic[CallableType, ResultType]):
160
164
  self.sub_type_iteration = sub_type_iteration
161
165
  self.name_prefix = name_prefix
162
166
  self.summary_generator = summary_generator
167
+ self.nesting_type = nesting_type
163
168
 
164
169
  # Event-driven state tracking for when the executor is done
165
170
  self._completion_event = threading.Event()
@@ -189,6 +194,14 @@ class ConcurrentExecutor(ABC, Generic[CallableType, ResultType]):
189
194
  """Execute a single executable in a child context and return the result."""
190
195
  raise NotImplementedError
191
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
+
192
205
  def execute(
193
206
  self, execution_state: ExecutionState, executor_context: DurableContext
194
207
  ) -> BatchResult[ResultType]:
@@ -391,29 +404,36 @@ class ConcurrentExecutor(ABC, Generic[CallableType, ResultType]):
391
404
  """
392
405
  Execute a single item in a derived child context.
393
406
 
394
- instead of relying on `executor_context.run_in_child_context`
395
- we generate an operation_id for the child, and then call `child_handler`
396
- directly. This avoids the hidden mutation of the context's internal counter.
397
- we can do this because we explicitly control the generation of step_id and do it
398
- using executable.index.
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.
399
413
 
400
-
401
- invariant: `operation_id` for a given executable is deterministic,
402
- and execution order invariant.
414
+ Invariant: `operation_id` for a given executable is deterministic
415
+ and execution-order invariant.
403
416
  """
404
417
 
405
- 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
406
419
  executable.index
407
420
  )
408
- name = f"{self.name_prefix}{executable.index}"
409
- 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.
410
430
  operation_identifier = OperationIdentifier(
411
431
  operation_id,
412
432
  executor_context._parent_id, # noqa: SLF001
413
433
  name,
414
434
  )
415
435
 
416
- def run_in_child_handler():
436
+ def run_in_child_handler() -> ResultType:
417
437
  return self.execute_item(child_context, executable)
418
438
 
419
439
  result: ResultType = child_handler(
@@ -424,6 +444,7 @@ class ConcurrentExecutor(ABC, Generic[CallableType, ResultType]):
424
444
  serdes=self.item_serdes or self.serdes,
425
445
  sub_type=self.sub_type_iteration,
426
446
  summary_generator=self.summary_generator,
447
+ is_virtual=is_virtual,
427
448
  ),
428
449
  )
429
450
  child_context.state.track_replay(operation_id=operation_id)
@@ -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)
@@ -12,6 +12,7 @@ from aws_durable_execution_sdk_python.config import (
12
12
  Duration,
13
13
  InvokeConfig,
14
14
  MapConfig,
15
+ ParallelBranch,
15
16
  ParallelConfig,
16
17
  StepConfig,
17
18
  WaitForCallbackConfig,
@@ -55,6 +56,7 @@ from aws_durable_execution_sdk_python.types import (
55
56
  WaitForConditionCheckContext,
56
57
  )
57
58
 
59
+
58
60
  if TYPE_CHECKING:
59
61
  from collections.abc import Callable, Sequence
60
62
 
@@ -119,6 +121,52 @@ def durable_with_child_context(
119
121
  return wrapper
120
122
 
121
123
 
124
+ def durable_parallel_branch(
125
+ name: str | None = None,
126
+ ) -> Callable[
127
+ [Callable[Concatenate[DurableContext, Params], T]],
128
+ Callable[Params, ParallelBranch[T]],
129
+ ]:
130
+ """Wrap your callable into a named ParallelBranch for use with context.parallel().
131
+
132
+ This is a decorator factory — call it with an optional name to produce
133
+ the actual decorator.
134
+
135
+ Args:
136
+ name: Optional custom name for this branch. When provided, replaces
137
+ the default "parallel-branch-{index}" naming in execution history.
138
+ If None, the function's __name__ is used.
139
+
140
+ Example:
141
+ @durable_parallel_branch(name="fetch-user-data")
142
+ def fetch_user(ctx: DurableContext, user_id: str) -> dict:
143
+ return ctx.step(lambda _: {"id": user_id, "name": "Jane"}, name="load_user")
144
+
145
+ @durable_parallel_branch(name="fetch-orders")
146
+ def fetch_orders(ctx: DurableContext, user_id: str) -> list:
147
+ return ctx.step(lambda _: ["order1", "order2"], name="load_orders")
148
+
149
+ # Usage in a durable handler:
150
+ results = context.parallel(
151
+ functions=[fetch_user(user_id), fetch_orders(user_id)],
152
+ name="load-data",
153
+ )
154
+ """
155
+
156
+ def decorator(
157
+ func: Callable[Concatenate[DurableContext, Params], T],
158
+ ) -> Callable[Params, ParallelBranch[T]]:
159
+ def wrapper(*args, **kwargs) -> ParallelBranch[T]:
160
+ def function_with_arguments(ctx: DurableContext) -> T:
161
+ return func(ctx, *args, **kwargs)
162
+
163
+ return ParallelBranch(func=function_with_arguments, name=name)
164
+
165
+ return wrapper
166
+
167
+ return decorator
168
+
169
+
122
170
  def durable_wait_for_callback(
123
171
  func: Callable[Concatenate[str, WaitForCallbackContext, Params], T],
124
172
  ) -> Callable[Params, Callable[[str, WaitForCallbackContext], T]]:
@@ -237,11 +285,20 @@ class DurableContext(DurableContextProtocol):
237
285
  lambda_context: LambdaContext | None = None,
238
286
  parent_id: str | None = None,
239
287
  logger: Logger | None = None,
288
+ step_id_prefix: str | None = None,
240
289
  ) -> None:
241
290
  self.state: ExecutionState = state
242
291
  self.execution_context: ExecutionContext = execution_context
243
292
  self.lambda_context = lambda_context
293
+ # operations inside this context use this id as their parent
244
294
  self._parent_id: str | None = parent_id
295
+ # child operations use this to generate deterministic step ids.
296
+ # differs from `parent_id` only for virtual contexts.
297
+ self._step_id_prefix: str | None = (
298
+ step_id_prefix if step_id_prefix is not None else parent_id
299
+ )
300
+ # cached at construction to make invariant even if parent/prefix mutates.
301
+ self._is_virtual: bool = self._parent_id != self._step_id_prefix
245
302
  self._step_counter: OrderedCounter = OrderedCounter()
246
303
 
247
304
  log_info = LogInfo(
@@ -254,6 +311,18 @@ class DurableContext(DurableContextProtocol):
254
311
  info=log_info,
255
312
  )
256
313
 
314
+ @property
315
+ def is_virtual(self) -> bool:
316
+ """True if this context does not checkpoint its own start and completion.
317
+
318
+ You create a virtual context by `create_child_context(..., is_virtual=True)`.
319
+ FLAT-mode `map`/`parallel` branches uses virtual contexts. Inner operations
320
+ use the grandfather as parent (enclosing non-virtual ancestor, skipping the branch level
321
+ in the hierarchy), while step ids are still prefixed with the branch's own
322
+ operation id so replay stays deterministic.
323
+ """
324
+ return self._is_virtual
325
+
257
326
  # region factories
258
327
  @staticmethod
259
328
  def from_lambda_context(
@@ -269,18 +338,43 @@ class DurableContext(DurableContextProtocol):
269
338
  parent_id=None,
270
339
  )
271
340
 
272
- def create_child_context(self, parent_id: str) -> DurableContext:
273
- """Create a child context from the given parent."""
274
- logger.debug("Creating child context for parent %s", parent_id)
341
+ def create_child_context(
342
+ self, operation_id: str, *, is_virtual: bool = False
343
+ ) -> DurableContext:
344
+ """Create a child context for the given operation.
345
+
346
+ Args:
347
+ operation_id: The operation id that owns the child context. Used as
348
+ the child's step-id prefix in all cases.
349
+ is_virtual: When `True`, create a virtual child whose inner
350
+ operations report to this context's own `_parent_id` (one
351
+ level up the hierarchy). When `False` (default), produce a
352
+ regular child whose inner operations report to
353
+ `operation_id`.
354
+
355
+ Returns:
356
+ A new `DurableContext` child for the current context.
357
+ """
358
+ # For a virtual child, propagate the current `_parent_id` so its
359
+ # inner operations refer to the grandparent rather than the parent.
360
+ # For a regular non-virtual child, the child's own `operation_id` is
361
+ # the parent id for its inner operations (standard nesting).
362
+ child_parent_id: str | None = self._parent_id if is_virtual else operation_id
363
+ logger.debug(
364
+ "Creating child context for operation %s (is_virtual=%s)",
365
+ operation_id,
366
+ is_virtual,
367
+ )
275
368
  return DurableContext(
276
369
  state=self.state,
277
370
  execution_context=self.execution_context,
278
371
  lambda_context=self.lambda_context,
279
- parent_id=parent_id,
372
+ parent_id=child_parent_id,
373
+ step_id_prefix=operation_id,
280
374
  logger=self.logger.with_log_info(
281
375
  LogInfo(
282
376
  execution_state=self.state,
283
- parent_id=parent_id,
377
+ parent_id=child_parent_id,
284
378
  )
285
379
  ),
286
380
  )
@@ -310,7 +404,8 @@ class DurableContext(DurableContextProtocol):
310
404
  This allows us to recover operation ids or even look
311
405
  forward without changing the internal state of this context.
312
406
  """
313
- step_id = f"{self._parent_id}-{step}" if self._parent_id else str(step)
407
+ prefix: str | None = self._step_id_prefix
408
+ step_id: str = f"{prefix}-{step}" if prefix else str(step)
314
409
  return hashlib.blake2b(step_id.encode()).hexdigest()[:64]
315
410
 
316
411
  def _create_step_id(self) -> str:
@@ -347,7 +442,9 @@ class DurableContext(DurableContextProtocol):
347
442
  executor: CallbackOperationExecutor = CallbackOperationExecutor(
348
443
  state=self.state,
349
444
  operation_identifier=OperationIdentifier(
350
- operation_id=operation_id, parent_id=self._parent_id, name=name
445
+ operation_id=operation_id,
446
+ parent_id=self._parent_id,
447
+ name=name,
351
448
  ),
352
449
  config=config,
353
450
  )
@@ -409,9 +506,11 @@ class DurableContext(DurableContextProtocol):
409
506
 
410
507
  operation_id = self._create_step_id()
411
508
  operation_identifier = OperationIdentifier(
412
- operation_id=operation_id, parent_id=self._parent_id, name=map_name
509
+ operation_id=operation_id,
510
+ parent_id=self._parent_id,
511
+ name=map_name,
413
512
  )
414
- map_context = self.create_child_context(parent_id=operation_id)
513
+ map_context = self.create_child_context(operation_id=operation_id)
415
514
 
416
515
  def map_in_child_context() -> BatchResult[R]:
417
516
  # map_context is a child_context of the context upon which `.map`
@@ -445,14 +544,14 @@ class DurableContext(DurableContextProtocol):
445
544
 
446
545
  def parallel(
447
546
  self,
448
- functions: Sequence[Callable[[DurableContext], T]],
547
+ functions: Sequence[Callable[[DurableContext], T] | ParallelBranch[T]],
449
548
  name: str | None = None,
450
549
  config: ParallelConfig | None = None,
451
550
  ) -> BatchResult[T]:
452
551
  """Execute multiple callables in parallel."""
453
552
  # _create_step_id() is thread-safe. rest of method is safe, since using local copy of parent id
454
553
  operation_id = self._create_step_id()
455
- parallel_context = self.create_child_context(parent_id=operation_id)
554
+ parallel_context = self.create_child_context(operation_id=operation_id)
456
555
  operation_identifier = OperationIdentifier(
457
556
  operation_id=operation_id, parent_id=self._parent_id, name=name
458
557
  )
@@ -508,14 +607,22 @@ class DurableContext(DurableContextProtocol):
508
607
  # _create_step_id() is thread-safe. rest of method is safe, since using local copy of parent id
509
608
  operation_id = self._create_step_id()
510
609
 
610
+ is_virtual: bool = config.is_virtual if config else False
611
+
511
612
  def callable_with_child_context():
512
- return func(self.create_child_context(parent_id=operation_id))
613
+ return func(
614
+ self.create_child_context(
615
+ operation_id=operation_id, is_virtual=is_virtual
616
+ )
617
+ )
513
618
 
514
619
  result: T = child_handler(
515
620
  func=callable_with_child_context,
516
621
  state=self.state,
517
622
  operation_identifier=OperationIdentifier(
518
- operation_id=operation_id, parent_id=self._parent_id, name=step_name
623
+ operation_id=operation_id,
624
+ parent_id=self._parent_id,
625
+ name=step_name,
519
626
  ),
520
627
  config=config,
521
628
  )