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