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.
- modmex_lambda/__init__.py +62 -0
- modmex_lambda/data_classes/__init__.py +49 -0
- modmex_lambda/data_classes/api_gateway_authorizer_event.py +38 -0
- modmex_lambda/data_classes/api_gateway_proxy_event.py +328 -0
- modmex_lambda/data_classes/api_gateway_websocket_event.py +40 -0
- modmex_lambda/data_classes/cognito_user_pool_event.py +599 -0
- modmex_lambda/data_classes/common.py +441 -0
- modmex_lambda/event_handler/__init__.py +45 -0
- modmex_lambda/event_handler/api_gateway.py +331 -0
- modmex_lambda/event_handler/constants.py +3 -0
- modmex_lambda/event_handler/content_types.py +13 -0
- modmex_lambda/event_handler/cors.py +97 -0
- modmex_lambda/event_handler/dependencies/__init__.py +0 -0
- modmex_lambda/event_handler/dependencies/compat.py +231 -0
- modmex_lambda/event_handler/dependencies/dependant.py +279 -0
- modmex_lambda/event_handler/dependencies/dependency_middleware.py +423 -0
- modmex_lambda/event_handler/dependencies/depends.py +184 -0
- modmex_lambda/event_handler/dependencies/params.py +317 -0
- modmex_lambda/event_handler/dependencies/types.py +14 -0
- modmex_lambda/event_handler/exception_handler.py +70 -0
- modmex_lambda/event_handler/exceptions.py +72 -0
- modmex_lambda/event_handler/gateway_response.py +96 -0
- modmex_lambda/event_handler/middlewares.py +33 -0
- modmex_lambda/event_handler/params.py +44 -0
- modmex_lambda/event_handler/request.py +70 -0
- modmex_lambda/event_handler/response.py +60 -0
- modmex_lambda/event_handler/routing.py +507 -0
- modmex_lambda/event_handler/routing_fallbacks.py +92 -0
- modmex_lambda/event_handler/types.py +31 -0
- modmex_lambda/event_sources.py +53 -0
- modmex_lambda/exceptions.py +3 -0
- modmex_lambda/logging.py +99 -0
- modmex_lambda/params.py +3 -0
- modmex_lambda/parser.py +47 -0
- modmex_lambda/request.py +3 -0
- modmex_lambda/resolver.py +3 -0
- modmex_lambda/response.py +3 -0
- modmex_lambda/routing.py +3 -0
- modmex_lambda/shared/__init__.py +0 -0
- modmex_lambda/shared/cookies.py +84 -0
- modmex_lambda/shared/headers_serializer.py +65 -0
- modmex_lambda/shared/json_encoder.py +53 -0
- modmex_lambda/shared/types.py +4 -0
- modmex_lambda/validation.py +178 -0
- modmex_lambda-0.1.0.dist-info/METADATA +375 -0
- modmex_lambda-0.1.0.dist-info/RECORD +48 -0
- modmex_lambda-0.1.0.dist-info/WHEEL +4 -0
- 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
|
+
|