diff --git a/.github/actions/verify-provenance/verify_provenance.sh b/.github/actions/verify-provenance/verify_provenance.sh index ca8bf57c6be..dbfe280473d 100755 --- a/.github/actions/verify-provenance/verify_provenance.sh +++ b/.github/actions/verify-provenance/verify_provenance.sh @@ -36,35 +36,58 @@ export readonly FILES=("${SLSA_VERIFIER_BINARY}" "${SLSA_VERIFIER_CHECKSUM_FILE} function debug() { TIMESTAMP=$(date -u "+%FT%TZ") # 2023-05-10T07:53:59Z - echo ""${TIMESTAMP}" DEBUG - $1" + echo ""${TIMESTAMP}" DEBUG - [*] $1" } -function download_slsa_verifier() { - debug "[*] Downloading SLSA Verifier for - Binary: slsa-verifier-${OS_NAME}-${ARCHITECTURE}" - curl --location --silent -O "https://github.com/slsa-framework/slsa-verifier/releases/download/v${SLSA_VERIFIER_VERSION}/slsa-verifier-${OS_NAME}-${ARCHITECTURE}" - - debug "[*] Downloading SLSA Verifier checksums" - curl --location --silent -O "https://raw.githubusercontent.com/slsa-framework/slsa-verifier/f59b55ef2190581d40fc1a5f3b7a51cab2f4a652/${SLSA_VERIFIER_CHECKSUM_FILE}" +function error() { + cleanup + TIMESTAMP=$(date -u "+%FT%TZ") # 2023-05-10T07:53:59Z + echo ""${TIMESTAMP}" ERROR - [!] $1" + echo ""${TIMESTAMP}" ERROR - [!] exiting" + exit 1 +} - debug "[*] Verifying SLSA Verifier binary integrity" +function download_slsa_verifier() { + readonly SLSA_URL="https://github.com/slsa-framework/slsa-verifier/releases/download/v${SLSA_VERIFIER_VERSION}/slsa-verifier-${OS_NAME}-${ARCHITECTURE}" + # debug "Downloading SLSA Verifier for - Binary: slsa-verifier-${OS_NAME}-${ARCHITECTURE}" + debug "Downloading SLSA Verifier binary: ${SLSA_URL}" + curl \ + --location \ + --fail \ + --silent \ + -O "${SLSA_URL}" || error "Failed to download SLSA Verifier binary" + + readonly SLSA_CHECKSUM_URL="https://raw.githubusercontent.com/slsa-framework/slsa-verifier/f59b55ef2190581d40fc1a5f3b7a51cab2f4a652/${SLSA_VERIFIER_CHECKSUM_FILE}" + debug "Downloading SLSA Verifier checksums" + curl \ + --location \ + --fail \ + --silent \ + -O "${SLSA_CHECKSUM_URL}" || error "Failed to download SLSA Verifier binary checksum file" + + debug "Verifying SLSA Verifier binary integrity" CURRENT_HASH=$(sha256sum "${SLSA_VERIFIER_BINARY}" | awk '{print $1}') if [[ $(grep "${CURRENT_HASH}" "${SLSA_VERIFIER_CHECKSUM_FILE}") ]]; then - debug "[*] SLSA Verifier binary integrity confirmed" + debug "SLSA Verifier binary integrity confirmed" chmod +x "${SLSA_VERIFIER_BINARY}" else - debug "[!] Failed integrity check for SLSA Verifier binary: ${SLSA_VERIFIER_BINARY}" - exit 1 + error "Failed integrity check for SLSA Verifier binary: ${SLSA_VERIFIER_BINARY}" fi } function download_provenance() { - debug "[*] Downloading attestation for - Release: https://github.com/${ORG}/${REPO}/releases/v${RELEASE_VERSION}" - - curl --location --silent -O "https://github.com/${ORG}/${REPO}/releases/download/v${RELEASE_VERSION}/${PROVENANCE_FILE}" + readonly PROVENANCE_URL="https://github.com/${ORG}/${REPO}/releases/download/v${RELEASE_VERSION}/${PROVENANCE_FILE}" + debug "Downloading attestation: ${PROVENANCE_URL}" + + curl \ + --location \ + --fail \ + --silent \ + -O ${PROVENANCE_URL} || error "Failed to download provenance. Does the release already exist?" } function download_release_artifact() { - debug "[*] Downloading ${RELEASE_VERSION} release from PyPi" + debug "Downloading ${RELEASE_VERSION} release from PyPi" python -m pip download \ --only-binary=:all: \ --no-deps \ @@ -73,7 +96,7 @@ function download_release_artifact() { } function verify_provenance() { - debug "[*] Verifying attestation with slsa-verifier" + debug "Verifying attestation with slsa-verifier" "${SLSA_VERIFIER_BINARY}" verify-artifact \ --provenance-path "${PROVENANCE_FILE}" \ --source-uri github.com/${ORG}/${REPO} \ @@ -81,11 +104,11 @@ function verify_provenance() { } function cleanup() { - debug "[*] Cleaning up previously downloaded files" - rm "${SLSA_VERIFIER_BINARY}" - rm "${SLSA_VERIFIER_CHECKSUM_FILE}" - rm "${PROVENANCE_FILE}" - rm "${RELEASE_BINARY}" + debug "Cleaning up previously downloaded files" + rm -f "${SLSA_VERIFIER_BINARY}" + rm -f "${SLSA_VERIFIER_CHECKSUM_FILE}" + rm -f "${PROVENANCE_FILE}" + rm -f "${RELEASE_BINARY}" echo "${FILES[@]}" | xargs -n1 echo "Removed file: " } diff --git a/.github/workflows/quality_check_pydanticv2.yml b/.github/workflows/quality_check_pydanticv2.yml new file mode 100644 index 00000000000..435ee5df868 --- /dev/null +++ b/.github/workflows/quality_check_pydanticv2.yml @@ -0,0 +1,76 @@ +name: Code quality - Pydanticv2 + +# PROCESS +# +# 1. Install all dependencies and spin off containers for all supported Python versions +# 2. Run code formatters and linters (various checks) for code standard +# 3. Run static typing checker for potential bugs +# 4. Run entire test suite for regressions except end-to-end (unit, functional, performance) +# 5. Run static analysis (in addition to CodeQL) for common insecure code practices +# 6. Run complexity baseline to avoid error-prone bugs and keep maintenance lower +# 7. Collect and report on test coverage + +# USAGE +# +# Always triggered on new PRs, PR changes and PR merge. + + +on: + pull_request: + paths: + - "aws_lambda_powertools/**" + - "tests/**" + - "pyproject.toml" + - "poetry.lock" + - "mypy.ini" + branches: + - develop + push: + paths: + - "aws_lambda_powertools/**" + - "tests/**" + - "pyproject.toml" + - "poetry.lock" + - "mypy.ini" + branches: + - develop + +permissions: + contents: read + +jobs: + quality_check: + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10"] + env: + PYTHON: "${{ matrix.python-version }}" + permissions: + contents: read # checkout code only + steps: + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install poetry + run: pipx install poetry + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@bd6b4b6205c4dbad673328db7b31b7fab9e241c0 # v4.6.1 + with: + python-version: ${{ matrix.python-version }} + cache: "poetry" + - name: Removing dev dependencies locked to Pydantic v1 + run: poetry remove cfn-lint + - name: Replacing Pydantic v1 with v2 > 2.0.3 + run: poetry add "pydantic=^2.0.3" + - name: Install dependencies + run: make dev + - name: Formatting and Linting + run: make lint + - name: Static type checking + run: make mypy + - name: Test with pytest + run: make test + - name: Security baseline + run: make security-baseline + - name: Complexity baseline + run: make complexity-baseline diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a5ee05b4f1..1d54c049272 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,80 +4,13 @@ # Unreleased -## Bug Fixes - -* **docs:** ensure alias is applied to versioned releases ([#2644](https://github.com/aws-powertools/powertools-lambda-python/issues/2644)) -* **docs:** ensure version alias is in an array to prevent "you're not viewing the latest version" incorrect message ([#2629](https://github.com/aws-powertools/powertools-lambda-python/issues/2629)) -* **logger:** ensure logs stream to stdout by default, not stderr ([#2736](https://github.com/aws-powertools/powertools-lambda-python/issues/2736)) - -## Code Refactoring - -* **parser:** convert functional tests to unit tests ([#2656](https://github.com/aws-powertools/powertools-lambda-python/issues/2656)) -## Documentation - -* **batch:** fix custom batch processor example ([#2714](https://github.com/aws-powertools/powertools-lambda-python/issues/2714)) -* **contributing:** add code integration journey graph ([#2685](https://github.com/aws-powertools/powertools-lambda-python/issues/2685)) -* **maintainers:** add cicd pipeline diagram ([#2692](https://github.com/aws-powertools/powertools-lambda-python/issues/2692)) -* **process:** explain our integration automated checks; revamp navigation ([#2764](https://github.com/aws-powertools/powertools-lambda-python/issues/2764)) - -## Features + +## [v2.20.0] - 2023-07-14 +## Maintenance -* **metrics:** support to set default dimension in EphemeralMetrics ([#2748](https://github.com/aws-powertools/powertools-lambda-python/issues/2748)) - -## Maintenance - -* **ci:** use deps sha for docs and gitpod images based on ossf findings ([#2662](https://github.com/aws-powertools/powertools-lambda-python/issues/2662)) -* **ci:** prevent merging PRs that do not meet minimum requirements ([#2639](https://github.com/aws-powertools/powertools-lambda-python/issues/2639)) -* **ci:** enforce pip --require-hashes to maybe satistify scorecard ([#2679](https://github.com/aws-powertools/powertools-lambda-python/issues/2679)) -* **ci:** enforce top-level permission to minimum fail-safe permission as per openssf ([#2638](https://github.com/aws-powertools/powertools-lambda-python/issues/2638)) -* **ci:** introduce provenance and attestation in release ([#2746](https://github.com/aws-powertools/powertools-lambda-python/issues/2746)) -* **ci:** propagate checkout permission to nested workflows ([#2642](https://github.com/aws-powertools/powertools-lambda-python/issues/2642)) -* **ci:** improves dependabot based on ossf scorecard recommendations ([#2647](https://github.com/aws-powertools/powertools-lambda-python/issues/2647)) -* **ci:** add gitleaks in pre-commit hooks as an extra safety measure ([#2677](https://github.com/aws-powertools/powertools-lambda-python/issues/2677)) -* **ci:** prevent sast codeql to run in forks ([#2711](https://github.com/aws-powertools/powertools-lambda-python/issues/2711)) -* **ci:** use sast on every commit on any supported language ([#2646](https://github.com/aws-powertools/powertools-lambda-python/issues/2646)) -* **ci:** address ossf scorecard findings on npm, pip, and top-level permission leftover ([#2694](https://github.com/aws-powertools/powertools-lambda-python/issues/2694)) -* **deps:** bump github.com/aws/aws-sdk-go-v2/config from 1.17.8 to 1.18.27 in /layer/scripts/layer-balancer ([#2651](https://github.com/aws-powertools/powertools-lambda-python/issues/2651)) -* **deps:** bump docker/setup-buildx-action from 2.8.0 to 2.9.0 ([#2718](https://github.com/aws-powertools/powertools-lambda-python/issues/2718)) -* **deps:** bump github.com/aws/aws-sdk-go-v2/service/lambda from 1.24.6 to 1.37.0 in /layer/scripts/layer-balancer ([#2653](https://github.com/aws-powertools/powertools-lambda-python/issues/2653)) -* **deps:** bump golang.org/x/sync from 0.1.0 to 0.3.0 in /layer/scripts/layer-balancer ([#2649](https://github.com/aws-powertools/powertools-lambda-python/issues/2649)) -* **deps:** bump actions/dependency-review-action from 2.5.1 to 3.0.6 ([#2650](https://github.com/aws-powertools/powertools-lambda-python/issues/2650)) -* **deps:** bump pydantic from 1.10.9 to 1.10.10 ([#2624](https://github.com/aws-powertools/powertools-lambda-python/issues/2624)) -* **deps:** bump pydantic from 1.10.10 to 1.10.11 ([#2671](https://github.com/aws-powertools/powertools-lambda-python/issues/2671)) -* **deps:** migrate from retry to retry2 to address CVE-2022-42969 ([#2665](https://github.com/aws-powertools/powertools-lambda-python/issues/2665)) -* **deps:** bump squidfunk/mkdocs-material from `3837c0f` to `a28ed81` in /docs ([#2669](https://github.com/aws-powertools/powertools-lambda-python/issues/2669)) -* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 2.1.3 to 2.1.4 ([#2738](https://github.com/aws-powertools/powertools-lambda-python/issues/2738)) -* **deps:** bump github.com/aws/aws-sdk-go-v2 from 1.16.16 to 1.18.1 in /layer/scripts/layer-balancer ([#2654](https://github.com/aws-powertools/powertools-lambda-python/issues/2654)) -* **deps:** bump docker/setup-buildx-action from 2.9.0 to 2.9.1 ([#2755](https://github.com/aws-powertools/powertools-lambda-python/issues/2755)) -* **deps:** bump pypa/gh-action-pypi-publish from 1.8.7 to 1.8.8 ([#2754](https://github.com/aws-powertools/powertools-lambda-python/issues/2754)) -* **deps:** bump actions/setup-node from 3.6.0 to 3.7.0 ([#2689](https://github.com/aws-powertools/powertools-lambda-python/issues/2689)) -* **deps-dev:** bump mypy-boto3-cloudwatch from 1.27.0 to 1.28.0 ([#2697](https://github.com/aws-powertools/powertools-lambda-python/issues/2697)) -* **deps-dev:** bump ruff from 0.0.276 to 0.0.277 ([#2682](https://github.com/aws-powertools/powertools-lambda-python/issues/2682)) -* **deps-dev:** bump mypy-boto3-cloudformation from 1.27.0 to 1.28.0 ([#2700](https://github.com/aws-powertools/powertools-lambda-python/issues/2700)) -* **deps-dev:** bump mypy-boto3-appconfigdata from 1.27.0 to 1.28.0 ([#2699](https://github.com/aws-powertools/powertools-lambda-python/issues/2699)) -* **deps-dev:** bump sentry-sdk from 1.27.0 to 1.27.1 ([#2701](https://github.com/aws-powertools/powertools-lambda-python/issues/2701)) -* **deps-dev:** bump typed-ast from 1.5.4 to 1.5.5 ([#2670](https://github.com/aws-powertools/powertools-lambda-python/issues/2670)) -* **deps-dev:** bump mypy-boto3-lambda from 1.27.0 to 1.28.0 ([#2698](https://github.com/aws-powertools/powertools-lambda-python/issues/2698)) -* **deps-dev:** bump ruff from 0.0.275 to 0.0.276 ([#2655](https://github.com/aws-powertools/powertools-lambda-python/issues/2655)) -* **deps-dev:** bump sentry-sdk from 1.26.0 to 1.27.0 ([#2652](https://github.com/aws-powertools/powertools-lambda-python/issues/2652)) -* **deps-dev:** bump aws-cdk from 2.86.0 to 2.87.0 ([#2696](https://github.com/aws-powertools/powertools-lambda-python/issues/2696)) -* **deps-dev:** bump mypy-boto3-dynamodb from 1.26.158 to 1.26.164 ([#2622](https://github.com/aws-powertools/powertools-lambda-python/issues/2622)) -* **deps-dev:** bump mypy-boto3-appconfig from 1.27.0 to 1.28.0 ([#2722](https://github.com/aws-powertools/powertools-lambda-python/issues/2722)) -* **deps-dev:** bump mypy-boto3-s3 from 1.27.0 to 1.28.0 ([#2721](https://github.com/aws-powertools/powertools-lambda-python/issues/2721)) -* **deps-dev:** bump mypy-boto3-logs from 1.27.0 to 1.28.1 ([#2723](https://github.com/aws-powertools/powertools-lambda-python/issues/2723)) -* **deps-dev:** bump mypy-boto3-ssm from 1.27.0 to 1.28.0 ([#2724](https://github.com/aws-powertools/powertools-lambda-python/issues/2724)) -* **deps-dev:** bump mypy-boto3-xray from 1.27.0 to 1.28.0 ([#2720](https://github.com/aws-powertools/powertools-lambda-python/issues/2720)) -* **deps-dev:** bump mypy-boto3-dynamodb from 1.27.0 to 1.28.0 ([#2740](https://github.com/aws-powertools/powertools-lambda-python/issues/2740)) -* **deps-dev:** bump mypy-boto3-secretsmanager from 1.27.0 to 1.28.0 ([#2739](https://github.com/aws-powertools/powertools-lambda-python/issues/2739)) -* **deps-dev:** bump sentry-sdk from 1.27.1 to 1.28.0 ([#2741](https://github.com/aws-powertools/powertools-lambda-python/issues/2741)) -* **deps-dev:** bump ruff from 0.0.277 to 0.0.278 ([#2758](https://github.com/aws-powertools/powertools-lambda-python/issues/2758)) -* **deps-dev:** bump pytest-asyncio from 0.21.0 to 0.21.1 ([#2756](https://github.com/aws-powertools/powertools-lambda-python/issues/2756)) -* **deps-dev:** bump mypy-boto3-appconfigdata from 1.26.70 to 1.27.0 ([#2636](https://github.com/aws-powertools/powertools-lambda-python/issues/2636)) -* **deps-dev:** bump cfn-lint from 0.77.10 to 0.78.1 ([#2757](https://github.com/aws-powertools/powertools-lambda-python/issues/2757)) -* **governance:** update active maintainers list ([#2715](https://github.com/aws-powertools/powertools-lambda-python/issues/2715)) -* **streaming:** replace deprecated Version classes from distutils ([#2752](https://github.com/aws-powertools/powertools-lambda-python/issues/2752)) -* **user-agent:** support patching botocore session ([#2614](https://github.com/aws-powertools/powertools-lambda-python/issues/2614)) +* version bump +* **deps-dev:** bump mypy-boto3-secretsmanager from 1.28.0 to 1.28.3 ([#2773](https://github.com/aws-powertools/powertools-lambda-python/issues/2773)) @@ -3537,7 +3470,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.19.0...HEAD +[Unreleased]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.20.0...HEAD +[v2.20.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.19.0...v2.20.0 [v2.19.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.18.0...v2.19.0 [v2.18.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.17.0...v2.18.0 [v2.17.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.16.2...v2.17.0 diff --git a/README.md b/README.md index 9ace4a52a2a..8f4b6344d14 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ The following companies, among others, use Powertools: * [CyberArk](https://www.cyberark.com/) * [globaldatanet](https://globaldatanet.com/) * [IMS](https://ims.tech/) +* [Jit Security](https://www.jit.io/) * [Propellor.ai](https://www.propellor.ai/) * [TopSport](https://www.topsport.com.au/) * [Trek10](https://www.trek10.com/) diff --git a/aws_lambda_powertools/shared/version.py b/aws_lambda_powertools/shared/version.py index 24ce0f2fd70..f22bb031e1d 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.19.0" +VERSION = "2.21.0" diff --git a/aws_lambda_powertools/utilities/batch/base.py b/aws_lambda_powertools/utilities/batch/base.py index 4ab2c1a2b0b..b00b31449f2 100644 --- a/aws_lambda_powertools/utilities/batch/base.py +++ b/aws_lambda_powertools/utilities/batch/base.py @@ -348,6 +348,11 @@ def _to_batch_type(self, record: dict, event_type: EventType) -> EventSourceData def _to_batch_type(self, record: dict, event_type: EventType, model: Optional["BatchTypeModels"] = None): if model is not None: + # If a model is provided, we assume Pydantic is installed and we need to disable v2 warnings + from aws_lambda_powertools.utilities.parser.compat import disable_pydantic_v2_warning + + disable_pydantic_v2_warning() + return model.parse_obj(record) return self._DATA_CLASS_MAPPING[event_type](record) @@ -500,8 +505,13 @@ def _process_record(self, record: dict) -> Union[SuccessResponse, FailureRespons # we need to handle that exception differently. # We check for a public attr in validation errors coming from Pydantic exceptions (subclass or not) # and we compare if it's coming from the same model that trigger the exception in the first place - model = getattr(exc, "model", None) - if model == self.model: + + # Pydantic v1 raises a ValidationError with ErrorWrappers and store the model instance in a class variable. + # Pydantic v2 simplifies this by adding a title variable to store the model name directly. + model = getattr(exc, "model", None) or getattr(exc, "title", None) + model_name = getattr(self.model, "__name__", None) + + if model == self.model or model == model_name: return self._register_model_validation_error_record(record) return self.failure_handler(record=data, exception=sys.exc_info()) @@ -644,8 +654,13 @@ async def _async_process_record(self, record: dict) -> Union[SuccessResponse, Fa # we need to handle that exception differently. # We check for a public attr in validation errors coming from Pydantic exceptions (subclass or not) # and we compare if it's coming from the same model that trigger the exception in the first place - model = getattr(exc, "model", None) - if model == self.model: + + # Pydantic v1 raises a ValidationError with ErrorWrappers and store the model instance in a class variable. + # Pydantic v2 simplifies this by adding a title variable to store the model name directly. + model = getattr(exc, "model", None) or getattr(exc, "title", None) + model_name = getattr(self.model, "__name__", None) + + if model == self.model or model == model_name: return self._register_model_validation_error_record(record) return self.failure_handler(record=data, exception=sys.exc_info()) diff --git a/aws_lambda_powertools/utilities/parser/compat.py b/aws_lambda_powertools/utilities/parser/compat.py new file mode 100644 index 00000000000..c73098421b1 --- /dev/null +++ b/aws_lambda_powertools/utilities/parser/compat.py @@ -0,0 +1,34 @@ +import functools + + +@functools.lru_cache(maxsize=None) +def disable_pydantic_v2_warning(): + """ + Disables the Pydantic version 2 warning by filtering out the related warnings. + + This function checks the version of Pydantic currently installed and if it is version 2, + it filters out the PydanticDeprecationWarning and PydanticDeprecatedSince20 warnings + to suppress them. + + Since we only need to run the code once, we are using lru_cache to improve performance. + + Note: This function assumes that Pydantic is installed. + + Usage: + disable_pydantic_v2_warning() + """ + try: + from pydantic import __version__ + + version = __version__.split(".") + + if int(version[0]) == 2: + import warnings + + from pydantic import PydanticDeprecatedSince20, PydanticDeprecationWarning + + warnings.filterwarnings("ignore", category=PydanticDeprecationWarning) + warnings.filterwarnings("ignore", category=PydanticDeprecatedSince20) + + except ImportError: + pass diff --git a/aws_lambda_powertools/utilities/parser/envelopes/base.py b/aws_lambda_powertools/utilities/parser/envelopes/base.py index 85486fdd876..101e157ef69 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/base.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/base.py @@ -2,6 +2,7 @@ from abc import ABC, abstractmethod from typing import Any, Dict, Optional, Type, TypeVar, Union +from aws_lambda_powertools.utilities.parser.compat import disable_pydantic_v2_warning from aws_lambda_powertools.utilities.parser.types import Model logger = logging.getLogger(__name__) @@ -26,6 +27,8 @@ def _parse(data: Optional[Union[Dict[str, Any], Any]], model: Type[Model]) -> Un Any Parsed data """ + disable_pydantic_v2_warning() + if data is None: logger.debug("Skipping parsing as event is None") return data diff --git a/aws_lambda_powertools/utilities/parser/models/__init__.py b/aws_lambda_powertools/utilities/parser/models/__init__.py index ddc76dc7819..f1b2d30d9cf 100644 --- a/aws_lambda_powertools/utilities/parser/models/__init__.py +++ b/aws_lambda_powertools/utilities/parser/models/__init__.py @@ -1,3 +1,7 @@ +from aws_lambda_powertools.utilities.parser.compat import disable_pydantic_v2_warning + +disable_pydantic_v2_warning() + from .alb import AlbModel, AlbRequestContext, AlbRequestContextData from .apigw import ( APIGatewayEventAuthorizer, diff --git a/aws_lambda_powertools/utilities/parser/models/apigw.py b/aws_lambda_powertools/utilities/parser/models/apigw.py index 82a3a6188d2..c17b094d0c0 100644 --- a/aws_lambda_powertools/utilities/parser/models/apigw.py +++ b/aws_lambda_powertools/utilities/parser/models/apigw.py @@ -21,74 +21,74 @@ class ApiGatewayUserCert(BaseModel): class APIGatewayEventIdentity(BaseModel): - accessKey: Optional[str] - accountId: Optional[str] - apiKey: Optional[str] - apiKeyId: Optional[str] - caller: Optional[str] - cognitoAuthenticationProvider: Optional[str] - cognitoAuthenticationType: Optional[str] - cognitoIdentityId: Optional[str] - cognitoIdentityPoolId: Optional[str] - principalOrgId: Optional[str] + accessKey: Optional[str] = None + accountId: Optional[str] = None + apiKey: Optional[str] = None + apiKeyId: Optional[str] = None + caller: Optional[str] = None + cognitoAuthenticationProvider: Optional[str] = None + cognitoAuthenticationType: Optional[str] = None + cognitoIdentityId: Optional[str] = None + cognitoIdentityPoolId: Optional[str] = None + principalOrgId: Optional[str] = None # see #1562, temp workaround until API Gateway fixes it the Test button payload # removing it will not be considered a regression in the future sourceIp: Union[IPvAnyNetwork, Literal["test-invoke-source-ip"]] - user: Optional[str] - userAgent: Optional[str] - userArn: Optional[str] - clientCert: Optional[ApiGatewayUserCert] + user: Optional[str] = None + userAgent: Optional[str] = None + userArn: Optional[str] = None + clientCert: Optional[ApiGatewayUserCert] = None class APIGatewayEventAuthorizer(BaseModel): - claims: Optional[Dict[str, Any]] - scopes: Optional[List[str]] + claims: Optional[Dict[str, Any]] = None + scopes: Optional[List[str]] = None class APIGatewayEventRequestContext(BaseModel): accountId: str apiId: str - authorizer: Optional[APIGatewayEventAuthorizer] + authorizer: Optional[APIGatewayEventAuthorizer] = None stage: str protocol: str identity: APIGatewayEventIdentity requestId: str requestTime: str requestTimeEpoch: datetime - resourceId: Optional[str] + resourceId: Optional[str] = None resourcePath: str - domainName: Optional[str] - domainPrefix: Optional[str] - extendedRequestId: Optional[str] + domainName: Optional[str] = None + domainPrefix: Optional[str] = None + extendedRequestId: Optional[str] = None httpMethod: Literal["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] path: str - connectedAt: Optional[datetime] - connectionId: Optional[str] - eventType: Optional[Literal["CONNECT", "MESSAGE", "DISCONNECT"]] - messageDirection: Optional[str] - messageId: Optional[str] - routeKey: Optional[str] - operationName: Optional[str] + connectedAt: Optional[datetime] = None + connectionId: Optional[str] = None + eventType: Optional[Literal["CONNECT", "MESSAGE", "DISCONNECT"]] = None + messageDirection: Optional[str] = None + messageId: Optional[str] = None + routeKey: Optional[str] = None + operationName: Optional[str] = None - @root_validator(allow_reuse=True) + @root_validator(allow_reuse=True, skip_on_failure=True) def check_message_id(cls, values): message_id, event_type = values.get("messageId"), values.get("eventType") if message_id is not None and event_type != "MESSAGE": - raise TypeError("messageId is available only when the `eventType` is `MESSAGE`") + raise ValueError("messageId is available only when the `eventType` is `MESSAGE`") return values class APIGatewayProxyEventModel(BaseModel): - version: Optional[str] + version: Optional[str] = None resource: str path: str httpMethod: Literal["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] headers: Dict[str, str] multiValueHeaders: Dict[str, List[str]] - queryStringParameters: Optional[Dict[str, str]] - multiValueQueryStringParameters: Optional[Dict[str, List[str]]] + queryStringParameters: Optional[Dict[str, str]] = None + multiValueQueryStringParameters: Optional[Dict[str, List[str]]] = None requestContext: APIGatewayEventRequestContext - pathParameters: Optional[Dict[str, str]] - stageVariables: Optional[Dict[str, str]] + pathParameters: Optional[Dict[str, str]] = None + stageVariables: Optional[Dict[str, str]] = None isBase64Encoded: bool - body: Optional[Union[str, Type[BaseModel]]] + body: Optional[Union[str, Type[BaseModel]]] = None diff --git a/aws_lambda_powertools/utilities/parser/models/apigwv2.py b/aws_lambda_powertools/utilities/parser/models/apigwv2.py index cb1f830bb47..3be793dd951 100644 --- a/aws_lambda_powertools/utilities/parser/models/apigwv2.py +++ b/aws_lambda_powertools/utilities/parser/models/apigwv2.py @@ -14,13 +14,13 @@ class RequestContextV2AuthorizerIamCognito(BaseModel): class RequestContextV2AuthorizerIam(BaseModel): - accessKey: Optional[str] - accountId: Optional[str] - callerId: Optional[str] - principalOrgId: Optional[str] - userArn: Optional[str] - userId: Optional[str] - cognitoIdentity: Optional[RequestContextV2AuthorizerIamCognito] + accessKey: Optional[str] = None + accountId: Optional[str] = None + callerId: Optional[str] = None + principalOrgId: Optional[str] = None + userArn: Optional[str] = None + userId: Optional[str] = None + cognitoIdentity: Optional[RequestContextV2AuthorizerIamCognito] = None class RequestContextV2AuthorizerJwt(BaseModel): @@ -29,8 +29,8 @@ class RequestContextV2AuthorizerJwt(BaseModel): class RequestContextV2Authorizer(BaseModel): - jwt: Optional[RequestContextV2AuthorizerJwt] - iam: Optional[RequestContextV2AuthorizerIam] + jwt: Optional[RequestContextV2AuthorizerJwt] = None + iam: Optional[RequestContextV2AuthorizerIam] = None lambda_value: Optional[Dict[str, Any]] = Field(None, alias="lambda") @@ -45,7 +45,7 @@ class RequestContextV2Http(BaseModel): class RequestContextV2(BaseModel): accountId: str apiId: str - authorizer: Optional[RequestContextV2Authorizer] + authorizer: Optional[RequestContextV2Authorizer] = None domainName: str domainPrefix: str requestId: str @@ -61,11 +61,11 @@ class APIGatewayProxyEventV2Model(BaseModel): routeKey: str rawPath: str rawQueryString: str - cookies: Optional[List[str]] + cookies: Optional[List[str]] = None headers: Dict[str, str] - queryStringParameters: Optional[Dict[str, str]] - pathParameters: Optional[Dict[str, str]] - stageVariables: Optional[Dict[str, str]] + queryStringParameters: Optional[Dict[str, str]] = None + pathParameters: Optional[Dict[str, str]] = None + stageVariables: Optional[Dict[str, str]] = None requestContext: RequestContextV2 - body: Optional[Union[str, Type[BaseModel]]] + body: Optional[Union[str, Type[BaseModel]]] = None isBase64Encoded: bool diff --git a/aws_lambda_powertools/utilities/parser/models/dynamodb.py b/aws_lambda_powertools/utilities/parser/models/dynamodb.py index 7a12bf195d3..679952a7181 100644 --- a/aws_lambda_powertools/utilities/parser/models/dynamodb.py +++ b/aws_lambda_powertools/utilities/parser/models/dynamodb.py @@ -7,10 +7,10 @@ class DynamoDBStreamChangedRecordModel(BaseModel): - ApproximateCreationDateTime: Optional[date] + ApproximateCreationDateTime: Optional[date] = None Keys: Dict[str, Dict[str, Any]] - NewImage: Optional[Union[Dict[str, Any], Type[BaseModel], BaseModel]] - OldImage: Optional[Union[Dict[str, Any], Type[BaseModel], BaseModel]] + NewImage: Optional[Union[Dict[str, Any], Type[BaseModel], BaseModel]] = None + OldImage: Optional[Union[Dict[str, Any], Type[BaseModel], BaseModel]] = None SequenceNumber: str SizeBytes: int StreamViewType: Literal["NEW_AND_OLD_IMAGES", "KEYS_ONLY", "NEW_IMAGE", "OLD_IMAGE"] @@ -40,7 +40,7 @@ class DynamoDBStreamRecordModel(BaseModel): awsRegion: str eventSourceARN: str dynamodb: DynamoDBStreamChangedRecordModel - userIdentity: Optional[UserIdentity] + userIdentity: Optional[UserIdentity] = None class DynamoDBStreamModel(BaseModel): diff --git a/aws_lambda_powertools/utilities/parser/models/kafka.py b/aws_lambda_powertools/utilities/parser/models/kafka.py index d4c36bf70f1..1d9d8114e65 100644 --- a/aws_lambda_powertools/utilities/parser/models/kafka.py +++ b/aws_lambda_powertools/utilities/parser/models/kafka.py @@ -19,8 +19,8 @@ class KafkaRecordModel(BaseModel): value: Union[str, Type[BaseModel]] headers: List[Dict[str, bytes]] - # validators - _decode_key = validator("key", allow_reuse=True)(base64_decode) + # Added type ignore to keep compatibility between Pydantic v1 and v2 + _decode_key = validator("key", allow_reuse=True)(base64_decode) # type: ignore[type-var, unused-ignore] @validator("value", pre=True, allow_reuse=True) def data_base64_decode(cls, value): diff --git a/aws_lambda_powertools/utilities/parser/models/kinesis_firehose.py b/aws_lambda_powertools/utilities/parser/models/kinesis_firehose.py index c59d8c680e5..7edc0ba4ebf 100644 --- a/aws_lambda_powertools/utilities/parser/models/kinesis_firehose.py +++ b/aws_lambda_powertools/utilities/parser/models/kinesis_firehose.py @@ -17,7 +17,7 @@ class KinesisFirehoseRecord(BaseModel): data: Union[bytes, Type[BaseModel]] # base64 encoded str is parsed into bytes recordId: str approximateArrivalTimestamp: PositiveInt - kinesisRecordMetadata: Optional[KinesisFirehoseRecordMetadata] + kinesisRecordMetadata: Optional[KinesisFirehoseRecordMetadata] = None @validator("data", pre=True, allow_reuse=True) def data_base64_decode(cls, value): @@ -28,5 +28,5 @@ class KinesisFirehoseModel(BaseModel): invocationId: str deliveryStreamArn: str region: str - sourceKinesisStreamArn: Optional[str] + sourceKinesisStreamArn: Optional[str] = None records: List[KinesisFirehoseRecord] diff --git a/aws_lambda_powertools/utilities/parser/models/kinesis_firehose_sqs.py b/aws_lambda_powertools/utilities/parser/models/kinesis_firehose_sqs.py index b649828853b..58a23e5006c 100644 --- a/aws_lambda_powertools/utilities/parser/models/kinesis_firehose_sqs.py +++ b/aws_lambda_powertools/utilities/parser/models/kinesis_firehose_sqs.py @@ -13,7 +13,7 @@ class KinesisFirehoseSqsRecord(BaseModel): data: SqsRecordModel recordId: str approximateArrivalTimestamp: PositiveInt - kinesisRecordMetadata: Optional[KinesisFirehoseRecordMetadata] + kinesisRecordMetadata: Optional[KinesisFirehoseRecordMetadata] = None @validator("data", pre=True, allow_reuse=True) def data_base64_decode(cls, value): @@ -25,5 +25,5 @@ class KinesisFirehoseSqsModel(BaseModel): invocationId: str deliveryStreamArn: str region: str - sourceKinesisStreamArn: Optional[str] + sourceKinesisStreamArn: Optional[str] = None records: List[KinesisFirehoseSqsRecord] diff --git a/aws_lambda_powertools/utilities/parser/models/s3.py b/aws_lambda_powertools/utilities/parser/models/s3.py index 01573b6d751..db6c41d30f3 100644 --- a/aws_lambda_powertools/utilities/parser/models/s3.py +++ b/aws_lambda_powertools/utilities/parser/models/s3.py @@ -45,10 +45,10 @@ class S3Bucket(BaseModel): class S3Object(BaseModel): key: str - size: Optional[NonNegativeFloat] - eTag: Optional[str] + size: Optional[NonNegativeFloat] = None + eTag: Optional[str] = None sequencer: str - versionId: Optional[str] + versionId: Optional[str] = None class S3Message(BaseModel): @@ -60,10 +60,10 @@ class S3Message(BaseModel): class S3EventNotificationObjectModel(BaseModel): key: str - size: Optional[NonNegativeFloat] + size: Optional[NonNegativeFloat] = None etag: str version_id: str = Field(None, alias="version-id") - sequencer: Optional[str] + sequencer: Optional[str] = None class S3EventNotificationEventBridgeBucketModel(BaseModel): @@ -77,7 +77,7 @@ class S3EventNotificationEventBridgeDetailModel(BaseModel): request_id: str = Field(None, alias="request-id") requester: str source_ip_address: str = Field(None, alias="source-ip-address") - reason: Optional[str] + reason: Optional[str] = None deletion_type: Optional[str] = Field(None, alias="deletion-type") restore_expiry_time: Optional[str] = Field(None, alias="restore-expiry-time") source_storage_class: Optional[str] = Field(None, alias="source-storage-class") @@ -99,9 +99,9 @@ class S3RecordModel(BaseModel): requestParameters: S3RequestParameters responseElements: S3ResponseElements s3: S3Message - glacierEventData: Optional[S3EventRecordGlacierEventData] + glacierEventData: Optional[S3EventRecordGlacierEventData] = None - @root_validator + @root_validator(allow_reuse=True, skip_on_failure=True) def validate_s3_object(cls, values): event_name = values.get("eventName") s3_object = values.get("s3").object diff --git a/aws_lambda_powertools/utilities/parser/models/s3_object_event.py b/aws_lambda_powertools/utilities/parser/models/s3_object_event.py index ef59e9c2f98..7ef98fe4bb2 100644 --- a/aws_lambda_powertools/utilities/parser/models/s3_object_event.py +++ b/aws_lambda_powertools/utilities/parser/models/s3_object_event.py @@ -22,7 +22,7 @@ class S3ObjectUserRequest(BaseModel): class S3ObjectSessionIssuer(BaseModel): type: str # noqa: A003, VNE003 - userName: Optional[str] + userName: Optional[str] = None principalId: str arn: str accountId: str @@ -42,10 +42,10 @@ class S3ObjectUserIdentity(BaseModel): type: str # noqa003 accountId: str accessKeyId: str - userName: Optional[str] + userName: Optional[str] = None principalId: str arn: str - sessionContext: Optional[S3ObjectSessionContext] + sessionContext: Optional[S3ObjectSessionContext] = None class S3ObjectLambdaEvent(BaseModel): diff --git a/aws_lambda_powertools/utilities/parser/models/ses.py b/aws_lambda_powertools/utilities/parser/models/ses.py index 77b23431099..2e9e93f368e 100644 --- a/aws_lambda_powertools/utilities/parser/models/ses.py +++ b/aws_lambda_powertools/utilities/parser/models/ses.py @@ -36,9 +36,9 @@ class SesMailHeaders(BaseModel): class SesMailCommonHeaders(BaseModel): header_from: List[str] = Field(None, alias="from") to: List[str] - cc: Optional[List[str]] - bcc: Optional[List[str]] - sender: Optional[List[str]] + cc: Optional[List[str]] = None + bcc: Optional[List[str]] = None + sender: Optional[List[str]] = None reply_to: Optional[List[str]] = Field(None, alias="reply-to") returnPath: str messageId: str diff --git a/aws_lambda_powertools/utilities/parser/models/sns.py b/aws_lambda_powertools/utilities/parser/models/sns.py index 6cd2fcec006..8f388f2974c 100644 --- a/aws_lambda_powertools/utilities/parser/models/sns.py +++ b/aws_lambda_powertools/utilities/parser/models/sns.py @@ -14,17 +14,17 @@ class SnsMsgAttributeModel(BaseModel): class SnsNotificationModel(BaseModel): - Subject: Optional[str] + Subject: Optional[str] = None TopicArn: str UnsubscribeUrl: HttpUrl Type: Literal["Notification"] - MessageAttributes: Optional[Dict[str, SnsMsgAttributeModel]] + MessageAttributes: Optional[Dict[str, SnsMsgAttributeModel]] = None Message: Union[str, TypingType[BaseModel]] MessageId: str - SigningCertUrl: Optional[HttpUrl] # NOTE: FIFO opt-in removes attribute - Signature: Optional[str] # NOTE: FIFO opt-in removes attribute + SigningCertUrl: Optional[HttpUrl] = None # NOTE: FIFO opt-in removes attribute + Signature: Optional[str] = None # NOTE: FIFO opt-in removes attribute Timestamp: datetime - SignatureVersion: Optional[str] # NOTE: FIFO opt-in removes attribute + SignatureVersion: Optional[str] = None # NOTE: FIFO opt-in removes attribute @root_validator(pre=True, allow_reuse=True) def check_sqs_protocol(cls, values): diff --git a/aws_lambda_powertools/utilities/parser/models/sqs.py b/aws_lambda_powertools/utilities/parser/models/sqs.py index 168707530f3..63ea4b76e0e 100644 --- a/aws_lambda_powertools/utilities/parser/models/sqs.py +++ b/aws_lambda_powertools/utilities/parser/models/sqs.py @@ -9,17 +9,17 @@ class SqsAttributesModel(BaseModel): ApproximateReceiveCount: str ApproximateFirstReceiveTimestamp: datetime - MessageDeduplicationId: Optional[str] - MessageGroupId: Optional[str] + MessageDeduplicationId: Optional[str] = None + MessageGroupId: Optional[str] = None SenderId: str SentTimestamp: datetime - SequenceNumber: Optional[str] - AWSTraceHeader: Optional[str] + SequenceNumber: Optional[str] = None + AWSTraceHeader: Optional[str] = None class SqsMsgAttributeModel(BaseModel): - stringValue: Optional[str] - binaryValue: Optional[str] + stringValue: Optional[str] = None + binaryValue: Optional[str] = None stringListValues: List[str] = [] binaryListValues: List[str] = [] dataType: str @@ -56,7 +56,7 @@ class SqsRecordModel(BaseModel): attributes: SqsAttributesModel messageAttributes: Dict[str, SqsMsgAttributeModel] md5OfBody: str - md5OfMessageAttributes: Optional[str] + md5OfMessageAttributes: Optional[str] = None eventSource: Literal["aws:sqs"] eventSourceARN: str awsRegion: str diff --git a/aws_lambda_powertools/utilities/parser/parser.py b/aws_lambda_powertools/utilities/parser/parser.py index eeaa5612fff..7e2d69e429c 100644 --- a/aws_lambda_powertools/utilities/parser/parser.py +++ b/aws_lambda_powertools/utilities/parser/parser.py @@ -1,6 +1,7 @@ import logging from typing import Any, Callable, Dict, Optional, Type, overload +from aws_lambda_powertools.utilities.parser.compat import disable_pydantic_v2_warning from aws_lambda_powertools.utilities.parser.types import EventParserReturnType, Model from ...middleware_factory import lambda_handler_decorator @@ -156,6 +157,7 @@ def handler(event: Order, context: LambdaContext): raise InvalidEnvelopeError(f"Envelope must implement BaseEnvelope, envelope={envelope}") try: + disable_pydantic_v2_warning() logger.debug("Parsing and validating event model; no envelope used") if isinstance(event, str): return model.parse_raw(event) diff --git a/docs/Dockerfile b/docs/Dockerfile index 50b8f6682c2..4d24b69c3f0 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -1,5 +1,5 @@ # v9.1.18 -FROM squidfunk/mkdocs-material@sha256:a28ed811514d6c6cf15a4bcf7a2ce21d03fcb5c8cbdc4e1fec8ba179ef58fbb5 +FROM squidfunk/mkdocs-material@sha256:33e28bdae302bc1aa9c6783dd863742416cb1174bae4ad9d7bcc5b2efe685639 # 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 aab8f441b1e..215450e8421 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,8 +26,8 @@ Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverles You can install Powertools for AWS Lambda (Python) using one of the following options: -* **Lambda Layer (x86_64)**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:37**](# "Replace {region} with your AWS region, e.g., eu-west-1"){: .copyMe}:clipboard: -* **Lambda Layer (arm64)**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37**](# "Replace {region} with your AWS region, e.g., eu-west-1"){: .copyMe}:clipboard: +* **Lambda Layer (x86_64)**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:38**](# "Replace {region} with your AWS region, e.g., eu-west-1"){: .copyMe}:clipboard: +* **Lambda Layer (arm64)**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38**](# "Replace {region} with your AWS region, e.g., eu-west-1"){: .copyMe}:clipboard: * **Pip**: **[`pip install "aws-lambda-powertools"`](#){: .copyMe}:clipboard:** !!! question "Looking for Pip signed releases? [Learn more about verifying signed builds](./security.md#verifying-signed-builds)" @@ -80,60 +80,60 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd | Region | Layer ARN | | ---------------- | ---------------------------------------------------------------------------------------------------------- | - | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `ap-south-2` | [arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `ap-southeast-4` | [arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `eu-central-2` | [arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `eu-south-2` | [arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `me-central-1` | [arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | - | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:37](#){: .copyMe}:clipboard: | + | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `ap-south-2` | [arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `ap-southeast-4` | [arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `eu-central-2` | [arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `eu-south-2` | [arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `me-central-1` | [arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | + | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:38](#){: .copyMe}:clipboard: | === "arm64" | Region | Layer ARN | | ---------------- | ---------------------------------------------------------------------------------------------------------------- | - | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | - | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37](#){: .copyMe}:clipboard: | + | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | + | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38](#){: .copyMe}:clipboard: | ??? note "Note: Click to expand and copy code snippets for popular frameworks" @@ -146,7 +146,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd Type: AWS::Serverless::Function Properties: Layers: - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:37 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:38 ``` === "Serverless framework" @@ -156,7 +156,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd hello: handler: lambda_function.lambda_handler layers: - - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:37 + - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:38 ``` === "CDK" @@ -172,7 +172,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd 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:37" + layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:38" ) aws_lambda.Function(self, 'sample-app-lambda', @@ -221,7 +221,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd role = aws_iam_role.iam_for_lambda.arn handler = "index.test" runtime = "python3.9" - layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:37"] + layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:38"] source_code_hash = filebase64sha256("lambda_function_payload.zip") } @@ -274,7 +274,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd ? 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:37 + ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:38 ❯ amplify push -y @@ -285,7 +285,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd - 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:37 + ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:38 ? Do you want to edit the local lambda function now? No ``` @@ -299,7 +299,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd Properties: Architectures: [arm64] Layers: - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38 ``` === "Serverless framework" @@ -310,7 +310,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd handler: lambda_function.lambda_handler architecture: arm64 layers: - - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37 + - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38 ``` === "CDK" @@ -326,7 +326,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd 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:37" + layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38" ) aws_lambda.Function(self, 'sample-app-lambda', @@ -376,7 +376,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd role = aws_iam_role.iam_for_lambda.arn handler = "index.test" runtime = "python3.9" - layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:37"] + layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38"] architectures = ["arm64"] source_code_hash = filebase64sha256("lambda_function_payload.zip") @@ -432,7 +432,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd ? 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:37 + ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38 ❯ amplify push -y @@ -443,7 +443,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd - 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:37 + ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:38 ? Do you want to edit the local lambda function now? No ``` @@ -451,7 +451,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd 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:37 --region {region} + aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:38 --region {region} ``` The pre-signed URL to download this Lambda Layer will be within `Location` key. @@ -753,6 +753,7 @@ The following companies, among others, use Powertools: * [CyberArk](https://www.cyberark.com/){target="_blank"} * [globaldatanet](https://globaldatanet.com/){target="_blank"} * [IMS](https://ims.tech/){target="_blank"} +* [Jit Security](https://www.jit.io/){target="_blank"} * [Propellor.ai](https://www.propellor.ai/){target="_blank"} * [TopSport](https://www.topsport.com.au/){target="_blank"} * [Trek10](https://www.trek10.com/){target="_blank"} diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index 685cad753f3..c951a6205df 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -649,7 +649,7 @@ Within AWS X-Ray, we can answer these questions by using two features: tracing * Let's put them into action. -```python title="Enriching traces with annotations and metadata" hl_lines="10 17-18 26-27 35 37-42 45" +```python title="Enriching traces with annotations and metadata" hl_lines="10 17-18 26-27 35 37-41 44" from aws_xray_sdk.core import patch_all, xray_recorder from aws_lambda_powertools import Logger @@ -687,11 +687,10 @@ def lambda_handler(event, context): global cold_start subsegment = xray_recorder.current_subsegment() + subsegment.put_annotation(key="ColdStart", value=cold_start) + if cold_start: - subsegment.put_annotation(key="ColdStart", value=cold_start) cold_start = False - else: - subsegment.put_annotation(key="ColdStart", value=cold_start) result = app.resolve(event, context) subsegment.put_metadata("response", result) @@ -705,8 +704,8 @@ Let's break it down: * **L17-18**: We use AWS X-Ray SDK to add `User` annotation on `hello_name` subsegment. This will allow us to filter traces using the `User` value. * **L26-27**: We repeat what we did in L17-18 except we use the value `unknown` since we don't have that information. * **L35**: We use `global` to modify our global variable defined in the outer scope. -* **37-42**: We add `ColdStart` annotation and flip the value of `cold_start` variable, so that subsequent requests annotates the value `false` when the sandbox is reused. -* **L45**: We include the final response under `response` key as part of the `handler` subsegment. +* **37-41**: We add `ColdStart` annotation and set `cold_start` variable to `false`, so that subsequent requests annotates the value `false` when the sandbox is reused. +* **L44**: We include the final response under `response` key as part of the `handler` subsegment. ???+ info If you want to understand how the Lambda execution environment (sandbox) works and why cold starts can occur, see this [blog series on Lambda performance](https://aws.amazon.com/blogs/compute/operating-lambda-performance-optimization-part-1/){target="_blank"}. diff --git a/docs/utilities/parser.md b/docs/utilities/parser.md index f482dcb0410..d98835a8381 100644 --- a/docs/utilities/parser.md +++ b/docs/utilities/parser.md @@ -11,14 +11,19 @@ This utility provides data parsing and deep validation using [Pydantic](https:// * Defines data in pure Python classes, then parse, validate and extract only what you want * Built-in envelopes to unwrap, extend, and validate popular event sources payloads * Enforces type hints at runtime with user-friendly errors +* Support for Pydantic v1 and v2 ## Getting started ### Install +Powertools for AWS Lambda (Python) supports Pydantic v1 and v2. Each Pydantic version requires different dependencies before you can use Parser. + +#### Using Pydantic v1 + !!! info "This is not necessary if you're installing Powertools for AWS Lambda (Python) via [Lambda Layer/SAR](../index.md#lambda-layer){target="_blank"}" -Add `aws-lambda-powertools[parser]` as a dependency in your preferred tool: _e.g._, _requirements.txt_, _pyproject.toml_. This will ensure you have the required dependencies before using Parser. +Add `aws-lambda-powertools[parser]` as a dependency in your preferred tool: _e.g._, _requirements.txt_, _pyproject.toml_. ???+ warning This will increase the compressed package size by >10MB due to the Pydantic dependency. @@ -28,6 +33,12 @@ Add `aws-lambda-powertools[parser]` as a dependency in your preferred tool: _e.g Pip example: `SKIP_CYTHON=1 pip install --no-binary pydantic aws-lambda-powertools[parser]` +#### Using Pydantic v2 + +You need to bring Pydantic v2.0.3 or later as an external dependency. Note that [we suppress Pydantic v2 deprecation warnings](https://github.com/aws-powertools/powertools-lambda-python/issues/2672){target="_blank"} to reduce noise and optimize log costs. + +Add `aws-lambda-powertools` and `pydantic>=2.0.3` as a dependency in your preferred tool: _e.g._, _requirements.txt_, _pyproject.toml_. + ### Defining models You can define models to parse incoming events by inheriting from `BaseModel`. @@ -45,7 +56,7 @@ class Order(BaseModel): id: int description: str items: List[OrderItem] # nesting models are supported - optional_field: Optional[str] # this field may or may not be available when parsing + optional_field: Optional[str] = None # this field may or may not be available when parsing ``` These are simply Python classes that inherit from BaseModel. **Parser** enforces type hints declared in your model at runtime. @@ -79,7 +90,7 @@ class Order(BaseModel): id: int description: str items: List[OrderItem] # nesting models are supported - optional_field: Optional[str] # this field may or may not be available when parsing + optional_field: Optional[str] = None # this field may or may not be available when parsing @event_parser(model=Order) @@ -124,7 +135,7 @@ class Order(BaseModel): id: int description: str items: List[OrderItem] # nesting models are supported - optional_field: Optional[str] # this field may or may not be available when parsing + optional_field: Optional[str] = None # this field may or may not be available when parsing payload = { diff --git a/examples/logger/sam/template.yaml b/examples/logger/sam/template.yaml index 2472b332651..a9842f53348 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:37 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:38 Resources: LoggerLambdaHandlerExample: diff --git a/examples/metrics/sam/template.yaml b/examples/metrics/sam/template.yaml index c49b9e0f733..c48ac3dbaa1 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:37 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:38 Resources: CaptureLambdaHandlerExample: diff --git a/examples/tracer/sam/template.yaml b/examples/tracer/sam/template.yaml index 6cc811d1b2a..1d22b83865b 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:37 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:38 Resources: CaptureLambdaHandlerExample: diff --git a/package-lock.json b/package-lock.json index 8921e96a70a..64ddf8ac968 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,13 +11,13 @@ "package-lock.json": "^1.0.0" }, "devDependencies": { - "aws-cdk": "^2.87.0" + "aws-cdk": "^2.88.0" } }, "node_modules/aws-cdk": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.87.0.tgz", - "integrity": "sha512-dBm74nl3dMUxoAzgjcfKnzJyoVNIV//B1sqDN11cC3LXEflYapcBxPxZHAyGcRXg5dW3m14dMdKVQfmt4N970g==", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.88.0.tgz", + "integrity": "sha512-7Tj0uusA2nsEOsqkd4kB5vmzciz7l/eGBN5a+Ce4/CCcoe4ZCvT85L+T6tK0aohUTLZTAlTPBceH34RN5iMYpA==", "dev": true, "bin": { "cdk": "bin/cdk" @@ -51,9 +51,9 @@ }, "dependencies": { "aws-cdk": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.87.0.tgz", - "integrity": "sha512-dBm74nl3dMUxoAzgjcfKnzJyoVNIV//B1sqDN11cC3LXEflYapcBxPxZHAyGcRXg5dW3m14dMdKVQfmt4N970g==", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.88.0.tgz", + "integrity": "sha512-7Tj0uusA2nsEOsqkd4kB5vmzciz7l/eGBN5a+Ce4/CCcoe4ZCvT85L+T6tK0aohUTLZTAlTPBceH34RN5iMYpA==", "dev": true, "requires": { "fsevents": "2.3.2" diff --git a/package.json b/package.json index 643bec22883..396ea6c7921 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.87.0" + "aws-cdk": "^2.88.0" }, "dependencies": { "package-lock.json": "^1.0.0" diff --git a/poetry.lock b/poetry.lock index d38f6010bb4..db21a2db772 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1331,13 +1331,13 @@ mkdocs = ">=0.17" [[package]] name = "mkdocs-material" -version = "9.1.18" +version = "9.1.19" description = "Documentation that simply works" optional = false python-versions = ">=3.7" files = [ - {file = "mkdocs_material-9.1.18-py3-none-any.whl", hash = "sha256:5bcf8fb79ac2f253c0ffe93fa181cba87718c6438f459dc4180ac7418cc9a450"}, - {file = "mkdocs_material-9.1.18.tar.gz", hash = "sha256:981dd39979723d4cda7cfc77bbbe5e54922d5761a7af23fb8ba9edb52f114b13"}, + {file = "mkdocs_material-9.1.19-py3-none-any.whl", hash = "sha256:fb0a149294b319aedf36983919d8c40c9e566db21ead16258e20ebd2e6c0961c"}, + {file = "mkdocs_material-9.1.19.tar.gz", hash = "sha256:73b94b08c765e92a80645aac58d6a741fc5f587deec2b715489c714827b15a6f"}, ] [package.dependencies] @@ -1526,13 +1526,13 @@ typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.9\""} [[package]] name = "mypy-boto3-s3" -version = "1.28.3" -description = "Type annotations for boto3.S3 1.28.3 service generated with mypy-boto3-builder 7.14.6" +version = "1.28.8" +description = "Type annotations for boto3.S3 1.28.8 service generated with mypy-boto3-builder 7.15.1" optional = false python-versions = ">=3.7" files = [ - {file = "mypy-boto3-s3-1.28.3.tar.gz", hash = "sha256:c2b454413126079a6434a3e05a75cb4cad947c7624e53ba3846cb829a2230c4b"}, - {file = "mypy_boto3_s3-1.28.3-py3-none-any.whl", hash = "sha256:58d4e65c9dabbdc9a8896f7cd3d6475f4331183220a2b41ba1862bf5b3c592bc"}, + {file = "mypy-boto3-s3-1.28.8.tar.gz", hash = "sha256:c9ed17fee2c0e2edeb2966b3796af7b349dcc4eeee54dbd59a269fdb9418eb55"}, + {file = "mypy_boto3_s3-1.28.8-py3-none-any.whl", hash = "sha256:75b929c517c5ad8f97c14dfba5f8521db569157dc4ac76a07a178805777cff8c"}, ] [package.dependencies] @@ -1540,13 +1540,13 @@ typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.9\""} [[package]] name = "mypy-boto3-secretsmanager" -version = "1.28.3" -description = "Type annotations for boto3.SecretsManager 1.28.3 service generated with mypy-boto3-builder 7.14.6" +version = "1.28.3.post2" +description = "Type annotations for boto3.SecretsManager 1.28.3 service generated with mypy-boto3-builder 7.15.0" optional = false python-versions = ">=3.7" files = [ - {file = "mypy-boto3-secretsmanager-1.28.3.tar.gz", hash = "sha256:d8e07c8d33392a626b174b6681df3b4c9dfe6ea76de00bda891720d26e63889a"}, - {file = "mypy_boto3_secretsmanager-1.28.3-py3-none-any.whl", hash = "sha256:c662b6ea74a218c801f692012d1d86e0f7f9f9fc6d3a53e7d6e99fae7d87e2c1"}, + {file = "mypy-boto3-secretsmanager-1.28.3.post2.tar.gz", hash = "sha256:f359f6446ac856d0887e40cb0f5bc6e0a60873524be5dd4b68be1d0fc4ac513e"}, + {file = "mypy_boto3_secretsmanager-1.28.3.post2-py3-none-any.whl", hash = "sha256:3a5e5619ee945f244d2dfefcb382c85874171a18b46f75403465622095284d25"}, ] [package.dependencies] @@ -2510,24 +2510,24 @@ test = ["mypy", "pytest", "typing-extensions"] [[package]] name = "types-python-dateutil" -version = "2.8.19.13" +version = "2.8.19.14" description = "Typing stubs for python-dateutil" optional = false python-versions = "*" files = [ - {file = "types-python-dateutil-2.8.19.13.tar.gz", hash = "sha256:09a0275f95ee31ce68196710ed2c3d1b9dc42e0b61cc43acc369a42cb939134f"}, - {file = "types_python_dateutil-2.8.19.13-py3-none-any.whl", hash = "sha256:0b0e7c68e7043b0354b26a1e0225cb1baea7abb1b324d02b50e2d08f1221043f"}, + {file = "types-python-dateutil-2.8.19.14.tar.gz", hash = "sha256:1f4f10ac98bb8b16ade9dbee3518d9ace017821d94b057a425b069f834737f4b"}, + {file = "types_python_dateutil-2.8.19.14-py3-none-any.whl", hash = "sha256:f977b8de27787639986b4e28963263fd0e5158942b3ecef91b9335c130cb1ce9"}, ] [[package]] name = "types-requests" -version = "2.31.0.1" +version = "2.31.0.2" description = "Typing stubs for requests" optional = false python-versions = "*" files = [ - {file = "types-requests-2.31.0.1.tar.gz", hash = "sha256:3de667cffa123ce698591de0ad7db034a5317457a596eb0b4944e5a9d9e8d1ac"}, - {file = "types_requests-2.31.0.1-py3-none-any.whl", hash = "sha256:afb06ef8f25ba83d59a1d424bd7a5a939082f94b94e90ab5e6116bd2559deaa3"}, + {file = "types-requests-2.31.0.2.tar.gz", hash = "sha256:6aa3f7faf0ea52d728bb18c0a0d1522d9bfd8c72d26ff6f61bfc3d06a411cf40"}, + {file = "types_requests-2.31.0.2-py3-none-any.whl", hash = "sha256:56d181c85b5925cbc59f4489a57e72a8b2166f18273fd8ba7b6fe0c0b986f12a"}, ] [package.dependencies] @@ -2749,4 +2749,4 @@ validation = ["fastjsonschema"] [metadata] lock-version = "2.0" python-versions = "^3.7.4" -content-hash = "a5c056fc77c478f4afcacebd74b687f8c8417fe53956fefc0526c407ebfd5424" +content-hash = "b3614829c4901603d139ee5cdb4b17d3a6c66877ced79779b92d26573f4dd84f" diff --git a/pyproject.toml b/pyproject.toml index b82bf9ddba0..c83d9b0a04a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aws_lambda_powertools" -version = "2.19.0" +version = "2.21.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"] @@ -64,11 +64,11 @@ mypy-boto3-lambda = "^1.28.0" mypy-boto3-logs = "^1.28.1" mypy-boto3-secretsmanager = "^1.28.3" mypy-boto3-ssm = "^1.28.0" -mypy-boto3-s3 = "^1.28.3" +mypy-boto3-s3 = "^1.28.8" mypy-boto3-xray = "^1.28.0" types-requests = "^2.31.0" typing-extensions = "^4.6.2" -mkdocs-material = "^9.1.17" +mkdocs-material = "^9.1.19" filelock = "^3.12.2" checksumdir = "^1.2.0" mypy-boto3-appconfigdata = "^1.28.0" @@ -161,6 +161,15 @@ markers = [ "perf: marks perf tests to be deselected (deselect with '-m \"not perf\"')", ] +# MAINTENANCE: Remove these lines when drop support to Pydantic v1 +filterwarnings=[ + "ignore:.*The `parse_obj` method is deprecated*:DeprecationWarning", + "ignore:.*The `parse_raw` method is deprecated*:DeprecationWarning", + "ignore:.*load_str_bytes is deprecated*:DeprecationWarning", + "ignore:.*The `dict` method is deprecated; use `model_dump` instead*:DeprecationWarning", + "ignore:.*Pydantic V1 style `@validator` validators are deprecated*:DeprecationWarning" +] + [build-system] requires = ["poetry-core>=1.3.2"] build-backend = "poetry.core.masonry.api" diff --git a/ruff.toml b/ruff.toml index f3a50abc720..424040ede1f 100644 --- a/ruff.toml +++ b/ruff.toml @@ -68,3 +68,4 @@ split-on-trailing-comma = true "tests/e2e/utils/data_builder/__init__.py" = ["F401"] "tests/e2e/utils/data_fetcher/__init__.py" = ["F401"] "aws_lambda_powertools/utilities/data_classes/s3_event.py" = ["A003"] +"aws_lambda_powertools/utilities/parser/models/__init__.py" = ["E402"] diff --git a/tests/functional/batch/sample_models.py b/tests/functional/batch/sample_models.py index 556ff0ebf8a..72029e154d5 100644 --- a/tests/functional/batch/sample_models.py +++ b/tests/functional/batch/sample_models.py @@ -35,12 +35,15 @@ class OrderDynamoDB(BaseModel): # so Pydantic can auto-initialize nested Order model @validator("Message", pre=True) def transform_message_to_dict(cls, value: Dict[Literal["S"], str]): - return json.loads(value["S"]) + try: + return json.loads(value["S"]) + except TypeError: + raise ValueError class OrderDynamoDBChangeRecord(DynamoDBStreamChangedRecordModel): - NewImage: Optional[OrderDynamoDB] - OldImage: Optional[OrderDynamoDB] + NewImage: Optional[OrderDynamoDB] = None + OldImage: Optional[OrderDynamoDB] = None class OrderDynamoDBRecord(DynamoDBStreamRecordModel): diff --git a/tests/functional/parser/conftest.py b/tests/functional/parser/conftest.py index 34199a322b2..41347bc5fa9 100644 --- a/tests/functional/parser/conftest.py +++ b/tests/functional/parser/conftest.py @@ -6,6 +6,15 @@ from aws_lambda_powertools.utilities.parser import BaseEnvelope +@pytest.fixture +def pydanticv2_only(): + from pydantic import __version__ + + version = __version__.split(".") + if version[0] != "2": + pytest.skip("pydanticv2 test only") + + @pytest.fixture def dummy_event(): return {"payload": {"message": "hello world"}} diff --git a/tests/functional/parser/test_parser.py b/tests/functional/parser/test_parser.py index c439134071c..1f948655917 100644 --- a/tests/functional/parser/test_parser.py +++ b/tests/functional/parser/test_parser.py @@ -1,6 +1,7 @@ import json from typing import Dict, Union +import pydantic import pytest from aws_lambda_powertools.utilities.parser import ( @@ -53,6 +54,27 @@ def handle_no_envelope(event: Dict, _: LambdaContext): handle_no_envelope(dummy_event["payload"], LambdaContext()) +@pytest.mark.usefixtures("pydanticv2_only") +def test_pydanticv2_validation(): + class FakeModel(pydantic.BaseModel): + region: str + event_name: str + version: int + + # WHEN using the validator for v2 + @pydantic.field_validator("version", mode="before") + def validate_field(cls, value): + return int(value) + + event_raw = {"region": "us-east-1", "event_name": "aws-powertools", "version": "10"} + event_parsed = FakeModel(**event_raw) + + # THEN parse the event as expected + assert event_parsed.region == event_raw["region"] + assert event_parsed.event_name == event_raw["event_name"] + assert event_parsed.version == int(event_raw["version"]) + + @pytest.mark.parametrize("invalid_schema", [None, str, bool(), [], (), object]) def test_parser_with_invalid_schema_type(dummy_event, invalid_schema): @event_parser(model=invalid_schema) diff --git a/tests/functional/test_utilities_batch.py b/tests/functional/test_utilities_batch.py index 1831ef973d9..e146d65744f 100644 --- a/tests/functional/test_utilities_batch.py +++ b/tests/functional/test_utilities_batch.py @@ -501,8 +501,8 @@ def transform_message_to_dict(cls, value: Dict[Literal["S"], str]): return json.loads(value["S"]) class OrderDynamoDBChangeRecord(DynamoDBStreamChangedRecordModel): - NewImage: Optional[OrderDynamoDB] - OldImage: Optional[OrderDynamoDB] + NewImage: Optional[OrderDynamoDB] = None + OldImage: Optional[OrderDynamoDB] = None class OrderDynamoDBRecord(DynamoDBStreamRecordModel): dynamodb: OrderDynamoDBChangeRecord @@ -545,8 +545,8 @@ def transform_message_to_dict(cls, value: Dict[Literal["S"], str]): return json.loads(value["S"]) class OrderDynamoDBChangeRecord(DynamoDBStreamChangedRecordModel): - NewImage: Optional[OrderDynamoDB] - OldImage: Optional[OrderDynamoDB] + NewImage: Optional[OrderDynamoDB] = None + OldImage: Optional[OrderDynamoDB] = None class OrderDynamoDBRecord(DynamoDBStreamRecordModel): dynamodb: OrderDynamoDBChangeRecord diff --git a/tests/unit/parser/schemas.py b/tests/unit/parser/schemas.py index 1da0213ff45..fd2f29697dc 100644 --- a/tests/unit/parser/schemas.py +++ b/tests/unit/parser/schemas.py @@ -22,8 +22,8 @@ class MyDynamoBusiness(BaseModel): class MyDynamoScheme(DynamoDBStreamChangedRecordModel): - NewImage: Optional[MyDynamoBusiness] - OldImage: Optional[MyDynamoBusiness] + NewImage: Optional[MyDynamoBusiness] = None + OldImage: Optional[MyDynamoBusiness] = None class MyDynamoDBStreamRecordModel(DynamoDBStreamRecordModel): diff --git a/tests/unit/parser/test_apigw.py b/tests/unit/parser/test_apigw.py index a65d181cc54..b2ed294ff7a 100644 --- a/tests/unit/parser/test_apigw.py +++ b/tests/unit/parser/test_apigw.py @@ -138,7 +138,9 @@ def test_apigw_event_with_invalid_websocket_request(): errors = err.value.errors() assert len(errors) == 1 expected_msg = "messageId is available only when the `eventType` is `MESSAGE`" - assert errors[0]["msg"] == expected_msg + # Pydantic v2 adds "Value error," to the error string. + # So to maintain compatibility with v1 and v2, we've changed the way we test this. + assert expected_msg in errors[0]["msg"] assert expected_msg in str(err.value) diff --git a/tests/unit/parser/test_cloudwatch.py b/tests/unit/parser/test_cloudwatch.py index bc8bf0776f9..48d296c40ef 100644 --- a/tests/unit/parser/test_cloudwatch.py +++ b/tests/unit/parser/test_cloudwatch.py @@ -86,7 +86,9 @@ def test_handle_invalid_cloudwatch_trigger_event_no_envelope(): with pytest.raises(ValidationError) as context: CloudWatchLogsModel(**raw_event) - assert context.value.errors()[0]["msg"] == "unable to decompress data" + # Pydantic v2 adds "Value error," to the error string. + # So to maintain compatibility with v1 and v2, we've changed the way we test this. + assert "unable to decompress data" in context.value.errors()[0]["msg"] def test_handle_invalid_event_with_envelope():