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,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)