aws-durable-execution-sdk-python-testing 1.2.0__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 (49) 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/routes.py +14 -6
  33. aws_durable_execution_sdk_python_testing/web/serialization.py +1 -2
  34. aws_durable_execution_sdk_python_testing/web/server.py +1 -1
  35. aws_durable_execution_sdk_python_testing/worker/__init__.py +9 -0
  36. aws_durable_execution_sdk_python_testing/worker/checkpoint_tasks.py +119 -0
  37. aws_durable_execution_sdk_python_testing/worker/execution_worker.py +117 -0
  38. aws_durable_execution_sdk_python_testing/worker/lane.py +118 -0
  39. aws_durable_execution_sdk_python_testing/worker/registry.py +124 -0
  40. aws_durable_execution_sdk_python_testing/worker/status.py +27 -0
  41. aws_durable_execution_sdk_python_testing/worker/task.py +57 -0
  42. {aws_durable_execution_sdk_python_testing-1.2.0.dist-info → aws_durable_execution_sdk_python_testing-1.3.0.dist-info}/METADATA +50 -58
  43. aws_durable_execution_sdk_python_testing-1.3.0.dist-info/RECORD +64 -0
  44. {aws_durable_execution_sdk_python_testing-1.2.0.dist-info → aws_durable_execution_sdk_python_testing-1.3.0.dist-info}/WHEEL +1 -1
  45. aws_durable_execution_sdk_python_testing/checkpoint/validators/transitions.py +0 -66
  46. aws_durable_execution_sdk_python_testing-1.2.0.dist-info/RECORD +0 -52
  47. {aws_durable_execution_sdk_python_testing-1.2.0.dist-info/licenses → aws_durable_execution_sdk_python_testing}/LICENSE +0 -0
  48. {aws_durable_execution_sdk_python_testing-1.2.0.dist-info/licenses → aws_durable_execution_sdk_python_testing}/NOTICE +0 -0
  49. {aws_durable_execution_sdk_python_testing-1.2.0.dist-info → aws_durable_execution_sdk_python_testing-1.3.0.dist-info}/entry_points.txt +0 -0
@@ -19,13 +19,13 @@ from dataclasses import dataclass
19
19
  from typing import Any
20
20
  from urllib.parse import urljoin
21
21
 
22
- import aws_durable_execution_sdk_python
23
22
  import boto3 # type: ignore
24
23
  from urllib.error import HTTPError, URLError
25
24
  from urllib.request import Request, urlopen
26
25
 
27
26
  from botocore.exceptions import ConnectionError # type: ignore
28
27
 
28
+ from aws_durable_execution_sdk_python_testing.child_dispatcher import FunctionConfigs
29
29
  from aws_durable_execution_sdk_python_testing.exceptions import (
30
30
  DurableFunctionsLocalRunnerError,
31
31
  DurableFunctionsTestError,
@@ -41,6 +41,14 @@ from aws_durable_execution_sdk_python_testing.web.server import WebServiceConfig
41
41
  logger = logging.getLogger(__name__)
42
42
 
43
43
 
44
+ def _function_configs(value: str) -> FunctionConfigs:
45
+ """argparse type for ``--function-configs``: parse at the boundary."""
46
+ try:
47
+ return FunctionConfigs.from_value(value)
48
+ except (ValueError, OSError) as exc:
49
+ raise argparse.ArgumentTypeError(str(exc)) from exc
50
+
51
+
44
52
  @dataclass(frozen=True)
45
53
  class CliConfig:
46
54
  """Configuration for the CLI application with environment variable support."""
@@ -210,6 +218,42 @@ class CliApp:
210
218
  default=self.config.store_path,
211
219
  help=f"Path for filesystem store (default: {self.config.store_path or '.durable_executions'}, env: AWS_DEX_STORE_PATH)",
212
220
  )
221
+ start_server_parser.add_argument(
222
+ "--execution-timeout",
223
+ type=int,
224
+ default=300,
225
+ help="Default execution timeout in seconds (default: 300)",
226
+ )
227
+ start_server_parser.add_argument(
228
+ "--invocation-timeout",
229
+ type=int,
230
+ default=900,
231
+ help=(
232
+ "Per-invocation timeout in seconds, simulates Lambda Timeout "
233
+ "(default: 900). Also bounds how long the runner waits on any "
234
+ "Invoke it sends, with 60 seconds of headroom"
235
+ ),
236
+ )
237
+ start_server_parser.add_argument(
238
+ "--skip-time",
239
+ action=argparse.BooleanOptionalAction,
240
+ default=False,
241
+ help="Skip durable timer wall-clock waits; history keeps real modeled durations. Default is real timing (--no-skip-time); pass --skip-time to opt in",
242
+ )
243
+ start_server_parser.add_argument(
244
+ "--function-configs",
245
+ default=None,
246
+ type=_function_configs,
247
+ help=(
248
+ "The Lambda functions a durable function may invoke: a JSON "
249
+ "object mapping each function name to its configuration in "
250
+ "Lambda's own shape, or file://<path> to a file holding it. "
251
+ "A durable function carries a DurableConfig, e.g. "
252
+ '{"ProcessPayment": {"DurableConfig": {"ExecutionTimeout": 60, '
253
+ '"RetentionPeriodInDays": 7}}, "LookupPrice": {}}. Required for '
254
+ "chained invokes; without it every chained invoke fails"
255
+ ),
256
+ )
213
257
  start_server_parser.set_defaults(func=self.start_server_command)
214
258
 
215
259
  def _create_invoke_parser(self, subparsers) -> None:
@@ -282,6 +326,9 @@ class CliApp:
282
326
  local_runner_mode=args.local_runner_mode,
283
327
  store_type=StoreType(args.store_type),
284
328
  store_path=args.store_path,
329
+ invocation_timeout_seconds=args.invocation_timeout,
330
+ skip_time=args.skip_time,
331
+ function_configs=args.function_configs,
285
332
  )
286
333
 
287
334
  logger.info(
@@ -9,42 +9,53 @@ from aws_durable_execution_sdk_python.lambda_service import (
9
9
  StateOutput,
10
10
  )
11
11
 
12
+ from aws_durable_execution_sdk_python_testing.clock import real_now
12
13
  from aws_durable_execution_sdk_python_testing.checkpoint.processor import (
13
14
  CheckpointProcessor,
14
15
  )
16
+ from aws_durable_execution_sdk_python_testing.worker.checkpoint_tasks import (
17
+ CheckpointTask,
18
+ GetStateTask,
19
+ )
20
+ from aws_durable_execution_sdk_python_testing.worker.registry import ExecutionRegistry
15
21
 
16
22
 
17
23
  class InMemoryServiceClient(DurableServiceClient):
18
24
  """An in-memory service client, that can replace the boto lambda service client."""
19
25
 
20
- def __init__(self, checkpoint_processor: CheckpointProcessor):
26
+ def __init__(
27
+ self,
28
+ checkpoint_processor: CheckpointProcessor,
29
+ registry: ExecutionRegistry,
30
+ ):
21
31
  self._checkpoint_processor: CheckpointProcessor = checkpoint_processor
32
+ self._registry: ExecutionRegistry = registry
22
33
 
23
34
  def checkpoint(
24
35
  self,
25
- durable_execution_arn: str, # noqa: ARG002
36
+ durable_execution_arn: str,
26
37
  checkpoint_token: str,
27
38
  updates: list[OperationUpdate],
28
39
  client_token: str | None,
29
40
  ) -> CheckpointOutput:
30
- # durable_execution_arn is not used in in-memory testing
31
- return self._checkpoint_processor.process_checkpoint(
32
- checkpoint_token, updates, client_token
41
+ task = CheckpointTask(
42
+ self._checkpoint_processor, checkpoint_token, updates, client_token
33
43
  )
44
+ return self._registry.submit(durable_execution_arn, task).result()
34
45
 
35
46
  def get_execution_state(
36
47
  self,
37
- durable_execution_arn: str, # noqa: ARG002
48
+ durable_execution_arn: str,
38
49
  checkpoint_token: str,
39
50
  next_marker: str,
40
51
  max_items: int = 1000,
41
52
  ) -> StateOutput:
42
- # durable_execution_arn is not used in in-memory testing
43
- return self._checkpoint_processor.get_execution_state(
44
- checkpoint_token, next_marker, max_items
53
+ task = GetStateTask(
54
+ self._checkpoint_processor, checkpoint_token, next_marker, max_items
45
55
  )
56
+ return self._registry.submit(durable_execution_arn, task).result()
46
57
 
47
58
  def stop(self, execution_arn: str, payload: bytes | None) -> datetime.datetime: # noqa: ARG002
48
59
  # TODO: implement
49
60
  # Return current time for in-memory testing
50
- return datetime.datetime.now(tz=datetime.UTC)
61
+ return real_now()
@@ -0,0 +1,69 @@
1
+ """Clock sources for scheduler-driven durable timers.
2
+
3
+ A clock decides how long the local runner actually waits before a
4
+ scheduled wake fires, and supplies the "now" used to stamp operation
5
+ and invocation timestamps. All time reads in the runner go through one
6
+ clock so history events share a single, totally ordered clock domain.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import datetime
12
+ import threading
13
+ from typing import Protocol
14
+
15
+
16
+ def real_now() -> datetime.datetime:
17
+ """Return the current wall-clock time in UTC."""
18
+ return datetime.datetime.now(datetime.UTC) # noqa: TID251
19
+
20
+
21
+ class Clock(Protocol):
22
+ """Time source used to arm timers and stamp timestamps."""
23
+
24
+ def now(self) -> datetime.datetime:
25
+ """Return the current time on this clock."""
26
+ ...
27
+
28
+ def arm(self, wake: datetime.datetime) -> float:
29
+ """Return the real seconds to wait before firing at ``wake``."""
30
+ ...
31
+
32
+
33
+ class RealClock:
34
+ """Wall-clock time source. Timers wait the real modeled duration."""
35
+
36
+ def now(self) -> datetime.datetime:
37
+ return real_now()
38
+
39
+ def arm(self, wake: datetime.datetime) -> float:
40
+ return max((wake - self.now()).total_seconds(), 0.0)
41
+
42
+
43
+ class SkipClock:
44
+ """Virtual clock that skips over armed timer horizons.
45
+
46
+ Tracks the total duration skipped so far: ``now`` returns real time
47
+ plus the accumulated skip, so the clock advances with real time
48
+ between jumps and every read is distinct and monotonic. Arming a
49
+ wake beyond virtual now adds the remaining gap to the accumulated
50
+ skip and returns a zero delay, so the wake fires on the next
51
+ event-loop turn rather than after the modeled duration. Guards its
52
+ state with a lock because ``arm`` and ``now`` run on different
53
+ threads.
54
+ """
55
+
56
+ def __init__(self) -> None:
57
+ self._skipped: datetime.timedelta = datetime.timedelta(0)
58
+ self._lock: threading.Lock = threading.Lock()
59
+
60
+ def now(self) -> datetime.datetime:
61
+ with self._lock:
62
+ return real_now() + self._skipped
63
+
64
+ def arm(self, wake: datetime.datetime) -> float:
65
+ with self._lock:
66
+ gap: datetime.timedelta = wake - (real_now() + self._skipped)
67
+ if gap > datetime.timedelta(0):
68
+ self._skipped += gap
69
+ return 0.0
@@ -109,6 +109,20 @@ class DurableFunctionsTestError(Exception):
109
109
  """Base class for testing errors."""
110
110
 
111
111
 
112
+ class InvalidInvocationOutputError(DurableFunctionsTestError):
113
+ """The handler's output cannot be parsed as an invocation output."""
114
+
115
+ def __init__(self, message: str, request_id: str | None = None) -> None:
116
+ """Initialize with the message and the invocation's request ID.
117
+
118
+ Args:
119
+ message: Why the output cannot be parsed
120
+ request_id: Request ID of the invocation that returned the output
121
+ """
122
+ self.request_id = request_id
123
+ super().__init__(message)
124
+
125
+
112
126
  # endregion Testing
113
127
 
114
128