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,96 @@
1
+ """API Gateway proxy response building."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import zlib
7
+ from collections.abc import Callable, Mapping
8
+ from typing import Any
9
+
10
+ from modmex_lambda.data_classes.common import BaseProxyEvent
11
+ from modmex_lambda.event_handler.cors import CORSConfig
12
+ from modmex_lambda.event_handler.response import Response
13
+ from modmex_lambda.event_handler.routing import Route
14
+
15
+
16
+ class GatewayResponseBuilder:
17
+ def __init__(self, response: Response, route: Route | None, json_serializer: Callable[[Any], str]) -> None:
18
+ self.response = response
19
+ self.route = route
20
+ self.json_serializer = json_serializer
21
+
22
+ def serialize(self, event: BaseProxyEvent, cors: CORSConfig | None = None) -> dict[str, Any]:
23
+ if self.response.is_json() and not isinstance(self.response.body, (str, bytes)):
24
+ self.response.body = self.json_serializer(self.response.body)
25
+
26
+ self._handle_route_configuration(event, cors)
27
+
28
+ if isinstance(self.response.body, bytes):
29
+ self.response.base64_encoded = True
30
+ self.response.body = base64.b64encode(self.response.body).decode()
31
+
32
+ return {
33
+ "statusCode": self.response.status_code,
34
+ "body": self.response.body,
35
+ "isBase64Encoded": self.response.base64_encoded,
36
+ **event.header_serializer().serialize(
37
+ headers=self.response.headers,
38
+ cookies=self.response.cookies,
39
+ ),
40
+ }
41
+
42
+ def _handle_route_configuration(self, event: BaseProxyEvent, cors: CORSConfig | None) -> None:
43
+ if self.route is None:
44
+ return
45
+ if self.route.cors:
46
+ self._add_cors(event, cors or CORSConfig())
47
+ if self.route.cache_control:
48
+ self._add_cache_control(self.route.cache_control)
49
+ if self._compression_enabled(
50
+ route_compression=self.route.compress,
51
+ response_compression=self.response.compress,
52
+ event=event,
53
+ ):
54
+ self._compress()
55
+
56
+ def _add_cors(self, event: BaseProxyEvent, cors: CORSConfig) -> None:
57
+ origin_header = self._extract_origin_header(event.resolved_headers_field)
58
+ origin = cors.allowed_origin(origin_header)
59
+ if origin is not None:
60
+ self.response.headers.update(cors.to_dict(origin))
61
+
62
+ def _add_cache_control(self, cache_control: str) -> None:
63
+ self.response.headers["Cache-Control"] = cache_control if self.response.status_code == 200 else "no-cache"
64
+
65
+ def _compress(self) -> None:
66
+ self.response.headers["Content-Encoding"] = "gzip"
67
+ if isinstance(self.response.body, str):
68
+ self.response.body = bytes(self.response.body, "utf-8")
69
+ gzip = zlib.compressobj(9, zlib.DEFLATED, zlib.MAX_WBITS | 16)
70
+ self.response.body = gzip.compress(self.response.body) + gzip.flush()
71
+
72
+ @staticmethod
73
+ def _extract_origin_header(resolved_headers: Mapping[str, Any]) -> str | None:
74
+ resolved_header = resolved_headers.get("origin")
75
+ if isinstance(resolved_header, list):
76
+ return resolved_header[0]
77
+ return resolved_header
78
+
79
+ @staticmethod
80
+ def _compression_enabled(
81
+ route_compression: bool,
82
+ response_compression: bool | None,
83
+ event: BaseProxyEvent,
84
+ ) -> bool:
85
+ encoding = event.resolved_headers_field.get("accept-encoding", "")
86
+ if isinstance(encoding, list):
87
+ encoding = ",".join(encoding)
88
+
89
+ if not isinstance(encoding, str) or "gzip" not in encoding:
90
+ return False
91
+ if response_compression is not None:
92
+ return response_compression
93
+ return route_compression
94
+
95
+
96
+ __all__ = ["GatewayResponseBuilder"]
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Protocol, Generic
5
+
6
+ from modmex_lambda.event_handler.response import Response
7
+ from modmex_lambda.event_handler.types import EventHandlerInstance, IApiGatewayResolver
8
+
9
+ __all__ = ["NextMiddleware", "IMiddleware"]
10
+
11
+
12
+ class NextMiddleware(Protocol):
13
+ def __call__(self, app: IApiGatewayResolver) -> Response:
14
+ ...
15
+
16
+ @property
17
+ def __name__(self) -> str:
18
+ ...
19
+
20
+
21
+ class IMiddleware(ABC, Generic[EventHandlerInstance]):
22
+
23
+ @abstractmethod
24
+ def handler(self, app: EventHandlerInstance, next_middleware: NextMiddleware) -> Response:
25
+ raise NotImplementedError()
26
+
27
+
28
+ @property
29
+ def __name__(self) -> str:
30
+ return self.__class__.__name__
31
+
32
+ def __call__(self, app: EventHandlerInstance, next_middleware: NextMiddleware) -> Response:
33
+ return self.handler(app, next_middleware)
@@ -0,0 +1,44 @@
1
+ """Route parameter binding markers.
2
+
3
+ Use these markers with ``Annotated[T, Body()]`` style annotations.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Generic, TypeVar
9
+
10
+ from modmex import BaseModel
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ class Param(BaseModel, Generic[T]):
16
+ name: str | None = None
17
+
18
+
19
+ class Body(Param[T]):
20
+ pass
21
+
22
+
23
+ class Query(Param[T]):
24
+ pass
25
+
26
+
27
+ class Path(Param[T]):
28
+ pass
29
+
30
+
31
+ class Header(Param[T]):
32
+ pass
33
+
34
+
35
+ class Cookie(Param[T]):
36
+ pass
37
+
38
+
39
+ class RawEvent(Param[T]):
40
+ pass
41
+
42
+
43
+ class LambdaContext(Param[T]):
44
+ pass
@@ -0,0 +1,70 @@
1
+ """API Gateway event normalization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from modmex_lambda.data_classes.common import BaseProxyEvent
8
+
9
+
10
+ class Request:
11
+
12
+ __slots__ = (
13
+ "_route_path",
14
+ "_path_parameters",
15
+ "_current_event",
16
+ "_context",
17
+ )
18
+
19
+ def __init__(
20
+ self,
21
+ route_path: str,
22
+ path_parameters: dict[str, Any],
23
+ current_event: BaseProxyEvent,
24
+ context: dict[str, Any] | None = None,
25
+ )->None:
26
+ self._route_path = route_path
27
+ self._path_parameters = path_parameters
28
+ self._current_event = current_event
29
+ self._context = context if context is not None else {}
30
+
31
+
32
+ @property
33
+ def route(self) -> str:
34
+ return self._route_path
35
+
36
+ @property
37
+ def path_parameters(self) -> dict[str, Any]:
38
+ return self._path_parameters
39
+
40
+ @property
41
+ def method(self) -> str:
42
+ return self._current_event.http_method.upper()
43
+
44
+ @property
45
+ def headers(self) -> dict[str, str]:
46
+ return self._current_event.headers or {}
47
+
48
+ @property
49
+ def query_parameters(self) -> dict[str, str] | None:
50
+ return self._current_event.query_string_parameters
51
+
52
+ @property
53
+ def resolved_event(self) -> BaseProxyEvent:
54
+ return self._current_event
55
+
56
+ @property
57
+ def body(self) -> Any:
58
+ return self._current_event.body
59
+
60
+ @property
61
+ def json_body(self) -> Any:
62
+ return self._current_event.json_body
63
+
64
+ @property
65
+ def current_event(self) -> BaseProxyEvent:
66
+ return self._current_event
67
+
68
+ @property
69
+ def context(self) -> dict[str, Any]:
70
+ return self._context
@@ -0,0 +1,60 @@
1
+ """API Gateway response helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TypeVar, Generic, Mapping
6
+ from modmex_lambda.shared.cookies import Cookie
7
+
8
+
9
+
10
+
11
+ T = TypeVar("T")
12
+
13
+
14
+ class Response(Generic[T]):
15
+ """Response data class that provides greater control over what is returned from the proxy event"""
16
+
17
+ def __init__(
18
+ self,
19
+ status_code: int,
20
+ content_type: str | None = None,
21
+ body: T | None = None,
22
+ headers: Mapping[str, str | list[str]] | None = None,
23
+ cookies: list[Cookie] | None = None,
24
+ compress: bool | None = None,
25
+ ):
26
+ """
27
+
28
+ Parameters
29
+ ----------
30
+ status_code: int
31
+ Http status code, example 200
32
+ content_type: str
33
+ Optionally set the Content-Type header, example "application/json". Note this will be merged into any
34
+ provided http headers
35
+ body: str | bytes | None
36
+ Optionally set the response body. Note: bytes body will be automatically base64 encoded
37
+ headers: Mapping[str, str | list[str]]
38
+ Optionally set specific http headers. Setting "Content-Type" here would override the `content_type` value.
39
+ cookies: list[Cookie]
40
+ Optionally set cookies.
41
+ """
42
+ self.status_code = status_code
43
+ self.body = body
44
+ self.base64_encoded = False
45
+ self.headers: dict[str, str | list[str]] = dict(headers) if headers else {}
46
+ self.cookies = cookies or []
47
+ self.compress = compress
48
+ self.content_type = content_type
49
+ if content_type:
50
+ self.headers.setdefault("Content-Type", content_type)
51
+
52
+ def is_json(self) -> bool:
53
+ """
54
+ Helper method to check if the response content type is JSON
55
+ """
56
+ content_type = self.headers.get("Content-Type", "")
57
+ if isinstance(content_type, list):
58
+ content_type = content_type[0]
59
+ return content_type.startswith("application/json")
60
+