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.
Files changed (52) hide show
  1. aws_durable_execution_sdk_python_testing/__about__.py +4 -0
  2. aws_durable_execution_sdk_python_testing/__init__.py +20 -0
  3. aws_durable_execution_sdk_python_testing/checkpoint/__init__.py +1 -0
  4. aws_durable_execution_sdk_python_testing/checkpoint/processor.py +101 -0
  5. aws_durable_execution_sdk_python_testing/checkpoint/processors/__init__.py +1 -0
  6. aws_durable_execution_sdk_python_testing/checkpoint/processors/base.py +199 -0
  7. aws_durable_execution_sdk_python_testing/checkpoint/processors/callback.py +89 -0
  8. aws_durable_execution_sdk_python_testing/checkpoint/processors/context.py +59 -0
  9. aws_durable_execution_sdk_python_testing/checkpoint/processors/execution.py +52 -0
  10. aws_durable_execution_sdk_python_testing/checkpoint/processors/step.py +124 -0
  11. aws_durable_execution_sdk_python_testing/checkpoint/processors/wait.py +95 -0
  12. aws_durable_execution_sdk_python_testing/checkpoint/transformer.py +104 -0
  13. aws_durable_execution_sdk_python_testing/checkpoint/validators/__init__.py +1 -0
  14. aws_durable_execution_sdk_python_testing/checkpoint/validators/checkpoint.py +242 -0
  15. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/__init__.py +1 -0
  16. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/callback.py +45 -0
  17. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/context.py +73 -0
  18. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/execution.py +47 -0
  19. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/invoke.py +56 -0
  20. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/step.py +106 -0
  21. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/wait.py +54 -0
  22. aws_durable_execution_sdk_python_testing/checkpoint/validators/transitions.py +66 -0
  23. aws_durable_execution_sdk_python_testing/cli.py +501 -0
  24. aws_durable_execution_sdk_python_testing/client.py +50 -0
  25. aws_durable_execution_sdk_python_testing/exceptions.py +288 -0
  26. aws_durable_execution_sdk_python_testing/execution.py +440 -0
  27. aws_durable_execution_sdk_python_testing/executor.py +1214 -0
  28. aws_durable_execution_sdk_python_testing/invoker.py +327 -0
  29. aws_durable_execution_sdk_python_testing/model.py +3267 -0
  30. aws_durable_execution_sdk_python_testing/observer.py +144 -0
  31. aws_durable_execution_sdk_python_testing/py.typed +1 -0
  32. aws_durable_execution_sdk_python_testing/runner.py +1185 -0
  33. aws_durable_execution_sdk_python_testing/scheduler.py +246 -0
  34. aws_durable_execution_sdk_python_testing/stores/__init__.py +1 -0
  35. aws_durable_execution_sdk_python_testing/stores/base.py +147 -0
  36. aws_durable_execution_sdk_python_testing/stores/filesystem.py +104 -0
  37. aws_durable_execution_sdk_python_testing/stores/memory.py +38 -0
  38. aws_durable_execution_sdk_python_testing/stores/sqlite.py +283 -0
  39. aws_durable_execution_sdk_python_testing/token.py +49 -0
  40. aws_durable_execution_sdk_python_testing/web/__init__.py +1 -0
  41. aws_durable_execution_sdk_python_testing/web/errors.py +8 -0
  42. aws_durable_execution_sdk_python_testing/web/handlers.py +807 -0
  43. aws_durable_execution_sdk_python_testing/web/models.py +266 -0
  44. aws_durable_execution_sdk_python_testing/web/routes.py +684 -0
  45. aws_durable_execution_sdk_python_testing/web/serialization.py +245 -0
  46. aws_durable_execution_sdk_python_testing/web/server.py +243 -0
  47. aws_durable_execution_sdk_python_testing-1.0.0.dist-info/METADATA +221 -0
  48. aws_durable_execution_sdk_python_testing-1.0.0.dist-info/RECORD +52 -0
  49. aws_durable_execution_sdk_python_testing-1.0.0.dist-info/WHEEL +4 -0
  50. aws_durable_execution_sdk_python_testing-1.0.0.dist-info/entry_points.txt +2 -0
  51. aws_durable_execution_sdk_python_testing-1.0.0.dist-info/licenses/LICENSE +175 -0
  52. 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.