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.
@@ -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.2.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 = app.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
- body={"ok": True},
174
- cookies=[Cookie("seen", "true", secure=True)],
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. Pydantic is not required
411
- for normal operation.
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=MqwHvf9d4qu_1U-qxx85q5m22BQ3kAzfhPeJeMUyaTs,16146
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.2.0.dist-info/METADATA,sha256=J_gbX6PVZhgRU5pv3fnY3pGeL1Mc1NzO9EznRQ4BTcI,10816
46
- modmex_lambda-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
47
- modmex_lambda-0.2.0.dist-info/licenses/LICENSE,sha256=_C2TDTOsYeJvE4vn9VB51laKvleBKbdNnn96wJVtXhQ,1063
48
- modmex_lambda-0.2.0.dist-info/RECORD,,
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,,