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.
Files changed (73) hide show
  1. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/.github/workflows/ci.yml +1 -0
  2. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/PKG-INFO +244 -21
  3. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/README.md +241 -20
  4. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/__init__.py +12 -1
  5. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/__init__.py +7 -1
  6. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/api_gateway.py +3 -0
  7. modmex_lambda-0.3.0/modmex_lambda/event_handler/dependencies/__init__.py +13 -0
  8. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/dependencies/dependant.py +4 -1
  9. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/dependencies/depends.py +85 -5
  10. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/routing.py +44 -1
  11. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/types.py +2 -0
  12. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/poetry.lock +18 -1
  13. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/pyproject.toml +2 -1
  14. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/event_handler/test_api_gateway.py +140 -1
  15. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/event_handler/test_dependencies.py +47 -0
  16. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/event_handler/test_routing.py +21 -1
  17. modmex_lambda-0.1.0/tests/event_handler/__init__.py +0 -0
  18. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/.github/workflows/release.yml +0 -0
  19. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/.gitignore +0 -0
  20. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/LICENSE +0 -0
  21. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/data_classes/__init__.py +0 -0
  22. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/data_classes/api_gateway_authorizer_event.py +0 -0
  23. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/data_classes/api_gateway_proxy_event.py +0 -0
  24. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/data_classes/api_gateway_websocket_event.py +0 -0
  25. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/data_classes/cognito_user_pool_event.py +0 -0
  26. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/data_classes/common.py +0 -0
  27. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/constants.py +0 -0
  28. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/content_types.py +0 -0
  29. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/cors.py +0 -0
  30. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/dependencies/compat.py +0 -0
  31. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/dependencies/dependency_middleware.py +0 -0
  32. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/dependencies/params.py +0 -0
  33. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/dependencies/types.py +0 -0
  34. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/exception_handler.py +0 -0
  35. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/exceptions.py +0 -0
  36. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/gateway_response.py +0 -0
  37. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/middlewares.py +0 -0
  38. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/params.py +0 -0
  39. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/request.py +0 -0
  40. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/response.py +0 -0
  41. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_handler/routing_fallbacks.py +0 -0
  42. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/event_sources.py +0 -0
  43. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/exceptions.py +0 -0
  44. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/logging.py +0 -0
  45. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/params.py +0 -0
  46. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/parser.py +0 -0
  47. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/request.py +0 -0
  48. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/resolver.py +0 -0
  49. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/response.py +0 -0
  50. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/routing.py +0 -0
  51. {modmex_lambda-0.1.0/modmex_lambda/event_handler/dependencies → modmex_lambda-0.3.0/modmex_lambda/shared}/__init__.py +0 -0
  52. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/shared/cookies.py +0 -0
  53. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/shared/headers_serializer.py +0 -0
  54. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/shared/json_encoder.py +0 -0
  55. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/shared/types.py +0 -0
  56. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/modmex_lambda/validation.py +0 -0
  57. {modmex_lambda-0.1.0/modmex_lambda/shared → modmex_lambda-0.3.0/tests}/__init__.py +0 -0
  58. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/conftest.py +0 -0
  59. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/data_classes/test_api_gateway_proxy_event.py +0 -0
  60. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/data_classes/test_cognito_user_pool_event.py +0 -0
  61. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/data_classes/test_common.py +0 -0
  62. {modmex_lambda-0.1.0/tests → modmex_lambda-0.3.0/tests/event_handler}/__init__.py +0 -0
  63. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/event_handler/test_cors.py +0 -0
  64. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/event_handler/test_exception_handler.py +0 -0
  65. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/event_handler/test_gateway_response.py +0 -0
  66. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/event_handler/test_request.py +0 -0
  67. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/event_handler/test_response.py +0 -0
  68. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/shared/test_cookies_headers.py +0 -0
  69. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/shared/test_json_encoder.py +0 -0
  70. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/test_logging.py +0 -0
  71. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/test_parser_event_sources.py +0 -0
  72. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/test_reexports.py +0 -0
  73. {modmex_lambda-0.1.0 → modmex_lambda-0.3.0}/tests/test_validation.py +0 -0
@@ -8,6 +8,7 @@ on:
8
8
 
9
9
  jobs:
10
10
  test:
11
+ name: CI Tests
11
12
  runs-on: ubuntu-latest
12
13
  strategy:
13
14
  fail-fast: false
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modmex-lambda
3
- Version: 0.1.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
- For local development:
31
+
32
+ To use the optional `injector` integration:
30
33
 
31
34
  ```bash
32
- poetry install --extras dev
33
- poetry run pytest -q
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 = app.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
- body={"ok": True},
166
- cookies=[Cookie("seen", "true", secure=True)],
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. Pydantic is not required
340
- 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.
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
- For local development:
16
+
17
+ To use the optional `injector` integration:
17
18
 
18
19
  ```bash
19
- poetry install --extras dev
20
- poetry run pytest -q
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 = app.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
- body={"ok": True},
153
- cookies=[Cookie("seen", "true", secure=True)],
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. Pydantic is not required
327
- for normal operation.
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 Depends
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",