diff --git a/.github/workflows/bootstrap_region.yml b/.github/workflows/bootstrap_region.yml index 96911a8c9b7..fafa01fbc1a 100644 --- a/.github/workflows/bootstrap_region.yml +++ b/.github/workflows/bootstrap_region.yml @@ -44,18 +44,18 @@ jobs: environment: layer-${{ inputs.environment }} steps: - name: checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.sha }} - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "22" - name: Setup dependencies - uses: aws-powertools/actions/.github/actions/cached-node-modules@828e78a26eee3554dc2e1d96048004548fbb169f + uses: aws-powertools/actions/.github/actions/cached-node-modules@4bf64a4072489399fbf39b78f558eeeed6767189 - id: credentials name: AWS Credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c with: aws-region: ${{ inputs.region }} role-to-assume: ${{ secrets.REGION_IAM_ROLE }} @@ -96,14 +96,14 @@ jobs: steps: - id: credentials name: AWS Credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: aws-region: us-east-1 role-to-assume: ${{ secrets.REGION_IAM_ROLE }} mask-aws-account-id: true - id: go-setup name: Setup Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: '>=1.23.0' - id: go-env diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a0ae48f9f6a..377e2bdbe2a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index f91282cb627..aa2c90007c0 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -20,6 +20,6 @@ jobs: pull-requests: write steps: - name: 'Checkout Repository' - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: 'Dependency Review' uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 diff --git a/.github/workflows/layer_govcloud.yml b/.github/workflows/layer_govcloud.yml index 97796a9f896..bcb0005308d 100644 --- a/.github/workflows/layer_govcloud.yml +++ b/.github/workflows/layer_govcloud.yml @@ -60,7 +60,7 @@ jobs: environment: Prod (Readonly) steps: - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} aws-region: us-east-1 @@ -118,7 +118,7 @@ jobs: SHA=$(jq -r '.Content.CodeSha256' '${{ matrix.layer }}_${{ matrix.arch }}.json') test "$(openssl dgst -sha256 -binary ${{ matrix.layer }}_${{ matrix.arch }}.zip | openssl enc -base64)" == "$SHA" && echo "SHA OK: ${SHA}" || exit 1 - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} aws-region: us-gov-east-1 @@ -188,7 +188,7 @@ jobs: SHA=$(jq -r '.Content.CodeSha256' '${{ matrix.layer }}_${{ matrix.arch }}.json') test "$(openssl dgst -sha256 -binary ${{ matrix.layer }}_${{ matrix.arch }}.zip | openssl enc -base64)" == "$SHA" && echo "SHA OK: ${SHA}" || exit 1 - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} aws-region: us-gov-west-1 diff --git a/.github/workflows/layer_govcloud_python313.yml b/.github/workflows/layer_govcloud_python313.yml index 9128fd9a264..285ed0081ea 100644 --- a/.github/workflows/layer_govcloud_python313.yml +++ b/.github/workflows/layer_govcloud_python313.yml @@ -55,7 +55,7 @@ jobs: environment: Prod (Readonly) steps: - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} aws-region: us-east-1 @@ -108,7 +108,7 @@ jobs: SHA=$(jq -r '.Content.CodeSha256' '${{ matrix.layer }}_${{ matrix.arch }}.json') test "$(openssl dgst -sha256 -binary ${{ matrix.layer }}_${{ matrix.arch }}.zip | openssl enc -base64)" == "$SHA" && echo "SHA OK: ${SHA}" || exit 1 - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} aws-region: us-gov-east-1 @@ -173,7 +173,7 @@ jobs: SHA=$(jq -r '.Content.CodeSha256' '${{ matrix.layer }}_${{ matrix.arch }}.json') test "$(openssl dgst -sha256 -binary ${{ matrix.layer }}_${{ matrix.arch }}.zip | openssl enc -base64)" == "$SHA" && echo "SHA OK: ${SHA}" || exit 1 - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} aws-region: us-gov-west-1 diff --git a/.github/workflows/layer_govcloud_verify.yml b/.github/workflows/layer_govcloud_verify.yml index 051a567463c..373029036db 100644 --- a/.github/workflows/layer_govcloud_verify.yml +++ b/.github/workflows/layer_govcloud_verify.yml @@ -40,7 +40,7 @@ jobs: environment: Prod (Readonly) steps: - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} aws-region: us-east-1 @@ -71,7 +71,7 @@ jobs: environment: GovCloud Prod (East) steps: - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} aws-region: us-gov-east-1 @@ -103,7 +103,7 @@ jobs: environment: GovCloud Prod (West) steps: - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} aws-region: us-gov-east-1 diff --git a/.github/workflows/layers_partition_verify.yml b/.github/workflows/layers_partition_verify.yml index 3cb8e68c3c7..4500a8a0ee9 100644 --- a/.github/workflows/layers_partition_verify.yml +++ b/.github/workflows/layers_partition_verify.yml @@ -88,7 +88,7 @@ jobs: - x86_64 steps: - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} aws-region: us-east-1 @@ -138,7 +138,7 @@ jobs: run: | echo 'CONVERTED_REGION=${{ matrix.region }}' | tr 'a-z\-' 'A-Z_' >> "$GITHUB_OUTPUT" - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: ${{ secrets[format('IAM_ROLE_{0}', steps.transform.outputs.CONVERTED_REGION)] }} aws-region: ${{ matrix.region}} diff --git a/.github/workflows/layers_partitions.yml b/.github/workflows/layers_partitions.yml index 515f7071e40..3d6246e5197 100644 --- a/.github/workflows/layers_partitions.yml +++ b/.github/workflows/layers_partitions.yml @@ -85,7 +85,7 @@ jobs: - x86_64 steps: - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} aws-region: us-east-1 @@ -150,7 +150,7 @@ jobs: run: | echo 'CONVERTED_REGION=${{ matrix.region }}' | tr 'a-z\-' 'A-Z_' >> "$GITHUB_OUTPUT" - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: ${{ secrets[format('IAM_ROLE_{0}', steps.transform.outputs.CONVERTED_REGION)] }} aws-region: ${{ matrix.region}} diff --git a/.github/workflows/ossf_scorecard.yml b/.github/workflows/ossf_scorecard.yml index d986af34dd3..430767741d7 100644 --- a/.github/workflows/ossf_scorecard.yml +++ b/.github/workflows/ossf_scorecard.yml @@ -22,12 +22,12 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 with: results_file: results.sarif results_format: sarif @@ -43,6 +43,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4 + uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 with: sarif_file: results.sarif diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index f4fd53b8a23..8e4e0fb75c8 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -59,7 +59,7 @@ jobs: artifact_name: ${{ steps.seal_source_code.outputs.artifact_name }} RELEASE_VERSION: ${{ steps.release_version.outputs.RELEASE_VERSION }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ env.RELEASE_COMMIT }} @@ -99,7 +99,7 @@ jobs: contents: read steps: # NOTE: we need actions/checkout to configure git first (pre-commit hooks in make dev) - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ env.RELEASE_COMMIT }} @@ -115,7 +115,7 @@ jobs: - name: Install poetry run: pipx install git+https://github.com/python-poetry/poetry@bd500dd3bdfaec3de6894144c9cedb3a9358be84 # v2.0.1 - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" cache: "poetry" @@ -140,7 +140,7 @@ jobs: attestation_hashes: ${{ steps.encoded_hash.outputs.attestation_hashes }} steps: # NOTE: we need actions/checkout to configure git first (pre-commit hooks in make dev) - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ env.RELEASE_COMMIT }} @@ -153,7 +153,7 @@ jobs: - name: Install poetry run: pipx install git+https://github.com/python-poetry/poetry@bd500dd3bdfaec3de6894144c9cedb3a9358be84 # v2.0.1 - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" cache: "poetry" @@ -209,7 +209,7 @@ jobs: RELEASE_VERSION: ${{ needs.seal.outputs.RELEASE_VERSION }} steps: # NOTE: we need actions/checkout in order to use our local actions (e.g., ./.github/actions) - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ env.RELEASE_COMMIT }} @@ -221,7 +221,7 @@ jobs: - name: Upload to PyPi prod if: ${{ !inputs.skip_pypi }} - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 # Creates a PR with the latest version we've just released # since our trunk is protected against any direct pushes from automation @@ -233,7 +233,7 @@ jobs: runs-on: ubuntu-latest steps: # NOTE: we need actions/checkout to authenticate and configure git first - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ env.RELEASE_COMMIT }} diff --git a/.github/workflows/publish_v3_layer.yml b/.github/workflows/publish_v3_layer.yml index e61462b572c..ab2f43285e5 100644 --- a/.github/workflows/publish_v3_layer.yml +++ b/.github/workflows/publish_v3_layer.yml @@ -108,7 +108,7 @@ jobs: working-directory: ./layer_v3 steps: - name: checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ env.RELEASE_COMMIT }} @@ -123,11 +123,11 @@ jobs: pipx install git+https://github.com/python-poetry/poetry@bd500dd3bdfaec3de6894144c9cedb3a9358be84 # v2.0.1 pipx inject poetry git+https://github.com/python-poetry/poetry-plugin-export@8c83d26603ca94f2e203bfded7b6d7f530960e06 # v1.8.0 - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "18.20.4" - name: Setup python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} cache: "pip" @@ -139,14 +139,14 @@ jobs: pip install --require-hashes -r requirements.txt - name: Set up QEMU - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 with: platforms: arm64 # NOTE: we need QEMU to build Layer against a different architecture (e.g., ARM) - name: Set up Docker Buildx id: builder - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 with: driver: docker platforms: linux/amd64,linux/arm64 @@ -264,7 +264,7 @@ jobs: pages: none steps: - name: Checkout repository # reusable workflows start clean, so we need to checkout again - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ env.RELEASE_COMMIT }} diff --git a/.github/workflows/quality_check.yml b/.github/workflows/quality_check.yml index ce3f5612db1..d434dbc841e 100644 --- a/.github/workflows/quality_check.yml +++ b/.github/workflows/quality_check.yml @@ -52,11 +52,11 @@ jobs: permissions: contents: read # checkout code only steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install poetry run: pipx install poetry - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/quality_check_docs.yml b/.github/workflows/quality_check_docs.yml index e5b67347dbd..2506412fc5d 100644 --- a/.github/workflows/quality_check_docs.yml +++ b/.github/workflows/quality_check_docs.yml @@ -35,9 +35,9 @@ jobs: permissions: contents: read # checkout code only steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: 3.14 - name: Install doc generation dependencies diff --git a/.github/workflows/quality_code_cdk_constructor.yml b/.github/workflows/quality_code_cdk_constructor.yml index e33106542c1..4dd9a631c51 100644 --- a/.github/workflows/quality_code_cdk_constructor.yml +++ b/.github/workflows/quality_code_cdk_constructor.yml @@ -42,22 +42,22 @@ jobs: run: working-directory: ./layer_v3/layer_constructors steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install poetry run: pipx install poetry - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} cache: "poetry" - name: Set up QEMU - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 with: platforms: arm64 # NOTE: we need QEMU to build Layer against a different architecture (e.g., ARM) - name: Set up Docker Buildx id: builder - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 with: driver: docker platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 6ca115c2460..a80f2273a05 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -27,4 +27,4 @@ jobs: permissions: contents: write # create release in draft mode steps: - - uses: release-drafter/release-drafter@693d20e7c1ce1a81d3a41962f85914253b518449 # v7.3.1 + - uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7.7.0 diff --git a/.github/workflows/release-v3.yml b/.github/workflows/release-v3.yml index 9eaf910682e..45e0ccb862d 100644 --- a/.github/workflows/release-v3.yml +++ b/.github/workflows/release-v3.yml @@ -89,7 +89,7 @@ jobs: RELEASE_VERSION="${RELEASE_TAG_VERSION:1}" echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "$GITHUB_OUTPUT" - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ env.RELEASE_COMMIT }} @@ -124,7 +124,7 @@ jobs: contents: read steps: # NOTE: we need actions/checkout to configure git first (pre-commit hooks in make dev) - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ env.RELEASE_COMMIT }} @@ -140,7 +140,7 @@ jobs: - name: Install poetry run: pipx install git+https://github.com/python-poetry/poetry@bd500dd3bdfaec3de6894144c9cedb3a9358be84 # v2.0.1 - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" cache: "poetry" @@ -165,7 +165,7 @@ jobs: attestation_hashes: ${{ steps.encoded_hash.outputs.attestation_hashes }} steps: # NOTE: we need actions/checkout to configure git first (pre-commit hooks in make dev) - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ env.RELEASE_COMMIT }} @@ -178,7 +178,7 @@ jobs: - name: Install poetry run: pipx install git+https://github.com/python-poetry/poetry@bd500dd3bdfaec3de6894144c9cedb3a9358be84 # v2.0.1 - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" cache: "poetry" @@ -234,7 +234,7 @@ jobs: RELEASE_VERSION: ${{ needs.seal.outputs.RELEASE_VERSION }} steps: # NOTE: we need actions/checkout in order to use our local actions (e.g., ./.github/actions) - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ env.RELEASE_COMMIT }} @@ -246,12 +246,12 @@ jobs: - name: Upload to PyPi prod if: ${{ !inputs.skip_pypi }} - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 # PyPi test maintenance affected us numerous times, leaving for history purposes # - name: Upload to PyPi test # if: ${{ !inputs.skip_pypi }} - # uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + # uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 # with: # repository-url: https://test.pypi.org/legacy/ @@ -268,7 +268,7 @@ jobs: contents: write steps: # NOTE: we need actions/checkout to authenticate and configure git first - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ env.RELEASE_COMMIT }} @@ -312,7 +312,7 @@ jobs: runs-on: ubuntu-latest steps: # NOTE: we need actions/checkout to authenticate and configure git first - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ env.RELEASE_COMMIT }} @@ -368,7 +368,7 @@ jobs: env: RELEASE_VERSION: ${{ needs.seal.outputs.RELEASE_VERSION }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ env.RELEASE_COMMIT }} - name: Restore sealed source code diff --git a/.github/workflows/reusable_deploy_v3_layer_stack.yml b/.github/workflows/reusable_deploy_v3_layer_stack.yml index dc5adefb695..ef0bb9a5cc2 100644 --- a/.github/workflows/reusable_deploy_v3_layer_stack.yml +++ b/.github/workflows/reusable_deploy_v3_layer_stack.yml @@ -142,7 +142,7 @@ jobs: has_arm64_support: "true" steps: - name: checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ env.RELEASE_COMMIT }} @@ -157,17 +157,17 @@ jobs: pipx install git+https://github.com/python-poetry/poetry@bd500dd3bdfaec3de6894144c9cedb3a9358be84 # v2.0.1 pipx inject poetry git+https://github.com/python-poetry/poetry-plugin-export@8c83d26603ca94f2e203bfded7b6d7f530960e06 # v1.8.0 - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: aws-region: ${{ matrix.region }} role-to-assume: ${{ secrets.AWS_LAYERS_ROLE_ARN }} mask-aws-account-id: true - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "18.20.4" - name: Setup python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} cache: "pip" diff --git a/.github/workflows/reusable_deploy_v3_sar.yml b/.github/workflows/reusable_deploy_v3_sar.yml index 6420bc080ae..f72519ecc5d 100644 --- a/.github/workflows/reusable_deploy_v3_sar.yml +++ b/.github/workflows/reusable_deploy_v3_sar.yml @@ -75,7 +75,7 @@ jobs: python-version: ["3.10","3.11","3.12","3.13","3.14"] steps: - name: checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ env.RELEASE_COMMIT }} @@ -87,7 +87,7 @@ jobs: - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: aws-region: ${{ env.AWS_REGION }} role-to-assume: ${{ secrets.AWS_LAYERS_ROLE_ARN }} @@ -98,7 +98,7 @@ jobs: # we then jump to our specific SAR Account with the correctly scoped IAM Role # this allows us to have a single trail when a release occurs for a given layer (beta+prod+SAR beta+SAR prod) - name: AWS credentials SAR role - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 id: aws-credentials-sar-role with: aws-access-key-id: ${{ env.AWS_ACCESS_KEY_ID }} @@ -109,7 +109,7 @@ jobs: role-to-assume: ${{ secrets.AWS_SAR_V3_ROLE_ARN }} mask-aws-account-id: true - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: ${{ env.NODE_VERSION }} - name: Download artifact diff --git a/.github/workflows/reusable_publish_changelog.yml b/.github/workflows/reusable_publish_changelog.yml index 2327e52fc5d..cb7d62285f3 100644 --- a/.github/workflows/reusable_publish_changelog.yml +++ b/.github/workflows/reusable_publish_changelog.yml @@ -26,7 +26,7 @@ jobs: pull-requests: write # create PR steps: - name: Checkout repository # reusable workflows start clean, so we need to checkout again - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: "Generate latest changelog" diff --git a/.github/workflows/reusable_publish_docs.yml b/.github/workflows/reusable_publish_docs.yml index 2ab38af74d0..6507e4922f9 100644 --- a/.github/workflows/reusable_publish_docs.yml +++ b/.github/workflows/reusable_publish_docs.yml @@ -42,12 +42,12 @@ jobs: permissions: id-token: write # trade JWT token for AWS credentials in AWS Docs account steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 ref: ${{ inputs.git_ref }} - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Install doc generation dependencies @@ -68,7 +68,7 @@ jobs: env: BRANCH: ${{ inputs.git_ref }} - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: aws-region: us-east-1 role-to-assume: ${{ secrets.AWS_DOCS_ROLE_ARN }} diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml index 50047a884fe..ce1457abc91 100644 --- a/.github/workflows/run-e2e-tests.yml +++ b/.github/workflows/run-e2e-tests.yml @@ -52,17 +52,17 @@ jobs: if: ${{ github.actor != 'dependabot[bot]' && github.repository == 'aws-powertools/powertools-lambda-python' }} steps: - name: "Checkout" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install poetry run: pipx install poetry - name: "Use Python" - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.version }} architecture: "x64" cache: "poetry" - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "20.10.0" - name: Install CDK CLI @@ -72,7 +72,7 @@ jobs: - name: Install dependencies run: make dev-quality-code - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: ${{ secrets.AWS_TEST_ROLE_ARN }} aws-region: ${{ env.AWS_DEFAULT_REGION }} diff --git a/.github/workflows/secure_workflows.yml b/.github/workflows/secure_workflows.yml index eb3893183a0..b21adc7f5df 100644 --- a/.github/workflows/secure_workflows.yml +++ b/.github/workflows/secure_workflows.yml @@ -30,9 +30,9 @@ jobs: contents: read # checkout code and subsequently GitHub action workflows steps: - name: Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Ensure 3rd party workflows have SHA pinned - uses: zgosalvez/github-actions-ensure-sha-pinned-actions@ca46236c6ce584ae24bc6283ba8dcf4b3ec8a066 # v5.0.4 + uses: zgosalvez/github-actions-ensure-sha-pinned-actions@c5fc58bd0be7a4b94b73ce40250322d5b838a108 # v5.0.7 with: allowlist: | slsa-framework/slsa-github-generator diff --git a/.github/workflows/update_ssm.yml b/.github/workflows/update_ssm.yml index e8c89c4141f..c71eafa1d86 100644 --- a/.github/workflows/update_ssm.yml +++ b/.github/workflows/update_ssm.yml @@ -89,7 +89,7 @@ jobs: run: | echo 'CONVERTED_REGION=${{ matrix.region }}' | tr 'a-z\-' 'A-Z_' >> "$GITHUB_OUTPUT" - id: creds - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: aws-region: ${{ matrix.region }} role-to-assume: ${{ secrets[format('{0}', steps.transform.outputs.CONVERTED_REGION)] }} diff --git a/CHANGELOG.md b/CHANGELOG.md index f2dc360bea2..6f1f07e1bc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,152 @@ # Unreleased + +## [v3.34.0] - 2026-08-10 +## Bug Fixes + +* **event_handler:** omit Content-Type header from OpenAPI ([#8374](https://github.com/aws-powertools/powertools-lambda-python/issues/8374)) + +## Maintenance + +* version bump + + + +## [v3.31.1] - 2026-07-13 +## Bug Fixes + +* EventBridgeEvent data class ([#8301](https://github.com/aws-powertools/powertools-lambda-python/issues/8301)) +* **circuit_breaker:** make probe election per-thread and synchronize local counters ([#8323](https://github.com/aws-powertools/powertools-lambda-python/issues/8323)) +* **circuit_breaker:** normalize handled/ignored exceptions to a validated tuple at construction ([#8319](https://github.com/aws-powertools/powertools-lambda-python/issues/8319)) +* **event_handler:** resolve dependency injection failure with arbitrary return types ([#8334](https://github.com/aws-powertools/powertools-lambda-python/issues/8334)) +* **event_source:** mypy strict mode compliance ([#8332](https://github.com/aws-powertools/powertools-lambda-python/issues/8332)) +* **parser:** fix DynamoDB Stream Lambda invocation record model ([#8321](https://github.com/aws-powertools/powertools-lambda-python/issues/8321)) + +## Features + +* **circuit_breaker:** support composite primary key in DynamoDB persistence ([#8316](https://github.com/aws-powertools/powertools-lambda-python/issues/8316)) + +## Maintenance + +* version bump +* **data-masking:** bump encryption sdk version ([#8335](https://github.com/aws-powertools/powertools-lambda-python/issues/8335)) +* **deps:** bump soupsieve from 2.7 to 2.8.4 in /docs ([#8328](https://github.com/aws-powertools/powertools-lambda-python/issues/8328)) +* **deps:** bump the github-actions group across 1 directory with 8 updates ([#8327](https://github.com/aws-powertools/powertools-lambda-python/issues/8327)) +* **deps:** bump protobuf from 7.34.1 to 7.35.1 ([#8312](https://github.com/aws-powertools/powertools-lambda-python/issues/8312)) +* **deps-dev:** bump types-python-dateutil from 2.9.0.20260508 to 2.9.0.20260518 ([#8311](https://github.com/aws-powertools/powertools-lambda-python/issues/8311)) +* **deps-dev:** bump aws-cdk from 2.1128.1 to 2.1129.0 in the aws-cdk group ([#8324](https://github.com/aws-powertools/powertools-lambda-python/issues/8324)) +* **deps-dev:** bump soupsieve from 2.7 to 2.8.4 ([#8329](https://github.com/aws-powertools/powertools-lambda-python/issues/8329)) +* **deps-dev:** bump requests from 2.33.1 to 2.34.2 ([#8313](https://github.com/aws-powertools/powertools-lambda-python/issues/8313)) +* **deps-dev:** bump filelock from 3.29.0 to 3.29.7 ([#8314](https://github.com/aws-powertools/powertools-lambda-python/issues/8314)) + + + +## [v3.31.0] - 2026-06-29 +## Features + +* adding circuit breaker feature ([#8266](https://github.com/aws-powertools/powertools-lambda-python/issues/8266)) +* **event_handler:** support any Pydantic Field annotation in parameter ([#8305](https://github.com/aws-powertools/powertools-lambda-python/issues/8305)) + +## Maintenance + +* version bump +* **deps:** bump redis from 7.4.0 to 8.0.1 ([#8297](https://github.com/aws-powertools/powertools-lambda-python/issues/8297)) +* **deps-dev:** bump sentry-sdk from 2.62.0 to 2.63.0 ([#8296](https://github.com/aws-powertools/powertools-lambda-python/issues/8296)) +* **deps-dev:** bump boto3-stubs from 1.43.29 to 1.43.36 ([#8295](https://github.com/aws-powertools/powertools-lambda-python/issues/8295)) + + + +## [v3.30.0] - 2026-06-24 +## Bug Fixes + +* **batch:** add optional logger injection for BatchProcessors ([#7553](https://github.com/aws-powertools/powertools-lambda-python/issues/7553)) ([#8272](https://github.com/aws-powertools/powertools-lambda-python/issues/8272)) +* **docs:** update broken AWS Lambda Metadata Endpoint link ([#8209](https://github.com/aws-powertools/powertools-lambda-python/issues/8209)) +* **event-handler:** handle CORS preflight OPTIONS in HttpResolverLocal ([#8268](https://github.com/aws-powertools/powertools-lambda-python/issues/8268)) +* **event-handler:** fix ruff lint violations in event_handler module ([#8208](https://github.com/aws-powertools/powertools-lambda-python/issues/8208)) + +## Code Refactoring + +* **event_handler:** promote HttpResolver to stable ([#8289](https://github.com/aws-powertools/powertools-lambda-python/issues/8289)) + +## Documentation + +* **event-handler:** add import path gotcha for dependency_overrides testing ([#8269](https://github.com/aws-powertools/powertools-lambda-python/issues/8269)) +* **metadata:** fix broken Lambda Metadata Endpoint link ([#8212](https://github.com/aws-powertools/powertools-lambda-python/issues/8212)) + +## Features + +* **event_handler:** allow custom serializer on BedrockAgentResolver ([#8271](https://github.com/aws-powertools/powertools-lambda-python/issues/8271)) + +## Maintenance + +* version bump +* fix minor typos and grammar in docs and comments ([#8245](https://github.com/aws-powertools/powertools-lambda-python/issues/8245)) +* **deps:** bump the github-actions group across 1 directory with 5 updates ([#8256](https://github.com/aws-powertools/powertools-lambda-python/issues/8256)) +* **deps:** bump ujson from 5.12.1 to 5.13.0 ([#8298](https://github.com/aws-powertools/powertools-lambda-python/issues/8298)) +* **deps:** bump pydantic-settings from 2.14.1 to 2.14.2 ([#8299](https://github.com/aws-powertools/powertools-lambda-python/issues/8299)) +* **deps:** bump cryptography from 46.0.7 to 48.0.1 ([#8286](https://github.com/aws-powertools/powertools-lambda-python/issues/8286)) +* **deps:** consolidate Dependabot dependency updates ([#8285](https://github.com/aws-powertools/powertools-lambda-python/issues/8285)) +* **deps:** batch dependency updates ([#8207](https://github.com/aws-powertools/powertools-lambda-python/issues/8207)) +* **deps:** bump gitpython from 3.1.47 to 3.1.50 in /docs ([#8213](https://github.com/aws-powertools/powertools-lambda-python/issues/8213)) +* **deps:** bump urllib3 from 2.6.3 to 2.7.0 in /docs ([#8216](https://github.com/aws-powertools/powertools-lambda-python/issues/8216)) +* **deps:** consolidate dependabot updates (13052026) ([#8228](https://github.com/aws-powertools/powertools-lambda-python/issues/8228)) +* **deps:** bump the github-actions group with 3 updates ([#8221](https://github.com/aws-powertools/powertools-lambda-python/issues/8221)) +* **deps:** bump codecov/codecov-action from 6.0.1 to 7.0.0 in the github-actions group ([#8260](https://github.com/aws-powertools/powertools-lambda-python/issues/8260)) +* **deps:** consolidate all open Dependabot updates ([#8241](https://github.com/aws-powertools/powertools-lambda-python/issues/8241)) +* **deps:** bump the github-actions group with 2 updates ([#8292](https://github.com/aws-powertools/powertools-lambda-python/issues/8292)) +* **deps:** bump pydantic from 2.12.5 to 2.13.4 ([#8252](https://github.com/aws-powertools/powertools-lambda-python/issues/8252)) +* **deps:** update requests requirement from >=2.33.1 to >=2.34.2 in /examples/event_handler_graphql/src ([#8254](https://github.com/aws-powertools/powertools-lambda-python/issues/8254)) +* **deps-dev:** bump aws-cdk from 2.1127.0 to 2.1128.1 in the aws-cdk group ([#8290](https://github.com/aws-powertools/powertools-lambda-python/issues/8290)) +* **deps-dev:** bump the dev-dependencies group across 1 directory with 2 updates ([#8242](https://github.com/aws-powertools/powertools-lambda-python/issues/8242)) +* **deps-dev:** bump aws-cdk from 2.1122.0 to 2.1124.1 in the aws-cdk group across 1 directory ([#8234](https://github.com/aws-powertools/powertools-lambda-python/issues/8234)) +* **deps-dev:** bump boto3-stubs from 1.43.3 to 1.43.24 ([#8265](https://github.com/aws-powertools/powertools-lambda-python/issues/8265)) +* **deps-dev:** bump aws-cdk from 2.1124.1 to 2.1126.0 in the aws-cdk group across 1 directory ([#8255](https://github.com/aws-powertools/powertools-lambda-python/issues/8255)) +* **deps-dev:** bump the dev-dependencies group with 3 updates ([#8261](https://github.com/aws-powertools/powertools-lambda-python/issues/8261)) +* **deps-dev:** bump urllib3 from 2.6.3 to 2.7.0 in /layer_v3 ([#8218](https://github.com/aws-powertools/powertools-lambda-python/issues/8218)) +* **deps-dev:** bump urllib3 from 2.6.3 to 2.7.0 ([#8217](https://github.com/aws-powertools/powertools-lambda-python/issues/8217)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.251.0a0 to 2.259.0a0 ([#8262](https://github.com/aws-powertools/powertools-lambda-python/issues/8262)) +* **deps-dev:** bump sentry-sdk from 2.57.0 to 2.62.0 ([#8263](https://github.com/aws-powertools/powertools-lambda-python/issues/8263)) +* **deps-dev:** bump gitpython from 3.1.47 to 3.1.50 ([#8214](https://github.com/aws-powertools/powertools-lambda-python/issues/8214)) +* **deps-dev:** bump the dev-dependencies group across 1 directory with 2 updates ([#8276](https://github.com/aws-powertools/powertools-lambda-python/issues/8276)) +* **deps-dev:** bump aws-cdk-lib from 2.259.0 to 2.260.0 ([#8294](https://github.com/aws-powertools/powertools-lambda-python/issues/8294)) +* **deps-dev:** bump the dev-dependencies group across 1 directory with 3 updates ([#8293](https://github.com/aws-powertools/powertools-lambda-python/issues/8293)) + + ## [v3.29.0] - 2026-05-04 ## Bug Fixes * **event_handler:** prevent deadlock when async middleware raises before calling next() ([#8196](https://github.com/aws-powertools/powertools-lambda-python/issues/8196)) +* **event_handler:** fix ALB resolver returns when response body is None ([#8194](https://github.com/aws-powertools/powertools-lambda-python/issues/8194)) +* **idempotency:** resolve tech debt issues with falsy responses and Redis persistency ([#8176](https://github.com/aws-powertools/powertools-lambda-python/issues/8176)) +* **parser:** type hints should reflect primitive types support ([#8175](https://github.com/aws-powertools/powertools-lambda-python/issues/8175)) + +## Code Refactoring + +* **event_handler:** extract async logic to a separated file ([#8138](https://github.com/aws-powertools/powertools-lambda-python/issues/8138)) + +## Documentation + +* add lambda templates content ([#8159](https://github.com/aws-powertools/powertools-lambda-python/issues/8159)) + +## Features + +* add AsyncMiddlewareFrame support ([#8158](https://github.com/aws-powertools/powertools-lambda-python/issues/8158)) +* **event-handler:** add _registered_api_adapter_async() internal building block ([#8157](https://github.com/aws-powertools/powertools-lambda-python/issues/8157)) +* **event_handler:** adding resolve async public API ([#8171](https://github.com/aws-powertools/powertools-lambda-python/issues/8171)) +* **event_handler:** adding resolve async internal ([#8170](https://github.com/aws-powertools/powertools-lambda-python/issues/8170)) +* **openapi:** add compute_field support ([#8188](https://github.com/aws-powertools/powertools-lambda-python/issues/8188)) ## Maintenance * version bump +* update aws-encryption-sdk allowed versions ([#8191](https://github.com/aws-powertools/powertools-lambda-python/issues/8191)) +* **deps:** batch dependency updates ([#8184](https://github.com/aws-powertools/powertools-lambda-python/issues/8184)) +* **deps:** bump gitpython from 3.1.44 to 3.1.47 in /docs ([#8172](https://github.com/aws-powertools/powertools-lambda-python/issues/8172)) +* **deps:** batch update dependencies ([#8168](https://github.com/aws-powertools/powertools-lambda-python/issues/8168)) +* **deps:** update requests requirement from >=2.32.0 to >=2.33.1 in /examples/event_handler_graphql/src ([#8189](https://github.com/aws-powertools/powertools-lambda-python/issues/8189)) +* **deps-dev:** bump gitpython from 3.1.44 to 3.1.47 ([#8173](https://github.com/aws-powertools/powertools-lambda-python/issues/8173)) @@ -7666,7 +7803,11 @@ * 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/v3.29.0...HEAD +[Unreleased]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.34.0...HEAD +[v3.34.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.31.1...v3.34.0 +[v3.31.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.31.0...v3.31.1 +[v3.31.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.30.0...v3.31.0 +[v3.30.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.29.0...v3.30.0 [v3.29.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.28.0...v3.29.0 [v3.28.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.27.0...v3.28.0 [v3.27.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.26.0...v3.27.0 diff --git a/aws_lambda_powertools/event_handler/__init__.py b/aws_lambda_powertools/event_handler/__init__.py index 98433b2e29b..2e4c008ec63 100644 --- a/aws_lambda_powertools/event_handler/__init__.py +++ b/aws_lambda_powertools/event_handler/__init__.py @@ -18,7 +18,7 @@ ) from aws_lambda_powertools.event_handler.depends import DependencyResolutionError, Depends from aws_lambda_powertools.event_handler.events_appsync.appsync_events import AppSyncEventsResolver -from aws_lambda_powertools.event_handler.http_resolver import HttpResolverLocal +from aws_lambda_powertools.event_handler.http_resolver import HttpResolver, HttpResolverLocal from aws_lambda_powertools.event_handler.lambda_function_url import ( LambdaFunctionUrlResolver, ) @@ -39,6 +39,7 @@ "CORSConfig", "Depends", "DependencyResolutionError", + "HttpResolver", "HttpResolverLocal", "LambdaFunctionUrlResolver", "Request", diff --git a/aws_lambda_powertools/event_handler/bedrock_agent.py b/aws_lambda_powertools/event_handler/bedrock_agent.py index a49ba38c214..6fef1044b97 100644 --- a/aws_lambda_powertools/event_handler/bedrock_agent.py +++ b/aws_lambda_powertools/event_handler/bedrock_agent.py @@ -99,12 +99,17 @@ def lambda_handler(event, context): current_event: BedrockAgentEvent - def __init__(self, debug: bool = False, enable_validation: bool = True): + def __init__( + self, + debug: bool = False, + enable_validation: bool = True, + serializer: Callable[[dict], str] | None = None, + ): super().__init__( proxy_type=ProxyEventType.BedrockAgentEvent, cors=None, debug=debug, - serializer=None, + serializer=serializer, strip_prefixes=None, enable_validation=enable_validation, json_body_deserializer=None, diff --git a/aws_lambda_powertools/event_handler/events_appsync/appsync_events.py b/aws_lambda_powertools/event_handler/events_appsync/appsync_events.py index ee03db5c625..ad718cca104 100644 --- a/aws_lambda_powertools/event_handler/events_appsync/appsync_events.py +++ b/aws_lambda_powertools/event_handler/events_appsync/appsync_events.py @@ -321,7 +321,7 @@ async def _call_publish_event_async_resolver( if isinstance(ret, Exception) else {"id": e.get("id"), "payload": ret} ) - for e, ret in zip(self.current_event.events, results) + for e, ret in zip(self.current_event.events, results, strict=True) ], ) diff --git a/aws_lambda_powertools/event_handler/http_resolver.py b/aws_lambda_powertools/event_handler/http_resolver.py index da72f6fca4d..7206dbe6f1c 100644 --- a/aws_lambda_powertools/event_handler/http_resolver.py +++ b/aws_lambda_powertools/event_handler/http_resolver.py @@ -1,8 +1,6 @@ from __future__ import annotations import base64 -import inspect -import warnings from typing import TYPE_CHECKING, Any, Callable from urllib.parse import parse_qs @@ -10,10 +8,7 @@ ApiGatewayResolver, BaseRouter, ProxyEventType, - Response, - Route, ) -from aws_lambda_powertools.event_handler.middlewares.async_utils import wrap_middleware_async from aws_lambda_powertools.shared.headers_serializer import BaseHeadersSerializer from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent @@ -166,21 +161,16 @@ def get_remaining_time_in_millis(self) -> int: # pragma: no cover class HttpResolverLocal(ApiGatewayResolver): """ - ASGI-compatible HTTP resolver for local development and testing. + ASGI-compatible HTTP resolver. - This resolver is designed specifically for local development workflows. - It allows you to run your Powertools application locally with any ASGI server + It allows you to run your Powertools application with any ASGI server (uvicorn, hypercorn, daphne, etc.) while maintaining full compatibility with Lambda. The same code works in both environments - locally via ASGI and in Lambda via the handler. + If your Lambda is behind Lambda Web Adapter or any other HTTP proxy, it works seamlessly. Supports both sync and async route handlers. - WARNING - ------- - This is intended for local development and testing only. - The API may change in future releases. Do not use in production environments. - Example ------- ```python @@ -214,11 +204,6 @@ def __init__( strip_prefixes: list[str | Any] | None = None, enable_validation: bool = False, ): - warnings.warn( - "HttpResolverLocal is intended for local development and testing only. " - "The API may change in future releases. Do not use in production environments.", - stacklevel=2, - ) super().__init__( proxy_type=ProxyEventType.APIGatewayProxyEvent, # Use REST API format internally cors=cors, @@ -240,113 +225,13 @@ def _get_base_path(self) -> str: return "" async def _resolve_async(self) -> dict: # type: ignore[override] - """Async version of resolve that supports async handlers.""" - method = self.current_event.http_method.upper() - path = self._remove_prefix(self.current_event.path) - - registered_routes = self._static_routes + self._dynamic_routes - - for route in registered_routes: - if method != route.method: - continue - match_results = route.rule.match(path) - if match_results: - self.append_context(_route=route, _path=path) - route_keys = self._convert_matches_into_route_keys(match_results) - return await self._call_route_async(route, route_keys) - - # Handle not found - return await self._handle_not_found_async() - - async def _call_route_async(self, route: Route, route_arguments: dict[str, str]) -> dict: # type: ignore[override] - """Call route handler, supporting both sync and async handlers.""" - from aws_lambda_powertools.event_handler.api_gateway import ResponseBuilder - - try: - self._reset_processed_stack() - - # Get the route args (may be modified by validation middleware) - self.append_context(_route_args=route_arguments) - - # Run middleware chain (sync for now, handlers can be async) - response = await self._run_middleware_chain_async(route) - - response_builder: ResponseBuilder = ResponseBuilder( - response=response, - serializer=self._serializer, - route=route, - ) - - return response_builder.build(self.current_event, self._cors) - - except Exception as exc: - exc_response_builder = self._call_exception_handler(exc, route) - if exc_response_builder: - return exc_response_builder.build(self.current_event, self._cors) - raise - - async def _run_middleware_chain_async(self, route: Route) -> Response: - """Run the middleware chain, awaiting async handlers.""" - # Build middleware list - all_middlewares: list[Callable[..., Any]] = [] - - # Determine if validation should be enabled for this route - # If route has explicit enable_validation setting, use it; otherwise, use resolver's global setting - route_validation_enabled = ( - route.enable_validation if route.enable_validation is not None else self._enable_validation - ) - - if route_validation_enabled and hasattr(self, "_request_validation_middleware"): - all_middlewares.append(self._request_validation_middleware) - - all_middlewares.extend(self._router_middlewares + route.middlewares) - - if route_validation_enabled and hasattr(self, "_response_validation_middleware"): - all_middlewares.append(self._response_validation_middleware) - - # Create the final handler that calls the route function - async def final_handler(app): - route_args = app.context.get("_route_args", {}) - result = route.func(**route_args) - - # Await if coroutine - if inspect.iscoroutine(result): - result = await result - - return self._to_response(result) - - # Build middleware chain from end to start - next_handler = final_handler - - for middleware in reversed(all_middlewares): - next_handler = wrap_middleware_async(middleware, next_handler) - - return await next_handler(self) - - async def _handle_not_found_async(self, method: str = "", path: str = "") -> dict: # type: ignore[override] - """Handle 404 responses, using custom not_found handler if registered.""" - from http import HTTPStatus - - from aws_lambda_powertools.event_handler.api_gateway import ResponseBuilder - from aws_lambda_powertools.event_handler.exceptions import NotFoundError - - # Check for custom not_found handler - custom_not_found_handler = self.exception_handler_manager.lookup_exception_handler(NotFoundError) - if custom_not_found_handler: - response = custom_not_found_handler(NotFoundError()) - else: - response = Response( - status_code=HTTPStatus.NOT_FOUND.value, - content_type="application/json", - body={"statusCode": HTTPStatus.NOT_FOUND.value, "message": "Not found"}, - ) - - response_builder: ResponseBuilder = ResponseBuilder( - response=response, - serializer=self._serializer, - route=None, - ) + """Thin async resolver: delegates entirely to the parent and serializes to dict. + The parent's _resolve_async handles route matching, CORS preflight, not-found + logic, and exception handling. The only adaptation needed here is converting + the returned ResponseBuilder into the dict format that asgi_handler expects. + """ + response_builder = await super()._resolve_async() return response_builder.build(self.current_event, self._cors) async def asgi_handler(self, scope: dict, receive: Callable, send: Callable) -> None: @@ -455,3 +340,6 @@ async def _send_response(self, send: Callable, response: dict) -> None: "body": body_bytes, }, ) + + +HttpResolver = HttpResolverLocal diff --git a/aws_lambda_powertools/event_handler/openapi/dependant.py b/aws_lambda_powertools/event_handler/openapi/dependant.py index 1e7f4327602..17d98914c73 100644 --- a/aws_lambda_powertools/event_handler/openapi/dependant.py +++ b/aws_lambda_powertools/event_handler/openapi/dependant.py @@ -156,6 +156,7 @@ def get_dependant( call: Callable[..., Any], name: str | None = None, responses: dict[int, OpenAPIResponse] | None = None, + is_dependency: bool = False, ) -> Dependant: """ Returns a dependant model for a handler function. A dependant model is a model that contains @@ -200,6 +201,7 @@ def get_dependant( sub_dependant = get_dependant( path=path, call=depends_instance.dependency, + is_dependency=True, ) dependant.dependencies.append( DependencyParam( @@ -229,7 +231,12 @@ def get_dependant( else: add_param_to_fields(field=param_field, dependant=dependant) - _add_return_annotation(dependant, endpoint_signature) + # A dependency's return value is injected directly into the handler, never serialized as a + # response body nor validated as an OpenAPI parameter — so building a Pydantic schema for it is + # both unused (no reader consumes a sub-dependency's return_param) and actively harmful: it crashes + # for arbitrary return types (e.g. boto3/botocore clients). Only the top-level handler needs it. + if not is_dependency: + _add_return_annotation(dependant, endpoint_signature) _add_extra_responses(dependant, responses) return dependant diff --git a/aws_lambda_powertools/event_handler/openapi/params.py b/aws_lambda_powertools/event_handler/openapi/params.py index 468e2253b39..29c0b2abcdb 100644 --- a/aws_lambda_powertools/event_handler/openapi/params.py +++ b/aws_lambda_powertools/event_handler/openapi/params.py @@ -953,54 +953,52 @@ def get_field_info_response_type(annotation, value) -> tuple[FieldInfo | None, A return get_field_info_and_type_annotation(inner_type, value, False, True) -def _has_discriminator(field_info: FieldInfo) -> bool: - """Check if a FieldInfo has a discriminator.""" - return hasattr(field_info, "discriminator") and field_info.discriminator is not None - - -def _handle_discriminator_with_param( +def _split_location_marker_and_field( annotations: list[FieldInfo], - annotation: Any, -) -> tuple[FieldInfo | None, Any, bool]: +) -> tuple[FieldInfo, FieldInfo]: """ - Handle the special case of Field(discriminator) + Body() combination. + Split a pair of FieldInfo into the Powertools location marker and the plain Pydantic Field. + + A parameter like ``Annotated[str, Field(gt=0), Query()]`` flattens to two FieldInfo: the + location marker (``Path``/``Query``/``Header``/``Body``, a ``Param`` or ``Body`` subclass) and + a plain Pydantic ``Field`` carrying constraints. We keep them apart so the location marker + drives where the value comes from, while the ``Field``'s constraints/metadata still apply. Returns: - tuple of (powertools_annotation, type_annotation, has_discriminator_with_body) + tuple of (location_marker, plain_field) """ - field_obj = None - body_obj = None + location_marker: FieldInfo | None = None + plain_field: FieldInfo | None = None for ann in annotations: - if isinstance(ann, Body): - body_obj = ann - elif _has_discriminator(ann): - field_obj = ann + if isinstance(ann, (Param, Body)): + location_marker = ann + else: + plain_field = ann - if field_obj and body_obj: - # Use Body as the primary annotation, preserve full annotation for validation - return body_obj, annotation, True + if location_marker is None or plain_field is None: + raise AssertionError("Only one FieldInfo can be used per parameter") - raise AssertionError("Only one FieldInfo can be used per parameter") + return location_marker, plain_field def _create_field_info( powertools_annotation: FieldInfo, type_annotation: Any, - has_discriminator_with_body: bool, + preserve_full_annotation: bool, ) -> FieldInfo: """Create or copy FieldInfo based on the annotation type.""" - field_info: FieldInfo - if has_discriminator_with_body: - # For discriminator + Body case, create a new Body instance directly - field_info = Body() + # Copy field_info because we mutate field_info.default later. + field_info = copy_field_info( + field_info=powertools_annotation, + annotation=type_annotation, + ) + if preserve_full_annotation: + # The location marker is paired with a plain Field (constraints or discriminator). + # copy_field_info strips the Pydantic Field out of the metadata, so re-attach the full + # Annotated type here, that's what makes the Field's metadata flow into the TypeAdapter + # that ModelField builds for validation. field_info.annotation = type_annotation - else: - # Copy field_info because we mutate field_info.default later - field_info = copy_field_info( - field_info=powertools_annotation, - annotation=type_annotation, - ) return field_info @@ -1043,13 +1041,16 @@ def get_field_info_annotated_type(annotation, value, is_path_param: bool) -> tup # Determine which annotation to use powertools_annotation: FieldInfo | None = None - has_discriminator_with_param = False + # When a plain Pydantic Field is paired with a location marker (e.g. Field(gt=0) + Query), + # we keep the full Annotated type as the annotation so the Field's metadata still validates. + preserve_full_annotation = False if len(powertools_annotations) == 2: - powertools_annotation, type_annotation, has_discriminator_with_param = _handle_discriminator_with_param( - powertools_annotations, - annotation, - ) + # A location marker (Path/Query/Header/Body) plus a plain Pydantic Field carrying + # constraints, a discriminator, or any other Field setting. The marker says where the + # value comes from; the Field says how to validate it. + powertools_annotation, _ = _split_location_marker_and_field(powertools_annotations) + preserve_full_annotation = True elif len(powertools_annotations) > 1: raise AssertionError("Only one FieldInfo can be used per parameter") else: @@ -1057,18 +1058,19 @@ def get_field_info_annotated_type(annotation, value, is_path_param: bool) -> tup # Reconstruct type_annotation with non-FieldInfo metadata if present # This ensures constraints like Interval are preserved - if other_metadata and not has_discriminator_with_param: + if other_metadata and not preserve_full_annotation: type_annotation = Annotated[(type_annotation, *other_metadata)] # Process the annotation if it exists field_info: FieldInfo | None = None - if isinstance(powertools_annotation, FieldInfo): # pragma: no cover - field_info = _create_field_info(powertools_annotation, type_annotation, has_discriminator_with_param) + if isinstance(powertools_annotation, FieldInfo): + # When pairing a location marker with a plain Field, hand the full Annotated[...] to the + # field so the Field's constraints/discriminator flow into the validating TypeAdapter. + field_annotation = annotation if preserve_full_annotation else type_annotation + field_info = _create_field_info(powertools_annotation, field_annotation, preserve_full_annotation) _set_field_default(field_info, value, is_path_param) - - # Preserve full annotated type for discriminated unions - if _has_discriminator(powertools_annotation): # pragma: no cover - type_annotation = annotation # pragma: no cover + if preserve_full_annotation: + type_annotation = annotation return field_info, type_annotation diff --git a/aws_lambda_powertools/event_handler/openapi/schema_generator.py b/aws_lambda_powertools/event_handler/openapi/schema_generator.py index 6334a3b53cc..096780996f9 100644 --- a/aws_lambda_powertools/event_handler/openapi/schema_generator.py +++ b/aws_lambda_powertools/event_handler/openapi/schema_generator.py @@ -174,9 +174,15 @@ def _build_operation_parameters( continue if _is_pydantic_model_param(field_info): - parameters.extend(_expand_pydantic_model_parameters(field_info)) + generated_parameters = _expand_pydantic_model_parameters(field_info) else: - parameters.append(_create_regular_parameter(param, model_name_map, field_mapping)) + generated_parameters = [_create_regular_parameter(param, model_name_map, field_mapping)] + + parameters.extend( + parameter + for parameter in generated_parameters + if not (parameter["in"] == "header" and parameter["name"].lower() == "content-type") + ) return parameters diff --git a/aws_lambda_powertools/shared/constants.py b/aws_lambda_powertools/shared/constants.py index bc19ff13b30..6c808d38758 100644 --- a/aws_lambda_powertools/shared/constants.py +++ b/aws_lambda_powertools/shared/constants.py @@ -76,3 +76,6 @@ # Idempotency constants IDEMPOTENCY_DISABLED_ENV: str = "POWERTOOLS_IDEMPOTENCY_DISABLED" + +# Circuit breaker constants +CIRCUIT_BREAKER_DISABLED_ENV: str = "POWERTOOLS_CIRCUIT_BREAKER_DISABLED" diff --git a/aws_lambda_powertools/shared/version.py b/aws_lambda_powertools/shared/version.py index 242e0b4ae14..f82ba509c24 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 = "3.29.0" +VERSION = "3.34.0" diff --git a/aws_lambda_powertools/tracing/base.py b/aws_lambda_powertools/tracing/base.py index 8469c075222..a448126eb88 100644 --- a/aws_lambda_powertools/tracing/base.py +++ b/aws_lambda_powertools/tracing/base.py @@ -14,6 +14,7 @@ import numbers import traceback from collections.abc import Generator, Sequence + from contextlib import AbstractAsyncContextManager class BaseSegment(abc.ABC): @@ -99,8 +100,7 @@ def in_subsegment(self, name=None, **kwargs) -> Generator[BaseSegment, None, Non """ @abc.abstractmethod - @contextmanager - def in_subsegment_async(self, name=None, **kwargs) -> Generator[BaseSegment, None, None]: + def in_subsegment_async(self, name=None, **kwargs) -> AbstractAsyncContextManager[BaseSegment]: """Return a subsegment async context manger. Parameters diff --git a/aws_lambda_powertools/utilities/batch/base.py b/aws_lambda_powertools/utilities/batch/base.py index 4cb1337cafd..e1266b69d2a 100644 --- a/aws_lambda_powertools/utilities/batch/base.py +++ b/aws_lambda_powertools/utilities/batch/base.py @@ -14,7 +14,7 @@ import sys from abc import ABC, abstractmethod from enum import Enum -from typing import TYPE_CHECKING, Any, Tuple, Union, overload +from typing import TYPE_CHECKING, Any, Tuple, TypeGuard, Union, overload from aws_lambda_powertools.shared import constants from aws_lambda_powertools.utilities.batch.exceptions import ( @@ -35,6 +35,7 @@ if TYPE_CHECKING: from collections.abc import Callable + from types import TracebackType from aws_lambda_powertools.utilities.batch.types import ( PartialItemFailureResponse, @@ -61,6 +62,10 @@ class EventType(Enum): FailureResponse = Tuple[str, str, BatchEventTypes] +def _has_traceback(exception: ExceptionInfo) -> TypeGuard[tuple[type[BaseException], BaseException, TracebackType]]: + return exception[0] is not None and exception[1] is not None and exception[2] is not None + + class BasePartialProcessor(ABC): """ Abstract class for batch processors. @@ -68,10 +73,11 @@ class BasePartialProcessor(ABC): lambda_context: LambdaContext - def __init__(self): + def __init__(self, logger: logging.Logger | None = None): self.success_messages: list[BatchEventTypes] = [] self.fail_messages: list[BatchEventTypes] = [] self.exceptions: list[ExceptionInfo] = [] + self.logger = logger @abstractmethod def _prepare(self): @@ -237,6 +243,13 @@ def failure_handler(self, record, exception: ExceptionInfo) -> FailureResponse: exception_string = f"{exception[0]}:{exception[1]}" entry = ("fail", exception_string, record) logger.debug(f"Record processing exception: {exception_string}") + + if self.logger is not None and _has_traceback(exception): + self.logger.warning( + "Record processing exception; skipping this record", + exc_info=exception, + ) + self.exceptions.append(exception) self.fail_messages.append(record) return entry @@ -250,6 +263,7 @@ def __init__( event_type: EventType, model: BatchTypeModels | None = None, raise_on_entire_batch_failure: bool = True, + logger: logging.Logger | None = None, ): """Process batch and partially report failed items @@ -262,6 +276,8 @@ def __init__( raise_on_entire_batch_failure: bool Raise an exception when the entire batch has failed processing. When set to False, partial failures are reported in the response + logger: logging.Logger | None + Optional Logger instance to output warnings with tracebacks for failed records. Exceptions ---------- @@ -285,7 +301,7 @@ def __init__( EventType.Kafka: KafkaEventRecord, } - super().__init__() + super().__init__(logger=logger) def response(self) -> PartialItemFailureResponse: """Batch items that failed processing, if any""" diff --git a/aws_lambda_powertools/utilities/batch/exceptions.py b/aws_lambda_powertools/utilities/batch/exceptions.py index 87a2df22d6d..247388a9886 100644 --- a/aws_lambda_powertools/utilities/batch/exceptions.py +++ b/aws_lambda_powertools/utilities/batch/exceptions.py @@ -6,9 +6,9 @@ import traceback from types import TracebackType -from typing import Optional, Tuple, Type +from typing import Tuple, Type -ExceptionInfo = Tuple[Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]] +ExceptionInfo = Tuple[Type[BaseException] | None, BaseException | None, TracebackType | None] class BaseBatchProcessingError(Exception): diff --git a/aws_lambda_powertools/utilities/batch/sqs_fifo_partial_processor.py b/aws_lambda_powertools/utilities/batch/sqs_fifo_partial_processor.py index 2e680e2f04e..88a6c3dfe37 100644 --- a/aws_lambda_powertools/utilities/batch/sqs_fifo_partial_processor.py +++ b/aws_lambda_powertools/utilities/batch/sqs_fifo_partial_processor.py @@ -66,7 +66,12 @@ def lambda_handler(event, context: LambdaContext): None, ) - def __init__(self, model: BatchSqsTypeModel | None = None, skip_group_on_error: bool = False): + def __init__( + self, + model: BatchSqsTypeModel | None = None, + skip_group_on_error: bool = False, + logger: logging.Logger | None = None, + ): """ Initialize the SqsFifoProcessor. @@ -77,12 +82,14 @@ def __init__(self, model: BatchSqsTypeModel | None = None, skip_group_on_error: skip_group_on_error: bool Determines whether to exclusively skip messages from the MessageGroupID that encountered processing failures Default is False. + logger: logging.Logger | None + Optional Logger instance to output warnings with tracebacks for failed records. """ self._skip_group_on_error: bool = skip_group_on_error self._current_group_id = None self._failed_group_ids: set[str] = set() - super().__init__(EventType.SQS, model) + super().__init__(EventType.SQS, model, logger=logger) def _process_record(self, record): self._current_group_id = record.get("attributes", {}).get("MessageGroupId") diff --git a/aws_lambda_powertools/utilities/batch/types.py b/aws_lambda_powertools/utilities/batch/types.py index ec543bf51ea..32e11b5ae07 100644 --- a/aws_lambda_powertools/utilities/batch/types.py +++ b/aws_lambda_powertools/utilities/batch/types.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from typing import Optional, Type, TypedDict, Union +from typing import Type, TypedDict, Union has_pydantic = "pydantic" in sys.modules @@ -14,15 +14,16 @@ ) from aws_lambda_powertools.utilities.parser.models.kafka import KafkaRecordModel - BatchTypeModels = Optional[ + BatchTypeModels = ( Union[ Type[SqsRecordModel], Type[DynamoDBStreamRecordModel], Type[KinesisDataStreamRecordModel], Type[KafkaRecordModel], ] - ] - BatchSqsTypeModel = Optional[Type[SqsRecordModel]] + | None + ) + BatchSqsTypeModel = Type[SqsRecordModel] | None else: # pragma: no cover BatchTypeModels = "BatchTypeModels" # type: ignore BatchSqsTypeModel = "BatchSqsTypeModel" # type: ignore diff --git a/aws_lambda_powertools/utilities/circuit_breaker/__init__.py b/aws_lambda_powertools/utilities/circuit_breaker/__init__.py new file mode 100644 index 00000000000..ffe5ce3cb7a --- /dev/null +++ b/aws_lambda_powertools/utilities/circuit_breaker/__init__.py @@ -0,0 +1,29 @@ +""" +Circuit Breaker utility for protecting unhealthy downstream dependencies. +""" + +from aws_lambda_powertools.utilities.circuit_breaker.circuit_breaker import circuit_breaker +from aws_lambda_powertools.utilities.circuit_breaker.config import CircuitBreakerConfig +from aws_lambda_powertools.utilities.circuit_breaker.exceptions import ( + CircuitBreakerConfigError, + CircuitBreakerError, + CircuitBreakerOpenError, + CircuitBreakerPersistenceError, +) +from aws_lambda_powertools.utilities.circuit_breaker.states import ( + CircuitInfo, + CircuitState, + CircuitTransition, +) + +__all__ = ( + "circuit_breaker", + "CircuitBreakerConfig", + "CircuitInfo", + "CircuitState", + "CircuitTransition", + "CircuitBreakerError", + "CircuitBreakerOpenError", + "CircuitBreakerConfigError", + "CircuitBreakerPersistenceError", +) diff --git a/aws_lambda_powertools/utilities/circuit_breaker/base.py b/aws_lambda_powertools/utilities/circuit_breaker/base.py new file mode 100644 index 00000000000..f0375885d18 --- /dev/null +++ b/aws_lambda_powertools/utilities/circuit_breaker/base.py @@ -0,0 +1,284 @@ +""" +Orchestrator for the Circuit Breaker utility. + +:class:`CircuitBreakerHandler` owns the state machine and the per-environment failure +counter; the persistence layer owns the shared truth. This split keeps the healthy +path write-free: failures are counted locally and only persisted on a state transition. +""" + +from __future__ import annotations + +import datetime +import logging +import os +import threading +import uuid +from typing import TYPE_CHECKING, Any + +from aws_lambda_powertools.utilities.circuit_breaker.exceptions import CircuitBreakerOpenError +from aws_lambda_powertools.utilities.circuit_breaker.states import CircuitState, CircuitTransition + +if TYPE_CHECKING: + from collections.abc import Callable + + from aws_lambda_powertools.utilities.circuit_breaker.config import CircuitBreakerConfig + from aws_lambda_powertools.utilities.circuit_breaker.persistence.base import ( + CircuitBreakerPersistenceLayer, + ) + from aws_lambda_powertools.utilities.circuit_breaker.states import CircuitInfo + +logger = logging.getLogger(__name__) + +# Per-environment, per-circuit consecutive counters. Module-level so they survive across +# invocations within the same execution environment, the same way idempotency caches do. +_LOCAL_FAILURES: dict[str, int] = {} +_LOCAL_SUCCESSES: dict[str, int] = {} + +# Tracks the last state this environment observed from the store, per circuit. Used to +# detect transitions back to CLOSED that happened externally (another env tripped and +# recovered), so stale local failure streaks can be invalidated. +_LAST_OBSERVED_STATE: dict[str, CircuitState] = {} + +# Guards the three dicts above. Increments are read-modify-write and a threshold +# crossing must be observed by exactly one thread, so every access goes through this +# lock. Held only while mutating the dicts, never across persistence writes or user +# callbacks. +_COUNTERS_LOCK = threading.Lock() + +# Identifier used to claim the half-open probe lock, unique per thread so the store's +# conditional election picks a single prober across threads as well as processes. +_PROBE_OWNER = threading.local() + + +def _probe_owner_id() -> str: + """ + Return this thread's stable probe-owner identifier, minting it on first use. + + A uuid in thread-local storage rather than ``threading.get_ident()``: the OS reuses + thread ids, and a recycled id would let an unrelated thread pass the owner check and + probe alongside the real owner. The pid check re-mints the id in forked children, + which inherit the forking thread's local storage. + """ + pid = os.getpid() + if getattr(_PROBE_OWNER, "pid", None) != pid: + _PROBE_OWNER.id = f"{pid}#{uuid.uuid4().hex}" + _PROBE_OWNER.pid = pid + return _PROBE_OWNER.id + + +class CircuitBreakerHandler: + """ + Drive a single protected call through the circuit breaker state machine. + + A new handler is created per invocation by the decorator. It reads the shared state, + routes the call (run, short-circuit, or probe), and records the outcome. + + Parameters + ---------- + function : Callable + The protected function. + name : str + Circuit name. + config : CircuitBreakerConfig + Circuit configuration. + persistence_store : CircuitBreakerPersistenceLayer + Shared state store. + on_circuit_open : Callable | None + Callback invoked with the protected call's own ``*args``/``**kwargs`` plus a + trailing ``circuit`` keyword argument when the circuit is open. If ``None``, an + open circuit raises :class:`CircuitBreakerOpenError`. + function_args : tuple + Positional arguments the protected function was called with. + function_kwargs : dict + Keyword arguments the protected function was called with. + """ + + def __init__( + self, + function: Callable, + name: str, + config: CircuitBreakerConfig, + persistence_store: CircuitBreakerPersistenceLayer, + on_circuit_open: Callable | None = None, + on_transition: Callable | None = None, + function_args: tuple | None = None, + function_kwargs: dict | None = None, + ): + self.function = function + self.name = name + self.config = config + self.on_circuit_open = on_circuit_open + self.on_transition = on_transition + self.fn_args = function_args or () + self.fn_kwargs = function_kwargs or {} + + persistence_store.configure(config=config, circuit_name=name) + self.persistence_store = persistence_store + + def handle(self) -> Any: + """ + Evaluate the circuit and route the call. + + Returns + ------- + Any + The protected function's result when the call runs, or the + ``on_circuit_open`` callback's return value when the circuit is open. + + Raises + ------ + CircuitBreakerOpenError + If the circuit is open and no callback is registered. + """ + record = self.persistence_store.get_state(self.name) + + if record.state == CircuitState.CLOSED: + # If we previously observed a non-CLOSED state and the circuit is now back to + # CLOSED, another environment completed the recovery cycle. Reset local counters + # so a stale partial failure streak doesn't immediately re-trip the circuit. + with _COUNTERS_LOCK: + prev = _LAST_OBSERVED_STATE.get(self.name) + if prev is not None and prev != CircuitState.CLOSED: + _LOCAL_FAILURES[self.name] = 0 + _LAST_OBSERVED_STATE[self.name] = CircuitState.CLOSED + return self._call_closed() + + if record.state == CircuitState.OPEN: + with _COUNTERS_LOCK: + _LAST_OBSERVED_STATE[self.name] = CircuitState.OPEN + # ``opened_at`` may legitimately be 0 (epoch); treat only None as missing. + opened_at = record.opened_at if record.opened_at is not None else self._now() + if self._now() >= opened_at + self.config.recovery_timeout: + # Recovery window elapsed: try to become the single prober. + if self.persistence_store.try_acquire_half_open(self.name, _probe_owner_id(), opened_at): + self._notify(CircuitState.OPEN, CircuitState.HALF_OPEN, opened_at=opened_at) + return self._call_probe() + return self._open_response(record.to_circuit_info()) + + # HALF_OPEN: only the thread that owns the probe lock runs. + with _COUNTERS_LOCK: + _LAST_OBSERVED_STATE[self.name] = CircuitState.HALF_OPEN + if record.half_open_owner == _probe_owner_id(): + return self._call_probe() + + # If the probe lease has expired (owner recycled mid-probe), take over. + if record.probe_lease_expiry is not None and self._now() >= record.probe_lease_expiry: + logger.debug("Circuit '%s' probe lease expired; attempting takeover.", self.name) + if self.persistence_store.try_acquire_half_open(self.name, _probe_owner_id(), record.opened_at or 0): + return self._call_probe() + + return self._open_response(record.to_circuit_info()) + + def _call_closed(self) -> Any: + """Run the protected call while the circuit is closed, tracking failures.""" + try: + result = self.function(*self.fn_args, **self.fn_kwargs) + except Exception as exc: + if not self.config.counts_as_failure(exc): + raise + # Increment and reset atomically so exactly one thread observes the threshold + # crossing; racing threads would otherwise lose increments (tripping late) or + # each persist the same transition. + with _COUNTERS_LOCK: + failures = _LOCAL_FAILURES.get(self.name, 0) + 1 + tripped = failures >= self.config.failure_threshold + _LOCAL_FAILURES[self.name] = 0 if tripped else failures + if tripped: + logger.debug("Circuit '%s' tripping CLOSED to OPEN after %d failures.", self.name, failures) + opened_at = self._now() + self._safe_persist( + self.persistence_store.save_open, + self.name, + failure_count=failures, + opened_at=opened_at, + ) + self._notify(CircuitState.CLOSED, CircuitState.OPEN, opened_at=opened_at) + raise + else: + with _COUNTERS_LOCK: + _LOCAL_FAILURES[self.name] = 0 + return result + + def _call_probe(self) -> Any: + """Run a probe during half-open, closing or reopening based on the outcome.""" + try: + result = self.function(*self.fn_args, **self.fn_kwargs) + except Exception as exc: + if not self.config.counts_as_failure(exc): + raise + logger.debug("Circuit '%s' probe failed; reopening.", self.name) + opened_at = self._now() + self._safe_persist(self.persistence_store.save_reopen, self.name, opened_at=opened_at) + with _COUNTERS_LOCK: + _LOCAL_SUCCESSES[self.name] = 0 + self._notify(CircuitState.HALF_OPEN, CircuitState.OPEN, opened_at=opened_at) + raise + else: + with _COUNTERS_LOCK: + successes = _LOCAL_SUCCESSES.get(self.name, 0) + 1 + closed = successes >= self.config.success_threshold + _LOCAL_SUCCESSES[self.name] = 0 if closed else successes + if closed: + _LOCAL_FAILURES[self.name] = 0 + if closed: + logger.debug("Circuit '%s' closing after %d probe successes.", self.name, successes) + self._safe_persist(self.persistence_store.save_closed, self.name) + self._notify(CircuitState.HALF_OPEN, CircuitState.CLOSED) + return result + + def _safe_persist(self, fn: Callable, *args: Any, **kwargs: Any) -> None: + """ + Call a persistence write, swallowing and logging failures. + + State-transition writes must never mask the downstream's real result or replace + the downstream's real exception. This mirrors the fail-open read policy in the + persistence layer. + """ + try: + fn(*args, **kwargs) + except Exception: + logger.warning( + "Circuit '%s': persistence write (%s) failed; the transition may be delayed but the " + "downstream result is preserved.", + self.name, + getattr(fn, "__name__", repr(fn)), + exc_info=True, + ) + + def _open_response(self, circuit: CircuitInfo) -> Any: + """Produce the response for an open circuit: callback result or raise.""" + if self.on_circuit_open is not None: + # Forward the protected call's arguments unchanged: positional stay positional, + # keyword stay keyword. The circuit snapshot is passed as a keyword argument so + # it never collides with positionalized kwargs nor depends on dict ordering. + return self.on_circuit_open(*self.fn_args, **self.fn_kwargs, circuit=circuit) + raise CircuitBreakerOpenError( + f"Circuit '{self.name}' is open.", + circuit=circuit, + ) + + def _notify(self, from_state: CircuitState, to_state: CircuitState, opened_at: int | None = None) -> None: + """ + Fire the ``on_transition`` hook for a state change. + + Called only on real transitions, never on the hot path. Any exception the hook + raises is swallowed and logged: observability must never break the protected call. + """ + if self.on_transition is None: + return + try: + self.on_transition( + CircuitTransition( + circuit_name=self.name, + from_state=from_state, + to_state=to_state, + opened_at=opened_at, + ), + ) + except Exception: + logger.warning("on_transition hook for circuit '%s' raised; ignoring.", self.name, exc_info=True) + + @staticmethod + def _now() -> int: + """Current unix timestamp in seconds.""" + return int(datetime.datetime.now().timestamp()) diff --git a/aws_lambda_powertools/utilities/circuit_breaker/circuit_breaker.py b/aws_lambda_powertools/utilities/circuit_breaker/circuit_breaker.py new file mode 100644 index 00000000000..f0d65542607 --- /dev/null +++ b/aws_lambda_powertools/utilities/circuit_breaker/circuit_breaker.py @@ -0,0 +1,119 @@ +""" +Primary interface for the Circuit Breaker utility. +""" + +from __future__ import annotations + +import functools +import logging +import os +import warnings +from typing import TYPE_CHECKING, Any + +from aws_lambda_powertools.shared import constants +from aws_lambda_powertools.shared.functions import strtobool +from aws_lambda_powertools.utilities.circuit_breaker.base import CircuitBreakerHandler +from aws_lambda_powertools.utilities.circuit_breaker.config import CircuitBreakerConfig +from aws_lambda_powertools.warnings import PowertoolsUserWarning + +if TYPE_CHECKING: + from collections.abc import Callable + + from aws_lambda_powertools.utilities.circuit_breaker.persistence.base import ( + CircuitBreakerPersistenceLayer, + ) + +logger = logging.getLogger(__name__) + + +def circuit_breaker( + name: str, + persistence_store: CircuitBreakerPersistenceLayer, + on_circuit_open: Callable | None = None, + on_transition: Callable | None = None, + config: CircuitBreakerConfig | None = None, +) -> Callable: + """ + Protect a function that calls an unhealthy-prone downstream with a circuit breaker. + + Wrap the function that makes the downstream call, not the whole Lambda handler, so a + tripped circuit reflects one dependency rather than unrelated handler logic. + + When the circuit is open the protected function is not called. Instead, if an + ``on_circuit_open`` callback is registered it runs and its return value becomes the + call's result; otherwise :class:`CircuitBreakerOpenError` is raised. + + Parameters + ---------- + name : str + Unique circuit name. Each name is an independent circuit; a function calling + several backends should use one circuit per backend. + persistence_store : CircuitBreakerPersistenceLayer + Shared state store (for example ``CircuitBreakerDynamoDBPersistence``). + on_circuit_open : Callable | None + Called when the circuit is open, with the protected function's own arguments + (positional stay positional, keyword stay keyword) plus a trailing ``circuit`` + keyword argument carrying a ``CircuitInfo``. Its return value becomes the call's + result. If ``None``, an open circuit raises ``CircuitBreakerOpenError``. + on_transition : Callable | None + Called with a single ``CircuitTransition`` argument whenever the circuit changes + state (open, probe, close, reopen). Fires only on transitions, never on the + per-invocation hot path, so it is a safe place to emit a CloudWatch metric. Any + exception it raises is swallowed and logged so observability never breaks the + protected call. + config : CircuitBreakerConfig | None + Tunables. Defaults to ``CircuitBreakerConfig()`` when omitted. + + Returns + ------- + Callable + The decorated function. + + Example + ------- + **Protect a payment backend, buffering rejected requests** + + from aws_lambda_powertools.utilities.circuit_breaker import circuit_breaker, CircuitInfo + from aws_lambda_powertools.utilities.circuit_breaker.persistence import ( + CircuitBreakerDynamoDBPersistence, + ) + + persistence = CircuitBreakerDynamoDBPersistence(table_name="CircuitBreakerState") + + def buffer(order: dict, circuit: CircuitInfo): + sqs.send_message(QueueUrl=url, MessageBody=json.dumps(order)) + + @circuit_breaker(name="payment-backend", persistence_store=persistence, on_circuit_open=buffer) + def charge(order: dict) -> dict: + return payment_api.charge(order) + """ + config = config or CircuitBreakerConfig() + + def decorator(function: Callable) -> Callable: + @functools.wraps(function) + def wrapper(*args, **kwargs) -> Any: + # Skip the circuit entirely when disabled (development only). + if strtobool(os.getenv(constants.CIRCUIT_BREAKER_DISABLED_ENV, "false")): + warnings.warn( + message="Disabling the circuit breaker is intended for development environments only " + "and should not be used in production.", + category=PowertoolsUserWarning, + stacklevel=2, + ) + return function(*args, **kwargs) + + handler = CircuitBreakerHandler( + function=function, + name=name, + config=config, + persistence_store=persistence_store, + on_circuit_open=on_circuit_open, + on_transition=on_transition, + function_args=args, + function_kwargs=kwargs, + ) + return handler.handle() + + return wrapper + + return decorator diff --git a/aws_lambda_powertools/utilities/circuit_breaker/config.py b/aws_lambda_powertools/utilities/circuit_breaker/config.py new file mode 100644 index 00000000000..7ee1259a697 --- /dev/null +++ b/aws_lambda_powertools/utilities/circuit_breaker/config.py @@ -0,0 +1,184 @@ +""" +Configuration for the Circuit Breaker utility. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from aws_lambda_powertools.utilities.circuit_breaker.exceptions import CircuitBreakerConfigError + +if TYPE_CHECKING: + from collections.abc import Iterable + + +class CircuitBreakerConfig: + """ + Tunables for a circuit breaker. + + All values have sensible defaults, so ``CircuitBreakerConfig()`` is a valid + production configuration. Pass an instance to ``@circuit_breaker(config=...)`` to + override them. + + Parameters + ---------- + failure_threshold : int + Number of *consecutive* failures that trips a closed circuit to open. Defaults to 5. + recovery_timeout : int + Seconds the circuit stays open before allowing a half-open probe. Defaults to 30. + success_threshold : int + Number of *consecutive* probe successes required to close a half-open circuit. + Defaults to 3. + handled_exceptions : type[Exception] | Iterable[type[Exception]] | None + Allowlist: only these exception types count as failures; anything else + propagates without affecting the circuit. Accepts a single exception type or + an iterable of them (normalized to a tuple). Mutually exclusive with + ``ignored_exceptions``. Defaults to ``None`` (treated as ``(Exception,)``). + ignored_exceptions : type[Exception] | Iterable[type[Exception]] | None + Denylist: every exception counts as a failure *except* these. Accepts a single + exception type or an iterable of them (normalized to a tuple). Mutually + exclusive with ``handled_exceptions``. Defaults to ``None``. + local_cache_max_age : int + Seconds a circuit's state is cached in the execution environment before a + read-through to the store. Matches the Parameters utility default. Defaults to 5. + + Raises + ------ + CircuitBreakerConfigError + If both ``handled_exceptions`` and ``ignored_exceptions`` are provided, a + numeric tunable is not a positive integer, or an exception allowlist/denylist + is empty or contains a value that is not an exception type. + + Example + ------- + **Only count timeouts and connection errors as failures** + + config = CircuitBreakerConfig( + failure_threshold=5, + recovery_timeout=30, + handled_exceptions=(TimeoutError, ConnectionError), + ) + """ + + def __init__( + self, + failure_threshold: int = 5, + recovery_timeout: int = 30, + success_threshold: int = 3, + handled_exceptions: type[Exception] | Iterable[type[Exception]] | None = None, + ignored_exceptions: type[Exception] | Iterable[type[Exception]] | None = None, + local_cache_max_age: int = 5, + ): + # Normalize first: a single exception type or any iterable becomes a tuple, and a + # bad value fails here (at construction) rather than as a cryptic isinstance + # TypeError later, the first time the circuit evaluates a failure. + handled_exceptions = self._normalize_exceptions(handled_exceptions, "handled_exceptions") + ignored_exceptions = self._normalize_exceptions(ignored_exceptions, "ignored_exceptions") + + self._validate( + failure_threshold=failure_threshold, + recovery_timeout=recovery_timeout, + success_threshold=success_threshold, + handled_exceptions=handled_exceptions, + ignored_exceptions=ignored_exceptions, + local_cache_max_age=local_cache_max_age, + ) + + self.failure_threshold = failure_threshold + self.recovery_timeout = recovery_timeout + self.success_threshold = success_threshold + self.handled_exceptions = handled_exceptions + self.ignored_exceptions = ignored_exceptions + self.local_cache_max_age = local_cache_max_age + + @staticmethod + def _validate( + failure_threshold: int, + recovery_timeout: int, + success_threshold: int, + handled_exceptions: tuple[type[Exception], ...] | None, + ignored_exceptions: tuple[type[Exception], ...] | None, + local_cache_max_age: int, + ) -> None: + if handled_exceptions and ignored_exceptions: + raise CircuitBreakerConfigError( + "handled_exceptions and ignored_exceptions are mutually exclusive; pass only one.", + ) + + # Thresholds and timeouts must be strictly positive; cache age may be 0 (always read through). + for field, value in ( + ("failure_threshold", failure_threshold), + ("recovery_timeout", recovery_timeout), + ("success_threshold", success_threshold), + ): + if not isinstance(value, int) or value <= 0: + raise CircuitBreakerConfigError(f"{field} must be a positive integer, got {value!r}.") + + if not isinstance(local_cache_max_age, int) or local_cache_max_age < 0: + raise CircuitBreakerConfigError( + f"local_cache_max_age must be a non-negative integer, got {local_cache_max_age!r}.", + ) + + @classmethod + def _normalize_exceptions( + cls, + value: type[Exception] | Iterable[type[Exception]] | None, + field: str, + ) -> tuple[type[Exception], ...] | None: + """Coerce a single exception type or an iterable of them into a validated, non-empty tuple. + + Runs at construction so a bad value fails immediately with a clear error, rather + than as a cryptic ``isinstance`` ``TypeError`` from ``counts_as_failure`` the + first time the circuit evaluates a failure (i.e. only once the dependency is + already unhealthy). + """ + if value is None: + return None + + invalid = f"{field} must be an exception type or an iterable of exception types, got {value!r}." + # A str is iterable; reject it rather than iterate it as a sequence of characters. + if isinstance(value, str): + raise CircuitBreakerConfigError(invalid) + + if isinstance(value, type): + # ty (unlike mypy) does not narrow the union here, so it needs the ignore. + exceptions: tuple[type[Exception], ...] = (value,) # ty: ignore[invalid-assignment] + else: + try: + exceptions = tuple(value) + except TypeError: + raise CircuitBreakerConfigError(invalid) from None + + cls._validate_exception_types(exceptions, field) + return exceptions + + @staticmethod + def _validate_exception_types(exceptions: tuple[type[Exception], ...], field: str) -> None: + """Require a non-empty tuple whose every element is an exception type.""" + if not exceptions: + raise CircuitBreakerConfigError(f"{field} must contain at least one exception type.") + for exception in exceptions: + if not (isinstance(exception, type) and issubclass(exception, Exception)): + raise CircuitBreakerConfigError(f"{field} must contain only exception types, got {exception!r}.") + + def counts_as_failure(self, exception: Exception) -> bool: + """ + Decide whether an exception raised by the protected call counts as a circuit failure. + + Parameters + ---------- + exception : Exception + The exception raised by the protected function. + + Returns + ------- + bool + ``True`` if the exception should increment the failure counter, ``False`` if + it should propagate without affecting the circuit. + """ + if self.handled_exceptions is not None: + return isinstance(exception, self.handled_exceptions) + if self.ignored_exceptions is not None: + return not isinstance(exception, self.ignored_exceptions) + # Default: any exception counts as a failure. + return True diff --git a/aws_lambda_powertools/utilities/circuit_breaker/exceptions.py b/aws_lambda_powertools/utilities/circuit_breaker/exceptions.py new file mode 100644 index 00000000000..063cf5f9c4a --- /dev/null +++ b/aws_lambda_powertools/utilities/circuit_breaker/exceptions.py @@ -0,0 +1,77 @@ +""" +Circuit Breaker exceptions. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from aws_lambda_powertools.utilities.circuit_breaker.states import CircuitInfo + + +class CircuitBreakerError(Exception): + """ + Base error class. + + Overrides message/details formatting so the printed exception stays readable. + See https://github.com/aws-powertools/powertools-lambda-python/issues/1772 + """ + + def __init__(self, *args: str | Exception | None): + self.message = str(args[0]) if args else "" + self.details = "".join(str(arg) for arg in args[1:]) if args[1:] else None + + def __str__(self): + """Return all arguments formatted, or the original message.""" + if self.message and self.details: + return f"{self.message} - ({self.details})" + return self.message + + +class CircuitBreakerOpenError(CircuitBreakerError): + """ + Raised when the circuit is open and no ``on_circuit_open`` callback is registered. + + The rejected request never reached the downstream. The circuit snapshot is attached + so the caller can decide how to respond. + + Parameters + ---------- + *args : str | Exception | None + Standard error message/details. + circuit : CircuitInfo | None + Snapshot of the circuit at rejection time. + + Example + ------- + **Handling an open circuit when no callback is registered** + + try: + charge(order) + except CircuitBreakerOpenError as exc: + logger.warning("rejected by circuit %s", exc.circuit.name) + return {"statusCode": 202} + """ + + def __init__(self, *args: str | Exception | None, circuit: CircuitInfo | None = None): + self.circuit = circuit + super().__init__(*args) + + +class CircuitBreakerConfigError(CircuitBreakerError): + """ + Raised when ``CircuitBreakerConfig`` is built with an unsupported combination of + options (for example, both ``handled_exceptions`` and ``ignored_exceptions``). + """ + + +class CircuitBreakerPersistenceError(CircuitBreakerError): + """ + Raised by a persistence backend for an unrecoverable store error on a *write* path + (persisting a state transition), where there is no safe local fallback. + + Reads never raise this: ``get_state`` fails open (treats the circuit as closed) and + only logs, so a degraded store can never become the outage the breaker is meant to + prevent. Custom backends may raise this from their write primitives. + """ diff --git a/aws_lambda_powertools/utilities/circuit_breaker/persistence/__init__.py b/aws_lambda_powertools/utilities/circuit_breaker/persistence/__init__.py new file mode 100644 index 00000000000..3669d207b26 --- /dev/null +++ b/aws_lambda_powertools/utilities/circuit_breaker/persistence/__init__.py @@ -0,0 +1,15 @@ +""" +Persistence layers for the Circuit Breaker utility. +""" + +from aws_lambda_powertools.utilities.circuit_breaker.persistence.base import CircuitBreakerPersistenceLayer +from aws_lambda_powertools.utilities.circuit_breaker.persistence.dynamodb import ( + CircuitBreakerDynamoDBPersistence, +) +from aws_lambda_powertools.utilities.circuit_breaker.persistence.record import CircuitStateRecord + +__all__ = ( + "CircuitBreakerPersistenceLayer", + "CircuitBreakerDynamoDBPersistence", + "CircuitStateRecord", +) diff --git a/aws_lambda_powertools/utilities/circuit_breaker/persistence/base.py b/aws_lambda_powertools/utilities/circuit_breaker/persistence/base.py new file mode 100644 index 00000000000..c7dc05c637f --- /dev/null +++ b/aws_lambda_powertools/utilities/circuit_breaker/persistence/base.py @@ -0,0 +1,327 @@ +""" +Abstract persistence layer for the Circuit Breaker utility. + +Concrete backends (DynamoDB, cache) subclass :class:`CircuitBreakerPersistenceLayer` +and implement the small set of store primitives. The base class owns the local +read-through cache and the fail-open policy so every backend behaves identically. +""" + +from __future__ import annotations + +import datetime +import logging +import threading +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, NamedTuple + +from aws_lambda_powertools.shared.cache_dict import LRUDict +from aws_lambda_powertools.utilities.circuit_breaker.persistence.record import CircuitStateRecord +from aws_lambda_powertools.utilities.circuit_breaker.states import CircuitState + +if TYPE_CHECKING: + from aws_lambda_powertools.utilities.circuit_breaker.config import CircuitBreakerConfig + +logger = logging.getLogger(__name__) + +# Circuit names are static in user code, so a handful of circuits per environment is the +# norm. This cap only guards the pathological case of dynamically generated names. +LOCAL_CACHE_MAX_ITEMS = 1024 + +# Slack added on top of a recovery cycle when computing the durable store TTL. The item +# must outlive any in-flight recovery window so a live circuit is never reaped mid-cycle, +# while an abandoned circuit (no traffic, no further writes) still self-cleans soon after. +PERSISTED_STATE_TTL_BUFFER = 3600 + + +class _CircuitSettings(NamedTuple): + """Per-circuit tunables the layer captures from :meth:`configure`.""" + + local_cache_max_age: int + recovery_timeout: int + + +# Fallback for direct layer use before configure() has run; mirrors CircuitBreakerConfig defaults. +_DEFAULT_SETTINGS = _CircuitSettings(local_cache_max_age=5, recovery_timeout=30) + + +class CircuitBreakerExistingLockError(Exception): + """Internal signal that a conditional half-open probe write lost the race.""" + + +class CircuitBreakerPersistenceLayer(ABC): + """ + Abstract base class for circuit breaker persistence layers. + + Owns the per-environment read cache and the fail-open behavior. Subclasses + implement :meth:`_get_record`, :meth:`_put_record`, and :meth:`_update_record` + for a specific store. + + A persistence layer is keyed by **circuit name**, not by a payload hash, which is + the main reason it does not reuse the Idempotency persistence layer. + """ + + def __init__(self) -> None: + """Initialize defaults; real configuration happens in :meth:`configure`.""" + # Per-circuit tunables, keyed by circuit name. One persistence instance is shared + # by every circuit (and thread) using the same store, so these must never live in + # plain instance attributes: circuits with different configs would stamp each + # other's TTL and probe lease. A plain dict, not an LRUDict: evicting a live + # circuit's settings would silently swap in the defaults (wrong lease and TTL), + # whereas evicting a cache entry below only costs a store re-read. + self._settings: dict[str, _CircuitSettings] = {} + # Maps circuit name -> the unix timestamp the locally cached record goes stale. + # Kept separate from the record's durable ``expiry_timestamp`` (the store TTL) so + # the short in-memory freshness window is never mistaken for the long store TTL. + self._cache: LRUDict = LRUDict(max_items=LOCAL_CACHE_MAX_ITEMS) + # One lock for both maps: LRUDict reorders entries even on reads, so unguarded + # concurrent access can corrupt it or raise. + self._lock = threading.Lock() + + def configure(self, config: CircuitBreakerConfig, circuit_name: str) -> None: + """ + Bind a circuit's configuration to the layer. + + Called once per invocation by the handler; the assignment is cheap and the + same persistence instance is reused across invocations within an environment. + + Parameters + ---------- + config : CircuitBreakerConfig + Configuration providing the local cache TTL and recovery timeout. + circuit_name : str + The circuit these settings apply to. + """ + with self._lock: + self._settings[circuit_name] = _CircuitSettings( + local_cache_max_age=config.local_cache_max_age, + recovery_timeout=config.recovery_timeout, + ) + + def _settings_for(self, name: str) -> _CircuitSettings: + """Return a circuit's configured settings, or the defaults if never configured.""" + with self._lock: + return self._settings.get(name, _DEFAULT_SETTINGS) + + # ------------------------------------------------------------------ cache + + def _cache_key(self, name: str) -> str: + return name + + def _durable_ttl(self, name: str) -> int: + """ + Compute the store TTL stamped on a persisted record. + + Sized to outlive a full recovery window so a live circuit is never reaped + mid-cycle, while an abandoned circuit (no further writes) self-cleans soon after. + """ + now = int(datetime.datetime.now().timestamp()) + return now + self._settings_for(name).recovery_timeout + PERSISTED_STATE_TTL_BUFFER + + def _save_to_cache(self, record: CircuitStateRecord) -> None: + """Cache a record locally with a short in-memory freshness window.""" + local_expiry = int(datetime.datetime.now().timestamp()) + self._settings_for(record.name).local_cache_max_age + with self._lock: + self._cache[self._cache_key(record.name)] = (local_expiry, record) + + def _retrieve_from_cache(self, name: str) -> CircuitStateRecord | None: + """Return a cached record if present and still within its local freshness window.""" + with self._lock: + cached = self._cache.get(self._cache_key(name)) + if cached is None: + return None + + local_expiry, record = cached + if int(datetime.datetime.now().timestamp()) >= local_expiry: + # Guarded del, not pop: on Python 3.10 OrderedDict.pop re-enters the + # subclass __getitem__ after detaching the node, so LRUDict.pop raises + # KeyError for a *present* key and corrupts the dict (fixed in 3.11). + try: + del self._cache[self._cache_key(name)] + except KeyError: + pass + return None + + return record + + # ------------------------------------------------------------- public API + + def get_state(self, name: str) -> CircuitStateRecord: + """ + Return the current circuit state, reading the store only on a cache miss. + + A cache miss (cold start or expired local entry) forces a read-through before + the caller routes the request, so a freshly started environment never assumes a + circuit is closed without checking. + + Fail-open: if the store read itself raises, the circuit is treated as + ``CLOSED``. A circuit breaker must never become the outage it is meant to + prevent. + + Parameters + ---------- + name : str + Circuit name. + + Returns + ------- + CircuitStateRecord + The current record, a synthesized closed record if none exists yet, or a + synthesized closed record if the store could not be reached. + """ + cached = self._retrieve_from_cache(name) + if cached is not None: + return cached + + try: + record = self._get_record(name) + except Exception: + # Fail open without caching, so the next invocation retries the store rather + # than serving a synthesized CLOSED for the whole local cache window. + logger.warning( + "Failed to read circuit state for '%s'; failing open (treating as CLOSED).", + name, + exc_info=True, + ) + return CircuitStateRecord(name=name, state=CircuitState.CLOSED) + + # A missing record is the expected cold-start case, not a failure: treat it as a + # closed circuit and cache it like any other read. + if record is None: + record = CircuitStateRecord(name=name, state=CircuitState.CLOSED) + + self._save_to_cache(record) + return record + + def save_open(self, name: str, failure_count: int, opened_at: int) -> None: + """ + Persist a CLOSED to OPEN transition. + + Parameters + ---------- + name : str + Circuit name. + failure_count : int + Consecutive failures that tripped the circuit. + opened_at : int + Unix timestamp the circuit opened; anchors the recovery timeout. + """ + record = CircuitStateRecord( + name=name, + state=CircuitState.OPEN, + failure_count=failure_count, + opened_at=opened_at, + expiry_timestamp=self._durable_ttl(name), + ) + self._put_record(record) + self._save_to_cache(record) + + def try_acquire_half_open(self, name: str, owner: str, opened_at: int) -> bool: + """ + Atomically elect a single worker to run the half-open probe. + + The conditional write succeeds only when the circuit is OPEN with no existing + lock owner AND the ``opened_at`` matches what the caller observed (guards against + stale eventually-consistent reads). A lease expiry is stamped so that if the + winning worker is recycled before completing the probe, others can take over + once the lease lapses. + + Parameters + ---------- + name : str + Circuit name. + owner : str + Identifier of the worker (one thread in one execution environment) + attempting the probe. + opened_at : int + The ``opened_at`` the caller observed, kept stable across the transition. + + Returns + ------- + bool + ``True`` if this worker won the probe lock, ``False`` if another + worker already holds it. + """ + # Lease = recovery_timeout gives the probe a full cycle to complete. + probe_lease_expiry = int(datetime.datetime.now().timestamp()) + self._settings_for(name).recovery_timeout + record = CircuitStateRecord( + name=name, + state=CircuitState.HALF_OPEN, + opened_at=opened_at, + half_open_owner=owner, + probe_lease_expiry=probe_lease_expiry, + expiry_timestamp=self._durable_ttl(name), + ) + try: + self._put_record(record, condition="half_open", expected_opened_at=opened_at) + except CircuitBreakerExistingLockError: + return False + self._save_to_cache(record) + return True + + def save_closed(self, name: str) -> None: + """Persist a transition back to CLOSED and reset counters.""" + record = CircuitStateRecord( + name=name, + state=CircuitState.CLOSED, + failure_count=0, + expiry_timestamp=self._durable_ttl(name), + ) + self._update_record(record) + self._save_to_cache(record) + + def save_reopen(self, name: str, opened_at: int) -> None: + """ + Persist a HALF_OPEN to OPEN transition after a failed probe. + + If this write fails the stored row stays in HALF_OPEN, but it does not strand the + circuit: the probe lease expiry still applies, so once it lapses another + environment wins the next election and drives the transition. + """ + record = CircuitStateRecord( + name=name, + state=CircuitState.OPEN, + opened_at=opened_at, + expiry_timestamp=self._durable_ttl(name), + ) + self._update_record(record) + self._save_to_cache(record) + + # --------------------------------------------------------- backend hooks + + @abstractmethod + def _get_record(self, name: str) -> CircuitStateRecord | None: + """ + Fetch a circuit record from the store, or ``None`` if no record exists for ``name``. + """ + raise NotImplementedError + + @abstractmethod + def _put_record( + self, + record: CircuitStateRecord, + condition: str | None = None, + expected_opened_at: int | None = None, + ) -> None: + """ + Write a circuit record. + + Parameters + ---------- + record : CircuitStateRecord + Record to write. + condition : str | None + When ``"half_open"``, the write must be conditional so only one + environment wins the probe lock; on a lost race the backend raises + :class:`CircuitBreakerExistingLockError`. + expected_opened_at : int | None + When set alongside ``condition="half_open"``, the write additionally + requires that the stored ``opened_at`` matches this value. This closes + a race where an eventually-consistent read could let a stale environment + win an election immediately after a failed probe reopened the circuit. + """ + raise NotImplementedError + + @abstractmethod + def _update_record(self, record: CircuitStateRecord) -> None: + """Update an existing circuit record (unconditional state change).""" + raise NotImplementedError diff --git a/aws_lambda_powertools/utilities/circuit_breaker/persistence/dynamodb.py b/aws_lambda_powertools/utilities/circuit_breaker/persistence/dynamodb.py new file mode 100644 index 00000000000..3b1f36aa9f3 --- /dev/null +++ b/aws_lambda_powertools/utilities/circuit_breaker/persistence/dynamodb.py @@ -0,0 +1,293 @@ +""" +DynamoDB persistence backend for the Circuit Breaker utility. +""" + +from __future__ import annotations + +import datetime +import logging +import os +import warnings +from typing import TYPE_CHECKING + +import boto3 +from boto3.dynamodb.types import TypeDeserializer +from botocore.exceptions import ClientError + +from aws_lambda_powertools.shared import constants, user_agent +from aws_lambda_powertools.utilities.circuit_breaker.exceptions import CircuitBreakerConfigError +from aws_lambda_powertools.utilities.circuit_breaker.persistence.base import ( + CircuitBreakerExistingLockError, + CircuitBreakerPersistenceLayer, +) +from aws_lambda_powertools.utilities.circuit_breaker.persistence.record import CircuitStateRecord +from aws_lambda_powertools.utilities.circuit_breaker.states import CircuitState +from aws_lambda_powertools.warnings import PowertoolsUserWarning + +if TYPE_CHECKING: + from botocore.config import Config + from mypy_boto3_dynamodb.client import DynamoDBClient + +logger = logging.getLogger(__name__) + + +class CircuitBreakerDynamoDBPersistence(CircuitBreakerPersistenceLayer): + """ + Store circuit state in an Amazon DynamoDB table, one item per circuit. + + The class name is prefixed with ``CircuitBreaker`` so a function using both the + Idempotency and Circuit Breaker utilities can import both persistence layers + without an alias. + + Parameters + ---------- + table_name : str + Name of the DynamoDB table that stores circuit state. + key_attr : str + Partition key attribute holding the circuit name. Defaults to ``"id"``. + static_pk_value : str, optional + Partition key value used when ``sort_key_attr`` is set, so the circuit name + moves to the sort key. Defaults to ``"circuit_breaker#"``. + sort_key_attr : str, optional + Sort key attribute holding the circuit name. When set, the table is treated as + composite: ``static_pk_value`` is written to the partition key and the circuit + name to the sort key. Omit it for the default partition-key-only behavior. + state_attr : str + Attribute holding the circuit state. Defaults to ``"state"``. + failure_count_attr : str + Attribute holding the consecutive failure count. Defaults to ``"failure_count"``. + opened_at_attr : str + Attribute holding the open timestamp. Defaults to ``"opened_at"``. + half_open_owner_attr : str + Attribute holding the half-open probe lock owner. Defaults to ``"half_open_owner"``. + probe_lease_expiry_attr : str + Attribute holding the probe lease expiry timestamp. Defaults to ``"probe_lease_expiry"``. + expiry_attr : str + TTL attribute. Defaults to ``"expiration"``. + boto_config : botocore.config.Config, optional + Botocore configuration used when creating the client. + boto3_session : boto3.session.Session, optional + Session used to create the client. + boto3_client : DynamoDBClient, optional + Pre-built client; ``boto3_session`` and ``boto_config`` are ignored if given. + + Example + ------- + **Create a DynamoDB-backed circuit breaker store** + + from aws_lambda_powertools.utilities.circuit_breaker.persistence import ( + CircuitBreakerDynamoDBPersistence, + ) + + persistence = CircuitBreakerDynamoDBPersistence(table_name="CircuitBreakerState") + """ + + def __init__( + self, + table_name: str, + key_attr: str = "id", + static_pk_value: str | None = None, + sort_key_attr: str | None = None, + state_attr: str = "state", + failure_count_attr: str = "failure_count", + opened_at_attr: str = "opened_at", + half_open_owner_attr: str = "half_open_owner", + probe_lease_expiry_attr: str = "probe_lease_expiry", + expiry_attr: str = "expiration", + boto_config: Config | None = None, + boto3_session: boto3.session.Session | None = None, + boto3_client: DynamoDBClient | None = None, + ): + if boto3_client is None: + boto3_session = boto3_session or boto3.session.Session() + boto3_client = boto3_session.client("dynamodb", config=boto_config) + self.client = boto3_client + + user_agent.register_feature_to_client(client=self.client, feature="circuit_breaker") + + if sort_key_attr == key_attr: + raise CircuitBreakerConfigError( + f"key_attr [{key_attr}] and sort_key_attr [{sort_key_attr}] cannot be the same!", + ) + + if static_pk_value is not None and sort_key_attr is None: + warnings.warn( + "static_pk_value is ignored unless sort_key_attr is also set.", + category=PowertoolsUserWarning, + stacklevel=2, + ) + + if static_pk_value is None: + static_pk_value = f"circuit_breaker#{os.getenv(constants.LAMBDA_FUNCTION_NAME_ENV, '')}" + + self.table_name = table_name + self.key_attr = key_attr + self.static_pk_value = static_pk_value + self.sort_key_attr = sort_key_attr + self.state_attr = state_attr + self.failure_count_attr = failure_count_attr + self.opened_at_attr = opened_at_attr + self.half_open_owner_attr = half_open_owner_attr + self.probe_lease_expiry_attr = probe_lease_expiry_attr + self.expiry_attr = expiry_attr + + self._deserializer = TypeDeserializer() + + super().__init__() + + def _item_to_record(self, item: dict) -> CircuitStateRecord: + """Translate a raw DynamoDB item into a :class:`CircuitStateRecord`.""" + data = self._deserializer.deserialize({"M": item}) + opened_at = data.get(self.opened_at_attr) + probe_lease_expiry = data.get(self.probe_lease_expiry_attr) + return CircuitStateRecord( + name=data[self.sort_key_attr] if self.sort_key_attr else data[self.key_attr], + state=CircuitState(data[self.state_attr]), + failure_count=int(data.get(self.failure_count_attr, 0)), + opened_at=int(opened_at) if opened_at is not None else None, + half_open_owner=data.get(self.half_open_owner_attr), + probe_lease_expiry=int(probe_lease_expiry) if probe_lease_expiry is not None else None, + expiry_timestamp=data.get(self.expiry_attr), + ) + + @staticmethod + def _n(value: int | str) -> dict: + """Wrap a value as a DynamoDB number attribute.""" + return {"N": str(value)} + + @staticmethod + def _s(value: str) -> dict: + """Wrap a value as a DynamoDB string attribute.""" + return {"S": value} + + def _get_key(self, name: str) -> dict: + """Build the simple or composite primary key for a circuit. + + When ``sort_key_attr`` is set the key is composite: ``static_pk_value`` in the + partition and the circuit name in the sort key. Otherwise the name is the + partition key. + """ + if self.sort_key_attr: + return {self.key_attr: self._s(self.static_pk_value), self.sort_key_attr: self._s(name)} + return {self.key_attr: self._s(name)} + + def _record_to_item(self, record: CircuitStateRecord) -> dict: + """Translate a :class:`CircuitStateRecord` into a DynamoDB item.""" + item: dict = { + **self._get_key(record.name), + self.state_attr: self._s(str(record.state)), + self.failure_count_attr: self._n(record.failure_count), + } + if record.opened_at is not None: + item[self.opened_at_attr] = self._n(record.opened_at) + if record.half_open_owner is not None: + item[self.half_open_owner_attr] = self._s(record.half_open_owner) + if record.probe_lease_expiry is not None: + item[self.probe_lease_expiry_attr] = self._n(record.probe_lease_expiry) + if record.expiry_timestamp is not None: + item[self.expiry_attr] = self._n(record.expiry_timestamp) + return item + + def _get_record(self, name: str) -> CircuitStateRecord | None: + # Eventually consistent on purpose: matches the local cache's stale tolerance + # and halves the read cost on the hot path. + response = self.client.get_item( + TableName=self.table_name, + Key=self._get_key(name), + ConsistentRead=False, + ) + item = response.get("Item") + if item is None: + return None + return self._item_to_record(item) + + def _build_half_open_condition(self, expected_opened_at: int | None = None) -> dict: + """Build the conditional expression kwargs for a half-open probe election.""" + condition_parts = [ + "(#state = :open AND attribute_not_exists(#half_open_owner))", + "(#state = :half_open AND #probe_lease_expiry <= :now)", + ] + expression_attr_names: dict = { + "#state": self.state_attr, + "#half_open_owner": self.half_open_owner_attr, + "#probe_lease_expiry": self.probe_lease_expiry_attr, + } + expression_values: dict = { + ":open": self._s(str(CircuitState.OPEN)), + ":half_open": self._s(str(CircuitState.HALF_OPEN)), + ":now": self._n(int(datetime.datetime.now().timestamp())), + } + + if expected_opened_at is not None: + condition_parts[0] = ( + "(#state = :open AND attribute_not_exists(#half_open_owner) AND #opened_at = :expected_opened_at)" + ) + expression_attr_names["#opened_at"] = self.opened_at_attr + expression_values[":expected_opened_at"] = self._n(expected_opened_at) + + return { + "ConditionExpression": " OR ".join(condition_parts), + "ExpressionAttributeNames": expression_attr_names, + "ExpressionAttributeValues": expression_values, + } + + def _put_record( + self, + record: CircuitStateRecord, + condition: str | None = None, + expected_opened_at: int | None = None, + ) -> None: + item = self._record_to_item(record) + + put_kwargs: dict = {"TableName": self.table_name, "Item": item} + + if condition == "half_open": + put_kwargs.update(self._build_half_open_condition(expected_opened_at)) + + try: + self.client.put_item(**put_kwargs) + except ClientError as exc: + if exc.response.get("Error", {}).get("Code") == "ConditionalCheckFailedException": + raise CircuitBreakerExistingLockError from exc + raise + + def _build_update_query(self, record: CircuitStateRecord) -> dict: + """Build the update_item kwargs for an unconditional state change.""" + update_expression = "SET #state = :state, #failure_count = :failure_count" + expression_attr_names = { + "#state": self.state_attr, + "#failure_count": self.failure_count_attr, + } + expression_attr_values: dict = { + ":state": self._s(str(record.state)), + ":failure_count": self._n(record.failure_count), + } + + if record.expiry_timestamp is not None: + update_expression += ", #expiration = :expiration" + expression_attr_names["#expiration"] = self.expiry_attr + expression_attr_values[":expiration"] = self._n(record.expiry_timestamp) + + # Clear the half-open owner lock and probe lease on every state change out of + # HALF_OPEN, whether the probe closed the circuit (opened_at is None) or reopened + # it (opened_at set). Otherwise the stale owner/lease makes the next probe + # election's condition fail forever, stranding the circuit. + expression_attr_names["#opened_at"] = self.opened_at_attr + expression_attr_names["#half_open_owner"] = self.half_open_owner_attr + expression_attr_names["#probe_lease_expiry"] = self.probe_lease_expiry_attr + if record.opened_at is not None: + update_expression += ", #opened_at = :opened_at REMOVE #half_open_owner, #probe_lease_expiry" + expression_attr_values[":opened_at"] = self._n(record.opened_at) + else: + update_expression += " REMOVE #opened_at, #half_open_owner, #probe_lease_expiry" + + return { + "TableName": self.table_name, + "Key": self._get_key(record.name), + "UpdateExpression": update_expression, + "ExpressionAttributeNames": expression_attr_names, + "ExpressionAttributeValues": expression_attr_values, + } + + def _update_record(self, record: CircuitStateRecord) -> None: + self.client.update_item(**self._build_update_query(record)) diff --git a/aws_lambda_powertools/utilities/circuit_breaker/persistence/record.py b/aws_lambda_powertools/utilities/circuit_breaker/persistence/record.py new file mode 100644 index 00000000000..55b486099cd --- /dev/null +++ b/aws_lambda_powertools/utilities/circuit_breaker/persistence/record.py @@ -0,0 +1,64 @@ +""" +Internal record type for circuit state held in a persistence store. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from aws_lambda_powertools.utilities.circuit_breaker.states import CircuitInfo, CircuitState + + +@dataclass +class CircuitStateRecord: + """ + The persisted state of a single circuit. + + One record exists per circuit name. This is the utility's internal representation; + user code never sees it directly, only the ``CircuitInfo`` produced by + :meth:`to_circuit_info`. + + Parameters + ---------- + name : str + Circuit name, used as the partition key in the store. + state : CircuitState + Current circuit state. + failure_count : int + Consecutive failures recorded by the environment that last wrote the record. + opened_at : int | None + Unix timestamp (seconds) the circuit opened. Anchors the recovery timeout; + ``None`` while closed. + half_open_owner : str | None + Identifier of the worker (one thread in one execution environment) that won the + half-open probe lock, if any. + expiry_timestamp : int | None + Unix timestamp (seconds) for the store's TTL attribute. + """ + + name: str + state: CircuitState + failure_count: int = 0 + opened_at: int | None = None + half_open_owner: str | None = None + probe_lease_expiry: int | None = None + expiry_timestamp: int | None = None + + def to_circuit_info(self) -> CircuitInfo: + """ + Project this record to the public ``CircuitInfo`` handed to user code. + + Strips internal fields (``half_open_owner``, ``expiry_timestamp``) so no + persistence detail leaks across the public boundary. + + Returns + ------- + CircuitInfo + Public snapshot of the circuit. + """ + return CircuitInfo( + name=self.name, + state=self.state, + failure_count=self.failure_count, + opened_at=self.opened_at, + ) diff --git a/aws_lambda_powertools/utilities/circuit_breaker/states.py b/aws_lambda_powertools/utilities/circuit_breaker/states.py new file mode 100644 index 00000000000..cc041e17a36 --- /dev/null +++ b/aws_lambda_powertools/utilities/circuit_breaker/states.py @@ -0,0 +1,126 @@ +""" +Public state types for the Circuit Breaker utility. + +These are the only circuit-breaker types handed to user code (callbacks and the +``CircuitInfo`` attached to ``CircuitBreakerOpenError``). They deliberately expose no +persistence internals. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum + + +class CircuitState(str, Enum): + """ + The state of a circuit. + + Subclasses ``str`` so the value serializes directly to a persistence store as a + plain string (e.g. DynamoDB) and compares equal to its string form. + + Attributes + ---------- + CLOSED : str + Normal operation. Requests reach the downstream and failures are counted. + OPEN : str + The downstream is considered unhealthy. The protected call is skipped. + HALF_OPEN : str + Recovery is being tested. A limited number of probe requests are allowed + through to decide whether the circuit should close again. + """ + + CLOSED = "CLOSED" + OPEN = "OPEN" + HALF_OPEN = "HALF_OPEN" + + def __str__(self) -> str: + """Return the bare value (e.g. ``"OPEN"``) rather than ``CircuitState.OPEN``.""" + return self.value + + +@dataclass(frozen=True) +class CircuitInfo: + """ + Immutable snapshot of a circuit, passed to user code. + + This is the public boundary of the utility: it is the single argument (alongside + the payload) handed to an ``on_circuit_open`` callback, and it is attached to + ``CircuitBreakerOpenError`` so a caller can inspect why the circuit rejected the + request. No persistence details (probe lock, TTL) are exposed. + + Parameters + ---------- + name : str + The circuit name, as given to the ``@circuit_breaker`` decorator. + state : CircuitState + The circuit state at the moment the request was evaluated. + failure_count : int + A point-in-time snapshot of the *consecutive* failures the environment that + last wrote the record had counted, captured at the moment of a state + transition. It is **not** a running total of failures across the fleet: the + failure counter lives in memory per execution environment (so the healthy path + stays write-free), and only the tripping environment's count is persisted when + the circuit opens. It is ``0`` in states reached without a fresh trip (for + example ``HALF_OPEN``, or ``OPEN`` re-entered after a failed probe). For failure + *volume*, emit a CloudWatch metric from your own code or an ``on_transition`` + hook rather than reading this field. + opened_at : int | None + Unix timestamp (seconds) at which the circuit opened, or ``None`` while the + circuit is closed. Drives the recovery timeout. + + Example + ------- + **Inspecting circuit details inside a callback** + + def on_open(payload: dict, circuit: CircuitInfo): + logger.warning("circuit %s open since %s", circuit.name, circuit.opened_at) + return {"statusCode": 503} + """ + + name: str + state: CircuitState + failure_count: int + opened_at: int | None = None + + +@dataclass(frozen=True) +class CircuitTransition: + """ + Immutable description of a circuit state change, passed to an ``on_transition`` hook. + + The hook fires only on the rare state transitions a circuit makes (open, probe, + close, reopen), never on the per-invocation hot path, so emitting a metric from it + does not undermine the write-free healthy path. + + Parameters + ---------- + circuit_name : str + The circuit name, as given to the ``@circuit_breaker`` decorator. + from_state : CircuitState + The state the circuit was in before the transition. + to_state : CircuitState + The state the circuit moved to. + opened_at : int | None + Unix timestamp (seconds) the circuit opened, when relevant to the new state. + + Example + ------- + **Emit a CloudWatch metric per transition** + + from aws_lambda_powertools.metrics import MetricUnit, single_metric + + def emit(transition: CircuitTransition) -> None: + with single_metric( + namespace="MyApp", + name=f"Circuit{transition.to_state}", + unit=MetricUnit.Count, + value=1, + ) as metric: + metric.add_dimension(name="circuit", value=transition.circuit_name) + """ + + circuit_name: str + from_state: CircuitState + to_state: CircuitState + opened_at: int | None = None diff --git a/aws_lambda_powertools/utilities/data_classes/common.py b/aws_lambda_powertools/utilities/data_classes/common.py index c35a02cc3ce..3b9171b286d 100644 --- a/aws_lambda_powertools/utilities/data_classes/common.py +++ b/aws_lambda_powertools/utilities/data_classes/common.py @@ -163,8 +163,8 @@ def _str_helper(self) -> dict[str, Any]: def _properties(self) -> list[str]: return [p for p in dir(self.__class__) if isinstance(getattr(self.__class__, p), property)] - def get(self, key: str, default: Any | None = None) -> Any | None: - return self._data.get(key, default) + def get(self, key: object, default: Any | None = None) -> Any | None: # type: ignore[override] + return self._data.get(str(key), default) @property def raw_event(self) -> dict[str, Any]: diff --git a/aws_lambda_powertools/utilities/data_classes/event_bridge_event.py b/aws_lambda_powertools/utilities/data_classes/event_bridge_event.py index f7ce1953e2a..4655701574d 100644 --- a/aws_lambda_powertools/utilities/data_classes/event_bridge_event.py +++ b/aws_lambda_powertools/utilities/data_classes/event_bridge_event.py @@ -68,4 +68,4 @@ def detail(self) -> dict[str, Any]: @property def replay_name(self) -> str | None: """Identifies whether the event is being replayed and what is the name of the replay.""" - return self["replay-name"] + return self.get("replay-name") diff --git a/aws_lambda_powertools/utilities/data_classes/event_source.py b/aws_lambda_powertools/utilities/data_classes/event_source.py index 6096e3ae7bc..58c8be968f5 100644 --- a/aws_lambda_powertools/utilities/data_classes/event_source.py +++ b/aws_lambda_powertools/utilities/data_classes/event_source.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Protocol, TypeVar, cast from aws_lambda_powertools.middleware_factory import lambda_handler_decorator @@ -10,14 +10,30 @@ from aws_lambda_powertools.utilities.data_classes.common import DictWrapper from aws_lambda_powertools.utilities.typing import LambdaContext +DataClassT = TypeVar("DataClassT", bound="DictWrapper") +OutputT = TypeVar("OutputT") + + +class _EventSourceDecorator(Protocol): + """Annotation of event_source, that lambda_handler_decorator erases at runtime.""" + + def __call__( + self, + *, + data_class: type[DataClassT], + ) -> Callable[ + [Callable[[DataClassT, LambdaContext], OutputT]], + Callable[[dict[str, Any], LambdaContext], OutputT], + ]: ... + @lambda_handler_decorator -def event_source( - handler: Callable[[Any, LambdaContext], Any], +def _event_source( + handler: Callable[[Any, LambdaContext], OutputT], event: dict[str, Any], context: LambdaContext, - data_class: type[DictWrapper], -): + data_class: type[DataClassT], +) -> OutputT: """Middleware to create an instance of the passed in event source data class Parameters @@ -43,3 +59,6 @@ def handler(event: S3Event, context): return {"key": event.object_key} """ return handler(data_class(event), context) + + +event_source = cast(_EventSourceDecorator, _event_source) diff --git a/aws_lambda_powertools/utilities/idempotency/persistence/base.py b/aws_lambda_powertools/utilities/idempotency/persistence/base.py index 3d54a01f018..2271520139d 100644 --- a/aws_lambda_powertools/utilities/idempotency/persistence/base.py +++ b/aws_lambda_powertools/utilities/idempotency/persistence/base.py @@ -131,7 +131,10 @@ def _get_hashed_idempotency_key(self, data: dict[str, Any]) -> str | None: @staticmethod def is_missing_idempotency_key(data) -> bool: - if isinstance(data, (tuple, list, dict)): + if isinstance(data, dict): + # JMESPath multi-select dicts retain their keys when all selected values are missing. + return all(x is None for x in data.values()) + elif isinstance(data, (tuple, list)): return all(x is None for x in data) elif isinstance(data, (int, float, bool)): return False diff --git a/aws_lambda_powertools/utilities/idempotency/persistence/redis.py b/aws_lambda_powertools/utilities/idempotency/persistence/redis.py index 82a44e079de..ce7100bf215 100644 --- a/aws_lambda_powertools/utilities/idempotency/persistence/redis.py +++ b/aws_lambda_powertools/utilities/idempotency/persistence/redis.py @@ -425,10 +425,12 @@ def _put_in_progress_record(self, data_record: DataRecord) -> None: # (meaning the timestamp is greater than the current timestamp in milliseconds), then we have encountered # a valid in-progress record. This indicates that another process is currently handling the request, and # to maintain idempotency, we raise an error to prevent concurrent processing of the same request. - if ( - idempotency_record.status == STATUS_CONSTANTS["INPROGRESS"] - and idempotency_record.in_progress_expiry_timestamp - and idempotency_record.in_progress_expiry_timestamp > int(now.timestamp() * 1000) + # + # Without an in-progress expiry, we cannot safely distinguish an active invocation from a timed-out one. + # Fail closed until the record TTL expires, consistent with the DynamoDB persistence layer. + if idempotency_record.status == STATUS_CONSTANTS["INPROGRESS"] and ( + idempotency_record.in_progress_expiry_timestamp is None + or idempotency_record.in_progress_expiry_timestamp > int(now.timestamp() * 1000) ): raise IdempotencyItemAlreadyExistsError diff --git a/aws_lambda_powertools/utilities/parser/models/apigw.py b/aws_lambda_powertools/utilities/parser/models/apigw.py index ea01a5e8a6b..349ebbb512b 100644 --- a/aws_lambda_powertools/utilities/parser/models/apigw.py +++ b/aws_lambda_powertools/utilities/parser/models/apigw.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Any, Dict, List, Literal, Optional, Type, Union +from typing import Any, Dict, List, Literal, Type, Union from pydantic import BaseModel, field_validator, model_validator from pydantic.networks import IPvAnyNetwork @@ -21,23 +21,23 @@ class ApiGatewayUserCert(BaseModel): class APIGatewayEventIdentity(BaseModel): - 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 + accessKey: str | None = None + accountId: str | None = None + apiKey: str | None = None + apiKeyId: str | None = None + caller: str | None = None + cognitoAuthenticationProvider: str | None = None + cognitoAuthenticationType: str | None = None + cognitoIdentityId: str | None = None + cognitoIdentityPoolId: str | None = None + principalOrgId: str | None = 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, str] - user: Optional[str] = None - userAgent: Optional[str] = None - userArn: Optional[str] = None - clientCert: Optional[ApiGatewayUserCert] = None + user: str | None = None + userAgent: str | None = None + userArn: str | None = None + clientCert: ApiGatewayUserCert | None = None @field_validator("sourceIp", mode="before") @classmethod @@ -46,34 +46,34 @@ def _validate_source_ip(cls, value): class APIGatewayEventAuthorizer(BaseModel): - claims: Optional[Dict[str, Any]] = None - scopes: Optional[List[str]] = None + claims: Dict[str, Any] | None = None + scopes: List[str] | None = None class APIGatewayEventRequestContext(BaseModel): accountId: str apiId: str - authorizer: Optional[APIGatewayEventAuthorizer] = None + authorizer: APIGatewayEventAuthorizer | None = None stage: str protocol: str identity: APIGatewayEventIdentity requestId: str requestTime: str requestTimeEpoch: datetime - resourceId: Optional[str] = None + resourceId: str | None = None resourcePath: str - domainName: Optional[str] = None - domainPrefix: Optional[str] = None - extendedRequestId: Optional[str] = None + domainName: str | None = None + domainPrefix: str | None = None + extendedRequestId: str | None = None httpMethod: Literal["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] path: 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 + connectedAt: datetime | None = None + connectionId: str | None = None + eventType: Literal["CONNECT", "MESSAGE", "DISCONNECT"] | None = None + messageDirection: str | None = None + messageId: str | None = None + routeKey: str | None = None + operationName: str | None = None @model_validator(mode="before") def check_message_id(cls, values): @@ -84,19 +84,19 @@ def check_message_id(cls, values): class APIGatewayProxyEventModel(BaseModel): - version: Optional[str] = None + version: str | None = 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]] = None - multiValueQueryStringParameters: Optional[Dict[str, List[str]]] = None + queryStringParameters: Dict[str, str] | None = None + multiValueQueryStringParameters: Dict[str, List[str]] | None = None requestContext: APIGatewayEventRequestContext - pathParameters: Optional[Dict[str, str]] = None - stageVariables: Optional[Dict[str, str]] = None - isBase64Encoded: Optional[bool] = None - body: Optional[Union[str, Type[BaseModel]]] = None + pathParameters: Dict[str, str] | None = None + stageVariables: Dict[str, str] | None = None + isBase64Encoded: bool | None = None + body: Union[str, Type[BaseModel]] | None = None class ApiGatewayAuthorizerToken(BaseModel): diff --git a/aws_lambda_powertools/utilities/parser/models/apigw_websocket.py b/aws_lambda_powertools/utilities/parser/models/apigw_websocket.py index b9e7ecd68c7..3454d7d30a0 100644 --- a/aws_lambda_powertools/utilities/parser/models/apigw_websocket.py +++ b/aws_lambda_powertools/utilities/parser/models/apigw_websocket.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Dict, List, Literal, Optional, Type, Union +from typing import Dict, List, Literal, Type, Union from pydantic import BaseModel, Field from pydantic.networks import IPvAnyNetwork @@ -7,7 +7,7 @@ class APIGatewayWebSocketEventIdentity(BaseModel): source_ip: IPvAnyNetwork = Field(alias="sourceIp") - user_agent: Optional[str] = Field(None, alias="userAgent") + user_agent: str | None = Field(None, alias="userAgent") class APIGatewayWebSocketEventRequestContextBase(BaseModel): @@ -61,4 +61,4 @@ class APIGatewayWebSocketDisconnectEventModel(BaseModel): class APIGatewayWebSocketMessageEventModel(BaseModel): request_context: APIGatewayWebSocketMessageEventRequestContext = Field(alias="requestContext") is_base64_encoded: bool = Field(alias="isBase64Encoded") - body: Optional[Union[str, Type[BaseModel]]] = Field(None, alias="body") + body: Union[str, Type[BaseModel]] | None = Field(None, alias="body") diff --git a/aws_lambda_powertools/utilities/parser/models/apigwv2.py b/aws_lambda_powertools/utilities/parser/models/apigwv2.py index 9bd66b7a585..b01cb1beb03 100644 --- a/aws_lambda_powertools/utilities/parser/models/apigwv2.py +++ b/aws_lambda_powertools/utilities/parser/models/apigwv2.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Any, Dict, List, Literal, Optional, Type, Union +from typing import Any, Dict, List, Literal, Type, Union from pydantic import BaseModel, Field, field_validator from pydantic.networks import IPvAnyNetwork @@ -14,24 +14,24 @@ class RequestContextV2AuthorizerIamCognito(BaseModel): class RequestContextV2AuthorizerIam(BaseModel): - 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 + accessKey: str | None = None + accountId: str | None = None + callerId: str | None = None + principalOrgId: str | None = None + userArn: str | None = None + userId: str | None = None + cognitoIdentity: RequestContextV2AuthorizerIamCognito | None = None class RequestContextV2AuthorizerJwt(BaseModel): claims: Dict[str, Any] - scopes: Optional[List[str]] = None + scopes: List[str] | None = None class RequestContextV2Authorizer(BaseModel): - jwt: Optional[RequestContextV2AuthorizerJwt] = None - iam: Optional[RequestContextV2AuthorizerIam] = None - lambda_value: Optional[Dict[str, Any]] = Field(None, alias="lambda") + jwt: RequestContextV2AuthorizerJwt | None = None + iam: RequestContextV2AuthorizerIam | None = None + lambda_value: Dict[str, Any] | None = Field(None, alias="lambda") class RequestContextV2Http(BaseModel): @@ -50,7 +50,7 @@ def _validate_source_ip(cls, value): class RequestContextV2(BaseModel): accountId: str apiId: str - authorizer: Optional[RequestContextV2Authorizer] = None + authorizer: RequestContextV2Authorizer | None = None domainName: str domainPrefix: str requestId: str @@ -66,17 +66,17 @@ class APIGatewayProxyEventV2Model(BaseModel): routeKey: str rawPath: str rawQueryString: str - cookies: Optional[List[str]] = None + cookies: List[str] | None = None headers: Dict[str, str] - queryStringParameters: Optional[Dict[str, str]] = None - pathParameters: Optional[Dict[str, str]] = None - stageVariables: Optional[Dict[str, str]] = None + queryStringParameters: Dict[str, str] | None = None + pathParameters: Dict[str, str] | None = None + stageVariables: Dict[str, str] | None = None requestContext: RequestContextV2 - body: Optional[Union[str, Type[BaseModel]]] = None - isBase64Encoded: Optional[bool] = None + body: Union[str, Type[BaseModel]] | None = None + isBase64Encoded: bool | None = None class ApiGatewayAuthorizerRequestV2(APIGatewayProxyEventV2Model): type: Literal["REQUEST"] routeArn: str - identitySource: Optional[List[str]] = None + identitySource: List[str] | None = None diff --git a/aws_lambda_powertools/utilities/parser/models/appsync.py b/aws_lambda_powertools/utilities/parser/models/appsync.py index 6d6deedcdcc..e9fe251d38f 100644 --- a/aws_lambda_powertools/utilities/parser/models/appsync.py +++ b/aws_lambda_powertools/utilities/parser/models/appsync.py @@ -1,16 +1,16 @@ -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Union from pydantic import BaseModel, Field class AppSyncIamIdentity(BaseModel): accountId: str = Field(description="The AWS account ID of the caller.", examples=["123456789012"]) - cognitoIdentityPoolId: Optional[str] = Field( + cognitoIdentityPoolId: str | None = Field( default=None, description="The Amazon Cognito identity pool ID associated with the caller.", examples=["us-east-1:12345678-1234-1234-1234-123456789012"], ) - cognitoIdentityId: Optional[str] = Field( + cognitoIdentityId: str | None = Field( default=None, description="The Amazon Cognito identity ID of the caller.", examples=["us-east-1:12345678-1234-1234-1234-123456789012"], @@ -29,12 +29,12 @@ class AppSyncIamIdentity(BaseModel): description="The Amazon Resource Name (ARN) of the IAM user.", examples=["arn:aws:iam::123456789012:user/appsync", "arn:aws:iam::123456789012:user/service-user"], ) - cognitoIdentityAuthType: Optional[str] = Field( + cognitoIdentityAuthType: str | None = Field( default=None, description="Either authenticated or unauthenticated based on the identity type.", examples=["authenticated", "unauthenticated"], ) - cognitoIdentityAuthProvider: Optional[str] = Field( + cognitoIdentityAuthProvider: str | None = Field( default=None, description=( "A comma-separated list of external identity provider information " @@ -74,7 +74,7 @@ class AppSyncCognitoIdentity(BaseModel): description="The default authorization strategy for this caller (ALLOW or DENY).", examples=["ALLOW", "DENY"], ) - groups: Optional[List[str]] = Field( + groups: List[str] | None = Field( default=None, description="The Cognito User Pool groups that the user belongs to.", examples=[["admin", "users"], ["developers"]], @@ -115,7 +115,7 @@ class AppSyncLambdaIdentity(BaseModel): class AppSyncRequestModel(BaseModel): - domainName: Optional[str] = Field( + domainName: str | None = Field( default=None, description=( "The custom domain name used to access the GraphQL endpoint. " @@ -190,11 +190,11 @@ class AppSyncResolverEventModel(BaseModel): {"page": 2, "size": 1, "name": "value"}, ], ) - identity: Optional[AppSyncIdentity] = Field( + identity: AppSyncIdentity | None = Field( default=None, description="Information about the caller identity (authenticated user or API key).", ) - source: Optional[Dict[str, Any]] = Field( + source: Dict[str, Any] | None = Field( default=None, description="The parent object for the field. For top-level fields, this will be null.", examples=[ @@ -208,7 +208,7 @@ class AppSyncResolverEventModel(BaseModel): info: AppSyncInfoModel = Field( description="Information about the GraphQL request including selection set and field details.", ) - prev: Optional[AppSyncPrevModel] = Field( + prev: AppSyncPrevModel | None = Field( default=None, description="Results from the previous resolver in a pipeline resolver.", ) diff --git a/aws_lambda_powertools/utilities/parser/models/appsync_events.py b/aws_lambda_powertools/utilities/parser/models/appsync_events.py index ceeb3ae621a..a1168d5dd3f 100644 --- a/aws_lambda_powertools/utilities/parser/models/appsync_events.py +++ b/aws_lambda_powertools/utilities/parser/models/appsync_events.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Literal, Optional +from typing import Any, Dict, List, Literal from pydantic import BaseModel, Field @@ -51,7 +51,7 @@ class AppSyncEventsEventModel(BaseModel): class AppSyncEventsModel(BaseModel): - identity: Optional[AppSyncIdentity] = Field( + identity: AppSyncIdentity | None = Field( default=None, description="Information about the caller identity (authenticated user or API key).", ) @@ -59,17 +59,17 @@ class AppSyncEventsModel(BaseModel): info: AppSyncEventsInfoModel = Field( description="Information about the AppSync Events operation including channel details.", ) - prev: Optional[str] = Field( + prev: str | None = Field( default=None, description="Results from the previous operation in a pipeline resolver.", examples=["previous-result-data"], ) - outErrors: Optional[List[str]] = Field( + outErrors: List[str] | None = Field( default=None, description="List of output errors that occurred during event processing.", examples=[["Error message 1", "Error message 2"]], ) - stash: Optional[Dict[str, Any]] = Field( + stash: Dict[str, Any] | None = Field( default=None, description=( "The stash is a map that is made available inside each resolver and function mapping template. " @@ -77,7 +77,7 @@ class AppSyncEventsModel(BaseModel): ), examples=[{"customData": "value", "userId": "123"}], ) - events: Optional[List[AppSyncEventsEventModel]] = Field( + events: List[AppSyncEventsEventModel] | None = Field( default=None, description="List of events being published or subscribed to in the AppSync Events operation.", examples=[ diff --git a/aws_lambda_powertools/utilities/parser/models/bedrock_agent.py b/aws_lambda_powertools/utilities/parser/models/bedrock_agent.py index 1aa5ae07a34..c06cfd457b6 100644 --- a/aws_lambda_powertools/utilities/parser/models/bedrock_agent.py +++ b/aws_lambda_powertools/utilities/parser/models/bedrock_agent.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional +from typing import Dict, List from pydantic import BaseModel, Field @@ -34,8 +34,8 @@ class BedrockAgentEventModel(BaseModel): session_attributes: Dict[str, str] = Field({}, alias="sessionAttributes") prompt_session_attributes: Dict[str, str] = Field({}, alias="promptSessionAttributes") agent: BedrockAgentModel - parameters: Optional[List[BedrockAgentPropertyModel]] = None - request_body: Optional[BedrockAgentRequestBodyModel] = Field(None, alias="requestBody") + parameters: List[BedrockAgentPropertyModel] | None = None + request_body: BedrockAgentRequestBodyModel | None = Field(None, alias="requestBody") class BedrockAgentFunctionEventModel(BaseModel): @@ -51,6 +51,6 @@ class BedrockAgentFunctionEventModel(BaseModel): session_id: str = Field(..., alias="sessionId") action_group: str = Field(..., alias="actionGroup") function: str - parameters: Optional[List[BedrockAgentPropertyModel]] = None + parameters: List[BedrockAgentPropertyModel] | None = None session_attributes: Dict[str, str] = Field({}, alias="sessionAttributes") prompt_session_attributes: Dict[str, str] = Field({}, alias="promptSessionAttributes") diff --git a/aws_lambda_powertools/utilities/parser/models/cloudwatch.py b/aws_lambda_powertools/utilities/parser/models/cloudwatch.py index 6c3d25e1727..0fdc7562962 100644 --- a/aws_lambda_powertools/utilities/parser/models/cloudwatch.py +++ b/aws_lambda_powertools/utilities/parser/models/cloudwatch.py @@ -3,7 +3,7 @@ import logging import zlib from datetime import datetime -from typing import List, Optional, Type, Union +from typing import List, Type, Union from pydantic import BaseModel, Field, field_validator @@ -47,7 +47,7 @@ class CloudWatchLogsDecode(BaseModel): description="Array of log events included in the message.", examples=[[{"id": "eventId1", "timestamp": 1673779200000, "message": "Sample log line"}]], ) - policyLevel: Optional[str] = Field( + policyLevel: str | None = Field( default=None, description="Optional field specifying the policy level applied to the subscription filter, if present.", examples=["ACCOUNT", "LOG_GROUP"], diff --git a/aws_lambda_powertools/utilities/parser/models/cognito.py b/aws_lambda_powertools/utilities/parser/models/cognito.py index 05726e37db4..75a21254324 100644 --- a/aws_lambda_powertools/utilities/parser/models/cognito.py +++ b/aws_lambda_powertools/utilities/parser/models/cognito.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Literal, Optional +from typing import Any, Dict, List, Literal from pydantic import BaseModel @@ -14,22 +14,22 @@ class CognitoTriggerBaseSchema(BaseModel): version: str region: str userPoolId: str - userName: Optional[str] = None + userName: str | None = None callerContext: CognitoCallerContextModel # Models for Pre-Signup flow class CognitoPreSignupRequestModel(BaseModel): userAttributes: Dict[str, Any] - validationData: Optional[Dict[str, Any]] = None - clientMetadata: Optional[Dict[str, Any]] = None - userNotFound: Optional[bool] = None + validationData: Dict[str, Any] | None = None + clientMetadata: Dict[str, Any] | None = None + userNotFound: bool | None = None class CognitoPreSignupResponseModel(BaseModel): - autoConfirmUser: Optional[bool] = False - autoVerifyPhone: Optional[bool] = False - autoVerifyEmail: Optional[bool] = False + autoConfirmUser: bool | None = False + autoVerifyPhone: bool | None = False + autoVerifyEmail: bool | None = False class CognitoPreSignupTriggerModel(CognitoTriggerBaseSchema): @@ -41,7 +41,7 @@ class CognitoPreSignupTriggerModel(CognitoTriggerBaseSchema): # Models for Post-Confirmation flow class CognitoPostConfirmationRequestModel(BaseModel): userAttributes: Dict[str, Any] - clientMetadata: Optional[Dict[str, Any]] = None + clientMetadata: Dict[str, Any] | None = None class CognitoPostConfirmationTriggerModel(CognitoTriggerBaseSchema): @@ -53,8 +53,8 @@ class CognitoPostConfirmationTriggerModel(CognitoTriggerBaseSchema): # Models for Pre-Authentication flow class CognitoPreAuthenticationRequestModel(BaseModel): userAttributes: Dict[str, Any] - validationData: Optional[Dict[str, Any]] = None - userNotFound: Optional[bool] = None + validationData: Dict[str, Any] | None = None + userNotFound: bool | None = None class CognitoPreAuthenticationTriggerModel(CognitoTriggerBaseSchema): @@ -66,8 +66,8 @@ class CognitoPreAuthenticationTriggerModel(CognitoTriggerBaseSchema): # Models for Post-Authentication flow class CognitoPostAuthenticationRequestModel(BaseModel): userAttributes: Dict[str, Any] - newDeviceUsed: Optional[bool] = None - clientMetadata: Optional[Dict[str, Any]] = None + newDeviceUsed: bool | None = None + clientMetadata: Dict[str, Any] | None = None class CognitoPostAuthenticationTriggerModel(CognitoTriggerBaseSchema): @@ -80,13 +80,13 @@ class CognitoPostAuthenticationTriggerModel(CognitoTriggerBaseSchema): class CognitoGroupConfigurationModel(BaseModel): groupsToOverride: List[str] iamRolesToOverride: List[str] - preferredRole: Optional[str] = None + preferredRole: str | None = None class CognitoPreTokenGenerationRequestModel(BaseModel): userAttributes: Dict[str, Any] groupConfiguration: CognitoGroupConfigurationModel - clientMetadata: Optional[Dict[str, Any]] = None + clientMetadata: Dict[str, Any] | None = None class CognitoPreTokenGenerationTriggerModelV1(CognitoTriggerBaseSchema): @@ -96,7 +96,7 @@ class CognitoPreTokenGenerationTriggerModelV1(CognitoTriggerBaseSchema): class CognitoPreTokenGenerationRequestModelV2AndV3(CognitoPreTokenGenerationRequestModel): - scopes: Optional[Dict[str, Any]] = None + scopes: Dict[str, Any] | None = None class CognitoPreTokenGenerationTriggerModelV2AndV3(CognitoTriggerBaseSchema): @@ -107,17 +107,17 @@ class CognitoPreTokenGenerationTriggerModelV2AndV3(CognitoTriggerBaseSchema): # Models for User Migration flow class CognitoMigrateUserRequestModel(BaseModel): password: str - validationData: Optional[Dict[str, Any]] = None - clientMetadata: Optional[Dict[str, Any]] = None + validationData: Dict[str, Any] | None = None + clientMetadata: Dict[str, Any] | None = None class CognitoMigrateUserResponseModel(BaseModel): - userAttributes: Optional[Dict[str, Any]] = None - finalUserStatus: Optional[str] = None - messageAction: Optional[str] = None - desiredDeliveryMediums: Optional[List[str]] = None - forceAliasCreation: Optional[bool] = None - enableSMSMFA: Optional[bool] = None + userAttributes: Dict[str, Any] | None = None + finalUserStatus: str | None = None + messageAction: str | None = None + desiredDeliveryMediums: List[str] | None = None + forceAliasCreation: bool | None = None + enableSMSMFA: bool | None = None class CognitoMigrateUserTriggerModel(CognitoTriggerBaseSchema): @@ -131,15 +131,15 @@ class CognitoMigrateUserTriggerModel(CognitoTriggerBaseSchema): class CognitoCustomMessageRequestModel(BaseModel): userAttributes: Dict[str, Any] codeParameter: str - linkParameter: Optional[str] = None - usernameParameter: Optional[str] = None - clientMetadata: Optional[Dict[str, Any]] = None + linkParameter: str | None = None + usernameParameter: str | None = None + clientMetadata: Dict[str, Any] | None = None class CognitoCustomMessageResponseModel(BaseModel): - smsMessage: Optional[str] = None - emailMessage: Optional[str] = None - emailSubject: Optional[str] = None + smsMessage: str | None = None + emailMessage: str | None = None + emailSubject: str | None = None class CognitoCustomMessageTriggerModel(CognitoTriggerBaseSchema): @@ -152,7 +152,7 @@ class CognitoCustomMessageTriggerModel(CognitoTriggerBaseSchema): class CognitoCustomEmailSMSSenderRequestModel(BaseModel): type: str code: str - clientMetadata: Optional[Dict[str, Any]] = None + clientMetadata: Dict[str, Any] | None = None userAttributes: Dict[str, Any] @@ -179,20 +179,20 @@ class CognitoChallengeResultModel(BaseModel): "ADMIN_NO_SRP_AUTH", ] challengeResult: bool - challengeMetadata: Optional[str] = None + challengeMetadata: str | None = None class CognitoAuthChallengeRequestModel(BaseModel): userAttributes: Dict[str, Any] session: List[CognitoChallengeResultModel] - clientMetadata: Optional[Dict[str, Any]] = None - userNotFound: Optional[bool] = None + clientMetadata: Dict[str, Any] | None = None + userNotFound: bool | None = None class CognitoDefineAuthChallengeResponseModel(BaseModel): - challengeName: Optional[str] = None - issueTokens: Optional[bool] = None - failAuthentication: Optional[bool] = None + challengeName: str | None = None + issueTokens: bool | None = None + failAuthentication: bool | None = None class CognitoDefineAuthChallengeTriggerModel(CognitoTriggerBaseSchema): @@ -202,9 +202,9 @@ class CognitoDefineAuthChallengeTriggerModel(CognitoTriggerBaseSchema): class CognitoCreateAuthChallengeResponseModel(BaseModel): - publicChallengeParameters: Optional[Dict[str, Any]] = None - privateChallengeParameters: Optional[Dict[str, Any]] = None - challengeMetadata: Optional[str] = None + publicChallengeParameters: Dict[str, Any] | None = None + privateChallengeParameters: Dict[str, Any] | None = None + challengeMetadata: str | None = None class CognitoCreateAuthChallengeTriggerModel(CognitoTriggerBaseSchema): @@ -217,8 +217,8 @@ class CognitoVerifyAuthChallengeRequestModel(BaseModel): userAttributes: Dict[str, Any] privateChallengeParameters: Dict[str, Any] challengeAnswer: str - clientMetadata: Optional[Dict[str, Any]] = None - userNotFound: Optional[bool] = None + clientMetadata: Dict[str, Any] | None = None + userNotFound: bool | None = None class CognitoVerifyAuthChallengeResponseModel(BaseModel): diff --git a/aws_lambda_powertools/utilities/parser/models/dynamodb.py b/aws_lambda_powertools/utilities/parser/models/dynamodb.py index d6091d32d7a..714cb06e90a 100644 --- a/aws_lambda_powertools/utilities/parser/models/dynamodb.py +++ b/aws_lambda_powertools/utilities/parser/models/dynamodb.py @@ -1,6 +1,6 @@ # ruff: noqa: FA100 from datetime import datetime -from typing import Any, Dict, List, Literal, Optional, Type, Union +from typing import Any, Dict, List, Literal, Type, Union from pydantic import BaseModel, ConfigDict, Field, field_validator from pydantic.alias_generators import to_camel @@ -11,18 +11,18 @@ class DynamoDBStreamChangedRecordModel(BaseModel): - ApproximateCreationDateTime: Optional[datetime] = Field( # AWS sends this as Unix epoch float + ApproximateCreationDateTime: datetime | None = Field( # AWS sends this as Unix epoch float default=None, description="The approximate date and time when the stream record was created (Unix epoch time).", examples=[1693997155.0], ) Keys: Dict[str, Any] = Field(description="Primary key attributes for the item.", examples=[{"Id": {"N": "101"}}]) - NewImage: Optional[Union[Dict[str, Any], Type[BaseModel], BaseModel]] = Field( + NewImage: Union[Dict[str, Any], Type[BaseModel], BaseModel] | None = Field( default=None, description="The item after modifications, in DynamoDB attribute-value format.", examples=[{"Message": {"S": "New item!"}, "Id": {"N": "101"}}], ) - OldImage: Optional[Union[Dict[str, Any], Type[BaseModel], BaseModel]] = Field( + OldImage: Union[Dict[str, Any], Type[BaseModel], BaseModel] | None = Field( default=None, description="The item before modifications, in DynamoDB attribute-value format.", examples=[{"Message": {"S": "Old item!"}, "Id": {"N": "100"}}], @@ -80,7 +80,7 @@ class DynamoDBStreamRecordModel(BaseModel): }, ], ) - userIdentity: Optional[UserIdentity] = Field( + userIdentity: UserIdentity | None = Field( default=None, description="Information about the identity that made the request.", examples=[{"type": "Service", "principalId": "dynamodb.amazonaws.com"}], @@ -175,7 +175,8 @@ class ResponseContext(BaseModel): description="The version of the Lambda executed", examples=["$LATEST"], ) - function_error: str = Field( + function_error: str | None = Field( + default=None, description="", examples=["Unhandled"], ) diff --git a/aws_lambda_powertools/utilities/parser/models/event_bridge.py b/aws_lambda_powertools/utilities/parser/models/event_bridge.py index 67eef21dbf2..11c2e28bc41 100644 --- a/aws_lambda_powertools/utilities/parser/models/event_bridge.py +++ b/aws_lambda_powertools/utilities/parser/models/event_bridge.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import List, Optional +from typing import List from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -50,7 +50,7 @@ class EventBridgeModel(BaseModel): detail: RawDictOrModel = Field( description="A JSON object, whose content is at the discretion of the service originating the event.", ) - replay_name: Optional[str] = Field( + replay_name: str | None = Field( None, alias="replay-name", description="Identifies whether the event is being replayed and what is the name of the replay.", diff --git a/aws_lambda_powertools/utilities/parser/models/iot_registry_events.py b/aws_lambda_powertools/utilities/parser/models/iot_registry_events.py index 7af5992a20d..950aa01fe53 100644 --- a/aws_lambda_powertools/utilities/parser/models/iot_registry_events.py +++ b/aws_lambda_powertools/utilities/parser/models/iot_registry_events.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Any, Dict, List, Literal, Optional +from typing import Any, Dict, List, Literal from pydantic import BaseModel, Field @@ -25,7 +25,7 @@ class IoTCoreThingEvent(IoTCoreRegistryEventsBase): account_id: str = Field(..., alias="accountId") thing_name: str = Field(..., alias="thingName") version_number: int = Field(..., alias="versionNumber") - thing_type_name: Optional[str] = Field(None, alias="thingTypeName") + thing_type_name: str | None = Field(None, alias="thingTypeName") attributes: Dict[str, Any] @@ -46,7 +46,7 @@ class IoTCoreThingTypeEvent(IoTCoreRegistryEventsBase): thing_type_id: str = Field(..., alias="thingTypeId") thing_type_name: str = Field(..., alias="thingTypeName") is_deprecated: bool = Field(..., alias="isDeprecated") - deprecation_date: Optional[datetime] = Field(None, alias="deprecationDate") + deprecation_date: datetime | None = Field(None, alias="deprecationDate") searchable_attributes: List[str] = Field(..., alias="searchableAttributes") propagating_attributes: List[Dict[str, str]] = Field(..., alias="propagatingAttributes") description: str @@ -84,12 +84,12 @@ class IoTCoreThingGroupEvent(IoTCoreRegistryEventsBase): thing_group_id: str = Field(..., alias="thingGroupId") thing_group_name: str = Field(..., alias="thingGroupName") version_number: int = Field(..., alias="versionNumber") - parent_group_name: Optional[str] = Field(None, alias="parentGroupName") - parent_group_id: Optional[str] = Field(None, alias="parentGroupId") + parent_group_name: str | None = Field(None, alias="parentGroupName") + parent_group_id: str | None = Field(None, alias="parentGroupId") description: str root_to_parent_thing_groups: List[Dict[str, str]] = Field(..., alias="rootToParentThingGroups") attributes: Dict[str, Any] - dynamic_group_mapping_id: Optional[str] = Field(None, alias="dynamicGroupMappingId") + dynamic_group_mapping_id: str | None = Field(None, alias="dynamicGroupMappingId") class IoTCoreAddOrRemoveFromThingGroupEvent(IoTCoreRegistryEventsBase): diff --git a/aws_lambda_powertools/utilities/parser/models/kafka.py b/aws_lambda_powertools/utilities/parser/models/kafka.py index df232469b95..9ce0063d09b 100644 --- a/aws_lambda_powertools/utilities/parser/models/kafka.py +++ b/aws_lambda_powertools/utilities/parser/models/kafka.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Dict, List, Literal, Optional, Type, Union +from typing import Dict, List, Literal, Type, Union from pydantic import BaseModel, Field, field_validator @@ -40,7 +40,7 @@ class KafkaRecordModel(BaseModel): description="The type of timestamp (CREATE_TIME or LOG_APPEND_TIME).", examples=["CREATE_TIME", "LOG_APPEND_TIME"], ) - key: Optional[bytes] = Field( + key: bytes | None = Field( default=None, description="The message key, base64-encoded. Can be null for messages without keys.", examples=["cmVjb3JkS2V5", "dXNlci0xMjM=", "b3JkZXItNDU2", None], @@ -61,12 +61,12 @@ class KafkaRecordModel(BaseModel): [], ], ) - keySchemaMetadata: Optional[KafkaRecordSchemaMetadata] = Field( + keySchemaMetadata: KafkaRecordSchemaMetadata | None = Field( default=None, description="Schema metadata for the message key when using schema registry.", examples=[{"dataFormat": "AVRO", "schemaId": "1234"}, None], ) - valueSchemaMetadata: Optional[KafkaRecordSchemaMetadata] = Field( + valueSchemaMetadata: KafkaRecordSchemaMetadata | None = Field( default=None, description="Schema metadata for the message value when using schema registry.", examples=[{"dataFormat": "AVRO", "schemaId": "1234"}, None], diff --git a/aws_lambda_powertools/utilities/parser/models/kinesis_firehose.py b/aws_lambda_powertools/utilities/parser/models/kinesis_firehose.py index 697a3fbdd89..77e7b47baa6 100644 --- a/aws_lambda_powertools/utilities/parser/models/kinesis_firehose.py +++ b/aws_lambda_powertools/utilities/parser/models/kinesis_firehose.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Type, Union +from typing import List, Type, Union from pydantic import BaseModel, Field, PositiveInt, field_validator @@ -45,7 +45,7 @@ class KinesisFirehoseRecord(BaseModel): (Unix timestamp in milliseconds).", examples=[1428537600000, 1609459200500], ) - kinesisRecordMetadata: Optional[KinesisFirehoseRecordMetadata] = Field( + kinesisRecordMetadata: KinesisFirehoseRecordMetadata | None = Field( None, description="Metadata about the original Kinesis stream record \ (only present when the delivery stream source is a Kinesis stream).", @@ -69,7 +69,7 @@ class KinesisFirehoseModel(BaseModel): description="The AWS region where the delivery stream is located.", examples=["us-east-1", "us-west-2", "eu-west-1"], ) - sourceKinesisStreamArn: Optional[str] = Field( + sourceKinesisStreamArn: str | None = Field( None, description="The ARN of the source Kinesis stream \ (only present when the delivery stream source is a Kinesis stream).", 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 b9032a4c934..ecad8b26fc7 100644 --- a/aws_lambda_powertools/utilities/parser/models/kinesis_firehose_sqs.py +++ b/aws_lambda_powertools/utilities/parser/models/kinesis_firehose_sqs.py @@ -1,5 +1,5 @@ import json -from typing import List, Optional +from typing import List from pydantic import BaseModel, Field, PositiveInt, field_validator @@ -20,7 +20,7 @@ class KinesisFirehoseSqsRecord(BaseModel): (Unix timestamp in milliseconds).", examples=[1428537600000, 1609459200500], ) - kinesisRecordMetadata: Optional[KinesisFirehoseRecordMetadata] = Field( + kinesisRecordMetadata: KinesisFirehoseRecordMetadata | None = Field( None, description="Metadata about the original Kinesis stream record \ (only present when the delivery stream source is a Kinesis stream).", @@ -45,7 +45,7 @@ class KinesisFirehoseSqsModel(BaseModel): description="The AWS region where the delivery stream is located.", examples=["us-east-1", "us-west-2", "eu-west-1"], ) - sourceKinesisStreamArn: Optional[str] = Field( + sourceKinesisStreamArn: str | None = Field( None, description="The ARN of the source Kinesis stream \ (only present when the delivery stream source is a Kinesis stream).", diff --git a/aws_lambda_powertools/utilities/parser/models/s3.py b/aws_lambda_powertools/utilities/parser/models/s3.py index d53a0fe5655..a2aeb945e9e 100644 --- a/aws_lambda_powertools/utilities/parser/models/s3.py +++ b/aws_lambda_powertools/utilities/parser/models/s3.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import List, Literal, Optional, Union +from typing import List, Literal, Union from pydantic import BaseModel, model_validator from pydantic.fields import Field @@ -130,12 +130,12 @@ class S3Object(BaseModel): "logs/2023/01/15/app.log", ], ) - size: Optional[NonNegativeFloat] = Field( + size: NonNegativeFloat | None = Field( default=None, description="The size of the object in bytes.", examples=[1024, 2048576, 0], ) - eTag: Optional[str] = Field( + eTag: str | None = Field( default=None, description="The entity tag (ETag) of the object.", examples=[ @@ -143,7 +143,7 @@ class S3Object(BaseModel): "098f6bcd4621d373cade4e832627b4f6", ], ) - sequencer: Optional[str] = Field( + sequencer: str | None = Field( default=None, description="A string representation of a hexadecimal value used to determine event sequence.", examples=[ @@ -151,7 +151,7 @@ class S3Object(BaseModel): "005B21C13A6F24045E", ], ) - versionId: Optional[str] = Field( + versionId: str | None = Field( default=None, description="The version ID of the object (if versioning is enabled).", examples=[ @@ -188,7 +188,7 @@ class S3Message(BaseModel): }, ], ) - object: Optional[S3Object] = Field( + object: S3Object | None = Field( default=None, description="The S3 object object. Used by most S3 event types.", examples=[ @@ -200,7 +200,7 @@ class S3Message(BaseModel): }, ], ) # noqa: A003 - get_object: Optional[S3Object] = Field( + get_object: S3Object | None = Field( default=None, alias="get_object", description="The S3 object object. Used by IntelligentTiering events instead of 'object'.", @@ -225,7 +225,7 @@ class S3EventNotificationObjectModel(BaseModel): "logs/2023/01/15/app.log", ], ) - size: Optional[NonNegativeFloat] = Field( + size: NonNegativeFloat | None = Field( default=None, description="The size of the object in bytes.", examples=[1024, 2048576, 0], @@ -238,7 +238,7 @@ class S3EventNotificationObjectModel(BaseModel): "098f6bcd4621d373cade4e832627b4f6", ], ) - version_id: Optional[str] = Field( + version_id: str | None = Field( default=None, alias="version-id", description="The version ID of the object (if versioning is enabled).", @@ -247,7 +247,7 @@ class S3EventNotificationObjectModel(BaseModel): "null", ], ) - sequencer: Optional[str] = Field( + sequencer: str | None = Field( default=None, description="A string representation of a hexadecimal value used to determine event sequence.", examples=[ @@ -307,7 +307,7 @@ class S3EventNotificationEventBridgeDetailModel(BaseModel): "123456789012", ], ) - source_ip_address: Optional[str] = Field( + source_ip_address: str | None = Field( None, alias="source-ip-address", description="Source IP address of S3 request. Only present for events triggered by an S3 request.", @@ -315,7 +315,7 @@ class S3EventNotificationEventBridgeDetailModel(BaseModel): "255.255.255.255", ], ) - reason: Optional[str] = Field( + reason: str | None = Field( default=None, description="For 'Object Created' events, the S3 API used to create the object: PutObject, POST Object, " "CopyObject, or CompleteMultipartUpload. For 'Object Deleted' events, this is set to 'DeleteObject' " @@ -327,7 +327,7 @@ class S3EventNotificationEventBridgeDetailModel(BaseModel): "DeleteObject", ], ) - deletion_type: Optional[str] = Field( + deletion_type: str | None = Field( default=None, alias="deletion-type", description="For 'Object Deleted' events, when an unversioned object is deleted, or a versioned object is " @@ -336,28 +336,28 @@ class S3EventNotificationEventBridgeDetailModel(BaseModel): "see https://docs.aws.amazon.com/AmazonS3/latest/userguide/DeletingObjectVersions.html", examples=["Delete Marker Created", "Permanently Deleted"], ) - restore_expiry_time: Optional[str] = Field( + restore_expiry_time: str | None = Field( default=None, alias="restore-expiry-time", description="For 'Object Restore Completed' events, the time when the temporary copy of the object will be " "deleted from S3. For more information, see https://docs.aws.amazon.com/AmazonS3/latest/userguide/archived-objects.html.", examples=["2021-11-13T00:00:00Z"], ) - source_storage_class: Optional[str] = Field( + source_storage_class: str | None = Field( default=None, alias="source-storage-class", description="For 'Object Restore Initiated' and 'Object Restore Completed' events, the storage class of the " "object being restored. For more information, see https://docs.aws.amazon.com/AmazonS3/latest/userguide/archived-objects.html.", examples=["GLACIER", "STANDARD", "STANDARD_IA"], ) - destination_storage_class: Optional[str] = Field( + destination_storage_class: str | None = Field( default=None, alias="destination-storage-class", description="For 'Object Storage Class Changed' events, the new storage class of the object. For more " "information, see https://docs.aws.amazon.com/AmazonS3/latest/userguide/lifecycle-transition-general-considerations.html.", examples=["INTELLIGENT_TIERING", "STANDARD", "STANDARD_IA"], ) - destination_access_tier: Optional[str] = Field( + destination_access_tier: str | None = Field( default=None, alias="destination-access-tier", description="For 'Object Access Tier Changed' events, the new access tier of the object. For more information, " @@ -462,7 +462,7 @@ class S3RecordModel(BaseModel): }, ], ) - glacierEventData: Optional[S3EventRecordGlacierEventData] = Field( + glacierEventData: S3EventRecordGlacierEventData | None = Field( default=None, description="The Glacier event data object.", examples=[ @@ -474,7 +474,7 @@ class S3RecordModel(BaseModel): }, ], ) - intelligentTieringEventData: Optional[S3EventRecordIntelligentTieringEventData] = Field( + intelligentTieringEventData: S3EventRecordIntelligentTieringEventData | None = Field( default=None, description="The Intelligent-Tiering event data object.", examples=[ diff --git a/aws_lambda_powertools/utilities/parser/models/s3_batch_operation.py b/aws_lambda_powertools/utilities/parser/models/s3_batch_operation.py index 934ace9ac07..b9581243f51 100644 --- a/aws_lambda_powertools/utilities/parser/models/s3_batch_operation.py +++ b/aws_lambda_powertools/utilities/parser/models/s3_batch_operation.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Literal, Optional +from typing import Any, Dict, List, Literal from pydantic import BaseModel, model_validator @@ -6,9 +6,9 @@ class S3BatchOperationTaskModel(BaseModel): taskId: str s3Key: str - s3VersionId: Optional[str] = None - s3BucketArn: Optional[str] = None - s3Bucket: Optional[str] = None + s3VersionId: str | None = None + s3BucketArn: str | None = None + s3Bucket: str | None = None @model_validator(mode="before") def validate_s3bucket(cls, values: Dict[str, Any]) -> Dict[str, Any]: @@ -20,7 +20,7 @@ def validate_s3bucket(cls, values: Dict[str, Any]) -> Dict[str, Any]: class S3BatchOperationJobModel(BaseModel): id: str - userArguments: Optional[Dict[str, Any]] = None + userArguments: Dict[str, Any] | None = None class S3BatchOperationModel(BaseModel): 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 867cd996fa0..1bd2c2628af 100644 --- a/aws_lambda_powertools/utilities/parser/models/s3_object_event.py +++ b/aws_lambda_powertools/utilities/parser/models/s3_object_event.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional, Type, Union +from typing import Dict, Type, Union from pydantic import BaseModel, HttpUrl @@ -22,7 +22,7 @@ class S3ObjectUserRequest(BaseModel): class S3ObjectSessionIssuer(BaseModel): type: str # noqa: A003, VNE003 - userName: Optional[str] = None + userName: str | None = None principalId: str arn: str accountId: str @@ -42,10 +42,10 @@ class S3ObjectUserIdentity(BaseModel): type: str # noqa: A003 accountId: str accessKeyId: str - userName: Optional[str] = None + userName: str | None = None principalId: str arn: str - sessionContext: Optional[S3ObjectSessionContext] = None + sessionContext: S3ObjectSessionContext | None = 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 9a7a9914e6e..833cc557df0 100644 --- a/aws_lambda_powertools/utilities/parser/models/ses.py +++ b/aws_lambda_powertools/utilities/parser/models/ses.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import List, Literal, Optional +from typing import List, Literal from pydantic import BaseModel, Field from pydantic.types import PositiveInt @@ -34,10 +34,10 @@ class SesMailHeaders(BaseModel): class SesMailCommonHeaders(BaseModel): header_from: List[str] = Field(..., alias="from") to: 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") + cc: List[str] | None = None + bcc: List[str] | None = None + sender: List[str] | None = None + reply_to: List[str] | None = Field(None, alias="reply-to") returnPath: str messageId: str date: str diff --git a/aws_lambda_powertools/utilities/parser/models/sns.py b/aws_lambda_powertools/utilities/parser/models/sns.py index 62b8efc2e17..c2a740dbcbd 100644 --- a/aws_lambda_powertools/utilities/parser/models/sns.py +++ b/aws_lambda_powertools/utilities/parser/models/sns.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Dict, List, Literal, Optional, Union +from typing import Dict, List, Literal, Union from typing import Type as TypingType from pydantic import BaseModel, Field, model_validator @@ -18,7 +18,7 @@ class SnsMsgAttributeModel(BaseModel): class SnsNotificationModel(BaseModel): - Subject: Optional[str] = Field( + Subject: str | None = Field( default=None, description="The subject parameter provided when the notification was published to the topic.", examples=["TestInvoke", "Alert: System maintenance", "Order Confirmation", None], @@ -48,7 +48,7 @@ class SnsNotificationModel(BaseModel): description="The type of message. For Lambda triggers, this is always 'Notification'.", examples=["Notification"], ) - MessageAttributes: Optional[Dict[str, SnsMsgAttributeModel]] = Field( + MessageAttributes: Dict[str, SnsMsgAttributeModel] | None = Field( default=None, description="User-defined message attributes as key-value pairs with type information.", examples=[ @@ -73,7 +73,7 @@ class SnsNotificationModel(BaseModel): "f3c8d4e2-1a2b-4c5d-9e8f-7g6h5i4j3k2l", ], ) - SigningCertUrl: Optional[HttpUrl] = Field( + SigningCertUrl: HttpUrl | None = Field( default=None, description=( "The URL to the certificate that was used to sign the message. " @@ -85,7 +85,7 @@ class SnsNotificationModel(BaseModel): None, ], ) # NOTE: FIFO opt-in removes attribute - Signature: Optional[str] = Field( + Signature: str | None = Field( default=None, description=( "Base64-encoded SHA1withRSA signature of the message. " @@ -105,7 +105,7 @@ class SnsNotificationModel(BaseModel): "2023-12-25T18:45:30.123Z", ], ) - SignatureVersion: Optional[str] = Field( + SignatureVersion: str | None = Field( default=None, description=( "Version of the Amazon SNS signature used. Not present for FIFO topics with content-based deduplication." diff --git a/aws_lambda_powertools/utilities/parser/models/sqs.py b/aws_lambda_powertools/utilities/parser/models/sqs.py index 06452cd2a8f..020fe2288dc 100644 --- a/aws_lambda_powertools/utilities/parser/models/sqs.py +++ b/aws_lambda_powertools/utilities/parser/models/sqs.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Dict, List, Literal, Optional, Sequence, Type, Union +from typing import Dict, List, Literal, Sequence, Type, Union from pydantic import BaseModel, Field @@ -13,12 +13,12 @@ class SqsAttributesModel(BaseModel): description="The time the message was first received from the queue (epoch time in milliseconds).", examples=["1545082649185", "1545082650649", "1713185156612"], ) - MessageDeduplicationId: Optional[str] = Field( + MessageDeduplicationId: str | None = Field( default=None, description="Returns the value provided by the producer that calls the SendMessage action.", examples=["msg-dedup-12345", "unique-msg-abc123", None], ) - MessageGroupId: Optional[str] = Field( + MessageGroupId: str | None = Field( default=None, description="Returns the value provided by the producer that calls the SendMessage action.", examples=["order-processing", "user-123-updates", None], @@ -31,17 +31,17 @@ class SqsAttributesModel(BaseModel): description="The time the message was sent to the queue (epoch time in milliseconds).", examples=["1545082649183", "1545082650636", "1713185156609"], ) - SequenceNumber: Optional[str] = Field( + SequenceNumber: str | None = Field( default=None, description="Returns the value provided by Amazon SQS.", examples=["18849496460467696128", "18849496460467696129", None], ) - AWSTraceHeader: Optional[str] = Field( + AWSTraceHeader: str | None = Field( default=None, description="The AWS X-Ray trace header for request tracing.", examples=["Root=1-5e1b4151-5ac6c58239c1e5b4", None], ) - DeadLetterQueueSourceArn: Optional[str] = Field( + DeadLetterQueueSourceArn: str | None = Field( default=None, description="The ARN of the dead-letter queue from which the message was moved.", examples=["arn:aws:sqs:eu-central-1:123456789012:sqs-redrive-SampleQueue-RNvLCpwGmLi7", None], @@ -49,12 +49,12 @@ class SqsAttributesModel(BaseModel): class SqsMsgAttributeModel(BaseModel): - stringValue: Optional[str] = Field( + stringValue: str | None = Field( default=None, description="The string value of the message attribute.", examples=["100", "active", "user-12345", None], ) - binaryValue: Optional[str] = Field( + binaryValue: str | None = Field( default=None, description="The binary value of the message attribute, base64-encoded.", examples=["base64Str", "SGVsbG8gV29ybGQ=", None], @@ -140,7 +140,7 @@ class SqsRecordModel(BaseModel): "6a204bd89f3c8348afd5c77c717a097a", ], ) - md5OfMessageAttributes: Optional[str] = Field( + md5OfMessageAttributes: str | None = Field( default=None, description="An MD5 digest of the non-URL-encoded message attribute string.", examples=[ diff --git a/aws_lambda_powertools/utilities/parser/models/transfer_family.py b/aws_lambda_powertools/utilities/parser/models/transfer_family.py index be23c29449f..8219cef179b 100644 --- a/aws_lambda_powertools/utilities/parser/models/transfer_family.py +++ b/aws_lambda_powertools/utilities/parser/models/transfer_family.py @@ -1,4 +1,4 @@ -from typing import Literal, Optional +from typing import Literal from pydantic import BaseModel, Field from pydantic.networks import IPvAnyAddress @@ -9,7 +9,7 @@ class TransferFamilyAuthorizer(BaseModel): description="The username of the user attempting to authenticate.", examples=["bobusa", "john.doe", "sftp-user-123", "data-transfer-user"], ) - password: Optional[str] = Field( + password: str | None = Field( default=None, description="The password for authentication.", examples=["", "", None], diff --git a/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py b/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py index cdb75642bd5..9ce8b79dd85 100644 --- a/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py +++ b/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py @@ -1,65 +1,65 @@ from datetime import datetime -from typing import Dict, Optional, Type, Union +from typing import Dict, Type, Union from pydantic import BaseModel, Field, field_validator class VpcLatticeV2RequestContextIdentity(BaseModel): - source_vpc_arn: Optional[str] = Field( + source_vpc_arn: str | None = Field( None, alias="sourceVpcArn", description="The ARN of the VPC from which the request originated.", examples=["arn:aws:ec2:us-east-2:123456789012:vpc/vpc-0b8276c84697e7339"], ) - get_type: Optional[str] = Field( + get_type: str | None = Field( None, alias="type", description="The type of identity making the request.", examples=["AWS_IAM", "NONE"], ) - principal: Optional[str] = Field( + principal: str | None = Field( None, alias="principal", description="The principal ARN of the identity making the request.", examples=["arn:aws:sts::123456789012:assumed-role/example-role/057d00f8b51257ba3c853a0f248943cf"], ) - principal_org_id: Optional[str] = Field( + principal_org_id: str | None = Field( None, alias="principalOrgID", description="The AWS organization ID of the principal.", examples=["o-1234567890"], ) - session_name: Optional[str] = Field( + session_name: str | None = Field( None, alias="sessionName", description="The session name for assumed role sessions.", examples=["057d00f8b51257ba3c853a0f248943cf"], ) - x509_subject_cn: Optional[str] = Field( + x509_subject_cn: str | None = Field( None, alias="X509SubjectCn", description="The X.509 certificate subject common name.", examples=["example.com"], ) - x509_issuer_ou: Optional[str] = Field( + x509_issuer_ou: str | None = Field( None, alias="X509IssuerOu", description="The X.509 certificate issuer organizational unit.", examples=["IT Department"], ) - x509_san_dns: Optional[str] = Field( + x509_san_dns: str | None = Field( None, alias="x509SanDns", description="The X.509 certificate Subject Alternative Name DNS entry.", examples=["example.com"], ) - x509_san_uri: Optional[str] = Field( + x509_san_uri: str | None = Field( None, alias="X509SanUri", description="The X.509 certificate Subject Alternative Name URI entry.", examples=["https://example.com"], ) - x509_san_name_cn: Optional[str] = Field( + x509_san_name_cn: str | None = Field( None, alias="X509SanNameCn", description="The X.509 certificate Subject Alternative Name common name.", @@ -120,17 +120,17 @@ class VpcLatticeV2Model(BaseModel): {"content-type": "application/json"}, ], ) - query_string_parameters: Optional[Dict[str, str]] = Field( + query_string_parameters: Dict[str, str] | None = Field( None, alias="queryStringParameters", description="The query string parameters as key-value pairs.", examples=[{"order-id": "1"}, {"page": "2", "limit": "10"}], ) - body: Optional[Union[str, Type[BaseModel]]] = Field( + body: Union[str, Type[BaseModel]] | None = Field( None, description="The request body. Can be a string or a parsed model if content-type allows parsing.", ) - is_base64_encoded: Optional[bool] = Field( + is_base64_encoded: bool | None = Field( None, alias="isBase64Encoded", description="Indicates whether the body is base64-encoded.", diff --git a/docs/api_doc/circuit_breaker/circuit_breaker.md b/docs/api_doc/circuit_breaker/circuit_breaker.md new file mode 100644 index 00000000000..2cb6f9b712e --- /dev/null +++ b/docs/api_doc/circuit_breaker/circuit_breaker.md @@ -0,0 +1,2 @@ + +::: aws_lambda_powertools.utilities.circuit_breaker.circuit_breaker diff --git a/docs/api_doc/circuit_breaker/config.md b/docs/api_doc/circuit_breaker/config.md new file mode 100644 index 00000000000..28c7f208022 --- /dev/null +++ b/docs/api_doc/circuit_breaker/config.md @@ -0,0 +1,2 @@ + +::: aws_lambda_powertools.utilities.circuit_breaker.config diff --git a/docs/api_doc/circuit_breaker/exceptions.md b/docs/api_doc/circuit_breaker/exceptions.md new file mode 100644 index 00000000000..72ff6a993b2 --- /dev/null +++ b/docs/api_doc/circuit_breaker/exceptions.md @@ -0,0 +1,2 @@ + +::: aws_lambda_powertools.utilities.circuit_breaker.exceptions diff --git a/docs/api_doc/circuit_breaker/persistence.md b/docs/api_doc/circuit_breaker/persistence.md new file mode 100644 index 00000000000..aeabefdfa0c --- /dev/null +++ b/docs/api_doc/circuit_breaker/persistence.md @@ -0,0 +1,2 @@ + +::: aws_lambda_powertools.utilities.circuit_breaker.persistence diff --git a/docs/api_doc/circuit_breaker/states.md b/docs/api_doc/circuit_breaker/states.md new file mode 100644 index 00000000000..215886daa74 --- /dev/null +++ b/docs/api_doc/circuit_breaker/states.md @@ -0,0 +1,2 @@ + +::: aws_lambda_powertools.utilities.circuit_breaker.states diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 076e2aa3c11..67bb07a25cc 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -64,7 +64,7 @@ By default, we will use `APIGatewayRestResolver` throughout the documentation. Y | **[`ALBResolver`](#application-load-balancer)** | Amazon Application Load Balancer (ALB) | | **[`LambdaFunctionUrlResolver`](#lambda-function-url)** | AWS Lambda Function URL | | **[`VPCLatticeResolver`](#vpc-lattice)** | Amazon VPC Lattice | -| **[`HttpResolverLocal`](#http-resolver-local)** | Local development with ASGI servers | +| **[`HttpResolverLocal`](#http-resolver-local)** | ASGI-compatible resolver | #### Response auto-serialization @@ -1459,6 +1459,33 @@ Use `dependency_overrides` to replace any dependency with a mock or stub during --8<-- "examples/event_handler_rest/src/dependency_injection_testing.py" ``` +???+ warning "Import path must match exactly when using dependency_overrides" + When using `dependency_overrides` in tests, the imported dependency reference must use the **exact same import path** as the handler file. Python treats differently-imported modules as different objects in `sys.modules`, so the override will be silently ignored otherwise. + + === "✅ Correct" + + ```python + # handler.py + from depends import get_config + + # test_handler.py - matches handler's import path exactly + from depends import get_config + + app.dependency_overrides[get_config] = lambda: "test-value" + ``` + + === "❌ Wrong" + + ```python + # handler.py + from depends import get_config + + # test_handler.py - different import path, override won't apply + from my_app.api_handler.depends import get_config + + app.dependency_overrides[get_config] = lambda: "test-value" + ``` + ???+ tip "Caching behavior" By default, dependencies are cached within the same invocation (`use_cache=True`). If the same dependency is used by multiple handlers or sub-dependencies, it is resolved once and the result is reused. Use `Depends(fn, use_cache=False)` to resolve every time. diff --git a/docs/includes/_http_resolver_local.md b/docs/includes/_http_resolver_local.md index a9789a54746..496b42eb4f2 100644 --- a/docs/includes/_http_resolver_local.md +++ b/docs/includes/_http_resolver_local.md @@ -1,11 +1,7 @@ -#### Http Resolver (Local Development) +#### Http Resolver (ASGI) -???+ warning "Local Development Only" - `HttpResolverLocal` is intended for local development and testing only. - The API may change in future releases. **Do not use in production environments.** - -When developing locally, you can use `HttpResolverLocal` to run your API with any ASGI server like [uvicorn](https://www.uvicorn.org/){target="_blank"}. It implements the [ASGI specification](https://asgi.readthedocs.io/){target="_blank"}, is lightweight with no external dependencies, and the same code works on any compute platform, including Lambda. +`HttpResolver` is an ASGI-compatible resolver that lets you run your Powertools application with any ASGI server like [uvicorn](https://www.uvicorn.org/){target="_blank"}. It implements the [ASGI specification](https://asgi.readthedocs.io/){target="_blank"}, is lightweight with no external dependencies, and works seamlessly with Lambda or any environment that speaks HTTP. If your Lambda is behind [Lambda Web Adapter](https://github.com/awslabs/aws-lambda-web-adapter){target="_blank"} or any other HTTP proxy that speaks the HTTP protocol, it works seamlessly. diff --git a/docs/includes/_layer_homepage_arm64.md b/docs/includes/_layer_homepage_arm64.md index e40a04078ec..c80ebf9abb2 100644 --- a/docs/includes/_layer_homepage_arm64.md +++ b/docs/includes/_layer_homepage_arm64.md @@ -6,168 +6,168 @@ | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:37**{: .copyMe} | === "Python 3.11" | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:37**{: .copyMe} | === "Python 3.12" | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37**{: .copyMe} | === "Python 3.13" | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:37**{: .copyMe} | === "Python 3.14" | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:37**{: .copyMe} | diff --git a/docs/includes/_layer_homepage_x86.md b/docs/includes/_layer_homepage_x86.md index 907788332f7..6c5961b2d0b 100644 --- a/docs/includes/_layer_homepage_x86.md +++ b/docs/includes/_layer_homepage_x86.md @@ -5,173 +5,173 @@ | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:37**{: .copyMe} | === "Python 3.11" | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:37**{: .copyMe} | === "Python 3.12" | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37**{: .copyMe} | === "Python 3.13" | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37**{: .copyMe} | === "Python 3.14" | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:37**{: .copyMe} | diff --git a/docs/requirements.in b/docs/requirements.in index 7457559b3e3..6664e88d346 100644 --- a/docs/requirements.in +++ b/docs/requirements.in @@ -1,4 +1,4 @@ mkdocs-git-revision-date-plugin==0.3.2 mkdocstrings-python==1.19.0 mkdocs-llmstxt==0.5.0 -mkdocs-material==9.7.6 +mkdocs-material==9.7.7 diff --git a/docs/requirements.txt b/docs/requirements.txt index a7b731a7d5a..c75a0a3cfc9 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -139,9 +139,9 @@ gitdb==4.0.12 \ --hash=sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571 \ --hash=sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf # via gitpython -gitpython==3.1.50 \ - --hash=sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc \ - --hash=sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9 +gitpython==3.1.58 \ + --hash=sha256:621416df10ef3fd0e19fabf9172ddeed0fa704d353d04f194eec56a625a95b22 \ + --hash=sha256:d331e722577f0fd7fc1f857419b3ecc07af66282b933d2a4d95f84a042fdd50f # via mkdocs-git-revision-date-plugin griffe==1.13.0 \ --hash=sha256:246ea436a5e78f7fbf5f24ca8a727bb4d2a4b442a2959052eea3d0bfe9a076e0 \ @@ -283,15 +283,15 @@ mkdocs-get-deps==0.2.0 \ # via mkdocs mkdocs-git-revision-date-plugin==0.3.2 \ --hash=sha256:2e67956cb01823dd2418e2833f3623dee8604cdf223bddd005fe36226a56f6ef - # via -r requirements.in + # via -r docs/requirements.in mkdocs-llmstxt==0.5.0 \ --hash=sha256:753c699913d2d619a9072604b26b6dc9f5fb6d257d9b107857f80c8a0b787533 \ --hash=sha256:b2fa9e6d68df41d7467e948a4745725b6c99434a36b36204857dbd7bb3dfe041 - # via -r requirements.in -mkdocs-material==9.7.6 \ - --hash=sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69 \ - --hash=sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba - # via -r requirements.in + # via -r docs/requirements.in +mkdocs-material==9.7.7 \ + --hash=sha256:8ea9bb1737a5b524a5f9dcf2e1b4ebda8274ae3008aa7845720a97083bef708f \ + --hash=sha256:c0649c065b1b0512d60aad8c10f947f8e455284475239b364b610f2deb4d0855 + # via -r docs/requirements.in mkdocs-material-extensions==1.3.1 \ --hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \ --hash=sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31 @@ -303,7 +303,7 @@ mkdocstrings==0.30.0 \ mkdocstrings-python==1.19.0 \ --hash=sha256:395c1032af8f005234170575cc0c5d4d20980846623b623b35594281be4a3059 \ --hash=sha256:917aac66cf121243c11db5b89f66b0ded6c53ec0de5318ff5e22424eb2f2e57c - # via -r requirements.in + # via -r docs/requirements.in packaging==25.0 \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f @@ -324,9 +324,9 @@ pygments==2.19.2 \ --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b # via mkdocs-material -pymdown-extensions==10.21.3 \ - --hash=sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354 \ - --hash=sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6 +pymdown-extensions==11.0.1 \ + --hash=sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2 \ + --hash=sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0 # via # mkdocs-material # mkdocstrings @@ -411,14 +411,65 @@ smmap==5.0.2 \ --hash=sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5 \ --hash=sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e # via gitdb -soupsieve==2.7 \ - --hash=sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4 \ - --hash=sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a +soupsieve==2.8.4 \ + --hash=sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e \ + --hash=sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65 # via beautifulsoup4 +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 + # via mdformat typing-extensions==4.14.0 \ --hash=sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4 \ --hash=sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af - # via beautifulsoup4 + # via + # beautifulsoup4 + # mkdocstrings-python urllib3==2.7.0 \ --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 diff --git a/docs/utilities/circuit_breaker.md b/docs/utilities/circuit_breaker.md new file mode 100644 index 00000000000..ab219c942c3 --- /dev/null +++ b/docs/utilities/circuit_breaker.md @@ -0,0 +1,256 @@ +--- +title: Circuit Breaker +description: Utility +--- + + + +The circuit breaker utility stops sending traffic to an unhealthy downstream dependency, giving it room to recover while you decide what happens to the rejected requests. + +## Key features + +* Stops calling an unhealthy downstream after a configurable number of consecutive failures +* Hands rejected requests to an `on_circuit_open` callback so you decide what happens next (buffer, drop, return a cached value) +* Tests recovery with an explicit half-open probe rather than blindly retrying everything at once +* Shares circuit state across execution environments via Amazon DynamoDB +* Safe under concurrency: one half-open probe across all threads and execution environments, and synchronized failure counting +* Keeps the healthy path write-free: failures are counted in memory and only persisted on a state transition + +## Terminology + +**Circuit** is a named guard around a single downstream dependency. Each `name` is an independent circuit. + +**State** is the circuit's current mode: `CLOSED` (healthy), `OPEN` (downstream considered unhealthy, calls skipped), or `HALF_OPEN` (testing recovery). + +**Persistence layer** is the shared storage that holds each circuit's state so every execution environment agrees on whether a circuit is open. + +**Recovery timeout** is how long a circuit stays open before allowing a half-open probe. + +
+```mermaid +stateDiagram-v2 + [*] --> CLOSED + CLOSED --> OPEN: N consecutive failures + OPEN --> HALF_OPEN: recovery timeout elapsed + HALF_OPEN --> CLOSED: probe succeeds + HALF_OPEN --> OPEN: probe fails +``` + +Circuit breaker state transitions +
+ +## Getting started + +We use Amazon DynamoDB as the persistence layer in this documentation. + +### IAM Permissions + +When using Amazon DynamoDB as the persistence layer, you will need the following IAM permissions: + +| IAM Permission | Operation | +| ------------------------------------ | ------------------------------------------------------- | +| **`dynamodb:GetItem`**{: .copyMe} | Read shared circuit state | +| **`dynamodb:PutItem`**{: .copyMe} | Persist an opened circuit and elect the half-open probe | +| **`dynamodb:UpdateItem`**{: .copyMe} | Close or reopen a circuit after a probe | + +### Required resources + +To start, you'll need: + + + +
+* **Persistent storage** + + --- + + [Amazon DynamoDB](#dynamodb-table) + +* **AWS Lambda function** + + --- + + With permissions to read and write your persistent storage + +
+ + + +#### DynamoDB table + +Unless you're looking to [customize each attribute](#customizing-the-dynamodb-table), you only need the following: + +| Configuration | Value | Notes | +| ------------------ | ------------ | ------------------------------------------------------------ | +| Partition key | `id` | Holds the circuit name | +| TTL attribute name | `expiration` | Using AWS Console? This is configurable after table creation | + +You **can** use a single DynamoDB table for all your circuits. Already have a single-table (composite key) design you want to reuse? See [Using a composite primary key](#using-a-composite-primary-key). + +##### DynamoDB IaC example + +=== "AWS Serverless Application Model (SAM) example" + + ```yaml hl_lines="3-15 24-29 32-33" + --8<-- "examples/circuit_breaker/templates/sam.yaml" + ``` + +### Circuit breaker in action + +The common case is the `@circuit_breaker` decorator wrapping the function that makes the downstream call. With no `config`, sensible defaults apply (open after 5 consecutive failures, probe after 30 seconds, close after 3 probe successes, count any exception as a failure). + +=== "getting_started_with_circuit_breaker.py" + + ```python hl_lines="3-6 9 19 27" + --8<-- "examples/circuit_breaker/src/getting_started_with_circuit_breaker.py" + ``` + +!!! note "Wrap the downstream call, not the whole handler" + The circuit protects a single dependency. If you decorate a handler that parses the + event, validates, and calls two backends, a parsing bug would trip a circuit named + after a backend that is perfectly healthy. Decorate the handler directly only when + the handler **is** the downstream call (a thin pass-through). + +### What the decorated function returns + +There is no wrapper type to inspect. The contract is: + +| Circuit state | Result | +| ------------------------------------- | ---------------------------------------------------------------------- | +| **Closed** | The protected function's return value | +| **Open**, `on_circuit_open` set | Whatever the callback returns | +| **Open**, no callback | Raises `CircuitBreakerOpenError` (with the `CircuitInfo` attached) | + +## Handling an open circuit + +### With a callback + +Register an `on_circuit_open` callback to decide what happens to a rejected request. The callback receives the same arguments the protected function was called with (positional arguments stay positional, keyword arguments stay keyword), plus a trailing `circuit` keyword argument carrying a `CircuitInfo` snapshot. Its return value becomes the result of the call. + +=== "working_with_callback.py" + + ```python hl_lines="6 24-28 31-35" + --8<-- "examples/circuit_breaker/src/working_with_callback.py" + ``` + +!!! info "Why a callback instead of built-in S3/SQS sinks?" + A callback keeps you in control of where rejected requests go. You pick the + destination, the client, and the IAM, and you avoid coupling your function to a + sink the utility manages for you. + +### Without a callback + +If you don't register a callback, an open circuit raises `CircuitBreakerOpenError`. Catch it to decide how to respond. The exception carries a `circuit` attribute (`CircuitInfo`) so you can inspect why the circuit rejected the request. + +=== "working_without_callback.py" + + ```python hl_lines="5 16-22 38-43" + --8<-- "examples/circuit_breaker/src/working_without_callback.py" + ``` + +## Configuration + +All options live on `CircuitBreakerConfig`. Every value has a default, so `CircuitBreakerConfig()` is a valid production configuration. + +| Parameter | Default | Description | +| ------------------------- | ------- | ------------------------------------------------------------------------------------------------- | +| **`failure_threshold`** | `5` | Consecutive failures that trip a closed circuit to open | +| **`recovery_timeout`** | `30` | Seconds the circuit stays open before a half-open probe | +| **`success_threshold`** | `3` | Consecutive probe successes required to close a half-open circuit | +| **`handled_exceptions`** | `None` | Allowlist: only these exception types count as failures. Mutually exclusive with the denylist | +| **`ignored_exceptions`** | `None` | Denylist: every exception counts as a failure except these. Mutually exclusive with the allowlist | +| **`local_cache_max_age`** | `5` | Seconds a circuit's state is cached per environment before a read-through | + +### Choosing which exceptions count as a failure + +By default, **any exception** counts as a failure. But not every error means the downstream is unhealthy: a `400` is the caller's fault, a `503` is not. Scope it from either side: + +* **`handled_exceptions`** (allowlist): only these count. Everything else propagates without affecting the circuit. +* **`ignored_exceptions`** (denylist): everything counts except these. + +Passing both raises `CircuitBreakerConfigError`. An exception that doesn't count is re-raised to the caller untouched. + +## Advanced + +### How recovery works + +After `recovery_timeout` seconds, the circuit moves to `HALF_OPEN` and elects a **single** execution environment (via a conditional DynamoDB write) to run a probe. If `success_threshold` consecutive probes succeed, the circuit closes; a single failing probe reopens it. This stops a thundering herd of every environment hammering a recovering backend at once. + +!!! note "Thread safety" + The utility is safe to share across threads: within a multi-threaded environment the probe election picks a single + thread, so the single-prober guarantee spans threads as well as environments, and the in-memory failure counter is + synchronized. Single-threaded functions (the normal Lambda model) are unaffected. + + Probe ownership belongs to the thread that won the election. If that thread never runs the circuit again (for + example, a thread-per-request worker pool), recovery waits for the probe lease to expire before another thread or + environment takes over. + +!!! warning "Make your hooks thread-safe" + If your function runs multiple threads, `on_circuit_open` and `on_transition` callbacks can run concurrently + for the same circuit. Make them thread-safe. + +### State coordination across environments + +The consecutive-failure counter lives in memory per execution environment, so a healthy circuit performs **no writes**. Only when an environment reaches `failure_threshold` does it persist `OPEN`. The shared state is cached locally for `local_cache_max_age` seconds to avoid a read per invocation. A cache miss (cold start or expired entry) forces a read-through before routing. + +!!! note "Fail-open by design" + If the utility cannot reach the persistence store when reading state, it treats the + circuit as **closed**. A circuit breaker should never become the outage it is meant to prevent. + +### Observability with metrics + +Register an `on_transition` hook to be notified whenever the circuit changes state (open, probe, close, reopen). The hook fires **only on transitions**, never on the per-invocation hot path, so it is a safe place to emit a CloudWatch metric without giving up the write-free healthy path. It receives a single `CircuitTransition` (`circuit_name`, `from_state`, `to_state`, `opened_at`). + +=== "working_with_metrics.py" + + ```python hl_lines="3 12-21 26" + --8<-- "examples/circuit_breaker/src/working_with_metrics.py" + ``` + +Any exception raised inside the hook is swallowed and logged, so a misbehaving metric call can never break the protected request. + +!!! warning "`failure_count` is a trip-time snapshot, not a running total" + The `failure_count` on `CircuitInfo` is the number of *consecutive* failures the + environment that tripped the circuit had counted at the moment it opened. Because the + failure counter lives in memory per execution environment (keeping the healthy path + write-free), it is **not** a fleet-wide total and reads `0` in states reached without a + fresh trip (such as `HALF_OPEN`). For failure **volume**, emit a metric from your own + code or the `on_transition` hook rather than reading this field. + +### Disabling the circuit breaker + +Set **`POWERTOOLS_CIRCUIT_BREAKER_DISABLED`**{: .copyMe} to a truthy value to bypass the circuit entirely and always call the protected function. This is intended for **development environments only** and emits a warning. + +### Customizing the DynamoDB table + +`CircuitBreakerDynamoDBPersistence` accepts attribute-name overrides (`key_attr`, `state_attr`, `failure_count_attr`, `opened_at_attr`, `half_open_owner_attr`, `expiry_attr`) and the usual boto3 escape hatches (`boto3_session`, `boto3_client`, `boto_config`) for reusing an existing table layout or client. + +#### Using a composite primary key + +Use the `sort_key_attr` parameter when your table is configured with a composite primary key *(hash+range key)*, as in a single-table design. + +When enabled, the circuit name is saved in the sort key instead, and the partition key defaults to `circuit_breaker#{LAMBDA_FUNCTION_NAME}` — this **namespaces circuits per function** so two functions can use the same circuit name without sharing state. (Without `sort_key_attr`, the default remains partition-key-only with the circuit name as the partition key.) + +Set `static_pk_value` to a fixed value (as below) when you instead want **multiple functions to share the same circuit** — for example, every function guarding the same downstream dependency should trip together. + +```python hl_lines="12-14" +--8<-- "examples/circuit_breaker/src/working_with_composite_primary_key.py" +``` + +??? note "Click to expand and learn how table items would look like" + + The circuit name is stored in the sort key, so several circuits share one partition. Attributes that don't apply to a state are simply absent. + + | PK | SK | state | failure_count | opened_at | half_open_owner | probe_lease_expiry | expiration | + | --------------- | --------------- | --------- | ------------- | ---------- | --------------- | ------------------ | ---------- | + | CIRCUIT_BREAKER | payment-backend | OPEN | 5 | 1699999999 | | | 1700003599 | + | CIRCUIT_BREAKER | inventory-api | HALF_OPEN | 5 | 1699999999 | 9f3c1a2b-env | 1700000030 | 1700003599 | + | CIRCUIT_BREAKER | email-service | CLOSED | 0 | | | | | + +## Testing your code + +When unit testing the function a circuit protects, set `POWERTOOLS_CIRCUIT_BREAKER_DISABLED=true` to bypass the circuit and persistence layer entirely, so your tests exercise the business logic without needing DynamoDB. + +```bash title="Disabling circuit breaker for tests" +POWERTOOLS_CIRCUIT_BREAKER_DISABLED=true python -m pytest +``` diff --git a/examples/batch_processing/src/advanced_accessing_lambda_context.py b/examples/batch_processing/src/advanced_accessing_lambda_context.py index b0e7eeb98af..d8820138fa1 100644 --- a/examples/batch_processing/src/advanced_accessing_lambda_context.py +++ b/examples/batch_processing/src/advanced_accessing_lambda_context.py @@ -1,5 +1,3 @@ -from typing import Optional - from aws_lambda_powertools import Logger, Tracer from aws_lambda_powertools.utilities.batch import ( BatchProcessor, @@ -15,7 +13,7 @@ @tracer.capture_method -def record_handler(record: SQSRecord, lambda_context: Optional[LambdaContext] = None): +def record_handler(record: SQSRecord, lambda_context: LambdaContext | None = None): if lambda_context is not None: remaining_time = lambda_context.get_remaining_time_in_millis() logger.info(remaining_time) diff --git a/examples/batch_processing/src/advanced_accessing_lambda_context_manager.py b/examples/batch_processing/src/advanced_accessing_lambda_context_manager.py index 17b719a84d4..11e92a13f93 100644 --- a/examples/batch_processing/src/advanced_accessing_lambda_context_manager.py +++ b/examples/batch_processing/src/advanced_accessing_lambda_context_manager.py @@ -1,5 +1,3 @@ -from typing import Optional - from aws_lambda_powertools import Logger, Tracer from aws_lambda_powertools.utilities.batch import BatchProcessor, EventType from aws_lambda_powertools.utilities.data_classes.sqs_event import SQSRecord @@ -11,7 +9,7 @@ @tracer.capture_method -def record_handler(record: SQSRecord, lambda_context: Optional[LambdaContext] = None): +def record_handler(record: SQSRecord, lambda_context: LambdaContext | None = None): if lambda_context is not None: remaining_time = lambda_context.get_remaining_time_in_millis() logger.info(remaining_time) diff --git a/examples/batch_processing/src/pydantic_dynamodb.py b/examples/batch_processing/src/pydantic_dynamodb.py index b46f5c78201..7e411a39caa 100644 --- a/examples/batch_processing/src/pydantic_dynamodb.py +++ b/examples/batch_processing/src/pydantic_dynamodb.py @@ -1,5 +1,5 @@ import json -from typing import Dict, Optional +from typing import Dict from typing_extensions import Literal @@ -32,8 +32,8 @@ def transform_message_to_dict(cls, value: Dict[Literal["S"], str]): class OrderDynamoDBChangeRecord(DynamoDBStreamChangedRecordModel): # type: ignore[override] - NewImage: Optional[OrderDynamoDB] - OldImage: Optional[OrderDynamoDB] + NewImage: OrderDynamoDB | None + OldImage: OrderDynamoDB | None class OrderDynamoDBRecord(DynamoDBStreamRecordModel): # type: ignore[override] diff --git a/examples/build_recipes/cdk/basic/app.py b/examples/build_recipes/cdk/basic/app.py index 6268d8e49c0..fe43b6dd479 100644 --- a/examples/build_recipes/cdk/basic/app.py +++ b/examples/build_recipes/cdk/basic/app.py @@ -24,7 +24,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: powertools_layer = _lambda.LayerVersion.from_layer_version_arn( self, "PowertoolsLayer", - layer_version_arn="arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33", + layer_version_arn="arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37", ) # Lambda Function diff --git a/examples/build_recipes/cdk/multi-stack/stacks/powertools_cdk_stack.py b/examples/build_recipes/cdk/multi-stack/stacks/powertools_cdk_stack.py index 3c7a480adbe..7e1e6be5796 100644 --- a/examples/build_recipes/cdk/multi-stack/stacks/powertools_cdk_stack.py +++ b/examples/build_recipes/cdk/multi-stack/stacks/powertools_cdk_stack.py @@ -47,7 +47,7 @@ def _create_powertools_layer(self) -> _lambda.ILayerVersion: return _lambda.LayerVersion.from_layer_version_arn( self, "PowertoolsLayer", - layer_version_arn="arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33", + layer_version_arn="arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37", ) def _create_dynamodb_table(self) -> dynamodb.Table: diff --git a/examples/build_recipes/poetry/app_poetry.py b/examples/build_recipes/poetry/app_poetry.py index d570cca39cf..47927eb25b9 100644 --- a/examples/build_recipes/poetry/app_poetry.py +++ b/examples/build_recipes/poetry/app_poetry.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import BaseModel from aws_lambda_powertools import Logger, Metrics, Tracer @@ -16,7 +14,7 @@ class UserModel(BaseModel): name: str email: str - age: Optional[int] = None + age: int | None = None @app.post("/users") diff --git a/examples/build_recipes/sam/multi-env/template.yaml b/examples/build_recipes/sam/multi-env/template.yaml index 8babe94f382..9bb9e85feae 100644 --- a/examples/build_recipes/sam/multi-env/template.yaml +++ b/examples/build_recipes/sam/multi-env/template.yaml @@ -61,7 +61,7 @@ Resources: CodeUri: src/ Handler: app.lambda_handler Layers: - - arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33 + - arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37 - !Ref DependenciesLayer Events: ApiEvent: diff --git a/examples/build_recipes/sam/no-layers/src/app_sam_no_layer.py b/examples/build_recipes/sam/no-layers/src/app_sam_no_layer.py index dcda4cede13..04c2d227c73 100644 --- a/examples/build_recipes/sam/no-layers/src/app_sam_no_layer.py +++ b/examples/build_recipes/sam/no-layers/src/app_sam_no_layer.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import BaseModel from aws_lambda_powertools import Logger, Metrics, Tracer @@ -16,7 +14,7 @@ class UserModel(BaseModel): name: str email: str - age: Optional[int] = None + age: int | None = None @app.get("/health") diff --git a/examples/build_recipes/sam/with-layers/src/app/app_sam_layer.py b/examples/build_recipes/sam/with-layers/src/app/app_sam_layer.py index 9d4b39e63a1..56425bce2aa 100644 --- a/examples/build_recipes/sam/with-layers/src/app/app_sam_layer.py +++ b/examples/build_recipes/sam/with-layers/src/app/app_sam_layer.py @@ -1,5 +1,3 @@ -from typing import Optional - import requests from pydantic import BaseModel @@ -17,7 +15,7 @@ class UserModel(BaseModel): name: str email: str - age: Optional[int] = None + age: int | None = None @app.get("/health") diff --git a/examples/build_recipes/sam/with-layers/template.yaml b/examples/build_recipes/sam/with-layers/template.yaml index fc9803b5a8a..c572f89d293 100644 --- a/examples/build_recipes/sam/with-layers/template.yaml +++ b/examples/build_recipes/sam/with-layers/template.yaml @@ -31,7 +31,7 @@ Resources: CodeUri: src/app/ Handler: app_sam_layer.lambda_handler Layers: - - arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33 + - arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37 - !Ref DependenciesLayer Events: ApiEvent: @@ -50,7 +50,7 @@ Resources: CodeUri: src/worker/ Handler: worker_sam_layer.lambda_handler Layers: - - arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33 + - arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:37 - !Ref DependenciesLayer Events: SQSEvent: diff --git a/examples/circuit_breaker/src/getting_started_with_circuit_breaker.py b/examples/circuit_breaker/src/getting_started_with_circuit_breaker.py new file mode 100644 index 00000000000..d97ff2bf235 --- /dev/null +++ b/examples/circuit_breaker/src/getting_started_with_circuit_breaker.py @@ -0,0 +1,29 @@ +import os + +from aws_lambda_powertools.utilities.circuit_breaker import circuit_breaker +from aws_lambda_powertools.utilities.circuit_breaker.persistence import ( + CircuitBreakerDynamoDBPersistence, +) +from aws_lambda_powertools.utilities.typing import LambdaContext + +table = os.getenv("CIRCUIT_BREAKER_TABLE", "") +persistence = CircuitBreakerDynamoDBPersistence(table_name=table) + + +class PaymentBackend: + def charge(self, order: dict): ... + + +payment_api = PaymentBackend() + + +@circuit_breaker(name="payment-backend", persistence_store=persistence) +def charge(order: dict) -> dict: + return payment_api.charge(order) # the protected downstream call + + +def lambda_handler(event: dict, context: LambdaContext): + # Circuit closed -> charge() runs and returns the backend response. + # Circuit open -> charge() is skipped and CircuitBreakerOpenError is raised, + # because no on_circuit_open callback is registered. + return charge(event) diff --git a/examples/circuit_breaker/src/working_with_callback.py b/examples/circuit_breaker/src/working_with_callback.py new file mode 100644 index 00000000000..5725ac32a9b --- /dev/null +++ b/examples/circuit_breaker/src/working_with_callback.py @@ -0,0 +1,44 @@ +import json +import os + +import boto3 + +from aws_lambda_powertools.utilities.circuit_breaker import CircuitInfo, circuit_breaker +from aws_lambda_powertools.utilities.circuit_breaker.persistence import ( + CircuitBreakerDynamoDBPersistence, +) +from aws_lambda_powertools.utilities.typing import LambdaContext + +table = os.getenv("CIRCUIT_BREAKER_TABLE", "") +queue_url = os.getenv("OVERFLOW_QUEUE_URL", "") +persistence = CircuitBreakerDynamoDBPersistence(table_name=table) +sqs = boto3.client("sqs") + + +class PaymentBackend: + def charge(self, order: dict): ... + + +payment_api = PaymentBackend() + + +def buffer_payload(order: dict, circuit: CircuitInfo) -> dict: + # Circuit is OPEN. The protected call never ran and the payload is yours to handle: + # buffer it, drop it, or return a cached value. Here we push it to an overflow queue. + sqs.send_message(QueueUrl=queue_url, MessageBody=json.dumps(order)) + return {"statusCode": 202, "circuit": circuit.name} + + +@circuit_breaker( + name="payment-backend", + persistence_store=persistence, + on_circuit_open=buffer_payload, +) +def charge(order: dict) -> dict: + return payment_api.charge(order) + + +def lambda_handler(event: dict, context: LambdaContext): + # Circuit closed -> returns the backend response. + # Circuit open -> buffer_payload(event, circuit) runs and its return value is returned. + return charge(event) diff --git a/examples/circuit_breaker/src/working_with_composite_primary_key.py b/examples/circuit_breaker/src/working_with_composite_primary_key.py new file mode 100644 index 00000000000..0bceacdd9fa --- /dev/null +++ b/examples/circuit_breaker/src/working_with_composite_primary_key.py @@ -0,0 +1,31 @@ +import os + +from aws_lambda_powertools.utilities.circuit_breaker import circuit_breaker +from aws_lambda_powertools.utilities.circuit_breaker.persistence import ( + CircuitBreakerDynamoDBPersistence, +) +from aws_lambda_powertools.utilities.typing import LambdaContext + +table = os.getenv("CIRCUIT_BREAKER_TABLE", "") +persistence = CircuitBreakerDynamoDBPersistence( + table_name=table, + key_attr="PK", + sort_key_attr="SK", + static_pk_value="CIRCUIT_BREAKER", +) + + +class PaymentBackend: + def charge(self, order: dict): ... + + +payment_api = PaymentBackend() + + +@circuit_breaker(name="payment-backend", persistence_store=persistence) +def charge(order: dict) -> dict: + return payment_api.charge(order) # the protected downstream call + + +def lambda_handler(event: dict, context: LambdaContext): + return charge(event) diff --git a/examples/circuit_breaker/src/working_with_metrics.py b/examples/circuit_breaker/src/working_with_metrics.py new file mode 100644 index 00000000000..8129e91f36e --- /dev/null +++ b/examples/circuit_breaker/src/working_with_metrics.py @@ -0,0 +1,45 @@ +import os + +from aws_lambda_powertools.metrics import MetricUnit, single_metric +from aws_lambda_powertools.utilities.circuit_breaker import ( + CircuitTransition, + circuit_breaker, +) +from aws_lambda_powertools.utilities.circuit_breaker.persistence import ( + CircuitBreakerDynamoDBPersistence, +) +from aws_lambda_powertools.utilities.typing import LambdaContext + +table = os.getenv("CIRCUIT_BREAKER_TABLE", "") +persistence = CircuitBreakerDynamoDBPersistence(table_name=table) + + +def emit_transition_metric(transition: CircuitTransition) -> None: + # Fires only when the circuit changes state, so this never runs on the hot path. + with single_metric( + namespace="MyApplication", + name=f"Circuit_{transition.to_state}", + unit=MetricUnit.Count, + value=1, + ) as metric: + metric.add_dimension(name="circuit", value=transition.circuit_name) + + +class PaymentBackend: + def charge(self, order: dict): ... + + +payment_api = PaymentBackend() + + +@circuit_breaker( + name="payment-backend", + persistence_store=persistence, + on_transition=emit_transition_metric, +) +def charge(order: dict) -> dict: + return payment_api.charge(order) + + +def lambda_handler(event: dict, context: LambdaContext): + return charge(event) diff --git a/examples/circuit_breaker/src/working_without_callback.py b/examples/circuit_breaker/src/working_without_callback.py new file mode 100644 index 00000000000..2ac205333ad --- /dev/null +++ b/examples/circuit_breaker/src/working_without_callback.py @@ -0,0 +1,44 @@ +import os + +from aws_lambda_powertools.utilities.circuit_breaker import ( + CircuitBreakerConfig, + CircuitBreakerOpenError, + circuit_breaker, +) +from aws_lambda_powertools.utilities.circuit_breaker.persistence import ( + CircuitBreakerDynamoDBPersistence, +) +from aws_lambda_powertools.utilities.typing import LambdaContext + +table = os.getenv("CIRCUIT_BREAKER_TABLE", "") +persistence = CircuitBreakerDynamoDBPersistence(table_name=table) + +config = CircuitBreakerConfig( + failure_threshold=5, # consecutive failures before opening + recovery_timeout=30, # seconds in OPEN before a half-open probe + success_threshold=3, # consecutive probe successes before closing + # Only these exceptions count as a failure. A ValueError (caller's fault) is + # re-raised without affecting the circuit. + handled_exceptions=(TimeoutError, ConnectionError), +) + + +class PaymentBackend: + def charge(self, order: dict): ... + + +payment_api = PaymentBackend() + + +@circuit_breaker(name="payment-backend", persistence_store=persistence, config=config) +def charge(order: dict) -> dict: + return payment_api.charge(order) + + +def lambda_handler(event: dict, context: LambdaContext): + try: + return charge(event) + except CircuitBreakerOpenError as exc: + # No callback registered, so we decide what to do with the rejected request here. + circuit_name = exc.circuit.name if exc.circuit else "unknown" + return {"statusCode": 202, "circuit": circuit_name} diff --git a/examples/circuit_breaker/templates/sam.yaml b/examples/circuit_breaker/templates/sam.yaml new file mode 100644 index 00000000000..686a2caaecf --- /dev/null +++ b/examples/circuit_breaker/templates/sam.yaml @@ -0,0 +1,33 @@ +Transform: AWS::Serverless-2016-10-31 +Resources: + CircuitBreakerTable: + Type: AWS::DynamoDB::Table + Properties: + AttributeDefinitions: + - AttributeName: id + AttributeType: S + KeySchema: + - AttributeName: id + KeyType: HASH + TimeToLiveSpecification: + AttributeName: expiration + Enabled: true + BillingMode: PAY_PER_REQUEST + + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + Runtime: python3.12 + Handler: app.py + Policies: + - Statement: + - Sid: AllowDynamodbReadWrite + Effect: Allow + Action: + - dynamodb:GetItem + - dynamodb:PutItem + - dynamodb:UpdateItem + Resource: !GetAtt CircuitBreakerTable.Arn + Environment: + Variables: + CIRCUIT_BREAKER_TABLE: !Ref CircuitBreakerTable diff --git a/examples/event_handler_graphql/src/enable_exceptions_batch_resolver.py b/examples/event_handler_graphql/src/enable_exceptions_batch_resolver.py index 1d94e4693c8..b32399167fa 100644 --- a/examples/event_handler_graphql/src/enable_exceptions_batch_resolver.py +++ b/examples/event_handler_graphql/src/enable_exceptions_batch_resolver.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional +from typing import Dict from aws_lambda_powertools.event_handler import AppSyncResolver from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent @@ -18,7 +18,7 @@ class PostRelatedNotFound(Exception): ... @app.batch_resolver(type_name="Query", field_name="relatedPosts", raise_on_error=True) # (1)! -def related_posts(event: AppSyncResolverEvent, post_id: str) -> Optional[Dict]: +def related_posts(event: AppSyncResolverEvent, post_id: str) -> Dict | None: post_found = posts_related.get(post_id, None) if not post_found: diff --git a/examples/event_handler_rest/src/accessing_request_details.py b/examples/event_handler_rest/src/accessing_request_details.py index e9a5d924017..cd6e8b6a56e 100644 --- a/examples/event_handler_rest/src/accessing_request_details.py +++ b/examples/event_handler_rest/src/accessing_request_details.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List import requests from requests import Response @@ -18,13 +18,13 @@ def get_todos(): todo_id: str = app.current_event.query_string_parameters["id"] # alternatively - _: Optional[str] = app.current_event.query_string_parameters.get("id") + _: str | None = app.current_event.query_string_parameters.get("id") # or multi-value query string parameters; ?category="red"&?category="blue" _: List[str] = app.current_event.multi_value_query_string_parameters["category"] # Payload - _: Optional[str] = app.current_event.body # raw str | None + _: str | None = app.current_event.body # raw str | None endpoint = "https://jsonplaceholder.typicode.com/todos" if todo_id: diff --git a/examples/event_handler_rest/src/customizing_response_validation.py b/examples/event_handler_rest/src/customizing_response_validation.py index 25aa07bf52a..2043c0c9c57 100644 --- a/examples/event_handler_rest/src/customizing_response_validation.py +++ b/examples/event_handler_rest/src/customizing_response_validation.py @@ -1,5 +1,4 @@ from http import HTTPStatus -from typing import Optional import requests from pydantic import BaseModel, Field @@ -19,7 +18,7 @@ class Todo(BaseModel): userId: int - id_: Optional[int] = Field(alias="id", default=None) + id_: int | None = Field(alias="id", default=None) title: str completed: bool diff --git a/examples/event_handler_rest/src/customizing_response_validation_exception.py b/examples/event_handler_rest/src/customizing_response_validation_exception.py index c94ace290d2..871c222151c 100644 --- a/examples/event_handler_rest/src/customizing_response_validation_exception.py +++ b/examples/event_handler_rest/src/customizing_response_validation_exception.py @@ -1,5 +1,4 @@ from http import HTTPStatus -from typing import Optional import requests from pydantic import BaseModel, Field @@ -21,7 +20,7 @@ class Todo(BaseModel): userId: int - id_: Optional[int] = Field(alias="id", default=None) + id_: int | None = Field(alias="id", default=None) title: str completed: bool diff --git a/examples/event_handler_rest/src/data_validation.py b/examples/event_handler_rest/src/data_validation.py index 1daa9fb2174..bbb43df74ea 100644 --- a/examples/event_handler_rest/src/data_validation.py +++ b/examples/event_handler_rest/src/data_validation.py @@ -1,5 +1,3 @@ -from typing import Optional - import requests from pydantic import BaseModel, Field @@ -15,7 +13,7 @@ class Todo(BaseModel): # (2)! userId: int - id_: Optional[int] = Field(alias="id", default=None) + id_: int | None = Field(alias="id", default=None) title: str completed: bool diff --git a/examples/event_handler_rest/src/data_validation_fine_grained_response.py b/examples/event_handler_rest/src/data_validation_fine_grained_response.py index 1209c6a1af1..36ee0a41299 100644 --- a/examples/event_handler_rest/src/data_validation_fine_grained_response.py +++ b/examples/event_handler_rest/src/data_validation_fine_grained_response.py @@ -1,5 +1,4 @@ from http import HTTPStatus -from typing import Optional import requests from pydantic import BaseModel, Field @@ -16,7 +15,7 @@ class Todo(BaseModel): userId: int - id_: Optional[int] = Field(alias="id", default=None) + id_: int | None = Field(alias="id", default=None) title: str completed: bool diff --git a/examples/event_handler_rest/src/data_validation_sanitized_error.py b/examples/event_handler_rest/src/data_validation_sanitized_error.py index 71849938f48..72d8c82b737 100644 --- a/examples/event_handler_rest/src/data_validation_sanitized_error.py +++ b/examples/event_handler_rest/src/data_validation_sanitized_error.py @@ -1,5 +1,3 @@ -from typing import Optional - import requests from pydantic import BaseModel, Field @@ -16,7 +14,7 @@ class Todo(BaseModel): userId: int - id_: Optional[int] = Field(alias="id", default=None) + id_: int | None = Field(alias="id", default=None) title: str completed: bool diff --git a/examples/event_handler_rest/src/http_resolver_basic.py b/examples/event_handler_rest/src/http_resolver_basic.py index 63d291c8e47..7f6aaadc57d 100644 --- a/examples/event_handler_rest/src/http_resolver_basic.py +++ b/examples/event_handler_rest/src/http_resolver_basic.py @@ -1,6 +1,6 @@ -from aws_lambda_powertools.event_handler import HttpResolverLocal +from aws_lambda_powertools.event_handler import HttpResolver -app = HttpResolverLocal() +app = HttpResolver() @app.get("/hello/") diff --git a/examples/event_handler_rest/src/http_resolver_exception_handling.py b/examples/event_handler_rest/src/http_resolver_exception_handling.py index 787687e6248..7a952812017 100644 --- a/examples/event_handler_rest/src/http_resolver_exception_handling.py +++ b/examples/event_handler_rest/src/http_resolver_exception_handling.py @@ -1,6 +1,6 @@ -from aws_lambda_powertools.event_handler import HttpResolverLocal, Response +from aws_lambda_powertools.event_handler import HttpResolver, Response -app = HttpResolverLocal() +app = HttpResolver() class NotFoundError(Exception): diff --git a/examples/event_handler_rest/src/http_resolver_validation_swagger.py b/examples/event_handler_rest/src/http_resolver_validation_swagger.py index 2c694c87bcc..d35e7964b9e 100644 --- a/examples/event_handler_rest/src/http_resolver_validation_swagger.py +++ b/examples/event_handler_rest/src/http_resolver_validation_swagger.py @@ -1,6 +1,6 @@ from pydantic import BaseModel -from aws_lambda_powertools.event_handler import HttpResolverLocal +from aws_lambda_powertools.event_handler import HttpResolver class User(BaseModel): @@ -8,7 +8,7 @@ class User(BaseModel): age: int -app = HttpResolverLocal(enable_validation=True) +app = HttpResolver(enable_validation=True) app.enable_swagger( title="My API", diff --git a/examples/event_handler_rest/src/skip_validating_query_strings.py b/examples/event_handler_rest/src/skip_validating_query_strings.py index 882769239a1..0cf6628035e 100644 --- a/examples/event_handler_rest/src/skip_validating_query_strings.py +++ b/examples/event_handler_rest/src/skip_validating_query_strings.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List import requests from pydantic import BaseModel, Field @@ -15,14 +15,14 @@ class Todo(BaseModel): userId: int - id_: Optional[int] = Field(alias="id", default=None) + id_: int | None = Field(alias="id", default=None) title: str completed: bool @app.get("/todos") @tracer.capture_method -def get_todos(completed: Optional[str] = None) -> List[Todo]: # (1)! +def get_todos(completed: str | None = None) -> List[Todo]: # (1)! url = "https://jsonplaceholder.typicode.com/todos" if completed is not None: diff --git a/examples/event_handler_rest/src/validating_headers.py b/examples/event_handler_rest/src/validating_headers.py index f92f0ead463..9c649c11bac 100644 --- a/examples/event_handler_rest/src/validating_headers.py +++ b/examples/event_handler_rest/src/validating_headers.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List import requests from pydantic import BaseModel, Field @@ -17,7 +17,7 @@ class Todo(BaseModel): userId: int - id_: Optional[int] = Field(alias="id", default=None) + id_: int | None = Field(alias="id", default=None) title: str completed: bool diff --git a/examples/event_handler_rest/src/validating_headers_with_pydantic.py b/examples/event_handler_rest/src/validating_headers_with_pydantic.py index 03f6c56e9fd..821ad566185 100644 --- a/examples/event_handler_rest/src/validating_headers_with_pydantic.py +++ b/examples/event_handler_rest/src/validating_headers_with_pydantic.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional +from typing import Any, Dict from pydantic import BaseModel, Field from typing_extensions import Annotated @@ -16,7 +16,7 @@ class Todo(BaseModel): userId: int - id_: Optional[int] = Field(alias="id", default=None) + id_: int | None = Field(alias="id", default=None) title: str completed: bool diff --git a/examples/event_handler_rest/src/validating_path.py b/examples/event_handler_rest/src/validating_path.py index c5ddbba4f37..f1001a6e18f 100644 --- a/examples/event_handler_rest/src/validating_path.py +++ b/examples/event_handler_rest/src/validating_path.py @@ -1,5 +1,3 @@ -from typing import Optional - import requests from pydantic import BaseModel, Field from typing_extensions import Annotated @@ -17,7 +15,7 @@ class Todo(BaseModel): userId: int - id_: Optional[int] = Field(alias="id", default=None) + id_: int | None = Field(alias="id", default=None) title: str completed: bool diff --git a/examples/event_handler_rest/src/validating_payload_subset.py b/examples/event_handler_rest/src/validating_payload_subset.py index ebd0cf0c20f..051505369f9 100644 --- a/examples/event_handler_rest/src/validating_payload_subset.py +++ b/examples/event_handler_rest/src/validating_payload_subset.py @@ -1,5 +1,3 @@ -from typing import Optional - import requests from pydantic import BaseModel, Field from typing_extensions import Annotated @@ -13,7 +11,7 @@ class Todo(BaseModel): userId: int - id_: Optional[int] = Field(alias="id", default=None) + id_: int | None = Field(alias="id", default=None) title: str completed: bool diff --git a/examples/event_handler_rest/src/validating_payloads.py b/examples/event_handler_rest/src/validating_payloads.py index 945cefd8089..a7ed6c88749 100644 --- a/examples/event_handler_rest/src/validating_payloads.py +++ b/examples/event_handler_rest/src/validating_payloads.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List import requests from pydantic import BaseModel, Field @@ -15,7 +15,7 @@ class Todo(BaseModel): # (2)! userId: int - id_: Optional[int] = Field(alias="id", default=None) + id_: int | None = Field(alias="id", default=None) title: str completed: bool diff --git a/examples/event_handler_rest/src/validating_query_string_with_pydantic.py b/examples/event_handler_rest/src/validating_query_string_with_pydantic.py index 75f6212e1a6..2ea42995c37 100644 --- a/examples/event_handler_rest/src/validating_query_string_with_pydantic.py +++ b/examples/event_handler_rest/src/validating_query_string_with_pydantic.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional +from typing import Any, Dict from pydantic import BaseModel, Field from typing_extensions import Annotated @@ -16,7 +16,7 @@ class Todo(BaseModel): userId: int - id_: Optional[int] = Field(alias="id", default=None) + id_: int | None = Field(alias="id", default=None) title: str completed: bool diff --git a/examples/event_handler_rest/src/validating_query_strings.py b/examples/event_handler_rest/src/validating_query_strings.py index 047e9973b63..75ff7450a1c 100644 --- a/examples/event_handler_rest/src/validating_query_strings.py +++ b/examples/event_handler_rest/src/validating_query_strings.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List import requests from pydantic import BaseModel, Field @@ -17,14 +17,14 @@ class Todo(BaseModel): userId: int - id_: Optional[int] = Field(alias="id", default=None) + id_: int | None = Field(alias="id", default=None) title: str completed: bool @app.get("/todos") @tracer.capture_method -def get_todos(completed: Annotated[Optional[str], Query(min_length=4)] = None) -> List[Todo]: # (3)! +def get_todos(completed: Annotated[str | None, Query(min_length=4)] = None) -> List[Todo]: # (3)! url = "https://jsonplaceholder.typicode.com/todos" if completed is not None: diff --git a/examples/event_sources/src/kinesis_firehose_response_exception.py b/examples/event_sources/src/kinesis_firehose_response_exception.py index 43ba3a039b2..5363107eea8 100644 --- a/examples/event_sources/src/kinesis_firehose_response_exception.py +++ b/examples/event_sources/src/kinesis_firehose_response_exception.py @@ -9,11 +9,10 @@ @event_source(data_class=KinesisFirehoseEvent) -def lambda_handler(event: dict, context: LambdaContext): - firehose_event = KinesisFirehoseEvent(event) +def lambda_handler(event: KinesisFirehoseEvent, context: LambdaContext): result = KinesisFirehoseDataTransformationResponse() - for record in firehose_event.records: + for record in event.records: try: payload = record.data_as_text # base64 decoded data as str diff --git a/examples/homepage/install/arm64/amplify.txt b/examples/homepage/install/arm64/amplify.txt index 8aadd5ecbba..3041343f94c 100644 --- a/examples/homepage/install/arm64/amplify.txt +++ b/examples/homepage/install/arm64/amplify.txt @@ -6,7 +6,7 @@ ? 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:AWSLambdaPowertoolsPythonV3-python312-arm64:33 +? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37 ❯ amplify push -y @@ -17,5 +17,5 @@ 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:AWSLambdaPowertoolsPythonV3-python312-arm64:33 +? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37 ? Do you want to edit the local lambda function now? No diff --git a/examples/homepage/install/arm64/cdk_arm64.py b/examples/homepage/install/arm64/cdk_arm64.py index 2b7d084a199..30977127c57 100644 --- a/examples/homepage/install/arm64/cdk_arm64.py +++ b/examples/homepage/install/arm64/cdk_arm64.py @@ -9,7 +9,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: powertools_layer = aws_lambda.LayerVersion.from_layer_version_arn( self, id="lambda-powertools", - layer_version_arn=f"arn:aws:lambda:{Aws.REGION}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33", + layer_version_arn=f"arn:aws:lambda:{Aws.REGION}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37", ) aws_lambda.Function( self, diff --git a/examples/homepage/install/arm64/pulumi_arm64.py b/examples/homepage/install/arm64/pulumi_arm64.py index 1659d2362ad..2f502d4d413 100644 --- a/examples/homepage/install/arm64/pulumi_arm64.py +++ b/examples/homepage/install/arm64/pulumi_arm64.py @@ -22,7 +22,7 @@ pulumi.Output.concat( "arn:aws:lambda:", aws.get_region_output().name, - ":017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33", + ":017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37", ), ], tracing_config={"mode": "Active"}, diff --git a/examples/homepage/install/arm64/sam.yaml b/examples/homepage/install/arm64/sam.yaml index 8d77abd0e02..6e76d90e197 100644 --- a/examples/homepage/install/arm64/sam.yaml +++ b/examples/homepage/install/arm64/sam.yaml @@ -9,4 +9,4 @@ Resources: Runtime: python3.12 Handler: app.lambda_handler Layers: - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37 diff --git a/examples/homepage/install/arm64/serverless.yml b/examples/homepage/install/arm64/serverless.yml index c3812016a42..06c437f6be1 100644 --- a/examples/homepage/install/arm64/serverless.yml +++ b/examples/homepage/install/arm64/serverless.yml @@ -10,4 +10,4 @@ functions: handler: lambda_function.lambda_handler architecture: arm64 layers: - - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33 + - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37 diff --git a/examples/homepage/install/arm64/terraform.tf b/examples/homepage/install/arm64/terraform.tf index dc46db6df87..a6ac2ac56a6 100644 --- a/examples/homepage/install/arm64/terraform.tf +++ b/examples/homepage/install/arm64/terraform.tf @@ -34,7 +34,7 @@ resource "aws_lambda_function" "test_lambda" { role = aws_iam_role.iam_for_lambda.arn handler = "index.test" runtime = "python3.12" - layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33"] + layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:37"] architectures = ["arm64"] source_code_hash = filebase64sha256("lambda_function_payload.zip") diff --git a/examples/homepage/install/x86_64/amplify.txt b/examples/homepage/install/x86_64/amplify.txt index 9a1223e9280..74a47656dd9 100644 --- a/examples/homepage/install/x86_64/amplify.txt +++ b/examples/homepage/install/x86_64/amplify.txt @@ -6,7 +6,7 @@ ? 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:AWSLambdaPowertoolsPythonV3-python312-x86_64:33 +? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37 ❯ amplify push -y @@ -17,5 +17,5 @@ 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:AWSLambdaPowertoolsPythonV3-python312-x86_64:33 +? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37 ? Do you want to edit the local lambda function now? No diff --git a/examples/homepage/install/x86_64/cdk_x86.py b/examples/homepage/install/x86_64/cdk_x86.py index 30d82a497dd..1b09ee48ce4 100644 --- a/examples/homepage/install/x86_64/cdk_x86.py +++ b/examples/homepage/install/x86_64/cdk_x86.py @@ -9,7 +9,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: powertools_layer = aws_lambda.LayerVersion.from_layer_version_arn( self, id="lambda-powertools", - layer_version_arn=f"arn:aws:lambda:{Aws.REGION}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33", + layer_version_arn=f"arn:aws:lambda:{Aws.REGION}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37", ) aws_lambda.Function( self, diff --git a/examples/homepage/install/x86_64/pulumi_x86.py b/examples/homepage/install/x86_64/pulumi_x86.py index 1130c67b644..b74df328c0d 100644 --- a/examples/homepage/install/x86_64/pulumi_x86.py +++ b/examples/homepage/install/x86_64/pulumi_x86.py @@ -22,7 +22,7 @@ pulumi.Output.concat( "arn:aws:lambda:", aws.get_region_output().name, - ":017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33", + ":017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37", ), ], tracing_config={"mode": "Active"}, diff --git a/examples/homepage/install/x86_64/sam.yaml b/examples/homepage/install/x86_64/sam.yaml index d6beb66ea52..85cb40ad532 100644 --- a/examples/homepage/install/x86_64/sam.yaml +++ b/examples/homepage/install/x86_64/sam.yaml @@ -8,4 +8,4 @@ Resources: Runtime: python3.12 Handler: app.lambda_handler Layers: - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37 diff --git a/examples/homepage/install/x86_64/serverless.yml b/examples/homepage/install/x86_64/serverless.yml index 56deaf1b0fa..2183bda869c 100644 --- a/examples/homepage/install/x86_64/serverless.yml +++ b/examples/homepage/install/x86_64/serverless.yml @@ -10,4 +10,4 @@ functions: handler: lambda_function.lambda_handler architecture: arm64 layers: - - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33 + - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37 diff --git a/examples/homepage/install/x86_64/terraform.tf b/examples/homepage/install/x86_64/terraform.tf index 56e73fe45b6..2b9703883bb 100644 --- a/examples/homepage/install/x86_64/terraform.tf +++ b/examples/homepage/install/x86_64/terraform.tf @@ -34,7 +34,7 @@ resource "aws_lambda_function" "test_lambda" { role = aws_iam_role.iam_for_lambda.arn handler = "index.test" runtime = "python3.12" - layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33"] + layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37"] source_code_hash = filebase64sha256("lambda_function_payload.zip") } diff --git a/examples/idempotency/src/bring_your_own_persistent_store.py b/examples/idempotency/src/bring_your_own_persistent_store.py index 83ab27b8c4e..50e3595e94f 100644 --- a/examples/idempotency/src/bring_your_own_persistent_store.py +++ b/examples/idempotency/src/bring_your_own_persistent_store.py @@ -1,6 +1,6 @@ import datetime import logging -from typing import Any, Dict, Optional +from typing import Any, Dict import boto3 from botocore.config import Config @@ -24,8 +24,8 @@ def __init__( status_attr: str = "status", data_attr: str = "data", validation_key_attr: str = "validation", - boto_config: Optional[Config] = None, - boto3_session: Optional[boto3.session.Session] = None, + boto_config: Config | None = None, + boto3_session: boto3.session.Session | None = None, ): boto3_session = boto3_session or boto3.session.Session() self._ddb_resource = boto3_session.resource("dynamodb", config=boto_config) diff --git a/examples/logger/sam/template.yaml b/examples/logger/sam/template.yaml index 092fb4166d3..a4c3125c76b 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:AWSLambdaPowertoolsPythonV3-python312-x86_64:33 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37 Resources: LoggerLambdaHandlerExample: diff --git a/examples/logger/src/bring_your_own_formatter_from_scratch.py b/examples/logger/src/bring_your_own_formatter_from_scratch.py index 425f00b0d28..7bcffa1d0af 100644 --- a/examples/logger/src/bring_your_own_formatter_from_scratch.py +++ b/examples/logger/src/bring_your_own_formatter_from_scratch.py @@ -1,13 +1,13 @@ import json import logging -from typing import Any, Dict, Iterable, List, Optional +from typing import Any, Dict, Iterable, List from aws_lambda_powertools import Logger from aws_lambda_powertools.logging.formatter import BasePowertoolsFormatter class CustomFormatter(BasePowertoolsFormatter): - def __init__(self, log_record_order: Optional[List[str]] = None, *args, **kwargs): + def __init__(self, log_record_order: List[str] | None = None, *args, **kwargs): self.log_record_order = log_record_order or ["level", "location", "message", "timestamp"] self.log_format = dict.fromkeys(self.log_record_order) super().__init__(*args, **kwargs) diff --git a/examples/metrics/sam/template.yaml b/examples/metrics/sam/template.yaml index dc7d0258fee..a90e5e0fc1d 100644 --- a/examples/metrics/sam/template.yaml +++ b/examples/metrics/sam/template.yaml @@ -16,7 +16,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:AWSLambdaPowertoolsPythonV3-python312-x86_64:33 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37 Resources: CaptureLambdaHandlerExample: diff --git a/examples/metrics_datadog/sam/template.yaml b/examples/metrics_datadog/sam/template.yaml index d43669484f8..38870c8c88e 100644 --- a/examples/metrics_datadog/sam/template.yaml +++ b/examples/metrics_datadog/sam/template.yaml @@ -20,7 +20,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:AWSLambdaPowertoolsPythonV3-python312-x86_64:33 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37 # Find the latest Layer version in the Datadog official documentation # Datadog SDK diff --git a/examples/streaming/src/assert_transformation_module.py b/examples/streaming/src/assert_transformation_module.py index eac11abd4af..cc9b1dc4984 100644 --- a/examples/streaming/src/assert_transformation_module.py +++ b/examples/streaming/src/assert_transformation_module.py @@ -1,5 +1,5 @@ import io -from typing import IO, Optional +from typing import IO from aws_lambda_powertools.utilities.streaming.transformations import BaseTransform @@ -9,7 +9,7 @@ def __init__(self, input_stream: IO[bytes], encoding: str): self.encoding = encoding self.input_stream = io.TextIOWrapper(input_stream, encoding=encoding) - def read(self, size: int = -1) -> Optional[bytes]: + def read(self, size: int = -1) -> bytes | None: data = self.input_stream.read(size) return data.upper().encode(self.encoding) diff --git a/examples/streaming/src/s3_json_transform.py b/examples/streaming/src/s3_json_transform.py index 30c31b0f32c..8588498e38b 100644 --- a/examples/streaming/src/s3_json_transform.py +++ b/examples/streaming/src/s3_json_transform.py @@ -1,5 +1,5 @@ import io -from typing import IO, Optional +from typing import IO import ijson @@ -11,10 +11,10 @@ class JsonDeserializer(io.RawIOBase): def __init__(self, input_stream: IO[bytes]): self.input = ijson.items(input_stream, "", multiple_values=True) - def read(self, size: int = -1) -> Optional[bytes]: + def read(self, size: int = -1) -> bytes | None: raise NotImplementedError(f"{__name__} does not implement read") - def readline(self, size: Optional[int] = None) -> bytes: + def readline(self, size: int | None = None) -> bytes: raise NotImplementedError(f"{__name__} does not implement readline") def read_object(self) -> dict: diff --git a/examples/tracer/sam/template.yaml b/examples/tracer/sam/template.yaml index 465ac3e2507..82fcc82b2ff 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:AWSLambdaPowertoolsPythonV3-python312-x86_64:33 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37 Resources: CaptureLambdaHandlerExample: diff --git a/layer_v3/poetry.lock b/layer_v3/poetry.lock index 79d0e9ccd06..7c33f06a244 100644 --- a/layer_v3/poetry.lock +++ b/layer_v3/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "attrs" @@ -14,75 +14,75 @@ files = [ [[package]] name = "aws-cdk-asset-awscli-v1" -version = "2.2.242" +version = "2.2.273" description = "A library that contains the AWS CLI for use in Lambda Layers" optional = false python-versions = "~=3.9" groups = ["main"] files = [ - {file = "aws_cdk_asset_awscli_v1-2.2.242-py3-none-any.whl", hash = "sha256:d1001bf56a12f7d1162d4211003d1e8f72a213159465e2d0e1c598cc0ea44aad"}, - {file = "aws_cdk_asset_awscli_v1-2.2.242.tar.gz", hash = "sha256:a957d679a118f4375307ed90b9aed7127c5c1402989438060eae4ab29ab0d13f"}, + {file = "aws_cdk_asset_awscli_v1-2.2.273-py3-none-any.whl", hash = "sha256:1a0994afa7b48f63b580603be64c7a99d19ed6777bdf81d3c2435d8b43cf0d71"}, + {file = "aws_cdk_asset_awscli_v1-2.2.273.tar.gz", hash = "sha256:6580dad3416e53712db434f81add6fb4a314e1a80f9c57cc42606df1f64c8e0f"}, ] [package.dependencies] -jsii = ">=1.112.0,<2.0.0" +jsii = ">=1.127.0,<2.0.0" publication = ">=0.0.3" -typeguard = ">=2.13.3,<4.3.0" +typeguard = "2.13.3" [[package]] name = "aws-cdk-asset-node-proxy-agent-v6" -version = "2.1.0" +version = "2.1.2" description = "@aws-cdk/asset-node-proxy-agent-v6" optional = false -python-versions = "~=3.8" +python-versions = "~=3.9" groups = ["main"] files = [ - {file = "aws_cdk.asset_node_proxy_agent_v6-2.1.0-py3-none-any.whl", hash = "sha256:24a388b69a44d03bae6dbf864c4e25ba650d4b61c008b4568b94ffbb9a69e40e"}, - {file = "aws_cdk_asset_node_proxy_agent_v6-2.1.0.tar.gz", hash = "sha256:1f292c0631f86708ba4ee328b3a2b229f7e46ea1c79fbde567ee9eb119c2b0e2"}, + {file = "aws_cdk_asset_node_proxy_agent_v6-2.1.2-py3-none-any.whl", hash = "sha256:1028bff16fdb87b8c82404e9b48f32ed383429dece84067f47379c4049da5da4"}, + {file = "aws_cdk_asset_node_proxy_agent_v6-2.1.2.tar.gz", hash = "sha256:1340588dd351dcae37e07188f39075364f73ccde6ed9468c1184b06ada4af665"}, ] [package.dependencies] -jsii = ">=1.103.1,<2.0.0" +jsii = ">=1.129.0,<2.0.0" publication = ">=0.0.3" -typeguard = ">=2.13.3,<5.0.0" +typeguard = "2.13.3" [[package]] name = "aws-cdk-cloud-assembly-schema" -version = "48.18.0" +version = "53.28.0" description = "Schema for the protocol between CDK framework and CDK CLI" optional = false -python-versions = "~=3.9" +python-versions = "~=3.10" groups = ["main"] files = [ - {file = "aws_cdk_cloud_assembly_schema-48.18.0-py3-none-any.whl", hash = "sha256:12b193fde580828d8c4909cfa62e1f83977fe20d0aa448c7e7e353f8e21e1b68"}, - {file = "aws_cdk_cloud_assembly_schema-48.18.0.tar.gz", hash = "sha256:dc8d1bbe09bca45d675fa7d5c59fd7c073ed86865fc97dc985c6a4caf1016b42"}, + {file = "aws_cdk_cloud_assembly_schema-53.28.0-py3-none-any.whl", hash = "sha256:c5de34f4a0d61eb48574a0264a9567175f601c27b4d053fb24ad1157c4ea0d99"}, + {file = "aws_cdk_cloud_assembly_schema-53.28.0.tar.gz", hash = "sha256:12e28dc44026e5f062041ba1450a510b1acddcfbe8358f73cfda8df993712d42"}, ] [package.dependencies] -jsii = ">=1.118.0,<2.0.0" +jsii = ">=1.131.0,<2.0.0" publication = ">=0.0.3" -typeguard = ">=2.13.3,<4.3.0" +typeguard = "2.13.3" [[package]] name = "aws-cdk-lib" -version = "2.223.0" +version = "2.253.0" description = "Version 2 of the AWS Cloud Development Kit library" optional = false python-versions = "~=3.9" groups = ["main"] files = [ - {file = "aws_cdk_lib-2.223.0-py3-none-any.whl", hash = "sha256:c6070a9449b2ce48987855ce273e65b9b18a134d98110ac86f23a393e1bf0b84"}, - {file = "aws_cdk_lib-2.223.0.tar.gz", hash = "sha256:8d2ea3c2754d1022db19f59e0558a9b23b26317a95b1327c502ca8afd94c5830"}, + {file = "aws_cdk_lib-2.253.0-py3-none-any.whl", hash = "sha256:58997c8a5af49d5328f5106468581a14235a1356dfe7574bb14656799594a2b5"}, + {file = "aws_cdk_lib-2.253.0.tar.gz", hash = "sha256:6db33e164f9afd6207698d382579bf62f762f14325f4b6d77643865e92448f20"}, ] [package.dependencies] -"aws-cdk.asset-awscli-v1" = "2.2.242" -"aws-cdk.asset-node-proxy-agent-v6" = ">=2.1.0,<3.0.0" -"aws-cdk.cloud-assembly-schema" = ">=48.6.0,<49.0.0" -constructs = ">=10.0.0,<11.0.0" -jsii = ">=1.118.0,<2.0.0" +"aws-cdk.asset-awscli-v1" = "2.2.273" +"aws-cdk.asset-node-proxy-agent-v6" = ">=2.1.1,<3.0.0" +"aws-cdk.cloud-assembly-schema" = ">=53.18.0,<54.0.0" +constructs = ">=10.5.0,<11.0.0" +jsii = ">=1.128.0,<2.0.0" publication = ">=0.0.3" -typeguard = ">=2.13.3,<4.3.0" +typeguard = "2.13.3" [[package]] name = "boto3" @@ -166,20 +166,19 @@ files = [ [[package]] name = "constructs" -version = "10.4.3" +version = "10.8.1" description = "A programming model for software-defined state" optional = false -python-versions = "~=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "constructs-10.4.3-py3-none-any.whl", hash = "sha256:43bbefa1ac1c044577d0b1a30648fe5b49557b8b95de2648186f6b909febf7f9"}, - {file = "constructs-10.4.3.tar.gz", hash = "sha256:bfe3657b0acb62af7aa1fda9a7e338ae5cae84ddf8f84f71125a2a3800d52ea0"}, + {file = "constructs-10.8.1-py3-none-any.whl", hash = "sha256:f71297db64723889147c82de46dcacaa76b120f856d5b5ac5f65abb22ea88355"}, + {file = "constructs-10.8.1.tar.gz", hash = "sha256:9f6e4eb1f6b8b1ac8dcbc85457dcbb1e3f9bd93bbeef03154f6cc7fbbba74b06"}, ] [package.dependencies] -jsii = ">=1.118.0,<2.0.0" +jsii = ">=1.139.0,<2.0.0" publication = ">=0.0.3" -typeguard = ">=2.13.3,<4.3.0" [[package]] name = "exceptiongroup" @@ -200,26 +199,6 @@ typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] -[[package]] -name = "importlib-resources" -version = "6.5.2" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec"}, - {file = "importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] -type = ["pytest-mypy"] - [[package]] name = "iniconfig" version = "2.3.0" @@ -246,23 +225,22 @@ files = [ [[package]] name = "jsii" -version = "1.119.0" +version = "1.139.0" description = "Python client for jsii runtime" optional = false -python-versions = "~=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "jsii-1.119.0-py3-none-any.whl", hash = "sha256:9203200ed5289ecc6198783513cbd7abef53d4f6eac0046181d647fa56eda6ab"}, - {file = "jsii-1.119.0.tar.gz", hash = "sha256:9f87508908bfa51dd9aac59fdbbeff347ae76377758ea5e5f83f149052211514"}, + {file = "jsii-1.139.0-py3-none-any.whl", hash = "sha256:11468f767c6da698ed9888a04352850af33ccccfec7656475626caf36fca8bbb"}, + {file = "jsii-1.139.0.tar.gz", hash = "sha256:163c5d3ec00fd4ec89a33b9097f20a6f27626dd69d6875a8f7cc7577b8021ad0"}, ] [package.dependencies] -attrs = ">=21.2,<26.0" -cattrs = ">=1.8,<25.4" -importlib_resources = ">=5.2.0" +attrs = ">=21.2,<27.0" +cattrs = ">=1.8,<27.0" publication = ">=0.0.3" python-dateutil = "*" -typeguard = ">=2.13.3,<4.5.0" +typeguard = "2.13.3" typing_extensions = ">=3.8,<5.0" [[package]] @@ -372,10 +350,10 @@ files = [ ] [package.dependencies] -botocore = ">=1.37.4,<2.0a.0" +botocore = ">=1.37.4,<2.0a0" [package.extras] -crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] +crt = ["botocore[crt] (>=1.37.4,<2.0a0)"] [[package]] name = "six" @@ -444,22 +422,19 @@ files = [ [[package]] name = "typeguard" -version = "4.2.1" +version = "2.13.3" description = "Run-time type checker for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.5.3" groups = ["main"] files = [ - {file = "typeguard-4.2.1-py3-none-any.whl", hash = "sha256:7da3bd46e61f03e0852f8d251dcbdc2a336aa495d7daff01e092b55327796eb8"}, - {file = "typeguard-4.2.1.tar.gz", hash = "sha256:c556a1b95948230510070ca53fa0341fb0964611bd05d598d87fb52115d65fee"}, + {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"}, + {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"}, ] -[package.dependencies] -typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} - [package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"] -test = ["coverage[toml] (>=7)", "mypy (>=1.2.0) ; platform_python_implementation != \"PyPy\"", "pytest (>=7)"] +doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["mypy ; platform_python_implementation != \"PyPy\"", "pytest", "typing-extensions"] [[package]] name = "typing-extensions" diff --git a/mkdocs.yml b/mkdocs.yml index 6bea356bac6..265560a55d5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,6 +33,7 @@ nav: - utilities/data_classes.md - utilities/parser.md - utilities/idempotency.md + - utilities/circuit_breaker.md - utilities/data_masking.md - utilities/feature_flags.md - utilities/metadata.md @@ -105,6 +106,12 @@ nav: - Exceptions: api_doc/feature_flags/exceptions.md - Feature flags: api_doc/feature_flags/feature_flags.md - Schema: api_doc/feature_flags/schema.md + - Circuit Breaker (alpha): + - Circuit Breaker: api_doc/circuit_breaker_alpha/circuit_breaker.md + - Config: api_doc/circuit_breaker_alpha/config.md + - States: api_doc/circuit_breaker_alpha/states.md + - Exceptions: api_doc/circuit_breaker_alpha/exceptions.md + - Persistence: api_doc/circuit_breaker_alpha/persistence.md - Idempotency: - Base: api_doc/idempotency/base.md - Config: api_doc/idempotency/config.md @@ -248,6 +255,7 @@ plugins: - utilities/data_classes.md - utilities/parser.md - utilities/idempotency.md + - utilities/circuit_breaker.md - utilities/data_masking.md - utilities/feature_flags.md - utilities/metadata.md diff --git a/package-lock.json b/package-lock.json index f0b022384df..cda2d626edd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,13 @@ "name": "aws-lambda-powertools-python-e2e", "version": "1.0.0", "devDependencies": { - "aws-cdk": "^2.1126.0" + "aws-cdk": "^2.1136.0" } }, "node_modules/aws-cdk": { - "version": "2.1126.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1126.0.tgz", - "integrity": "sha512-uNoocb3vCPiAT3j9+SwL6pn/VVggHWBsgC2XpxyhNvYQYt6cE9BM/149GWwtdcwnLrPjnwW1+CV/5nSSh5dV+w==", + "version": "2.1136.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1136.0.tgz", + "integrity": "sha512-n6kCL9R9uuvb7WXEwiSs3rYGwnTy9AHHmxOT5Pj5/++E4PYz2bLDXIxzoiOMIj5maoyC52PTe/0GezFV+O0ILQ==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 42de5c30465..d429b6b453c 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,6 @@ "name": "aws-lambda-powertools-python-e2e", "version": "1.0.0", "devDependencies": { - "aws-cdk": "^2.1126.0" + "aws-cdk": "^2.1136.0" } } diff --git a/poetry.lock b/poetry.lock index 1bafb91820d..6234daec8f2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -29,6 +29,7 @@ markers = {main = "extra == \"valkey\""} [package.dependencies] exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" +trio = {version = ">=0.32.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"trio\""} typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] @@ -51,45 +52,69 @@ test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] [[package]] name = "ast-serialize" -version = "0.5.0" +version = "0.8.0" description = "Python bindings for mypy AST serialization" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb"}, - {file = "ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101"}, - {file = "ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934"}, - {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759"}, - {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887"}, - {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27"}, - {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d"}, - {file = "ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a"}, - {file = "ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590"}, - {file = "ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642"}, - {file = "ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6"}, + {file = "ast_serialize-0.8.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3d822605fa7bb326ef868d25fafced7fc660fa46d9b90c02ea86d5e2f5d325f7"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:2efa40b068197d5efb62655b43baadb842ed71c4958cccd3e8b86a35726f0119"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:db1b957291bca08c7e72f43a12357b2948e20775d970e3fc3dac0aa3160ab725"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdc0d5b18ff8fb364e87923e47c0a91d0d69dbcaeaa274591f7fd26892cc3a3a"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9da7330f3e235bf7da89b8d39205c6350fc0c08a85379743f2df9fff87d6d980"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3186969ee66a9863b00acc6523ace44c56974eecb348a7ea4b228d9f0b80e19"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40a57b73731be45da4fa41430c4d5dc94a24b3a4faba7b9e069978c0402064ea"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b9da3ef807eda752502446dfecea3b381c4900b7e27a5d5f4f899eb39951"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:293cc1c5bfa741f8e3fbe8175b9c07beee487c9a6fdbb25a5acad9f1df2d30a9"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0910c3442a75216dde0f102d854ba2aaa71d2482e0ee213630b9bf29584fba3"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43dd6d596879bb1cb8a12cc9dae7bb10090a39a35883026c24f82488a195619a"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8c9d537f59e936392cfd3597789d1390304dd659efc3c486ce7f40fb6b8a9f53"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f0190a33d7f97c65e9069f7a7f40499eea6b5cbe260c558378109caf20ce934b"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:77308ae6c5cf5264cc0f01a7c556ec77a9e68eb1f61b093534d698139fdc3b14"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8d53a23f27e1ed3a36b2d26fd2a1a6228c8e85a1ed62ff7cdb44bd610769f20a"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa5e7cb08f96fed9121f77b224151e41caf88feab9d652bb46c78202b6fbeda"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:fa70ed4dea0bb18b30a1789c77baa701d0ef30c474f2ccabdea61e25623a8827"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d8b3c8eee4c1baef9d4e84d2a59a805501617127be42615cb48970b15b0892b6"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:ac4f0a83c55a9b782f79ad55a5247b7db123c1db405959791c2ef886e9710c9f"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:86b8a1e6d90467345356098b040150e82fbc26d24a7a202224b13dc1f6264ca0"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:39e92ff8e8cb45947fe9007174b2950e1fb098e6abd00266a13cd3bcf6675068"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85d8d18db5b2dfcb3b7e38a4d600ca35504c0ed8a6f75cd1c811e4ffe248a15"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9830ff7e764f74d9eefb01170c61a9f0fd2c027dac5fcb72e064decd57d56371"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6479d9722a4cd21b578f5478074c41e6169f04811996ec881655560f703a5bba"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a63bed264e818cd83eec11feed0f50aa162542b91132ef58afebc857182763a5"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d187197d234aa45d6cfa2b096be5f666e8cc2e7eb3722d0ab8926293cf5720c"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:2d39a56282cfcc0d8eeea37267c754be59c98d48505c23b1dae5c6011f3813dd"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7cc5f10386994c0f4844f1e6d6a97127e9b478660eb6dec2b257644f0acab64"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6102f2f985c2e542be85cd857678ec9356fefa792b93cadfadd31139f5696f27"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:3a8660fe66667b76a6e9dccd1d33e66b229fde3b308db991c041609226c005b6"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:e7266307e5fba39836edb79def8608887af48820508bff3c5f2941e1e04d1534"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca7e6fd1ad845d1cc649dc2ecd499db2f8f46af5bf8da7b70dd858774cc038b"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2880350b13d3eae69a0d70bc1fb6c9bfaca4dbd0e20ba8cd1aa483080b56ff06"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:ab0f9a59f7d63d0d441b56b9a818b273705264352d5115cfee12e940e816d958"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:0485a25ef519c62e749ee3c1ad8070e591b380d67226349eb5a70b228dc1ac4a"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:bd84d60bca7079e741be4ac5dbe237751a59d7f6f9f0126b11880d63822cbe16"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:057769b5921336eb2d9124f2a731b42ed05ffdac559b840dbdf6f3937cf153dc"}, + {file = "ast_serialize-0.8.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:a02cbed7d8bfdcdee88edaac12bd50d53d9953aaa2e1852ef078625be5f1c0b5"}, + {file = "ast_serialize-0.8.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e1bd223df0f6c96b396975fa604cb33bce53d9b4a0185490be4c4a289f7c9c87"}, + {file = "ast_serialize-0.8.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ddd3b61f45c132da66c5476b281891e08c1fd87fbdabe8a6973e1622efc85f06"}, + {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9caa63fad8241257ae401b5ff0a64026c6adb36b8e86cbe8782d9ea505daf6"}, + {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3926fa117b5e65019853a2969966d11c7175af377a3425991f3fe73784412405"}, + {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485f1113af805e9e170b95ef993ca3fbd4f89c04bab25c58b4fc632d854801ab"}, + {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccebbed24f1281062d5852353c72c47502955926cfcb8345ffb3a44d87ff3d3"}, + {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:252f883290d1cdb728eb7fe1d9a7221b88af5a329aae0bc91ddee4dafb820331"}, + {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:96abc072ad29db8d02194afd47d68987322622787daceae82398d7b69f3ba2e6"}, + {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9118ad3e369727060b2696fc4078f250ecffca4248ba87f537f55cea9f9dce06"}, + {file = "ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f359df4bd921918af8bebd142a376c77511d7151cc8ba852760b587b5a4a54f3"}, + {file = "ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e94f9121d13fa36cbf21314783c77d05ae3a0868decd18cf5233fdcc6de49ac8"}, + {file = "ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:54f95b486018d262bcb387a9afd96f0da74508b442762b80c769454a6fbb3ee3"}, + {file = "ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c38b915511e32bc718c49dbce98ff9af36bac0ad6a604f58000cd5e3aecdba7"}, + {file = "ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a2ef9cf12f2de4f1028c42c1dd7d775255e0fb3e5bb48896c97e35ef52366fe"}, + {file = "ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f18048fe9f6dd266bd577cdec48bdcecb74faaa01fe941324435483b013ed2a"}, + {file = "ast_serialize-0.8.0-cp39-abi3-win32.whl", hash = "sha256:31883542dd6c94d178f5db3d32fbd69c5eb88b3a7c018e7ac8cc0c45195ddbed"}, + {file = "ast_serialize-0.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:861794565b06337005c1447ef23103a3d5a627d08bdc827870d00d0b28ef5f51"}, + {file = "ast_serialize-0.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0"}, + {file = "ast_serialize-0.8.0.tar.gz", hash = "sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010"}, ] [[package]] @@ -116,7 +141,7 @@ files = [ {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, ] -markers = {main = "extra == \"all\" or extra == \"datamasking\""} +markers = {main = "extra == \"valkey\" or extra == \"all\" or extra == \"datamasking\""} [[package]] name = "avro" @@ -137,20 +162,19 @@ zstandard = ["zstandard"] [[package]] name = "aws-cdk-asset-awscli-v1" -version = "2.2.282" +version = "2.2.292" description = "A library that contains the AWS CLI for use in Lambda Layers" optional = false -python-versions = "~=3.10" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "aws_cdk_asset_awscli_v1-2.2.282-py3-none-any.whl", hash = "sha256:24139da0eb1be59f8cfda22c6343d51e8d50bb89b5052b049cfab92683de7790"}, - {file = "aws_cdk_asset_awscli_v1-2.2.282.tar.gz", hash = "sha256:f86e61653927f77fb235b5ec148c955c610117430c869d182c8b20f7e07e2df2"}, + {file = "aws_cdk_asset_awscli_v1-2.2.292-py3-none-any.whl", hash = "sha256:01cfb33e2913df2d63448b0954ea4ed1bcb1d064514b4b7aa266eda915e8ebfc"}, + {file = "aws_cdk_asset_awscli_v1-2.2.292.tar.gz", hash = "sha256:06da203eeaecbbbf5f1ecd3b12140104e6a59b6a7f6182cfbda5c5deda4b6344"}, ] [package.dependencies] -jsii = ">=1.130.0,<2.0.0" +jsii = ">=1.139.0,<2.0.0" publication = ">=0.0.3" -typeguard = "2.13.3" [[package]] name = "aws-cdk-asset-node-proxy-agent-v6" @@ -249,72 +273,69 @@ typeguard = ">=2.13.3,<2.14.0" [[package]] name = "aws-cdk-aws-lambda-python-alpha" -version = "2.259.0a0" +version = "2.266.0a0" description = "The CDK Construct Library for AWS Lambda in Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "aws_cdk_aws_lambda_python_alpha-2.259.0a0-py3-none-any.whl", hash = "sha256:6ab839026a26fd9e9c7d0df04e08dbbfe32e9a22d89726175e165e2b5da268f2"}, - {file = "aws_cdk_aws_lambda_python_alpha-2.259.0a0.tar.gz", hash = "sha256:5d061cb7ba7a76e5699e7a0cc1aaa2ba329d5242ddf945a79eda717c287d2e09"}, + {file = "aws_cdk_aws_lambda_python_alpha-2.266.0a0-py3-none-any.whl", hash = "sha256:be7c34c7296d9e6c478df9065510b2013a52d27629be6d90b4c9221043e71077"}, + {file = "aws_cdk_aws_lambda_python_alpha-2.266.0a0.tar.gz", hash = "sha256:eb7bb2890c722dc7d49851c61f8df8f7dd339a9135216e8f007c5c5be1b05cdb"}, ] [package.dependencies] -aws-cdk-lib = ">=2.259.0,<3.0.0" +aws-cdk-lib = ">=2.266.0,<3.0.0" constructs = ">=10.5.0,<11.0.0" -jsii = ">=1.133.0,<2.0.0" +jsii = ">=1.139.0,<2.0.0" publication = ">=0.0.3" -typeguard = "2.13.3" [[package]] name = "aws-cdk-cloud-assembly-schema" -version = "54.2.0" +version = "54.21.0" description = "Schema for the protocol between CDK framework and CDK CLI" optional = false -python-versions = "~=3.10" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "aws_cdk_cloud_assembly_schema-54.2.0-py3-none-any.whl", hash = "sha256:a7038bffa1aaf3365b72147987396bcf0b9951c3b53b2eebfc181b7cb4227067"}, - {file = "aws_cdk_cloud_assembly_schema-54.2.0.tar.gz", hash = "sha256:2e3a05fca6a28f49811b7b011fce12b03dd0adb15c37a0ad2d1b16d8d66fe95a"}, + {file = "aws_cdk_cloud_assembly_schema-54.21.0-py3-none-any.whl", hash = "sha256:e5951260bb7ca20956240a96f90aa423c33467a5a37b40e338dd4e2c9c02fb0c"}, + {file = "aws_cdk_cloud_assembly_schema-54.21.0.tar.gz", hash = "sha256:bd66b07ab1a1570801219a3ad3e438a6853db284771cff7aa5e18935b2b3dd0c"}, ] [package.dependencies] -jsii = ">=1.132.0,<2.0.0" +jsii = ">=1.139.0,<2.0.0" publication = ">=0.0.3" -typeguard = "2.13.3" [[package]] name = "aws-cdk-lib" -version = "2.259.0" +version = "2.267.0" description = "Version 2 of the AWS Cloud Development Kit library" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "aws_cdk_lib-2.259.0-py3-none-any.whl", hash = "sha256:4ea916f3673bd372d5e5a7bf84eb5bfb20c171b74a02c14ec8af2620441457a3"}, - {file = "aws_cdk_lib-2.259.0.tar.gz", hash = "sha256:5481f23b51dd771146c747d54e5d442f25a346a78c60077bd21b7d968af898e7"}, + {file = "aws_cdk_lib-2.267.0-py3-none-any.whl", hash = "sha256:83e269742e10069b5df80b2b99ec55ffb097f31cbf62317b6cf44eb63d4a9430"}, + {file = "aws_cdk_lib-2.267.0.tar.gz", hash = "sha256:2778f1befdda7401ad064d60f5442858c2dad2a1a87591264ca6f17da6ebd4a2"}, ] [package.dependencies] -"aws-cdk.asset-awscli-v1" = "2.2.282" +"aws-cdk.asset-awscli-v1" = "2.2.292" "aws-cdk.asset-node-proxy-agent-v6" = ">=2.1.2,<3.0.0" -"aws-cdk.cloud-assembly-schema" = ">=54.0.0,<55.0.0" +"aws-cdk.cloud-assembly-schema" = ">=54.11.0,<55.0.0" constructs = ">=10.5.0,<11.0.0" -jsii = ">=1.133.0,<2.0.0" +jsii = ">=1.139.0,<2.0.0" publication = ">=0.0.3" -typeguard = "2.13.3" [[package]] name = "aws-encryption-sdk" -version = "4.0.5" +version = "4.0.6" description = "AWS Encryption SDK implementation for Python" optional = true python-versions = "*" groups = ["main"] markers = "extra == \"all\" or extra == \"datamasking\"" files = [ - {file = "aws_encryption_sdk-4.0.5-py2.py3-none-any.whl", hash = "sha256:3e6b76afb94c28730487dee71fa1bfc217fcdbab061733c220ff87b88630e40e"}, - {file = "aws_encryption_sdk-4.0.5.tar.gz", hash = "sha256:a36136181a4d63cbf0d7347d29786c80a4d74a07c79c88a8799e27b27a9c3fc1"}, + {file = "aws_encryption_sdk-4.0.6-py2.py3-none-any.whl", hash = "sha256:35f62f775a8d4a6cb677a4a8927bb19acb3da35308405213f668396ee6298ad4"}, + {file = "aws_encryption_sdk-4.0.6.tar.gz", hash = "sha256:ec03bfa2df64ef21417bd6ff749bb63e84149586a4f7fbe528ec6b443326ea9e"}, ] [package.dependencies] @@ -498,14 +519,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "boto3-stubs" -version = "1.43.24" -description = "Type annotations for boto3 1.43.24 generated with mypy-boto3-builder 8.12.0" +version = "1.43.36" +description = "Type annotations for boto3 1.43.36 generated with mypy-boto3-builder 8.12.0" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "boto3_stubs-1.43.24-py3-none-any.whl", hash = "sha256:d5d8c00769d46970efedbbe24fb99c26568dfa4cc12a9acb2ddba8667160dcc1"}, - {file = "boto3_stubs-1.43.24.tar.gz", hash = "sha256:1b60837a40179f668a6b2f5206fd67acb4bf636aff2cd401f84fcf651cb4c988"}, + {file = "boto3_stubs-1.43.36-py3-none-any.whl", hash = "sha256:e89e48f937710919ef2d36623a8a98c3768568ca6fb9726f7b2369f681ec5745"}, + {file = "boto3_stubs-1.43.36.tar.gz", hash = "sha256:35bdcef0f3e5b6bb1f1f6e61ab813fdde0d739508512450b6ea12509b516cdcb"}, ] [package.dependencies] @@ -530,7 +551,7 @@ account = ["mypy-boto3-account (>=1.43.0,<1.44.0)"] acm = ["mypy-boto3-acm (>=1.43.0,<1.44.0)"] acm-pca = ["mypy-boto3-acm-pca (>=1.43.0,<1.44.0)"] aiops = ["mypy-boto3-aiops (>=1.43.0,<1.44.0)"] -all = ["mypy-boto3-accessanalyzer (>=1.43.0,<1.44.0)", "mypy-boto3-account (>=1.43.0,<1.44.0)", "mypy-boto3-acm (>=1.43.0,<1.44.0)", "mypy-boto3-acm-pca (>=1.43.0,<1.44.0)", "mypy-boto3-aiops (>=1.43.0,<1.44.0)", "mypy-boto3-amp (>=1.43.0,<1.44.0)", "mypy-boto3-amplify (>=1.43.0,<1.44.0)", "mypy-boto3-amplifybackend (>=1.43.0,<1.44.0)", "mypy-boto3-amplifyuibuilder (>=1.43.0,<1.44.0)", "mypy-boto3-apigateway (>=1.43.0,<1.44.0)", "mypy-boto3-apigatewaymanagementapi (>=1.43.0,<1.44.0)", "mypy-boto3-apigatewayv2 (>=1.43.0,<1.44.0)", "mypy-boto3-appconfig (>=1.43.0,<1.44.0)", "mypy-boto3-appconfigdata (>=1.43.0,<1.44.0)", "mypy-boto3-appfabric (>=1.43.0,<1.44.0)", "mypy-boto3-appflow (>=1.43.0,<1.44.0)", "mypy-boto3-appintegrations (>=1.43.0,<1.44.0)", "mypy-boto3-application-autoscaling (>=1.43.0,<1.44.0)", "mypy-boto3-application-insights (>=1.43.0,<1.44.0)", "mypy-boto3-application-signals (>=1.43.0,<1.44.0)", "mypy-boto3-applicationcostprofiler (>=1.43.0,<1.44.0)", "mypy-boto3-appmesh (>=1.43.0,<1.44.0)", "mypy-boto3-apprunner (>=1.43.0,<1.44.0)", "mypy-boto3-appstream (>=1.43.0,<1.44.0)", "mypy-boto3-appsync (>=1.43.0,<1.44.0)", "mypy-boto3-arc-region-switch (>=1.43.0,<1.44.0)", "mypy-boto3-arc-zonal-shift (>=1.43.0,<1.44.0)", "mypy-boto3-artifact (>=1.43.0,<1.44.0)", "mypy-boto3-athena (>=1.43.0,<1.44.0)", "mypy-boto3-auditmanager (>=1.43.0,<1.44.0)", "mypy-boto3-autoscaling (>=1.43.0,<1.44.0)", "mypy-boto3-autoscaling-plans (>=1.43.0,<1.44.0)", "mypy-boto3-b2bi (>=1.43.0,<1.44.0)", "mypy-boto3-backup (>=1.43.0,<1.44.0)", "mypy-boto3-backup-gateway (>=1.43.0,<1.44.0)", "mypy-boto3-backupsearch (>=1.43.0,<1.44.0)", "mypy-boto3-batch (>=1.43.0,<1.44.0)", "mypy-boto3-bcm-dashboards (>=1.43.0,<1.44.0)", "mypy-boto3-bcm-data-exports (>=1.43.0,<1.44.0)", "mypy-boto3-bcm-pricing-calculator (>=1.43.0,<1.44.0)", "mypy-boto3-bcm-recommended-actions (>=1.43.0,<1.44.0)", "mypy-boto3-bedrock (>=1.43.0,<1.44.0)", "mypy-boto3-bedrock-agent (>=1.43.0,<1.44.0)", "mypy-boto3-bedrock-agent-runtime (>=1.43.0,<1.44.0)", "mypy-boto3-bedrock-agentcore (>=1.43.0,<1.44.0)", "mypy-boto3-bedrock-agentcore-control (>=1.43.0,<1.44.0)", "mypy-boto3-bedrock-data-automation (>=1.43.0,<1.44.0)", "mypy-boto3-bedrock-data-automation-runtime (>=1.43.0,<1.44.0)", "mypy-boto3-bedrock-runtime (>=1.43.0,<1.44.0)", "mypy-boto3-billing (>=1.43.0,<1.44.0)", "mypy-boto3-billingconductor (>=1.43.0,<1.44.0)", "mypy-boto3-braket (>=1.43.0,<1.44.0)", "mypy-boto3-budgets (>=1.43.0,<1.44.0)", "mypy-boto3-ce (>=1.43.0,<1.44.0)", "mypy-boto3-chatbot (>=1.43.0,<1.44.0)", "mypy-boto3-chime (>=1.43.0,<1.44.0)", "mypy-boto3-chime-sdk-identity (>=1.43.0,<1.44.0)", "mypy-boto3-chime-sdk-media-pipelines (>=1.43.0,<1.44.0)", "mypy-boto3-chime-sdk-meetings (>=1.43.0,<1.44.0)", "mypy-boto3-chime-sdk-messaging (>=1.43.0,<1.44.0)", "mypy-boto3-chime-sdk-voice (>=1.43.0,<1.44.0)", "mypy-boto3-cleanrooms (>=1.43.0,<1.44.0)", "mypy-boto3-cleanroomsml (>=1.43.0,<1.44.0)", "mypy-boto3-cloud9 (>=1.43.0,<1.44.0)", "mypy-boto3-cloudcontrol (>=1.43.0,<1.44.0)", "mypy-boto3-clouddirectory (>=1.43.0,<1.44.0)", "mypy-boto3-cloudformation (>=1.43.0,<1.44.0)", "mypy-boto3-cloudfront (>=1.43.0,<1.44.0)", "mypy-boto3-cloudfront-keyvaluestore (>=1.43.0,<1.44.0)", "mypy-boto3-cloudhsm (>=1.43.0,<1.44.0)", "mypy-boto3-cloudhsmv2 (>=1.43.0,<1.44.0)", "mypy-boto3-cloudsearch (>=1.43.0,<1.44.0)", "mypy-boto3-cloudsearchdomain (>=1.43.0,<1.44.0)", "mypy-boto3-cloudtrail (>=1.43.0,<1.44.0)", "mypy-boto3-cloudtrail-data (>=1.43.0,<1.44.0)", "mypy-boto3-cloudwatch (>=1.43.0,<1.44.0)", "mypy-boto3-codeartifact (>=1.43.0,<1.44.0)", "mypy-boto3-codebuild (>=1.43.0,<1.44.0)", "mypy-boto3-codecatalyst (>=1.43.0,<1.44.0)", "mypy-boto3-codecommit (>=1.43.0,<1.44.0)", "mypy-boto3-codeconnections (>=1.43.0,<1.44.0)", "mypy-boto3-codedeploy (>=1.43.0,<1.44.0)", "mypy-boto3-codeguru-reviewer (>=1.43.0,<1.44.0)", "mypy-boto3-codeguru-security (>=1.43.0,<1.44.0)", "mypy-boto3-codeguruprofiler (>=1.43.0,<1.44.0)", "mypy-boto3-codepipeline (>=1.43.0,<1.44.0)", "mypy-boto3-codestar-connections (>=1.43.0,<1.44.0)", "mypy-boto3-codestar-notifications (>=1.43.0,<1.44.0)", "mypy-boto3-cognito-identity (>=1.43.0,<1.44.0)", "mypy-boto3-cognito-idp (>=1.43.0,<1.44.0)", "mypy-boto3-cognito-sync (>=1.43.0,<1.44.0)", "mypy-boto3-comprehend (>=1.43.0,<1.44.0)", "mypy-boto3-comprehendmedical (>=1.43.0,<1.44.0)", "mypy-boto3-compute-optimizer (>=1.43.0,<1.44.0)", "mypy-boto3-compute-optimizer-automation (>=1.43.0,<1.44.0)", "mypy-boto3-config (>=1.43.0,<1.44.0)", "mypy-boto3-connect (>=1.43.0,<1.44.0)", "mypy-boto3-connect-contact-lens (>=1.43.0,<1.44.0)", "mypy-boto3-connectcampaigns (>=1.43.0,<1.44.0)", "mypy-boto3-connectcampaignsv2 (>=1.43.0,<1.44.0)", "mypy-boto3-connectcases (>=1.43.0,<1.44.0)", "mypy-boto3-connecthealth (>=1.43.0,<1.44.0)", "mypy-boto3-connectparticipant (>=1.43.0,<1.44.0)", "mypy-boto3-controlcatalog (>=1.43.0,<1.44.0)", "mypy-boto3-controltower (>=1.43.0,<1.44.0)", "mypy-boto3-cost-optimization-hub (>=1.43.0,<1.44.0)", "mypy-boto3-cur (>=1.43.0,<1.44.0)", "mypy-boto3-customer-profiles (>=1.43.0,<1.44.0)", "mypy-boto3-databrew (>=1.43.0,<1.44.0)", "mypy-boto3-dataexchange (>=1.43.0,<1.44.0)", "mypy-boto3-datapipeline (>=1.43.0,<1.44.0)", "mypy-boto3-datasync (>=1.43.0,<1.44.0)", "mypy-boto3-datazone (>=1.43.0,<1.44.0)", "mypy-boto3-dax (>=1.43.0,<1.44.0)", "mypy-boto3-deadline (>=1.43.0,<1.44.0)", "mypy-boto3-detective (>=1.43.0,<1.44.0)", "mypy-boto3-devicefarm (>=1.43.0,<1.44.0)", "mypy-boto3-devops-agent (>=1.43.0,<1.44.0)", "mypy-boto3-devops-guru (>=1.43.0,<1.44.0)", "mypy-boto3-directconnect (>=1.43.0,<1.44.0)", "mypy-boto3-discovery (>=1.43.0,<1.44.0)", "mypy-boto3-dlm (>=1.43.0,<1.44.0)", "mypy-boto3-dms (>=1.43.0,<1.44.0)", "mypy-boto3-docdb (>=1.43.0,<1.44.0)", "mypy-boto3-docdb-elastic (>=1.43.0,<1.44.0)", "mypy-boto3-drs (>=1.43.0,<1.44.0)", "mypy-boto3-ds (>=1.43.0,<1.44.0)", "mypy-boto3-ds-data (>=1.43.0,<1.44.0)", "mypy-boto3-dsql (>=1.43.0,<1.44.0)", "mypy-boto3-dynamodb (>=1.43.0,<1.44.0)", "mypy-boto3-dynamodbstreams (>=1.43.0,<1.44.0)", "mypy-boto3-ebs (>=1.43.0,<1.44.0)", "mypy-boto3-ec2 (>=1.43.0,<1.44.0)", "mypy-boto3-ec2-instance-connect (>=1.43.0,<1.44.0)", "mypy-boto3-ecr (>=1.43.0,<1.44.0)", "mypy-boto3-ecr-public (>=1.43.0,<1.44.0)", "mypy-boto3-ecs (>=1.43.0,<1.44.0)", "mypy-boto3-efs (>=1.43.0,<1.44.0)", "mypy-boto3-eks (>=1.43.0,<1.44.0)", "mypy-boto3-eks-auth (>=1.43.0,<1.44.0)", "mypy-boto3-elasticache (>=1.43.0,<1.44.0)", "mypy-boto3-elasticbeanstalk (>=1.43.0,<1.44.0)", "mypy-boto3-elb (>=1.43.0,<1.44.0)", "mypy-boto3-elbv2 (>=1.43.0,<1.44.0)", "mypy-boto3-elementalinference (>=1.43.0,<1.44.0)", "mypy-boto3-emr (>=1.43.0,<1.44.0)", "mypy-boto3-emr-containers (>=1.43.0,<1.44.0)", "mypy-boto3-emr-serverless (>=1.43.0,<1.44.0)", "mypy-boto3-entityresolution (>=1.43.0,<1.44.0)", "mypy-boto3-es (>=1.43.0,<1.44.0)", "mypy-boto3-events (>=1.43.0,<1.44.0)", "mypy-boto3-evs (>=1.43.0,<1.44.0)", "mypy-boto3-finspace (>=1.43.0,<1.44.0)", "mypy-boto3-finspace-data (>=1.43.0,<1.44.0)", "mypy-boto3-firehose (>=1.43.0,<1.44.0)", "mypy-boto3-fis (>=1.43.0,<1.44.0)", "mypy-boto3-fms (>=1.43.0,<1.44.0)", "mypy-boto3-forecast (>=1.43.0,<1.44.0)", "mypy-boto3-forecastquery (>=1.43.0,<1.44.0)", "mypy-boto3-frauddetector (>=1.43.0,<1.44.0)", "mypy-boto3-freetier (>=1.43.0,<1.44.0)", "mypy-boto3-fsx (>=1.43.0,<1.44.0)", "mypy-boto3-gamelift (>=1.43.0,<1.44.0)", "mypy-boto3-gameliftstreams (>=1.43.0,<1.44.0)", "mypy-boto3-geo-maps (>=1.43.0,<1.44.0)", "mypy-boto3-geo-places (>=1.43.0,<1.44.0)", "mypy-boto3-geo-routes (>=1.43.0,<1.44.0)", "mypy-boto3-glacier (>=1.43.0,<1.44.0)", "mypy-boto3-globalaccelerator (>=1.43.0,<1.44.0)", "mypy-boto3-glue (>=1.43.0,<1.44.0)", "mypy-boto3-grafana (>=1.43.0,<1.44.0)", "mypy-boto3-greengrass (>=1.43.0,<1.44.0)", "mypy-boto3-greengrassv2 (>=1.43.0,<1.44.0)", "mypy-boto3-groundstation (>=1.43.0,<1.44.0)", "mypy-boto3-guardduty (>=1.43.0,<1.44.0)", "mypy-boto3-health (>=1.43.0,<1.44.0)", "mypy-boto3-healthlake (>=1.43.0,<1.44.0)", "mypy-boto3-iam (>=1.43.0,<1.44.0)", "mypy-boto3-identitystore (>=1.43.0,<1.44.0)", "mypy-boto3-imagebuilder (>=1.43.0,<1.44.0)", "mypy-boto3-importexport (>=1.43.0,<1.44.0)", "mypy-boto3-inspector (>=1.43.0,<1.44.0)", "mypy-boto3-inspector-scan (>=1.43.0,<1.44.0)", "mypy-boto3-inspector2 (>=1.43.0,<1.44.0)", "mypy-boto3-interconnect (>=1.43.0,<1.44.0)", "mypy-boto3-internetmonitor (>=1.43.0,<1.44.0)", "mypy-boto3-invoicing (>=1.43.0,<1.44.0)", "mypy-boto3-iot (>=1.43.0,<1.44.0)", "mypy-boto3-iot-data (>=1.43.0,<1.44.0)", "mypy-boto3-iot-jobs-data (>=1.43.0,<1.44.0)", "mypy-boto3-iot-managed-integrations (>=1.43.0,<1.44.0)", "mypy-boto3-iotdeviceadvisor (>=1.43.0,<1.44.0)", "mypy-boto3-iotevents (>=1.43.0,<1.44.0)", "mypy-boto3-iotevents-data (>=1.43.0,<1.44.0)", "mypy-boto3-iotfleetwise (>=1.43.0,<1.44.0)", "mypy-boto3-iotsecuretunneling (>=1.43.0,<1.44.0)", "mypy-boto3-iotsitewise (>=1.43.0,<1.44.0)", "mypy-boto3-iotthingsgraph (>=1.43.0,<1.44.0)", "mypy-boto3-iottwinmaker (>=1.43.0,<1.44.0)", "mypy-boto3-iotwireless (>=1.43.0,<1.44.0)", "mypy-boto3-ivs (>=1.43.0,<1.44.0)", "mypy-boto3-ivs-realtime (>=1.43.0,<1.44.0)", "mypy-boto3-ivschat (>=1.43.0,<1.44.0)", "mypy-boto3-kafka (>=1.43.0,<1.44.0)", "mypy-boto3-kafkaconnect (>=1.43.0,<1.44.0)", "mypy-boto3-kendra (>=1.43.0,<1.44.0)", "mypy-boto3-kendra-ranking (>=1.43.0,<1.44.0)", "mypy-boto3-keyspaces (>=1.43.0,<1.44.0)", "mypy-boto3-keyspacesstreams (>=1.43.0,<1.44.0)", "mypy-boto3-kinesis (>=1.43.0,<1.44.0)", "mypy-boto3-kinesis-video-archived-media (>=1.43.0,<1.44.0)", "mypy-boto3-kinesis-video-media (>=1.43.0,<1.44.0)", "mypy-boto3-kinesis-video-signaling (>=1.43.0,<1.44.0)", "mypy-boto3-kinesis-video-webrtc-storage (>=1.43.0,<1.44.0)", "mypy-boto3-kinesisanalytics (>=1.43.0,<1.44.0)", "mypy-boto3-kinesisanalyticsv2 (>=1.43.0,<1.44.0)", "mypy-boto3-kinesisvideo (>=1.43.0,<1.44.0)", "mypy-boto3-kms (>=1.43.0,<1.44.0)", "mypy-boto3-lakeformation (>=1.43.0,<1.44.0)", "mypy-boto3-lambda (>=1.43.0,<1.44.0)", "mypy-boto3-launch-wizard (>=1.43.0,<1.44.0)", "mypy-boto3-lex-models (>=1.43.0,<1.44.0)", "mypy-boto3-lex-runtime (>=1.43.0,<1.44.0)", "mypy-boto3-lexv2-models (>=1.43.0,<1.44.0)", "mypy-boto3-lexv2-runtime (>=1.43.0,<1.44.0)", "mypy-boto3-license-manager (>=1.43.0,<1.44.0)", "mypy-boto3-license-manager-linux-subscriptions (>=1.43.0,<1.44.0)", "mypy-boto3-license-manager-user-subscriptions (>=1.43.0,<1.44.0)", "mypy-boto3-lightsail (>=1.43.0,<1.44.0)", "mypy-boto3-location (>=1.43.0,<1.44.0)", "mypy-boto3-logs (>=1.43.0,<1.44.0)", "mypy-boto3-lookoutequipment (>=1.43.0,<1.44.0)", "mypy-boto3-m2 (>=1.43.0,<1.44.0)", "mypy-boto3-machinelearning (>=1.43.0,<1.44.0)", "mypy-boto3-macie2 (>=1.43.0,<1.44.0)", "mypy-boto3-mailmanager (>=1.43.0,<1.44.0)", "mypy-boto3-managedblockchain (>=1.43.0,<1.44.0)", "mypy-boto3-managedblockchain-query (>=1.43.0,<1.44.0)", "mypy-boto3-marketplace-agreement (>=1.43.0,<1.44.0)", "mypy-boto3-marketplace-catalog (>=1.43.0,<1.44.0)", "mypy-boto3-marketplace-deployment (>=1.43.0,<1.44.0)", "mypy-boto3-marketplace-discovery (>=1.43.0,<1.44.0)", "mypy-boto3-marketplace-entitlement (>=1.43.0,<1.44.0)", "mypy-boto3-marketplace-reporting (>=1.43.0,<1.44.0)", "mypy-boto3-marketplacecommerceanalytics (>=1.43.0,<1.44.0)", "mypy-boto3-mediaconnect (>=1.43.0,<1.44.0)", "mypy-boto3-mediaconvert (>=1.43.0,<1.44.0)", "mypy-boto3-medialive (>=1.43.0,<1.44.0)", "mypy-boto3-mediapackage (>=1.43.0,<1.44.0)", "mypy-boto3-mediapackage-vod (>=1.43.0,<1.44.0)", "mypy-boto3-mediapackagev2 (>=1.43.0,<1.44.0)", "mypy-boto3-mediastore (>=1.43.0,<1.44.0)", "mypy-boto3-mediastore-data (>=1.43.0,<1.44.0)", "mypy-boto3-mediatailor (>=1.43.0,<1.44.0)", "mypy-boto3-medical-imaging (>=1.43.0,<1.44.0)", "mypy-boto3-memorydb (>=1.43.0,<1.44.0)", "mypy-boto3-meteringmarketplace (>=1.43.0,<1.44.0)", "mypy-boto3-mgh (>=1.43.0,<1.44.0)", "mypy-boto3-mgn (>=1.43.0,<1.44.0)", "mypy-boto3-migration-hub-refactor-spaces (>=1.43.0,<1.44.0)", "mypy-boto3-migrationhub-config (>=1.43.0,<1.44.0)", "mypy-boto3-migrationhuborchestrator (>=1.43.0,<1.44.0)", "mypy-boto3-migrationhubstrategy (>=1.43.0,<1.44.0)", "mypy-boto3-mpa (>=1.43.0,<1.44.0)", "mypy-boto3-mq (>=1.43.0,<1.44.0)", "mypy-boto3-mturk (>=1.43.0,<1.44.0)", "mypy-boto3-mwaa (>=1.43.0,<1.44.0)", "mypy-boto3-mwaa-serverless (>=1.43.0,<1.44.0)", "mypy-boto3-neptune (>=1.43.0,<1.44.0)", "mypy-boto3-neptune-graph (>=1.43.0,<1.44.0)", "mypy-boto3-neptunedata (>=1.43.0,<1.44.0)", "mypy-boto3-network-firewall (>=1.43.0,<1.44.0)", "mypy-boto3-networkflowmonitor (>=1.43.0,<1.44.0)", "mypy-boto3-networkmanager (>=1.43.0,<1.44.0)", "mypy-boto3-networkmonitor (>=1.43.0,<1.44.0)", "mypy-boto3-notifications (>=1.43.0,<1.44.0)", "mypy-boto3-notificationscontacts (>=1.43.0,<1.44.0)", "mypy-boto3-nova-act (>=1.43.0,<1.44.0)", "mypy-boto3-oam (>=1.43.0,<1.44.0)", "mypy-boto3-observabilityadmin (>=1.43.0,<1.44.0)", "mypy-boto3-odb (>=1.43.0,<1.44.0)", "mypy-boto3-omics (>=1.43.0,<1.44.0)", "mypy-boto3-opensearch (>=1.43.0,<1.44.0)", "mypy-boto3-opensearchserverless (>=1.43.0,<1.44.0)", "mypy-boto3-organizations (>=1.43.0,<1.44.0)", "mypy-boto3-osis (>=1.43.0,<1.44.0)", "mypy-boto3-outposts (>=1.43.0,<1.44.0)", "mypy-boto3-panorama (>=1.43.0,<1.44.0)", "mypy-boto3-partnercentral-account (>=1.43.0,<1.44.0)", "mypy-boto3-partnercentral-benefits (>=1.43.0,<1.44.0)", "mypy-boto3-partnercentral-channel (>=1.43.0,<1.44.0)", "mypy-boto3-partnercentral-selling (>=1.43.0,<1.44.0)", "mypy-boto3-payment-cryptography (>=1.43.0,<1.44.0)", "mypy-boto3-payment-cryptography-data (>=1.43.0,<1.44.0)", "mypy-boto3-pca-connector-ad (>=1.43.0,<1.44.0)", "mypy-boto3-pca-connector-scep (>=1.43.0,<1.44.0)", "mypy-boto3-pcs (>=1.43.0,<1.44.0)", "mypy-boto3-personalize (>=1.43.0,<1.44.0)", "mypy-boto3-personalize-events (>=1.43.0,<1.44.0)", "mypy-boto3-personalize-runtime (>=1.43.0,<1.44.0)", "mypy-boto3-pi (>=1.43.0,<1.44.0)", "mypy-boto3-pinpoint (>=1.43.0,<1.44.0)", "mypy-boto3-pinpoint-email (>=1.43.0,<1.44.0)", "mypy-boto3-pinpoint-sms-voice (>=1.43.0,<1.44.0)", "mypy-boto3-pinpoint-sms-voice-v2 (>=1.43.0,<1.44.0)", "mypy-boto3-pipes (>=1.43.0,<1.44.0)", "mypy-boto3-polly (>=1.43.0,<1.44.0)", "mypy-boto3-pricing (>=1.43.0,<1.44.0)", "mypy-boto3-proton (>=1.43.0,<1.44.0)", "mypy-boto3-qapps (>=1.43.0,<1.44.0)", "mypy-boto3-qbusiness (>=1.43.0,<1.44.0)", "mypy-boto3-qconnect (>=1.43.0,<1.44.0)", "mypy-boto3-quicksight (>=1.43.0,<1.44.0)", "mypy-boto3-ram (>=1.43.0,<1.44.0)", "mypy-boto3-rbin (>=1.43.0,<1.44.0)", "mypy-boto3-rds (>=1.43.0,<1.44.0)", "mypy-boto3-rds-data (>=1.43.0,<1.44.0)", "mypy-boto3-redshift (>=1.43.0,<1.44.0)", "mypy-boto3-redshift-data (>=1.43.0,<1.44.0)", "mypy-boto3-redshift-serverless (>=1.43.0,<1.44.0)", "mypy-boto3-rekognition (>=1.43.0,<1.44.0)", "mypy-boto3-repostspace (>=1.43.0,<1.44.0)", "mypy-boto3-resiliencehub (>=1.43.0,<1.44.0)", "mypy-boto3-resiliencehubv2 (>=1.43.0,<1.44.0)", "mypy-boto3-resource-explorer-2 (>=1.43.0,<1.44.0)", "mypy-boto3-resource-groups (>=1.43.0,<1.44.0)", "mypy-boto3-resourcegroupstaggingapi (>=1.43.0,<1.44.0)", "mypy-boto3-rolesanywhere (>=1.43.0,<1.44.0)", "mypy-boto3-route53 (>=1.43.0,<1.44.0)", "mypy-boto3-route53-recovery-cluster (>=1.43.0,<1.44.0)", "mypy-boto3-route53-recovery-control-config (>=1.43.0,<1.44.0)", "mypy-boto3-route53-recovery-readiness (>=1.43.0,<1.44.0)", "mypy-boto3-route53domains (>=1.43.0,<1.44.0)", "mypy-boto3-route53globalresolver (>=1.43.0,<1.44.0)", "mypy-boto3-route53profiles (>=1.43.0,<1.44.0)", "mypy-boto3-route53resolver (>=1.43.0,<1.44.0)", "mypy-boto3-rtbfabric (>=1.43.0,<1.44.0)", "mypy-boto3-rum (>=1.43.0,<1.44.0)", "mypy-boto3-s3 (>=1.43.0,<1.44.0)", "mypy-boto3-s3control (>=1.43.0,<1.44.0)", "mypy-boto3-s3files (>=1.43.0,<1.44.0)", "mypy-boto3-s3outposts (>=1.43.0,<1.44.0)", "mypy-boto3-s3tables (>=1.43.0,<1.44.0)", "mypy-boto3-s3vectors (>=1.43.0,<1.44.0)", "mypy-boto3-sagemaker (>=1.43.0,<1.44.0)", "mypy-boto3-sagemaker-a2i-runtime (>=1.43.0,<1.44.0)", "mypy-boto3-sagemaker-edge (>=1.43.0,<1.44.0)", "mypy-boto3-sagemaker-featurestore-runtime (>=1.43.0,<1.44.0)", "mypy-boto3-sagemaker-geospatial (>=1.43.0,<1.44.0)", "mypy-boto3-sagemaker-metrics (>=1.43.0,<1.44.0)", "mypy-boto3-sagemaker-runtime (>=1.43.0,<1.44.0)", "mypy-boto3-sagemakerjobruntime (>=1.43.0,<1.44.0)", "mypy-boto3-savingsplans (>=1.43.0,<1.44.0)", "mypy-boto3-scheduler (>=1.43.0,<1.44.0)", "mypy-boto3-schemas (>=1.43.0,<1.44.0)", "mypy-boto3-sdb (>=1.43.0,<1.44.0)", "mypy-boto3-secretsmanager (>=1.43.0,<1.44.0)", "mypy-boto3-security-ir (>=1.43.0,<1.44.0)", "mypy-boto3-securityagent (>=1.43.0,<1.44.0)", "mypy-boto3-securityhub (>=1.43.0,<1.44.0)", "mypy-boto3-securitylake (>=1.43.0,<1.44.0)", "mypy-boto3-serverlessrepo (>=1.43.0,<1.44.0)", "mypy-boto3-service-quotas (>=1.43.0,<1.44.0)", "mypy-boto3-servicecatalog (>=1.43.0,<1.44.0)", "mypy-boto3-servicecatalog-appregistry (>=1.43.0,<1.44.0)", "mypy-boto3-servicediscovery (>=1.43.0,<1.44.0)", "mypy-boto3-ses (>=1.43.0,<1.44.0)", "mypy-boto3-sesv2 (>=1.43.0,<1.44.0)", "mypy-boto3-shield (>=1.43.0,<1.44.0)", "mypy-boto3-signer (>=1.43.0,<1.44.0)", "mypy-boto3-signer-data (>=1.43.0,<1.44.0)", "mypy-boto3-signin (>=1.43.0,<1.44.0)", "mypy-boto3-simpledbv2 (>=1.43.0,<1.44.0)", "mypy-boto3-simspaceweaver (>=1.43.0,<1.44.0)", "mypy-boto3-snow-device-management (>=1.43.0,<1.44.0)", "mypy-boto3-snowball (>=1.43.0,<1.44.0)", "mypy-boto3-sns (>=1.43.0,<1.44.0)", "mypy-boto3-socialmessaging (>=1.43.0,<1.44.0)", "mypy-boto3-sqs (>=1.43.0,<1.44.0)", "mypy-boto3-ssm (>=1.43.0,<1.44.0)", "mypy-boto3-ssm-contacts (>=1.43.0,<1.44.0)", "mypy-boto3-ssm-guiconnect (>=1.43.0,<1.44.0)", "mypy-boto3-ssm-incidents (>=1.43.0,<1.44.0)", "mypy-boto3-ssm-quicksetup (>=1.43.0,<1.44.0)", "mypy-boto3-ssm-sap (>=1.43.0,<1.44.0)", "mypy-boto3-sso (>=1.43.0,<1.44.0)", "mypy-boto3-sso-admin (>=1.43.0,<1.44.0)", "mypy-boto3-sso-oidc (>=1.43.0,<1.44.0)", "mypy-boto3-stepfunctions (>=1.43.0,<1.44.0)", "mypy-boto3-storagegateway (>=1.43.0,<1.44.0)", "mypy-boto3-sts (>=1.43.0,<1.44.0)", "mypy-boto3-supplychain (>=1.43.0,<1.44.0)", "mypy-boto3-support (>=1.43.0,<1.44.0)", "mypy-boto3-support-app (>=1.43.0,<1.44.0)", "mypy-boto3-sustainability (>=1.43.0,<1.44.0)", "mypy-boto3-swf (>=1.43.0,<1.44.0)", "mypy-boto3-synthetics (>=1.43.0,<1.44.0)", "mypy-boto3-taxsettings (>=1.43.0,<1.44.0)", "mypy-boto3-textract (>=1.43.0,<1.44.0)", "mypy-boto3-timestream-influxdb (>=1.43.0,<1.44.0)", "mypy-boto3-timestream-query (>=1.43.0,<1.44.0)", "mypy-boto3-timestream-write (>=1.43.0,<1.44.0)", "mypy-boto3-tnb (>=1.43.0,<1.44.0)", "mypy-boto3-transcribe (>=1.43.0,<1.44.0)", "mypy-boto3-transfer (>=1.43.0,<1.44.0)", "mypy-boto3-translate (>=1.43.0,<1.44.0)", "mypy-boto3-trustedadvisor (>=1.43.0,<1.44.0)", "mypy-boto3-uxc (>=1.43.0,<1.44.0)", "mypy-boto3-verifiedpermissions (>=1.43.0,<1.44.0)", "mypy-boto3-voice-id (>=1.43.0,<1.44.0)", "mypy-boto3-vpc-lattice (>=1.43.0,<1.44.0)", "mypy-boto3-waf (>=1.43.0,<1.44.0)", "mypy-boto3-waf-regional (>=1.43.0,<1.44.0)", "mypy-boto3-wafv2 (>=1.43.0,<1.44.0)", "mypy-boto3-wellarchitected (>=1.43.0,<1.44.0)", "mypy-boto3-wickr (>=1.43.0,<1.44.0)", "mypy-boto3-wisdom (>=1.43.0,<1.44.0)", "mypy-boto3-workdocs (>=1.43.0,<1.44.0)", "mypy-boto3-workmail (>=1.43.0,<1.44.0)", "mypy-boto3-workmailmessageflow (>=1.43.0,<1.44.0)", "mypy-boto3-workspaces (>=1.43.0,<1.44.0)", "mypy-boto3-workspaces-instances (>=1.43.0,<1.44.0)", "mypy-boto3-workspaces-thin-client (>=1.43.0,<1.44.0)", "mypy-boto3-workspaces-web (>=1.43.0,<1.44.0)", "mypy-boto3-xray (>=1.43.0,<1.44.0)"] +all = ["mypy-boto3-accessanalyzer (>=1.43.0,<1.44.0)", "mypy-boto3-account (>=1.43.0,<1.44.0)", "mypy-boto3-acm (>=1.43.0,<1.44.0)", "mypy-boto3-acm-pca (>=1.43.0,<1.44.0)", "mypy-boto3-aiops (>=1.43.0,<1.44.0)", "mypy-boto3-amp (>=1.43.0,<1.44.0)", "mypy-boto3-amplify (>=1.43.0,<1.44.0)", "mypy-boto3-amplifybackend (>=1.43.0,<1.44.0)", "mypy-boto3-amplifyuibuilder (>=1.43.0,<1.44.0)", "mypy-boto3-apigateway (>=1.43.0,<1.44.0)", "mypy-boto3-apigatewaymanagementapi (>=1.43.0,<1.44.0)", "mypy-boto3-apigatewayv2 (>=1.43.0,<1.44.0)", "mypy-boto3-appconfig (>=1.43.0,<1.44.0)", "mypy-boto3-appconfigdata (>=1.43.0,<1.44.0)", "mypy-boto3-appfabric (>=1.43.0,<1.44.0)", "mypy-boto3-appflow (>=1.43.0,<1.44.0)", "mypy-boto3-appintegrations (>=1.43.0,<1.44.0)", "mypy-boto3-application-autoscaling (>=1.43.0,<1.44.0)", "mypy-boto3-application-insights (>=1.43.0,<1.44.0)", "mypy-boto3-application-signals (>=1.43.0,<1.44.0)", "mypy-boto3-applicationcostprofiler (>=1.43.0,<1.44.0)", "mypy-boto3-appmesh (>=1.43.0,<1.44.0)", "mypy-boto3-apprunner (>=1.43.0,<1.44.0)", "mypy-boto3-appstream (>=1.43.0,<1.44.0)", "mypy-boto3-appsync (>=1.43.0,<1.44.0)", "mypy-boto3-arc-region-switch (>=1.43.0,<1.44.0)", "mypy-boto3-arc-zonal-shift (>=1.43.0,<1.44.0)", "mypy-boto3-artifact (>=1.43.0,<1.44.0)", "mypy-boto3-athena (>=1.43.0,<1.44.0)", "mypy-boto3-auditmanager (>=1.43.0,<1.44.0)", "mypy-boto3-autoscaling (>=1.43.0,<1.44.0)", "mypy-boto3-autoscaling-plans (>=1.43.0,<1.44.0)", "mypy-boto3-b2bi (>=1.43.0,<1.44.0)", "mypy-boto3-backup (>=1.43.0,<1.44.0)", "mypy-boto3-backup-gateway (>=1.43.0,<1.44.0)", "mypy-boto3-backupsearch (>=1.43.0,<1.44.0)", "mypy-boto3-batch (>=1.43.0,<1.44.0)", "mypy-boto3-bcm-dashboards (>=1.43.0,<1.44.0)", "mypy-boto3-bcm-data-exports (>=1.43.0,<1.44.0)", "mypy-boto3-bcm-pricing-calculator (>=1.43.0,<1.44.0)", "mypy-boto3-bcm-recommended-actions (>=1.43.0,<1.44.0)", "mypy-boto3-bedrock (>=1.43.0,<1.44.0)", "mypy-boto3-bedrock-agent (>=1.43.0,<1.44.0)", "mypy-boto3-bedrock-agent-runtime (>=1.43.0,<1.44.0)", "mypy-boto3-bedrock-agentcore (>=1.43.0,<1.44.0)", "mypy-boto3-bedrock-agentcore-control (>=1.43.0,<1.44.0)", "mypy-boto3-bedrock-data-automation (>=1.43.0,<1.44.0)", "mypy-boto3-bedrock-data-automation-runtime (>=1.43.0,<1.44.0)", "mypy-boto3-bedrock-runtime (>=1.43.0,<1.44.0)", "mypy-boto3-billing (>=1.43.0,<1.44.0)", "mypy-boto3-billingconductor (>=1.43.0,<1.44.0)", "mypy-boto3-braket (>=1.43.0,<1.44.0)", "mypy-boto3-budgets (>=1.43.0,<1.44.0)", "mypy-boto3-ce (>=1.43.0,<1.44.0)", "mypy-boto3-chatbot (>=1.43.0,<1.44.0)", "mypy-boto3-chime (>=1.43.0,<1.44.0)", "mypy-boto3-chime-sdk-identity (>=1.43.0,<1.44.0)", "mypy-boto3-chime-sdk-media-pipelines (>=1.43.0,<1.44.0)", "mypy-boto3-chime-sdk-meetings (>=1.43.0,<1.44.0)", "mypy-boto3-chime-sdk-messaging (>=1.43.0,<1.44.0)", "mypy-boto3-chime-sdk-voice (>=1.43.0,<1.44.0)", "mypy-boto3-cleanrooms (>=1.43.0,<1.44.0)", "mypy-boto3-cleanroomsml (>=1.43.0,<1.44.0)", "mypy-boto3-cloud9 (>=1.43.0,<1.44.0)", "mypy-boto3-cloudcontrol (>=1.43.0,<1.44.0)", "mypy-boto3-clouddirectory (>=1.43.0,<1.44.0)", "mypy-boto3-cloudformation (>=1.43.0,<1.44.0)", "mypy-boto3-cloudfront (>=1.43.0,<1.44.0)", "mypy-boto3-cloudfront-keyvaluestore (>=1.43.0,<1.44.0)", "mypy-boto3-cloudhsm (>=1.43.0,<1.44.0)", "mypy-boto3-cloudhsmv2 (>=1.43.0,<1.44.0)", "mypy-boto3-cloudsearch (>=1.43.0,<1.44.0)", "mypy-boto3-cloudsearchdomain (>=1.43.0,<1.44.0)", "mypy-boto3-cloudtrail (>=1.43.0,<1.44.0)", "mypy-boto3-cloudtrail-data (>=1.43.0,<1.44.0)", "mypy-boto3-cloudwatch (>=1.43.0,<1.44.0)", "mypy-boto3-codeartifact (>=1.43.0,<1.44.0)", "mypy-boto3-codebuild (>=1.43.0,<1.44.0)", "mypy-boto3-codecatalyst (>=1.43.0,<1.44.0)", "mypy-boto3-codecommit (>=1.43.0,<1.44.0)", "mypy-boto3-codeconnections (>=1.43.0,<1.44.0)", "mypy-boto3-codedeploy (>=1.43.0,<1.44.0)", "mypy-boto3-codeguru-reviewer (>=1.43.0,<1.44.0)", "mypy-boto3-codeguru-security (>=1.43.0,<1.44.0)", "mypy-boto3-codeguruprofiler (>=1.43.0,<1.44.0)", "mypy-boto3-codepipeline (>=1.43.0,<1.44.0)", "mypy-boto3-codestar-connections (>=1.43.0,<1.44.0)", "mypy-boto3-codestar-notifications (>=1.43.0,<1.44.0)", "mypy-boto3-cognito-identity (>=1.43.0,<1.44.0)", "mypy-boto3-cognito-idp (>=1.43.0,<1.44.0)", "mypy-boto3-cognito-sync (>=1.43.0,<1.44.0)", "mypy-boto3-comprehend (>=1.43.0,<1.44.0)", "mypy-boto3-comprehendmedical (>=1.43.0,<1.44.0)", "mypy-boto3-compute-optimizer (>=1.43.0,<1.44.0)", "mypy-boto3-compute-optimizer-automation (>=1.43.0,<1.44.0)", "mypy-boto3-config (>=1.43.0,<1.44.0)", "mypy-boto3-connect (>=1.43.0,<1.44.0)", "mypy-boto3-connect-contact-lens (>=1.43.0,<1.44.0)", "mypy-boto3-connectcampaigns (>=1.43.0,<1.44.0)", "mypy-boto3-connectcampaignsv2 (>=1.43.0,<1.44.0)", "mypy-boto3-connectcases (>=1.43.0,<1.44.0)", "mypy-boto3-connecthealth (>=1.43.0,<1.44.0)", "mypy-boto3-connectparticipant (>=1.43.0,<1.44.0)", "mypy-boto3-controlcatalog (>=1.43.0,<1.44.0)", "mypy-boto3-controltower (>=1.43.0,<1.44.0)", "mypy-boto3-cost-optimization-hub (>=1.43.0,<1.44.0)", "mypy-boto3-cur (>=1.43.0,<1.44.0)", "mypy-boto3-customer-profiles (>=1.43.0,<1.44.0)", "mypy-boto3-databrew (>=1.43.0,<1.44.0)", "mypy-boto3-dataexchange (>=1.43.0,<1.44.0)", "mypy-boto3-datapipeline (>=1.43.0,<1.44.0)", "mypy-boto3-datasync (>=1.43.0,<1.44.0)", "mypy-boto3-datazone (>=1.43.0,<1.44.0)", "mypy-boto3-dax (>=1.43.0,<1.44.0)", "mypy-boto3-deadline (>=1.43.0,<1.44.0)", "mypy-boto3-detective (>=1.43.0,<1.44.0)", "mypy-boto3-devicefarm (>=1.43.0,<1.44.0)", "mypy-boto3-devops-agent (>=1.43.0,<1.44.0)", "mypy-boto3-devops-guru (>=1.43.0,<1.44.0)", "mypy-boto3-directconnect (>=1.43.0,<1.44.0)", "mypy-boto3-discovery (>=1.43.0,<1.44.0)", "mypy-boto3-dlm (>=1.43.0,<1.44.0)", "mypy-boto3-dms (>=1.43.0,<1.44.0)", "mypy-boto3-docdb (>=1.43.0,<1.44.0)", "mypy-boto3-docdb-elastic (>=1.43.0,<1.44.0)", "mypy-boto3-drs (>=1.43.0,<1.44.0)", "mypy-boto3-ds (>=1.43.0,<1.44.0)", "mypy-boto3-ds-data (>=1.43.0,<1.44.0)", "mypy-boto3-dsql (>=1.43.0,<1.44.0)", "mypy-boto3-dynamodb (>=1.43.0,<1.44.0)", "mypy-boto3-dynamodbstreams (>=1.43.0,<1.44.0)", "mypy-boto3-ebs (>=1.43.0,<1.44.0)", "mypy-boto3-ec2 (>=1.43.0,<1.44.0)", "mypy-boto3-ec2-instance-connect (>=1.43.0,<1.44.0)", "mypy-boto3-ecr (>=1.43.0,<1.44.0)", "mypy-boto3-ecr-public (>=1.43.0,<1.44.0)", "mypy-boto3-ecs (>=1.43.0,<1.44.0)", "mypy-boto3-efs (>=1.43.0,<1.44.0)", "mypy-boto3-eks (>=1.43.0,<1.44.0)", "mypy-boto3-eks-auth (>=1.43.0,<1.44.0)", "mypy-boto3-elasticache (>=1.43.0,<1.44.0)", "mypy-boto3-elasticbeanstalk (>=1.43.0,<1.44.0)", "mypy-boto3-elb (>=1.43.0,<1.44.0)", "mypy-boto3-elbv2 (>=1.43.0,<1.44.0)", "mypy-boto3-elementalinference (>=1.43.0,<1.44.0)", "mypy-boto3-emr (>=1.43.0,<1.44.0)", "mypy-boto3-emr-containers (>=1.43.0,<1.44.0)", "mypy-boto3-emr-serverless (>=1.43.0,<1.44.0)", "mypy-boto3-entityresolution (>=1.43.0,<1.44.0)", "mypy-boto3-es (>=1.43.0,<1.44.0)", "mypy-boto3-events (>=1.43.0,<1.44.0)", "mypy-boto3-evs (>=1.43.0,<1.44.0)", "mypy-boto3-finspace (>=1.43.0,<1.44.0)", "mypy-boto3-finspace-data (>=1.43.0,<1.44.0)", "mypy-boto3-firehose (>=1.43.0,<1.44.0)", "mypy-boto3-fis (>=1.43.0,<1.44.0)", "mypy-boto3-fms (>=1.43.0,<1.44.0)", "mypy-boto3-forecast (>=1.43.0,<1.44.0)", "mypy-boto3-forecastquery (>=1.43.0,<1.44.0)", "mypy-boto3-frauddetector (>=1.43.0,<1.44.0)", "mypy-boto3-freetier (>=1.43.0,<1.44.0)", "mypy-boto3-fsx (>=1.43.0,<1.44.0)", "mypy-boto3-gamelift (>=1.43.0,<1.44.0)", "mypy-boto3-gameliftstreams (>=1.43.0,<1.44.0)", "mypy-boto3-geo-maps (>=1.43.0,<1.44.0)", "mypy-boto3-geo-places (>=1.43.0,<1.44.0)", "mypy-boto3-geo-routes (>=1.43.0,<1.44.0)", "mypy-boto3-glacier (>=1.43.0,<1.44.0)", "mypy-boto3-globalaccelerator (>=1.43.0,<1.44.0)", "mypy-boto3-glue (>=1.43.0,<1.44.0)", "mypy-boto3-grafana (>=1.43.0,<1.44.0)", "mypy-boto3-greengrass (>=1.43.0,<1.44.0)", "mypy-boto3-greengrassv2 (>=1.43.0,<1.44.0)", "mypy-boto3-groundstation (>=1.43.0,<1.44.0)", "mypy-boto3-guardduty (>=1.43.0,<1.44.0)", "mypy-boto3-health (>=1.43.0,<1.44.0)", "mypy-boto3-healthlake (>=1.43.0,<1.44.0)", "mypy-boto3-iam (>=1.43.0,<1.44.0)", "mypy-boto3-identitystore (>=1.43.0,<1.44.0)", "mypy-boto3-imagebuilder (>=1.43.0,<1.44.0)", "mypy-boto3-importexport (>=1.43.0,<1.44.0)", "mypy-boto3-inspector (>=1.43.0,<1.44.0)", "mypy-boto3-inspector-scan (>=1.43.0,<1.44.0)", "mypy-boto3-inspector2 (>=1.43.0,<1.44.0)", "mypy-boto3-interconnect (>=1.43.0,<1.44.0)", "mypy-boto3-internetmonitor (>=1.43.0,<1.44.0)", "mypy-boto3-invoicing (>=1.43.0,<1.44.0)", "mypy-boto3-iot (>=1.43.0,<1.44.0)", "mypy-boto3-iot-data (>=1.43.0,<1.44.0)", "mypy-boto3-iot-jobs-data (>=1.43.0,<1.44.0)", "mypy-boto3-iot-managed-integrations (>=1.43.0,<1.44.0)", "mypy-boto3-iotdeviceadvisor (>=1.43.0,<1.44.0)", "mypy-boto3-iotevents (>=1.43.0,<1.44.0)", "mypy-boto3-iotevents-data (>=1.43.0,<1.44.0)", "mypy-boto3-iotfleetwise (>=1.43.0,<1.44.0)", "mypy-boto3-iotsecuretunneling (>=1.43.0,<1.44.0)", "mypy-boto3-iotsitewise (>=1.43.0,<1.44.0)", "mypy-boto3-iotthingsgraph (>=1.43.0,<1.44.0)", "mypy-boto3-iottwinmaker (>=1.43.0,<1.44.0)", "mypy-boto3-iotwireless (>=1.43.0,<1.44.0)", "mypy-boto3-ivs (>=1.43.0,<1.44.0)", "mypy-boto3-ivs-realtime (>=1.43.0,<1.44.0)", "mypy-boto3-ivschat (>=1.43.0,<1.44.0)", "mypy-boto3-kafka (>=1.43.0,<1.44.0)", "mypy-boto3-kafkaconnect (>=1.43.0,<1.44.0)", "mypy-boto3-kendra (>=1.43.0,<1.44.0)", "mypy-boto3-kendra-ranking (>=1.43.0,<1.44.0)", "mypy-boto3-keyspaces (>=1.43.0,<1.44.0)", "mypy-boto3-keyspacesstreams (>=1.43.0,<1.44.0)", "mypy-boto3-kinesis (>=1.43.0,<1.44.0)", "mypy-boto3-kinesis-video-archived-media (>=1.43.0,<1.44.0)", "mypy-boto3-kinesis-video-media (>=1.43.0,<1.44.0)", "mypy-boto3-kinesis-video-signaling (>=1.43.0,<1.44.0)", "mypy-boto3-kinesis-video-webrtc-storage (>=1.43.0,<1.44.0)", "mypy-boto3-kinesisanalytics (>=1.43.0,<1.44.0)", "mypy-boto3-kinesisanalyticsv2 (>=1.43.0,<1.44.0)", "mypy-boto3-kinesisvideo (>=1.43.0,<1.44.0)", "mypy-boto3-kms (>=1.43.0,<1.44.0)", "mypy-boto3-lakeformation (>=1.43.0,<1.44.0)", "mypy-boto3-lambda (>=1.43.0,<1.44.0)", "mypy-boto3-lambda-core (>=1.43.0,<1.44.0)", "mypy-boto3-lambda-microvms (>=1.43.0,<1.44.0)", "mypy-boto3-launch-wizard (>=1.43.0,<1.44.0)", "mypy-boto3-lex-models (>=1.43.0,<1.44.0)", "mypy-boto3-lex-runtime (>=1.43.0,<1.44.0)", "mypy-boto3-lexv2-models (>=1.43.0,<1.44.0)", "mypy-boto3-lexv2-runtime (>=1.43.0,<1.44.0)", "mypy-boto3-license-manager (>=1.43.0,<1.44.0)", "mypy-boto3-license-manager-linux-subscriptions (>=1.43.0,<1.44.0)", "mypy-boto3-license-manager-user-subscriptions (>=1.43.0,<1.44.0)", "mypy-boto3-lightsail (>=1.43.0,<1.44.0)", "mypy-boto3-location (>=1.43.0,<1.44.0)", "mypy-boto3-logs (>=1.43.0,<1.44.0)", "mypy-boto3-lookoutequipment (>=1.43.0,<1.44.0)", "mypy-boto3-m2 (>=1.43.0,<1.44.0)", "mypy-boto3-machinelearning (>=1.43.0,<1.44.0)", "mypy-boto3-macie2 (>=1.43.0,<1.44.0)", "mypy-boto3-mailmanager (>=1.43.0,<1.44.0)", "mypy-boto3-managedblockchain (>=1.43.0,<1.44.0)", "mypy-boto3-managedblockchain-query (>=1.43.0,<1.44.0)", "mypy-boto3-marketplace-agreement (>=1.43.0,<1.44.0)", "mypy-boto3-marketplace-catalog (>=1.43.0,<1.44.0)", "mypy-boto3-marketplace-deployment (>=1.43.0,<1.44.0)", "mypy-boto3-marketplace-discovery (>=1.43.0,<1.44.0)", "mypy-boto3-marketplace-entitlement (>=1.43.0,<1.44.0)", "mypy-boto3-marketplace-reporting (>=1.43.0,<1.44.0)", "mypy-boto3-marketplacecommerceanalytics (>=1.43.0,<1.44.0)", "mypy-boto3-mediaconnect (>=1.43.0,<1.44.0)", "mypy-boto3-mediaconvert (>=1.43.0,<1.44.0)", "mypy-boto3-medialive (>=1.43.0,<1.44.0)", "mypy-boto3-mediapackage (>=1.43.0,<1.44.0)", "mypy-boto3-mediapackage-vod (>=1.43.0,<1.44.0)", "mypy-boto3-mediapackagev2 (>=1.43.0,<1.44.0)", "mypy-boto3-mediastore (>=1.43.0,<1.44.0)", "mypy-boto3-mediastore-data (>=1.43.0,<1.44.0)", "mypy-boto3-mediatailor (>=1.43.0,<1.44.0)", "mypy-boto3-medical-imaging (>=1.43.0,<1.44.0)", "mypy-boto3-memorydb (>=1.43.0,<1.44.0)", "mypy-boto3-meteringmarketplace (>=1.43.0,<1.44.0)", "mypy-boto3-mgh (>=1.43.0,<1.44.0)", "mypy-boto3-mgn (>=1.43.0,<1.44.0)", "mypy-boto3-migration-hub-refactor-spaces (>=1.43.0,<1.44.0)", "mypy-boto3-migrationhub-config (>=1.43.0,<1.44.0)", "mypy-boto3-migrationhuborchestrator (>=1.43.0,<1.44.0)", "mypy-boto3-migrationhubstrategy (>=1.43.0,<1.44.0)", "mypy-boto3-mpa (>=1.43.0,<1.44.0)", "mypy-boto3-mq (>=1.43.0,<1.44.0)", "mypy-boto3-mturk (>=1.43.0,<1.44.0)", "mypy-boto3-mwaa (>=1.43.0,<1.44.0)", "mypy-boto3-mwaa-serverless (>=1.43.0,<1.44.0)", "mypy-boto3-neptune (>=1.43.0,<1.44.0)", "mypy-boto3-neptune-graph (>=1.43.0,<1.44.0)", "mypy-boto3-neptunedata (>=1.43.0,<1.44.0)", "mypy-boto3-network-firewall (>=1.43.0,<1.44.0)", "mypy-boto3-networkflowmonitor (>=1.43.0,<1.44.0)", "mypy-boto3-networkmanager (>=1.43.0,<1.44.0)", "mypy-boto3-networkmonitor (>=1.43.0,<1.44.0)", "mypy-boto3-notifications (>=1.43.0,<1.44.0)", "mypy-boto3-notificationscontacts (>=1.43.0,<1.44.0)", "mypy-boto3-nova-act (>=1.43.0,<1.44.0)", "mypy-boto3-oam (>=1.43.0,<1.44.0)", "mypy-boto3-observabilityadmin (>=1.43.0,<1.44.0)", "mypy-boto3-odb (>=1.43.0,<1.44.0)", "mypy-boto3-omics (>=1.43.0,<1.44.0)", "mypy-boto3-opensearch (>=1.43.0,<1.44.0)", "mypy-boto3-opensearchserverless (>=1.43.0,<1.44.0)", "mypy-boto3-organizations (>=1.43.0,<1.44.0)", "mypy-boto3-osis (>=1.43.0,<1.44.0)", "mypy-boto3-outposts (>=1.43.0,<1.44.0)", "mypy-boto3-panorama (>=1.43.0,<1.44.0)", "mypy-boto3-partnercentral-account (>=1.43.0,<1.44.0)", "mypy-boto3-partnercentral-benefits (>=1.43.0,<1.44.0)", "mypy-boto3-partnercentral-channel (>=1.43.0,<1.44.0)", "mypy-boto3-partnercentral-selling (>=1.43.0,<1.44.0)", "mypy-boto3-payment-cryptography (>=1.43.0,<1.44.0)", "mypy-boto3-payment-cryptography-data (>=1.43.0,<1.44.0)", "mypy-boto3-pca-connector-ad (>=1.43.0,<1.44.0)", "mypy-boto3-pca-connector-scep (>=1.43.0,<1.44.0)", "mypy-boto3-pcs (>=1.43.0,<1.44.0)", "mypy-boto3-personalize (>=1.43.0,<1.44.0)", "mypy-boto3-personalize-events (>=1.43.0,<1.44.0)", "mypy-boto3-personalize-runtime (>=1.43.0,<1.44.0)", "mypy-boto3-pi (>=1.43.0,<1.44.0)", "mypy-boto3-pinpoint (>=1.43.0,<1.44.0)", "mypy-boto3-pinpoint-email (>=1.43.0,<1.44.0)", "mypy-boto3-pinpoint-sms-voice (>=1.43.0,<1.44.0)", "mypy-boto3-pinpoint-sms-voice-v2 (>=1.43.0,<1.44.0)", "mypy-boto3-pipes (>=1.43.0,<1.44.0)", "mypy-boto3-polly (>=1.43.0,<1.44.0)", "mypy-boto3-pricing (>=1.43.0,<1.44.0)", "mypy-boto3-proton (>=1.43.0,<1.44.0)", "mypy-boto3-qapps (>=1.43.0,<1.44.0)", "mypy-boto3-qbusiness (>=1.43.0,<1.44.0)", "mypy-boto3-qconnect (>=1.43.0,<1.44.0)", "mypy-boto3-quicksight (>=1.43.0,<1.44.0)", "mypy-boto3-ram (>=1.43.0,<1.44.0)", "mypy-boto3-rbin (>=1.43.0,<1.44.0)", "mypy-boto3-rds (>=1.43.0,<1.44.0)", "mypy-boto3-rds-data (>=1.43.0,<1.44.0)", "mypy-boto3-redshift (>=1.43.0,<1.44.0)", "mypy-boto3-redshift-data (>=1.43.0,<1.44.0)", "mypy-boto3-redshift-serverless (>=1.43.0,<1.44.0)", "mypy-boto3-rekognition (>=1.43.0,<1.44.0)", "mypy-boto3-repostspace (>=1.43.0,<1.44.0)", "mypy-boto3-resiliencehub (>=1.43.0,<1.44.0)", "mypy-boto3-resiliencehubv2 (>=1.43.0,<1.44.0)", "mypy-boto3-resource-explorer-2 (>=1.43.0,<1.44.0)", "mypy-boto3-resource-groups (>=1.43.0,<1.44.0)", "mypy-boto3-resourcegroupstaggingapi (>=1.43.0,<1.44.0)", "mypy-boto3-rolesanywhere (>=1.43.0,<1.44.0)", "mypy-boto3-route53 (>=1.43.0,<1.44.0)", "mypy-boto3-route53-recovery-cluster (>=1.43.0,<1.44.0)", "mypy-boto3-route53-recovery-control-config (>=1.43.0,<1.44.0)", "mypy-boto3-route53-recovery-readiness (>=1.43.0,<1.44.0)", "mypy-boto3-route53domains (>=1.43.0,<1.44.0)", "mypy-boto3-route53globalresolver (>=1.43.0,<1.44.0)", "mypy-boto3-route53profiles (>=1.43.0,<1.44.0)", "mypy-boto3-route53resolver (>=1.43.0,<1.44.0)", "mypy-boto3-rtbfabric (>=1.43.0,<1.44.0)", "mypy-boto3-rum (>=1.43.0,<1.44.0)", "mypy-boto3-s3 (>=1.43.0,<1.44.0)", "mypy-boto3-s3control (>=1.43.0,<1.44.0)", "mypy-boto3-s3files (>=1.43.0,<1.44.0)", "mypy-boto3-s3outposts (>=1.43.0,<1.44.0)", "mypy-boto3-s3tables (>=1.43.0,<1.44.0)", "mypy-boto3-s3vectors (>=1.43.0,<1.44.0)", "mypy-boto3-sagemaker (>=1.43.0,<1.44.0)", "mypy-boto3-sagemaker-a2i-runtime (>=1.43.0,<1.44.0)", "mypy-boto3-sagemaker-edge (>=1.43.0,<1.44.0)", "mypy-boto3-sagemaker-featurestore-runtime (>=1.43.0,<1.44.0)", "mypy-boto3-sagemaker-geospatial (>=1.43.0,<1.44.0)", "mypy-boto3-sagemaker-metrics (>=1.43.0,<1.44.0)", "mypy-boto3-sagemaker-runtime (>=1.43.0,<1.44.0)", "mypy-boto3-sagemakerjobruntime (>=1.43.0,<1.44.0)", "mypy-boto3-savingsplans (>=1.43.0,<1.44.0)", "mypy-boto3-scheduler (>=1.43.0,<1.44.0)", "mypy-boto3-schemas (>=1.43.0,<1.44.0)", "mypy-boto3-sdb (>=1.43.0,<1.44.0)", "mypy-boto3-secretsmanager (>=1.43.0,<1.44.0)", "mypy-boto3-security-ir (>=1.43.0,<1.44.0)", "mypy-boto3-securityagent (>=1.43.0,<1.44.0)", "mypy-boto3-securityhub (>=1.43.0,<1.44.0)", "mypy-boto3-securitylake (>=1.43.0,<1.44.0)", "mypy-boto3-serverlessrepo (>=1.43.0,<1.44.0)", "mypy-boto3-service-quotas (>=1.43.0,<1.44.0)", "mypy-boto3-servicecatalog (>=1.43.0,<1.44.0)", "mypy-boto3-servicecatalog-appregistry (>=1.43.0,<1.44.0)", "mypy-boto3-servicediscovery (>=1.43.0,<1.44.0)", "mypy-boto3-ses (>=1.43.0,<1.44.0)", "mypy-boto3-sesv2 (>=1.43.0,<1.44.0)", "mypy-boto3-shield (>=1.43.0,<1.44.0)", "mypy-boto3-signer (>=1.43.0,<1.44.0)", "mypy-boto3-signer-data (>=1.43.0,<1.44.0)", "mypy-boto3-signin (>=1.43.0,<1.44.0)", "mypy-boto3-simpledbv2 (>=1.43.0,<1.44.0)", "mypy-boto3-simspaceweaver (>=1.43.0,<1.44.0)", "mypy-boto3-snow-device-management (>=1.43.0,<1.44.0)", "mypy-boto3-snowball (>=1.43.0,<1.44.0)", "mypy-boto3-sns (>=1.43.0,<1.44.0)", "mypy-boto3-socialmessaging (>=1.43.0,<1.44.0)", "mypy-boto3-sqs (>=1.43.0,<1.44.0)", "mypy-boto3-ssm (>=1.43.0,<1.44.0)", "mypy-boto3-ssm-contacts (>=1.43.0,<1.44.0)", "mypy-boto3-ssm-guiconnect (>=1.43.0,<1.44.0)", "mypy-boto3-ssm-incidents (>=1.43.0,<1.44.0)", "mypy-boto3-ssm-quicksetup (>=1.43.0,<1.44.0)", "mypy-boto3-ssm-sap (>=1.43.0,<1.44.0)", "mypy-boto3-sso (>=1.43.0,<1.44.0)", "mypy-boto3-sso-admin (>=1.43.0,<1.44.0)", "mypy-boto3-sso-oidc (>=1.43.0,<1.44.0)", "mypy-boto3-stepfunctions (>=1.43.0,<1.44.0)", "mypy-boto3-storagegateway (>=1.43.0,<1.44.0)", "mypy-boto3-sts (>=1.43.0,<1.44.0)", "mypy-boto3-supplychain (>=1.43.0,<1.44.0)", "mypy-boto3-support (>=1.43.0,<1.44.0)", "mypy-boto3-support-app (>=1.43.0,<1.44.0)", "mypy-boto3-sustainability (>=1.43.0,<1.44.0)", "mypy-boto3-swf (>=1.43.0,<1.44.0)", "mypy-boto3-synthetics (>=1.43.0,<1.44.0)", "mypy-boto3-taxsettings (>=1.43.0,<1.44.0)", "mypy-boto3-textract (>=1.43.0,<1.44.0)", "mypy-boto3-timestream-influxdb (>=1.43.0,<1.44.0)", "mypy-boto3-timestream-query (>=1.43.0,<1.44.0)", "mypy-boto3-timestream-write (>=1.43.0,<1.44.0)", "mypy-boto3-tnb (>=1.43.0,<1.44.0)", "mypy-boto3-transcribe (>=1.43.0,<1.44.0)", "mypy-boto3-transfer (>=1.43.0,<1.44.0)", "mypy-boto3-translate (>=1.43.0,<1.44.0)", "mypy-boto3-trustedadvisor (>=1.43.0,<1.44.0)", "mypy-boto3-uxc (>=1.43.0,<1.44.0)", "mypy-boto3-verifiedpermissions (>=1.43.0,<1.44.0)", "mypy-boto3-voice-id (>=1.43.0,<1.44.0)", "mypy-boto3-vpc-lattice (>=1.43.0,<1.44.0)", "mypy-boto3-waf (>=1.43.0,<1.44.0)", "mypy-boto3-waf-regional (>=1.43.0,<1.44.0)", "mypy-boto3-wafv2 (>=1.43.0,<1.44.0)", "mypy-boto3-wellarchitected (>=1.43.0,<1.44.0)", "mypy-boto3-wickr (>=1.43.0,<1.44.0)", "mypy-boto3-wisdom (>=1.43.0,<1.44.0)", "mypy-boto3-workdocs (>=1.43.0,<1.44.0)", "mypy-boto3-workmail (>=1.43.0,<1.44.0)", "mypy-boto3-workmailmessageflow (>=1.43.0,<1.44.0)", "mypy-boto3-workspaces (>=1.43.0,<1.44.0)", "mypy-boto3-workspaces-instances (>=1.43.0,<1.44.0)", "mypy-boto3-workspaces-thin-client (>=1.43.0,<1.44.0)", "mypy-boto3-workspaces-web (>=1.43.0,<1.44.0)", "mypy-boto3-xray (>=1.43.0,<1.44.0)"] amp = ["mypy-boto3-amp (>=1.43.0,<1.44.0)"] amplify = ["mypy-boto3-amplify (>=1.43.0,<1.44.0)"] amplifybackend = ["mypy-boto3-amplifybackend (>=1.43.0,<1.44.0)"] @@ -577,7 +598,7 @@ bedrock-data-automation-runtime = ["mypy-boto3-bedrock-data-automation-runtime ( bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.43.0,<1.44.0)"] billing = ["mypy-boto3-billing (>=1.43.0,<1.44.0)"] billingconductor = ["mypy-boto3-billingconductor (>=1.43.0,<1.44.0)"] -boto3 = ["boto3 (==1.43.24)"] +boto3 = ["boto3 (==1.43.36)"] braket = ["mypy-boto3-braket (>=1.43.0,<1.44.0)"] budgets = ["mypy-boto3-budgets (>=1.43.0,<1.44.0)"] ce = ["mypy-boto3-ce (>=1.43.0,<1.44.0)"] @@ -749,6 +770,8 @@ kinesisvideo = ["mypy-boto3-kinesisvideo (>=1.43.0,<1.44.0)"] kms = ["mypy-boto3-kms (>=1.43.0,<1.44.0)"] lakeformation = ["mypy-boto3-lakeformation (>=1.43.0,<1.44.0)"] lambda = ["mypy-boto3-lambda (>=1.43.0,<1.44.0)"] +lambda-core = ["mypy-boto3-lambda-core (>=1.43.0,<1.44.0)"] +lambda-microvms = ["mypy-boto3-lambda-microvms (>=1.43.0,<1.44.0)"] launch-wizard = ["mypy-boto3-launch-wizard (>=1.43.0,<1.44.0)"] lex-models = ["mypy-boto3-lex-models (>=1.43.0,<1.44.0)"] lex-runtime = ["mypy-boto3-lex-runtime (>=1.43.0,<1.44.0)"] @@ -1055,14 +1078,14 @@ typeguard = "2.13.3" [[package]] name = "cdklabs-generative-ai-cdk-constructs" -version = "0.1.317" +version = "0.1.318" description = "AWS Generative AI CDK Constructs is a library for well-architected generative AI patterns." optional = false python-versions = "~=3.10" groups = ["dev"] files = [ - {file = "cdklabs_generative_ai_cdk_constructs-0.1.317-py3-none-any.whl", hash = "sha256:86359377bbb56c946a460b0b7244ad5f9b8be3ab290201ad2e197fc7a6628dfb"}, - {file = "cdklabs_generative_ai_cdk_constructs-0.1.317.tar.gz", hash = "sha256:e04ed66168736f65cc4d13449a719dbb8d84c7ffb054d22a034865918315d157"}, + {file = "cdklabs_generative_ai_cdk_constructs-0.1.318-py3-none-any.whl", hash = "sha256:1a6717eeaf2b86cf1a60965eba52cf875355427c4de5ebded0b6d34c83182753"}, + {file = "cdklabs_generative_ai_cdk_constructs-0.1.318.tar.gz", hash = "sha256:bc685eeb633142abb7ffed4a90f029aec8e10ada8e63b99398b13f21b4d81f6f"}, ] [package.dependencies] @@ -1179,7 +1202,7 @@ files = [ {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, ] -markers = {main = "platform_python_implementation != \"PyPy\" and (extra == \"all\" or extra == \"datamasking\")", dev = "platform_python_implementation != \"PyPy\""} +markers = {main = "(extra == \"all\" or extra == \"datamasking\" or extra == \"valkey\") and (platform_python_implementation != \"PyPy\" or extra == \"valkey\")", dev = "platform_python_implementation != \"PyPy\""} [package.dependencies] pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} @@ -1399,118 +1422,133 @@ typeguard = "2.13.3" [[package]] name = "coverage" -version = "7.14.1" +version = "7.15.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "coverage-7.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3e3680291c4a1d0dadfa84a2c459576a4af5133abb617905714339a0c73138cf"}, - {file = "coverage-7.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5274669f37f2343635a347b91a60777621341ab3378e9c6ac9335eee704bddf"}, - {file = "coverage-7.14.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe5a5fec635799ef33428f1e5e61bafa45a92a96190ba731561ba558ccc214d"}, - {file = "coverage-7.14.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:62a9f70b52e0b5a95cfef4a5c5641b06983cadc5e538a3feeb5c00211f523ac2"}, - {file = "coverage-7.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c18ebc343e15be53049b3a2dce38fe82d58f37e20ab9094b3a39c0aa4f6bb47"}, - {file = "coverage-7.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b84ffdf877644e7096aa936991efeed873f7f3df57b9cd001312b7668ab08550"}, - {file = "coverage-7.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e854312c4103f2ad4c0dc023b69b77ebfd2c89db5f86c4c94dc2353f9a92167e"}, - {file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c643734307300234fafa36bf2a040a7235f8f177ea1fd6ec1423aea6fb7b929f"}, - {file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84ac9499e48700399a5dd0ea7085b5091961fec52c68d66b4ec0d3cf7f4441b1"}, - {file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7f02d09f70776579b926d889a4c9c235070a1f47c40458aeaca563fae5acfdb5"}, - {file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ce66d8e46da2bb5ee313a745cbd2e391d319176c1f7a9451bfcd3a2fb920859b"}, - {file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c912c259304cfb5ee584481cfb7ce1ff932b4d61e6c9140b8f19cb7b5ed82332"}, - {file = "coverage-7.14.1-cp310-cp310-win32.whl", hash = "sha256:1238cb94638e610e972c60dac68e813f868dc7d6e982535270558443058d9d59"}, - {file = "coverage-7.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:fc459e5d73be2d6332fcfe8dbf3d8994671fe33c700f4565988ecfa511547253"}, - {file = "coverage-7.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:478b5bcd63c2e1357c5c7e16c070690df7b07f676b1c114d7b93e533c664309f"}, - {file = "coverage-7.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a24a81f9715ee42ef59a316cc11611c98fe23920f7c81861315c9f3ff4a230f4"}, - {file = "coverage-7.14.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:196a13319ad88d6d8ef5ab489ec4f44ddde2143c0c7d5b27786f6c3ffd56a7e1"}, - {file = "coverage-7.14.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d452fd08b5c72c5167c93e6867b5c08500bd40f2a21e1e854a500550b6cc36f"}, - {file = "coverage-7.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23bf7fa51ac02e07fc7c96849b82946da47ae862dc8f86d183b2a4864fc38129"}, - {file = "coverage-7.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcaa50684dcaadfa599ac48f81103c756d791cfd85c97203d2217c593d48b860"}, - {file = "coverage-7.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ea1c034f95c9b056e856b794630b17f9fa3d57e4800ff1e503d3be0f9c9078c"}, - {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e057326434e441306226fbeb5d1aaf14a2637efe97ba668306635835f32ad7"}, - {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:59baf88468dbc8d63b1887afd92bda52e40bb1561696e5819670601403810cec"}, - {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d75f892b3ab73ba11cab5442cce7b3e168fd64162b16f0e1e0d09c508edef"}, - {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3a56abc20a472baf0304c455721bc601477440d28ecfde8a03dde79ede07e0df"}, - {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6a3cb83d1552c0cd1b4906655b6a33fd4a8473229633a901c6b73bf86914dee9"}, - {file = "coverage-7.14.1-cp311-cp311-win32.whl", hash = "sha256:10274a1fbeb8ec5d72966e17bb198a3104257aca4ac09d98667c5f8aca8c8548"}, - {file = "coverage-7.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:87ebdf787d4888e3f3f2d523eadc6e18c6d18c6d0eb173801a189641627fb37e"}, - {file = "coverage-7.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:dd34767fa19848d35659ffc0a75314f58c7af3f1cd87ec521e8292a1238398a3"}, - {file = "coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c"}, - {file = "coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c"}, - {file = "coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b"}, - {file = "coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6"}, - {file = "coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37"}, - {file = "coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad"}, - {file = "coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84"}, - {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54"}, - {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7"}, - {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9"}, - {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02"}, - {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a"}, - {file = "coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1"}, - {file = "coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e"}, - {file = "coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a"}, - {file = "coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793"}, - {file = "coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d"}, - {file = "coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247"}, - {file = "coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d"}, - {file = "coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b"}, - {file = "coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be"}, - {file = "coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43"}, - {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901"}, - {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff"}, - {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4"}, - {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d"}, - {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33"}, - {file = "coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c"}, - {file = "coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416"}, - {file = "coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42"}, - {file = "coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d"}, - {file = "coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5"}, - {file = "coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52"}, - {file = "coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a"}, - {file = "coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a"}, - {file = "coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2"}, - {file = "coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e"}, - {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d"}, - {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb"}, - {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d"}, - {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69"}, - {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54"}, - {file = "coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1"}, - {file = "coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce"}, - {file = "coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1"}, - {file = "coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee"}, - {file = "coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500"}, - {file = "coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906"}, - {file = "coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42"}, - {file = "coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8"}, - {file = "coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851"}, - {file = "coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034"}, - {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c"}, - {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36"}, - {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5"}, - {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4"}, - {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d"}, - {file = "coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee"}, - {file = "coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7"}, - {file = "coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343"}, - {file = "coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1"}, - {file = "coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b"}, - {file = "coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474"}, - {file = "coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86"}, - {file = "coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e"}, - {file = "coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65"}, - {file = "coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e"}, - {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8"}, - {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07"}, - {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de"}, - {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890"}, - {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd"}, - {file = "coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e"}, - {file = "coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c"}, - {file = "coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af"}, - {file = "coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2"}, - {file = "coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be"}, + {file = "coverage-7.15.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d0be6daac4cce6b8c8dc65886bae1b082ddbca4da8e5cbb5e15166acf253e264"}, + {file = "coverage-7.15.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b24e078eabcd6a9caa8b0713f9bc1eeb310bcc960a29d45a3b4fcd4b16d5b11d"}, + {file = "coverage-7.15.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe20cc8cf8821d4fe54f89106cbf06aa27f37b5bbe3535568065a81539b4150"}, + {file = "coverage-7.15.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:83cf06cdd687677742caff1a9134833b7a8b75f111519d2cb0e0ba1b9a851e15"}, + {file = "coverage-7.15.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fa4de68e2a752468ff14b4e15db7def689a71be759e826a31ccecbef69c5fd0"}, + {file = "coverage-7.15.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4dff9daa47d83120c3ec38ce921214242944a832aa04e903e50b5b7ebac8972d"}, + {file = "coverage-7.15.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a093fd37229918976f602aa07aa59e0973cde82186f220c8e197f721f5be0ce4"}, + {file = "coverage-7.15.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:317db01a2cb02552fd67e2b1cca77a4b528a2a277176c5e0bf2cecbb639d3f54"}, + {file = "coverage-7.15.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8ee3838dcb656602c3b51e16aed9bfb0822f8d8d6d1c5966d32ec8c104be8e20"}, + {file = "coverage-7.15.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:425920379052ff1fe465268f3361d35804a241bbdd5a1b592c8cb60df4c52325"}, + {file = "coverage-7.15.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:69bb2400abef928e365ea7d4d9925169ada78ed2295546780002d4b65de3df88"}, + {file = "coverage-7.15.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:81661f82d302484e3119e7c80c519c02fa9bcc2a6b339baf67d67bc89c580f04"}, + {file = "coverage-7.15.4-cp310-cp310-win32.whl", hash = "sha256:cb476b2e828ecb71cb6b6a928d23fd20a7ddb501188022dae1c37499149cc338"}, + {file = "coverage-7.15.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fc2130bf37df31852a8384f12601563a45a0024bccc6624f38355cba7a8d360"}, + {file = "coverage-7.15.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbac5abad70df71019988f83f26ac7092ff2642975def4429e98dc7585ef3490"}, + {file = "coverage-7.15.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:357a173465c7ce028d07a95cc2b63b5bf59f50ecdd5ad75c5cbb78ada984048e"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:21b803935e2efc3acebe9697197a294fccf5dc4e5382bd6369542ff7a7d2a1d7"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a2b580774a4786c1053157c0165e04476e03ff293993d7c148eee784a94bae6"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9464451c4efffe8d47ace5a540b10b0dc10e879066290f8600872b7f54a419d"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de602f34123c2f4af1c1869c6dbbbd60da6d5983bf01937367295d135cccbfce"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6879ded16a27f3eeca19b900c147e81616e7054db451471a611b2755ee5249f7"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:986be58c3ab54aae8d3496a6225eea74f760fdbe739b38bd442c7e8d133aa53b"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c6103639613fe6c1e989082948419bc77a2d26b6c825c99d7fad25f7d3d87afc"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3af93dddb5659276c63bc16ac6466ac2033a70ca816097bbc06345b8ccdf571"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b10075e5421d04265766a6d1dac809bbeb8a946fbb23c8f82c227409b2190719"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a67a9f78b2942d87ba8ce3059c642164d2aedd65337377fb52fe9803656bc5c7"}, + {file = "coverage-7.15.4-cp311-cp311-win32.whl", hash = "sha256:69484d1aca26e322e1c3ce03f09341e84524ababad2d7202161738d83cc9f82e"}, + {file = "coverage-7.15.4-cp311-cp311-win_amd64.whl", hash = "sha256:63fd6fcd1dd6e158f7eb78606e72933b3f6d01e7b747f99c6c12d764307a0fdc"}, + {file = "coverage-7.15.4-cp311-cp311-win_arm64.whl", hash = "sha256:ea82116c9893fa89e929b7f197ee5a1950a76e91cc5c85ba503fc02379d04890"}, + {file = "coverage-7.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4fedd1f7f428f9fe83b1ead5e7cc87a43427be31aadafbac3ac0636dc7abb22"}, + {file = "coverage-7.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37e2f0cdf58e2e1fed4e4d5a8f8786ae2f7eb80b478016876667dc4a01d60a97"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4"}, + {file = "coverage-7.15.4-cp312-cp312-win32.whl", hash = "sha256:5c3431b2161279b7db5c2a1aa58ae02e5cb8c3c42d93a5094be3f5537bd5b11b"}, + {file = "coverage-7.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd"}, + {file = "coverage-7.15.4-cp312-cp312-win_arm64.whl", hash = "sha256:67bc345491ab55b837277d76f5775d057e8c7f1ac44d890d8c2c82adde258c6f"}, + {file = "coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921"}, + {file = "coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff"}, + {file = "coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c"}, + {file = "coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4"}, + {file = "coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf"}, + {file = "coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f"}, + {file = "coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5"}, + {file = "coverage-7.15.4-cp314-cp314-win32.whl", hash = "sha256:7bbd7d6418e0dab31a206af5203bd43ae36edb8e7fba1940b055d3e9249290d7"}, + {file = "coverage-7.15.4-cp314-cp314-win_amd64.whl", hash = "sha256:f0204ed122758782970526057093f448051a39db9d810d4e344bb87a3546f425"}, + {file = "coverage-7.15.4-cp314-cp314-win_arm64.whl", hash = "sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8"}, + {file = "coverage-7.15.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7c922735321eef3f87c280a3d39afff6b646723a2880b862cda4ac7a093b8aa8"}, + {file = "coverage-7.15.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f41c17c4668a655ce96d090d8d5ffdc24ef64b5a02f9753884d08483e8a4a41a"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839"}, + {file = "coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85"}, + {file = "coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e"}, + {file = "coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e"}, + {file = "coverage-7.15.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9"}, + {file = "coverage-7.15.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:e101dbb4b9b72f0cddd8cdc8c9c5b47f456766f5e0ac82dbfb75e5c55409b78a"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753"}, + {file = "coverage-7.15.4-cp315-cp315-win32.whl", hash = "sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2"}, + {file = "coverage-7.15.4-cp315-cp315-win_amd64.whl", hash = "sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809"}, + {file = "coverage-7.15.4-cp315-cp315-win_arm64.whl", hash = "sha256:def597967dafc2e8d97c9097ea453c464e0bb8ed38f193a43070f10dc623bb6d"}, + {file = "coverage-7.15.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d"}, + {file = "coverage-7.15.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2413074a5ecbb61a01a7888fc72db0ca324d13588c5b38bc0dd8564cdcdfea26"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc"}, + {file = "coverage-7.15.4-cp315-cp315t-win32.whl", hash = "sha256:ebd5a6d8466ff30836572f3ba2cae8a5e8f85029b1c6d5e2ed338dc472a5166a"}, + {file = "coverage-7.15.4-cp315-cp315t-win_amd64.whl", hash = "sha256:288bde2a2d7ab6b6c2d7252fcde8b524387f2d970bdba9658fc6f8bbcaef0f9b"}, + {file = "coverage-7.15.4-cp315-cp315t-win_arm64.whl", hash = "sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278"}, + {file = "coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84"}, + {file = "coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00"}, ] [package.dependencies] @@ -1521,77 +1559,67 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" -version = "46.0.7" +version = "50.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = "!=3.9.0,!=3.9.1,>=3.8" +python-versions = "!=3.9.0,!=3.9.1,>=3.9" groups = ["main", "dev"] files = [ - {file = "cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b"}, - {file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85"}, - {file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e"}, - {file = "cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457"}, - {file = "cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b"}, - {file = "cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2"}, - {file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e"}, - {file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee"}, - {file = "cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298"}, - {file = "cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb"}, - {file = "cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0"}, - {file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85"}, - {file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e"}, - {file = "cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246"}, - {file = "cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4"}, - {file = "cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5"}, + {file = "cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f"}, + {file = "cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105"}, + {file = "cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef"}, + {file = "cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30"}, + {file = "cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c"}, + {file = "cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c"}, + {file = "cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95"}, + {file = "cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269"}, + {file = "cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47"}, + {file = "cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9"}, + {file = "cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7"}, + {file = "cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9"}, + {file = "cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9"}, ] markers = {main = "extra == \"all\" or extra == \"datamasking\""} [package.dependencies] -cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} +cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\""} typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] -docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox[uv] (>=2024.4.15)"] -pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] -sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==46.0.7)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] -test-randomorder = ["pytest-randomly"] [[package]] name = "datadog" @@ -1882,15 +1910,15 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "fastjsonschema" -version = "2.21.2" +version = "2.22.1" description = "Fastest Python implementation of JSON schema" optional = true -python-versions = "*" +python-versions = ">=3.10" groups = ["main"] markers = "extra == \"all\" or extra == \"validation\"" files = [ - {file = "fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463"}, - {file = "fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de"}, + {file = "fastjsonschema-2.22.1-py3-none-any.whl", hash = "sha256:cf377ff5c9a6f4f3125fb35f75a2c5767bd824ffbcf62c209a93cd48d1453999"}, + {file = "fastjsonschema-2.22.1.tar.gz", hash = "sha256:0b83d1ce8d7845b959dcb20e1a5c3c8883b6541d9c52ab02cce5166b75ec805f"}, ] [package.extras] @@ -1898,14 +1926,14 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "filelock" -version = "3.29.0" +version = "3.29.7" description = "A platform independent file lock." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258"}, - {file = "filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90"}, + {file = "filelock-3.29.7-py3-none-any.whl", hash = "sha256:987db6f789a3a2a59f55081801b2b3697cb97e2a736b5f1a9e99b559285fbc51"}, + {file = "filelock-3.29.7.tar.gz", hash = "sha256:5b481979797ae69e72f0b389d89a80bdd585c260c5b3f1fb9c0a5ba9bb3f195d"}, ] [[package]] @@ -1943,14 +1971,14 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.50" +version = "3.1.58" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9"}, - {file = "gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc"}, + {file = "gitpython-3.1.58-py3-none-any.whl", hash = "sha256:d331e722577f0fd7fc1f857419b3ecc07af66282b933d2a4d95f84a042fdd50f"}, + {file = "gitpython-3.1.58.tar.gz", hash = "sha256:621416df10ef3fd0e19fabf9172ddeed0fa704d353d04f194eec56a625a95b22"}, ] [package.dependencies] @@ -1958,42 +1986,7 @@ gitdb = ">=4.0.1,<5" [package.extras] doc = ["sphinx (>=7.4.7,<8)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] -test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy (==1.18.2) ; python_version >= \"3.9\"", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] - -[[package]] -name = "griffe" -version = "2.0.0" -description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "griffe-2.0.0-py3-none-any.whl", hash = "sha256:5418081135a391c3e6e757a7f3f156f1a1a746cc7b4023868ff7d5e2f9a980aa"}, - {file = "griffe-2.0.0.tar.gz", hash = "sha256:c68979cd8395422083a51ea7cf02f9c119d889646d99b7b656ee43725de1b80f"}, -] - -[package.dependencies] -griffecli = "2.0.0" -griffelib = "2.0.0" - -[package.extras] -pypi = ["griffelib[pypi] (==2.0.0)"] - -[[package]] -name = "griffecli" -version = "2.0.0" -description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "griffecli-2.0.0-py3-none-any.whl", hash = "sha256:9f7cd9ee9b21d55e91689358978d2385ae65c22f307a63fb3269acf3f21e643d"}, - {file = "griffecli-2.0.0.tar.gz", hash = "sha256:312fa5ebb4ce6afc786356e2d0ce85b06c1c20d45abc42d74f0cda65e159f6ef"}, -] - -[package.dependencies] -colorama = ">=0.4" -griffelib = "2.0.0" +test = ["basedpyright (==1.39.9) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy (==1.18.2) ; python_version >= \"3.9\"", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] [[package]] name = "griffelib" @@ -2113,7 +2106,7 @@ files = [ {file = "idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"}, {file = "idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"}, ] -markers = {main = "extra == \"datadog\" or extra == \"valkey\""} +markers = {main = "extra == \"valkey\" or extra == \"datadog\""} [package.extras] all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] @@ -2307,14 +2300,14 @@ files = [ [[package]] name = "jsii" -version = "1.135.0" +version = "1.140.0" description = "Python client for jsii runtime" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "jsii-1.135.0-py3-none-any.whl", hash = "sha256:bfab0cc3f99bfbc13683af2afb8d62522af867fb826fc825ab6b27fe636bb087"}, - {file = "jsii-1.135.0.tar.gz", hash = "sha256:3d860404e9e350beae0bdae48b473173653bd2be3a901c620feb656b690f9c2f"}, + {file = "jsii-1.140.0-py3-none-any.whl", hash = "sha256:aa24f27d15d8d7db1ff32a9c56074bc87e728b30330ead97fb149419a3721a50"}, + {file = "jsii-1.140.0.tar.gz", hash = "sha256:454c874d963a862f6b90bc013c45a720e51292659f57249d9cae7cb2a189ec0e"}, ] [package.dependencies] @@ -2404,103 +2397,150 @@ referencing = ">=0.31.0" [[package]] name = "librt" -version = "0.11.0" +version = "0.15.0" description = "Mypyc runtime library" optional = false python-versions = ">=3.9" groups = ["dev"] markers = "platform_python_implementation != \"PyPy\"" files = [ - {file = "librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f"}, - {file = "librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45"}, - {file = "librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c"}, - {file = "librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33"}, - {file = "librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884"}, - {file = "librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280"}, - {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c"}, - {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb"}, - {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783"}, - {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0"}, - {file = "librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89"}, - {file = "librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4"}, - {file = "librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29"}, - {file = "librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9"}, - {file = "librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5"}, - {file = "librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b"}, - {file = "librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89"}, - {file = "librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc"}, - {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5"}, - {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7"}, - {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d"}, - {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412"}, - {file = "librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d"}, - {file = "librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73"}, - {file = "librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c"}, - {file = "librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46"}, - {file = "librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3"}, - {file = "librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67"}, - {file = "librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a"}, - {file = "librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a"}, - {file = "librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f"}, - {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b"}, - {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766"}, - {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d"}, - {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8"}, - {file = "librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a"}, - {file = "librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9"}, - {file = "librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c"}, - {file = "librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894"}, - {file = "librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c"}, - {file = "librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea"}, - {file = "librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230"}, - {file = "librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2"}, - {file = "librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3"}, - {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21"}, - {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930"}, - {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be"}, - {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e"}, - {file = "librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e"}, - {file = "librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47"}, - {file = "librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44"}, - {file = "librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd"}, - {file = "librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4"}, - {file = "librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8"}, - {file = "librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b"}, - {file = "librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175"}, - {file = "librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03"}, - {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c"}, - {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3"}, - {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96"}, - {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe"}, - {file = "librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f"}, - {file = "librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7"}, - {file = "librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1"}, - {file = "librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72"}, - {file = "librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa"}, - {file = "librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548"}, - {file = "librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2"}, - {file = "librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f"}, - {file = "librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51"}, - {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2"}, - {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085"}, - {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3"}, - {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd"}, - {file = "librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8"}, - {file = "librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c"}, - {file = "librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253"}, - {file = "librt-0.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6bd72d903911d995ab666dbd1871f8b1e80925a699af8063fbf50053329fb05f"}, - {file = "librt-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ef69ac715f3cd8e5cd252cb2aebfa72c015492aacc339d5d7bf8fef3c62c677"}, - {file = "librt-0.11.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:624a40c4a4ad7773315c287276cd024509b2c66ff5904f504bfc08d2c70293ab"}, - {file = "librt-0.11.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:41dc19fe150b69716c8ece4f76773a9e8813fe3e35e032a58b4d46423fb8d7c0"}, - {file = "librt-0.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e8bd98ea9c47ae90b319a087ab28dac493f1ffbc1ecd1f28fcdbf3b7e1108d1"}, - {file = "librt-0.11.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84308fc49423ce6475d1c5d1985cd69a8ca9f0325fc7d5f81bb690a3f3625d4e"}, - {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ff0fbaf5f44a21beeb0110f2ab64f45135a9536a834b79c0d1ef018f2786bbfa"}, - {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9c028a9442a18e266955d364ce42259136e79a7ba14d773e0d778d5f70cd56f1"}, - {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9f1692105a02bcf853f355032a5fdc5494358ef83d8fd22d16de375c85cec3f5"}, - {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7a80a71e1fda83cc752a9141e87aae7fef279538597564d670e9ce513f286192"}, - {file = "librt-0.11.0-cp39-cp39-win32.whl", hash = "sha256:140695816ddf3c86eb972981a26f35efd871c44b0c3aed44c8cd01749386617f"}, - {file = "librt-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:92f7ff819c197fc30473190a12c2856f325ac90aabfccbeb2072d28cc2e234e3"}, - {file = "librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1"}, + {file = "librt-0.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1a49adf16a7c9d9646816c2946135527197b6fcf4347c7b8b761cf1bfbf4489"}, + {file = "librt-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81a398f45b45a59200e13cd5ad1ae1d3f44334de98b148331afe2cdfee701c52"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4eafbaff06b9563f8b1c850621ce51605de05208e09d4d71ce490bc972b7b9e8"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b0411b4066db926b80258c60dcb0e6db4c9cee312eab45b7e8866b17ddf9ada1"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:febb1ce6cac545a54e6b769982824e955a700fdd9fbf3a08a3d82c990968b57d"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b230acc1c3bfe2d6f2627ba2b95dc92e58aa494600e9722d0e6ccbc931e59702"}, + {file = "librt-0.15.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6da110e5f314c19ab8478464d02ae18808ae73d522c15260fa4918acdcd64da9"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:eab9208b00ca55bf75983ec99f7bf13acc746a36102e98953addaad7f7ea1e1b"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c013cd3a1721e69e14380ada97eaa4b7b0cdf1c6b96fa765d4ea47c875088db"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:567b1c430f8bd560e689421468278ac5941bab4a05303b5d95b6ae10db03f451"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:29c4cab9df457b19672c39be7f384ebb2bc925c4e2684b8780c222b43eb36389"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bccbd8e5b0bffb7106cf18eb1baa3d7194b1cebb3b4b1cdbd4bdb19382a6ee6c"}, + {file = "librt-0.15.0-cp310-cp310-win32.whl", hash = "sha256:8ae493ed5f659a7761c43d42f183db514536073ded9bcf671d2d1df47e29a07e"}, + {file = "librt-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc25fb356d0c7810bb49ff3df908ad1fda6995d660ab099ded69244ed7ab6053"}, + {file = "librt-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22"}, + {file = "librt-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a"}, + {file = "librt-0.15.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0"}, + {file = "librt-0.15.0-cp311-cp311-win32.whl", hash = "sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb"}, + {file = "librt-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db"}, + {file = "librt-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56"}, + {file = "librt-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e"}, + {file = "librt-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa"}, + {file = "librt-0.15.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2"}, + {file = "librt-0.15.0-cp312-cp312-win32.whl", hash = "sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1"}, + {file = "librt-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022"}, + {file = "librt-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570"}, + {file = "librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26"}, + {file = "librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2"}, + {file = "librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b"}, + {file = "librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab"}, + {file = "librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890"}, + {file = "librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8"}, + {file = "librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad"}, + {file = "librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993"}, + {file = "librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879"}, + {file = "librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65"}, + {file = "librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622"}, + {file = "librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15"}, + {file = "librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28"}, + {file = "librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95"}, + {file = "librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714"}, + {file = "librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc"}, + {file = "librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf"}, + {file = "librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915"}, + {file = "librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605"}, + {file = "librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca"}, + {file = "librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0"}, + {file = "librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d"}, + {file = "librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374"}, + {file = "librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9"}, + {file = "librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8"}, + {file = "librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b"}, + {file = "librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54"}, + {file = "librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b"}, + {file = "librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162"}, + {file = "librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1"}, + {file = "librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc"}, + {file = "librt-0.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0e2d0c0acf5b0ada7d045912b7cf787c21315c95b38b1fa939ef72d45d366b3d"}, + {file = "librt-0.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9ca190fe9edc0eb08eec558a509a16d28d91c35667b8f043cba40ed5e77a959"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80811e1c42386ea95c6fb30571d3250ad43d7863f883f787f70517f441150e59"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:88c2a17815c266e6d8180204ff62cb739ab869ada4a746d4c505331526ac58f1"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a5fa8f1f916988d0bf1afea005bda37f56ac41a18016e813ccf0097a8d460ca4"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:355e3a4c725225a14262004fc1872a552b9d3634b4f791a0dfc80804aafbfd55"}, + {file = "librt-0.15.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc1ed11c4ad0b91af24def2050f2840ea4567828e3dd058fbe608d982f6e5465"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1f4ef2e71db33df4309167ed7f1520c4fae5e611226e159fa9cf33f93e6ddb3d"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1a1a8cd430c7dd0c083f455cb1b328d7fc682b05c31b940906f7845bdff80881"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:04d5387b908676c0b8d5d2f5fb58373b4ea382d81f7a6f0fab8ea2a462bb4738"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:1172c6ad2a88b646e7fe3b480e3fac4ab4418b3443fd8a4061fdd531e0622fc7"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52e8db01f603f5da0ca30987479acff98769382efc8e142fa3962395dcf3ffdb"}, + {file = "librt-0.15.0-cp39-cp39-win32.whl", hash = "sha256:e4c911f15a1652ca94ae9f1abd92e74cbb1b3597d2d92fdd556202f94e8cd455"}, + {file = "librt-0.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:68242379c9b65a582b6e97318a1e9fbd6d445e58954f2d437991c4804ab11578"}, + {file = "librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162"}, ] [[package]] @@ -2816,14 +2856,14 @@ mdformat = ">=0.7.21" [[package]] name = "mkdocs-material" -version = "9.7.6" +version = "9.7.7" description = "Documentation that simply works" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba"}, - {file = "mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69"}, + {file = "mkdocs_material-9.7.7-py3-none-any.whl", hash = "sha256:8ea9bb1737a5b524a5f9dcf2e1b4ebda8274ae3008aa7845720a97083bef708f"}, + {file = "mkdocs_material-9.7.7.tar.gz", hash = "sha256:c0649c065b1b0512d60aad8c10f947f8e455284475239b364b610f2deb4d0855"}, ] [package.dependencies] @@ -2883,18 +2923,18 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] [[package]] name = "mkdocstrings-python" -version = "1.19.0" +version = "2.0.5" description = "A Python handler for mkdocstrings." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "mkdocstrings_python-1.19.0-py3-none-any.whl", hash = "sha256:395c1032af8f005234170575cc0c5d4d20980846623b623b35594281be4a3059"}, - {file = "mkdocstrings_python-1.19.0.tar.gz", hash = "sha256:917aac66cf121243c11db5b89f66b0ded6c53ec0de5318ff5e22424eb2f2e57c"}, + {file = "mkdocstrings_python-2.0.5-py3-none-any.whl", hash = "sha256:30c837bbff016549f659fcba6539ac351303f0fd7e713c89a040611072236e9d"}, + {file = "mkdocstrings_python-2.0.5.tar.gz", hash = "sha256:3a4d92556ad39637e88af94a5374213af9a8e3040c3824ceaed04b486c017594"}, ] [package.dependencies] -griffe = ">=1.13" +griffelib = ">=2.0" mkdocs-autorefs = ">=1.4" mkdocstrings = ">=0.30" typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} @@ -2948,61 +2988,69 @@ dill = ">=0.4.1" [[package]] name = "mypy" -version = "2.1.0" +version = "2.3.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a6beb180257a805961aea9ec591bbd0bd17f1e18d35b8456d57aee5bedfedc"}, - {file = "mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ef78c1d306bbf9a8a12f526c44902c9c28dffd6c52c52bf6a72641ce18d3849"}, - {file = "mypy-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c209a90853081ff01d01ee895cafe10f7db1474e0d95beaeef0f6c1db9119bbd"}, - {file = "mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47cebf61abde7c088a4e27718a8b13a81655686b2e9c251f5c0915a802248166"}, - {file = "mypy-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d57a90ae5e872138a425ec328edbc9b235d1934c4377881a33ec05b341acc9a8"}, - {file = "mypy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aea7f7a8a55b459c34275fc468ada6ca7c173a5e43a68f5dbe588a563d8a06b8"}, - {file = "mypy-2.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c989640253f0d76843e9c6c1bbf4bd48c5e85ada61bde4beb37cb3eca035685e"}, - {file = "mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41"}, - {file = "mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca"}, - {file = "mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538"}, - {file = "mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398"}, - {file = "mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563"}, - {file = "mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389"}, - {file = "mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666"}, - {file = "mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af"}, - {file = "mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6"}, - {file = "mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211"}, - {file = "mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b"}, - {file = "mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22"}, - {file = "mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b"}, - {file = "mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8"}, - {file = "mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5"}, - {file = "mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e"}, - {file = "mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e"}, - {file = "mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285"}, - {file = "mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5"}, - {file = "mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65"}, - {file = "mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d"}, - {file = "mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2"}, - {file = "mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f"}, - {file = "mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4"}, - {file = "mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef"}, - {file = "mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135"}, - {file = "mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21"}, - {file = "mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57"}, - {file = "mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e"}, - {file = "mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780"}, - {file = "mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd"}, - {file = "mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08"}, - {file = "mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081"}, - {file = "mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7"}, - {file = "mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6"}, - {file = "mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289"}, - {file = "mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633"}, + {file = "mypy-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:57a936373fc690c43a8cd7e7e12a35148e4ec5aa7698ad7fc0a9f918bdc5be41"}, + {file = "mypy-2.3.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d00d769056bde2f4e69c175071eba45cfb44fa1ed92bdfbfe64a93e0543b0cf0"}, + {file = "mypy-2.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2166b29228835e1f88ff411e96639e6ca3c7fdde84b62ec211f70f86b4051167"}, + {file = "mypy-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:83d36c2924df7426333abe7faf4724a7e1aab0d9fd41625e81b4683034b80c13"}, + {file = "mypy-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:f12fdb70459d0060dea40b29e52163a961b156106d68d57882a6a9f648983a53"}, + {file = "mypy-2.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:e099200a1b1b1223a4951f0a90cbff1b8c91b250ba599dab1f7217a628144d90"}, + {file = "mypy-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94f04929f1c44c35fb0061e912087edaf504acede963a4a7d00680bd089d8531"}, + {file = "mypy-2.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5d716048611e85ca9eefb2e1baa5d73ede389b5820ded260ea27c757d667af8"}, + {file = "mypy-2.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b091a455111214cb5c9d54a57b9618e9a49f9fe2a42e4e1ac86e9d104ed96ce8"}, + {file = "mypy-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df12e20c9efd614738c71b390007ecd0181125afc4ccafca04d78a1d2eed2c01"}, + {file = "mypy-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:52eaf3a155f35cf80b40220288c861eb45f14a2340c1f6cbfbdb0feff32879d1"}, + {file = "mypy-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9b4eacbee8a69836c06eff6d0dd4e134a07c2b047755b30c08625fe214f322c6"}, + {file = "mypy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a32bbbb940af990d3be0b8af321c7b6815bb1b3b48142fe7459b9cc5f58959ff"}, + {file = "mypy-2.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff715e45b2231a8e85de1d163d1b42791e4d7aab8f5145f85fee1b710b735aff"}, + {file = "mypy-2.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:858fc57d3d91fa728e33e7ad71def60fc6272694607b306cd3292db53ae39080"}, + {file = "mypy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:851833db876e7b650f93719c74b7879a08e338979c96054fdfc3bfd90a486355"}, + {file = "mypy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c5095a327483591c94e0c8d3ef9e50d4ab1369b541eae007c1f23bc2a41f6bb"}, + {file = "mypy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfe022634a2a195406bd469e888d2eaf193b02ba7e607391cd7640374aaae3b"}, + {file = "mypy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d"}, + {file = "mypy-2.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb"}, + {file = "mypy-2.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226"}, + {file = "mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74"}, + {file = "mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6"}, + {file = "mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac"}, + {file = "mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d"}, + {file = "mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3"}, + {file = "mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f"}, + {file = "mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82"}, + {file = "mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9"}, + {file = "mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d"}, + {file = "mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595"}, + {file = "mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2"}, + {file = "mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc"}, + {file = "mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045"}, + {file = "mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0"}, + {file = "mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63"}, + {file = "mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4"}, + {file = "mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57"}, + {file = "mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b"}, + {file = "mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561"}, + {file = "mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133"}, + {file = "mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9"}, + {file = "mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3"}, + {file = "mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523"}, + {file = "mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306"}, + {file = "mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021"}, + {file = "mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e"}, + {file = "mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc"}, + {file = "mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4"}, + {file = "mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29"}, + {file = "mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb"}, + {file = "mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419"}, ] [package.dependencies] -ast-serialize = ">=0.3.0,<1.0.0" -librt = {version = ">=0.11.0", markers = "platform_python_implementation != \"PyPy\""} +ast-serialize = ">=0.6.0,<1.0.0" +librt = {version = ">=0.13.0", markers = "platform_python_implementation != \"PyPy\""} mypy_extensions = ">=1.0.0" pathspec = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} @@ -3308,6 +3356,22 @@ files = [ importlib-metadata = ">=6.0,<8.8.0" typing-extensions = ">=4.5.0" +[[package]] +name = "outcome" +version = "1.3.0.post0" +description = "Capture the outcome of Python function calls." +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"valkey\"" +files = [ + {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, + {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, +] + +[package.dependencies] +attrs = ">=19.2.0" + [[package]] name = "packaging" version = "26.0" @@ -3384,20 +3448,20 @@ testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "protobuf" -version = "7.34.1" +version = "7.35.1" description = "" optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7"}, - {file = "protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b"}, - {file = "protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a"}, - {file = "protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4"}, - {file = "protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a"}, - {file = "protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c"}, - {file = "protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11"}, - {file = "protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280"}, + {file = "protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6"}, + {file = "protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799"}, + {file = "protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4"}, + {file = "protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4"}, + {file = "protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30"}, + {file = "protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87"}, + {file = "protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9"}, + {file = "protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a"}, ] markers = {main = "extra == \"kafka-consumer-protobuf\" or extra == \"valkey\""} @@ -3414,15 +3478,15 @@ files = [ ] [[package]] -name = "py-cpuinfo" -version = "9.0.0" +name = "py-cpuinfo2" +version = "10.1.1" description = "Get CPU info with pure Python" optional = false -python-versions = "*" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"}, - {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, + {file = "py_cpuinfo2-10.1.1-py3-none-any.whl", hash = "sha256:adc53396bfb206e6498d078ec2ab407f85799ecd819584ac36a8f80a2d4d762d"}, + {file = "py_cpuinfo2-10.1.1.tar.gz", hash = "sha256:7861133863663f16e06eca63b12904ef100b5760415e92372dac0162799a4771"}, ] [[package]] @@ -3436,7 +3500,7 @@ files = [ {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, ] -markers = {main = "platform_python_implementation != \"PyPy\" and (extra == \"all\" or extra == \"datamasking\") and implementation_name != \"PyPy\"", dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""} +markers = {main = "(extra == \"all\" or extra == \"datamasking\" or extra == \"valkey\") and (platform_python_implementation != \"PyPy\" or extra == \"valkey\") and implementation_name != \"PyPy\"", dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""} [[package]] name = "pydantic" @@ -3597,15 +3661,15 @@ typing-extensions = ">=4.14.1" [[package]] name = "pydantic-settings" -version = "2.14.1" +version = "2.14.2" description = "Settings management using Pydantic" optional = true python-versions = ">=3.10" groups = ["main"] markers = "extra == \"all\"" files = [ - {file = "pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de"}, - {file = "pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa"}, + {file = "pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440"}, + {file = "pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f"}, ] [package.dependencies] @@ -3637,14 +3701,14 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymdown-extensions" -version = "10.21.3" +version = "11.0.1" description = "Extension pack for Python Markdown." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6"}, - {file = "pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354"}, + {file = "pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2"}, + {file = "pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0"}, ] [package.dependencies] @@ -3656,14 +3720,14 @@ extra = ["pygments (>=2.19.1)"] [[package]] name = "pytest" -version = "9.1.0" +version = "9.1.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32"}, - {file = "pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c"}, + {file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"}, + {file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"}, ] [package.dependencies] @@ -3701,18 +3765,18 @@ testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-benchmark" -version = "5.2.3" +version = "5.3.0" description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803"}, - {file = "pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779"}, + {file = "pytest_benchmark-5.3.0-py3-none-any.whl", hash = "sha256:920ab1dfcffa718d49aa15ba144c7e357bda59216a0dc308016cc1c7236f719d"}, + {file = "pytest_benchmark-5.3.0.tar.gz", hash = "sha256:358444d4e89be901ee2b6404fb043ac3d7684002ad7f3563cc153fca6339c965"}, ] [package.dependencies] -py-cpuinfo = "*" +py-cpuinfo2 = ">=10.1" pytest = ">=8.1" [package.extras] @@ -3760,14 +3824,14 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "pytest-socket" -version = "0.8.0" +version = "0.8.1" description = "Pytest Plugin to disable socket calls during tests" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pytest_socket-0.8.0-py3-none-any.whl", hash = "sha256:81821ba59f07d7600fe2b551d8714f40b068bd46e8b6704c48664e9d60cdacb8"}, - {file = "pytest_socket-0.8.0.tar.gz", hash = "sha256:af9bb5f487da72be63573a6194cfac033b6c7a1c1561e150521105970f9e99f2"}, + {file = "pytest_socket-0.8.1-py3-none-any.whl", hash = "sha256:f9846bed1dcd96eed459e5e14795bbaf96715cf4e827891fe70773817ecb8ed4"}, + {file = "pytest_socket-0.8.1.tar.gz", hash = "sha256:2f57787914ad2e1308d09ce141b95c3e55741fbb4fb7b7556593a6b063e0c9c7"}, ] [package.dependencies] @@ -3996,14 +4060,14 @@ toml = ["tomli (>=2.0.1)"] [[package]] name = "redis" -version = "7.4.0" +version = "8.1.0" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "redis-7.4.0-py3-none-any.whl", hash = "sha256:a9c74a5c893a5ef8455a5adb793a31bb70feb821c86eccb62eebef5a19c429ec"}, - {file = "redis-7.4.0.tar.gz", hash = "sha256:64a6ea7bf567ad43c964d2c30d82853f8df927c5c9017766c55a1d1ed95d18ad"}, + {file = "redis-8.1.0-py3-none-any.whl", hash = "sha256:a4fe1aac3d3b3cc791d4b3d5931c5a956045dc951ee74d1c913ee3ac4d2ee9fb"}, + {file = "redis-8.1.0.tar.gz", hash = "sha256:6e1a19beef9225c83efd689c7e6b7da2d5215b1f42cd13b7fc3714d0a09c7b25"}, ] markers = {main = "extra == \"redis\""} @@ -4013,7 +4077,7 @@ async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\ [package.extras] circuit-breaker = ["pybreaker (>=1.4.0)"] hiredis = ["hiredis (>=3.2.0)"] -jwt = ["pyjwt (>=2.9.0)"] +jwt = ["pyjwt (>=2.13.0)"] ocsp = ["cryptography (>=36.0.1)", "pyopenssl (>=20.0.1)", "requests (>=2.31.0)"] otel = ["opentelemetry-api (>=1.39.1)", "opentelemetry-exporter-otlp-proto-http (>=1.39.1)", "opentelemetry-sdk (>=1.39.1)"] xxhash = ["xxhash (>=3.6.0,<3.7.0)"] @@ -4161,14 +4225,14 @@ files = [ [[package]] name = "requests" -version = "2.33.1" +version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a"}, - {file = "requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] markers = {main = "extra == \"datadog\""} @@ -4342,30 +4406,30 @@ files = [ [[package]] name = "ruff" -version = "0.15.17" +version = "0.16.4" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.15.17-py3-none-linux_armv6l.whl", hash = "sha256:d9feddb927fc68bd295f5eebc587a7e42cfaf9b65f60ca4a2386febff575da8f"}, - {file = "ruff-0.15.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:25805a226d741c47d274a35ad5c10a7dde175fcddfa511d7cf3da0a21eb3eab7"}, - {file = "ruff-0.15.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f6ad73b14c2d18a3bf8ad7cb6974294d7f613a7898604826058e6ac64918ef4d"}, - {file = "ruff-0.15.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ba0c1e4f95bcb3869d0d30cbd5917071ef2e28665abfec970cdab0492c713ed"}, - {file = "ruff-0.15.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81647960f10bff57d2e51cadd0c3950fe598400c852863a038720ef5b8cca91e"}, - {file = "ruff-0.15.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e01a84ddbc8c16c23055ba3924476850f1bbc1917cebbb9376665a63e74260d"}, - {file = "ruff-0.15.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fe9f653152f8f294f9f7e03bf3a453d8b4a27f7a59c78c8666167f2b17b96c"}, - {file = "ruff-0.15.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c0fe88a7676e7a05b73174d4d4a59cb2ac21ff8263583f87a81a6018475a978"}, - {file = "ruff-0.15.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecfc3c7878fff94633ab0348524e093f9ce3243080416dd7d14f8ba400174719"}, - {file = "ruff-0.15.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b8461180b22420b1bdc289909410930761629fddf2a5aaf60fae1ab26cedc4c4"}, - {file = "ruff-0.15.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6eccbe50a038b503e7140b441aa9c7fc8c1f36edf23ebef9f4165c2f28f568b7"}, - {file = "ruff-0.15.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:382fc0521025f5a8ad447d8bdd523545d0d7646adb718eb1c2dac5065ec27c0f"}, - {file = "ruff-0.15.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:456d41fcd1b2777ad63f09a6e7121d43f7b688bbc76a800c10f7f8fb1f912c3f"}, - {file = "ruff-0.15.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1a04bcc94ae6194e9db05d16ad31f298a7194bfbcb08258bbe589cee1d587b8"}, - {file = "ruff-0.15.17-py3-none-win32.whl", hash = "sha256:596065960ab1ff593f744220c9fe6580eda00a95003cffa9f4048bb5b1bf0392"}, - {file = "ruff-0.15.17-py3-none-win_amd64.whl", hash = "sha256:6769e5fa1710b179b92e0bfa5a51735b35baea9013dadb06d5f44cbcf9547084"}, - {file = "ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456"}, - {file = "ruff-0.15.17.tar.gz", hash = "sha256:2ec446937fd16c8c4de2674a209cc5af64d9c6f17d21fbf1151054fa0bcf5219"}, + {file = "ruff-0.16.4-py3-none-linux_armv6l.whl", hash = "sha256:df4075f71ddac40b9934af60c3ec8a53047dd5a5fdc43224e6e4e8e9a27cb6f7"}, + {file = "ruff-0.16.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0c95538517af68004306b0fb3214ff2f2af67a65092aee77cd9eb86db6656604"}, + {file = "ruff-0.16.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:963f83df8e69e575b64d67dd447ebbc917db41a14bf38d4593a4183e7aaa8255"}, + {file = "ruff-0.16.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32a5057c7ff3f6e6480a48fccfb3a412a690f48a3d03ac5cf08177d6c2da3ade"}, + {file = "ruff-0.16.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3dce8d9b0c57c265b91885a66a567d8ea1372e8eb4e250fa8e5e3f579e99cff"}, + {file = "ruff-0.16.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7dc651db49283c69f8e72c834eec4fe5573e4c646856aebece0ce385dceb2a80"}, + {file = "ruff-0.16.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3817b87dbcabc92f13b05019257c5b89b5b4d51b5fb20f56fb5235ceb723cd07"}, + {file = "ruff-0.16.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9fce1499134b2c8c68e5166f95705a5812062bb93aacc5f9873bb1a27084bc7"}, + {file = "ruff-0.16.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2d812e482f5a7e02eee26cd73d2a37ebbdf47d795ea63ba1b89110ae93e9fb3"}, + {file = "ruff-0.16.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6baaf984aa7976edf93d3b627fe2d1d22ee94bbca05fa6f90fc76d73924e3454"}, + {file = "ruff-0.16.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bdfcf0b28662eb890372d50f92c283bb94e67e7635ed93c7fd533970acff7b2b"}, + {file = "ruff-0.16.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b66b02cb9b04f537643cadf5768e5f98dc461890d530cb67113d71c8c76e605d"}, + {file = "ruff-0.16.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8528bf9a4b291a60bf02ea453511e8ce6215bd2b982ee80405b66b008b6c30a0"}, + {file = "ruff-0.16.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fbd85d2875fdd67e833213a651f613bbf25303abf6aa822a5121f4531195678d"}, + {file = "ruff-0.16.4-py3-none-win32.whl", hash = "sha256:312769988007aaeb8e189b443ccdd03c0e6374489e053467be6d96518ebff76e"}, + {file = "ruff-0.16.4-py3-none-win_amd64.whl", hash = "sha256:05d9d27a18c4bcbefada602480ec9e01e0bc949d432e0ced5df77edac195919c"}, + {file = "ruff-0.16.4-py3-none-win_arm64.whl", hash = "sha256:a3a61621c9b6f6a89573e938a080e648f1695baa3f58570a3a707bc51ff65a21"}, + {file = "ruff-0.16.4.tar.gz", hash = "sha256:13171aa9d9af2240ee3504e639de73122c67e74036de5ba2e1d01422cd17e3dc"}, ] [[package]] @@ -4405,14 +4469,14 @@ pathspec = ">=0.10.1" [[package]] name = "sentry-sdk" -version = "2.62.0" +version = "2.63.0" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" groups = ["dev"] files = [ - {file = "sentry_sdk-2.62.0-py3-none-any.whl", hash = "sha256:27f61d13a86c3c1648dec666dd5a64f79772dd6a84b446f11866601ecab24f6f"}, - {file = "sentry_sdk-2.62.0.tar.gz", hash = "sha256:3c870b9f50d9fd15b58c817dbde1c7cfaa9fe3f05df0a4c6edd5571cb82f5491"}, + {file = "sentry_sdk-2.63.0-py3-none-any.whl", hash = "sha256:3a9b5ddd403f79eb73bd670f75f04485819db53d28f76ced7bc09041cb0dfd6a"}, + {file = "sentry_sdk-2.63.0.tar.gz", hash = "sha256:2a1502bf864769275dbc8c2c9fc7a0f7f5e18358180b615d262d13a31ffba216"}, ] [package.dependencies] @@ -4505,16 +4569,29 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +optional = true +python-versions = "*" +groups = ["main"] +markers = "extra == \"valkey\"" +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + [[package]] name = "soupsieve" -version = "2.8.3" +version = "2.8.4" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95"}, - {file = "soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349"}, + {file = "soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65"}, + {file = "soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e"}, ] [[package]] @@ -4549,14 +4626,14 @@ dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"] [[package]] name = "testcontainers" -version = "4.14.2" +version = "4.15.0" description = "Python library for throwaway instances of anything that can run in a Docker container" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "testcontainers-4.14.2-py3-none-any.whl", hash = "sha256:0d0522c3cd8f8d9627cda41f7a6b51b639fa57bdc492923c045117933c668d68"}, - {file = "testcontainers-4.14.2.tar.gz", hash = "sha256:1340ccf16fe3acd9389a6c9e1d9ab21d9fe99a8afdf8165f89c3e69c1967d239"}, + {file = "testcontainers-4.15.0-py3-none-any.whl", hash = "sha256:8796c14e76604031ad39cf0ed3b8e9806283a1fbf5270965c2b1c594caa31b74"}, + {file = "testcontainers-4.15.0.tar.gz", hash = "sha256:085cde086337632e19002719460b7b80bbab2bdd51bb3ea04f77d0de96504706"}, ] [package.dependencies] @@ -4574,8 +4651,9 @@ azurite = ["azure-storage-blob (>=12)"] chroma = ["chromadb-client (>=1)"] clickhouse = ["clickhouse-driver"] cosmosdb = ["azure-cosmos (>=4)"] -db2 = ["ibm-db-sa ; platform_machine != \"aarch64\" and platform_machine != \"arm64\"", "sqlalchemy (>=2)"] -generic = ["httpx", "redis (>=7)"] +cratedb = ["httpx", "sqlalchemy-cratedb"] +db2 = ["ibm-db-sa ; platform_machine != \"aarch64\" and platform_machine != \"arm64\"", "sqlalchemy"] +generic = ["httpx", "redis (>=7)", "sqlalchemy"] google = ["google-cloud-datastore (>=2)", "google-cloud-pubsub (>=2)"] influxdb = ["influxdb (>=5)", "influxdb-client (>=1)"] k3s = ["kubernetes", "pyyaml (>=6.0.3)"] @@ -4584,19 +4662,19 @@ localstack = ["boto3 (>=1)"] mailpit = ["cryptography"] minio = ["minio (>=7)"] mongodb = ["pymongo (>=4)"] -mssql = ["pymssql (>=2)", "sqlalchemy (>=2)"] -mysql = ["pymysql[rsa] (>=1)", "sqlalchemy (>=2)"] +mssql = ["pymssql (>=2)", "sqlalchemy"] +mysql = ["pymysql[rsa] (>=1)", "sqlalchemy"] nats = ["nats-py (>=2)"] neo4j = ["neo4j (>=6)"] openfga = ["openfga-sdk"] opensearch = ["opensearch-py (>=3) ; python_version < \"4.0\""] -oracle = ["oracledb (>=3)", "sqlalchemy (>=2)"] -oracle-free = ["oracledb (>=3)", "sqlalchemy (>=2)"] +oracle = ["oracledb (>=3)", "sqlalchemy"] +oracle-free = ["oracledb (>=3)", "sqlalchemy"] qdrant = ["qdrant-client (>=1)"] rabbitmq = ["pika (>=1)"] redis = ["redis (>=7)"] registry = ["bcrypt (>=5)"] -scylla = ["cassandra-driver (>=3)"] +scylla = ["cassandra-driver (>=3) ; python_version < \"3.14\""] selenium = ["selenium (>=4)"] sftp = ["cryptography"] test-module-import = ["httpx"] @@ -4661,32 +4739,54 @@ files = [ {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, ] +[[package]] +name = "trio" +version = "0.33.0" +description = "A friendly Python library for async concurrency and I/O" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"valkey\"" +files = [ + {file = "trio-0.33.0-py3-none-any.whl", hash = "sha256:3bd5d87f781d9b0192d592aef28691f8951d6c2e41b7e1da4c25cde6c180ae9b"}, + {file = "trio-0.33.0.tar.gz", hash = "sha256:a29b92b73f09d4b48ed249acd91073281a7f1063f09caba5dc70465b5c7aa970"}, +] + +[package.dependencies] +attrs = ">=23.2.0" +cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +idna = "*" +outcome = "*" +sniffio = ">=1.3.0" +sortedcontainers = "*" + [[package]] name = "ty" -version = "0.0.35" +version = "0.0.49" description = "An extremely fast Python type checker, written in Rust." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "ty-0.0.35-py3-none-linux_armv6l.whl", hash = "sha256:85ae1e59b9fb0b40e9d84fe61b29653c5f2f5e78b487ece371a7a38c20c781cf"}, - {file = "ty-0.0.35-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:709dbb7af4fcadb1196863c00b8791bbbbcc9dacbe15a0ff17f0af82b35d415b"}, - {file = "ty-0.0.35-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2cb0877419ab0c8708b6925cb0c2800b263842bd3c425113f200538772f3a0cc"}, - {file = "ty-0.0.35-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7afbcfc61904b7e82e7fe1a1db832a40d8f01e69dee1775f6594e552980536c"}, - {file = "ty-0.0.35-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b61498cc3e4178031c079951257fbdb209a891b4feb10ad6c40f615a51846f41"}, - {file = "ty-0.0.35-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:573b1eacda349fc8dba0d767b41631c3a6f66412363127c5bf2b1b40a1d898d2"}, - {file = "ty-0.0.35-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7209746158d6393c1040aa64b3ca29622e212ea7d8bae22ba50dbcbb4f96f0a"}, - {file = "ty-0.0.35-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4466a1470aa4418d49a9aa45d9da7de42033addd0a2837c5b2b0eb71d3c2bcd3"}, - {file = "ty-0.0.35-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb44bb742d52c309dcaa6598bcf4d82eb4bf1241b9e4940461e522e30093fe8b"}, - {file = "ty-0.0.35-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:34b219250736c989b2670a03782c61315f523f3a2be37f1f90b1207e2212c188"}, - {file = "ty-0.0.35-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:88e2ac497decc0940ef1a07571dee8a746112a93a09cdc7f8bca0099752e2e05"}, - {file = "ty-0.0.35-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:02cae51b53e6ec17d5d827ff1a3a76fd119705b56a92156e04399eda6e911596"}, - {file = "ty-0.0.35-py3-none-musllinux_1_2_i686.whl", hash = "sha256:11871d730c9400d899ac0b9f3d660ed2e7e433377c8725549f8250a36a7f2620"}, - {file = "ty-0.0.35-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ad0a2f0530d0933dcc99ad36ac556c63e384ea72ab9a18d23ad2e2c9fd61c73"}, - {file = "ty-0.0.35-py3-none-win32.whl", hash = "sha256:0e25d63ec4ab116e7f6757e44d16ca9216bca679d19ecc36d119cf80faada61a"}, - {file = "ty-0.0.35-py3-none-win_amd64.whl", hash = "sha256:6a0a6d259f6f2f8f2f954c6f013d4e0b5eba68af6b353bf19a47d59ec254a3d5"}, - {file = "ty-0.0.35-py3-none-win_arm64.whl", hash = "sha256:619c52c0fb2aa21961a848a1995135ad3b6d0a9aa54da0194e60f679cc200e13"}, - {file = "ty-0.0.35.tar.gz", hash = "sha256:8375c240ab38138a19db07996c9808fb7a92047c1492e1ce587c2ef5112ad3a9"}, + {file = "ty-0.0.49-py3-none-linux_armv6l.whl", hash = "sha256:12c0c4310b936d762a8586c210b53d4fa4bb361a04429afa89bf84b922e5e065"}, + {file = "ty-0.0.49-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:737bfdc2caf9712a8580944dcdc80a450a37a4f2bc83c8fa9b7433b374f9e471"}, + {file = "ty-0.0.49-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ab90c1baf3b1701d282fce4b02fa552a962d109f8972c46ef6b22429503bfea4"}, + {file = "ty-0.0.49-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ce8ecf6ba6fc79bd137cc0557a754f7e5f2dfe9436412551d480d680e248ad"}, + {file = "ty-0.0.49-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10d85c6865c984e78661e0bd20b180514b4a289739224e84816e342bdf381e04"}, + {file = "ty-0.0.49-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d96a67a206619e01fa92f35a22267ec634bba62be24b1d0e947020cc179995b"}, + {file = "ty-0.0.49-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3de9f648564e0a66344ef397770387cb0d093735f8679d2c5a08a4741e79814d"}, + {file = "ty-0.0.49-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5779179ab397d15f8c9dbb8f506ec1b1745f54eac639982f76ef3ce538943b50"}, + {file = "ty-0.0.49-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:792d4974e93cc09bd32f934586080bbbe21b8e777099cb521cb2de18b68a49f0"}, + {file = "ty-0.0.49-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:727bda86deb136073e525c2e78d60e38aedcce5d80579170844a52bbf7c1440d"}, + {file = "ty-0.0.49-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4f2fc2bc4a8d2ff1cca59fd94772cabdfec4062d47a0b3a0784be46d94d0540b"}, + {file = "ty-0.0.49-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3724bd9badef333321578b6a941fbc571ebf49141ec2356a8590fbe4c9aa588d"}, + {file = "ty-0.0.49-py3-none-musllinux_1_2_i686.whl", hash = "sha256:166c6eb52ee4af3c5a9bb267d165d93000daa55c6758cd8ff3199741fb75917d"}, + {file = "ty-0.0.49-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:91e81d832c287b05782ee32eb1b801f62c1fa08df37d589d2b88c3f1d51c9731"}, + {file = "ty-0.0.49-py3-none-win32.whl", hash = "sha256:7186af5ca9829d1f5d8916bcf767b8e819bfbf61b1b8ec843bb3fc699cb502e1"}, + {file = "ty-0.0.49-py3-none-win_amd64.whl", hash = "sha256:ae2142fc126a01effcca0c222908b0e6654b5ba1266d4e4d406e4866aef8e1d1"}, + {file = "ty-0.0.49-py3-none-win_arm64.whl", hash = "sha256:75d5e2e7649765f31f4bed6c8adb149a75b18edd3fa6336dac4d0efc1a66466f"}, + {file = "ty-0.0.49.tar.gz", hash = "sha256:0a027bd0c9c75d035641a365d087ad883446057f9be0b9826251c2aecafbf145"}, ] [[package]] @@ -4762,14 +4862,14 @@ types-cffi = "*" [[package]] name = "types-python-dateutil" -version = "2.9.0.20260508" +version = "2.9.0.20260518" description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "types_python_dateutil-2.9.0.20260508-py3-none-any.whl", hash = "sha256:bfc6fd2d81aa86e5ac97206a64304f6bd247426eedbca9b98619bbc48c6a1c10"}, - {file = "types_python_dateutil-2.9.0.20260508.tar.gz", hash = "sha256:596a6d63d81f587bf04c8254fb78df9d2344e915ce67948d7400512e3a6206d5"}, + {file = "types_python_dateutil-2.9.0.20260518-py3-none-any.whl", hash = "sha256:d6a9c5bd0de61460c8fdef8ab2b400f956a1a1075cce08d4e2b4434e478c50b8"}, + {file = "types_python_dateutil-2.9.0.20260518.tar.gz", hash = "sha256:51f02dc03b61c7f6a07df45797d4dfe8a1aa47f0b7db9ad89f6fd3a1a70e1b51"}, ] [[package]] @@ -4790,14 +4890,14 @@ types-pyOpenSSL = "*" [[package]] name = "types-requests" -version = "2.33.0.20260518" +version = "2.33.0.20260712" description = "Typing stubs for requests" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "types_requests-2.33.0.20260518-py3-none-any.whl", hash = "sha256:626d697d1adaaff76e2044dc8c5c051d8f21abc157bdfe204a75558076fe0bf0"}, - {file = "types_requests-2.33.0.20260518.tar.gz", hash = "sha256:df7bd3bfe0ca8402dfb841e7d9be714bb5578203283d66d7dc4ef69343449a5e"}, + {file = "types_requests-2.33.0.20260712-py3-none-any.whl", hash = "sha256:de027e28c171d3da529689cbfa023b0b4eab188c8dfa22fd834eebd2cee6e7bb"}, + {file = "types_requests-2.33.0.20260712.tar.gz", hash = "sha256:2141b67ab534a5c5cd2dac5034f2a35f42e699c5bf185eee608c5246a069d7fb"}, ] [package.dependencies] @@ -4857,89 +4957,106 @@ typing-extensions = ">=4.12.0" [[package]] name = "ujson" -version = "5.12.1" +version = "5.13.0" description = "Ultra fast JSON encoder and decoder for Python" optional = true python-versions = ">=3.10" groups = ["main"] markers = "extra == \"datadog\"" files = [ - {file = "ujson-5.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:71bdb5d10c6d7e710cfa78e743d9fb79a37c7c66fa916cd287bffbaa520f5abe"}, - {file = "ujson-5.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:558673c6c3a2309775683ca96d5f1e4cd99889f71b1ba5cb6be8aa37ae67f9e0"}, - {file = "ujson-5.12.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4b0c9f6a56aa94bb98b403e1f57a866f0b43abaa89757b24d4a4b3cd8643ced"}, - {file = "ujson-5.12.1-cp310-cp310-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:7bba5ab7965619db7d6f5503133b8e2d8bfce9bb6754224ca64d19261cc52f7c"}, - {file = "ujson-5.12.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:191d2077fd53441599a2efd3dcc205b9cc5f3a4d685a76e9f73f4b6c19aee0c9"}, - {file = "ujson-5.12.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d90d27953716ef206c42f166932b3dbb264dc638bbf32acae81b216ae35f566d"}, - {file = "ujson-5.12.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b6afa86c117b66034004ee83c5149c6dccf7cb88941f9d3a1640c7076577f2d4"}, - {file = "ujson-5.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9523d67d45334f9a1d62e423bd72be62b58d2289a50420ffffa9363763eab73f"}, - {file = "ujson-5.12.1-cp310-cp310-win32.whl", hash = "sha256:757f2026bef09d231d63a2250a2c7ad21ea1c9cb1ded6480659d202c4e2ef09e"}, - {file = "ujson-5.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:7e31afad20cd6837a5ac6965d95b44b0ff06e42a82b01a8d3dc606a07f0b7a2a"}, - {file = "ujson-5.12.1-cp310-cp310-win_arm64.whl", hash = "sha256:80f58ae2be100da0f525330ee274accd8892d1c125fea75076f60539d9a5f9cd"}, - {file = "ujson-5.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:26dcb43869057373048cbd2678293c5b0f962d5774cc76fc9488564a209bcbf2"}, - {file = "ujson-5.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bca3f04b2f590a8211acdc3ca06649b65a7ed1e999437dccf095310be9d3ba4e"}, - {file = "ujson-5.12.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29d1d64ed2c3c17666f4f0e15462800f3477255dc53667ad5d099277866c5666"}, - {file = "ujson-5.12.1-cp311-cp311-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:2cfbd6b0c677d5d053964b8f98d8bb1af10c591c8c24454bcd40006ac8ba18db"}, - {file = "ujson-5.12.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f75caed5b6d1fc271bb720a780c4199914267f7b865f9bf17826c4feccea582c"}, - {file = "ujson-5.12.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b21b4c680594c8686bcd4cdda0fd3ea2567b9d42bcf1d1e3d92d39bcdb02e8f1"}, - {file = "ujson-5.12.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:50d07e79ec70d32b4fbe18ab706ed0b172be08710d5901b9d067d7951bfaa164"}, - {file = "ujson-5.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:080bc65ac7c0a6314d45d55b6171d3a48b1aeaf89895654d625b291cfe46309f"}, - {file = "ujson-5.12.1-cp311-cp311-win32.whl", hash = "sha256:251ba8229e19b4b0b3efb5e7e3ddfa67c5c466aa492707bc3f6568bf714604dc"}, - {file = "ujson-5.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:46315b82505c99101dcab3bd979f15fecfde85c02df7efbb4e428fa357665290"}, - {file = "ujson-5.12.1-cp311-cp311-win_arm64.whl", hash = "sha256:12e99e49c62322ed0394c914aff15403ba7ede0b74f05a0faa4ec12c7d17a139"}, - {file = "ujson-5.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:10f44bd08ae52ee23ca6e8b472692e5da1768af2d53ff1bad6f40b532e0bc7ee"}, - {file = "ujson-5.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cc6ea753b7303fa5629fa9ac9257ea4b001c4d72583b2bb36ff1855a07db49f"}, - {file = "ujson-5.12.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:289f13095764d03734adfa10107da9b530ceb64dc1b02a5f507588d978d5b7df"}, - {file = "ujson-5.12.1-cp312-cp312-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:427893168d074e59214b0ee058337c57f5bb80175cdd5b4799a9c931aae22022"}, - {file = "ujson-5.12.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7a81724d5d90a2da7155d15d8b156ce57eaed7cdd622df813f36a8e612fd4c8"}, - {file = "ujson-5.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3a6efff7dc6515416366819de4a1bc449b77107c5b48508b101fd40f7f8bec08"}, - {file = "ujson-5.12.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77a71fe53427a0cf49d56eafd801d9f7e203b784b7f99cc717783fd6f6f7b732"}, - {file = "ujson-5.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ea3bed53d2ea8e5642e814a9e41f3e29420a8067874ba03ace8c0462e160490c"}, - {file = "ujson-5.12.1-cp312-cp312-win32.whl", hash = "sha256:758e5c8fbe4e6d483041e03b307b01fb5d2f2dd4452d4d4b927ab902e188939e"}, - {file = "ujson-5.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:f6074d3d3267ba1914c624b6e1fa3d8152648ff36b0ab77ddf83b92db488c30d"}, - {file = "ujson-5.12.1-cp312-cp312-win_arm64.whl", hash = "sha256:7642a41520ac1b2bc25ea282b66b8da522cc43424442e6fb5e039be4d4f96530"}, - {file = "ujson-5.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c4bdc052a5d097f0a2e56d93aed97355f9f7a62ef9baa4f8517e43245434af9c"}, - {file = "ujson-5.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5dc91fa06ea35920b704fd9d70871897680145998071cfbf5ee3e19f2c9fc242"}, - {file = "ujson-5.12.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5db0849c0e3da54822a5834f2dc51d7c51072d7f7d665014ee34600dc10889b"}, - {file = "ujson-5.12.1-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:949cb4863a5d4847edeb47c5364b334e8cadf23a7cbdaa547d86098a4b093106"}, - {file = "ujson-5.12.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8aa731138d6dfca4ab84501b72384e6c544bfb48cb87a0dd4d304df3246cac25"}, - {file = "ujson-5.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:727e983ef27892d86ee2d28fd517eeb02b2c1165aafcbe929dce988aeee81bfe"}, - {file = "ujson-5.12.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d57d731ecf492d3d011e65369f8330654f0875b19f646be5270d478e843d3b81"}, - {file = "ujson-5.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a09636220f26c66f80c6c6283023cb53120e843825f890be92696cd1aa43f39"}, - {file = "ujson-5.12.1-cp313-cp313-win32.whl", hash = "sha256:ee83fbac03a0896faf190177c938f94eb610b798d495a19d50997242c4eca685"}, - {file = "ujson-5.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:e08d9e096c416ddc34519241f97c201258b42639f2012d9547d8ae32921800dd"}, - {file = "ujson-5.12.1-cp313-cp313-win_arm64.whl", hash = "sha256:963287e4b1bc463735c4056968a2dfa59bb831b6daba68bddd14f451191fe9e5"}, - {file = "ujson-5.12.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f19e9a407a24230df0cc1ec1c0f5999872ba526b14a780f80ad6479f5eed9bc"}, - {file = "ujson-5.12.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8b657e870c77aaacdeea86cfad3e6d2ef9b52517e45988c9c367f7ee764fe4dd"}, - {file = "ujson-5.12.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:984b5a99d1e0a037c2046c3c4b34cec832565d62d5017be0a035bf3cbfab72dc"}, - {file = "ujson-5.12.1-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:f48ef8a16f1d85bd7982beac7adfd3fb704058631db84c1c61c8a1b7072b1508"}, - {file = "ujson-5.12.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f39ba3b65cc637b59731532f7e7c807786bff1d0332ab2d5b96a04d2584d78f"}, - {file = "ujson-5.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:07f307780f85b49cba93f291718421b6f5f3b627a323b431fad937a18f6587cb"}, - {file = "ujson-5.12.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1c335caea51c31494e514b82d50763b9792d3960d2c7d9fdb6b6fb8ed50ebdd0"}, - {file = "ujson-5.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:19ea07e29a45d199f926aadf93a9974128438c01b83141fba32477c0ee604b33"}, - {file = "ujson-5.12.1-cp314-cp314-win32.whl", hash = "sha256:c8e626b6bc9bdd2e8f7393b7d99f3daa2ca4022e6203662e70de7bb3604b21b9"}, - {file = "ujson-5.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:c6d3bdd020333688ee60559437021ed68a98a28fdd609b5af16de5dd58f90cba"}, - {file = "ujson-5.12.1-cp314-cp314-win_arm64.whl", hash = "sha256:e3c9c894971f4ada3ded16a804ed4640e1f2b3e5239beaeec7c48296f39f4232"}, - {file = "ujson-5.12.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:49dd9c378e1c8e676785ff2b62cb490074229f15ab54abf45b623713cb2c36b5"}, - {file = "ujson-5.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d8827904358d7da59ccf2e1fd8de59e78248036d17fecc0462e62c6721f1102"}, - {file = "ujson-5.12.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc26caebea90425662ef0b979f945f6ac832651881107d6ec9a3c4d4a4ba929c"}, - {file = "ujson-5.12.1-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:45022aae09ac3d45bda6fbfc631088d1aff9a0465542d40bd6d295ced378c430"}, - {file = "ujson-5.12.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b22aa0f644516d3d5b29464949e4b23fe784f84b4a1030ab9ac3cb42aaedabb1"}, - {file = "ujson-5.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7dc5cf44ea42365cd1b66e6ed3fc6ca040c86587b024a6659b98e99d31cff2cd"}, - {file = "ujson-5.12.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8df5d984ff4ac1ef292d70f30da03417038a7e1e0bc272d28ca9d34f02f41682"}, - {file = "ujson-5.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:485f0182a0c0b54c304061cdc826d8343ce595c4055f7a24e72772a8520e5f7b"}, - {file = "ujson-5.12.1-cp314-cp314t-win32.whl", hash = "sha256:4e12ca368b397aed7fa1eec534ea1ba8d94977b376f9df3e93ae1acfd004ec40"}, - {file = "ujson-5.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:cec6b9b539539affc1f01a795c99574592a635ce22331b64f2b42e0af570659e"}, - {file = "ujson-5.12.1-cp314-cp314t-win_arm64.whl", hash = "sha256:696224d4cfb8883fa5c0285dff31e5ce924704dd9ccd38e9ea8b5bf4a42b12fc"}, - {file = "ujson-5.12.1-graalpy312-graalpy250_312_native-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c419bf42ae40963fc27f70c59e24e9a97f5cf168dbce2c572f3c0ce3595912"}, - {file = "ujson-5.12.1-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0be2b4f2f547b9f0f3d902640e410e5a2fc851576cbe033c88445a23e3e7aef1"}, - {file = "ujson-5.12.1-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:4ea0c490c702c20495e97345acfcf0c2f3153e658ef537ff111929c48b89e10a"}, - {file = "ujson-5.12.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3e30fa6bc7156ed709e13f8b52e917db08fbfd611ba61346b62630974ec0ba8e"}, - {file = "ujson-5.12.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f67c5f0d64eba0fbbd6d2d6a79b0c43c5bc06f27564378fd5d716e0d40360068"}, - {file = "ujson-5.12.1-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8416bb724db9accfa97bdb77245952494b1800c23e42defd46afb5c661c9af19"}, - {file = "ujson-5.12.1-pp311-pypy311_pp73-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:66005b49c753a1b9f2f8853919dc58e1e6bd66846ea341a33afa76c6d7602485"}, - {file = "ujson-5.12.1-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdc6b277dcd27663f7fb76b6a5088424c66e0407c23e9884f80cd733f7d71b19"}, - {file = "ujson-5.12.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7957b64583793042521f7f7c71c01626b3d32a17528eaab980eb8cdc3d4eec68"}, - {file = "ujson-5.12.1.tar.gz", hash = "sha256:5b7e96406c301a1366534479a7352ec40ec68bb327c0c119091635acd5925e35"}, + {file = "ujson-5.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:770643b4752266c5a466149848b78c3874940926a4ecef304f518b2b6cdb432f"}, + {file = "ujson-5.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8e8c1203cb1a27720debc334f840a9170da741503522f86999710cb4738fbe3"}, + {file = "ujson-5.13.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:65c1813fdd742fe3c249d9c417fa490e5b54e8a91bf343a88486ec50d175c444"}, + {file = "ujson-5.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5aa9bf16f0131812720dd4ae70bc1a0cf68f79e93c07c66100d328e75944b567"}, + {file = "ujson-5.13.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82c17b904c03c2b9629486ec91a8fa46a15f10d03504284f54ed7257a917d9f1"}, + {file = "ujson-5.13.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0b5f6983b2469db00e540b68fa8297b7a0ccd0d5173c60cc3e84f336b09395f"}, + {file = "ujson-5.13.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b877fe7926107d25d54b655c5b8dd94b294b22c157233163ad29fdb54ac10cf4"}, + {file = "ujson-5.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f3cae1c811e787b9500e2830af8632dcb32a78dea5baf15aac51d681c59a59dd"}, + {file = "ujson-5.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:820b78a6a183ab6591b2ea888020032ef0fe328d852af9a5c8d8084ababb2218"}, + {file = "ujson-5.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1209c985d1f4c2ae085ced7325509650cdb3533bb7294558dd1f26d48377942"}, + {file = "ujson-5.13.0-cp310-cp310-win32.whl", hash = "sha256:326553ae6c063c8246974906a6c137a0780fe46d143abebc52cd2cadda0f1814"}, + {file = "ujson-5.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:0354d6b50b0d153ed7c629845b18a953d0c727b7e768fd94a94e0602abfa1f29"}, + {file = "ujson-5.13.0-cp310-cp310-win_arm64.whl", hash = "sha256:7b4a05f61a96553995da6a4d502e07bc8aa5d07a90031df006239c6b00ce1c83"}, + {file = "ujson-5.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b7badefa73f96bad9e295ea22bd06967b851c8aad68c74196437e3584f25de5"}, + {file = "ujson-5.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:09effd42924a80df20a63b31a1ede905e66b0ce24aafe7a4cbedb05c783f8bb3"}, + {file = "ujson-5.13.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:89d0bfc986d02b4ce76b00e0f560bc8d30dfe8c05a1bfd8529e085eb6c1a77d9"}, + {file = "ujson-5.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cdd9618e07b3b142a02f0ab8227fd52453688b8e8e60ac0511f13a25fa8009db"}, + {file = "ujson-5.13.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0806683e8171ec06817e6af22f14ba0cd2f16def8a2ffb22a28a10615249355d"}, + {file = "ujson-5.13.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1cf46c79498c81f088cad4165b1669a78bba7bfbeb778c7cc1aff316e062b0d"}, + {file = "ujson-5.13.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2282039eb26f08ed1a1381360395f8a310f39a45ef7314cb0f258a7d2917ea3"}, + {file = "ujson-5.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c75eb7fac0fe92925b959cf2fa18f88d9fb76b10781fd2a6ccb895d5fb89171"}, + {file = "ujson-5.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:12078e81def2790140583abefc1b979f3c77be15d53c524bf0e232f669822052"}, + {file = "ujson-5.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e12192a37c6c0e476554b62647acdf6139a47b6f13d8bad8dd2316763c4cee12"}, + {file = "ujson-5.13.0-cp311-cp311-win32.whl", hash = "sha256:ae53b3f046529c193d533ca8111492330b204d6007611cdfa20e8b764c7c1389"}, + {file = "ujson-5.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:2275bbaaea3eddd2e8ec0863e28a420f5f520b14a760fc3f1e49fd07a974448c"}, + {file = "ujson-5.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:32a59e7151fe2fec8fdf9a565ee66fcf87918d48827e21cdab5e15ff9f274b78"}, + {file = "ujson-5.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bf81570ac056cb058f9117b52ca5dd800bfe9381d0076d0bb30a08a54591d654"}, + {file = "ujson-5.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7edf16359c52ed53406e216565d83e6b98c23c3cb9a0a01673f2493f8fb15edf"}, + {file = "ujson-5.13.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:24539618fb3243cfdf27dab9a850acab80798a01501e9586b61fb9ecd016a891"}, + {file = "ujson-5.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fdde6341d213b29f413b5fa9fad1392d5408074c75f0900ed949e97e546fa5df"}, + {file = "ujson-5.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:229faf041ef249ee3fd57bac1cedb123d2718ab63f6ccd50eca95ea902eb0dca"}, + {file = "ujson-5.13.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d02f31c2f59cc6a1c2c3633b377701fc2d8e876cc01950735d7a01132ccc233"}, + {file = "ujson-5.13.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea7204e9fa7538bfbb1396e1ee8c2bbcd3818b3633ef5bb14d4fdea52994d14d"}, + {file = "ujson-5.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c5a2478a3a1fa4421f7e035b87194eea0cf44c7971a3f32ad1b42a0dfd63c03"}, + {file = "ujson-5.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b535e0970c96957e999cfe5ec89361f0e8d0bb987fb5d5144f6f495cb3ed9e19"}, + {file = "ujson-5.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d0ad1207694988498fca7e0bb28eba7564fa33261d2f9fdf66a3aaab376b803"}, + {file = "ujson-5.13.0-cp312-cp312-win32.whl", hash = "sha256:d6bc9fa43a49e403c68c7eb164eef0feee9dd29474a7c6e0d3b6267025371990"}, + {file = "ujson-5.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:6692d49ff970aaa5008f4a6fe06974bc91fd957bf13173f765e46d8ba44906ea"}, + {file = "ujson-5.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:5737ffe0740a788b0e6255f0ffb281db49305fd6e6a587be44c73d9e92b554c4"}, + {file = "ujson-5.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46998fc8d11aec34a20e2010905e7059732a3d192d9a3c3fe4f9ffd146c87ec8"}, + {file = "ujson-5.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ee03ce288ba25b05cf0de87203165642277a25caa4f00a437e13152e5214e310"}, + {file = "ujson-5.13.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:cdf33b588a81b05d0b585c66f83050c49cb670623424d10e4d1ad37ba2f7eed9"}, + {file = "ujson-5.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4cabd73c114ce93c21d7db2e2d8e16217fd8a5b2b3ec754629eebef5c262d47f"}, + {file = "ujson-5.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ffc61fc756a64f4d169a78cc638d769e3c324f45fc51997626abf4e5e5dd6460"}, + {file = "ujson-5.13.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c00323c13a35822c9a67a26c0b2a0787510bf1ef490922b58009b362d1a3e21"}, + {file = "ujson-5.13.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:496e662a6b46d5f936d77fb68259cece19213bb2301ddd520dbd75ac7c90c5f4"}, + {file = "ujson-5.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7fd41b86444df14f8b4b7afaaa9f27bacfbf8c18380872317aeab6cd125dcede"}, + {file = "ujson-5.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bf3c2c4ea55d4187903fcdc689a9bf5b0fc72d8c0eaff39db18c1f337c8832c1"}, + {file = "ujson-5.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6eca7751d61045a9b1e7f9a8c97ac24b164f085b60bef1c4668654bb2338011"}, + {file = "ujson-5.13.0-cp313-cp313-win32.whl", hash = "sha256:b63d3820f978bc8e98cc3f1fe26a33b0d2ea237733a23fe5e9cb5d51f466bd97"}, + {file = "ujson-5.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:17a59d5cf23ef98f7c9314524976b4b288374d83200add01d953024fb06404f9"}, + {file = "ujson-5.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:b49516fbe803ff30d6caa9ccc3799ec7f968992747ce3099eae4758928577b53"}, + {file = "ujson-5.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc9dfd41fed397ab03bb9d9fe1cbd83301211c772a17536033ce7d68877ac82b"}, + {file = "ujson-5.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca7ef2fa6c408a7c0f558e4d33d93b32ddc35ed6d3cfc505747931a64b7465d5"}, + {file = "ujson-5.13.0-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a554b2e5bee85030369514cef8b0b913cebe1a4c2c0c13541966d50bcba22b1a"}, + {file = "ujson-5.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ea939ff629ab03ae970d03eca6d1febd8ed55ba38ca44aec64ce997537cd3fa0"}, + {file = "ujson-5.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b98bf2faa5e37ecfe752226ea08290031e375a0c43d425a0b955fb3e702a2a71"}, + {file = "ujson-5.13.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a4b92344b16e414aeb609e57f62c466500e53c94f1698f5b149dc0b7223ec3e"}, + {file = "ujson-5.13.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7df805aad707507a1fa165fb716218ca3a89f142125dc4b23c9fcc08fa402d97"}, + {file = "ujson-5.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7576bdbef327c3528f011002a2d74486f6fe4e33289bdb7a042b7f1a6e9d8285"}, + {file = "ujson-5.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6eee5d7cce3f32a468905f9ff61807a60287a90258d849460f6fa826e810870d"}, + {file = "ujson-5.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:144e9d8a454cfa727e0f755e1863738ed68068583bda5463052cb446835bd56c"}, + {file = "ujson-5.13.0-cp314-cp314-win32.whl", hash = "sha256:576f35c35b918d67d41b933878062ec0a5c9f4d1e9e14e04aeef35384963feae"}, + {file = "ujson-5.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:d5e206e9f849ead27e51ef8da44e52b38da7c6dbd929a7340ab44533edcda8d7"}, + {file = "ujson-5.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:dc470179775468f9a007d3a6a2734624248c94bf47c6645e808c7e50a5070d1a"}, + {file = "ujson-5.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:69b4e36bb7d5f413ba8c00c8006b2ec627cc5ace97301462f6aadb66ec9d2979"}, + {file = "ujson-5.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b644d50f66de5490c1823c7176618cead5e8e8a88cba9f40a6308ca52e79267"}, + {file = "ujson-5.13.0-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:15107aaa4f559d55201165ec32abb35c283a861be1fa67229578cb7d93fcd93a"}, + {file = "ujson-5.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6e343c5f0c058523f1edbf6ae4eceb4e0d934205a53bbdd8d9a945c83324662a"}, + {file = "ujson-5.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02200035bc80e830f076ffc1b329a94c295aee6d9de8c9043647cb9a7bd4f76f"}, + {file = "ujson-5.13.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f19b81b73ff28f5c5022ee794f94122bfcda07a76423078e349465d71223a1"}, + {file = "ujson-5.13.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82e1393e6dbe3c95fdfc95c6c528890e191351a1f024ef51126cf1f22543af52"}, + {file = "ujson-5.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38afcf994b28ed85ea2420e2a8d79a37d0a77348b3daf53850c16edda66f942d"}, + {file = "ujson-5.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1bdf2518971586f2b413156c49d9dd8b56cc990a8647081e1bd00af60564d469"}, + {file = "ujson-5.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:751ad01042472f1c7c02f5c597c7aee79834e82a6cc384ca302173bbc8e8deb8"}, + {file = "ujson-5.13.0-cp314-cp314t-win32.whl", hash = "sha256:74f3dd61aeb01b7b2a6754e400224e819279041b3867935a55ccf57fb86a43b2"}, + {file = "ujson-5.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5c31317d5e4504dae8f98795358b6082fc0ef96e7394806db0a76a4a8717f446"}, + {file = "ujson-5.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aefd3c9c95f9b62348956396ff7b31818476f8f54dc4a4e64cbd4f0491db6fca"}, + {file = "ujson-5.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3e074a1f7778d58aa3b3056bab7b6251aabb3f381808018ca2b7fb8dbdeef7ab"}, + {file = "ujson-5.13.0-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bb53ef95d35875262b8d0aa28506ca612ddd07058bee2a90f609938e69dc801"}, + {file = "ujson-5.13.0-graalpy312-graalpy250_312_native-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fb296a0aa480ab88d895ddaa90372604c08ccc72323f02590612c775426ab413"}, + {file = "ujson-5.13.0-graalpy312-graalpy250_312_native-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2862f81af44b3a7e74c5d80caa118d736be1991ce6f1d5c723716fa403060cc6"}, + {file = "ujson-5.13.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c16e07581172f08585b409246f4535dab13ee85af0e3d3cfa8684b653ca44fa8"}, + {file = "ujson-5.13.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:9bd0f2dd05937c3b089af316884de18c6f6182ddb8ffce597d2e7c7a9ba9f447"}, + {file = "ujson-5.13.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:96e7e019b097b4b25fccddadb369d13f412c13695fcf0680b6bf906376156151"}, + {file = "ujson-5.13.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:7892ea6dd85ede6d30fbbd22af1239b9d81dabdf9b7a8f10ca6d4464d4d9b8ab"}, + {file = "ujson-5.13.0-pp311-pypy311_pp73-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:e1842e10adc8f0db0d3e8aa3a1f8b05ce0456b39e180c8553d7f36dd0bf24b6b"}, + {file = "ujson-5.13.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8bbb1f5ab810954fb307b6c5e68af58210c31da8569f9e6498a3958c1859e72"}, + {file = "ujson-5.13.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3f6d55c68985654a84a9b47ac51f2655adac4fd264e4189f832960c2270dcf70"}, + {file = "ujson-5.13.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b266f182d4bce74a6a9e1f86988485e8cd422efdcc7c3f537e00cc956f52678"}, + {file = "ujson-5.13.0-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfaa8302eb9bb7f5e231f256caf83040760585e32751d19155c0f0c0225f8de1"}, + {file = "ujson-5.13.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:43dee3081b00fe447c5b9e2fe7ebfd57f7bcc5dd25b8a439c26c8c174dd581be"}, + {file = "ujson-5.13.0.tar.gz", hash = "sha256:d62e3d7625384c08082abad81a077af587fdef2761bb14c3822f4234b8d07d75"}, ] [[package]] @@ -4963,55 +5080,48 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "valkey-glide" -version = "2.3.1" +version = "2.5.1" description = "Valkey GLIDE Async client. Supports Valkey and Redis OSS." optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"valkey\"" files = [ - {file = "valkey_glide-2.3.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:736a3e58393fa4f0f2fbb10031d46da5f18ebb8e72d2f9428ff24f0f6addeb3f"}, - {file = "valkey_glide-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b2cd6f5c4e9b67b78873f34f19b9182bab5b07a9151855cf059303e05dac3b2f"}, - {file = "valkey_glide-2.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ddf70bc7888d565273e4bf858ff6047d5284140ff380a732f807c775be8e108"}, - {file = "valkey_glide-2.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f947dd44ba9741eadcab154443f447c19f23dab56de33f56d5f133ee0d597c2"}, - {file = "valkey_glide-2.3.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:6ddc4c6bee1a9c102f003cddc5d1bad8173a9d90e1c9a0f73a285228ed8625af"}, - {file = "valkey_glide-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30590532136e4ea38b6a6389cbcfe4edc554418563c6e4f6357b0749907b2c20"}, - {file = "valkey_glide-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bbdb7baa7aac12c109aefd97f69f9780a4812429db18786254ef288ecf75f19"}, - {file = "valkey_glide-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fd64d77ae26efd524be58456e22636ce4cb0a6110ad722e89f249a45d098692"}, - {file = "valkey_glide-2.3.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:406b73f5ee080406fbfeda542d37de7e330fb4d83b0aa7212b92707d7b7b82a6"}, - {file = "valkey_glide-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0940d4069cbc4896dec3a1ab39db7bf86667fb32892df4dbf3b043129d26d6e5"}, - {file = "valkey_glide-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b47de0ec3d5a253c2b37d33266aaeb22503014f9e8f0611ba999e06f9804966a"}, - {file = "valkey_glide-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a364210002dd0e7c3362299f61a2a1cacf867594a8a0bbf157a345f3f40d4d94"}, - {file = "valkey_glide-2.3.1-cp313-cp313-macosx_10_7_x86_64.whl", hash = "sha256:86d56756842acd6286601128822c5f1f9dcd61305f0c6a80c3e7fb3a7e0404ef"}, - {file = "valkey_glide-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b307795a23473b8e7cff781eb54936cc672a430820f5fa71c6b6fb3748cc1189"}, - {file = "valkey_glide-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cb570f5d637ee55300ccdecd39a51cbf25c67ab6e25f2022d42f32a7bec6163"}, - {file = "valkey_glide-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:506c7800eec05caf17136645cc642941a9536578f4d6733845e7d0ed36ed4e3e"}, - {file = "valkey_glide-2.3.1-cp314-cp314-macosx_10_7_x86_64.whl", hash = "sha256:3d6626e6f9ddfa7f8706023e167b4a2eca8a0f7b7fee1d30f91a83b4811349e4"}, - {file = "valkey_glide-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3466a0c113a951d722036704795ff0377eef11a44ab224472f98d99ac2c5ef28"}, - {file = "valkey_glide-2.3.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe53e4808bdac5b4e6482c66583e1980ecf75666b4e4d0984d89e8b693026543"}, - {file = "valkey_glide-2.3.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1a9662885ea8f3df97a6d873131dea983d42e4735750af368fe2d47e7e44f0c"}, - {file = "valkey_glide-2.3.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7cd8c7d52411d8add7640eb4982942e0fd0154db5d010e3a8094ae028f91d136"}, - {file = "valkey_glide-2.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b3037c5d477172d82ae443f1fbc664dba04a0184efd5a227d541cdf06be478f"}, - {file = "valkey_glide-2.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcac3e3064de88c57042eaa34980c6f35c8c09138148cceafd7dddbd830602ea"}, - {file = "valkey_glide-2.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1de33b66aba099e3f17199da4edefbeb38c91349a6c0d958581fa77be3475140"}, - {file = "valkey_glide-2.3.1-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:5533a090953fd6af4c07b80bd042231540fbd1ede95fff42614750b435f01184"}, - {file = "valkey_glide-2.3.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f814ad759e9fdc6c5ced18ddba38cc2a3badb2839ce3555ec9b44beb794096e4"}, - {file = "valkey_glide-2.3.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dc6dea7ce627a8b166d33232aa7bc7f8dd9d224870235a560bc5d1c4ccec8cb"}, - {file = "valkey_glide-2.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1e135bb43e50b1cd6558d93b3108c40a79ce8dc119de883cebb7458d470f629"}, - {file = "valkey_glide-2.3.1-pp311-pypy311_pp73-macosx_10_7_x86_64.whl", hash = "sha256:993c9bffde847fa3d36c6f11e5e50872dd491f245850d7c6ae1bbb8db5bff346"}, - {file = "valkey_glide-2.3.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:918ce3b8a2a3602e82d03f254bad5cc5bd1398eb84dec8eef77aefccc039bd5d"}, - {file = "valkey_glide-2.3.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28d4cbf00b07db273214488f17d59232baaddd0cc30c26064cf3bf384b03e9cd"}, - {file = "valkey_glide-2.3.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d93ef822a524c8f18c1b750f061373d95e842005116ebcf832d166533bf2bc2"}, - {file = "valkey_glide-2.3.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:f66459238f68b165d6b3b8c1a0cb48e63ce291afa82e82f029dbd37b7b27099b"}, - {file = "valkey_glide-2.3.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7bc45d65196d03f8651a4f40d5be5faf6925edb3d8d37cc57a79fd555f70c368"}, - {file = "valkey_glide-2.3.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115bd85a443fec57d12094aa8ae627a718df64044a978bc4d407f82a29db4c83"}, - {file = "valkey_glide-2.3.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beb25b2445e9be63784e67200fdf708694e842f3a6e93530fe974200411dfaf4"}, - {file = "valkey_glide-2.3.1.tar.gz", hash = "sha256:f4bae030c0aa6e55edb2c27dbd55f82cfb5f581904fff1318eec1c062f30d4b3"}, + {file = "valkey_glide-2.5.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:d66f47bbc5cea7155b0b3ca002043bfd950c09a9ac08e10923a6bcb0a3aa0a1c"}, + {file = "valkey_glide-2.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:47a666862faa47766d4c4814203c3b9f9a8cb466200fd5f71e330eef6ed0d8f1"}, + {file = "valkey_glide-2.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cecd82dab35f30d96785b1405ebe3a6931dd858c8f26a0b7a12b46609e75212"}, + {file = "valkey_glide-2.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44435ed9fdb6322a4b167c9f56fdc0145a1fab4736dd2659165bbb5358d3a68f"}, + {file = "valkey_glide-2.5.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:d9bbf26f9ea2b72ef0b8541dfefa479a82a69bdec77fce917192a262893edfda"}, + {file = "valkey_glide-2.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94256d3335f4c31f19ad1ff02447d9eecd94b8c9e8d51c7b77811277c957e807"}, + {file = "valkey_glide-2.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d46a9903acb1204deefad88162ac77404f052c7d51ab9d5851d2335ed2fda1e8"}, + {file = "valkey_glide-2.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e5c17169d07564f0cc23b78c4904c8e03063c8750d8ccf413d229ce9a3092d5"}, + {file = "valkey_glide-2.5.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:75da4cb54fe6a0e03a78681237545aa0638dd5a6dd0ff51af6ff4cacf9f99443"}, + {file = "valkey_glide-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:787010bbce1b8f270ae5d74ca8af15bca3bbc625e1a5467fb01025864bc4e792"}, + {file = "valkey_glide-2.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de2e202a8376c6b67735bd589ce0e46683f8a782fcb1745b0b44cce768e13856"}, + {file = "valkey_glide-2.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ab1c7ce52b098fcb0ee6223136a024fc90a8ae1c600db298c30c044d1b47ee2"}, + {file = "valkey_glide-2.5.1-cp313-cp313-macosx_10_7_x86_64.whl", hash = "sha256:55c87ecc3146480adaddf42d659b688089ca417596208ca9df5c2249c8f9e6fe"}, + {file = "valkey_glide-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae6b0cedd3d2aafdc53c460bdfe4d0319d10301fd2128a56d41486e49de88dfe"}, + {file = "valkey_glide-2.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8a3c5298899ea18fc7e40438643e2f8ee15955fe9bba66ce5daf3dfd29462f2"}, + {file = "valkey_glide-2.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5e0094d576ce99a2453c9fdd923f70732db6f4b59a722c27c420343d96b75d"}, + {file = "valkey_glide-2.5.1-cp314-cp314-macosx_10_7_x86_64.whl", hash = "sha256:1bc5d0e0241aa5b78c398b981fe19801e36c09572c1dac69286a9a3c817e0b35"}, + {file = "valkey_glide-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec658ed7770c05f3ea431ca1301e47d0a24cf27c4c23ca30eb030f7e0ac7488d"}, + {file = "valkey_glide-2.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2cdb26952b650e25af4a14360f65364b8015e49e2967779ba850c9e62c1f237"}, + {file = "valkey_glide-2.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:696635581f0692f640b94021b4cfbc2ec01108f6ae02f25dfea3cfdaa98331de"}, + {file = "valkey_glide-2.5.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:f5e733adc4718b93c5b228de7e543a6641e231d5839202ce7b22487e079059e7"}, + {file = "valkey_glide-2.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ffd8f0fdb6ef982161a2350ad1562e5e345bea263b81304c7282f3385deac20"}, + {file = "valkey_glide-2.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6b81727c4d7a3b49f10172787296c87d6ef8d729fb958de358b278d5a6c563f"}, + {file = "valkey_glide-2.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12fff287f02caa9302d7c64d0520c3cee86e0f952ec55a537a4f9378889265d7"}, + {file = "valkey_glide-2.5.1-pp311-pypy311_pp73-macosx_10_7_x86_64.whl", hash = "sha256:f656ef6e722d6ab4a28bc66e36ef616dff28622bc712c41f4172ac1e7c4826d8"}, + {file = "valkey_glide-2.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:65b2add9f19a2e48080130ee968af45dc14e8457b5db20262703a4604afc478b"}, + {file = "valkey_glide-2.5.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9b725afe94f0515e296003942d6c96c5090a956f2217f6305dc87fa14dfa51"}, + {file = "valkey_glide-2.5.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a131c08951b865f753f1bd42f7db19c6a96e7c4faf9dc78718f8909fc95eb8b4"}, + {file = "valkey_glide-2.5.1.tar.gz", hash = "sha256:b2efb23c987a995ad6ce2a72616996638137dd2504ac262bdd6c045378394090"}, ] [package.dependencies] -anyio = ">=4.9.0" -protobuf = ">=6.20" +anyio = {version = ">=4.9.0", extras = ["trio"]} +cffi = ">=1.0.0" +protobuf = ">=3.20" sniffio = "*" typing-extensions = {version = ">=4.8.0", markers = "python_version < \"3.11\""} @@ -5223,4 +5333,4 @@ valkey = ["valkey-glide"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0.0" -content-hash = "325feb84a282310b016844cd52100d9a18841c17bb80d556c550fe8f83c86f04" +content-hash = "368940a68ae8b3bbb72ab67bbb91e4da4170e07a7faf28011343cfcb809ce84a" diff --git a/pyproject.toml b/pyproject.toml index f4c2d56541e..0510319e5f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aws_lambda_powertools" -version = "3.29.0" +version = "3.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"] @@ -50,9 +50,9 @@ fastjsonschema = { version = "^2.14.5", optional = true } pydantic = { version = "^2.4.0", optional = true } pydantic-settings = {version = "^2.6.1", optional = true} boto3 = { version = "^1.34.32", optional = true } -redis = { version = ">=4.4,<8.0", optional = true } +redis = { version = ">=4.4,<9.0", optional = true } valkey-glide = { version = ">=1.3.5,<3.0", optional = true } -aws-encryption-sdk = { version = ">=3.3.1,!=4.0.0,!=4.0.1,!=4.0.2,!=4.0.3,!=4.0.4,<5.0.0", optional = true } +aws-encryption-sdk = { version = ">=4.0.5,<5.0.0", optional = true } jsonpath-ng = { version = "^1.6.0", optional = true } datadog-lambda = { version = ">=8.114.0,<9.0.0", optional = true } avro = { version = "^1.12.0", optional = true } @@ -116,7 +116,7 @@ types-python-dateutil = "^2.8.19.6" aws-cdk-aws-appsync-alpha = "^2.59.0a0" httpx = ">=0.23.3,<0.29.0" sentry-sdk = ">=1.22.2,<3.0.0" -ruff = ">=0.5.1,<0.15.18" +ruff = ">=0.5.1,<0.16.5" retry2 = "^0.9.5" pytest-socket = ">=0.6,<0.9" types-redis = "^4.6.0.7" @@ -124,12 +124,12 @@ testcontainers = { extras = ["redis"], version = ">=3.7.1,<5.0.0" } multiprocess = "^0.70.16" boto3-stubs = {extras = ["appconfig", "appconfigdata", "cloudformation", "cloudwatch", "dynamodb", "lambda", "logs", "s3", "secretsmanager", "ssm", "xray"], version = "^1.34.139"} nox = ">=2024.4.15,<2027.0.0" -mkdocstrings-python = "^1.13.0" +mkdocstrings-python = ">=1.13,<3.0" mkdocs-llmstxt = ">=0.2,<0.5" avro = "^1.12.0" protobuf = ">=6.30.2,<8.0.0" types-protobuf = ">=6.30.2.20250516,<8.0.0.0" -ty = ">=0.0.23,<0.0.36" +ty = ">=0.0.23,<0.0.50" [tool.coverage.run] source = ["aws_lambda_powertools"] diff --git a/ruff.toml b/ruff.toml index fa89816a7de..1bdb7ffa4ae 100644 --- a/ruff.toml +++ b/ruff.toml @@ -33,6 +33,7 @@ lint.select = [ lint.ignore = [ "W291", # https://beta.ruff.rs/docs/rules/trailing-whitespace/ "PLR0913", # https://beta.ruff.rs/docs/rules/too-many-arguments/ + "PLR0917", # https://docs.astral.sh/ruff/rules/too-many-positional-arguments/ "PLR2004", #https://beta.ruff.rs/docs/rules/magic-value-comparison/ "PLW0603", #https://beta.ruff.rs/docs/rules/global-statement/ "B904", # raise-without-from-inside-except - disabled temporarily @@ -41,6 +42,9 @@ lint.ignore = [ "A005", "TC006", # https://docs.astral.sh/ruff/rules/runtime-cast-value/ "PLC0415", # https://docs.astral.sh/ruff/rules/import-outside-top-level/ + "UP006", + "UP007", + "UP035", ] # Exclude files and directories @@ -62,7 +66,7 @@ exclude = [ # Maximum line length line-length = 120 -target-version = "py38" +target-version = "py310" fix = true lint.fixable = ["I", "COM812", "W"] diff --git a/tests/e2e/utils/data_fetcher/common.py b/tests/e2e/utils/data_fetcher/common.py index 1300daa55aa..61a56386251 100644 --- a/tests/e2e/utils/data_fetcher/common.py +++ b/tests/e2e/utils/data_fetcher/common.py @@ -2,7 +2,7 @@ import time from concurrent.futures import Future, ThreadPoolExecutor from datetime import datetime -from typing import List, Optional, Tuple +from typing import List, Tuple import boto3 import requests @@ -18,8 +18,8 @@ class GetLambdaResponseOptions(BaseModel): lambda_arn: str - payload: Optional[str] = None - client: Optional[LambdaClient] = None + payload: str | None = None + client: LambdaClient | None = None raise_on_error: bool = True model_config = ConfigDict( @@ -29,8 +29,8 @@ class GetLambdaResponseOptions(BaseModel): def get_lambda_response( lambda_arn: str, - payload: Optional[str] = None, - client: Optional[LambdaClient] = None, + payload: str | None = None, + client: LambdaClient | None = None, raise_on_error: bool = True, ) -> GetLambdaResponse: """Invoke function synchronously @@ -39,9 +39,9 @@ def get_lambda_response( ---------- lambda_arn : str Lambda function ARN to invoke - payload : Optional[str], optional + payload : str | None, optional JSON payload for Lambda invocation, by default None - client : Optional[LambdaClient], optional + client : LambdaClient | None, optional Boto3 Lambda SDK client, by default None raise_on_error : bool, optional Whether to raise exception upon invocation error, by default True diff --git a/tests/e2e/utils/data_fetcher/logs.py b/tests/e2e/utils/data_fetcher/logs.py index dc036cba36e..eaa783fbba5 100644 --- a/tests/e2e/utils/data_fetcher/logs.py +++ b/tests/e2e/utils/data_fetcher/logs.py @@ -1,6 +1,6 @@ import json from datetime import datetime -from typing import List, Optional, Union +from typing import Any, List, Union import boto3 from mypy_boto3_logs.client import CloudWatchLogsClient @@ -14,12 +14,12 @@ class Log(BaseModel, extra="allow"): message: Union[dict, str] timestamp: str service: str - cold_start: Optional[bool] = None - function_name: Optional[str] = None - function_memory_size: Optional[str] = None - function_arn: Optional[str] = None - function_request_id: Optional[str] = None - xray_trace_id: Optional[str] = None + cold_start: bool | None = None + function_name: str | None = None + function_memory_size: str | None = None + function_arn: str | None = None + function_request_id: str | None = None + xray_trace_id: str | None = None class LogFetcher: @@ -27,8 +27,8 @@ def __init__( self, function_name: str, start_time: datetime, - log_client: Optional[CloudWatchLogsClient] = None, - filter_expression: Optional[str] = None, + log_client: CloudWatchLogsClient | None = None, + filter_expression: str | None = None, minimum_log_entries: int = 1, ): """Fetch and expose Powertools for AWS Lambda (Python) Logger logs from CloudWatch Logs @@ -39,9 +39,9 @@ def __init__( Name of Lambda function to fetch logs for start_time : datetime Start date range to filter traces - log_client : Optional[CloudWatchLogsClient], optional + log_client : CloudWatchLogsClient | None, optional Amazon CloudWatch Logs Client, by default boto3.client('logs) - filter_expression : Optional[str], optional + filter_expression : str | None, optional CloudWatch Logs Filter Pattern expression, by default "message" minimum_log_entries: int Minimum number of log entries to be retrieved before exhausting retry attempts @@ -54,14 +54,14 @@ def __init__( self.minimum_log_entries = minimum_log_entries self.logs: List[Log] = self._get_logs() - def get_log(self, key: str, value: Optional[any] = None) -> List[Log]: + def get_log(self, key: str, value: Any | None = None) -> List[Log]: """Get logs based on key or key and value Parameters ---------- key : str Log key name - value : Optional[any], optional + value : Any | None, optional Log value, by default None Returns @@ -132,8 +132,8 @@ def get_logs( function_name: str, start_time: datetime, minimum_log_entries: int = 1, - filter_expression: Optional[str] = None, - log_client: Optional[CloudWatchLogsClient] = None, + filter_expression: str | None = None, + log_client: CloudWatchLogsClient | None = None, ) -> LogFetcher: """_summary_ @@ -145,9 +145,9 @@ def get_logs( Start date range to filter traces minimum_log_entries : int Minimum number of log entries to be retrieved before exhausting retry attempts - log_client : Optional[CloudWatchLogsClient], optional + log_client : CloudWatchLogsClient | None, optional Amazon CloudWatch Logs Client, by default boto3.client('logs) - filter_expression : Optional[str], optional + filter_expression : str | None, optional CloudWatch Logs Filter Pattern expression, by default "message" Returns diff --git a/tests/e2e/utils/data_fetcher/traces.py b/tests/e2e/utils/data_fetcher/traces.py index f8b364fc97c..4309fa0d520 100644 --- a/tests/e2e/utils/data_fetcher/traces.py +++ b/tests/e2e/utils/data_fetcher/traces.py @@ -1,6 +1,6 @@ import json from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional +from typing import TYPE_CHECKING, Any, Dict, Generator, List import boto3 from botocore.paginate import PageIterator @@ -17,10 +17,10 @@ class TraceSubsegment(BaseModel): name: str start_time: float end_time: float - aws: Optional[dict] = None - subsegments: Optional[List["TraceSubsegment"]] = None - annotations: Optional[Dict[str, Any]] = None - metadata: Optional[Dict[str, Dict[str, Any]]] = None + aws: dict | None = None + subsegments: List["TraceSubsegment"] | None = None + annotations: Dict[str, Any] | None = None + metadata: Dict[str, Dict[str, Any]] | None = None class TraceDocument(BaseModel): @@ -29,10 +29,10 @@ class TraceDocument(BaseModel): start_time: float end_time: float trace_id: str - parent_id: Optional[str] = None + parent_id: str | None = None aws: Dict origin: str - subsegments: Optional[List[TraceSubsegment]] = None + subsegments: List[TraceSubsegment] | None = None class TraceFetcher: @@ -42,11 +42,11 @@ def __init__( self, filter_expression: str, start_date: datetime, - end_date: Optional[datetime] = None, - xray_client: Optional[XRayClient] = None, - exclude_segment_name: Optional[List[str]] = None, - resource_name: Optional[List[str]] = None, - origin: Optional[List[str]] = None, + end_date: datetime | None = None, + xray_client: XRayClient | None = None, + exclude_segment_name: List[str] | None = None, + resource_name: List[str] | None = None, + origin: List[str] | None = None, minimum_traces: int = 1, ): """Fetch and expose traces from X-Ray based on parameters @@ -67,15 +67,15 @@ def __init__( see: https://docs.aws.amazon.com/xray/latest/devguide/xray-console-filters.html start_date : datetime Start date range to filter traces - end_date : Optional[datetime], optional + end_date : datetime | None, optional End date range to filter traces, by default 5 minutes past start_date - xray_client : Optional[XRayClient], optional + xray_client : XRayClient | None, optional AWS X-Ray SDK Client, by default boto3.client('xray') - exclude_segment_name : Optional[List[str]], optional + exclude_segment_name : List[str] | None, optional Name of segments to exclude, by default ["Initialization", "Invocation", "Overhead"] - resource_name : Optional[List[str]], optional + resource_name : List[str] | None, optional Name of resource to filter traces (e.g., function name), by default None - origin : Optional[List[str]], optional + origin : List[str] | None, optional Trace origin name to filter traces, by default ["AWS::Lambda::Function"] minimum_traces : int Minimum number of traces to be retrieved before exhausting retry attempts @@ -106,7 +106,7 @@ def __init__( self.trace_documents = self._get_trace_documents(trace_ids) self.subsegments = self._get_subsegments() - def get_annotation(self, key: str, value: Optional[any] = None) -> List: + def get_annotation(self, key: str, value: Any | None = None) -> List: return [ annotation for annotation in self.annotations @@ -223,11 +223,11 @@ def _get_trace_documents(self, trace_ids: List[str]) -> Dict[str, TraceDocument] def get_traces( filter_expression: str, start_date: datetime, - end_date: Optional[datetime] = None, - xray_client: Optional[XRayClient] = None, - exclude_segment_name: Optional[List[str]] = None, - resource_name: Optional[List[str]] = None, - origin: Optional[List[str]] = None, + end_date: datetime | None = None, + xray_client: XRayClient | None = None, + exclude_segment_name: List[str] | None = None, + resource_name: List[str] | None = None, + origin: List[str] | None = None, minimum_traces: int = 1, ) -> TraceFetcher: """Fetch traces from AWS X-Ray @@ -239,15 +239,15 @@ def get_traces( see: https://docs.aws.amazon.com/xray/latest/devguide/xray-console-filters.html start_date : datetime Start date range to filter traces - end_date : Optional[datetime], optional + end_date : datetime | None, optional End date range to filter traces, by default 5 minutes past start_date - xray_client : Optional[XRayClient], optional + xray_client : XRayClient | None, optional AWS X-Ray SDK Client, by default boto3.client('xray') - exclude_segment_name : Optional[List[str]], optional + exclude_segment_name : List[str] | None, optional Name of segments to exclude, by default ["Initialization", "Invocation", "Overhead"] - resource_name : Optional[List[str]], optional + resource_name : List[str] | None, optional Name of resource to filter traces (e.g., function name), by default None - origin : Optional[List[str]], optional + origin : List[str] | None, optional Trace origin name to filter traces, by default ["AWS::Lambda::Function"] minimum_traces : int Minimum number of traces to be retrieved before exhausting retry attempts diff --git a/tests/events/eventBridgeEvent.json b/tests/events/eventBridgeEvent.json index 65872cf9a34..f7c6d4c0d37 100644 --- a/tests/events/eventBridgeEvent.json +++ b/tests/events/eventBridgeEvent.json @@ -6,12 +6,9 @@ "account": "111122223333", "time": "2017-12-22T18:43:48Z", "region": "us-west-1", - "resources": [ - "arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0" - ], + "resources": [], "detail": { "instance_id": "i-1234567890abcdef0", "state": "terminated" - }, - "replay-name": "replay_archive" -} + } +} \ No newline at end of file diff --git a/tests/functional/batch/_pydantic/sample_models.py b/tests/functional/batch/_pydantic/sample_models.py index c2912b3f8a3..d3974ccce82 100644 --- a/tests/functional/batch/_pydantic/sample_models.py +++ b/tests/functional/batch/_pydantic/sample_models.py @@ -1,5 +1,5 @@ import json -from typing import Dict, Optional +from typing import Dict from pydantic import field_validator @@ -44,8 +44,8 @@ def transform_message_to_dict(cls, value: Dict[Literal["S"], str]): class OrderDynamoDBChangeRecord(DynamoDBStreamChangedRecordModel): - NewImage: Optional[OrderDynamoDB] = None - OldImage: Optional[OrderDynamoDB] = None + NewImage: OrderDynamoDB | None = None + OldImage: OrderDynamoDB | None = None class OrderDynamoDBRecord(DynamoDBStreamRecordModel): diff --git a/tests/functional/batch/_pydantic/test_utilities_batch_pydantic.py b/tests/functional/batch/_pydantic/test_utilities_batch_pydantic.py index 382cbdb0335..1e6a0452d2a 100644 --- a/tests/functional/batch/_pydantic/test_utilities_batch_pydantic.py +++ b/tests/functional/batch/_pydantic/test_utilities_batch_pydantic.py @@ -1,7 +1,7 @@ import json import uuid from random import randint -from typing import Any, Awaitable, Callable, Dict, Optional +from typing import Any, Awaitable, Callable, Dict import pytest from pydantic import BaseModel, field_validator @@ -311,8 +311,8 @@ def transform_message_to_dict(cls, value: Dict[Literal["S"], str]): return json.loads(value) class OrderDynamoDBChangeRecord(DynamoDBStreamChangedRecordModel): - NewImage: Optional[OrderDynamoDB] = None - OldImage: Optional[OrderDynamoDB] = None + NewImage: OrderDynamoDB | None = None + OldImage: OrderDynamoDB | None = None class OrderDynamoDBRecord(DynamoDBStreamRecordModel): dynamodb: OrderDynamoDBChangeRecord @@ -355,8 +355,8 @@ def transform_message_to_dict(cls, value: Dict[Literal["S"], str]): return json.loads(value) class OrderDynamoDBChangeRecord(DynamoDBStreamChangedRecordModel): - NewImage: Optional[OrderDynamoDB] = None - OldImage: Optional[OrderDynamoDB] = None + NewImage: OrderDynamoDB | None = None + OldImage: OrderDynamoDB | None = None class OrderDynamoDBRecord(DynamoDBStreamRecordModel): dynamodb: OrderDynamoDBChangeRecord diff --git a/tests/functional/batch/required_dependencies/test_utilities_batch.py b/tests/functional/batch/required_dependencies/test_utilities_batch.py index 43c2aa16191..e8e544c127e 100644 --- a/tests/functional/batch/required_dependencies/test_utilities_batch.py +++ b/tests/functional/batch/required_dependencies/test_utilities_batch.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import logging import uuid from random import randint from typing import TYPE_CHECKING, Any @@ -861,3 +862,76 @@ async def simple_async_handler(record: SQSRecord): # THEN record is processed successfully using asyncio.run() assert result == {"batchItemFailures": []} assert result == {"batchItemFailures": []} + + +def test_batch_processor_logs_exception_with_injected_logger(sqs_event_factory, caplog): + fail_record = sqs_event_factory("fail") + success_record = sqs_event_factory("success") + + def handler(record): + if "fail" in record["body"]: + raise ValueError("intentional failure") + return record["body"] + + test_logger = logging.getLogger("test_logger") + processor = BatchProcessor(event_type=EventType.SQS, logger=test_logger) + + with caplog.at_level(logging.WARNING, logger="test_logger"): + process_partial_response( + event={"Records": [fail_record, success_record]}, + record_handler=handler, + processor=processor, + ) + + warning_records = [r for r in caplog.records if r.levelno == logging.WARNING] + assert len(warning_records) == 1, f"Expected 1 WARNING log, got {len(warning_records)}" + assert "intentional failure" in warning_records[0].getMessage() or warning_records[0].exc_info is not None + assert warning_records[0].exc_info is not None, "Expected exc_info (traceback) in log record" + assert warning_records[0].exc_info[0] is ValueError + + +def test_batch_processor_does_not_log_without_injected_logger(sqs_event_factory, caplog): + fail_record = sqs_event_factory("fail") + + def handler(record): + raise ValueError("intentional failure") + + processor = BatchProcessor(event_type=EventType.SQS, raise_on_entire_batch_failure=False, logger=None) + + with caplog.at_level(logging.WARNING, logger="aws_lambda_powertools.utilities.batch.base"): + process_partial_response( + event={"Records": [fail_record]}, + record_handler=handler, + processor=processor, + ) + + warning_records = [r for r in caplog.records if r.levelno == logging.WARNING] + assert len(warning_records) == 0, "Expected no WARNING logs when logger is None" + + +def test_sqs_fifo_circuit_breaker_does_not_log(sqs_event_fifo_factory, caplog): + failing_record = sqs_event_fifo_factory("fail", "group-1") + short_circuited_record = sqs_event_fifo_factory("would-succeed", "group-1") + + def handler(record): + if "fail" in record["body"]: + raise ValueError("first record failure") + return record["body"] + + test_logger = logging.getLogger("test_logger") + processor = SqsFifoPartialProcessor(logger=test_logger) + processor.raise_on_entire_batch_failure = False + + with caplog.at_level(logging.WARNING, logger="test_logger"): + process_partial_response( + event={"Records": [failing_record, short_circuited_record]}, + record_handler=handler, + processor=processor, + ) + + warning_records = [r for r in caplog.records if r.levelno == logging.WARNING] + assert len(warning_records) == 1, ( + f"Expected exactly 1 WARNING (real exception only), got {len(warning_records)}: " + + str([r.getMessage() for r in warning_records]) + ) + assert warning_records[0].exc_info[0] is ValueError diff --git a/tests/functional/circuit_breaker/__init__.py b/tests/functional/circuit_breaker/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/circuit_breaker/conftest.py b/tests/functional/circuit_breaker/conftest.py new file mode 100644 index 00000000000..30925dfc5e8 --- /dev/null +++ b/tests/functional/circuit_breaker/conftest.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import threading + +import pytest + +import aws_lambda_powertools.utilities.circuit_breaker.base as base_module +from aws_lambda_powertools.utilities.circuit_breaker.base import CircuitBreakerHandler +from aws_lambda_powertools.utilities.circuit_breaker.persistence.base import ( + CircuitBreakerExistingLockError, + CircuitBreakerPersistenceLayer, +) +from aws_lambda_powertools.utilities.circuit_breaker.persistence.record import CircuitStateRecord +from aws_lambda_powertools.utilities.circuit_breaker.states import CircuitState + + +class FakePersistence(CircuitBreakerPersistenceLayer): + """In-memory store for exercising the handler state machine without DynamoDB.""" + + # Each DynamoDB operation is atomic, including the conditional put; the fake must be + # too, or threaded tests race inside the fake instead of the code under test. + # Class-level so store instances sharing a db (multi-environment tests) share it. + _db_lock = threading.Lock() + + def __init__(self): + self.db: dict[str, CircuitStateRecord] = {} + super().__init__() + + def _get_record(self, name: str) -> CircuitStateRecord | None: + with self._db_lock: + if name not in self.db: + return None + stored = self.db[name] + # Return a copy so the handler can't mutate stored state by reference. + return CircuitStateRecord( + name=stored.name, + state=stored.state, + failure_count=stored.failure_count, + opened_at=stored.opened_at, + half_open_owner=stored.half_open_owner, + probe_lease_expiry=stored.probe_lease_expiry, + ) + + def _put_record( + self, + record: CircuitStateRecord, + condition: str | None = None, + expected_opened_at: int | None = None, + ) -> None: + with self._db_lock: + if condition == "half_open": + existing = self.db.get(record.name) + now = CircuitBreakerHandler._now() + + # Mirror the DynamoDB condition: two valid paths + # Path 1: state=OPEN AND no owner (AND opened_at matches if provided) + # Path 2: state=HALF_OPEN AND probe_lease_expiry <= now (lease takeover) + fresh_election_ok = existing is None or ( + existing.state == CircuitState.OPEN + and existing.half_open_owner is None + and (expected_opened_at is None or existing.opened_at == expected_opened_at) + ) + lease_takeover_ok = ( + existing is not None + and existing.state == CircuitState.HALF_OPEN + and existing.probe_lease_expiry is not None + and now >= existing.probe_lease_expiry + ) + + if not fresh_election_ok and not lease_takeover_ok: + raise CircuitBreakerExistingLockError + self.db[record.name] = record + + def _update_record(self, record: CircuitStateRecord) -> None: + # Mirror DynamoDB UpdateItem semantics: a partial merge driven by which + # attributes the backend actually writes, NOT a wholesale replace. This is + # what exposes attributes the update path forgets to clear (e.g. a stale + # half_open_owner left behind on reopen). + with self._db_lock: + existing = self.db.get(record.name) + if existing is None: + self.db[record.name] = record + return + existing.state = record.state + existing.failure_count = record.failure_count + existing.expiry_timestamp = record.expiry_timestamp + # Leaving HALF_OPEN (close or reopen) always releases the probe-owner lock and + # probe lease; only opened_at differs between the two transitions. + existing.half_open_owner = None + existing.probe_lease_expiry = None + existing.opened_at = record.opened_at + + +@pytest.fixture +def store() -> FakePersistence: + return FakePersistence() + + +@pytest.fixture(autouse=True) +def reset_local_counters(): + """Clear the per-environment module-level counters between tests.""" + base_module._LOCAL_FAILURES.clear() + base_module._LOCAL_SUCCESSES.clear() + base_module._LAST_OBSERVED_STATE.clear() + yield + base_module._LOCAL_FAILURES.clear() + base_module._LOCAL_SUCCESSES.clear() + base_module._LAST_OBSERVED_STATE.clear() + + +@pytest.fixture +def now() -> int: + return base_module.CircuitBreakerHandler._now() diff --git a/tests/functional/circuit_breaker/test_circuit_breaker.py b/tests/functional/circuit_breaker/test_circuit_breaker.py new file mode 100644 index 00000000000..1db372ee900 --- /dev/null +++ b/tests/functional/circuit_breaker/test_circuit_breaker.py @@ -0,0 +1,836 @@ +from __future__ import annotations + +import threading +import time +import warnings + +import pytest + +import aws_lambda_powertools.utilities.circuit_breaker.base as base_module +from aws_lambda_powertools.utilities.circuit_breaker import ( + CircuitBreakerConfig, + CircuitBreakerOpenError, + CircuitState, + CircuitTransition, + circuit_breaker, +) +from aws_lambda_powertools.utilities.circuit_breaker.exceptions import CircuitBreakerError +from aws_lambda_powertools.utilities.circuit_breaker.persistence.record import CircuitStateRecord + +# All tests disable the local read cache (max_age=0) so each call re-reads the fake store. + + +def test_closed_circuit_returns_result_and_writes_nothing(store): + @circuit_breaker(name="c", persistence_store=store, config=CircuitBreakerConfig(local_cache_max_age=0)) + def call(value): + return value * 2 + + assert call(21) == 42 + assert store.db == {}, "healthy path must not write to the store" + + +def test_trips_open_after_failure_threshold(store): + config = CircuitBreakerConfig(failure_threshold=3, local_cache_max_age=0) + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + raise ConnectionError("downstream down") + + for _ in range(3): + with pytest.raises(ConnectionError): + call() + + assert store.db["c"].state == CircuitState.OPEN + assert store.db["c"].opened_at is not None + + +def test_open_with_callback_returns_callback_value_without_calling_protected(store, now): + store.db["c"] = CircuitStateRecord(name="c", state=CircuitState.OPEN, failure_count=5, opened_at=now) + protected_ran = {"value": False} + + def on_open(order, circuit): + return {"buffered": order, "state": str(circuit.state)} + + config = CircuitBreakerConfig(recovery_timeout=9999, local_cache_max_age=0) + + @circuit_breaker(name="c", persistence_store=store, on_circuit_open=on_open, config=config) + def charge(order): + protected_ran["value"] = True + return f"charged {order}" + + result = charge({"id": 1}) + assert result == {"buffered": {"id": 1}, "state": "OPEN"} + assert protected_ran["value"] is False + + +def test_open_without_callback_raises_with_circuit_info(store, now): + store.db["c"] = CircuitStateRecord(name="c", state=CircuitState.OPEN, failure_count=7, opened_at=now) + config = CircuitBreakerConfig(recovery_timeout=9999, local_cache_max_age=0) + + @circuit_breaker(name="c", persistence_store=store, config=config) + def charge(order): + return f"charged {order}" + + with pytest.raises(CircuitBreakerOpenError) as exc_info: + charge({"id": 1}) + + assert exc_info.value.circuit.name == "c" + assert exc_info.value.circuit.failure_count == 7 + + +def test_half_open_probe_success_closes_after_success_threshold(store, now): + store.db["c"] = CircuitStateRecord(name="c", state=CircuitState.OPEN, failure_count=5, opened_at=now - 100) + config = CircuitBreakerConfig(recovery_timeout=30, success_threshold=2, local_cache_max_age=0) + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + return "ok" + + call() # wins the probe lock, state becomes HALF_OPEN + assert store.db["c"].state == CircuitState.HALF_OPEN + + call() # second consecutive probe success closes the circuit + assert store.db["c"].state == CircuitState.CLOSED + + +def test_half_open_probe_failure_reopens(store, now): + store.db["c"] = CircuitStateRecord(name="c", state=CircuitState.OPEN, failure_count=5, opened_at=now - 100) + config = CircuitBreakerConfig(recovery_timeout=30, success_threshold=2, local_cache_max_age=0) + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + raise ConnectionError("still down") + + with pytest.raises(ConnectionError): + call() + + assert store.db["c"].state == CircuitState.OPEN + + +# --------------------------------------------------------------------------- bug regressions + + +def test_half_open_can_be_reacquired_after_failed_probe_reopens(store, now): + # Bug #1: a failed probe (HALF_OPEN -> OPEN) must clear the half_open_owner so a + # later recovery window can elect a prober again. Otherwise the circuit is stuck + # OPEN forever because attribute_not_exists(half_open_owner) never holds again. + store.db["c"] = CircuitStateRecord(name="c", state=CircuitState.OPEN, failure_count=5, opened_at=now - 100) + config = CircuitBreakerConfig(recovery_timeout=30, success_threshold=2, local_cache_max_age=0) + + outcomes = iter([ConnectionError("still down"), "recovered"]) + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + result = next(outcomes) + if isinstance(result, Exception): + raise result + return result + + # First recovery window: env wins the probe, probe fails, circuit reopens. + with pytest.raises(ConnectionError): + call() + assert store.db["c"].state == CircuitState.OPEN + assert store.db["c"].half_open_owner is None, "reopen must release the probe lock" + + # Second recovery window (opened_at is fresh, push it into the past again). + store.db["c"].opened_at = now - 100 + # Downstream recovered: the env must be able to acquire the probe again and run it. + assert call() == "recovered" + assert store.db["c"].state == CircuitState.HALF_OPEN + + +def test_open_callback_receives_keyword_arguments_intact(store, now): + # Bug #3: the callback must receive the same kwargs the protected function got, + # as kwargs, not flattened into positional values. + store.db["c"] = CircuitStateRecord(name="c", state=CircuitState.OPEN, failure_count=5, opened_at=now) + config = CircuitBreakerConfig(recovery_timeout=9999, local_cache_max_age=0) + + captured = {} + + def on_open(order, customer, circuit): + captured["order"] = order + captured["customer"] = customer + return "buffered" + + @circuit_breaker(name="c", persistence_store=store, on_circuit_open=on_open, config=config) + def charge(order, customer): + return "charged" + + # Called entirely with keyword arguments, deliberately out of signature order. + assert charge(customer="alice", order={"id": 1}) == "buffered" + assert captured["order"] == {"id": 1} + assert captured["customer"] == "alice" + + +def test_consecutive_failures_trip_but_a_success_resets_the_streak(store): + # The failure counter is per-environment and counts *consecutive* failures as this + # env sees them: any success in between must reset the streak. + config = CircuitBreakerConfig(failure_threshold=3, local_cache_max_age=0) + + should_fail = {"value": True} + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + if should_fail["value"]: + raise ConnectionError("down") + return "ok" + + # Two failures, then a success: the streak resets, so the circuit must NOT be tripping. + for _ in range(2): + with pytest.raises(ConnectionError): + call() + should_fail["value"] = False + assert call() == "ok" + assert "c" not in store.db + + # A fresh run of 3 consecutive failures from here must trip it. + should_fail["value"] = True + for _ in range(3): + with pytest.raises(ConnectionError): + call() + assert store.db["c"].state == CircuitState.OPEN + + +def test_circuit_can_retrip_after_a_previous_close(store): + # Regression guard: a healthy circuit's steady state is a *persisted CLOSED* record. + # Reading that record must not reset the running failure counter, or the circuit + # could never trip again after it has closed once. + config = CircuitBreakerConfig(failure_threshold=3, local_cache_max_age=0) + + # Pretend a prior recovery left a persisted CLOSED record behind. + store.db["c"] = CircuitStateRecord(name="c", state=CircuitState.CLOSED, failure_count=0) + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + raise ConnectionError("down again") + + for _ in range(3): + with pytest.raises(ConnectionError): + call() + + assert store.db["c"].state == CircuitState.OPEN, "circuit must re-trip even with a prior CLOSED record present" + + +def test_full_lifecycle_survives_multiple_recovery_cycles(store): + # End-to-end guard tying #1 together: the circuit must cycle OPEN -> HALF_OPEN -> + # CLOSED, re-trip, survive a failed probe (which reopens and must release the lock), + # and then recover again. Before the owner-release fix the second recovery dead-locked. + config = CircuitBreakerConfig( + failure_threshold=2, + recovery_timeout=30, + success_threshold=2, + local_cache_max_age=0, + ) + mode = {"value": "fail"} + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + if mode["value"] == "fail": + raise ConnectionError("down") + return "ok" + + def elapse_recovery_window(): + store.db["c"].opened_at -= 100 + + # Trip open. + for _ in range(2): + with pytest.raises(ConnectionError): + call() + assert store.db["c"].state == CircuitState.OPEN + + # Recover through two successful probes. + mode["value"] = "ok" + elapse_recovery_window() + assert call() == "ok" + assert store.db["c"].state == CircuitState.HALF_OPEN + assert call() == "ok" + assert store.db["c"].state == CircuitState.CLOSED + + # Re-trip from the now-CLOSED state. + mode["value"] = "fail" + for _ in range(2): + with pytest.raises(ConnectionError): + call() + assert store.db["c"].state == CircuitState.OPEN + + # A failed probe reopens it and must release the probe lock. + elapse_recovery_window() + with pytest.raises(ConnectionError): + call() + assert store.db["c"].state == CircuitState.OPEN + assert store.db["c"].half_open_owner is None + + # The lock being free, recovery must be possible again. + mode["value"] = "ok" + elapse_recovery_window() + assert call() == "ok" + assert store.db["c"].state == CircuitState.HALF_OPEN + assert call() == "ok" + assert store.db["c"].state == CircuitState.CLOSED + + +def test_opened_at_zero_is_treated_as_a_real_timestamp(store): + # Bug #7: opened_at == 0 is a valid (if pathological) epoch timestamp, not "missing". + # `record.opened_at or self._now()` wrongly re-anchors it to now, pinning OPEN forever. + store.db["c"] = CircuitStateRecord(name="c", state=CircuitState.OPEN, failure_count=5, opened_at=0) + config = CircuitBreakerConfig(recovery_timeout=30, success_threshold=1, local_cache_max_age=0) + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + return "recovered" + + # opened_at=0 is far in the past, so the recovery window has long elapsed: + # the call must be allowed to probe, not short-circuited. + assert call() == "recovered" + assert store.db["c"].state in (CircuitState.HALF_OPEN, CircuitState.CLOSED) + + +def test_ignored_exception_does_not_trip_circuit(store): + config = CircuitBreakerConfig( + failure_threshold=2, + handled_exceptions=(ConnectionError,), + local_cache_max_age=0, + ) + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + raise ValueError("caller error, not a downstream failure") + + for _ in range(5): + with pytest.raises(ValueError): + call() + + assert "c" not in store.db + + +def test_disabled_env_bypasses_circuit(store, now, monkeypatch): + monkeypatch.setenv("POWERTOOLS_CIRCUIT_BREAKER_DISABLED", "true") + store.db["c"] = CircuitStateRecord(name="c", state=CircuitState.OPEN, failure_count=9, opened_at=now) + config = CircuitBreakerConfig(recovery_timeout=9999, local_cache_max_age=0) + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + return "ran anyway" + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + assert call() == "ran anyway" + + +def test_config_is_optional(store): + @circuit_breaker(name="c", persistence_store=store) + def call(): + return "ok" + + assert call() == "ok" + + +# --------------------------------------------------------------------------- on_transition hook + + +def test_on_transition_fires_for_each_state_change(store): + transitions: list[CircuitTransition] = [] + config = CircuitBreakerConfig( + failure_threshold=2, + recovery_timeout=30, + success_threshold=1, + local_cache_max_age=0, + ) + mode = {"value": "fail"} + + @circuit_breaker( + name="c", + persistence_store=store, + on_transition=transitions.append, + config=config, + ) + def call(): + if mode["value"] == "fail": + raise ConnectionError("down") + return "ok" + + # CLOSED -> OPEN after 2 failures. + for _ in range(2): + with pytest.raises(ConnectionError): + call() + # OPEN -> HALF_OPEN (election) -> CLOSED (success_threshold=1) on the recovery probe. + mode["value"] = "ok" + store.db["c"].opened_at -= 100 + assert call() == "ok" + + pairs = [(t.from_state, t.to_state) for t in transitions] + assert pairs == [ + (CircuitState.CLOSED, CircuitState.OPEN), + (CircuitState.OPEN, CircuitState.HALF_OPEN), + (CircuitState.HALF_OPEN, CircuitState.CLOSED), + ] + assert all(t.circuit_name == "c" for t in transitions) + # opened_at carried on the open/probe transitions, absent on close. + assert transitions[0].opened_at is not None + assert transitions[-1].opened_at is None + + +def test_on_transition_fires_on_failed_probe_reopen(store, now): + transitions: list[CircuitTransition] = [] + store.db["c"] = CircuitStateRecord(name="c", state=CircuitState.OPEN, failure_count=5, opened_at=now - 100) + config = CircuitBreakerConfig(recovery_timeout=30, success_threshold=2, local_cache_max_age=0) + + @circuit_breaker(name="c", persistence_store=store, on_transition=transitions.append, config=config) + def call(): + raise ConnectionError("still down") + + with pytest.raises(ConnectionError): + call() + + pairs = [(t.from_state, t.to_state) for t in transitions] + assert pairs == [ + (CircuitState.OPEN, CircuitState.HALF_OPEN), + (CircuitState.HALF_OPEN, CircuitState.OPEN), + ] + + +def test_raising_on_transition_hook_is_swallowed(store): + config = CircuitBreakerConfig(failure_threshold=1, local_cache_max_age=0) + + def boom(_transition): + raise RuntimeError("hook blew up") + + @circuit_breaker(name="c", persistence_store=store, on_transition=boom, config=config) + def call(): + raise ConnectionError("down") + + # The hook raises during the CLOSED->OPEN notify, but the protected call's own + # ConnectionError must surface unchanged, not the hook's RuntimeError. + with pytest.raises(ConnectionError): + call() + assert store.db["c"].state == CircuitState.OPEN + + +# --------------------------------------------------------------------------- bug regressions + + +def test_stale_local_failures_reset_on_external_close(store, now): + """Bug 1: a partial failure streak in env A must not survive an external recovery cycle.""" + config = CircuitBreakerConfig(failure_threshold=3, local_cache_max_age=0) + mode = {"value": "fail"} + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + if mode["value"] == "fail": + raise ConnectionError("down") + return "ok" + + # Env A accumulates 2 failures (below threshold of 3). + for _ in range(2): + with pytest.raises(ConnectionError): + call() + assert base_module._LOCAL_FAILURES["c"] == 2 + + # Simulate: another env trips the circuit, then a third closes it. + store.db["c"] = CircuitStateRecord(name="c", state=CircuitState.OPEN, failure_count=3, opened_at=now) + # Force env A to observe OPEN so _LAST_OBSERVED_STATE tracks the transition. + with pytest.raises(CircuitBreakerOpenError): + call() + # Now the circuit is externally closed. + store.db["c"] = CircuitStateRecord(name="c", state=CircuitState.CLOSED, failure_count=0) + + # A single failure in env A must NOT re-trip (stale streak was invalidated). + mode["value"] = "fail" + with pytest.raises(ConnectionError): + call() + assert "c" not in store.db or store.db["c"].state == CircuitState.CLOSED + assert base_module._LOCAL_FAILURES["c"] == 1 # fresh count, not 3 + + +def test_store_write_failure_does_not_mask_downstream_result(store, now): + """Bug 2: if save_closed() fails, the successful probe result must still be returned.""" + config = CircuitBreakerConfig(failure_threshold=3, recovery_timeout=30, success_threshold=1, local_cache_max_age=0) + + store.db["c"] = CircuitStateRecord(name="c", state=CircuitState.OPEN, failure_count=3, opened_at=now - 100) + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + return "payment_charged" + + # Make the persistence write fail. + original_update = store._update_record + + def failing_update(record): + raise RuntimeError("DynamoDB timeout") + + store._update_record = failing_update + + # The probe should still return the downstream result despite the write failure. + result = call() + assert result == "payment_charged" + + store._update_record = original_update + + +def test_store_write_failure_on_trip_does_not_replace_downstream_exception(store): + """Bug 2 (trip path): if save_open() fails, the downstream's own exception must propagate.""" + config = CircuitBreakerConfig(failure_threshold=2, local_cache_max_age=0) + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + raise ConnectionError("the real error") + + # Make the persistence write fail. + def failing_put(record, condition=None, expected_opened_at=None): + raise RuntimeError("DynamoDB throttle") + + store._put_record = failing_put + + # Two failures should raise the ORIGINAL ConnectionError, not a RuntimeError from the store. + for _ in range(2): + with pytest.raises(ConnectionError, match="the real error"): + call() + + +def test_probe_lease_takeover_when_owner_recycled(store, now): + """Design issue 1: a stranded HALF_OPEN probe with expired lease can be taken over.""" + config = CircuitBreakerConfig(recovery_timeout=30, success_threshold=1, local_cache_max_age=0) + + # Simulate: the probe owner ("dead-env") was recycled, lease has expired. + store.db["c"] = CircuitStateRecord( + name="c", + state=CircuitState.HALF_OPEN, + opened_at=now - 200, + half_open_owner="dead-env", + probe_lease_expiry=now - 10, # expired + ) + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + return "recovered" + + # Current environment should take over the expired lease and succeed. + result = call() + assert result == "recovered" + assert store.db["c"].state == CircuitState.CLOSED + + +def test_half_open_non_owner_with_active_lease_is_rejected(store, now): + """Design issue 1 negative: active lease must NOT allow takeover.""" + config = CircuitBreakerConfig(recovery_timeout=30, success_threshold=1, local_cache_max_age=0) + + # Probe owner is alive (lease not expired yet). + store.db["c"] = CircuitStateRecord( + name="c", + state=CircuitState.HALF_OPEN, + opened_at=now - 50, + half_open_owner="other-env", + probe_lease_expiry=now + 999, # far in the future + ) + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + return "should not run" + + with pytest.raises(CircuitBreakerOpenError): + call() + + +def test_half_open_expired_lease_lost_takeover_returns_open_response(store, now): + """Losing the takeover election must NOT probe: another thread/environment won + the conditional write between our expiry check and our acquire, so the call + has to get the open-circuit response — electing every candidate is exactly + the multi-prober bug the per-thread owner id exists to prevent.""" + config = CircuitBreakerConfig(recovery_timeout=30, success_threshold=1, local_cache_max_age=0) + + # A stranded probe with an expired lease, owned by someone else. + store.db["c"] = CircuitStateRecord( + name="c", + state=CircuitState.HALF_OPEN, + opened_at=now - 200, + half_open_owner="dead-env", + probe_lease_expiry=now - 10, # expired — takeover will be attempted + ) + + # The race's loser: the conditional election fails. + def lost_election(name, owner_id, opened_at): + return False + + store.try_acquire_half_open = lost_election + + protected_ran = {"value": False} + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + protected_ran["value"] = True + return "must not probe" + + with pytest.raises(CircuitBreakerOpenError): + call() + assert protected_ran["value"] is False + + +def test_open_lost_election_returns_open_response(store, now): + """Branch: try_acquire_half_open returns False (another env won the race).""" + config = CircuitBreakerConfig(recovery_timeout=30, success_threshold=1, local_cache_max_age=0) + + # Circuit is OPEN and recovery window has elapsed, but election will fail + # because another env already holds the lock. + store.db["c"] = CircuitStateRecord( + name="c", + state=CircuitState.OPEN, + failure_count=5, + opened_at=now - 100, + half_open_owner="winner-env", + ) + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + return "should not run" + + with pytest.raises(CircuitBreakerOpenError): + call() + + +def test_probe_ignored_exception_propagates_without_affecting_circuit(store, now): + """Branch: probe raises an exception that doesn't count as failure.""" + config = CircuitBreakerConfig( + recovery_timeout=30, + success_threshold=1, + handled_exceptions=(ConnectionError,), + local_cache_max_age=0, + ) + + store.db["c"] = CircuitStateRecord(name="c", state=CircuitState.OPEN, failure_count=5, opened_at=now - 100) + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + raise ValueError("not a counted failure") + + with pytest.raises(ValueError, match="not a counted failure"): + call() + # Circuit should still be HALF_OPEN (not reopened), the ValueError didn't count. + assert store.db["c"].state == CircuitState.HALF_OPEN + + +def test_local_cache_serves_state_without_store_read(store): + """Covers the cache hit path (persistence/base.py line 140).""" + config = CircuitBreakerConfig(failure_threshold=3, local_cache_max_age=60) + call_count = {"value": 0} + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + call_count["value"] += 1 + return "ok" + + # First call reads the store (cache miss). + assert call() == "ok" + # Second call should serve from cache — override _get_record to detect reads. + original_get = store._get_record + get_called = {"value": False} + + def tracked_get(name): + get_called["value"] = True + return original_get(name) + + store._get_record = tracked_get + assert call() == "ok" + assert not get_called["value"], "second call should have used the cache, not the store" + store._get_record = original_get + + +def test_error_with_details_formatting(): + """Covers exceptions.py line 28 — __str__ with details.""" + err = CircuitBreakerError("main message", "extra detail") + assert str(err) == "main message - (extra detail)" + + +# --------------------------------------------------------------------------- thread safety (#8320) + + +def _run_in_threads(worker, count): + threads = [threading.Thread(target=worker) for _ in range(count)] + for thread in threads: + thread.start() + for thread in threads: + thread.join() + + +def test_threads_elect_a_single_prober_on_recovery(store, now): + """Regression for #8320: after one thread wins the election, siblings must not also probe.""" + store.db["c"] = CircuitStateRecord(name="c", state=CircuitState.OPEN, failure_count=5, opened_at=now - 60) + # success_threshold=2 keeps the circuit HALF_OPEN after the winning probe, so a + # late-reading loser can never legitimately run the function through a closed circuit. + config = CircuitBreakerConfig(recovery_timeout=30, success_threshold=2, local_cache_max_age=0) + probes = [] + open_responses = [] + barrier = threading.Barrier(8) + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + probes.append(threading.current_thread().name) + return "ok" + + def worker(): + barrier.wait(timeout=10) + try: + call() + except CircuitBreakerOpenError: + open_responses.append(threading.current_thread().name) + + _run_in_threads(worker, 8) + + assert len(probes) == 1, "exactly one thread must probe per recovery window" + assert len(open_responses) == 7, "every other thread must get the open response" + assert store.db["c"].state == CircuitState.HALF_OPEN + assert store.db["c"].half_open_owner is not None + + +class _RacyCounters(dict): + """Counter dict that yields between the read and the write of an increment. + + Without this, the GIL makes the unlocked read-modify-write effectively atomic + (the interpreter rarely switches threads inside those few bytecodes), so a + missing lock would still pass. Holding every reader mid-increment long enough + for its siblings to read the same stale value makes the lost update + deterministic: unlocked, all threads write back the same count and the + threshold is never reached. + """ + + def get(self, key, default=None): + value = super().get(key, default) + time.sleep(0.005) # park mid-increment so sibling threads read the same stale value + return value + + +def test_threads_trip_at_exactly_the_failure_threshold(store, monkeypatch): + """Racing increments must neither lose updates (tripping late) nor persist the trip twice.""" + monkeypatch.setattr(base_module, "_LOCAL_FAILURES", _RacyCounters()) + threads_count, threshold = 8, 5 + config = CircuitBreakerConfig(failure_threshold=threshold, local_cache_max_age=0) + barrier = threading.Barrier(threads_count) + siblings_done = threading.Event() + done_lock = threading.Lock() + finished = [] + save_open_counts = [] + original_save_open = store.save_open + + def blocking_save_open(name, failure_count, opened_at): + save_open_counts.append(failure_count) + if len(save_open_counts) > 1: + # A second persist is the bug itself; unblock immediately so it fails fast. + siblings_done.set() + # Park the tripping thread mid-persist until every sibling has recorded its + # failure. Code that resets the counter only after persisting lets the + # remaining threads cross the threshold too and persist the trip again. + siblings_done.wait(timeout=10) + original_save_open(name, failure_count=failure_count, opened_at=opened_at) + + store.save_open = blocking_save_open + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + # Hold every thread inside the protected call so all pass the CLOSED check + # before any failure is recorded, forcing the increments to race. + barrier.wait(timeout=10) + raise ConnectionError("downstream down") + + raised = [] + + def worker(): + try: + call() + except ConnectionError: + raised.append(threading.current_thread().name) + finally: + with done_lock: + finished.append(threading.current_thread().name) + if len(finished) == threads_count - 1: + # Only the tripping thread is still parked in the persist spy. + siblings_done.set() + + _run_in_threads(worker, threads_count) + + assert save_open_counts == [threshold], "the trip must be persisted exactly once, at the threshold" + assert store.db["c"].state == CircuitState.OPEN + assert len(raised) == threads_count + assert base_module._LOCAL_FAILURES["c"] == threads_count - threshold, "post-trip failures restart the streak" + + +def test_probe_ownership_is_per_thread_and_stable_across_invocations(store, now): + store.db["c"] = CircuitStateRecord(name="c", state=CircuitState.OPEN, failure_count=5, opened_at=now - 60) + config = CircuitBreakerConfig(recovery_timeout=30, success_threshold=2, local_cache_max_age=0) + calls = [] + + @circuit_breaker(name="c", persistence_store=store, config=config) + def call(): + calls.append(threading.current_thread().name) + return "ok" + + # This thread wins the election; one more probe success is needed to close. + assert call() == "ok" + assert store.db["c"].state == CircuitState.HALF_OPEN + + # A sibling thread must not inherit ownership (it did when the id was per-process). + sibling_outcome = [] + + def sibling(): + try: + sibling_outcome.append(call()) + except CircuitBreakerOpenError: + sibling_outcome.append("rejected") + + _run_in_threads(sibling, 1) + assert sibling_outcome == ["rejected"] + + # The owner's identity must survive across invocations so it can finish the recovery; + # a per-handler or per-invocation id would strand the circuit until the lease expired. + assert call() == "ok" + assert store.db["c"].state == CircuitState.CLOSED + assert len(calls) == 2 + + +def test_single_probe_spans_environments_and_threads(store, now): + """The same election covers threads in one process and separate environments.""" + other_env = type(store)() + other_env.db = store.db + store.db["c"] = CircuitStateRecord(name="c", state=CircuitState.OPEN, failure_count=5, opened_at=now - 60) + config = CircuitBreakerConfig(recovery_timeout=30, success_threshold=2, local_cache_max_age=0) + probes = [] + open_responses = [] + barrier = threading.Barrier(8) + + def protect(persistence): + @circuit_breaker(name="c", persistence_store=persistence, config=config) + def call(): + probes.append(threading.current_thread().name) + return "ok" + + return call + + calls = [protect(store), protect(other_env)] + + def worker(index): + env_call = calls[index % 2] + barrier.wait(timeout=10) + try: + env_call() + except CircuitBreakerOpenError: + open_responses.append(threading.current_thread().name) + + threads = [threading.Thread(target=worker, args=(index,)) for index in range(8)] + for thread in threads: + thread.start() + for thread in threads: + thread.join() + + assert len(probes) == 1, "one probe across all threads of all environments" + assert len(open_responses) == 7 + + +def test_settings_are_kept_per_circuit_on_a_shared_layer(store, now): + """Configuring another circuit on the same layer must not stamp this one's probe lease.""" + store.configure(CircuitBreakerConfig(recovery_timeout=30, local_cache_max_age=0), circuit_name="a") + store.configure(CircuitBreakerConfig(recovery_timeout=9999, local_cache_max_age=0), circuit_name="b") + store.db["a"] = CircuitStateRecord(name="a", state=CircuitState.OPEN, failure_count=5, opened_at=now - 60) + + assert store.try_acquire_half_open("a", "owner-1", now - 60) + + lease = store.db["a"].probe_lease_expiry + assert lease is not None + assert lease <= now + 30 + 2, "the lease must derive from circuit 'a' recovery_timeout, not 'b'" diff --git a/tests/functional/circuit_breaker/test_dynamodb_persistence.py b/tests/functional/circuit_breaker/test_dynamodb_persistence.py new file mode 100644 index 00000000000..ac67bfd0897 --- /dev/null +++ b/tests/functional/circuit_breaker/test_dynamodb_persistence.py @@ -0,0 +1,397 @@ +from __future__ import annotations + +import boto3 +import pytest +from botocore.config import Config +from botocore.stub import Stubber + +from aws_lambda_powertools.utilities.circuit_breaker.config import CircuitBreakerConfig +from aws_lambda_powertools.utilities.circuit_breaker.exceptions import CircuitBreakerConfigError +from aws_lambda_powertools.utilities.circuit_breaker.persistence import ( + CircuitBreakerDynamoDBPersistence, +) +from aws_lambda_powertools.utilities.circuit_breaker.states import CircuitState +from aws_lambda_powertools.warnings import PowertoolsUserWarning + +TABLE_NAME = "CircuitBreakerState" + + +@pytest.fixture +def persistence(): + client = boto3.client("dynamodb", config=Config(region_name="us-east-1")) + layer = CircuitBreakerDynamoDBPersistence(table_name=TABLE_NAME, boto3_client=client) + layer.configure(CircuitBreakerConfig(local_cache_max_age=0), circuit_name="payment") + return layer + + +def test_get_state_missing_item_returns_closed(persistence): + stubber = Stubber(persistence.client) + stubber.add_response( + "get_item", + {}, + {"TableName": TABLE_NAME, "Key": {"id": {"S": "payment"}}, "ConsistentRead": False}, + ) + with stubber: + record = persistence.get_state("payment") + assert record.state == CircuitState.CLOSED + + +def test_get_state_failing_store_fails_open(persistence): + stubber = Stubber(persistence.client) + stubber.add_client_error("get_item", service_error_code="InternalServerError") + with stubber: + record = persistence.get_state("payment") + assert record.state == CircuitState.CLOSED, "store failure must fail open (CLOSED)" + + +def _capture_put_item(persistence): + """Patch put_item to capture its params instead of asserting a time-dependent TTL.""" + captured = {} + original = persistence.client.put_item + + def capturing(**kwargs): + captured.update(kwargs) + return {} + + persistence.client.put_item = capturing + return captured, lambda: setattr(persistence.client, "put_item", original) + + +def test_save_open_writes_open_item(persistence): + captured, restore = _capture_put_item(persistence) + try: + persistence.save_open("payment", failure_count=5, opened_at=1000) + finally: + restore() + + item = captured["Item"] + assert item["id"] == {"S": "payment"} + assert item["state"] == {"S": "OPEN"} + assert item["failure_count"] == {"N": "5"} + assert item["opened_at"] == {"N": "1000"} + assert "expiration" in item, "open item must carry a TTL" + + +def test_try_acquire_half_open_wins(persistence): + captured, restore = _capture_put_item(persistence) + try: + assert persistence.try_acquire_half_open("payment", "env-a", 1000) is True + finally: + restore() + + item = captured["Item"] + assert item["state"] == {"S": "HALF_OPEN"} + assert item["half_open_owner"] == {"S": "env-a"} + assert item["opened_at"] == {"N": "1000"} + assert "expiration" in item + # Condition supports both fresh election (OPEN, no owner, matching opened_at) and + # lease takeover (HALF_OPEN with expired lease). + assert "#state = :open AND attribute_not_exists(#half_open_owner)" in captured["ConditionExpression"] + assert "#probe_lease_expiry <= :now" in captured["ConditionExpression"] + assert captured["ExpressionAttributeValues"][":open"] == {"S": "OPEN"} + assert captured["ExpressionAttributeValues"][":expected_opened_at"] == {"N": "1000"} + assert "probe_lease_expiry" in item + + +def test_try_acquire_half_open_loses_on_conditional_failure(persistence): + stubber = Stubber(persistence.client) + stubber.add_client_error("put_item", service_error_code="ConditionalCheckFailedException") + with stubber: + assert persistence.try_acquire_half_open("payment", "env-b", 1000) is False + + +def test_save_closed_updates_record(persistence): + stubber = Stubber(persistence.client) + stubber.add_response("update_item", {}) + with stubber: + persistence.save_closed("payment") + stubber.assert_no_pending_responses() + + +# --------------------------------------------------------------------------- bug regressions + + +def test_save_reopen_removes_half_open_owner(persistence): + # Bug #1: HALF_OPEN -> OPEN must clear half_open_owner, otherwise the next + # probe election (attribute_not_exists(half_open_owner)) can never succeed. + captured = {} + original_update = persistence.client.update_item + + def capturing_update(**kwargs): + captured.update(kwargs) + return {} + + persistence.client.update_item = capturing_update + try: + persistence.save_reopen("payment", opened_at=2000) + finally: + persistence.client.update_item = original_update + + expression = captured["UpdateExpression"] + assert "REMOVE" in expression + assert captured["ExpressionAttributeNames"]["#half_open_owner"] == "half_open_owner" + assert "#half_open_owner" in expression.split("REMOVE", 1)[1], "owner must be in the REMOVE clause" + + +def test_save_open_item_contains_expiration_attribute(persistence): + # Bug #2: the written item must carry the TTL (expiration) attribute, otherwise + # the documented self-cleaning of abandoned circuits never happens. Capture the + # actual PutItem params rather than asserting an exact (time-dependent) value. + captured = {} + + original_put = persistence.client.put_item + + def capturing_put(**kwargs): + captured.update(kwargs) + return {} + + persistence.client.put_item = capturing_put + try: + persistence.save_open("payment", failure_count=5, opened_at=1000) + finally: + persistence.client.put_item = original_put + + item = captured["Item"] + assert "expiration" in item, "open item must carry a DynamoDB TTL attribute" + + +# --------------------------------------------------------------------------- serialization coverage + + +def test_item_to_record_full_item(persistence): + """Cover _item_to_record with all fields present (lines 109-122).""" + item = { + "id": {"S": "payment"}, + "state": {"S": "HALF_OPEN"}, + "failure_count": {"N": "3"}, + "opened_at": {"N": "1000"}, + "half_open_owner": {"S": "env-x"}, + "probe_lease_expiry": {"N": "2000"}, + "expiration": {"N": "9999"}, + } + record = persistence._item_to_record(item) + assert record.name == "payment" + assert record.state == CircuitState.HALF_OPEN + assert record.failure_count == 3 + assert record.opened_at == 1000 + assert record.half_open_owner == "env-x" + assert record.probe_lease_expiry == 2000 + assert record.expiry_timestamp == 9999 + + +def test_item_to_record_minimal_item(persistence): + """Cover _item_to_record with optional fields absent.""" + item = { + "id": {"S": "payment"}, + "state": {"S": "CLOSED"}, + } + record = persistence._item_to_record(item) + assert record.name == "payment" + assert record.state == CircuitState.CLOSED + assert record.failure_count == 0 + assert record.opened_at is None + assert record.half_open_owner is None + assert record.probe_lease_expiry is None + + +def test_record_to_item_full_record(persistence): + """Cover _record_to_item with all fields set (lines 124-139).""" + from aws_lambda_powertools.utilities.circuit_breaker.persistence.record import CircuitStateRecord + + record = CircuitStateRecord( + name="payment", + state=CircuitState.HALF_OPEN, + failure_count=5, + opened_at=1000, + half_open_owner="env-a", + probe_lease_expiry=2000, + expiry_timestamp=9999, + ) + item = persistence._record_to_item(record) + assert item["id"] == {"S": "payment"} + assert item["state"] == {"S": "HALF_OPEN"} + assert item["failure_count"] == {"N": "5"} + assert item["opened_at"] == {"N": "1000"} + assert item["half_open_owner"] == {"S": "env-a"} + assert item["probe_lease_expiry"] == {"N": "2000"} + assert item["expiration"] == {"N": "9999"} + + +def test_record_to_item_minimal_record(persistence): + """Cover _record_to_item with optional fields as None (branch misses on lines 131-137).""" + from aws_lambda_powertools.utilities.circuit_breaker.persistence.record import CircuitStateRecord + + record = CircuitStateRecord(name="payment", state=CircuitState.CLOSED) + item = persistence._record_to_item(record) + assert item == { + "id": {"S": "payment"}, + "state": {"S": "CLOSED"}, + "failure_count": {"N": "0"}, + } + assert "opened_at" not in item + assert "half_open_owner" not in item + assert "probe_lease_expiry" not in item + assert "expiration" not in item + + +def test_get_record_returns_deserialized_item(persistence): + """Cover _get_record success path (lines 141-153).""" + stubber = Stubber(persistence.client) + stubber.add_response( + "get_item", + { + "Item": { + "id": {"S": "payment"}, + "state": {"S": "OPEN"}, + "failure_count": {"N": "5"}, + "opened_at": {"N": "1000"}, + }, + }, + {"TableName": TABLE_NAME, "Key": {"id": {"S": "payment"}}, "ConsistentRead": False}, + ) + with stubber: + record = persistence._get_record("payment") + assert record.state == CircuitState.OPEN + assert record.failure_count == 5 + assert record.opened_at == 1000 + + +def test_build_half_open_condition_without_expected_opened_at(persistence): + """Cover _build_half_open_condition when expected_opened_at is None (line 172 branch).""" + result = persistence._build_half_open_condition(expected_opened_at=None) + assert ":expected_opened_at" not in result["ConditionExpression"] + assert "#opened_at" not in result["ExpressionAttributeNames"] + + +def test_build_half_open_condition_with_expected_opened_at(persistence): + """Cover _build_half_open_condition when expected_opened_at is set (lines 172-177).""" + result = persistence._build_half_open_condition(expected_opened_at=5000) + assert "#opened_at = :expected_opened_at" in result["ConditionExpression"] + assert result["ExpressionAttributeNames"]["#opened_at"] == "opened_at" + assert result["ExpressionAttributeValues"][":expected_opened_at"] == {"N": "5000"} + + +# --------------------------------------------------------------------------- composite (single-table) key + + +COMPOSITE_KEY = {"PK": {"S": "CIRCUIT_BREAKER"}, "SK": {"S": "payment"}} + + +@pytest.fixture +def composite_persistence(): + client = boto3.client("dynamodb", config=Config(region_name="us-east-1")) + layer = CircuitBreakerDynamoDBPersistence( + table_name=TABLE_NAME, + boto3_client=client, + key_attr="PK", + sort_key_attr="SK", + static_pk_value="CIRCUIT_BREAKER", + ) + layer.configure(CircuitBreakerConfig(local_cache_max_age=0), circuit_name="payment") + return layer + + +def test_composite_get_state_reads_composite_key(composite_persistence): + stubber = Stubber(composite_persistence.client) + stubber.add_response( + "get_item", + {}, + {"TableName": TABLE_NAME, "Key": COMPOSITE_KEY, "ConsistentRead": False}, + ) + with stubber: + record = composite_persistence.get_state("payment") + assert record.state == CircuitState.CLOSED + + +def test_composite_save_open_writes_composite_key(composite_persistence): + captured, restore = _capture_put_item(composite_persistence) + try: + composite_persistence.save_open("payment", failure_count=5, opened_at=1000) + finally: + restore() + + item = captured["Item"] + assert item["PK"] == {"S": "CIRCUIT_BREAKER"} + assert item["SK"] == {"S": "payment"} + assert "id" not in item + assert item["state"] == {"S": "OPEN"} + + +def test_composite_try_acquire_half_open_writes_composite_key(composite_persistence): + captured, restore = _capture_put_item(composite_persistence) + try: + assert composite_persistence.try_acquire_half_open("payment", "env-a", 1000) is True + finally: + restore() + + item = captured["Item"] + assert item["PK"] == {"S": "CIRCUIT_BREAKER"} + assert item["SK"] == {"S": "payment"} + assert item["state"] == {"S": "HALF_OPEN"} + # The conditional election is still emitted in composite mode (condition is key-independent). + assert "#state = :open AND attribute_not_exists(#half_open_owner)" in captured["ConditionExpression"] + assert "#probe_lease_expiry <= :now" in captured["ConditionExpression"] + + +def test_composite_try_acquire_half_open_loses_on_conditional_failure(composite_persistence): + stubber = Stubber(composite_persistence.client) + stubber.add_client_error("put_item", service_error_code="ConditionalCheckFailedException") + with stubber: + assert composite_persistence.try_acquire_half_open("payment", "env-b", 1000) is False + + +def test_composite_save_closed_updates_composite_key(composite_persistence): + captured = {} + original_update = composite_persistence.client.update_item + + def capturing_update(**kwargs): + captured.update(kwargs) + return {} + + composite_persistence.client.update_item = capturing_update + try: + composite_persistence.save_closed("payment") + finally: + composite_persistence.client.update_item = original_update + + assert captured["Key"] == COMPOSITE_KEY + + +def test_composite_item_to_record_reads_name_from_sort_key(composite_persistence): + item = { + "PK": {"S": "CIRCUIT_BREAKER"}, + "SK": {"S": "payment"}, + "state": {"S": "OPEN"}, + "failure_count": {"N": "2"}, + } + record = composite_persistence._item_to_record(item) + assert record.name == "payment" + assert record.state == CircuitState.OPEN + + +def test_sort_key_equal_to_key_attr_raises(): + client = boto3.client("dynamodb", config=Config(region_name="us-east-1")) + with pytest.raises(CircuitBreakerConfigError, match="cannot be the same"): + CircuitBreakerDynamoDBPersistence( + table_name=TABLE_NAME, + boto3_client=client, + key_attr="PK", + sort_key_attr="PK", + ) + + +def test_default_static_pk_value_namespaces_function_name(monkeypatch): + monkeypatch.setenv("AWS_LAMBDA_FUNCTION_NAME", "orders-fn") + client = boto3.client("dynamodb", config=Config(region_name="us-east-1")) + layer = CircuitBreakerDynamoDBPersistence(table_name=TABLE_NAME, boto3_client=client, sort_key_attr="SK") + assert layer.static_pk_value == "circuit_breaker#orders-fn" + + +def test_static_pk_value_without_sort_key_attr_warns(): + client = boto3.client("dynamodb", config=Config(region_name="us-east-1")) + with pytest.warns(PowertoolsUserWarning, match="static_pk_value is ignored unless sort_key_attr"): + CircuitBreakerDynamoDBPersistence( + table_name=TABLE_NAME, + boto3_client=client, + static_pk_value="CIRCUIT_BREAKER", + ) diff --git a/tests/functional/event_handler/_pydantic/test_bedrock_agent.py b/tests/functional/event_handler/_pydantic/test_bedrock_agent.py index 9c46fe24eb7..90e9e69790b 100644 --- a/tests/functional/event_handler/_pydantic/test_bedrock_agent.py +++ b/tests/functional/event_handler/_pydantic/test_bedrock_agent.py @@ -1,5 +1,6 @@ import json -from typing import Any, Dict, Optional +from functools import partial +from typing import Any, Dict import pytest from typing_extensions import Annotated @@ -191,7 +192,7 @@ def test_openapi_schema_for_pydanticv2(openapi30_schema): # WHEN we have a simple handler @app.get("/", description="Testing") - def handler() -> Optional[Dict]: + def handler() -> Dict | None: pass # WHEN we get the schema @@ -335,7 +336,7 @@ def test_bedrock_resolver_with_openapi_extensions(): # WHEN we have a simple handler with openapi extension @app.get("/", description="Testing", openapi_extensions={"x-requireConfirmation": "ENABLED"}) - def handler() -> Optional[Dict]: + def handler() -> Dict | None: pass # WHEN we get the schema @@ -383,3 +384,37 @@ def run_sql_query(query: Annotated[str, Query()]): # THEN the parameter with commas should be correctly passed to the handler body = json.loads(result["response"]["responseBody"]["application/json"]["body"]) assert body["result"] == "SELECT a.source_name, b.thing FROM table" + + +def test_bedrock_agent_with_default_serializer_escapes_non_ascii(): + # GIVEN a Bedrock Agent resolver using the default serializer + app = BedrockAgentResolver() + + @app.get("/claims", description="Gets claims") + def claims() -> Dict[str, Any]: + return {"output": "잔액은 1,000원입니다 💰"} + + # WHEN calling the event handler + result = app(load_event("bedrockAgentEvent.json"), {}) + + # THEN the body is valid JSON and non-ASCII characters are escaped (default json.dumps behavior) + body = result["response"]["responseBody"]["application/json"]["body"] + assert "\\uc794" in body # "잔" escaped + assert json.loads(body) == {"output": "잔액은 1,000원입니다 💰"} + + +def test_bedrock_agent_with_custom_serializer_preserves_non_ascii(): + # GIVEN a Bedrock Agent resolver initialized with a custom serializer that keeps non-ASCII characters + app = BedrockAgentResolver(serializer=partial(json.dumps, ensure_ascii=False)) + + @app.get("/claims", description="Gets claims") + def claims() -> Dict[str, Any]: + return {"output": "잔액은 1,000원입니다 💰"} + + # WHEN calling the event handler + result = app(load_event("bedrockAgentEvent.json"), {}) + + # THEN the non-ASCII characters are preserved verbatim in the response body + body = result["response"]["responseBody"]["application/json"]["body"] + assert "잔액은 1,000원입니다 💰" in body + assert json.loads(body) == {"output": "잔액은 1,000원입니다 💰"} diff --git a/tests/functional/event_handler/_pydantic/test_depends.py b/tests/functional/event_handler/_pydantic/test_depends.py index 0cbc05b83c7..1a4be667377 100644 --- a/tests/functional/event_handler/_pydantic/test_depends.py +++ b/tests/functional/event_handler/_pydantic/test_depends.py @@ -214,3 +214,82 @@ def handler(name: str = "world", greeting: Annotated[str, Depends(get_greeting)] result = app(event, {}) assert result["statusCode"] == 200 assert json.loads(result["body"]) == {"message": "hello, Lambda!"} + + +class ArbitraryClient: + """Stand-in for an arbitrary non-Pydantic type such as a boto3 client.""" + + def __init__(self, name: str = "default"): + self.name = name + + +def test_depends_with_arbitrary_return_type_and_validation(): + """A dependency returning a non-Pydantic type must not crash under enable_validation (#8330).""" + app = APIGatewayHttpResolver(enable_validation=True) + + def get_client() -> ArbitraryClient: + return ArbitraryClient(name="orders") + + @app.get("/items") + def handler(client: Annotated[ArbitraryClient, Depends(get_client)]): + return {"client": client.name} + + event = {**API_GW_V2_EVENT} + event["rawPath"] = "/items" + event["requestContext"] = { + **event["requestContext"], + "http": {"method": "GET", "path": "/items"}, + } + + result = app(event, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"client": "orders"} + + +def test_depends_nested_arbitrary_return_types_and_validation(): + """A nested dependency chain of non-Pydantic return types must resolve (#8330). + + Mirrors the reported chain: botocore session -> boto3 session -> dynamodb client. + """ + app = APIGatewayHttpResolver(enable_validation=True) + + def get_session() -> ArbitraryClient: + return ArbitraryClient(name="session") + + def get_client(session: Annotated[ArbitraryClient, Depends(get_session)]) -> ArbitraryClient: + return ArbitraryClient(name=f"client-of-{session.name}") + + @app.get("/items") + def handler(client: Annotated[ArbitraryClient, Depends(get_client)]): + return {"client": client.name} + + event = {**API_GW_V2_EVENT} + event["rawPath"] = "/items" + event["requestContext"] = { + **event["requestContext"], + "http": {"method": "GET", "path": "/items"}, + } + + result = app(event, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"client": "client-of-session"} + + +def test_depends_arbitrary_return_type_excluded_from_openapi_schema(): + """A non-Pydantic dependency return type must not appear in (or break) the OpenAPI schema (#8330).""" + app = APIGatewayHttpResolver(enable_validation=True) + + def get_client() -> ArbitraryClient: + return ArbitraryClient() + + @app.get("/items") + def handler(client: Annotated[ArbitraryClient, Depends(get_client)]): + return {"ok": True} + + # Schema generation itself must not raise, and the dependency must not leak into params/body. + schema = app.get_openapi_schema() + get_op = schema.paths["/items"].get + param_names = [p.name for p in (get_op.parameters or [])] + + assert "client" not in param_names + assert get_op.requestBody is None diff --git a/tests/functional/event_handler/_pydantic/test_http_resolver_pydantic.py b/tests/functional/event_handler/_pydantic/test_http_resolver_pydantic.py index 7cab58a1b70..e088527e359 100644 --- a/tests/functional/event_handler/_pydantic/test_http_resolver_pydantic.py +++ b/tests/functional/event_handler/_pydantic/test_http_resolver_pydantic.py @@ -13,10 +13,6 @@ from aws_lambda_powertools.event_handler.http_resolver import MockLambdaContext from aws_lambda_powertools.event_handler.openapi.params import Query -# Suppress warning for all tests -pytestmark = pytest.mark.filterwarnings("ignore:HttpResolverLocal is intended for local development") - - # ============================================================================= # ASGI Test Helpers # ============================================================================= diff --git a/tests/functional/event_handler/_pydantic/test_openapi_params.py b/tests/functional/event_handler/_pydantic/test_openapi_params.py index 18087a228d1..b9426bf4da7 100644 --- a/tests/functional/event_handler/_pydantic/test_openapi_params.py +++ b/tests/functional/event_handler/_pydantic/test_openapi_params.py @@ -1,7 +1,7 @@ import json from dataclasses import dataclass from datetime import datetime -from typing import List, Optional, Tuple +from typing import List, Literal, Tuple import pytest from pydantic import BaseModel, Field @@ -34,7 +34,7 @@ def test_openapi_pydantic_query_params(): class QueryParams(BaseModel): limit: int = Field(default=10, ge=1, le=100, description="Number of items to return") offset: int = Field(default=0, ge=0, description="Number of items to skip") - search: Optional[str] = Field(default=None, description="Search term") + search: str | None = Field(default=None, description="Search term") @app.get("/search") def search_handler(params: Annotated[QueryParams, Query()]): @@ -82,7 +82,7 @@ def test_openapi_pydantic_header_params(): class HeaderParams(BaseModel): authorization: str = Field(description="Authorization token") user_agent: str = Field(default="PowerTools/1.0", description="User agent") - language: Optional[str] = Field(default=None, alias="accept-language", description="Language preference") + language: str | None = Field(default=None, alias="accept-language", description="Language preference") @app.get("/protected") def protected_handler(headers: Annotated[HeaderParams, Header()]): @@ -915,6 +915,24 @@ def mixed_body_endpoint(user_data: Annotated[UserData, Body(media_type="applicat assert "application/json" in request_body.content +def test_openapi_excludes_content_type_header_parameter(): + """Content-Type is described by requestBody content, not an OpenAPI header parameter.""" + app = APIGatewayRestResolver(enable_validation=True) + + @app.patch("/json-patch") + def json_patch( + operations: Annotated[list[dict], Body(media_type="application/json-patch+json")], + content_type: Annotated[Literal["application/json-patch+json"], Header(alias="Content-Type")], + ): + return {"status": "updated"} + + schema = app.get_openapi_schema() + patch_op = schema.paths["/json-patch"].patch + + assert "application/json-patch+json" in patch_op.requestBody.content + assert all(parameter.name.lower() != "content-type" for parameter in patch_op.parameters or []) + + def test_openapi_form_parameter_edge_cases(): """Test Form parameters with various edge cases.""" @@ -923,7 +941,7 @@ def test_openapi_form_parameter_edge_cases(): @app.post("/form-edge-cases") def form_edge_cases( required_field: Annotated[str, Form(description="Required field")], - optional_field: Annotated[Optional[str], Form(description="Optional field")] = None, + optional_field: Annotated[str | None, Form(description="Optional field")] = None, field_with_default: Annotated[str, Form(description="Field with default")] = "default_value", ): return {"required": required_field, "optional": optional_field, "default": field_with_default} @@ -986,7 +1004,7 @@ def get_items(params: Annotated[QueryParams, Query()]): def test_openapi_pydantic_header_with_alias(): - """Test that Pydantic field aliases work correctly in Header parameters""" + """Test that Pydantic header aliases are emitted, except Content-Type.""" app = APIGatewayRestResolver() class HeaderParams(BaseModel): @@ -1003,7 +1021,7 @@ def test_handler(headers: Annotated[HeaderParams, Header()]): # Check that aliases are used as parameter names param_names = [param.name for param in get_operation.parameters] - assert "content-type" in param_names + assert "content-type" not in param_names assert "user-agent" in param_names assert "content_type" not in param_names # Original field name should not be used assert "user_agent" not in param_names @@ -1016,7 +1034,7 @@ def test_openapi_pydantic_required_vs_optional(): class QueryParams(BaseModel): required_field: str = Field(description="Required field") optional_with_default: str = Field(default="default", description="Optional with default") - optional_nullable: Optional[str] = Field(default=None, description="Optional nullable") + optional_nullable: str | None = Field(default=None, description="Optional nullable") @app.get("/test") def test_handler(params: Annotated[QueryParams, Query()]): diff --git a/tests/functional/event_handler/_pydantic/test_openapi_responses.py b/tests/functional/event_handler/_pydantic/test_openapi_responses.py index dccfae3e28d..c00b4e00462 100644 --- a/tests/functional/event_handler/_pydantic/test_openapi_responses.py +++ b/tests/functional/event_handler/_pydantic/test_openapi_responses.py @@ -1,5 +1,5 @@ from secrets import randbelow -from typing import Optional, Union +from typing import Union from pydantic import BaseModel @@ -325,7 +325,7 @@ def test_openapi_response_examples_preserved_with_model(): class UserResponse(BaseModel): id: int name: str - email: Optional[str] = None + email: str | None = None @app.get( "/", diff --git a/tests/functional/event_handler/_pydantic/test_openapi_schema_pydantic_v2.py b/tests/functional/event_handler/_pydantic/test_openapi_schema_pydantic_v2.py index d25811d24ae..6f4b89e47cc 100644 --- a/tests/functional/event_handler/_pydantic/test_openapi_schema_pydantic_v2.py +++ b/tests/functional/event_handler/_pydantic/test_openapi_schema_pydantic_v2.py @@ -1,6 +1,6 @@ import json import warnings -from typing import Literal, Optional +from typing import Literal import pytest from pydantic import BaseModel, Field, computed_field @@ -52,7 +52,7 @@ def test_openapi_3_1_complex_handler(openapi31_schema): # GIVEN a complex pydantic model class TodoAttributes(BaseModel): userId: int - id_: Optional[int] = Field(alias="id", default=None) + id_: int | None = Field(alias="id", default=None) title: str completed: bool diff --git a/tests/functional/event_handler/_pydantic/test_openapi_serialization.py b/tests/functional/event_handler/_pydantic/test_openapi_serialization.py index ef5c8ddd938..9bad2c61422 100644 --- a/tests/functional/event_handler/_pydantic/test_openapi_serialization.py +++ b/tests/functional/event_handler/_pydantic/test_openapi_serialization.py @@ -1,6 +1,6 @@ import json from dataclasses import dataclass -from typing import Dict, Optional, Set +from typing import Dict, Set import pytest from pydantic import BaseModel @@ -81,7 +81,7 @@ class Model(BaseModel): age: int @app.get("/valid_optional") - def handler_valid_optional() -> Optional[Model]: + def handler_valid_optional() -> Model | None: return Model(name="John", age=30) # WHEN returning a valid model for an Optional type diff --git a/tests/functional/event_handler/_pydantic/test_openapi_validation_middleware.py b/tests/functional/event_handler/_pydantic/test_openapi_validation_middleware.py index 0da092f8aea..ec14c14fa08 100644 --- a/tests/functional/event_handler/_pydantic/test_openapi_validation_middleware.py +++ b/tests/functional/event_handler/_pydantic/test_openapi_validation_middleware.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from enum import Enum from pathlib import PurePath -from typing import Any, Dict, List, Literal, Optional, Tuple, Union +from typing import Any, Dict, List, Literal, Tuple, Union import pytest from pydantic import ( @@ -30,7 +30,7 @@ VPCLatticeV2Resolver, ) from aws_lambda_powertools.event_handler.openapi.exceptions import ResponseValidationError -from aws_lambda_powertools.event_handler.openapi.params import Body, Form, Header, Query +from aws_lambda_powertools.event_handler.openapi.params import Body, Form, Header, Path, Query from tests.functional.utils import load_event @@ -59,6 +59,27 @@ def handler(user_id: int): assert any(text in result["body"] for text in ["type_error.integer", "int_parsing"]) +def test_content_type_header_validation_remains_enabled(gw_event): + """Content-Type header validation remains available when it is omitted from the OpenAPI schema.""" + app = APIGatewayRestResolver(enable_validation=True) + + @app.patch("/json-patch") + def json_patch( + operations: Annotated[list[dict], Body(media_type="application/json-patch+json")], + content_type: Annotated[Literal["application/json-patch+json"], Header(alias="Content-Type")], + ): + return {"status": "updated"} + + gw_event["httpMethod"] = "PATCH" + gw_event["path"] = "/json-patch" + gw_event["headers"]["Content-Type"] = "application/json" + gw_event["body"] = '[{"op": "replace"}]' + + result = app(gw_event, {}) + + assert result["statusCode"] == 422 + + def test_validate_pydantic_query_params(gw_event): """Test that Pydantic models in Query parameters are validated correctly""" @@ -67,7 +88,7 @@ def test_validate_pydantic_query_params(gw_event): class QueryParams(BaseModel): limit: int = Field(default=10, ge=1, le=100, description="Number of items") - search: Optional[str] = Field(default=None, description="Search term") + search: str | None = Field(default=None, description="Search term") @app.get("/search") def search_handler(params: Annotated[QueryParams, Query()]): @@ -1756,7 +1777,7 @@ class Model(BaseModel): age: int @app.get("/none_allowed") - def handler_none_allowed() -> Optional[Model]: + def handler_none_allowed() -> Model | None: return None # WHEN returning None for an Optional type @@ -2654,6 +2675,77 @@ def create_action(action: Annotated[action_type, Body()]): assert result["statusCode"] == 422 +def test_field_annotation_with_all_param_types(gw_event): + """A reusable Annotated type carrying a Pydantic Field works with every parameter location.""" + app = APIGatewayRestResolver(enable_validation=True) + + # Reusable annotated type, the same kind you'd use inside a model + str_field = Annotated[str, Field()] + + @app.get("/header") + def get_header(h: Annotated[str_field, Header()]): + return {"value": h} + + @app.get("/path/

") + def get_path(p: Annotated[str_field, Path()]): + return {"value": p} + + @app.get("/query") + def get_query(q: Annotated[str_field, Query()]): + return {"value": q} + + @app.post("/body") + def post_body(b: Annotated[str_field, Body()]): + return {"value": b} + + del gw_event["multiValueHeaders"] + del gw_event["multiValueQueryStringParameters"] + + # Header + gw_event["path"] = "/header" + gw_event["httpMethod"] = "GET" + gw_event["headers"] = {"h": "test"} + assert app(gw_event, {})["statusCode"] == 200 + + # Path + gw_event["path"] = "/path/test" + gw_event["pathParameters"] = {"p": "test"} + assert app(gw_event, {})["statusCode"] == 200 + + # Query + gw_event["path"] = "/query" + gw_event["pathParameters"] = None + gw_event["queryStringParameters"] = {"q": "test"} + assert app(gw_event, {})["statusCode"] == 200 + + # Body + gw_event["path"] = "/body" + gw_event["httpMethod"] = "POST" + gw_event["headers"]["content-type"] = "application/json" + gw_event["body"] = '"test"' + assert app(gw_event, {})["statusCode"] == 200 + + +def test_field_constraints_apply_with_param_type(gw_event): + """Constraints declared on a Field are enforced when paired with a location marker.""" + app = APIGatewayRestResolver(enable_validation=True) + + @app.get("/items") + def get_items(quantity: Annotated[int, Field(gt=0), Query()]): + return {"quantity": quantity} + + gw_event["path"] = "/items" + gw_event["httpMethod"] = "GET" + + # Passes the gt=0 constraint + gw_event["queryStringParameters"] = {"quantity": "5"} + assert app(gw_event, {})["statusCode"] == 200 + + # Violates gt=0 + gw_event["queryStringParameters"] = {"quantity": "-1"} + assert app(gw_event, {})["statusCode"] == 422 + + def test_validate_pydantic_query_params_with_config_dict_and_validators(gw_event): """Test that Pydantic models with ConfigDict, aliases, and validators work correctly""" @@ -3031,15 +3123,15 @@ def _post_json(app, path, payload): return result["statusCode"], json.loads(result["body"]) -# ---------- Optional[List[Model]] ---------- +# ---------- List[Model] | None ---------- def test_optional_list_body_with_list(): - """Optional[List[Model]] must preserve the full list.""" + """List[Model] | None must preserve the full list.""" app = APIGatewayRestResolver(enable_validation=True) @app.post("/items") - def handler(items: Annotated[Optional[List[_Item]], Body()]) -> Dict[str, Any]: + def handler(items: Annotated[List[_Item] | None, Body()]) -> Dict[str, Any]: assert isinstance(items, list) return {"count": len(items)} @@ -3049,11 +3141,11 @@ def handler(items: Annotated[Optional[List[_Item]], Body()]) -> Dict[str, Any]: def test_optional_list_body_with_none(): - """Optional[List[Model]] must accept a null body gracefully.""" + """List[Model] | None must accept a null body gracefully.""" app = APIGatewayRestResolver(enable_validation=True) @app.post("/items") - def handler(items: Annotated[Optional[List[_Item]], Body()] = None) -> Dict[str, Any]: + def handler(items: Annotated[List[_Item] | None, Body()] = None) -> Dict[str, Any]: return {"received_none": items is None} status, body = _post_json(app, "/items", None) @@ -3061,15 +3153,15 @@ def handler(items: Annotated[Optional[List[_Item]], Body()] = None) -> Dict[str, assert body["received_none"] is True -# ---------- Optional[Union[Model, List[Model]]] ---------- +# ---------- Union[Model, List[Model]] | None ---------- def test_optional_union_model_or_list_with_list(): - """Optional[Union[Model, List[Model]]] — send list, get full list.""" + """Union[Model, List[Model]] | None — send list, get full list.""" app = APIGatewayRestResolver(enable_validation=True) @app.post("/items") - def handler(items: Annotated[Optional[Union[_Item, List[_Item]]], Body()]) -> Dict[str, Any]: + def handler(items: Annotated[Union[_Item, List[_Item]] | None, Body()]) -> Dict[str, Any]: assert isinstance(items, list) return {"count": len(items)} @@ -3079,11 +3171,11 @@ def handler(items: Annotated[Optional[Union[_Item, List[_Item]]], Body()]) -> Di def test_optional_union_model_or_list_with_single(): - """Optional[Union[Model, List[Model]]] — send single obj, get single obj.""" + """Union[Model, List[Model]] | None — send single obj, get single obj.""" app = APIGatewayRestResolver(enable_validation=True) @app.post("/items") - def handler(items: Annotated[Optional[Union[_Item, List[_Item]]], Body()]) -> Dict[str, Any]: + def handler(items: Annotated[Union[_Item, List[_Item]] | None, Body()]) -> Dict[str, Any]: assert not isinstance(items, list) return {"name": items.name} @@ -3093,11 +3185,11 @@ def handler(items: Annotated[Optional[Union[_Item, List[_Item]]], Body()]) -> Di def test_optional_union_model_or_list_with_none(): - """Optional[Union[Model, List[Model]]] — send null, get None.""" + """Union[Model, List[Model]] | None — send null, get None.""" app = APIGatewayRestResolver(enable_validation=True) @app.post("/items") - def handler(items: Annotated[Optional[Union[_Item, List[_Item]]], Body()] = None) -> Dict[str, Any]: + def handler(items: Annotated[Union[_Item, List[_Item]] | None, Body()] = None) -> Dict[str, Any]: return {"is_none": items is None} status, body = _post_json(app, "/items", None) @@ -3196,11 +3288,11 @@ def handler(data: Annotated[Union[str, List[Dict[str, Any]]], Body()]) -> Dict[s def test_optional_rootmodel_list_body(): - """Optional[RootModel[List[Model]]] — list must not be truncated.""" + """RootModel[List[Model]] | None — list must not be truncated.""" app = APIGatewayRestResolver(enable_validation=True) @app.post("/items") - def handler(items: Annotated[Optional[_ItemCollection], Body()]) -> Dict[str, Any]: + def handler(items: Annotated[_ItemCollection | None, Body()]) -> Dict[str, Any]: return {"count": len(items.root)} status, body = _post_json(app, "/items", _THREE_ITEMS) diff --git a/tests/functional/event_handler/required_dependencies/test_http_resolver.py b/tests/functional/event_handler/required_dependencies/test_http_resolver.py index 40fb3d20c64..4665812e64a 100644 --- a/tests/functional/event_handler/required_dependencies/test_http_resolver.py +++ b/tests/functional/event_handler/required_dependencies/test_http_resolver.py @@ -1,4 +1,4 @@ -"""Tests for HttpResolverLocal - ASGI-compatible HTTP resolver for local development.""" +"""Tests for HttpResolverLocal - ASGI-compatible HTTP resolver.""" from __future__ import annotations @@ -11,10 +11,6 @@ from aws_lambda_powertools.event_handler import HttpResolverLocal, Response from aws_lambda_powertools.event_handler.http_resolver import MockLambdaContext -# Suppress warning for all tests -pytestmark = pytest.mark.filterwarnings("ignore:HttpResolverLocal is intended for local development") - - # ============================================================================= # ASGI Test Helpers # ============================================================================= @@ -1242,3 +1238,219 @@ def hello(): # THEN it returns 404 (method mismatch is treated as not found) assert captured["status_code"] == 404 + + +# ============================================================================= +# CORS Tests (issue #8267) +# ============================================================================= + + +@pytest.mark.asyncio +async def test_cors_options_preflight_returns_204(): + # GIVEN an app with CORSConfig and a POST route + from aws_lambda_powertools.event_handler.api_gateway import CORSConfig + + app = HttpResolverLocal(cors=CORSConfig(allow_origin="*")) + + @app.post("/items") + def create_item(): + return {"ok": True} + + # WHEN a browser sends a CORS preflight OPTIONS request + scope = { + "type": "http", + "method": "OPTIONS", + "path": "/items", + "query_string": b"", + "headers": [ + (b"origin", b"http://localhost:3000"), + (b"access-control-request-method", b"POST"), + ], + } + + receive = make_asgi_receive() + captured: dict[str, Any] = {"status_code": None, "headers": []} + + async def send(message: dict[str, Any]) -> None: + await asyncio.sleep(0) + if message["type"] == "http.response.start": + captured["status_code"] = message["status"] + captured["headers"].extend(message.get("headers", [])) + + await app(scope, receive, send) + + # THEN it returns 204 with CORS headers (not 500 or 404) + assert captured["status_code"] == 204 + + header_names = [name.lower() for name, _ in captured["headers"]] + assert b"access-control-allow-origin" in header_names + assert b"access-control-allow-methods" in header_names + + +@pytest.mark.asyncio +async def test_cors_options_preflight_with_exception_handler_does_not_return_500(): + # GIVEN an app with CORSConfig and a generic exception handler that returns 500 + import json + + from aws_lambda_powertools.event_handler.api_gateway import CORSConfig + + app = HttpResolverLocal(cors=CORSConfig(allow_origin="*")) + + @app.post("/items") + def create_item(): + return {"ok": True} + + @app.exception_handler(Exception) + def handle_server_error(ex: Exception): + return Response( + status_code=500, + content_type="application/json", + body=json.dumps({"error": "internal"}), + ) + + # WHEN a browser sends a CORS preflight OPTIONS request + scope = { + "type": "http", + "method": "OPTIONS", + "path": "/items", + "query_string": b"", + "headers": [ + (b"origin", b"http://localhost:3000"), + (b"access-control-request-method", b"POST"), + ], + } + + receive = make_asgi_receive() + captured: dict[str, Any] = {"status_code": None, "headers": []} + + async def send(message: dict[str, Any]) -> None: + await asyncio.sleep(0) + if message["type"] == "http.response.start": + captured["status_code"] = message["status"] + captured["headers"].extend(message.get("headers", [])) + + await app(scope, receive, send) + + # THEN the OPTIONS request returns 204, not 500 + assert captured["status_code"] == 204 + header_names = [name.lower() for name, _ in captured["headers"]] + assert b"access-control-allow-origin" in header_names + + +@pytest.mark.asyncio +async def test_no_cors_options_returns_404(): + # GIVEN an app WITHOUT CORSConfig + app = HttpResolverLocal() + + @app.post("/items") + def create_item(): + return {"ok": True} + + # WHEN a browser sends an OPTIONS request (no CORS configured) + scope = { + "type": "http", + "method": "OPTIONS", + "path": "/items", + "query_string": b"", + "headers": [], + } + + receive = make_asgi_receive() + send, captured = make_asgi_send() + + await app(scope, receive, send) + + # THEN it returns 404 (no CORS config, no special handling) + assert captured["status_code"] == 404 + + +@pytest.mark.asyncio +async def test_cors_options_includes_allowed_methods_header(): + # GIVEN an app with CORSConfig and multiple routes + from aws_lambda_powertools.event_handler.api_gateway import CORSConfig + + app = HttpResolverLocal(cors=CORSConfig(allow_origin="https://example.com")) + + @app.get("/resource") + def get_resource(): + return {"method": "GET"} + + @app.post("/resource") + def post_resource(): + return {"method": "POST"} + + # WHEN an OPTIONS preflight is sent + scope = { + "type": "http", + "method": "OPTIONS", + "path": "/resource", + "query_string": b"", + "headers": [ + (b"origin", b"https://example.com"), + (b"access-control-request-method", b"GET"), + ], + } + + receive = make_asgi_receive() + captured: dict[str, Any] = {"status_code": None, "headers": []} + + async def send(message: dict[str, Any]) -> None: + await asyncio.sleep(0) + if message["type"] == "http.response.start": + captured["status_code"] = message["status"] + captured["headers"].extend(message.get("headers", [])) + + await app(scope, receive, send) + + # THEN 204 is returned with Access-Control-Allow-Methods header + assert captured["status_code"] == 204 + allow_methods_headers = [v for name, v in captured["headers"] if name.lower() == b"access-control-allow-methods"] + assert len(allow_methods_headers) == 1 + + +@pytest.mark.asyncio +async def test_cors_disallowed_header_not_in_allow_headers(): + # GIVEN an app with CORSConfig that only allows specific headers + from aws_lambda_powertools.event_handler.api_gateway import CORSConfig + + app = HttpResolverLocal(cors=CORSConfig(allow_origin="*", allow_headers=["X-Custom-Allowed"])) + + @app.post("/items") + def create_item(): + return {"ok": True} + + # WHEN a preflight requests an unlisted header + scope = { + "type": "http", + "method": "OPTIONS", + "path": "/items", + "query_string": b"", + "headers": [ + (b"origin", b"http://localhost:3000"), + (b"access-control-request-method", b"POST"), + (b"access-control-request-headers", b"X-Not-Allowed"), + ], + } + + receive = make_asgi_receive() + captured: dict[str, Any] = {"status_code": None, "headers": []} + + async def send(message: dict[str, Any]) -> None: + await asyncio.sleep(0) + if message["type"] == "http.response.start": + captured["status_code"] = message["status"] + captured["headers"].extend(message.get("headers", [])) + + await app(scope, receive, send) + + # THEN the server still returns 204 (browser enforces the rejection, not the server) + assert captured["status_code"] == 204 + + # AND the unlisted header is absent from Access-Control-Allow-Headers + allow_headers_value = next( + (v.decode() for name, v in captured["headers"] if name.lower() == b"access-control-allow-headers"), + "", + ) + assert "X-Not-Allowed" not in allow_headers_value + # AND the explicitly allowed header IS present + assert "X-Custom-Allowed" in allow_headers_value diff --git a/tests/functional/idempotency/_boto3/test_idempotency.py b/tests/functional/idempotency/_boto3/test_idempotency.py index e5916dba0fa..56b317a9b5a 100644 --- a/tests/functional/idempotency/_boto3/test_idempotency.py +++ b/tests/functional/idempotency/_boto3/test_idempotency.py @@ -2,7 +2,7 @@ import dataclasses import datetime import warnings -from typing import Any, Optional +from typing import Any from unittest.mock import MagicMock, Mock import jmespath @@ -1046,6 +1046,10 @@ def test_is_missing_idempotency_key(): assert BasePersistenceLayer.is_missing_idempotency_key((None, None)) # GIVEN a dict of Nones THEN is_missing_idempotency_key is True assert BasePersistenceLayer.is_missing_idempotency_key({None: None}) + # GIVEN a dict with all-None values THEN is_missing_idempotency_key is True + assert BasePersistenceLayer.is_missing_idempotency_key({"user": None, "order": None}) + # GIVEN a dict with a non-None value THEN is_missing_idempotency_key is False + assert BasePersistenceLayer.is_missing_idempotency_key({"user": "abc"}) is False # GIVEN True THEN is_missing_idempotency_key is False assert BasePersistenceLayer.is_missing_idempotency_key(True) is False @@ -1114,6 +1118,19 @@ def test_raise_on_no_idempotency_key( assert "No data found to create a hashed idempotency_key" in str(excinfo.value) +def test_raise_on_no_idempotency_key_for_dict_jmespath(persistence_store: DynamoDBPersistenceLayer): + # GIVEN a dict multi-select expression whose values are missing + idempotency_config = IdempotencyConfig( + event_key_jmespath="{user: headers.user_id, order: body.order_id}", + raise_on_no_idempotency_key=True, + ) + persistence_store.configure(idempotency_config) + + # WHEN extracting the idempotency key THEN raise IdempotencyKeyError + with pytest.raises(IdempotencyKeyError, match="No data found to create a hashed idempotency_key"): + persistence_store._get_hashed_idempotency_key({"headers": {}, "body": {}}) + + @pytest.mark.parametrize( "idempotency_config", [ @@ -2086,7 +2103,7 @@ class PaymentOutput: config=config, output_serializer=output_serializer, ) - def collect_payment(payment: PaymentInput) -> Optional[PaymentOutput]: + def collect_payment(payment: PaymentInput) -> PaymentOutput | None: return PaymentOutput(**dataclasses.asdict(payment)) # WHEN diff --git a/tests/functional/idempotency/_pydantic/test_idempotency_with_pydantic.py b/tests/functional/idempotency/_pydantic/test_idempotency_with_pydantic.py index f8e3debbc30..b5decebafbe 100644 --- a/tests/functional/idempotency/_pydantic/test_idempotency_with_pydantic.py +++ b/tests/functional/idempotency/_pydantic/test_idempotency_with_pydantic.py @@ -1,5 +1,3 @@ -from typing import Optional - import pytest from pydantic import BaseModel @@ -252,7 +250,7 @@ class PaymentOutput(BaseModel): config=config, output_serializer=output_serializer, ) - def collect_payment(payment: PaymentInput) -> Optional[PaymentOutput]: + def collect_payment(payment: PaymentInput) -> PaymentOutput | None: return PaymentOutput(**payment.dict()) # WHEN diff --git a/tests/functional/idempotency/_redis/test_redis_layer.py b/tests/functional/idempotency/_redis/test_redis_layer.py index 22c3b9a6d83..6adb97a64a4 100644 --- a/tests/functional/idempotency/_redis/test_redis_layer.py +++ b/tests/functional/idempotency/_redis/test_redis_layer.py @@ -3,6 +3,7 @@ import datetime import json import time as t +from threading import Event, Lock as ThreadLock, Thread from unittest import mock import pytest @@ -26,6 +27,7 @@ STATUS_CONSTANTS, DataRecord, ) +from aws_lambda_powertools.utilities.idempotency.persistence.cache import CachePersistenceLayer from aws_lambda_powertools.utilities.idempotency.persistence.redis import ( RedisCachePersistenceLayer, ) @@ -198,6 +200,16 @@ def valid_record(): ) +@pytest.fixture +def in_progress_record_missing_expiry(): + return DataRecord( + idempotency_key="test_orphan_key", + status=STATUS_CONSTANTS["INPROGRESS"], + expiry_timestamp=int(datetime.datetime.now().timestamp()) + 60, + in_progress_expiry_timestamp=None, + ) + + @mock.patch("aws_lambda_powertools.utilities.idempotency.persistence.redis.redis", MockRedis()) def test_redis_connection_standalone(): # when RedisCachePersistenceLayer is init with the following params @@ -303,6 +315,73 @@ def test_redis_orphan_record_lock(orphan_record, valid_record): ) +@mock.patch("aws_lambda_powertools.utilities.idempotency.persistence.redis.redis", MockRedis()) +def test_redis_in_progress_record_missing_expiry_is_not_treated_as_orphan(in_progress_record_missing_expiry): + layer = RedisCachePersistenceLayer(host="host") + layer._put_in_progress_record(in_progress_record_missing_expiry) + + contender = DataRecord( + idempotency_key=in_progress_record_missing_expiry.idempotency_key, + status=STATUS_CONSTANTS["INPROGRESS"], + expiry_timestamp=in_progress_record_missing_expiry.expiry_timestamp + 60, + in_progress_expiry_timestamp=None, + ) + + with pytest.raises(IdempotencyItemAlreadyExistsError): + layer._put_in_progress_record(contender) + + stored_record = layer._get_record(in_progress_record_missing_expiry.idempotency_key) + assert stored_record.status == STATUS_CONSTANTS["INPROGRESS"] + assert stored_record.expiry_timestamp == in_progress_record_missing_expiry.expiry_timestamp + + +@pytest.mark.filterwarnings("ignore:Couldn't determine the remaining time left") +def test_idempotent_function_blocks_concurrent_invocation_without_lambda_context(): + layer = CachePersistenceLayer(client=MockRedis(host="localhost")) + first_invocation_started = Event() + release_first_invocation = Event() + execution_lock = ThreadLock() + execution_count = 0 + first_result = [] + first_errors = [] + + @idempotent_function(data_keyword_argument="record", persistence_store=layer) + def process(record): + nonlocal execution_count + with execution_lock: + execution_count += 1 + current_execution = execution_count + + if current_execution == 1: + first_invocation_started.set() + if not release_first_invocation.wait(timeout=5): + raise TimeoutError("Timed out waiting to release the first invocation") + + return {"execution": current_execution} + + def invoke_first(): + try: + first_result.append(process(record={"id": "same"})) + except Exception as exc: + first_errors.append(exc) + + first_invocation = Thread(target=invoke_first) + first_invocation.start() + assert first_invocation_started.wait(timeout=2) + + try: + with pytest.raises(IdempotencyAlreadyInProgressError): + process(record={"id": "same"}) + finally: + release_first_invocation.set() + first_invocation.join(timeout=5) + + assert not first_invocation.is_alive() + assert first_errors == [] + assert first_result == [{"execution": 1}] + assert execution_count == 1 + + @mock.patch("aws_lambda_powertools.utilities.idempotency.persistence.redis.redis", MockRedis()) def test_redis_error_in_progress(valid_record): layer = RedisCachePersistenceLayer(host="host", mode="standalone") diff --git a/tests/functional/idempotency/utils.py b/tests/functional/idempotency/utils.py index 2e1ee4ab821..c6b3ee9b1a0 100644 --- a/tests/functional/idempotency/utils.py +++ b/tests/functional/idempotency/utils.py @@ -1,6 +1,6 @@ import hashlib import json -from typing import Any, Dict, Optional +from typing import Any, Dict from botocore import stub from pytest import FixtureRequest @@ -88,7 +88,7 @@ def build_idempotency_put_item_response_stub( expiration: int, status: str, request: FixtureRequest, - validation_data: Optional[Any], + validation_data: Any | None, ): response = { "Item": { diff --git a/tests/functional/validator/_fastjsonschema/test_validator.py b/tests/functional/validator/_fastjsonschema/test_validator.py index d29efd09cae..e14bc5e01c1 100644 --- a/tests/functional/validator/_fastjsonschema/test_validator.py +++ b/tests/functional/validator/_fastjsonschema/test_validator.py @@ -77,12 +77,13 @@ def test_validate_accept_schema_custom_format( ) -@pytest.mark.parametrize("invalid_format", [None, False, {}, [], object]) +@pytest.mark.parametrize("invalid_format", [object, 123, 1.5, True]) def test_validate_invalid_custom_format( eventbridge_schema_registry_cloudtrail_v2_s3, eventbridge_cloudtrail_s3_head_object_event, invalid_format, ): + # formats must be a mapping; anything we can't look a format name up in fails to compile with pytest.raises(exceptions.InvalidSchemaFormatError): validate( event=eventbridge_cloudtrail_s3_head_object_event, diff --git a/tests/performance/data_masking/load_test_data_masking/pt-load-test-stack/function_1024/requirements.txt b/tests/performance/data_masking/load_test_data_masking/pt-load-test-stack/function_1024/requirements.txt index 397c11ba436..c168cb36ead 100644 --- a/tests/performance/data_masking/load_test_data_masking/pt-load-test-stack/function_1024/requirements.txt +++ b/tests/performance/data_masking/load_test_data_masking/pt-load-test-stack/function_1024/requirements.txt @@ -1,3 +1,3 @@ requests>=2.32.0 aws-lambda-powertools[tracer] -aws-encryption-sdk>=3.3.1,!=4.0.0,!=4.0.1,!=4.0.2,!=4.0.3,!=4.0.4 +aws-encryption-sdk>=4.0.5,<5.0.0 diff --git a/tests/performance/data_masking/load_test_data_masking/pt-load-test-stack/function_128/requirements.txt b/tests/performance/data_masking/load_test_data_masking/pt-load-test-stack/function_128/requirements.txt index 397c11ba436..c168cb36ead 100644 --- a/tests/performance/data_masking/load_test_data_masking/pt-load-test-stack/function_128/requirements.txt +++ b/tests/performance/data_masking/load_test_data_masking/pt-load-test-stack/function_128/requirements.txt @@ -1,3 +1,3 @@ requests>=2.32.0 aws-lambda-powertools[tracer] -aws-encryption-sdk>=3.3.1,!=4.0.0,!=4.0.1,!=4.0.2,!=4.0.3,!=4.0.4 +aws-encryption-sdk>=4.0.5,<5.0.0 diff --git a/tests/performance/data_masking/load_test_data_masking/pt-load-test-stack/function_1769/requirements.txt b/tests/performance/data_masking/load_test_data_masking/pt-load-test-stack/function_1769/requirements.txt index 397c11ba436..c168cb36ead 100644 --- a/tests/performance/data_masking/load_test_data_masking/pt-load-test-stack/function_1769/requirements.txt +++ b/tests/performance/data_masking/load_test_data_masking/pt-load-test-stack/function_1769/requirements.txt @@ -1,3 +1,3 @@ requests>=2.32.0 aws-lambda-powertools[tracer] -aws-encryption-sdk>=3.3.1,!=4.0.0,!=4.0.1,!=4.0.2,!=4.0.3,!=4.0.4 +aws-encryption-sdk>=4.0.5,<5.0.0 diff --git a/tests/unit/circuit_breaker/__init__.py b/tests/unit/circuit_breaker/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit/circuit_breaker/test_config_and_states.py b/tests/unit/circuit_breaker/test_config_and_states.py new file mode 100644 index 00000000000..03700dc91d3 --- /dev/null +++ b/tests/unit/circuit_breaker/test_config_and_states.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +import dataclasses + +import pytest + +from aws_lambda_powertools.utilities.circuit_breaker.config import CircuitBreakerConfig +from aws_lambda_powertools.utilities.circuit_breaker.exceptions import ( + CircuitBreakerConfigError, + CircuitBreakerOpenError, +) +from aws_lambda_powertools.utilities.circuit_breaker.persistence.record import CircuitStateRecord +from aws_lambda_powertools.utilities.circuit_breaker.states import CircuitInfo, CircuitState + + +def test_circuit_state_serializes_to_plain_string(): + assert str(CircuitState.OPEN) == "OPEN" + assert CircuitState.OPEN == "OPEN" + + +def test_circuit_info_is_immutable(): + info = CircuitInfo(name="payment", state=CircuitState.OPEN, failure_count=5, opened_at=123) + with pytest.raises(dataclasses.FrozenInstanceError): + info.name = "other" # type: ignore[misc] + + +def test_config_defaults(): + config = CircuitBreakerConfig() + assert config.failure_threshold == 5 + assert config.recovery_timeout == 30 + assert config.success_threshold == 3 + assert config.local_cache_max_age == 5 + assert config.handled_exceptions is None + assert config.ignored_exceptions is None + + +def test_config_rejects_both_exception_lists(): + with pytest.raises(CircuitBreakerConfigError, match="mutually exclusive"): + CircuitBreakerConfig(handled_exceptions=(TimeoutError,), ignored_exceptions=(ValueError,)) + + +@pytest.mark.parametrize("field", ["failure_threshold", "recovery_timeout", "success_threshold"]) +def test_config_rejects_non_positive_thresholds(field): + with pytest.raises(CircuitBreakerConfigError, match="positive integer"): + CircuitBreakerConfig(**{field: 0}) + + +def test_config_allows_zero_cache_age(): + assert CircuitBreakerConfig(local_cache_max_age=0).local_cache_max_age == 0 + + +def test_config_rejects_negative_cache_age(): + with pytest.raises(CircuitBreakerConfigError, match="non-negative"): + CircuitBreakerConfig(local_cache_max_age=-1) + + +def test_counts_as_failure_default_any_exception(): + config = CircuitBreakerConfig() + assert config.counts_as_failure(ValueError()) is True + assert config.counts_as_failure(TimeoutError()) is True + + +def test_counts_as_failure_allowlist(): + config = CircuitBreakerConfig(handled_exceptions=(TimeoutError, ConnectionError)) + assert config.counts_as_failure(TimeoutError()) is True + assert config.counts_as_failure(ValueError()) is False + + +def test_counts_as_failure_denylist(): + config = CircuitBreakerConfig(ignored_exceptions=(ValueError,)) + assert config.counts_as_failure(ValueError()) is False + assert config.counts_as_failure(KeyError()) is True + + +def test_config_normalizes_handled_exceptions_list_to_tuple(): + config = CircuitBreakerConfig(handled_exceptions=[TimeoutError, ConnectionError]) + assert config.handled_exceptions == (TimeoutError, ConnectionError) + # The reported bug: a list must not break counts_as_failure when the circuit evaluates a failure. + assert config.counts_as_failure(TimeoutError()) is True + assert config.counts_as_failure(ValueError()) is False + + +def test_config_normalizes_single_exception_type_to_tuple(): + config = CircuitBreakerConfig(handled_exceptions=ValueError) + assert config.handled_exceptions == (ValueError,) + assert config.counts_as_failure(ValueError()) is True + + +def test_config_normalizes_ignored_exceptions_list_to_tuple(): + config = CircuitBreakerConfig(ignored_exceptions=[ValueError]) + assert config.ignored_exceptions == (ValueError,) + assert config.counts_as_failure(ValueError()) is False + assert config.counts_as_failure(KeyError()) is True + + +def test_config_normalizes_iterator_of_exceptions(): + config = CircuitBreakerConfig(handled_exceptions=iter((TimeoutError, KeyError))) + assert config.handled_exceptions == (TimeoutError, KeyError) + + +@pytest.mark.parametrize("field", ["handled_exceptions", "ignored_exceptions"]) +def test_config_rejects_non_exception_type_in_list(field): + with pytest.raises(CircuitBreakerConfigError, match="only exception types"): + CircuitBreakerConfig(**{field: ["not-an-exception"]}) + + +@pytest.mark.parametrize("field", ["handled_exceptions", "ignored_exceptions"]) +@pytest.mark.parametrize("value", [5, "ValueError"]) +def test_config_rejects_non_iterable_or_str_exceptions(field, value): + with pytest.raises(CircuitBreakerConfigError, match="iterable of exception types"): + CircuitBreakerConfig(**{field: value}) + + +@pytest.mark.parametrize("field", ["handled_exceptions", "ignored_exceptions"]) +def test_config_rejects_empty_exceptions(field): + with pytest.raises(CircuitBreakerConfigError, match="at least one exception type"): + CircuitBreakerConfig(**{field: []}) + + +def test_open_error_carries_circuit_info(): + info = CircuitInfo(name="payment", state=CircuitState.OPEN, failure_count=5, opened_at=123) + error = CircuitBreakerOpenError("open", circuit=info) + assert error.circuit is info + + +def test_record_to_circuit_info_strips_internal_fields(): + record = CircuitStateRecord( + name="payment", + state=CircuitState.OPEN, + failure_count=5, + opened_at=123, + half_open_owner="env-abc", + expiry_timestamp=999, + ) + info = record.to_circuit_info() + assert info == CircuitInfo(name="payment", state=CircuitState.OPEN, failure_count=5, opened_at=123) + assert not hasattr(info, "half_open_owner") + assert not hasattr(info, "expiry_timestamp") diff --git a/tests/unit/data_classes/required_dependencies/test_bedrock_agent_function_event.py b/tests/unit/data_classes/required_dependencies/test_bedrock_agent_function_event.py index e055c894604..2ff81a21bb8 100644 --- a/tests/unit/data_classes/required_dependencies/test_bedrock_agent_function_event.py +++ b/tests/unit/data_classes/required_dependencies/test_bedrock_agent_function_event.py @@ -32,7 +32,7 @@ def test_bedrock_agent_function_event(): raw_parameters = raw_event["parameters"] assert len(parameters) == len(raw_parameters) - for param, raw_param in zip(parameters, raw_parameters): + for param, raw_param in zip(parameters, raw_parameters, strict=True): assert param.name == raw_param["name"] assert param.type == raw_param["type"] assert param.value == raw_param["value"] diff --git a/tests/unit/data_classes/required_dependencies/test_event_bridge_event.py b/tests/unit/data_classes/required_dependencies/test_event_bridge_event.py index 6dfc0c82485..e581cacae32 100644 --- a/tests/unit/data_classes/required_dependencies/test_event_bridge_event.py +++ b/tests/unit/data_classes/required_dependencies/test_event_bridge_event.py @@ -17,4 +17,4 @@ def test_event_bridge_event(): assert parsed_event.source == raw_event["source"] assert parsed_event.detail_type == raw_event["detail-type"] assert parsed_event.detail == raw_event["detail"] - assert parsed_event.replay_name == "replay_archive" + assert parsed_event.replay_name is None diff --git a/tests/unit/parser/_pydantic/schemas.py b/tests/unit/parser/_pydantic/schemas.py index 0713924c486..2565d649ee9 100644 --- a/tests/unit/parser/_pydantic/schemas.py +++ b/tests/unit/parser/_pydantic/schemas.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List from pydantic import BaseModel @@ -21,8 +21,8 @@ class MyDynamoBusiness(BaseModel): class MyDynamoScheme(DynamoDBStreamChangedRecordModel): - NewImage: Optional[MyDynamoBusiness] = None - OldImage: Optional[MyDynamoBusiness] = None + NewImage: MyDynamoBusiness | None = None + OldImage: MyDynamoBusiness | None = None class MyDynamoDBStreamRecordModel(DynamoDBStreamRecordModel): diff --git a/tests/unit/parser/_pydantic/test_dynamodb.py b/tests/unit/parser/_pydantic/test_dynamodb.py index 13ee5610e6d..f21f6d717c3 100644 --- a/tests/unit/parser/_pydantic/test_dynamodb.py +++ b/tests/unit/parser/_pydantic/test_dynamodb.py @@ -1,3 +1,5 @@ +from typing import Any + import pytest from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, parse @@ -86,8 +88,26 @@ def test_validate_event_does_not_conform_with_model(): parse(event=raw_event, model=MyDynamoBusiness, envelope=envelopes.DynamoDBStreamEnvelope) -def test_dynamo_db_stream_lambda_invocation_event(): +@pytest.mark.parametrize( + "response_context", + [ + pytest.param( + {"statusCode": 200, "executedVersion": "$LATEST"}, + id="without function error", + ), + pytest.param( + {"statusCode": 200, "executedVersion": "$LATEST", "functionError": None}, + id="with null function error", + ), + pytest.param( + {"statusCode": 200, "executedVersion": "$LATEST", "functionError": "Unhandled"}, + id="with function error", + ), + ], +) +def test_dynamo_db_stream_lambda_invocation_event(response_context: dict[str, Any]): raw_event = load_event("dynamoStreamLambdaInvocationEvent.json") + raw_event["responseContext"] = response_context parsed_event: DynamoDBStreamLambdaOnFailureDestinationModel = parse( event=raw_event, model=DynamoDBStreamLambdaOnFailureDestinationModel, @@ -111,6 +131,8 @@ def test_dynamo_db_stream_lambda_invocation_event(): ) assert parsed_event.ddb_stream_batch_info.stream_arn == raw_event["DDBStreamBatchInfo"]["streamArn"] assert parsed_event.request_context.model_dump(by_alias=True) == raw_event["requestContext"] - assert parsed_event.response_context.model_dump(by_alias=True) == raw_event["responseContext"] + assert parsed_event.response_context.status_code == response_context["statusCode"] + assert parsed_event.response_context.executed_version == response_context["executedVersion"] + assert parsed_event.response_context.function_error == response_context.get("functionError") assert parsed_event.timestamp.strftime("%Y-%m-%dT%H:%M:%SZ") == raw_event["timestamp"] assert parsed_event.version == raw_event["version"] diff --git a/tests/unit/parser/_pydantic/test_eventbridge.py b/tests/unit/parser/_pydantic/test_eventbridge.py index 585406e7095..86ebc5bc204 100644 --- a/tests/unit/parser/_pydantic/test_eventbridge.py +++ b/tests/unit/parser/_pydantic/test_eventbridge.py @@ -45,7 +45,7 @@ def test_handle_eventbridge_trigger_event_no_envelope(): assert parsed_event.resources == raw_event["resources"] assert parsed_event.source == raw_event["source"] assert parsed_event.detail_type == raw_event["detail-type"] - assert parsed_event.replay_name == raw_event["replay-name"] + assert parsed_event.replay_name is None def test_handle_invalid_event_with_eventbridge_envelope():