aws-durable-execution-sdk-python-testing 1.2.1__py3-none-any.whl → 1.3.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 (48) hide show
  1. aws_durable_execution_sdk_python_testing/__about__.py +1 -1
  2. aws_durable_execution_sdk_python_testing/checkpoint/core.py +171 -0
  3. aws_durable_execution_sdk_python_testing/checkpoint/effects.py +61 -0
  4. aws_durable_execution_sdk_python_testing/checkpoint/processor.py +124 -45
  5. aws_durable_execution_sdk_python_testing/checkpoint/processors/base.py +40 -14
  6. aws_durable_execution_sdk_python_testing/checkpoint/processors/callback.py +5 -2
  7. aws_durable_execution_sdk_python_testing/checkpoint/processors/context.py +6 -0
  8. aws_durable_execution_sdk_python_testing/checkpoint/processors/execution.py +4 -9
  9. aws_durable_execution_sdk_python_testing/checkpoint/processors/invoke.py +139 -0
  10. aws_durable_execution_sdk_python_testing/checkpoint/processors/step.py +15 -21
  11. aws_durable_execution_sdk_python_testing/checkpoint/processors/wait.py +10 -16
  12. aws_durable_execution_sdk_python_testing/checkpoint/transformer.py +145 -48
  13. aws_durable_execution_sdk_python_testing/checkpoint/validators/checkpoint.py +65 -8
  14. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/invoke.py +79 -18
  15. aws_durable_execution_sdk_python_testing/child_dispatcher.py +828 -0
  16. aws_durable_execution_sdk_python_testing/cli.py +48 -1
  17. aws_durable_execution_sdk_python_testing/client.py +21 -10
  18. aws_durable_execution_sdk_python_testing/clock.py +69 -0
  19. aws_durable_execution_sdk_python_testing/exceptions.py +14 -0
  20. aws_durable_execution_sdk_python_testing/execution.py +583 -49
  21. aws_durable_execution_sdk_python_testing/executor.py +1813 -349
  22. aws_durable_execution_sdk_python_testing/invoker.py +356 -85
  23. aws_durable_execution_sdk_python_testing/model.py +81 -73
  24. aws_durable_execution_sdk_python_testing/observer.py +100 -88
  25. aws_durable_execution_sdk_python_testing/runner.py +407 -30
  26. aws_durable_execution_sdk_python_testing/scheduler.py +38 -13
  27. aws_durable_execution_sdk_python_testing/stores/filesystem.py +10 -2
  28. aws_durable_execution_sdk_python_testing/stores/sqlite.py +18 -6
  29. aws_durable_execution_sdk_python_testing/threads.py +157 -0
  30. aws_durable_execution_sdk_python_testing/token.py +11 -2
  31. aws_durable_execution_sdk_python_testing/web/handlers.py +33 -7
  32. aws_durable_execution_sdk_python_testing/web/serialization.py +1 -2
  33. aws_durable_execution_sdk_python_testing/web/server.py +1 -1
  34. aws_durable_execution_sdk_python_testing/worker/__init__.py +9 -0
  35. aws_durable_execution_sdk_python_testing/worker/checkpoint_tasks.py +119 -0
  36. aws_durable_execution_sdk_python_testing/worker/execution_worker.py +117 -0
  37. aws_durable_execution_sdk_python_testing/worker/lane.py +118 -0
  38. aws_durable_execution_sdk_python_testing/worker/registry.py +124 -0
  39. aws_durable_execution_sdk_python_testing/worker/status.py +27 -0
  40. aws_durable_execution_sdk_python_testing/worker/task.py +57 -0
  41. {aws_durable_execution_sdk_python_testing-1.2.1.dist-info → aws_durable_execution_sdk_python_testing-1.3.0.dist-info}/METADATA +50 -58
  42. aws_durable_execution_sdk_python_testing-1.3.0.dist-info/RECORD +64 -0
  43. {aws_durable_execution_sdk_python_testing-1.2.1.dist-info → aws_durable_execution_sdk_python_testing-1.3.0.dist-info}/WHEEL +1 -1
  44. aws_durable_execution_sdk_python_testing/checkpoint/validators/transitions.py +0 -66
  45. aws_durable_execution_sdk_python_testing-1.2.1.dist-info/RECORD +0 -52
  46. {aws_durable_execution_sdk_python_testing-1.2.1.dist-info/licenses → aws_durable_execution_sdk_python_testing}/LICENSE +0 -0
  47. {aws_durable_execution_sdk_python_testing-1.2.1.dist-info/licenses → aws_durable_execution_sdk_python_testing}/NOTICE +0 -0
  48. {aws_durable_execution_sdk_python_testing-1.2.1.dist-info → aws_durable_execution_sdk_python_testing-1.3.0.dist-info}/entry_points.txt +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.2.1"
4
+ __version__ = "1.3.0"
@@ -0,0 +1,171 @@
1
+ """The shared checkpoint write-transaction.
2
+
3
+ Both checkpoint entry points apply a batch of updates and compute the
4
+ delta of operations the handler has not yet seen. :class:`CheckpointCore`
5
+ holds that common logic so the two callers differ only in their gate,
6
+ locking, and response type.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING, NamedTuple
12
+
13
+ from aws_durable_execution_sdk_python_testing.checkpoint.validators.checkpoint import (
14
+ CheckpointValidator,
15
+ )
16
+ from aws_durable_execution_sdk_python_testing.execution import (
17
+ CheckpointIdempotencyRecord,
18
+ OperationPaginatorState,
19
+ )
20
+ from aws_durable_execution_sdk_python_testing.token import CheckpointToken
21
+
22
+ if TYPE_CHECKING:
23
+ from datetime import datetime
24
+
25
+ from aws_durable_execution_sdk_python.lambda_service import (
26
+ Operation,
27
+ OperationUpdate,
28
+ )
29
+
30
+ from aws_durable_execution_sdk_python_testing.checkpoint.effects import (
31
+ CheckpointEffect,
32
+ )
33
+ from aws_durable_execution_sdk_python_testing.checkpoint.transformer import (
34
+ CheckpointRequestDispatcher,
35
+ )
36
+ from aws_durable_execution_sdk_python_testing.execution import Execution
37
+
38
+
39
+ class CheckpointResult(NamedTuple):
40
+ """Outcome of applying a checkpoint: the new token, the operations to
41
+ return to the handler this round, and the lifecycle effects raised.
42
+
43
+ ``checkpoint_token`` is None when the execution is paused and the token
44
+ is withheld, telling the SDK this invocation may checkpoint no further.
45
+ """
46
+
47
+ checkpoint_token: str | None
48
+ operations: list[Operation]
49
+ effects: list[CheckpointEffect]
50
+
51
+
52
+ class CheckpointCore:
53
+ """The checkpoint write-transaction shared by both entry points."""
54
+
55
+ @staticmethod
56
+ def match_cached(
57
+ execution: Execution,
58
+ checkpoint_token: str,
59
+ client_token: str | None,
60
+ ) -> CheckpointIdempotencyRecord | None:
61
+ """Return the cached idempotency record when the incoming
62
+ ``(client_token, checkpoint_token)`` matches the last checkpoint,
63
+ else ``None``.
64
+
65
+ A missing ``client_token`` cannot replay: idempotency requires an
66
+ explicit client identifier. The caller builds its own response
67
+ type from the returned record.
68
+ """
69
+ if not client_token:
70
+ return None
71
+ cached: CheckpointIdempotencyRecord | None = execution.last_checkpoint
72
+ if cached is None:
73
+ return None
74
+ if (
75
+ cached.client_token != client_token
76
+ or cached.inbound_checkpoint_token != checkpoint_token
77
+ ):
78
+ return None
79
+ return cached
80
+
81
+ @staticmethod
82
+ def apply(
83
+ execution: Execution,
84
+ checkpoint_token: str,
85
+ updates: list[OperationUpdate],
86
+ client_token: str | None,
87
+ dispatcher: CheckpointRequestDispatcher,
88
+ now: datetime,
89
+ ) -> CheckpointResult:
90
+ """Apply ``updates`` to ``execution`` and compute the response delta.
91
+
92
+ Advances ``token_sequence`` exactly once, returns the full set of
93
+ operations the handler has not yet seen unless paused, advances
94
+ ``handler_seen_seq`` only for returned operations, and records an
95
+ idempotency entry for a byte-identical replay of a retried call
96
+ only when returning a checkpoint token.
97
+
98
+ The caller is responsible for the invocation gate, locking,
99
+ persistence, and applying the returned effects.
100
+
101
+ ``now`` is the checkpoint's single source of "now", resolved from
102
+ the execution's clock, so all timestamps stamped by this apply
103
+ advance with modeled time under a skip clock.
104
+
105
+ Before computing the delta, completes every wait and step retry
106
+ whose scheduled moment has passed, so those transitions are
107
+ delivered in this response and a suspended branch resumes within
108
+ the running invocation instead of waiting for the next one.
109
+ """
110
+ effects: list[CheckpointEffect] = []
111
+ if updates:
112
+ CheckpointValidator.validate_input(updates, execution)
113
+ effects = dispatcher.apply_updates(
114
+ execution=execution,
115
+ updates=updates,
116
+ client_token=client_token,
117
+ touch=execution.touch_operation,
118
+ now=now,
119
+ )
120
+
121
+ new_token_sequence: int = execution.advance_token_sequence()
122
+
123
+ # Pick up scheduler-due completions before pinning the paginator:
124
+ # each transition touches its operation, so it lands in this
125
+ # response's unseen delta and the handler sees it immediately.
126
+ execution.complete_due_operations(now)
127
+
128
+ paginator: OperationPaginatorState = OperationPaginatorState.pin(execution)
129
+ # The checkpoint response returns the full unseen delta in a single
130
+ # response. Advance handler_seen_seq to cover every returned op so
131
+ # the next delta carries only operations touched after this response.
132
+ # Paused checkpoints persist updates without delivering any state.
133
+ response_ops: list[Operation] = (
134
+ [] if execution.is_paused else paginator.unseen_operations()
135
+ )
136
+ if response_ops:
137
+ highest_delivered_seq: int = max(
138
+ execution.operation_last_touched_seq[op.operation_id]
139
+ for op in response_ops
140
+ )
141
+ paginator.advance_handler_seen(highest_delivered_seq)
142
+
143
+ new_token: str = CheckpointToken(
144
+ execution_arn=execution.durable_execution_arn,
145
+ token_sequence=new_token_sequence,
146
+ invocation_id=execution.current_invocation_id,
147
+ ).to_str()
148
+
149
+ # A paused execution registers this checkpoint's updates but withholds
150
+ # the token: a response without one tells the SDK this invocation may
151
+ # checkpoint no further, so it reports PENDING at its next checkpoint
152
+ # rather than continuing, and owes a re-invoke once resumed.
153
+ #
154
+ # Checkpoints that were interrupted by a pause are not added to the
155
+ # idempotency record. A retry of this checkpoint will fail rather than be
156
+ # answered again.
157
+
158
+ outbound_token: str | None = new_token
159
+ if execution.is_paused:
160
+ outbound_token = None
161
+ execution.defer_invocation()
162
+ else:
163
+ execution.last_checkpoint = CheckpointIdempotencyRecord(
164
+ client_token=client_token or "",
165
+ inbound_checkpoint_token=checkpoint_token,
166
+ outbound_checkpoint_token=outbound_token,
167
+ operations=list(response_ops),
168
+ next_marker=None,
169
+ )
170
+
171
+ return CheckpointResult(outbound_token, response_ops, effects)
@@ -0,0 +1,61 @@
1
+ """Lifecycle effects produced by applying checkpoint updates.
2
+
3
+ Applying a batch of operation updates can imply actions on the execution
4
+ beyond the state change itself: the execution completed, it failed, or a
5
+ callback was created and now needs a timeout. These are returned as data
6
+ so the caller can act on them after the write finishes, rather than
7
+ triggering them in the middle of it.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from typing import TYPE_CHECKING
14
+
15
+ if TYPE_CHECKING:
16
+ from aws_durable_execution_sdk_python.lambda_service import (
17
+ CallbackOptions,
18
+ ErrorObject,
19
+ )
20
+
21
+ from aws_durable_execution_sdk_python_testing.token import CallbackToken
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class Completed:
26
+ """The execution completed successfully with ``result``."""
27
+
28
+ execution_arn: str
29
+ result: str | None = None
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class Failed:
34
+ """The execution failed with ``error``."""
35
+
36
+ execution_arn: str
37
+ error: ErrorObject | None
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class CallbackCreated:
42
+ """A callback was created and its timeout (if any) must be scheduled."""
43
+
44
+ execution_arn: str
45
+ operation_id: str
46
+ callback_options: CallbackOptions | None
47
+ callback_token: CallbackToken
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class ChainedInvokeStarted:
52
+ """A chained invoke was accepted and its target must be dispatched."""
53
+
54
+ execution_arn: str
55
+ operation_id: str
56
+ function_name: str
57
+ tenant_id: str | None
58
+ payload: str | None
59
+
60
+
61
+ CheckpointEffect = Completed | Failed | CallbackCreated | ChainedInvokeStarted
@@ -1,4 +1,10 @@
1
- """Main checkpoint processor that orchestrates operation transformations."""
1
+ """In-process checkpoint flow.
2
+
3
+ Orchestrates the full checkpoint request for in-process callers
4
+ (``InMemoryServiceClient``). Mirrors the flow inside
5
+ ``Executor.checkpoint_execution`` used by the HTTP path, so both
6
+ entry points share identical delta + watermark semantics.
7
+ """
2
8
 
3
9
  from __future__ import annotations
4
10
 
@@ -7,81 +13,128 @@ from typing import TYPE_CHECKING
7
13
  from aws_durable_execution_sdk_python.lambda_service import (
8
14
  CheckpointOutput,
9
15
  CheckpointUpdatedExecutionState,
10
- OperationUpdate,
11
16
  StateOutput,
12
17
  )
13
18
 
19
+ from aws_durable_execution_sdk_python_testing.checkpoint.core import CheckpointCore
14
20
  from aws_durable_execution_sdk_python_testing.checkpoint.transformer import (
15
- OperationTransformer,
16
- )
17
- from aws_durable_execution_sdk_python_testing.checkpoint.validators.checkpoint import (
18
- CheckpointValidator,
21
+ CheckpointRequestDispatcher,
19
22
  )
23
+ from aws_durable_execution_sdk_python_testing.clock import RealClock
20
24
  from aws_durable_execution_sdk_python_testing.exceptions import (
21
25
  InvalidParameterValueException,
22
26
  )
23
- from aws_durable_execution_sdk_python_testing.observer import ExecutionNotifier
27
+ from aws_durable_execution_sdk_python_testing.observer import apply_effects
24
28
  from aws_durable_execution_sdk_python_testing.token import CheckpointToken
25
29
 
26
30
 
27
31
  if TYPE_CHECKING:
32
+ from collections.abc import Callable
33
+
34
+ from aws_durable_execution_sdk_python.lambda_service import (
35
+ ErrorObject,
36
+ OperationUpdate,
37
+ )
38
+
39
+ from aws_durable_execution_sdk_python_testing.clock import Clock
28
40
  from aws_durable_execution_sdk_python_testing.execution import Execution
41
+ from aws_durable_execution_sdk_python_testing.observer import ExecutionObserver
29
42
  from aws_durable_execution_sdk_python_testing.scheduler import Scheduler
30
43
  from aws_durable_execution_sdk_python_testing.stores.base import ExecutionStore
31
44
 
32
45
 
46
+ # Canonical invocation-page byte budget, shared with the Executor.
47
+ DEFAULT_MAX_INVOCATION_PAGE_BYTES = 5 * 1024 * 1024
48
+
49
+
33
50
  class CheckpointProcessor:
34
- """Handle OperationUpdate transformations and execution state updates."""
51
+ """In-process checkpoint flow used by ``InMemoryServiceClient``.
35
52
 
36
- def __init__(self, store: ExecutionStore, scheduler: Scheduler):
37
- self._store = store
38
- self._scheduler = scheduler
39
- self._notifier = ExecutionNotifier()
40
- self._transformer = OperationTransformer()
53
+ Uses the same :class:`CheckpointRequestDispatcher` + paginator
54
+ primitives as ``Executor.checkpoint_execution`` so observable
55
+ behaviour is identical across the two entry points.
56
+ """
41
57
 
42
- def add_execution_observer(self, observer) -> None:
58
+ def __init__(
59
+ self,
60
+ store: ExecutionStore,
61
+ scheduler: Scheduler, # noqa: ARG002 — kept for backward-compatible signature
62
+ clock: Clock | None = None,
63
+ ):
64
+ self._store = store
65
+ self._observers: list[ExecutionObserver] = []
66
+ self._dispatcher = CheckpointRequestDispatcher()
67
+ # The runner's clock, shared with the Executor so stamping uses
68
+ # the same "now" as timer arming. Defaults to a real clock for
69
+ # standalone construction (e.g. direct unit tests).
70
+ self._clock: Clock = clock if clock is not None else RealClock()
71
+
72
+ def add_execution_observer(self, observer: ExecutionObserver) -> None:
43
73
  """Add observer for execution events."""
44
- self._notifier.add_observer(observer)
74
+ self._observers.append(observer)
75
+
76
+ def set_chained_invoke_preflight(
77
+ self, preflight: Callable[[str], ErrorObject | None]
78
+ ) -> None:
79
+ """Resolve chained-invoke targets at checkpoint time with ``preflight``.
80
+
81
+ A target it fails comes back FAILED in the checkpoint response
82
+ instead of being dispatched (see ``ChainedInvokeProcessor``).
83
+ """
84
+ self._dispatcher = CheckpointRequestDispatcher(
85
+ chained_invoke_preflight=preflight
86
+ )
45
87
 
46
88
  def process_checkpoint(
47
89
  self,
48
90
  checkpoint_token: str,
49
91
  updates: list[OperationUpdate],
50
- client_token: str | None, # noqa: ARG002
92
+ client_token: str | None,
51
93
  ) -> CheckpointOutput:
52
- """Process checkpoint updates and return result with updated execution state."""
53
- # 1. Get current execution state
54
- token: CheckpointToken = CheckpointToken.from_str(checkpoint_token)
94
+ """Apply ``updates`` and return the delta since the handler's
95
+ last observation.
96
+
97
+ Advances ``token_sequence`` exactly once; bumps
98
+ ``seq_counter`` once per accepted update via
99
+ ``touch_operation``; advances ``handler_seen_seq`` only for
100
+ operations actually returned in the response.
101
+ """
102
+ token = CheckpointToken.from_str(checkpoint_token)
55
103
  execution: Execution = self._store.load(token.execution_arn)
56
104
 
57
- # 2. Validate checkpoint token
58
- if execution.is_complete or token.token_sequence != execution.token_sequence:
59
- msg: str = "Invalid checkpoint token"
60
-
105
+ # Idempotency first, before the token-sequence check.
106
+ cached = _maybe_replay_cached(execution, checkpoint_token, client_token)
107
+ if cached is not None:
108
+ return cached
109
+
110
+ if (
111
+ execution.is_complete
112
+ or token.token_sequence != execution.token_sequence
113
+ or token.invocation_id != execution.current_invocation_id
114
+ ):
115
+ msg = "Invalid checkpoint token"
61
116
  raise InvalidParameterValueException(msg)
62
117
 
63
- # 3. Validate all updates, state transitions are valid, sizes etc.
64
- CheckpointValidator.validate_input(updates, execution)
65
-
66
- # 4. Transform OperationUpdate -> Operation and schedule future replays
67
- updated_operations, all_updates = self._transformer.process_updates(
68
- updates=updates,
69
- current_operations=execution.operations,
70
- notifier=self._notifier,
71
- execution_arn=token.execution_arn,
118
+ now = self._clock.now()
119
+ result = CheckpointCore.apply(
120
+ execution,
121
+ checkpoint_token,
122
+ updates,
123
+ client_token,
124
+ self._dispatcher,
125
+ now,
72
126
  )
73
127
 
74
- # 5. Generate a new checkpoint token and save updated operations
75
- new_checkpoint_token = execution.get_new_checkpoint_token()
76
- execution.operations = updated_operations
77
- execution.updates.extend(all_updates)
78
128
  self._store.update(execution)
79
129
 
80
- # 6. Return checkpoint result
130
+ for observer in self._observers:
131
+ apply_effects(result.effects, observer)
132
+
81
133
  return CheckpointOutput(
82
- checkpoint_token=new_checkpoint_token,
134
+ checkpoint_token=result.checkpoint_token,
83
135
  new_execution_state=CheckpointUpdatedExecutionState(
84
- operations=execution.get_navigable_operations(), next_marker=None
136
+ operations=result.operations,
137
+ next_marker=None,
85
138
  ),
86
139
  )
87
140
 
@@ -91,11 +144,37 @@ class CheckpointProcessor:
91
144
  next_marker: str, # noqa: ARG002
92
145
  max_items: int = 1000, # noqa: ARG002
93
146
  ) -> StateOutput:
94
- """Get current execution state."""
95
- token: CheckpointToken = CheckpointToken.from_str(checkpoint_token)
96
- execution: Execution = self._store.load(token.execution_arn)
97
-
98
- # TODO: paging when size or max
147
+ """Get current execution state.
148
+
149
+ Returns the full navigable operation list with a null marker.
150
+ Marker round-tripping and the invocation-state gate are
151
+ enforced on the HTTP path in :class:`Executor`.
152
+ """
153
+ token = CheckpointToken.from_str(checkpoint_token)
154
+ execution = self._store.load(token.execution_arn)
99
155
  return StateOutput(
100
- operations=execution.get_navigable_operations(), next_marker=None
156
+ operations=execution.get_navigable_operations(),
157
+ next_marker=None,
101
158
  )
159
+
160
+
161
+ def _maybe_replay_cached(
162
+ execution: Execution,
163
+ checkpoint_token: str,
164
+ client_token: str | None,
165
+ ) -> CheckpointOutput | None:
166
+ """Replay the cached response when the incoming
167
+ ``(client_token, checkpoint_token)`` matches the last checkpoint.
168
+
169
+ Returns ``None`` when there is no matching cached record.
170
+ """
171
+ cached = CheckpointCore.match_cached(execution, checkpoint_token, client_token)
172
+ if cached is None:
173
+ return None
174
+ return CheckpointOutput(
175
+ checkpoint_token=cached.outbound_checkpoint_token,
176
+ new_execution_state=CheckpointUpdatedExecutionState(
177
+ operations=list(cached.operations),
178
+ next_marker=cached.next_marker,
179
+ ),
180
+ )
@@ -34,22 +34,42 @@ class OperationProcessor:
34
34
  current_op: Operation | None,
35
35
  notifier: ExecutionNotifier,
36
36
  execution_arn: str,
37
+ now: datetime.datetime,
37
38
  ) -> Operation | None:
38
- """Process an operation update and return the transformed operation."""
39
+ """Process an operation update and return the transformed operation.
40
+
41
+ ``now`` is the checkpoint's single source of "now", resolved from
42
+ the execution's clock so timestamps advance with modeled time
43
+ under a skip clock and match wall-clock under a real clock.
44
+ """
39
45
  raise NotImplementedError
40
46
 
47
+ def stored_update(
48
+ self,
49
+ update: OperationUpdate,
50
+ current_op: Operation | None, # noqa: ARG002
51
+ updated_op: Operation, # noqa: ARG002
52
+ ) -> OperationUpdate:
53
+ """The update to keep in the execution's record for history.
54
+
55
+ Most processors keep the update as sent. A processor overrides
56
+ this when the service keeps less than it was sent.
57
+ """
58
+ return update
59
+
41
60
  def _get_start_time(
42
- self, current_operation: Operation | None
61
+ self, current_operation: Operation | None, now: datetime.datetime
43
62
  ) -> datetime.datetime | None:
44
63
  start_time: datetime.datetime | None = (
45
- current_operation.start_timestamp
46
- if current_operation
47
- else datetime.datetime.now(tz=datetime.UTC)
64
+ current_operation.start_timestamp if current_operation else now
48
65
  )
49
66
  return start_time
50
67
 
51
68
  def _get_end_time(
52
- self, current_operation: Operation | None, status: OperationStatus
69
+ self,
70
+ current_operation: Operation | None,
71
+ status: OperationStatus,
72
+ now: datetime.datetime,
53
73
  ) -> datetime.datetime | None:
54
74
  """Get end timestamp for operation based on current state and status."""
55
75
  if current_operation and current_operation.end_timestamp:
@@ -61,7 +81,7 @@ class OperationProcessor:
61
81
  OperationStatus.TIMED_OUT,
62
82
  OperationStatus.STOPPED,
63
83
  }:
64
- return datetime.datetime.now(tz=datetime.UTC)
84
+ return now
65
85
  return None
66
86
 
67
87
  def _create_execution_details(
@@ -151,11 +171,14 @@ class OperationProcessor:
151
171
  update: OperationUpdate,
152
172
  current_operation: Operation | None,
153
173
  status: OperationStatus,
174
+ now: datetime.datetime,
154
175
  ) -> Operation:
155
176
  """Transform OperationUpdate to Operation, always creating new Operation."""
156
- start_time: datetime.datetime | None = self._get_start_time(current_operation)
177
+ start_time: datetime.datetime | None = self._get_start_time(
178
+ current_operation, now
179
+ )
157
180
  end_time: datetime.datetime | None = self._get_end_time(
158
- current_operation, status
181
+ current_operation, status, now
159
182
  )
160
183
 
161
184
  execution_details = self._create_execution_details(update)
@@ -163,7 +186,7 @@ class OperationProcessor:
163
186
  step_details = self._create_step_details(update, current_operation)
164
187
  callback_details = self._create_callback_details(update)
165
188
  invoke_details = self._create_invoke_details(update)
166
- wait_details = self._create_wait_details(update, current_operation)
189
+ wait_details = self._create_wait_details(update, current_operation, now)
167
190
 
168
191
  return Operation(
169
192
  operation_id=update.operation_id,
@@ -183,7 +206,10 @@ class OperationProcessor:
183
206
  )
184
207
 
185
208
  def _create_wait_details(
186
- self, update: OperationUpdate, current_operation: Operation | None
209
+ self,
210
+ update: OperationUpdate,
211
+ current_operation: Operation | None,
212
+ now: datetime.datetime,
187
213
  ) -> WaitDetails | None:
188
214
  """Create WaitDetails from OperationUpdate."""
189
215
  if update.operation_type == OperationType.WAIT and update.wait_options:
@@ -192,8 +218,8 @@ class OperationProcessor:
192
218
  current_operation.wait_details.scheduled_end_timestamp
193
219
  )
194
220
  else:
195
- scheduled_end_timestamp = datetime.datetime.now(
196
- tz=datetime.UTC
197
- ) + timedelta(seconds=update.wait_options.wait_seconds)
221
+ scheduled_end_timestamp = now + timedelta(
222
+ seconds=update.wait_options.wait_seconds
223
+ )
198
224
  return WaitDetails(scheduled_end_timestamp=scheduled_end_timestamp)
199
225
  return None
@@ -35,6 +35,7 @@ class CallbackProcessor(OperationProcessor):
35
35
  current_op: Operation | None,
36
36
  notifier: ExecutionNotifier, # noqa: ARG002
37
37
  execution_arn: str, # noqa: ARG002
38
+ now: datetime.datetime,
38
39
  ) -> Operation:
39
40
  """Process CALLBACK operation update with scheduler integration for activities."""
40
41
  match update.action:
@@ -58,10 +59,12 @@ class CallbackProcessor(OperationProcessor):
58
59
 
59
60
  status: OperationStatus = OperationStatus.STARTED
60
61
 
61
- start_time: datetime.datetime | None = self._get_start_time(current_op)
62
+ start_time: datetime.datetime | None = self._get_start_time(
63
+ current_op, now
64
+ )
62
65
 
63
66
  end_time: datetime.datetime | None = self._get_end_time(
64
- current_op, status
67
+ current_op, status, now
65
68
  )
66
69
 
67
70
  operation: Operation = Operation(
@@ -20,6 +20,8 @@ from aws_durable_execution_sdk_python_testing.exceptions import (
20
20
 
21
21
 
22
22
  if TYPE_CHECKING:
23
+ from datetime import datetime
24
+
23
25
  from aws_durable_execution_sdk_python_testing.observer import ExecutionNotifier
24
26
 
25
27
 
@@ -32,6 +34,7 @@ class ContextProcessor(OperationProcessor):
32
34
  current_op: Operation | None,
33
35
  notifier: ExecutionNotifier, # noqa: ARG002
34
36
  execution_arn: str, # noqa: ARG002
37
+ now: datetime,
35
38
  ) -> Operation:
36
39
  """Process CONTEXT operation update for context state transitions."""
37
40
  match update.action:
@@ -41,18 +44,21 @@ class ContextProcessor(OperationProcessor):
41
44
  update=update,
42
45
  current_operation=current_op,
43
46
  status=OperationStatus.STARTED,
47
+ now=now,
44
48
  )
45
49
  case OperationAction.SUCCEED:
46
50
  return self._translate_update_to_operation(
47
51
  update=update,
48
52
  current_operation=current_op,
49
53
  status=OperationStatus.SUCCEEDED,
54
+ now=now,
50
55
  )
51
56
  case OperationAction.FAIL:
52
57
  return self._translate_update_to_operation(
53
58
  update=update,
54
59
  current_operation=current_op,
55
60
  status=OperationStatus.FAILED,
61
+ now=now,
56
62
  )
57
63
  case _:
58
64
  msg: str = "Invalid action for CONTEXT operation."
@@ -5,7 +5,6 @@ from __future__ import annotations
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  from aws_durable_execution_sdk_python.lambda_service import (
8
- ErrorObject,
9
8
  Operation,
10
9
  OperationAction,
11
10
  OperationUpdate,
@@ -17,6 +16,8 @@ from aws_durable_execution_sdk_python_testing.checkpoint.processors.base import
17
16
 
18
17
 
19
18
  if TYPE_CHECKING:
19
+ from datetime import datetime
20
+
20
21
  from aws_durable_execution_sdk_python_testing.observer import ExecutionNotifier
21
22
 
22
23
 
@@ -29,6 +30,7 @@ class ExecutionProcessor(OperationProcessor):
29
30
  current_op: Operation | None, # noqa: ARG002
30
31
  notifier: ExecutionNotifier,
31
32
  execution_arn: str,
33
+ now: datetime, # noqa: ARG002
32
34
  ) -> Operation | None:
33
35
  """Process EXECUTION operation update for workflow completion/failure."""
34
36
  match update.action:
@@ -38,15 +40,8 @@ class ExecutionProcessor(OperationProcessor):
38
40
  )
39
41
  case _:
40
42
  # intentional. actual service will fail any EXECUTION update that is not SUCCEED.
41
- error = (
42
- update.error
43
- if update.error
44
- else ErrorObject.from_message(
45
- "There is no error details but EXECUTION checkpoint action is not SUCCEED."
46
- )
47
- )
48
43
  # All EXECUTION failures go through normal fail path
49
44
  # Timeout/Stop status is set by executor based on the operation that caused it
50
- notifier.notify_failed(execution_arn=execution_arn, error=error)
45
+ notifier.notify_failed(execution_arn=execution_arn, error=update.error)
51
46
  # TODO: Svc doesn't actually create checkpoint for EXECUTION. might have to for localrunner though.
52
47
  return None