From 2a51de0208ab17dfbb6710de692fd327d68fcff1 Mon Sep 17 00:00:00 2001 From: Heitor Lessa Date: Thu, 7 Jul 2022 16:47:43 +0200 Subject: [PATCH] docs(event-handler): snippets split, improved, and lint (#1279) --- docs/core/event_handler/api_gateway.md | 1166 +++-------------- docs/core/logger.md | 3 + docs/core/metrics.md | 3 + docs/core/tracer.md | 3 + examples/event_handler_rest/sam/template.yaml | 56 + .../src/accessing_request_details.py | 40 + .../src/accessing_request_details_headers.py | 30 + .../src/assert_http_response.py | 28 + .../src/assert_http_response_module.py | 27 + .../src/binary_responses.json | 8 + .../src/binary_responses.py | 27 + .../src/binary_responses_logo.svg | 14 + .../src/binary_responses_output.json | 8 + .../src/compressing_responses.json | 8 + .../src/compressing_responses.py | 28 + .../src/compressing_responses_output.json | 9 + .../src/custom_api_mapping.json | 5 + .../src/custom_api_mapping.py | 20 + .../src/custom_serializer.py | 58 + examples/event_handler_rest/src/debug_mode.py | 28 + .../src/dynamic_routes.json | 5 + .../event_handler_rest/src/dynamic_routes.py | 27 + .../src/dynamic_routes_catch_all.json | 5 + .../src/dynamic_routes_catch_all.py | 21 + .../src/exception_handling.py | 43 + .../src/fine_grained_responses.py | 36 + .../src/fine_grained_responses_output.json | 9 + .../src/getting_started_alb_api_resolver.py | 28 + .../src/getting_started_http_api_resolver.py | 28 + .../getting_started_rest_api_resolver.json | 58 + .../src/getting_started_rest_api_resolver.py | 28 + ...ting_started_rest_api_resolver_output.json | 8 + .../event_handler_rest/src/http_methods.json | 6 + .../event_handler_rest/src/http_methods.py | 28 + .../src/http_methods_multiple.py | 29 + .../src/not_found_routes.py | 35 + .../src/raising_http_errors.py | 59 + .../event_handler_rest/src/setting_cors.py | 44 + .../src/setting_cors_output.json | 10 + .../event_handler_rest/src/split_route.py | 18 + .../src/split_route_module.py | 33 + .../src/split_route_prefix.py | 19 + .../src/split_route_prefix_module.py | 36 + 43 files changed, 1164 insertions(+), 1018 deletions(-) create mode 100644 examples/event_handler_rest/sam/template.yaml create mode 100644 examples/event_handler_rest/src/accessing_request_details.py create mode 100644 examples/event_handler_rest/src/accessing_request_details_headers.py create mode 100644 examples/event_handler_rest/src/assert_http_response.py create mode 100644 examples/event_handler_rest/src/assert_http_response_module.py create mode 100644 examples/event_handler_rest/src/binary_responses.json create mode 100644 examples/event_handler_rest/src/binary_responses.py create mode 100644 examples/event_handler_rest/src/binary_responses_logo.svg create mode 100644 examples/event_handler_rest/src/binary_responses_output.json create mode 100644 examples/event_handler_rest/src/compressing_responses.json create mode 100644 examples/event_handler_rest/src/compressing_responses.py create mode 100644 examples/event_handler_rest/src/compressing_responses_output.json create mode 100644 examples/event_handler_rest/src/custom_api_mapping.json create mode 100644 examples/event_handler_rest/src/custom_api_mapping.py create mode 100644 examples/event_handler_rest/src/custom_serializer.py create mode 100644 examples/event_handler_rest/src/debug_mode.py create mode 100644 examples/event_handler_rest/src/dynamic_routes.json create mode 100644 examples/event_handler_rest/src/dynamic_routes.py create mode 100644 examples/event_handler_rest/src/dynamic_routes_catch_all.json create mode 100644 examples/event_handler_rest/src/dynamic_routes_catch_all.py create mode 100644 examples/event_handler_rest/src/exception_handling.py create mode 100644 examples/event_handler_rest/src/fine_grained_responses.py create mode 100644 examples/event_handler_rest/src/fine_grained_responses_output.json create mode 100644 examples/event_handler_rest/src/getting_started_alb_api_resolver.py create mode 100644 examples/event_handler_rest/src/getting_started_http_api_resolver.py create mode 100644 examples/event_handler_rest/src/getting_started_rest_api_resolver.json create mode 100644 examples/event_handler_rest/src/getting_started_rest_api_resolver.py create mode 100644 examples/event_handler_rest/src/getting_started_rest_api_resolver_output.json create mode 100644 examples/event_handler_rest/src/http_methods.json create mode 100644 examples/event_handler_rest/src/http_methods.py create mode 100644 examples/event_handler_rest/src/http_methods_multiple.py create mode 100644 examples/event_handler_rest/src/not_found_routes.py create mode 100644 examples/event_handler_rest/src/raising_http_errors.py create mode 100644 examples/event_handler_rest/src/setting_cors.py create mode 100644 examples/event_handler_rest/src/setting_cors_output.json create mode 100644 examples/event_handler_rest/src/split_route.py create mode 100644 examples/event_handler_rest/src/split_route_module.py create mode 100644 examples/event_handler_rest/src/split_route_prefix.py create mode 100644 examples/event_handler_rest/src/split_route_prefix_module.py diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index cf99b615a80..9db219e994e 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -8,14 +8,14 @@ Event handler for Amazon API Gateway REST and HTTP APIs, and Application Loader ## Key Features * Lightweight routing to reduce boilerplate for API Gateway REST/HTTP API and ALB -* Seamless support for CORS, binary and Gzip compression -* Integrates with [Data classes utilities](../../utilities/data_classes.md){target="_blank"} to easily access event and identity information -* Built-in support for Decimals JSON encoding -* Support for dynamic path expressions -* Router to allow for splitting up the handler across multiple files +* Support for CORS, binary and Gzip compression, Decimals JSON encoding and bring your own JSON serializer +* Built-in integration with [Event Source Data Classes utilities](../../utilities/data_classes.md){target="_blank"} for self-documented event schema ## Getting started +???+ tip + All examples shared in this documentation are available within the [project repository](https://github.com/awslabs/aws-lambda-powertools-python/tree/develop/examples){target="_blank"}. + ### Required resources You must have an existing [API Gateway Proxy integration](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html){target="_blank"} or [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html){target="_blank"} configured to invoke your Lambda function. @@ -25,54 +25,14 @@ This is the sample infrastructure for API Gateway we are using for the examples ???+ info "There is no additional permissions or dependencies required to use this utility." ```yaml title="AWS Serverless Application Model (SAM) example" -AWSTemplateFormatVersion: "2010-09-09" -Transform: AWS::Serverless-2016-10-31 -Description: Hello world event handler API Gateway - -Globals: - Api: - TracingEnabled: true - Cors: # see CORS section - AllowOrigin: "'https://example.com'" - AllowHeaders: "'Content-Type,Authorization,X-Amz-Date'" - MaxAge: "'300'" - BinaryMediaTypes: # see Binary responses section - - "*~1*" # converts to */* for any binary type - Function: - Timeout: 5 - Runtime: python3.8 - Tracing: Active - Environment: - Variables: - LOG_LEVEL: INFO - POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 - POWERTOOLS_LOGGER_LOG_EVENT: true - POWERTOOLS_METRICS_NAMESPACE: MyServerlessApplication - POWERTOOLS_SERVICE_NAME: my_api-service - -Resources: - ApiFunction: - Type: AWS::Serverless::Function - Properties: - Handler: app.lambda_handler - CodeUri: api_handler/ - Description: API handler function - Events: - ApiEvent: - Type: Api - Properties: - # NOTE: this is a catch-all rule to simply the documentation. - # explicit routes and methods are recommended for prod instead - # for example, Path: /hello, Method: GET - Path: /{proxy+} # Send requests on any path to the lambda function - Method: ANY # Send requests using any http method to the lambda function +--8<-- "examples/event_handler_rest/sam/template.yaml" ``` ### Event Resolvers Before you decorate your functions to handle a given path and HTTP method(s), you need to initialize a resolver. -A resolver will handle request resolution, include [one or more routers](#split-routes-with-router), and give you access to the current event via typed properties. +A resolver will handle request resolution, including [one or more routers](#split-routes-with-router), and give you access to the current event via typed properties. For resolvers, we provide: `APIGatewayRestResolver`, `APIGatewayHttpResolver`, and `ALBResolver`. @@ -83,113 +43,29 @@ For resolvers, we provide: `APIGatewayRestResolver`, `APIGatewayHttpResolver`, a When using Amazon API Gateway REST API to front your Lambda functions, you can use `APIGatewayRestResolver`. -Here's an example on how we can handle the `/hello` path. +Here's an example on how we can handle the `/todos` path. ???+ info We automatically serialize `Dict` responses as JSON, trim whitespace for compact responses, and set content-type to `application/json`. === "app.py" - ```python hl_lines="3 7 9 12 18" - from aws_lambda_powertools import Logger, Tracer - from aws_lambda_powertools.logging import correlation_paths - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - - tracer = Tracer() - logger = Logger() - app = APIGatewayRestResolver() - - @app.get("/hello") - @tracer.capture_method - def get_hello_universe(): - return {"message": "hello universe"} - - # You can continue to use other utilities just as before - @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) + ```python hl_lines="5 11 14 28" + --8<-- "examples/event_handler_rest/src/getting_started_rest_api_resolver.py" ``` -=== "hello_event.json" + +=== "Request" This utility uses `path` and `httpMethod` to route to the right function. This helps make unit tests and local invocation easier too. ```json hl_lines="4-5" - { - "body": "hello", - "resource": "/hello", - "path": "/hello", - "httpMethod": "GET", - "isBase64Encoded": false, - "queryStringParameters": { - "foo": "bar" - }, - "multiValueQueryStringParameters": {}, - "pathParameters": { - "hello": "/hello" - }, - "stageVariables": {}, - "headers": { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "Accept-Encoding": "gzip, deflate, sdch", - "Accept-Language": "en-US,en;q=0.8", - "Cache-Control": "max-age=0", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Host": "1234567890.execute-api.us-east-1.amazonaws.com", - "Upgrade-Insecure-Requests": "1", - "User-Agent": "Custom User Agent String", - "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", - "X-Forwarded-For": "127.0.0.1, 127.0.0.2", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "multiValueHeaders": {}, - "requestContext": { - "accountId": "123456789012", - "resourceId": "123456", - "stage": "Prod", - "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", - "requestTime": "25/Jul/2020:12:34:56 +0000", - "requestTimeEpoch": 1428582896000, - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "accessKey": null, - "sourceIp": "127.0.0.1", - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "Custom User Agent String", - "user": null - }, - "path": "/Prod/hello", - "resourcePath": "/hello", - "httpMethod": "POST", - "apiId": "1234567890", - "protocol": "HTTP/1.1" - } - } + --8<-- "examples/event_handler_rest/src/getting_started_rest_api_resolver.json" ``` -=== "response.json" +=== "Response" ```json - { - "statusCode": 200, - "headers": { - "Content-Type": "application/json" - }, - "body": "{\"message\":\"hello universe\"}", - "isBase64Encoded": false - } + --8<-- "examples/event_handler_rest/src/getting_started_rest_api_resolver_output.json" ``` #### API Gateway HTTP API @@ -199,477 +75,166 @@ When using Amazon API Gateway HTTP API to front your Lambda functions, you can u ???+ note Using HTTP API v1 payload? Use `APIGatewayRestResolver` instead. `APIGatewayHttpResolver` defaults to v2 payload. -Here's an example on how we can handle the `/hello` path. - -```python hl_lines="3 7" title="Using HTTP API resolver" -from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.logging import correlation_paths -from aws_lambda_powertools.event_handler import APIGatewayHttpResolver - -tracer = Tracer() -logger = Logger() -app = APIGatewayHttpResolver() - -@app.get("/hello") -@tracer.capture_method -def get_hello_universe(): - return {"message": "hello universe"} - -# You can continue to use other utilities just as before -@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) -@tracer.capture_lambda_handler -def lambda_handler(event, context): - return app.resolve(event, context) +```python hl_lines="5 11" title="Using HTTP API resolver" +--8<-- "examples/event_handler_rest/src/getting_started_http_api_resolver.py" ``` #### Application Load Balancer -When using Amazon Application Load Balancer to front your Lambda functions, you can use `ALBResolver`. - -```python hl_lines="3 7" title="Using ALB resolver" -from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.logging import correlation_paths -from aws_lambda_powertools.event_handler import ALBResolver - -tracer = Tracer() -logger = Logger() -app = ALBResolver() +When using Amazon Application Load Balancer (ALB) to front your Lambda functions, you can use `ALBResolver`. -@app.get("/hello") -@tracer.capture_method -def get_hello_universe(): - return {"message": "hello universe"} - -# You can continue to use other utilities just as before -@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPLICATION_LOAD_BALANCER) -@tracer.capture_lambda_handler -def lambda_handler(event, context): - return app.resolve(event, context) +```python hl_lines="5 11" title="Using ALB resolver" +--8<-- "examples/event_handler_rest/src/getting_started_alb_api_resolver.py" ``` ### Dynamic routes -You can use `/path/{dynamic_value}` when configuring dynamic URL paths. This allows you to define such dynamic value as part of your function signature. - -=== "app.py" - - ```python hl_lines="9 11" - from aws_lambda_powertools import Logger, Tracer - from aws_lambda_powertools.logging import correlation_paths - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - - tracer = Tracer() - logger = Logger() - app = APIGatewayRestResolver() - - @app.get("/hello/") - @tracer.capture_method - def get_hello_you(name): - return {"message": f"hello {name}"} - - # You can continue to use other utilities just as before - @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) - ``` - -=== "sample_request.json" +You can use `/todos/` to configure dynamic URL paths, where `` will be resolved at runtime. - ```json - { - "resource": "/hello/{name}", - "path": "/hello/lessa", - "httpMethod": "GET", - ... - } - ``` +Each dynamic route you set must be part of your function signature. This allows us to call your function using keyword arguments when matching your dynamic route. -#### Nested routes - -You can also nest paths as configured earlier in [our sample infrastructure](#required-resources): `/{message}/{name}`. +???+ note + For brevity, we will only include the necessary keys for each sample request for the example to work. === "app.py" - ```python hl_lines="9 11" - from aws_lambda_powertools import Logger, Tracer - from aws_lambda_powertools.logging import correlation_paths - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - - tracer = Tracer() - logger = Logger() - app = APIGatewayRestResolver() - - @app.get("//") - @tracer.capture_method - def get_message(message, name): - return {"message": f"{message}, {name}"} - - # You can continue to use other utilities just as before - @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) + ```python hl_lines="14 16" + --8<-- "examples/event_handler_rest/src/dynamic_routes.py" ``` -=== "sample_request.json" +=== "Request" ```json - { - "resource": "/{message}/{name}", - "path": "/hi/michael", - "httpMethod": "GET", - ... - } + --8<-- "examples/event_handler_rest/src/dynamic_routes.json" ``` +???+ tip + You can also nest dynamic paths, for example `/todos//`. + #### Catch-all routes ???+ note We recommend having explicit routes whenever possible; use catch-all routes sparingly. -You can use a regex string to handle an arbitrary number of paths within a request, for example `.+`. +You can use a [regex](https://docs.python.org/3/library/re.html#regular-expression-syntax){target="_blank"} string to handle an arbitrary number of paths within a request, for example `.+`. You can also combine nested paths with greedy regex to catch in between routes. ???+ warning - We will choose the more explicit registered route that match incoming event. + We choose the most explicit registered route that matches an incoming event. === "app.py" - ```python hl_lines="5" - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - - app = APIGatewayRestResolver() - - @app.get(".+") - def catch_any_route_after_any(): - return {"path_received": app.current_event.path} - - def lambda_handler(event, context): - return app.resolve(event, context) + ```python hl_lines="11" + --8<-- "examples/event_handler_rest/src/dynamic_routes_catch_all.py" ``` -=== "sample_request.json" +=== "Request" ```json - { - "resource": "/any/route/should/work", - "path": "/any/route/should/work", - "httpMethod": "GET", - ... - } + --8<-- "examples/event_handler_rest/src/dynamic_routes_catch_all.json" ``` ### HTTP Methods -You can use named decorators to specify the HTTP method that should be handled in your functions. As well as the -`get` method already shown above, you can use `post`, `put`, `patch`, `delete`, and `patch`. +You can use named decorators to specify the HTTP method that should be handled in your functions. That is, `app.`, where the HTTP method could be `get`, `post`, `put`, `patch`, `delete`, and `options`. === "app.py" - ```python hl_lines="9-10" - from aws_lambda_powertools import Logger, Tracer - from aws_lambda_powertools.logging import correlation_paths - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - - tracer = Tracer() - logger = Logger() - app = APIGatewayRestResolver() - - # Only POST HTTP requests to the path /hello will route to this function - @app.post("/hello") - @tracer.capture_method - def get_hello_you(): - name = app.current_event.json_body.get("name") - return {"message": f"hello {name}"} - - # You can continue to use other utilities just as before - @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) + ```python hl_lines="14 17" + --8<-- "examples/event_handler_rest/src/http_methods.py" ``` -=== "sample_request.json" +=== "Request" ```json - { - "resource": "/hello/{name}", - "path": "/hello/lessa", - "httpMethod": "GET", - ... - } - ``` - -If you need to accept multiple HTTP methods in a single function, you can use the `route` method and pass a list of -HTTP methods. - -=== "app.py" - - ```python hl_lines="9-10" - from aws_lambda_powertools import Logger, Tracer - from aws_lambda_powertools.logging import correlation_paths - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - - tracer = Tracer() - logger = Logger() - app = APIGatewayRestResolver() - - # PUT and POST HTTP requests to the path /hello will route to this function - @app.route("/hello", method=["PUT", "POST"]) - @tracer.capture_method - def get_hello_you(): - name = app.current_event.json_body.get("name") - return {"message": f"hello {name}"} - - # You can continue to use other utilities just as before - @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) + --8<-- "examples/event_handler_rest/src/http_methods.json" ``` -=== "sample_request.json" +If you need to accept multiple HTTP methods in a single function, you can use the `route` method and pass a list of HTTP methods. - ```json - { - "resource": "/hello/{name}", - "path": "/hello/lessa", - "httpMethod": "GET", - ... - } - ``` +```python hl_lines="15" title="Handling multiple HTTP Methods" +--8<-- "examples/event_handler_rest/src/http_methods_multiple.py" +``` ???+ note - It is usually better to have separate functions for each HTTP method, as the functionality tends to differ depending on which method is used. + It is generally better to have separate functions for each HTTP method, as the functionality tends to differ depending on which method is used. ### Accessing request details -By integrating with [Data classes utilities](../../utilities/data_classes.md){target="_blank"}, you have access to request details, Lambda context and also some convenient methods. +Event Handler integrates with [Event Source Data Classes utilities](../../utilities/data_classes.md){target="_blank"}, and it exposes their respective resolver request details and convenient methods under `app.current_event`. -These are made available in the response returned when instantiating `APIGatewayRestResolver`, for example `app.current_event` and `app.lambda_context`. +That is why you see `app.resolve(event, context)` in every example. This allows Event Handler to resolve requests, and expose data like `app.lambda_context` and `app.current_event`. #### Query strings and payload -Within `app.current_event` property, you can access query strings as dictionary via `query_string_parameters`, or by name via `get_query_string_value` method. - -You can access the raw payload via `body` property, or if it's a JSON string you can quickly deserialize it via `json_body` property. +Within `app.current_event` property, you can access all available query strings as a dictionary via `query_string_parameters`, or a specific one via `get_query_string_value` method. -```python hl_lines="7-9 11" title="Accessing query strings, JSON payload, and raw payload" -from aws_lambda_powertools.event_handler import APIGatewayRestResolver +You can access the raw payload via `body` property, or if it's a JSON string you can quickly deserialize it via `json_body` property - like the earlier example in the [HTTP Methods](#http-methods) section. -app = APIGatewayRestResolver() - -@app.get("/hello") -def get_hello_you(): - query_strings_as_dict = app.current_event.query_string_parameters - json_payload = app.current_event.json_body - payload = app.current_event.body - - name = app.current_event.get_query_string_value(name="name", default_value="") - return {"message": f"hello {name}"} - -def lambda_handler(event, context): - return app.resolve(event, context) +```python hl_lines="19 24" title="Accessing query strings and raw payload" +--8<-- "examples/event_handler_rest/src/accessing_request_details.py" ``` #### Headers Similarly to [Query strings](#query-strings-and-payload), you can access headers as dictionary via `app.current_event.headers`, or by name via `get_header_value`. -```python hl_lines="7-8" title="Accessing HTTP Headers" -from aws_lambda_powertools.event_handler import APIGatewayRestResolver - -app = APIGatewayRestResolver() - -@app.get("/hello") -def get_hello_you(): - headers_as_dict = app.current_event.headers - name = app.current_event.get_header_value(name="X-Name", default_value="") - - return {"message": f"hello {name}"} - -def lambda_handler(event, context): - return app.resolve(event, context) +```python hl_lines="19" title="Accessing HTTP Headers" +--8<-- "examples/event_handler_rest/src/accessing_request_details_headers.py" ``` ### Handling not found routes By default, we return `404` for any unmatched route. -You can use **`not_found`** decorator to override this behaviour, and return a custom **`Response`**. - -```python hl_lines="11 13 16" title="Handling not found" -from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.logging import correlation_paths -from aws_lambda_powertools.event_handler import content_types -from aws_lambda_powertools.event_handler.api_gateway import APIGatewayRestResolver, Response -from aws_lambda_powertools.event_handler.exceptions import NotFoundError - -tracer = Tracer() -logger = Logger() -app = APIGatewayRestResolver() - -@app.not_found -@tracer.capture_method -def handle_not_found_errors(exc: NotFoundError) -> Response: - # Return 418 upon 404 errors - logger.info(f"Not found route: {app.current_event.path}") - return Response( - status_code=418, - content_type=content_types.TEXT_PLAIN, - body="I'm a teapot!" - ) - - -@app.get("/catch/me/if/you/can") -@tracer.capture_method -def catch_me_if_you_can(): - return {"message": "oh hey"} - -@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) -@tracer.capture_lambda_handler -def lambda_handler(event, context): - return app.resolve(event, context) +You can use **`not_found`** decorator to override this behavior, and return a custom **`Response`**. + +```python hl_lines="14 18" title="Handling not found" +--8<-- "examples/event_handler_rest/src/not_found_routes.py" ``` ### Exception handling You can use **`exception_handler`** decorator with any Python exception. This allows you to handle a common exception outside your route, for example validation errors. -```python hl_lines="10 15" title="Exception handling" -from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.logging import correlation_paths -from aws_lambda_powertools.event_handler import content_types -from aws_lambda_powertools.event_handler.api_gateway import APIGatewayRestResolver, Response - -tracer = Tracer() -logger = Logger() -app = APIGatewayRestResolver() - -@app.exception_handler(ValueError) -def handle_value_error(ex: ValueError): - metadata = {"path": app.current_event.path} - logger.error(f"Malformed request: {ex}", extra=metadata) - - return Response( - status_code=400, - content_type=content_types.TEXT_PLAIN, - body="Invalid request", - ) - - -@app.get("/hello") -@tracer.capture_method -def hello_name(): - name = app.current_event.get_query_string_value(name="name") - if name is not None: - raise ValueError("name query string must be present") - return {"message": f"hello {name}"} - -@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) -@tracer.capture_lambda_handler -def lambda_handler(event, context): - return app.resolve(event, context) +```python hl_lines="14 15" title="Exception handling" +--8<-- "examples/event_handler_rest/src/exception_handling.py" ``` ### Raising HTTP errors -You can easily raise any HTTP Error back to the client using `ServiceError` exception. +You can easily raise any HTTP Error back to the client using `ServiceError` exception. This ensures your Lambda function doesn't fail but return the correct HTTP response signalling the error. ???+ info If you need to send custom headers, use [Response](#fine-grained-responses) class instead. -Additionally, we provide pre-defined errors for the most popular ones such as HTTP 400, 401, 404, 500. - -```python hl_lines="4-10 20 25 30 35 39" title="Raising common HTTP Status errors (4xx, 5xx)" -from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.logging import correlation_paths -from aws_lambda_powertools.event_handler import APIGatewayRestResolver -from aws_lambda_powertools.event_handler.exceptions import ( - BadRequestError, - InternalServerError, - NotFoundError, - ServiceError, - UnauthorizedError, -) - -tracer = Tracer() -logger = Logger() - -app = APIGatewayRestResolver() - -@app.get(rule="/bad-request-error") -def bad_request_error(): - # HTTP 400 - raise BadRequestError("Missing required parameter") - -@app.get(rule="/unauthorized-error") -def unauthorized_error(): - # HTTP 401 - raise UnauthorizedError("Unauthorized") - -@app.get(rule="/not-found-error") -def not_found_error(): - # HTTP 404 - raise NotFoundError - -@app.get(rule="/internal-server-error") -def internal_server_error(): - # HTTP 500 - raise InternalServerError("Internal server error") - -@app.get(rule="/service-error", cors=True) -def service_error(): - raise ServiceError(502, "Something went wrong!") - # alternatively - # from http import HTTPStatus - # raise ServiceError(HTTPStatus.BAD_GATEWAY.value, "Something went wrong) - -def handler(event, context): - return app.resolve(event, context) +We provide pre-defined errors for the most popular ones such as HTTP 400, 401, 404, 500. + +```python hl_lines="6-11 23 28 33 38 43" title="Raising common HTTP Status errors (4xx, 5xx)" +--8<-- "examples/event_handler_rest/src/raising_http_errors.py" ``` ### Custom Domain API Mappings -When using Custom Domain API Mappings feature, you must use **`strip_prefixes`** param in the `APIGatewayRestResolver` constructor. - -Scenario: You have a custom domain `api.mydomain.dev` and set an API Mapping `payment` to forward requests to your Payments API, the path argument will be `/payment/`. +When using [Custom Domain API Mappings feature](https://docs.aws.amazon.com/apigateway/latest/developerguide/rest-api-mappings.html){target="_blank"}, you must use **`strip_prefixes`** param in the `APIGatewayRestResolver` constructor. -This will lead to a HTTP 404 despite having your Lambda configured correctly. See the example below on how to account for this change. - -=== "app.py" +**Scenario**: You have a custom domain `api.mydomain.dev`. Then you set `/payment` API Mapping to forward any payment requests to your Payments API. - ```python hl_lines="7" - from aws_lambda_powertools import Logger, Tracer - from aws_lambda_powertools.logging import correlation_paths - from aws_lambda_powertools.event_handler import APIGatewayRestResolver +**Challenge**: This means your `path` value for any API requests will always contain `/payment/`, leading to HTTP 404 as Event Handler is trying to match what's after `payment/`. This gets further complicated with an [arbitrary level of nesting](https://github.com/awslabs/aws-lambda-powertools-roadmap/issues/34). - tracer = Tracer() - logger = Logger() - app = APIGatewayRestResolver(strip_prefixes=["/payment"]) +To address this API Gateway behavior, we use `strip_prefixes` parameter to account for these prefixes that are now injected into the path regardless of which type of API Gateway you're using. - @app.get("/subscriptions/") - @tracer.capture_method - def get_subscription(subscription): - return {"subscription_id": subscription} +=== "app.py" - @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) + ```python hl_lines="8" + --8<-- "examples/event_handler_rest/src/custom_api_mapping.py" ``` -=== "sample_request.json" +=== "Request" ```json - { - "resource": "/subscriptions/{subscription}", - "path": "/payment/subscriptions/123", - "httpMethod": "GET", - ... - } + --8<-- "examples/event_handler_rest/src/custom_api_mapping.json" ``` ???+ note @@ -685,67 +250,21 @@ You can configure CORS at the `APIGatewayRestResolver` constructor via `cors` pa This will ensure that CORS headers are always returned as part of the response when your functions match the path invoked. -=== "app.py" - - ```python hl_lines="9 11" - from aws_lambda_powertools import Logger, Tracer - from aws_lambda_powertools.logging import correlation_paths - from aws_lambda_powertools.event_handler.api_gateway import APIGatewayRestResolver, CORSConfig - - tracer = Tracer() - logger = Logger() - - cors_config = CORSConfig(allow_origin="https://example.com", max_age=300) - app = APIGatewayRestResolver(cors=cors_config) - - @app.get("/hello/") - @tracer.capture_method - def get_hello_you(name): - return {"message": f"hello {name}"} - - @app.get("/hello", cors=False) # optionally exclude CORS from response, if needed - @tracer.capture_method - def get_hello_no_cors_needed(): - return {"message": "hello, no CORS needed for this path ;)"} - - # You can continue to use other utilities just as before - @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) - ``` +???+ tip + Optionally disable CORS on a per path basis with `cors=False` parameter. -=== "response.json" +=== "app.py" - ```json - { - "statusCode": 200, - "headers": { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "https://www.example.com", - "Access-Control-Allow-Headers": "Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token,X-Api-Key" - }, - "body": "{\"message\":\"hello lessa\"}", - "isBase64Encoded": false - } + ```python hl_lines="5 11-12 34" + --8<-- "examples/event_handler_rest/src/setting_cors.py" ``` -=== "response_no_cors.json" +=== "Response" ```json - { - "statusCode": 200, - "headers": { - "Content-Type": "application/json" - }, - "body": "{\"message\":\"hello lessa\"}", - "isBase64Encoded": false - } + --8<-- "examples/event_handler_rest/src/setting_cors_output.json" ``` -???+ tip - Optionally disable CORS on a per path basis with `cors=False` parameter. - #### Pre-flight Pre-flight (OPTIONS) calls are typically handled at the API Gateway level as per [our sample infrastructure](#required-resources), no Lambda integration necessary. However, ALB expects you to handle pre-flight requests. @@ -773,40 +292,15 @@ You can use the `Response` class to have full control over the response, for exa === "app.py" - ```python hl_lines="11-16" - import json - from aws_lambda_powertools.event_handler.api_gateway import APIGatewayRestResolver, Response - - app = APIGatewayRestResolver() - - @app.get("/hello") - def get_hello_you(): - payload = json.dumps({"message": "I'm a teapot"}) - custom_headers = {"X-Custom": "X-Value"} - - return Response( - status_code=418, - content_type="application/json", - body=payload, - headers=custom_headers, - ) - - def lambda_handler(event, context): - return app.resolve(event, context) + ```python hl_lines="7 24-28" + --8<-- "examples/event_handler_rest/src/fine_grained_responses.py" ``` -=== "response.json" +=== "Response" ```json - { - "body": "{\"message\":\"I\'m a teapot\"}", - "headers": { - "Content-Type": "application/json", - "X-Custom": "X-Value" - }, - "isBase64Encoded": false, - "statusCode": 418 - } + --8<-- "examples/event_handler_rest/src/fine_grained_responses_output.json" + ``` ### Compress @@ -817,44 +311,20 @@ You can compress with gzip and base64 encode your responses via `compress` param === "app.py" - ```python hl_lines="5 7" - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - - app = APIGatewayRestResolver() - - @app.get("/hello", compress=True) - def get_hello_you(): - return {"message": "hello universe"} - - def lambda_handler(event, context): - return app.resolve(event, context) + ```python hl_lines="14" + --8<-- "examples/event_handler_rest/src/compressing_responses.py" ``` -=== "sample_request.json" +=== "Request" ```json - { - "headers": { - "Accept-Encoding": "gzip" - }, - "httpMethod": "GET", - "path": "/hello", - ... - } + --8<-- "examples/event_handler_rest/src/compressing_responses.json" ``` -=== "response.json" +=== "Response" ```json - { - "body": "H4sIAAAAAAACE6tWyk0tLk5MT1WyUspIzcnJVyjNyyxLLSpOVaoFANha8kEcAAAA", - "headers": { - "Content-Encoding": "gzip", - "Content-Type": "application/json" - }, - "isBase64Encoded": true, - "statusCode": 200 - } + --8<-- "examples/event_handler_rest/src/compressing_responses_output.json" ``` ### Binary responses @@ -868,89 +338,26 @@ Like `compress` feature, the client must send the `Accept` header with the corre === "app.py" - ```python hl_lines="4 7 11" - import os - from pathlib import Path - - from aws_lambda_powertools.event_handler.api_gateway import APIGatewayRestResolver, Response - - app = APIGatewayRestResolver() - logo_file: bytes = Path(os.getenv("LAMBDA_TASK_ROOT") + "/logo.svg").read_bytes() - - @app.get("/logo") - def get_logo(): - return Response(status_code=200, content_type="image/svg+xml", body=logo_file) - - def lambda_handler(event, context): - return app.resolve(event, context) + ```python hl_lines="14 20" + --8<-- "examples/event_handler_rest/src/binary_responses.py" ``` === "logo.svg" ```xml - - - - - - - - - - - - + --8<-- "examples/event_handler_rest/src/binary_responses_logo.svg" ``` -=== "sample_request.json" + +=== "Request" ```json - { - "headers": { - "Accept": "image/svg+xml" - }, - "httpMethod": "GET", - "path": "/logo", - ... - } + --8<-- "examples/event_handler_rest/src/binary_responses.json" ``` -=== "response.json" +=== "Response" ```json - { - "body": "H4sIAAAAAAACE3VXa2scRxD87ID/w+byKTCzN899yFZMLBLHYEMg4K9BHq0l4c2duDudZIf891TVrPwiMehmd+fR3dXV1eOnz+7/mpvjtNtfbzenK9+6VTNtyvbienN5uro9vLPD6tlPj797+r21zYtpM+3OD9vdSfPzxfbt1Lyc59v9QZ8aP7au9ab5482L5pf7m+3u0Pw+317al5um1cc31chJ07XONc9vr+eLxv3YNNby/P3x8ks3/Kq5vjhdvTr/MO3+xAu83OxPV1eHw83Jen13d9fexXa7u1wH59wam5clJ/fz9eb9fy304ziuNYulpyt3c79qPtTx8XePmuP1dPd8y4nGNdGlxg9h1ewPH+bpdDVtzt/Ok317Xt5f7ra3m4uTzXTXfLHyicyf7G/OC5bf7Kb9tDtOKwXGI5rDhxtMHKb7w7rs95x41O4P7u931/N88sOv+vfkn/rV66vd3c7TyXScNtuLiydlvr75+su3O5+uZYkmL3n805vzw1VT5vM9cIOpVQM8Xw9dm0yHn+JMbHvj+IoRiJuhHYtrBxPagPfBpLbDmmD6NuB7NpxzWttpDG3EKd46vAfr29HE2XZtxMYABx4VzIxY2VmvnaMN2jkW642zAdPZRkyms76DndGZPpthgEt9MvB0wEJM91gacUpsvc3c3eO4sYXJHuf52A42jNjEp2qXRzjrMzaENtngLGOwCS4krO7xzXscoIeR4WFLNpFbEo7GNrhdOhkEGElrgUyCx3gokQYAHMOLxjvFVY1XVDNQy0AKkx4PgPSIjcALv8QDf0He9NZ3BaEFhTdgInESMPKBMwAemzxTZT1zgFP5vRekOJTg8zucquEvCULsXOx1hjY5bWKuAh1fFkbuIGABa71+4cuRcMHfuiboMB6Kw8gGW5mQtDUwBa1f4s/Kd6+1iD8oplyIvq9oebEFYBOKsXi+ORNEJBKLbBhaXzIcZ0YGbgMF9IAkdG9I4Y/N65RhaYCLi+morPSipK8RMlmdIgahbFR+s2UF+Gpe3ieip6/kayCbkHpYRUp6QgH6MGFEgLuiFQHbviLO/DkdEGkbk4ljsawtR7J1zIAFk0aTioBBpIQYbmWNJArqKQlXxh9UoSQXjZxFIGoGFmzSPM/8FD+w8IDNmxG+l1pwlr5Ey/rwzP1gay1mG5Ykj6/GrpoIRZOMYqR3GiudHijAFJPJiePVCGBr2mIlE0bEUKpIMFrQwjCEcQabB4pOmJVyPolCYWEnYJZVyU+VE4JrQC56cPWtpfSVHfhkJD60RDy6foYyRNv1NZlCXoh/YwM05C7rEU0sitKERehqrLkiYCrhvcSO53VFrzxeAqB0UxHzbMFPb/q+1ltVRoITiTnNKRWm0ownRlbpFUu/iI5uYRMEoMb/kLt+yR3BSq98xtkQXElWl5h1yg6nvcz5SrVFta1UHTz3v4koIEzIVPgRKlkkc44ykipJsip7kVMWdICDFPBMMoOwUhlbRb23NX/UjqHYesi4sK2OmDhaWpLKiE1YzxbCsUhATZUlb2q7iBX7Kj/Kc80atEz66yWyXorhGTIkRqnrSURu8fWhdNIFKT7B8UnNJPIUwYLgLVHkOD7knC4rjNpFeturrBRRbmtHkpTh5VVIncmBnYlpjhT3HhMUd1urK0rQE7AE14goJdFRWBYZHyUIcLLm3AuhwF5qO7Zg4B+KTodiJCaSOMN4SXbRC+pR1Vs8FEZGOcnCtKvNvnC/aoiKj2+dekO1GdS4VMfAQo2++KXOonIgf5ifoo6hOkm6EFDP8pItNXvVpFNdxiNErThVXG1UQXHEz/eEYWk/jEmCRcyyaKtWKbVSr1YNc6rytcLnq6AORazytbMa9nqOutgYdUPmGL72nyKmlzxMVcjpPLPdE7cC1MlQQkpyZHasjPbRFVpJ+mNPqlcln6Tekk5lg7cd/9CbJMkkXFInSmrcw4PHQS1p0HZSANa6s8CqNiN/Qh7hI0vVfK7aj6u1Lnq67n173/P1vhd6Nf+ETgJLgSyjjYGpj2SVD3JM96PM+xRRZYcMtV8NJHKn3bW+pUydGMFg1CMelUSIgjwj4nGUVULDxxJJM1zvsM/q0uZ5TQggwFnoRanI9h76gcSJDPYLz5dA/y/EgXnygRcGostStqFXv0KdD7qP6MYUTKVXr1uhEzty8QP5plqDXbZuk1mtuUZGv3jtg8JIFKHTJrt6H9AduN4TAE6q95qzMEikMmkVRq+bKQXrC0cfUrdm7h5+8b8YjP8Cgadmu5INAAA=", - "headers": { - "Content-Type": "image/svg+xml" - }, - "isBase64Encoded": true, - "statusCode": 200 - } + --8<-- "examples/event_handler_rest/src/binary_responses_output.json" ``` ### Debug mode @@ -964,326 +371,88 @@ This will enable full tracebacks errors in the response, print request and respo It's best to use for local development only! -```python hl_lines="3" title="Enabling debug mode" -from aws_lambda_powertools.event_handler import APIGatewayRestResolver - -app = APIGatewayRestResolver(debug=True) - -@app.get("/hello") -def get_hello_universe(): - return {"message": "hello universe"} - -def lambda_handler(event, context): - return app.resolve(event, context) +```python hl_lines="11" title="Enabling debug mode" +--8<-- "examples/event_handler_rest/src/debug_mode.py" ``` ### Custom serializer You can instruct API Gateway handler to use a custom serializer to best suit your needs, for example take into account Enums when serializing. -```python hl_lines="21-22 26" title="Using a custom JSON serializer for responses" -import json -from enum import Enum -from json import JSONEncoder -from typing import Dict - -from aws_lambda_powertools.event_handler import APIGatewayRestResolver - -class CustomEncoder(JSONEncoder): - """Your customer json encoder""" - def default(self, obj): - if isinstance(obj, Enum): - return obj.value - try: - iterable = iter(obj) - except TypeError: - pass - else: - return sorted(iterable) - return JSONEncoder.default(self, obj) - -def custom_serializer(obj) -> str: - """Your custom serializer function APIGatewayRestResolver will use""" - return json.dumps(obj, cls=CustomEncoder) - -# Assigning your custom serializer -app = APIGatewayRestResolver(serializer=custom_serializer) - -class Color(Enum): - RED = 1 - BLUE = 2 - -@app.get("/colors") -def get_color() -> Dict: - return { - # Color.RED will be serialized to 1 as expected now - "color": Color.RED, - "variations": {"light", "dark"}, - } +```python hl_lines="35 40" title="Using a custom JSON serializer for responses" +--8<-- "examples/event_handler_rest/src/custom_serializer.py" ``` ### Split routes with Router As you grow the number of routes a given Lambda function should handle, it is natural to split routes into separate files to ease maintenance - That's where the `Router` feature is useful. -Let's assume you have `app.py` as your Lambda function entrypoint and routes in `users.py`, this is how you'd use the `Router` feature. +Let's assume you have `app.py` as your Lambda function entrypoint and routes in `todos.py`, this is how you'd use the `Router` feature. -=== "users.py" +=== "todos.py" We import **Router** instead of **APIGatewayRestResolver**; syntax wise is exactly the same. - ```python hl_lines="5 8 12 15 21" - import itertools - from typing import Dict - - from aws_lambda_powertools import Logger - from aws_lambda_powertools.event_handler.api_gateway import Router - - logger = Logger(child=True) - router = Router() - USERS = {"user1": "details_here", "user2": "details_here", "user3": "details_here"} - - - @router.get("/users") - def get_users() -> Dict: - # /users?limit=1 - pagination_limit = router.current_event.get_query_string_value(name="limit", default_value=10) - - logger.info(f"Fetching the first {pagination_limit} users...") - ret = dict(itertools.islice(USERS.items(), int(pagination_limit))) - return {"items": [ret]} - - @router.get("/users/") - def get_user(username: str) -> Dict: - logger.info(f"Fetching username {username}") - return {"details": USERS.get(username, {})} - - # many other related /users routing + ```python hl_lines="5 13 16 25 28" + --8<-- "examples/event_handler_rest/src/split_route_module.py" ``` === "app.py" We use `include_router` method and include all user routers registered in the `router` global object. - ```python hl_lines="7 10-11" - from typing import Dict - - from aws_lambda_powertools import Logger - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - from aws_lambda_powertools.utilities.typing import LambdaContext - - import users - - logger = Logger() - app = APIGatewayRestResolver() - app.include_router(users.router) - - - def lambda_handler(event: Dict, context: LambdaContext): - return app.resolve(event, context) + ```python hl_lines="11" + --8<-- "examples/event_handler_rest/src/split_route.py" ``` #### Route prefix -In the previous example, `users.py` routes had a `/users` prefix. This might grow over time and become repetitive. +In the previous example, `todos.py` routes had a `/todos` prefix. This might grow over time and become repetitive. -When necessary, you can set a prefix when including a router object. This means you could remove `/users` prefix in `users.py` altogether. +When necessary, you can set a prefix when including a router object. This means you could remove `/todos` prefix in `todos.py` altogether. === "app.py" - ```python hl_lines="9" - from typing import Dict - - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - from aws_lambda_powertools.utilities.typing import LambdaContext - - import users - - app = APIGatewayRestResolver() - app.include_router(users.router, prefix="/users") # prefix '/users' to any route in `users.router` - - - def lambda_handler(event: Dict, context: LambdaContext): - return app.resolve(event, context) + ```python hl_lines="12" + --8<-- "examples/event_handler_rest/src/split_route_prefix.py" ``` -=== "users.py" - - ```python hl_lines="11 15" - from typing import Dict - - from aws_lambda_powertools import Logger - from aws_lambda_powertools.event_handler.api_gateway import Router - - logger = Logger(child=True) - router = Router() - USERS = {"user1": "details", "user2": "details", "user3": "details"} - +=== "todos.py" - @router.get("/") # /users, when we set the prefix in app.py - def get_users() -> Dict: - ... - - @router.get("/") - def get_user(username: str) -> Dict: - ... - - # many other related /users routing + ```python hl_lines="13 25" + --8<-- "examples/event_handler_rest/src/split_route_prefix_module.py" ``` #### Sample layout -This sample project contains a Users function with two distinct set of routes, `/users` and `/health`. The layout optimizes for code sharing, no custom build tooling, and it uses [Lambda Layers](../../index.md#lambda-layer) to install Lambda Powertools. - -=== "Project layout" - - ```python hl_lines="1 8 10 12-15" - . - ├── Pipfile # project app & dev dependencies; poetry, pipenv, etc. - ├── Pipfile.lock - ├── README.md - ├── src - │ ├── __init__.py - │ ├── requirements.txt # sam build detect it automatically due to CodeUri: src, e.g. pipenv lock -r > src/requirements.txt - │ └── users - │ ├── __init__.py - │ ├── main.py # this will be our users Lambda fn; it could be split in folders if we want separate fns same code base - │ └── routers # routers module - │ ├── __init__.py - │ ├── health.py # /users routes, e.g. from routers import users; users.router - │ └── users.py # /users routes, e.g. from .routers import users; users.router - ├── template.yml # SAM template.yml, CodeUri: src, Handler: users.main.lambda_handler - └── tests +This is a sample project layout for a monolithic function with routes split in different files (`/todos`, `/health`). + +```shell hl_lines="4 7 10 12-13" title="Sample project layout" +. +├── pyproject.toml # project app & dev dependencies; poetry, pipenv, etc. +├── poetry.lock +├── src +│ ├── __init__.py +│ ├── requirements.txt # sam build detect it automatically due to CodeUri: src. poetry export --format src/requirements.txt +│ └── todos +│ ├── __init__.py +│ ├── main.py # this will be our todos Lambda fn; it could be split in folders if we want separate fns same code base +│ └── routers # routers module +│ ├── __init__.py +│ ├── health.py # /health routes. from routers import todos; health.router +│ └── todos.py # /todos routes. from .routers import todos; todos.router +├── template.yml # SAM. CodeUri: src, Handler: todos.main.lambda_handler +└── tests + ├── __init__.py + ├── unit + │ ├── __init__.py + │ └── test_todos.py # unit tests for the todos router + │ └── test_health.py # unit tests for the health router + └── functional ├── __init__.py - ├── unit - │ ├── __init__.py - │ └── test_users.py # unit tests for the users router - │ └── test_health.py # unit tests for the health router - └── functional - ├── __init__.py - ├── conftest.py # pytest fixtures for the functional tests - └── test_main.py # functional tests for the main lambda handler - ``` - -=== "template.yml" - - ```yaml hl_lines="22-23" - AWSTemplateFormatVersion: '2010-09-09' - Transform: AWS::Serverless-2016-10-31 - Description: Example service with multiple routes - Globals: - Function: - Timeout: 10 - MemorySize: 512 - Runtime: python3.9 - Tracing: Active - Architectures: - - x86_64 - Environment: - Variables: - LOG_LEVEL: INFO - POWERTOOLS_LOGGER_LOG_EVENT: true - POWERTOOLS_METRICS_NAMESPACE: MyServerlessApplication - POWERTOOLS_SERVICE_NAME: users - Resources: - UsersService: - Type: AWS::Serverless::Function - Properties: - Handler: users.main.lambda_handler - CodeUri: src - Layers: - # Latest version: https://awslabs.github.io/aws-lambda-powertools-python/latest/#lambda-layer - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:4 - Events: - ByUser: - Type: Api - Properties: - Path: /users/{name} - Method: GET - AllUsers: - Type: Api - Properties: - Path: /users - Method: GET - HealthCheck: - Type: Api - Properties: - Path: /status - Method: GET - Outputs: - UsersApiEndpoint: - Description: "API Gateway endpoint URL for Prod environment for Users Function" - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod" - AllUsersURL: - Description: "URL to fetch all registered users" - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/users" - ByUserURL: - Description: "URL to retrieve details by user" - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/users/test" - UsersServiceFunctionArn: - Description: "Users Lambda Function ARN" - Value: !GetAtt UsersService.Arn - ``` - -=== "src/users/main.py" - - ```python hl_lines="8 14-15" - from typing import Dict - - from aws_lambda_powertools import Logger, Tracer - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - from aws_lambda_powertools.logging.correlation_paths import APPLICATION_LOAD_BALANCER - from aws_lambda_powertools.utilities.typing import LambdaContext - - from .routers import health, users - - tracer = Tracer() - logger = Logger() - app = APIGatewayRestResolver() - - app.include_router(health.router) - app.include_router(users.router) - - - @logger.inject_lambda_context(correlation_id_path=API_GATEWAY_REST) - @tracer.capture_lambda_handler - def lambda_handler(event: Dict, context: LambdaContext): - return app.resolve(event, context) - ``` - -=== "src/users/routers/health.py" - - ```python hl_lines="4 6-7 10" - from typing import Dict - - from aws_lambda_powertools import Logger - from aws_lambda_powertools.event_handler.api_gateway import Router - - router = Router() - logger = Logger(child=True) - - - @router.get("/status") - def health() -> Dict: - logger.debug("Health check called") - return {"status": "OK"} - ``` - -=== "tests/functional/test_users.py" - - ```python hl_lines="3" - import json - - from src.users import main # follows namespace package from root - - - def test_lambda_handler(apigw_event, lambda_context): - ret = main.lambda_handler(apigw_event, lambda_context) - expected = json.dumps({"message": "hello universe"}, separators=(",", ":")) - - assert ret["statusCode"] == 200 - assert ret["body"] == expected - ``` + ├── conftest.py # pytest fixtures for the functional tests + └── test_main.py # functional tests for the main lambda handler +``` ### Considerations @@ -1342,53 +511,14 @@ You can test your routes by passing a proxy event request where `path` and `http === "test_app.py" - ```python hl_lines="18-24" - from dataclasses import dataclass - - import pytest - import app - - @pytest.fixture - def lambda_context(): - @dataclass - class LambdaContext: - function_name: str = "test" - memory_limit_in_mb: int = 128 - invoked_function_arn: str = "arn:aws:lambda:eu-west-1:809313241:function:test" - aws_request_id: str = "52fdfc07-2182-154f-163f-5f0f9a621d72" - - return LambdaContext() - - def test_lambda_handler(lambda_context): - minimal_event = { - "path": "/hello", - "httpMethod": "GET", - "requestContext": { # correlation ID - "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef" - } - } - - app.lambda_handler(minimal_event, lambda_context) + ```python hl_lines="21-24" + --8<-- "examples/event_handler_rest/src/assert_http_response.py" ``` === "app.py" ```python - from aws_lambda_powertools import Logger - from aws_lambda_powertools.logging import correlation_paths - from aws_lambda_powertools.event_handler import APIGatewayRestResolver - - logger = Logger() - app = APIGatewayRestResolver() # API Gateway REST API (v1) - - @app.get("/hello") - def get_hello_universe(): - return {"message": "hello universe"} - - # You can continue to use other utilities just as before - @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) - def lambda_handler(event, context): - return app.resolve(event, context) + --8<-- "examples/event_handler_rest/src/assert_http_response_module.py" ``` ## FAQ diff --git a/docs/core/logger.md b/docs/core/logger.md index 23d57e251b9..b09cc6c85d3 100644 --- a/docs/core/logger.md +++ b/docs/core/logger.md @@ -14,6 +14,9 @@ Logger provides an opinionated logger with output structured as JSON. ## Getting started +???+ tip + All examples shared in this documentation are available within the [project repository](https://github.com/awslabs/aws-lambda-powertools-python/tree/develop/examples){target="_blank"}. + Logger requires two settings: | Setting | Description | Environment variable | Constructor parameter | diff --git a/docs/core/metrics.md b/docs/core/metrics.md index 24a8f1e6fda..713a53b193c 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -28,6 +28,9 @@ If you're new to Amazon CloudWatch, there are two terminologies you must be awar ## Getting started +???+ tip + All examples shared in this documentation are available within the [project repository](https://github.com/awslabs/aws-lambda-powertools-python/tree/develop/examples){target="_blank"}. + Metric has two global settings that will be used across all metrics emitted: | Setting | Description | Environment variable | Constructor parameter | diff --git a/docs/core/tracer.md b/docs/core/tracer.md index c8037eff241..7664231cc31 100644 --- a/docs/core/tracer.md +++ b/docs/core/tracer.md @@ -16,6 +16,9 @@ Tracer is an opinionated thin wrapper for [AWS X-Ray Python SDK](https://github. ## Getting started +???+ tip + All examples shared in this documentation are available within the [project repository](https://github.com/awslabs/aws-lambda-powertools-python/tree/develop/examples){target="_blank"}. + ### Permissions Before your use this utility, your AWS Lambda function [must have permissions](https://docs.aws.amazon.com/lambda/latest/dg/services-xray.html#services-xray-permissions) to send traces to AWS X-Ray. diff --git a/examples/event_handler_rest/sam/template.yaml b/examples/event_handler_rest/sam/template.yaml new file mode 100644 index 00000000000..f9837e729a5 --- /dev/null +++ b/examples/event_handler_rest/sam/template.yaml @@ -0,0 +1,56 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: Hello world event handler API Gateway + +Globals: + Api: + TracingEnabled: true + Cors: # see CORS section + AllowOrigin: "'https://example.com'" + AllowHeaders: "'Content-Type,Authorization,X-Amz-Date'" + MaxAge: "'300'" + BinaryMediaTypes: # see Binary responses section + - "*~1*" # converts to */* for any binary type + Function: + Timeout: 5 + Runtime: python3.8 + Tracing: Active + Environment: + Variables: + LOG_LEVEL: INFO + POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 + POWERTOOLS_LOGGER_LOG_EVENT: true + POWERTOOLS_SERVICE_NAME: example + +Resources: + ApiFunction: + Type: AWS::Serverless::Function + Properties: + Handler: app.lambda_handler + CodeUri: api_handler/ + Description: API handler function + Events: + AnyApiEvent: + Type: Api + Properties: + # NOTE: this is a catch-all rule to simplify the documentation. + # explicit routes and methods are recommended for prod instead (see below) + Path: /{proxy+} # Send requests on any path to the lambda function + Method: ANY # Send requests using any http method to the lambda function + + + # GetAllTodos: + # Type: Api + # Properties: + # Path: /todos + # Method: GET + # GetTodoById: + # Type: Api + # Properties: + # Path: /todos/{todo_id} + # Method: GET + # CreateTodo: + # Type: Api + # Properties: + # Path: /todos + # Method: POST diff --git a/examples/event_handler_rest/src/accessing_request_details.py b/examples/event_handler_rest/src/accessing_request_details.py new file mode 100644 index 00000000000..9929b601db0 --- /dev/null +++ b/examples/event_handler_rest/src/accessing_request_details.py @@ -0,0 +1,40 @@ +from typing import Optional + +import requests +from requests import Response + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver() + + +@app.get("/todos") +@tracer.capture_method +def get_todos(): + todo_id: str = app.current_event.get_query_string_value(name="id", default_value="") + # alternatively + _: Optional[str] = app.current_event.query_string_parameters.get("id") + + # Payload + _: Optional[str] = app.current_event.body # raw str | None + + endpoint = "https://jsonplaceholder.typicode.com/todos" + if todo_id: + endpoint = f"{endpoint}/{todo_id}" + + todos: Response = requests.get(endpoint) + todos.raise_for_status() + + return {"todos": todos.json()} + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/accessing_request_details_headers.py b/examples/event_handler_rest/src/accessing_request_details_headers.py new file mode 100644 index 00000000000..f6bfb88c869 --- /dev/null +++ b/examples/event_handler_rest/src/accessing_request_details_headers.py @@ -0,0 +1,30 @@ +import requests +from requests import Response + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver() + + +@app.get("/todos") +@tracer.capture_method +def get_todos(): + endpoint = "https://jsonplaceholder.typicode.com/todos" + + api_key: str = app.current_event.get_header_value(name="X-Api-Key", case_sensitive=True, default_value="") + todos: Response = requests.get(endpoint, headers={"X-Api-Key": api_key}) + todos.raise_for_status() + + return {"todos": todos.json()} + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/assert_http_response.py b/examples/event_handler_rest/src/assert_http_response.py new file mode 100644 index 00000000000..95d56599288 --- /dev/null +++ b/examples/event_handler_rest/src/assert_http_response.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass + +import assert_http_response_module +import pytest + + +@pytest.fixture +def lambda_context(): + @dataclass + class LambdaContext: + function_name: str = "test" + memory_limit_in_mb: int = 128 + invoked_function_arn: str = "arn:aws:lambda:eu-west-1:123456789012:function:test" + aws_request_id: str = "da658bd3-2d6f-4e7b-8ec2-937234644fdc" + + return LambdaContext() + + +def test_lambda_handler(lambda_context): + minimal_event = { + "path": "/todos", + "httpMethod": "GET", + "requestContext": {"requestId": "227b78aa-779d-47d4-a48e-ce62120393b8"}, # correlation ID + } + + ret = assert_http_response_module.lambda_handler(minimal_event, lambda_context) + assert ret["statusCode"] == 200 + assert ret["body"] != "" diff --git a/examples/event_handler_rest/src/assert_http_response_module.py b/examples/event_handler_rest/src/assert_http_response_module.py new file mode 100644 index 00000000000..ea5d839fb72 --- /dev/null +++ b/examples/event_handler_rest/src/assert_http_response_module.py @@ -0,0 +1,27 @@ +import requests +from requests import Response + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver() + + +@app.get("/todos") +@tracer.capture_method +def get_todos(): + todos: Response = requests.get("https://jsonplaceholder.typicode.com/todos") + todos.raise_for_status() + + return {"todos": todos.json()[:10]} + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/binary_responses.json b/examples/event_handler_rest/src/binary_responses.json new file mode 100644 index 00000000000..fcdf86dfebe --- /dev/null +++ b/examples/event_handler_rest/src/binary_responses.json @@ -0,0 +1,8 @@ +{ + "headers": { + "Accept": "image/svg+xml" + }, + "resource": "/logo", + "path": "/logo", + "httpMethod": "GET" +} diff --git a/examples/event_handler_rest/src/binary_responses.py b/examples/event_handler_rest/src/binary_responses.py new file mode 100644 index 00000000000..00c027937b8 --- /dev/null +++ b/examples/event_handler_rest/src/binary_responses.py @@ -0,0 +1,27 @@ +import os +from pathlib import Path + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler.api_gateway import APIGatewayRestResolver, Response +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() + + +app = APIGatewayRestResolver() +logo_file: bytes = Path(os.getenv("LAMBDA_TASK_ROOT") + "/logo.svg").read_bytes() + + +@app.get("/logo") +@tracer.capture_method +def get_logo(): + return Response(status_code=200, content_type="image/svg+xml", body=logo_file) + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/binary_responses_logo.svg b/examples/event_handler_rest/src/binary_responses_logo.svg new file mode 100644 index 00000000000..fccb29e01ed --- /dev/null +++ b/examples/event_handler_rest/src/binary_responses_logo.svg @@ -0,0 +1,14 @@ + + + AWS Lambda + + + + + + + + + + + diff --git a/examples/event_handler_rest/src/binary_responses_output.json b/examples/event_handler_rest/src/binary_responses_output.json new file mode 100644 index 00000000000..0938dee6811 --- /dev/null +++ b/examples/event_handler_rest/src/binary_responses_output.json @@ -0,0 +1,8 @@ +{ + "body": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjU2cHgiIGhlaWdodD0iMjU2cHgiIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj4KICAgIDx0aXRsZT5BV1MgTGFtYmRhPC90aXRsZT4KICAgIDxkZWZzPgogICAgICAgIDxsaW5lYXJHcmFkaWVudCB4MT0iMCUiIHkxPSIxMDAlIiB4Mj0iMTAwJSIgeTI9IjAlIiBpZD0ibGluZWFyR3JhZGllbnQtMSI+CiAgICAgICAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiNDODUxMUIiIG9mZnNldD0iMCUiPjwvc3RvcD4KICAgICAgICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iI0ZGOTkwMCIgb2Zmc2V0PSIxMDAlIj48L3N0b3A+CiAgICAgICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDwvZGVmcz4KICAgIDxnPgogICAgICAgIDxyZWN0IGZpbGw9InVybCgjbGluZWFyR3JhZGllbnQtMSkiIHg9IjAiIHk9IjAiIHdpZHRoPSIyNTYiIGhlaWdodD0iMjU2Ij48L3JlY3Q+CiAgICAgICAgPHBhdGggZD0iTTg5LjYyNDExMjYsMjExLjIgTDQ5Ljg5MDMyNzcsMjExLjIgTDkzLjgzNTQ4MzIsMTE5LjM0NzIgTDExMy43NDcyOCwxNjAuMzM5MiBMODkuNjI0MTEyNiwyMTEuMiBaIE05Ni43MDI5MzU3LDExMC41Njk2IEM5Ni4xNjQwODU4LDEwOS40NjU2IDk1LjA0MTQ4MTMsMTA4Ljc2NDggOTMuODE2MjM4NCwxMDguNzY0OCBMOTMuODA2NjE2MywxMDguNzY0OCBDOTIuNTcxNzUxNCwxMDguNzY4IDkxLjQ0OTE0NjYsMTA5LjQ3NTIgOTAuOTE5OTE4NywxMTAuNTg1NiBMNDEuOTEzNDIwOCwyMTMuMDIwOCBDNDEuNDM4NzE5NywyMTQuMDEyOCA0MS41MDYwNzU4LDIxNS4xNzc2IDQyLjA5NjI0NTEsMjE2LjEwODggQzQyLjY3OTk5OTQsMjE3LjAzNjggNDMuNzA2MzgwNSwyMTcuNiA0NC44MDY1MzMxLDIxNy42IEw5MS42NTQ0MjMsMjE3LjYgQzkyLjg5NTcwMjcsMjE3LjYgOTQuMDIxNTE0OSwyMTYuODg2NCA5NC41NTM5NTAxLDIxNS43Njk2IEwxMjAuMjAzODU5LDE2MS42ODk2IEMxMjAuNjE3NjE5LDE2MC44MTI4IDEyMC42MTQ0MTIsMTU5Ljc5ODQgMTIwLjE4NzgyMiwxNTguOTI4IEw5Ni43MDI5MzU3LDExMC41Njk2IFogTTIwNy45ODUxMTcsMjExLjIgTDE2OC41MDc5MjgsMjExLjIgTDEwNS4xNzM3ODksNzguNjI0IEMxMDQuNjQ0NTYxLDc3LjUxMDQgMTAzLjUxNTU0MSw3Ni44IDEwMi4yNzc0NjksNzYuOCBMNzYuNDQ3OTQzLDc2LjggTDc2LjQ3NjgwOTksNDQuOCBMMTI3LjEwMzA2Niw0NC44IEwxOTAuMTQ1MzI4LDE3Ny4zNzI4IEMxOTAuNjc0NTU2LDE3OC40ODY0IDE5MS44MDM1NzUsMTc5LjIgMTkzLjA0MTY0NywxNzkuMiBMMjA3Ljk4NTExNywxNzkuMiBMMjA3Ljk4NTExNywyMTEuMiBaIE0yMTEuMTkyNTU4LDE3Mi44IEwxOTUuMDcxOTU4LDE3Mi44IEwxMzIuMDI5Njk2LDQwLjIyNzIgQzEzMS41MDA0NjgsMzkuMTEzNiAxMzAuMzcxNDQ5LDM4LjQgMTI5LjEzMDE2OSwzOC40IEw3My4yNzI1NzYsMzguNCBDNzEuNTA1Mjc1OCwzOC40IDcwLjA2ODM0MjEsMzkuODMwNCA3MC4wNjUxMzQ0LDQxLjU5NjggTDcwLjAyOTg1MjgsNzkuOTk2OCBDNzAuMDI5ODUyOCw4MC44NDggNzAuMzYzNDI2Niw4MS42NjA4IDcwLjk2OTYzMyw4Mi4yNjI0IEM3MS41Njk0MjQ2LDgyLjg2NCA3Mi4zODQxMTQ2LDgzLjIgNzMuMjM3Mjk0MSw4My4yIEwxMDAuMjUzNTczLDgzLjIgTDE2My41OTA5MiwyMTUuNzc2IEMxNjQuMTIzMzU1LDIxNi44ODk2IDE2NS4yNDU5NiwyMTcuNiAxNjYuNDg0MDMyLDIxNy42IEwyMTEuMTkyNTU4LDIxNy42IEMyMTIuOTY2Mjc0LDIxNy42IDIxNC40LDIxNi4xNjY0IDIxNC40LDIxNC40IEwyMTQuNCwxNzYgQzIxNC40LDE3NC4yMzM2IDIxMi45NjYyNzQsMTcyLjggMjExLjE5MjU1OCwxNzIuOCBMMjExLjE5MjU1OCwxNzIuOCBaIiBmaWxsPSIjRkZGRkZGIj48L3BhdGg+CiAgICA8L2c+Cjwvc3ZnPg==", + "headers": { + "Content-Type": "image/svg+xml" + }, + "isBase64Encoded": true, + "statusCode": 200 +} diff --git a/examples/event_handler_rest/src/compressing_responses.json b/examples/event_handler_rest/src/compressing_responses.json new file mode 100644 index 00000000000..f706df20d58 --- /dev/null +++ b/examples/event_handler_rest/src/compressing_responses.json @@ -0,0 +1,8 @@ +{ + "headers": { + "Accept-Encoding": "gzip" + }, + "resource": "/todos", + "path": "/todos", + "httpMethod": "GET" +} diff --git a/examples/event_handler_rest/src/compressing_responses.py b/examples/event_handler_rest/src/compressing_responses.py new file mode 100644 index 00000000000..1af4b9a58b2 --- /dev/null +++ b/examples/event_handler_rest/src/compressing_responses.py @@ -0,0 +1,28 @@ +import requests +from requests import Response + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver() + + +@app.get("/todos", compress=True) +@tracer.capture_method +def get_todos(): + todos: Response = requests.get("https://jsonplaceholder.typicode.com/todos") + todos.raise_for_status() + + # for brevity, we'll limit to the first 10 only + return {"todos": todos.json()[:10]} + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/compressing_responses_output.json b/examples/event_handler_rest/src/compressing_responses_output.json new file mode 100644 index 00000000000..0836b3aa726 --- /dev/null +++ b/examples/event_handler_rest/src/compressing_responses_output.json @@ -0,0 +1,9 @@ +{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json", + "Content-Encoding": "gzip" + }, + "body": "H4sIAAAAAAACE42STU4DMQyFrxJl3QXln96AMyAW7sSDLCVxiJ0Kqerd8TCCUOgii1EmP/783pOPXjmw+N3L0TfB+hz8brvxtC5KGtHvfMCIkzZx0HT5MPmNnziViIr2dIYoeNr8Q1x3xHsjcVadIbkZJoq2RXU8zzQROLseQ9505NzeCNQdMJNBE+UmY4zbzjAJhWtlZ57sB84BWtul+rteH2HPlVgWARwjqXkxpklK5gmEHAQqJBMtFsGVygcKmNVRjG0wxvuzGF2L0dpVUOKMC3bfJNjJgWMrCuZk7cUp02AiD72D6WKHHwUDKbiJs6AZ0VZXKOUx4uNvzdxT+E4mLcMA+6G8nzrLQkaxkNEVrFKW2VGbJCoCY7q2V3+tiv5kGThyxfTecDWbgGz/NfYXhL6ePgF9PnFdPgMAAA==", + "isBase64Encoded": true +} diff --git a/examples/event_handler_rest/src/custom_api_mapping.json b/examples/event_handler_rest/src/custom_api_mapping.json new file mode 100644 index 00000000000..eb1d68afbf9 --- /dev/null +++ b/examples/event_handler_rest/src/custom_api_mapping.json @@ -0,0 +1,5 @@ +{ + "resource": "/subscriptions/{subscription}", + "path": "/payment/subscriptions/123", + "httpMethod": "GET" +} diff --git a/examples/event_handler_rest/src/custom_api_mapping.py b/examples/event_handler_rest/src/custom_api_mapping.py new file mode 100644 index 00000000000..0b180d54f01 --- /dev/null +++ b/examples/event_handler_rest/src/custom_api_mapping.py @@ -0,0 +1,20 @@ +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver(strip_prefixes=["/payment"]) + + +@app.get("/subscriptions/") +@tracer.capture_method +def get_subscription(subscription): + return {"subscription_id": subscription} + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/custom_serializer.py b/examples/event_handler_rest/src/custom_serializer.py new file mode 100644 index 00000000000..cfb8cefd2d9 --- /dev/null +++ b/examples/event_handler_rest/src/custom_serializer.py @@ -0,0 +1,58 @@ +import json +from dataclasses import asdict, dataclass, is_dataclass +from json import JSONEncoder + +import requests +from requests import Response + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver() + + +@dataclass +class Todo: + userId: str + id: str # noqa: A003 VNE003 "id" field is reserved + title: str + completed: bool + + +class DataclassCustomEncoder(JSONEncoder): + """A custom JSON encoder to serialize dataclass obj""" + + def default(self, obj): + # Only called for values that aren't JSON serializable + # where `obj` will be an instance of Todo in this example + return asdict(obj) if is_dataclass(obj) else super().default(obj) + + +def custom_serializer(obj) -> str: + """Your custom serializer function APIGatewayRestResolver will use""" + return json.dumps(obj, separators=(",", ":"), cls=DataclassCustomEncoder) + + +app = APIGatewayRestResolver(serializer=custom_serializer) + + +@app.get("/todos") +@tracer.capture_method +def get_todos(): + ret: Response = requests.get("https://jsonplaceholder.typicode.com/todos") + ret.raise_for_status() + todos = [Todo(**todo) for todo in ret.json()] + + # for brevity, we'll limit to the first 10 only + return {"todos": todos[:10]} + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/debug_mode.py b/examples/event_handler_rest/src/debug_mode.py new file mode 100644 index 00000000000..47ffb8905eb --- /dev/null +++ b/examples/event_handler_rest/src/debug_mode.py @@ -0,0 +1,28 @@ +import requests +from requests import Response + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver(debug=True) + + +@app.get("/todos") +@tracer.capture_method +def get_todos(): + todos: Response = requests.get("https://jsonplaceholder.typicode.com/todos") + todos.raise_for_status() + + # for brevity, we'll limit to the first 10 only + return {"todos": todos.json()[:10]} + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/dynamic_routes.json b/examples/event_handler_rest/src/dynamic_routes.json new file mode 100644 index 00000000000..23e8261d283 --- /dev/null +++ b/examples/event_handler_rest/src/dynamic_routes.json @@ -0,0 +1,5 @@ +{ + "resource": "/todos/{id}", + "path": "/todos/1", + "httpMethod": "GET" +} diff --git a/examples/event_handler_rest/src/dynamic_routes.py b/examples/event_handler_rest/src/dynamic_routes.py new file mode 100644 index 00000000000..2ee2dc21044 --- /dev/null +++ b/examples/event_handler_rest/src/dynamic_routes.py @@ -0,0 +1,27 @@ +import requests +from requests import Response + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver() + + +@app.get("/todos/") +@tracer.capture_method +def get_todo_by_id(todo_id: str): # value come as str + todos: Response = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") + todos.raise_for_status() + + return {"todos": todos.json()} + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/dynamic_routes_catch_all.json b/examples/event_handler_rest/src/dynamic_routes_catch_all.json new file mode 100644 index 00000000000..c9395f23027 --- /dev/null +++ b/examples/event_handler_rest/src/dynamic_routes_catch_all.json @@ -0,0 +1,5 @@ +{ + "resource": "/{proxy+}", + "path": "/any/route/should/work", + "httpMethod": "GET" +} diff --git a/examples/event_handler_rest/src/dynamic_routes_catch_all.py b/examples/event_handler_rest/src/dynamic_routes_catch_all.py new file mode 100644 index 00000000000..f615f2a8dee --- /dev/null +++ b/examples/event_handler_rest/src/dynamic_routes_catch_all.py @@ -0,0 +1,21 @@ +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver() + + +@app.get(".+") +@tracer.capture_method +def catch_any_route_get_method(): + return {"path_received": app.current_event.path} + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/exception_handling.py b/examples/event_handler_rest/src/exception_handling.py new file mode 100644 index 00000000000..fdac8589299 --- /dev/null +++ b/examples/event_handler_rest/src/exception_handling.py @@ -0,0 +1,43 @@ +import requests +from requests import Response + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver, content_types +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver() + + +@app.exception_handler(ValueError) +def handle_invalid_limit_qs(ex: ValueError): # receives exception raised + metadata = {"path": app.current_event.path, "query_strings": app.current_event.query_string_parameters} + logger.error(f"Malformed request: {ex}", extra=metadata) + + return Response( + status_code=400, + content_type=content_types.TEXT_PLAIN, + body="Invalid request parameters.", + ) + + +@app.get("/todos") +@tracer.capture_method +def get_todos(): + # educational purpose only: we should receive a `ValueError` + # if a query string value for `limit` cannot be coerced to int + max_results: int = int(app.current_event.get_query_string_value(name="limit", default_value=0)) + + todos: Response = requests.get(f"https://jsonplaceholder.typicode.com/todos?limit={max_results}") + todos.raise_for_status() + + return {"todos": todos.json()} + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/fine_grained_responses.py b/examples/event_handler_rest/src/fine_grained_responses.py new file mode 100644 index 00000000000..3e477160307 --- /dev/null +++ b/examples/event_handler_rest/src/fine_grained_responses.py @@ -0,0 +1,36 @@ +from http import HTTPStatus +from uuid import uuid4 + +import requests + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response, content_types +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver() + + +@app.get("/todos") +@tracer.capture_method +def get_todos(): + todos: requests.Response = requests.get("https://jsonplaceholder.typicode.com/todos") + todos.raise_for_status() + + custom_headers = {"X-Transaction-Id": f"{uuid4()}"} + + return Response( + status_code=HTTPStatus.OK.value, # 200 + content_type=content_types.APPLICATION_JSON, + body=todos.json()[:10], + headers=custom_headers, + ) + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/fine_grained_responses_output.json b/examples/event_handler_rest/src/fine_grained_responses_output.json new file mode 100644 index 00000000000..c3d58098e80 --- /dev/null +++ b/examples/event_handler_rest/src/fine_grained_responses_output.json @@ -0,0 +1,9 @@ +{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json", + "X-Transaction-Id": "3490eea9-791b-47a0-91a4-326317db61a9" + }, + "body": "{\"todos\":[{\"userId\":1,\"id\":1,\"title\":\"delectus aut autem\",\"completed\":false},{\"userId\":1,\"id\":2,\"title\":\"quis ut nam facilis et officia qui\",\"completed\":false},{\"userId\":1,\"id\":3,\"title\":\"fugiat veniam minus\",\"completed\":false},{\"userId\":1,\"id\":4,\"title\":\"et porro tempora\",\"completed\":true},{\"userId\":1,\"id\":5,\"title\":\"laboriosam mollitia et enim quasi adipisci quia provident illum\",\"completed\":false},{\"userId\":1,\"id\":6,\"title\":\"qui ullam ratione quibusdam voluptatem quia omnis\",\"completed\":false},{\"userId\":1,\"id\":7,\"title\":\"illo expedita consequatur quia in\",\"completed\":false},{\"userId\":1,\"id\":8,\"title\":\"quo adipisci enim quam ut ab\",\"completed\":true},{\"userId\":1,\"id\":9,\"title\":\"molestiae perspiciatis ipsa\",\"completed\":false},{\"userId\":1,\"id\":10,\"title\":\"illo est ratione doloremque quia maiores aut\",\"completed\":true}]}", + "isBase64Encoded": false +} diff --git a/examples/event_handler_rest/src/getting_started_alb_api_resolver.py b/examples/event_handler_rest/src/getting_started_alb_api_resolver.py new file mode 100644 index 00000000000..612823625ec --- /dev/null +++ b/examples/event_handler_rest/src/getting_started_alb_api_resolver.py @@ -0,0 +1,28 @@ +import requests +from requests import Response + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import ALBResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = ALBResolver() + + +@app.get("/todos") +@tracer.capture_method +def get_todos(): + todos: Response = requests.get("https://jsonplaceholder.typicode.com/todos") + todos.raise_for_status() + + # for brevity, we'll limit to the first 10 only + return {"todos": todos.json()[:10]} + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPLICATION_LOAD_BALANCER) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/getting_started_http_api_resolver.py b/examples/event_handler_rest/src/getting_started_http_api_resolver.py new file mode 100644 index 00000000000..e976ef4169f --- /dev/null +++ b/examples/event_handler_rest/src/getting_started_http_api_resolver.py @@ -0,0 +1,28 @@ +import requests +from requests import Response + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayHttpResolver() + + +@app.get("/todos") +@tracer.capture_method +def get_todos(): + todos: Response = requests.get("https://jsonplaceholder.typicode.com/todos") + todos.raise_for_status() + + # for brevity, we'll limit to the first 10 only + return {"todos": todos.json()[:10]} + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/getting_started_rest_api_resolver.json b/examples/event_handler_rest/src/getting_started_rest_api_resolver.json new file mode 100644 index 00000000000..92d3e40f139 --- /dev/null +++ b/examples/event_handler_rest/src/getting_started_rest_api_resolver.json @@ -0,0 +1,58 @@ +{ + "body": "", + "resource": "/todos", + "path": "/todos", + "httpMethod": "GET", + "isBase64Encoded": false, + "queryStringParameters": {}, + "multiValueQueryStringParameters": {}, + "pathParameters": {}, + "stageVariables": {}, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": {}, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "Prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "25/Jul/2020:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/Prod/todos", + "resourcePath": "/todos", + "httpMethod": "GET", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/examples/event_handler_rest/src/getting_started_rest_api_resolver.py b/examples/event_handler_rest/src/getting_started_rest_api_resolver.py new file mode 100644 index 00000000000..3b30b5810f2 --- /dev/null +++ b/examples/event_handler_rest/src/getting_started_rest_api_resolver.py @@ -0,0 +1,28 @@ +import requests +from requests import Response + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver() + + +@app.get("/todos") +@tracer.capture_method +def get_todos(): + todos: Response = requests.get("https://jsonplaceholder.typicode.com/todos") + todos.raise_for_status() + + # for brevity, we'll limit to the first 10 only + return {"todos": todos.json()[:10]} + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/getting_started_rest_api_resolver_output.json b/examples/event_handler_rest/src/getting_started_rest_api_resolver_output.json new file mode 100644 index 00000000000..2ef3714531f --- /dev/null +++ b/examples/event_handler_rest/src/getting_started_rest_api_resolver_output.json @@ -0,0 +1,8 @@ +{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\"todos\":[{\"userId\":1,\"id\":1,\"title\":\"delectus aut autem\",\"completed\":false},{\"userId\":1,\"id\":2,\"title\":\"quis ut nam facilis et officia qui\",\"completed\":false},{\"userId\":1,\"id\":3,\"title\":\"fugiat veniam minus\",\"completed\":false},{\"userId\":1,\"id\":4,\"title\":\"et porro tempora\",\"completed\":true},{\"userId\":1,\"id\":5,\"title\":\"laboriosam mollitia et enim quasi adipisci quia provident illum\",\"completed\":false},{\"userId\":1,\"id\":6,\"title\":\"qui ullam ratione quibusdam voluptatem quia omnis\",\"completed\":false},{\"userId\":1,\"id\":7,\"title\":\"illo expedita consequatur quia in\",\"completed\":false},{\"userId\":1,\"id\":8,\"title\":\"quo adipisci enim quam ut ab\",\"completed\":true},{\"userId\":1,\"id\":9,\"title\":\"molestiae perspiciatis ipsa\",\"completed\":false},{\"userId\":1,\"id\":10,\"title\":\"illo est ratione doloremque quia maiores aut\",\"completed\":true}]}", + "isBase64Encoded": false +} diff --git a/examples/event_handler_rest/src/http_methods.json b/examples/event_handler_rest/src/http_methods.json new file mode 100644 index 00000000000..e0f775d72df --- /dev/null +++ b/examples/event_handler_rest/src/http_methods.json @@ -0,0 +1,6 @@ +{ + "resource": "/todos", + "path": "/todos", + "httpMethod": "POST", + "body": "{\"title\": \"foo\", \"userId\": 1, \"completed\": false}" +} diff --git a/examples/event_handler_rest/src/http_methods.py b/examples/event_handler_rest/src/http_methods.py new file mode 100644 index 00000000000..47eb1499a38 --- /dev/null +++ b/examples/event_handler_rest/src/http_methods.py @@ -0,0 +1,28 @@ +import requests +from requests import Response + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver() + + +@app.post("/todos") +@tracer.capture_method +def create_todo(): + 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()} + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/http_methods_multiple.py b/examples/event_handler_rest/src/http_methods_multiple.py new file mode 100644 index 00000000000..a482c96d80f --- /dev/null +++ b/examples/event_handler_rest/src/http_methods_multiple.py @@ -0,0 +1,29 @@ +import requests +from requests import Response + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver() + + +# PUT and POST HTTP requests to the path /hello will route to this function +@app.route("/todos", method=["PUT", "POST"]) +@tracer.capture_method +def create_todo(): + 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()} + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/not_found_routes.py b/examples/event_handler_rest/src/not_found_routes.py new file mode 100644 index 00000000000..889880292c0 --- /dev/null +++ b/examples/event_handler_rest/src/not_found_routes.py @@ -0,0 +1,35 @@ +import requests + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response, content_types +from aws_lambda_powertools.event_handler.exceptions import NotFoundError +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver() + + +@app.not_found +@tracer.capture_method +def handle_not_found_errors(exc: NotFoundError) -> Response: + logger.info(f"Not found route: {app.current_event.path}") + return Response(status_code=418, content_type=content_types.TEXT_PLAIN, body="I'm a teapot!") + + +@app.get("/todos") +@tracer.capture_method +def get_todos(): + todos: requests.Response = requests.get("https://jsonplaceholder.typicode.com/todos") + todos.raise_for_status() + + # for brevity, we'll limit to the first 10 only + return {"todos": todos.json()[:10]} + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/raising_http_errors.py b/examples/event_handler_rest/src/raising_http_errors.py new file mode 100644 index 00000000000..97e7cc5048f --- /dev/null +++ b/examples/event_handler_rest/src/raising_http_errors.py @@ -0,0 +1,59 @@ +import requests +from requests import Response + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.event_handler.exceptions import ( + BadRequestError, + InternalServerError, + NotFoundError, + ServiceError, + UnauthorizedError, +) +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver() + + +@app.get(rule="/bad-request-error") +def bad_request_error(): + raise BadRequestError("Missing required parameter") # HTTP 400 + + +@app.get(rule="/unauthorized-error") +def unauthorized_error(): + raise UnauthorizedError("Unauthorized") # HTTP 401 + + +@app.get(rule="/not-found-error") +def not_found_error(): + raise NotFoundError # HTTP 404 + + +@app.get(rule="/internal-server-error") +def internal_server_error(): + raise InternalServerError("Internal server error") # HTTP 500 + + +@app.get(rule="/service-error", cors=True) +def service_error(): + raise ServiceError(502, "Something went wrong!") + + +@app.get("/todos") +@tracer.capture_method +def get_todos(): + todos: Response = requests.get("https://jsonplaceholder.typicode.com/todos") + todos.raise_for_status() + + return {"todos": todos.json()[:10]} + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/setting_cors.py b/examples/event_handler_rest/src/setting_cors.py new file mode 100644 index 00000000000..101e013e552 --- /dev/null +++ b/examples/event_handler_rest/src/setting_cors.py @@ -0,0 +1,44 @@ +import requests +from requests import Response + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver, CORSConfig +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +cors_config = CORSConfig(allow_origin="https://example.com", max_age=300) +app = APIGatewayRestResolver(cors=cors_config) + + +@app.get("/todos") +@tracer.capture_method +def get_todos(): + todos: Response = requests.get("https://jsonplaceholder.typicode.com/todos") + todos.raise_for_status() + + # for brevity, we'll limit to the first 10 only + return {"todos": todos.json()[:10]} + + +@app.get("/todos/") +@tracer.capture_method +def get_todo_by_id(todo_id: str): # value come as str + todos: Response = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") + todos.raise_for_status() + + return {"todos": todos.json()} + + +@app.get("/healthcheck", cors=False) # optionally removes CORS for a given route +@tracer.capture_method +def am_i_alive(): + return {"am_i_alive": "yes"} + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/setting_cors_output.json b/examples/event_handler_rest/src/setting_cors_output.json new file mode 100644 index 00000000000..ca86e892d38 --- /dev/null +++ b/examples/event_handler_rest/src/setting_cors_output.json @@ -0,0 +1,10 @@ +{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "https://www.example.com", + "Access-Control-Allow-Headers": "Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token,X-Api-Key" + }, + "body": "{\"todos\":[{\"userId\":1,\"id\":1,\"title\":\"delectus aut autem\",\"completed\":false},{\"userId\":1,\"id\":2,\"title\":\"quis ut nam facilis et officia qui\",\"completed\":false},{\"userId\":1,\"id\":3,\"title\":\"fugiat veniam minus\",\"completed\":false},{\"userId\":1,\"id\":4,\"title\":\"et porro tempora\",\"completed\":true},{\"userId\":1,\"id\":5,\"title\":\"laboriosam mollitia et enim quasi adipisci quia provident illum\",\"completed\":false},{\"userId\":1,\"id\":6,\"title\":\"qui ullam ratione quibusdam voluptatem quia omnis\",\"completed\":false},{\"userId\":1,\"id\":7,\"title\":\"illo expedita consequatur quia in\",\"completed\":false},{\"userId\":1,\"id\":8,\"title\":\"quo adipisci enim quam ut ab\",\"completed\":true},{\"userId\":1,\"id\":9,\"title\":\"molestiae perspiciatis ipsa\",\"completed\":false},{\"userId\":1,\"id\":10,\"title\":\"illo est ratione doloremque quia maiores aut\",\"completed\":true}]}", + "isBase64Encoded": false +} diff --git a/examples/event_handler_rest/src/split_route.py b/examples/event_handler_rest/src/split_route.py new file mode 100644 index 00000000000..6c0933ea08e --- /dev/null +++ b/examples/event_handler_rest/src/split_route.py @@ -0,0 +1,18 @@ +import split_route_module + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver() +app.include_router(split_route_module.router) + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/split_route_module.py b/examples/event_handler_rest/src/split_route_module.py new file mode 100644 index 00000000000..eeb696ede56 --- /dev/null +++ b/examples/event_handler_rest/src/split_route_module.py @@ -0,0 +1,33 @@ +import requests +from requests import Response + +from aws_lambda_powertools import Tracer +from aws_lambda_powertools.event_handler.api_gateway import Router + +tracer = Tracer() +router = Router() + +endpoint = "https://jsonplaceholder.typicode.com/todos" + + +@router.get("/todos") +@tracer.capture_method +def get_todos(): + api_key: str = router.current_event.get_header_value(name="X-Api-Key", case_sensitive=True, default_value="") + + todos: Response = requests.get(endpoint, headers={"X-Api-Key": api_key}) + todos.raise_for_status() + + # for brevity, we'll limit to the first 10 only + return {"todos": todos.json()[:10]} + + +@router.get("/todos/") +@tracer.capture_method +def get_todo_by_id(todo_id: str): # value come as str + api_key: str = router.current_event.get_header_value(name="X-Api-Key", case_sensitive=True, default_value="") + + todos: Response = requests.get(f"{endpoint}/{todo_id}", headers={"X-Api-Key": api_key}) + todos.raise_for_status() + + return {"todos": todos.json()} diff --git a/examples/event_handler_rest/src/split_route_prefix.py b/examples/event_handler_rest/src/split_route_prefix.py new file mode 100644 index 00000000000..01129c80148 --- /dev/null +++ b/examples/event_handler_rest/src/split_route_prefix.py @@ -0,0 +1,19 @@ +import split_route_module + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver() +# prefix '/todos' to any route in `split_route_module.router` +app.include_router(split_route_module.router, prefix="/todos") + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/split_route_prefix_module.py b/examples/event_handler_rest/src/split_route_prefix_module.py new file mode 100644 index 00000000000..b4035282776 --- /dev/null +++ b/examples/event_handler_rest/src/split_route_prefix_module.py @@ -0,0 +1,36 @@ +import requests +from requests import Response + +from aws_lambda_powertools import Tracer +from aws_lambda_powertools.event_handler.api_gateway import Router + +tracer = Tracer() +router = Router() + +endpoint = "https://jsonplaceholder.typicode.com/todos" + + +@router.get("/") +@tracer.capture_method +def get_todos(): + api_key: str = router.current_event.get_header_value(name="X-Api-Key", case_sensitive=True, default_value="") + + todos: Response = requests.get(endpoint, headers={"X-Api-Key": api_key}) + todos.raise_for_status() + + # for brevity, we'll limit to the first 10 only + return {"todos": todos.json()[:10]} + + +@router.get("/") +@tracer.capture_method +def get_todo_by_id(todo_id: str): # value come as str + api_key: str = router.current_event.get_header_value(name="X-Api-Key", case_sensitive=True, default_value="") + + todos: Response = requests.get(f"{endpoint}/{todo_id}", headers={"X-Api-Key": api_key}) + todos.raise_for_status() + + return {"todos": todos.json()} + + +# many more routes