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,184 @@
|
|
|
1
|
+
"""Dependency injection primitives for event handler resolvers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated, Any, Callable, get_args, get_origin, get_type_hints
|
|
6
|
+
|
|
7
|
+
from modmex_lambda.event_handler.dependencies.compat import ModelField
|
|
8
|
+
from modmex_lambda.event_handler.dependencies.types import CacheKey
|
|
9
|
+
from modmex_lambda.event_handler.request import Request
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Dependant:
|
|
13
|
+
"""Route callable metadata used by dependency parsing and request validation."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
*,
|
|
18
|
+
path_params: list[ModelField] | None = None,
|
|
19
|
+
query_params: list[ModelField] | None = None,
|
|
20
|
+
header_params: list[ModelField] | None = None,
|
|
21
|
+
cookie_params: list[ModelField] | None = None,
|
|
22
|
+
body_params: list[ModelField] | None = None,
|
|
23
|
+
return_param: ModelField | None = None,
|
|
24
|
+
name: str | None = None,
|
|
25
|
+
call: Callable[..., Any] | None = None,
|
|
26
|
+
dependencies: list[DependencyParam] | None = None,
|
|
27
|
+
path: str | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
self.path_params = path_params or []
|
|
30
|
+
self.query_params = query_params or []
|
|
31
|
+
self.header_params = header_params or []
|
|
32
|
+
self.cookie_params = cookie_params or []
|
|
33
|
+
self.body_params = body_params or []
|
|
34
|
+
self.return_param = return_param
|
|
35
|
+
self.dependencies = dependencies or []
|
|
36
|
+
self.name = name
|
|
37
|
+
self.call = call
|
|
38
|
+
self.path = path
|
|
39
|
+
self.cache_key: CacheKey = call
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DependencyResolutionError(Exception):
|
|
43
|
+
"""Raised when a dependency cannot be resolved."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Depends:
|
|
47
|
+
def __init__(self, dependency: Callable[..., Any], *, use_cache: bool = True) -> None:
|
|
48
|
+
if not callable(dependency):
|
|
49
|
+
raise DependencyResolutionError(
|
|
50
|
+
f"Depends() requires a callable, got {type(dependency).__name__}: {dependency!r}",
|
|
51
|
+
)
|
|
52
|
+
self.dependency = dependency
|
|
53
|
+
self.use_cache = use_cache
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class _DependencyNode:
|
|
57
|
+
"""Lightweight node in a dependency tree."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, *, param_name: str, depends: Depends, sub_tree: DependencyTree) -> None:
|
|
60
|
+
self.param_name = param_name
|
|
61
|
+
self.depends = depends
|
|
62
|
+
self.dependant = sub_tree
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DependencyTree:
|
|
66
|
+
"""Lightweight dependency tree for call-time dependency resolution."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, *, dependencies: list[_DependencyNode] | None = None) -> None:
|
|
69
|
+
self.dependencies = dependencies or []
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class DependencyParam:
|
|
73
|
+
"""Dependency metadata attached to a route parameter."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, *, param_name: str, depends: Depends, dependant: Dependant) -> None:
|
|
76
|
+
self.param_name = param_name
|
|
77
|
+
self.depends = depends
|
|
78
|
+
self.dependant = dependant
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _get_depends_from_annotation(annotation: Any) -> Depends | None:
|
|
82
|
+
if get_origin(annotation) is Annotated:
|
|
83
|
+
for arg in get_args(annotation)[1:]:
|
|
84
|
+
if isinstance(arg, Depends):
|
|
85
|
+
return arg
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def build_dependency_tree(func: Callable[..., Any]) -> DependencyTree:
|
|
90
|
+
"""Build a dependency tree from Annotated parameters."""
|
|
91
|
+
try:
|
|
92
|
+
hints = get_type_hints(func, include_extras=True)
|
|
93
|
+
except Exception:
|
|
94
|
+
return DependencyTree()
|
|
95
|
+
|
|
96
|
+
dependencies: list[_DependencyNode] = []
|
|
97
|
+
for param_name, annotation in hints.items():
|
|
98
|
+
if param_name == "return":
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
depends = _get_depends_from_annotation(annotation)
|
|
102
|
+
if depends is None:
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
dependencies.append(
|
|
106
|
+
_DependencyNode(
|
|
107
|
+
param_name=param_name,
|
|
108
|
+
depends=depends,
|
|
109
|
+
sub_tree=build_dependency_tree(depends.dependency),
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return DependencyTree(dependencies=dependencies)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def solve_dependencies(
|
|
117
|
+
*,
|
|
118
|
+
dependant: Dependant | DependencyTree,
|
|
119
|
+
request: Request | None = None,
|
|
120
|
+
dependency_overrides: dict[Callable[..., Any], Callable[..., Any]] | None = None,
|
|
121
|
+
dependency_cache: dict[Callable[..., Any], Any] | None = None,
|
|
122
|
+
) -> dict[str, Any]:
|
|
123
|
+
"""Resolve all Depends parameters for a route or lightweight dependency tree."""
|
|
124
|
+
from modmex_lambda.event_handler.request import Request as RequestClass
|
|
125
|
+
|
|
126
|
+
cache = dependency_cache if dependency_cache is not None else {}
|
|
127
|
+
overrides = dependency_overrides or {}
|
|
128
|
+
values: dict[str, Any] = {}
|
|
129
|
+
|
|
130
|
+
for dep in dependant.dependencies:
|
|
131
|
+
dependency = overrides.get(dep.depends.dependency, dep.depends.dependency)
|
|
132
|
+
|
|
133
|
+
if dep.depends.use_cache and dependency in cache:
|
|
134
|
+
values[dep.param_name] = cache[dependency]
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
sub_values = solve_dependencies(
|
|
138
|
+
dependant=dep.dependant,
|
|
139
|
+
request=request,
|
|
140
|
+
dependency_overrides=overrides,
|
|
141
|
+
dependency_cache=cache,
|
|
142
|
+
)
|
|
143
|
+
sub_values.update(_request_injection_values(dependency, request, RequestClass))
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
solved = dependency(**sub_values)
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
dep_name = getattr(dependency, "__name__", repr(dependency))
|
|
149
|
+
raise DependencyResolutionError(
|
|
150
|
+
f"Failed to resolve dependency '{dep_name}' for parameter '{dep.param_name}': {exc}",
|
|
151
|
+
) from exc
|
|
152
|
+
|
|
153
|
+
if dep.depends.use_cache:
|
|
154
|
+
cache[dependency] = solved
|
|
155
|
+
|
|
156
|
+
values[dep.param_name] = solved
|
|
157
|
+
|
|
158
|
+
return values
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _request_injection_values(
|
|
162
|
+
dependency: Callable[..., Any],
|
|
163
|
+
request: Request | None,
|
|
164
|
+
request_class: type[Request],
|
|
165
|
+
) -> dict[str, Request]:
|
|
166
|
+
if request is None:
|
|
167
|
+
return {}
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
hints = get_type_hints(dependency)
|
|
171
|
+
except Exception:
|
|
172
|
+
return {}
|
|
173
|
+
|
|
174
|
+
return {name: request for name, annotation in hints.items() if annotation is request_class}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
__all__ = [
|
|
178
|
+
"Dependant",
|
|
179
|
+
"DependencyParam",
|
|
180
|
+
"Depends",
|
|
181
|
+
"_get_depends_from_annotation",
|
|
182
|
+
"build_dependency_tree",
|
|
183
|
+
"solve_dependencies",
|
|
184
|
+
]
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Literal, Annotated, get_args, get_origin
|
|
6
|
+
|
|
7
|
+
from modmex import BaseModel, FieldInfo, create_model
|
|
8
|
+
|
|
9
|
+
import modmex_lambda.event_handler.params as public_params
|
|
10
|
+
from modmex_lambda.event_handler.dependencies.compat import (
|
|
11
|
+
ModelField,
|
|
12
|
+
Required,
|
|
13
|
+
copy_field_info,
|
|
14
|
+
field_annotation_is_scalar,
|
|
15
|
+
get_annotation_from_field_info,
|
|
16
|
+
lenient_issubclass,
|
|
17
|
+
)
|
|
18
|
+
from modmex_lambda.event_handler.dependencies.depends import Dependant
|
|
19
|
+
from modmex_lambda.event_handler.dependencies.types import CacheKey
|
|
20
|
+
from modmex_lambda.event_handler.response import Response
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ParamTypes(Enum):
|
|
24
|
+
query = "query"
|
|
25
|
+
header = "header"
|
|
26
|
+
path = "path"
|
|
27
|
+
cookie = "cookie"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Param(FieldInfo): # type: ignore[misc]
|
|
31
|
+
in_ = ParamTypes
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Path(Param): # type: ignore[misc]
|
|
35
|
+
in_ = ParamTypes.path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Query(Param): # type: ignore[misc]
|
|
39
|
+
in_ = ParamTypes.query
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Header(Param): # type: ignore[misc]
|
|
43
|
+
in_ = ParamTypes.header
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Cookie(Param): # type: ignore[misc]
|
|
47
|
+
in_ = ParamTypes.cookie
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Body(FieldInfo): # type: ignore[misc]
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Form(Body): # type: ignore[misc]
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class UploadFile:
|
|
59
|
+
"""Uploaded file value produced by multipart form parsing."""
|
|
60
|
+
|
|
61
|
+
__slots__ = ("content", "content_type", "filename")
|
|
62
|
+
|
|
63
|
+
def __init__(self, *, content: bytes, filename: str | None = None, content_type: str | None = None):
|
|
64
|
+
self.content = content
|
|
65
|
+
self.filename = filename
|
|
66
|
+
self.content_type = content_type
|
|
67
|
+
|
|
68
|
+
def __len__(self) -> int:
|
|
69
|
+
return len(self.content)
|
|
70
|
+
|
|
71
|
+
def __repr__(self) -> str:
|
|
72
|
+
return f"UploadFile(filename={self.filename!r}, content_type={self.content_type!r}, size={len(self.content)})"
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def _validate(cls, v: Any) -> UploadFile:
|
|
76
|
+
if isinstance(v, cls):
|
|
77
|
+
return v
|
|
78
|
+
raise ValueError(f"Expected UploadFile, got {type(v).__name__}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class File(Form): # type: ignore[misc]
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
_PUBLIC_PARAM_TO_INTERNAL: dict[type[Any], type[FieldInfo]] = {
|
|
86
|
+
public_params.Body: Body,
|
|
87
|
+
public_params.Query: Query,
|
|
88
|
+
public_params.Path: Path,
|
|
89
|
+
public_params.Header: Header,
|
|
90
|
+
public_params.Cookie: Cookie,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _is_public_param_marker(value: Any) -> bool:
|
|
95
|
+
if isinstance(value, type):
|
|
96
|
+
return issubclass(value, public_params.Param)
|
|
97
|
+
return isinstance(value, public_params.Param)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _public_param_to_field_info(marker: Any) -> FieldInfo:
|
|
101
|
+
marker_type = marker if isinstance(marker, type) else type(marker)
|
|
102
|
+
alias = None if isinstance(marker, type) else marker.name
|
|
103
|
+
return _PUBLIC_PARAM_TO_INTERNAL[marker_type](alias=alias)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_flat_dependant(
|
|
107
|
+
dependant: Dependant,
|
|
108
|
+
visited: list[CacheKey] | None = None,
|
|
109
|
+
) -> Dependant:
|
|
110
|
+
visited = visited or []
|
|
111
|
+
visited.append(dependant.cache_key)
|
|
112
|
+
|
|
113
|
+
flat = Dependant(
|
|
114
|
+
path_params=dependant.path_params.copy(),
|
|
115
|
+
query_params=dependant.query_params.copy(),
|
|
116
|
+
header_params=dependant.header_params.copy(),
|
|
117
|
+
cookie_params=dependant.cookie_params.copy(),
|
|
118
|
+
body_params=dependant.body_params.copy(),
|
|
119
|
+
path=dependant.path,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
for dep in dependant.dependencies:
|
|
123
|
+
if dep.dependant.cache_key in visited:
|
|
124
|
+
continue
|
|
125
|
+
sub_flat = get_flat_dependant(dep.dependant, visited=visited)
|
|
126
|
+
flat.path_params.extend(sub_flat.path_params)
|
|
127
|
+
flat.query_params.extend(sub_flat.query_params)
|
|
128
|
+
flat.header_params.extend(sub_flat.header_params)
|
|
129
|
+
flat.cookie_params.extend(sub_flat.cookie_params)
|
|
130
|
+
flat.body_params.extend(sub_flat.body_params)
|
|
131
|
+
|
|
132
|
+
return flat
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def analyze_param(
|
|
136
|
+
*,
|
|
137
|
+
param_name: str,
|
|
138
|
+
annotation: Any,
|
|
139
|
+
value: Any,
|
|
140
|
+
is_path_param: bool,
|
|
141
|
+
is_response_param: bool,
|
|
142
|
+
) -> ModelField | None:
|
|
143
|
+
field_info, type_annotation = _resolve_field_info(annotation, value, is_path_param, is_response_param)
|
|
144
|
+
|
|
145
|
+
if isinstance(value, FieldInfo):
|
|
146
|
+
if field_info is not None:
|
|
147
|
+
raise AssertionError("Cannot use a FieldInfo as a parameter annotation and pass a FieldInfo as a value")
|
|
148
|
+
field_info = value
|
|
149
|
+
field_info.annotation = type_annotation
|
|
150
|
+
|
|
151
|
+
if field_info is None:
|
|
152
|
+
field_info = _default_field_info(type_annotation, value, is_path_param)
|
|
153
|
+
|
|
154
|
+
if is_response_param:
|
|
155
|
+
field_info.default = Required
|
|
156
|
+
|
|
157
|
+
return _create_model_field(field_info, type_annotation, param_name, is_path_param, is_response_param)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _resolve_field_info(
|
|
161
|
+
annotation: Any,
|
|
162
|
+
value: Any,
|
|
163
|
+
is_path_param: bool,
|
|
164
|
+
is_response_param: bool,
|
|
165
|
+
) -> tuple[FieldInfo | None, Any]:
|
|
166
|
+
if annotation is inspect.Signature.empty:
|
|
167
|
+
return None, Any
|
|
168
|
+
|
|
169
|
+
origin = get_origin(annotation)
|
|
170
|
+
args = get_args(annotation)
|
|
171
|
+
|
|
172
|
+
if origin is Annotated:
|
|
173
|
+
return _resolve_annotated(annotation, value, is_path_param)
|
|
174
|
+
if _is_public_param_marker(origin):
|
|
175
|
+
return _field_from_public_marker(origin, args[0] if args else Any, value, is_path_param)
|
|
176
|
+
if _is_public_param_marker(annotation):
|
|
177
|
+
return _field_from_public_marker(annotation, Any, value, is_path_param)
|
|
178
|
+
if origin is Response:
|
|
179
|
+
(inner_type,) = args
|
|
180
|
+
return _resolve_field_info(inner_type, value, False, True)
|
|
181
|
+
if is_response_param and origin is tuple and len(args) == 2:
|
|
182
|
+
inner_type = args[0]
|
|
183
|
+
if get_origin(inner_type) is Annotated:
|
|
184
|
+
return _resolve_annotated(inner_type, value, False)
|
|
185
|
+
return None, inner_type
|
|
186
|
+
|
|
187
|
+
return None, annotation
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _field_from_public_marker(
|
|
191
|
+
marker: Any,
|
|
192
|
+
type_annotation: Any,
|
|
193
|
+
value: Any,
|
|
194
|
+
is_path_param: bool,
|
|
195
|
+
) -> tuple[FieldInfo, Any]:
|
|
196
|
+
field_info = _public_param_to_field_info(marker)
|
|
197
|
+
field_info.annotation = type_annotation
|
|
198
|
+
_set_field_default(field_info, value, is_path_param)
|
|
199
|
+
return field_info, type_annotation
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _default_field_info(type_annotation: Any, value: Any, is_path_param: bool) -> FieldInfo:
|
|
203
|
+
default = value if value is not inspect.Signature.empty else Required
|
|
204
|
+
if is_path_param:
|
|
205
|
+
return Path(annotation=type_annotation)
|
|
206
|
+
if field_annotation_is_scalar(type_annotation):
|
|
207
|
+
return Query(annotation=type_annotation, default=default)
|
|
208
|
+
return Body(annotation=type_annotation, default=default)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _resolve_annotated(annotation: Any, value: Any, is_path_param: bool) -> tuple[FieldInfo | None, Any]:
|
|
212
|
+
annotated_args = get_args(annotation)
|
|
213
|
+
type_annotation = annotated_args[0]
|
|
214
|
+
markers: list[FieldInfo] = []
|
|
215
|
+
metadata: list[Any] = []
|
|
216
|
+
|
|
217
|
+
for item in annotated_args[1:]:
|
|
218
|
+
marker = _annotation_to_field_info(item)
|
|
219
|
+
if marker is None:
|
|
220
|
+
metadata.append(item)
|
|
221
|
+
else:
|
|
222
|
+
markers.append(marker)
|
|
223
|
+
|
|
224
|
+
if len(markers) > 1:
|
|
225
|
+
raise AssertionError("Only one FieldInfo can be used per parameter")
|
|
226
|
+
|
|
227
|
+
if metadata:
|
|
228
|
+
type_annotation = Annotated[(type_annotation, *metadata)]
|
|
229
|
+
|
|
230
|
+
if not markers:
|
|
231
|
+
return None, type_annotation
|
|
232
|
+
|
|
233
|
+
field_info = copy_field_info(field_info=markers[0], annotation=type_annotation)
|
|
234
|
+
_set_field_default(field_info, value, is_path_param)
|
|
235
|
+
return field_info, type_annotation
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _annotation_to_field_info(annotation: Any) -> FieldInfo | None:
|
|
239
|
+
if isinstance(annotation, FieldInfo):
|
|
240
|
+
return annotation
|
|
241
|
+
if isinstance(annotation, type) and issubclass(annotation, FieldInfo):
|
|
242
|
+
return annotation()
|
|
243
|
+
if _is_public_param_marker(annotation):
|
|
244
|
+
return _public_param_to_field_info(annotation)
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _set_field_default(field_info: FieldInfo, value: Any, is_path_param: bool) -> None:
|
|
249
|
+
if field_info.default not in [None, Required]:
|
|
250
|
+
raise AssertionError("FieldInfo needs to have a default value of None or Required")
|
|
251
|
+
|
|
252
|
+
if value is inspect.Signature.empty:
|
|
253
|
+
field_info.default = Required
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
if is_path_param:
|
|
257
|
+
raise AssertionError("Cannot use a FieldInfo as a path parameter and pass a value")
|
|
258
|
+
|
|
259
|
+
if isinstance(field_info, Header) and isinstance(value, str) and field_info.alias is None:
|
|
260
|
+
field_info.alias = value.lower()
|
|
261
|
+
field_info.default = Required
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
field_info.default = value
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def create_response_field(
|
|
268
|
+
name: str,
|
|
269
|
+
type_: Any,
|
|
270
|
+
default: Any | None = None,
|
|
271
|
+
field_info: FieldInfo | None = None,
|
|
272
|
+
alias: str | None = None,
|
|
273
|
+
mode: Literal["validation", "serialization"] = "validation",
|
|
274
|
+
) -> ModelField:
|
|
275
|
+
field_info = field_info or FieldInfo(annotation=type_, default=default, alias=alias)
|
|
276
|
+
return ModelField(name=name, field_info=field_info, mode=mode)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _apply_header_model_aliases(field_info: FieldInfo, type_annotation: Any) -> tuple[FieldInfo, Any]:
|
|
280
|
+
if not isinstance(field_info, Header) or not lenient_issubclass(type_annotation, BaseModel):
|
|
281
|
+
return field_info, type_annotation
|
|
282
|
+
|
|
283
|
+
header_model = create_model(
|
|
284
|
+
f"{type_annotation.__name__}WithHeaderAliases",
|
|
285
|
+
__base__=type_annotation,
|
|
286
|
+
__config__={"alias_generator": lambda name: name.replace("_", "-")},
|
|
287
|
+
)
|
|
288
|
+
field_info.annotation = header_model
|
|
289
|
+
return field_info, header_model
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _create_model_field(
|
|
293
|
+
field_info: FieldInfo | None,
|
|
294
|
+
type_annotation: Any,
|
|
295
|
+
param_name: str,
|
|
296
|
+
is_path_param: bool,
|
|
297
|
+
is_response_param: bool = False,
|
|
298
|
+
) -> ModelField | None:
|
|
299
|
+
if field_info is None:
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
if is_path_param and not isinstance(field_info, Path):
|
|
303
|
+
raise AssertionError("Path parameters must be of type Path")
|
|
304
|
+
if not is_path_param and isinstance(field_info, Param) and getattr(field_info, "in_", None) is None:
|
|
305
|
+
field_info.in_ = ParamTypes.query
|
|
306
|
+
|
|
307
|
+
field_info, type_annotation = _apply_header_model_aliases(field_info, type_annotation)
|
|
308
|
+
use_annotation = get_annotation_from_field_info(type_annotation, field_info, param_name)
|
|
309
|
+
|
|
310
|
+
return create_response_field(
|
|
311
|
+
name=param_name,
|
|
312
|
+
type_=use_annotation,
|
|
313
|
+
default=field_info.default,
|
|
314
|
+
alias=field_info.alias,
|
|
315
|
+
field_info=field_info,
|
|
316
|
+
mode="serialization" if is_response_param else "validation",
|
|
317
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import types
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from modmex import BaseModel
|
|
4
|
+
from typing import Union, Any, Dict, Set, Type, Callable
|
|
5
|
+
|
|
6
|
+
UnionType = getattr(types, "UnionType", Union)
|
|
7
|
+
|
|
8
|
+
IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]]
|
|
9
|
+
|
|
10
|
+
TypeModelOrEnum = Union[Type[BaseModel], Type[Enum]]
|
|
11
|
+
|
|
12
|
+
ModelNameMap = Dict[TypeModelOrEnum, str]
|
|
13
|
+
|
|
14
|
+
CacheKey = Union[Callable[..., Any], None]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Mapping, Any
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ExceptionHandlerManager:
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
"""Initialize an empty dictionary to store exception handlers."""
|
|
11
|
+
self._exception_handlers: dict[type[Exception], Callable] = {}
|
|
12
|
+
|
|
13
|
+
def exception_handler(self, exc_class: type[Exception] | list[type[Exception]]):
|
|
14
|
+
"""
|
|
15
|
+
A decorator function that registers a handler for one or more exception types.
|
|
16
|
+
"""
|
|
17
|
+
classes = exc_class if isinstance(exc_class, list) else [exc_class]
|
|
18
|
+
|
|
19
|
+
def register_exception_handler(func: Callable)-> Callable[[Exception], Any]:
|
|
20
|
+
for cls in classes:
|
|
21
|
+
self._exception_handlers[cls] = func
|
|
22
|
+
return func
|
|
23
|
+
|
|
24
|
+
return register_exception_handler
|
|
25
|
+
|
|
26
|
+
def lookup_exception_handler(self, exp_type: type) -> Callable | None:
|
|
27
|
+
for cls in exp_type.__mro__:
|
|
28
|
+
if cls in self._exception_handlers:
|
|
29
|
+
return self._exception_handlers[cls]
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
def update_exception_handlers(self, handlers: Mapping[type[Exception], Callable]) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Updates the exception handlers dictionary with new handler mappings.
|
|
35
|
+
This method allows bulk updates of exception handlers by providing a dictionary
|
|
36
|
+
mapping exception types to handler functions.
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
handlers : Mapping[Type[Exception], Callable]
|
|
40
|
+
A dictionary mapping exception types to handler functions.
|
|
41
|
+
Example
|
|
42
|
+
-------
|
|
43
|
+
>>> def handle_value_error(e):
|
|
44
|
+
... print(f"Value error: {e}")
|
|
45
|
+
...
|
|
46
|
+
>>> def handle_key_error(e):
|
|
47
|
+
... print(f"Key error: {e}")
|
|
48
|
+
...
|
|
49
|
+
>>> handler_manager.update_exception_handlers({
|
|
50
|
+
... ValueError: handle_value_error,
|
|
51
|
+
... KeyError: handle_key_error
|
|
52
|
+
... })
|
|
53
|
+
"""
|
|
54
|
+
self._exception_handlers.update(handlers)
|
|
55
|
+
|
|
56
|
+
def get_registered_handlers(self) -> dict[type[Exception], Callable]:
|
|
57
|
+
"""
|
|
58
|
+
Returns all registered exception handlers.
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
Dict[Type[Exception], Callable]
|
|
62
|
+
A dictionary mapping exception types to their handler functions.
|
|
63
|
+
"""
|
|
64
|
+
return self._exception_handlers.copy()
|
|
65
|
+
|
|
66
|
+
def clear_handlers(self) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Clears all registered exception handlers.
|
|
69
|
+
"""
|
|
70
|
+
self._exception_handlers.clear()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Framework exceptions with API Gateway friendly defaults."""
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from http import HTTPStatus
|
|
4
|
+
from typing import Any
|
|
5
|
+
from modmex import ValidationError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ValidationException(Exception):
|
|
9
|
+
"""
|
|
10
|
+
Base exception for all validation errors
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, errors: Sequence[Any]) -> None:
|
|
14
|
+
self._errors = errors
|
|
15
|
+
|
|
16
|
+
def errors(self) -> Sequence[Any]:
|
|
17
|
+
return self._errors
|
|
18
|
+
|
|
19
|
+
class ServiceError(Exception):
|
|
20
|
+
"""HTTP Service Error"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, status_code: int, msg: str | dict):
|
|
23
|
+
"""
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
status_code: int
|
|
27
|
+
HTTP status code
|
|
28
|
+
msg: str | dict
|
|
29
|
+
Error message. Can be a string or a dictionary
|
|
30
|
+
"""
|
|
31
|
+
self.status_code = status_code
|
|
32
|
+
self.msg = msg
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RequestValidationError(ValidationException):
|
|
36
|
+
"""Raised when request validation fails."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class NotFoundError(ServiceError):
|
|
41
|
+
"""Raised when no route matches a request path."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, msg: str | dict = "Not Found"):
|
|
44
|
+
super().__init__(HTTPStatus.NOT_FOUND, msg)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class MethodNotAllowedError(ServiceError):
|
|
48
|
+
"""Raised when a path exists but the HTTP method is not allowed."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, msg: str | dict = "Method Not Allowed"):
|
|
51
|
+
super().__init__(HTTPStatus.METHOD_NOT_ALLOWED, msg)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class BadRequestError(ServiceError):
|
|
55
|
+
"""Raised when an incoming event cannot be normalized."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, msg: str | dict = "Bad Request"):
|
|
58
|
+
super().__init__(HTTPStatus.BAD_REQUEST, msg)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class UnauthorizedError(ServiceError):
|
|
62
|
+
"""Raised when a request is not authenticated."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, msg: str | dict = "Unauthorized"):
|
|
65
|
+
super().__init__(HTTPStatus.UNAUTHORIZED, msg)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ForbiddenError(ServiceError):
|
|
69
|
+
"""Raised when a request is authenticated but not authorized."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, msg: str | dict = "Forbidden"):
|
|
72
|
+
super().__init__(HTTPStatus.FORBIDDEN, msg)
|