modmex-lambda 0.1.0__tar.gz → 0.2.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.2.0}/.github/workflows/ci.yml +1 -0
  2. modmex_lambda-0.1.0/README.md → modmex_lambda-0.2.0/PKG-INFO +87 -3
  3. modmex_lambda-0.1.0/PKG-INFO → modmex_lambda-0.2.0/README.md +72 -16
  4. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/__init__.py +12 -1
  5. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/__init__.py +7 -1
  6. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/api_gateway.py +3 -0
  7. modmex_lambda-0.2.0/modmex_lambda/event_handler/dependencies/__init__.py +13 -0
  8. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/dependencies/dependant.py +4 -1
  9. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/dependencies/depends.py +85 -5
  10. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/routing.py +1 -0
  11. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/types.py +2 -0
  12. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/poetry.lock +18 -1
  13. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/pyproject.toml +2 -1
  14. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/tests/event_handler/test_api_gateway.py +120 -0
  15. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/tests/event_handler/test_dependencies.py +47 -0
  16. modmex_lambda-0.1.0/tests/event_handler/__init__.py +0 -0
  17. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/.github/workflows/release.yml +0 -0
  18. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/.gitignore +0 -0
  19. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/LICENSE +0 -0
  20. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/data_classes/__init__.py +0 -0
  21. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/data_classes/api_gateway_authorizer_event.py +0 -0
  22. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/data_classes/api_gateway_proxy_event.py +0 -0
  23. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/data_classes/api_gateway_websocket_event.py +0 -0
  24. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/data_classes/cognito_user_pool_event.py +0 -0
  25. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/data_classes/common.py +0 -0
  26. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/constants.py +0 -0
  27. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/content_types.py +0 -0
  28. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/cors.py +0 -0
  29. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/dependencies/compat.py +0 -0
  30. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/dependencies/dependency_middleware.py +0 -0
  31. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/dependencies/params.py +0 -0
  32. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/dependencies/types.py +0 -0
  33. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/exception_handler.py +0 -0
  34. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/exceptions.py +0 -0
  35. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/gateway_response.py +0 -0
  36. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/middlewares.py +0 -0
  37. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/params.py +0 -0
  38. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/request.py +0 -0
  39. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/response.py +0 -0
  40. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_handler/routing_fallbacks.py +0 -0
  41. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/event_sources.py +0 -0
  42. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/exceptions.py +0 -0
  43. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/logging.py +0 -0
  44. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/params.py +0 -0
  45. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/parser.py +0 -0
  46. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/request.py +0 -0
  47. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/resolver.py +0 -0
  48. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/response.py +0 -0
  49. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/routing.py +0 -0
  50. {modmex_lambda-0.1.0/modmex_lambda/event_handler/dependencies → modmex_lambda-0.2.0/modmex_lambda/shared}/__init__.py +0 -0
  51. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/shared/cookies.py +0 -0
  52. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/shared/headers_serializer.py +0 -0
  53. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/shared/json_encoder.py +0 -0
  54. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/shared/types.py +0 -0
  55. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/modmex_lambda/validation.py +0 -0
  56. {modmex_lambda-0.1.0/modmex_lambda/shared → modmex_lambda-0.2.0/tests}/__init__.py +0 -0
  57. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/tests/conftest.py +0 -0
  58. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/tests/data_classes/test_api_gateway_proxy_event.py +0 -0
  59. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/tests/data_classes/test_cognito_user_pool_event.py +0 -0
  60. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/tests/data_classes/test_common.py +0 -0
  61. {modmex_lambda-0.1.0/tests → modmex_lambda-0.2.0/tests/event_handler}/__init__.py +0 -0
  62. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/tests/event_handler/test_cors.py +0 -0
  63. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/tests/event_handler/test_exception_handler.py +0 -0
  64. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/tests/event_handler/test_gateway_response.py +0 -0
  65. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/tests/event_handler/test_request.py +0 -0
  66. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/tests/event_handler/test_response.py +0 -0
  67. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/tests/event_handler/test_routing.py +0 -0
  68. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/tests/shared/test_cookies_headers.py +0 -0
  69. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/tests/shared/test_json_encoder.py +0 -0
  70. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/tests/test_logging.py +0 -0
  71. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/tests/test_parser_event_sources.py +0 -0
  72. {modmex_lambda-0.1.0 → modmex_lambda-0.2.0}/tests/test_reexports.py +0 -0
  73. {modmex_lambda-0.1.0 → modmex_lambda-0.2.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,3 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: modmex-lambda
3
+ Version: 0.2.0
4
+ Summary: Ultra-lightweight AWS Lambda utilities for API Gateway routing and event handling.
5
+ Author: Modmex
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: modmex<2.0.0,>=1.1.10
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=8.0; extra == 'dev'
12
+ Provides-Extra: injector
13
+ Requires-Dist: injector<1.0.0,>=0.24.0; extra == 'injector'
14
+ Description-Content-Type: text/markdown
15
+
1
16
  # modmex-lambda
2
17
 
3
18
  Ultra-lightweight AWS Lambda utilities for API Gateway-first workloads.
@@ -13,11 +28,17 @@ logging with a small dependency footprint.
13
28
  pip install modmex-lambda
14
29
  ```
15
30
 
16
- For local development:
31
+
32
+ To use the optional `injector` integration:
17
33
 
18
34
  ```bash
19
- poetry install --extras dev
20
- poetry run pytest -q
35
+ pip install "modmex-lambda[injector]"
36
+ ```
37
+
38
+ With Poetry:
39
+
40
+ ```bash
41
+ poetry add "modmex-lambda[injector]"
21
42
  ```
22
43
 
23
44
  ## API Gateway Resolvers
@@ -223,6 +244,69 @@ def get_user(
223
244
  return repository.get_user(user_id)
224
245
  ```
225
246
 
247
+ For constructor-heavy services, install the optional `injector` extra and pass
248
+ an `InjectorDependencyResolver` to the app. `Depends()` without a callable uses
249
+ the parameter annotation as the dependency token.
250
+
251
+ ```python
252
+ from typing import Annotated
253
+
254
+ from injector import Injector, Module, inject, provider, singleton
255
+ from modmex_lambda import ApiGatewayHttpResolver, Depends, InjectorDependencyResolver
256
+ from modmex_lambda.event_handler.params import Path
257
+
258
+
259
+ class Settings:
260
+ def __init__(self, tenant_id: str):
261
+ self.tenant_id = tenant_id
262
+
263
+
264
+ class UserRepository:
265
+ def __init__(self, settings: Settings):
266
+ self.settings = settings
267
+
268
+ def get_user(self, user_id: int) -> dict:
269
+ return {"id": user_id, "tenant_id": self.settings.tenant_id}
270
+
271
+
272
+ class UserService:
273
+ def __init__(self, repository: UserRepository):
274
+ self.repository = repository
275
+
276
+ def get_user(self, user_id: int) -> dict:
277
+ return self.repository.get_user(user_id)
278
+
279
+
280
+ class AppModule(Module):
281
+ @singleton
282
+ @provider
283
+ def provide_settings(self) -> Settings:
284
+ return Settings(tenant_id="mx")
285
+
286
+ @singleton
287
+ @provider
288
+ @inject
289
+ def provide_repository(self, settings: Settings) -> UserRepository:
290
+ return UserRepository(settings)
291
+
292
+ @singleton
293
+ @provider
294
+ @inject
295
+ def provide_service(self, repository: UserRepository) -> UserService:
296
+ return UserService(repository)
297
+
298
+
299
+ container = Injector([AppModule()])
300
+ app = ApiGatewayHttpResolver(dependency_resolver=InjectorDependencyResolver(container))
301
+
302
+ @app.get("/users/<user_id>")
303
+ def get_user(
304
+ user_id: Annotated[int, Path()],
305
+ service: Annotated[UserService, Depends()],
306
+ ):
307
+ return service.get_user(user_id)
308
+ ```
309
+
226
310
  Disable dependency caching when a dependency must run every time:
227
311
 
228
312
  ```python
@@ -1,16 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: modmex-lambda
3
- Version: 0.1.0
4
- Summary: Ultra-lightweight AWS Lambda utilities for API Gateway routing and event handling.
5
- Author: Modmex
6
- License: MIT
7
- License-File: LICENSE
8
- Requires-Python: >=3.10
9
- Requires-Dist: modmex<2.0.0,>=1.1.10
10
- Provides-Extra: dev
11
- Requires-Dist: pytest>=8.0; extra == 'dev'
12
- Description-Content-Type: text/markdown
13
-
14
1
  # modmex-lambda
15
2
 
16
3
  Ultra-lightweight AWS Lambda utilities for API Gateway-first workloads.
@@ -26,11 +13,17 @@ logging with a small dependency footprint.
26
13
  pip install modmex-lambda
27
14
  ```
28
15
 
29
- For local development:
16
+
17
+ To use the optional `injector` integration:
30
18
 
31
19
  ```bash
32
- poetry install --extras dev
33
- poetry run pytest -q
20
+ pip install "modmex-lambda[injector]"
21
+ ```
22
+
23
+ With Poetry:
24
+
25
+ ```bash
26
+ poetry add "modmex-lambda[injector]"
34
27
  ```
35
28
 
36
29
  ## API Gateway Resolvers
@@ -236,6 +229,69 @@ def get_user(
236
229
  return repository.get_user(user_id)
237
230
  ```
238
231
 
232
+ For constructor-heavy services, install the optional `injector` extra and pass
233
+ an `InjectorDependencyResolver` to the app. `Depends()` without a callable uses
234
+ the parameter annotation as the dependency token.
235
+
236
+ ```python
237
+ from typing import Annotated
238
+
239
+ from injector import Injector, Module, inject, provider, singleton
240
+ from modmex_lambda import ApiGatewayHttpResolver, Depends, InjectorDependencyResolver
241
+ from modmex_lambda.event_handler.params import Path
242
+
243
+
244
+ class Settings:
245
+ def __init__(self, tenant_id: str):
246
+ self.tenant_id = tenant_id
247
+
248
+
249
+ class UserRepository:
250
+ def __init__(self, settings: Settings):
251
+ self.settings = settings
252
+
253
+ def get_user(self, user_id: int) -> dict:
254
+ return {"id": user_id, "tenant_id": self.settings.tenant_id}
255
+
256
+
257
+ class UserService:
258
+ def __init__(self, repository: UserRepository):
259
+ self.repository = repository
260
+
261
+ def get_user(self, user_id: int) -> dict:
262
+ return self.repository.get_user(user_id)
263
+
264
+
265
+ class AppModule(Module):
266
+ @singleton
267
+ @provider
268
+ def provide_settings(self) -> Settings:
269
+ return Settings(tenant_id="mx")
270
+
271
+ @singleton
272
+ @provider
273
+ @inject
274
+ def provide_repository(self, settings: Settings) -> UserRepository:
275
+ return UserRepository(settings)
276
+
277
+ @singleton
278
+ @provider
279
+ @inject
280
+ def provide_service(self, repository: UserRepository) -> UserService:
281
+ return UserService(repository)
282
+
283
+
284
+ container = Injector([AppModule()])
285
+ app = ApiGatewayHttpResolver(dependency_resolver=InjectorDependencyResolver(container))
286
+
287
+ @app.get("/users/<user_id>")
288
+ def get_user(
289
+ user_id: Annotated[int, Path()],
290
+ service: Annotated[UserService, Depends()],
291
+ ):
292
+ return service.get_user(user_id)
293
+ ```
294
+
239
295
  Disable dependency caching when a dependency must run every time:
240
296
 
241
297
  ```python
@@ -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",
@@ -10,13 +10,16 @@ if TYPE_CHECKING:
10
10
  ApiGatewayRestResolver,
11
11
  Response,
12
12
  )
13
- from .dependencies.depends import Depends
13
+ from .dependencies.depends import DefaultDependencyResolver, DependencyResolver, Depends, InjectorDependencyResolver
14
14
 
15
15
  _EXPORTS = {
16
16
  "ApiGatewayHttpResolver": ("modmex_lambda.event_handler.api_gateway", "ApiGatewayHttpResolver"),
17
17
  "ApiGatewayRestResolver": ("modmex_lambda.event_handler.api_gateway", "ApiGatewayRestResolver"),
18
18
  "Response": ("modmex_lambda.event_handler.api_gateway", "Response"),
19
+ "DefaultDependencyResolver": ("modmex_lambda.event_handler.dependencies.depends", "DefaultDependencyResolver"),
20
+ "DependencyResolver": ("modmex_lambda.event_handler.dependencies.depends", "DependencyResolver"),
19
21
  "Depends": ("modmex_lambda.event_handler.dependencies.depends", "Depends"),
22
+ "InjectorDependencyResolver": ("modmex_lambda.event_handler.dependencies.depends", "InjectorDependencyResolver"),
20
23
  "content_types": ("modmex_lambda.event_handler.content_types", None),
21
24
  }
22
25
 
@@ -24,7 +27,10 @@ __all__ = [
24
27
  "ApiGatewayHttpResolver",
25
28
  "ApiGatewayRestResolver",
26
29
  "Response",
30
+ "DefaultDependencyResolver",
31
+ "DependencyResolver",
27
32
  "Depends",
33
+ "InjectorDependencyResolver",
28
34
  "content_types",
29
35
  ]
30
36
 
@@ -27,6 +27,7 @@ from modmex_lambda.data_classes.api_gateway_proxy_event import APIGatewayProxyEv
27
27
  from modmex_lambda.shared.types import AnyCallableT
28
28
  from modmex_lambda.event_handler.types import IApiGatewayResolver
29
29
  from modmex_lambda.event_handler.dependencies.dependency_middleware import DependencyMiddleware
30
+ from modmex_lambda.event_handler.dependencies.depends import DefaultDependencyResolver, DependencyResolver
30
31
  from modmex_lambda.event_handler.middlewares import NextMiddleware
31
32
  from modmex_lambda.event_handler.cors import CORSConfig
32
33
  from modmex_lambda.shared.json_encoder import JSONEncoder
@@ -104,6 +105,7 @@ class ApiGatewayResolver(BaseRouter, IApiGatewayResolver):
104
105
  strip_prefixes: list[str | Pattern] | None = None,
105
106
  json_body_deserializer: Callable[[str], dict] | None = None,
106
107
  logger: Any | None = None,
108
+ dependency_resolver: DependencyResolver | None = None,
107
109
  ) -> None:
108
110
  self._cors = cors
109
111
  self._cors_enabled = cors is not None
@@ -118,6 +120,7 @@ class ApiGatewayResolver(BaseRouter, IApiGatewayResolver):
118
120
  self._router_middlewares: list[Middleware] = []
119
121
 
120
122
  self._logger = logger
123
+ self.dependency_resolver = dependency_resolver or DefaultDependencyResolver()
121
124
  self.dependency_overrides: dict[Callable[..., Any], Callable[..., Any]] = {}
122
125
  self.current_event = None
123
126
  self.current_context = None
@@ -0,0 +1,13 @@
1
+ from modmex_lambda.event_handler.dependencies.depends import (
2
+ DefaultDependencyResolver,
3
+ DependencyResolver,
4
+ Depends,
5
+ InjectorDependencyResolver,
6
+ )
7
+
8
+ __all__ = [
9
+ "DefaultDependencyResolver",
10
+ "DependencyResolver",
11
+ "Depends",
12
+ "InjectorDependencyResolver",
13
+ ]
@@ -131,12 +131,15 @@ def get_dependant(
131
131
 
132
132
  depends = _get_depends_from_annotation(param.annotation)
133
133
  if depends is not None:
134
+ depends = depends.resolve_for_annotation(param.annotation)
134
135
  _inherit_local_namespace(parent=call, dependency=depends.dependency)
135
136
  dependant.dependencies.append(
136
137
  DependencyParam(
137
138
  param_name=param_name,
138
139
  depends=depends,
139
- dependant=get_dependant(path=path, call=depends.dependency),
140
+ dependant=Dependant(call=depends.dependency)
141
+ if inspect.isclass(depends.dependency)
142
+ else get_dependant(path=path, call=depends.dependency),
140
143
  ),
141
144
  )
142
145
  continue
@@ -2,7 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Annotated, Any, Callable, get_args, get_origin, get_type_hints
5
+ import inspect
6
+ from typing import Annotated, Any, Callable, Protocol, get_args, get_origin, get_type_hints
6
7
 
7
8
  from modmex_lambda.event_handler.dependencies.compat import ModelField
8
9
  from modmex_lambda.event_handler.dependencies.types import CacheKey
@@ -43,15 +44,83 @@ class DependencyResolutionError(Exception):
43
44
  """Raised when a dependency cannot be resolved."""
44
45
 
45
46
 
47
+ class DependencyResolver(Protocol):
48
+ """Resolves a dependency token into the value passed to a route handler."""
49
+
50
+ def resolve(
51
+ self,
52
+ dependency: Callable[..., Any] | type[Any],
53
+ *,
54
+ values: dict[str, Any] | None = None,
55
+ request: Request | None = None,
56
+ ) -> Any:
57
+ ...
58
+
59
+
60
+ class DefaultDependencyResolver:
61
+ """Default resolver that calls dependency functions with solved values."""
62
+
63
+ def resolve(
64
+ self,
65
+ dependency: Callable[..., Any] | type[Any],
66
+ *,
67
+ values: dict[str, Any] | None = None,
68
+ request: Request | None = None,
69
+ ) -> Any:
70
+ return dependency(**(values or {}))
71
+
72
+
73
+ class InjectorDependencyResolver:
74
+ """Optional adapter for the ``injector`` package."""
75
+
76
+ def __init__(self, injector: Any) -> None:
77
+ self.injector = injector
78
+
79
+ def resolve(
80
+ self,
81
+ dependency: Callable[..., Any] | type[Any],
82
+ *,
83
+ values: dict[str, Any] | None = None,
84
+ request: Request | None = None,
85
+ ) -> Any:
86
+ kwargs = values or {}
87
+
88
+ if hasattr(self.injector, "call_with_injection") and not inspect.isclass(dependency):
89
+ return self.injector.call_with_injection(dependency, kwargs=kwargs)
90
+
91
+ if not kwargs and inspect.isclass(dependency) and hasattr(self.injector, "get"):
92
+ return self.injector.get(dependency)
93
+
94
+ return dependency(**kwargs)
95
+
96
+
46
97
  class Depends:
47
- def __init__(self, dependency: Callable[..., Any], *, use_cache: bool = True) -> None:
48
- if not callable(dependency):
98
+ def __init__(
99
+ self,
100
+ dependency: Callable[..., Any] | type[Any] | None = None,
101
+ *,
102
+ use_cache: bool = True,
103
+ ) -> None:
104
+ if dependency is not None and not callable(dependency):
49
105
  raise DependencyResolutionError(
50
106
  f"Depends() requires a callable, got {type(dependency).__name__}: {dependency!r}",
51
107
  )
52
108
  self.dependency = dependency
53
109
  self.use_cache = use_cache
54
110
 
111
+ def resolve_for_annotation(self, annotation: Any) -> Depends:
112
+ if self.dependency is not None:
113
+ return self
114
+
115
+ if get_origin(annotation) is Annotated:
116
+ dependency = get_args(annotation)[0]
117
+ if callable(dependency):
118
+ return Depends(dependency, use_cache=self.use_cache)
119
+
120
+ raise DependencyResolutionError(
121
+ "Depends() without a callable requires a callable parameter annotation",
122
+ )
123
+
55
124
 
56
125
  class _DependencyNode:
57
126
  """Lightweight node in a dependency tree."""
@@ -101,12 +170,17 @@ def build_dependency_tree(func: Callable[..., Any]) -> DependencyTree:
101
170
  depends = _get_depends_from_annotation(annotation)
102
171
  if depends is None:
103
172
  continue
173
+ depends = depends.resolve_for_annotation(annotation)
104
174
 
105
175
  dependencies.append(
106
176
  _DependencyNode(
107
177
  param_name=param_name,
108
178
  depends=depends,
109
- sub_tree=build_dependency_tree(depends.dependency),
179
+ sub_tree=(
180
+ DependencyTree()
181
+ if inspect.isclass(depends.dependency)
182
+ else build_dependency_tree(depends.dependency)
183
+ ),
110
184
  ),
111
185
  )
112
186
 
@@ -119,12 +193,14 @@ def solve_dependencies(
119
193
  request: Request | None = None,
120
194
  dependency_overrides: dict[Callable[..., Any], Callable[..., Any]] | None = None,
121
195
  dependency_cache: dict[Callable[..., Any], Any] | None = None,
196
+ dependency_resolver: DependencyResolver | None = None,
122
197
  ) -> dict[str, Any]:
123
198
  """Resolve all Depends parameters for a route or lightweight dependency tree."""
124
199
  from modmex_lambda.event_handler.request import Request as RequestClass
125
200
 
126
201
  cache = dependency_cache if dependency_cache is not None else {}
127
202
  overrides = dependency_overrides or {}
203
+ resolver = dependency_resolver or DefaultDependencyResolver()
128
204
  values: dict[str, Any] = {}
129
205
 
130
206
  for dep in dependant.dependencies:
@@ -139,11 +215,12 @@ def solve_dependencies(
139
215
  request=request,
140
216
  dependency_overrides=overrides,
141
217
  dependency_cache=cache,
218
+ dependency_resolver=resolver,
142
219
  )
143
220
  sub_values.update(_request_injection_values(dependency, request, RequestClass))
144
221
 
145
222
  try:
146
- solved = dependency(**sub_values)
223
+ solved = resolver.resolve(dependency, values=sub_values, request=request)
147
224
  except Exception as exc:
148
225
  dep_name = getattr(dependency, "__name__", repr(dependency))
149
226
  raise DependencyResolutionError(
@@ -177,7 +254,10 @@ def _request_injection_values(
177
254
  __all__ = [
178
255
  "Dependant",
179
256
  "DependencyParam",
257
+ "DependencyResolver",
258
+ "DefaultDependencyResolver",
180
259
  "Depends",
260
+ "InjectorDependencyResolver",
181
261
  "_get_depends_from_annotation",
182
262
  "build_dependency_tree",
183
263
  "solve_dependencies",
@@ -492,6 +492,7 @@ class RouteEndpointInvoker:
492
492
  dependant=route.dependant,
493
493
  request=app.request,
494
494
  dependency_overrides=app.dependency_overrides or None,
495
+ dependency_resolver=app.dependency_resolver,
495
496
  ),
496
497
  )
497
498
 
@@ -4,6 +4,7 @@ from typing import Any, Callable, TypeVar, Union
4
4
 
5
5
  from abc import ABC
6
6
  from modmex_lambda.data_classes.api_gateway_proxy_event import APIGatewayProxyEvent, APIGatewayProxyEventV2
7
+ from modmex_lambda.event_handler.dependencies.depends import DependencyResolver
7
8
  from modmex_lambda.event_handler.request import Request
8
9
  from modmex_lambda.event_handler.response import Response
9
10
 
@@ -11,6 +12,7 @@ from modmex_lambda.event_handler.response import Response
11
12
  class IApiGatewayResolver(ABC):
12
13
  context: dict[str, Any]
13
14
  current_event: Union[APIGatewayProxyEvent, APIGatewayProxyEventV2]
15
+ dependency_resolver: DependencyResolver
14
16
  dependency_overrides: dict[Callable[..., Any], Callable[..., Any]]
15
17
  _dependency_middleware: Callable[..., Any]
16
18
 
@@ -167,6 +167,22 @@ files = [
167
167
  ]
168
168
  markers = {main = "extra == \"dev\""}
169
169
 
170
+ [[package]]
171
+ name = "injector"
172
+ version = "0.24.0"
173
+ description = "Injector - Python dependency injection framework, inspired by Guice"
174
+ optional = true
175
+ python-versions = "*"
176
+ groups = ["main"]
177
+ markers = "extra == \"injector\""
178
+ files = [
179
+ {file = "injector-0.24.0-py3-none-any.whl", hash = "sha256:47294c7a7fdb811f0d1b442a1e0152bb2fc28b2ccaaba4cba44e5e125e0da2d0"},
180
+ {file = "injector-0.24.0.tar.gz", hash = "sha256:e85a75d1516cff2f03170f3fd1219f56acb25c9a05e307819ae0dcde3dad3d3f"},
181
+ ]
182
+
183
+ [package.extras]
184
+ dev = ["black (==24.3.0) ; implementation_name == \"cpython\"", "build (==1.0.3)", "check-manifest (==0.49)", "click (==8.1.7)", "coverage[toml] (==7.3.2)", "exceptiongroup (==1.2.0)", "importlib-metadata (==7.0.0)", "iniconfig (==2.0.0)", "mypy (==1.7.1) ; implementation_name == \"cpython\"", "mypy-extensions (==1.0.0)", "packaging (==25.0)", "pathspec (==0.12.1)", "platformdirs (==4.1.0)", "pluggy (==1.3.0)", "pyproject-hooks (==1.0.0)", "pytest (==7.4.3)", "pytest-cov (==4.1.0)", "tomli (==2.0.1)", "typing-extensions (==4.9.0) ; python_version < \"3.9\"", "zipp (==3.19.1)"]
185
+
170
186
  [[package]]
171
187
  name = "modmex"
172
188
  version = "1.1.10"
@@ -432,8 +448,9 @@ markers = {main = "extra == \"dev\" and python_version == \"3.10\"", dev = "pyth
432
448
 
433
449
  [extras]
434
450
  dev = ["pytest"]
451
+ injector = ["injector"]
435
452
 
436
453
  [metadata]
437
454
  lock-version = "2.1"
438
455
  python-versions = ">=3.10"
439
- content-hash = "7417c68d7731202521d408794e74b5caacc8d03b334529a981c6f8c867174b90"
456
+ content-hash = "76e16dbf080a877a8ede81c719d72cd7a6023c75506305088b55d814d3cae4a4"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "modmex-lambda"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Ultra-lightweight AWS Lambda utilities for API Gateway routing and event handling."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -16,6 +16,7 @@ dependencies = [
16
16
 
17
17
  [project.optional-dependencies]
18
18
  dev = ["pytest>=8.0"]
19
+ injector = ["injector>=0.24.0,<1.0.0"]
19
20
 
20
21
  [tool.pytest.ini_options]
21
22
  pythonpath = ["."]
@@ -29,6 +29,35 @@ class CreateUserRequest:
29
29
  self.age = age
30
30
 
31
31
 
32
+ class Settings:
33
+ def __init__(self, tenant_id: str) -> None:
34
+ self.tenant_id = tenant_id
35
+
36
+
37
+ class UserRepository:
38
+ def __init__(self, settings: Settings) -> None:
39
+ self.settings = settings
40
+
41
+ def get_user(self, user_id: int) -> dict[str, str | int]:
42
+ return {"id": user_id, "tenant_id": self.settings.tenant_id}
43
+
44
+
45
+ class UserService:
46
+ def __init__(self, repository: UserRepository) -> None:
47
+ self.repository = repository
48
+
49
+ def get_user(self, user_id: int) -> dict[str, str | int]:
50
+ return self.repository.get_user(user_id)
51
+
52
+
53
+ class AuditService:
54
+ def __init__(self, tenant_id: str) -> None:
55
+ self.tenant_id = tenant_id
56
+
57
+ def record(self, user_id: int) -> dict[str, str | int]:
58
+ return {"user_id": user_id, "tenant_id": self.tenant_id}
59
+
60
+
32
61
  def test_public_api_requires_explicit_api_gateway_resolver() -> None:
33
62
  assert modmex_lambda.ApiGatewayHttpResolver is ApiGatewayHttpResolver
34
63
  assert modmex_lambda.ApiGatewayRestResolver is ApiGatewayRestResolver
@@ -437,6 +466,97 @@ def test_dependency_injection_without_cache() -> None:
437
466
  assert response_body(response) == {"a": 1, "b": 2}
438
467
 
439
468
 
469
+ def test_dependency_injection_uses_custom_dependency_resolver_for_annotation_token() -> None:
470
+ class UserService:
471
+ def __init__(self, tenant_id: str) -> None:
472
+ self.tenant_id = tenant_id
473
+
474
+ class Resolver:
475
+ def resolve(self, dependency, *, values=None, request=None):
476
+ assert dependency is UserService
477
+ assert values == {}
478
+ assert request.headers["x-tenant-id"] == "mx"
479
+ return UserService(tenant_id="mx")
480
+
481
+ app = ApiGatewayHttpResolver(dependency_resolver=Resolver())
482
+
483
+ @app.get("/di-resolver")
484
+ def handler(service: Annotated[UserService, Depends()]):
485
+ return {"tenant_id": service.tenant_id}
486
+
487
+ response = app.resolve(http_v2_event("GET", "/di-resolver", headers={"x-tenant-id": "mx"}), object())
488
+
489
+ assert response_body(response) == {"tenant_id": "mx"}
490
+
491
+
492
+ def test_http_resolver_supports_real_injector_class_token_dependency() -> None:
493
+ pytest.importorskip("injector")
494
+ from injector import Injector, Module, inject, provider, singleton
495
+ from modmex_lambda import InjectorDependencyResolver
496
+
497
+ class MyModule(Module):
498
+ @singleton
499
+ @provider
500
+ def provide_settings(self) -> Settings:
501
+ return Settings(tenant_id="mx")
502
+
503
+ @singleton
504
+ @provider
505
+ @inject
506
+ def provide_repository(self, settings: Settings) -> UserRepository:
507
+ return UserRepository(settings)
508
+
509
+ @singleton
510
+ @provider
511
+ @inject
512
+ def provide_service(self, repository: UserRepository) -> UserService:
513
+ return UserService(repository)
514
+
515
+ container = Injector([MyModule()])
516
+ app = ApiGatewayHttpResolver(dependency_resolver=InjectorDependencyResolver(container))
517
+
518
+ @app.get("/injector/users/<user_id>")
519
+ def handler(
520
+ user_id: Annotated[int, Path()],
521
+ service: Annotated[UserService, Depends()],
522
+ ):
523
+ return service.get_user(user_id)
524
+
525
+ response = app.resolve(http_v2_event("GET", "/injector/users/42"), object())
526
+
527
+ assert response_body(response) == {"id": 42, "tenant_id": "mx"}
528
+
529
+
530
+ def test_rest_resolver_supports_real_injector_factory_dependency() -> None:
531
+ pytest.importorskip("injector")
532
+ from injector import Injector, Module, inject, provider, singleton
533
+ from modmex_lambda import InjectorDependencyResolver
534
+
535
+ class MyModule(Module):
536
+ @singleton
537
+ @provider
538
+ def provide_settings(self) -> Settings:
539
+ return Settings(tenant_id="mx")
540
+
541
+ @inject
542
+ def get_audit_service(settings: Settings) -> AuditService:
543
+ return AuditService(settings.tenant_id)
544
+
545
+ container = Injector([MyModule()])
546
+ app = ApiGatewayRestResolver(dependency_resolver=InjectorDependencyResolver(container))
547
+
548
+ @app.get("/injector/audit/<user_id>")
549
+ def handler(
550
+ user_id: Annotated[int, Path()],
551
+ audit: Annotated[AuditService, Depends(get_audit_service)],
552
+ ):
553
+ return audit.record(user_id)
554
+
555
+ response = app.resolve(rest_event("GET", "/injector/audit/7"), object())
556
+
557
+ assert response_body(response) == {"user_id": 7, "tenant_id": "mx"}
558
+
559
+
440
560
  def test_validation_error_maps_to_400() -> None:
441
561
  app = ApiGatewayHttpResolver()
442
562
 
@@ -39,6 +39,7 @@ from modmex_lambda.event_handler.dependencies.depends import (
39
39
  DependencyParam,
40
40
  DependencyResolutionError,
41
41
  Depends,
42
+ InjectorDependencyResolver,
42
43
  build_dependency_tree,
43
44
  solve_dependencies,
44
45
  )
@@ -120,6 +121,15 @@ def endpoint_with_broken_dependency(value: Annotated[str, Depends(broken_depende
120
121
  return None
121
122
 
122
123
 
124
+ class ComplexService:
125
+ def __init__(self, dependency: object) -> None:
126
+ self.dependency = dependency
127
+
128
+
129
+ def endpoint_with_annotation_dependency(service: Annotated[ComplexService, Depends()]) -> None:
130
+ return None
131
+
132
+
123
133
  def _field(
124
134
  name: str,
125
135
  annotation: Any,
@@ -149,6 +159,15 @@ def test_depends_rejects_non_callable_and_extracts_annotated_dependency() -> Non
149
159
  assert compat._normalize_errors([{"loc": ("x",), "type": "missing"}]) == [{"loc": ("x",), "type": "missing"}]
150
160
 
151
161
 
162
+ def test_depends_without_callable_uses_parameter_annotation_as_dependency_token() -> None:
163
+ tree = build_dependency_tree(endpoint_with_annotation_dependency)
164
+
165
+ assert len(tree.dependencies) == 1
166
+ assert tree.dependencies[0].param_name == "service"
167
+ assert tree.dependencies[0].depends.dependency is ComplexService
168
+ assert tree.dependencies[0].dependant.dependencies == []
169
+
170
+
152
171
  def test_solve_dependencies_supports_cache_overrides_request_and_errors() -> None:
153
172
  CALLS["token"] = 0
154
173
  event = APIGatewayProxyEventV2(http_v2_event("GET", "/items", headers={"x-tenant-id": "mx"}))
@@ -170,6 +189,34 @@ def test_solve_dependencies_supports_cache_overrides_request_and_errors() -> Non
170
189
  solve_dependencies(dependant=build_dependency_tree(endpoint_with_broken_dependency))
171
190
 
172
191
 
192
+ def test_solve_dependencies_accepts_custom_dependency_resolver() -> None:
193
+ class Resolver:
194
+ def resolve(self, dependency, *, values=None, request=None):
195
+ assert dependency is ComplexService
196
+ assert values == {}
197
+ return ComplexService(dependency="container")
198
+
199
+ values = solve_dependencies(
200
+ dependant=build_dependency_tree(endpoint_with_annotation_dependency),
201
+ dependency_resolver=Resolver(),
202
+ )
203
+
204
+ assert values["service"].dependency == "container"
205
+
206
+
207
+ def test_injector_dependency_resolver_uses_injector_get_for_class_tokens() -> None:
208
+ service = ComplexService(dependency="injector")
209
+
210
+ class FakeInjector:
211
+ def get(self, dependency):
212
+ assert dependency is ComplexService
213
+ return service
214
+
215
+ resolver = InjectorDependencyResolver(FakeInjector())
216
+
217
+ assert resolver.resolve(ComplexService) is service
218
+
219
+
173
220
  def test_solve_dependencies_honors_use_cache_false() -> None:
174
221
  CALLS["counter"] = 0
175
222
 
File without changes
File without changes
File without changes