From c999ba723357eb95d91d4f5871b490cc6425a578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Tue, 10 Jan 2023 11:48:41 +0100 Subject: [PATCH 1/6] feat: introduce specialized routers for typing --- .../event_handler/api_gateway.py | 189 +-------------- aws_lambda_powertools/event_handler/router.py | 219 ++++++++++++++++++ docs/core/event_handler/api_gateway.md | 15 ++ .../src/split_route_append_context_module.py | 2 +- .../src/split_route_module.py | 2 +- .../src/split_route_prefix_module.py | 2 +- .../src/split_route_specialized_router.py | 11 + .../event_handler/test_api_gateway.py | 2 +- tests/functional/event_handler/test_router.py | 76 ++++++ 9 files changed, 327 insertions(+), 191 deletions(-) create mode 100644 aws_lambda_powertools/event_handler/router.py create mode 100644 examples/event_handler_rest/src/split_route_specialized_router.py create mode 100644 tests/functional/event_handler/test_router.py diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index f96b8789308..65b8785a624 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -5,7 +5,6 @@ import traceback import warnings import zlib -from abc import ABC, abstractmethod from enum import Enum from functools import partial from http import HTTPStatus @@ -25,6 +24,7 @@ from aws_lambda_powertools.event_handler import content_types from aws_lambda_powertools.event_handler.exceptions import NotFoundError, ServiceError +from aws_lambda_powertools.event_handler.router import BaseRouter, Router from aws_lambda_powertools.shared.cookies import Cookie from aws_lambda_powertools.shared.functions import powertools_dev_is_set from aws_lambda_powertools.shared.json_encoder import Encoder @@ -35,7 +35,6 @@ LambdaFunctionUrlEvent, ) from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent -from aws_lambda_powertools.utilities.typing import LambdaContext logger = logging.getLogger(__name__) @@ -253,165 +252,6 @@ def build(self, event: BaseProxyEvent, cors: Optional[CORSConfig] = None) -> Dic } -class BaseRouter(ABC): - current_event: BaseProxyEvent - lambda_context: LambdaContext - context: dict - - @abstractmethod - def route( - self, - rule: str, - method: Any, - cors: Optional[bool] = None, - compress: bool = False, - cache_control: Optional[str] = None, - ): - raise NotImplementedError() - - def get(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): - """Get route decorator with GET `method` - - Examples - -------- - Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator - - ```python - from aws_lambda_powertools import Tracer - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - - tracer = Tracer() - app = APIGatewayRestResolver() - - @app.get("/get-call") - def simple_get(): - return {"message": "Foo"} - - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) - ``` - """ - return self.route(rule, "GET", cors, compress, cache_control) - - def post(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): - """Post route decorator with POST `method` - - Examples - -------- - Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator - - ```python - from aws_lambda_powertools import Tracer - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - - tracer = Tracer() - app = APIGatewayRestResolver() - - @app.post("/post-call") - def simple_post(): - post_data: dict = app.current_event.json_body - return {"message": post_data["value"]} - - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) - ``` - """ - return self.route(rule, "POST", cors, compress, cache_control) - - def put(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): - """Put route decorator with PUT `method` - - Examples - -------- - Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator - - ```python - from aws_lambda_powertools import Tracer - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - - tracer = Tracer() - app = APIGatewayRestResolver() - - @app.put("/put-call") - def simple_put(): - put_data: dict = app.current_event.json_body - return {"message": put_data["value"]} - - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) - ``` - """ - return self.route(rule, "PUT", cors, compress, cache_control) - - def delete( - self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None - ): - """Delete route decorator with DELETE `method` - - Examples - -------- - Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator - - ```python - from aws_lambda_powertools import Tracer - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - - tracer = Tracer() - app = APIGatewayRestResolver() - - @app.delete("/delete-call") - def simple_delete(): - return {"message": "deleted"} - - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) - ``` - """ - return self.route(rule, "DELETE", cors, compress, cache_control) - - def patch( - self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None - ): - """Patch route decorator with PATCH `method` - - Examples - -------- - Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator - - ```python - from aws_lambda_powertools import Tracer - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - - tracer = Tracer() - app = APIGatewayRestResolver() - - @app.patch("/patch-call") - def simple_patch(): - patch_data: dict = app.current_event.json_body - patch_data["value"] = patched - - return {"message": patch_data} - - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) - ``` - """ - return self.route(rule, "PATCH", cors, compress, cache_control) - - def append_context(self, **additional_context): - """Append key=value data as routing context""" - self.context.update(**additional_context) - - def clear_context(self): - """Resets routing context""" - self.context.clear() - - class ApiGatewayResolver(BaseRouter): """API Gateway and ALB proxy resolver @@ -755,7 +595,7 @@ def include_router(self, router: "Router", prefix: Optional[str] = None) -> None Parameters ---------- - router : Router + router : aws_lambda_powertools.event_handler.router.Router The Router containing a list of routes to be registered after the existing routes prefix : str, optional An optional prefix to be added to the originally defined rule @@ -778,31 +618,6 @@ def include_router(self, router: "Router", prefix: Optional[str] = None) -> None self.route(*route)(func) -class Router(BaseRouter): - """Router helper class to allow splitting ApiGatewayResolver into multiple files""" - - def __init__(self): - self._routes: Dict[tuple, Callable] = {} - self.api_resolver: Optional[BaseRouter] = None - self.context = {} # early init as customers might add context before event resolution - - def route( - self, - rule: str, - method: Union[str, Union[List[str], Tuple[str]]], - cors: Optional[bool] = None, - compress: bool = False, - cache_control: Optional[str] = None, - ): - def register_route(func: Callable): - # Convert methods to tuple. It needs to be hashable as its part of the self._routes dict key - methods = (method,) if isinstance(method, str) else tuple(method) - self._routes[(rule, methods, cors, compress, cache_control)] = func - return func - - return register_route - - class APIGatewayRestResolver(ApiGatewayResolver): current_event: APIGatewayProxyEvent diff --git a/aws_lambda_powertools/event_handler/router.py b/aws_lambda_powertools/event_handler/router.py new file mode 100644 index 00000000000..bce79dcc741 --- /dev/null +++ b/aws_lambda_powertools/event_handler/router.py @@ -0,0 +1,219 @@ +from abc import ABC, abstractmethod +from typing import Any, Callable, Dict, List, Optional, Tuple, Union + +from aws_lambda_powertools.utilities.data_classes import ( + ALBEvent, + APIGatewayProxyEvent, + APIGatewayProxyEventV2, + LambdaFunctionUrlEvent, +) +from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent +from aws_lambda_powertools.utilities.typing import LambdaContext + + +class BaseRouter(ABC): + current_event: BaseProxyEvent + lambda_context: LambdaContext + context: dict + + @abstractmethod + def route( + self, + rule: str, + method: Any, + cors: Optional[bool] = None, + compress: bool = False, + cache_control: Optional[str] = None, + ): + raise NotImplementedError() + + def get(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): + """Get route decorator with GET `method` + + Examples + -------- + Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator + + ```python + from aws_lambda_powertools import Tracer + from aws_lambda_powertools.event_handler import APIGatewayRestResolver + + tracer = Tracer() + app = APIGatewayRestResolver() + + @app.get("/get-call") + def simple_get(): + return {"message": "Foo"} + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ + return self.route(rule, "GET", cors, compress, cache_control) + + def post(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): + """Post route decorator with POST `method` + + Examples + -------- + Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator + + ```python + from aws_lambda_powertools import Tracer + from aws_lambda_powertools.event_handler import APIGatewayRestResolver + + tracer = Tracer() + app = APIGatewayRestResolver() + + @app.post("/post-call") + def simple_post(): + post_data: dict = app.current_event.json_body + return {"message": post_data["value"]} + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ + return self.route(rule, "POST", cors, compress, cache_control) + + def put(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): + """Put route decorator with PUT `method` + + Examples + -------- + Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator + + ```python + from aws_lambda_powertools import Tracer + from aws_lambda_powertools.event_handler import APIGatewayRestResolver + + tracer = Tracer() + app = APIGatewayRestResolver() + + @app.put("/put-call") + def simple_put(): + put_data: dict = app.current_event.json_body + return {"message": put_data["value"]} + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ + return self.route(rule, "PUT", cors, compress, cache_control) + + def delete( + self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None + ): + """Delete route decorator with DELETE `method` + + Examples + -------- + Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator + + ```python + from aws_lambda_powertools import Tracer + from aws_lambda_powertools.event_handler import APIGatewayRestResolver + + tracer = Tracer() + app = APIGatewayRestResolver() + + @app.delete("/delete-call") + def simple_delete(): + return {"message": "deleted"} + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ + return self.route(rule, "DELETE", cors, compress, cache_control) + + def patch( + self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None + ): + """Patch route decorator with PATCH `method` + + Examples + -------- + Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator + + ```python + from aws_lambda_powertools import Tracer + from aws_lambda_powertools.event_handler import APIGatewayRestResolver + + tracer = Tracer() + app = APIGatewayRestResolver() + + @app.patch("/patch-call") + def simple_patch(): + patch_data: dict = app.current_event.json_body + patch_data["value"] = patched + + return {"message": patch_data} + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ + return self.route(rule, "PATCH", cors, compress, cache_control) + + def append_context(self, **additional_context): + """Append key=value data as routing context""" + self.context.update(**additional_context) + + def clear_context(self): + """Resets routing context""" + self.context.clear() + + +class Router(BaseRouter): + """Router helper class to allow splitting ApiGatewayResolver into multiple files""" + + def __init__(self): + self._routes: Dict[tuple, Callable] = {} + self.api_resolver: Optional[BaseRouter] = None + self.context = {} # early init as customers might add context before event resolution + + def route( + self, + rule: str, + method: Union[str, Union[List[str], Tuple[str]]], + cors: Optional[bool] = None, + compress: bool = False, + cache_control: Optional[str] = None, + ): + def register_route(func: Callable): + # Convert methods to tuple. It needs to be hashable as its part of the self._routes dict key + methods = (method,) if isinstance(method, str) else tuple(method) + self._routes[(rule, methods, cors, compress, cache_control)] = func + return func + + return register_route + + +class APIGatewayRouter(Router): + """Specialized Router class that exposes current_event as an APIGatewayProxyEvent""" + + current_event: APIGatewayProxyEvent + + +class APIGatewayHttpRouter(Router): + """Specialized Router class that exposes current_event as an APIGatewayProxyEventV2""" + + current_event: APIGatewayProxyEventV2 + + +class LambdaFunctionUrlRouter(Router): + """Specialized Router class that exposes current_event as a LambdaFunctionUrlEvent""" + + current_event: LambdaFunctionUrlEvent + + +class ALBRouter(Router): + """Specialized Router class that exposes current_event as an ALBEvent""" + + current_event: ALBEvent diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index ca092e30c04..5a805050ee8 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -459,6 +459,21 @@ When necessary, you can set a prefix when including a router object. This means --8<-- "examples/event_handler_rest/src/split_route_prefix_module.py" ``` +#### Specialized router types + +You can use specialized router classes according to the type of event that you are resolving. This way you'll get type hints from your IDE as you access the `current_event` property. + +| Router | Resolver | `current_event` type | +|-------------------------|---------------------------|------------------------| +| APIGatewayRouter | APIGatewayRestResolver | APIGatewayProxyEvent | +| APIGatewayHttpRouter | APIGatewayHttpResolver | APIGatewayProxyEventV2 | +| ALBRouter | ALBResolver | ALBEvent | +| LambdaFunctionUrlRouter | LambdaFunctionUrlResolver | LambdaFunctionUrlEvent | + +```python hl_lines="3 9" +--8<-- "examples/event_handler_rest/src/split_route_specialized_router.py" +``` + #### Sharing contextual data You can use `append_context` when you want to share data between your App and Router instances. Any data you share will be available via the `context` dictionary available in your App or Router context. diff --git a/examples/event_handler_rest/src/split_route_append_context_module.py b/examples/event_handler_rest/src/split_route_append_context_module.py index 0b9a0cd5fa0..e463bd6ef9a 100644 --- a/examples/event_handler_rest/src/split_route_append_context_module.py +++ b/examples/event_handler_rest/src/split_route_append_context_module.py @@ -2,7 +2,7 @@ from requests import Response from aws_lambda_powertools import Tracer -from aws_lambda_powertools.event_handler.api_gateway import Router +from aws_lambda_powertools.event_handler.router import Router tracer = Tracer() router = Router() diff --git a/examples/event_handler_rest/src/split_route_module.py b/examples/event_handler_rest/src/split_route_module.py index 0462623f90b..16a1d8a6013 100644 --- a/examples/event_handler_rest/src/split_route_module.py +++ b/examples/event_handler_rest/src/split_route_module.py @@ -2,7 +2,7 @@ from requests import Response from aws_lambda_powertools import Tracer -from aws_lambda_powertools.event_handler.api_gateway import Router +from aws_lambda_powertools.event_handler.router import Router tracer = Tracer() router = Router() diff --git a/examples/event_handler_rest/src/split_route_prefix_module.py b/examples/event_handler_rest/src/split_route_prefix_module.py index 41fcf8eed31..e66694b4117 100644 --- a/examples/event_handler_rest/src/split_route_prefix_module.py +++ b/examples/event_handler_rest/src/split_route_prefix_module.py @@ -2,7 +2,7 @@ from requests import Response from aws_lambda_powertools import Tracer -from aws_lambda_powertools.event_handler.api_gateway import Router +from aws_lambda_powertools.event_handler.router import Router tracer = Tracer() router = Router() diff --git a/examples/event_handler_rest/src/split_route_specialized_router.py b/examples/event_handler_rest/src/split_route_specialized_router.py new file mode 100644 index 00000000000..4b059655b49 --- /dev/null +++ b/examples/event_handler_rest/src/split_route_specialized_router.py @@ -0,0 +1,11 @@ +from aws_lambda_powertools.event_handler.router import APIGatewayRouter + +router = APIGatewayRouter() + + +@router.get("/me") +def get_self(): + # router.current_event is a APIGatewayProxyEvent + principal_id = router.current_event.request_context.authorizer.principal_id + + return {"principal_id": principal_id} diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 61c2f715b18..5dd3e5458c0 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -20,7 +20,6 @@ ProxyEventType, Response, ResponseBuilder, - Router, ) from aws_lambda_powertools.event_handler.exceptions import ( BadRequestError, @@ -29,6 +28,7 @@ ServiceError, UnauthorizedError, ) +from aws_lambda_powertools.event_handler.router import Router from aws_lambda_powertools.shared import constants from aws_lambda_powertools.shared.cookies import Cookie from aws_lambda_powertools.shared.json_encoder import Encoder diff --git a/tests/functional/event_handler/test_router.py b/tests/functional/event_handler/test_router.py new file mode 100644 index 00000000000..d96f5035114 --- /dev/null +++ b/tests/functional/event_handler/test_router.py @@ -0,0 +1,76 @@ +from aws_lambda_powertools.event_handler import ( + ALBResolver, + APIGatewayHttpResolver, + APIGatewayRestResolver, + LambdaFunctionUrlResolver, + Response, +) +from aws_lambda_powertools.event_handler.router import ( + ALBRouter, + APIGatewayHttpRouter, + APIGatewayRouter, + LambdaFunctionUrlRouter, +) +from aws_lambda_powertools.utilities.data_classes import ( + ALBEvent, + APIGatewayProxyEvent, + APIGatewayProxyEventV2, + LambdaFunctionUrlEvent, +) +from tests.functional.utils import load_event + + +def test_alb_router_event_type(): + app = ALBResolver() + router = ALBRouter() + + @router.route(rule="/lambda", method=["GET"]) + def foo(): + assert type(router.current_event) is ALBEvent + return Response(status_code=200, body="routed") + + app.include_router(router) + result = app(load_event("albEvent.json"), {}) + assert result["body"] == "routed" + + +def test_apigateway_router_event_type(): + app = APIGatewayRestResolver() + router = APIGatewayRouter() + + @router.route(rule="/my/path", method=["GET"]) + def foo(): + assert type(router.current_event) is APIGatewayProxyEvent + return Response(status_code=200, body="routed") + + app.include_router(router) + result = app(load_event("apiGatewayProxyEvent.json"), {}) + assert result["body"] == "routed" + + +def test_apigatewayhttp_router_event_type(): + app = APIGatewayHttpResolver() + router = APIGatewayHttpRouter() + + @router.route(rule="/my/path", method=["POST"]) + def foo(): + assert type(router.current_event) is APIGatewayProxyEventV2 + return Response(status_code=200, body="routed") + + app.include_router(router) + result = app(load_event("apiGatewayProxyV2Event.json"), {}) + assert result["body"] == "routed" + + +def test_lambda_function_url_router_event_type(): + app = LambdaFunctionUrlResolver() + router = LambdaFunctionUrlRouter() + + @router.route(rule="/", method=["GET"]) + def foo(): + assert type(router.current_event) is LambdaFunctionUrlEvent + return Response(status_code=200, body="routed") + + app.include_router(router) + result = app(load_event("lambdaFunctionUrlEvent.json"), {}) + assert result["body"] == "routed" From a2c071a2a67729c07f69019b536b69e5b1a60ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Tue, 10 Jan 2023 12:06:18 +0100 Subject: [PATCH 2/6] chore: export top level constants --- aws_lambda_powertools/event_handler/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/aws_lambda_powertools/event_handler/__init__.py b/aws_lambda_powertools/event_handler/__init__.py index 75d03d771e0..e4ea63aee73 100644 --- a/aws_lambda_powertools/event_handler/__init__.py +++ b/aws_lambda_powertools/event_handler/__init__.py @@ -12,14 +12,26 @@ ) from .appsync import AppSyncResolver from .lambda_function_url import LambdaFunctionUrlResolver +from .router import ( + ALBRouter, + APIGatewayHttpRouter, + APIGatewayRouter, + LambdaFunctionUrlRouter, + Router, +) __all__ = [ "AppSyncResolver", "APIGatewayRestResolver", + "APIGatewayRouter", "APIGatewayHttpResolver", + "APIGatewayHttpRouter", "ALBResolver", + "ALBRouter", "ApiGatewayResolver", "CORSConfig", "LambdaFunctionUrlResolver", + "LambdaFunctionUrlRouter", "Response", + "Router", ] From 9d3a3a0cc992488a12c8fa63225585ea583a34b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Tue, 10 Jan 2023 13:38:27 +0100 Subject: [PATCH 3/6] fix: move router back to api_gateway.py --- .../event_handler/__init__.py | 2 +- .../event_handler/api_gateway.py | 189 ++++++++++++++++- aws_lambda_powertools/event_handler/router.py | 190 +----------------- .../src/split_route_append_context_module.py | 2 +- .../src/split_route_module.py | 2 +- .../src/split_route_prefix_module.py | 2 +- .../event_handler/test_api_gateway.py | 3 +- 7 files changed, 193 insertions(+), 197 deletions(-) diff --git a/aws_lambda_powertools/event_handler/__init__.py b/aws_lambda_powertools/event_handler/__init__.py index e4ea63aee73..15024b1e366 100644 --- a/aws_lambda_powertools/event_handler/__init__.py +++ b/aws_lambda_powertools/event_handler/__init__.py @@ -9,6 +9,7 @@ APIGatewayRestResolver, CORSConfig, Response, + Router, ) from .appsync import AppSyncResolver from .lambda_function_url import LambdaFunctionUrlResolver @@ -17,7 +18,6 @@ APIGatewayHttpRouter, APIGatewayRouter, LambdaFunctionUrlRouter, - Router, ) __all__ = [ diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 65b8785a624..5b630c358f9 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -5,6 +5,7 @@ import traceback import warnings import zlib +from abc import ABC, abstractmethod from enum import Enum from functools import partial from http import HTTPStatus @@ -24,7 +25,6 @@ from aws_lambda_powertools.event_handler import content_types from aws_lambda_powertools.event_handler.exceptions import NotFoundError, ServiceError -from aws_lambda_powertools.event_handler.router import BaseRouter, Router from aws_lambda_powertools.shared.cookies import Cookie from aws_lambda_powertools.shared.functions import powertools_dev_is_set from aws_lambda_powertools.shared.json_encoder import Encoder @@ -35,6 +35,7 @@ LambdaFunctionUrlEvent, ) from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent +from aws_lambda_powertools.utilities.typing import LambdaContext logger = logging.getLogger(__name__) @@ -252,6 +253,165 @@ def build(self, event: BaseProxyEvent, cors: Optional[CORSConfig] = None) -> Dic } +class BaseRouter(ABC): + current_event: BaseProxyEvent + lambda_context: LambdaContext + context: dict + + @abstractmethod + def route( + self, + rule: str, + method: Any, + cors: Optional[bool] = None, + compress: bool = False, + cache_control: Optional[str] = None, + ): + raise NotImplementedError() + + def get(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): + """Get route decorator with GET `method` + + Examples + -------- + Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator + + ```python + from aws_lambda_powertools import Tracer + from aws_lambda_powertools.event_handler import APIGatewayRestResolver + + tracer = Tracer() + app = APIGatewayRestResolver() + + @app.get("/get-call") + def simple_get(): + return {"message": "Foo"} + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ + return self.route(rule, "GET", cors, compress, cache_control) + + def post(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): + """Post route decorator with POST `method` + + Examples + -------- + Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator + + ```python + from aws_lambda_powertools import Tracer + from aws_lambda_powertools.event_handler import APIGatewayRestResolver + + tracer = Tracer() + app = APIGatewayRestResolver() + + @app.post("/post-call") + def simple_post(): + post_data: dict = app.current_event.json_body + return {"message": post_data["value"]} + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ + return self.route(rule, "POST", cors, compress, cache_control) + + def put(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): + """Put route decorator with PUT `method` + + Examples + -------- + Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator + + ```python + from aws_lambda_powertools import Tracer + from aws_lambda_powertools.event_handler import APIGatewayRestResolver + + tracer = Tracer() + app = APIGatewayRestResolver() + + @app.put("/put-call") + def simple_put(): + put_data: dict = app.current_event.json_body + return {"message": put_data["value"]} + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ + return self.route(rule, "PUT", cors, compress, cache_control) + + def delete( + self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None + ): + """Delete route decorator with DELETE `method` + + Examples + -------- + Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator + + ```python + from aws_lambda_powertools import Tracer + from aws_lambda_powertools.event_handler import APIGatewayRestResolver + + tracer = Tracer() + app = APIGatewayRestResolver() + + @app.delete("/delete-call") + def simple_delete(): + return {"message": "deleted"} + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ + return self.route(rule, "DELETE", cors, compress, cache_control) + + def patch( + self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None + ): + """Patch route decorator with PATCH `method` + + Examples + -------- + Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator + + ```python + from aws_lambda_powertools import Tracer + from aws_lambda_powertools.event_handler import APIGatewayRestResolver + + tracer = Tracer() + app = APIGatewayRestResolver() + + @app.patch("/patch-call") + def simple_patch(): + patch_data: dict = app.current_event.json_body + patch_data["value"] = patched + + return {"message": patch_data} + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ + return self.route(rule, "PATCH", cors, compress, cache_control) + + def append_context(self, **additional_context): + """Append key=value data as routing context""" + self.context.update(**additional_context) + + def clear_context(self): + """Resets routing context""" + self.context.clear() + + class ApiGatewayResolver(BaseRouter): """API Gateway and ALB proxy resolver @@ -595,7 +755,7 @@ def include_router(self, router: "Router", prefix: Optional[str] = None) -> None Parameters ---------- - router : aws_lambda_powertools.event_handler.router.Router + router : aws_lambda_powertools.event_handler.Router The Router containing a list of routes to be registered after the existing routes prefix : str, optional An optional prefix to be added to the originally defined rule @@ -618,6 +778,31 @@ def include_router(self, router: "Router", prefix: Optional[str] = None) -> None self.route(*route)(func) +class Router(BaseRouter): + """Router helper class to allow splitting ApiGatewayResolver into multiple files""" + + def __init__(self): + self._routes: Dict[tuple, Callable] = {} + self.api_resolver: Optional[BaseRouter] = None + self.context = {} # early init as customers might add context before event resolution + + def route( + self, + rule: str, + method: Union[str, Union[List[str], Tuple[str]]], + cors: Optional[bool] = None, + compress: bool = False, + cache_control: Optional[str] = None, + ): + def register_route(func: Callable): + # Convert methods to tuple. It needs to be hashable as its part of the self._routes dict key + methods = (method,) if isinstance(method, str) else tuple(method) + self._routes[(rule, methods, cors, compress, cache_control)] = func + return func + + return register_route + + class APIGatewayRestResolver(ApiGatewayResolver): current_event: APIGatewayProxyEvent diff --git a/aws_lambda_powertools/event_handler/router.py b/aws_lambda_powertools/event_handler/router.py index bce79dcc741..2512d63a5d9 100644 --- a/aws_lambda_powertools/event_handler/router.py +++ b/aws_lambda_powertools/event_handler/router.py @@ -1,198 +1,10 @@ -from abc import ABC, abstractmethod -from typing import Any, Callable, Dict, List, Optional, Tuple, Union - +from aws_lambda_powertools.event_handler import Router from aws_lambda_powertools.utilities.data_classes import ( ALBEvent, APIGatewayProxyEvent, APIGatewayProxyEventV2, LambdaFunctionUrlEvent, ) -from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent -from aws_lambda_powertools.utilities.typing import LambdaContext - - -class BaseRouter(ABC): - current_event: BaseProxyEvent - lambda_context: LambdaContext - context: dict - - @abstractmethod - def route( - self, - rule: str, - method: Any, - cors: Optional[bool] = None, - compress: bool = False, - cache_control: Optional[str] = None, - ): - raise NotImplementedError() - - def get(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): - """Get route decorator with GET `method` - - Examples - -------- - Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator - - ```python - from aws_lambda_powertools import Tracer - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - - tracer = Tracer() - app = APIGatewayRestResolver() - - @app.get("/get-call") - def simple_get(): - return {"message": "Foo"} - - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) - ``` - """ - return self.route(rule, "GET", cors, compress, cache_control) - - def post(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): - """Post route decorator with POST `method` - - Examples - -------- - Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator - - ```python - from aws_lambda_powertools import Tracer - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - - tracer = Tracer() - app = APIGatewayRestResolver() - - @app.post("/post-call") - def simple_post(): - post_data: dict = app.current_event.json_body - return {"message": post_data["value"]} - - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) - ``` - """ - return self.route(rule, "POST", cors, compress, cache_control) - - def put(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): - """Put route decorator with PUT `method` - - Examples - -------- - Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator - - ```python - from aws_lambda_powertools import Tracer - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - - tracer = Tracer() - app = APIGatewayRestResolver() - - @app.put("/put-call") - def simple_put(): - put_data: dict = app.current_event.json_body - return {"message": put_data["value"]} - - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) - ``` - """ - return self.route(rule, "PUT", cors, compress, cache_control) - - def delete( - self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None - ): - """Delete route decorator with DELETE `method` - - Examples - -------- - Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator - - ```python - from aws_lambda_powertools import Tracer - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - - tracer = Tracer() - app = APIGatewayRestResolver() - - @app.delete("/delete-call") - def simple_delete(): - return {"message": "deleted"} - - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) - ``` - """ - return self.route(rule, "DELETE", cors, compress, cache_control) - - def patch( - self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None - ): - """Patch route decorator with PATCH `method` - - Examples - -------- - Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator - - ```python - from aws_lambda_powertools import Tracer - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - - tracer = Tracer() - app = APIGatewayRestResolver() - - @app.patch("/patch-call") - def simple_patch(): - patch_data: dict = app.current_event.json_body - patch_data["value"] = patched - - return {"message": patch_data} - - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) - ``` - """ - return self.route(rule, "PATCH", cors, compress, cache_control) - - def append_context(self, **additional_context): - """Append key=value data as routing context""" - self.context.update(**additional_context) - - def clear_context(self): - """Resets routing context""" - self.context.clear() - - -class Router(BaseRouter): - """Router helper class to allow splitting ApiGatewayResolver into multiple files""" - - def __init__(self): - self._routes: Dict[tuple, Callable] = {} - self.api_resolver: Optional[BaseRouter] = None - self.context = {} # early init as customers might add context before event resolution - - def route( - self, - rule: str, - method: Union[str, Union[List[str], Tuple[str]]], - cors: Optional[bool] = None, - compress: bool = False, - cache_control: Optional[str] = None, - ): - def register_route(func: Callable): - # Convert methods to tuple. It needs to be hashable as its part of the self._routes dict key - methods = (method,) if isinstance(method, str) else tuple(method) - self._routes[(rule, methods, cors, compress, cache_control)] = func - return func - - return register_route class APIGatewayRouter(Router): diff --git a/examples/event_handler_rest/src/split_route_append_context_module.py b/examples/event_handler_rest/src/split_route_append_context_module.py index e463bd6ef9a..6ef4478f9bf 100644 --- a/examples/event_handler_rest/src/split_route_append_context_module.py +++ b/examples/event_handler_rest/src/split_route_append_context_module.py @@ -2,7 +2,7 @@ from requests import Response from aws_lambda_powertools import Tracer -from aws_lambda_powertools.event_handler.router import Router +from aws_lambda_powertools.event_handler import Router tracer = Tracer() router = Router() diff --git a/examples/event_handler_rest/src/split_route_module.py b/examples/event_handler_rest/src/split_route_module.py index 16a1d8a6013..4f747f1e2e3 100644 --- a/examples/event_handler_rest/src/split_route_module.py +++ b/examples/event_handler_rest/src/split_route_module.py @@ -2,7 +2,7 @@ from requests import Response from aws_lambda_powertools import Tracer -from aws_lambda_powertools.event_handler.router import Router +from aws_lambda_powertools.event_handler import Router tracer = Tracer() router = Router() diff --git a/examples/event_handler_rest/src/split_route_prefix_module.py b/examples/event_handler_rest/src/split_route_prefix_module.py index e66694b4117..2f1e53891b6 100644 --- a/examples/event_handler_rest/src/split_route_prefix_module.py +++ b/examples/event_handler_rest/src/split_route_prefix_module.py @@ -2,7 +2,7 @@ from requests import Response from aws_lambda_powertools import Tracer -from aws_lambda_powertools.event_handler.router import Router +from aws_lambda_powertools.event_handler import Router tracer = Tracer() router = Router() diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 5dd3e5458c0..9937589022c 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -10,7 +10,7 @@ import pytest -from aws_lambda_powertools.event_handler import content_types +from aws_lambda_powertools.event_handler import Router, content_types from aws_lambda_powertools.event_handler.api_gateway import ( ALBResolver, APIGatewayHttpResolver, @@ -28,7 +28,6 @@ ServiceError, UnauthorizedError, ) -from aws_lambda_powertools.event_handler.router import Router from aws_lambda_powertools.shared import constants from aws_lambda_powertools.shared.cookies import Cookie from aws_lambda_powertools.shared.json_encoder import Encoder From 1945cc455779aa6f007b96da8d2cb91f7130fd6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Tue, 10 Jan 2023 13:43:15 +0100 Subject: [PATCH 4/6] fix: remove old changes --- aws_lambda_powertools/event_handler/__init__.py | 12 ------------ aws_lambda_powertools/event_handler/api_gateway.py | 2 +- .../src/split_route_append_context_module.py | 2 +- .../event_handler_rest/src/split_route_module.py | 2 +- .../src/split_route_prefix_module.py | 2 +- tests/functional/event_handler/test_api_gateway.py | 3 ++- 6 files changed, 6 insertions(+), 17 deletions(-) diff --git a/aws_lambda_powertools/event_handler/__init__.py b/aws_lambda_powertools/event_handler/__init__.py index 15024b1e366..75d03d771e0 100644 --- a/aws_lambda_powertools/event_handler/__init__.py +++ b/aws_lambda_powertools/event_handler/__init__.py @@ -9,29 +9,17 @@ APIGatewayRestResolver, CORSConfig, Response, - Router, ) from .appsync import AppSyncResolver from .lambda_function_url import LambdaFunctionUrlResolver -from .router import ( - ALBRouter, - APIGatewayHttpRouter, - APIGatewayRouter, - LambdaFunctionUrlRouter, -) __all__ = [ "AppSyncResolver", "APIGatewayRestResolver", - "APIGatewayRouter", "APIGatewayHttpResolver", - "APIGatewayHttpRouter", "ALBResolver", - "ALBRouter", "ApiGatewayResolver", "CORSConfig", "LambdaFunctionUrlResolver", - "LambdaFunctionUrlRouter", "Response", - "Router", ] diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 5b630c358f9..f96b8789308 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -755,7 +755,7 @@ def include_router(self, router: "Router", prefix: Optional[str] = None) -> None Parameters ---------- - router : aws_lambda_powertools.event_handler.Router + router : Router The Router containing a list of routes to be registered after the existing routes prefix : str, optional An optional prefix to be added to the originally defined rule diff --git a/examples/event_handler_rest/src/split_route_append_context_module.py b/examples/event_handler_rest/src/split_route_append_context_module.py index 6ef4478f9bf..0b9a0cd5fa0 100644 --- a/examples/event_handler_rest/src/split_route_append_context_module.py +++ b/examples/event_handler_rest/src/split_route_append_context_module.py @@ -2,7 +2,7 @@ from requests import Response from aws_lambda_powertools import Tracer -from aws_lambda_powertools.event_handler import Router +from aws_lambda_powertools.event_handler.api_gateway import Router tracer = Tracer() router = Router() diff --git a/examples/event_handler_rest/src/split_route_module.py b/examples/event_handler_rest/src/split_route_module.py index 4f747f1e2e3..0462623f90b 100644 --- a/examples/event_handler_rest/src/split_route_module.py +++ b/examples/event_handler_rest/src/split_route_module.py @@ -2,7 +2,7 @@ from requests import Response from aws_lambda_powertools import Tracer -from aws_lambda_powertools.event_handler import Router +from aws_lambda_powertools.event_handler.api_gateway import Router tracer = Tracer() router = Router() diff --git a/examples/event_handler_rest/src/split_route_prefix_module.py b/examples/event_handler_rest/src/split_route_prefix_module.py index 2f1e53891b6..41fcf8eed31 100644 --- a/examples/event_handler_rest/src/split_route_prefix_module.py +++ b/examples/event_handler_rest/src/split_route_prefix_module.py @@ -2,7 +2,7 @@ from requests import Response from aws_lambda_powertools import Tracer -from aws_lambda_powertools.event_handler import Router +from aws_lambda_powertools.event_handler.api_gateway import Router tracer = Tracer() router = Router() diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 9937589022c..61c2f715b18 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -10,7 +10,7 @@ import pytest -from aws_lambda_powertools.event_handler import Router, content_types +from aws_lambda_powertools.event_handler import content_types from aws_lambda_powertools.event_handler.api_gateway import ( ALBResolver, APIGatewayHttpResolver, @@ -20,6 +20,7 @@ ProxyEventType, Response, ResponseBuilder, + Router, ) from aws_lambda_powertools.event_handler.exceptions import ( BadRequestError, From 482263d627888fc72d5f11f2d9d826d9c51bbcda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Tue, 10 Jan 2023 13:44:33 +0100 Subject: [PATCH 5/6] fix: review --- aws_lambda_powertools/event_handler/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws_lambda_powertools/event_handler/router.py b/aws_lambda_powertools/event_handler/router.py index 2512d63a5d9..85f2bbbef82 100644 --- a/aws_lambda_powertools/event_handler/router.py +++ b/aws_lambda_powertools/event_handler/router.py @@ -1,4 +1,4 @@ -from aws_lambda_powertools.event_handler import Router +from aws_lambda_powertools.event_handler.api_gateway import Router from aws_lambda_powertools.utilities.data_classes import ( ALBEvent, APIGatewayProxyEvent, From 2def8416ca87b546b07ca90a1bd5bbbb70f97b0f Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Wed, 11 Jan 2023 15:31:33 +0000 Subject: [PATCH 6/6] docs(apigw) - adding full example --- docs/core/event_handler/api_gateway.md | 2 +- .../src/split_route_specialized_router.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 5a805050ee8..cee848c24c3 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -470,7 +470,7 @@ You can use specialized router classes according to the type of event that you a | ALBRouter | ALBResolver | ALBEvent | | LambdaFunctionUrlRouter | LambdaFunctionUrlResolver | LambdaFunctionUrlEvent | -```python hl_lines="3 9" +```python hl_lines="1 5 9" --8<-- "examples/event_handler_rest/src/split_route_specialized_router.py" ``` diff --git a/examples/event_handler_rest/src/split_route_specialized_router.py b/examples/event_handler_rest/src/split_route_specialized_router.py index 4b059655b49..f9002be4209 100644 --- a/examples/event_handler_rest/src/split_route_specialized_router.py +++ b/examples/event_handler_rest/src/split_route_specialized_router.py @@ -1,11 +1,20 @@ +from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.event_handler.router import APIGatewayRouter +app = APIGatewayRestResolver() router = APIGatewayRouter() @router.get("/me") def get_self(): # router.current_event is a APIGatewayProxyEvent - principal_id = router.current_event.request_context.authorizer.principal_id + account_id = router.current_event.request_context.account_id - return {"principal_id": principal_id} + return {"account_id": account_id} + + +app.include_router(router) + + +def lambda_handler(event, context): + return app.resolve(event, context)