From 33147f917cfa1184d639a2840869f7836b6fa3b4 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 17 Oct 2021 10:08:41 -0700 Subject: [PATCH 01/15] docs(api-gw-router): Add docs for the routers --- docs/core/event_handler/api_gateway.md | 32 ++++++++++++++++++++++++++ tests/app/app.py | 0 tests/app/routers/health.py | 0 3 files changed, 32 insertions(+) create mode 100644 tests/app/app.py create mode 100644 tests/app/routers/health.py diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index aeaa75e0d2a..42006cb742c 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -852,6 +852,38 @@ You can instruct API Gateway handler to use a custom serializer to best suit you "variations": {"light", "dark"}, } ``` +### Splitting routes across multiple files + +```text +. +├── Makefile +├── Pipfile +├── Pipfile.lock +├── events +│ └── health_status_event.json +├── src +│ ├── __init__.py +│ ├── requirements.txt # +│ └── app +│ ├── __init__.py # this file makes "app" a "Python package" +│ ├── main.py # Main lambda handler +│ └── routers +│ ├── __init__.py +│ ├── items.py # "items" submodule, e.g. from .routers import items +│ ├── health.py # "health" submodule, e.g. from .routers import health +│ └── users.py # "users" submodule, e.g. from .routers import users +├── template.yaml +└── tests + ├── __init__.py + ├── conftest.py + ├── unit + │ ├── __init__.py + │ └── test_health.py # unit tests for the health router + └── functional + ├── __init__.py + └── test_app_main.py # functional tests for the main lambda handler +``` + ## Testing your code diff --git a/tests/app/app.py b/tests/app/app.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/app/routers/health.py b/tests/app/routers/health.py new file mode 100644 index 00000000000..e69de29bb2d From e7ca987c038af27a8c029972ab6f66f943df7955 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 17 Oct 2021 10:11:12 -0700 Subject: [PATCH 02/15] chore: test test files --- tests/app/__init__.py | 0 tests/app/app.py | 18 ++++++++++++++++++ tests/app/routers/__init__.py | 0 tests/app/routers/health.py | 10 ++++++++++ 4 files changed, 28 insertions(+) create mode 100644 tests/app/__init__.py create mode 100644 tests/app/routers/__init__.py diff --git a/tests/app/__init__.py b/tests/app/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/app/app.py b/tests/app/app.py index e69de29bb2d..4dfac74a69f 100644 --- a/tests/app/app.py +++ b/tests/app/app.py @@ -0,0 +1,18 @@ +from typing import Dict + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import ApiGatewayResolver +from aws_lambda_powertools.event_handler.api_gateway import ProxyEventType +from aws_lambda_powertools.utilities.typing import LambdaContext + +from .routers import health + +tracer = Tracer() +logger = Logger() +app = ApiGatewayResolver(proxy_type=ProxyEventType.ALBEvent) +app.include_router(health.router, prefix="/health") + + +@tracer.capture_lambda_handler +def handler(event: Dict, context: LambdaContext): + app.resolve(event, context) diff --git a/tests/app/routers/__init__.py b/tests/app/routers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/app/routers/health.py b/tests/app/routers/health.py index e69de29bb2d..8eb62c63650 100644 --- a/tests/app/routers/health.py +++ b/tests/app/routers/health.py @@ -0,0 +1,10 @@ +from typing import Dict + +from aws_lambda_powertools.event_handler.api_gateway import Router + +router = Router() + + +@router.get("/status") +def health() -> Dict: + return {"status": "OK"} From dac648ccac6b6b17be15f216af89c7eefcfd96b3 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 17 Oct 2021 19:55:56 +0000 Subject: [PATCH 03/15] docs: Add code examples --- docs/core/event_handler/api_gateway.md | 87 ++++++++++++++++++-------- tests/app/app.py | 6 +- tests/app/routers/items.py | 3 + tests/app/routers/users.py | 10 +++ 4 files changed, 78 insertions(+), 28 deletions(-) create mode 100644 tests/app/routers/items.py create mode 100644 tests/app/routers/users.py diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 42006cb742c..d8926ea50fe 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -852,38 +852,71 @@ You can instruct API Gateway handler to use a custom serializer to best suit you "variations": {"light", "dark"}, } ``` + ### Splitting routes across multiple files -```text -. -├── Makefile -├── Pipfile -├── Pipfile.lock -├── events -│ └── health_status_event.json -├── src -│ ├── __init__.py -│ ├── requirements.txt # -│ └── app -│ ├── __init__.py # this file makes "app" a "Python package" -│ ├── main.py # Main lambda handler -│ └── routers -│ ├── __init__.py -│ ├── items.py # "items" submodule, e.g. from .routers import items -│ ├── health.py # "health" submodule, e.g. from .routers import health -│ └── users.py # "users" submodule, e.g. from .routers import users -├── template.yaml -└── tests - ├── __init__.py - ├── conftest.py - ├── unit +When building a larger application, sometimes to helps to split out your routes into multiple file. Also +there might be cases where you have some shared routes for multiple lambdas like a `health` status lambda +to be used with Application Load Balancer. + +Below is an example project layout for AWS Lambda Functions using AWS SAM CLI that allows for relative path +imports. + + ```text + . + ├── Pipfile + ├── Pipfile.lock + ├── events + │ └── health_status_event.json + ├── src │ ├── __init__.py - │ └── test_health.py # unit tests for the health router - └── functional + │ ├── requirements.txt # pipenv lock -r > src/requirements.txt + │ └── app + │ ├── __init__.py # this file makes "app" a "Python package" + │ ├── main.py # Main lambda handler (app.py, index.py, handler.py) + │ └── routers # routers module + │ ├── __init__.py + │ ├── items.py # "items" submodule, e.g. from .routers import items + │ ├── health.py # "health" submodule, e.g. from .routers import health + │ └── users.py # "users" submodule, e.g. from .routers import users + ├── template.yaml # SAM template.yml + └── tests ├── __init__.py - └── test_app_main.py # functional tests for the main lambda handler -``` + ├── conftest.py + ├── unit + │ ├── __init__.py + │ └── test_health.py # unit tests for the health router + └── functional + ├── __init__.py + └── test_app_main.py # functional tests for the main lambda handler + ``` + +=== "src/app/main.py" + + ```python + from typing import Dict + + from aws_lambda_powertools import Logger, Tracer + from aws_lambda_powertools.event_handler import ApiGatewayResolver + from aws_lambda_powertools.event_handler.api_gateway import ProxyEventType + from aws_lambda_powertools.logging.correlation_paths import API_GATEWAY_HTTP + from aws_lambda_powertools.utilities.typing import LambdaContext + + from .routers import health, items, users + tracer = Tracer() + logger = Logger() + app = ApiGatewayResolver(proxy_type=ProxyEventType.ALBEvent) + app.include_router(health.router, prefix="/health") + app.include_router(items.router) + app.include_router(users.router) + + + @logger.inject_lambda_context(correlation_id_path=API_GATEWAY_HTTP) + @tracer.capture_lambda_handler + def handler(event: Dict, context: LambdaContext): + app.resolve(event, context) + ``` ## Testing your code diff --git a/tests/app/app.py b/tests/app/app.py index 4dfac74a69f..d1d95682a64 100644 --- a/tests/app/app.py +++ b/tests/app/app.py @@ -3,16 +3,20 @@ from aws_lambda_powertools import Logger, Tracer from aws_lambda_powertools.event_handler import ApiGatewayResolver from aws_lambda_powertools.event_handler.api_gateway import ProxyEventType +from aws_lambda_powertools.logging.correlation_paths import API_GATEWAY_HTTP from aws_lambda_powertools.utilities.typing import LambdaContext -from .routers import health +from .routers import health, items, users tracer = Tracer() logger = Logger() app = ApiGatewayResolver(proxy_type=ProxyEventType.ALBEvent) app.include_router(health.router, prefix="/health") +app.include_router(items.router) +app.include_router(users.router) +@logger.inject_lambda_context(correlation_id_path=API_GATEWAY_HTTP) @tracer.capture_lambda_handler def handler(event: Dict, context: LambdaContext): app.resolve(event, context) diff --git a/tests/app/routers/items.py b/tests/app/routers/items.py new file mode 100644 index 00000000000..52b541d9f1d --- /dev/null +++ b/tests/app/routers/items.py @@ -0,0 +1,3 @@ +from aws_lambda_powertools.event_handler.api_gateway import Router + +router = Router() diff --git a/tests/app/routers/users.py b/tests/app/routers/users.py new file mode 100644 index 00000000000..42c7943de4c --- /dev/null +++ b/tests/app/routers/users.py @@ -0,0 +1,10 @@ +from typing import Dict + +from aws_lambda_powertools.event_handler.api_gateway import Router + +router = Router() + + +@router.get("/users/") +def find_users_by_name(name: str) -> Dict: + return {"users": [{"name": name}]} From 8fddae14cdb8deadb5015ce27779f233b76df9ff Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 17 Oct 2021 21:05:51 +0000 Subject: [PATCH 04/15] docs: more examples --- docs/core/event_handler/api_gateway.md | 106 ++++++++++++++++++++++++- tests/app/__init__.py | 0 tests/app/app.py | 22 ----- tests/app/routers/__init__.py | 0 tests/app/routers/health.py | 10 --- tests/app/routers/items.py | 3 - tests/app/routers/users.py | 10 --- 7 files changed, 105 insertions(+), 46 deletions(-) delete mode 100644 tests/app/__init__.py delete mode 100644 tests/app/app.py delete mode 100644 tests/app/routers/__init__.py delete mode 100644 tests/app/routers/health.py delete mode 100644 tests/app/routers/items.py delete mode 100644 tests/app/routers/users.py diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index d8926ea50fe..7212ea3fc56 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -862,6 +862,8 @@ to be used with Application Load Balancer. Below is an example project layout for AWS Lambda Functions using AWS SAM CLI that allows for relative path imports. +=== "Project layout" + ```text . ├── Pipfile @@ -891,6 +893,77 @@ imports. └── test_app_main.py # functional tests for the main lambda handler ``` +=== "template.yml" + + ```yml + AWSTemplateFormatVersion: '2010-09-09' + Transform: AWS::Serverless-2016-10-31 + Description: > + app + + Globals: + Api: + EndpointConfiguration: REGIONAL + TracingEnabled: true + Cors: + # AllowOrigin: "'https://example.com'" + AllowOrigin: "'*'" # Dev only + AllowHeaders: "'Content-Type,Authorization,X-Amz-Date'" + MaxAge: "'300'" + BinaryMediaTypes: + - '*~1*' + Function: + Timeout: 20 + MemorySize: 512 + Runtime: python3.9 + Tracing: Active + AutoPublishAlias: live + Environment: + Variables: + LOG_LEVEL: INFO + POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 + POWERTOOLS_LOGGER_LOG_EVENT: true + POWERTOOLS_METRICS_NAMESPACE: MyServerlessApplication + POWERTOOLS_SERVICE_NAME: app + + Resources: + AppFunction: + Type: AWS::Serverless::Function + Properties: + Handler: app.main.lambda_handler + CodeUri: src + Description: App function + Events: + HealthPath: + Type: Api + Properties: + Path: /health/status + Method: GET + ItemsPath: + Type: Api + Properties: + Path: /items + Method: GET + UserPath: + Type: Api + Properties: + Path: /users/{name} + Method: GET + Environment: + Variables: + PARAM1: VALUE + Tags: + LambdaPowertools: python + Outputs: + AppApigwURL: + Description: "API Gateway endpoint URL for Prod environment for App Function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/app" + + AppFunction: + Description: "App Lambda Function ARN" + Value: !GetAtt AppFunction.Arn + ``` + === "src/app/main.py" ```python @@ -914,10 +987,41 @@ imports. @logger.inject_lambda_context(correlation_id_path=API_GATEWAY_HTTP) @tracer.capture_lambda_handler - def handler(event: Dict, context: LambdaContext): + def lambda_handler(event: Dict, context: LambdaContext): app.resolve(event, context) ``` +=== "src/app/routers/health.py" + + ```python + from typing import Dict + + from aws_lambda_powertools.event_handler.api_gateway import Router + + router = Router() + + + @router.get("/status") + def health() -> Dict: + return {"status": "OK"} + ``` + +=== "tests/functional/test_app_main.py" + + ```python + import json + + from src.app import main + + + 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 + ``` + ## Testing your code You can test your routes by passing a proxy event request where `path` and `httpMethod`. diff --git a/tests/app/__init__.py b/tests/app/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/app/app.py b/tests/app/app.py deleted file mode 100644 index d1d95682a64..00000000000 --- a/tests/app/app.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Dict - -from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler import ApiGatewayResolver -from aws_lambda_powertools.event_handler.api_gateway import ProxyEventType -from aws_lambda_powertools.logging.correlation_paths import API_GATEWAY_HTTP -from aws_lambda_powertools.utilities.typing import LambdaContext - -from .routers import health, items, users - -tracer = Tracer() -logger = Logger() -app = ApiGatewayResolver(proxy_type=ProxyEventType.ALBEvent) -app.include_router(health.router, prefix="/health") -app.include_router(items.router) -app.include_router(users.router) - - -@logger.inject_lambda_context(correlation_id_path=API_GATEWAY_HTTP) -@tracer.capture_lambda_handler -def handler(event: Dict, context: LambdaContext): - app.resolve(event, context) diff --git a/tests/app/routers/__init__.py b/tests/app/routers/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/app/routers/health.py b/tests/app/routers/health.py deleted file mode 100644 index 8eb62c63650..00000000000 --- a/tests/app/routers/health.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Dict - -from aws_lambda_powertools.event_handler.api_gateway import Router - -router = Router() - - -@router.get("/status") -def health() -> Dict: - return {"status": "OK"} diff --git a/tests/app/routers/items.py b/tests/app/routers/items.py deleted file mode 100644 index 52b541d9f1d..00000000000 --- a/tests/app/routers/items.py +++ /dev/null @@ -1,3 +0,0 @@ -from aws_lambda_powertools.event_handler.api_gateway import Router - -router = Router() diff --git a/tests/app/routers/users.py b/tests/app/routers/users.py deleted file mode 100644 index 42c7943de4c..00000000000 --- a/tests/app/routers/users.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Dict - -from aws_lambda_powertools.event_handler.api_gateway import Router - -router = Router() - - -@router.get("/users/") -def find_users_by_name(name: str) -> Dict: - return {"users": [{"name": name}]} From ebe8fdeab0027e13aca70a56f2528b2db8e4594c Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 17 Oct 2021 21:14:47 +0000 Subject: [PATCH 05/15] docs: highlight the new Router feature --- docs/core/event_handler/api_gateway.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 7212ea3fc56..fdc229dfad9 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -12,6 +12,7 @@ Event handler for Amazon API Gateway REST/HTTP APIs and Application Loader Balan * 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 accross multiple files ## Getting started From ebadf791a3eb080e7be43cff86d1de54362ac337 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 17 Oct 2021 18:27:22 -0700 Subject: [PATCH 06/15] chore: add some line highlights --- docs/core/event_handler/api_gateway.md | 55 +++++++++----------------- 1 file changed, 19 insertions(+), 36 deletions(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index fdc229dfad9..ee7d1120160 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -76,12 +76,11 @@ This is the sample infrastructure for API Gateway we are using for the examples Outputs: HelloWorldApigwURL: - Description: "API Gateway endpoint URL for Prod environment for Hello World Function" - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello" - - HelloWorldFunction: - Description: "Hello World Lambda Function ARN" - Value: !GetAtt HelloWorldFunction.Arn + Description: "API Gateway endpoint URL for Prod environment for Hello World Function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn ``` ### API Gateway decorator @@ -861,7 +860,7 @@ there might be cases where you have some shared routes for multiple lambdas like to be used with Application Load Balancer. Below is an example project layout for AWS Lambda Functions using AWS SAM CLI that allows for relative path -imports. +imports (ie: `from .routers import health` ). === "Project layout" @@ -869,8 +868,6 @@ imports. . ├── Pipfile ├── Pipfile.lock - ├── events - │ └── health_status_event.json ├── src │ ├── __init__.py │ ├── requirements.txt # pipenv lock -r > src/requirements.txt @@ -878,54 +875,41 @@ imports. │ ├── __init__.py # this file makes "app" a "Python package" │ ├── main.py # Main lambda handler (app.py, index.py, handler.py) │ └── routers # routers module - │ ├── __init__.py + │ ├── __init__.py # this file makes "routers" a "Python package" │ ├── items.py # "items" submodule, e.g. from .routers import items │ ├── health.py # "health" submodule, e.g. from .routers import health │ └── users.py # "users" submodule, e.g. from .routers import users ├── template.yaml # SAM template.yml └── tests ├── __init__.py - ├── conftest.py ├── unit │ ├── __init__.py │ └── test_health.py # unit tests for the health router └── functional ├── __init__.py + ├── conftest.py # pytest fixtures for the functional tests └── test_app_main.py # functional tests for the main lambda handler ``` === "template.yml" - ```yml + ```yaml hl_lines="22 23" AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 - Description: > - app + Description: Example service with multiple routes Globals: - Api: - EndpointConfiguration: REGIONAL - TracingEnabled: true - Cors: - # AllowOrigin: "'https://example.com'" - AllowOrigin: "'*'" # Dev only - AllowHeaders: "'Content-Type,Authorization,X-Amz-Date'" - MaxAge: "'300'" - BinaryMediaTypes: - - '*~1*' Function: Timeout: 20 MemorySize: 512 Runtime: python3.9 Tracing: Active - AutoPublishAlias: live Environment: Variables: LOG_LEVEL: INFO - POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 POWERTOOLS_LOGGER_LOG_EVENT: true POWERTOOLS_METRICS_NAMESPACE: MyServerlessApplication - POWERTOOLS_SERVICE_NAME: app + POWERTOOLS_SERVICE_NAME: ServiceName Resources: AppFunction: @@ -933,7 +917,7 @@ imports. Properties: Handler: app.main.lambda_handler CodeUri: src - Description: App function + Description: App function description Events: HealthPath: Type: Api @@ -957,17 +941,16 @@ imports. LambdaPowertools: python Outputs: AppApigwURL: - Description: "API Gateway endpoint URL for Prod environment for App Function" - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/app" - + Description: "API Gateway endpoint URL for Prod environment for App Function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/app" AppFunction: - Description: "App Lambda Function ARN" - Value: !GetAtt AppFunction.Arn + Description: "App Lambda Function ARN" + Value: !GetAtt AppFunction.Arn ``` === "src/app/main.py" - ```python + ```python hl_lines="9 14-16" from typing import Dict from aws_lambda_powertools import Logger, Tracer @@ -994,7 +977,7 @@ imports. === "src/app/routers/health.py" - ```python + ```python hl_lines="3 5 8" from typing import Dict from aws_lambda_powertools.event_handler.api_gateway import Router @@ -1009,7 +992,7 @@ imports. === "tests/functional/test_app_main.py" - ```python + ```python hl_lines="3" import json from src.app import main From d1f57653d21e1efb57d178d293eff209c3c61779 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 17 Oct 2021 18:36:23 -0700 Subject: [PATCH 07/15] chore: add some tips --- docs/core/event_handler/api_gateway.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index ee7d1120160..3bf5ce60d83 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -860,7 +860,10 @@ there might be cases where you have some shared routes for multiple lambdas like to be used with Application Load Balancer. Below is an example project layout for AWS Lambda Functions using AWS SAM CLI that allows for relative path -imports (ie: `from .routers import health` ). +imports (ie: `from .routers import health`). + +!!! tip "See in `src/app/main.py`, when including a route we can add a prefix to those routes ie: `prefix="/health"`." +!!! tip "See in `src/app/routers/health.py`, when adding a child logger we use `Logger(child=True)`." === "Project layout" @@ -977,16 +980,19 @@ imports (ie: `from .routers import health` ). === "src/app/routers/health.py" - ```python hl_lines="3 5 8" + ```python hl_lines="4 6-7 10 12" 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() @router.get("/status") def health() -> Dict: + logger.debug("Health check called") return {"status": "OK"} ``` From 944dc0c5e8c273afb04895089620f622bb48bb50 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Mon, 18 Oct 2021 14:25:21 -0700 Subject: [PATCH 08/15] chore: remove items --- docs/core/event_handler/api_gateway.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 3bf5ce60d83..a31cc6deee5 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -879,7 +879,6 @@ imports (ie: `from .routers import health`). │ ├── main.py # Main lambda handler (app.py, index.py, handler.py) │ └── routers # routers module │ ├── __init__.py # this file makes "routers" a "Python package" - │ ├── items.py # "items" submodule, e.g. from .routers import items │ ├── health.py # "health" submodule, e.g. from .routers import health │ └── users.py # "users" submodule, e.g. from .routers import users ├── template.yaml # SAM template.yml @@ -927,11 +926,6 @@ imports (ie: `from .routers import health`). Properties: Path: /health/status Method: GET - ItemsPath: - Type: Api - Properties: - Path: /items - Method: GET UserPath: Type: Api Properties: @@ -962,13 +956,12 @@ imports (ie: `from .routers import health`). from aws_lambda_powertools.logging.correlation_paths import API_GATEWAY_HTTP from aws_lambda_powertools.utilities.typing import LambdaContext - from .routers import health, items, users + from .routers import health, users tracer = Tracer() logger = Logger() app = ApiGatewayResolver(proxy_type=ProxyEventType.ALBEvent) app.include_router(health.router, prefix="/health") - app.include_router(items.router) app.include_router(users.router) From 4b4dd5974f5b430acfb7502ea50d1533309ee677 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Thu, 21 Oct 2021 07:14:20 -0700 Subject: [PATCH 09/15] Update docs/core/event_handler/api_gateway.md Co-authored-by: Dani Comnea --- docs/core/event_handler/api_gateway.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index a31cc6deee5..47c8f91d6d3 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -855,7 +855,7 @@ You can instruct API Gateway handler to use a custom serializer to best suit you ### Splitting routes across multiple files -When building a larger application, sometimes to helps to split out your routes into multiple file. Also +When building a larger application, sometimes it helps to split out your routes into multiple file. Also there might be cases where you have some shared routes for multiple lambdas like a `health` status lambda to be used with Application Load Balancer. From dd9c3d6388d537e3c0dba71490605aa9d59c2711 Mon Sep 17 00:00:00 2001 From: Steve Cook Date: Sat, 23 Oct 2021 15:58:11 +1000 Subject: [PATCH 10/15] Removed unused import, added typing imports, fixed typo in example. (#774) --- docs/utilities/parser.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/utilities/parser.md b/docs/utilities/parser.md index 9f1bed3c0cb..7c9af95896f 100644 --- a/docs/utilities/parser.md +++ b/docs/utilities/parser.md @@ -57,8 +57,9 @@ Use the decorator for fail fast scenarios where you want your Lambda function to === "event_parser_decorator.py" ```python hl_lines="18" - from aws_lambda_powertools.utilities.parser import event_parser, BaseModel, ValidationError + from aws_lambda_powertools.utilities.parser import event_parser, BaseModel from aws_lambda_powertools.utilities.typing import LambdaContext + from typing import List, Optional import json @@ -80,7 +81,7 @@ Use the decorator for fail fast scenarios where you want your Lambda function to print(event.description) print(event.items) - order_items = [items for item in event.items] + order_items = [item for item in event.items] ... payload = { @@ -107,6 +108,7 @@ Use this standalone function when you want more control over the data validation ```python hl_lines="21 30" from aws_lambda_powertools.utilities.parser import parse, BaseModel, ValidationError + from typing import List, Optional class OrderItem(BaseModel): id: int From 48aceb014681e6a3a83ce57f790bba7e36787cbc Mon Sep 17 00:00:00 2001 From: Arthur Freund Date: Sat, 23 Oct 2021 07:59:15 +0200 Subject: [PATCH 11/15] Fix middleware sample (#772) --- docs/utilities/middleware_factory.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/utilities/middleware_factory.md b/docs/utilities/middleware_factory.md index 366ae7eda66..253bf6157c3 100644 --- a/docs/utilities/middleware_factory.md +++ b/docs/utilities/middleware_factory.md @@ -47,9 +47,8 @@ You can also have your own keyword arguments after the mandatory arguments. # Obfuscate email before calling Lambda handler if fields: for field in fields: - field = event.get(field, "") if field in event: - event[field] = obfuscate(field) + event[field] = obfuscate(event[field]) return handler(event, context) From 3632d188400661bbeedea1027c9ff0186f015a9b Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 29 Oct 2021 18:28:42 +0200 Subject: [PATCH 12/15] docs: break down into smaller sections --- docs/core/event_handler/api_gateway.md | 128 ++++++++++++++++++++++--- 1 file changed, 117 insertions(+), 11 deletions(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index a31cc6deee5..5bef6a58ab6 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -853,27 +853,129 @@ You can instruct API Gateway handler to use a custom serializer to best suit you } ``` -### Splitting routes across multiple files +### Split routes with Router -When building a larger application, sometimes to helps to split out your routes into multiple file. Also -there might be cases where you have some shared routes for multiple lambdas like a `health` status lambda -to be used with Application Load Balancer. +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. -Below is an example project layout for AWS Lambda Functions using AWS SAM CLI that allows for relative path -imports (ie: `from .routers import health`). +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. -!!! tip "See in `src/app/main.py`, when including a route we can add a prefix to those routes ie: `prefix="/health"`." -!!! tip "See in `src/app/routers/health.py`, when adding a child logger we use `Logger(child=True)`." +=== "users.py" + + We import **Router** instead of **ApiGatewayResolver**; syntax wise is exactly the same. + + ```python hl_lines="4 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: + # get query string ?limit=10 + 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(), 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 + ``` + +=== "app.py" + + We use `include_router` method and include all user routers registered in the `router` global object. + + ```python hl_lines="6 8-9" + from typing import Dict + + from aws_lambda_powertools.event_handler import ApiGatewayResolver + from aws_lambda_powertools.utilities.typing import LambdaContext + + import users + + app = ApiGatewayResolver() + app.include_router(users.router) + + + def lambda_handler(event: Dict, context: LambdaContext): + app.resolve(event, context) + ``` + +#### Route prefix + +As routes are now split in their own files, you can optionally instruct `Router` to inject a prefix for all routes during registration. + +In the previous example, `users.py` routes had a `/users` prefix. We could remove `/users` from all route definitions, and then set `include_router(users.router, prefix="/users")` in the `app.py`. + +=== "app.py" + + ```python hl_lines="9" + from typing import Dict + + from aws_lambda_powertools.event_handler import ApiGatewayResolver + from aws_lambda_powertools.utilities.typing import LambdaContext + + import users + + app = ApiGatewayResolver() + app.include_router(users.router, prefix="/users") # prefix '/users' to any route in `users.router` + + def lambda_handler(event: Dict, context: LambdaContext): + app.resolve(event, context) + ``` + +=== "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_here", "user2": "details_here", "user3": "details_here"} + + @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 + ``` + +#### Sample larger layout + +Below is an example project layout where we have Users routes similar to the previous example, and health check route - We use ALB to demonstrate the UX remains the same. + +Note that this layout optimizes for code sharing and for those familiar with Python modules. This means multiple functions will share the same `CodeUri` and package, though they are only built once. + +!!! tip "External dependencies can be [built as a Lambda Layer](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/building-layers.html){target="_blank"} and set as `dev` dependencies for the project, though outside of scope for this documentation." === "Project layout" - ```text + ```python hl_lines="10-13" . - ├── Pipfile + ├── Pipfile # project dev dependencies ├── Pipfile.lock ├── src │ ├── __init__.py - │ ├── requirements.txt # pipenv lock -r > src/requirements.txt + │ ├── requirements.txt # dummy for `sam build`, as external deps are Lambda Layers │ └── app │ ├── __init__.py # this file makes "app" a "Python package" │ ├── main.py # Main lambda handler (app.py, index.py, handler.py) @@ -1005,6 +1107,10 @@ imports (ie: `from .routers import health`). assert ret["body"] == expected ``` +#### Trade-offs + +!!! todo "TODO" + ## Testing your code You can test your routes by passing a proxy event request where `path` and `httpMethod`. From 241c58746d0f069f9c6252ffef5bf3fa0777c135 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 12 Nov 2021 16:19:33 +0100 Subject: [PATCH 13/15] docs: fix relative imports, spacing Signed-off-by: heitorlessa --- docs/core/event_handler/api_gateway.md | 33 ++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 5bef6a58ab6..ca5af72a6b5 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -872,16 +872,16 @@ Let's assume you have `app.py` as your Lambda function entrypoint and routes in logger = Logger(child=True) router = Router() - USERS = {"user1": "details_here", "user2": "details_here", "user3": "details_here"} + @router.get("/users") def get_users() -> Dict: - # get query string ?limit=10 + # /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(), pagination_limit)) + ret = dict(itertools.islice(USERS.items(), int(pagination_limit))) return {"items": [ret]} @router.get("/users/") @@ -909,14 +909,14 @@ Let's assume you have `app.py` as your Lambda function entrypoint and routes in def lambda_handler(event: Dict, context: LambdaContext): - app.resolve(event, context) + return app.resolve(event, context) ``` #### Route prefix -As routes are now split in their own files, you can optionally instruct `Router` to inject a prefix for all routes during registration. +In the previous example, `users.py` routes had a `/users` prefix. This might grow over time and become repetitive. -In the previous example, `users.py` routes had a `/users` prefix. We could remove `/users` from all route definitions, and then set `include_router(users.router, prefix="/users")` in the `app.py`. +When necessary, you can set a prefix when including a router object. This means you could remove `/users` prefix in `users.py` altogether. === "app.py" @@ -931,8 +931,9 @@ In the previous example, `users.py` routes had a `/users` prefix. We could remov app = ApiGatewayResolver() app.include_router(users.router, prefix="/users") # prefix '/users' to any route in `users.router` + def lambda_handler(event: Dict, context: LambdaContext): - app.resolve(event, context) + return app.resolve(event, context) ``` === "users.py" @@ -945,8 +946,8 @@ In the previous example, `users.py` routes had a `/users` prefix. We could remov logger = Logger(child=True) router = Router() + USERS = {"user1": "details", "user2": "details", "user3": "details"} - USERS = {"user1": "details_here", "user2": "details_here", "user3": "details_here"} @router.get("/") # /users, when we set the prefix in app.py def get_users() -> Dict: @@ -961,7 +962,9 @@ In the previous example, `users.py` routes had a `/users` prefix. We could remov #### Sample larger layout -Below is an example project layout where we have Users routes similar to the previous example, and health check route - We use ALB to demonstrate the UX remains the same. +!!! info "We use ALB to demonstrate that the UX remains the same" + +Below is an example project layout where we have Users routes similar to the previous example, and health check route. Note that this layout optimizes for code sharing and for those familiar with Python modules. This means multiple functions will share the same `CodeUri` and package, though they are only built once. @@ -1026,7 +1029,7 @@ Note that this layout optimizes for code sharing and for those familiar with Pyt HealthPath: Type: Api Properties: - Path: /health/status + Path: /status Method: GET UserPath: Type: Api @@ -1055,22 +1058,22 @@ Note that this layout optimizes for code sharing and for those familiar with Pyt from aws_lambda_powertools import Logger, Tracer from aws_lambda_powertools.event_handler import ApiGatewayResolver from aws_lambda_powertools.event_handler.api_gateway import ProxyEventType - from aws_lambda_powertools.logging.correlation_paths import API_GATEWAY_HTTP + from aws_lambda_powertools.logging.correlation_paths import ALB from aws_lambda_powertools.utilities.typing import LambdaContext - from .routers import health, users + from routers import health, users tracer = Tracer() logger = Logger() app = ApiGatewayResolver(proxy_type=ProxyEventType.ALBEvent) - app.include_router(health.router, prefix="/health") + app.include_router(health.router) app.include_router(users.router) - @logger.inject_lambda_context(correlation_id_path=API_GATEWAY_HTTP) + @logger.inject_lambda_context(correlation_id_path=ALB) @tracer.capture_lambda_handler def lambda_handler(event: Dict, context: LambdaContext): - app.resolve(event, context) + return app.resolve(event, context) ``` === "src/app/routers/health.py" From bf2f6feaac65ff559f607d55fb9ba4f5c52fe61e Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 12 Nov 2021 16:20:27 +0100 Subject: [PATCH 14/15] docs: temporarily remove larger layout sample Signed-off-by: heitorlessa --- docs/core/event_handler/api_gateway.md | 149 ------------------------- 1 file changed, 149 deletions(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index ca5af72a6b5..5897659efca 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -960,155 +960,6 @@ When necessary, you can set a prefix when including a router object. This means # many other related /users routing ``` -#### Sample larger layout - -!!! info "We use ALB to demonstrate that the UX remains the same" - -Below is an example project layout where we have Users routes similar to the previous example, and health check route. - -Note that this layout optimizes for code sharing and for those familiar with Python modules. This means multiple functions will share the same `CodeUri` and package, though they are only built once. - -!!! tip "External dependencies can be [built as a Lambda Layer](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/building-layers.html){target="_blank"} and set as `dev` dependencies for the project, though outside of scope for this documentation." - -=== "Project layout" - - ```python hl_lines="10-13" - . - ├── Pipfile # project dev dependencies - ├── Pipfile.lock - ├── src - │ ├── __init__.py - │ ├── requirements.txt # dummy for `sam build`, as external deps are Lambda Layers - │ └── app - │ ├── __init__.py # this file makes "app" a "Python package" - │ ├── main.py # Main lambda handler (app.py, index.py, handler.py) - │ └── routers # routers module - │ ├── __init__.py # this file makes "routers" a "Python package" - │ ├── health.py # "health" submodule, e.g. from .routers import health - │ └── users.py # "users" submodule, e.g. from .routers import users - ├── template.yaml # SAM template.yml - └── tests - ├── __init__.py - ├── unit - │ ├── __init__.py - │ └── test_health.py # unit tests for the health router - └── functional - ├── __init__.py - ├── conftest.py # pytest fixtures for the functional tests - └── test_app_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: 20 - MemorySize: 512 - Runtime: python3.9 - Tracing: Active - Environment: - Variables: - LOG_LEVEL: INFO - POWERTOOLS_LOGGER_LOG_EVENT: true - POWERTOOLS_METRICS_NAMESPACE: MyServerlessApplication - POWERTOOLS_SERVICE_NAME: ServiceName - - Resources: - AppFunction: - Type: AWS::Serverless::Function - Properties: - Handler: app.main.lambda_handler - CodeUri: src - Description: App function description - Events: - HealthPath: - Type: Api - Properties: - Path: /status - Method: GET - UserPath: - Type: Api - Properties: - Path: /users/{name} - Method: GET - Environment: - Variables: - PARAM1: VALUE - Tags: - LambdaPowertools: python - Outputs: - AppApigwURL: - Description: "API Gateway endpoint URL for Prod environment for App Function" - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/app" - AppFunction: - Description: "App Lambda Function ARN" - Value: !GetAtt AppFunction.Arn - ``` - -=== "src/app/main.py" - - ```python hl_lines="9 14-16" - from typing import Dict - - from aws_lambda_powertools import Logger, Tracer - from aws_lambda_powertools.event_handler import ApiGatewayResolver - from aws_lambda_powertools.event_handler.api_gateway import ProxyEventType - from aws_lambda_powertools.logging.correlation_paths import ALB - from aws_lambda_powertools.utilities.typing import LambdaContext - - from routers import health, users - - tracer = Tracer() - logger = Logger() - app = ApiGatewayResolver(proxy_type=ProxyEventType.ALBEvent) - app.include_router(health.router) - app.include_router(users.router) - - - @logger.inject_lambda_context(correlation_id_path=ALB) - @tracer.capture_lambda_handler - def lambda_handler(event: Dict, context: LambdaContext): - return app.resolve(event, context) - ``` - -=== "src/app/routers/health.py" - - ```python hl_lines="4 6-7 10 12" - 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() - - - @router.get("/status") - def health() -> Dict: - logger.debug("Health check called") - return {"status": "OK"} - ``` - -=== "tests/functional/test_app_main.py" - - ```python hl_lines="3" - import json - - from src.app import main - - - 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 - ``` #### Trade-offs From a4424727e96f7bd6b8aa7c0bf4e26617e09e673f Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 12 Nov 2021 18:44:36 +0100 Subject: [PATCH 15/15] docs: add trade-offs sub-section Signed-off-by: heitorlessa --- docs/core/event_handler/api_gateway.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 5897659efca..70fdc92f3ae 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -963,7 +963,19 @@ When necessary, you can set a prefix when including a router object. This means #### Trade-offs -!!! todo "TODO" +!!! tip "TL;DR. Balance your latency requirements, cognitive overload, least privilege, and operational overhead to decide between one, few, or many single purpose functions." + +Route splitting feature helps accommodate customers familiar with popular frameworks and practices found in the Python community. + +It can help better organize your code and reason + +This can also quickly lead to discussions whether it facilitates a monolithic vs single-purpose function. To this end, these are common trade-offs you'll encounter as you grow your Serverless service, specifically synchronous functions. + +**Least privilege**. Start with a monolithic function, then split them as their data access & boundaries become clearer. Treat Lambda functions as separate logical resources to more easily scope permissions. + +**Package size**. Consider Lambda Layers for third-party dependencies and service-level shared code. Treat third-party dependencies as dev dependencies, and Lambda Layers as a mechanism to speed up build and deployments. + +**Cold start**. High load can diminish the benefit of monolithic functions depending on your latency requirements. Always load test to pragmatically balance between your customer experience and development cognitive load. ## Testing your code