modmex-lambda 0.1.0__tar.gz → 0.3.0__tar.gz
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-0.1.0 → modmex_lambda-0.3.0}/.github/workflows/ci.yml +1 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/PKG-INFO +244 -21
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/README.md +241 -20
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/__init__.py +12 -1
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/__init__.py +7 -1
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/api_gateway.py +3 -0
- modmex_lambda-0.3.0/modmex_lambda/event_handler/dependencies/__init__.py +13 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/dependencies/dependant.py +4 -1
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/dependencies/depends.py +85 -5
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/routing.py +44 -1
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/types.py +2 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/poetry.lock +18 -1
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/pyproject.toml +2 -1
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/event_handler/test_api_gateway.py +140 -1
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/event_handler/test_dependencies.py +47 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/event_handler/test_routing.py +21 -1
- modmex_lambda-0.1.0/tests/event_handler/__init__.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/.github/workflows/release.yml +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/.gitignore +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/LICENSE +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/data_classes/__init__.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/data_classes/api_gateway_authorizer_event.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/data_classes/api_gateway_proxy_event.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/data_classes/api_gateway_websocket_event.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/data_classes/cognito_user_pool_event.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/data_classes/common.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/constants.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/content_types.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/cors.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/dependencies/compat.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/dependencies/dependency_middleware.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/dependencies/params.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/dependencies/types.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/exception_handler.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/exceptions.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/gateway_response.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/middlewares.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/params.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/request.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/response.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/routing_fallbacks.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_sources.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/exceptions.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/logging.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/params.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/parser.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/request.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/resolver.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/response.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/routing.py +0 -0
- {modmex_lambda-0.1.0/modmex_lambda/event_handler/dependencies → modmex_lambda-0.3.0/modmex_lambda/shared}/__init__.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/shared/cookies.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/shared/headers_serializer.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/shared/json_encoder.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/shared/types.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/validation.py +0 -0
- {modmex_lambda-0.1.0/modmex_lambda/shared → modmex_lambda-0.3.0/tests}/__init__.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/conftest.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/data_classes/test_api_gateway_proxy_event.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/data_classes/test_cognito_user_pool_event.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/data_classes/test_common.py +0 -0
- {modmex_lambda-0.1.0/tests → modmex_lambda-0.3.0/tests/event_handler}/__init__.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/event_handler/test_cors.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/event_handler/test_exception_handler.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/event_handler/test_gateway_response.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/event_handler/test_request.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/event_handler/test_response.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/shared/test_cookies_headers.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/shared/test_json_encoder.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/test_logging.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/test_parser_event_sources.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/test_reexports.py +0 -0
- {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/test_validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modmex-lambda
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Ultra-lightweight AWS Lambda utilities for API Gateway routing and event handling.
|
|
5
5
|
Author: Modmex
|
|
6
6
|
License: MIT
|
|
@@ -9,6 +9,8 @@ Requires-Python: >=3.10
|
|
|
9
9
|
Requires-Dist: modmex<2.0.0,>=1.1.10
|
|
10
10
|
Provides-Extra: dev
|
|
11
11
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
12
|
+
Provides-Extra: injector
|
|
13
|
+
Requires-Dist: injector<1.0.0,>=0.24.0; extra == 'injector'
|
|
12
14
|
Description-Content-Type: text/markdown
|
|
13
15
|
|
|
14
16
|
# modmex-lambda
|
|
@@ -26,11 +28,17 @@ logging with a small dependency footprint.
|
|
|
26
28
|
pip install modmex-lambda
|
|
27
29
|
```
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
|
|
32
|
+
To use the optional `injector` integration:
|
|
30
33
|
|
|
31
34
|
```bash
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
pip install "modmex-lambda[injector]"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
With Poetry:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
poetry add "modmex-lambda[injector]"
|
|
34
42
|
```
|
|
35
43
|
|
|
36
44
|
## API Gateway Resolvers
|
|
@@ -52,7 +60,9 @@ def ping():
|
|
|
52
60
|
return {"message": "pong"}
|
|
53
61
|
|
|
54
62
|
|
|
55
|
-
handler
|
|
63
|
+
def handler(event, context):
|
|
64
|
+
return app.resolve(event, context)
|
|
65
|
+
|
|
56
66
|
```
|
|
57
67
|
|
|
58
68
|
The internal base resolver is intentionally not exported from the package root;
|
|
@@ -76,6 +86,25 @@ def create_user():
|
|
|
76
86
|
Supported route decorators include `get`, `post`, `put`, `patch`, `delete`,
|
|
77
87
|
`options`, and `any`.
|
|
78
88
|
|
|
89
|
+
You can also declare routes on a standalone router and include it in the
|
|
90
|
+
resolver:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from modmex_lambda import ApiGatewayHttpResolver
|
|
94
|
+
from modmex_lambda.routing import Router
|
|
95
|
+
|
|
96
|
+
app = ApiGatewayHttpResolver()
|
|
97
|
+
router = Router()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@router.get("/health")
|
|
101
|
+
def health():
|
|
102
|
+
return {"ok": True}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
app.include_router(router)
|
|
106
|
+
```
|
|
107
|
+
|
|
79
108
|
Routers can also strip deployment prefixes:
|
|
80
109
|
|
|
81
110
|
```python
|
|
@@ -153,6 +182,38 @@ Route return values are converted to API Gateway proxy responses:
|
|
|
153
182
|
- `(body, status_code)` sets the response status.
|
|
154
183
|
- `Response` gives full control over status, headers, cookies, and content type.
|
|
155
184
|
|
|
185
|
+
Use plain return values for simple JSON endpoints:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from modmex import BaseModel
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class User(BaseModel):
|
|
192
|
+
id: int
|
|
193
|
+
name: str
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@app.get("/users/<user_id>")
|
|
197
|
+
def get_user(user_id: int):
|
|
198
|
+
user = User(id=user_id, name="Ada")
|
|
199
|
+
return user.model_dump()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@app.post("/users", status_code=201)
|
|
203
|
+
def create_user():
|
|
204
|
+
user = User(id=42, name="Ada")
|
|
205
|
+
return user.model_dump()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@app.delete("/users/<user_id>")
|
|
209
|
+
def delete_user(user_id: int):
|
|
210
|
+
return {"deleted": user_id}, 202
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Use `Response` when the endpoint needs explicit response metadata. If you are
|
|
214
|
+
returning the same `User` model, pass `user.model_dump_json()` as the body and
|
|
215
|
+
set `content_type="application/json"`:
|
|
216
|
+
|
|
156
217
|
```python
|
|
157
218
|
from modmex_lambda import Response
|
|
158
219
|
from modmex_lambda.shared.cookies import Cookie
|
|
@@ -160,13 +221,63 @@ from modmex_lambda.shared.cookies import Cookie
|
|
|
160
221
|
|
|
161
222
|
@app.get("/session")
|
|
162
223
|
def session():
|
|
224
|
+
user = User(id=42, name="Ada")
|
|
225
|
+
return Response(
|
|
226
|
+
status_code=200,
|
|
227
|
+
content_type="application/json",
|
|
228
|
+
body=user.model_dump_json(),
|
|
229
|
+
headers={"x-app": "users"},
|
|
230
|
+
cookies=[
|
|
231
|
+
Cookie(
|
|
232
|
+
"session",
|
|
233
|
+
"abc",
|
|
234
|
+
path="/",
|
|
235
|
+
http_only=True,
|
|
236
|
+
secure=True,
|
|
237
|
+
max_age=3600,
|
|
238
|
+
),
|
|
239
|
+
],
|
|
240
|
+
)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`Response` accepts:
|
|
244
|
+
|
|
245
|
+
- `status_code`: the HTTP status code returned to API Gateway.
|
|
246
|
+
- `body`: a JSON-serializable object, `str`, `bytes`, or `None`.
|
|
247
|
+
- `content_type`: sets `Content-Type` unless the header is already present.
|
|
248
|
+
- `headers`: a mapping of header names to a string or list of strings.
|
|
249
|
+
- `cookies`: a list of `Cookie` objects.
|
|
250
|
+
- `compress`: overrides route-level gzip compression for that response.
|
|
251
|
+
|
|
252
|
+
When `Content-Type` starts with `application/json`, non-string bodies are
|
|
253
|
+
serialized with the app serializer. Binary bodies are base64 encoded.
|
|
254
|
+
|
|
255
|
+
For `modmex` models, prefer `model_dump()` when returning plain JSON objects.
|
|
256
|
+
Use `model_dump_json()` when you already need to build a `Response` and want to
|
|
257
|
+
send the serialized JSON string directly with `content_type="application/json"`.
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
@app.get("/avatar/<user_id>")
|
|
261
|
+
def avatar(user_id: int):
|
|
262
|
+
image_bytes = load_avatar(user_id)
|
|
163
263
|
return Response(
|
|
164
264
|
status_code=200,
|
|
165
|
-
|
|
166
|
-
|
|
265
|
+
content_type="image/png",
|
|
266
|
+
body=image_bytes,
|
|
267
|
+
headers={"Cache-Control": "max-age=3600"},
|
|
167
268
|
)
|
|
168
269
|
```
|
|
169
270
|
|
|
271
|
+
Route options can add response behavior without constructing `Response` in every
|
|
272
|
+
handler:
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
@app.get("/report", cache_control="max-age=60", compress=True)
|
|
276
|
+
def report():
|
|
277
|
+
return {"items": build_report()}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Compression is applied only when the request includes `Accept-Encoding: gzip`.
|
|
170
281
|
REST API responses use `multiValueHeaders`; HTTP API responses use the v2
|
|
171
282
|
`headers` and `cookies` shape.
|
|
172
283
|
|
|
@@ -236,6 +347,69 @@ def get_user(
|
|
|
236
347
|
return repository.get_user(user_id)
|
|
237
348
|
```
|
|
238
349
|
|
|
350
|
+
For constructor-heavy services, install the optional `injector` extra and pass
|
|
351
|
+
an `InjectorDependencyResolver` to the app. `Depends()` without a callable uses
|
|
352
|
+
the parameter annotation as the dependency token.
|
|
353
|
+
|
|
354
|
+
```python
|
|
355
|
+
from typing import Annotated
|
|
356
|
+
|
|
357
|
+
from injector import Injector, Module, inject, provider, singleton
|
|
358
|
+
from modmex_lambda import ApiGatewayHttpResolver, Depends, InjectorDependencyResolver
|
|
359
|
+
from modmex_lambda.event_handler.params import Path
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class Settings:
|
|
363
|
+
def __init__(self, tenant_id: str):
|
|
364
|
+
self.tenant_id = tenant_id
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class UserRepository:
|
|
368
|
+
def __init__(self, settings: Settings):
|
|
369
|
+
self.settings = settings
|
|
370
|
+
|
|
371
|
+
def get_user(self, user_id: int) -> dict:
|
|
372
|
+
return {"id": user_id, "tenant_id": self.settings.tenant_id}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class UserService:
|
|
376
|
+
def __init__(self, repository: UserRepository):
|
|
377
|
+
self.repository = repository
|
|
378
|
+
|
|
379
|
+
def get_user(self, user_id: int) -> dict:
|
|
380
|
+
return self.repository.get_user(user_id)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class AppModule(Module):
|
|
384
|
+
@singleton
|
|
385
|
+
@provider
|
|
386
|
+
def provide_settings(self) -> Settings:
|
|
387
|
+
return Settings(tenant_id="mx")
|
|
388
|
+
|
|
389
|
+
@singleton
|
|
390
|
+
@provider
|
|
391
|
+
@inject
|
|
392
|
+
def provide_repository(self, settings: Settings) -> UserRepository:
|
|
393
|
+
return UserRepository(settings)
|
|
394
|
+
|
|
395
|
+
@singleton
|
|
396
|
+
@provider
|
|
397
|
+
@inject
|
|
398
|
+
def provide_service(self, repository: UserRepository) -> UserService:
|
|
399
|
+
return UserService(repository)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
container = Injector([AppModule()])
|
|
403
|
+
app = ApiGatewayHttpResolver(dependency_resolver=InjectorDependencyResolver(container))
|
|
404
|
+
|
|
405
|
+
@app.get("/users/<user_id>")
|
|
406
|
+
def get_user(
|
|
407
|
+
user_id: Annotated[int, Path()],
|
|
408
|
+
service: Annotated[UserService, Depends()],
|
|
409
|
+
):
|
|
410
|
+
return service.get_user(user_id)
|
|
411
|
+
```
|
|
412
|
+
|
|
239
413
|
Disable dependency caching when a dependency must run every time:
|
|
240
414
|
|
|
241
415
|
```python
|
|
@@ -336,8 +510,69 @@ def lambda_handler(event: APIGatewayHttpEvent, context):
|
|
|
336
510
|
|
|
337
511
|
## Validation
|
|
338
512
|
|
|
339
|
-
Modmex is the default validation and coercion engine.
|
|
340
|
-
|
|
513
|
+
Modmex is the default validation and coercion engine. It is used for path,
|
|
514
|
+
query, header, cookie, and body parameters declared with `Annotated`, and it is
|
|
515
|
+
paired with the default JSON serializer for common values like enums, dates,
|
|
516
|
+
datetimes, decimals, and dataclasses.
|
|
517
|
+
|
|
518
|
+
```python
|
|
519
|
+
from datetime import date
|
|
520
|
+
from decimal import Decimal
|
|
521
|
+
from enum import Enum
|
|
522
|
+
from typing import Annotated
|
|
523
|
+
|
|
524
|
+
from modmex import BaseModel
|
|
525
|
+
from modmex_lambda import ApiGatewayHttpResolver
|
|
526
|
+
from modmex_lambda.event_handler.params import Body, Path, Query
|
|
527
|
+
|
|
528
|
+
app = ApiGatewayHttpResolver()
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
class Plan(str, Enum):
|
|
532
|
+
FREE = "free"
|
|
533
|
+
PRO = "pro"
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
class CreateAccount(BaseModel):
|
|
537
|
+
name: str
|
|
538
|
+
plan: Plan = Plan.FREE
|
|
539
|
+
trial_ends_on: date | None = None
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
class Account(BaseModel):
|
|
543
|
+
id: int
|
|
544
|
+
name: str
|
|
545
|
+
plan: Plan
|
|
546
|
+
balance: Decimal
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@app.post("/accounts", status_code=201)
|
|
550
|
+
def create_account(payload: Annotated[CreateAccount, Body()]):
|
|
551
|
+
account = Account(
|
|
552
|
+
id=42,
|
|
553
|
+
name=payload.name,
|
|
554
|
+
plan=payload.plan,
|
|
555
|
+
balance=Decimal("0.00"),
|
|
556
|
+
)
|
|
557
|
+
# Return model_dump() when you want the response body to be a JSON object.
|
|
558
|
+
return account.model_dump()
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
@app.get("/accounts/<account_id>")
|
|
562
|
+
def get_account(
|
|
563
|
+
account_id: Annotated[int, Path()],
|
|
564
|
+
include_usage: Annotated[bool, Query()] = False,
|
|
565
|
+
):
|
|
566
|
+
return {
|
|
567
|
+
"id": account_id,
|
|
568
|
+
"include_usage": include_usage,
|
|
569
|
+
"created_on": date(2026, 1, 1),
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
If validation fails, the resolver returns `400` with a compact validation error
|
|
574
|
+
payload. For domain-specific errors, register an exception handler and return a
|
|
575
|
+
`Response` with the shape your API expects.
|
|
341
576
|
|
|
342
577
|
## Logging
|
|
343
578
|
|
|
@@ -355,18 +590,6 @@ def lambda_handler(event, context):
|
|
|
355
590
|
The logger emits structured JSON and can extract Lambda request IDs and API
|
|
356
591
|
Gateway correlation IDs.
|
|
357
592
|
|
|
358
|
-
## Benchmarks
|
|
359
|
-
|
|
360
|
-
The benchmark suite lives in `.benchmark/api_gateway_benchmark.py`.
|
|
361
|
-
|
|
362
|
-
It covers cold imports, app setup, route registration, API Gateway v1/v2
|
|
363
|
-
invocation, `event_parser`, `event_source`, and logger hot paths.
|
|
364
|
-
|
|
365
|
-
```bash
|
|
366
|
-
poetry run python .benchmark/api_gateway_benchmark.py
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
More details are in `.benchmark/README.md`.
|
|
370
593
|
|
|
371
594
|
## Limitations
|
|
372
595
|
|
|
@@ -13,11 +13,17 @@ logging with a small dependency footprint.
|
|
|
13
13
|
pip install modmex-lambda
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
To use the optional `injector` integration:
|
|
17
18
|
|
|
18
19
|
```bash
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
pip install "modmex-lambda[injector]"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
With Poetry:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
poetry add "modmex-lambda[injector]"
|
|
21
27
|
```
|
|
22
28
|
|
|
23
29
|
## API Gateway Resolvers
|
|
@@ -39,7 +45,9 @@ def ping():
|
|
|
39
45
|
return {"message": "pong"}
|
|
40
46
|
|
|
41
47
|
|
|
42
|
-
handler
|
|
48
|
+
def handler(event, context):
|
|
49
|
+
return app.resolve(event, context)
|
|
50
|
+
|
|
43
51
|
```
|
|
44
52
|
|
|
45
53
|
The internal base resolver is intentionally not exported from the package root;
|
|
@@ -63,6 +71,25 @@ def create_user():
|
|
|
63
71
|
Supported route decorators include `get`, `post`, `put`, `patch`, `delete`,
|
|
64
72
|
`options`, and `any`.
|
|
65
73
|
|
|
74
|
+
You can also declare routes on a standalone router and include it in the
|
|
75
|
+
resolver:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from modmex_lambda import ApiGatewayHttpResolver
|
|
79
|
+
from modmex_lambda.routing import Router
|
|
80
|
+
|
|
81
|
+
app = ApiGatewayHttpResolver()
|
|
82
|
+
router = Router()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@router.get("/health")
|
|
86
|
+
def health():
|
|
87
|
+
return {"ok": True}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
app.include_router(router)
|
|
91
|
+
```
|
|
92
|
+
|
|
66
93
|
Routers can also strip deployment prefixes:
|
|
67
94
|
|
|
68
95
|
```python
|
|
@@ -140,6 +167,38 @@ Route return values are converted to API Gateway proxy responses:
|
|
|
140
167
|
- `(body, status_code)` sets the response status.
|
|
141
168
|
- `Response` gives full control over status, headers, cookies, and content type.
|
|
142
169
|
|
|
170
|
+
Use plain return values for simple JSON endpoints:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from modmex import BaseModel
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class User(BaseModel):
|
|
177
|
+
id: int
|
|
178
|
+
name: str
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@app.get("/users/<user_id>")
|
|
182
|
+
def get_user(user_id: int):
|
|
183
|
+
user = User(id=user_id, name="Ada")
|
|
184
|
+
return user.model_dump()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@app.post("/users", status_code=201)
|
|
188
|
+
def create_user():
|
|
189
|
+
user = User(id=42, name="Ada")
|
|
190
|
+
return user.model_dump()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@app.delete("/users/<user_id>")
|
|
194
|
+
def delete_user(user_id: int):
|
|
195
|
+
return {"deleted": user_id}, 202
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Use `Response` when the endpoint needs explicit response metadata. If you are
|
|
199
|
+
returning the same `User` model, pass `user.model_dump_json()` as the body and
|
|
200
|
+
set `content_type="application/json"`:
|
|
201
|
+
|
|
143
202
|
```python
|
|
144
203
|
from modmex_lambda import Response
|
|
145
204
|
from modmex_lambda.shared.cookies import Cookie
|
|
@@ -147,13 +206,63 @@ from modmex_lambda.shared.cookies import Cookie
|
|
|
147
206
|
|
|
148
207
|
@app.get("/session")
|
|
149
208
|
def session():
|
|
209
|
+
user = User(id=42, name="Ada")
|
|
210
|
+
return Response(
|
|
211
|
+
status_code=200,
|
|
212
|
+
content_type="application/json",
|
|
213
|
+
body=user.model_dump_json(),
|
|
214
|
+
headers={"x-app": "users"},
|
|
215
|
+
cookies=[
|
|
216
|
+
Cookie(
|
|
217
|
+
"session",
|
|
218
|
+
"abc",
|
|
219
|
+
path="/",
|
|
220
|
+
http_only=True,
|
|
221
|
+
secure=True,
|
|
222
|
+
max_age=3600,
|
|
223
|
+
),
|
|
224
|
+
],
|
|
225
|
+
)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`Response` accepts:
|
|
229
|
+
|
|
230
|
+
- `status_code`: the HTTP status code returned to API Gateway.
|
|
231
|
+
- `body`: a JSON-serializable object, `str`, `bytes`, or `None`.
|
|
232
|
+
- `content_type`: sets `Content-Type` unless the header is already present.
|
|
233
|
+
- `headers`: a mapping of header names to a string or list of strings.
|
|
234
|
+
- `cookies`: a list of `Cookie` objects.
|
|
235
|
+
- `compress`: overrides route-level gzip compression for that response.
|
|
236
|
+
|
|
237
|
+
When `Content-Type` starts with `application/json`, non-string bodies are
|
|
238
|
+
serialized with the app serializer. Binary bodies are base64 encoded.
|
|
239
|
+
|
|
240
|
+
For `modmex` models, prefer `model_dump()` when returning plain JSON objects.
|
|
241
|
+
Use `model_dump_json()` when you already need to build a `Response` and want to
|
|
242
|
+
send the serialized JSON string directly with `content_type="application/json"`.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
@app.get("/avatar/<user_id>")
|
|
246
|
+
def avatar(user_id: int):
|
|
247
|
+
image_bytes = load_avatar(user_id)
|
|
150
248
|
return Response(
|
|
151
249
|
status_code=200,
|
|
152
|
-
|
|
153
|
-
|
|
250
|
+
content_type="image/png",
|
|
251
|
+
body=image_bytes,
|
|
252
|
+
headers={"Cache-Control": "max-age=3600"},
|
|
154
253
|
)
|
|
155
254
|
```
|
|
156
255
|
|
|
256
|
+
Route options can add response behavior without constructing `Response` in every
|
|
257
|
+
handler:
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
@app.get("/report", cache_control="max-age=60", compress=True)
|
|
261
|
+
def report():
|
|
262
|
+
return {"items": build_report()}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Compression is applied only when the request includes `Accept-Encoding: gzip`.
|
|
157
266
|
REST API responses use `multiValueHeaders`; HTTP API responses use the v2
|
|
158
267
|
`headers` and `cookies` shape.
|
|
159
268
|
|
|
@@ -223,6 +332,69 @@ def get_user(
|
|
|
223
332
|
return repository.get_user(user_id)
|
|
224
333
|
```
|
|
225
334
|
|
|
335
|
+
For constructor-heavy services, install the optional `injector` extra and pass
|
|
336
|
+
an `InjectorDependencyResolver` to the app. `Depends()` without a callable uses
|
|
337
|
+
the parameter annotation as the dependency token.
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
from typing import Annotated
|
|
341
|
+
|
|
342
|
+
from injector import Injector, Module, inject, provider, singleton
|
|
343
|
+
from modmex_lambda import ApiGatewayHttpResolver, Depends, InjectorDependencyResolver
|
|
344
|
+
from modmex_lambda.event_handler.params import Path
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class Settings:
|
|
348
|
+
def __init__(self, tenant_id: str):
|
|
349
|
+
self.tenant_id = tenant_id
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class UserRepository:
|
|
353
|
+
def __init__(self, settings: Settings):
|
|
354
|
+
self.settings = settings
|
|
355
|
+
|
|
356
|
+
def get_user(self, user_id: int) -> dict:
|
|
357
|
+
return {"id": user_id, "tenant_id": self.settings.tenant_id}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class UserService:
|
|
361
|
+
def __init__(self, repository: UserRepository):
|
|
362
|
+
self.repository = repository
|
|
363
|
+
|
|
364
|
+
def get_user(self, user_id: int) -> dict:
|
|
365
|
+
return self.repository.get_user(user_id)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class AppModule(Module):
|
|
369
|
+
@singleton
|
|
370
|
+
@provider
|
|
371
|
+
def provide_settings(self) -> Settings:
|
|
372
|
+
return Settings(tenant_id="mx")
|
|
373
|
+
|
|
374
|
+
@singleton
|
|
375
|
+
@provider
|
|
376
|
+
@inject
|
|
377
|
+
def provide_repository(self, settings: Settings) -> UserRepository:
|
|
378
|
+
return UserRepository(settings)
|
|
379
|
+
|
|
380
|
+
@singleton
|
|
381
|
+
@provider
|
|
382
|
+
@inject
|
|
383
|
+
def provide_service(self, repository: UserRepository) -> UserService:
|
|
384
|
+
return UserService(repository)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
container = Injector([AppModule()])
|
|
388
|
+
app = ApiGatewayHttpResolver(dependency_resolver=InjectorDependencyResolver(container))
|
|
389
|
+
|
|
390
|
+
@app.get("/users/<user_id>")
|
|
391
|
+
def get_user(
|
|
392
|
+
user_id: Annotated[int, Path()],
|
|
393
|
+
service: Annotated[UserService, Depends()],
|
|
394
|
+
):
|
|
395
|
+
return service.get_user(user_id)
|
|
396
|
+
```
|
|
397
|
+
|
|
226
398
|
Disable dependency caching when a dependency must run every time:
|
|
227
399
|
|
|
228
400
|
```python
|
|
@@ -323,8 +495,69 @@ def lambda_handler(event: APIGatewayHttpEvent, context):
|
|
|
323
495
|
|
|
324
496
|
## Validation
|
|
325
497
|
|
|
326
|
-
Modmex is the default validation and coercion engine.
|
|
327
|
-
|
|
498
|
+
Modmex is the default validation and coercion engine. It is used for path,
|
|
499
|
+
query, header, cookie, and body parameters declared with `Annotated`, and it is
|
|
500
|
+
paired with the default JSON serializer for common values like enums, dates,
|
|
501
|
+
datetimes, decimals, and dataclasses.
|
|
502
|
+
|
|
503
|
+
```python
|
|
504
|
+
from datetime import date
|
|
505
|
+
from decimal import Decimal
|
|
506
|
+
from enum import Enum
|
|
507
|
+
from typing import Annotated
|
|
508
|
+
|
|
509
|
+
from modmex import BaseModel
|
|
510
|
+
from modmex_lambda import ApiGatewayHttpResolver
|
|
511
|
+
from modmex_lambda.event_handler.params import Body, Path, Query
|
|
512
|
+
|
|
513
|
+
app = ApiGatewayHttpResolver()
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
class Plan(str, Enum):
|
|
517
|
+
FREE = "free"
|
|
518
|
+
PRO = "pro"
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
class CreateAccount(BaseModel):
|
|
522
|
+
name: str
|
|
523
|
+
plan: Plan = Plan.FREE
|
|
524
|
+
trial_ends_on: date | None = None
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
class Account(BaseModel):
|
|
528
|
+
id: int
|
|
529
|
+
name: str
|
|
530
|
+
plan: Plan
|
|
531
|
+
balance: Decimal
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
@app.post("/accounts", status_code=201)
|
|
535
|
+
def create_account(payload: Annotated[CreateAccount, Body()]):
|
|
536
|
+
account = Account(
|
|
537
|
+
id=42,
|
|
538
|
+
name=payload.name,
|
|
539
|
+
plan=payload.plan,
|
|
540
|
+
balance=Decimal("0.00"),
|
|
541
|
+
)
|
|
542
|
+
# Return model_dump() when you want the response body to be a JSON object.
|
|
543
|
+
return account.model_dump()
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
@app.get("/accounts/<account_id>")
|
|
547
|
+
def get_account(
|
|
548
|
+
account_id: Annotated[int, Path()],
|
|
549
|
+
include_usage: Annotated[bool, Query()] = False,
|
|
550
|
+
):
|
|
551
|
+
return {
|
|
552
|
+
"id": account_id,
|
|
553
|
+
"include_usage": include_usage,
|
|
554
|
+
"created_on": date(2026, 1, 1),
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
If validation fails, the resolver returns `400` with a compact validation error
|
|
559
|
+
payload. For domain-specific errors, register an exception handler and return a
|
|
560
|
+
`Response` with the shape your API expects.
|
|
328
561
|
|
|
329
562
|
## Logging
|
|
330
563
|
|
|
@@ -342,18 +575,6 @@ def lambda_handler(event, context):
|
|
|
342
575
|
The logger emits structured JSON and can extract Lambda request IDs and API
|
|
343
576
|
Gateway correlation IDs.
|
|
344
577
|
|
|
345
|
-
## Benchmarks
|
|
346
|
-
|
|
347
|
-
The benchmark suite lives in `.benchmark/api_gateway_benchmark.py`.
|
|
348
|
-
|
|
349
|
-
It covers cold imports, app setup, route registration, API Gateway v1/v2
|
|
350
|
-
invocation, `event_parser`, `event_source`, and logger hot paths.
|
|
351
|
-
|
|
352
|
-
```bash
|
|
353
|
-
poetry run python .benchmark/api_gateway_benchmark.py
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
More details are in `.benchmark/README.md`.
|
|
357
578
|
|
|
358
579
|
## Limitations
|
|
359
580
|
|
|
@@ -13,7 +13,12 @@ if TYPE_CHECKING:
|
|
|
13
13
|
)
|
|
14
14
|
from .event_handler.request import Request
|
|
15
15
|
from .event_sources import event_source
|
|
16
|
-
from .event_handler.dependencies.depends import
|
|
16
|
+
from .event_handler.dependencies.depends import (
|
|
17
|
+
DefaultDependencyResolver,
|
|
18
|
+
DependencyResolver,
|
|
19
|
+
Depends,
|
|
20
|
+
InjectorDependencyResolver,
|
|
21
|
+
)
|
|
17
22
|
from .logging import Logger
|
|
18
23
|
from .parser import event_parser, parse
|
|
19
24
|
from .validation import ModmexValidator, ValidationError
|
|
@@ -26,7 +31,10 @@ _EXPORTS = {
|
|
|
26
31
|
"parse": ("modmex_lambda.parser", "parse"),
|
|
27
32
|
"event_parser": ("modmex_lambda.parser", "event_parser"),
|
|
28
33
|
"event_source": ("modmex_lambda.event_sources", "event_source"),
|
|
34
|
+
"DefaultDependencyResolver": ("modmex_lambda.event_handler.dependencies.depends", "DefaultDependencyResolver"),
|
|
35
|
+
"DependencyResolver": ("modmex_lambda.event_handler.dependencies.depends", "DependencyResolver"),
|
|
29
36
|
"Depends": ("modmex_lambda.event_handler.dependencies.depends", "Depends"),
|
|
37
|
+
"InjectorDependencyResolver": ("modmex_lambda.event_handler.dependencies.depends", "InjectorDependencyResolver"),
|
|
30
38
|
"Logger": ("modmex_lambda.logging", "Logger"),
|
|
31
39
|
"ModmexValidator": ("modmex_lambda.validation", "ModmexValidator"),
|
|
32
40
|
"ValidationError": ("modmex_lambda.validation", "ValidationError"),
|
|
@@ -40,7 +48,10 @@ __all__ = [
|
|
|
40
48
|
"parse",
|
|
41
49
|
"event_parser",
|
|
42
50
|
"event_source",
|
|
51
|
+
"DefaultDependencyResolver",
|
|
52
|
+
"DependencyResolver",
|
|
43
53
|
"Depends",
|
|
54
|
+
"InjectorDependencyResolver",
|
|
44
55
|
"Logger",
|
|
45
56
|
"ModmexValidator",
|
|
46
57
|
"ValidationError",
|