diff --git a/.github/actions/create-pr/action.yml b/.github/actions/create-pr/action.yml index dcf2df738bd..39ba6f60b1f 100644 --- a/.github/actions/create-pr/action.yml +++ b/.github/actions/create-pr/action.yml @@ -64,7 +64,7 @@ runs: name: Git client setup and refresh tip run: | git config user.name "Powertools for AWS Lambda (Python) bot" - git config user.email "aws-lambda-powertools-feedback@amazon.com" + git config user.email "151832416+aws-powertools-bot@users.noreply.github.com" git config pull.rebase true git config remote.origin.url >&- shell: bash diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 3fee4d6b427..4537173ac39 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -19,4 +19,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: 'Dependency Review' - uses: actions/dependency-review-action@4901385134134e04cec5fbe5ddfe3b2c5bd5d976 # v4.0.0 + uses: actions/dependency-review-action@9129d7d40b8c12c1ed0f60400d00c92d437adcce # v4.1.3 diff --git a/.github/workflows/publish_v2_layer.yml b/.github/workflows/publish_v2_layer.yml index 3dfb4cc1446..ab6a59a09a8 100644 --- a/.github/workflows/publish_v2_layer.yml +++ b/.github/workflows/publish_v2_layer.yml @@ -257,11 +257,12 @@ jobs: integrity_hash: ${{ inputs.source_code_integrity_hash }} artifact_name: ${{ inputs.source_code_artifact_name }} - - name: Download CDK layer artifact + - name: Download CDK layer artifacts uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 with: - name: cdk-layer-stack - path: cdk-layer-stack/ + path: cdk-layer-stack + pattern: cdk-layer-stack-* # merge all Layer artifacts created per region earlier (reusable_deploy_v2_layer_stack.yml; step "Save Layer ARN artifact") + merge-multiple: true - name: Replace layer versions in documentation run: | ls -la cdk-layer-stack/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fac762bdbeb..d9c7b9d8cad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -273,7 +273,7 @@ jobs: name: Git client setup and refresh tip run: | git config user.name "Powertools for AWS Lambda (Python) bot" - git config user.email "aws-lambda-powertools-feedback@amazon.com" + git config user.email "151832416+aws-powertools-bot@users.noreply.github.com" git config remote.origin.url >&- - name: Create Git Tag diff --git a/.github/workflows/reusable_deploy_v2_layer_stack.yml b/.github/workflows/reusable_deploy_v2_layer_stack.yml index dd7c5384970..c022ac06c0d 100644 --- a/.github/workflows/reusable_deploy_v2_layer_stack.yml +++ b/.github/workflows/reusable_deploy_v2_layer_stack.yml @@ -193,16 +193,15 @@ jobs: run: | mkdir cdk-layer-stack jq -r -c '.LayerV2Stack.LatestLayerArn' cdk-outputs.json > cdk-layer-stack/${{ matrix.region }}-layer-version.txt - jq -r -c '.LayerV2Stack.LatestLayerArm64Arn' cdk-outputs.json >> cdk-layer-stack/${{ matrix.region }}-layer-version.txt + jq -r -c '.LayerV2Stack.LatestLayerArm64Arn' cdk-outputs.json > cdk-layer-stack/${{ matrix.region }}-layer-version.txt cat cdk-layer-stack/${{ matrix.region }}-layer-version.txt - name: Save Layer ARN artifact if: ${{ inputs.stage == 'PROD' }} uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: - name: cdk-layer-stack + name: cdk-layer-stack-${{ matrix.region }} path: ./layer/cdk-layer-stack/* # NOTE: upload-artifact does not inherit working-directory setting. if-no-files-found: error retention-days: 1 - overwrite: true - name: CDK Deploy Canary run: npx cdk deploy --app cdk.out --context region=${{ matrix.region }} --parameters DeployStage="${{ inputs.stage }}" --parameters HasARM64Support=${{ matrix.has_arm64_support }} 'CanaryV2Stack' --require-approval never --verbose diff --git a/CHANGELOG.md b/CHANGELOG.md index 45aa708bb97..2fc9e714fbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,37 +4,95 @@ # Unreleased +## Bug Fixes + +* **event-handler:** multi-value query string and validation of scalar parameters ([#3795](https://github.com/aws-powertools/powertools-lambda-python/issues/3795)) +* **event-handler:** swagger schema respects api stage ([#3796](https://github.com/aws-powertools/powertools-lambda-python/issues/3796)) +* **event-handler:** handle aliased parameters e.g., Query(alias="categoryType") ([#3766](https://github.com/aws-powertools/powertools-lambda-python/issues/3766)) + +## Code Refactoring + +* **feature-flags:** add intersection tests; structure refinement ([#3775](https://github.com/aws-powertools/powertools-lambda-python/issues/3775)) + +## Documentation + +* **feature_flags:** fix incorrect line markers and envelope name ([#3792](https://github.com/aws-powertools/powertools-lambda-python/issues/3792)) +* **home:** update layer version to 62 for package version 2.33.1 ([#3778](https://github.com/aws-powertools/powertools-lambda-python/issues/3778)) +* **home:** add note about POWERTOOLS_DEV side effects in CloudWatch Logs ([#3770](https://github.com/aws-powertools/powertools-lambda-python/issues/3770)) +* **homepage:** discord flat badge style; remove former devax email ([#3768](https://github.com/aws-powertools/powertools-lambda-python/issues/3768)) +* **homepage:** remove leftover announcement banner ([#3783](https://github.com/aws-powertools/powertools-lambda-python/issues/3783)) +* **roadmap:** latest roadmap update; use new grid to de-clutter homepage ([#3755](https://github.com/aws-powertools/powertools-lambda-python/issues/3755)) +* **we-made-this:** add reinvent 2023 session ([#3790](https://github.com/aws-powertools/powertools-lambda-python/issues/3790)) + +## Features + +* **feature_flags:** add intersect actions for conditions ([#3692](https://github.com/aws-powertools/powertools-lambda-python/issues/3692)) + +## Maintenance + +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3784](https://github.com/aws-powertools/powertools-lambda-python/issues/3784)) +* **deps:** bump actions/dependency-review-action from 4.0.0 to 4.1.0 ([#3771](https://github.com/aws-powertools/powertools-lambda-python/issues/3771)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#3764](https://github.com/aws-powertools/powertools-lambda-python/issues/3764)) +* **deps:** bump squidfunk/mkdocs-material from `6a72238` to `62d3668` in /docs ([#3756](https://github.com/aws-powertools/powertools-lambda-python/issues/3756)) +* **deps:** bump squidfunk/mkdocs-material from `62d3668` to `43b898a` in /docs ([#3801](https://github.com/aws-powertools/powertools-lambda-python/issues/3801)) +* **deps:** bump actions/dependency-review-action from 4.1.0 to 4.1.2 ([#3800](https://github.com/aws-powertools/powertools-lambda-python/issues/3800)) +* **deps-dev:** bump cfn-lint from 0.85.1 to 0.85.2 ([#3786](https://github.com/aws-powertools/powertools-lambda-python/issues/3786)) +* **deps-dev:** bump pytest-asyncio from 0.21.1 to 0.23.5 ([#3773](https://github.com/aws-powertools/powertools-lambda-python/issues/3773)) +* **deps-dev:** bump aws-cdk from 2.127.0 to 2.128.0 ([#3776](https://github.com/aws-powertools/powertools-lambda-python/issues/3776)) +* **deps-dev:** bump sentry-sdk from 1.40.3 to 1.40.4 ([#3765](https://github.com/aws-powertools/powertools-lambda-python/issues/3765)) +* **deps-dev:** bump the boto-typing group with 2 updates ([#3797](https://github.com/aws-powertools/powertools-lambda-python/issues/3797)) +* **deps-dev:** bump aws-cdk-lib from 2.127.0 to 2.128.0 ([#3777](https://github.com/aws-powertools/powertools-lambda-python/issues/3777)) +* **deps-dev:** bump mkdocs-material from 9.5.9 to 9.5.10 ([#3803](https://github.com/aws-powertools/powertools-lambda-python/issues/3803)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3757](https://github.com/aws-powertools/powertools-lambda-python/issues/3757)) +* **deps-dev:** bump aws-cdk-lib from 2.126.0 to 2.127.0 ([#3758](https://github.com/aws-powertools/powertools-lambda-python/issues/3758)) +* **deps-dev:** bump aws-cdk from 2.126.0 to 2.127.0 ([#3761](https://github.com/aws-powertools/powertools-lambda-python/issues/3761)) +* **deps-dev:** bump mkdocs-material from 9.5.8 to 9.5.9 ([#3759](https://github.com/aws-powertools/powertools-lambda-python/issues/3759)) +* **deps-dev:** bump sentry-sdk from 1.40.2 to 1.40.3 ([#3750](https://github.com/aws-powertools/powertools-lambda-python/issues/3750)) +* **deps-dev:** bump cfn-lint from 0.85.0 to 0.85.1 ([#3749](https://github.com/aws-powertools/powertools-lambda-python/issues/3749)) +* **deps-dev:** bump types-redis from 4.6.0.20240106 to 4.6.0.20240218 ([#3804](https://github.com/aws-powertools/powertools-lambda-python/issues/3804)) +* **deps-dev:** bump sentry-sdk from 1.40.4 to 1.40.5 ([#3805](https://github.com/aws-powertools/powertools-lambda-python/issues/3805)) + + + +## [v2.33.1] - 2024-02-09 +## Bug Fixes + +* **typing:** make Response headers covariant ([#3745](https://github.com/aws-powertools/powertools-lambda-python/issues/3745)) + ## Documentation * Add nathan hanks post community ([#3727](https://github.com/aws-powertools/powertools-lambda-python/issues/3727)) ## Maintenance +* version bump * **ci:** drop support for Python 3.7 ([#3638](https://github.com/aws-powertools/powertools-lambda-python/issues/3638)) * **ci:** enable Redis e2e tests ([#3718](https://github.com/aws-powertools/powertools-lambda-python/issues/3718)) -* **deps:** bump actions/download-artifact from 3.0.2 to 4.1.1 ([#3612](https://github.com/aws-powertools/powertools-lambda-python/issues/3612)) +* **deps:** bump actions/setup-node from 4.0.1 to 4.0.2 ([#3737](https://github.com/aws-powertools/powertools-lambda-python/issues/3737)) +* **deps:** bump squidfunk/mkdocs-material from `e0d6c67` to `6a72238` in /docs ([#3735](https://github.com/aws-powertools/powertools-lambda-python/issues/3735)) * **deps:** bump actions/dependency-review-action from 3.1.5 to 4.0.0 ([#3646](https://github.com/aws-powertools/powertools-lambda-python/issues/3646)) -* **deps:** bump actions/download-artifact from 4.1.1 to 4.1.2 ([#3725](https://github.com/aws-powertools/powertools-lambda-python/issues/3725)) * **deps:** bump release-drafter/release-drafter from 5.25.0 to 6.0.0 ([#3699](https://github.com/aws-powertools/powertools-lambda-python/issues/3699)) -* **deps:** revert aws-cdk-lib as a runtime dep ([#3730](https://github.com/aws-powertools/powertools-lambda-python/issues/3730)) +* **deps:** bump actions/download-artifact from 4.1.1 to 4.1.2 ([#3725](https://github.com/aws-powertools/powertools-lambda-python/issues/3725)) * **deps:** bump squidfunk/mkdocs-material from `a4a2029` to `e0d6c67` in /docs ([#3708](https://github.com/aws-powertools/powertools-lambda-python/issues/3708)) -* **deps:** bump actions/upload-artifact from 3.1.3 to 4.3.1 ([#3714](https://github.com/aws-powertools/powertools-lambda-python/issues/3714)) -* **deps:** bump squidfunk/mkdocs-material from `e0d6c67` to `6a72238` in /docs ([#3735](https://github.com/aws-powertools/powertools-lambda-python/issues/3735)) -* **deps:** bump actions/setup-node from 4.0.1 to 4.0.2 ([#3737](https://github.com/aws-powertools/powertools-lambda-python/issues/3737)) * **deps:** bump codecov/codecov-action from 3.1.6 to 4.0.1 ([#3700](https://github.com/aws-powertools/powertools-lambda-python/issues/3700)) -* **deps-dev:** bump mypy from 1.4.1 to 1.8.0 ([#3710](https://github.com/aws-powertools/powertools-lambda-python/issues/3710)) +* **deps:** bump actions/download-artifact from 3.0.2 to 4.1.1 ([#3612](https://github.com/aws-powertools/powertools-lambda-python/issues/3612)) +* **deps:** revert aws-cdk-lib as a runtime dep ([#3730](https://github.com/aws-powertools/powertools-lambda-python/issues/3730)) +* **deps:** bump actions/upload-artifact from 3.1.3 to 4.3.1 ([#3714](https://github.com/aws-powertools/powertools-lambda-python/issues/3714)) +* **deps-dev:** bump cfn-lint from 0.83.8 to 0.85.0 ([#3724](https://github.com/aws-powertools/powertools-lambda-python/issues/3724)) * **deps-dev:** bump httpx from 0.24.1 to 0.26.0 ([#3712](https://github.com/aws-powertools/powertools-lambda-python/issues/3712)) +* **deps-dev:** bump pytest from 7.4.4 to 8.0.0 ([#3711](https://github.com/aws-powertools/powertools-lambda-python/issues/3711)) +* **deps-dev:** bump sentry-sdk from 1.40.1 to 1.40.2 ([#3740](https://github.com/aws-powertools/powertools-lambda-python/issues/3740)) * **deps-dev:** bump coverage from 7.2.7 to 7.4.1 ([#3713](https://github.com/aws-powertools/powertools-lambda-python/issues/3713)) * **deps-dev:** bump the boto-typing group with 7 updates ([#3709](https://github.com/aws-powertools/powertools-lambda-python/issues/3709)) -* **deps-dev:** bump pytest from 7.4.4 to 8.0.0 ([#3711](https://github.com/aws-powertools/powertools-lambda-python/issues/3711)) * **deps-dev:** bump types-python-dateutil from 2.8.19.14 to 2.8.19.20240106 ([#3720](https://github.com/aws-powertools/powertools-lambda-python/issues/3720)) -* **deps-dev:** bump cfn-lint from 0.83.8 to 0.85.0 ([#3724](https://github.com/aws-powertools/powertools-lambda-python/issues/3724)) +* **deps-dev:** bump mypy from 1.4.1 to 1.8.0 ([#3710](https://github.com/aws-powertools/powertools-lambda-python/issues/3710)) +* **deps-dev:** bump ruff from 0.2.0 to 0.2.1 ([#3742](https://github.com/aws-powertools/powertools-lambda-python/issues/3742)) * **deps-dev:** bump isort from 5.11.5 to 5.13.2 ([#3723](https://github.com/aws-powertools/powertools-lambda-python/issues/3723)) -* **deps-dev:** bump sentry-sdk from 1.40.1 to 1.40.2 ([#3740](https://github.com/aws-powertools/powertools-lambda-python/issues/3740)) +* **deps-dev:** bump pytest-socket from 0.6.0 to 0.7.0 ([#3721](https://github.com/aws-powertools/powertools-lambda-python/issues/3721)) * **deps-dev:** bump ruff from 0.1.15 to 0.2.0 ([#3702](https://github.com/aws-powertools/powertools-lambda-python/issues/3702)) * **deps-dev:** bump aws-cdk from 2.125.0 to 2.126.0 ([#3701](https://github.com/aws-powertools/powertools-lambda-python/issues/3701)) +* **deps-dev:** bump hvac from 1.2.1 to 2.1.0 ([#3738](https://github.com/aws-powertools/powertools-lambda-python/issues/3738)) * **deps-dev:** bump black from 23.12.1 to 24.1.1 ([#3739](https://github.com/aws-powertools/powertools-lambda-python/issues/3739)) -* **deps-dev:** bump ruff from 0.2.0 to 0.2.1 ([#3742](https://github.com/aws-powertools/powertools-lambda-python/issues/3742)) @@ -4343,7 +4401,8 @@ * Merge pull request [#5](https://github.com/aws-powertools/powertools-lambda-python/issues/5) from jfuss/feat/python38 -[Unreleased]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.33.0...HEAD +[Unreleased]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.33.1...HEAD +[v2.33.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.33.0...v2.33.1 [v2.33.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.32.0...v2.33.0 [v2.32.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.31.0...v2.32.0 [v2.31.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.30.2...v2.31.0 diff --git a/README.md b/README.md index c1ab7abaf29..00e217c29c7 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build](https://github.com/aws-powertools/powertools-lambda-python/actions/workflows/quality_check.yml/badge.svg)](https://github.com/aws-powertools/powertools-lambda-python/actions/workflows/python_build.yml) [![codecov.io](https://codecov.io/github/aws-powertools/powertools-lambda-python/branch/develop/graphs/badge.svg)](https://app.codecov.io/gh/aws-powertools/powertools-lambda-python) -![PythonSupport](https://img.shields.io/static/v1?label=python&message=%203.8|%203.9|%203.10|%203.11|%203.12&color=blue?style=flat-square&logo=python) ![PyPI version](https://badge.fury.io/py/aws-lambda-powertools.svg) ![PyPi monthly downloads](https://img.shields.io/pypi/dm/aws-lambda-powertools) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/aws-powertools/powertools-lambda-python/badge)](https://api.securityscorecards.dev/projects/github.com/aws-powertools/powertools-lambda-python) [![Join our Discord](https://dcbadge.vercel.app/api/server/B8zZKbbyET)](https://discord.gg/B8zZKbbyET) +![PythonSupport](https://img.shields.io/static/v1?label=python&message=%203.8|%203.9|%203.10|%203.11|%203.12&color=blue?style=flat-square&logo=python) ![PyPI version](https://badge.fury.io/py/aws-lambda-powertools.svg) ![PyPi monthly downloads](https://img.shields.io/pypi/dm/aws-lambda-powertools) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/aws-powertools/powertools-lambda-python/badge)](https://api.securityscorecards.dev/projects/github.com/aws-powertools/powertools-lambda-python) [![Join our Discord](https://dcbadge.vercel.app/api/server/B8zZKbbyET?style=flat-square)](https://discord.gg/B8zZKbbyET) Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless [best practices and increase developer velocity](https://docs.powertools.aws.dev/lambda/python/latest/#features). @@ -11,8 +11,6 @@ Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverles **[πŸ“œDocumentation](https://docs.powertools.aws.dev/lambda/python/)** | **[🐍PyPi](https://pypi.org/project/aws-lambda-powertools/)** | **[Roadmap](https://docs.powertools.aws.dev/lambda/python/latest/roadmap/)** | **[Detailed blog post](https://aws.amazon.com/blogs/opensource/simplifying-serverless-best-practices-with-lambda-powertools/)** -> **An AWS Developer Acceleration (DevAx) initiative by Specialist Solution Architects | ** - ![hero-image](https://user-images.githubusercontent.com/3340292/198254617-d0fdb672-86a6-4988-8a40-adf437135e0a.png) ## Features @@ -83,7 +81,7 @@ This helps us understand who uses Powertools for AWS Lambda (Python) in a non-in ## Connect * **Powertools for AWS Lambda on Discord**: `#python` - **[Invite link](https://discord.gg/B8zZKbbyET)** -* **Email**: +* **Email**: ## Security disclosures diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 43b5bf139ea..271c767c060 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -1692,7 +1692,13 @@ def swagger_handler(): body=escaped_spec, ) - body = generate_swagger_html(escaped_spec, path, swagger_js, swagger_css, swagger_base_url) + body = generate_swagger_html( + escaped_spec, + f"{base_path}{path}", + swagger_js, + swagger_css, + swagger_base_url, + ) return Response( status_code=200, diff --git a/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py b/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py index 54c48189282..241a9972953 100644 --- a/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py +++ b/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py @@ -368,7 +368,10 @@ def _get_embed_body( return received_body, field_alias_omitted -def _normalize_multi_query_string_with_param(query_string: Optional[Dict[str, str]], params: Sequence[ModelField]): +def _normalize_multi_query_string_with_param( + query_string: Dict[str, List[str]], + params: Sequence[ModelField], +) -> Dict[str, Any]: """ Extract and normalize resolved_query_string_parameters @@ -383,15 +386,15 @@ def _normalize_multi_query_string_with_param(query_string: Optional[Dict[str, st ------- A dictionary containing the processed multi_query_string_parameters. """ - if query_string: - for param in filter(is_scalar_field, params): - try: - # if the target parameter is a scalar, we keep the first value of the query string - # regardless if there are more in the payload - query_string[param.name] = query_string[param.name][0] - except KeyError: - pass - return query_string + resolved_query_string: Dict[str, Any] = query_string + for param in filter(is_scalar_field, params): + try: + # if the target parameter is a scalar, we keep the first value of the query string + # regardless if there are more in the payload + resolved_query_string[param.alias] = query_string[param.alias][0] + except KeyError: + pass + return resolved_query_string def _normalize_multi_header_values_with_param(headers: Optional[Dict[str, str]], params: Sequence[ModelField]): diff --git a/aws_lambda_powertools/shared/version.py b/aws_lambda_powertools/shared/version.py index 7cac1198767..4df200c6942 100644 --- a/aws_lambda_powertools/shared/version.py +++ b/aws_lambda_powertools/shared/version.py @@ -1,3 +1,3 @@ """Exposes version constant to avoid circular dependencies.""" -VERSION = "2.33.0" +VERSION = "2.34.0" diff --git a/aws_lambda_powertools/utilities/data_classes/alb_event.py b/aws_lambda_powertools/utilities/data_classes/alb_event.py index 98f37b4f415..1ec2535850b 100644 --- a/aws_lambda_powertools/utilities/data_classes/alb_event.py +++ b/aws_lambda_powertools/utilities/data_classes/alb_event.py @@ -36,11 +36,11 @@ def multi_value_query_string_parameters(self) -> Optional[Dict[str, List[str]]]: return self.get("multiValueQueryStringParameters") @property - def resolved_query_string_parameters(self) -> Optional[Dict[str, Any]]: + def resolved_query_string_parameters(self) -> Dict[str, List[str]]: if self.multi_value_query_string_parameters: return self.multi_value_query_string_parameters - return self.query_string_parameters + return super().resolved_query_string_parameters @property def resolved_headers_field(self) -> Optional[Dict[str, Any]]: diff --git a/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py b/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py index c37bd22ca53..ff24e908d1a 100644 --- a/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py +++ b/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py @@ -119,11 +119,11 @@ def multi_value_query_string_parameters(self) -> Optional[Dict[str, List[str]]]: return self.get("multiValueQueryStringParameters") @property - def resolved_query_string_parameters(self) -> Optional[Dict[str, Any]]: + def resolved_query_string_parameters(self) -> Dict[str, List[str]]: if self.multi_value_query_string_parameters: return self.multi_value_query_string_parameters - return self.query_string_parameters + return super().resolved_query_string_parameters @property def resolved_headers_field(self) -> Optional[Dict[str, Any]]: @@ -318,16 +318,6 @@ def http_method(self) -> str: def header_serializer(self): return HttpApiHeadersSerializer() - @property - def resolved_query_string_parameters(self) -> Optional[Dict[str, Any]]: - if self.query_string_parameters is not None: - query_string = { - key: value.split(",") if "," in value else value for key, value in self.query_string_parameters.items() - } - return query_string - - return {} - @property def resolved_headers_field(self) -> Optional[Dict[str, Any]]: if self.headers is not None: diff --git a/aws_lambda_powertools/utilities/data_classes/bedrock_agent_event.py b/aws_lambda_powertools/utilities/data_classes/bedrock_agent_event.py index 0fa97036a3e..399c435b3ec 100644 --- a/aws_lambda_powertools/utilities/data_classes/bedrock_agent_event.py +++ b/aws_lambda_powertools/utilities/data_classes/bedrock_agent_event.py @@ -109,10 +109,6 @@ def query_string_parameters(self) -> Optional[Dict[str, str]]: # together with the other parameters. So we just return all parameters here. return {x["name"]: x["value"] for x in self["parameters"]} if self.get("parameters") else None - @property - def resolved_query_string_parameters(self) -> Optional[Dict[str, str]]: - return self.query_string_parameters - @property def resolved_headers_field(self) -> Optional[Dict[str, Any]]: return {} diff --git a/aws_lambda_powertools/utilities/data_classes/common.py b/aws_lambda_powertools/utilities/data_classes/common.py index 25fb5a4c170..067706140fd 100644 --- a/aws_lambda_powertools/utilities/data_classes/common.py +++ b/aws_lambda_powertools/utilities/data_classes/common.py @@ -104,7 +104,7 @@ def query_string_parameters(self) -> Optional[Dict[str, str]]: return self.get("queryStringParameters") @property - def resolved_query_string_parameters(self) -> Optional[Dict[str, str]]: + def resolved_query_string_parameters(self) -> Dict[str, List[str]]: """ This property determines the appropriate query string parameter to be used as a trusted source for validating OpenAPI. @@ -112,7 +112,11 @@ def resolved_query_string_parameters(self) -> Optional[Dict[str, str]]: This is necessary because different resolvers use different formats to encode multi query string parameters. """ - return self.query_string_parameters + if self.query_string_parameters is not None: + query_string = {key: value.split(",") for key, value in self.query_string_parameters.items()} + return query_string + + return {} @property def resolved_headers_field(self) -> Optional[Dict[str, Any]]: @@ -186,8 +190,7 @@ def get_header_value( name: str, default_value: str, case_sensitive: Optional[bool] = False, - ) -> str: - ... + ) -> str: ... @overload def get_header_value( @@ -195,8 +198,7 @@ def get_header_value( name: str, default_value: Optional[str] = None, case_sensitive: Optional[bool] = False, - ) -> Optional[str]: - ... + ) -> Optional[str]: ... def get_header_value( self, diff --git a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py index 15144e41d7d..f997d4b3f04 100644 --- a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py @@ -73,8 +73,7 @@ def get_header_value( name: str, default_value: str, case_sensitive: Optional[bool] = False, - ) -> str: - ... + ) -> str: ... @overload def get_header_value( @@ -82,8 +81,7 @@ def get_header_value( name: str, default_value: Optional[str] = None, case_sensitive: Optional[bool] = False, - ) -> Optional[str]: - ... + ) -> Optional[str]: ... def get_header_value( self, @@ -140,10 +138,6 @@ def query_string_parameters(self) -> Dict[str, str]: """The request query string parameters.""" return self["query_string_parameters"] - @property - def resolved_query_string_parameters(self) -> Optional[Dict[str, str]]: - return self.query_string_parameters - @property def resolved_headers_field(self) -> Optional[Dict[str, Any]]: if self.headers is not None: @@ -255,17 +249,21 @@ def path(self) -> str: @property def request_context(self) -> vpcLatticeEventV2RequestContext: - """he VPC Lattice v2 Event request context.""" + """The VPC Lattice v2 Event request context.""" return vpcLatticeEventV2RequestContext(self["requestContext"]) @property def query_string_parameters(self) -> Optional[Dict[str, str]]: - """The request query string parameters.""" - return self.get("queryStringParameters") + """The request query string parameters. - @property - def resolved_query_string_parameters(self) -> Optional[Dict[str, str]]: - return self.query_string_parameters + For VPC Lattice V2, the queryStringParameters will contain a Dict[str, List[str]] + so to keep compatibility with existing utilities, we merge all the values with a comma. + """ + params = self.get("queryStringParameters") + if params: + return {key: ",".join(value) for key, value in params.items()} + else: + return None @property def resolved_headers_field(self) -> Optional[Dict[str, str]]: diff --git a/aws_lambda_powertools/utilities/feature_flags/__init__.py b/aws_lambda_powertools/utilities/feature_flags/__init__.py index db7dfca5b57..e8d8229c9dc 100644 --- a/aws_lambda_powertools/utilities/feature_flags/__init__.py +++ b/aws_lambda_powertools/utilities/feature_flags/__init__.py @@ -1,4 +1,5 @@ """Advanced feature flags utility""" + from .appconfig import AppConfigStore from .base import StoreProvider from .exceptions import ConfigurationStoreError diff --git a/aws_lambda_powertools/utilities/feature_flags/comparators.py b/aws_lambda_powertools/utilities/feature_flags/comparators.py index 78370f1b5b1..03cb91e649a 100644 --- a/aws_lambda_powertools/utilities/feature_flags/comparators.py +++ b/aws_lambda_powertools/utilities/feature_flags/comparators.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from datetime import datetime, tzinfo from typing import Any, Dict, Optional @@ -82,3 +84,66 @@ def compare_modulo_range(context_value: int, condition_value: Dict) -> bool: end = condition_value.get(ModuloRangeValues.END.value, 1) return start <= context_value % base <= end + + +def compare_any_in_list(context_value: list, condition_value: list) -> bool: + """Comparator for ANY_IN_VALUE action + + Parameters + ---------- + context_value : list + user-defined context for flag evaluation + condition_value : list + schema value available for condition being evaluated + + Returns + ------- + bool + Whether any list item in context_value is available in condition_value + """ + if not isinstance(context_value, list): + raise ValueError("Context provided must be a list. Unable to compare ANY_IN_VALUE action.") + + return any(key in condition_value for key in context_value) + + +def compare_all_in_list(context_value: list, condition_value: list) -> bool: + """Comparator for ALL_IN_VALUE action + + Parameters + ---------- + context_value : list + user-defined context for flag evaluation + condition_value : list + schema value available for condition being evaluated + + Returns + ------- + bool + Whether all list items in context_value are available in condition_value + """ + if not isinstance(context_value, list): + raise ValueError("Context provided must be a list. Unable to compare ALL_IN_VALUE action.") + + return all(key in condition_value for key in context_value) + + +def compare_none_in_list(context_value: list, condition_value: list) -> bool: + """Comparator for NONE_IN_VALUE action + + Parameters + ---------- + context_value : list + user-defined context for flag evaluation + condition_value : list + schema value available for condition being evaluated + + Returns + ------- + bool + Whether list items in context_value are **not** available in condition_value + """ + if not isinstance(context_value, list): + raise ValueError("Context provided must be a list. Unable to compare NONE_IN_VALUE action.") + + return all(key not in condition_value for key in context_value) diff --git a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py index 8610d68a8f6..bd7e19d0efe 100644 --- a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py +++ b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py @@ -1,18 +1,52 @@ +from __future__ import annotations + import logging -from typing import Any, Dict, List, Optional, Union, cast +from typing import Any, Callable, Dict, List, Optional, TypeVar, Union, cast + +from typing_extensions import ParamSpec from ... import Logger from ...shared.types import JSONType from . import schema from .base import StoreProvider from .comparators import ( + compare_all_in_list, + compare_any_in_list, compare_datetime_range, compare_days_of_week, compare_modulo_range, + compare_none_in_list, compare_time_range, ) from .exceptions import ConfigurationStoreError +T = TypeVar("T") +P = ParamSpec("P") + +RULE_ACTION_MAPPING = { + schema.RuleAction.EQUALS.value: lambda a, b: a == b, + schema.RuleAction.NOT_EQUALS.value: lambda a, b: a != b, + schema.RuleAction.KEY_GREATER_THAN_VALUE.value: lambda a, b: a > b, + schema.RuleAction.KEY_GREATER_THAN_OR_EQUAL_VALUE.value: lambda a, b: a >= b, + schema.RuleAction.KEY_LESS_THAN_VALUE.value: lambda a, b: a < b, + schema.RuleAction.KEY_LESS_THAN_OR_EQUAL_VALUE.value: lambda a, b: a <= b, + schema.RuleAction.STARTSWITH.value: lambda a, b: a.startswith(b), + schema.RuleAction.ENDSWITH.value: lambda a, b: a.endswith(b), + schema.RuleAction.IN.value: lambda a, b: a in b, + schema.RuleAction.NOT_IN.value: lambda a, b: a not in b, + schema.RuleAction.KEY_IN_VALUE.value: lambda a, b: a in b, + schema.RuleAction.KEY_NOT_IN_VALUE.value: lambda a, b: a not in b, + schema.RuleAction.VALUE_IN_KEY.value: lambda a, b: b in a, + schema.RuleAction.VALUE_NOT_IN_KEY.value: lambda a, b: b not in a, + schema.RuleAction.ALL_IN_VALUE.value: lambda a, b: compare_all_in_list(a, b), + schema.RuleAction.ANY_IN_VALUE.value: lambda a, b: compare_any_in_list(a, b), + schema.RuleAction.NONE_IN_VALUE.value: lambda a, b: compare_none_in_list(a, b), + schema.RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value: lambda a, b: compare_time_range(a, b), + schema.RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value: lambda a, b: compare_datetime_range(a, b), + schema.RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value: lambda a, b: compare_days_of_week(a, b), + schema.RuleAction.MODULO_RANGE.value: lambda a, b: compare_modulo_range(a, b), +} + class FeatureFlags: def __init__(self, store: StoreProvider, logger: Optional[Union[logging.Logger, Logger]] = None): @@ -46,34 +80,20 @@ def __init__(self, store: StoreProvider, logger: Optional[Union[logging.Logger, """ self.store = store self.logger = logger or logging.getLogger(__name__) + self._exception_handlers: dict[Exception, Callable] = {} def _match_by_action(self, action: str, condition_value: Any, context_value: Any) -> bool: - mapping_by_action = { - schema.RuleAction.EQUALS.value: lambda a, b: a == b, - schema.RuleAction.NOT_EQUALS.value: lambda a, b: a != b, - schema.RuleAction.KEY_GREATER_THAN_VALUE.value: lambda a, b: a > b, - schema.RuleAction.KEY_GREATER_THAN_OR_EQUAL_VALUE.value: lambda a, b: a >= b, - schema.RuleAction.KEY_LESS_THAN_VALUE.value: lambda a, b: a < b, - schema.RuleAction.KEY_LESS_THAN_OR_EQUAL_VALUE.value: lambda a, b: a <= b, - schema.RuleAction.STARTSWITH.value: lambda a, b: a.startswith(b), - schema.RuleAction.ENDSWITH.value: lambda a, b: a.endswith(b), - schema.RuleAction.IN.value: lambda a, b: a in b, - schema.RuleAction.NOT_IN.value: lambda a, b: a not in b, - schema.RuleAction.KEY_IN_VALUE.value: lambda a, b: a in b, - schema.RuleAction.KEY_NOT_IN_VALUE.value: lambda a, b: a not in b, - schema.RuleAction.VALUE_IN_KEY.value: lambda a, b: b in a, - schema.RuleAction.VALUE_NOT_IN_KEY.value: lambda a, b: b not in a, - schema.RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value: lambda a, b: compare_time_range(a, b), - schema.RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value: lambda a, b: compare_datetime_range(a, b), - schema.RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value: lambda a, b: compare_days_of_week(a, b), - schema.RuleAction.MODULO_RANGE.value: lambda a, b: compare_modulo_range(a, b), - } - try: - func = mapping_by_action.get(action, lambda a, b: False) + func = RULE_ACTION_MAPPING.get(action, lambda a, b: False) return func(context_value, condition_value) except Exception as exc: self.logger.debug(f"caught exception while matching action: action={action}, exception={str(exc)}") + + handler = self._lookup_exception_handler(exc) + if handler: + self.logger.debug("Exception handler found! Delegating response.") + return handler(exc) + return False def _evaluate_conditions( @@ -203,6 +223,22 @@ def evaluate(self, *, name: str, context: Optional[Dict[str, Any]] = None, defau 2. Feature exists but has either no rules or no match, return feature default value 3. Feature doesn't exist in stored schema, encountered an error when fetching -> return default value provided + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Feature flags │──────▢ Get Configuration β”œβ”€β”€β”€β”€β”€β”€β”€β–Ά Evaluate rules β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ + β”‚β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ β”‚β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ + β”‚β”‚ Fetch schema β”‚β”‚ β”‚β”‚ Match rule β”‚β”‚ + β”‚β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ β”‚β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ + β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ + β”‚β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ β”‚β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ + β”‚β”‚ Cache schema β”‚β”‚ β”‚β”‚ Match condition β”‚β”‚ + β”‚β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ β”‚β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ + β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ + β”‚β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ β”‚β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ + β”‚β”‚ Validate schema β”‚β”‚ β”‚β”‚ Match action β”‚β”‚ + β”‚β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ β”‚β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + Parameters ---------- name: str @@ -216,6 +252,31 @@ def evaluate(self, *, name: str, context: Optional[Dict[str, Any]] = None, defau or there has been an error when fetching the configuration from the store Can be boolean or any JSON values for non-boolean features. + + Examples + -------- + + ```python + from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags + from aws_lambda_powertools.utilities.typing import LambdaContext + + app_config = AppConfigStore(environment="dev", application="product-catalogue", name="features") + + feature_flags = FeatureFlags(store=app_config) + + + def lambda_handler(event: dict, context: LambdaContext): + # Get customer's tier from incoming request + ctx = {"tier": event.get("tier", "standard")} + + # Evaluate whether customer's tier has access to premium features + # based on `has_premium_features` rules + has_premium_features: bool = feature_flags.evaluate(name="premium_features", context=ctx, default=False) + if has_premium_features: + # enable premium features + ... + ``` + Returns ------ JSONType @@ -329,3 +390,45 @@ def get_enabled_features(self, *, context: Optional[Dict[str, Any]] = None) -> L features_enabled.append(name) return features_enabled + + def validation_exception_handler(self, exc_class: Exception | list[Exception]): + """Registers function to handle unexpected validation exceptions when evaluating flags. + + It does not override the function of a default flag value in case of network and IAM permissions. + For example, you won't be able to catch ConfigurationStoreError exception. + + Parameters + ---------- + exc_class : Exception | list[Exception] + One or more exceptions to catch + + Examples + -------- + + ```python + feature_flags = FeatureFlags(store=app_config) + + @feature_flags.validation_exception_handler(Exception) # any exception + def catch_exception(exc): + raise TypeError("re-raised") from exc + ``` + """ + + def register_exception_handler(func: Callable[P, T]) -> Callable[P, T]: + if isinstance(exc_class, list): + for exp in exc_class: + self._exception_handlers[exp] = func + else: + self._exception_handlers[exc_class] = func + + return func + + return register_exception_handler + + def _lookup_exception_handler(self, exc: BaseException) -> Callable | None: + # Use "Method Resolution Order" to allow for matching against a base class + # of an exception + for cls in type(exc).__mro__: + if cls in self._exception_handlers: + return self._exception_handlers[cls] # type: ignore[index] # index is correct + return None diff --git a/aws_lambda_powertools/utilities/feature_flags/schema.py b/aws_lambda_powertools/utilities/feature_flags/schema.py index 0dc5e8d56bc..1df16677bd8 100644 --- a/aws_lambda_powertools/utilities/feature_flags/schema.py +++ b/aws_lambda_powertools/utilities/feature_flags/schema.py @@ -1,8 +1,11 @@ +from __future__ import annotations + import logging import re from datetime import datetime from enum import Enum -from typing import Any, Callable, Dict, List, Optional, Union +from functools import lru_cache +from typing import Any, Dict, List, Optional, Union from dateutil import tz @@ -19,9 +22,11 @@ CONDITION_ACTION = "action" FEATURE_DEFAULT_VAL_TYPE_KEY = "boolean_type" TIME_RANGE_FORMAT = "%H:%M" # hour:min 24 hours clock -TIME_RANGE_RE_PATTERN = re.compile(r"2[0-3]:[0-5]\d|[0-1]\d:[0-5]\d") # 24 hour clock +TIME_RANGE_PATTERN = re.compile(r"2[0-3]:[0-5]\d|[0-1]\d:[0-5]\d") # 24 hour clock HOUR_MIN_SEPARATOR = ":" +LOGGER: logging.Logger | Logger = logging.getLogger(__name__) + class RuleAction(str, Enum): EQUALS = "EQUALS" @@ -38,6 +43,9 @@ class RuleAction(str, Enum): KEY_NOT_IN_VALUE = "KEY_NOT_IN_VALUE" VALUE_IN_KEY = "VALUE_IN_KEY" VALUE_NOT_IN_KEY = "VALUE_NOT_IN_KEY" + ALL_IN_VALUE = "ALL_IN_VALUE" + ANY_IN_VALUE = "ANY_IN_VALUE" + NONE_IN_VALUE = "NONE_IN_VALUE" SCHEDULE_BETWEEN_TIME_RANGE = "SCHEDULE_BETWEEN_TIME_RANGE" # hour:min 24 hours clock SCHEDULE_BETWEEN_DATETIME_RANGE = "SCHEDULE_BETWEEN_DATETIME_RANGE" # full datetime format, excluding timezone SCHEDULE_BETWEEN_DAYS_OF_WEEK = "SCHEDULE_BETWEEN_DAYS_OF_WEEK" # MONDAY, TUESDAY, .... see TimeValues enum @@ -71,6 +79,11 @@ class TimeValues(Enum): FRIDAY = "FRIDAY" SATURDAY = "SATURDAY" + @classmethod + @lru_cache(maxsize=1) + def days(cls) -> list[str]: + return [day.value for day in cls if day.value not in ["START", "END", "TIMEZONE"]] + class ModuloRangeValues(Enum): """ @@ -185,7 +198,12 @@ class SchemaValidator(BaseValidator): def __init__(self, schema: Dict[str, Any], logger: Optional[Union[logging.Logger, Logger]] = None): self.schema = schema - self.logger = logger or logging.getLogger(__name__) + self.logger = logger or LOGGER + + # Validators are designed for modular testing + # therefore we link the custom logger with global LOGGER + # so custom validators can use them when necessary + SchemaValidator._link_global_logger(self.logger) def validate(self) -> None: self.logger.debug("Validating schema") @@ -195,13 +213,18 @@ def validate(self) -> None: features = FeaturesValidator(schema=self.schema, logger=self.logger) features.validate() + @staticmethod + def _link_global_logger(logger: logging.Logger | Logger): + global LOGGER + LOGGER = logger + class FeaturesValidator(BaseValidator): """Validates each feature and calls RulesValidator to validate its rules""" def __init__(self, schema: Dict, logger: Optional[Union[logging.Logger, Logger]] = None): self.schema = schema - self.logger = logger or logging.getLogger(__name__) + self.logger = logger or LOGGER def validate(self): for name, feature in self.schema.items(): @@ -239,7 +262,7 @@ def __init__( self.feature = feature self.feature_name = next(iter(self.feature)) self.rules: Optional[Dict] = self.feature.get(RULES_KEY) - self.logger = logger or logging.getLogger(__name__) + self.logger = logger or LOGGER self.boolean_feature = boolean_feature def validate(self): @@ -286,7 +309,7 @@ class ConditionsValidator(BaseValidator): def __init__(self, rule: Dict[str, Any], rule_name: str, logger: Optional[Union[logging.Logger, Logger]] = None): self.conditions: List[Dict[str, Any]] = rule.get(CONDITIONS_KEY, {}) self.rule_name = rule_name - self.logger = logger or logging.getLogger(__name__) + self.logger = logger or LOGGER def validate(self): if not self.conditions or not isinstance(self.conditions, list): @@ -322,23 +345,26 @@ def validate_condition_key(condition: Dict[str, Any], rule_name: str): if not key or not isinstance(key, str): raise SchemaValidationError(f"'key' value must be a non empty string, rule={rule_name}") - # time actions need to have very specific keys - # SCHEDULE_BETWEEN_TIME_RANGE => CURRENT_TIME - # SCHEDULE_BETWEEN_DATETIME_RANGE => CURRENT_DATETIME - # SCHEDULE_BETWEEN_DAYS_OF_WEEK => CURRENT_DAY_OF_WEEK action = condition.get(CONDITION_ACTION, "") - if action == RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value and key != TimeKeys.CURRENT_TIME.value: - raise SchemaValidationError( - f"'condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a 'CURRENT_TIME' condition key, rule={rule_name}", # noqa: E501 - ) - if action == RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value and key != TimeKeys.CURRENT_DATETIME.value: - raise SchemaValidationError( - f"'condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a 'CURRENT_DATETIME' condition key, rule={rule_name}", # noqa: E501 - ) - if action == RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value and key != TimeKeys.CURRENT_DAY_OF_WEEK.value: - raise SchemaValidationError( - f"'condition with a 'SCHEDULE_BETWEEN_DAYS_OF_WEEK' action must have a 'CURRENT_DAY_OF_WEEK' condition key, rule={rule_name}", # noqa: E501 - ) + + # To allow for growth and prevent if/elif chains, we align extra validators based on the action name. + # for example: + # + # SCHEDULE_BETWEEN_DAYS_OF_WEEK_KEY + # - extra validation: `_validate_schedule_between_days_of_week_key` + # + # maintenance: we should split to separate file/classes for better organization, e.g., visitor pattern. + + custom_validator = getattr(ConditionsValidator, f"_validate_{action.lower()}_key", None) + + # ~90% of actions available don't require a custom validator + # logging a debug statement for no-match will increase CPU cycles for most customers + # for that reason only, we invert and log only when extra validation is found. + if custom_validator is None: + return + + LOGGER.debug(f"{action} requires key validation. Running '{custom_validator}' validator.") + custom_validator(key, rule_name) @staticmethod def validate_condition_value(condition: Dict[str, Any], rule_name: str): @@ -347,65 +373,35 @@ def validate_condition_value(condition: Dict[str, Any], rule_name: str): raise SchemaValidationError(f"'value' key must not be null, rule={rule_name}") action = condition.get(CONDITION_ACTION, "") - # time actions need to be parsed to make sure date and time format is valid and timezone is recognized - if action == RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value: - ConditionsValidator._validate_schedule_between_time_and_datetime_ranges( - value, - rule_name, - action, - ConditionsValidator._validate_time_value, - ) - elif action == RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value: - ConditionsValidator._validate_schedule_between_time_and_datetime_ranges( - value, - rule_name, - action, - ConditionsValidator._validate_datetime_value, - ) - elif action == RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value: - ConditionsValidator._validate_schedule_between_days_of_week(value, rule_name) - # modulo range condition needs validation on base, start, and end attributes - elif action == RuleAction.MODULO_RANGE.value: - ConditionsValidator._validate_modulo_range(value, rule_name) + # To allow for growth and prevent if/elif chains, we align extra validators based on the action name. + # for example: + # + # SCHEDULE_BETWEEN_DAYS_OF_WEEK_KEY + # - extra validation: `_validate_schedule_between_days_of_week_value` + # + # maintenance: we should split to separate file/classes for better organization, e.g., visitor pattern. - @staticmethod - def _validate_datetime_value(datetime_str: str, rule_name: str): - date = None + custom_validator = getattr(ConditionsValidator, f"_validate_{action.lower()}_value", None) - # We try to parse first with timezone information in order to return the correct error messages - # when a timestamp with timezone is used. Otherwise, the user would get the first error "must be a valid - # ISO8601 time format" which is misleading + # ~90% of actions available don't require a custom validator + # logging a debug statement for no-match will increase CPU cycles for most customers + # for that reason only, we invert and log only when extra validation is found. + if custom_validator is None: + return - try: - # python < 3.11 don't support the Z timezone on datetime.fromisoformat, - # so we replace any Z with the equivalent "+00:00" - # datetime.fromisoformat is orders of magnitude faster than datetime.strptime - date = datetime.fromisoformat(datetime_str.replace("Z", "+00:00")) - except Exception: - raise SchemaValidationError(f"'START' and 'END' must be a valid ISO8601 time format, rule={rule_name}") + LOGGER.debug(f"{action} requires value validation. Running '{custom_validator}' validator.") - # we only allow timezone information to be set via the TIMEZONE field - # this way we can encode DST into the calculation. For instance, Copenhagen is - # UTC+2 during winter, and UTC+1 during summer, which would be impossible to define - # using a single ISO datetime string - if date.tzinfo is not None: - raise SchemaValidationError( - "'START' and 'END' must not include timezone information. Set the timezone using the 'TIMEZONE' " - f"field, rule={rule_name} ", - ) + custom_validator(value, rule_name) @staticmethod - def _validate_time_value(time: str, rule_name: str): - # Using a regex instead of strptime because it's several orders of magnitude faster - match = TIME_RANGE_RE_PATTERN.match(time) - - if not match: + def _validate_schedule_between_days_of_week_key(key: str, rule_name: str): + if key != TimeKeys.CURRENT_DAY_OF_WEEK.value: raise SchemaValidationError( - f"'START' and 'END' must be a valid time format, time_format={TIME_RANGE_FORMAT}, rule={rule_name}", + f"'condition with a 'SCHEDULE_BETWEEN_DAYS_OF_WEEK' action must have a 'CURRENT_DAY_OF_WEEK' condition key, rule={rule_name}", # noqa: E501 ) @staticmethod - def _validate_schedule_between_days_of_week(value: Any, rule_name: str): + def _validate_schedule_between_days_of_week_value(value: dict, rule_name: str): error_str = f"condition with a CURRENT_DAY_OF_WEEK action must have a condition value dictionary with 'DAYS' and 'TIMEZONE' (optional) keys, rule={rule_name}" # noqa: E501 if not isinstance(value, dict): raise SchemaValidationError(error_str) @@ -413,59 +409,70 @@ def _validate_schedule_between_days_of_week(value: Any, rule_name: str): days = value.get(TimeValues.DAYS.value) if not isinstance(days, list) or not value: raise SchemaValidationError(error_str) + + valid_days = TimeValues.days() for day in days: - if not isinstance(day, str) or day not in [ - TimeValues.MONDAY.value, - TimeValues.TUESDAY.value, - TimeValues.WEDNESDAY.value, - TimeValues.THURSDAY.value, - TimeValues.FRIDAY.value, - TimeValues.SATURDAY.value, - TimeValues.SUNDAY.value, - ]: + if not isinstance(day, str) or day not in valid_days: raise SchemaValidationError( f"condition value DAYS must represent a day of the week in 'TimeValues' enum, rule={rule_name}", ) - timezone = value.get(TimeValues.TIMEZONE.value, "UTC") - if not isinstance(timezone, str): - raise SchemaValidationError(error_str) + ConditionsValidator._validate_timezone(timezone=value.get(TimeValues.TIMEZONE.value), rule=rule_name) - # try to see if the timezone string corresponds to any known timezone - if not tz.gettz(timezone): - raise SchemaValidationError(f"'TIMEZONE' value must represent a valid IANA timezone, rule={rule_name}") + @staticmethod + def _validate_schedule_between_time_range_key(key: str, rule_name: str): + if key != TimeKeys.CURRENT_TIME.value: + raise SchemaValidationError( + f"'condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a 'CURRENT_TIME' condition key, rule={rule_name}", # noqa: E501 + ) @staticmethod - def _validate_schedule_between_time_and_datetime_ranges( - value: Any, - rule_name: str, - action_name: str, - validator: Callable[[str, str], None], - ): - error_str = f"condition with a '{action_name}' action must have a condition value type dictionary with 'START' and 'END' keys, rule={rule_name}" # noqa: E501 + def _validate_schedule_between_time_range_value(value: Dict, rule_name: str): if not isinstance(value, dict): - raise SchemaValidationError(error_str) + raise SchemaValidationError( + f"{RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value} action must have a dictionary with 'START' and 'END' keys, rule={rule_name}", # noqa: E501 + ) + + start_time = value.get(TimeValues.START.value, "") + end_time = value.get(TimeValues.END.value, "") - start_time = value.get(TimeValues.START.value) - end_time = value.get(TimeValues.END.value) - if not start_time or not end_time: - raise SchemaValidationError(error_str) if not isinstance(start_time, str) or not isinstance(end_time, str): raise SchemaValidationError(f"'START' and 'END' must be a non empty string, rule={rule_name}") - validator(start_time, rule_name) - validator(end_time, rule_name) + # Using a regex instead of strptime because it's several orders of magnitude faster + if not TIME_RANGE_PATTERN.match(start_time) or not TIME_RANGE_PATTERN.match(end_time): + raise SchemaValidationError( + f"'START' and 'END' must be a valid time format, time_format={TIME_RANGE_FORMAT}, rule={rule_name}", + ) - timezone = value.get(TimeValues.TIMEZONE.value, "UTC") - if not isinstance(timezone, str): - raise SchemaValidationError(f"'TIMEZONE' must be a string, rule={rule_name}") + ConditionsValidator._validate_timezone(timezone=value.get(TimeValues.TIMEZONE.value), rule=rule_name) - # try to see if the timezone string corresponds to any known timezone - if not tz.gettz(timezone): - raise SchemaValidationError(f"'TIMEZONE' value must represent a valid IANA timezone, rule={rule_name}") + @staticmethod + def _validate_schedule_between_datetime_range_key(key: str, rule_name: str): + if key != TimeKeys.CURRENT_DATETIME.value: + raise SchemaValidationError( + f"'condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a 'CURRENT_DATETIME' condition key, rule={rule_name}", # noqa: E501 + ) @staticmethod - def _validate_modulo_range(value: Any, rule_name: str): + def _validate_schedule_between_datetime_range_value(value: dict, rule_name: str): + if not isinstance(value, dict): + raise SchemaValidationError( + f"{RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value} action must have a dictionary with 'START' and 'END' keys, rule={rule_name}", # noqa: E501 + ) + + start_time = value.get(TimeValues.START.value, "") + end_time = value.get(TimeValues.END.value, "") + + if not isinstance(start_time, str) or not isinstance(end_time, str): + raise SchemaValidationError(f"'START' and 'END' must be a non empty string, rule={rule_name}") + + ConditionsValidator._validate_datetime(start_time, rule_name) + ConditionsValidator._validate_datetime(end_time, rule_name) + ConditionsValidator._validate_timezone(timezone=value.get(TimeValues.TIMEZONE.value), rule=rule_name) + + @staticmethod + def _validate_modulo_range_value(value: dict, rule_name: str): error_str = f"condition with a 'MODULO_RANGE' action must have a condition value type dictionary with 'BASE', 'START' and 'END' keys, rule={rule_name}" # noqa: E501 if not isinstance(value, dict): raise SchemaValidationError(error_str) @@ -473,8 +480,10 @@ def _validate_modulo_range(value: Any, rule_name: str): base = value.get(ModuloRangeValues.BASE.value) start = value.get(ModuloRangeValues.START.value) end = value.get(ModuloRangeValues.END.value) + if base is None or start is None or end is None: raise SchemaValidationError(error_str) + if not isinstance(base, int) or not isinstance(start, int) or not isinstance(end, int): raise SchemaValidationError(f"'BASE', 'START' and 'END' must be integers, rule={rule_name}") @@ -482,3 +491,55 @@ def _validate_modulo_range(value: Any, rule_name: str): raise SchemaValidationError( f"condition with 'MODULO_RANGE' action must satisfy 0 <= START <= END <= BASE-1, rule={rule_name}", ) + + @staticmethod + def _validate_all_in_value_value(value: list, rule_name: str): + if not (isinstance(value, list)): + raise SchemaValidationError(f"ALL_IN_VALUE action must have a list value, rule={rule_name}") + + @staticmethod + def _validate_any_in_value_value(value: list, rule_name: str): + if not (isinstance(value, list)): + raise SchemaValidationError(f"ANY_IN_VALUE action must have a list value, rule={rule_name}") + + @staticmethod + def _validate_none_in_value_value(value: list, rule_name: str): + if not (isinstance(value, list)): + raise SchemaValidationError(f"NONE_IN_VALUE action must have a list value, rule={rule_name}") + + @staticmethod + def _validate_datetime(datetime_str: str, rule_name: str): + date = None + + # We try to parse first with timezone information in order to return the correct error messages + # when a timestamp with timezone is used. Otherwise, the user would get the first error "must be a valid + # ISO8601 time format" which is misleading + + try: + # python < 3.11 don't support the Z timezone on datetime.fromisoformat, + # so we replace any Z with the equivalent "+00:00" + # datetime.fromisoformat is orders of magnitude faster than datetime.strptime + date = datetime.fromisoformat(datetime_str.replace("Z", "+00:00")) + except Exception: + raise SchemaValidationError(f"'START' and 'END' must be a valid ISO8601 time format, rule={rule_name}") + + # we only allow timezone information to be set via the TIMEZONE field + # this way we can encode DST into the calculation. For instance, Copenhagen is + # UTC+2 during winter, and UTC+1 during summer, which would be impossible to define + # using a single ISO datetime string + if date.tzinfo is not None: + raise SchemaValidationError( + "'START' and 'END' must not include timezone information. Set the timezone using the 'TIMEZONE' " + f"field, rule={rule_name} ", + ) + + @staticmethod + def _validate_timezone(rule: str, timezone: str | None = None): + timezone = timezone or "UTC" + + if not isinstance(timezone, str): + raise SchemaValidationError(f"'TIMEZONE' must be a string, rule={str}") + + # try to see if the timezone string corresponds to any known timezone + if not tz.gettz(timezone): + raise SchemaValidationError(f"'TIMEZONE' value must represent a valid IANA timezone, rule={rule}") diff --git a/docs/Dockerfile b/docs/Dockerfile index 9de94938c2b..4445acc55c7 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -1,5 +1,5 @@ # v9.1.18 -FROM squidfunk/mkdocs-material@sha256:6a72238e24c73e4cebb1ceddf8603778d25739ffbf480a314628a3d81aee2214 +FROM squidfunk/mkdocs-material@sha256:43b898a5520bbe5ee0080568c002491cd8fcd2269e64f7ad2ba4c9c419acb866 # pip-compile --generate-hashes --output-file=requirements.txt requirements.in COPY requirements.txt /tmp/ RUN pip install --require-hashes -r /tmp/requirements.txt diff --git a/docs/index.md b/docs/index.md index a5132120490..7aa9a7d956d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,461 +7,482 @@ description: Powertools for AWS Lambda (Python) Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity. -???+ tip - Powertools for AWS Lambda (Python) is also available for [Java](https://docs.powertools.aws.dev/lambda/java/){target="_blank"}, [TypeScript](https://docs.powertools.aws.dev/lambda/typescript/latest/){target="_blank" }, and [.NET](https://docs.powertools.aws.dev/lambda/dotnet/){target="_blank"} + +
-??? hint "Support this project by becoming a reference customer, sharing your work, or using Layers/SAR :heart:" +- :material-battery-charging:{ .lg .middle } __Features__ - You can choose to support us in three ways: + --- - 1) [**Become a reference customer**](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=customer-reference&template=support_powertools.yml&title=%5BSupport+Lambda+Powertools%5D%3A+%3Cyour+organization+name%3E){target="_blank"}. This gives us permission to list your company in our documentation. + Adopt one, a few, or all industry practices. **Progressively**. - 2) [**Share your work**](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=community-content&template=share_your_work.yml&title=%5BI+Made+This%5D%3A+%3CTITLE%3E){target="_blank"}. Blog posts, video, sample projects you used Powertools! + [:octicons-arrow-right-24: All features](#features) - 3) Use [**Lambda Layers**](#lambda-layer) or [**SAR**](#sar), if possible. This helps us understand who uses Powertools for AWS Lambda (Python) in a non-intrusive way, and helps us gain future investments for other Powertools for AWS Lambda languages. +- :heart:{ .lg .middle } __Support this project__ - When using Layers, you can add Powertools for AWS Lambda (Python) as a dev dependency (or as part of your virtual env) to not impact the development process. + --- -## Install + Become a public reference customer, share your work, contribute, use Lambda Layers, etc. -You can install Powertools for AWS Lambda (Python) using one of the following options: + [:octicons-arrow-right-24: Support](#support-powertools-for-aws-lambda-python) -* **Lambda Layer (x86_64)**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:61**](# "Replace {region} with your AWS region, e.g., eu-west-1"){: .copyMe}:clipboard: -* **Lambda Layer (arm64)**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61**](# "Replace {region} with your AWS region, e.g., eu-west-1"){: .copyMe}:clipboard: -* **Pip**: **[`pip install "aws-lambda-powertools"`](#){: .copyMe}:clipboard:** +- :material-file-code:{ .lg .middle } __Available languages__ -!!! question "Looking for Pip signed releases? [Learn more about verifying signed builds](./security.md#verifying-signed-builds)" + --- -??? question "Using Pip? You might need to install additional dependencies." - [**Tracer**](./core/tracer.md){target="_blank"}, [**Validation**](./utilities/validation.md){target="_blank"} and [**Parser**](./utilities/parser.md){target="_blank"} require additional dependencies. If you prefer to install all of them, use [**`pip install "aws-lambda-powertools[all]"`**](#){: .copyMe}:clipboard:. + Powertools for AWS Lambda is also available in other languages - For example: + :octicons-arrow-right-24: [Java](https://docs.powertools.aws.dev/lambda/java/){target="_blank"}, [TypeScript](https://docs.powertools.aws.dev/lambda/typescript/latest/){target="_blank" }, and [.NET](https://docs.powertools.aws.dev/lambda/dotnet/){target="_blank"} - * **Tracer**: **[`pip install "aws-lambda-powertools[tracer]"`](#){: .copyMe}:clipboard:** - * **Validation**: **[`pip install "aws-lambda-powertools[validation]"`](#){: .copyMe}:clipboard:** - * **Parser**: **[`pip install "aws-lambda-powertools[parser]"`](#){: .copyMe}:clipboard:** - * **Tracer** and **Parser**: **[`pip install "aws-lambda-powertools[tracer,parser]"`](#){: .copyMe}:clipboard:** +
-### Local development +## Install -!!! info "Using Lambda Layer? Simply add [**`"aws-lambda-powertools[all]"`**](#){: .copyMe}:clipboard: as a development dependency." +You can install Powertools for AWS Lambda (Python) using your favorite dependency management, or Lambda Layers: -Powertools for AWS Lambda (Python) relies on the [AWS SDK bundled in the Lambda runtime](https://docs.aws.amazon.com/lambda/latest/dg/lambda-python.html){target="_blank"}. This helps us achieve an optimal package size and initialization. However, when developing locally, you need to install AWS SDK as a development dependency (not as a production dependency): +=== "Pip" -* **Pip**: [**`pip install "aws-lambda-powertools[aws-sdk]"`**](#){: .copyMe}:clipboard: -* **Poetry**: [**`poetry add "aws-lambda-powertools[aws-sdk]" --group dev`**](#){: .copyMe}:clipboard: -* **Pipenv**: [**`pipenv install --dev "aws-lambda-powertools[aws-sdk]"`**](#){: .copyMe}:clipboard: + You can install [all extra dependencies](#extra-dependencies) at once with the `[all]` extras. -??? question "Why is that necessary?" - Powertools for AWS Lambda (Python) relies on the AWS SDK being available to use in the target runtime (AWS Lambda). + * **pip**: [**`pip install "aws-lambda-powertools[all]"`**](#){: .copyMe} + * **poetry**: [**`poetry add "aws-lambda-powertools[all]"`**](#){: .copyMe} + * **pdm**: [**`pdm add "aws-lambda-powertools[all]"`**](#){: .copyMe} - As a result, it affects your favorite IDE in terms of code auto-completion, or running your tests suite locally with no Lambda emulation such as [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html){target="_blank"}. + Alternatively, see [extra dependencies](#extra-dependencies) if you want to install only what you need. -**A word about dependency resolution** +=== "Lambda Layer" -In this context, `[aws-sdk]` is an alias to the `boto3` package. Due to dependency resolution, it'll either install: + You can add our layer both in the [AWS Lambda Console _(under `Layers`)_](https://eu-west-1.console.aws.amazon.com/lambda/home#/add/layer){target="_blank"}, or via your favorite infrastructure as code framework with the ARN value. -* **(A)** the SDK version available in [Lambda runtime](https://docs.aws.amazon.com/lambda/latest/dg/lambda-python.html){target="_blank"} -* **(B)** a more up-to-date version if another package you use also depends on `boto3`, for example [Powertools for AWS Lambda (Python) Tracer](core/tracer.md){target="_blank"} + For the latter, make sure to replace `{region}` with your AWS region, e.g., `eu-west-1`. -### Lambda Layer + * **x86 architecture**: [__arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:62__](# "Replace {region} with your AWS region, e.g., eu-west-1"){: .copyMe}:clipboard: + * **ARM architecture**: [__arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62__](# "Replace {region} with your AWS region, e.g., eu-west-1"){: .copyMe}:clipboard: -???+ warning "As of now, Container Image deployment (OCI) or inline Lambda functions do not support Lambda Layers." + ???+ note "Code snippets for popular infrastructure as code frameworks" -[Lambda Layer](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html){target="_blank"} is a .zip file archive that can contain additional code, pre-packaged dependencies, data, or configuration files. Layers promote code sharing and separation of responsibilities so that you can iterate faster on writing business logic. + === "x86_64" -For our Layers, we compile and optimize [all dependencies](https://github.com/aws-powertools/powertools-lambda-python/blob/develop/pyproject.toml#L98){target="_blank"}, and [remove duplicate dependencies already available in the Lambda runtime](https://github.com/awslabs/cdk-aws-lambda-powertools-layer/blob/main/layer/Python/Dockerfile#L36){target="_blank"} to achieve the most optimal size. + === "SAM" -You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambda Console](https://docs.aws.amazon.com/lambda/latest/dg/invocation-layers.html#invocation-layers-using){target="_blank"}, or your preferred deployment framework. + ```yaml hl_lines="5" + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Layers: + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:62 + ``` -??? note "Note: Click to expand and copy any regional Lambda Layer ARN" + === "Serverless framework" - === "x86_64" + ```yaml hl_lines="5" + functions: + hello: + handler: lambda_function.lambda_handler + layers: + - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:62 + ``` - | Region | Layer ARN | - | ---------------- | ---------------------------------------------------------------------------------------------------------- | - | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `ap-south-2` | [arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `ap-southeast-4` | [arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `ca-west-1` | [arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `eu-central-2` | [arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `eu-south-2` | [arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `il-central-1` | [arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `me-central-1` | [arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | - | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:61](#){: .copyMe}:clipboard: | + === "CDK" - === "arm64" + ```python hl_lines="11 16" + from aws_cdk import core, aws_lambda - | Region | Layer ARN | - | ---------------- | ---------------------------------------------------------------------------------------------------------------- | - | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `ap-south-2` | [arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `eu-central-2` | [arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `eu-south-2` | [arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `il-central-1` | [arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `me-central-1` | [arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61](#){: .copyMe}:clipboard: | - -??? note "Note: Click to expand and copy code snippets for popular frameworks" + class SampleApp(core.Construct): - === "x86_64" + def __init__(self, scope: core.Construct, id_: str, env: core.Environment) -> None: + super().__init__(scope, id_) - === "SAM" + powertools_layer = aws_lambda.LayerVersion.from_layer_version_arn( + self, + id="lambda-powertools", + layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:62" + ) + aws_lambda.Function(self, + 'sample-app-lambda', + runtime=aws_lambda.Runtime.PYTHON_3_9, + layers=[powertools_layer] + # other props... + ) + ``` - ```yaml hl_lines="5" - MyLambdaFunction: - Type: AWS::Serverless::Function - Properties: - Layers: - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:61 - ``` - - === "Serverless framework" - - ```yaml hl_lines="5" - functions: - hello: - handler: lambda_function.lambda_handler - layers: - - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:61 - ``` - - === "CDK" - - ```python hl_lines="11 16" - from aws_cdk import core, aws_lambda - - class SampleApp(core.Construct): - - def __init__(self, scope: core.Construct, id_: str, env: core.Environment) -> None: - super().__init__(scope, id_) - - powertools_layer = aws_lambda.LayerVersion.from_layer_version_arn( - self, - id="lambda-powertools", - layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:61" - ) - aws_lambda.Function(self, - 'sample-app-lambda', - runtime=aws_lambda.Runtime.PYTHON_3_9, - layers=[powertools_layer] - # other props... - ) - ``` - - === "Terraform" - - ```terraform hl_lines="9 38" - terraform { - required_version = "~> 1.0.5" - required_providers { - aws = "~> 3.50.0" - } - } - - provider "aws" { - region = "{region}" - } - - resource "aws_iam_role" "iam_for_lambda" { - name = "iam_for_lambda" - - assume_role_policy = < - ? Choose the runtime that you want to use: Python - ? 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:AWSLambdaPowertoolsPythonV2:61 - ❯ amplify push -y - - - # Updating an existing function and add the layer - ❯ amplify update function - ? Select the Lambda function you want to update test2 - General information - - 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:AWSLambdaPowertoolsPythonV2:61 - ? Do you want to edit the local lambda function now? No - ``` + runtime=aws.lambda_.Runtime.PYTHON3D9, + handler="index.handler", + role=role.arn, + architectures=["x86_64"], + code=pulumi.FileArchive("lambda_function_payload.zip") + ) + ``` + + === "Amplify" + + ```zsh + # Create a new one with the layer + ❯ amplify add function + ? Select which capability you want to add: Lambda function (serverless function) + ? Provide an AWS Lambda function name: + ? Choose the runtime that you want to use: Python + ? 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:AWSLambdaPowertoolsPythonV2:62 + ❯ amplify push -y + + + # Updating an existing function and add the layer + ❯ amplify update function + ? Select the Lambda function you want to update test2 + General information + - 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:AWSLambdaPowertoolsPythonV2:62 + ? Do you want to edit the local lambda function now? No + ``` + + === "arm64" + + === "SAM" + + ```yaml hl_lines="6" + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Architectures: [arm64] + Layers: + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62 + ``` + + === "Serverless framework" + + ```yaml hl_lines="6" + functions: + hello: + handler: lambda_function.lambda_handler + architecture: arm64 + layers: + - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62 + ``` + + === "CDK" + + ```python hl_lines="11 17" + from aws_cdk import core, aws_lambda + + class SampleApp(core.Construct): + + def __init__(self, scope: core.Construct, id_: str, env: core.Environment) -> None: + super().__init__(scope, id_) + + powertools_layer = aws_lambda.LayerVersion.from_layer_version_arn( + self, + id="lambda-powertools", + layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62" + ) + aws_lambda.Function(self, + 'sample-app-lambda', + runtime=aws_lambda.Runtime.PYTHON_3_9, + architecture=aws_lambda.Architecture.ARM_64, + layers=[powertools_layer] + # other props... + ) + ``` + + === "Terraform" + + ```terraform hl_lines="9 37" + terraform { + required_version = "~> 1.0.5" + required_providers { + aws = "~> 3.50.0" + } + } - === "arm64" + provider "aws" { + region = "{region}" + } - === "SAM" + resource "aws_iam_role" "iam_for_lambda" { + name = "iam_for_lambda" - ```yaml hl_lines="6" - MyLambdaFunction: - Type: AWS::Serverless::Function - Properties: - Architectures: [arm64] - Layers: - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61 - ``` - - === "Serverless framework" - - ```yaml hl_lines="6" - functions: - hello: - handler: lambda_function.lambda_handler - architecture: arm64 - layers: - - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61 - ``` - - === "CDK" - - ```python hl_lines="11 17" - from aws_cdk import core, aws_lambda - - class SampleApp(core.Construct): - - def __init__(self, scope: core.Construct, id_: str, env: core.Environment) -> None: - super().__init__(scope, id_) - - powertools_layer = aws_lambda.LayerVersion.from_layer_version_arn( - self, - id="lambda-powertools", - layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:61" - ) - aws_lambda.Function(self, - 'sample-app-lambda', - runtime=aws_lambda.Runtime.PYTHON_3_9, - architecture=aws_lambda.Architecture.ARM_64, - layers=[powertools_layer] - # other props... - ) - ``` - - === "Terraform" - - ```terraform hl_lines="9 37" - terraform { - required_version = "~> 1.0.5" - required_providers { - aws = "~> 3.50.0" - } - } - - provider "aws" { - region = "{region}" - } - - resource "aws_iam_role" "iam_for_lambda" { - name = "iam_for_lambda" - - assume_role_policy = < + ? Choose the runtime that you want to use: Python + ? 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:AWSLambdaPowertoolsPythonV2-Arm64:62 + ❯ amplify push -y + + + # Updating an existing function and add the layer + ❯ amplify update function + ? Select the Lambda function you want to update test2 + General information + - 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:AWSLambdaPowertoolsPythonV2-Arm64:62 + ? Do you want to edit the local lambda function now? No + ``` + +### Extra dependencies + +The vast majority of [features](#features) rely on standard library and AWS SDK _(boto3)_ only. The following features however require additional dependencies: + +| Feature | Pip | Dependency | +| ------------------- | --------------------------------------------------------------------------------- | ----------------------------------- | +| Tracer | **[`pip install "aws-lambda-powertools[tracer]"`](#){: .copyMe}:clipboard:** | `aws-xray-sdk` | +| Validation | **[`pip install "aws-lambda-powertools[validation]"`](#){: .copyMe}:clipboard:** | `fastjsonschema` | +| Parser | **[`pip install "aws-lambda-powertools[parser]"`](#){: .copyMe}:clipboard:** | `pydantic` | +| Data Masking | **[`pip install "aws-lambda-powertools[datamasking]"`](#){: .copyMe}:clipboard:** | `aws-encryption-sdk`, `jsonpath-ng` | +| Idempotency (Redis) | **[`pip install "aws-lambda-powertools[redis]"`](#){: .copyMe}:clipboard:** | `redis` | + +> New to pip? + +You can use `,` delimiter to install multiple at once: [**`pip install "aws-lambda-powertools[tracer,parser,datamasking"]`**](#){: .copyMe}:clipboard: - ``` +### Local development - === "Pulumi" +!!! info "Using Lambda Layer? Simply add [**`"aws-lambda-powertools[all]"`**](#){: .copyMe}:clipboard: as a development dependency." - ```python - import json - import pulumi - import pulumi_aws as aws +Powertools for AWS Lambda (Python) relies on the [AWS SDK bundled in the Lambda runtime](https://docs.aws.amazon.com/lambda/latest/dg/lambda-python.html){target="_blank"}. This helps us achieve an optimal package size and initialization. However, when developing locally, you need to install AWS SDK as a development dependency to support IDE auto-completion and to run your tests locally: - role = aws.iam.Role("role", - assume_role_policy=json.dumps({ - "Version": "2012-10-17", - "Statement": [ - { - "Action": "sts:AssumeRole", - "Principal": { - "Service": "lambda.amazonaws.com" - }, - "Effect": "Allow" - } - ] - }), - managed_policy_arns=[aws.iam.ManagedPolicy.AWS_LAMBDA_BASIC_EXECUTION_ROLE] - ) - - lambda_function = aws.lambda_.Function("function", - layers=[pulumi.Output.concat("arn:aws:lambda:",aws.get_region_output().name,":017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11")], - tracing_config={ - "mode": "Active" - }, - runtime=aws.lambda_.Runtime.PYTHON3D9, - handler="index.handler", - role=role.arn, - architectures=["arm64"], - code=pulumi.FileArchive("lambda_function_payload.zip") - ) - ``` - - === "Amplify" - - ```zsh - # Create a new one with the layer - ❯ amplify add function - ? Select which capability you want to add: Lambda function (serverless function) - ? Provide an AWS Lambda function name: - ? Choose the runtime that you want to use: Python - ? 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:AWSLambdaPowertoolsPythonV2-Arm64:61 - ❯ amplify push -y - - - # Updating an existing function and add the layer - ❯ amplify update function - ? Select the Lambda function you want to update test2 - General information - - 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:AWSLambdaPowertoolsPythonV2-Arm64:61 - ? Do you want to edit the local lambda function now? No - ``` - -??? question "Want to inspect the contents of the Layer?" - 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:AWSLambdaPowertoolsPythonV2:61 --region {region} - ``` +- __Pip__: [**`pip install "aws-lambda-powertools[aws-sdk]"`**](#){: .copyMe}:clipboard: +- __Poetry__: [**`poetry add "aws-lambda-powertools[aws-sdk]" --group dev`**](#){: .copyMe}:clipboard: +- __Pdm__: [**`pdm add -dG "aws-lambda-powertools[aws-sdk]"`**](#){: .copyMe}:clipboard: + +__A word about dependency resolution__ + +In this context, `[aws-sdk]` is an alias to the `boto3` package. Due to dependency resolution, it'll either install: + +- __(A)__ the SDK version available in [Lambda runtime](https://docs.aws.amazon.com/lambda/latest/dg/lambda-python.html){target="_blank"} +- __(B)__ a more up-to-date version if another package you use also depends on `boto3`, for example [Powertools for AWS Lambda (Python) Tracer](core/tracer.md){target="_blank"} + +### Lambda Layer + +[Lambda Layer](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html){target="_blank"} is a .zip file archive that can contain additional code, pre-packaged dependencies, data, or configuration files. We compile and optimize [all dependencies](#extra-dependencies), and [remove duplicate dependencies already available in the Lambda runtime](https://github.com/awslabs/cdk-aws-lambda-powertools-layer/blob/main/layer/Python/Dockerfile#L36){target="_blank"} to achieve the most optimal size. + +??? note "Note: Click to expand and copy any regional Lambda Layer ARN" + + === "x86_64" - The pre-signed URL to download this Lambda Layer will be within `Location` key. + | Region | Layer ARN | + | ---------------- | ---------------------------------------------------------------------------------------------------------- | + | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `ap-south-2` | [arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `ap-southeast-4` | [arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `ca-west-1` | [arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `eu-central-2` | [arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `eu-south-2` | [arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `il-central-1` | [arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `me-central-1` | [arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:62](#){: .copyMe}:clipboard: | + + === "arm64" + + | Region | Layer ARN | + | ---------------- | ---------------------------------------------------------------------------------------------------------------- | + | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `ap-south-2` | [arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `eu-central-2` | [arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `eu-south-2` | [arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `il-central-1` | [arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `me-central-1` | [arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:62](#){: .copyMe}:clipboard: | + +**Want to inspect the contents of the Layer?** + +Replace `{region}` with your AWS region, _e.g. `eu-west-1`_. The pre-signed URL to download this Lambda Layer will be within `Location` key in the CLI output. + +```bash title="AWS CLI command to download Lambda Layer content" +aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:62 --region {region} +``` #### SAR @@ -469,10 +490,10 @@ Serverless Application Repository (SAR) App deploys a CloudFormation stack with Compared with the [public Layer ARN](#lambda-layer) option, SAR allows you to choose a semantic version and deploys a Layer in your target account. -| App | ARN | Description | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- | -| [aws-lambda-powertools-python-layer](https://serverlessrepo.aws.amazon.com/applications/eu-west-1/057560766410/aws-lambda-powertools-python-layer){target="_blank"} | [arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer](#){: .copyMe}:clipboard: | Contains all extra dependencies (e.g: pydantic). | -| [aws-lambda-powertools-python-layer-arm64](https://serverlessrepo.aws.amazon.com/applications/eu-west-1/057560766410/aws-lambda-powertools-python-layer-arm64){target="_blank"} | [arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer-arm64](#){: .copyMe}:clipboard: | Contains all extra dependencies (e.g: pydantic). For arm64 functions. | +| App | ARN | Description | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- | +| [**aws-lambda-powertools-python-layer**](https://serverlessrepo.aws.amazon.com/applications/eu-west-1/057560766410/aws-lambda-powertools-python-layer){target="_blank"} | [arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer](#){: .copyMe}:clipboard: | Contains all extra dependencies (e.g: pydantic). | +| [**aws-lambda-powertools-python-layer-arm64**](https://serverlessrepo.aws.amazon.com/applications/eu-west-1/057560766410/aws-lambda-powertools-python-layer-arm64){target="_blank"} | [arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer-arm64](#){: .copyMe}:clipboard: | Contains all extra dependencies (e.g: pydantic). For arm64 functions. | ??? note "Click to expand and copy SAR code snippets for popular frameworks" @@ -602,78 +623,63 @@ Compared with the [public Layer ARN](#lambda-layer) option, SAR allows you to ch } ``` -??? example "Example: Least-privileged IAM permissions to deploy Layer" - - > Credits to [mwarkentin](https://github.com/mwarkentin){target="_blank" rel="nofollow"} for providing the scoped down IAM permissions. - - The region and the account id for `CloudFormationTransform` and `GetCfnTemplate` are fixed. - - === "template.yml" - - ```yaml hl_lines="21-52" - AWSTemplateFormatVersion: "2010-09-09" - Resources: - PowertoolsLayerIamRole: - Type: "AWS::IAM::Role" - Properties: - AssumeRolePolicyDocument: - Version: "2012-10-17" - Statement: - - Effect: "Allow" - Principal: - Service: - - "cloudformation.amazonaws.com" - Action: - - "sts:AssumeRole" - Path: "/" - PowertoolsLayerIamPolicy: - Type: "AWS::IAM::Policy" - Properties: - PolicyName: PowertoolsLambdaLayerPolicy - PolicyDocument: - Version: "2012-10-17" - Statement: - - Sid: CloudFormationTransform - Effect: Allow - Action: cloudformation:CreateChangeSet - Resource: - - arn:aws:cloudformation:us-east-1:aws:transform/Serverless-2016-10-31 - - Sid: GetCfnTemplate - Effect: Allow - Action: - - serverlessrepo:CreateCloudFormationTemplate - - serverlessrepo:GetCloudFormationTemplate - Resource: - # this is arn of the Powertools for AWS Lambda (Python) SAR app - - arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer - - Sid: S3AccessLayer - Effect: Allow - Action: - - s3:GetObject - Resource: - # AWS publishes to an external S3 bucket locked down to your account ID - # The below example is us publishing Powertools for AWS Lambda (Python) - # Bucket: awsserverlessrepo-changesets-plntc6bfnfj - # Key: *****/arn:aws:serverlessrepo:eu-west-1:057560766410:applications-aws-lambda-powertools-python-layer-versions-1.10.2/aeeccf50-****-****-****-********* - - arn:aws:s3:::awsserverlessrepo-changesets-*/* - - Sid: GetLayerVersion - Effect: Allow - Action: - - lambda:PublishLayerVersion - - lambda:GetLayerVersion - Resource: - - !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:aws-lambda-powertools-python-layer* - Roles: - - Ref: "PowertoolsLayerIamRole" - ``` - -??? note "Click to expand and copy an AWS CLI command to list all versions available in SAR" - - You can fetch available versions via SAR ListApplicationVersions API: - - ```bash title="AWS CLI example" - aws serverlessrepo list-application-versions \ - --application-id arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer + Credits to [mwarkentin](https://github.com/mwarkentin){target="_blank" rel="nofollow"} for providing the scoped down IAM permissions below. + + ```yaml hl_lines="21-52" title="Least-privileged IAM permissions SAM example" + AWSTemplateFormatVersion: "2010-09-09" + Resources: + PowertoolsLayerIamRole: + Type: "AWS::IAM::Role" + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + Service: + - "cloudformation.amazonaws.com" + Action: + - "sts:AssumeRole" + Path: "/" + PowertoolsLayerIamPolicy: + Type: "AWS::IAM::Policy" + Properties: + PolicyName: PowertoolsLambdaLayerPolicy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: CloudFormationTransform + Effect: Allow + Action: cloudformation:CreateChangeSet + Resource: + - arn:aws:cloudformation:us-east-1:aws:transform/Serverless-2016-10-31 + - Sid: GetCfnTemplate + Effect: Allow + Action: + - serverlessrepo:CreateCloudFormationTemplate + - serverlessrepo:GetCloudFormationTemplate + Resource: + # this is arn of the Powertools for AWS Lambda (Python) SAR app + - arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer + - Sid: S3AccessLayer + Effect: Allow + Action: + - s3:GetObject + Resource: + # AWS publishes to an external S3 bucket locked down to your account ID + # The below example is us publishing Powertools for AWS Lambda (Python) + # Bucket: awsserverlessrepo-changesets-plntc6bfnfj + # Key: *****/arn:aws:serverlessrepo:eu-west-1:057560766410:applications-aws-lambda-powertools-python-layer-versions-1.10.2/aeeccf50-****-****-****-********* + - arn:aws:s3:::awsserverlessrepo-changesets-*/* + - Sid: GetLayerVersion + Effect: Allow + Action: + - lambda:PublishLayerVersion + - lambda:GetLayerVersion + Resource: + - !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:aws-lambda-powertools-python-layer* + Roles: + - Ref: "PowertoolsLayerIamRole" ``` ## Quick getting started @@ -688,22 +694,22 @@ Core utilities such as Tracing, Logging, Metrics, and Event Handler will be avai | Utility | Description | | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [**Tracing**](./core/tracer.md){target="_blank"} | Decorators and utilities to trace Lambda function handlers, and both synchronous and asynchronous functions | -| [**Logger**](./core/logger.md){target="_blank"} | Structured logging made easier, and decorator to enrich structured logging with key Lambda context details | -| [**Metrics**](./core/metrics.md){target="_blank"} | Custom Metrics created asynchronously via CloudWatch Embedded Metric Format (EMF) | -| [**Event handler: AppSync**](./core/event_handler/appsync.md){target="_blank"} | AppSync event handler for Lambda Direct Resolver and Amplify GraphQL Transformer function | -| [**Event handler: API Gateway, ALB and Lambda Function URL**](https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/) | Amazon API Gateway REST/HTTP API and ALB event handler for Lambda functions invoked using Proxy integration, and Lambda Function URL | -| [**Middleware factory**](./utilities/middleware_factory.md){target="_blank"} | Decorator factory to create your own middleware to run logic before, and after each Lambda invocation | -| [**Parameters**](./utilities/parameters.md){target="_blank"} | Retrieve parameter values from AWS Systems Manager Parameter Store, AWS Secrets Manager, or Amazon DynamoDB, and cache them for a specific amount of time | -| [**Batch processing**](./utilities/batch.md){target="_blank"} | Handle partial failures for AWS SQS batch processing | -| [**Typing**](./utilities/typing.md){target="_blank"} | Static typing classes to speedup development in your IDE | -| [**Validation**](./utilities/validation.md){target="_blank"} | JSON Schema validator for inbound events and responses | -| [**Event source data classes**](./utilities/data_classes.md){target="_blank"} | Data classes describing the schema of common Lambda event triggers | -| [**Parser**](./utilities/parser.md){target="_blank"} | Data parsing and deep validation using Pydantic | -| [**Idempotency**](./utilities/idempotency.md){target="_blank"} | Idempotent Lambda handler | -| [**Data Masking**](./utilities/data_masking.md){target="_blank"} | Protect confidential data with easy removal or encryption | -| [**Feature Flags**](./utilities/feature_flags.md){target="_blank"} | A simple rule engine to evaluate when one or multiple features should be enabled depending on the input | -| [**Streaming**](./utilities/streaming.md){target="_blank"} | Streams datasets larger than the available memory as streaming data. | +| [__Tracing__](./core/tracer.md){target="_blank"} | Decorators and utilities to trace Lambda function handlers, and both synchronous and asynchronous functions | +| [__Logger__](./core/logger.md){target="_blank"} | Structured logging made easier, and decorator to enrich structured logging with key Lambda context details | +| [__Metrics__](./core/metrics.md){target="_blank"} | Custom Metrics created asynchronously via CloudWatch Embedded Metric Format (EMF) | +| [__Event handler: AppSync__](./core/event_handler/appsync.md){target="_blank"} | AppSync event handler for Lambda Direct Resolver and Amplify GraphQL Transformer function | +| [__Event handler: API Gateway, ALB and Lambda Function URL__](https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/) | Amazon API Gateway REST/HTTP API and ALB event handler for Lambda functions invoked using Proxy integration, and Lambda Function URL | +| [__Middleware factory__](./utilities/middleware_factory.md){target="_blank"} | Decorator factory to create your own middleware to run logic before, and after each Lambda invocation | +| [__Parameters__](./utilities/parameters.md){target="_blank"} | Retrieve parameter values from AWS Systems Manager Parameter Store, AWS Secrets Manager, or Amazon DynamoDB, and cache them for a specific amount of time | +| [__Batch processing__](./utilities/batch.md){target="_blank"} | Handle partial failures for AWS SQS batch processing | +| [__Typing__](./utilities/typing.md){target="_blank"} | Static typing classes to speedup development in your IDE | +| [__Validation__](./utilities/validation.md){target="_blank"} | JSON Schema validator for inbound events and responses | +| [__Event source data classes__](./utilities/data_classes.md){target="_blank"} | Data classes describing the schema of common Lambda event triggers | +| [__Parser__](./utilities/parser.md){target="_blank"} | Data parsing and deep validation using Pydantic | +| [__Idempotency__](./utilities/idempotency.md){target="_blank"} | Idempotent Lambda handler | +| [__Data Masking__](./utilities/data_masking.md){target="_blank"} | Protect confidential data with easy removal or encryption | +| [__Feature Flags__](./utilities/feature_flags.md){target="_blank"} | A simple rule engine to evaluate when one or multiple features should be enabled depending on the input | +| [__Streaming__](./utilities/streaming.md){target="_blank"} | Streams datasets larger than the available memory as streaming data. | ## Environment variables @@ -712,34 +718,33 @@ Core utilities such as Tracing, Logging, Metrics, and Event Handler will be avai | Environment variable | Description | Utility | Default | | ----------------------------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------- | -| **POWERTOOLS_SERVICE_NAME** | Sets service name used for tracing namespace, metrics dimension and structured logging | All | `"service_undefined"` | -| **POWERTOOLS_METRICS_NAMESPACE** | Sets namespace used for metrics | [Metrics](./core/metrics.md){target="_blank"} | `None` | -| **POWERTOOLS_TRACE_DISABLED** | Explicitly disables tracing | [Tracing](./core/tracer.md){target="_blank"} | `false` | -| **POWERTOOLS_TRACER_CAPTURE_RESPONSE** | Captures Lambda or method return as metadata. | [Tracing](./core/tracer.md){target="_blank"} | `true` | -| **POWERTOOLS_TRACER_CAPTURE_ERROR** | Captures Lambda or method exception as metadata. | [Tracing](./core/tracer.md){target="_blank"} | `true` | -| **POWERTOOLS_TRACE_MIDDLEWARES** | Creates sub-segment for each custom middleware | [Middleware factory](./utilities/middleware_factory.md){target="_blank"} | `false` | -| **POWERTOOLS_LOGGER_LOG_EVENT** | Logs incoming event | [Logging](./core/logger.md){target="_blank"} | `false` | -| **POWERTOOLS_LOGGER_SAMPLE_RATE** | Debug log sampling | [Logging](./core/logger.md){target="_blank"} | `0` | -| **POWERTOOLS_LOG_DEDUPLICATION_DISABLED** | Disables log deduplication filter protection to use Pytest Live Log feature | [Logging](./core/logger.md){target="_blank"} | `false` | -| **POWERTOOLS_PARAMETERS_MAX_AGE** | Adjust how long values are kept in cache (in seconds) | [Parameters](./utilities/parameters.md#adjusting-cache-ttl){target="_blank"} | `5` | -| **POWERTOOLS_PARAMETERS_SSM_DECRYPT** | Sets whether to decrypt or not values retrieved from AWS SSM Parameters Store | [Parameters](./utilities/parameters.md#ssmprovider){target="_blank"} | `false` | -| **POWERTOOLS_DEV** | Increases verbosity across utilities | Multiple; see [POWERTOOLS_DEV effect below](#optimizing-for-non-production-environments) | `false` | -| **POWERTOOLS_LOG_LEVEL** | Sets logging level | [Logging](./core/logger.md){target="_blank"} | `INFO` | +| __POWERTOOLS_SERVICE_NAME__ | Sets service name used for tracing namespace, metrics dimension and structured logging | All | `"service_undefined"` | +| __POWERTOOLS_METRICS_NAMESPACE__ | Sets namespace used for metrics | [Metrics](./core/metrics.md){target="_blank"} | `None` | +| __POWERTOOLS_TRACE_DISABLED__ | Explicitly disables tracing | [Tracing](./core/tracer.md){target="_blank"} | `false` | +| __POWERTOOLS_TRACER_CAPTURE_RESPONSE__ | Captures Lambda or method return as metadata. | [Tracing](./core/tracer.md){target="_blank"} | `true` | +| __POWERTOOLS_TRACER_CAPTURE_ERROR__ | Captures Lambda or method exception as metadata. | [Tracing](./core/tracer.md){target="_blank"} | `true` | +| __POWERTOOLS_TRACE_MIDDLEWARES__ | Creates sub-segment for each custom middleware | [Middleware factory](./utilities/middleware_factory.md){target="_blank"} | `false` | +| __POWERTOOLS_LOGGER_LOG_EVENT__ | Logs incoming event | [Logging](./core/logger.md){target="_blank"} | `false` | +| __POWERTOOLS_LOGGER_SAMPLE_RATE__ | Debug log sampling | [Logging](./core/logger.md){target="_blank"} | `0` | +| __POWERTOOLS_LOG_DEDUPLICATION_DISABLED__ | Disables log deduplication filter protection to use Pytest Live Log feature | [Logging](./core/logger.md){target="_blank"} | `false` | +| __POWERTOOLS_PARAMETERS_MAX_AGE__ | Adjust how long values are kept in cache (in seconds) | [Parameters](./utilities/parameters.md#adjusting-cache-ttl){target="_blank"} | `5` | +| __POWERTOOLS_PARAMETERS_SSM_DECRYPT__ | Sets whether to decrypt or not values retrieved from AWS SSM Parameters Store | [Parameters](./utilities/parameters.md#ssmprovider){target="_blank"} | `false` | +| __POWERTOOLS_DEV__ | Increases verbosity across utilities | Multiple; see [POWERTOOLS_DEV effect below](#optimizing-for-non-production-environments) | `false` | +| __POWERTOOLS_LOG_LEVEL__ | Sets logging level | [Logging](./core/logger.md){target="_blank"} | `INFO` | ### Optimizing for non-production environments -Whether you're prototyping locally or against a non-production environment, you can use `POWERTOOLS_DEV` to increase verbosity across multiple utilities. +!!! info "We will emit a warning when this feature is used to help you detect misuse in production." -???+ info - We will emit a warning when `POWERTOOLS_DEV` is enabled to help you detect misuse in production environments. +Whether you're prototyping locally or against a non-production environment, you can use `POWERTOOLS_DEV` to increase verbosity across multiple utilities. When `POWERTOOLS_DEV` is set to a truthy value (`1`, `true`), it'll have the following effects: -| Utility | Effect | -| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **Logger** | Increase JSON indentation to 4. This will ease local debugging when running functions locally under emulators or direct calls while not affecting unit tests | -| **Event Handler** | Enable full traceback errors in the response, indent request/responses, and CORS in dev mode (`*`). | -| **Tracer** | Future-proof safety to disables tracing operations in non-Lambda environments. This already happens automatically in the Tracer utility. | +| Utility | Effect | +| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| __Logger__ | Increase JSON indentation to 4. This will ease local debugging when running functions locally under emulators or direct calls while not affecting unit tests.

However, Amazon CloudWatch Logs view will degrade as each new line is treated as a new message. | +| __Event Handler__ | Enable full traceback errors in the response, indent request/responses, and CORS in dev mode (`*`). | +| __Tracer__ | Future-proof safety to disables tracing operations in non-Lambda environments. This already happens automatically in the Tracer utility. | ## Debug mode @@ -747,42 +752,98 @@ As a best practice for libraries, Powertools module logging statements are suppr When necessary, you can use `POWERTOOLS_DEBUG` environment variable to enable debugging. This will provide additional information on every internal operation. -## How to support Powertools for AWS Lambda (Python)? +## Support Powertools for AWS Lambda (Python) + +There are many ways you can help us gain future investments to improve everyone's experience: + +
+ +- :heart:{ .lg .middle } __Become a public reference__ + + --- + + Add your company name and logo on our [landing page](https://powertools.aws.dev). + + [:octicons-arrow-right-24: GitHub Issue template]((https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=customer-reference&template=support_powertools.yml&title=%5BSupport+Lambda+Powertools%5D%3A+%3Cyour+organization+name%3E){target="_blank"}) + +- :mega:{ .lg .middle } __Share your work__ + + --- + + Blog posts, video, and sample projects about Powertools for AWS Lambda. + + [:octicons-arrow-right-24: GitHub Issue template](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=community-content&template=share_your_work.yml&title=%5BI+Made+This%5D%3A+%3CTITLE%3E){target="_blank"} + +- :partying_face:{ .lg .middle } __Join the community__ + + --- + + Connect, ask questions, and share what features you use. + + [:octicons-arrow-right-24: Discord invite](https://discord.gg/B8zZKbbyET){target="blank"} + +
### Becoming a reference customer -Knowing which companies are using this library is important to help prioritize the project internally. If your company is using Powertools for AWS Lambda (Python), you can request to have your name and logo added to the README file by raising a [Support Powertools for AWS Lambda (Python) (become a reference)](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=customer-reference&template=support_powertools.yml&title=%5BSupport+Lambda+Powertools%5D%3A+%3Cyour+organization+name%3E){target="_blank"} issue. +Knowing which companies are using this library is important to help prioritize the project internally. The following companies, among others, use Powertools: + +
+ +[**Capital One**](https://www.capitalone.com/){target="_blank" rel="nofollow"} +{ .card } + +[**CPQi (Exadel Financial Services)**](https://cpqi.com/){target="_blank" rel="nofollow"} +{ .card } + +[**CloudZero**](https://www.cloudzero.com/){target="_blank" rel="nofollow"} +{ .card } + +[**CyberArk**](https://www.cyberark.com/){target="_blank" rel="nofollow"} +{ .card } + +[**globaldatanet**](https://globaldatanet.com/){target="_blank" rel="nofollow"} +{ .card } + +[**IMS**](https://ims.tech/){target="_blank" rel="nofollow"} +{ .card } + +[**Jit Security**](https://www.jit.io/){target="_blank" rel="nofollow"} +{ .card } + +[**Propellor.ai**](https://www.propellor.ai/){target="_blank" rel="nofollow"} +{ .card } + +[**TopSport**](https://www.topsport.com.au/){target="_blank" rel="nofollow"} +{ .card } + +[**Transformity**](https://transformity.tech/){target="_blank" rel="nofollow"} +{ .card } + +[**Trek10**](https://www.trek10.com/){target="_blank" rel="nofollow"} +{ .card } -The following companies, among others, use Powertools: +[**Vertex Pharmaceuticals**](https://www.vrtx.com/){target="_blank" rel="nofollow"} +{ .card } -* [Capital One](https://www.capitalone.com/){target="_blank" rel="nofollow"} -* [CPQi (Exadel Financial Services)](https://cpqi.com/){target="_blank" rel="nofollow"} -* [CloudZero](https://www.cloudzero.com/){target="_blank" rel="nofollow"} -* [CyberArk](https://www.cyberark.com/){target="_blank" rel="nofollow"} -* [globaldatanet](https://globaldatanet.com/){target="_blank" rel="nofollow"} -* [IMS](https://ims.tech/){target="_blank" rel="nofollow"} -* [Jit Security](https://www.jit.io/){target="_blank" rel="nofollow"} -* [Propellor.ai](https://www.propellor.ai/){target="_blank" rel="nofollow"} -* [TopSport](https://www.topsport.com.au/){target="_blank" rel="nofollow"} -* [Transformity](https://transformity.tech/){target="_blank" rel="nofollow"} -* [Trek10](https://www.trek10.com/){target="_blank" rel="nofollow"} -* [Vertex Pharmaceuticals](https://www.vrtx.com/){target="_blank" rel="nofollow"} +[**Alma Media**](https://www.almamedia.fi/en/){target="_blank" rel="nofollow} +{ .card } -### Sharing your work +
-Share what you did with Powertools for AWS Lambda (Python) πŸ’žπŸ’ž. Blog post, workshops, presentation, sample apps and others. Check out what the community has already shared about Powertools for AWS Lambda (Python) [here](https://docs.powertools.aws.dev/lambda/python/latest/we_made_this/){target="_blank"}. +### Using Lambda Layers -### Using Lambda Layer or SAR +!!! note "Layers help us understand who uses Powertools for AWS Lambda (Python) in a non-intrusive way." -This helps us understand who uses Powertools for AWS Lambda (Python) in a non-intrusive way, and helps us gain future investments for other Powertools for AWS Lambda languages. When [using Layers](https://docs.powertools.aws.dev/lambda/python/latest/#lambda-layer), you can add Powertools for AWS Lambda (Python) as a dev dependency (or as part of your virtual env) to not impact the development process. +When [using Layers](#lambda-layer), you can add Powertools for AWS Lambda (Python) as a dev dependency to not impact the development process. For Layers, we pre-package all dependencies, compile and optimize for storage and both x86 and ARM architecture. ## Tenets These are our core principles to guide our decision making. -* **AWS Lambda only**. We optimise for AWS Lambda function environments and supported runtimes only. Utilities might work with web frameworks and non-Lambda environments, though they are not officially supported. -* **Eases the adoption of best practices**. The main priority of the utilities is to facilitate best practices adoption, as defined in the AWS Well-Architected Serverless Lens; all other functionality is optional. -* **Keep it lean**. Additional dependencies are carefully considered for security and ease of maintenance, and prevent negatively impacting startup time. -* **We strive for backwards compatibility**. New features and changes should keep backwards compatibility. If a breaking change cannot be avoided, the deprecation and migration process should be clearly defined. -* **We work backwards from the community**. We aim to strike a balance of what would work best for 80% of customers. Emerging practices are considered and discussed via Requests for Comment (RFCs) -* **Progressive**. Utilities are designed to be incrementally adoptable for customers at any stage of their Serverless journey. They follow language idioms and their community’s common practices. +- __AWS Lambda only__. We optimise for AWS Lambda function environments and supported runtimes only. Utilities might work with web frameworks and non-Lambda environments, though they are not officially supported. +- __Eases the adoption of best practices__. The main priority of the utilities is to facilitate best practices adoption, as defined in the AWS Well-Architected Serverless Lens; all other functionality is optional. +- __Keep it lean__. Additional dependencies are carefully considered for security and ease of maintenance, and prevent negatively impacting startup time. +- __We strive for backwards compatibility__. New features and changes should keep backwards compatibility. If a breaking change cannot be avoided, the deprecation and migration process should be clearly defined. +- __We work backwards from the community__. We aim to strike a balance of what would work best for 80% of customers. Emerging practices are considered and discussed via Requests for Comment (RFCs) +- __Progressive__. Utilities are designed to be incrementally adoptable for customers at any stage of their Serverless journey. They follow language idioms and their community’s common practices. diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 44935986883..0af326afb24 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,10 +1,5 @@ {% extends "base.html" %} -{% block announce %} -

🚨 As of February 8, 2024, AWS Lambda will no longer allow Python 3.7 functions to be updated. Inline with this, Powertools releases will stop supporting it.

-

Please ensure you update your functions to Python 3.8 or later to continue to use the latest version of Powertools for AWS Lambda (Python).

-{% endblock %} - {% block outdated %} You're not viewing the latest version. diff --git a/docs/roadmap.md b/docs/roadmap.md index 766733c754c..6395bc344de 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -2,70 +2,66 @@ ## Overview -Our public roadmap outlines the high level direction we are working towards, namely [Themes](#themes). We update this document when our priorities change: security and stability is our top priority. +Our public roadmap outlines the high level direction we are working towards. We update this document when our priorities change: security and stability are our top priority. -!!! info "For most up-to-date information, see our [board of activities](https://github.com/orgs/aws-powertools/projects/3/views/2?query=is%3Aopen+sort%3Aupdated-desc){target="_blank"}." +!!! info "See our [current iteration cycle](https://github.com/orgs/aws-powertools/projects/3/views/14?query=is%3Aopen+sort%3Aupdated-desc){target="_blank"} for the most up-to-date information." -## Themes +## Key areas -Operational Excellence is priority number 1. This means bug fixing, stability, security, customer's support, and governance will take precedence above all else. +Security and operational excellence take precedence above all else. This means bug fixing, stability, customer's support, and internal compliance may delay one or more key areas below. -**What are themes?** +### Amazon Bedrock Agent Event Handler -They are key activities maintainers are focusing on. These are updated periodically and you can find the latest [under Themes in our public board](https://github.com/orgs/aws-powertools/projects/3/views/11?query=is%3Aopen+sort%3Aupdated-desc){target="_blank"}. - -### Observability providers - -We want to extend Tracer, Metrics, and Logger to support any [AWS Lambda certified observability partner](https://go.aws/3HtU6CZ){target="_blank"}, along with OpenTelemetry. - -At launch, we will support Datadog since it's [most requested observability provider](https://github.com/aws-powertools/powertools-lambda-python/issues/1433). OpenTelemetry will be a fast follow-up as we need to decide on a stable solution to cold start penalty. - -!!! tip "Help us identify which observability providers we should integrate next. Open [feature request](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=feature-request%2Ctriage&projects=&template=feature_request.yml&title=Feature+request%3A+TITLE){target="_blank"} or by voting `+1` in existing issues" +Based on [customers](https://github.com/aws-powertools/powertools-lambda-python#connect){target="_blank"} at re:Invent 2023, we will add a new Event Handler resolver to improve authoring and maintenance of Amazon Bedrock Agents. **Major updates** -- [x] [Document how customers can use any provider with Logger](https://docs.powertools.aws.dev/lambda/python/latest/core/logger/#observability-providers) -- [x] [Extend Metrics to add support for any Provider](https://github.com/aws-powertools/powertools-lambda-python/pull/2194) -- [ ] [Extend Tracer to add support for any Provider](https://github.com/aws-powertools/powertools-lambda-python/issues/2030) -- [ ] Investigate alternative solution to OpenTelemetry cold start performance +* [x] [Event Source Data Classes support](https://github.com/aws-powertools/powertools-lambda-python/pull/3262) +* [x] [Pydantic model _(Parser)_ support](https://github.com/aws-powertools/powertools-lambda-python/pull/3286) +* [x] [MVP Event Handler](https://github.com/aws-powertools/powertools-lambda-python/pull/3285) +* [ ] [New feature documentation](https://github.com/aws-powertools/powertools-lambda-python/pull/3602) +* [ ] Video to walkthrough use cases for anyone new to LLM Agents +* [ ] Launch amplifier (_e.g., What's New, Blog post_) -### Sensitive Data Masking +### Setting Parameters and Secrets -Data Masking will be a new utility to mask/unmask sensitive data using encryption providers. It's the second most voted feature request (behind [Observability Providers](#observability-providers)). +As of today, the [Parameters](./utilities/parameters.md){target="_blank"} feature is used to retrieve data, not to create or update existing parameters. Based on community feedback, we plan to enhance Parameters to allow set operations. **Major updates** -- [x] [RFC to agree on design and MVP](https://github.com/aws-powertools/powertools-lambda-python/issues/1858) -- [x] [POC with AWS KMS as the default provider](https://github.com/aws-powertools/powertools-lambda-python/pull/2197) -- [ ] User-guide documentation and include when not to use it (e.g., when to use SNS data policy, CloudWatch Logs data policy) -- [ ] Decide whether to use Encryption SDK to bring their own provider or a simply a contract (e.g., `ItsDangerous`) +* [x] [RFC](https://github.com/aws-powertools/powertools-lambda-python/issues/3040) +* [ ] [MVP](https://github.com/aws-powertools/powertools-lambda-python/pull/2858) -### Revamp Event Handler +### Observability providers -Event Handler provides lightweight routing for both [**REST**: Amazon API Gateway, Amazon Elastic Load Balancer and AWS Lambda Function URL](./core/event_handler/api_gateway.md), and [**GraphQL**: AWS AppSync](./core/event_handler/appsync.md). +We want to extend Tracer, Metrics, and Logger to support any [AWS Lambda certified observability partner](https://go.aws/3HtU6CZ){target="_blank"}, along with OpenTelemetry. -Based on customers feedback, we want to provide middleware authoring support for cross-cutting concerns. For REST APIs, we are also looking into auto-generate OpenAPI Schemas and a SwaggerUI route. For GraphQL, we are working on supporting batch invocations (N+1 problem) along with partial failure support. +At launch, we will support Datadog since it's [most requested observability provider](https://github.com/aws-powertools/powertools-lambda-python/issues/1433). OpenTelemetry will be a fast follow-up as we need to decide on a stable solution to cold start penalty. -**Major updates** +!!! tip "Help us identify which observability providers we should integrate next. Open [feature request](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=feature-request%2Ctriage&projects=&template=feature_request.yml&title=Feature+request%3A+TITLE){target="_blank"} or by voting `+1` in existing issues" -- [x] [Agree on experience for middleware support](https://github.com/aws-powertools/powertools-lambda-python/issues/953#issuecomment-1450223155) -- [x] [RFC to outline initial thoughts on OpenAPI integration](https://github.com/aws-powertools/powertools-lambda-python/issues/2421) -- [x] [MVP for REST middleware](./core/event_handler/api_gateway.md#middleware) -- [ ] [MVP for OpenAPI and SwaggerUI](https://github.com/aws-powertools/powertools-lambda-python/pull/3109) -- [ ] [MVP for AppSync Batch invoke and partial failure support](https://github.com/aws-powertools/powertools-lambda-python/pull/1998) +**Major updates** -### Lambda Layer in release notes +* [x] [Document how customers can use any provider with Logger](https://docs.powertools.aws.dev/lambda/python/latest/core/logger/#observability-providers) +* [x] [Extend Metrics to add support for any Provider](https://github.com/aws-powertools/powertools-lambda-python/pull/2194) +* [ ] [Extend Tracer to add support for any Provider](https://github.com/aws-powertools/powertools-lambda-python/issues/2030) +* [ ] Investigate alternative solution to OpenTelemetry cold start performance -We want to publish a JSON with a map of region and Lambda Layer ARN as a GitHub Release Note asset. +### Revamp Event Handler -As of V2, we prioritize Lambda Layers being available before release notes are out. This is due to X86 and ARM64 compilation for smaller binaries and extra speed. +Event Handler provides lightweight routing for both [**REST**: Amazon API Gateway, Amazon Elastic Load Balancer and AWS Lambda Function URL](./core/event_handler/api_gateway.md), and [**GraphQL**: AWS AppSync](./core/event_handler/appsync.md). -This means we have room to include a JSON map for Lambda Layers and facilitate automation for customers wanting the latest version as soon as it's available. + +Based on customers feedback, we want to provide [middleware authoring support](https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/#middleware) for cross-cutting concerns. For REST APIs, we are also looking into auto-generate [OpenAPI Schemas](https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/#data-validation) and a [SwaggerUI route](https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/#enabling-swaggerui). For GraphQL, we are working on supporting batch invocations (N+1 problem) along with partial failure support. + **Major updates** -- [x] Create secure mechanism to upload signed assets to GitHub Release Notes -- [ ] Create feature request to agree on JSON structure and asset name +* [x] [Agree on experience for middleware support](https://github.com/aws-powertools/powertools-lambda-python/issues/953#issuecomment-1450223155) +* [x] [RFC to outline initial thoughts on OpenAPI integration](https://github.com/aws-powertools/powertools-lambda-python/issues/2421) +* [x] [MVP for REST middleware](./core/event_handler/api_gateway.md#middleware) +* [x] [MVP for OpenAPI and SwaggerUI](https://github.com/aws-powertools/powertools-lambda-python/pull/3109) +* [ ] [MVP for AppSync Batch invoke and partial failure support](https://github.com/aws-powertools/powertools-lambda-python/pull/1998) ### Office hours @@ -77,7 +73,11 @@ Timezones being tricky, we plan to experiment with an afternoon slot in Central **Major updates** -- [ ] Decide whether to use Amazon Chime or Zoom (we had audio setup issues on Discord) +* [x] Decide whether to use Amazon Chime or Zoom (we had audio setup issues on Discord) +* [ ] Experiment running monthly roadmap review as an open call + * [ ] Settle on monthly roadmap review agenda + * [ ] Invite Discord community + * [ ] Update roadmap page with Discord event ### Authentication (SigV4) @@ -87,8 +87,8 @@ Since JWT is a close second, this new utility would cover higher level functions **Major updates** -- [ ] RFC to outline challenges, alternative solutions and desired experience -- [ ] MVP based off RFC +* [ ] RFC to outline challenges, alternative solutions and desired experience +* [ ] [MVP for AWS SigV4](https://github.com/aws-powertools/powertools-lambda-python/pull/2435) ### Enhanced operational metrics @@ -100,43 +100,45 @@ We want to make this easier by extending certain utilities to accept a `metrics` **Major updates** -- [ ] RFC to outline metrics for Batch (_e.g., Failed items, Batch size_) -- [ ] RFC to outline metrics for Feature flags (_e.g., matched rules_) -- [ ] RFC to outline metrics for Event Handler (_e.g., validation errors_ ) -- [ ] RFC to outline metrics for Idempotency (_e.g., cache hit_) +* [ ] RFC to outline metrics for Batch (_e.g., Failed items, Batch size_) +* [ ] RFC to outline metrics for Feature flags (_e.g., matched rules_) +* [ ] RFC to outline metrics for Event Handler (_e.g., validation errors_ ) +* [ ] RFC to outline metrics for Idempotency (_e.g., cache hit_) ### Lambda Layer in GovCloud and China region We want to investigate security and scaling requirements for these special regions, so they're in sync for every release. -!!! note "Help us prioritize it by reaching out to your AWS representatives or [via email](mailto:aws-lambda-powertools-feedback@amazon.com)." +!!! note "Help us prioritize it by reaching out to your AWS representatives or [via email](mailto:aws-powertools-maintainers@amazon.com)." **Major updates** -- [x] Gather agencies and customers name to prioritize it -- [x] Investigate security requirements for special regions -- [ ] Create additional infrastructure for special regions -- [ ] Update CDK Layer construct to include regions +* [x] Gather agencies and customers name to prioritize it +* [x] Investigate security requirements for special regions +* [x] Create additional infrastructure for special regions +* [ ] AppSec review +* [ ] Distribution sign-off +* [ ] Update CDK Layer construct to include regions ### V3 -We are in the process of planning the roadmap for v3. As always, our approach includes providing sufficient advance notice, a comprehensive upgrade guide, and minimizing breaking changes to facilitate a smooth transition (e.g., it took ~7 months from v2 to surpass v1 downloads). +We are in the process of planning the roadmap for v3. As always, [our approach](./versioning.md){target="_blank"} includes providing sufficient advance notice, a comprehensive upgrade guide, and minimizing breaking changes to facilitate a smooth transition (e.g., it took ~7 months from v2 to surpass v1 downloads). For example, these are on our mind but not settled yet until we have a public tracker to discuss what these means in detail. -- **Parser**: Drop Pydantic v1 -- **Parser**: Deserialize Amazon DynamoDB data types automatically (like Event Source Data Classes) -- **Parameters**: Increase default `max_age` for `get_secret` -- **Event Source Data Classes**: Return sane defaults for any property that has `Optional[]` returns -- **Upgrade tool**: Consider building a CST (Concrete Syntax Tree) tool to ease certain upgrade actions like `pyupgrade` and `django-upgrade` -- **Batch**: Stop at first error for Amazon DynamoDB Streams and Amazon Kinesis Data Streams (e.g., `stop_on_failure=True`) +* **Parser**: Drop Pydantic v1 +* **Parser**: Deserialize Amazon DynamoDB data types automatically (like Event Source Data Classes) +* **Parameters**: Increase default `max_age` for `get_secret` +* **Event Source Data Classes**: Return sane defaults for any property that has `Optional[]` returns +* **Upgrade tool**: Consider building a CST (Concrete Syntax Tree) tool to ease certain upgrade actions like `pyupgrade` and `django-upgrade` +* **Batch**: Stop at first error for Amazon DynamoDB Streams and Amazon Kinesis Data Streams (e.g., `stop_on_failure=True`) **Major updates** -- [ ] Create an issue to track breaking changes we consider making -- [ ] Create a v3 branch to allow early experimentation -- [ ] Create workflows to allow pre-releases -- [ ] Create a mechanism to keep ideas for breaking change somewhere regardless of v3 +* [ ] Create an issue to track breaking changes we consider making +* [ ] Create a v3 branch to allow early experimentation +* [ ] Create workflows to allow pre-releases +* [ ] Create a mechanism to keep ideas for breaking change somewhere regardless of v3 ## Roadmap status definition @@ -150,11 +152,11 @@ graph LR Within our [public board](https://github.com/orgs/aws-powertools/projects/3/views/1?query=is%3Aopen+sort%3Aupdated-desc){target="_blank"}, you'll see the following values in the `Status` column: -- **Ideas**. Incoming and existing feature requests that are not being actively considered yet. These will be reviewed when bandwidth permits. -- **Backlog**. Accepted feature requests or enhancements that we want to work on. -- **Working on it**. Features or enhancements we're currently either researching or implementing it. -- **Coming soon**. Any feature, enhancement, or bug fixes that have been merged and are coming in the next release. -- **Shipped**. Features or enhancements that are now available in the most recent release. +* **Ideas**. Incoming and existing feature requests that are not being actively considered yet. These will be reviewed when bandwidth permits. +* **Backlog**. Accepted feature requests or enhancements that we want to work on. +* **Working on it**. Features or enhancements we're currently either researching or implementing it. +* **Coming soon**. Any feature, enhancement, or bug fixes that have been merged and are coming in the next release. +* **Shipped**. Features or enhancements that are now available in the most recent release. > Tasks or issues with empty `Status` will be categorized in upcoming review cycles. @@ -176,12 +178,12 @@ graph LR Our end-to-end mechanism follows four major steps: -- **Feature Request**. Ideas start with a [feature request](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=feature-request%2Ctriage&template=feature_request.yml&title=Feature+request%3A+TITLE){target="_blank"} to outline their use case at a high level. For complex use cases, maintainers might ask for/write a RFC. - - Maintainers review requests based on [project tenets](index.md#tenets){target="_blank"}, customers reaction (πŸ‘), and use cases. -- **Request-for-comments (RFC)**. Design proposals use our [RFC issue template](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=RFC%2Ctriage&template=rfc.yml&title=RFC%3A+TITLE){target="_blank"} to describe its implementation, challenges, developer experience, dependencies, and alternative solutions. - - This helps refine the initial idea with community feedback before a decision is made. -- **Decision**. After carefully reviewing and discussing them, maintainers make a final decision on whether to start implementation, defer or reject it, and update everyone with the next steps. -- **Implementation**. For approved features, maintainers give priority to the original authors for implementation unless it is a sensitive task that is best handled by maintainers. +* **Feature Request**. Ideas start with a [feature request](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=feature-request%2Ctriage&template=feature_request.yml&title=Feature+request%3A+TITLE){target="_blank"} to outline their use case at a high level. For complex use cases, maintainers might ask for/write a RFC. + * Maintainers review requests based on [project tenets](index.md#tenets){target="_blank"}, customers reaction (πŸ‘), and use cases. +* **Request-for-comments (RFC)**. Design proposals use our [RFC issue template](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=RFC%2Ctriage&template=rfc.yml&title=RFC%3A+TITLE){target="_blank"} to describe its implementation, challenges, developer experience, dependencies, and alternative solutions. + * This helps refine the initial idea with community feedback before a decision is made. +* **Decision**. After carefully reviewing and discussing them, maintainers make a final decision on whether to start implementation, defer or reject it, and update everyone with the next steps. +* **Implementation**. For approved features, maintainers give priority to the original authors for implementation unless it is a sensitive task that is best handled by maintainers. ???+ info "See [Maintainers](https://github.com/aws-powertools/powertools-lambda-python/blob/develop/MAINTAINERS.md){target="_blank"} document to understand how we triage issues and pull requests, labels and governance." @@ -204,3 +206,45 @@ A: Because job zero is security and operational stability, we can't provide spec **Q: How can I provide feedback or ask for more information?** A: For existing features, you can directly comment on issues. For anything else, please open an issue. + +## Launched + +### Sensitive Data Masking + +> [Docs](./utilities/data_masking.md) + +Data Masking will be a new utility to mask/unmask sensitive data using encryption providers. It's the second most voted feature request (behind [Observability Providers](#observability-providers)). + +**Major updates** + +* [x] [RFC to agree on design and MVP](https://github.com/aws-powertools/powertools-lambda-python/issues/1858) +* [x] [POC with AWS KMS as the default provider](https://github.com/aws-powertools/powertools-lambda-python/pull/2197) +* [x] User-guide documentation and include when not to use it (e.g., when to use SNS data policy, CloudWatch Logs data policy) +* [x] Decide whether to use Encryption SDK to bring their own provider or a simply a contract (e.g., `ItsDangerous`) + +### Deprecate Python 3.7 support + +AWS Lambda will officially block updates to Lambda functions using Python 3.7 support. We will drop support as soon as [that is official](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html#runtime-support-policy){target="_blank"}. + +**Major updates** + +* [x] [Drop Python 3.7 support](https://github.com/aws-powertools/powertools-lambda-python/pull/3638) +* [x] [Add documentation banner](https://github.com/aws-powertools/powertools-lambda-python/pull/3618) +* [x] [Publish versioning policy docs](https://github.com/aws-powertools/powertools-lambda-python/pull/3682) + +## Dropped + +### Lambda Layer in release notes + +> **Reason**: We are looking at more accessible alternatives based on customer feedback (e.g., AWS System Manager public parameters) + +We want to publish a JSON with a map of region and Lambda Layer ARN as a GitHub Release Note asset. + +As of V2, we prioritize Lambda Layers being available before release notes are out. This is due to X86 and ARM64 compilation for smaller binaries and extra speed. + +This means we have room to include a JSON map for Lambda Layers and facilitate automation for customers wanting the latest version as soon as it's available. + +**Major updates** + +* [x] Create secure mechanism to upload signed assets to GitHub Release Notes +* [ ] Create feature request to agree on JSON structure and asset name diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index 5442a213562..c5acf22cead 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -1045,4 +1045,4 @@ This requires a change in mindset to ensure operational excellence is part of th Powertools for AWS Lambda (Python) is largely designed to make some of these practices easier to adopt from day 1. ???+ question "Have ideas for other tutorials?" - You can open up a [documentation issue](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=documentation&template=documentation-improvements.md&title=Tutorial%20Suggestion){target="_blank"}, or via e-mail [aws-lambda-powertools-feedback@amazon.com](mailto:aws-lambda-powertools-feedback@amazon.com). + You can open up a [documentation issue](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=documentation&template=documentation-improvements.md&title=Tutorial%20Suggestion){target="_blank"}, or via e-mail [aws-powertools-maintainers@amazon.com](mailto:aws-powertools-maintainers@amazon.com). diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index b68fcc594fb..57069681a72 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -178,7 +178,7 @@ You can use `get_enabled_features` method for scenarios where you need a list of === "getting_all_enabled_features.py" - ```python hl_lines="2 9 26" + ```python hl_lines="4 9 11 28" --8<-- "examples/feature_flags/src/getting_all_enabled_features.py" ``` @@ -431,10 +431,13 @@ The `action` configuration can have the following values, where the expressions | **ENDSWITH** | `lambda a, b: a.endswith(b)` | | **KEY_IN_VALUE** | `lambda a, b: a in b` | | **KEY_NOT_IN_VALUE** | `lambda a, b: a not in b` | +| **ANY_IN_VALUE** | `lambda a, b: any of a is in b` | +| **ALL_IN_VALUE** | `lambda a, b: all of a is in b` | +| **NONE_IN_VALUE** | `lambda a, b: none of a is in b` | | **VALUE_IN_KEY** | `lambda a, b: b in a` | | **VALUE_NOT_IN_KEY** | `lambda a, b: b not in a` | -| **SCHEDULE_BETWEEN_TIME_RANGE** | `lambda a, b: b.start <= time(a) <= b.end` | -| **SCHEDULE_BETWEEN_DATETIME_RANGE** | `lambda a, b: b.start <= datetime(a) <= b.end` | +| **SCHEDULE_BETWEEN_TIME_RANGE** | `lambda a, b: b.start <= time(a) <= b.end` | +| **SCHEDULE_BETWEEN_DATETIME_RANGE** | `lambda a, b: b.start <= datetime(a) <= b.end` | | **SCHEDULE_BETWEEN_DAYS_OF_WEEK** | `lambda a, b: day_of_week(a) in b` | | **MODULO_RANGE** | `lambda a, b: b.start <= a % b.base <= b.end` | @@ -469,7 +472,7 @@ For this to work, you need to use a JMESPath expression via the `envelope` param === "extracting_envelope.py" - ```python hl_lines="7" + ```python hl_lines="10" --8<-- "examples/feature_flags/src/extracting_envelope.py" ``` diff --git a/docs/we_made_this.md b/docs/we_made_this.md index d57a0b7325c..ea51f65c3d2 100644 --- a/docs/we_made_this.md +++ b/docs/we_made_this.md @@ -39,6 +39,8 @@ A collection of articles explaining in detail how Lambda Powertools helps with a * [Effective Amazon SQS Batch Handling with Powertools for AWS Lambda (Python)](https://www.ranthebuilder.cloud/post/effective-amazon-sqs-batch-handling-with-aws-lambda-powertools){:target="_blank"} +* [Serverless API Documentation with Powertools for AWS](https://www.ranthebuilder.cloud/post/serverless-open-api-documentation-with-aws-powertools){:target="_blank"} + ### Making all your APIs idempotent > **Author: [Michael Walmsley](https://twitter.com/walmsles){target="_blank" rel="nofollow"}** :material-twitter: @@ -140,7 +142,17 @@ Feature flags can improve your CI/CD process by enabling capabilities otherwise In this talk, you will learn the added value of using feature flags as part of your CI/CD process and how AWS Lambda Powertools can help with that. - +#### AWS re:invent 2023 - OPN305 - The Pragmatic Serverless Python Developer + +> **Author: Heitor Lessa & Ran Isenberg** + +Are you developing AWS Lambda functions with Python? Always looking for tools to make you more productive? What if you could hear directly from practitioners? + +This session covers an opinionated approach to Python project setup, testing, profiling, deployments, and operations. Learn about many open source tools, including Powertools for AWS Lambdaβ€”a toolkit that can help you implement serverless best practices and increase developer velocity. + +Join to discover tools and patterns for effective serverless development with Python. To maximize your learning experience, the session includes a sample application that implements what’s described. + + ## Workshops @@ -170,6 +182,15 @@ This handler embodies Serverless best practices and has all the bells and whistl :material-github: [github.com/ran-isenberg/aws-lambda-handler-cookbook](https://github.com/ran-isenberg/aws-lambda-handler-cookbook){:target="_blank"} +> **Author: [Ran Isenberg & Heitor Lessa](mailto:ran.isenberg@ranthebuilder.cloud) [:material-twitter:](https://twitter.com/IsenbergRan){target="_blank" rel="nofollow"} [:material-linkedin:](https://www.linkedin.com/in/ranisenberg/){target="_blank" rel="nofollow"}** + +This project covers an opinionated approach to Python project setup, testing, profiling, deployments, and operations. Learn about many open source tools, including Powertools for AWS Lambdaβ€”a toolkit that can help you implement serverless best practices and increase developer velocity. + +It is based on the AWS Lambda handler cookbook project and served as the examples for the AWS re:invent 2023 +session: OPN305 - The pragmatic serverless python developer. + +:material-github: [https://github.com/ran-isenberg/serverless-python-demo](https://github.com/ran-isenberg/serverless-python-demo){:target="_blank"} + ### Serverless Transactional Message App > **Author: [Santiago Garcia Arango](mailto:san99tiago@gmail.com) [:material-web:](https://san99tiago.com/){target="_blank" rel="nofollow"} [:material-linkedin:](https://www.linkedin.com/in/san99tiago/){target="_blank" rel="nofollow"}** diff --git a/examples/feature_flags/src/extracting_envelope.py b/examples/feature_flags/src/extracting_envelope.py index 74111157704..44935314dd5 100644 --- a/examples/feature_flags/src/extracting_envelope.py +++ b/examples/feature_flags/src/extracting_envelope.py @@ -6,8 +6,8 @@ app_config = AppConfigStore( environment="dev", application="product-catalogue", - name="features", - envelope="feature_flags", + name="feature_flags", + envelope="features", ) feature_flags = FeatureFlags(store=app_config) diff --git a/examples/logger/sam/template.yaml b/examples/logger/sam/template.yaml index 02885ffb3bb..1728dd91e37 100644 --- a/examples/logger/sam/template.yaml +++ b/examples/logger/sam/template.yaml @@ -14,7 +14,7 @@ Globals: Layers: # Find the latest Layer version in the official documentation # https://docs.powertools.aws.dev/lambda/python/latest/#lambda-layer - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:61 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:62 Resources: LoggerLambdaHandlerExample: diff --git a/examples/metrics/sam/template.yaml b/examples/metrics/sam/template.yaml index 2b9e885f20d..d3c0bb3c720 100644 --- a/examples/metrics/sam/template.yaml +++ b/examples/metrics/sam/template.yaml @@ -15,7 +15,7 @@ Globals: Layers: # Find the latest Layer version in the official documentation # https://docs.powertools.aws.dev/lambda/python/latest/#lambda-layer - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:61 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:62 Resources: CaptureLambdaHandlerExample: diff --git a/examples/tracer/sam/template.yaml b/examples/tracer/sam/template.yaml index a7961c7c44b..5abd93f9713 100644 --- a/examples/tracer/sam/template.yaml +++ b/examples/tracer/sam/template.yaml @@ -13,7 +13,7 @@ Globals: Layers: # Find the latest Layer version in the official documentation # https://docs.powertools.aws.dev/lambda/python/latest/#lambda-layer - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:61 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:62 Resources: CaptureLambdaHandlerExample: diff --git a/layer/scripts/layer-balancer/go.mod b/layer/scripts/layer-balancer/go.mod index 4548c3f44f2..0f3727c91e3 100644 --- a/layer/scripts/layer-balancer/go.mod +++ b/layer/scripts/layer-balancer/go.mod @@ -3,25 +3,25 @@ module layerbalancer go 1.18 require ( - github.com/aws/aws-sdk-go-v2 v1.24.1 - github.com/aws/aws-sdk-go-v2/config v1.26.6 - github.com/aws/aws-sdk-go-v2/service/lambda v1.49.7 + github.com/aws/aws-sdk-go-v2 v1.25.0 + github.com/aws/aws-sdk-go-v2/config v1.27.1 + github.com/aws/aws-sdk-go-v2/service/lambda v1.52.0 golang.org/x/exp v0.0.0-20230321023759-10a507213a29 golang.org/x/sync v0.6.0 ) require ( - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect - github.com/aws/smithy-go v1.19.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.0 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.1 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.19.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.27.1 // indirect + github.com/aws/smithy-go v1.20.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect ) diff --git a/layer/scripts/layer-balancer/go.sum b/layer/scripts/layer-balancer/go.sum index ec29fc3ea00..fa8ccfa9f7f 100644 --- a/layer/scripts/layer-balancer/go.sum +++ b/layer/scripts/layer-balancer/go.sum @@ -1,33 +1,33 @@ -github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU= -github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo= -github.com/aws/aws-sdk-go-v2/config v1.26.6 h1:Z/7w9bUqlRI0FFQpetVuFYEsjzE3h7fpU6HuGmfPL/o= -github.com/aws/aws-sdk-go-v2/config v1.26.6/go.mod h1:uKU6cnDmYCvJ+pxO9S4cWDb2yWWIH5hra+32hVh1MI4= -github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8= -github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw= -github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 h1:n3GDfwqF2tzEkXlv5cuy4iy7LpKDtqDMcNLfZDu9rls= -github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino= -github.com/aws/aws-sdk-go-v2/service/lambda v1.49.7 h1:YCvhGwdiZ9tKTjoIOE8jLt+3JBK4quAQyhoMCWtxhQc= -github.com/aws/aws-sdk-go-v2/service/lambda v1.49.7/go.mod h1:xqjYGK1M7YTmyfZBW8LVAx7QnefUb/mE5BglUnxtx6E= -github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow= -github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8= -github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0= -github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U= -github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= -github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= +github.com/aws/aws-sdk-go-v2 v1.25.0 h1:sv7+1JVJxOu/dD/sz/csHX7jFqmP001TIY7aytBWDSQ= +github.com/aws/aws-sdk-go-v2 v1.25.0/go.mod h1:G104G1Aho5WqF+SR3mDIobTABQzpYV0WxMsKxlMggOA= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.0 h1:2UO6/nT1lCZq1LqM67Oa4tdgP1CvL1sLSxvuD+VrOeE= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.0/go.mod h1:5zGj2eA85ClyedTDK+Whsu+w9yimnVIZvhvBKrDquM8= +github.com/aws/aws-sdk-go-v2/config v1.27.1 h1:oxvGd/cielb+oumJkQmXI0i5tQCRqfdCHV58AfE0pGY= +github.com/aws/aws-sdk-go-v2/config v1.27.1/go.mod h1:SpmaZYWeTF91NQcnnp2AScnZawBWwdkYCupHRNIhVSQ= +github.com/aws/aws-sdk-go-v2/credentials v1.17.1 h1:H4WlK2OnVotRmbVgS8Ww2Z4B3/dDHxDS7cW6EiCECN4= +github.com/aws/aws-sdk-go-v2/credentials v1.17.1/go.mod h1:qTfT/OIE9RAVirZDq0PcEYOOM4Pkmf1Hrk1iInKRS4k= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0 h1:xWCwjjvVz2ojYTP4kBKUuUh9ZrXfcAXpflhOUUeXg1k= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0/go.mod h1:j3fACuqXg4oMTQOR2yY7m0NmJY0yBK4L4sLsRXq1Ins= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.0 h1:NPs/EqVO+ajwOoq56EfcGKa3L3ruWuazkIw1BqxwOPw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.0/go.mod h1:D+duLy2ylgatV+yTlQ8JTuLfDD0BnFvnQRc+o6tbZ4M= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.0 h1:ks7KGMVUMoDzcxNWUlEdI+/lokMFD136EL6DWmUOV80= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.0/go.mod h1:hL6BWM/d/qz113fVitZjbXR0E+RCTU1+x+1Idyn5NgE= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.0 h1:a33HuFlO0KsveiP90IUJh8Xr/cx9US2PqkSroaLc+o8= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.0/go.mod h1:SxIkWpByiGbhbHYTo9CMTUnx2G4p4ZQMrDPcRRy//1c= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.0 h1:SHN/umDLTmFTmYfI+gkanz6da3vK8Kvj/5wkqnTHbuA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.0/go.mod h1:l8gPU5RYGOFHJqWEpPMoRTP0VoaWQSkJdKo+hwWnnDA= +github.com/aws/aws-sdk-go-v2/service/lambda v1.52.0 h1:RYN5WmLgyqgexDSeRjNBczKF35ik4D4ydQwujTUn2I8= +github.com/aws/aws-sdk-go-v2/service/lambda v1.52.0/go.mod h1:yEO3Ejj0qBhdIDlRYQ8O9+gB5CAUKyaYYiFBkvGX8ZA= +github.com/aws/aws-sdk-go-v2/service/sso v1.19.1 h1:GokXLGW3JkH/XzEVp1jDVRxty1eNGB7emkjDG1qxGK8= +github.com/aws/aws-sdk-go-v2/service/sso v1.19.1/go.mod h1:YqbU3RS/pkDVu+v+Nwxvn0i1WB0HkNWEePWbmODEbbs= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.1 h1:2oxSGiYNxTHsuRuPD9McWvcvR6s61G3ssZLyQzcxQL0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.1/go.mod h1:olUAyg+FaoFaL/zFaeQQONjOZ9HXoxgvI/c7mQTYz7M= +github.com/aws/aws-sdk-go-v2/service/sts v1.27.1 h1:QFT2KUWaVwwGi5/2sQNBOViFpLSkZmiyiHUxE2k6sOU= +github.com/aws/aws-sdk-go-v2/service/sts v1.27.1/go.mod h1:nXfOBMWPokIbOY+Gi7a1psWMSvskUCemZzI+SMB7Akc= +github.com/aws/smithy-go v1.20.0 h1:6+kZsCXZwKxZS9RfISnPc4EXlHoyAkm2hPuM8X2BrrQ= +github.com/aws/smithy-go v1.20.0/go.mod h1:uo5RKksAl4PzhqaAbjd4rLgFoq5koTsQKYuGe7dklGc= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= diff --git a/mkdocs.yml b/mkdocs.yml index 50fe632539c..fc3373b5c98 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -87,6 +87,7 @@ theme: - navigation.tabs - content.code.annotate - content.code.copy + - content.tabs.link icon: repo: fontawesome/brands/github logo: media/aws-logo-light.svg @@ -98,6 +99,9 @@ markdown_extensions: - abbr - pymdownx.tabbed: alternate_style: true + slugify: !!python/object/apply:pymdownx.slugs.slugify + kwds: + case: lower - pymdownx.highlight: linenums: true - pymdownx.details @@ -112,9 +116,10 @@ markdown_extensions: permalink: true toc_depth: 4 - attr_list + - md_in_html - pymdownx.emoji: - emoji_index: !!python/name:materialx.emoji.twemoji - emoji_generator: !!python/name:materialx.emoji.to_svg + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.inlinehilite - pymdownx.superfences: custom_fences: diff --git a/package-lock.json b/package-lock.json index ed5b64eee4e..35371ce0b22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,13 +11,13 @@ "package-lock.json": "^1.0.0" }, "devDependencies": { - "aws-cdk": "^2.126.0" + "aws-cdk": "^2.128.0" } }, "node_modules/aws-cdk": { - "version": "2.126.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.126.0.tgz", - "integrity": "sha512-hEyy8UCEEUnkieH6JbJBN8XAbvuVZNdBmVQ8wHCqo8RSNqmpwM1qvLiyXV/2JvCqJJ0bl9uBiZ98Ytd5i3wW7g==", + "version": "2.128.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.128.0.tgz", + "integrity": "sha512-epOAr/0WKqmyaKqBc7N0Ky5++93pu+v6yVN9jNOa4JYkAkGbeTS3vR9bj/W0o94jnlgWevG3HNHr83jtRvw/4A==", "dev": true, "bin": { "cdk": "bin/cdk" diff --git a/package.json b/package.json index 130e74e8067..510bb9132ea 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "aws-lambda-powertools-python-e2e", "version": "1.0.0", "devDependencies": { - "aws-cdk": "^2.126.0" + "aws-cdk": "^2.128.0" }, "dependencies": { "package-lock.json": "^1.0.0" diff --git a/poetry.lock b/poetry.lock index 09329553a25..d2c07ddab5e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -158,13 +158,13 @@ typeguard = ">=2.13.3,<2.14.0" [[package]] name = "aws-cdk-lib" -version = "2.126.0" +version = "2.128.0" description = "Version 2 of the AWS Cloud Development Kit library" optional = false python-versions = "~=3.8" files = [ - {file = "aws-cdk-lib-2.126.0.tar.gz", hash = "sha256:291f9a15fb45f1461644ffa2ff360c8c2ed2797ac29082655252de291fa04333"}, - {file = "aws_cdk_lib-2.126.0-py3-none-any.whl", hash = "sha256:8e00c0a3bfe8f5c20d305d149df251b8fbf292aa7e74df0765ba47fdbbec82d6"}, + {file = "aws-cdk-lib-2.128.0.tar.gz", hash = "sha256:796459062daa0dbe0581925874db121d4c220295c6c35e73dedfe39e82ca301f"}, + {file = "aws_cdk_lib-2.128.0-py3-none-any.whl", hash = "sha256:49170b21cb738d30d67f7aa361b78ba3a8b711f8dd15523cbfe64710f9386553"}, ] [package.dependencies] @@ -485,17 +485,17 @@ pycparser = "*" [[package]] name = "cfn-lint" -version = "0.85.0" +version = "0.85.2" description = "Checks CloudFormation templates for practices and behaviour that could potentially be improved" optional = false python-versions = ">=3.8, <=4.0, !=4.0" files = [ - {file = "cfn-lint-0.85.0.tar.gz", hash = "sha256:64d6e8d85cdc573b61add78f9ff95a142a1834edb4793d1291551f6d953f73fe"}, - {file = "cfn_lint-0.85.0-py3-none-any.whl", hash = "sha256:e4849e1779bd1a9f4543617372708a20519b6d7cad5f980e20c6deaa227361a2"}, + {file = "cfn-lint-0.85.2.tar.gz", hash = "sha256:f8a5cc55daeaaa747b8d776dcf62fe1b6bfb8cb46ae60950cbe627601facccd7"}, + {file = "cfn_lint-0.85.2-py3-none-any.whl", hash = "sha256:e7a0aafb9ad93dbe5db54cbefca92a94f2d173309218273ef997ecb048125d89"}, ] [package.dependencies] -aws-sam-translator = ">=1.83.0" +aws-sam-translator = ">=1.84.0" jschema-to-python = ">=1.2.3,<1.3.0" jsonpatch = "*" jsonschema = ">=3.0,<5" @@ -659,63 +659,63 @@ typeguard = ">=2.13.3,<2.14.0" [[package]] name = "coverage" -version = "7.4.1" +version = "7.4.2" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, - {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, - {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, - {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, - {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, - {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, - {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, - {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, - {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, - {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, - {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, - {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, - {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, - {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, - {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, - {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, - {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, - {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, - {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, - {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, - {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, - {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, + {file = "coverage-7.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf54c3e089179d9d23900e3efc86d46e4431188d9a657f345410eecdd0151f50"}, + {file = "coverage-7.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fe6e43c8b510719b48af7db9631b5fbac910ade4bd90e6378c85ac5ac706382c"}, + {file = "coverage-7.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b98c89db1b150d851a7840142d60d01d07677a18f0f46836e691c38134ed18b"}, + {file = "coverage-7.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5f9683be6a5b19cd776ee4e2f2ffb411424819c69afab6b2db3a0a364ec6642"}, + {file = "coverage-7.4.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78cdcbf7b9cb83fe047ee09298e25b1cd1636824067166dc97ad0543b079d22f"}, + {file = "coverage-7.4.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2599972b21911111114100d362aea9e70a88b258400672626efa2b9e2179609c"}, + {file = "coverage-7.4.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ef00d31b7569ed3cb2036f26565f1984b9fc08541731ce01012b02a4c238bf03"}, + {file = "coverage-7.4.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:20a875bfd8c282985c4720c32aa05056f77a68e6d8bbc5fe8632c5860ee0b49b"}, + {file = "coverage-7.4.2-cp310-cp310-win32.whl", hash = "sha256:b3f2b1eb229f23c82898eedfc3296137cf1f16bb145ceab3edfd17cbde273fb7"}, + {file = "coverage-7.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7df95fdd1432a5d2675ce630fef5f239939e2b3610fe2f2b5bf21fa505256fa3"}, + {file = "coverage-7.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8ddbd158e069dded57738ea69b9744525181e99974c899b39f75b2b29a624e2"}, + {file = "coverage-7.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81a5fb41b0d24447a47543b749adc34d45a2cf77b48ca74e5bf3de60a7bd9edc"}, + {file = "coverage-7.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2412e98e70f16243be41d20836abd5f3f32edef07cbf8f407f1b6e1ceae783ac"}, + {file = "coverage-7.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb79414c15c6f03f56cc68fa06994f047cf20207c31b5dad3f6bab54a0f66ef"}, + {file = "coverage-7.4.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf89ab85027427d351f1de918aff4b43f4eb5f33aff6835ed30322a86ac29c9e"}, + {file = "coverage-7.4.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a178b7b1ac0f1530bb28d2e51f88c0bab3e5949835851a60dda80bff6052510c"}, + {file = "coverage-7.4.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:06fe398145a2e91edaf1ab4eee66149c6776c6b25b136f4a86fcbbb09512fd10"}, + {file = "coverage-7.4.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:18cac867950943fe93d6cd56a67eb7dcd2d4a781a40f4c1e25d6f1ed98721a55"}, + {file = "coverage-7.4.2-cp311-cp311-win32.whl", hash = "sha256:f72cdd2586f9a769570d4b5714a3837b3a59a53b096bb954f1811f6a0afad305"}, + {file = "coverage-7.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:d779a48fac416387dd5673fc5b2d6bd903ed903faaa3247dc1865c65eaa5a93e"}, + {file = "coverage-7.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:adbdfcda2469d188d79771d5696dc54fab98a16d2ef7e0875013b5f56a251047"}, + {file = "coverage-7.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ac4bab32f396b03ebecfcf2971668da9275b3bb5f81b3b6ba96622f4ef3f6e17"}, + {file = "coverage-7.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:006d220ba2e1a45f1de083d5022d4955abb0aedd78904cd5a779b955b019ec73"}, + {file = "coverage-7.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3733545eb294e5ad274abe131d1e7e7de4ba17a144505c12feca48803fea5f64"}, + {file = "coverage-7.4.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42a9e754aa250fe61f0f99986399cec086d7e7a01dd82fd863a20af34cbce962"}, + {file = "coverage-7.4.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2ed37e16cf35c8d6e0b430254574b8edd242a367a1b1531bd1adc99c6a5e00fe"}, + {file = "coverage-7.4.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b953275d4edfab6cc0ed7139fa773dfb89e81fee1569a932f6020ce7c6da0e8f"}, + {file = "coverage-7.4.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32b4ab7e6c924f945cbae5392832e93e4ceb81483fd6dc4aa8fb1a97b9d3e0e1"}, + {file = "coverage-7.4.2-cp312-cp312-win32.whl", hash = "sha256:f5df76c58977bc35a49515b2fbba84a1d952ff0ec784a4070334dfbec28a2def"}, + {file = "coverage-7.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:34423abbaad70fea9d0164add189eabaea679068ebdf693baa5c02d03e7db244"}, + {file = "coverage-7.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b11f9c6587668e495cc7365f85c93bed34c3a81f9f08b0920b87a89acc13469"}, + {file = "coverage-7.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:51593a1f05c39332f623d64d910445fdec3d2ac2d96b37ce7f331882d5678ddf"}, + {file = "coverage-7.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69f1665165ba2fe7614e2f0c1aed71e14d83510bf67e2ee13df467d1c08bf1e8"}, + {file = "coverage-7.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3c8bbb95a699c80a167478478efe5e09ad31680931ec280bf2087905e3b95ec"}, + {file = "coverage-7.4.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:175f56572f25e1e1201d2b3e07b71ca4d201bf0b9cb8fad3f1dfae6a4188de86"}, + {file = "coverage-7.4.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8562ca91e8c40864942615b1d0b12289d3e745e6b2da901d133f52f2d510a1e3"}, + {file = "coverage-7.4.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d9a1ef0f173e1a19738f154fb3644f90d0ada56fe6c9b422f992b04266c55d5a"}, + {file = "coverage-7.4.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f40ac873045db4fd98a6f40387d242bde2708a3f8167bd967ccd43ad46394ba2"}, + {file = "coverage-7.4.2-cp38-cp38-win32.whl", hash = "sha256:d1b750a8409bec61caa7824bfd64a8074b6d2d420433f64c161a8335796c7c6b"}, + {file = "coverage-7.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b4ae777bebaed89e3a7e80c4a03fac434a98a8abb5251b2a957d38fe3fd30088"}, + {file = "coverage-7.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ff7f92ae5a456101ca8f48387fd3c56eb96353588e686286f50633a611afc95"}, + {file = "coverage-7.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:861d75402269ffda0b33af94694b8e0703563116b04c681b1832903fac8fd647"}, + {file = "coverage-7.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3507427d83fa961cbd73f11140f4a5ce84208d31756f7238d6257b2d3d868405"}, + {file = "coverage-7.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf711d517e21fb5bc429f5c4308fbc430a8585ff2a43e88540264ae87871e36a"}, + {file = "coverage-7.4.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c00e54f0bd258ab25e7f731ca1d5144b0bf7bec0051abccd2bdcff65fa3262c9"}, + {file = "coverage-7.4.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f8e845d894e39fb53834da826078f6dc1a933b32b1478cf437007367efaf6f6a"}, + {file = "coverage-7.4.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:840456cb1067dc350af9080298c7c2cfdddcedc1cb1e0b30dceecdaf7be1a2d3"}, + {file = "coverage-7.4.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c11ca2df2206a4e3e4c4567f52594637392ed05d7c7fb73b4ea1c658ba560265"}, + {file = "coverage-7.4.2-cp39-cp39-win32.whl", hash = "sha256:3ff5bdb08d8938d336ce4088ca1a1e4b6c8cd3bef8bb3a4c0eb2f37406e49643"}, + {file = "coverage-7.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:ac9e95cefcf044c98d4e2c829cd0669918585755dd9a92e28a1a7012322d0a95"}, + {file = "coverage-7.4.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:f593a4a90118d99014517c2679e04a4ef5aee2d81aa05c26c734d271065efcb6"}, + {file = "coverage-7.4.2.tar.gz", hash = "sha256:1a5ee18e3a8d766075ce9314ed1cb695414bae67df6a4b0805f5137d93d6f1cb"}, ] [package.dependencies] @@ -1751,13 +1751,13 @@ mkdocs = ">=0.17" [[package]] name = "mkdocs-material" -version = "9.5.8" +version = "9.5.10" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.8-py3-none-any.whl", hash = "sha256:14563314bbf97da4bfafc69053772341babfaeb3329cde01d3e63cec03997af8"}, - {file = "mkdocs_material-9.5.8.tar.gz", hash = "sha256:2a429213e83f84eda7a588e2b186316d806aac602b7f93990042f7a1f3d3cf65"}, + {file = "mkdocs_material-9.5.10-py3-none-any.whl", hash = "sha256:3c6c46b57d2ee3c8890e6e0406e68b6863cf65768f0f436990a742702d198442"}, + {file = "mkdocs_material-9.5.10.tar.gz", hash = "sha256:6ad626dbb31070ebbaedff813323a16a406629620e04b96458f16e6e9c7008fe"}, ] [package.dependencies] @@ -1897,13 +1897,13 @@ typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-cloudwatch" -version = "1.34.0" -description = "Type annotations for boto3.CloudWatch 1.34.0 service generated with mypy-boto3-builder 7.21.0" +version = "1.34.40" +description = "Type annotations for boto3.CloudWatch 1.34.40 service generated with mypy-boto3-builder 7.23.1" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mypy-boto3-cloudwatch-1.34.0.tar.gz", hash = "sha256:cc18aa2b1a89eb4898a6c213db594abf62fe9afca6e8c6bd9aa04b9996193635"}, - {file = "mypy_boto3_cloudwatch-1.34.0-py3-none-any.whl", hash = "sha256:be4a8c90446595c6970898682a5f6eedb4a6b1d2d0b0cbe57ed021c818c48e14"}, + {file = "mypy-boto3-cloudwatch-1.34.40.tar.gz", hash = "sha256:33f0b747389ee5d72fe9319597b8a52395a24f5b9816b0862feec581afb148bc"}, + {file = "mypy_boto3_cloudwatch-1.34.40-py3-none-any.whl", hash = "sha256:9e66e0ab1006cb45ff36df1ab453bdef6ec700d6940466161049dc6efbd3e1b4"}, ] [package.dependencies] @@ -1925,13 +1925,13 @@ typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-lambda" -version = "1.34.0" -description = "Type annotations for boto3.Lambda 1.34.0 service generated with mypy-boto3-builder 7.21.0" +version = "1.34.44" +description = "Type annotations for boto3.Lambda 1.34.44 service generated with mypy-boto3-builder 7.23.1" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mypy-boto3-lambda-1.34.0.tar.gz", hash = "sha256:e74c0ce548da747a8c6e643c39dad8aa54d67e057f57740ec780a7e565590627"}, - {file = "mypy_boto3_lambda-1.34.0-py3-none-any.whl", hash = "sha256:109a7e126e84d6da6cacf8ab5c7c6f2be022417fe7bfb7f9b019767d7034f73b"}, + {file = "mypy-boto3-lambda-1.34.44.tar.gz", hash = "sha256:b465e00c33267ceffdf3040c9562755d73aee21902a16d9b84294f7f0e378ab9"}, + {file = "mypy_boto3_lambda-1.34.44-py3-none-any.whl", hash = "sha256:0ef2063a00fad20a4fc096720a8c5c43b08a133cf27738ba9b1c70ea71b271b9"}, ] [package.dependencies] @@ -1967,13 +1967,13 @@ typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-secretsmanager" -version = "1.34.17" -description = "Type annotations for boto3.SecretsManager 1.34.17 service generated with mypy-boto3-builder 7.23.1" +version = "1.34.43" +description = "Type annotations for boto3.SecretsManager 1.34.43 service generated with mypy-boto3-builder 7.23.1" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-boto3-secretsmanager-1.34.17.tar.gz", hash = "sha256:a547932d99c3f711b27b9ea1c38fc063050910c0bf6c8eb346abd96ace61668e"}, - {file = "mypy_boto3_secretsmanager-1.34.17-py3-none-any.whl", hash = "sha256:0dbd1cdbe7992324c3414cccf0256e3905827bbf1f6a8d58c255635f6a2b4bfb"}, + {file = "mypy-boto3-secretsmanager-1.34.43.tar.gz", hash = "sha256:abbf560775c2fe0dc383b7f70c16a1bf753d9b3ffc0caa5e35447e685783a68b"}, + {file = "mypy_boto3_secretsmanager-1.34.43-py3-none-any.whl", hash = "sha256:64e9df58f71072f0a912ecaca626683f4536da078caa204ac07928c4b1481b8b"}, ] [package.dependencies] @@ -2301,13 +2301,13 @@ extra = ["pygments (>=2.12)"] [[package]] name = "pytest" -version = "8.0.0" +version = "8.0.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"}, - {file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"}, + {file = "pytest-8.0.1-py3-none-any.whl", hash = "sha256:3e4f16fe1c0a9dc9d9389161c127c3edc5d810c38d6793042fb81d9f48a59fca"}, + {file = "pytest-8.0.1.tar.gz", hash = "sha256:267f6563751877d772019b13aacbe4e860d73fe8f651f28112e9ac37de7513ae"}, ] [package.dependencies] @@ -2323,21 +2323,21 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "pytest-asyncio" -version = "0.21.1" +version = "0.23.5" description = "Pytest support for asyncio" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, - {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, + {file = "pytest-asyncio-0.23.5.tar.gz", hash = "sha256:3a048872a9c4ba14c3e90cc1aa20cbc2def7d01c7c8db3777ec281ba9c057675"}, + {file = "pytest_asyncio-0.23.5-py3-none-any.whl", hash = "sha256:4e7093259ba018d58ede7d5315131d21923a60f8a6e9ee266ce1589685c89eac"}, ] [package.dependencies] -pytest = ">=7.0.0" +pytest = ">=7.0.0,<9" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] -testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-benchmark" @@ -2924,13 +2924,13 @@ pbr = "*" [[package]] name = "sentry-sdk" -version = "1.40.2" +version = "1.40.5" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = "*" files = [ - {file = "sentry-sdk-1.40.2.tar.gz", hash = "sha256:c98c8e9bb4dc8ff1e67473caf6467acfccf915dadcc26d0efb0d6791a8652610"}, - {file = "sentry_sdk-1.40.2-py2.py3-none-any.whl", hash = "sha256:696ef61a323a207e6a20b018ddc6591adb81c671434c88d1a4f2e95ffa75556c"}, + {file = "sentry-sdk-1.40.5.tar.gz", hash = "sha256:d2dca2392cc5c9a2cc9bb874dd7978ebb759682fe4fe889ee7e970ee8dd1c61e"}, + {file = "sentry_sdk-1.40.5-py2.py3-none-any.whl", hash = "sha256:d188b407c9bacbe2a50a824e1f8fb99ee1aeb309133310488c570cb6d7056643"}, ] [package.dependencies] @@ -2956,7 +2956,7 @@ huey = ["huey (>=2)"] loguru = ["loguru (>=0.5)"] opentelemetry = ["opentelemetry-distro (>=0.35b0)"] opentelemetry-experimental = ["opentelemetry-distro (>=0.40b0,<1.0)", "opentelemetry-instrumentation-aiohttp-client (>=0.40b0,<1.0)", "opentelemetry-instrumentation-django (>=0.40b0,<1.0)", "opentelemetry-instrumentation-fastapi (>=0.40b0,<1.0)", "opentelemetry-instrumentation-flask (>=0.40b0,<1.0)", "opentelemetry-instrumentation-requests (>=0.40b0,<1.0)", "opentelemetry-instrumentation-sqlite3 (>=0.40b0,<1.0)", "opentelemetry-instrumentation-urllib (>=0.40b0,<1.0)"] -pure-eval = ["asttokens", "executing", "pure_eval"] +pure-eval = ["asttokens", "executing", "pure-eval"] pymongo = ["pymongo (>=3.1)"] pyspark = ["pyspark (>=2.4.4)"] quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] @@ -3131,13 +3131,13 @@ files = [ [[package]] name = "types-redis" -version = "4.6.0.20240106" +version = "4.6.0.20240218" description = "Typing stubs for redis" optional = false python-versions = ">=3.8" files = [ - {file = "types-redis-4.6.0.20240106.tar.gz", hash = "sha256:2b2fa3a78f84559616242d23f86de5f4130dfd6c3b83fb2d8ce3329e503f756e"}, - {file = "types_redis-4.6.0.20240106-py3-none-any.whl", hash = "sha256:912de6507b631934bd225cdac310b04a58def94391003ba83939e5a10e99568d"}, + {file = "types-redis-4.6.0.20240218.tar.gz", hash = "sha256:5103d7e690e5c74c974a161317b2d59ac2303cf8bef24175b04c2a4c3486cb39"}, + {file = "types_redis-4.6.0.20240218-py3-none-any.whl", hash = "sha256:dc9c45a068240e33a04302aec5655cf41e80f91eecffccbb2df215b2f6fc375d"}, ] [package.dependencies] @@ -3402,4 +3402,4 @@ validation = ["fastjsonschema"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<4.0.0" -content-hash = "e9b5704f5b3140785eacf0670b7ff2ea12e6732baef7f45c5c65c3b5d31d67bb" +content-hash = "e7d6c9d13e4dc00d43ae34d68ec69620a4529f90edbf6eee9984a36ad276fd8f" diff --git a/pyproject.toml b/pyproject.toml index c152995dfeb..5758bdb89d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aws_lambda_powertools" -version = "2.33.0" +version = "2.34.0" description = "Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity." authors = ["Amazon Web Services"] include = ["aws_lambda_powertools/py.typed", "THIRD-PARTY-LICENSES"] @@ -51,38 +51,38 @@ jsonpath-ng = { version = "^1.6.0", optional = true } [tool.poetry.dev-dependencies] coverage = { extras = ["toml"], version = "^7.4" } -pytest = "^8.0.0" +pytest = "^8.0.1" black = "^24.1" boto3 = "^1.26.164" isort = "^5.13.2" pytest-cov = "^4.1.0" pytest-mock = "^3.11.1" pdoc3 = "^0.10.0" -pytest-asyncio = "^0.21.1" +pytest-asyncio = "^0.23.5" bandit = "^1.7.5" radon = "^6.0.1" xenon = "^0.9.1" mkdocs-git-revision-date-plugin = "^0.3.2" mike = "^1.1.2" pytest-xdist = "^3.5.0" -aws-cdk-lib = "^2.126.0" +aws-cdk-lib = "^2.128.0" "aws-cdk.aws-apigatewayv2-alpha" = "^2.38.1-alpha.0" "aws-cdk.aws-apigatewayv2-integrations-alpha" = "^2.38.1-alpha.0" "aws-cdk.aws-apigatewayv2-authorizers-alpha" = "^2.38.1-alpha.0" pytest-benchmark = "^4.0.0" mypy-boto3-appconfig = "^1.34.0" mypy-boto3-cloudformation = "^1.34.32" -mypy-boto3-cloudwatch = "^1.34.0" +mypy-boto3-cloudwatch = "^1.34.40" mypy-boto3-dynamodb = "^1.34.34" -mypy-boto3-lambda = "^1.34.0" +mypy-boto3-lambda = "^1.34.44" mypy-boto3-logs = "^1.34.16" -mypy-boto3-secretsmanager = "^1.34.17" +mypy-boto3-secretsmanager = "^1.34.43" mypy-boto3-ssm = "^1.34.32" mypy-boto3-s3 = "^1.34.14" mypy-boto3-xray = "^1.34.0" types-requests = "^2.31.0" typing-extensions = "^4.6.2" -mkdocs-material = "^9.2.7" +mkdocs-material = "^9.5.10" filelock = "^3.12.2" checksumdir = "^1.2.0" mypy-boto3-appconfigdata = "^1.34.24" @@ -110,7 +110,7 @@ datadog = ["datadog-lambda"] datamasking = ["aws-encryption-sdk", "jsonpath-ng"] [tool.poetry.group.dev.dependencies] -cfn-lint = "0.85.0" +cfn-lint = "0.85.2" mypy = "^1.1.1" types-python-dateutil = "^2.8.19.6" httpx = ">=0.23.3,<0.27.0" diff --git a/ruff.toml b/ruff.toml index 553a8c47b3d..374a183541b 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,5 +1,5 @@ # Enable rules. -select = [ +lint.select = [ "A", # flake8-builtins - https://beta.ruff.rs/docs/rules/#flake8-builtins-a "B", # flake8-bugbear-b - https://beta.ruff.rs/docs/rules/#flake8-bugbear-b "C4", # flake8-comprehensions - https://beta.ruff.rs/docs/rules/#flake8-comprehensions-c4 @@ -28,7 +28,7 @@ select = [ ] # Ignore specific rules -ignore = [ +lint.ignore = [ "W291", # https://beta.ruff.rs/docs/rules/trailing-whitespace/ "PLR0913", # https://beta.ruff.rs/docs/rules/too-many-arguments/ "PLR2004", #https://beta.ruff.rs/docs/rules/magic-value-comparison/ @@ -60,28 +60,28 @@ line-length = 120 fix = true -fixable = ["I", "COM812", "W"] +lint.fixable = ["I", "COM812", "W"] # See: https://github.com/astral-sh/ruff/issues/128 -typing-modules = [ +lint.typing-modules = [ "aws_lambda_powertools.utilities.parser.types", "aws_lambda_powertools.shared.types", ] -[mccabe] +[lint.mccabe] # Maximum cyclomatic complexity max-complexity = 15 -[pylint] +[lint.pylint] # Maximum number of nested blocks max-branches = 15 # Maximum number of if statements in a function max-statements = 70 -[isort] +[lint.isort] split-on-trailing-comma = true -[per-file-ignores] +[lint.per-file-ignores] # Ignore specific rules for specific files "tests/e2e/utils/data_builder/__init__.py" = ["F401"] "tests/e2e/utils/data_fetcher/__init__.py" = ["F401"] diff --git a/tests/e2e/idempotency/handlers/payload_tampering_validation_handler.py b/tests/e2e/idempotency/handlers/payload_tampering_validation_handler.py new file mode 100644 index 00000000000..dacb6ce63e0 --- /dev/null +++ b/tests/e2e/idempotency/handlers/payload_tampering_validation_handler.py @@ -0,0 +1,17 @@ +import os +import uuid + +from aws_lambda_powertools.utilities.idempotency import ( + DynamoDBPersistenceLayer, + IdempotencyConfig, + idempotent, +) + +TABLE_NAME = os.getenv("IdempotencyTable", "") +persistence_layer = DynamoDBPersistenceLayer(table_name=TABLE_NAME) +config = IdempotencyConfig(event_key_jmespath='["refund_id", "customer_id"]', payload_validation_jmespath="details") + + +@idempotent(config=config, persistence_store=persistence_layer) +def lambda_handler(event, context): + return {"request": str(uuid.uuid4())} diff --git a/tests/e2e/idempotency/infrastructure.py b/tests/e2e/idempotency/infrastructure.py index 692e0e8ce81..d42cc67d40d 100644 --- a/tests/e2e/idempotency/infrastructure.py +++ b/tests/e2e/idempotency/infrastructure.py @@ -17,6 +17,7 @@ def create_resources(self): table.grant_read_write_data(functions["ParallelExecutionHandler"]) table.grant_read_write_data(functions["FunctionThreadSafetyHandler"]) table.grant_read_write_data(functions["OptionalIdempotencyKeyHandler"]) + table.grant_read_write_data(functions["PayloadTamperingValidationHandler"]) def _create_dynamodb_table(self) -> Table: table = dynamodb.Table( diff --git a/tests/e2e/idempotency/test_idempotency_dynamodb.py b/tests/e2e/idempotency/test_idempotency_dynamodb.py index 1d61cb69f9f..fdd1b79259b 100644 --- a/tests/e2e/idempotency/test_idempotency_dynamodb.py +++ b/tests/e2e/idempotency/test_idempotency_dynamodb.py @@ -1,10 +1,14 @@ import json +from copy import deepcopy from time import sleep import pytest from tests.e2e.utils import data_fetcher -from tests.e2e.utils.data_fetcher.common import GetLambdaResponseOptions, get_lambda_response_in_parallel +from tests.e2e.utils.data_fetcher.common import ( + GetLambdaResponseOptions, + get_lambda_response_in_parallel, +) @pytest.fixture @@ -32,6 +36,11 @@ def optional_idempotency_key_fn_arn(infrastructure: dict) -> str: return infrastructure.get("OptionalIdempotencyKeyHandlerArn", "") +@pytest.fixture +def payload_tampering_validation_fn_arn(infrastructure: dict) -> str: + return infrastructure.get("PayloadTamperingValidationHandlerArn", "") + + @pytest.fixture def idempotency_table_name(infrastructure: dict) -> str: return infrastructure.get("DynamoDBTable", "") @@ -186,3 +195,27 @@ def test_optional_idempotency_key(optional_idempotency_key_fn_arn: str): assert first_execution_response != second_execution_response assert first_execution_response != third_execution_response assert second_execution_response != third_execution_response + + +@pytest.mark.xdist_group(name="idempotency") +def test_payload_tampering_validation(payload_tampering_validation_fn_arn: str): + # GIVEN a transaction with the idempotency key on refund and customer IDs + transaction = { + "refund_id": "ffd11882-d476-4598-bbf1-643f2be5addf", + "customer_id": "9e9fc440-9e65-49b5-9e71-1382ea1b1658", + "details": {"company_name": "Parker, Johnson and Rath", "currency": "Turkish Lira"}, + } + + # AND a second transaction with the exact idempotency key but different currency + tampered_transaction = deepcopy(transaction) + tampered_transaction["details"]["currency"] = "Euro" + + # WHEN we make both requests to a Lambda Function that enabled payload validation + data_fetcher.get_lambda_response(lambda_arn=payload_tampering_validation_fn_arn, payload=json.dumps(transaction)) + + # THEN we should receive a payload validation error in the second request + with pytest.raises(RuntimeError, match="Payload does not match stored record"): + data_fetcher.get_lambda_response( + lambda_arn=payload_tampering_validation_fn_arn, + payload=json.dumps(tampered_transaction), + ) diff --git a/tests/functional/event_handler/conftest.py b/tests/functional/event_handler/conftest.py index c7a4ac6e500..5c2bdb7729a 100644 --- a/tests/functional/event_handler/conftest.py +++ b/tests/functional/event_handler/conftest.py @@ -2,6 +2,8 @@ import pytest +from tests.functional.utils import load_event + @pytest.fixture def json_dump(): @@ -39,3 +41,33 @@ def validation_schema(): @pytest.fixture def raw_event(): return {"message": "hello hello", "username": "blah blah"} + + +@pytest.fixture +def gw_event(): + return load_event("apiGatewayProxyEvent.json") + + +@pytest.fixture +def gw_event_http(): + return load_event("apiGatewayProxyV2Event.json") + + +@pytest.fixture +def gw_event_alb(): + return load_event("albMultiValueQueryStringEvent.json") + + +@pytest.fixture +def gw_event_lambda_url(): + return load_event("lambdaFunctionUrlEventWithHeaders.json") + + +@pytest.fixture +def gw_event_vpc_lattice(): + return load_event("vpcLatticeV2EventWithHeaders.json") + + +@pytest.fixture +def gw_event_vpc_lattice_v1(): + return load_event("vpcLatticeEvent.json") diff --git a/tests/functional/event_handler/test_openapi_swagger.py b/tests/functional/event_handler/test_openapi_swagger.py index 27fca16f2fa..45e908742b4 100644 --- a/tests/functional/event_handler/test_openapi_swagger.py +++ b/tests/functional/event_handler/test_openapi_swagger.py @@ -73,3 +73,30 @@ def test_openapi_swagger_json_view_with_custom_path(): assert result["multiValueHeaders"]["Content-Type"] == ["application/json"] assert isinstance(json.loads(result["body"]), Dict) assert "OpenAPI JSON View" in result["body"] + + +def test_openapi_swagger_with_rest_api_default_stage(): + app = APIGatewayRestResolver(enable_validation=True) + app.enable_swagger() + + event = load_event("apiGatewayProxyEvent.json") + event["path"] = "/swagger" + event["requestContext"]["stage"] = "$default" + + result = app(event, {}) + assert result["statusCode"] == 200 + assert "ui.specActions.updateUrl('/swagger?format=json')" in result["body"] + + +def test_openapi_swagger_with_rest_api_stage(): + app = APIGatewayRestResolver(enable_validation=True) + app.enable_swagger() + + event = load_event("apiGatewayProxyEvent.json") + event["path"] = "/swagger" + event["requestContext"]["stage"] = "prod" + event["requestContext"]["path"] = "/prod/swagger" + + result = app(event, {}) + assert result["statusCode"] == 200 + assert "ui.specActions.updateUrl('/prod/swagger?format=json')" in result["body"] diff --git a/tests/functional/event_handler/test_openapi_validation_middleware.py b/tests/functional/event_handler/test_openapi_validation_middleware.py index 07e2a34ac42..a9396644b98 100644 --- a/tests/functional/event_handler/test_openapi_validation_middleware.py +++ b/tests/functional/event_handler/test_openapi_validation_middleware.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from enum import Enum from pathlib import PurePath -from typing import List, Tuple +from typing import List, Optional, Tuple import pytest from pydantic import BaseModel @@ -18,17 +18,9 @@ ) from aws_lambda_powertools.event_handler.openapi.params import Body, Header, Query from aws_lambda_powertools.shared.types import Annotated -from tests.functional.utils import load_event -LOAD_GW_EVENT = load_event("apiGatewayProxyEvent.json") -LOAD_GW_EVENT_HTTP = load_event("apiGatewayProxyV2Event.json") -LOAD_GW_EVENT_ALB = load_event("albMultiValueQueryStringEvent.json") -LOAD_GW_EVENT_LAMBDA_URL = load_event("lambdaFunctionUrlEventWithHeaders.json") -LOAD_GW_EVENT_VPC_LATTICE = load_event("vpcLatticeV2EventWithHeaders.json") -LOAD_GW_EVENT_VPC_LATTICE_V1 = load_event("vpcLatticeEvent.json") - -def test_validate_scalars(): +def test_validate_scalars(gw_event): # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -38,22 +30,22 @@ def handler(user_id: int): print(user_id) # sending a number - LOAD_GW_EVENT["path"] = "/users/123" + gw_event["path"] = "/users/123" # THEN the handler should be invoked and return 200 - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 200 # sending a string - LOAD_GW_EVENT["path"] = "/users/abc" + gw_event["path"] = "/users/abc" # THEN the handler should be invoked and return 422 - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 422 assert any(text in result["body"] for text in ["type_error.integer", "int_parsing"]) -def test_validate_scalars_with_default(): +def test_validate_scalars_with_default(gw_event): # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -63,22 +55,22 @@ def handler(user_id: int = 123): print(user_id) # sending a number - LOAD_GW_EVENT["path"] = "/users/123" + gw_event["path"] = "/users/123" # THEN the handler should be invoked and return 200 - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 200 # sending a string - LOAD_GW_EVENT["path"] = "/users/abc" + gw_event["path"] = "/users/abc" # THEN the handler should be invoked and return 422 - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 422 assert any(text in result["body"] for text in ["type_error.integer", "int_parsing"]) -def test_validate_scalars_with_default_and_optional(): +def test_validate_scalars_with_default_and_optional(gw_event): # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -88,22 +80,22 @@ def handler(user_id: int = 123, include_extra: bool = False): print(user_id) # sending a number - LOAD_GW_EVENT["path"] = "/users/123" + gw_event["path"] = "/users/123" # THEN the handler should be invoked and return 200 - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 200 # sending a string - LOAD_GW_EVENT["path"] = "/users/abc" + gw_event["path"] = "/users/abc" # THEN the handler should be invoked and return 422 - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 422 assert any(text in result["body"] for text in ["type_error.integer", "int_parsing"]) -def test_validate_return_type(): +def test_validate_return_type(gw_event): # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -112,16 +104,16 @@ def test_validate_return_type(): def handler() -> int: return 123 - LOAD_GW_EVENT["path"] = "/" + gw_event["path"] = "/" # THEN the handler should be invoked and return 200 # THEN the body must be 123 - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 200 assert result["body"] == "123" -def test_validate_return_list(): +def test_validate_return_list(gw_event): # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -130,16 +122,16 @@ def test_validate_return_list(): def handler() -> List[int]: return [123, 234] - LOAD_GW_EVENT["path"] = "/" + gw_event["path"] = "/" # THEN the handler should be invoked and return 200 # THEN the body must be [123, 234] - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 200 assert json.loads(result["body"]) == [123, 234] -def test_validate_return_tuple(): +def test_validate_return_tuple(gw_event): # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -150,16 +142,16 @@ def test_validate_return_tuple(): def handler() -> Tuple: return sample_tuple - LOAD_GW_EVENT["path"] = "/" + gw_event["path"] = "/" # THEN the handler should be invoked and return 200 # THEN the body must be a tuple - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 200 assert json.loads(result["body"]) == [1, 2, 3] -def test_validate_return_purepath(): +def test_validate_return_purepath(gw_event): # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -171,16 +163,16 @@ def test_validate_return_purepath(): def handler() -> str: return sample_path.as_posix() - LOAD_GW_EVENT["path"] = "/" + gw_event["path"] = "/" # THEN the handler should be invoked and return 200 # THEN the body must be a string - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 200 assert result["body"] == sample_path.as_posix() -def test_validate_return_enum(): +def test_validate_return_enum(gw_event): # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -192,16 +184,16 @@ class Model(Enum): def handler() -> Model: return Model.name.value - LOAD_GW_EVENT["path"] = "/" + gw_event["path"] = "/" # THEN the handler should be invoked and return 200 # THEN the body must be a string - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 200 assert result["body"] == "powertools" -def test_validate_return_dataclass(): +def test_validate_return_dataclass(gw_event): # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -215,16 +207,16 @@ class Model: def handler() -> Model: return Model(name="John", age=30) - LOAD_GW_EVENT["path"] = "/" + gw_event["path"] = "/" # THEN the handler should be invoked and return 200 # THEN the body must be a JSON object - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 200 assert json.loads(result["body"]) == {"name": "John", "age": 30} -def test_validate_return_model(): +def test_validate_return_model(gw_event): # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -237,16 +229,16 @@ class Model(BaseModel): def handler() -> Model: return Model(name="John", age=30) - LOAD_GW_EVENT["path"] = "/" + gw_event["path"] = "/" # THEN the handler should be invoked and return 200 # THEN the body must be a JSON object - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 200 assert json.loads(result["body"]) == {"name": "John", "age": 30} -def test_validate_invalid_return_model(): +def test_validate_invalid_return_model(gw_event): # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -259,16 +251,16 @@ class Model(BaseModel): def handler() -> Model: return {"name": "John"} # type: ignore - LOAD_GW_EVENT["path"] = "/" + gw_event["path"] = "/" # THEN the handler should be invoked and return 422 # THEN the body must be a dict - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 422 assert "missing" in result["body"] -def test_validate_body_param(): +def test_validate_body_param(gw_event): # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -281,18 +273,18 @@ class Model(BaseModel): def handler(user: Model) -> Model: return user - LOAD_GW_EVENT["httpMethod"] = "POST" - LOAD_GW_EVENT["path"] = "/" - LOAD_GW_EVENT["body"] = json.dumps({"name": "John", "age": 30}) + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/" + gw_event["body"] = json.dumps({"name": "John", "age": 30}) # THEN the handler should be invoked and return 200 # THEN the body must be a JSON object - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 200 assert json.loads(result["body"]) == {"name": "John", "age": 30} -def test_validate_body_param_with_stripped_headers(): +def test_validate_body_param_with_stripped_headers(gw_event): # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -306,19 +298,19 @@ class Model(BaseModel): def handler(user: Model) -> Model: return user - LOAD_GW_EVENT["httpMethod"] = "POST" - LOAD_GW_EVENT["headers"] = {"Content-type": " application/json "} - LOAD_GW_EVENT["path"] = "/" - LOAD_GW_EVENT["body"] = json.dumps({"name": "John", "age": 30}) + gw_event["httpMethod"] = "POST" + gw_event["headers"] = {"Content-type": " application/json "} + gw_event["path"] = "/" + gw_event["body"] = json.dumps({"name": "John", "age": 30}) # THEN the handler should be invoked and return 200 # THEN the body must be a JSON object - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 200 assert json.loads(result["body"]) == {"name": "John", "age": 30} -def test_validate_body_param_with_invalid_date(): +def test_validate_body_param_with_invalid_date(gw_event): # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -331,18 +323,18 @@ class Model(BaseModel): def handler(user: Model) -> Model: return user - LOAD_GW_EVENT["httpMethod"] = "POST" - LOAD_GW_EVENT["path"] = "/" - LOAD_GW_EVENT["body"] = "{" # invalid JSON + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/" + gw_event["body"] = "{" # invalid JSON # THEN the handler should be invoked and return 422 # THEN the body must have the "json_invalid" error message - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 422 assert "json_invalid" in result["body"] -def test_validate_embed_body_param(): +def test_validate_embed_body_param(gw_event): # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -355,24 +347,24 @@ class Model(BaseModel): def handler(user: Annotated[Model, Body(embed=True)]) -> Model: return user - LOAD_GW_EVENT["httpMethod"] = "POST" - LOAD_GW_EVENT["path"] = "/" - LOAD_GW_EVENT["body"] = json.dumps({"name": "John", "age": 30}) + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/" + gw_event["body"] = json.dumps({"name": "John", "age": 30}) # THEN the handler should be invoked and return 422 # THEN the body must be a dict - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 422 assert "missing" in result["body"] # THEN the handler should be invoked and return 200 # THEN the body must be a dict - LOAD_GW_EVENT["body"] = json.dumps({"user": {"name": "John", "age": 30}}) - result = app(LOAD_GW_EVENT, {}) + gw_event["body"] = json.dumps({"user": {"name": "John", "age": 30}}) + result = app(gw_event, {}) assert result["statusCode"] == 200 -def test_validate_response_return(): +def test_validate_response_return(gw_event): # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -385,18 +377,18 @@ class Model(BaseModel): def handler(user: Model) -> Response[Model]: return Response(body=user, status_code=200, content_type="application/json") - LOAD_GW_EVENT["httpMethod"] = "POST" - LOAD_GW_EVENT["path"] = "/" - LOAD_GW_EVENT["body"] = json.dumps({"name": "John", "age": 30}) + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/" + gw_event["body"] = json.dumps({"name": "John", "age": 30}) # THEN the handler should be invoked and return 200 # THEN the body must be a dict - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 200 assert json.loads(result["body"]) == {"name": "John", "age": 30} -def test_validate_response_invalid_return(): +def test_validate_response_invalid_return(gw_event): # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -409,13 +401,13 @@ class Model(BaseModel): def handler(user: Model) -> Response[Model]: return Response(body=user, status_code=200) - LOAD_GW_EVENT["httpMethod"] = "POST" - LOAD_GW_EVENT["path"] = "/" - LOAD_GW_EVENT["body"] = json.dumps({}) + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/" + gw_event["body"] = json.dumps({}) # THEN the handler should be invoked and return 422 # THEN the body should have the word missing - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == 422 assert "missing" in result["body"] @@ -429,12 +421,17 @@ def handler(user: Model) -> Response[Model]: ("handler3_without_query_params", 200, None), ], ) -def test_validation_query_string_with_api_rest_resolver(handler_func, expected_status_code, expected_error_text): +def test_validation_query_string_with_api_rest_resolver( + handler_func, + expected_status_code, + expected_error_text, + gw_event, +): # GIVEN a APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) - LOAD_GW_EVENT["httpMethod"] = "GET" - LOAD_GW_EVENT["path"] = "/users" + gw_event["httpMethod"] = "GET" + gw_event["path"] = "/users" # WHEN a handler is defined with various parameters and routes # Define handler1 with correct params @@ -453,8 +450,8 @@ def handler2(parameter1: Annotated[List[int], Query()], parameter2: str): # Define handler3 without params if handler_func == "handler3_without_query_params": - LOAD_GW_EVENT["queryStringParameters"] = None - LOAD_GW_EVENT["multiValueQueryStringParameters"] = None + gw_event["queryStringParameters"] = None + gw_event["multiValueQueryStringParameters"] = None @app.get("/users") def handler3(): @@ -462,7 +459,7 @@ def handler3(): # THEN the handler should be invoked with the expected result # AND the status code should match the expected_status_code - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == expected_status_code # IF expected_error_text is provided, THEN check for its presence in the response body @@ -478,13 +475,18 @@ def handler3(): ("handler3_without_query_params", 200, None), ], ) -def test_validation_query_string_with_api_http_resolver(handler_func, expected_status_code, expected_error_text): +def test_validation_query_string_with_api_http_resolver( + handler_func, + expected_status_code, + expected_error_text, + gw_event_http, +): # GIVEN a APIGatewayHttpResolver with validation enabled app = APIGatewayHttpResolver(enable_validation=True) - LOAD_GW_EVENT_HTTP["rawPath"] = "/users" - LOAD_GW_EVENT_HTTP["requestContext"]["http"]["method"] = "GET" - LOAD_GW_EVENT_HTTP["requestContext"]["http"]["path"] = "/users" + gw_event_http["rawPath"] = "/users" + gw_event_http["requestContext"]["http"]["method"] = "GET" + gw_event_http["requestContext"]["http"]["path"] = "/users" # WHEN a handler is defined with various parameters and routes # Define handler1 with correct params @@ -503,7 +505,7 @@ def handler2(parameter1: Annotated[List[int], Query()], parameter2: str): # Define handler3 without params if handler_func == "handler3_without_query_params": - LOAD_GW_EVENT_HTTP["queryStringParameters"] = None + gw_event_http["queryStringParameters"] = None @app.get("/users") def handler3(): @@ -511,7 +513,7 @@ def handler3(): # THEN the handler should be invoked with the expected result # AND the status code should match the expected_status_code - result = app(LOAD_GW_EVENT_HTTP, {}) + result = app(gw_event_http, {}) assert result["statusCode"] == expected_status_code # IF expected_error_text is provided, THEN check for its presence in the response body @@ -527,13 +529,18 @@ def handler3(): ("handler3_without_query_params", 200, None), ], ) -def test_validation_query_string_with_alb_resolver(handler_func, expected_status_code, expected_error_text): +def test_validation_query_string_with_alb_resolver( + handler_func, + expected_status_code, + expected_error_text, + gw_event_alb, +): # GIVEN a ALBResolver with validation enabled app = ALBResolver(enable_validation=True) - LOAD_GW_EVENT_ALB["path"] = "/users" - # WHEN a handler is defined with various parameters and routes + gw_event_alb["path"] = "/users" + # WHEN a handler is defined with various parameters and routes # Define handler1 with correct params if handler_func == "handler1_with_correct_params": @@ -550,7 +557,7 @@ def handler2(parameter1: Annotated[List[int], Query()], parameter2: str): # Define handler3 without params if handler_func == "handler3_without_query_params": - LOAD_GW_EVENT_HTTP["multiValueQueryStringParameters"] = None + gw_event_alb["multiValueQueryStringParameters"] = None @app.get("/users") def handler3(): @@ -558,7 +565,7 @@ def handler3(): # THEN the handler should be invoked with the expected result # AND the status code should match the expected_status_code - result = app(LOAD_GW_EVENT_ALB, {}) + result = app(gw_event_alb, {}) assert result["statusCode"] == expected_status_code # IF expected_error_text is provided, THEN check for its presence in the response body @@ -574,13 +581,18 @@ def handler3(): ("handler3_without_query_params", 200, None), ], ) -def test_validation_query_string_with_lambda_url_resolver(handler_func, expected_status_code, expected_error_text): +def test_validation_query_string_with_lambda_url_resolver( + handler_func, + expected_status_code, + expected_error_text, + gw_event_lambda_url, +): # GIVEN a LambdaFunctionUrlResolver with validation enabled app = LambdaFunctionUrlResolver(enable_validation=True) - LOAD_GW_EVENT_LAMBDA_URL["rawPath"] = "/users" - LOAD_GW_EVENT_LAMBDA_URL["requestContext"]["http"]["method"] = "GET" - LOAD_GW_EVENT_LAMBDA_URL["requestContext"]["http"]["path"] = "/users" + gw_event_lambda_url["rawPath"] = "/users" + gw_event_lambda_url["requestContext"]["http"]["method"] = "GET" + gw_event_lambda_url["requestContext"]["http"]["path"] = "/users" # WHEN a handler is defined with various parameters and routes # Define handler1 with correct params @@ -599,7 +611,7 @@ def handler2(parameter1: Annotated[List[int], Query()], parameter2: str): # Define handler3 without params if handler_func == "handler3_without_query_params": - LOAD_GW_EVENT_LAMBDA_URL["queryStringParameters"] = None + gw_event_lambda_url["queryStringParameters"] = None @app.get("/users") def handler3(): @@ -607,7 +619,7 @@ def handler3(): # THEN the handler should be invoked with the expected result # AND the status code should match the expected_status_code - result = app(LOAD_GW_EVENT_LAMBDA_URL, {}) + result = app(gw_event_lambda_url, {}) assert result["statusCode"] == expected_status_code # IF expected_error_text is provided, THEN check for its presence in the response body @@ -623,11 +635,16 @@ def handler3(): ("handler3_without_query_params", 200, None), ], ) -def test_validation_query_string_with_vpc_lattice_resolver(handler_func, expected_status_code, expected_error_text): +def test_validation_query_string_with_vpc_lattice_resolver( + handler_func, + expected_status_code, + expected_error_text, + gw_event_vpc_lattice, +): # GIVEN a VPCLatticeV2Resolver with validation enabled app = VPCLatticeV2Resolver(enable_validation=True) - LOAD_GW_EVENT_VPC_LATTICE["path"] = "/users" + gw_event_vpc_lattice["path"] = "/users" # WHEN a handler is defined with various parameters and routes @@ -647,7 +664,7 @@ def handler2(parameter1: Annotated[List[int], Query()], parameter2: str): # Define handler3 without params if handler_func == "handler3_without_query_params": - LOAD_GW_EVENT_VPC_LATTICE["queryStringParameters"] = None + gw_event_vpc_lattice["queryStringParameters"] = None @app.get("/users") def handler3(): @@ -655,7 +672,7 @@ def handler3(): # THEN the handler should be invoked with the expected result # AND the status code should match the expected_status_code - result = app(LOAD_GW_EVENT_VPC_LATTICE, {}) + result = app(gw_event_vpc_lattice, {}) assert result["statusCode"] == expected_status_code # IF expected_error_text is provided, THEN check for its presence in the response body @@ -673,12 +690,17 @@ def handler3(): ("handler4_without_header_params", 200, None), ], ) -def test_validation_header_with_api_rest_resolver(handler_func, expected_status_code, expected_error_text): +def test_validation_header_with_api_rest_resolver( + handler_func, + expected_status_code, + expected_error_text, + gw_event, +): # GIVEN a APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) - LOAD_GW_EVENT["httpMethod"] = "GET" - LOAD_GW_EVENT["path"] = "/users" + gw_event["httpMethod"] = "GET" + gw_event["path"] = "/users" # WHEN a handler is defined with various parameters and routes # Define handler1 with correct params @@ -707,8 +729,8 @@ def handler3( # Define handler4 without params if handler_func == "handler4_without_header_params": - LOAD_GW_EVENT["headers"] = None - LOAD_GW_EVENT["multiValueHeaders"] = None + gw_event["headers"] = None + gw_event["multiValueHeaders"] = None @app.get("/users") def handler4(): @@ -716,7 +738,7 @@ def handler4(): # THEN the handler should be invoked with the expected result # AND the status code should match the expected_status_code - result = app(LOAD_GW_EVENT, {}) + result = app(gw_event, {}) assert result["statusCode"] == expected_status_code # IF expected_error_text is provided, THEN check for its presence in the response body @@ -733,13 +755,18 @@ def handler4(): ("handler4_without_header_params", 200, None), ], ) -def test_validation_header_with_http_rest_resolver(handler_func, expected_status_code, expected_error_text): +def test_validation_header_with_http_rest_resolver( + handler_func, + expected_status_code, + expected_error_text, + gw_event_http, +): # GIVEN a APIGatewayHttpResolver with validation enabled app = APIGatewayHttpResolver(enable_validation=True) - LOAD_GW_EVENT_HTTP["rawPath"] = "/users" - LOAD_GW_EVENT_HTTP["requestContext"]["http"]["method"] = "GET" - LOAD_GW_EVENT_HTTP["requestContext"]["http"]["path"] = "/users" + gw_event_http["rawPath"] = "/users" + gw_event_http["requestContext"]["http"]["method"] = "GET" + gw_event_http["requestContext"]["http"]["path"] = "/users" # WHEN a handler is defined with various parameters and routes # Define handler1 with correct params @@ -768,7 +795,7 @@ def handler3( # Define handler4 without params if handler_func == "handler4_without_header_params": - LOAD_GW_EVENT_HTTP["headers"] = None + gw_event_http["headers"] = None @app.get("/users") def handler4(): @@ -776,7 +803,7 @@ def handler4(): # THEN the handler should be invoked with the expected result # AND the status code should match the expected_status_code - result = app(LOAD_GW_EVENT_HTTP, {}) + result = app(gw_event_http, {}) assert result["statusCode"] == expected_status_code # IF expected_error_text is provided, THEN check for its presence in the response body @@ -793,11 +820,16 @@ def handler4(): ("handler4_without_header_params", 200, None), ], ) -def test_validation_header_with_alb_resolver(handler_func, expected_status_code, expected_error_text): +def test_validation_header_with_alb_resolver( + handler_func, + expected_status_code, + expected_error_text, + gw_event_alb, +): # GIVEN a ALBResolver with validation enabled app = ALBResolver(enable_validation=True) - LOAD_GW_EVENT_ALB["path"] = "/users" + gw_event_alb["path"] = "/users" # WHEN a handler is defined with various parameters and routes # Define handler1 with correct params @@ -826,7 +858,7 @@ def handler3( # Define handler4 without params if handler_func == "handler4_without_header_params": - LOAD_GW_EVENT_ALB["multiValueHeaders"] = None + gw_event_alb["multiValueHeaders"] = None @app.get("/users") def handler4(): @@ -834,7 +866,7 @@ def handler4(): # THEN the handler should be invoked with the expected result # AND the status code should match the expected_status_code - result = app(LOAD_GW_EVENT_ALB, {}) + result = app(gw_event_alb, {}) assert result["statusCode"] == expected_status_code # IF expected_error_text is provided, THEN check for its presence in the response body @@ -851,13 +883,18 @@ def handler4(): ("handler4_without_header_params", 200, None), ], ) -def test_validation_header_with_lambda_url_resolver(handler_func, expected_status_code, expected_error_text): +def test_validation_header_with_lambda_url_resolver( + handler_func, + expected_status_code, + expected_error_text, + gw_event_lambda_url, +): # GIVEN a LambdaFunctionUrlResolver with validation enabled app = LambdaFunctionUrlResolver(enable_validation=True) - LOAD_GW_EVENT_LAMBDA_URL["rawPath"] = "/users" - LOAD_GW_EVENT_LAMBDA_URL["requestContext"]["http"]["method"] = "GET" - LOAD_GW_EVENT_LAMBDA_URL["requestContext"]["http"]["path"] = "/users" + gw_event_lambda_url["rawPath"] = "/users" + gw_event_lambda_url["requestContext"]["http"]["method"] = "GET" + gw_event_lambda_url["requestContext"]["http"]["path"] = "/users" # WHEN a handler is defined with various parameters and routes # Define handler1 with correct params @@ -886,7 +923,7 @@ def handler3( # Define handler4 without params if handler_func == "handler4_without_header_params": - LOAD_GW_EVENT_LAMBDA_URL["headers"] = None + gw_event_lambda_url["headers"] = None @app.get("/users") def handler4(): @@ -894,7 +931,7 @@ def handler4(): # THEN the handler should be invoked with the expected result # AND the status code should match the expected_status_code - result = app(LOAD_GW_EVENT_LAMBDA_URL, {}) + result = app(gw_event_lambda_url, {}) assert result["statusCode"] == expected_status_code # IF expected_error_text is provided, THEN check for its presence in the response body @@ -911,12 +948,17 @@ def handler4(): ("handler4_without_header_params", 200, None), ], ) -def test_validation_header_with_vpc_lattice_v1_resolver(handler_func, expected_status_code, expected_error_text): +def test_validation_header_with_vpc_lattice_v1_resolver( + handler_func, + expected_status_code, + expected_error_text, + gw_event_vpc_lattice_v1, +): # GIVEN a VPCLatticeResolver with validation enabled app = VPCLatticeResolver(enable_validation=True) - LOAD_GW_EVENT_VPC_LATTICE_V1["raw_path"] = "/users" - LOAD_GW_EVENT_VPC_LATTICE_V1["method"] = "GET" + gw_event_vpc_lattice_v1["raw_path"] = "/users" + gw_event_vpc_lattice_v1["method"] = "GET" # WHEN a handler is defined with various parameters and routes # Define handler1 with correct params @@ -945,7 +987,7 @@ def handler3( # Define handler4 without params if handler_func == "handler4_without_header_params": - LOAD_GW_EVENT_VPC_LATTICE_V1["headers"] = None + gw_event_vpc_lattice_v1["headers"] = None @app.get("/users") def handler4(): @@ -953,7 +995,7 @@ def handler4(): # THEN the handler should be invoked with the expected result # AND the status code should match the expected_status_code - result = app(LOAD_GW_EVENT_VPC_LATTICE_V1, {}) + result = app(gw_event_vpc_lattice_v1, {}) assert result["statusCode"] == expected_status_code # IF expected_error_text is provided, THEN check for its presence in the response body @@ -970,12 +1012,17 @@ def handler4(): ("handler4_without_header_params", 200, None), ], ) -def test_validation_header_with_vpc_lattice_v2_resolver(handler_func, expected_status_code, expected_error_text): +def test_validation_header_with_vpc_lattice_v2_resolver( + handler_func, + expected_status_code, + expected_error_text, + gw_event_vpc_lattice, +): # GIVEN a VPCLatticeV2Resolver with validation enabled app = VPCLatticeV2Resolver(enable_validation=True) - LOAD_GW_EVENT_VPC_LATTICE["path"] = "/users" - LOAD_GW_EVENT_VPC_LATTICE["method"] = "GET" + gw_event_vpc_lattice["path"] = "/users" + gw_event_vpc_lattice["method"] = "GET" # WHEN a handler is defined with various parameters and routes # Define handler1 with correct params @@ -1004,7 +1051,7 @@ def handler3( # Define handler4 without params if handler_func == "handler4_without_header_params": - LOAD_GW_EVENT_VPC_LATTICE["headers"] = None + gw_event_vpc_lattice["headers"] = None @app.get("/users") def handler3(): @@ -1012,9 +1059,52 @@ def handler3(): # THEN the handler should be invoked with the expected result # AND the status code should match the expected_status_code - result = app(LOAD_GW_EVENT_VPC_LATTICE, {}) + result = app(gw_event_vpc_lattice, {}) assert result["statusCode"] == expected_status_code # IF expected_error_text is provided, THEN check for its presence in the response body if expected_error_text: assert any(text in result["body"] for text in expected_error_text) + + +def test_validation_with_alias(gw_event): + # GIVEN a REST API V2 proxy type event + app = APIGatewayRestResolver(enable_validation=True) + + # GIVEN that it has a multiple parameters called "parameter1" + gw_event["queryStringParameters"] = { + "parameter1": "value1,value2", + } + + @app.get("/my/path") + def my_path( + parameter: Annotated[Optional[str], Query(alias="parameter1")] = None, + ) -> str: + assert parameter == "value1" + return parameter + + result = app(gw_event, {}) + assert result["statusCode"] == 200 + + +def test_validation_with_http_single_param(gw_event_http): + # GIVEN a HTTP API V2 proxy type event + app = APIGatewayHttpResolver(enable_validation=True) + + # GIVEN that it has a single parameter called "parameter2" + gw_event_http["queryStringParameters"] = { + "parameter1": "value1,value2", + "parameter2": "value", + } + + # WHEN a handler is defined with a single parameter + @app.post("/my/path") + def my_path( + parameter2: str, + ) -> str: + assert parameter2 == "value" + return parameter2 + + # THEN the handler should be invoked and return 200 + result = app(gw_event_http, {}) + assert result["statusCode"] == 200 diff --git a/tests/functional/feature_flags/test_feature_flags.py b/tests/functional/feature_flags/test_feature_flags.py index 12adaa4525b..cc6aa60aaac 100644 --- a/tests/functional/feature_flags/test_feature_flags.py +++ b/tests/functional/feature_flags/test_feature_flags.py @@ -1410,3 +1410,260 @@ def test_get_all_enabled_features_non_boolean_truthy_defaults(mocker, config): feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) enabled_list: List[str] = feature_flags.get_enabled_features(context={"tenant_id": "6", "username": "a"}) assert enabled_list == expected_value + + +def test_flags_any_in_value_match(mocker, config): + expected_value = True + mocked_app_config_schema = { + "my_feature": { + "default": False, + "rules": { + "tenant_id is in allowed list": { + "when_match": expected_value, + "conditions": [ + { + "action": RuleAction.ANY_IN_VALUE.value, + "key": "tenant_id", + "value": ["Łukasz", "Gerald", "Leandro", "Heitor"], + }, + ], + }, + }, + }, + } + + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={"tenant_id": ["Gerald"]}, + default=False, + ) + assert toggle == expected_value + + +def test_flags_any_in_value_no_match(mocker, config): + expected_value = False + mocked_app_config_schema = { + "my_feature": { + "default": False, + "rules": { + "tenant_id is in allowed list": { + "when_match": expected_value, + "conditions": [ + { + "action": RuleAction.ANY_IN_VALUE.value, + "key": "tenant_id", + "value": ["Łukasz", "Gerald", "Leandro", "Heitor"], + }, + ], + }, + }, + }, + } + + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={"tenant_id": ["Simon"]}, + default=False, + ) + assert toggle == expected_value + + +def test_flags_all_in_value_match(mocker, config): + expected_value = True + mocked_app_config_schema = { + "my_feature": { + "default": False, + "rules": { + "tenant_id is in allowed list": { + "when_match": expected_value, + "conditions": [ + { + "action": RuleAction.ALL_IN_VALUE.value, + "key": "tenant_id", + "value": ["Łukasz", "Gerald", "Leandro", "Heitor"], + }, + ], + }, + }, + }, + } + + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={"tenant_id": ["Gerald"]}, + default=False, + ) + + assert toggle == expected_value + + +def test_flags_all_in_value_no_match(mocker, config): + expected_value = False + mocked_app_config_schema = { + "my_feature": { + "default": False, + "rules": { + "tenant_id is in allowed list": { + "when_match": expected_value, + "conditions": [ + { + "action": RuleAction.ALL_IN_VALUE.value, + "key": "tenant_id", + "value": ["Łukasz", "Gerald", "Leandro", "Heitor"], + }, + ], + }, + }, + }, + } + + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={"tenant_id": ["Gerald", "Simon"]}, + default=False, + ) + + assert toggle == expected_value + + +def test_flags_none_in_value_match(mocker, config): + expected_value = True + mocked_app_config_schema = { + "my_feature": { + "default": False, + "rules": { + "tenant_id is in allowed list": { + "when_match": expected_value, + "conditions": [ + { + "action": RuleAction.NONE_IN_VALUE.value, + "key": "tenant_id", + "value": ["Łukasz", "Gerald", "Leandro", "Heitor"], + }, + ], + }, + }, + }, + } + + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={"tenant_id": ["Rubao"]}, + default=False, + ) + + assert toggle == expected_value + + +def test_flags_none_in_value_no_match(mocker, config): + expected_value = False + mocked_app_config_schema = { + "my_feature": { + "default": False, + "rules": { + "tenant_id is in allowed list": { + "when_match": expected_value, + "conditions": [ + { + "action": RuleAction.NONE_IN_VALUE.value, + "key": "tenant_id", + "value": ["Łukasz", "Gerald", "Leandro", "Heitor"], + }, + ], + }, + }, + }, + } + + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={"tenant_id": ["Heitor"]}, + default=False, + ) + + assert toggle == expected_value + + +@pytest.mark.parametrize( + "intersection_action", + [ + RuleAction.ALL_IN_VALUE.value, + RuleAction.ANY_IN_VALUE.value, + RuleAction.NONE_IN_VALUE.value, + ], +) +def test_intersection_non_list_value(mocker, config, intersection_action): + # GIVEN a schema with list intersection action + expected_value = False + mocked_app_config_schema = { + "my_feature": { + "default": False, + "rules": { + "tenant_id is in allowed list": { + "when_match": expected_value, + "conditions": [ + { + "action": intersection_action, + "key": "tenant_id", + "value": ["Łukasz", "Gerald", "Leandro", "Heitor"], + }, + ], + }, + }, + }, + } + + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + + # WHEN a context value isn't a list + toggle = feature_flags.evaluate( + name="my_feature", + context={"tenant_id": "not a list value"}, + default=False, + ) + + # THEN TypeError should be swallowed and use default value + assert toggle == expected_value + + +def test_exception_handler(mocker, config): + # GIVEN a schema with list intersection action + expected_value = False + mocked_app_config_schema = { + "my_feature": { + "default": False, + "rules": { + "tenant_id is in allowed list": { + "when_match": expected_value, + "conditions": [ + { + "action": RuleAction.ANY_IN_VALUE.value, + "key": "tenant_id", + "value": ["Łukasz", "Gerald", "Leandro", "Heitor"], + }, + ], + }, + }, + }, + } + + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + + @feature_flags.validation_exception_handler(ValueError) + def catch_exception(exc): + raise TypeError("re-raised") + + # WHEN a context value isn't a list + # THEN exception handler should be able to intercept and raise, instead of returning `False` + with pytest.raises(TypeError): + feature_flags.evaluate( + name="my_feature", + context={"tenant_id": "not a list value"}, + default=False, + ) diff --git a/tests/functional/feature_flags/test_schema_validation.py b/tests/functional/feature_flags/test_schema_validation.py index 654cfbcab40..45b4c7dbeda 100644 --- a/tests/functional/feature_flags/test_schema_validation.py +++ b/tests/functional/feature_flags/test_schema_validation.py @@ -1,8 +1,8 @@ -import logging import re -import pytest # noqa: F401 +import pytest +from aws_lambda_powertools.logging.logger import Logger # noqa: F401 from aws_lambda_powertools.utilities.feature_flags.exceptions import ( SchemaValidationError, ) @@ -24,8 +24,6 @@ TimeValues, ) -logger = logging.getLogger(__name__) - EMPTY_SCHEMA = {"": ""} @@ -441,7 +439,7 @@ def test_validate_time_condition_between_time_range_invalid_condition_value(): # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a condition value type dictionary with 'START' and 'END' keys, rule={rule_name}", # noqa: E501 + match=f"SCHEDULE_BETWEEN_TIME_RANGE action must have a dictionary with 'START' and 'END' keys, rule={rule_name}", # noqa: E501 ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) @@ -460,7 +458,7 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_no_s # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a condition value type dictionary with 'START' and 'END' keys, rule={rule_name}", # noqa: E501 + match="'START' and 'END' must be a valid time format", ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) @@ -477,10 +475,7 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_no_e # WHEN calling validate_condition # THEN raise SchemaValidationError - with pytest.raises( - SchemaValidationError, - match=f"condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a condition value type dictionary with 'START' and 'END' keys, rule={rule_name}", # noqa: E501 - ): + with pytest.raises(SchemaValidationError, match="'START' and 'END' must be a valid time format"): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) @@ -649,7 +644,7 @@ def test_a_validate_time_condition_between_datetime_range_invalid_condition_valu # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a condition value type dictionary with 'START' and 'END' keys, rule={rule_name}", # noqa: E501 + match=f"SCHEDULE_BETWEEN_DATETIME_RANGE action must have a dictionary with 'START' and 'END' keys, rule={rule_name}", # noqa: E501 ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) @@ -668,7 +663,7 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a condition value type dictionary with 'START' and 'END' keys, rule={rule_name}", # noqa: E501 + match=f"'START' and 'END' must be a valid ISO8601 time format, rule={rule_name}", ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) @@ -687,7 +682,7 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a condition value type dictionary with 'START' and 'END' keys, rule={rule_name}", # noqa: E501 + match="'START' and 'END' must not include timezone information.*", ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) @@ -1032,3 +1027,48 @@ def test_validate_modulo_range_condition_valid(): # WHEN calling validate_condition # THEN nothing is raised ConditionsValidator.validate_condition_value(condition=condition, rule_name="dummy") + + +def test_validate_any_in_value_condition_invalid_value(): + # GIVEN a schema with a ANY_IN_VALUE action with non-list value + condition = { + CONDITION_ACTION: RuleAction.ANY_IN_VALUE.value, + CONDITION_VALUE: "Gerald", + } + + rule_name = "non-list value for ANY_IN_VALUE" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises(SchemaValidationError, match="ANY_IN_VALUE action must have a list"): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +def test_validate_all_in_value_condition_invalid_value(): + # GIVEN a schema with a ANY_IN_VALUE action with non-list value + condition = { + CONDITION_ACTION: RuleAction.ALL_IN_VALUE.value, + CONDITION_VALUE: "Leandro", + } + + rule_name = "non-list value for ALL_IN_VALUE" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises(SchemaValidationError, match="ALL_IN_VALUE action must have a list"): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +def test_validate_none_in_value_condition_invalid_value(): + # GIVEN a schema with a ANY_IN_VALUE action with non-list value + condition = { + CONDITION_ACTION: RuleAction.NONE_IN_VALUE.value, + CONDITION_VALUE: "Heitor", + } + + rule_name = "non-list value for NONE_IN_VALUE" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises(SchemaValidationError, match="NONE_IN_VALUE action must have a list"): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) diff --git a/tests/functional/idempotency/test_idempotency.py b/tests/functional/idempotency/test_idempotency.py index f5b441e5e91..03cfc850f5c 100644 --- a/tests/functional/idempotency/test_idempotency.py +++ b/tests/functional/idempotency/test_idempotency.py @@ -8,6 +8,7 @@ from botocore import stub from botocore.config import Config from pydantic import BaseModel +from pytest import FixtureRequest from aws_lambda_powertools.utilities.data_classes import ( APIGatewayProxyEventV2, @@ -38,11 +39,18 @@ BasePersistenceLayer, DataRecord, ) -from aws_lambda_powertools.utilities.idempotency.serialization.custom_dict import CustomDictSerializer -from aws_lambda_powertools.utilities.idempotency.serialization.dataclass import DataclassSerializer -from aws_lambda_powertools.utilities.idempotency.serialization.pydantic import PydanticSerializer +from aws_lambda_powertools.utilities.idempotency.serialization.custom_dict import ( + CustomDictSerializer, +) +from aws_lambda_powertools.utilities.idempotency.serialization.dataclass import ( + DataclassSerializer, +) +from aws_lambda_powertools.utilities.idempotency.serialization.pydantic import ( + PydanticSerializer, +) from aws_lambda_powertools.utilities.validation import envelopes, validator from tests.functional.idempotency.utils import ( + build_idempotency_put_item_response_stub, build_idempotency_put_item_stub, build_idempotency_update_item_stub, hash_idempotency_key, @@ -406,6 +414,8 @@ def test_idempotent_lambda_already_completed_with_validation_bad_payload( Test idempotent decorator where event with matching event key has already been successfully processed """ + # GIVEN an idempotent record already exists for the same transaction + # and payload validation was enabled ('validation' key) stubber = stub.Stubber(persistence_store.client) ddb_response = { "Item": { @@ -423,8 +433,11 @@ def test_idempotent_lambda_already_completed_with_validation_bad_payload( def lambda_handler(event, context): return lambda_response + # WHEN the subsequent request is the same but validated field is tampered + lambda_apigw_event["requestContext"]["accountId"] += "1" # Alter the request payload + + # THEN we should raise with pytest.raises(IdempotencyValidationError): - lambda_apigw_event["requestContext"]["accountId"] += "1" # Alter the request payload lambda_handler(lambda_apigw_event, lambda_context) stubber.assert_no_pending_responses() @@ -1172,11 +1185,9 @@ def _put_record(self, data_record: DataRecord) -> None: def _update_record(self, data_record: DataRecord) -> None: assert data_record.idempotency_key == self.expected_idempotency_key - def _get_record(self, idempotency_key) -> DataRecord: - ... + def _get_record(self, idempotency_key) -> DataRecord: ... - def _delete_record(self, data_record: DataRecord) -> None: - ... + def _delete_record(self, data_record: DataRecord) -> None: ... def test_idempotent_lambda_event_source(lambda_context): @@ -1860,3 +1871,60 @@ def lambda_handler(event, context): stubber.assert_no_pending_responses() stubber.deactivate() + + +def test_idempotency_payload_validation_with_tampering_nested_object( + persistence_store: DynamoDBPersistenceLayer, + timestamp_future, + lambda_context, + request: FixtureRequest, +): + # GIVEN an idempotency config with a compound idempotency key (refund, customer_id) + # AND with payload validation key to prevent tampering + + validation_key = "details" + idempotency_config = IdempotencyConfig( + event_key_jmespath='["refund_id", "customer_id"]', + payload_validation_jmespath=validation_key, + use_local_cache=False, + ) + + # AND a previous transaction already processed in the persistent store + transaction = { + "refund_id": "ffd11882-d476-4598-bbf1-643f2be5addf", + "customer_id": "9e9fc440-9e65-49b5-9e71-1382ea1b1658", + "details": [ + { + "company_name": "Parker, Johnson and Rath", + "currency": "Turkish Lira", + }, + ], + } + + stubber = stub.Stubber(persistence_store.client) + ddb_response = build_idempotency_put_item_response_stub( + data=transaction, + expiration=timestamp_future, + status="COMPLETED", + request=request, + validation_data=transaction[validation_key], + ) + + stubber.add_client_error("put_item", "ConditionalCheckFailedException", modeled_fields=ddb_response) + stubber.activate() + + # AND an upcoming tampered transaction + tampered_transaction = copy.deepcopy(transaction) + tampered_transaction["details"][0]["currency"] = "Euro" + + @idempotent(config=idempotency_config, persistence_store=persistence_store) + def lambda_handler(event, context): + return event + + # WHEN the tampered request is made + # THEN we should raise + with pytest.raises(IdempotencyValidationError): + lambda_handler(tampered_transaction, lambda_context) + + stubber.assert_no_pending_responses() + stubber.deactivate() diff --git a/tests/functional/idempotency/utils.py b/tests/functional/idempotency/utils.py index 60f28b075b6..c396e40957c 100644 --- a/tests/functional/idempotency/utils.py +++ b/tests/functional/idempotency/utils.py @@ -1,7 +1,11 @@ +from __future__ import annotations + import hashlib +import json from typing import Any, Dict from botocore import stub +from pytest import FixtureRequest from tests.functional.utils import json_serialize @@ -75,3 +79,30 @@ def build_idempotency_update_item_stub( "TableName": "TEST_TABLE", "UpdateExpression": "SET #response_data = :response_data, #expiry = :expiry, #status = :status", } + + +def build_idempotency_key_id(data: Dict, request: FixtureRequest): + return f"test-func.{request.function.__module__}.{request.function.__qualname__}..lambda_handler#{hash_idempotency_key(data)}" # noqa: E501 + + +def build_idempotency_put_item_response_stub( + data: Dict, + expiration: int, + status: str, + request: FixtureRequest, + validation_data: Any | None, +): + response = { + "Item": { + "id": {"S": build_idempotency_key_id(data, request)}, + "expiration": {"N": expiration}, + "data": {"S": json.dumps(data)}, + "status": {"S": status}, + "validation": {"S": hash_idempotency_key(validation_data)}, + }, + } + + if validation_data is not None: + response["Item"]["validation"] = {"S": hash_idempotency_key(validation_data)} + + return response