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,62 @@
1
+ """Public API for modmex-lambda."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib import import_module
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ if TYPE_CHECKING:
9
+ from .event_handler import (
10
+ ApiGatewayHttpResolver,
11
+ ApiGatewayRestResolver,
12
+ Response,
13
+ )
14
+ from .event_handler.request import Request
15
+ from .event_sources import event_source
16
+ from .event_handler.dependencies.depends import Depends
17
+ from .logging import Logger
18
+ from .parser import event_parser, parse
19
+ from .validation import ModmexValidator, ValidationError
20
+
21
+ _EXPORTS = {
22
+ "ApiGatewayHttpResolver": ("modmex_lambda.event_handler", "ApiGatewayHttpResolver"),
23
+ "ApiGatewayRestResolver": ("modmex_lambda.event_handler", "ApiGatewayRestResolver"),
24
+ "Request": ("modmex_lambda.event_handler.request", "Request"),
25
+ "Response": ("modmex_lambda.event_handler", "Response"),
26
+ "parse": ("modmex_lambda.parser", "parse"),
27
+ "event_parser": ("modmex_lambda.parser", "event_parser"),
28
+ "event_source": ("modmex_lambda.event_sources", "event_source"),
29
+ "Depends": ("modmex_lambda.event_handler.dependencies.depends", "Depends"),
30
+ "Logger": ("modmex_lambda.logging", "Logger"),
31
+ "ModmexValidator": ("modmex_lambda.validation", "ModmexValidator"),
32
+ "ValidationError": ("modmex_lambda.validation", "ValidationError"),
33
+ }
34
+
35
+ __all__ = [
36
+ "ApiGatewayHttpResolver",
37
+ "ApiGatewayRestResolver",
38
+ "Request",
39
+ "Response",
40
+ "parse",
41
+ "event_parser",
42
+ "event_source",
43
+ "Depends",
44
+ "Logger",
45
+ "ModmexValidator",
46
+ "ValidationError",
47
+ ]
48
+
49
+
50
+ def __getattr__(name: str) -> Any:
51
+ target = _EXPORTS.get(name)
52
+ if target is None:
53
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
54
+
55
+ module_name, attr = target
56
+ value = getattr(import_module(module_name), attr)
57
+ globals()[name] = value
58
+ return value
59
+
60
+
61
+ def __dir__() -> list[str]:
62
+ return sorted([*globals().keys(), *__all__])
@@ -0,0 +1,49 @@
1
+ from modmex_lambda.data_classes.api_gateway_proxy_event import (
2
+ APIGatewayProxyEvent,
3
+ APIGatewayProxyEventV2,
4
+ )
5
+ from modmex_lambda.data_classes.api_gateway_authorizer_event import APIGatewayAuthorizerEvent
6
+ from modmex_lambda.data_classes.api_gateway_websocket_event import APIGatewayWebSocketEvent
7
+ from modmex_lambda.data_classes.common import DictWrapper
8
+ from modmex_lambda.data_classes.cognito_user_pool_event import (
9
+ CognitoUserPoolEvent,
10
+ CreateAuthChallengeTriggerEvent,
11
+ CustomEmailSenderTriggerEvent,
12
+ CustomMessageTriggerEvent,
13
+ CustomSMSSenderTriggerEvent,
14
+ DefineAuthChallengeTriggerEvent,
15
+ PostAuthenticationTriggerEvent,
16
+ PostConfirmationTriggerEvent,
17
+ PreAuthenticationTriggerEvent,
18
+ PreSignUpTriggerEvent,
19
+ PreTokenGenerationTriggerEvent,
20
+ PreTokenGenerationV2TriggerEvent,
21
+ PreTokenGenerationV3TriggerEvent,
22
+ UserMigrationTriggerEvent,
23
+ VerifyAuthChallengeResponseTriggerEvent,
24
+ )
25
+
26
+ __all__ = [
27
+ "DictWrapper",
28
+ "APIGatewayProxyEvent",
29
+ "APIGatewayProxyEventV2",
30
+ "APIGatewayRestEvent",
31
+ "APIGatewayHttpEvent",
32
+ "APIGatewayAuthorizerEvent",
33
+ "APIGatewayWebSocketEvent",
34
+ "CognitoUserPoolEvent",
35
+ "PreSignUpTriggerEvent",
36
+ "PostConfirmationTriggerEvent",
37
+ "UserMigrationTriggerEvent",
38
+ "CustomMessageTriggerEvent",
39
+ "PreAuthenticationTriggerEvent",
40
+ "PostAuthenticationTriggerEvent",
41
+ "PreTokenGenerationTriggerEvent",
42
+ "PreTokenGenerationV2TriggerEvent",
43
+ "PreTokenGenerationV3TriggerEvent",
44
+ "DefineAuthChallengeTriggerEvent",
45
+ "CreateAuthChallengeTriggerEvent",
46
+ "VerifyAuthChallengeResponseTriggerEvent",
47
+ "CustomEmailSenderTriggerEvent",
48
+ "CustomSMSSenderTriggerEvent",
49
+ ]
@@ -0,0 +1,38 @@
1
+ """API Gateway Lambda authorizer event data class."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from modmex_lambda.data_classes.common import DictWrapper
8
+
9
+
10
+ class APIGatewayAuthorizerEvent(DictWrapper):
11
+ @property
12
+ def type(self) -> str:
13
+ return str(self.get("type") or "")
14
+
15
+ @property
16
+ def method_arn(self) -> str:
17
+ return str(self.get("methodArn") or "")
18
+
19
+ @property
20
+ def authorization_token(self) -> str | None:
21
+ value = self.get("authorizationToken")
22
+ return None if value is None else str(value)
23
+
24
+ @property
25
+ def headers(self) -> dict[str, Any]:
26
+ return dict(self.get("headers") or {})
27
+
28
+ @property
29
+ def query_string_parameters(self) -> dict[str, Any]:
30
+ return dict(self.get("queryStringParameters") or {})
31
+
32
+ @property
33
+ def path_parameters(self) -> dict[str, Any]:
34
+ return dict(self.get("pathParameters") or {})
35
+
36
+ @property
37
+ def request_context(self) -> dict[str, Any]:
38
+ return dict(self.get("requestContext") or {})
@@ -0,0 +1,328 @@
1
+ """API Gateway event source data classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ from typing import Any
7
+ from functools import cached_property
8
+ from modmex_lambda.data_classes.common import (
9
+ DictWrapper,
10
+ BaseProxyEvent,
11
+ CaseInsensitiveDict,
12
+ BaseRequestContext,
13
+ BaseRequestContextV2,
14
+ )
15
+ from modmex_lambda.shared.headers_serializer import HttpApiHeadersSerializer, MultiValueHeadersSerializer, BaseHeadersSerializer
16
+
17
+
18
+ class APIGatewayEventAuthorizer(DictWrapper):
19
+ @property
20
+ def claims(self) -> dict[str, Any]:
21
+ return self.get("claims") or {} # key might exist but can be `null`
22
+
23
+ @property
24
+ def scopes(self) -> list[str]:
25
+ return self.get("scopes") or [] # key might exist but can be `null`
26
+
27
+ @property
28
+ def principal_id(self) -> str:
29
+ """The principal user identification associated with the token sent by the client and returned from an
30
+ API Gateway Lambda authorizer (formerly known as a custom authorizer)"""
31
+ return self.get("principalId") or "" # key might exist but can be `null`
32
+
33
+ @property
34
+ def integration_latency(self) -> int | None:
35
+ """The authorizer latency in ms."""
36
+ return self.get("integrationLatency")
37
+
38
+ def get_context(self) -> dict[str, Any]:
39
+ """Retrieve the authorization context details injected by a Lambda Authorizer.
40
+
41
+ Example
42
+ --------
43
+
44
+ ```python
45
+ ctx: dict = request_context.authorizer.get_context()
46
+
47
+ tenant_id = ctx.get("tenant_id")
48
+ ```
49
+
50
+ Returns:
51
+ --------
52
+ dict[str, Any]
53
+ A dictionary containing Lambda authorization context details.
54
+ """
55
+ return self._data
56
+
57
+
58
+ class APIGatewayEventRequestContext(BaseRequestContext):
59
+ @property
60
+ def connected_at(self) -> int | None:
61
+ """The Epoch-formatted connection time. (WebSocket API)"""
62
+ return self.get("connectedAt")
63
+
64
+ @property
65
+ def connection_id(self) -> str | None:
66
+ """A unique ID for the connection that can be used to make a callback to the client. (WebSocket API)"""
67
+ return self.get("connectionId")
68
+
69
+ @property
70
+ def event_type(self) -> str | None:
71
+ """The event type: `CONNECT`, `MESSAGE`, or `DISCONNECT`. (WebSocket API)"""
72
+ return self.get("eventType")
73
+
74
+ @property
75
+ def message_direction(self) -> str | None:
76
+ """Message direction (WebSocket API)"""
77
+ return self.get("messageDirection")
78
+
79
+ @property
80
+ def message_id(self) -> str | None:
81
+ """A unique server-side ID for a message. Available only when the `eventType` is `MESSAGE`."""
82
+ return self.get("messageId")
83
+
84
+ @property
85
+ def operation_name(self) -> str | None:
86
+ """The name of the operation being performed"""
87
+ return self.get("operationName")
88
+
89
+ @property
90
+ def route_key(self) -> str | None:
91
+ """The selected route key."""
92
+ return self.get("routeKey")
93
+
94
+ @property
95
+ def authorizer(self) -> APIGatewayEventAuthorizer:
96
+ return APIGatewayEventAuthorizer(self.get("authorizer") or {})
97
+
98
+
99
+
100
+
101
+ class APIGatewayProxyEvent(BaseProxyEvent):
102
+
103
+ @property
104
+ def version(self) -> str:
105
+ return self.get("version") or "1.0"
106
+
107
+ @property
108
+ def resource(self) -> str:
109
+ return self["resource"]
110
+
111
+ @property
112
+ def multi_value_headers(self) -> dict[str, list[str]]:
113
+ return CaseInsensitiveDict(self.get("multiValueHeaders"))
114
+
115
+ @property
116
+ def resolved_query_string_parameters(self) -> dict[str, list[str]]:
117
+ multi_value = self.multi_value_query_string_parameters
118
+ single_value = super().resolved_query_string_parameters
119
+
120
+ if not multi_value:
121
+ return single_value
122
+
123
+ if not single_value:
124
+ return multi_value
125
+
126
+ # Merge both: multi_value takes precedence, single_value fills missing keys
127
+ return {**single_value, **multi_value}
128
+
129
+ @property
130
+ def resolved_headers_field(self) -> dict[str, Any]:
131
+ return self.multi_value_headers or self.headers
132
+
133
+ @property
134
+ def request_context(self) -> APIGatewayEventRequestContext:
135
+ return APIGatewayEventRequestContext(self["requestContext"])
136
+
137
+ @property
138
+ def path_parameters(self) -> dict[str, str]:
139
+ return self.get("pathParameters") or {}
140
+
141
+ @property
142
+ def stage_variables(self) -> dict[str, str]:
143
+ return self.get("stageVariables") or {}
144
+
145
+ def header_serializer(self) -> BaseHeadersSerializer:
146
+ return MultiValueHeadersSerializer()
147
+
148
+
149
+
150
+ class RequestContextV2AuthorizerIam(DictWrapper):
151
+ @property
152
+ def access_key(self) -> str:
153
+ """The IAM user access key associated with the request."""
154
+ return self.get("accessKey") or "" # key might exist but can be `null`
155
+
156
+ @property
157
+ def account_id(self) -> str:
158
+ """The AWS account ID associated with the request."""
159
+ return self.get("accountId") or "" # key might exist but can be `null`
160
+
161
+ @property
162
+ def caller_id(self) -> str:
163
+ """The principal identifier of the caller making the request."""
164
+ return self.get("callerId") or "" # key might exist but can be `null`
165
+
166
+ def _cognito_identity(self) -> dict:
167
+ return self.get("cognitoIdentity") or {} # not available in FunctionURL; key might exist but can be `null`
168
+
169
+ @property
170
+ def cognito_amr(self) -> list[str]:
171
+ """This represents how the user was authenticated.
172
+ AMR stands for Authentication Methods References as per the openid spec"""
173
+ return self._cognito_identity().get("amr", [])
174
+
175
+ @property
176
+ def cognito_identity_id(self) -> str:
177
+ """The Amazon Cognito identity ID of the caller making the request.
178
+ Available only if the request was signed with Amazon Cognito credentials."""
179
+ return self._cognito_identity().get("identityId", "")
180
+
181
+ @property
182
+ def cognito_identity_pool_id(self) -> str:
183
+ """The Amazon Cognito identity pool ID of the caller making the request.
184
+ Available only if the request was signed with Amazon Cognito credentials."""
185
+ return self._cognito_identity().get("identityPoolId") or "" # key might exist but can be `null`
186
+
187
+ @property
188
+ def principal_org_id(self) -> str:
189
+ """The AWS organization ID."""
190
+ return self.get("principalOrgId") or "" # key might exist but can be `null`
191
+
192
+ @property
193
+ def user_arn(self) -> str:
194
+ """The Amazon Resource Name (ARN) of the effective user identified after authentication."""
195
+ return self.get("userArn") or "" # key might exist but can be `null`
196
+
197
+ @property
198
+ def user_id(self) -> str:
199
+ """The IAM user ID of the effective user identified after authentication."""
200
+ return self.get("userId") or "" # key might exist but can be `null`
201
+
202
+
203
+ class RequestContextV2Authorizer(DictWrapper):
204
+ @property
205
+ def jwt_claim(self) -> dict[str, Any]:
206
+ jwt = self.get("jwt") or {} # not available in FunctionURL; key might exist but can be `null`
207
+ return jwt.get("claims") or {} # key might exist but can be `null`
208
+
209
+ @property
210
+ def jwt_scopes(self) -> list[str]:
211
+ jwt = self.get("jwt") or {} # not available in FunctionURL; key might exist but can be `null`
212
+ return jwt.get("scopes", [])
213
+
214
+ @property
215
+ def get_lambda(self) -> dict[str, Any]:
216
+ """Lambda authorization context details"""
217
+ return self.get("lambda") or {} # key might exist but can be `null`
218
+
219
+ def get_context(self) -> dict[str, Any]:
220
+ """Retrieve the authorization context details injected by a Lambda Authorizer.
221
+
222
+ Example
223
+ --------
224
+
225
+ ```python
226
+ ctx: dict = request_context.authorizer.get_context()
227
+
228
+ tenant_id = ctx.get("tenant_id")
229
+ ```
230
+
231
+ Returns:
232
+ --------
233
+ dict[str, Any]
234
+ A dictionary containing Lambda authorization context details.
235
+ """
236
+ return self.get_lambda
237
+
238
+ @property
239
+ def iam(self) -> RequestContextV2AuthorizerIam:
240
+ """IAM authorization details used for making the request."""
241
+ iam = self.get("iam") or {} # key might exist but can be `null`
242
+ return RequestContextV2AuthorizerIam(iam)
243
+
244
+
245
+ class RequestContextV2(BaseRequestContextV2):
246
+ @property
247
+ def authorizer(self) -> RequestContextV2Authorizer:
248
+ return RequestContextV2Authorizer(self.get("authorizer") or {})
249
+
250
+
251
+ class APIGatewayProxyEventV2(BaseProxyEvent):
252
+ """AWS Lambda proxy V2 event
253
+
254
+ Notes:
255
+ -----
256
+ Format 2.0 doesn't have multiValueHeaders or multiValueQueryStringParameters fields. Duplicate headers
257
+ are combined with commas and included in the headers field. Duplicate query strings are combined with
258
+ commas and included in the queryStringParameters field.
259
+
260
+ Format 2.0 includes a new cookies field. All cookie headers in the request are combined with commas and
261
+ added to the cookies field. In the response to the client, each cookie becomes a set-cookie header.
262
+
263
+ Documentation:
264
+ --------------
265
+ - https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
266
+ """
267
+
268
+ @property
269
+ def version(self) -> str:
270
+ return self["version"]
271
+
272
+ @property
273
+ def route_key(self) -> str:
274
+ return self["routeKey"]
275
+
276
+ @property
277
+ def raw_path(self) -> str:
278
+ return self["rawPath"]
279
+
280
+ @property
281
+ def raw_query_string(self) -> str:
282
+ return self["rawQueryString"]
283
+
284
+ @property
285
+ def cookies(self) -> list[str]:
286
+ return self.get("cookies") or []
287
+
288
+ @property
289
+ def resolved_cookies_field(self) -> dict[str, str]:
290
+ """
291
+ Parse cookies from the dedicated ``cookies`` field in API Gateway HTTP API v2 format.
292
+
293
+ The ``cookies`` field contains a list of strings like ``["session=abc", "theme=dark"]``.
294
+ """
295
+ from modmex_lambda.data_classes.common import _parse_cookie_string
296
+
297
+ return _parse_cookie_string("; ".join(self.cookies))
298
+
299
+ @property
300
+ def request_context(self) -> RequestContextV2:
301
+ return RequestContextV2(self["requestContext"])
302
+
303
+ @property
304
+ def path_parameters(self) -> dict[str, str]:
305
+ return self.get("pathParameters") or {}
306
+
307
+ @property
308
+ def stage_variables(self) -> dict[str, str]:
309
+ return self.get("stageVariables") or {}
310
+
311
+ @property
312
+ def path(self) -> str:
313
+ stage = self.request_context.stage
314
+ if stage != "$default":
315
+ return self.raw_path[len("/" + stage) :]
316
+ return self.raw_path
317
+
318
+ @property
319
+ def http_method(self) -> str:
320
+ """The HTTP method used. Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT."""
321
+ return self.request_context.http.method
322
+
323
+ def header_serializer(self):
324
+ return HttpApiHeadersSerializer()
325
+
326
+ @cached_property
327
+ def resolved_headers_field(self) -> dict[str, Any]:
328
+ return CaseInsensitiveDict((k, v.split(",") if "," in v else v) for k, v in self.headers.items())
@@ -0,0 +1,40 @@
1
+ """API Gateway WebSocket event data class."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from modmex_lambda.data_classes.common import DictWrapper
9
+
10
+
11
+ class APIGatewayWebSocketEvent(DictWrapper):
12
+ @property
13
+ def request_context(self) -> dict[str, Any]:
14
+ return dict(self.get("requestContext") or {})
15
+
16
+ @property
17
+ def route_key(self) -> str:
18
+ return str((self.request_context or {}).get("routeKey") or "")
19
+
20
+ @property
21
+ def event_type(self) -> str:
22
+ return str((self.request_context or {}).get("eventType") or "")
23
+
24
+ @property
25
+ def connection_id(self) -> str:
26
+ return str((self.request_context or {}).get("connectionId") or "")
27
+
28
+ @property
29
+ def body(self) -> Any:
30
+ return self.get("body")
31
+
32
+ @property
33
+ def json_body(self) -> Any:
34
+ body = self.body
35
+ if isinstance(body, str):
36
+ try:
37
+ return json.loads(body)
38
+ except json.JSONDecodeError:
39
+ return body
40
+ return body