async-durable-execution-runner 2.0.0a1__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.
- LICENSE +175 -0
- NOTICE +8 -0
- VERSION.py +5 -0
- async_durable_execution_runner/__about__.py +33 -0
- async_durable_execution_runner/__init__.py +23 -0
- async_durable_execution_runner/checkpoint/__init__.py +1 -0
- async_durable_execution_runner/checkpoint/processor.py +101 -0
- async_durable_execution_runner/checkpoint/processors/__init__.py +1 -0
- async_durable_execution_runner/checkpoint/processors/base.py +199 -0
- async_durable_execution_runner/checkpoint/processors/callback.py +89 -0
- async_durable_execution_runner/checkpoint/processors/context.py +59 -0
- async_durable_execution_runner/checkpoint/processors/execution.py +52 -0
- async_durable_execution_runner/checkpoint/processors/step.py +124 -0
- async_durable_execution_runner/checkpoint/processors/wait.py +95 -0
- async_durable_execution_runner/checkpoint/transformer.py +104 -0
- async_durable_execution_runner/checkpoint/validators/__init__.py +1 -0
- async_durable_execution_runner/checkpoint/validators/checkpoint.py +242 -0
- async_durable_execution_runner/checkpoint/validators/operations/__init__.py +1 -0
- async_durable_execution_runner/checkpoint/validators/operations/callback.py +45 -0
- async_durable_execution_runner/checkpoint/validators/operations/context.py +73 -0
- async_durable_execution_runner/checkpoint/validators/operations/execution.py +47 -0
- async_durable_execution_runner/checkpoint/validators/operations/invoke.py +56 -0
- async_durable_execution_runner/checkpoint/validators/operations/step.py +106 -0
- async_durable_execution_runner/checkpoint/validators/operations/wait.py +54 -0
- async_durable_execution_runner/checkpoint/validators/transitions.py +66 -0
- async_durable_execution_runner/cli.py +498 -0
- async_durable_execution_runner/client.py +50 -0
- async_durable_execution_runner/exceptions.py +288 -0
- async_durable_execution_runner/execution.py +444 -0
- async_durable_execution_runner/executor.py +1234 -0
- async_durable_execution_runner/invoker.py +340 -0
- async_durable_execution_runner/model.py +3296 -0
- async_durable_execution_runner/observer.py +144 -0
- async_durable_execution_runner/py.typed +1 -0
- async_durable_execution_runner/runner.py +1167 -0
- async_durable_execution_runner/scheduler.py +246 -0
- async_durable_execution_runner/stores/__init__.py +1 -0
- async_durable_execution_runner/stores/base.py +147 -0
- async_durable_execution_runner/stores/filesystem.py +79 -0
- async_durable_execution_runner/stores/memory.py +38 -0
- async_durable_execution_runner/stores/sqlite.py +273 -0
- async_durable_execution_runner/token.py +49 -0
- async_durable_execution_runner/web/__init__.py +1 -0
- async_durable_execution_runner/web/errors.py +8 -0
- async_durable_execution_runner/web/handlers.py +813 -0
- async_durable_execution_runner/web/models.py +266 -0
- async_durable_execution_runner/web/routes.py +692 -0
- async_durable_execution_runner/web/serialization.py +235 -0
- async_durable_execution_runner/web/server.py +243 -0
- async_durable_execution_runner-2.0.0a1.dist-info/METADATA +238 -0
- async_durable_execution_runner-2.0.0a1.dist-info/RECORD +55 -0
- async_durable_execution_runner-2.0.0a1.dist-info/WHEEL +4 -0
- async_durable_execution_runner-2.0.0a1.dist-info/entry_points.txt +2 -0
- async_durable_execution_runner-2.0.0a1.dist-info/licenses/LICENSE +175 -0
- async_durable_execution_runner-2.0.0a1.dist-info/licenses/NOTICE +1 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
"""Command-line interface for the AWS Durable Functions Local Runner.
|
|
2
|
+
|
|
3
|
+
This module provides the dex-local-runner CLI with commands for:
|
|
4
|
+
- start-server: Start the local web server
|
|
5
|
+
- invoke: Invoke a durable execution
|
|
6
|
+
- get-durable-execution: Get execution details
|
|
7
|
+
- get-durable-execution-history: Get execution history
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import uuid
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Any
|
|
20
|
+
from urllib.parse import urljoin
|
|
21
|
+
|
|
22
|
+
import async_durable_execution
|
|
23
|
+
import boto3 # type: ignore
|
|
24
|
+
from urllib.error import HTTPError, URLError
|
|
25
|
+
from urllib.request import Request, urlopen
|
|
26
|
+
|
|
27
|
+
from botocore.exceptions import ConnectionError # type: ignore
|
|
28
|
+
|
|
29
|
+
from async_durable_execution_runner.exceptions import (
|
|
30
|
+
DurableFunctionsLocalRunnerError,
|
|
31
|
+
DurableFunctionsTestError,
|
|
32
|
+
)
|
|
33
|
+
from async_durable_execution_runner.model import (
|
|
34
|
+
StartDurableExecutionInput,
|
|
35
|
+
)
|
|
36
|
+
from async_durable_execution_runner.runner import WebRunner, WebRunnerConfig
|
|
37
|
+
from async_durable_execution_runner.stores.base import StoreType
|
|
38
|
+
from async_durable_execution_runner.web.server import WebServiceConfig
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class CliConfig:
|
|
46
|
+
"""Configuration for the CLI application with environment variable support."""
|
|
47
|
+
|
|
48
|
+
# Server configuration
|
|
49
|
+
host: str = "0.0.0.0" # noqa:S104
|
|
50
|
+
port: int = 5000
|
|
51
|
+
log_level: int = logging.INFO
|
|
52
|
+
lambda_endpoint: str = "http://127.0.0.1:3001"
|
|
53
|
+
local_runner_endpoint: str = "http://0.0.0.0:5000"
|
|
54
|
+
local_runner_region: str = "us-west-2"
|
|
55
|
+
local_runner_mode: str = "local"
|
|
56
|
+
|
|
57
|
+
# Store configuration
|
|
58
|
+
store_type: StoreType = StoreType.MEMORY
|
|
59
|
+
store_path: str | None = None
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_environment(cls) -> CliConfig:
|
|
63
|
+
"""Create configuration from environment variables with defaults."""
|
|
64
|
+
# Convert log level string to integer if provided
|
|
65
|
+
log_level_str = os.getenv("AWS_DEX_LOG_LEVEL", "INFO")
|
|
66
|
+
log_level = logging.getLevelNamesMapping().get(log_level_str, logging.INFO)
|
|
67
|
+
|
|
68
|
+
return cls(
|
|
69
|
+
host=os.getenv("AWS_DEX_HOST", "0.0.0.0"), # noqa:S104
|
|
70
|
+
port=int(os.getenv("AWS_DEX_PORT", "5000")),
|
|
71
|
+
log_level=log_level,
|
|
72
|
+
lambda_endpoint=os.getenv(
|
|
73
|
+
"AWS_DEX_LAMBDA_ENDPOINT", "http://127.0.0.1:3001"
|
|
74
|
+
),
|
|
75
|
+
local_runner_endpoint=os.getenv(
|
|
76
|
+
"AWS_DEX_LOCAL_RUNNER_ENDPOINT", "http://0.0.0.0:5000"
|
|
77
|
+
),
|
|
78
|
+
local_runner_region=os.getenv("AWS_DEX_LOCAL_RUNNER_REGION", "us-west-2"),
|
|
79
|
+
local_runner_mode=os.getenv("AWS_DEX_LOCAL_RUNNER_MODE", "local"),
|
|
80
|
+
store_type=StoreType(os.getenv("AWS_DEX_STORE_TYPE", "memory")),
|
|
81
|
+
store_path=os.getenv("AWS_DEX_STORE_PATH"),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class CliApp:
|
|
86
|
+
"""Main CLI application for dex-local-runner."""
|
|
87
|
+
|
|
88
|
+
def __init__(self) -> None:
|
|
89
|
+
"""Initialize the CLI application."""
|
|
90
|
+
self.config = CliConfig.from_environment()
|
|
91
|
+
|
|
92
|
+
def run(self, args: list[str] | None = None) -> int:
|
|
93
|
+
"""Run the CLI application with the given arguments.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
args: Command line arguments. If None, uses sys.argv[1:]
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Exit code (0 for success, non-zero for error)
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
parser = self._create_parsers()
|
|
103
|
+
parsed_args = parser.parse_args(args)
|
|
104
|
+
|
|
105
|
+
# Configure logging based on log level
|
|
106
|
+
if hasattr(parsed_args, "log_level") and isinstance(
|
|
107
|
+
parsed_args.log_level, str
|
|
108
|
+
):
|
|
109
|
+
level = logging.getLevelNamesMapping().get(
|
|
110
|
+
parsed_args.log_level, logging.INFO
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
# config.log_level is always an integer
|
|
114
|
+
level = self.config.log_level
|
|
115
|
+
|
|
116
|
+
logging.basicConfig(
|
|
117
|
+
level=level,
|
|
118
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
119
|
+
)
|
|
120
|
+
logging.getLogger("botocore").setLevel(logging.WARNING)
|
|
121
|
+
|
|
122
|
+
# Execute the appropriate command
|
|
123
|
+
return parsed_args.func(parsed_args)
|
|
124
|
+
|
|
125
|
+
except SystemExit as e:
|
|
126
|
+
# argparse calls sys.exit() for help, errors, etc.
|
|
127
|
+
return int(e.code) if e.code is not None else 1
|
|
128
|
+
except KeyboardInterrupt:
|
|
129
|
+
print("\nOperation cancelled by user", file=sys.stderr) # noqa: T201
|
|
130
|
+
return 130 # Standard exit code for SIGINT
|
|
131
|
+
except DurableFunctionsTestError:
|
|
132
|
+
logger.exception("Error")
|
|
133
|
+
return 1
|
|
134
|
+
except Exception:
|
|
135
|
+
logger.exception("Unexpected error.")
|
|
136
|
+
return 1
|
|
137
|
+
|
|
138
|
+
def _create_parsers(self) -> argparse.ArgumentParser:
|
|
139
|
+
"""Create the argument parsers for all commands."""
|
|
140
|
+
parser = argparse.ArgumentParser(
|
|
141
|
+
prog="dex-local-runner",
|
|
142
|
+
description="AWS Durable Functions Local Runner CLI",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
subparsers = parser.add_subparsers(
|
|
146
|
+
dest="command", help="Available commands", required=True
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Create individual parsers
|
|
150
|
+
self._create_start_server_parser(subparsers)
|
|
151
|
+
self._create_invoke_parser(subparsers)
|
|
152
|
+
self._create_get_durable_execution_parser(subparsers)
|
|
153
|
+
self._create_get_durable_execution_history_parser(subparsers)
|
|
154
|
+
|
|
155
|
+
return parser
|
|
156
|
+
|
|
157
|
+
# region parsers
|
|
158
|
+
|
|
159
|
+
def _create_start_server_parser(self, subparsers) -> None:
|
|
160
|
+
"""Create the start-server command parser."""
|
|
161
|
+
start_server_parser = subparsers.add_parser(
|
|
162
|
+
"start-server", help="Start the local Durable Functions Server"
|
|
163
|
+
)
|
|
164
|
+
start_server_parser.add_argument(
|
|
165
|
+
"--host",
|
|
166
|
+
default=self.config.host,
|
|
167
|
+
help=f"Server bind address (default: {self.config.host}, env: AWS_DEX_HOST)",
|
|
168
|
+
)
|
|
169
|
+
start_server_parser.add_argument(
|
|
170
|
+
"--port",
|
|
171
|
+
type=int,
|
|
172
|
+
default=self.config.port,
|
|
173
|
+
help=f"Server port (default: {self.config.port}, env: AWS_DEX_PORT)",
|
|
174
|
+
)
|
|
175
|
+
start_server_parser.add_argument(
|
|
176
|
+
"--log-level",
|
|
177
|
+
type=str,
|
|
178
|
+
choices=list(logging.getLevelNamesMapping().keys()),
|
|
179
|
+
default=logging.getLevelName(self.config.log_level),
|
|
180
|
+
help=f"Logging level (default: {logging.getLevelName(self.config.log_level)}, env: AWS_DEX_LOG_LEVEL)",
|
|
181
|
+
)
|
|
182
|
+
start_server_parser.add_argument(
|
|
183
|
+
"--lambda-endpoint",
|
|
184
|
+
default=self.config.lambda_endpoint,
|
|
185
|
+
help=f"Lambda Service endpoint (default: {self.config.lambda_endpoint}, env: AWS_DEX_LAMBDA_ENDPOINT)",
|
|
186
|
+
)
|
|
187
|
+
start_server_parser.add_argument(
|
|
188
|
+
"--local-runner-endpoint",
|
|
189
|
+
default=self.config.local_runner_endpoint,
|
|
190
|
+
help=f"Local Runner endpoint (default: {self.config.local_runner_endpoint}, env: AWS_DEX_LOCAL_RUNNER_ENDPOINT)",
|
|
191
|
+
)
|
|
192
|
+
start_server_parser.add_argument(
|
|
193
|
+
"--local-runner-region",
|
|
194
|
+
default=self.config.local_runner_region,
|
|
195
|
+
help=f"Local Runner region (default: {self.config.local_runner_region}, env: AWS_DEX_LOCAL_RUNNER_REGION)",
|
|
196
|
+
)
|
|
197
|
+
start_server_parser.add_argument(
|
|
198
|
+
"--local-runner-mode",
|
|
199
|
+
default=self.config.local_runner_mode,
|
|
200
|
+
help=f"Local Runner mode (default: {self.config.local_runner_mode}, env: AWS_DEX_LOCAL_RUNNER_MODE)",
|
|
201
|
+
)
|
|
202
|
+
start_server_parser.add_argument(
|
|
203
|
+
"--store-type",
|
|
204
|
+
choices=[store_type.value for store_type in StoreType],
|
|
205
|
+
default=self.config.store_type.value,
|
|
206
|
+
help=f"Store type for execution persistence (default: {self.config.store_type.value}, env: AWS_DEX_STORE_TYPE)",
|
|
207
|
+
)
|
|
208
|
+
start_server_parser.add_argument(
|
|
209
|
+
"--store-path",
|
|
210
|
+
default=self.config.store_path,
|
|
211
|
+
help=f"Path for filesystem store (default: {self.config.store_path or '.durable_executions'}, env: AWS_DEX_STORE_PATH)",
|
|
212
|
+
)
|
|
213
|
+
start_server_parser.set_defaults(func=self.start_server_command)
|
|
214
|
+
|
|
215
|
+
def _create_invoke_parser(self, subparsers) -> None:
|
|
216
|
+
"""Create the invoke command parser."""
|
|
217
|
+
invoke_parser = subparsers.add_parser(
|
|
218
|
+
"invoke", help="Invoke a Durable Execution"
|
|
219
|
+
)
|
|
220
|
+
invoke_parser.add_argument(
|
|
221
|
+
"--function-name", required=True, help="Function name (required)"
|
|
222
|
+
)
|
|
223
|
+
invoke_parser.add_argument(
|
|
224
|
+
"--input", default="{}", help="Input data (default: {})"
|
|
225
|
+
)
|
|
226
|
+
invoke_parser.add_argument(
|
|
227
|
+
"--durable-execution-name", help="Durable execution name (optional)"
|
|
228
|
+
)
|
|
229
|
+
invoke_parser.set_defaults(func=self.invoke_command)
|
|
230
|
+
|
|
231
|
+
def _create_get_durable_execution_parser(self, subparsers) -> None:
|
|
232
|
+
"""Create the get-durable-execution command parser."""
|
|
233
|
+
get_execution_parser = subparsers.add_parser(
|
|
234
|
+
"get-durable-execution", help="Get execution details"
|
|
235
|
+
)
|
|
236
|
+
get_execution_parser.add_argument(
|
|
237
|
+
"--durable-execution-arn",
|
|
238
|
+
required=True,
|
|
239
|
+
help="Durable execution ARN (required)",
|
|
240
|
+
)
|
|
241
|
+
get_execution_parser.set_defaults(func=self.get_durable_execution_command)
|
|
242
|
+
|
|
243
|
+
def _create_get_durable_execution_history_parser(self, subparsers) -> None:
|
|
244
|
+
"""Create the get-durable-execution-history command parser."""
|
|
245
|
+
get_history_parser = subparsers.add_parser(
|
|
246
|
+
"get-durable-execution-history", help="Get execution history"
|
|
247
|
+
)
|
|
248
|
+
get_history_parser.add_argument(
|
|
249
|
+
"--durable-execution-arn",
|
|
250
|
+
required=True,
|
|
251
|
+
help="Durable execution ARN (required)",
|
|
252
|
+
)
|
|
253
|
+
get_history_parser.set_defaults(func=self.get_durable_execution_history_command)
|
|
254
|
+
|
|
255
|
+
# endregion parsers
|
|
256
|
+
|
|
257
|
+
# region commands
|
|
258
|
+
|
|
259
|
+
def start_server_command(self, args: argparse.Namespace) -> int:
|
|
260
|
+
"""Execute the start-server command.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
args: Parsed command line arguments
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Exit code (0 for success, non-zero for error)
|
|
267
|
+
"""
|
|
268
|
+
try:
|
|
269
|
+
# Create web service configuration from CLI arguments
|
|
270
|
+
web_config = WebServiceConfig(
|
|
271
|
+
host=args.host,
|
|
272
|
+
port=args.port,
|
|
273
|
+
log_level=args.log_level,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Create web runner configuration with composition
|
|
277
|
+
runner_config = WebRunnerConfig(
|
|
278
|
+
web_service=web_config,
|
|
279
|
+
lambda_endpoint=args.lambda_endpoint,
|
|
280
|
+
local_runner_endpoint=args.local_runner_endpoint,
|
|
281
|
+
local_runner_region=args.local_runner_region,
|
|
282
|
+
local_runner_mode=args.local_runner_mode,
|
|
283
|
+
store_type=StoreType(args.store_type),
|
|
284
|
+
store_path=args.store_path,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
logger.info(
|
|
288
|
+
"Starting Durable Functions Local Runner on %s:%s",
|
|
289
|
+
args.host,
|
|
290
|
+
args.port,
|
|
291
|
+
)
|
|
292
|
+
logger.info("Configuration:")
|
|
293
|
+
logger.info(" Host: %s", args.host)
|
|
294
|
+
logger.info(" Port: %s", args.port)
|
|
295
|
+
logger.info(" Log Level: %s", args.log_level)
|
|
296
|
+
logger.info(" Lambda Endpoint: %s", args.lambda_endpoint)
|
|
297
|
+
logger.info(" Local Runner Endpoint: %s", args.local_runner_endpoint)
|
|
298
|
+
logger.info(" Local Runner Region: %s", args.local_runner_region)
|
|
299
|
+
logger.info(" Local Runner Mode: %s", args.local_runner_mode)
|
|
300
|
+
logger.info(" Store Type: %s", args.store_type)
|
|
301
|
+
if StoreType(args.store_type) == StoreType.FILESYSTEM:
|
|
302
|
+
store_path = args.store_path or ".durable_executions"
|
|
303
|
+
logger.info(" Store Path: %s", store_path)
|
|
304
|
+
|
|
305
|
+
# Use runner as context manager for proper lifecycle
|
|
306
|
+
with WebRunner(runner_config) as runner:
|
|
307
|
+
logger.info("Server started successfully. Press Ctrl+C to stop.")
|
|
308
|
+
runner.serve_forever()
|
|
309
|
+
|
|
310
|
+
return 0 # noqa: TRY300
|
|
311
|
+
|
|
312
|
+
except KeyboardInterrupt:
|
|
313
|
+
logger.info("Received shutdown signal, stopping server...")
|
|
314
|
+
return 130 # Standard exit code for SIGINT
|
|
315
|
+
except Exception:
|
|
316
|
+
logger.exception("Failed to start server")
|
|
317
|
+
return 1
|
|
318
|
+
|
|
319
|
+
def invoke_command(self, args: argparse.Namespace) -> int:
|
|
320
|
+
"""Execute the invoke command.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
args: Parsed command line arguments
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Exit code (0 for success, non-zero for error)
|
|
327
|
+
"""
|
|
328
|
+
# Validate input JSON
|
|
329
|
+
try:
|
|
330
|
+
json.loads(args.input) # Just validate, don't store
|
|
331
|
+
except json.JSONDecodeError:
|
|
332
|
+
logger.exception("JSON decode error")
|
|
333
|
+
return 1
|
|
334
|
+
|
|
335
|
+
try:
|
|
336
|
+
# Create StartDurableExecutionInput
|
|
337
|
+
start_input = StartDurableExecutionInput(
|
|
338
|
+
account_id="123456789012", # Default account ID for local testing
|
|
339
|
+
function_name=args.function_name,
|
|
340
|
+
function_qualifier="$LATEST", # Default qualifier
|
|
341
|
+
execution_name=args.durable_execution_name
|
|
342
|
+
or f"{args.function_name}-execution",
|
|
343
|
+
execution_timeout_seconds=300, # 5 minutes default
|
|
344
|
+
execution_retention_period_days=7, # 1 week default
|
|
345
|
+
invocation_id=str(uuid.uuid4()), # Generate unique invocation ID
|
|
346
|
+
input=args.input,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Make HTTP request to start-durable-execution endpoint
|
|
350
|
+
endpoint_url = self.config.local_runner_endpoint
|
|
351
|
+
url = urljoin(endpoint_url, "/start-durable-execution")
|
|
352
|
+
|
|
353
|
+
payload = start_input.to_dict()
|
|
354
|
+
data = json.dumps(payload).encode("utf-8")
|
|
355
|
+
req = Request(
|
|
356
|
+
url,
|
|
357
|
+
data=data,
|
|
358
|
+
headers={"Content-Type": "application/json"},
|
|
359
|
+
method="POST",
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
with urlopen(req, timeout=10) as response: # noqa: S310
|
|
364
|
+
result = json.loads(response.read().decode("utf-8"))
|
|
365
|
+
print(json.dumps(result, indent=2)) # noqa: T201
|
|
366
|
+
return 0
|
|
367
|
+
except HTTPError as e:
|
|
368
|
+
try:
|
|
369
|
+
error_data = json.loads(e.read().decode("utf-8"))
|
|
370
|
+
logger.exception("HTTP error response")
|
|
371
|
+
print( # noqa: T201
|
|
372
|
+
f"Error: {error_data.get('ErrorMessage', 'Unknown error')}",
|
|
373
|
+
file=sys.stderr,
|
|
374
|
+
)
|
|
375
|
+
except json.JSONDecodeError:
|
|
376
|
+
logger.exception("Non-JSON error response")
|
|
377
|
+
return 1
|
|
378
|
+
|
|
379
|
+
except URLError:
|
|
380
|
+
logger.exception(
|
|
381
|
+
"Error: Could not connect to the local runner server. Is it running?"
|
|
382
|
+
)
|
|
383
|
+
return 1
|
|
384
|
+
except Exception:
|
|
385
|
+
logger.exception("Unexpected error in invoke command")
|
|
386
|
+
return 1
|
|
387
|
+
|
|
388
|
+
def get_durable_execution_command(self, args: argparse.Namespace) -> int:
|
|
389
|
+
"""Execute the get-durable-execution command.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
args: Parsed command line arguments
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Exit code (0 for success, non-zero for error)
|
|
396
|
+
"""
|
|
397
|
+
try:
|
|
398
|
+
# Set up boto3 client with local endpoint
|
|
399
|
+
client = self._create_boto3_client()
|
|
400
|
+
|
|
401
|
+
# Call get_durable_execution
|
|
402
|
+
response = client.get_durable_execution(
|
|
403
|
+
DurableExecutionArn=args.durable_execution_arn
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# Print formatted response
|
|
407
|
+
print(json.dumps(response, indent=2, default=str)) # noqa: T201
|
|
408
|
+
return 0 # noqa: TRY300
|
|
409
|
+
|
|
410
|
+
except client.exceptions.InvalidParameterValueException as e:
|
|
411
|
+
print(f"Error: Invalid parameter - {e}", file=sys.stderr) # noqa: T201
|
|
412
|
+
return 1
|
|
413
|
+
except client.exceptions.ResourceNotFoundException as e:
|
|
414
|
+
print(f"Error: Execution not found - {e}", file=sys.stderr) # noqa: T201
|
|
415
|
+
return 1
|
|
416
|
+
except client.exceptions.TooManyRequestsException as e:
|
|
417
|
+
print(f"Error: Too many requests - {e}", file=sys.stderr) # noqa: T201
|
|
418
|
+
return 1
|
|
419
|
+
except client.exceptions.ServiceException as e:
|
|
420
|
+
print(f"Error: Service error - {e}", file=sys.stderr) # noqa: T201
|
|
421
|
+
return 1
|
|
422
|
+
except ConnectionError:
|
|
423
|
+
logger.exception(
|
|
424
|
+
"Error: Could not connect to the local runner server. Is it running?"
|
|
425
|
+
)
|
|
426
|
+
return 1
|
|
427
|
+
except Exception:
|
|
428
|
+
logger.exception("Unexpected error in get-durable-execution command")
|
|
429
|
+
return 1
|
|
430
|
+
|
|
431
|
+
def get_durable_execution_history_command(self, args: argparse.Namespace) -> int:
|
|
432
|
+
"""Execute the get-durable-execution-history command.
|
|
433
|
+
|
|
434
|
+
TODO: implement - this is incomplete
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
args: Parsed command line arguments
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
Exit code (0 for success, non-zero for error)
|
|
441
|
+
"""
|
|
442
|
+
try:
|
|
443
|
+
# Set up boto3 client with local endpoint
|
|
444
|
+
client = self._create_boto3_client()
|
|
445
|
+
|
|
446
|
+
# Call get_durable_execution_history
|
|
447
|
+
response = client.get_durable_execution_history(
|
|
448
|
+
DurableExecutionArn=args.durable_execution_arn
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
print(json.dumps(response, indent=2, default=str)) # noqa: T201
|
|
452
|
+
return 0 # noqa: TRY300
|
|
453
|
+
|
|
454
|
+
except Exception:
|
|
455
|
+
logger.exception("General error")
|
|
456
|
+
return 1
|
|
457
|
+
|
|
458
|
+
# endregion commands
|
|
459
|
+
|
|
460
|
+
def _create_boto3_client(
|
|
461
|
+
self, endpoint_url: str | None = None, region_name: str | None = None
|
|
462
|
+
) -> Any:
|
|
463
|
+
"""Create boto3 client for Lambda service.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
endpoint_url: Optional endpoint URL override
|
|
467
|
+
region_name: Optional region name override
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
Configured boto3 client for local runner
|
|
471
|
+
|
|
472
|
+
Raises:
|
|
473
|
+
Exception: If client creation fails
|
|
474
|
+
"""
|
|
475
|
+
try:
|
|
476
|
+
# Use provided values or fall back to config
|
|
477
|
+
final_endpoint = endpoint_url or self.config.local_runner_endpoint
|
|
478
|
+
final_region = region_name or self.config.local_runner_region
|
|
479
|
+
|
|
480
|
+
# Create client with local endpoint - no AWS access keys required
|
|
481
|
+
return boto3.client(
|
|
482
|
+
"lambda",
|
|
483
|
+
endpoint_url=final_endpoint,
|
|
484
|
+
region_name=final_region,
|
|
485
|
+
)
|
|
486
|
+
except Exception as e:
|
|
487
|
+
msg = f"Failed to create boto3 client: {e}"
|
|
488
|
+
raise DurableFunctionsLocalRunnerError(msg) from e
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def main() -> int:
|
|
492
|
+
"""Main entry point for the dex-local-runner CLI."""
|
|
493
|
+
app = CliApp()
|
|
494
|
+
return app.run()
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
if __name__ == "__main__":
|
|
498
|
+
sys.exit(main())
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""An in-memory service client, that can replace the boto lambda service client."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
|
|
5
|
+
from async_durable_execution.lambda_service import (
|
|
6
|
+
CheckpointOutput,
|
|
7
|
+
DurableServiceClient,
|
|
8
|
+
OperationUpdate,
|
|
9
|
+
StateOutput,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from async_durable_execution_runner.checkpoint.processor import (
|
|
13
|
+
CheckpointProcessor,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InMemoryServiceClient(DurableServiceClient):
|
|
18
|
+
"""An in-memory service client, that can replace the boto lambda service client."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, checkpoint_processor: CheckpointProcessor):
|
|
21
|
+
self._checkpoint_processor: CheckpointProcessor = checkpoint_processor
|
|
22
|
+
|
|
23
|
+
def checkpoint(
|
|
24
|
+
self,
|
|
25
|
+
durable_execution_arn: str, # noqa: ARG002
|
|
26
|
+
checkpoint_token: str,
|
|
27
|
+
updates: list[OperationUpdate],
|
|
28
|
+
client_token: str | None,
|
|
29
|
+
) -> 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
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def get_execution_state(
|
|
36
|
+
self,
|
|
37
|
+
durable_execution_arn: str, # noqa: ARG002
|
|
38
|
+
checkpoint_token: str,
|
|
39
|
+
next_marker: str,
|
|
40
|
+
max_items: int = 1000,
|
|
41
|
+
) -> 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
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def stop(self, execution_arn: str, payload: bytes | None) -> datetime.datetime: # noqa: ARG002
|
|
48
|
+
# TODO: implement
|
|
49
|
+
# Return current time for in-memory testing
|
|
50
|
+
return datetime.datetime.now(tz=datetime.UTC)
|