From 6c1a4d2c9a23cd1c56d58ae5b12a62b3164be8ac Mon Sep 17 00:00:00 2001 From: Heitor Lessa Date: Mon, 4 Jul 2022 13:44:34 +0200 Subject: [PATCH 01/20] fix(logger): preserve std keys when using custom formatters (#1264) --- aws_lambda_powertools/logging/formatter.py | 5 ++++ aws_lambda_powertools/logging/logger.py | 16 +++++++--- tests/functional/test_logger.py | 34 ++++++++++++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/aws_lambda_powertools/logging/formatter.py b/aws_lambda_powertools/logging/formatter.py index becfc9de85c..1f01015051c 100644 --- a/aws_lambda_powertools/logging/formatter.py +++ b/aws_lambda_powertools/logging/formatter.py @@ -1,3 +1,4 @@ +import inspect import json import logging import os @@ -286,3 +287,7 @@ def _strip_none_records(records: Dict[str, Any]) -> Dict[str, Any]: JsonFormatter = LambdaPowertoolsFormatter # alias to previous formatter + + +# Fetch current and future parameters from PowertoolsFormatter that should be reserved +RESERVED_FORMATTER_CUSTOM_KEYS: List[str] = inspect.getfullargspec(LambdaPowertoolsFormatter).args[1:] diff --git a/aws_lambda_powertools/logging/logger.py b/aws_lambda_powertools/logging/logger.py index 157d53adf7e..f70224cabae 100644 --- a/aws_lambda_powertools/logging/logger.py +++ b/aws_lambda_powertools/logging/logger.py @@ -12,7 +12,7 @@ from ..shared.functions import resolve_env_var_choice, resolve_truthy_env_var_choice from .exceptions import InvalidLoggerSamplingRateError from .filters import SuppressFilter -from .formatter import BasePowertoolsFormatter, LambdaPowertoolsFormatter +from .formatter import RESERVED_FORMATTER_CUSTOM_KEYS, BasePowertoolsFormatter, LambdaPowertoolsFormatter from .lambda_context import build_lambda_context_model logger = logging.getLogger(__name__) @@ -82,7 +82,7 @@ class Logger(logging.Logger): # lgtm [py/missing-call-to-init] datefmt: str, optional String directives (strftime) to format log timestamp using `time`, by default it uses RFC 3339. - use_datetime_directive: str, optional + use_datetime_directive: bool, optional Interpret `datefmt` as a format string for `datetime.datetime.strftime`, rather than `time.strftime`. @@ -368,7 +368,7 @@ def registered_handler(self) -> logging.Handler: return handlers[0] @property - def registered_formatter(self) -> PowertoolsFormatter: + def registered_formatter(self) -> BasePowertoolsFormatter: """Convenience property to access logger formatter""" return self.registered_handler.formatter # type: ignore @@ -395,7 +395,15 @@ def structure_logs(self, append: bool = False, **keys): is_logger_preconfigured = getattr(self._logger, "init", False) if not is_logger_preconfigured: formatter = self.logger_formatter or LambdaPowertoolsFormatter(**log_keys) # type: ignore - return self.registered_handler.setFormatter(formatter) + self.registered_handler.setFormatter(formatter) + + # when using a custom Lambda Powertools Formatter + # standard and custom keys that are not Powertools Formatter parameters should be appended + # and custom keys that might happen to be Powertools Formatter parameters should be discarded + # this prevents adding them as custom keys, for example, `json_default=` + # see https://github.com/awslabs/aws-lambda-powertools-python/issues/1263 + custom_keys = {k: v for k, v in log_keys.items() if k not in RESERVED_FORMATTER_CUSTOM_KEYS} + return self.registered_formatter.append_keys(**custom_keys) # Mode 2 (legacy) if append: diff --git a/tests/functional/test_logger.py b/tests/functional/test_logger.py index c76faa7cde5..c8b3dc61755 100644 --- a/tests/functional/test_logger.py +++ b/tests/functional/test_logger.py @@ -625,6 +625,40 @@ def handler(event, context): assert all(k in second_log for k in lambda_context_keys) +def test_logger_custom_formatter_has_standard_and_custom_keys(stdout, service_name, lambda_context): + class CustomFormatter(LambdaPowertoolsFormatter): + ... + + # GIVEN a Logger is initialized with a custom formatter + logger = Logger(service=service_name, stream=stdout, logger_formatter=CustomFormatter(), my_key="value") + + # WHEN a lambda function is decorated with logger + @logger.inject_lambda_context + def handler(event, context): + logger.info("Hello") + + handler({}, lambda_context) + + standard_keys = ( + "level", + "location", + "message", + "timestamp", + "service", + "cold_start", + "function_name", + "function_memory_size", + "function_arn", + "function_request_id", + ) + + log = capture_logging_output(stdout) + + # THEN all standard keys should be available + assert all(k in log for k in standard_keys) + assert "my_key" in log + + def test_logger_custom_handler(lambda_context, service_name, tmp_path): # GIVEN a Logger is initialized with a FileHandler log_file = tmp_path / "log.json" From c6de9c1e56889cbff1b68850e04a0f9eebbbbdd3 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 4 Jul 2022 14:46:37 +0200 Subject: [PATCH 02/20] fix(ci): checkout project before validating related issue workflow --- .github/workflows/on_merged_pr.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/on_merged_pr.yml b/.github/workflows/on_merged_pr.yml index 97029740cdb..70f43daab3b 100644 --- a/.github/workflows/on_merged_pr.yml +++ b/.github/workflows/on_merged_pr.yml @@ -18,6 +18,7 @@ jobs: issues: write # required for new scoped token pull-requests: write # required for new scoped token steps: + - uses: actions/checkout@v3 - name: "Label PR related issue for release" uses: actions/github-script@v6 with: From 0f1f1cdb06de9355dcd4af3bac355fcdedac8df7 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 4 Jul 2022 17:05:32 +0200 Subject: [PATCH 03/20] chore(layers): bump to 22 for 1.26.3 --- docs/index.md | 58 +++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/index.md b/docs/index.md index cc3d437334e..5396ccb3f2e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,7 +15,7 @@ A suite of utilities for AWS Lambda functions to ease adopting best practices su Powertools is available in the following formats: -* **Lambda Layer**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:21**](#){: .copyMe}:clipboard: +* **Lambda Layer**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:22**](#){: .copyMe}:clipboard: * **PyPi**: **`pip install aws-lambda-powertools`** ???+ hint "Support this project by using Lambda Layers :heart:" @@ -33,23 +33,23 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https: | Region | Layer ARN | | ---------------- | -------------------------------------------------------------------------------------------------------- | - | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPython:21](#){: .copyMe}:clipboard: | - | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPython:21](#){: .copyMe}:clipboard: | - | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPython:21](#){: .copyMe}:clipboard: | - | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPython:21](#){: .copyMe}:clipboard: | - | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPython:21](#){: .copyMe}:clipboard: | - | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPython:21](#){: .copyMe}:clipboard: | - | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPython:21](#){: .copyMe}:clipboard: | - | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPython:21](#){: .copyMe}:clipboard: | - | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPython:21](#){: .copyMe}:clipboard: | - | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPython:21](#){: .copyMe}:clipboard: | - | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:21](#){: .copyMe}:clipboard: | - | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPython:21](#){: .copyMe}:clipboard: | - | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPython:21](#){: .copyMe}:clipboard: | - | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPython:21](#){: .copyMe}:clipboard: | - | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPython:21](#){: .copyMe}:clipboard: | - | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPython:21](#){: .copyMe}:clipboard: | - | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPython:21](#){: .copyMe}:clipboard: | + | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPython:22](#){: .copyMe}:clipboard: | + | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPython:22](#){: .copyMe}:clipboard: | + | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPython:22](#){: .copyMe}:clipboard: | + | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPython:22](#){: .copyMe}:clipboard: | + | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPython:22](#){: .copyMe}:clipboard: | + | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPython:22](#){: .copyMe}:clipboard: | + | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPython:22](#){: .copyMe}:clipboard: | + | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPython:22](#){: .copyMe}:clipboard: | + | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPython:22](#){: .copyMe}:clipboard: | + | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPython:22](#){: .copyMe}:clipboard: | + | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:22](#){: .copyMe}:clipboard: | + | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPython:22](#){: .copyMe}:clipboard: | + | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPython:22](#){: .copyMe}:clipboard: | + | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPython:22](#){: .copyMe}:clipboard: | + | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPython:22](#){: .copyMe}:clipboard: | + | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPython:22](#){: .copyMe}:clipboard: | + | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPython:22](#){: .copyMe}:clipboard: | ??? question "Can't find our Lambda Layer for your preferred AWS region?" You can use [Serverless Application Repository (SAR)](#sar) method, our [CDK Layer Construct](https://github.com/aws-samples/cdk-lambda-powertools-python-layer){target="_blank"}, or PyPi like you normally would for any other library. @@ -63,7 +63,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https: Type: AWS::Serverless::Function Properties: Layers: - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:21 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:22 ``` === "Serverless framework" @@ -73,7 +73,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https: hello: handler: lambda_function.lambda_handler layers: - - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPython:21 + - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPython:22 ``` === "CDK" @@ -89,7 +89,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https: powertools_layer = aws_lambda.LayerVersion.from_layer_version_arn( self, id="lambda-powertools", - layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPython:21" + layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPython:22" ) aws_lambda.Function(self, 'sample-app-lambda', @@ -138,7 +138,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https: role = aws_iam_role.iam_for_lambda.arn handler = "index.test" runtime = "python3.9" - layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:21"] + layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:22"] source_code_hash = filebase64sha256("lambda_function_payload.zip") } @@ -157,7 +157,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https: ? Do you want to configure advanced settings? Yes ... ? Do you want to enable Lambda layers for this function? Yes - ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:21 + ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:22 ❯ amplify push -y @@ -168,7 +168,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https: - Name: ? Which setting do you want to update? Lambda layers configuration ? Do you want to enable Lambda layers for this function? Yes - ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:21 + ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:22 ? Do you want to edit the local lambda function now? No ``` @@ -176,7 +176,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https: Change {region} to your AWS region, e.g. `eu-west-1` ```bash title="AWS CLI" - aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:21 --region {region} + aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:22 --region {region} ``` The pre-signed URL to download this Lambda Layer will be within `Location` key. @@ -214,7 +214,7 @@ If using SAM, you can include this SAR App as part of your shared Layers stack, Properties: Location: ApplicationId: arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer - SemanticVersion: 1.25.10 # change to latest semantic version available in SAR + SemanticVersion: 1.26.3 # change to latest semantic version available in SAR MyLambdaFunction: Type: AWS::Serverless::Function @@ -242,7 +242,7 @@ If using SAM, you can include this SAR App as part of your shared Layers stack, Location: ApplicationId: arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer # Find latest from github.com/awslabs/aws-lambda-powertools-python/releases - SemanticVersion: 1.25.10 + SemanticVersion: 1.26.3 ``` === "CDK" @@ -252,7 +252,7 @@ If using SAM, you can include this SAR App as part of your shared Layers stack, POWERTOOLS_BASE_NAME = 'AWSLambdaPowertools' # Find latest from github.com/awslabs/aws-lambda-powertools-python/releases - POWERTOOLS_VER = '1.23.0' + POWERTOOLS_VER = '1.26.3' POWERTOOLS_ARN = 'arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer' class SampleApp(core.Construct): @@ -316,7 +316,7 @@ If using SAM, you can include this SAR App as part of your shared Layers stack, variable "aws_powertools_version" { type = string - default = "1.20.2" + default = "1.26.3" description = "The AWS Powertools release version" } From 89d33d376e54d7a414af23a533290a960da6f3d7 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 4 Jul 2022 17:10:45 +0200 Subject: [PATCH 04/20] chore(ci): limits concurrency for docs workflow --- .github/workflows/python_docs.yml | 4 ++ .github/workflows/rebuild_latest_docs.yml | 88 ++++++++++++----------- 2 files changed, 49 insertions(+), 43 deletions(-) diff --git a/.github/workflows/python_docs.yml b/.github/workflows/python_docs.yml index 3a6e15e5431..d7ae6c2cc52 100644 --- a/.github/workflows/python_docs.yml +++ b/.github/workflows/python_docs.yml @@ -12,6 +12,10 @@ on: jobs: docs: + # Force Github action to run only a single job at a time (based on the group name) + # This is to prevent "race-condition" in publishing a new version of doc to `gh-pages` + concurrency: + group: on-docs-build runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/rebuild_latest_docs.yml b/.github/workflows/rebuild_latest_docs.yml index b185556f2ff..92cb8a3635d 100644 --- a/.github/workflows/rebuild_latest_docs.yml +++ b/.github/workflows/rebuild_latest_docs.yml @@ -7,56 +7,58 @@ name: Rebuild latest docs # 2. Use the latest version released under Releases e.g. v1.22.0 # 3. Set `Build and publish docs only` field to `true` - on: workflow_dispatch: inputs: latest_published_version: - description: 'Latest PyPi published version to rebuild latest docs for, e.g. v1.22.0' - default: 'v1.22.0' + description: "Latest PyPi published version to rebuild latest docs for, e.g. v1.22.0" + default: "v1.22.0" required: true - jobs: release: + # Force Github action to run only a single job at a time (based on the group name) + # This is to prevent "race-condition" in publishing a new version of doc to `gh-pages` + concurrency: + group: on-docs-rebuild runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.8" - - name: Set release notes tag - run: | - RELEASE_TAG_VERSION=${{ github.event.inputs.latest_published_version }} - echo "RELEASE_TAG_VERSION=${RELEASE_TAG_VERSION:1}" >> $GITHUB_ENV - - name: Ensure new version is also set in pyproject and CHANGELOG - run: | - grep --regexp "${RELEASE_TAG_VERSION}" CHANGELOG.md - grep --regexp "version \= \"${RELEASE_TAG_VERSION}\"" pyproject.toml - - name: Install dependencies - run: make dev - - name: Setup doc deploy - run: | - git config --global user.name Docs deploy - git config --global user.email aws-devax-open-source@amazon.com - - name: Build docs website and API reference - run: | - make release-docs VERSION=${RELEASE_TAG_VERSION} ALIAS="latest" - poetry run mike set-default --push latest - - name: Release API docs to release version - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./api - keep_files: true - destination_dir: ${{ env.RELEASE_TAG_VERSION }}/api - - name: Release API docs to latest - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./api - keep_files: true - destination_dir: latest/api + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.8" + - name: Set release notes tag + run: | + RELEASE_TAG_VERSION=${{ github.event.inputs.latest_published_version }} + echo "RELEASE_TAG_VERSION=${RELEASE_TAG_VERSION:1}" >> $GITHUB_ENV + - name: Ensure new version is also set in pyproject and CHANGELOG + run: | + grep --regexp "${RELEASE_TAG_VERSION}" CHANGELOG.md + grep --regexp "version \= \"${RELEASE_TAG_VERSION}\"" pyproject.toml + - name: Install dependencies + run: make dev + - name: Setup doc deploy + run: | + git config --global user.name Docs deploy + git config --global user.email aws-devax-open-source@amazon.com + - name: Build docs website and API reference + run: | + make release-docs VERSION=${RELEASE_TAG_VERSION} ALIAS="latest" + poetry run mike set-default --push latest + - name: Release API docs to release version + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./api + keep_files: true + destination_dir: ${{ env.RELEASE_TAG_VERSION }}/api + - name: Release API docs to latest + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./api + keep_files: true + destination_dir: latest/api From c0bb85f341a4e98a5a19e1f27f824166974b66a8 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 5 Jul 2022 12:16:35 +0200 Subject: [PATCH 05/20] fix(ci): regex to catch combination of related issues workflow --- .github/scripts/label_related_issue.js | 31 +++++++++++++------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/.github/scripts/label_related_issue.js b/.github/scripts/label_related_issue.js index 1953412ae41..8f851056cb3 100644 --- a/.github/scripts/label_related_issue.js +++ b/.github/scripts/label_related_issue.js @@ -4,27 +4,26 @@ module.exports = async ({github, context}) => { const releaseLabel = process.env.RELEASE_LABEL; const maintainersTeam = process.env.MAINTAINERS_TEAM - const RELATED_ISSUE_REGEX = /Issue number:.+(\d)/ + const RELATED_ISSUE_REGEX = /Issue number:[^\d\r\n]+(?\d+)/; - const matcher = new RegExp(RELATED_ISSUE_REGEX) - const isMatch = matcher.exec(prBody) - if (isMatch != null) { - let relatedIssueNumber = isMatch[1] - console.info(`Auto-labeling related issue ${relatedIssueNumber} for release`) - - return await github.rest.issues.addLabels({ - issue_number: relatedIssueNumber, - owner: context.repo.owner, - repo: context.repo.repo, - labels: [releaseLabel] - }) - } else { - let msg = `${maintainersTeam} No related issues found. Please ensure '${releaseLabel}' label is applied before releasing.`; + const isMatch = RELATED_ISSUE_REGEX.exec(body); + if (!isMatch) { + core.setFailed(`Unable to find related issue for PR number ${prNumber}.\n\n Body details: ${prBody}`); return await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, - body: msg, + body: `${maintainersTeam} No related issues found. Please ensure '${releaseLabel}' label is applied before releasing.`, issue_number: prNumber, }); } + + const { groups: {relatedIssueNumber} } = isMatch + + core.info(`Auto-labeling related issue ${relatedIssueNumber} for release`) + return await github.rest.issues.addLabels({ + issue_number: relatedIssueNumber, + owner: context.repo.owner, + repo: context.repo.repo, + labels: [releaseLabel] + }) } From 1f570a62aaffbb7c4c4434404c7086ebede2ab37 Mon Sep 17 00:00:00 2001 From: Alexander Melnyk Date: Thu, 7 Jul 2022 12:54:00 +0200 Subject: [PATCH 06/20] chore(layers): add release pipeline in GitHub Actions (#1278) * chore: add layer project * reduce to 1 region for dev * chore: shorter name for the workflow * fix ignore markdown lint for now * fix: more f strings * ignore mdlint * add reusable workflow for both beta and prod * Update layer/layer/canary/app.py Co-authored-by: Heitor Lessa * Update layer/layer/canary/app.py Co-authored-by: Heitor Lessa * readme review * rephrase canary stack ssm parameter usage * add default RELEASE_TAG_VERSION assignment based on the input (release or manual trigger) * add reference to layer docs * wording * move version trackign arn to canary stack * remove outdated npm caching, add release tag resolution for manual workflow trigger * review: fix layer name and remove dependencies from reusable workflow * remove debug statement, add default working dir * pin versions and hashes for requirements with pip-compile * rename reusable workflow * pass artefact name to the reusable workflow to prevent potential future conflicts Co-authored-by: Heitor Lessa --- .github/workflows/publish_layer.yml | 80 +++++++++++++++ .../workflows/reusable_deploy_layer_stack.yml | 87 ++++++++++++++++ layer/.gitignore | 10 ++ layer/README.md | 27 +++++ layer/app.py | 23 +++++ layer/cdk.json | 35 +++++++ layer/layer/__init__.py | 0 layer/layer/canary/app.py | 99 +++++++++++++++++++ layer/layer/canary_stack.py | 75 ++++++++++++++ layer/layer/layer_stack.py | 19 ++++ layer/requirements-dev.txt | 2 + layer/requirements.txt | 76 ++++++++++++++ 12 files changed, 533 insertions(+) create mode 100644 .github/workflows/publish_layer.yml create mode 100644 .github/workflows/reusable_deploy_layer_stack.yml create mode 100644 layer/.gitignore create mode 100644 layer/README.md create mode 100644 layer/app.py create mode 100644 layer/cdk.json create mode 100644 layer/layer/__init__.py create mode 100644 layer/layer/canary/app.py create mode 100644 layer/layer/canary_stack.py create mode 100644 layer/layer/layer_stack.py create mode 100644 layer/requirements-dev.txt create mode 100644 layer/requirements.txt diff --git a/.github/workflows/publish_layer.yml b/.github/workflows/publish_layer.yml new file mode 100644 index 00000000000..d4001e0bdce --- /dev/null +++ b/.github/workflows/publish_layer.yml @@ -0,0 +1,80 @@ +name: Deploy layer to all regions + +permissions: + id-token: write + contents: read + +on: + workflow_dispatch: + inputs: + latest_published_version: + description: "Latest PyPi published version to rebuild latest docs for, e.g. v1.22.0" + default: "v1.22.0" + required: true + workflow_run: + workflows: [ "Publish to PyPi" ] + types: + - completed + + +jobs: + build-layer: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./layer + steps: + - name: checkout + uses: actions/checkout@v2 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: '16.12' + cache: 'npm' + - name: Setup python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + cache: 'pip' + - name: Set release notes tag + run: | + RELEASE_INPUT=${{ inputs.latest_published_version }} + GITHUB_EVENT_RELEASE_TAG=${{ github.event.release.tag_name }} + RELEASE_TAG_VERSION=${GITHUB_EVENT_RELEASE_TAG:-$RELEASE_INPUT} + echo "RELEASE_TAG_VERSION=${RELEASE_TAG_VERSION:1}" >> $GITHUB_ENV + - name: install cdk and deps + run: | + npm install -g aws-cdk@2.29.0 + cdk --version + - name: install deps + run: | + pip install -r requirements.txt + - name: CDK build + run: cdk synth --context version=$RELEASE_TAG_VERSION -o cdk.out + - name: zip output + run: zip -r cdk.out.zip cdk.out + - name: Archive CDK artifacts + uses: actions/upload-artifact@v3 + with: + name: cdk-layer-artefact + path: cdk.out.zip + + deploy-beta: + needs: + - build-layer + uses: ./.github/workflows/reusable_deploy_layer_stack.yml + with: + stage: "BETA" + artifact-name: "cdk-layer-artefact" + secrets: + target-account: ${{ secrets.LAYERS_BETA_ACCOUNT }} + + deploy-prod: + needs: + - deploy-beta + uses: ./.github/workflows/reusable_deploy_layer_stack.yml + with: + stage: "PROD" + artifact-name: "cdk-layer-artefact" + secrets: + target-account: ${{ secrets.LAYERS_PROD_ACCOUNT }} diff --git a/.github/workflows/reusable_deploy_layer_stack.yml b/.github/workflows/reusable_deploy_layer_stack.yml new file mode 100644 index 00000000000..f31449e7ba6 --- /dev/null +++ b/.github/workflows/reusable_deploy_layer_stack.yml @@ -0,0 +1,87 @@ +name: Deploy cdk stack + +permissions: + id-token: write + contents: read + +on: + workflow_call: + inputs: + stage: + required: true + type: string + artefact-name: + required: true + type: string + secrets: + target-account: + required: true + +jobs: + deploy-cdk-stack: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./layer + strategy: + fail-fast: false + matrix: + region: [ + "af-south-1", + # "eu-central-1", + # "us-east-1", + # "us-east-2", + # "us-west-1", + # "us-west-2", + # "ap-east-1", + # "ap-south-1", + # "ap-northeast-1", + # "ap-northeast-2", + # "ap-southeast-1", + # "ap-southeast-2", + # "ca-central-1", + # "eu-west-1", + # "eu-west-2", + # "eu-west-3", + # "eu-south-1", + # "eu-north-1", + # "sa-east-1", + # "ap-southeast-3", + # "ap-northeast-3", + # "me-south-1" + ] + steps: + - name: checkout + uses: actions/checkout@v2 + - name: aws credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-region: ${{ matrix.region }} + role-to-assume: arn:aws:iam::${{ secrets.target-account }}:role/${{ secrets.AWS_GITHUB_OIDC_ROLE }} + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: '16.12' + cache: 'npm' + - name: Setup python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + cache: 'pip' + - name: install cdk and deps + run: | + npm install -g aws-cdk@2.29.0 + cdk --version + - name: install deps + run: | + pip install -r requirements.txt + - name: Download artifact + uses: actions/download-artifact@v3 + with: + name: ${{ inputs.artefact-name }} + - name: unzip artefact + run: unzip cdk.out.zip + - name: CDK Deploy Layer + run: cdk deploy --app cdk.out --context region=${{ matrix.region }} 'LayerStack ' --require-approval never --verbose + - name: CDK Deploy Canary + run: cdk deploy --app cdk.out --context region=${{ matrix.region}} --parameters DeployStage="${{ input.stage }}" 'CanaryStack' --require-approval never --verbose diff --git a/layer/.gitignore b/layer/.gitignore new file mode 100644 index 00000000000..37833f8beb2 --- /dev/null +++ b/layer/.gitignore @@ -0,0 +1,10 @@ +*.swp +package-lock.json +__pycache__ +.pytest_cache +.venv +*.egg-info + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/layer/README.md b/layer/README.md new file mode 100644 index 00000000000..99da0083ffc --- /dev/null +++ b/layer/README.md @@ -0,0 +1,27 @@ + +# CDK Powertools layer + +This is a CDK project to build and deploy AWS Lambda Powertools [Lambda layer](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-concepts.html#gettingstarted-concepts-layer) to multiple commercial regions. + +## Build the layer + +To build the layer construct you need to provide the Powertools version that is [available in PyPi](https://pypi.org/project/aws-lambda-powertools/). +You can pass it as a context variable when running `synth` or `deploy`, + +```shell +cdk synth --context version=1.25.1 +``` + +## Canary stack + +We use a canary stack to verify that the deployment is successful and we can use the layer by adding it to a newly created Lambda function. +The canary is deployed after the layer construct. Because the layer ARN is created during the deploy we need to pass this information async via SSM parameter. +To achieve that we use SSM parameter store to pass the layer ARN to the canary. +The layer stack writes the layer ARN after the deployment as SSM parameter and the canary stacks reads this information and adds the layer to the function. + +## Version tracking + +AWS Lambda versions Lambda layers by incrementing a number at the end of the ARN. +This makes it challenging to know which Powertools version a layer contains. +For better tracking of the ARNs and the corresponding version we need to keep track which powertools version was deployed to which layer. +To achieve that we created two components. First, we created a version tracking app which receives events via EventBridge. Second, after a successful canary deployment we send the layer ARN, Powertools version, and the region to this EventBridge. diff --git a/layer/app.py b/layer/app.py new file mode 100644 index 00000000000..78e99b17654 --- /dev/null +++ b/layer/app.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +import aws_cdk as cdk + +from layer.canary_stack import CanaryStack +from layer.layer_stack import LayerStack + +app = cdk.App() + +POWERTOOLS_VERSION: str = app.node.try_get_context("version") +SSM_PARAM_LAYER_ARN: str = "/layers/powertools-layer-arn" + +if not POWERTOOLS_VERSION: + raise ValueError( + "Please set the version for Powertools by passing the '--context=version:' parameter to the CDK " + "synth step." + ) + +LayerStack(app, "LayerStack", powertools_version=POWERTOOLS_VERSION, ssm_paramter_layer_arn=SSM_PARAM_LAYER_ARN) + +CanaryStack(app, "CanaryStack", powertools_version=POWERTOOLS_VERSION, ssm_paramter_layer_arn=SSM_PARAM_LAYER_ARN) + +app.synth() diff --git a/layer/cdk.json b/layer/cdk.json new file mode 100644 index 00000000000..c120c5f4765 --- /dev/null +++ b/layer/cdk.json @@ -0,0 +1,35 @@ +{ + "app": "python3 app.py", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "source.bat", + "**/__init__.py", + "python/__pycache__", + "tests" + ] + }, + "context": { + "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, + "@aws-cdk/core:stackRelativeExports": true, + "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, + "@aws-cdk/aws-lambda:recognizeVersionProps": true, + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ] + } +} diff --git a/layer/layer/__init__.py b/layer/layer/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/layer/layer/canary/app.py b/layer/layer/canary/app.py new file mode 100644 index 00000000000..31db94dd92b --- /dev/null +++ b/layer/layer/canary/app.py @@ -0,0 +1,99 @@ +import datetime +import json +import os +from importlib.metadata import version + +import boto3 + +from aws_lambda_powertools import Logger, Metrics, Tracer + +logger = Logger(service="version-track") +tracer = Tracer() +metrics = Metrics(namespace="powertools-layer-canary", service="PowertoolsLayerCanary") + +layer_arn = os.getenv("POWERTOOLS_LAYER_ARN") +powertools_version = os.getenv("POWERTOOLS_VERSION") +stage = os.getenv("LAYER_PIPELINE_STAGE") +event_bus_arn = os.getenv("VERSION_TRACKING_EVENT_BUS_ARN") + + +def handler(event): + logger.info("Running checks") + check_envs() + verify_powertools_version() + send_notification() + return True + + +@logger.inject_lambda_context(log_event=True) +def on_event(event, context): + request_type = event["RequestType"] + # we handle only create events, because we recreate the canary on each run + if request_type == "Create": + return on_create(event) + + return "Nothing to be processed" + + +def on_create(event): + props = event["ResourceProperties"] + logger.info("create new resource with properties %s" % props) + handler(event) + + +def check_envs(): + logger.info('Checking required envs ["POWERTOOLS_LAYER_ARN", "AWS_REGION", "STAGE"]') + if not layer_arn: + raise ValueError("POWERTOOLS_LAYER_ARN is not set. Aborting...") + if not powertools_version: + raise ValueError("POWERTOOLS_VERSION is not set. Aborting...") + if not stage: + raise ValueError("LAYER_PIPELINE_STAGE is not set. Aborting...") + if not event_bus_arn: + raise ValueError("VERSION_TRACKING_EVENT_BUS_ARN is not set. Aborting...") + logger.info("All envs configured, continue...") + + +def verify_powertools_version() -> None: + """ + fetches the version that we import from the powertools layer and compares + it with expected version set in environment variable, which we pass during deployment. + :raise ValueError if the expected version is not the same as the version we get from the layer + """ + logger.info("Checking Powertools version in library...") + current_version = version("aws_lambda_powertools") + if powertools_version != current_version: + raise ValueError( + f'Expected powertoosl version is "{powertools_version}", but layer contains version "{current_version}"' + ) + logger.info(f"Current Powertools version is: {current_version}") + + +def send_notification(): + """ + sends an event to version tracking event bridge + """ + event = { + "Time": datetime.datetime.now(), + "Source": "powertools.layer.canary", + "EventBusName": event_bus_arn, + "DetailType": "deployment", + "Detail": json.dumps( + { + "id": "powertools-python", + "stage": stage, + "region": os.environ["AWS_REGION"], + "version": powertools_version, + "layerArn": layer_arn, + } + ), + } + + logger.info(f"sending notification event: {event}") + + client = boto3.client("events", region_name="eu-central-1") + resp = client.put_events(Entries=[event]) + logger.info(resp) + if resp["FailedEntryCount"] != 0: + logger.error(resp) + raise ValueError("Failed to send deployment notification to version tracking") diff --git a/layer/layer/canary_stack.py b/layer/layer/canary_stack.py new file mode 100644 index 00000000000..15bc80214d3 --- /dev/null +++ b/layer/layer/canary_stack.py @@ -0,0 +1,75 @@ +import uuid + +from aws_cdk import CfnParameter, CustomResource, Duration, Stack +from aws_cdk.aws_iam import Effect, ManagedPolicy, PolicyStatement, Role, ServicePrincipal +from aws_cdk.aws_lambda import Code, Function, LayerVersion, Runtime +from aws_cdk.aws_logs import RetentionDays +from aws_cdk.aws_ssm import StringParameter +from aws_cdk.custom_resources import Provider +from constructs import Construct + + +class CanaryStack(Stack): + def __init__( + self, + scope: Construct, + construct_id: str, + powertools_version: str, + ssm_paramter_layer_arn: str, + **kwargs, + ) -> None: + super().__init__(scope, construct_id, **kwargs) + + VERSION_TRACKING_EVENT_BUS_ARN: str = ( + "arn:aws:events:eu-central-1:027876851704:event-bus/VersionTrackingEventBus" + ) + + layer_arn = StringParameter.from_string_parameter_attributes( + self, "LayerVersionArnParam", parameter_name=ssm_paramter_layer_arn + ).string_value + + layer = LayerVersion.from_layer_version_arn(self, "PowertoolsLayer", layer_version_arn=layer_arn) + deploy_stage = CfnParameter(self, "DeployStage", description="Deployment stage for canary").value_as_string + + execution_role = Role(self, "LambdaExecutionRole", assumed_by=ServicePrincipal("lambda.amazonaws.com")) + + execution_role.add_managed_policy( + ManagedPolicy.from_aws_managed_policy_name("service-role/AWSLambdaBasicExecutionRole") + ) + + execution_role.add_to_policy( + PolicyStatement(effect=Effect.ALLOW, actions=["lambda:GetFunction"], resources=["*"]) + ) + + canary_lambda = Function( + self, + "CanaryLambdaFunction", + function_name="CanaryLambdaFunction", + code=Code.from_asset("layer/canary"), + handler="app.on_event", + layers=[layer], + memory_size=512, + timeout=Duration.seconds(10), + runtime=Runtime.PYTHON_3_9, + log_retention=RetentionDays.ONE_MONTH, + role=execution_role, + environment={ + "POWERTOOLS_VERSION": powertools_version, + "POWERTOOLS_LAYER_ARN": layer_arn, + "VERSION_TRACKING_EVENT_BUS_ARN": VERSION_TRACKING_EVENT_BUS_ARN, + "LAYER_PIPELINE_STAGE": deploy_stage, + }, + ) + + canary_lambda.add_to_role_policy( + PolicyStatement( + effect=Effect.ALLOW, actions=["events:PutEvents"], resources=[VERSION_TRACKING_EVENT_BUS_ARN] + ) + ) + + # custom resource provider configuration + provider = Provider( + self, "CanaryCustomResource", on_event_handler=canary_lambda, log_retention=RetentionDays.ONE_MONTH + ) + # force to recreate resource on each deployment with randomized name + CustomResource(self, f"CanaryTrigger-{str(uuid.uuid4())[0:7]}", service_token=provider.service_token) diff --git a/layer/layer/layer_stack.py b/layer/layer/layer_stack.py new file mode 100644 index 00000000000..8b32de9c206 --- /dev/null +++ b/layer/layer/layer_stack.py @@ -0,0 +1,19 @@ +from aws_cdk import Stack +from aws_cdk.aws_ssm import StringParameter +from cdk_lambda_powertools_python_layer import LambdaPowertoolsLayer +from constructs import Construct + + +class LayerStack(Stack): + def __init__( + self, scope: Construct, construct_id: str, powertools_version: str, ssm_paramter_layer_arn: str, **kwargs + ) -> None: + super().__init__(scope, construct_id, **kwargs) + + layer = LambdaPowertoolsLayer( + self, "Layer", layer_version_name="AWSLambdaPowertoolsPython", version=powertools_version + ) + + layer.add_permission("PublicLayerAccess", account_id="*") + + StringParameter(self, "VersionArn", parameter_name=ssm_paramter_layer_arn, string_value=layer.layer_version_arn) diff --git a/layer/requirements-dev.txt b/layer/requirements-dev.txt new file mode 100644 index 00000000000..f3ec7d732b5 --- /dev/null +++ b/layer/requirements-dev.txt @@ -0,0 +1,2 @@ +pytest==6.2.5 +boto3==1.24.22 diff --git a/layer/requirements.txt b/layer/requirements.txt new file mode 100644 index 00000000000..0484892d321 --- /dev/null +++ b/layer/requirements.txt @@ -0,0 +1,76 @@ +# +# This file is autogenerated by pip-compile with python 3.9 +# To update, run: +# +# pip-compile --generate-hashes requirements.txt +# +attrs==21.4.0 \ + --hash=sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4 \ + --hash=sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd + # via + # -r requirements.txt + # cattrs + # jsii +aws-cdk-lib==2.29.0 \ + --hash=sha256:4f852105cafd28a2bbd9bd2c6d24a2e1ab503bba923fd49a1782390b235af999 \ + --hash=sha256:53a78788219d9bf3a998211223225b34a10f066124e2812adcd40fd0a2058572 + # via + # -r requirements.txt + # cdk-lambda-powertools-python-layer +cattrs==22.1.0 \ + --hash=sha256:94b67b64cf92c994f8784c40c082177dc916e0489a73a9a36b24eb18a9db40c6 \ + --hash=sha256:d55c477b4672f93606e992049f15d526dc7867e6c756cd6256d4af92e2b1e364 + # via + # -r requirements.txt + # jsii +cdk-lambda-powertools-python-layer==2.0.48 \ + --hash=sha256:7bdd5a196e74b48d403223722f2838d1d10064d02e960a5565482cc0b7aad18d \ + --hash=sha256:9afeacea31eba14d67360db71af385c654c9e0af9b29a0d4e0922b52f862ae03 + # via -r requirements.txt +constructs==10.1.43 \ + --hash=sha256:69fd6da574c9506f44ca61e112af7d5db08ebb29b4bedc67b6d200b616f4abce \ + --hash=sha256:f37e8c3432f94f403b50bf69476bea55719bcc3fa0d3a0e60bf0975dfe492867 + # via + # -r requirements.txt + # aws-cdk-lib + # cdk-lambda-powertools-python-layer +exceptiongroup==1.0.0rc8 \ + --hash=sha256:6990c24f06b8d33c8065cfe43e5e8a4bfa384e0358be036af9cc60b6321bd11a \ + --hash=sha256:ab0a968e1ef769e55d9a596f4a89f7be9ffedbc9fdefdb77cc68cf5c33ce1035 + # via + # -r requirements.txt + # cattrs +jsii==1.61.0 \ + --hash=sha256:542a72cd1a144d36fa530dc359b5295b82d9e7ecdd76d5c7b4b61195f132a746 \ + --hash=sha256:b2899f24bcc95ce009bc256558c81cde8cff9f830eddbe9b0d581c40558a1ff0 + # via + # -r requirements.txt + # aws-cdk-lib + # cdk-lambda-powertools-python-layer + # constructs +publication==0.0.3 \ + --hash=sha256:0248885351febc11d8a1098d5c8e3ab2dabcf3e8c0c96db1e17ecd12b53afbe6 \ + --hash=sha256:68416a0de76dddcdd2930d1c8ef853a743cc96c82416c4e4d3b5d901c6276dc4 + # via + # -r requirements.txt + # aws-cdk-lib + # cdk-lambda-powertools-python-layer + # constructs +python-dateutil==2.8.2 \ + --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ + --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 + # via + # -r requirements.txt + # jsii +six==1.16.0 \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 + # via + # -r requirements.txt + # python-dateutil +typing-extensions==4.3.0 \ + --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \ + --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6 + # via + # -r requirements.txt + # jsii From 577a410ab3bcde7d2017eedd59bb4f37ccd2687f Mon Sep 17 00:00:00 2001 From: Alexander Melnyk Date: Thu, 7 Jul 2022 13:05:41 +0200 Subject: [PATCH 07/20] fix: typo in input for layer workflow --- .github/workflows/publish_layer.yml | 4 ++-- .github/workflows/reusable_deploy_layer_stack.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish_layer.yml b/.github/workflows/publish_layer.yml index d4001e0bdce..7192bf8b6ce 100644 --- a/.github/workflows/publish_layer.yml +++ b/.github/workflows/publish_layer.yml @@ -65,7 +65,7 @@ jobs: uses: ./.github/workflows/reusable_deploy_layer_stack.yml with: stage: "BETA" - artifact-name: "cdk-layer-artefact" + artefact-name: "cdk-layer-artefact" secrets: target-account: ${{ secrets.LAYERS_BETA_ACCOUNT }} @@ -75,6 +75,6 @@ jobs: uses: ./.github/workflows/reusable_deploy_layer_stack.yml with: stage: "PROD" - artifact-name: "cdk-layer-artefact" + artefact-name: "cdk-layer-artefact" secrets: target-account: ${{ secrets.LAYERS_PROD_ACCOUNT }} diff --git a/.github/workflows/reusable_deploy_layer_stack.yml b/.github/workflows/reusable_deploy_layer_stack.yml index f31449e7ba6..ffeadf43045 100644 --- a/.github/workflows/reusable_deploy_layer_stack.yml +++ b/.github/workflows/reusable_deploy_layer_stack.yml @@ -84,4 +84,4 @@ jobs: - name: CDK Deploy Layer run: cdk deploy --app cdk.out --context region=${{ matrix.region }} 'LayerStack ' --require-approval never --verbose - name: CDK Deploy Canary - run: cdk deploy --app cdk.out --context region=${{ matrix.region}} --parameters DeployStage="${{ input.stage }}" 'CanaryStack' --require-approval never --verbose + run: cdk deploy --app cdk.out --context region=${{ matrix.region}} --parameters DeployStage="${{ inputs.stage }}" 'CanaryStack' --require-approval never --verbose From e0b3b58d9774d5203d6370a2a16968026f751fb2 Mon Sep 17 00:00:00 2001 From: Alexander Melnyk Date: Thu, 7 Jul 2022 13:15:30 +0200 Subject: [PATCH 08/20] fix: no need to cache npm since we only install cdk cli and don't have .lock files --- .github/workflows/publish_layer.yml | 1 - .github/workflows/reusable_deploy_layer_stack.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/publish_layer.yml b/.github/workflows/publish_layer.yml index 7192bf8b6ce..c69e8cb5b9e 100644 --- a/.github/workflows/publish_layer.yml +++ b/.github/workflows/publish_layer.yml @@ -30,7 +30,6 @@ jobs: uses: actions/setup-node@v2 with: node-version: '16.12' - cache: 'npm' - name: Setup python uses: actions/setup-python@v4 with: diff --git a/.github/workflows/reusable_deploy_layer_stack.yml b/.github/workflows/reusable_deploy_layer_stack.yml index ffeadf43045..2923c3efdbb 100644 --- a/.github/workflows/reusable_deploy_layer_stack.yml +++ b/.github/workflows/reusable_deploy_layer_stack.yml @@ -62,7 +62,6 @@ jobs: uses: actions/setup-node@v2 with: node-version: '16.12' - cache: 'npm' - name: Setup python uses: actions/setup-python@v4 with: From 925c14583d0815fec22dc606d350baef4dbd3d56 Mon Sep 17 00:00:00 2001 From: Alexander Melnyk Date: Thu, 7 Jul 2022 13:43:49 +0200 Subject: [PATCH 09/20] fix: add entire ARN role instead of account and role name --- .github/workflows/publish_layer.yml | 4 ++-- .github/workflows/reusable_deploy_layer_stack.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish_layer.yml b/.github/workflows/publish_layer.yml index c69e8cb5b9e..df20241c331 100644 --- a/.github/workflows/publish_layer.yml +++ b/.github/workflows/publish_layer.yml @@ -66,7 +66,7 @@ jobs: stage: "BETA" artefact-name: "cdk-layer-artefact" secrets: - target-account: ${{ secrets.LAYERS_BETA_ACCOUNT }} + target-account-role: arn:aws:iam::${{ secrets.LAYERS_BETA_ACCOUNT }}:role/${{ secrets.AWS_GITHUB_OIDC_ROLE }} deploy-prod: needs: @@ -76,4 +76,4 @@ jobs: stage: "PROD" artefact-name: "cdk-layer-artefact" secrets: - target-account: ${{ secrets.LAYERS_PROD_ACCOUNT }} + target-account-role: arn:aws:iam::${{ secrets.LAYERS_PROD_ACCOUNT }}:role/${{ secrets.AWS_GITHUB_OIDC_ROLE }} diff --git a/.github/workflows/reusable_deploy_layer_stack.yml b/.github/workflows/reusable_deploy_layer_stack.yml index 2923c3efdbb..506cae96809 100644 --- a/.github/workflows/reusable_deploy_layer_stack.yml +++ b/.github/workflows/reusable_deploy_layer_stack.yml @@ -14,7 +14,7 @@ on: required: true type: string secrets: - target-account: + target-account-role: required: true jobs: @@ -57,7 +57,7 @@ jobs: uses: aws-actions/configure-aws-credentials@v1 with: aws-region: ${{ matrix.region }} - role-to-assume: arn:aws:iam::${{ secrets.target-account }}:role/${{ secrets.AWS_GITHUB_OIDC_ROLE }} + role-to-assume: ${{ secrets.target-account-role }} - name: Setup Node.js uses: actions/setup-node@v2 with: From 5bcd0260e8e793e55258cc1a795845656b3acfad Mon Sep 17 00:00:00 2001 From: Alexander Melnyk Date: Thu, 7 Jul 2022 15:47:53 +0200 Subject: [PATCH 10/20] fix: path to artefact --- .github/workflows/publish_layer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish_layer.yml b/.github/workflows/publish_layer.yml index df20241c331..b4814fbb98a 100644 --- a/.github/workflows/publish_layer.yml +++ b/.github/workflows/publish_layer.yml @@ -56,7 +56,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: cdk-layer-artefact - path: cdk.out.zip + path: layer/cdk.out.zip deploy-beta: needs: From 46c1754ac073b7e0b9151d07ce26eec4ba7033dc Mon Sep 17 00:00:00 2001 From: Alexander Melnyk Date: Thu, 7 Jul 2022 16:03:44 +0200 Subject: [PATCH 11/20] fix: unzip the right artifact name --- .github/workflows/reusable_deploy_layer_stack.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable_deploy_layer_stack.yml b/.github/workflows/reusable_deploy_layer_stack.yml index 506cae96809..31f8830931d 100644 --- a/.github/workflows/reusable_deploy_layer_stack.yml +++ b/.github/workflows/reusable_deploy_layer_stack.yml @@ -79,7 +79,7 @@ jobs: with: name: ${{ inputs.artefact-name }} - name: unzip artefact - run: unzip cdk.out.zip + run: unzip ${{ inputs.artefact-name }} - name: CDK Deploy Layer run: cdk deploy --app cdk.out --context region=${{ matrix.region }} 'LayerStack ' --require-approval never --verbose - name: CDK Deploy Canary From 1170e7674d8d09fb7938dae79d332371e85e2329 Mon Sep 17 00:00:00 2001 From: Heitor Lessa Date: Thu, 7 Jul 2022 16:47:43 +0200 Subject: [PATCH 12/20] 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 From a34d34d066e8e5fb086f831d3f382aa20f23f005 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Thu, 7 Jul 2022 16:52:02 +0200 Subject: [PATCH 13/20] chore(ci): fix reference error in related_issue --- .github/scripts/label_related_issue.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/label_related_issue.js b/.github/scripts/label_related_issue.js index 8f851056cb3..fb47a7bc4b0 100644 --- a/.github/scripts/label_related_issue.js +++ b/.github/scripts/label_related_issue.js @@ -6,7 +6,7 @@ module.exports = async ({github, context}) => { const RELATED_ISSUE_REGEX = /Issue number:[^\d\r\n]+(?\d+)/; - const isMatch = RELATED_ISSUE_REGEX.exec(body); + const isMatch = RELATED_ISSUE_REGEX.exec(prBody); if (!isMatch) { core.setFailed(`Unable to find related issue for PR number ${prNumber}.\n\n Body details: ${prBody}`); return await github.rest.issues.createComment({ From ad86b1018af1275d8d74f7299cb3aa214d9a8979 Mon Sep 17 00:00:00 2001 From: Alexander Melnyk Date: Thu, 7 Jul 2022 17:03:43 +0200 Subject: [PATCH 14/20] fix: download artefact into the layer dir --- .github/workflows/reusable_deploy_layer_stack.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/reusable_deploy_layer_stack.yml b/.github/workflows/reusable_deploy_layer_stack.yml index 31f8830931d..6ac646f2bd8 100644 --- a/.github/workflows/reusable_deploy_layer_stack.yml +++ b/.github/workflows/reusable_deploy_layer_stack.yml @@ -78,6 +78,7 @@ jobs: uses: actions/download-artifact@v3 with: name: ${{ inputs.artefact-name }} + path: layer - name: unzip artefact run: unzip ${{ inputs.artefact-name }} - name: CDK Deploy Layer From 1962f192917413d308fa26e8faa81e61d66fd135 Mon Sep 17 00:00:00 2001 From: Alexander Melnyk Date: Thu, 7 Jul 2022 17:46:28 +0200 Subject: [PATCH 15/20] fix: mathc the name of the cdk synth from the build phase --- .github/workflows/reusable_deploy_layer_stack.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable_deploy_layer_stack.yml b/.github/workflows/reusable_deploy_layer_stack.yml index 6ac646f2bd8..69f182c4189 100644 --- a/.github/workflows/reusable_deploy_layer_stack.yml +++ b/.github/workflows/reusable_deploy_layer_stack.yml @@ -80,7 +80,7 @@ jobs: name: ${{ inputs.artefact-name }} path: layer - name: unzip artefact - run: unzip ${{ inputs.artefact-name }} + run: unzip cdk.out.zip - name: CDK Deploy Layer run: cdk deploy --app cdk.out --context region=${{ matrix.region }} 'LayerStack ' --require-approval never --verbose - name: CDK Deploy Canary From 2ded8a6481836da7bdccf7a005fbe31bdf9dbd68 Mon Sep 17 00:00:00 2001 From: Alexander Melnyk Date: Thu, 7 Jul 2022 17:58:45 +0200 Subject: [PATCH 16/20] fix: sight, yes a whitespace character breaks the build --- .github/workflows/reusable_deploy_layer_stack.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable_deploy_layer_stack.yml b/.github/workflows/reusable_deploy_layer_stack.yml index 69f182c4189..f03cb117d6f 100644 --- a/.github/workflows/reusable_deploy_layer_stack.yml +++ b/.github/workflows/reusable_deploy_layer_stack.yml @@ -82,6 +82,6 @@ jobs: - name: unzip artefact run: unzip cdk.out.zip - name: CDK Deploy Layer - run: cdk deploy --app cdk.out --context region=${{ matrix.region }} 'LayerStack ' --require-approval never --verbose + run: cdk deploy --app cdk.out --context region=${{ matrix.region }} 'LayerStack' --require-approval never --verbose - name: CDK Deploy Canary run: cdk deploy --app cdk.out --context region=${{ matrix.region}} --parameters DeployStage="${{ inputs.stage }}" 'CanaryStack' --require-approval never --verbose From e27029a7e5fe54422bf5e305871dbfb5163f72f8 Mon Sep 17 00:00:00 2001 From: Alexander Melnyk Date: Tue, 19 Jul 2022 13:05:39 +0200 Subject: [PATCH 17/20] chore(ci): keep layer version, trigger only after success --- .github/workflows/publish_layer.yml | 18 ++++++++++++++++-- layer/layer/layer_stack.py | 3 ++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish_layer.yml b/.github/workflows/publish_layer.yml index b4814fbb98a..9365968fb2c 100644 --- a/.github/workflows/publish_layer.yml +++ b/.github/workflows/publish_layer.yml @@ -18,8 +18,22 @@ on: jobs: + on-success: + runs-on: ubuntu-latest + if: ${{ (github.event.workflow_run.conclusion == 'success') || (github.event_name == 'workflow_dispatch') }} + steps: + - run: echo 'The triggering workflow passed, continue' + + on-failure: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'failure' }} + steps: + - run: echo 'The triggering workflow failed, aborting' + + build-layer: runs-on: ubuntu-latest + needs: on-success defaults: run: working-directory: ./layer @@ -38,8 +52,8 @@ jobs: - name: Set release notes tag run: | RELEASE_INPUT=${{ inputs.latest_published_version }} - GITHUB_EVENT_RELEASE_TAG=${{ github.event.release.tag_name }} - RELEASE_TAG_VERSION=${GITHUB_EVENT_RELEASE_TAG:-$RELEASE_INPUT} + LATEST_TAG=$(git describe --tag --abbrev=0) + RELEASE_TAG_VERSION=${RELEASE_INPUT:-$LATEST_TAG} echo "RELEASE_TAG_VERSION=${RELEASE_TAG_VERSION:1}" >> $GITHUB_ENV - name: install cdk and deps run: | diff --git a/layer/layer/layer_stack.py b/layer/layer/layer_stack.py index 8b32de9c206..15f3d3fdcb8 100644 --- a/layer/layer/layer_stack.py +++ b/layer/layer/layer_stack.py @@ -1,4 +1,4 @@ -from aws_cdk import Stack +from aws_cdk import RemovalPolicy, Stack from aws_cdk.aws_ssm import StringParameter from cdk_lambda_powertools_python_layer import LambdaPowertoolsLayer from constructs import Construct @@ -15,5 +15,6 @@ def __init__( ) layer.add_permission("PublicLayerAccess", account_id="*") + layer.apply_removal_policy(RemovalPolicy.RETAIN) StringParameter(self, "VersionArn", parameter_name=ssm_paramter_layer_arn, string_value=layer.layer_version_arn) From b7f1cea564c14e9dd8f45b8b7a9b7d9b3dbd07e8 Mon Sep 17 00:00:00 2001 From: Alexander Melnyk Date: Tue, 19 Jul 2022 13:09:58 +0200 Subject: [PATCH 18/20] chore(ci): checkout depth 0 to get tags --- .github/workflows/publish_layer.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish_layer.yml b/.github/workflows/publish_layer.yml index 9365968fb2c..2706e5ae9f9 100644 --- a/.github/workflows/publish_layer.yml +++ b/.github/workflows/publish_layer.yml @@ -40,6 +40,8 @@ jobs: steps: - name: checkout uses: actions/checkout@v2 + with: + fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v2 with: From 50e3825f25f273566af2279494b163de9d9723e4 Mon Sep 17 00:00:00 2001 From: Alexander Melnyk Date: Tue, 19 Jul 2022 14:44:52 +0200 Subject: [PATCH 19/20] Update .github/workflows/publish_layer.yml Co-authored-by: Heitor Lessa --- .github/workflows/publish_layer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish_layer.yml b/.github/workflows/publish_layer.yml index 12172fa3ffb..d8e3f7f22be 100644 --- a/.github/workflows/publish_layer.yml +++ b/.github/workflows/publish_layer.yml @@ -33,7 +33,7 @@ jobs: build-layer: runs-on: ubuntu-latest - needs: on-success + if: ${{ (github.event.workflow_run.conclusion == 'success') || (github.event_name == 'workflow_dispatch') }} defaults: run: working-directory: ./layer From 7a6b34283fdfdce150e71d85af11b7488b227412 Mon Sep 17 00:00:00 2001 From: Alexander Melnyk Date: Tue, 19 Jul 2022 14:46:21 +0200 Subject: [PATCH 20/20] remove dummy jobs --- .github/workflows/publish_layer.yml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.github/workflows/publish_layer.yml b/.github/workflows/publish_layer.yml index d8e3f7f22be..3597f0d8253 100644 --- a/.github/workflows/publish_layer.yml +++ b/.github/workflows/publish_layer.yml @@ -18,19 +18,6 @@ on: jobs: - on-success: - runs-on: ubuntu-latest - if: ${{ (github.event.workflow_run.conclusion == 'success') || (github.event_name == 'workflow_dispatch') }} - steps: - - run: echo 'The triggering workflow passed, continue' - - on-failure: - runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'failure' }} - steps: - - run: echo 'The triggering workflow failed, aborting' - - build-layer: runs-on: ubuntu-latest if: ${{ (github.event.workflow_run.conclusion == 'success') || (github.event_name == 'workflow_dispatch') }}