aws-durable-execution-sdk-python-testing 1.0.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.
- aws_durable_execution_sdk_python_testing/__about__.py +4 -0
- aws_durable_execution_sdk_python_testing/__init__.py +20 -0
- aws_durable_execution_sdk_python_testing/checkpoint/__init__.py +1 -0
- aws_durable_execution_sdk_python_testing/checkpoint/processor.py +101 -0
- aws_durable_execution_sdk_python_testing/checkpoint/processors/__init__.py +1 -0
- aws_durable_execution_sdk_python_testing/checkpoint/processors/base.py +199 -0
- aws_durable_execution_sdk_python_testing/checkpoint/processors/callback.py +89 -0
- aws_durable_execution_sdk_python_testing/checkpoint/processors/context.py +59 -0
- aws_durable_execution_sdk_python_testing/checkpoint/processors/execution.py +52 -0
- aws_durable_execution_sdk_python_testing/checkpoint/processors/step.py +124 -0
- aws_durable_execution_sdk_python_testing/checkpoint/processors/wait.py +95 -0
- aws_durable_execution_sdk_python_testing/checkpoint/transformer.py +104 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/__init__.py +1 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/checkpoint.py +242 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/__init__.py +1 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/callback.py +45 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/context.py +73 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/execution.py +47 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/invoke.py +56 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/step.py +106 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/wait.py +54 -0
- aws_durable_execution_sdk_python_testing/checkpoint/validators/transitions.py +66 -0
- aws_durable_execution_sdk_python_testing/cli.py +501 -0
- aws_durable_execution_sdk_python_testing/client.py +50 -0
- aws_durable_execution_sdk_python_testing/exceptions.py +288 -0
- aws_durable_execution_sdk_python_testing/execution.py +440 -0
- aws_durable_execution_sdk_python_testing/executor.py +1214 -0
- aws_durable_execution_sdk_python_testing/invoker.py +327 -0
- aws_durable_execution_sdk_python_testing/model.py +3267 -0
- aws_durable_execution_sdk_python_testing/observer.py +144 -0
- aws_durable_execution_sdk_python_testing/py.typed +1 -0
- aws_durable_execution_sdk_python_testing/runner.py +1185 -0
- aws_durable_execution_sdk_python_testing/scheduler.py +246 -0
- aws_durable_execution_sdk_python_testing/stores/__init__.py +1 -0
- aws_durable_execution_sdk_python_testing/stores/base.py +147 -0
- aws_durable_execution_sdk_python_testing/stores/filesystem.py +104 -0
- aws_durable_execution_sdk_python_testing/stores/memory.py +38 -0
- aws_durable_execution_sdk_python_testing/stores/sqlite.py +283 -0
- aws_durable_execution_sdk_python_testing/token.py +49 -0
- aws_durable_execution_sdk_python_testing/web/__init__.py +1 -0
- aws_durable_execution_sdk_python_testing/web/errors.py +8 -0
- aws_durable_execution_sdk_python_testing/web/handlers.py +807 -0
- aws_durable_execution_sdk_python_testing/web/models.py +266 -0
- aws_durable_execution_sdk_python_testing/web/routes.py +684 -0
- aws_durable_execution_sdk_python_testing/web/serialization.py +245 -0
- aws_durable_execution_sdk_python_testing/web/server.py +243 -0
- aws_durable_execution_sdk_python_testing-1.0.0.dist-info/METADATA +221 -0
- aws_durable_execution_sdk_python_testing-1.0.0.dist-info/RECORD +52 -0
- aws_durable_execution_sdk_python_testing-1.0.0.dist-info/WHEEL +4 -0
- aws_durable_execution_sdk_python_testing-1.0.0.dist-info/entry_points.txt +2 -0
- aws_durable_execution_sdk_python_testing-1.0.0.dist-info/licenses/LICENSE +175 -0
- aws_durable_execution_sdk_python_testing-1.0.0.dist-info/licenses/NOTICE +1 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""SQLite-based execution store implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sqlite3
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, cast
|
|
10
|
+
|
|
11
|
+
from aws_durable_execution_sdk_python_testing.exceptions import (
|
|
12
|
+
ResourceNotFoundException,
|
|
13
|
+
InvalidParameterValueException,
|
|
14
|
+
RuntimeException,
|
|
15
|
+
)
|
|
16
|
+
from aws_durable_execution_sdk_python_testing.execution import Execution
|
|
17
|
+
from aws_durable_execution_sdk_python_testing.stores.base import (
|
|
18
|
+
ExecutionStore,
|
|
19
|
+
)
|
|
20
|
+
from aws_durable_execution_sdk_python_testing.stores.filesystem import (
|
|
21
|
+
DateTimeEncoder,
|
|
22
|
+
datetime_object_hook,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SQLiteExecutionStore(ExecutionStore):
|
|
27
|
+
"""SQLite-based execution store for efficient querying."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, db_path: Path) -> None:
|
|
30
|
+
self.db_path: Path = db_path
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def create_and_initialize(
|
|
34
|
+
cls, db_path: Path | str | None = None
|
|
35
|
+
) -> SQLiteExecutionStore:
|
|
36
|
+
"""Create SQLite store with default path."""
|
|
37
|
+
path: Path = Path(db_path) if db_path else Path("durable-executions.db")
|
|
38
|
+
path.parent.mkdir(exist_ok=True)
|
|
39
|
+
store: SQLiteExecutionStore = cls(path)
|
|
40
|
+
store._init_db()
|
|
41
|
+
return store
|
|
42
|
+
|
|
43
|
+
def _get_connection(self) -> sqlite3.Connection:
|
|
44
|
+
"""Get SQLite connection with optimizations."""
|
|
45
|
+
conn: sqlite3.Connection = sqlite3.connect(self.db_path, timeout=30.0)
|
|
46
|
+
conn.execute("PRAGMA journal_mode=WAL;")
|
|
47
|
+
conn.execute("PRAGMA synchronous=NORMAL;")
|
|
48
|
+
return conn
|
|
49
|
+
|
|
50
|
+
def _init_db(self) -> None:
|
|
51
|
+
"""Initialize database schema."""
|
|
52
|
+
try:
|
|
53
|
+
with self._get_connection() as conn:
|
|
54
|
+
conn.execute("""
|
|
55
|
+
CREATE TABLE IF NOT EXISTS executions (
|
|
56
|
+
durable_execution_arn TEXT PRIMARY KEY,
|
|
57
|
+
function_name TEXT NOT NULL,
|
|
58
|
+
execution_name TEXT,
|
|
59
|
+
status TEXT NOT NULL,
|
|
60
|
+
start_timestamp REAL,
|
|
61
|
+
end_timestamp REAL,
|
|
62
|
+
data TEXT NOT NULL
|
|
63
|
+
)
|
|
64
|
+
""")
|
|
65
|
+
# Create indexes for better query performance
|
|
66
|
+
conn.execute(
|
|
67
|
+
"CREATE INDEX IF NOT EXISTS idx_function_name ON executions(function_name)"
|
|
68
|
+
)
|
|
69
|
+
conn.execute(
|
|
70
|
+
"CREATE INDEX IF NOT EXISTS idx_status ON executions(status)"
|
|
71
|
+
)
|
|
72
|
+
conn.execute(
|
|
73
|
+
"CREATE INDEX IF NOT EXISTS idx_start_timestamp ON executions(start_timestamp)"
|
|
74
|
+
)
|
|
75
|
+
conn.execute(
|
|
76
|
+
"CREATE INDEX IF NOT EXISTS idx_composite ON executions(function_name, status, start_timestamp)"
|
|
77
|
+
)
|
|
78
|
+
except sqlite3.Error as e:
|
|
79
|
+
raise RuntimeError(f"Failed to initialize database: {e}") from e
|
|
80
|
+
|
|
81
|
+
def save(self, execution: Execution) -> None:
|
|
82
|
+
"""Save execution to SQLite."""
|
|
83
|
+
try:
|
|
84
|
+
execution_op = execution.get_operation_execution_started()
|
|
85
|
+
status: str = execution.current_status().value
|
|
86
|
+
|
|
87
|
+
with self._get_connection() as conn:
|
|
88
|
+
conn.execute(
|
|
89
|
+
"""
|
|
90
|
+
INSERT OR REPLACE INTO executions
|
|
91
|
+
(durable_execution_arn, function_name, execution_name, status, start_timestamp, end_timestamp, data)
|
|
92
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
93
|
+
""",
|
|
94
|
+
(
|
|
95
|
+
execution.durable_execution_arn,
|
|
96
|
+
execution.start_input.function_name,
|
|
97
|
+
execution.start_input.execution_name,
|
|
98
|
+
status,
|
|
99
|
+
execution_op.start_timestamp.timestamp()
|
|
100
|
+
if execution_op.start_timestamp
|
|
101
|
+
else None,
|
|
102
|
+
execution_op.end_timestamp.timestamp()
|
|
103
|
+
if execution_op.end_timestamp
|
|
104
|
+
else None,
|
|
105
|
+
json.dumps(execution.to_dict(), cls=DateTimeEncoder),
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
except sqlite3.Error as e:
|
|
109
|
+
raise RuntimeError(
|
|
110
|
+
f"Failed to save execution {execution.durable_execution_arn}: {e}"
|
|
111
|
+
) from e
|
|
112
|
+
except (AttributeError, TypeError) as e:
|
|
113
|
+
raise ValueError(f"Invalid execution data: {e}") from e
|
|
114
|
+
|
|
115
|
+
def load(self, execution_arn: str) -> Execution:
|
|
116
|
+
"""Load execution from SQLite."""
|
|
117
|
+
try:
|
|
118
|
+
with self._get_connection() as conn:
|
|
119
|
+
cursor: sqlite3.Cursor = conn.execute(
|
|
120
|
+
"SELECT data FROM executions WHERE durable_execution_arn = ?",
|
|
121
|
+
(execution_arn,),
|
|
122
|
+
)
|
|
123
|
+
row: tuple[str] | None = cursor.fetchone()
|
|
124
|
+
|
|
125
|
+
if not row:
|
|
126
|
+
raise ResourceNotFoundException(f"Execution {execution_arn} not found")
|
|
127
|
+
|
|
128
|
+
return Execution.from_dict(
|
|
129
|
+
json.loads(row[0], object_hook=datetime_object_hook)
|
|
130
|
+
)
|
|
131
|
+
except sqlite3.Error as e:
|
|
132
|
+
raise RuntimeError(f"Failed to load execution {execution_arn}: {e}") from e
|
|
133
|
+
except json.JSONDecodeError as e:
|
|
134
|
+
raise ValueError(
|
|
135
|
+
f"Corrupted execution data for {execution_arn}: {e}"
|
|
136
|
+
) from e
|
|
137
|
+
|
|
138
|
+
def update(self, execution: Execution) -> None:
|
|
139
|
+
"""Update execution (same as save)."""
|
|
140
|
+
self.save(execution)
|
|
141
|
+
|
|
142
|
+
def query(
|
|
143
|
+
self,
|
|
144
|
+
function_name: str | None = None,
|
|
145
|
+
execution_name: str | None = None,
|
|
146
|
+
status_filter: str | None = None,
|
|
147
|
+
started_after: str | None = None,
|
|
148
|
+
started_before: str | None = None,
|
|
149
|
+
limit: int | None = None,
|
|
150
|
+
offset: int = 0,
|
|
151
|
+
reverse_order: bool = False,
|
|
152
|
+
) -> tuple[list[Execution], str | None]:
|
|
153
|
+
"""Query executions with efficient SQL filtering."""
|
|
154
|
+
try:
|
|
155
|
+
# Build query safely with parameterized conditions
|
|
156
|
+
conditions: list[str] = []
|
|
157
|
+
params: list[str | float | int] = []
|
|
158
|
+
|
|
159
|
+
if function_name:
|
|
160
|
+
conditions.append("function_name = ?")
|
|
161
|
+
params.append(function_name)
|
|
162
|
+
|
|
163
|
+
if execution_name:
|
|
164
|
+
conditions.append("execution_name = ?")
|
|
165
|
+
params.append(execution_name)
|
|
166
|
+
|
|
167
|
+
if status_filter:
|
|
168
|
+
conditions.append("status = ?")
|
|
169
|
+
params.append(status_filter)
|
|
170
|
+
|
|
171
|
+
if started_after:
|
|
172
|
+
started_after_float: float = datetime.fromisoformat(
|
|
173
|
+
started_after
|
|
174
|
+
).timestamp()
|
|
175
|
+
conditions.append("start_timestamp >= ?")
|
|
176
|
+
params.append(started_after_float)
|
|
177
|
+
|
|
178
|
+
if started_before:
|
|
179
|
+
started_before_float: float = datetime.fromisoformat(
|
|
180
|
+
started_before
|
|
181
|
+
).timestamp()
|
|
182
|
+
conditions.append("start_timestamp <= ?")
|
|
183
|
+
params.append(started_before_float)
|
|
184
|
+
|
|
185
|
+
# Build WHERE clause safely
|
|
186
|
+
where_clause: str = ""
|
|
187
|
+
if conditions:
|
|
188
|
+
where_clause = "WHERE " + " AND ".join(conditions)
|
|
189
|
+
|
|
190
|
+
# Build ORDER BY clause
|
|
191
|
+
order_direction: str = "DESC" if reverse_order else "ASC"
|
|
192
|
+
order_clause: str = f"ORDER BY start_timestamp {order_direction}"
|
|
193
|
+
|
|
194
|
+
# For better performance, only get metadata for counting and pagination
|
|
195
|
+
base_query: str = f"FROM executions {where_clause}"
|
|
196
|
+
count_query: str = f"SELECT COUNT(*) {base_query}"
|
|
197
|
+
|
|
198
|
+
limit_exists: bool = limit is not None and limit > 0
|
|
199
|
+
|
|
200
|
+
# Only fetch data we need
|
|
201
|
+
if limit_exists:
|
|
202
|
+
data_query: str = f"SELECT durable_execution_arn, data {base_query} {order_clause} LIMIT ? OFFSET ?"
|
|
203
|
+
params_with_limit: list[str | float | int] = params + [
|
|
204
|
+
cast(int, limit),
|
|
205
|
+
offset,
|
|
206
|
+
]
|
|
207
|
+
else:
|
|
208
|
+
data_query = (
|
|
209
|
+
f"SELECT durable_execution_arn, data {base_query} {order_clause}"
|
|
210
|
+
)
|
|
211
|
+
params_with_limit = params
|
|
212
|
+
|
|
213
|
+
with self._get_connection() as conn:
|
|
214
|
+
# Get total count for pagination
|
|
215
|
+
total_count: int = int(conn.execute(count_query, params).fetchone()[0])
|
|
216
|
+
|
|
217
|
+
# Get actual data
|
|
218
|
+
cursor: sqlite3.Cursor = conn.execute(data_query, params_with_limit)
|
|
219
|
+
rows: list[tuple[str, str]] = cursor.fetchall()
|
|
220
|
+
|
|
221
|
+
# Only deserialize the executions we actually need
|
|
222
|
+
executions: list[Execution] = []
|
|
223
|
+
for durable_execution_arn, data in rows:
|
|
224
|
+
try:
|
|
225
|
+
executions.append(
|
|
226
|
+
Execution.from_dict(
|
|
227
|
+
json.loads(data, object_hook=datetime_object_hook)
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
231
|
+
# Log corrupted data but continue with other records
|
|
232
|
+
print(
|
|
233
|
+
f"Warning: Skipping corrupted execution {durable_execution_arn}: {e}"
|
|
234
|
+
)
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
# Calculate pagination
|
|
238
|
+
has_more: bool = limit_exists and (offset + len(executions) < total_count)
|
|
239
|
+
next_marker: str | None = (
|
|
240
|
+
str(offset + len(executions)) if has_more else None
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return executions, next_marker
|
|
244
|
+
|
|
245
|
+
except sqlite3.Error as e:
|
|
246
|
+
raise RuntimeException(f"Query failed: {e}") from e
|
|
247
|
+
except ValueError as e:
|
|
248
|
+
raise InvalidParameterValueException(
|
|
249
|
+
f"Invalid query parameters: {e}"
|
|
250
|
+
) from e
|
|
251
|
+
|
|
252
|
+
def list_all(self) -> list[Execution]:
|
|
253
|
+
"""List all executions (for backward compatibility)."""
|
|
254
|
+
executions, _ = self.query()
|
|
255
|
+
return executions
|
|
256
|
+
|
|
257
|
+
def get_execution_metadata(self, execution_arn: str) -> dict[str, Any] | None:
|
|
258
|
+
"""Get just the metadata without full deserialization for performance."""
|
|
259
|
+
try:
|
|
260
|
+
with self._get_connection() as conn:
|
|
261
|
+
cursor: sqlite3.Cursor = conn.execute(
|
|
262
|
+
"SELECT function_name, execution_name, status, start_timestamp, end_timestamp FROM executions WHERE durable_execution_arn = ?",
|
|
263
|
+
(execution_arn,),
|
|
264
|
+
)
|
|
265
|
+
row: tuple[str, str | None, str, float | None, float | None] | None = (
|
|
266
|
+
cursor.fetchone()
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if not row:
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
"durable_execution_arn": execution_arn,
|
|
274
|
+
"function_name": row[0],
|
|
275
|
+
"execution_name": row[1],
|
|
276
|
+
"status": row[2],
|
|
277
|
+
"start_timestamp": row[3],
|
|
278
|
+
"end_timestamp": row[4],
|
|
279
|
+
}
|
|
280
|
+
except sqlite3.Error as e:
|
|
281
|
+
raise RuntimeError(
|
|
282
|
+
f"Failed to get metadata for {execution_arn}: {e}"
|
|
283
|
+
) from e
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Token models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class CheckpointToken:
|
|
12
|
+
"""Model a checkpoint token. This isn't exactly the same format as the actual svc, but it will do for testing purposes."""
|
|
13
|
+
|
|
14
|
+
execution_arn: str
|
|
15
|
+
token_sequence: int
|
|
16
|
+
|
|
17
|
+
def to_str(self) -> str:
|
|
18
|
+
data = {"arn": self.execution_arn, "seq": self.token_sequence}
|
|
19
|
+
json_str = json.dumps(data, separators=(",", ":"))
|
|
20
|
+
# str -> bytes -> base64 bytes -> str
|
|
21
|
+
return base64.b64encode(json_str.encode()).decode()
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_str(cls, token: str) -> CheckpointToken:
|
|
25
|
+
# str -> base64 bytes -> str
|
|
26
|
+
decoded = base64.b64decode(token).decode()
|
|
27
|
+
data = json.loads(decoded)
|
|
28
|
+
return cls(execution_arn=data["arn"], token_sequence=data["seq"])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class CallbackToken:
|
|
33
|
+
"""Model a callback token."""
|
|
34
|
+
|
|
35
|
+
execution_arn: str
|
|
36
|
+
operation_id: str
|
|
37
|
+
|
|
38
|
+
def to_str(self) -> str:
|
|
39
|
+
data = {"arn": self.execution_arn, "op": self.operation_id}
|
|
40
|
+
json_str = json.dumps(data, separators=(",", ":"))
|
|
41
|
+
# str -> bytes -> base64 bytes -> str
|
|
42
|
+
return base64.b64encode(json_str.encode()).decode()
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_str(cls, token: str) -> CallbackToken:
|
|
46
|
+
# str -> base64 bytes -> str
|
|
47
|
+
decoded = base64.b64decode(token).decode()
|
|
48
|
+
data = json.loads(decoded)
|
|
49
|
+
return cls(execution_arn=data["arn"], operation_id=data["op"])
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Web server module for the AWS Durable Functions SDK Python Testing Framework."""
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Error handling utilities for AWS Lambda Durable Functions web service.
|
|
2
|
+
|
|
3
|
+
This module is deprecated and will be removed. All error handling now uses
|
|
4
|
+
AWS-compliant exception classes directly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# This file is kept temporarily for backward compatibility during migration.
|
|
8
|
+
# All functionality has been moved to direct AWS exception usage.
|