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,692 @@
|
|
|
1
|
+
"""Strongly-typed route parsing system for HTTP request routing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from urllib.parse import unquote
|
|
7
|
+
|
|
8
|
+
from async_durable_execution_runner.exceptions import (
|
|
9
|
+
UnknownRouteError,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class Route:
|
|
15
|
+
"""Base route with segments and pattern matching capabilities."""
|
|
16
|
+
|
|
17
|
+
raw_path: str
|
|
18
|
+
segments: list[str]
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_route(cls, _route: Route) -> Route:
|
|
22
|
+
"""Create a typed route from a base Route.
|
|
23
|
+
|
|
24
|
+
Note: Call is_match(route, method) first to ensure the route is valid for this type.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
route: Base route to convert
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Typed route instance
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
NotImplementedError: This is an abstract method that must be implemented by subclasses
|
|
34
|
+
"""
|
|
35
|
+
msg = "Subclasses must implement from_route()"
|
|
36
|
+
raise NotImplementedError(msg)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_string(cls, path: str) -> Route:
|
|
40
|
+
"""Create a Route from a string.
|
|
41
|
+
|
|
42
|
+
Each segment is URL-decoded; ``raw_path`` is preserved as the
|
|
43
|
+
original wire path. Splitting on ``/`` happens before decoding so
|
|
44
|
+
that an encoded ``%2F`` inside a captured value (e.g. an ARN that
|
|
45
|
+
contains ``/``) stays inside its segment instead of being treated
|
|
46
|
+
as a path separator.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
path: The raw path string
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Route instance with parsed, URL-decoded segments
|
|
53
|
+
"""
|
|
54
|
+
# Remove leading/trailing slashes, split on '/', then URL-decode each
|
|
55
|
+
# segment. Order matters: split on the literal '/' first so '%2F'-
|
|
56
|
+
# encoded slashes inside values don't act as separators.
|
|
57
|
+
segments = [unquote(s) for s in path.strip("/").split("/") if s]
|
|
58
|
+
return cls(raw_path=path, segments=segments)
|
|
59
|
+
|
|
60
|
+
def matches_pattern(self, pattern: list[str]) -> bool:
|
|
61
|
+
"""Check if route matches the given pattern.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
pattern: List of pattern segments. Use '*' for wildcards.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
True if the route matches the pattern
|
|
68
|
+
"""
|
|
69
|
+
if len(self.segments) != len(pattern):
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
for segment, pattern_part in zip(self.segments, pattern, strict=False):
|
|
73
|
+
if pattern_part not in ("*", segment):
|
|
74
|
+
return False
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def is_match(cls, _route: Route, _method: str) -> bool:
|
|
79
|
+
"""Check if the route and HTTP method match this route type.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
_route: Route to check
|
|
83
|
+
_method: HTTP method to check
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
True if the route and method match
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
NotImplementedError: This is an abstract method that must be implemented by subclasses
|
|
90
|
+
"""
|
|
91
|
+
msg = "Subclasses must implement is_match()"
|
|
92
|
+
raise NotImplementedError(msg)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class StartExecutionRoute(Route):
|
|
97
|
+
"""Route: POST /start-durable-execution"""
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def is_match(cls, route: Route, method: str) -> bool:
|
|
101
|
+
"""Check if the route and HTTP method match this route type.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
route: Route to check
|
|
105
|
+
method: HTTP method to check
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True if the route and method match
|
|
109
|
+
"""
|
|
110
|
+
return route.raw_path == "/start-durable-execution" and method == "POST"
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def from_route(cls, route: Route) -> StartExecutionRoute:
|
|
114
|
+
"""Create a StartExecutionRoute from a base Route.
|
|
115
|
+
|
|
116
|
+
Note: Call is_match(route, method) first to ensure the route is valid for this type.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
route: Base route to convert
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
StartExecutionRoute instance
|
|
123
|
+
"""
|
|
124
|
+
return cls(raw_path=route.raw_path, segments=route.segments)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(frozen=True)
|
|
128
|
+
class GetDurableExecutionRoute(Route):
|
|
129
|
+
"""Route: GET /2025-12-01/durable-executions/{arn}"""
|
|
130
|
+
|
|
131
|
+
arn: str
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def is_match(cls, route: Route, method: str) -> bool:
|
|
135
|
+
"""Check if the route and HTTP method match this route type.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
route: Route to check
|
|
139
|
+
method: HTTP method to check
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
True if the route and method match
|
|
143
|
+
"""
|
|
144
|
+
return (
|
|
145
|
+
route.matches_pattern(["2025-12-01", "durable-executions", "*"])
|
|
146
|
+
and method == "GET"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def from_route(cls, route: Route) -> GetDurableExecutionRoute:
|
|
151
|
+
"""Create a GetDurableExecutionRoute from a base Route.
|
|
152
|
+
|
|
153
|
+
Note: Call is_match(route, method) first to ensure the route is valid for this type.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
route: Base route to convert
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
GetDurableExecutionRoute instance with extracted ARN
|
|
160
|
+
"""
|
|
161
|
+
return cls(
|
|
162
|
+
raw_path=route.raw_path,
|
|
163
|
+
segments=route.segments,
|
|
164
|
+
arn=route.segments[2],
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass(frozen=True)
|
|
169
|
+
class CheckpointDurableExecutionRoute(Route):
|
|
170
|
+
"""Route: POST /2025-12-01/durable-executions/{arn}/checkpoint"""
|
|
171
|
+
|
|
172
|
+
arn: str
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def is_match(cls, route: Route, method: str) -> bool:
|
|
176
|
+
"""Check if the route and HTTP method match this route type.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
route: Route to check
|
|
180
|
+
method: HTTP method to check
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
True if the route and method match
|
|
184
|
+
"""
|
|
185
|
+
return (
|
|
186
|
+
route.matches_pattern(
|
|
187
|
+
["2025-12-01", "durable-executions", "*", "checkpoint"]
|
|
188
|
+
)
|
|
189
|
+
and method == "POST"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
def from_route(cls, route: Route) -> CheckpointDurableExecutionRoute:
|
|
194
|
+
"""Create a CheckpointDurableExecutionRoute from a base Route.
|
|
195
|
+
|
|
196
|
+
Note: Call is_match(route, method) first to ensure the route is valid for this type.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
route: Base route to convert
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
CheckpointDurableExecutionRoute instance with extracted ARN
|
|
203
|
+
"""
|
|
204
|
+
return cls(
|
|
205
|
+
raw_path=route.raw_path,
|
|
206
|
+
segments=route.segments,
|
|
207
|
+
arn=route.segments[2],
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dataclass(frozen=True)
|
|
212
|
+
class StopDurableExecutionRoute(Route):
|
|
213
|
+
"""Route: POST /2025-12-01/durable-executions/{arn}/stop"""
|
|
214
|
+
|
|
215
|
+
arn: str
|
|
216
|
+
|
|
217
|
+
@classmethod
|
|
218
|
+
def is_match(cls, route: Route, method: str) -> bool:
|
|
219
|
+
"""Check if the route and HTTP method match this route type.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
route: Route to check
|
|
223
|
+
method: HTTP method to check
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
True if the route and method match
|
|
227
|
+
"""
|
|
228
|
+
return (
|
|
229
|
+
route.matches_pattern(["2025-12-01", "durable-executions", "*", "stop"])
|
|
230
|
+
and method == "POST"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def from_route(cls, route: Route) -> StopDurableExecutionRoute:
|
|
235
|
+
"""Create a StopDurableExecutionRoute from a base Route.
|
|
236
|
+
|
|
237
|
+
Note: Call is_match(route, method) first to ensure the route is valid for this type.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
route: Base route to convert
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
StopDurableExecutionRoute instance with extracted ARN
|
|
244
|
+
"""
|
|
245
|
+
return cls(
|
|
246
|
+
raw_path=route.raw_path,
|
|
247
|
+
segments=route.segments,
|
|
248
|
+
arn=route.segments[2],
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@dataclass(frozen=True)
|
|
253
|
+
class GetDurableExecutionStateRoute(Route):
|
|
254
|
+
"""Route: GET /2025-12-01/durable-executions/{arn}/state"""
|
|
255
|
+
|
|
256
|
+
arn: str
|
|
257
|
+
|
|
258
|
+
@classmethod
|
|
259
|
+
def is_match(cls, route: Route, method: str) -> bool:
|
|
260
|
+
"""Check if the route and HTTP method match this route type.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
route: Route to check
|
|
264
|
+
method: HTTP method to check
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
True if the route and method match
|
|
268
|
+
"""
|
|
269
|
+
return (
|
|
270
|
+
route.matches_pattern(["2025-12-01", "durable-executions", "*", "state"])
|
|
271
|
+
and method == "GET"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
@classmethod
|
|
275
|
+
def from_route(cls, route: Route) -> GetDurableExecutionStateRoute:
|
|
276
|
+
"""Create a GetDurableExecutionStateRoute from a base Route.
|
|
277
|
+
|
|
278
|
+
Note: Call is_match(route, method) first to ensure the route is valid for this type.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
route: Base route to convert
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
GetDurableExecutionStateRoute instance with extracted ARN
|
|
285
|
+
"""
|
|
286
|
+
return cls(
|
|
287
|
+
raw_path=route.raw_path,
|
|
288
|
+
segments=route.segments,
|
|
289
|
+
arn=route.segments[2],
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@dataclass(frozen=True)
|
|
294
|
+
class GetDurableExecutionHistoryRoute(Route):
|
|
295
|
+
"""Route: GET /2025-12-01/durable-executions/{arn}/history"""
|
|
296
|
+
|
|
297
|
+
arn: str
|
|
298
|
+
|
|
299
|
+
@classmethod
|
|
300
|
+
def is_match(cls, route: Route, method: str) -> bool:
|
|
301
|
+
"""Check if the route and HTTP method match this route type.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
route: Route to check
|
|
305
|
+
method: HTTP method to check
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
True if the route and method match
|
|
309
|
+
"""
|
|
310
|
+
return (
|
|
311
|
+
route.matches_pattern(["2025-12-01", "durable-executions", "*", "history"])
|
|
312
|
+
and method == "GET"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
@classmethod
|
|
316
|
+
def from_route(cls, route: Route) -> GetDurableExecutionHistoryRoute:
|
|
317
|
+
"""Create a GetDurableExecutionHistoryRoute from a base Route.
|
|
318
|
+
|
|
319
|
+
Note: Call is_match(route, method) first to ensure the route is valid for this type.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
route: Base route to convert
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
GetDurableExecutionHistoryRoute instance with extracted ARN
|
|
326
|
+
"""
|
|
327
|
+
return cls(
|
|
328
|
+
raw_path=route.raw_path,
|
|
329
|
+
segments=route.segments,
|
|
330
|
+
arn=route.segments[2],
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@dataclass(frozen=True)
|
|
335
|
+
class ListDurableExecutionsRoute(Route):
|
|
336
|
+
"""Route: GET /2025-12-01/durable-executions"""
|
|
337
|
+
|
|
338
|
+
@classmethod
|
|
339
|
+
def is_match(cls, route: Route, method: str) -> bool:
|
|
340
|
+
"""Check if the route and HTTP method match this route type.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
route: Route to check
|
|
344
|
+
method: HTTP method to check
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
True if the route and method match
|
|
348
|
+
"""
|
|
349
|
+
return (
|
|
350
|
+
route.matches_pattern(["2025-12-01", "durable-executions"])
|
|
351
|
+
and method == "GET"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
@classmethod
|
|
355
|
+
def from_route(cls, route: Route) -> ListDurableExecutionsRoute:
|
|
356
|
+
"""Create a ListDurableExecutionsRoute from a base Route.
|
|
357
|
+
|
|
358
|
+
Note: Call is_match(route, method) first to ensure the route is valid for this type.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
route: Base route to convert
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
ListDurableExecutionsRoute instance
|
|
365
|
+
"""
|
|
366
|
+
return cls(raw_path=route.raw_path, segments=route.segments)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@dataclass(frozen=True)
|
|
370
|
+
class ListDurableExecutionsByFunctionRoute(Route):
|
|
371
|
+
"""Route: GET /2025-12-01/functions/{function_name}/durable-executions"""
|
|
372
|
+
|
|
373
|
+
function_name: str
|
|
374
|
+
|
|
375
|
+
@classmethod
|
|
376
|
+
def is_match(cls, route: Route, method: str) -> bool:
|
|
377
|
+
"""Check if the route and HTTP method match this route type.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
route: Route to check
|
|
381
|
+
method: HTTP method to check
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
True if the route and method match
|
|
385
|
+
"""
|
|
386
|
+
return (
|
|
387
|
+
route.matches_pattern(
|
|
388
|
+
["2025-12-01", "functions", "*", "durable-executions"]
|
|
389
|
+
)
|
|
390
|
+
and method == "GET"
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
@classmethod
|
|
394
|
+
def from_route(cls, route: Route) -> ListDurableExecutionsByFunctionRoute:
|
|
395
|
+
"""Create a ListDurableExecutionsByFunctionRoute from a base Route.
|
|
396
|
+
|
|
397
|
+
Note: Call is_match(route, method) first to ensure the route is valid for this type.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
route: Base route to convert
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
ListDurableExecutionsByFunctionRoute instance with extracted function name
|
|
404
|
+
"""
|
|
405
|
+
return cls(
|
|
406
|
+
raw_path=route.raw_path,
|
|
407
|
+
segments=route.segments,
|
|
408
|
+
function_name=route.segments[2],
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@dataclass(frozen=True)
|
|
413
|
+
class BytesPayloadRoute(Route):
|
|
414
|
+
"""Base class for routes that handle raw bytes payloads instead of JSON."""
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
@dataclass(frozen=True)
|
|
418
|
+
class CallbackSuccessRoute(BytesPayloadRoute):
|
|
419
|
+
"""Route: POST /2025-12-01/durable-execution-callbacks/{callback_id}/succeed"""
|
|
420
|
+
|
|
421
|
+
callback_id: str
|
|
422
|
+
|
|
423
|
+
@classmethod
|
|
424
|
+
def is_match(cls, route: Route, method: str) -> bool:
|
|
425
|
+
"""Check if the route and HTTP method match this route type.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
route: Route to check
|
|
429
|
+
method: HTTP method to check
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
True if the route and method match
|
|
433
|
+
"""
|
|
434
|
+
return (
|
|
435
|
+
route.matches_pattern(
|
|
436
|
+
["2025-12-01", "durable-execution-callbacks", "*", "succeed"]
|
|
437
|
+
)
|
|
438
|
+
and method == "POST"
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
@classmethod
|
|
442
|
+
def from_route(cls, route: Route) -> CallbackSuccessRoute:
|
|
443
|
+
"""Create a CallbackSuccessRoute from a base Route.
|
|
444
|
+
|
|
445
|
+
Note: Call is_match(route, method) first to ensure the route is valid for this type.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
route: Base route to convert
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
CallbackSuccessRoute instance with extracted callback ID
|
|
452
|
+
"""
|
|
453
|
+
return cls(
|
|
454
|
+
raw_path=route.raw_path,
|
|
455
|
+
segments=route.segments,
|
|
456
|
+
callback_id=route.segments[2],
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
@dataclass(frozen=True)
|
|
461
|
+
class CallbackFailureRoute(BytesPayloadRoute):
|
|
462
|
+
"""Route: POST /2025-12-01/durable-execution-callbacks/{callback_id}/fail"""
|
|
463
|
+
|
|
464
|
+
callback_id: str
|
|
465
|
+
|
|
466
|
+
@classmethod
|
|
467
|
+
def is_match(cls, route: Route, method: str) -> bool:
|
|
468
|
+
"""Check if the route and HTTP method match this route type.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
route: Route to check
|
|
472
|
+
method: HTTP method to check
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
True if the route and method match
|
|
476
|
+
"""
|
|
477
|
+
return (
|
|
478
|
+
route.matches_pattern(
|
|
479
|
+
["2025-12-01", "durable-execution-callbacks", "*", "fail"]
|
|
480
|
+
)
|
|
481
|
+
and method == "POST"
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
@classmethod
|
|
485
|
+
def from_route(cls, route: Route) -> CallbackFailureRoute:
|
|
486
|
+
"""Create a CallbackFailureRoute from a base Route.
|
|
487
|
+
|
|
488
|
+
Note: Call is_match(route, method) first to ensure the route is valid for this type.
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
route: Base route to convert
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
CallbackFailureRoute instance with extracted callback ID
|
|
495
|
+
"""
|
|
496
|
+
return cls(
|
|
497
|
+
raw_path=route.raw_path,
|
|
498
|
+
segments=route.segments,
|
|
499
|
+
callback_id=route.segments[2],
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
@dataclass(frozen=True)
|
|
504
|
+
class CallbackHeartbeatRoute(Route):
|
|
505
|
+
"""Route: POST /2025-12-01/durable-execution-callbacks/{callback_id}/heartbeat"""
|
|
506
|
+
|
|
507
|
+
callback_id: str
|
|
508
|
+
|
|
509
|
+
@classmethod
|
|
510
|
+
def is_match(cls, route: Route, method: str) -> bool:
|
|
511
|
+
"""Check if the route and HTTP method match this route type.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
route: Route to check
|
|
515
|
+
method: HTTP method to check
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
True if the route and method match
|
|
519
|
+
"""
|
|
520
|
+
return (
|
|
521
|
+
route.matches_pattern(
|
|
522
|
+
["2025-12-01", "durable-execution-callbacks", "*", "heartbeat"]
|
|
523
|
+
)
|
|
524
|
+
and method == "POST"
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
@classmethod
|
|
528
|
+
def from_route(cls, route: Route) -> CallbackHeartbeatRoute:
|
|
529
|
+
"""Create a CallbackHeartbeatRoute from a base Route.
|
|
530
|
+
|
|
531
|
+
Note: Call is_match(route, method) first to ensure the route is valid for this type.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
route: Base route to convert
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
CallbackHeartbeatRoute instance with extracted callback ID
|
|
538
|
+
"""
|
|
539
|
+
return cls(
|
|
540
|
+
raw_path=route.raw_path,
|
|
541
|
+
segments=route.segments,
|
|
542
|
+
callback_id=route.segments[2],
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
@dataclass(frozen=True)
|
|
547
|
+
class HealthRoute(Route):
|
|
548
|
+
"""Route: GET /health"""
|
|
549
|
+
|
|
550
|
+
@classmethod
|
|
551
|
+
def is_match(cls, route: Route, method: str) -> bool:
|
|
552
|
+
"""Check if the route and HTTP method match this route type.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
route: Route to check
|
|
556
|
+
method: HTTP method to check
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
True if the route and method match
|
|
560
|
+
"""
|
|
561
|
+
return route.raw_path == "/health" and method == "GET"
|
|
562
|
+
|
|
563
|
+
@classmethod
|
|
564
|
+
def from_route(cls, route: Route) -> HealthRoute:
|
|
565
|
+
"""Create a HealthRoute from a base Route.
|
|
566
|
+
|
|
567
|
+
Note: Call is_match(route, method) first to ensure the route is valid for this type.
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
route: Base route to convert
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
HealthRoute instance
|
|
574
|
+
"""
|
|
575
|
+
return cls(raw_path=route.raw_path, segments=route.segments)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
@dataclass(frozen=True)
|
|
579
|
+
class UpdateLambdaEndpointRoute(Route):
|
|
580
|
+
"""Route: PUT /lambda-endpoint"""
|
|
581
|
+
|
|
582
|
+
@classmethod
|
|
583
|
+
def is_match(cls, route: Route, method: str) -> bool:
|
|
584
|
+
"""Check if the route and HTTP method match this route type.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
route: Route to check
|
|
588
|
+
method: HTTP method to check
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
True if the route and method match
|
|
592
|
+
"""
|
|
593
|
+
return route.raw_path == "/lambda-endpoint" and method == "PUT"
|
|
594
|
+
|
|
595
|
+
@classmethod
|
|
596
|
+
def from_route(cls, route: Route) -> UpdateLambdaEndpointRoute:
|
|
597
|
+
"""Create UpdateLambdaEndpointRoute from base route.
|
|
598
|
+
|
|
599
|
+
Args:
|
|
600
|
+
route: Base route to convert
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
UpdateLambdaEndpointRoute instance
|
|
604
|
+
"""
|
|
605
|
+
return cls(raw_path=route.raw_path, segments=route.segments)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
@dataclass(frozen=True)
|
|
609
|
+
class MetricsRoute(Route):
|
|
610
|
+
"""Route: GET /metrics"""
|
|
611
|
+
|
|
612
|
+
@classmethod
|
|
613
|
+
def is_match(cls, route: Route, method: str) -> bool:
|
|
614
|
+
"""Check if the route and HTTP method match this route type.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
route: Route to check
|
|
618
|
+
method: HTTP method to check
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
True if the route and method match
|
|
622
|
+
"""
|
|
623
|
+
return route.raw_path == "/metrics" and method == "GET"
|
|
624
|
+
|
|
625
|
+
@classmethod
|
|
626
|
+
def from_route(cls, route: Route) -> MetricsRoute:
|
|
627
|
+
"""Create a MetricsRoute from a base Route.
|
|
628
|
+
|
|
629
|
+
Note: Call is_match(route, method) first to ensure the route is valid for this type.
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
route: Base route to convert
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
MetricsRoute instance
|
|
636
|
+
"""
|
|
637
|
+
return cls(raw_path=route.raw_path, segments=route.segments)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
# Default registry of all route types for matching
|
|
641
|
+
DEFAULT_ROUTE_TYPES: list[type[Route]] = [
|
|
642
|
+
StartExecutionRoute,
|
|
643
|
+
GetDurableExecutionRoute,
|
|
644
|
+
CheckpointDurableExecutionRoute,
|
|
645
|
+
StopDurableExecutionRoute,
|
|
646
|
+
GetDurableExecutionStateRoute,
|
|
647
|
+
GetDurableExecutionHistoryRoute,
|
|
648
|
+
ListDurableExecutionsRoute,
|
|
649
|
+
ListDurableExecutionsByFunctionRoute,
|
|
650
|
+
CallbackSuccessRoute,
|
|
651
|
+
CallbackFailureRoute,
|
|
652
|
+
CallbackHeartbeatRoute,
|
|
653
|
+
HealthRoute,
|
|
654
|
+
UpdateLambdaEndpointRoute,
|
|
655
|
+
MetricsRoute,
|
|
656
|
+
]
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
class Router:
|
|
660
|
+
"""HTTP request router that matches routes to strongly-typed route objects."""
|
|
661
|
+
|
|
662
|
+
def __init__(self, route_types: list[type[Route]] | None = None) -> None:
|
|
663
|
+
"""Initialize the router with route types.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
route_types: List of route type classes to use for matching.
|
|
667
|
+
If None, uses the default route types.
|
|
668
|
+
"""
|
|
669
|
+
self._route_types = (
|
|
670
|
+
route_types if route_types is not None else DEFAULT_ROUTE_TYPES
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
def find_route(self, path: str, method: str) -> Route:
|
|
674
|
+
"""Find a matching route for the given path and HTTP method.
|
|
675
|
+
|
|
676
|
+
Args:
|
|
677
|
+
path: The raw path string to parse
|
|
678
|
+
method: The HTTP method (GET, POST, etc.)
|
|
679
|
+
|
|
680
|
+
Returns:
|
|
681
|
+
Strongly-typed Route instance
|
|
682
|
+
|
|
683
|
+
Raises:
|
|
684
|
+
UnknownRouteError: If the path and method don't match any known pattern
|
|
685
|
+
"""
|
|
686
|
+
base_route = Route.from_string(path)
|
|
687
|
+
|
|
688
|
+
for route_type in self._route_types:
|
|
689
|
+
if route_type.is_match(base_route, method):
|
|
690
|
+
return route_type.from_route(base_route)
|
|
691
|
+
|
|
692
|
+
raise UnknownRouteError(method, path)
|