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,441 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import json
5
+ from functools import cached_property
6
+ from collections.abc import Callable, Iterator, Mapping
7
+ from typing import Any, overload
8
+ from modmex_lambda.shared.headers_serializer import BaseHeadersSerializer
9
+
10
+
11
+ def _parse_cookie_string(cookie_string: str) -> dict[str, str]:
12
+ """Parse a cookie string (``key=value; key2=value2``) into a dict."""
13
+ cookies: dict[str, str] = {}
14
+ for segment in cookie_string.split(";"):
15
+ stripped = segment.strip()
16
+ if "=" in stripped:
17
+ name, _, value = stripped.partition("=")
18
+ cookies[name.strip()] = value.strip()
19
+ return cookies
20
+
21
+
22
+ class CaseInsensitiveDict(dict):
23
+ """Case insensitive dict implementation. Assumes string keys only."""
24
+
25
+ def __init__(self, data=None, **kwargs):
26
+ super().__init__()
27
+ self.update(data, **kwargs)
28
+
29
+ def get(self, k, default=None):
30
+ return super().get(k.lower(), default)
31
+
32
+ def pop(self, k, *args):
33
+ return super().pop(k.lower(), *args)
34
+
35
+ def setdefault(self, k, default=None):
36
+ return super().setdefault(k.lower(), default)
37
+
38
+ def update(self, data=None, **kwargs):
39
+ if data is not None:
40
+ if isinstance(data, Mapping):
41
+ data = data.items()
42
+ super().update((k.lower(), v) for k, v in data)
43
+ super().update((k.lower(), v) for k, v in kwargs)
44
+
45
+ def __contains__(self, k):
46
+ return super().__contains__(k.lower())
47
+
48
+ def __delitem__(self, k):
49
+ super().__delitem__(k.lower())
50
+
51
+ def __eq__(self, other):
52
+ if not isinstance(other, Mapping):
53
+ return False
54
+ if not isinstance(other, CaseInsensitiveDict):
55
+ other = CaseInsensitiveDict(other)
56
+ return super().__eq__(other)
57
+
58
+ def __getitem__(self, k):
59
+ return super().__getitem__(k.lower())
60
+
61
+ def __setitem__(self, k, v):
62
+ super().__setitem__(k.lower(), v)
63
+
64
+ def __hash__(self):
65
+ # Convert the dictionary to a frozenset of tuples (key, value)
66
+ # where all keys are lowercase
67
+ items = frozenset((k.lower(), v) for k, v in self.items())
68
+ return hash(items)
69
+
70
+
71
+ class DictWrapper(Mapping[str, Any]):
72
+ """read-only dict wrapper with helper accessors."""
73
+
74
+ def __init__(self, data: dict[str, Any], json_deserializer: Callable[[str], Any] | None = None) -> None:
75
+ self._data = data
76
+ self._json_deserializer = json_deserializer or json.loads
77
+
78
+ @property
79
+ def raw_event(self) -> dict[str, Any]:
80
+ return self._data
81
+
82
+ def __getitem__(self, key: str) -> Any:
83
+ return self._data[key]
84
+
85
+ def __iter__(self) -> Iterator[str]:
86
+ return iter(self._data)
87
+
88
+ def __len__(self) -> int:
89
+ return len(self._data)
90
+
91
+ def get(self, key: str, default: Any = None) -> Any:
92
+ return self._data.get(key, default)
93
+
94
+
95
+
96
+ class BaseProxyEvent(DictWrapper):
97
+
98
+ @property
99
+ def headers(self) -> dict[str, str]:
100
+ return CaseInsensitiveDict(self.get("headers"))
101
+
102
+ @property
103
+ def query_string_parameters(self) -> dict[str, str]:
104
+ return self.get("queryStringParameters", {}) or {}
105
+
106
+ @property
107
+ def multi_value_query_string_parameters(self) -> dict[str, list[str]]:
108
+ return self.get("multiValueQueryStringParameters", {}) or {}
109
+
110
+ @property
111
+ def resolved_query_string_parameters(self) -> dict[str, list[str]]:
112
+ return {k: v.split(",") for k, v in self.query_string_parameters.items()}
113
+
114
+ @property
115
+ def resolved_headers_field(self) -> dict[str, str]:
116
+ return self.headers
117
+
118
+ @property
119
+ def is_base64_encoded(self) -> bool:
120
+ return self.get("isBase64Encoded", False)
121
+
122
+ @property
123
+ def body(self) -> str | None:
124
+ """ return body of the request as string"""
125
+ return self.get("body")
126
+
127
+ @cached_property
128
+ def json_body(self) -> Any:
129
+ """Parses the body as json"""
130
+ if self.decoded_body:
131
+ return self._json_deserializer(self.decoded_body)
132
+ return None
133
+
134
+ @cached_property
135
+ def decoded_body(self) -> str | None:
136
+ """ return body from base64 if encoded, otherwise return raw body"""
137
+ body = self.body
138
+ if self.is_base64_encoded and body:
139
+ return base64.b64decode(body.encode()).decode()
140
+ return body
141
+
142
+
143
+ @property
144
+ def path(self)->str:
145
+ return self['path']
146
+
147
+ @property
148
+ def http_method(self)->str:
149
+ return self['httpMethod']
150
+
151
+
152
+ @overload
153
+ def get_query_string_value(self, name: str, default_value: str) -> str: ...
154
+
155
+ @overload
156
+ def get_query_string_value(self, name: str, default_value: str | None = None) -> str | None: ...
157
+
158
+ def get_query_string_value(self, name: str, default_value: str | None = None) -> str | None:
159
+ """Get query string value by name"""
160
+ return self.query_string_parameters.get(name, default_value)
161
+
162
+ def get_multi_value_query_string_values(self, name: str, default_values: list[str] | None = None) -> list[str]:
163
+ """Get multi value query string values by name"""
164
+ default = default_values or []
165
+ return self.multi_value_query_string_parameters.get(name, default)
166
+
167
+ def header_serializer(self) -> BaseHeadersSerializer:
168
+ raise NotImplementedError()
169
+
170
+
171
+ class RequestContextClientCert(DictWrapper):
172
+ @property
173
+ def client_cert_pem(self) -> str:
174
+ """Client certificate pem"""
175
+ return self["clientCertPem"]
176
+
177
+ @property
178
+ def issuer_dn(self) -> str:
179
+ """Issuer Distinguished Name"""
180
+ return self["issuerDN"]
181
+
182
+ @property
183
+ def serial_number(self) -> str:
184
+ """Unique serial number for client cert"""
185
+ return self["serialNumber"]
186
+
187
+ @property
188
+ def subject_dn(self) -> str:
189
+ """Subject Distinguished Name"""
190
+ return self["subjectDN"]
191
+
192
+ @property
193
+ def validity_not_after(self) -> str:
194
+ """Date when the cert is no longer valid
195
+
196
+ eg: Aug 5 00:28:21 2120 GMT"""
197
+ return self["validity"]["notAfter"]
198
+
199
+ @property
200
+ def validity_not_before(self) -> str:
201
+ """Cert is not valid before this date
202
+
203
+ eg: Aug 29 00:28:21 2020 GMT"""
204
+ return self["validity"]["notBefore"]
205
+
206
+
207
+ class APIGatewayEventIdentity(DictWrapper):
208
+ @property
209
+ def access_key(self) -> str | None:
210
+ return self.get("accessKey")
211
+
212
+ @property
213
+ def account_id(self) -> str | None:
214
+ """The AWS account ID associated with the request."""
215
+ return self.get("accountId")
216
+
217
+ @property
218
+ def api_key(self) -> str | None:
219
+ """For API methods that require an API key, this variable is the API key associated with the method request.
220
+ For methods that don't require an API key, this variable is null."""
221
+ return self.get("apiKey")
222
+
223
+ @property
224
+ def api_key_id(self) -> str | None:
225
+ """The API key ID associated with an API request that requires an API key."""
226
+ return self.get("apiKeyId")
227
+
228
+ @property
229
+ def caller(self) -> str | None:
230
+ """The principal identifier of the caller making the request."""
231
+ return self.get("caller")
232
+
233
+ @property
234
+ def cognito_authentication_provider(self) -> str | None:
235
+ """A comma-separated list of the Amazon Cognito authentication providers used by the caller
236
+ making the request. Available only if the request was signed with Amazon Cognito credentials."""
237
+ return self.get("cognitoAuthenticationProvider")
238
+
239
+ @property
240
+ def cognito_authentication_type(self) -> str | None:
241
+ """The Amazon Cognito authentication type of the caller making the request.
242
+ Available only if the request was signed with Amazon Cognito credentials."""
243
+ return self.get("cognitoAuthenticationType")
244
+
245
+ @property
246
+ def cognito_identity_id(self) -> str | None:
247
+ """The Amazon Cognito identity ID of the caller making the request.
248
+ Available only if the request was signed with Amazon Cognito credentials."""
249
+ return self.get("cognitoIdentityId")
250
+
251
+ @property
252
+ def cognito_identity_pool_id(self) -> str | None:
253
+ """The Amazon Cognito identity pool ID of the caller making the request.
254
+ Available only if the request was signed with Amazon Cognito credentials."""
255
+ return self.get("cognitoIdentityPoolId")
256
+
257
+ @property
258
+ def principal_org_id(self) -> str | None:
259
+ """The AWS organization ID."""
260
+ return self.get("principalOrgId")
261
+
262
+ @property
263
+ def source_ip(self) -> str:
264
+ """The source IP address of the TCP connection making the request to API Gateway."""
265
+ return self["sourceIp"]
266
+
267
+ @property
268
+ def user(self) -> str | None:
269
+ """The principal identifier of the user making the request."""
270
+ return self.get("user")
271
+
272
+ @property
273
+ def user_agent(self) -> str | None:
274
+ """The User Agent of the API caller."""
275
+ return self.get("userAgent")
276
+
277
+ @property
278
+ def user_arn(self) -> str | None:
279
+ """The Amazon Resource Name (ARN) of the effective user identified after authentication."""
280
+ return self.get("userArn")
281
+
282
+ @property
283
+ def client_cert(self) -> RequestContextClientCert | None:
284
+ client_cert = self.get("clientCert")
285
+ return None if client_cert is None else RequestContextClientCert(client_cert)
286
+
287
+
288
+
289
+ class BaseRequestContext(DictWrapper):
290
+ @property
291
+ def account_id(self) -> str:
292
+ """The AWS account ID associated with the request."""
293
+ return self["accountId"]
294
+
295
+ @property
296
+ def api_id(self) -> str:
297
+ """The identifier API Gateway assigns to your API."""
298
+ return self["apiId"]
299
+
300
+ @property
301
+ def domain_name(self) -> str | None:
302
+ """A domain name"""
303
+ return self.get("domainName")
304
+
305
+ @property
306
+ def domain_prefix(self) -> str | None:
307
+ return self.get("domainPrefix")
308
+
309
+ @property
310
+ def extended_request_id(self) -> str | None:
311
+ """An automatically generated ID for the API call, which contains more useful information
312
+ for debugging/troubleshooting."""
313
+ return self.get("extendedRequestId")
314
+
315
+ @property
316
+ def protocol(self) -> str:
317
+ """The request protocol, for example, HTTP/1.1."""
318
+ return self["protocol"]
319
+
320
+ @property
321
+ def http_method(self) -> str:
322
+ """The HTTP method used. Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT."""
323
+ return self["httpMethod"]
324
+
325
+ @property
326
+ def identity(self) -> APIGatewayEventIdentity:
327
+ return APIGatewayEventIdentity(self["identity"])
328
+
329
+ @property
330
+ def path(self) -> str:
331
+ return self["path"]
332
+
333
+ @property
334
+ def stage(self) -> str:
335
+ """The deployment stage of the API request"""
336
+ return self["stage"]
337
+
338
+ @property
339
+ def request_id(self) -> str:
340
+ """The ID that API Gateway assigns to the API request."""
341
+ return self["requestId"]
342
+
343
+ @property
344
+ def request_time(self) -> str | None:
345
+ """The CLF-formatted request time (dd/MMM/yyyy:HH:mm:ss +-hhmm)"""
346
+ return self.get("requestTime")
347
+
348
+ @property
349
+ def request_time_epoch(self) -> int:
350
+ """The Epoch-formatted request time."""
351
+ return self["requestTimeEpoch"]
352
+
353
+ @property
354
+ def resource_id(self) -> str:
355
+ return self["resourceId"]
356
+
357
+ @property
358
+ def resource_path(self) -> str:
359
+ return self["resourcePath"]
360
+
361
+
362
+ class RequestContextV2Http(DictWrapper):
363
+ @property
364
+ def method(self) -> str:
365
+ return self["method"]
366
+
367
+ @property
368
+ def path(self) -> str:
369
+ return self["path"]
370
+
371
+ @property
372
+ def protocol(self) -> str:
373
+ """The request protocol, for example, HTTP/1.1."""
374
+ return self["protocol"]
375
+
376
+ @property
377
+ def source_ip(self) -> str:
378
+ """The source IP address of the TCP connection making the request to API Gateway."""
379
+ return self["sourceIp"]
380
+
381
+ @property
382
+ def user_agent(self) -> str:
383
+ """The User Agent of the API caller."""
384
+ return self["userAgent"]
385
+
386
+
387
+ class BaseRequestContextV2(DictWrapper):
388
+ @property
389
+ def account_id(self) -> str:
390
+ """The AWS account ID associated with the request."""
391
+ return self["accountId"]
392
+
393
+ @property
394
+ def api_id(self) -> str:
395
+ """The identifier API Gateway assigns to your API."""
396
+ return self["apiId"]
397
+
398
+ @property
399
+ def domain_name(self) -> str:
400
+ """A domain name"""
401
+ return self["domainName"]
402
+
403
+ @property
404
+ def domain_prefix(self) -> str:
405
+ return self["domainPrefix"]
406
+
407
+ @property
408
+ def http(self) -> RequestContextV2Http:
409
+ return RequestContextV2Http(self["http"])
410
+
411
+ @property
412
+ def request_id(self) -> str:
413
+ """The ID that API Gateway assigns to the API request."""
414
+ return self["requestId"]
415
+
416
+ @property
417
+ def route_key(self) -> str:
418
+ """The selected route key."""
419
+ return self["routeKey"]
420
+
421
+ @property
422
+ def stage(self) -> str:
423
+ """The deployment stage of the API request"""
424
+ return self["stage"]
425
+
426
+ @property
427
+ def time(self) -> str:
428
+ """The CLF-formatted request time (dd/MMM/yyyy:HH:mm:ss +-hhmm)."""
429
+ return self["time"]
430
+
431
+ @property
432
+ def time_epoch(self) -> int:
433
+ """The Epoch-formatted request time."""
434
+ return self["timeEpoch"]
435
+
436
+ @property
437
+ def authentication(self) -> RequestContextClientCert | None:
438
+ """Optional when using mutual TLS authentication"""
439
+ authentication = self.get("authentication") or {}
440
+ client_cert = authentication.get("clientCert")
441
+ return None if client_cert is None else RequestContextClientCert(client_cert)
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib import import_module
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ if TYPE_CHECKING:
7
+ from . import content_types
8
+ from .api_gateway import (
9
+ ApiGatewayHttpResolver,
10
+ ApiGatewayRestResolver,
11
+ Response,
12
+ )
13
+ from .dependencies.depends import Depends
14
+
15
+ _EXPORTS = {
16
+ "ApiGatewayHttpResolver": ("modmex_lambda.event_handler.api_gateway", "ApiGatewayHttpResolver"),
17
+ "ApiGatewayRestResolver": ("modmex_lambda.event_handler.api_gateway", "ApiGatewayRestResolver"),
18
+ "Response": ("modmex_lambda.event_handler.api_gateway", "Response"),
19
+ "Depends": ("modmex_lambda.event_handler.dependencies.depends", "Depends"),
20
+ "content_types": ("modmex_lambda.event_handler.content_types", None),
21
+ }
22
+
23
+ __all__ = [
24
+ "ApiGatewayHttpResolver",
25
+ "ApiGatewayRestResolver",
26
+ "Response",
27
+ "Depends",
28
+ "content_types",
29
+ ]
30
+
31
+
32
+ def __getattr__(name: str) -> Any:
33
+ target = _EXPORTS.get(name)
34
+ if target is None:
35
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
36
+
37
+ module_name, attr = target
38
+ module = import_module(module_name)
39
+ value = module if attr is None else getattr(module, attr)
40
+ globals()[name] = value
41
+ return value
42
+
43
+
44
+ def __dir__() -> list[str]:
45
+ return sorted([*globals().keys(), *__all__])