modmex-lambda 0.1.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 (48) hide show
  1. modmex_lambda/__init__.py +62 -0
  2. modmex_lambda/data_classes/__init__.py +49 -0
  3. modmex_lambda/data_classes/api_gateway_authorizer_event.py +38 -0
  4. modmex_lambda/data_classes/api_gateway_proxy_event.py +328 -0
  5. modmex_lambda/data_classes/api_gateway_websocket_event.py +40 -0
  6. modmex_lambda/data_classes/cognito_user_pool_event.py +599 -0
  7. modmex_lambda/data_classes/common.py +441 -0
  8. modmex_lambda/event_handler/__init__.py +45 -0
  9. modmex_lambda/event_handler/api_gateway.py +331 -0
  10. modmex_lambda/event_handler/constants.py +3 -0
  11. modmex_lambda/event_handler/content_types.py +13 -0
  12. modmex_lambda/event_handler/cors.py +97 -0
  13. modmex_lambda/event_handler/dependencies/__init__.py +0 -0
  14. modmex_lambda/event_handler/dependencies/compat.py +231 -0
  15. modmex_lambda/event_handler/dependencies/dependant.py +279 -0
  16. modmex_lambda/event_handler/dependencies/dependency_middleware.py +423 -0
  17. modmex_lambda/event_handler/dependencies/depends.py +184 -0
  18. modmex_lambda/event_handler/dependencies/params.py +317 -0
  19. modmex_lambda/event_handler/dependencies/types.py +14 -0
  20. modmex_lambda/event_handler/exception_handler.py +70 -0
  21. modmex_lambda/event_handler/exceptions.py +72 -0
  22. modmex_lambda/event_handler/gateway_response.py +96 -0
  23. modmex_lambda/event_handler/middlewares.py +33 -0
  24. modmex_lambda/event_handler/params.py +44 -0
  25. modmex_lambda/event_handler/request.py +70 -0
  26. modmex_lambda/event_handler/response.py +60 -0
  27. modmex_lambda/event_handler/routing.py +507 -0
  28. modmex_lambda/event_handler/routing_fallbacks.py +92 -0
  29. modmex_lambda/event_handler/types.py +31 -0
  30. modmex_lambda/event_sources.py +53 -0
  31. modmex_lambda/exceptions.py +3 -0
  32. modmex_lambda/logging.py +99 -0
  33. modmex_lambda/params.py +3 -0
  34. modmex_lambda/parser.py +47 -0
  35. modmex_lambda/request.py +3 -0
  36. modmex_lambda/resolver.py +3 -0
  37. modmex_lambda/response.py +3 -0
  38. modmex_lambda/routing.py +3 -0
  39. modmex_lambda/shared/__init__.py +0 -0
  40. modmex_lambda/shared/cookies.py +84 -0
  41. modmex_lambda/shared/headers_serializer.py +65 -0
  42. modmex_lambda/shared/json_encoder.py +53 -0
  43. modmex_lambda/shared/types.py +4 -0
  44. modmex_lambda/validation.py +178 -0
  45. modmex_lambda-0.1.0.dist-info/METADATA +375 -0
  46. modmex_lambda-0.1.0.dist-info/RECORD +48 -0
  47. modmex_lambda-0.1.0.dist-info/WHEEL +4 -0
  48. modmex_lambda-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,507 @@
1
+ """Routing primitives for API Gateway resolvers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import re
7
+ from abc import ABC, abstractmethod
8
+ from typing import Any, Callable, Pattern
9
+
10
+ from modmex_lambda.event_handler.constants import DEFAULT_STATUS_CODE
11
+ from modmex_lambda.event_handler.dependencies.dependant import get_dependant, is_request_annotation
12
+ from modmex_lambda.event_handler.dependencies.depends import solve_dependencies
13
+ from modmex_lambda.event_handler.dependencies.params import Dependant
14
+ from modmex_lambda.event_handler.response import Response
15
+ from modmex_lambda.event_handler.types import IApiGatewayResolver
16
+ from modmex_lambda.shared.types import AnyCallableT
17
+
18
+
19
+ Handler = AnyCallableT
20
+ NextMiddleware = Callable[..., dict | tuple | Response]
21
+
22
+ _DYNAMIC_ROUTE_PATTERN = r"(<\w+>)"
23
+ _SAFE_URI = "-._~()'!*:@,;=+&$" # https://www.ietf.org/rfc/rfc3986.txt
24
+ _UNSAFE_URI = r"%<> \[\]{}|^"
25
+ _NAMED_GROUP_BOUNDARY_PATTERN = rf"(?P\1[{_SAFE_URI}{_UNSAFE_URI}\\w]+)"
26
+ _ROUTE_REGEX = "^{}$"
27
+
28
+
29
+ class IRoute(ABC):
30
+ method: str
31
+ path: str
32
+
33
+ @abstractmethod
34
+ def invoke(
35
+ self,
36
+ router_middlewares: list[Callable],
37
+ app: IApiGatewayResolver,
38
+ route_arguments: dict[str, str],
39
+ ) -> Response:
40
+ raise NotImplementedError
41
+
42
+ @abstractmethod
43
+ def match(self, path: str) -> dict[str, str] | None:
44
+ raise NotImplementedError
45
+
46
+ @property
47
+ @abstractmethod
48
+ def dependant(self) -> Dependant:
49
+ raise NotImplementedError()
50
+
51
+
52
+ class HasRoutes(ABC):
53
+ @abstractmethod
54
+ def route(
55
+ self,
56
+ rule: str,
57
+ method: str | list[str] | tuple[str],
58
+ description: str | None = None,
59
+ status_code: int = DEFAULT_STATUS_CODE,
60
+ middlewares: list[AnyCallableT] | None = None,
61
+ cors: bool | None = None,
62
+ compress: bool = False,
63
+ cache_control: str | None = None,
64
+ ):
65
+ raise NotImplementedError
66
+
67
+ def get(
68
+ self,
69
+ rule: str,
70
+ description: str | None = None,
71
+ status_code: int = DEFAULT_STATUS_CODE,
72
+ middlewares: list[AnyCallableT] | None = None,
73
+ cors: bool | None = None,
74
+ compress: bool = False,
75
+ cache_control: str | None = None,
76
+ ) -> Callable[[AnyCallableT], AnyCallableT]:
77
+ return self.route(
78
+ rule=rule,
79
+ method="GET",
80
+ description=description,
81
+ status_code=status_code,
82
+ middlewares=middlewares,
83
+ cors=cors,
84
+ compress=compress,
85
+ cache_control=cache_control,
86
+ )
87
+
88
+ def post(
89
+ self,
90
+ rule: str,
91
+ description: str | None = None,
92
+ status_code: int = DEFAULT_STATUS_CODE,
93
+ middlewares: list[AnyCallableT] | None = None,
94
+ cors: bool | None = None,
95
+ compress: bool = False,
96
+ cache_control: str | None = None,
97
+ ) -> Callable[[AnyCallableT], AnyCallableT]:
98
+ return self.route(
99
+ rule=rule,
100
+ method="POST",
101
+ description=description,
102
+ status_code=status_code,
103
+ middlewares=middlewares,
104
+ cors=cors,
105
+ compress=compress,
106
+ cache_control=cache_control,
107
+ )
108
+
109
+ def put(
110
+ self,
111
+ rule: str,
112
+ description: str | None = None,
113
+ status_code: int = DEFAULT_STATUS_CODE,
114
+ middlewares: list[AnyCallableT] | None = None,
115
+ cors: bool | None = None,
116
+ compress: bool = False,
117
+ cache_control: str | None = None,
118
+ ) -> Callable[[AnyCallableT], AnyCallableT]:
119
+ return self.route(
120
+ rule=rule,
121
+ method="PUT",
122
+ description=description,
123
+ status_code=status_code,
124
+ middlewares=middlewares,
125
+ cors=cors,
126
+ compress=compress,
127
+ cache_control=cache_control,
128
+ )
129
+
130
+ def patch(
131
+ self,
132
+ rule: str,
133
+ description: str | None = None,
134
+ status_code: int = DEFAULT_STATUS_CODE,
135
+ middlewares: list[AnyCallableT] | None = None,
136
+ cors: bool | None = None,
137
+ compress: bool = False,
138
+ cache_control: str | None = None,
139
+ ) -> Callable[[AnyCallableT], AnyCallableT]:
140
+ return self.route(
141
+ rule=rule,
142
+ method="PATCH",
143
+ description=description,
144
+ status_code=status_code,
145
+ middlewares=middlewares,
146
+ cors=cors,
147
+ compress=compress,
148
+ cache_control=cache_control,
149
+ )
150
+
151
+ def delete(
152
+ self,
153
+ rule: str,
154
+ description: str | None = None,
155
+ status_code: int = DEFAULT_STATUS_CODE,
156
+ middlewares: list[AnyCallableT] | None = None,
157
+ cors: bool | None = None,
158
+ compress: bool = False,
159
+ cache_control: str | None = None,
160
+ ) -> Callable[[AnyCallableT], AnyCallableT]:
161
+ return self.route(
162
+ rule=rule,
163
+ method="DELETE",
164
+ description=description,
165
+ status_code=status_code,
166
+ middlewares=middlewares,
167
+ cors=cors,
168
+ compress=compress,
169
+ cache_control=cache_control,
170
+ )
171
+
172
+ def header(
173
+ self,
174
+ rule: str,
175
+ description: str | None = None,
176
+ status_code: int = DEFAULT_STATUS_CODE,
177
+ middlewares: list[AnyCallableT] | None = None,
178
+ cors: bool | None = None,
179
+ compress: bool = False,
180
+ cache_control: str | None = None,
181
+ ) -> Callable[[AnyCallableT], AnyCallableT]:
182
+ return self.route(
183
+ rule=rule,
184
+ method="HEAD",
185
+ description=description,
186
+ status_code=status_code,
187
+ middlewares=middlewares,
188
+ cors=cors,
189
+ compress=compress,
190
+ cache_control=cache_control,
191
+ )
192
+
193
+
194
+ class IRouter(ABC):
195
+ _routes: list[IRoute]
196
+
197
+ @abstractmethod
198
+ def include_router(self, router: IRouter, prefix: str = "") -> None:
199
+ raise NotImplementedError
200
+
201
+
202
+ class MiddlewareChainLink:
203
+ def __init__(
204
+ self,
205
+ current_middleware: Callable[..., Any],
206
+ next_middleware: Callable[..., Any],
207
+ ) -> None:
208
+ self.current_middleware = current_middleware
209
+ self.next_middleware = next_middleware
210
+ self._next_middleware_name = callable_name(next_middleware)
211
+
212
+ @property
213
+ def __name__(self) -> str:
214
+ return callable_name(self.current_middleware)
215
+
216
+ def __str__(self) -> str:
217
+ return f"{self.__name__} -> {self._next_middleware_name}"
218
+
219
+ def __call__(self, app: IApiGatewayResolver) -> dict | tuple | Response:
220
+ return self.current_middleware(app, self.next_middleware)
221
+
222
+
223
+ def callable_name(value: Callable[..., Any]) -> str:
224
+ return getattr(value, "__name__", value.__class__.__name__)
225
+
226
+
227
+ class Route(IRoute):
228
+ __slots__ = (
229
+ "method",
230
+ "path",
231
+ "handler",
232
+ "pattern",
233
+ "description",
234
+ "status_code",
235
+ "middlewares",
236
+ "_middleware_stack",
237
+ "_middleware_stack_built",
238
+ "cors",
239
+ "cache_control",
240
+ "compress",
241
+ )
242
+
243
+ def __init__(
244
+ self,
245
+ method: str,
246
+ path: str,
247
+ handler: Callable,
248
+ cors: bool,
249
+ pattern: Pattern,
250
+ description: str | None = None,
251
+ status_code: int | None = DEFAULT_STATUS_CODE,
252
+ middlewares: list[AnyCallableT] | None = None,
253
+ compress: bool = False,
254
+ cache_control: str | None = None,
255
+ ) -> None:
256
+ self.method = method
257
+ self.path = path
258
+ self.handler = handler
259
+ self.cors = cors if cors is not None else False
260
+ self._middleware_stack = handler
261
+ self.pattern = pattern
262
+ self.middlewares = middlewares or []
263
+ self._middleware_stack_built = False
264
+ self.description = description
265
+ self.status_code = status_code
266
+ self._dependant: Dependant | None = None
267
+ self.responses: dict[int, Any] | None = None
268
+ self.compress = compress
269
+ self.cache_control = cache_control
270
+ self.request_param_name: str | None = None
271
+ self.request_param_name_checked = False
272
+
273
+ @property
274
+ def dependant(self) -> Dependant:
275
+ if not self._dependant:
276
+ self._dependant = get_dependant(path=self.path, call=self.handler, responses=self.responses)
277
+ return self._dependant
278
+
279
+ @property
280
+ def has_dependencies(self) -> bool:
281
+ return bool(self.dependant.dependencies)
282
+
283
+ def invoke(
284
+ self,
285
+ router_middlewares: list[Callable],
286
+ app: IApiGatewayResolver,
287
+ route_arguments: dict[str, str],
288
+ ) -> Response:
289
+ if not self._middleware_stack_built:
290
+ self._build_middleware_stack(router_middlewares=router_middlewares, app=app)
291
+
292
+ app.append_context(_route_args=route_arguments)
293
+ return self._middleware_stack(app)
294
+
295
+ def match(self, path: str) -> dict[str, str] | None:
296
+ match = self.pattern.match(path)
297
+ if match is None:
298
+ return None
299
+ if isinstance(match, dict):
300
+ return match
301
+ return match.groupdict()
302
+
303
+ def _build_middleware_stack(self, router_middlewares: list[Callable[..., Any]], app: IApiGatewayResolver) -> None:
304
+ middlewares = [
305
+ app._dependency_middleware,
306
+ *router_middlewares,
307
+ *self.middlewares,
308
+ RouteEndpointInvoker(),
309
+ ]
310
+
311
+ for handler in reversed(middlewares):
312
+ self._middleware_stack = MiddlewareChainLink(
313
+ current_middleware=handler,
314
+ next_middleware=self._middleware_stack,
315
+ )
316
+
317
+ self._middleware_stack_built = True
318
+
319
+
320
+ class Router(IRouter):
321
+ __slots__ = (
322
+ "_routes",
323
+ "_routes_with_middlewares",
324
+ "context",
325
+ "_dynamic_routes",
326
+ "_static_routes",
327
+ "_path_methods",
328
+ "cors_methods",
329
+ "exception_handler_manager",
330
+ )
331
+
332
+ def __init__(self):
333
+ from modmex_lambda.event_handler.exception_handler import ExceptionHandlerManager
334
+
335
+ self._routes: list[Route] = []
336
+ self._routes_with_middlewares: dict[tuple, list[Callable]] = {}
337
+ self.context = {}
338
+ self._dynamic_routes: list[Route] = []
339
+ self._static_routes: dict[tuple[str, str], Route] = {}
340
+ self._path_methods: dict[str, set[str]] = {}
341
+ self.cors_methods: set[str] = set()
342
+ self.exception_handler_manager = ExceptionHandlerManager()
343
+
344
+ def route(
345
+ self,
346
+ rule: str,
347
+ method: str | list[str] | tuple[str],
348
+ description: str | None = None,
349
+ status_code: int = DEFAULT_STATUS_CODE,
350
+ middlewares: list[AnyCallableT] | None = None,
351
+ cors: bool | None = None,
352
+ compress: bool = False,
353
+ cache_control: str | None = None,
354
+ ) -> Callable[[AnyCallableT], AnyCallableT]:
355
+ def decorator(func: AnyCallableT) -> AnyCallableT:
356
+ capture_definition_locals(func)
357
+ methods = (method,) if isinstance(method, str) else method
358
+ for item in methods:
359
+ self._add_route(
360
+ rule=rule,
361
+ method=item,
362
+ handler=func,
363
+ description=description,
364
+ status_code=status_code,
365
+ middlewares=middlewares,
366
+ cors=cors,
367
+ compress=compress,
368
+ cache_control=cache_control,
369
+ )
370
+ return func
371
+
372
+ return decorator
373
+
374
+ def _add_route(
375
+ self,
376
+ rule: str,
377
+ method: str,
378
+ handler: AnyCallableT,
379
+ description: str | None = None,
380
+ status_code: int | None = DEFAULT_STATUS_CODE,
381
+ middlewares: list[AnyCallableT] | None = None,
382
+ cors: bool | None = None,
383
+ compress: bool = False,
384
+ cache_control: str | None = None,
385
+ ) -> None:
386
+ method = method.upper()
387
+ route = Route(
388
+ method=method,
389
+ path=rule,
390
+ handler=handler,
391
+ pattern=self.compile_regex(rule),
392
+ description=description,
393
+ status_code=status_code,
394
+ middlewares=middlewares,
395
+ cors=cors,
396
+ compress=compress,
397
+ cache_control=cache_control,
398
+ )
399
+ self._routes.append(route)
400
+ self._path_methods.setdefault(rule, set()).add(method)
401
+
402
+ if route.pattern.groups > 0:
403
+ self._dynamic_routes.append(route)
404
+ else:
405
+ self._static_routes[(method, rule)] = route
406
+
407
+ self.cors_methods.add(method)
408
+
409
+ def include_router(self, router: Router, prefix: str = "") -> None:
410
+ for route in router._routes:
411
+ self._add_route(
412
+ rule=prefix + route.path,
413
+ method=route.method,
414
+ handler=route.handler,
415
+ description=route.description,
416
+ status_code=route.status_code,
417
+ middlewares=route.middlewares,
418
+ cors=route.cors,
419
+ compress=route.compress,
420
+ cache_control=route.cache_control,
421
+ )
422
+
423
+ def exception_handler(self, exc_class: type[Exception] | list[type[Exception]]):
424
+ return self.exception_handler_manager.exception_handler(exc_class)
425
+
426
+ def match(self, method: str, path: str) -> tuple[Route | None, dict[str, str], set[str]]:
427
+ method = method.upper()
428
+ route = self._static_routes.get((method, path)) or self._static_routes.get(("ANY", path))
429
+ if route is not None:
430
+ return route, {}, set()
431
+
432
+ allowed_methods = set(self._path_methods.get(path, set()))
433
+
434
+ for route in self._dynamic_routes:
435
+ path_params = route.match(path)
436
+ if path_params is None:
437
+ continue
438
+ allowed_methods.add(route.method.upper())
439
+ if route.method.upper() in {method, "ANY"}:
440
+ return route, path_params, set()
441
+ return None, {}, allowed_methods
442
+
443
+ @staticmethod
444
+ def compile_regex(rule: str, base_regex: str = _ROUTE_REGEX):
445
+ rule_regex = re.sub(_DYNAMIC_ROUTE_PATTERN, _NAMED_GROUP_BOUNDARY_PATTERN, rule)
446
+ return re.compile(base_regex.format(rule_regex))
447
+
448
+
449
+ def capture_definition_locals(func: Callable[..., Any]) -> None:
450
+ frame = inspect.currentframe()
451
+ if frame is not None and frame.f_back is not None and frame.f_back.f_back is not None:
452
+ setattr(func, "__modmex_lambda_localns__", dict(frame.f_back.f_back.f_locals))
453
+
454
+
455
+ def _find_request_param_name(func: Callable) -> str | None:
456
+ from typing import get_type_hints
457
+
458
+ try:
459
+ hints = get_type_hints(func)
460
+ except Exception:
461
+ hints = {}
462
+
463
+ for param_name, annotation in hints.items():
464
+ if is_request_annotation(annotation):
465
+ return param_name
466
+
467
+ return None
468
+
469
+
470
+ class RouteEndpointInvoker:
471
+ def __call__(self, app: IApiGatewayResolver, next_middleware: NextMiddleware) -> Response:
472
+ return self.handler(app, next_middleware)
473
+
474
+ def handler(self, app: IApiGatewayResolver, next_middleware: NextMiddleware) -> Response:
475
+ route_args: dict = app.context.get("_route_args", {})
476
+ route: Route | None = app.context.get("_route")
477
+
478
+ if route is not None:
479
+ if not route.request_param_name_checked:
480
+ route.request_param_name = _find_request_param_name(next_middleware)
481
+ route.request_param_name_checked = True
482
+
483
+ if route.request_param_name:
484
+ route_args = {
485
+ **route_args,
486
+ route.request_param_name: app.request,
487
+ }
488
+
489
+ if route.has_dependencies:
490
+ route_args.update(
491
+ solve_dependencies(
492
+ dependant=route.dependant,
493
+ request=app.request,
494
+ dependency_overrides=app.dependency_overrides or None,
495
+ ),
496
+ )
497
+
498
+ return app._to_response(next_middleware(**route_args))
499
+
500
+
501
+ __all__ = [
502
+ "HasRoutes",
503
+ "IRoute",
504
+ "IRouter",
505
+ "Route",
506
+ "Router",
507
+ ]
@@ -0,0 +1,92 @@
1
+ """Synthetic fallback routes for API Gateway routing misses."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from http import HTTPStatus
6
+ from typing import TYPE_CHECKING, Callable
7
+
8
+ from modmex_lambda.event_handler import content_types
9
+ from modmex_lambda.event_handler.cors import CORSConfig
10
+ from modmex_lambda.event_handler.exceptions import MethodNotAllowedError, NotFoundError
11
+ from modmex_lambda.event_handler.response import Response
12
+ from modmex_lambda.event_handler.routing import Route
13
+
14
+ if TYPE_CHECKING:
15
+ from modmex_lambda.event_handler.api_gateway import ApiGatewayResolver
16
+
17
+
18
+ class RoutingFallbackHandler:
19
+ def __init__(self, app: ApiGatewayResolver) -> None:
20
+ self.app = app
21
+
22
+ def not_found(self, method: str, path: str) -> Response:
23
+ def not_found_handler() -> Response:
24
+ if self.app._cors_enabled and method == "OPTIONS":
25
+ return self._preflight_response(self.app._router.cors_methods)
26
+
27
+ custom_response = self._custom_response(NotFoundError)
28
+ if custom_response is not None:
29
+ return custom_response
30
+
31
+ return self._not_found_response()
32
+
33
+ return self._call_synthetic_route(method=method, path=path, handler=not_found_handler)
34
+
35
+ def method_not_allowed(self, method: str, path: str, allowed_methods: set[str]) -> Response:
36
+ def method_not_allowed_handler() -> Response:
37
+ if self.app._cors_enabled and method == "OPTIONS":
38
+ return self._preflight_response(allowed_methods | {"OPTIONS"})
39
+
40
+ custom_response = self._custom_response(MethodNotAllowedError)
41
+ if custom_response is not None:
42
+ return custom_response
43
+
44
+ return self._method_not_allowed_response(allowed_methods)
45
+
46
+ return self._call_synthetic_route(method=method, path=path, handler=method_not_allowed_handler)
47
+
48
+ def _call_synthetic_route(self, method: str, path: str, handler: Callable[[], Response]) -> Response:
49
+ route = Route(
50
+ method=method,
51
+ path=path,
52
+ pattern=self.app._router.compile_regex(r"^.*$"),
53
+ handler=handler,
54
+ cors=self.app._cors_enabled,
55
+ )
56
+ self.app.append_context(_route=route, _path=path)
57
+ return self.app._call_route(route, route_arguments={})
58
+
59
+ def _preflight_response(self, allowed_methods: set[str]) -> Response:
60
+ return Response(
61
+ status_code=HTTPStatus.NO_CONTENT.value,
62
+ content_type=None,
63
+ headers={"Access-Control-Allow-Methods": CORSConfig.build_allow_methods(allowed_methods)},
64
+ body="",
65
+ )
66
+
67
+ def _custom_response(self, exc_class: type[Exception]) -> Response | None:
68
+ handler = self.app._router.exception_handler_manager.lookup_exception_handler(exc_class)
69
+ if handler is None:
70
+ return None
71
+ return handler(exc_class())
72
+
73
+ def _not_found_response(self) -> Response:
74
+ return Response(
75
+ status_code=HTTPStatus.NOT_FOUND.value,
76
+ content_type=content_types.APPLICATION_JSON,
77
+ body={"statusCode": HTTPStatus.NOT_FOUND.value, "message": "Not Found"},
78
+ )
79
+
80
+ def _method_not_allowed_response(self, allowed_methods: set[str]) -> Response:
81
+ return Response(
82
+ status_code=HTTPStatus.METHOD_NOT_ALLOWED.value,
83
+ content_type=content_types.APPLICATION_JSON,
84
+ headers={"Allow": CORSConfig.build_allow_methods(allowed_methods)},
85
+ body={
86
+ "statusCode": HTTPStatus.METHOD_NOT_ALLOWED.value,
87
+ "message": "Method Not Allowed",
88
+ },
89
+ )
90
+
91
+
92
+ __all__ = ["RoutingFallbackHandler"]
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, TypeVar, Union
4
+
5
+ from abc import ABC
6
+ from modmex_lambda.data_classes.api_gateway_proxy_event import APIGatewayProxyEvent, APIGatewayProxyEventV2
7
+ from modmex_lambda.event_handler.request import Request
8
+ from modmex_lambda.event_handler.response import Response
9
+
10
+
11
+ class IApiGatewayResolver(ABC):
12
+ context: dict[str, Any]
13
+ current_event: Union[APIGatewayProxyEvent, APIGatewayProxyEventV2]
14
+ dependency_overrides: dict[Callable[..., Any], Callable[..., Any]]
15
+ _dependency_middleware: Callable[..., Any]
16
+
17
+ @property
18
+ def request(self) -> Request:
19
+ raise NotImplementedError
20
+
21
+ def resolve(self, event: dict[str, Any], context: object) -> dict[str, Any]:
22
+ raise NotImplementedError()
23
+
24
+ def append_context(self, **kwargs: Any) -> None:
25
+ raise NotImplementedError
26
+
27
+ def _to_response(self, result: dict | tuple | Response) -> Response:
28
+ raise NotImplementedError
29
+
30
+
31
+ EventHandlerInstance = TypeVar("EventHandlerInstance", bound=IApiGatewayResolver)
@@ -0,0 +1,53 @@
1
+ """event source decorator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from functools import wraps
6
+ from typing import Any, Callable, TYPE_CHECKING
7
+
8
+ from modmex_lambda.parser import parse
9
+
10
+ if TYPE_CHECKING:
11
+ from .validation import Validator
12
+
13
+
14
+ def event_source(
15
+ *,
16
+ data_class: type,
17
+ model: Any | None = None,
18
+ source: str | None = None,
19
+ validator: "Validator | None" = None,
20
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
21
+ """Wrap raw Lambda event with a data class and optionally parse nested payloads."""
22
+
23
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
24
+ @wraps(func)
25
+ def wrapper(event: dict[str, Any], context: object, *args: Any, **kwargs: Any) -> Any:
26
+ wrapped_event = data_class(event)
27
+ if model is not None and source:
28
+ _parse_nested_payloads(wrapped_event, model=model, source=source, validator=validator)
29
+ return func(wrapped_event, context, *args, **kwargs)
30
+
31
+ return wrapper
32
+
33
+ return decorator
34
+
35
+
36
+ def _parse_nested_payloads(wrapped_event: Any, *, model: Any, source: str, validator: "Validator | None") -> None:
37
+ parsed_attr = f"parsed_{source}"
38
+
39
+ records = getattr(wrapped_event, "records", None)
40
+ if records is not None:
41
+ for record in records:
42
+ value = getattr(record, source)
43
+ parsed = parse(event=value, model=model, validator=validator)
44
+ setattr(record, parsed_attr, parsed)
45
+ return
46
+
47
+ if hasattr(wrapped_event, source):
48
+ value = getattr(wrapped_event, source)
49
+ parsed = parse(event=value, model=model, validator=validator)
50
+ setattr(wrapped_event, parsed_attr, parsed)
51
+
52
+
53
+ __all__ = ["event_source"]
@@ -0,0 +1,3 @@
1
+ """Compatibility re-export for event handler exceptions."""
2
+
3
+ from modmex_lambda.event_handler.exceptions import * # noqa: F403