From befd54e9a5573c9c0af6d633a682c040a74e172f Mon Sep 17 00:00:00 2001 From: Matthew Coyle Date: Thu, 19 Jan 2023 11:56:31 +0000 Subject: [PATCH 1/5] Add handling of tuples in api gateway response resolver Support returning dict responses and status codes from api gateway route functions Signed-off-by: Matthew Coyle --- aws_lambda_powertools/event_handler/api_gateway.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 8ced47f81e2..566975c46aa 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -728,21 +728,26 @@ def _call_exception_handler(self, exp: Exception, route: Route) -> Optional[Resp return None - def _to_response(self, result: Union[Dict, Response]) -> Response: + def _to_response(self, result: Union[Dict, Tuple, Response]) -> Response: """Convert the route's result to a Response - 2 main result types are supported: + 3 main result types are supported: - Dict[str, Any]: Rest api response with just the Dict to json stringify and content-type is set to application/json + - Tuple[dict, str]: Same dict handling as above but with the option of including a status code - Response: returned as is, and allows for more flexibility """ + status_code = 200 if isinstance(result, Response): return result + elif isinstance(result, tuple) and len(tuple) == 2: + # Unpack result dict and status code from tuple + result, status_code = result logger.debug("Simple response detected, serializing return before constructing final response") return Response( - status_code=200, + status_code=status_code, content_type=content_types.APPLICATION_JSON, body=self._json_dump(result), ) From 4242edb3b94aaf21a9f0a7917d4dd264007e9cdf Mon Sep 17 00:00:00 2001 From: Matthew Coyle Date: Mon, 23 Jan 2023 16:27:41 +0000 Subject: [PATCH 2/5] Add tests, docs and fix for typo and status code literal --- .../event_handler/api_gateway.py | 6 ++-- .../event_handler_rest/src/http_methods.py | 2 +- .../src/http_methods_multiple.py | 2 +- ...started_middleware_after_logic_function.py | 2 +- .../event_handler/test_api_gateway.py | 36 +++++++++++++++++++ 5 files changed, 42 insertions(+), 6 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 566975c46aa..7b4001c7265 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -735,13 +735,13 @@ def _to_response(self, result: Union[Dict, Tuple, Response]) -> Response: - Dict[str, Any]: Rest api response with just the Dict to json stringify and content-type is set to application/json - - Tuple[dict, str]: Same dict handling as above but with the option of including a status code + - Tuple[dict, int]: Same dict handling as above but with the option of including a status code - Response: returned as is, and allows for more flexibility """ - status_code = 200 + status_code = HTTPStatus.OK if isinstance(result, Response): return result - elif isinstance(result, tuple) and len(tuple) == 2: + elif isinstance(result, tuple) and len(result) == 2: # Unpack result dict and status code from tuple result, status_code = result diff --git a/examples/event_handler_rest/src/http_methods.py b/examples/event_handler_rest/src/http_methods.py index 47eb1499a38..c0a0a652558 100644 --- a/examples/event_handler_rest/src/http_methods.py +++ b/examples/event_handler_rest/src/http_methods.py @@ -18,7 +18,7 @@ def create_todo(): todo: Response = requests.post("https://jsonplaceholder.typicode.com/todos", data=todo_data) todo.raise_for_status() - return {"todo": todo.json()} + return {"todo": todo.json()}, 201 # You can continue to use other utilities just as before diff --git a/examples/event_handler_rest/src/http_methods_multiple.py b/examples/event_handler_rest/src/http_methods_multiple.py index a482c96d80f..7593ee393f5 100644 --- a/examples/event_handler_rest/src/http_methods_multiple.py +++ b/examples/event_handler_rest/src/http_methods_multiple.py @@ -19,7 +19,7 @@ def create_todo(): todo: Response = requests.post("https://jsonplaceholder.typicode.com/todos", data=todo_data) todo.raise_for_status() - return {"todo": todo.json()} + return {"todo": todo.json()}, 201 # You can continue to use other utilities just as before diff --git a/examples/middleware_factory/src/getting_started_middleware_after_logic_function.py b/examples/middleware_factory/src/getting_started_middleware_after_logic_function.py index e77328ca8f7..9c52467341d 100644 --- a/examples/middleware_factory/src/getting_started_middleware_after_logic_function.py +++ b/examples/middleware_factory/src/getting_started_middleware_after_logic_function.py @@ -31,7 +31,7 @@ def create_todo() -> dict: todo: Response = requests.post("https://jsonplaceholder.typicode.com/todos", data=todo_data) todo.raise_for_status() - return {"todo": todo.json()} + return {"todo": todo.json()}, 201 @middleware_after diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index cf560fbcc34..ad9f834dbb2 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -1579,3 +1579,39 @@ def get_lambda() -> Response: # AND set the current_event type as APIGatewayProxyEvent assert result["statusCode"] == 200 assert result2["statusCode"] == 200 + + +def test_dict_response(): + # GIVEN a dict is returned + app = ApiGatewayResolver() + + @app.get("/lambda") + def get_message(): + return {"message": "success"} + + # WHEN calling handler + response = app({"httpMethod": "GET", "path": "/lambda"}, None) + + # THEN the body is correctly formatted, the status code is 200 and the content type is json + assert response["statusCode"] == 200 + assert response["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] + response_body = json.loads(response["body"]) + assert response_body["message"] == "success" + + +def test_dict_response_with_status_code(): + # GIVEN a dict is returned with a status code + app = ApiGatewayResolver() + + @app.get("/lambda") + def get_message(): + return {"message": "success"}, 201 + + # WHEN calling handler + response = app({"httpMethod": "GET", "path": "/lambda"}, None) + + # THEN the body is correctly formatted, the status code is 201 and the content type is json + assert response["statusCode"] == 201 + assert response["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] + response_body = json.loads(response["body"]) + assert response_body["message"] == "success" From db9e9d55f77b2c59eb1b4cb2e8c1757af4a4aaa8 Mon Sep 17 00:00:00 2001 From: Matthew Coyle Date: Mon, 23 Jan 2023 16:41:10 +0000 Subject: [PATCH 3/5] Fix type-hinting --- .../src/getting_started_middleware_after_logic_function.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/middleware_factory/src/getting_started_middleware_after_logic_function.py b/examples/middleware_factory/src/getting_started_middleware_after_logic_function.py index 9c52467341d..b2014e6a745 100644 --- a/examples/middleware_factory/src/getting_started_middleware_after_logic_function.py +++ b/examples/middleware_factory/src/getting_started_middleware_after_logic_function.py @@ -26,7 +26,7 @@ def middleware_after(handler, event, context) -> Callable: @app.post("/todos") -def create_todo() -> dict: +def create_todo() -> Tuple[dict, int]: todo_data: dict = app.current_event.json_body # deserialize json str to dict todo: Response = requests.post("https://jsonplaceholder.typicode.com/todos", data=todo_data) todo.raise_for_status() From b154a1c67f254c5f1570cebc5feb58f539c5bc5c Mon Sep 17 00:00:00 2001 From: Matthew Coyle Date: Mon, 23 Jan 2023 16:57:31 +0000 Subject: [PATCH 4/5] Fix missing import --- .../src/getting_started_middleware_after_logic_function.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/middleware_factory/src/getting_started_middleware_after_logic_function.py b/examples/middleware_factory/src/getting_started_middleware_after_logic_function.py index b2014e6a745..fb842c406cc 100644 --- a/examples/middleware_factory/src/getting_started_middleware_after_logic_function.py +++ b/examples/middleware_factory/src/getting_started_middleware_after_logic_function.py @@ -1,5 +1,5 @@ import time -from typing import Callable +from typing import Callable, Tuple import requests from requests import Response From a9cb0d610347aa169fa700b30dc707503b18a71c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Tue, 24 Jan 2023 11:45:19 +0000 Subject: [PATCH 5/5] chore: update documentation --- docs/core/event_handler/api_gateway.md | 7 ++++++- .../src/getting_started_return_tuple.py | 20 +++++++++++++++++++ .../event_handler_rest/src/http_methods.py | 2 +- .../src/http_methods_multiple.py | 2 +- ...started_middleware_after_logic_function.py | 6 +++--- 5 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 examples/event_handler_rest/src/getting_started_return_tuple.py diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index cee848c24c3..802b6112e04 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -45,7 +45,12 @@ A resolver will handle request resolution, including [one or more routers](#spli For resolvers, we provide: `APIGatewayRestResolver`, `APIGatewayHttpResolver`, `ALBResolver`, and `LambdaFunctionUrlResolver`. From here on, we will default to `APIGatewayRestResolver` across examples. ???+ info "Auto-serialization" - We serialize `Dict` responses as JSON, trim whitespace for compact responses, and set content-type to `application/json`. + We serialize `Dict` responses as JSON, trim whitespace for compact responses, set content-type to `application/json`, and + return a 200 OK HTTP status. You can optionally set a different HTTP status code as the second argument of the tuple: + + ```python hl_lines="15 16" + --8<-- "examples/event_handler_rest/src/getting_started_return_tuple.py" + ``` #### API Gateway REST API diff --git a/examples/event_handler_rest/src/getting_started_return_tuple.py b/examples/event_handler_rest/src/getting_started_return_tuple.py new file mode 100644 index 00000000000..1c26970c1c1 --- /dev/null +++ b/examples/event_handler_rest/src/getting_started_return_tuple.py @@ -0,0 +1,20 @@ +import requests +from requests import Response + +from aws_lambda_powertools.event_handler import ALBResolver +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = ALBResolver() + + +@app.post("/todo") +def create_todo(): + data: dict = app.current_event.json_body + todo: Response = requests.post("https://jsonplaceholder.typicode.com/todos", data=data) + + # Returns the created todo object, with a HTTP 201 Created status + return {"todo": todo.json()}, 201 + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/http_methods.py b/examples/event_handler_rest/src/http_methods.py index c0a0a652558..47eb1499a38 100644 --- a/examples/event_handler_rest/src/http_methods.py +++ b/examples/event_handler_rest/src/http_methods.py @@ -18,7 +18,7 @@ def create_todo(): todo: Response = requests.post("https://jsonplaceholder.typicode.com/todos", data=todo_data) todo.raise_for_status() - return {"todo": todo.json()}, 201 + return {"todo": todo.json()} # You can continue to use other utilities just as before diff --git a/examples/event_handler_rest/src/http_methods_multiple.py b/examples/event_handler_rest/src/http_methods_multiple.py index 7593ee393f5..a482c96d80f 100644 --- a/examples/event_handler_rest/src/http_methods_multiple.py +++ b/examples/event_handler_rest/src/http_methods_multiple.py @@ -19,7 +19,7 @@ def create_todo(): todo: Response = requests.post("https://jsonplaceholder.typicode.com/todos", data=todo_data) todo.raise_for_status() - return {"todo": todo.json()}, 201 + return {"todo": todo.json()} # You can continue to use other utilities just as before diff --git a/examples/middleware_factory/src/getting_started_middleware_after_logic_function.py b/examples/middleware_factory/src/getting_started_middleware_after_logic_function.py index fb842c406cc..e77328ca8f7 100644 --- a/examples/middleware_factory/src/getting_started_middleware_after_logic_function.py +++ b/examples/middleware_factory/src/getting_started_middleware_after_logic_function.py @@ -1,5 +1,5 @@ import time -from typing import Callable, Tuple +from typing import Callable import requests from requests import Response @@ -26,12 +26,12 @@ def middleware_after(handler, event, context) -> Callable: @app.post("/todos") -def create_todo() -> Tuple[dict, int]: +def create_todo() -> dict: todo_data: dict = app.current_event.json_body # deserialize json str to dict todo: Response = requests.post("https://jsonplaceholder.typicode.com/todos", data=todo_data) todo.raise_for_status() - return {"todo": todo.json()}, 201 + return {"todo": todo.json()} @middleware_after