modmex-lambda 0.2.0__py3-none-any.whl → 0.3.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/event_handler/routing.py +43 -1
- {modmex_lambda-0.2.0.dist-info → modmex_lambda-0.3.0.dist-info}/METADATA +170 -18
- {modmex_lambda-0.2.0.dist-info → modmex_lambda-0.3.0.dist-info}/RECORD +5 -5
- {modmex_lambda-0.2.0.dist-info → modmex_lambda-0.3.0.dist-info}/WHEEL +0 -0
- {modmex_lambda-0.2.0.dist-info → modmex_lambda-0.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -190,8 +190,50 @@ class HasRoutes(ABC):
|
|
|
190
190
|
cache_control=cache_control,
|
|
191
191
|
)
|
|
192
192
|
|
|
193
|
+
def options(
|
|
194
|
+
self,
|
|
195
|
+
rule: str,
|
|
196
|
+
description: str | None = None,
|
|
197
|
+
status_code: int = DEFAULT_STATUS_CODE,
|
|
198
|
+
middlewares: list[AnyCallableT] | None = None,
|
|
199
|
+
cors: bool | None = None,
|
|
200
|
+
compress: bool = False,
|
|
201
|
+
cache_control: str | None = None,
|
|
202
|
+
) -> Callable[[AnyCallableT], AnyCallableT]:
|
|
203
|
+
return self.route(
|
|
204
|
+
rule=rule,
|
|
205
|
+
method="OPTIONS",
|
|
206
|
+
description=description,
|
|
207
|
+
status_code=status_code,
|
|
208
|
+
middlewares=middlewares,
|
|
209
|
+
cors=cors,
|
|
210
|
+
compress=compress,
|
|
211
|
+
cache_control=cache_control,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def any(
|
|
215
|
+
self,
|
|
216
|
+
rule: str,
|
|
217
|
+
description: str | None = None,
|
|
218
|
+
status_code: int = DEFAULT_STATUS_CODE,
|
|
219
|
+
middlewares: list[AnyCallableT] | None = None,
|
|
220
|
+
cors: bool | None = None,
|
|
221
|
+
compress: bool = False,
|
|
222
|
+
cache_control: str | None = None,
|
|
223
|
+
) -> Callable[[AnyCallableT], AnyCallableT]:
|
|
224
|
+
return self.route(
|
|
225
|
+
rule=rule,
|
|
226
|
+
method="ANY",
|
|
227
|
+
description=description,
|
|
228
|
+
status_code=status_code,
|
|
229
|
+
middlewares=middlewares,
|
|
230
|
+
cors=cors,
|
|
231
|
+
compress=compress,
|
|
232
|
+
cache_control=cache_control,
|
|
233
|
+
)
|
|
234
|
+
|
|
193
235
|
|
|
194
|
-
class IRouter(ABC):
|
|
236
|
+
class IRouter(HasRoutes, ABC):
|
|
195
237
|
_routes: list[IRoute]
|
|
196
238
|
|
|
197
239
|
@abstractmethod
|
|
@@ -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
|
|
@@ -60,7 +60,9 @@ def ping():
|
|
|
60
60
|
return {"message": "pong"}
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
handler
|
|
63
|
+
def handler(event, context):
|
|
64
|
+
return app.resolve(event, context)
|
|
65
|
+
|
|
64
66
|
```
|
|
65
67
|
|
|
66
68
|
The internal base resolver is intentionally not exported from the package root;
|
|
@@ -84,6 +86,25 @@ def create_user():
|
|
|
84
86
|
Supported route decorators include `get`, `post`, `put`, `patch`, `delete`,
|
|
85
87
|
`options`, and `any`.
|
|
86
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
|
+
|
|
87
108
|
Routers can also strip deployment prefixes:
|
|
88
109
|
|
|
89
110
|
```python
|
|
@@ -161,6 +182,38 @@ Route return values are converted to API Gateway proxy responses:
|
|
|
161
182
|
- `(body, status_code)` sets the response status.
|
|
162
183
|
- `Response` gives full control over status, headers, cookies, and content type.
|
|
163
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
|
+
|
|
164
217
|
```python
|
|
165
218
|
from modmex_lambda import Response
|
|
166
219
|
from modmex_lambda.shared.cookies import Cookie
|
|
@@ -168,13 +221,63 @@ from modmex_lambda.shared.cookies import Cookie
|
|
|
168
221
|
|
|
169
222
|
@app.get("/session")
|
|
170
223
|
def session():
|
|
224
|
+
user = User(id=42, name="Ada")
|
|
171
225
|
return Response(
|
|
172
226
|
status_code=200,
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
],
|
|
175
240
|
)
|
|
176
241
|
```
|
|
177
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)
|
|
263
|
+
return Response(
|
|
264
|
+
status_code=200,
|
|
265
|
+
content_type="image/png",
|
|
266
|
+
body=image_bytes,
|
|
267
|
+
headers={"Cache-Control": "max-age=3600"},
|
|
268
|
+
)
|
|
269
|
+
```
|
|
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`.
|
|
178
281
|
REST API responses use `multiValueHeaders`; HTTP API responses use the v2
|
|
179
282
|
`headers` and `cookies` shape.
|
|
180
283
|
|
|
@@ -407,8 +510,69 @@ def lambda_handler(event: APIGatewayHttpEvent, context):
|
|
|
407
510
|
|
|
408
511
|
## Validation
|
|
409
512
|
|
|
410
|
-
Modmex is the default validation and coercion engine.
|
|
411
|
-
|
|
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.
|
|
412
576
|
|
|
413
577
|
## Logging
|
|
414
578
|
|
|
@@ -426,18 +590,6 @@ def lambda_handler(event, context):
|
|
|
426
590
|
The logger emits structured JSON and can extract Lambda request IDs and API
|
|
427
591
|
Gateway correlation IDs.
|
|
428
592
|
|
|
429
|
-
## Benchmarks
|
|
430
|
-
|
|
431
|
-
The benchmark suite lives in `.benchmark/api_gateway_benchmark.py`.
|
|
432
|
-
|
|
433
|
-
It covers cold imports, app setup, route registration, API Gateway v1/v2
|
|
434
|
-
invocation, `event_parser`, `event_source`, and logger hot paths.
|
|
435
|
-
|
|
436
|
-
```bash
|
|
437
|
-
poetry run python .benchmark/api_gateway_benchmark.py
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
More details are in `.benchmark/README.md`.
|
|
441
593
|
|
|
442
594
|
## Limitations
|
|
443
595
|
|
|
@@ -27,7 +27,7 @@ modmex_lambda/event_handler/middlewares.py,sha256=oOsQ0nOl7lc_N-kt8wTx7qOXNE5VtR
|
|
|
27
27
|
modmex_lambda/event_handler/params.py,sha256=nNhLKduyeQ6TI9Zli32_XtRM93e973igOOZSY0vcpy4,544
|
|
28
28
|
modmex_lambda/event_handler/request.py,sha256=f5TMv6muYfYOsLbZMPdd6kRMza8U2-UiyNmvuB5grH4,1705
|
|
29
29
|
modmex_lambda/event_handler/response.py,sha256=izW9v_0E91UsQKX6-W26wnuAy-YP2_kTPbA5OW9L3tM,1988
|
|
30
|
-
modmex_lambda/event_handler/routing.py,sha256=
|
|
30
|
+
modmex_lambda/event_handler/routing.py,sha256=ei2cZNwf850cPrOwMt-_LEoP9VEH6bSDpll5SgtVve8,17443
|
|
31
31
|
modmex_lambda/event_handler/routing_fallbacks.py,sha256=vx23xyg4vEKCqGg1bxBP0OxRsN8NbMkAdehfcNiqwjw,3669
|
|
32
32
|
modmex_lambda/event_handler/types.py,sha256=VRYaZVipqvwT_PyJ5teGADPIDyt_xRpqJ4bmqhd-lIA,1184
|
|
33
33
|
modmex_lambda/event_handler/dependencies/__init__.py,sha256=KqYkgjAYNlZ2_PIDWAQSG4syIgeTdVmDQC5_lVjfyKI,288
|
|
@@ -42,7 +42,7 @@ modmex_lambda/shared/cookies.py,sha256=E2hTuht5_Xljw9zOQLW6vjsOWEpQSIqD2D3Tl1Co_
|
|
|
42
42
|
modmex_lambda/shared/headers_serializer.py,sha256=Ao-wpx0cmy9E2ACrh7R7RIDUF0h24gax1AMtY_MjIWQ,2505
|
|
43
43
|
modmex_lambda/shared/json_encoder.py,sha256=Mo4tlrhWvlBhhRYoPt2PAP6b_KsEKbk1pK6e5q0gbuk,1297
|
|
44
44
|
modmex_lambda/shared/types.py,sha256=lH3i9MF7EQa1et8dD_qagEjlNzqznNHc0YZawp3toMc,134
|
|
45
|
-
modmex_lambda-0.
|
|
46
|
-
modmex_lambda-0.
|
|
47
|
-
modmex_lambda-0.
|
|
48
|
-
modmex_lambda-0.
|
|
45
|
+
modmex_lambda-0.3.0.dist-info/METADATA,sha256=w87GM_X_jWixeYRoMBZEjx7KnlpkzMkhu6mIvro54E0,14774
|
|
46
|
+
modmex_lambda-0.3.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
47
|
+
modmex_lambda-0.3.0.dist-info/licenses/LICENSE,sha256=_C2TDTOsYeJvE4vn9VB51laKvleBKbdNnn96wJVtXhQ,1063
|
|
48
|
+
modmex_lambda-0.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|