diff --git a/.github/workflows/bootstrap_region.yml b/.github/workflows/bootstrap_region.yml index ab10d310b0e..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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.sha }} - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.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@ec61189d14ec14c8efccab744f656cffd0e33f37 + 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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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 7aeb1c4e2bd..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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + 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 86dc50dd0eb..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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: 'Dependency Review' - uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 diff --git a/.github/workflows/layer_govcloud.yml b/.github/workflows/layer_govcloud.yml index 0ce1b7af356..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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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 6f7c6a94d38..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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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 3cce653182e..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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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 c1d72353898..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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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 caceb773dc6..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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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 3c6e87ab8c7..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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + 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 07cf205ba20..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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - 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 200e850675f..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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + 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@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.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@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.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@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + 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 19dcb626f41..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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - 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 @@ -78,7 +78,7 @@ jobs: - name: Complexity baseline run: make complexity-baseline - name: Upload coverage to Codecov - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # 6.0.0 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # 7.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml diff --git a/.github/workflows/quality_check_docs.yml b/.github/workflows/quality_check_docs.yml index 9a61cf90a23..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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - 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 497d0bea446..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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - 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@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.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@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.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 c6b23774277..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@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0 + - uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7.7.0 diff --git a/.github/workflows/release-v3.yml b/.github/workflows/release-v3.yml index 64099e11413..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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - 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 8112b19d953..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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + 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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.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 17f63216996..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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + 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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.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 adccac305cc..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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + 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 683992f4ac4..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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - 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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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 6a1e861d3de..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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + 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@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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 e2f187a40f2..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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + 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 994f1fb7685..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@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.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 9ef5c4b5674..6f1f07e1bc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,192 @@ # 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)) + + ## [v3.28.0] - 2026-04-14 ## Bug Fixes * **data_class:** merge querystring parameters in ALB/APIGW classes ([#8154](https://github.com/aws-powertools/powertools-lambda-python/issues/8154)) +* **event_handler:** read swagger files with UTF-8 encoding ([#8131](https://github.com/aws-powertools/powertools-lambda-python/issues/8131)) + +## Code Refactoring + +* **event_handler:** refactoring encoder file ([#8126](https://github.com/aws-powertools/powertools-lambda-python/issues/8126)) +* **event_handler:** refactoring proxy events ([#8125](https://github.com/aws-powertools/powertools-lambda-python/issues/8125)) +* **event_handler:** refactoring params to reduce code ([#8124](https://github.com/aws-powertools/powertools-lambda-python/issues/8124)) +* **event_handler:** extract OpenAPI schema generation from Route class ([#8098](https://github.com/aws-powertools/powertools-lambda-python/issues/8098)) +* **event_handlers:** remove unnecessary init methods ([#8127](https://github.com/aws-powertools/powertools-lambda-python/issues/8127)) + +## Documentation + +* adding new Lambda features ([#7917](https://github.com/aws-powertools/powertools-lambda-python/issues/7917)) +* add openapi docs ([#7939](https://github.com/aws-powertools/powertools-lambda-python/issues/7939)) + +## Features + +* **event_handler:** enrich request object ([#8153](https://github.com/aws-powertools/powertools-lambda-python/issues/8153)) +* **event_handler:** adding status_code OpenAPI field ([#8130](https://github.com/aws-powertools/powertools-lambda-python/issues/8130)) +* **event_handler:** add Dependency injection with Depends() ([#8128](https://github.com/aws-powertools/powertools-lambda-python/issues/8128)) ## Maintenance * version bump +* bump dependabot dependencies. ([#8152](https://github.com/aws-powertools/powertools-lambda-python/issues/8152)) +* **deps:** bump cryptography from 46.0.6 to 46.0.7 ([#8132](https://github.com/aws-powertools/powertools-lambda-python/issues/8132)) +* **deps-dev:** bump aws-cdk from 2.1117.0 to 2.1118.0 in the aws-cdk group ([#8142](https://github.com/aws-powertools/powertools-lambda-python/issues/8142)) +* **deps-dev:** bump types-python-dateutil from 2.9.0.20260305 to 2.9.0.20260402 ([#8114](https://github.com/aws-powertools/powertools-lambda-python/issues/8114)) +* **deps-dev:** bump aws-cdk from 2.1115.0 to 2.1117.0 in the aws-cdk group ([#8111](https://github.com/aws-powertools/powertools-lambda-python/issues/8111)) +* **deps-dev:** bump boto3-stubs from 1.42.74 to 1.42.84 ([#8115](https://github.com/aws-powertools/powertools-lambda-python/issues/8115)) +* **deps-dev:** bump types-protobuf from 6.32.1.20260221 to 7.34.1.20260403 ([#8117](https://github.com/aws-powertools/powertools-lambda-python/issues/8117)) +* **deps-dev:** bump testcontainers from 4.14.1 to 4.14.2 ([#8116](https://github.com/aws-powertools/powertools-lambda-python/issues/8116)) +* **deps-dev:** bump cfn-lint from 1.46.0 to 1.48.1 ([#8113](https://github.com/aws-powertools/powertools-lambda-python/issues/8113)) @@ -7626,7 +7803,12 @@ * 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.28.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 [v3.26.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.25.0...v3.26.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/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 041f6f7abf3..a323cf67d56 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -613,6 +613,63 @@ def _build_middleware_stack(self, router_middlewares: list[Callable[..., Any]], self._middleware_stack_built = True + async def call_async( + self, + router_middlewares: list[Callable], + app: ApiGatewayResolver, + route_arguments: dict[str, str], + ) -> dict | tuple | Response: + from aws_lambda_powertools.event_handler.middlewares.async_utils import ( + AsyncMiddlewareFrame, + _registered_api_adapter_async, + ) + + all_middlewares: list[Callable[..., Any]] = [] + + route_validation_enabled = ( + self.enable_validation if self.enable_validation is not None else app._enable_validation + ) + + if route_validation_enabled and not hasattr(app, "_request_validation_middleware"): + from aws_lambda_powertools.event_handler.middlewares.openapi_validation import ( + OpenAPIRequestValidationMiddleware, + OpenAPIResponseValidationMiddleware, + ) + + app._request_validation_middleware = OpenAPIRequestValidationMiddleware() + app._response_validation_middleware = OpenAPIResponseValidationMiddleware( + validation_serializer=app._serializer, + has_response_validation_error=app._has_response_validation_error, + ) + + if route_validation_enabled and hasattr(app, "_request_validation_middleware"): + all_middlewares.append(app._request_validation_middleware) + + all_middlewares.extend(router_middlewares + self.middlewares) + + if route_validation_enabled and hasattr(app, "_response_validation_middleware"): + all_middlewares.append(app._response_validation_middleware) + + all_middlewares.append(_registered_api_adapter_async) + + logger.debug(f"Building async middleware stack: {all_middlewares}") + + if app._debug: + print(f"\nProcessing Route (async):::{self.func.__name__} ({app.context['_path']})") + print("\nAsync Middleware Stack:") + print("=================") + print("\n".join(getattr(item, "__name__", "Unknown") for item in all_middlewares)) + print("=================") + + app.append_context(_route_args=route_arguments) + + # Build async chain from inside-out (not cached, avoids state conflicts with sync cache) + next_handler: Callable = self.func + for handler in reversed(all_middlewares): + next_handler = AsyncMiddlewareFrame(current_middleware=handler, next_middleware=next_handler) + + return await next_handler(app) + @property def dependant(self) -> Dependant: if self._dependant is None: @@ -2509,6 +2566,154 @@ def resolve(self, event: Mapping[str, Any], context: LambdaContext) -> dict[str, return response + async def resolve_async(self, event: Mapping[str, Any], context: LambdaContext) -> dict[str, Any]: + """Async version of resolve() for native async handler support. + + Use this method when your route handlers use async/await. The resolution + pipeline supports both sync and async handlers transparently. + + Parameters + ---------- + event: dict[str, Any] + Event + context: LambdaContext + Lambda context + Returns + ------- + dict + Returns the dict response + + Example + ------- + + ```python + import asyncio + from aws_lambda_powertools.event_handler import APIGatewayHttpResolver + + app = APIGatewayHttpResolver() + + @app.get("/async") + async def async_handler(): + return {"message": "async works"} + + def lambda_handler(event, context): + return asyncio.run(app.resolve_async(event, context)) + ``` + """ + if isinstance(event, BaseProxyEvent): + warnings.warn( + "You don't need to serialize event to Event Source Data Class when using Event Handler; " + "see issue #1152", + stacklevel=2, + ) + event = event.raw_event + + if self._debug: + print(self._serializer(cast(dict, event))) + + BaseRouter.current_event = self._to_proxy_event(cast(dict, event)) + BaseRouter.lambda_context = context + + response = (await self._resolve_async()).build(self.current_event, self._cors) + + if self._debug: + print("\nProcessed Middlewares:") + print("======================") + print("\n".join(self.processed_stack_frames)) + print("======================") + + self.clear_context() + + return response + + async def _resolve_async(self) -> ResponseBuilder: + 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: Match | None = route.rule.match(path) + if match_results: + logger.debug("Found a registered route. Calling async function") + 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) + + return await self._handle_not_found_async(method=method, path=path) + + async def _call_route_async(self, route: Route, route_arguments: dict[str, str]) -> ResponseBuilder: + try: + self._reset_processed_stack() + + response = await route.call_async( + router_middlewares=self._router_middlewares, + app=self, + route_arguments=route_arguments, + ) + + return self._response_builder_class( + response=self._to_response(response), # type: ignore[arg-type] + serializer=self._serializer, + route=route, + ) + except Exception as exc: + response_builder = self._call_exception_handler(exc, route) + if response_builder: + return response_builder + + logger.exception(exc) + if self._debug: + return self._response_builder_class( + response=Response( + status_code=500, + content_type=content_types.TEXT_PLAIN, + body="".join(traceback.format_exc()), + ), + serializer=self._serializer, + route=route, + ) + + raise + + async def _handle_not_found_async(self, method: str, path: str) -> ResponseBuilder: + logger.debug(f"No match found for path {path} and method {method}") + + def not_found_handler(): + _headers: dict[str, Any] = {} + + if self._cors and method == "OPTIONS": + logger.debug("Pre-flight request detected. Returning CORS with empty response") + _headers["Access-Control-Allow-Methods"] = CORSConfig.build_allow_methods(self._cors_methods) + return Response(status_code=204, content_type=None, headers=_headers, body="") + + custom_not_found_handler = self.exception_handler_manager.lookup_exception_handler(NotFoundError) + if custom_not_found_handler: + return custom_not_found_handler(NotFoundError()) + + return Response( + status_code=HTTPStatus.NOT_FOUND.value, + content_type=content_types.APPLICATION_JSON, + headers=_headers, + body={"statusCode": HTTPStatus.NOT_FOUND.value, "message": "Not found"}, + ) + + route = Route( + rule=self._compile_regex(r".*"), + method=method, + path=path, + func=not_found_handler, + cors=self._cors_enabled, + compress=False, + ) + + self.append_context(_route=route, _path=path) + + return await self._call_route_async(route=route, route_arguments={}) + def __call__(self, event, context) -> Any: return self.resolve(event, context) @@ -3138,22 +3343,13 @@ def _get_base_path(self) -> str: # ALB doesn't have a stage variable, so we just return an empty string return "" - # BedrockResponse is not used here but adding the same signature to keep strong typing @override def _to_response(self, result: dict | tuple | Response | BedrockResponse) -> Response | BedrockResponse: """Convert the route's result to a Response ALB requires a non-null body otherwise it converts as HTTP 5xx - - 3 main result types are supported: - - - Dict[str, Any]: Rest api response with just the Dict to json stringify and content-type is set to - application/json - - Tuple[dict, int]: Same dict handling as above but with the option of including a status code - - Response: returned as is, and allows for more flexibility """ - - # NOTE: Minor override for early return on Response with null body for ALB + # ALB doesn't support null body - convert before building the final response if isinstance(result, Response) and result.body is None: logger.debug("ALB doesn't allow None responses; converting to empty string") result.body = "" diff --git a/aws_lambda_powertools/event_handler/appsync.py b/aws_lambda_powertools/event_handler/appsync.py index 29c48d71cb1..253fad15d74 100644 --- a/aws_lambda_powertools/event_handler/appsync.py +++ b/aws_lambda_powertools/event_handler/appsync.py @@ -392,7 +392,7 @@ def resolver(self, type_name: str = "*", field_name: str | None = None) -> Calla ---------- type_name : str, optional GraphQL type e.g., Query, Mutation, by default "*" meaning any - field_name : Optional[str], optional + field_name : str | None, optional GraphQL field e.g., getTodo, createTodo, by default None Returns @@ -447,7 +447,7 @@ def batch_resolver( ---------- type_name : str, optional GraphQL type e.g., Query, Mutation, by default "*" meaning any - field_name : Optional[str], optional + field_name : str | None, optional GraphQL field e.g., getTodo, createTodo, by default None raise_on_error : bool, optional Whether to fail entire batch upon error, or handle errors gracefully (None), by default False 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/graphql_appsync/_registry.py b/aws_lambda_powertools/event_handler/graphql_appsync/_registry.py index dc88d904c25..1c4ed136c1b 100644 --- a/aws_lambda_powertools/event_handler/graphql_appsync/_registry.py +++ b/aws_lambda_powertools/event_handler/graphql_appsync/_registry.py @@ -64,7 +64,7 @@ def find_resolver(self, type_name: str, field_name: str) -> dict | None: Field name Return ---------- - Optional[Dict] + dict | None A dictionary with the resolver and if raise exception on error """ logger.debug(f"Looking for resolver for type={type_name}, field={field_name}.") diff --git a/aws_lambda_powertools/event_handler/graphql_appsync/base.py b/aws_lambda_powertools/event_handler/graphql_appsync/base.py index ea03c44a3b0..2e4986cd194 100644 --- a/aws_lambda_powertools/event_handler/graphql_appsync/base.py +++ b/aws_lambda_powertools/event_handler/graphql_appsync/base.py @@ -25,8 +25,6 @@ def resolver(self, type_name: str = "*", field_name: str | None = None) -> Calla Examples -------- ```python - from typing import Optional - from aws_lambda_powertools.event_handler import AppSyncResolver from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent from aws_lambda_powertools.utilities.typing import LambdaContext @@ -34,7 +32,7 @@ def resolver(self, type_name: str = "*", field_name: str | None = None) -> Calla app = AppSyncResolver() @app.resolver(type_name="Query", field_name="getPost") - def related_posts(event: AppSyncResolverEvent) -> Optional[list]: + def related_posts(event: AppSyncResolverEvent) -> list | None: return {"success": "ok"} def lambda_handler(event, context: LambdaContext) -> dict: @@ -76,8 +74,6 @@ def batch_resolver( Examples -------- ```python - from typing import Optional - from aws_lambda_powertools.event_handler import AppSyncResolver from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent from aws_lambda_powertools.utilities.typing import LambdaContext @@ -85,7 +81,7 @@ def batch_resolver( app = AppSyncResolver() @app.batch_resolver(type_name="Query", field_name="getPost") - def related_posts(event: AppSyncResolverEvent, id) -> Optional[list]: + def related_posts(event: AppSyncResolverEvent, id) -> list | None: return {"post_id": id} def lambda_handler(event, context: LambdaContext) -> dict: @@ -127,8 +123,6 @@ def async_batch_resolver( Examples -------- ```python - from typing import Optional - from aws_lambda_powertools.event_handler import AppSyncResolver from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent from aws_lambda_powertools.utilities.typing import LambdaContext @@ -136,7 +130,7 @@ def async_batch_resolver( app = AppSyncResolver() @app.async_batch_resolver(type_name="Query", field_name="getPost") - async def related_posts(event: AppSyncResolverEvent, id) -> Optional[list]: + async def related_posts(event: AppSyncResolverEvent, id) -> list | None: return {"post_id": id} def lambda_handler(event, context: LambdaContext) -> dict: diff --git a/aws_lambda_powertools/event_handler/http_resolver.py b/aws_lambda_powertools/event_handler/http_resolver.py index 168a6f44b8e..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, @@ -239,114 +224,14 @@ def _get_base_path(self) -> str: """Return the base path for HTTP resolver (no stage prefix).""" return "" - async def _resolve_async(self) -> dict: - """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: - """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) -> dict: - """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, - ) + async def _resolve_async(self) -> dict: # type: ignore[override] + """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/middlewares/async_utils.py b/aws_lambda_powertools/event_handler/middlewares/async_utils.py index 469ed1e96b1..4f375bc9b0b 100644 --- a/aws_lambda_powertools/event_handler/middlewares/async_utils.py +++ b/aws_lambda_powertools/event_handler/middlewares/async_utils.py @@ -86,13 +86,20 @@ def run_middleware() -> None: middleware_result_holder.append(result) except Exception as e: middleware_error_holder.append(e) + finally: + middleware_called_next.set() thread = threading.Thread(target=run_middleware, daemon=True) thread.start() - # Wait for the middleware to call next() + # Wait for the middleware to call next() or raise await middleware_called_next.wait() + # If middleware raised before calling next, propagate immediately + if not next_app_holder: + thread.join() + raise middleware_error_holder[0] + # Resolve the async next_handler on the event-loop real_response = await next_handler(next_app_holder[0]) real_response_holder.append(real_response) @@ -110,6 +117,56 @@ def run_middleware() -> None: return middleware_result_holder[0] +class AsyncMiddlewareFrame: + """Async version of MiddlewareFrame for the async middleware chain. + + Each instance wraps a middleware (sync or async) and the next handler in the stack. + When called, it auto-detects whether the current middleware is sync or async: + + - **Async middleware**: awaited directly with ``(app, next_middleware)`` + - **Sync middleware**: executed in a background thread so the event loop is never blocked + + Parameters + ---------- + current_middleware : Callable + The current middleware function to be called as a request is processed. + next_middleware : Callable + The next middleware in the middleware stack. + """ + + def __init__( + self, + current_middleware: Callable[..., Any], + next_middleware: Callable[..., Any], + ) -> None: + self.current_middleware: Callable[..., Any] = current_middleware + self.next_middleware: Callable[..., Any] = next_middleware + self._next_middleware_name = next_middleware.__name__ + + @property + def __name__(self) -> str: # noqa: A003 + return self.current_middleware.__name__ + + def __str__(self) -> str: + middleware_name = self.__name__ + return f"[{middleware_name}] next call chain is {middleware_name} -> {self._next_middleware_name}" + + async def __call__(self, app: ApiGatewayResolver) -> dict | tuple | Response: + logger.debug("AsyncMiddlewareFrame: %s", self) + app._push_processed_stack_frame(str(self)) + + if inspect.iscoroutinefunction(self.current_middleware): + return await self.current_middleware(app, self.next_middleware) + + loop = asyncio.get_running_loop() + + def sync_next(app: ApiGatewayResolver) -> Any: + future = asyncio.run_coroutine_threadsafe(self.next_middleware(app), loop) + return future.result() + + return await asyncio.to_thread(self.current_middleware, app, sync_next) + + async def _registered_api_adapter_async( app: ApiGatewayResolver, next_middleware: Callable[..., Any], diff --git a/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py b/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py index 05306b5ca8b..a0311184a51 100644 --- a/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py +++ b/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py @@ -296,9 +296,13 @@ def _handle_response(self, *, route: Route, response: Response): # JSON serialize the body without validation response.body = jsonable_encoder(response.body, custom_serializer=self._validation_serializer) else: + # ALB resolver converts None body to "" to prevent ALB 5xx errors, + # but the validation should still see it as None. + response_content = None if response.body == "" and field.type_ in (None, type(None)) else response.body + response.body = self._serialize_response_with_validation( field=field, - response_content=response.body, + response_content=response_content, has_route_custom_response_validation=route.custom_response_validation_http_code is not None, ) @@ -501,11 +505,11 @@ def _is_or_contains_sequence(annotation: Any) -> bool: This function handles complex type annotations like: - List[Model] - direct sequence - - Union[Model, List[Model]] - checks if any Union member is a sequence - - Optional[List[Model]] - Union[List[Model], None] - - RootModel[List[Model]] - checks if the RootModel wraps a sequence - - Optional[RootModel[List[Model]]] - Union member that is a RootModel - - RootModel[Union[Model, List[Model]]] - RootModel wrapping a Union with a sequence + - Union[Model, list[Model]] - checks if any Union member is a sequence + - list[Model] | None - Union[list[Model], None] + - RootModel[list[Model]] - checks if the RootModel wraps a sequence + - RootModel[list[Model]] | None - Union member that is a RootModel + - RootModel[Union[Model, list[Model]]] - RootModel wrapping a Union with a sequence """ # Direct sequence check if field_annotation_is_sequence(annotation): 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/models.py b/aws_lambda_powertools/event_handler/openapi/models.py index 53becd3f870..0372c6b21f2 100644 --- a/aws_lambda_powertools/event_handler/openapi/models.py +++ b/aws_lambda_powertools/event_handler/openapi/models.py @@ -1,6 +1,6 @@ # ruff: noqa: FA100 from enum import Enum -from typing import Any, Dict, List, Literal, Optional, Set, Union +from typing import Any, Literal, Union from pydantic import AnyUrl, BaseModel, ConfigDict, Field, model_validator from typing_extensions import Annotated @@ -25,7 +25,7 @@ class OpenAPIExtensions(BaseModel): and add only the provided value in the schema. """ - openapi_extensions: Optional[Dict[str, Any]] = None + openapi_extensions: dict[str, Any] | None = None # If the 'openapi_extensions' field is present in the 'values' dictionary, # And if the extension starts with x- (must respect the RFC) @@ -50,9 +50,9 @@ def serialize_openapi_extension_v2(self): # https://swagger.io/specification/#contact-object class Contact(BaseModel): - name: Optional[str] = None - url: Optional[AnyUrl] = None - email: Optional[str] = None + name: str | None = None + url: AnyUrl | None = None + email: str | None = None model_config = MODEL_CONFIG_ALLOW @@ -60,8 +60,8 @@ class Contact(BaseModel): # https://swagger.io/specification/#license-object class License(BaseModel): name: str - identifier: Optional[str] = None - url: Optional[AnyUrl] = None + identifier: str | None = None + url: AnyUrl | None = None model_config = MODEL_CONFIG_ALLOW @@ -69,21 +69,21 @@ class License(BaseModel): # https://swagger.io/specification/#info-object class Info(BaseModel): title: str - description: Optional[str] = None - termsOfService: Optional[str] = None - contact: Optional[Contact] = None - license: Optional[License] = None # noqa: A003 + description: str | None = None + termsOfService: str | None = None + contact: Contact | None = None + license: License | None = None # noqa: A003 version: str - summary: Optional[str] = None + summary: str | None = None model_config = MODEL_CONFIG_IGNORE # https://swagger.io/specification/#server-variable-object class ServerVariable(BaseModel): - enum: Annotated[Optional[List[str]], Field(min_length=1)] = None + enum: Annotated[list[str] | None, Field(min_length=1)] = None default: str - description: Optional[str] = None + description: str | None = None model_config = MODEL_CONFIG_ALLOW @@ -91,8 +91,8 @@ class ServerVariable(BaseModel): # https://swagger.io/specification/#server-object class Server(OpenAPIExtensions): url: Union[AnyUrl, str] - description: Optional[str] = None - variables: Optional[Dict[str, ServerVariable]] = None + description: str | None = None + variables: dict[str, ServerVariable] | None = None model_config = MODEL_CONFIG_ALLOW @@ -105,23 +105,23 @@ class Reference(BaseModel): # https://swagger.io/specification/#discriminator-object class Discriminator(BaseModel): propertyName: str - mapping: Optional[Dict[str, str]] = None + mapping: dict[str, str] | None = None # https://swagger.io/specification/#xml-object class XML(BaseModel): - name: Optional[str] = None - namespace: Optional[str] = None - prefix: Optional[str] = None - attribute: Optional[bool] = None - wrapped: Optional[bool] = None + name: str | None = None + namespace: str | None = None + prefix: str | None = None + attribute: bool | None = None + wrapped: bool | None = None model_config = MODEL_CONFIG_ALLOW # https://swagger.io/specification/#external-documentation-object class ExternalDocumentation(BaseModel): - description: Optional[str] = None + description: str | None = None url: AnyUrl model_config = MODEL_CONFIG_ALLOW @@ -131,81 +131,81 @@ class ExternalDocumentation(BaseModel): class Schema(BaseModel): # Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-the-json-schema-core-vocabu # Core Vocabulary - schema_: Optional[str] = Field(default=None, alias="$schema") - vocabulary: Optional[str] = Field(default=None, alias="$vocabulary") - id: Optional[str] = Field(default=None, alias="$id") # noqa: A003 - anchor: Optional[str] = Field(default=None, alias="$anchor") - dynamicAnchor: Optional[str] = Field(default=None, alias="$dynamicAnchor") - ref: Optional[str] = Field(default=None, alias="$ref") - dynamicRef: Optional[str] = Field(default=None, alias="$dynamicRef") - defs: Optional[Dict[str, "SchemaOrBool"]] = Field(default=None, alias="$defs") - comment: Optional[str] = Field(default=None, alias="$comment") + schema_: str | None = Field(default=None, alias="$schema") + vocabulary: str | None = Field(default=None, alias="$vocabulary") + id: str | None = Field(default=None, alias="$id") # noqa: A003 + anchor: str | None = Field(default=None, alias="$anchor") + dynamicAnchor: str | None = Field(default=None, alias="$dynamicAnchor") + ref: str | None = Field(default=None, alias="$ref") + dynamicRef: str | None = Field(default=None, alias="$dynamicRef") + defs: dict[str, "SchemaOrBool"] | None = Field(default=None, alias="$defs") + comment: str | None = Field(default=None, alias="$comment") # Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-a-vocabulary-for-applying-s # A Vocabulary for Applying Subschemas - allOf: Optional[List["SchemaOrBool"]] = None - anyOf: Optional[List["SchemaOrBool"]] = None - oneOf: Optional[List["SchemaOrBool"]] = None - not_: Optional["SchemaOrBool"] = Field(default=None, alias="not") - if_: Optional["SchemaOrBool"] = Field(default=None, alias="if") - then: Optional["SchemaOrBool"] = None - else_: Optional["SchemaOrBool"] = Field(default=None, alias="else") - dependentSchemas: Optional[Dict[str, "SchemaOrBool"]] = None - prefixItems: Optional[List["SchemaOrBool"]] = None + allOf: list["SchemaOrBool"] | None = None + anyOf: list["SchemaOrBool"] | None = None + oneOf: list["SchemaOrBool"] | None = None + not_: "SchemaOrBool | None" = Field(default=None, alias="not") + if_: "SchemaOrBool | None" = Field(default=None, alias="if") + then: "SchemaOrBool | None" = None + else_: "SchemaOrBool | None" = Field(default=None, alias="else") + dependentSchemas: dict[str, "SchemaOrBool"] | None = None + prefixItems: list["SchemaOrBool"] | None = None # MAINTENANCE: uncomment and remove below when deprecating Pydantic v1 # MAINTENANCE: It generates a list of schemas for tuples, before prefixItems was available # MAINTENANCE: items: Optional["SchemaOrBool"] = None - items: Optional[Union["SchemaOrBool", List["SchemaOrBool"]]] = None - contains: Optional["SchemaOrBool"] = None - properties: Optional[Dict[str, "SchemaOrBool"]] = None - patternProperties: Optional[Dict[str, "SchemaOrBool"]] = None - additionalProperties: Optional["SchemaOrBool"] = None - propertyNames: Optional["SchemaOrBool"] = None - unevaluatedItems: Optional["SchemaOrBool"] = None - unevaluatedProperties: Optional["SchemaOrBool"] = None + items: Union["SchemaOrBool", list["SchemaOrBool"]] | None = None + contains: "SchemaOrBool | None" = None + properties: dict[str, "SchemaOrBool"] | None = None + patternProperties: dict[str, "SchemaOrBool"] | None = None + additionalProperties: "SchemaOrBool | None" = None + propertyNames: "SchemaOrBool | None" = None + unevaluatedItems: "SchemaOrBool | None" = None + unevaluatedProperties: "SchemaOrBool | None" = None # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-structural # A Vocabulary for Structural Validation - type: Optional[str] = None # noqa: A003 - enum: Optional[List[Any]] = None - const: Optional[Any] = None - multipleOf: Optional[float] = Field(default=None, gt=0) - maximum: Optional[float] = None - exclusiveMaximum: Optional[float] = None - minimum: Optional[float] = None - exclusiveMinimum: Optional[float] = None - maxLength: Optional[int] = Field(default=None, ge=0) - minLength: Optional[int] = Field(default=None, ge=0) - pattern: Optional[str] = None - maxItems: Optional[int] = Field(default=None, ge=0) - minItems: Optional[int] = Field(default=None, ge=0) - uniqueItems: Optional[bool] = None - maxContains: Optional[int] = Field(default=None, ge=0) - minContains: Optional[int] = Field(default=None, ge=0) - maxProperties: Optional[int] = Field(default=None, ge=0) - minProperties: Optional[int] = Field(default=None, ge=0) - required: Optional[List[str]] = None - dependentRequired: Optional[Dict[str, Set[str]]] = None + type: str | None = None # noqa: A003 + enum: list[Any] | None = None + const: Any | None = None + multipleOf: float | None = Field(default=None, gt=0) + maximum: float | None = None + exclusiveMaximum: float | None = None + minimum: float | None = None + exclusiveMinimum: float | None = None + maxLength: int | None = Field(default=None, ge=0) + minLength: int | None = Field(default=None, ge=0) + pattern: str | None = None + maxItems: int | None = Field(default=None, ge=0) + minItems: int | None = Field(default=None, ge=0) + uniqueItems: bool | None = None + maxContains: int | None = Field(default=None, ge=0) + minContains: int | None = Field(default=None, ge=0) + maxProperties: int | None = Field(default=None, ge=0) + minProperties: int | None = Field(default=None, ge=0) + required: list[str] | None = None + dependentRequired: dict[str, set[str]] | None = None # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-vocabularies-for-semantic-c # Vocabularies for Semantic Content With "format" - format: Optional[str] = None # noqa: A003 + format: str | None = None # noqa: A003 # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-the-conten # A Vocabulary for the Contents of String-Encoded Data - contentEncoding: Optional[str] = None - contentMediaType: Optional[str] = None - contentSchema: Optional["SchemaOrBool"] = None + contentEncoding: str | None = None + contentMediaType: str | None = None + contentSchema: "SchemaOrBool | None" = None # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-basic-meta # A Vocabulary for Basic Meta-Data Annotations - title: Optional[str] = None - description: Optional[str] = None - default: Optional[Any] = None - deprecated: Optional[bool] = None - readOnly: Optional[bool] = None - writeOnly: Optional[bool] = None - examples: Optional[List[Any]] = None + title: str | None = None + description: str | None = None + default: Any | None = None + deprecated: bool | None = None + readOnly: bool | None = None + writeOnly: bool | None = None + examples: list[Any] | None = None # Ref: OpenAPI 3.0.0: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#schema-object # Schema Object - discriminator: Optional[Discriminator] = None - xml: Optional[XML] = None - externalDocs: Optional[ExternalDocumentation] = None + discriminator: Discriminator | None = None + xml: XML | None = None + externalDocs: ExternalDocumentation | None = None model_config = MODEL_CONFIG_ALLOW @@ -217,10 +217,10 @@ class Schema(BaseModel): # https://swagger.io/specification/#example-object class Example(BaseModel): - summary: Optional[str] = None - description: Optional[str] = None - value: Optional[Any] = None - externalValue: Optional[AnyUrl] = None + summary: str | None = None + description: str | None = None + value: Any | None = None + externalValue: AnyUrl | None = None model_config = MODEL_CONFIG_ALLOW @@ -234,37 +234,37 @@ class ParameterInType(Enum): # https://swagger.io/specification/#encoding-object class Encoding(BaseModel): - contentType: Optional[str] = None - headers: Optional[Dict[str, Union["Header", Reference]]] = None - style: Optional[str] = None - explode: Optional[bool] = None - allowReserved: Optional[bool] = None + contentType: str | None = None + headers: dict[str, Union["Header", Reference]] | None = None + style: str | None = None + explode: bool | None = None + allowReserved: bool | None = None model_config = MODEL_CONFIG_ALLOW # https://swagger.io/specification/#media-type-object class MediaType(BaseModel): - schema_: Optional[Union[Schema, Reference]] = Field(default=None, alias="schema") - examples: Optional[Dict[str, Union[Example, Reference]]] = None - encoding: Optional[Dict[str, Encoding]] = None + schema_: Union[Schema, Reference] | None = Field(default=None, alias="schema") + examples: dict[str, Union[Example, Reference]] | None = None + encoding: dict[str, Encoding] | None = None model_config = MODEL_CONFIG_ALLOW # https://swagger.io/specification/#parameter-object class ParameterBase(BaseModel): - description: Optional[str] = None - required: Optional[bool] = None - deprecated: Optional[bool] = None + description: str | None = None + required: bool | None = None + deprecated: bool | None = None # Serialization rules for simple scenarios - style: Optional[str] = None - explode: Optional[bool] = None - allowReserved: Optional[bool] = None - schema_: Optional[Union[Schema, Reference]] = Field(default=None, alias="schema") - examples: Optional[Dict[str, Union[Example, Reference]]] = None + style: str | None = None + explode: bool | None = None + allowReserved: bool | None = None + schema_: Union[Schema, Reference] | None = Field(default=None, alias="schema") + examples: dict[str, Union[Example, Reference]] | None = None # Serialization rules for more complex scenarios - content: Optional[Dict[str, MediaType]] = None + content: dict[str, MediaType] | None = None model_config = MODEL_CONFIG_ALLOW @@ -280,21 +280,21 @@ class Header(ParameterBase): # https://swagger.io/specification/#request-body-object class RequestBody(BaseModel): - description: Optional[str] = None - content: Dict[str, MediaType] - required: Optional[bool] = None + description: str | None = None + content: dict[str, MediaType] + required: bool | None = None model_config = MODEL_CONFIG_ALLOW # https://swagger.io/specification/#link-object class Link(BaseModel): - operationRef: Optional[str] = None - operationId: Optional[str] = None - parameters: Optional[Dict[str, Union[Any, str]]] = None - requestBody: Optional[Union[Any, str]] = None - description: Optional[str] = None - server: Optional[Server] = None + operationRef: str | None = None + operationId: str | None = None + parameters: dict[str, Union[Any, str]] | None = None + requestBody: Union[Any, str] | None = None + description: str | None = None + server: Server | None = None model_config = MODEL_CONFIG_ALLOW @@ -302,9 +302,9 @@ class Link(BaseModel): # https://swagger.io/specification/#response-object class Response(BaseModel): description: str - headers: Optional[Dict[str, Union[Header, Reference]]] = None - content: Optional[Dict[str, MediaType]] = None - links: Optional[Dict[str, Union[Link, Reference]]] = None + headers: dict[str, Union[Header, Reference]] | None = None + content: dict[str, MediaType] | None = None + links: dict[str, Union[Link, Reference]] | None = None model_config = MODEL_CONFIG_ALLOW @@ -312,46 +312,46 @@ class Response(BaseModel): # https://swagger.io/specification/#tag-object class Tag(BaseModel): name: str - description: Optional[str] = None - externalDocs: Optional[ExternalDocumentation] = None + description: str | None = None + externalDocs: ExternalDocumentation | None = None model_config = MODEL_CONFIG_ALLOW # https://swagger.io/specification/#operation-object class Operation(OpenAPIExtensions): - tags: Optional[List[str]] = None - summary: Optional[str] = None - description: Optional[str] = None - externalDocs: Optional[ExternalDocumentation] = None - operationId: Optional[str] = None - parameters: Optional[List[Union[Parameter, Reference]]] = None - requestBody: Optional[Union[RequestBody, Reference]] = None + tags: list[str] | None = None + summary: str | None = None + description: str | None = None + externalDocs: ExternalDocumentation | None = None + operationId: str | None = None + parameters: list[Union[Parameter, Reference]] | None = None + requestBody: Union[RequestBody, Reference] | None = None # Using Any for Specification Extensions - responses: Optional[Dict[int, Union[Response, Any]]] = None - callbacks: Optional[Dict[str, Union[Dict[str, "PathItem"], Reference]]] = None - deprecated: Optional[bool] = None - security: Optional[List[Dict[str, List[str]]]] = None - servers: Optional[List[Server]] = None + responses: dict[int, Union[Response, Any]] | None = None + callbacks: dict[str, Union[dict[str, "PathItem"], Reference]] | None = None + deprecated: bool | None = None + security: list[dict[str, list[str]]] | None = None + servers: list[Server] | None = None model_config = MODEL_CONFIG_ALLOW # https://swagger.io/specification/#path-item-object class PathItem(BaseModel): - ref: Optional[str] = Field(default=None, alias="$ref") - summary: Optional[str] = None - description: Optional[str] = None - get: Optional[Operation] = None - put: Optional[Operation] = None - post: Optional[Operation] = None - delete: Optional[Operation] = None - options: Optional[Operation] = None - head: Optional[Operation] = None - patch: Optional[Operation] = None - trace: Optional[Operation] = None - servers: Optional[List[Server]] = None - parameters: Optional[List[Union[Parameter, Reference]]] = None + ref: str | None = Field(default=None, alias="$ref") + summary: str | None = None + description: str | None = None + get: Operation | None = None + put: Operation | None = None + post: Operation | None = None + delete: Operation | None = None + options: Operation | None = None + head: Operation | None = None + patch: Operation | None = None + trace: Operation | None = None + servers: list[Server] | None = None + parameters: list[Union[Parameter, Reference]] | None = None model_config = MODEL_CONFIG_ALLOW @@ -367,7 +367,7 @@ class SecuritySchemeType(Enum): class SecurityBase(OpenAPIExtensions): type_: SecuritySchemeType = Field(alias="type") - description: Optional[str] = None + description: str | None = None model_config = {"extra": "allow", "populate_by_name": True} @@ -391,12 +391,12 @@ class HTTPBase(SecurityBase): class HTTPBearer(HTTPBase): # type: ignore[override] scheme: Literal["bearer"] = "bearer" - bearerFormat: Optional[str] = None + bearerFormat: str | None = None class OAuthFlow(BaseModel): - refreshUrl: Optional[str] = None - scopes: Dict[str, str] = {} + refreshUrl: str | None = None + scopes: dict[str, str] = {} model_config = MODEL_CONFIG_ALLOW @@ -419,10 +419,10 @@ class OAuthFlowAuthorizationCode(OAuthFlow): class OAuthFlows(BaseModel): - implicit: Optional[OAuthFlowImplicit] = None - password: Optional[OAuthFlowPassword] = None - clientCredentials: Optional[OAuthFlowClientCredentials] = None - authorizationCode: Optional[OAuthFlowAuthorizationCode] = None + implicit: OAuthFlowImplicit | None = None + password: OAuthFlowPassword | None = None + clientCredentials: OAuthFlowClientCredentials | None = None + authorizationCode: OAuthFlowAuthorizationCode | None = None model_config = MODEL_CONFIG_ALLOW @@ -449,17 +449,17 @@ class MutualTLS(SecurityBase): # https://swagger.io/specification/#components-object class Components(BaseModel): - schemas: Optional[Dict[str, Union[Schema, Reference]]] = None - responses: Optional[Dict[str, Union[Response, Reference]]] = None - parameters: Optional[Dict[str, Union[Parameter, Reference]]] = None - examples: Optional[Dict[str, Union[Example, Reference]]] = None - requestBodies: Optional[Dict[str, Union[RequestBody, Reference]]] = None - headers: Optional[Dict[str, Union[Header, Reference]]] = None - securitySchemes: Optional[Dict[str, Union[SecurityScheme, Reference]]] = None - links: Optional[Dict[str, Union[Link, Reference]]] = None + schemas: dict[str, Union[Schema, Reference]] | None = None + responses: dict[str, Union[Response, Reference]] | None = None + parameters: dict[str, Union[Parameter, Reference]] | None = None + examples: dict[str, Union[Example, Reference]] | None = None + requestBodies: dict[str, Union[RequestBody, Reference]] | None = None + headers: dict[str, Union[Header, Reference]] | None = None + securitySchemes: dict[str, Union[SecurityScheme, Reference]] | None = None + links: dict[str, Union[Link, Reference]] | None = None # Using Any for Specification Extensions - callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None - pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None + callbacks: dict[str, Union[dict[str, PathItem], Reference, Any]] | None = None + pathItems: dict[str, Union[PathItem, Reference]] | None = None model_config = MODEL_CONFIG_ALLOW @@ -468,15 +468,15 @@ class Components(BaseModel): class OpenAPI(OpenAPIExtensions): openapi: str info: Info - jsonSchemaDialect: Optional[str] = None - servers: Optional[List[Server]] = None + jsonSchemaDialect: str | None = None + servers: list[Server] | None = None # Using Any for Specification Extensions - paths: Optional[Dict[str, Union[PathItem, Any]]] = None - webhooks: Optional[Dict[str, Union[PathItem, Reference]]] = None - components: Optional[Components] = None - security: Optional[List[Dict[str, List[str]]]] = None - tags: Optional[List[Tag]] = None - externalDocs: Optional[ExternalDocumentation] = None + paths: dict[str, Union[PathItem, Any]] | None = None + webhooks: dict[str, Union[PathItem, Reference]] | None = None + components: Components | None = None + security: list[dict[str, list[str]]] | None = None + tags: list[Tag] | None = None + externalDocs: ExternalDocumentation | None = None model_config = MODEL_CONFIG_ALLOW diff --git a/aws_lambda_powertools/event_handler/openapi/params.py b/aws_lambda_powertools/event_handler/openapi/params.py index 1ade081959f..29c0b2abcdb 100644 --- a/aws_lambda_powertools/event_handler/openapi/params.py +++ b/aws_lambda_powertools/event_handler/openapi/params.py @@ -901,7 +901,7 @@ def analyze_param( if is_response_param: field_info.default = Required - field = _create_model_field(field_info, type_annotation, param_name, is_path_param) + field = _create_model_field(field_info, type_annotation, param_name, is_path_param, is_response_param) return field @@ -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 @@ -1138,6 +1140,7 @@ def _create_model_field( type_annotation: Any, param_name: str, is_path_param: bool, + is_response_param: bool = False, ) -> ModelField | None: """ Create a new ModelField from a FieldInfo and type annotation. @@ -1164,4 +1167,5 @@ def _create_model_field( alias=field_info.alias, required=field_info.default in (Required, Undefined), field_info=field_info, + mode="serialization" if is_response_param else "validation", ) 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/event_handler/openapi/swagger_ui/oauth2.py b/aws_lambda_powertools/event_handler/openapi/swagger_ui/oauth2.py index 4cafdfe401c..0f810afdb98 100644 --- a/aws_lambda_powertools/event_handler/openapi/swagger_ui/oauth2.py +++ b/aws_lambda_powertools/event_handler/openapi/swagger_ui/oauth2.py @@ -1,6 +1,8 @@ # ruff: noqa: E501 FA100 +from __future__ import annotations + import warnings -from typing import Dict, Optional, Sequence +from typing import Sequence from pydantic import BaseModel, Field, field_validator @@ -17,14 +19,14 @@ class OAuth2Config(BaseModel): """ # The client ID for the OAuth2 application - clientId: Optional[str] = Field(alias="client_id", default=None) + clientId: str | None = Field(alias="client_id", default=None) # The client secret for the OAuth2 application. This is sensitive information and requires the explicit presence # of the POWERTOOLS_DEV environment variable. - clientSecret: Optional[str] = Field(alias="client_secret", default=None) + clientSecret: str | None = Field(alias="client_secret", default=None) # The realm in which the OAuth2 application is registered. Optional. - realm: Optional[str] = Field(default=None) + realm: str | None = Field(default=None) # The name of the OAuth2 application appName: str = Field(alias="app_name") @@ -33,7 +35,7 @@ class OAuth2Config(BaseModel): scopes: Sequence[str] = Field(default=[]) # Additional query string parameters to be included in the OAuth2 request. Defaults to an empty dictionary. - additionalQueryStringParams: Dict[str, str] = Field(alias="additional_query_string_params", default={}) + additionalQueryStringParams: dict[str, str] = Field(alias="additional_query_string_params", default={}) # Whether to use basic authentication with the access code grant type. Defaults to False. useBasicAuthenticationWithAccessCodeGrant: bool = Field( @@ -47,7 +49,7 @@ class OAuth2Config(BaseModel): model_config = MODEL_CONFIG_ALLOW @field_validator("clientSecret") - def client_secret_only_on_dev(cls, v: Optional[str]) -> Optional[str]: + def client_secret_only_on_dev(cls, v: str | None) -> str | None: if not v: return None diff --git a/aws_lambda_powertools/event_handler/util.py b/aws_lambda_powertools/event_handler/util.py index 02fb805fa52..ed53469b0a6 100644 --- a/aws_lambda_powertools/event_handler/util.py +++ b/aws_lambda_powertools/event_handler/util.py @@ -74,9 +74,9 @@ def _validate_openapi_security_parameters( Parameters ---------- - security: List[Dict[str, List[str]]] + security: list[dict[str, list[str]]] A list of security requirements - security_schemes: Optional[Dict[str, Any]] + security_schemes: dict[str, Any] | None A dictionary mapping security scheme names to their corresponding security scheme objects. Returns 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 067def9eece..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.28.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/data_masking/provider/base.py b/aws_lambda_powertools/utilities/data_masking/provider/base.py index 7905fa57db8..d05e8bde1cf 100644 --- a/aws_lambda_powertools/utilities/data_masking/provider/base.py +++ b/aws_lambda_powertools/utilities/data_masking/provider/base.py @@ -11,7 +11,7 @@ from collections.abc import Callable PRESERVE_CHARS = set("-_. ") -_regex_cache = {} +_regex_cache: dict[str, re.Pattern[str]] = {} JSON_DUMPS_CALL = functools.partial(json.dumps, ensure_ascii=False) diff --git a/aws_lambda_powertools/utilities/idempotency/base.py b/aws_lambda_powertools/utilities/idempotency/base.py index f6a3563c103..bee109ef842 100644 --- a/aws_lambda_powertools/utilities/idempotency/base.py +++ b/aws_lambda_powertools/utilities/idempotency/base.py @@ -167,7 +167,7 @@ def _process_idempotency(self, is_replay: bool): # We give preference to ReturnValuesOnConditionCheckFailure because it is a faster and more cost-effective # way of retrieving the existing record after a failed conditional write operation. record = exc.old_data_record or self._get_idempotency_record() - if is_replay and record is not None and record.status == "INPROGRESS": + if is_replay and record is not None and record.status == STATUS_CONSTANTS["INPROGRESS"]: return self._get_function_response() # If a record is found, handle it for status if record: @@ -296,7 +296,7 @@ def _get_function_response(self): else: try: - serialized_response: dict = self.output_serializer.to_dict(response) if response else None + serialized_response: dict = self.output_serializer.to_dict(response) if response is not None else None self.persistence_store.save_success(data=self.data, result=serialized_response) except Exception as save_exception: raise IdempotencyPersistenceLayerError( 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 d1c490ee0f3..ce7100bf215 100644 --- a/aws_lambda_powertools/utilities/idempotency/persistence/redis.py +++ b/aws_lambda_powertools/utilities/idempotency/persistence/redis.py @@ -5,7 +5,7 @@ import logging from contextlib import contextmanager from datetime import timedelta -from typing import Any, Literal, Protocol +from typing import Any, Literal, Protocol, cast import redis from typing_extensions import TypeAlias, deprecated @@ -76,7 +76,7 @@ def set( # noqa ) -> bool | None: raise NotImplementedError - def delete(self, keys: bytes | str | memoryview) -> Any: + def delete(self, *names: bytes | str | memoryview) -> Any: raise NotImplementedError @@ -185,7 +185,7 @@ def _init_client(self) -> RedisClientProtocol: try: if self.url: logger.debug(f"Using URL format to connect to Cache: {self.host}") - return client.from_url(url=self.url) + return cast(RedisClientProtocol, client.from_url(url=self.url)) else: # Cache in cluster mode doesn't support db parameter extra_param_connection: dict[str, Any] = {} @@ -193,14 +193,17 @@ def _init_client(self) -> RedisClientProtocol: extra_param_connection = {"db": self.db_index} logger.debug(f"Using arguments to connect to Cache: {self.host}") - return client( - host=self.host, - port=self.port, - username=self.username, - password=self.password, - decode_responses=True, - ssl=self.ssl, - **extra_param_connection, + return cast( + RedisClientProtocol, + client( + host=self.host, + port=self.port, + username=self.username, + password=self.password, + decode_responses=True, + ssl=self.ssl, + **extra_param_connection, + ), ) except redis.exceptions.ConnectionError as exc: logger.debug(f"Cannot connect to Cache endpoint: {self.host}") @@ -332,8 +335,8 @@ def _item_to_data_record(self, idempotency_key: str, item: dict[str, Any]) -> Da idempotency_key=idempotency_key, status=item[self.status_attr], in_progress_expiry_timestamp=in_progress_expiry_timestamp, - response_data=str(item.get(self.data_attr)), - payload_hash=str(item.get(self.validation_key_attr)), + response_data=item.get(self.data_attr, ""), + payload_hash=item.get(self.validation_key_attr, ""), expiry_timestamp=item.get("expiration"), ) @@ -422,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/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index fde8d980494..7c2b5e1017e 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -504,7 +504,7 @@ def get_parameters_by_name( # NOTE: We need to find out whether all parameters must be decrypted or not to know which API to use ## Logic: ## - ## GetParameters API -> When decrypt is used for all parameters in the the batch + ## GetParameters API -> When decrypt is used for all parameters in the batch ## GetParameter API -> When decrypt is used for one or more in the batch if len(decrypt_params) != len(parameters): diff --git a/aws_lambda_powertools/utilities/parser/envelopes/apigw.py b/aws_lambda_powertools/utilities/parser/envelopes/apigw.py index 1a81124cf09..2a7b0c75bd0 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/apigw.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/apigw.py @@ -7,7 +7,7 @@ from aws_lambda_powertools.utilities.parser.models import APIGatewayProxyEventModel if TYPE_CHECKING: - from aws_lambda_powertools.utilities.parser.types import Model + from aws_lambda_powertools.utilities.parser.types import T logger = logging.getLogger(__name__) @@ -15,19 +15,19 @@ class ApiGatewayEnvelope(BaseEnvelope): """API Gateway envelope to extract data within body key""" - def parse(self, data: dict[str, Any] | Any | None, model: type[Model]) -> Model | None: + def parse(self, data: dict[str, Any] | Any | None, model: type[T]) -> T | None: """Parses data found with model provided Parameters ---------- data : dict Lambda event to be parsed - model : type[Model] + model : type[T] Data model provided to parse after extracting data using envelope Returns ------- - Any + T | None Parsed detail payload with model provided """ logger.debug(f"Parsing incoming data with Api Gateway model {APIGatewayProxyEventModel}") diff --git a/aws_lambda_powertools/utilities/parser/envelopes/apigw_websocket.py b/aws_lambda_powertools/utilities/parser/envelopes/apigw_websocket.py index 37d08dec180..3f5adcde040 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/apigw_websocket.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/apigw_websocket.py @@ -7,7 +7,7 @@ from aws_lambda_powertools.utilities.parser.models import APIGatewayWebSocketMessageEventModel if TYPE_CHECKING: - from aws_lambda_powertools.utilities.parser.types import Model + from aws_lambda_powertools.utilities.parser.types import T logger = logging.getLogger(__name__) @@ -16,19 +16,19 @@ class ApiGatewayWebSocketEnvelope(BaseEnvelope): """API Gateway WebSockets envelope to extract data within body key of messages routes (not disconnect or connect)""" - def parse(self, data: dict[str, Any] | Any | None, model: type[Model]) -> Model | None: + def parse(self, data: dict[str, Any] | Any | None, model: type[T]) -> T | None: """Parses data found with model provided Parameters ---------- data : dict Lambda event to be parsed - model : type[Model] + model : type[T] Data model provided to parse after extracting data using envelope Returns ------- - Any + T | None Parsed detail payload with model provided """ logger.debug( diff --git a/aws_lambda_powertools/utilities/parser/envelopes/apigwv2.py b/aws_lambda_powertools/utilities/parser/envelopes/apigwv2.py index cb0c6b980d1..760f9aad15e 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/apigwv2.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/apigwv2.py @@ -7,7 +7,7 @@ from aws_lambda_powertools.utilities.parser.models import APIGatewayProxyEventV2Model if TYPE_CHECKING: - from aws_lambda_powertools.utilities.parser.types import Model + from aws_lambda_powertools.utilities.parser.types import T logger = logging.getLogger(__name__) @@ -15,19 +15,19 @@ class ApiGatewayV2Envelope(BaseEnvelope): """API Gateway V2 envelope to extract data within body key""" - def parse(self, data: dict[str, Any] | Any | None, model: type[Model]) -> Model | None: + def parse(self, data: dict[str, Any] | Any | None, model: type[T]) -> T | None: """Parses data found with model provided Parameters ---------- data : dict Lambda event to be parsed - model : type[Model] + model : type[T] Data model provided to parse after extracting data using envelope Returns ------- - Any + T | None Parsed detail payload with model provided """ logger.debug(f"Parsing incoming data with Api Gateway model V2 {APIGatewayProxyEventV2Model}") diff --git a/aws_lambda_powertools/utilities/parser/envelopes/base.py b/aws_lambda_powertools/utilities/parser/envelopes/base.py index dbd76eafe7d..83209422e55 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/base.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/base.py @@ -44,7 +44,11 @@ def _parse(data: dict[str, Any] | Any | None, model: type[T]) -> T | None: return _parse_and_validate_event(data=data, adapter=adapter) @abstractmethod - def parse(self, data: dict[str, Any] | Any | None, model: type[T]): + def parse( + self, + data: dict[str, Any] | Any | None, + model: type[T], + ) -> T | list[T | None] | list[dict[str, T | None]] | None: """Implementation to parse data against envelope model, then against the data model NOTE: Call `_parse` method to fully parse data with model provided. diff --git a/aws_lambda_powertools/utilities/parser/envelopes/bedrock_agent.py b/aws_lambda_powertools/utilities/parser/envelopes/bedrock_agent.py index 392c17cc425..61745f34edd 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/bedrock_agent.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/bedrock_agent.py @@ -7,7 +7,7 @@ from aws_lambda_powertools.utilities.parser.models import BedrockAgentEventModel, BedrockAgentFunctionEventModel if TYPE_CHECKING: - from aws_lambda_powertools.utilities.parser.types import Model + from aws_lambda_powertools.utilities.parser.types import T logger = logging.getLogger(__name__) @@ -15,19 +15,19 @@ class BedrockAgentEnvelope(BaseEnvelope): """Bedrock Agent envelope to extract data within input_text key""" - def parse(self, data: dict[str, Any] | Any | None, model: type[Model]) -> Model | None: + def parse(self, data: dict[str, Any] | Any | None, model: type[T]) -> T | None: """Parses data found with model provided Parameters ---------- data : dict Lambda event to be parsed - model : type[Model] + model : type[T] Data model provided to parse after extracting data using envelope Returns ------- - Model | None + T | None Parsed detail payload with model provided """ logger.debug(f"Parsing incoming data with Bedrock Agent model {BedrockAgentEventModel}") @@ -39,19 +39,19 @@ def parse(self, data: dict[str, Any] | Any | None, model: type[Model]) -> Model class BedrockAgentFunctionEnvelope(BaseEnvelope): """Bedrock Agent Function envelope to extract data within input_text key""" - def parse(self, data: dict[str, Any] | Any | None, model: type[Model]) -> Model | None: + def parse(self, data: dict[str, Any] | Any | None, model: type[T]) -> T | None: """Parses data found with model provided Parameters ---------- data : dict Lambda event to be parsed - model : type[Model] + model : type[T] Data model provided to parse after extracting data using envelope Returns ------- - Model | None + T | None Parsed detail payload with model provided """ logger.debug(f"Parsing incoming data with Bedrock Agent Function model {BedrockAgentFunctionEventModel}") diff --git a/aws_lambda_powertools/utilities/parser/envelopes/cloudwatch.py b/aws_lambda_powertools/utilities/parser/envelopes/cloudwatch.py index 0cfe151b789..a6dac2c5859 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/cloudwatch.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/cloudwatch.py @@ -7,7 +7,7 @@ from aws_lambda_powertools.utilities.parser.models import CloudWatchLogsModel if TYPE_CHECKING: - from aws_lambda_powertools.utilities.parser.types import Model + from aws_lambda_powertools.utilities.parser.types import T logger = logging.getLogger(__name__) @@ -22,19 +22,19 @@ class CloudWatchLogsEnvelope(BaseEnvelope): Note: The record will be parsed the same way so if model is str """ - def parse(self, data: dict[str, Any] | Any | None, model: type[Model]) -> list[Model | None]: + def parse(self, data: dict[str, Any] | Any | None, model: type[T]) -> list[T | None]: """Parses records found with model provided Parameters ---------- data : dict Lambda event to be parsed - model : type[Model] + model : type[T] Data model provided to parse after extracting data using envelope Returns ------- - list + list[T | None] List of records parsed with model provided """ logger.debug(f"Parsing incoming data with SNS model {CloudWatchLogsModel}") diff --git a/aws_lambda_powertools/utilities/parser/envelopes/dynamodb.py b/aws_lambda_powertools/utilities/parser/envelopes/dynamodb.py index a7d56abdb11..4458a1553ea 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/dynamodb.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/dynamodb.py @@ -7,7 +7,7 @@ from aws_lambda_powertools.utilities.parser.models import DynamoDBStreamModel if TYPE_CHECKING: - from aws_lambda_powertools.utilities.parser.types import Model + from aws_lambda_powertools.utilities.parser.types import T logger = logging.getLogger(__name__) @@ -19,19 +19,19 @@ class DynamoDBStreamEnvelope(BaseEnvelope): length of the list is the record's amount in the original event. """ - def parse(self, data: dict[str, Any] | Any | None, model: type[Model]) -> list[dict[str, Model | None]]: + def parse(self, data: dict[str, Any] | Any | None, model: type[T]) -> list[dict[str, T | None]]: """Parses DynamoDB Stream records found in either NewImage and OldImage with model provided Parameters ---------- data : dict Lambda event to be parsed - model : type[Model] + model : type[T] Data model provided to parse after extracting data using envelope Returns ------- - list + list[T | None] List of dictionaries with NewImage and OldImage records parsed with model provided """ logger.debug(f"Parsing incoming data with DynamoDB Stream model {DynamoDBStreamModel}") diff --git a/aws_lambda_powertools/utilities/parser/envelopes/event_bridge.py b/aws_lambda_powertools/utilities/parser/envelopes/event_bridge.py index c123319ca7d..ea972452564 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/event_bridge.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/event_bridge.py @@ -7,7 +7,7 @@ from aws_lambda_powertools.utilities.parser.models import EventBridgeModel if TYPE_CHECKING: - from aws_lambda_powertools.utilities.parser.types import Model + from aws_lambda_powertools.utilities.parser.types import T logger = logging.getLogger(__name__) @@ -15,19 +15,19 @@ class EventBridgeEnvelope(BaseEnvelope): """EventBridge envelope to extract data within detail key""" - def parse(self, data: dict[str, Any] | Any | None, model: type[Model]) -> Model | None: + def parse(self, data: dict[str, Any] | Any | None, model: type[T]) -> T | None: """Parses data found with model provided Parameters ---------- data : dict Lambda event to be parsed - model : type[Model] + model : type[T] Data model provided to parse after extracting data using envelope Returns ------- - Any + T | None Parsed detail payload with model provided """ logger.debug(f"Parsing incoming data with EventBridge model {EventBridgeModel}") diff --git a/aws_lambda_powertools/utilities/parser/envelopes/kafka.py b/aws_lambda_powertools/utilities/parser/envelopes/kafka.py index cba374730c6..da08f19b863 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/kafka.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/kafka.py @@ -7,7 +7,7 @@ from aws_lambda_powertools.utilities.parser.models import KafkaMskEventModel, KafkaSelfManagedEventModel if TYPE_CHECKING: - from aws_lambda_powertools.utilities.parser.types import Model + from aws_lambda_powertools.utilities.parser.types import T logger = logging.getLogger(__name__) @@ -21,19 +21,19 @@ class KafkaEnvelope(BaseEnvelope): all items in the list will be parsed as str and npt as JSON (and vice versa) """ - def parse(self, data: dict[str, Any] | Any | None, model: type[Model]) -> list[Model | None]: + def parse(self, data: dict[str, Any] | Any | None, model: type[T]) -> list[T | None]: """Parses data found with model provided Parameters ---------- data : dict Lambda event to be parsed - model : type[Model] + model : type[T] Data model provided to parse after extracting data using envelope Returns ------- - list + list[T | None] List of records parsed with model provided """ event_source = cast(dict, data).get("eventSource") diff --git a/aws_lambda_powertools/utilities/parser/envelopes/kinesis.py b/aws_lambda_powertools/utilities/parser/envelopes/kinesis.py index 41527e03930..0085dade352 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/kinesis.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/kinesis.py @@ -8,7 +8,7 @@ from aws_lambda_powertools.utilities.parser.models import KinesisDataStreamModel if TYPE_CHECKING: - from aws_lambda_powertools.utilities.parser.types import Model + from aws_lambda_powertools.utilities.parser.types import T logger = logging.getLogger(__name__) @@ -24,19 +24,19 @@ class KinesisDataStreamEnvelope(BaseEnvelope): all items in the list will be parsed as str and not as JSON (and vice versa) """ - def parse(self, data: dict[str, Any] | Any | None, model: type[Model]) -> list[Model | None]: + def parse(self, data: dict[str, Any] | Any | None, model: type[T]) -> list[T | None]: """Parses records found with model provided Parameters ---------- data : dict Lambda event to be parsed - model : type[Model] + model : type[T] Data model provided to parse after extracting data using envelope Returns ------- - list + list[T | None] List of records parsed with model provided """ logger.debug(f"Parsing incoming data with Kinesis model {KinesisDataStreamModel}") diff --git a/aws_lambda_powertools/utilities/parser/envelopes/kinesis_firehose.py b/aws_lambda_powertools/utilities/parser/envelopes/kinesis_firehose.py index e816ac877e9..d478421633d 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/kinesis_firehose.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/kinesis_firehose.py @@ -7,7 +7,7 @@ from aws_lambda_powertools.utilities.parser.models import KinesisFirehoseModel if TYPE_CHECKING: - from aws_lambda_powertools.utilities.parser.types import Model + from aws_lambda_powertools.utilities.parser.types import T logger = logging.getLogger(__name__) @@ -25,19 +25,19 @@ class KinesisFirehoseEnvelope(BaseEnvelope): https://docs.aws.amazon.com/lambda/latest/dg/services-kinesisfirehose.html """ - def parse(self, data: dict[str, Any] | Any | None, model: type[Model]) -> list[Model | None]: + def parse(self, data: dict[str, Any] | Any | None, model: type[T]) -> list[T | None]: """Parses records found with model provided Parameters ---------- data : dict Lambda event to be parsed - model : type[Model] + model : type[T] Data model provided to parse after extracting data using envelope Returns ------- - list + list[T | None] List of records parsed with model provided """ logger.debug(f"Parsing incoming data with Kinesis Firehose model {KinesisFirehoseModel}") diff --git a/aws_lambda_powertools/utilities/parser/envelopes/lambda_function_url.py b/aws_lambda_powertools/utilities/parser/envelopes/lambda_function_url.py index 123cfd514b7..80aeb24930c 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/lambda_function_url.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/lambda_function_url.py @@ -7,7 +7,7 @@ from aws_lambda_powertools.utilities.parser.models import LambdaFunctionUrlModel if TYPE_CHECKING: - from aws_lambda_powertools.utilities.parser.types import Model + from aws_lambda_powertools.utilities.parser.types import T logger = logging.getLogger(__name__) @@ -15,19 +15,19 @@ class LambdaFunctionUrlEnvelope(BaseEnvelope): """Lambda function URL envelope to extract data within body key""" - def parse(self, data: dict[str, Any] | Any | None, model: type[Model]) -> Model | None: + def parse(self, data: dict[str, Any] | Any | None, model: type[T]) -> T | None: """Parses data found with model provided Parameters ---------- data : dict Lambda event to be parsed - model : type[Model] + model : type[T] Data model provided to parse after extracting data using envelope Returns ------- - Any + T | None Parsed detail payload with model provided """ logger.debug(f"Parsing incoming data with Lambda function URL model {LambdaFunctionUrlModel}") diff --git a/aws_lambda_powertools/utilities/parser/envelopes/sns.py b/aws_lambda_powertools/utilities/parser/envelopes/sns.py index 98e198c898d..c6d21231e60 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/sns.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/sns.py @@ -7,7 +7,7 @@ from aws_lambda_powertools.utilities.parser.models import SnsModel, SnsNotificationModel, SqsModel if TYPE_CHECKING: - from aws_lambda_powertools.utilities.parser.types import Model + from aws_lambda_powertools.utilities.parser.types import T logger = logging.getLogger(__name__) @@ -22,19 +22,19 @@ class SnsEnvelope(BaseEnvelope): all items in the list will be parsed as str and npt as JSON (and vice versa) """ - def parse(self, data: dict[str, Any] | Any | None, model: type[Model]) -> list[Model | None]: + def parse(self, data: dict[str, Any] | Any | None, model: type[T]) -> list[T | None]: """Parses records found with model provided Parameters ---------- data : dict Lambda event to be parsed - model : type[Model] + model : type[T] Data model provided to parse after extracting data using envelope Returns ------- - list + list[T | None] List of records parsed with model provided """ logger.debug(f"Parsing incoming data with SNS model {SnsModel}") @@ -54,19 +54,19 @@ class SnsSqsEnvelope(BaseEnvelope): 3. Finally, parse provided model against payload extracted """ - def parse(self, data: dict[str, Any] | Any | None, model: type[Model]) -> list[Model | None]: + def parse(self, data: dict[str, Any] | Any | None, model: type[T]) -> list[T | None]: """Parses records found with model provided Parameters ---------- data : dict Lambda event to be parsed - model : type[Model] + model : type[T] Data model provided to parse after extracting data using envelope Returns ------- - list + list[T | None] List of records parsed with model provided """ logger.debug(f"Parsing incoming data with SQS model {SqsModel}") diff --git a/aws_lambda_powertools/utilities/parser/envelopes/sqs.py b/aws_lambda_powertools/utilities/parser/envelopes/sqs.py index 9c64808d3ca..9fe42aed4da 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/sqs.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/sqs.py @@ -7,7 +7,7 @@ from aws_lambda_powertools.utilities.parser.models import SqsModel if TYPE_CHECKING: - from aws_lambda_powertools.utilities.parser.types import Model + from aws_lambda_powertools.utilities.parser.types import T logger = logging.getLogger(__name__) @@ -22,19 +22,19 @@ class SqsEnvelope(BaseEnvelope): all items in the list will be parsed as str and not as JSON (and vice versa) """ - def parse(self, data: dict[str, Any] | Any | None, model: type[Model]) -> list[Model | None]: + def parse(self, data: dict[str, Any] | Any | None, model: type[T]) -> list[T | None]: """Parses records found with model provided Parameters ---------- data : dict Lambda event to be parsed - model : type[Model] + model : type[T] Data model provided to parse after extracting data using envelope Returns ------- - list + list[T | None] List of records parsed with model provided """ logger.debug(f"Parsing incoming data with SQS model {SqsModel}") diff --git a/aws_lambda_powertools/utilities/parser/envelopes/vpc_lattice.py b/aws_lambda_powertools/utilities/parser/envelopes/vpc_lattice.py index 42facf8d279..38c454b9bd8 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/vpc_lattice.py @@ -7,7 +7,7 @@ from aws_lambda_powertools.utilities.parser.models import VpcLatticeModel if TYPE_CHECKING: - from aws_lambda_powertools.utilities.parser.types import Model + from aws_lambda_powertools.utilities.parser.types import T logger = logging.getLogger(__name__) @@ -15,19 +15,19 @@ class VpcLatticeEnvelope(BaseEnvelope): """Amazon VPC Lattice envelope to extract data within body key""" - def parse(self, data: dict[str, Any] | Any | None, model: type[Model]) -> Model | None: + def parse(self, data: dict[str, Any] | Any | None, model: type[T]) -> T | None: """Parses data found with model provided Parameters ---------- data : dict Lambda event to be parsed - model : type[Model] + model : type[T] Data model provided to parse after extracting data using envelope Returns ------- - Model | None + T | None Parsed detail payload with model provided """ logger.debug(f"Parsing incoming data with VPC Lattice model {VpcLatticeModel}") diff --git a/aws_lambda_powertools/utilities/parser/envelopes/vpc_latticev2.py b/aws_lambda_powertools/utilities/parser/envelopes/vpc_latticev2.py index d70a68296a0..f1a218b46a4 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/vpc_latticev2.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/vpc_latticev2.py @@ -7,7 +7,7 @@ from aws_lambda_powertools.utilities.parser.models import VpcLatticeV2Model if TYPE_CHECKING: - from aws_lambda_powertools.utilities.parser.types import Model + from aws_lambda_powertools.utilities.parser.types import T logger = logging.getLogger(__name__) @@ -15,19 +15,19 @@ class VpcLatticeV2Envelope(BaseEnvelope): """Amazon VPC Lattice envelope to extract data within body key""" - def parse(self, data: dict[str, Any] | Any | None, model: type[Model]) -> Model | None: + def parse(self, data: dict[str, Any] | Any | None, model: type[T]) -> T | None: """Parses data found with model provided Parameters ---------- data : dict Lambda event to be parsed - model : type[Model] + model : type[T] Data model provided to parse after extracting data using envelope Returns ------- - Model | None + T | None Parsed detail payload with model provided """ logger.debug(f"Parsing incoming data with VPC Lattice V2 model {VpcLatticeV2Model}") diff --git a/aws_lambda_powertools/utilities/parser/functions.py b/aws_lambda_powertools/utilities/parser/functions.py index 72dde64f12f..d2acb9a7965 100644 --- a/aws_lambda_powertools/utilities/parser/functions.py +++ b/aws_lambda_powertools/utilities/parser/functions.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) -def _retrieve_or_set_model_from_cache(model: type[T]) -> TypeAdapter: +def _retrieve_or_set_model_from_cache(model: type[T]) -> TypeAdapter[T]: """ Retrieves or sets a TypeAdapter instance from the cache for the given model. @@ -49,7 +49,7 @@ def _retrieve_or_set_model_from_cache(model: type[T]) -> TypeAdapter: return CACHE_TYPE_ADAPTER[id_model] -def _parse_and_validate_event(data: dict[str, Any] | Any, adapter: TypeAdapter): +def _parse_and_validate_event(data: dict[str, Any] | Any, adapter: TypeAdapter[T]): """ Parse and validate the event data using the provided adapter. 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/aws_lambda_powertools/utilities/parser/parser.py b/aws_lambda_powertools/utilities/parser/parser.py index 3afad192a01..652cc1ebf1e 100644 --- a/aws_lambda_powertools/utilities/parser/parser.py +++ b/aws_lambda_powertools/utilities/parser/parser.py @@ -124,7 +124,11 @@ def parse(event: dict[str, Any], model: type[T]) -> T: ... # pragma: no cover @overload -def parse(event: dict[str, Any], model: type[T], envelope: type[Envelope]) -> T: ... # pragma: no cover +def parse( # pragma: no cover + event: dict[str, Any], + model: type[T], + envelope: type[Envelope], +) -> T | list[T | None] | list[dict[str, T | None]] | None: ... def parse(event: dict[str, Any], model: type[T], envelope: type[Envelope] | None = None): diff --git a/benchmark/src/instrumented/main.py b/benchmark/src/instrumented/main.py index e26d9326c26..7632e4f608c 100644 --- a/benchmark/src/instrumented/main.py +++ b/benchmark/src/instrumented/main.py @@ -1,5 +1,4 @@ -from aws_lambda_powertools import (Logger, Metrics, Tracer) - +from aws_lambda_powertools import Logger, Metrics, Tracer # Initialize core utilities logger = Logger() @@ -13,5 +12,5 @@ @tracer.capture_lambda_handler def handler(event, context): return { - "message": "success" - } \ No newline at end of file + "message": "success", + } diff --git a/benchmark/src/reference/main.py b/benchmark/src/reference/main.py index 4b5fb3900a7..3127cfb7a29 100644 --- a/benchmark/src/reference/main.py +++ b/benchmark/src/reference/main.py @@ -1,4 +1,4 @@ def handler(event, context): return { - "message": "success" - } \ No newline at end of file + "message": "success", + } 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 2a7955f38c0..67bb07a25cc 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -11,6 +11,7 @@ Event handler for Amazon API Gateway REST and HTTP APIs, Application Load Balanc * Support for CORS, binary and Gzip compression, Decimals JSON encoding and bring your own JSON serializer * Built-in integration with [Event Source Data Classes utilities](../../utilities/data_classes.md){target="_blank"} for self-documented event schema * Works with micro function (one or a few routes) and monolithic functions (all routes) +* Native async handler support with `resolve_async()` for non-blocking I/O * Support for Middleware * Support for OpenAPI schema generation * Support data validation for requests/responses @@ -63,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 @@ -1458,12 +1459,132 @@ 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. ???+ info "`append_context` vs `Depends()`" `append_context` remains available for backward compatibility. `Depends()` is recommended for new code because it provides type safety, IDE autocomplete, composable dependency trees, and `dependency_overrides` for testing. +### Async support + +Use `resolve_async()` to natively support async route handlers with `async/await`. This enables non-blocking I/O operations like concurrent HTTP calls, database queries, and parallel processing within your Lambda function. + +Both sync and async handlers can coexist in the same resolver. Async handlers are automatically detected and awaited. + +=== "Getting started" + + ```python hl_lines="9 22" title="async_resolve_getting_started.py" + --8<-- "examples/event_handler_rest/src/async_resolve_getting_started.py" + ``` + + 1. Define your route handler as `async def` to use `await` + 2. Sync handlers continue to work as before, no changes needed + 3. Use `resolve_async()` instead of `resolve()` and wrap with `asyncio.run()` + +=== "Concurrent I/O with gather" + + ```python hl_lines="21-24" title="async_resolve_concurrent.py" + --8<-- "examples/event_handler_rest/src/async_resolve_concurrent.py" + ``` + + 1. `asyncio.gather()` runs multiple I/O operations concurrently, reducing total latency + +=== "All resolvers" + + ```python hl_lines="1 10-12" title="async_resolve_all_resolvers.py" + --8<-- "examples/event_handler_rest/src/async_resolve_all_resolvers.py" + ``` + + 1. API Gateway REST API + 2. API Gateway HTTP API + 3. Application Load Balancer + +#### Middlewares + +Both sync and async middlewares work in the async chain. Sync middlewares are executed in a background thread so the event loop is never blocked. + +=== "Sync middleware" + + ```python hl_lines="11 24" title="async_resolve_middleware.py" + --8<-- "examples/event_handler_rest/src/async_resolve_middleware.py" + ``` + + 1. Sync middleware works as-is, no changes needed + 2. Async handler is awaited natively in the async chain + +=== "Async middleware" + + ```python hl_lines="11 16" title="async_resolve_async_middleware.py" + --8<-- "examples/event_handler_rest/src/async_resolve_async_middleware.py" + ``` + + 1. Define your middleware as `async def` to use `await` + 2. Use `await next_middleware(app)` instead of `next_middleware(app)` + +#### Async with data validation + +Data validation with Pydantic works with async handlers. Use `enable_validation=True` as you would with sync handlers. + +```python hl_lines="1 3 7" +app = APIGatewayHttpResolver(enable_validation=True) + +@app.get("/todos/") +async def get_todo(todo_id: int) -> dict: + return {"todo_id": todo_id} + +def lambda_handler(event, context): + return asyncio.run(app.resolve_async(event, context)) +``` + +#### Operations that remain synchronous + +These operations run synchronously on the event loop. They are CPU-bound and complete in microseconds, so they do not benefit from async. + +| Operation | Why it stays synchronous | +| ----------------------------- | --------------------------------------------------------------- | +| **Route matching** | Regex matching and string comparison against registered routes | +| **Event deserialization** | Converting the raw event dict into a proxy event data class | +| **Response serialization** | JSON encoding, base64 encoding, header assembly | +| **Response validation** | Pydantic model validation is CPU-bound | +| **Request validation** | Pydantic model validation is CPU-bound | +| **Compression** | Gzip compression of response body | +| **CORS header injection** | Building Access-Control headers from config | +| **Dependency resolution** | `Depends()` tree is resolved synchronously | + +#### Known limitations + +| Limitation | Detail | +| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| **AWS X-Ray with `asyncio.gather`** | X-Ray SDK does not propagate trace context across `asyncio.gather` tasks. Use individual `await` calls if you need per-call tracing. | +| **Sync middlewares use thread pool** | Sync middlewares run in the default `ThreadPoolExecutor`. Avoid long blocking I/O inside sync middlewares when using `resolve_async()`. | + ### Considerations This utility is optimized for fast startup, minimal feature set, and to quickly on-board customers familiar with frameworks like Flask — it's not meant to be a fully fledged framework. @@ -1546,6 +1667,20 @@ Each endpoint will be it's own Lambda function that is configured as a [Lambda i ## Testing your code +### Testing async handlers + +You can test async handlers by calling `resolve_async()` with `asyncio.run()`. + +```python hl_lines="24 26" title="async_resolve_testing.py" +--8<-- "examples/event_handler_rest/src/async_resolve_testing.py" +``` + +1. Import your app as usual +2. Use `asyncio.run(app.resolve_async(...))` instead of `app.resolve(...)` +3. Assert on the response dict as you would with sync handlers + +### Testing sync handlers + You can test your routes by passing a proxy event request with required params. ???+ info 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 eb5391393bf..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:32**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:32**{: .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:32**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:32**{: .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:32**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:32**{: .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:32**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:32**{: .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:32**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:32**{: .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 8e88de48908..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:32**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:32**{: .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:32**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:32**{: .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:32**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:32**{: .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:32**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:32**{: .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:32**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:32**{: .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 514279c944c..c75a0a3cfc9 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -139,17 +139,17 @@ gitdb==4.0.12 \ --hash=sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571 \ --hash=sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf # via gitpython -gitpython==3.1.44 \ - --hash=sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110 \ - --hash=sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269 +gitpython==3.1.58 \ + --hash=sha256:621416df10ef3fd0e19fabf9172ddeed0fa704d353d04f194eec56a625a95b22 \ + --hash=sha256:d331e722577f0fd7fc1f857419b3ecc07af66282b933d2a4d95f84a042fdd50f # via mkdocs-git-revision-date-plugin griffe==1.13.0 \ --hash=sha256:246ea436a5e78f7fbf5f24ca8a727bb4d2a4b442a2959052eea3d0bfe9a076e0 \ --hash=sha256:470fde5b735625ac0a36296cd194617f039e9e83e301fcbd493e2b58382d0559 # via mkdocstrings-python -idna==3.10 \ - --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ - --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 +idna==3.15 \ + --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ + --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc # via requests jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ @@ -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.16.1 \ - --hash=sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91 \ - --hash=sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d +pymdown-extensions==11.0.1 \ + --hash=sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2 \ + --hash=sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0 # via # mkdocs-material # mkdocstrings @@ -411,17 +411,68 @@ 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 -urllib3==2.6.3 \ - --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ - --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 + # via + # beautifulsoup4 + # mkdocstrings-python +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via requests watchdog==6.0.0 \ --hash=sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a \ 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/docs/utilities/metadata.md b/docs/utilities/metadata.md index b1154d4cecd..4959d81d8c7 100644 --- a/docs/utilities/metadata.md +++ b/docs/utilities/metadata.md @@ -6,7 +6,7 @@ status: new -The Metadata utility allows you to fetch data from the [AWS Lambda Metadata Endpoint (LMDS)](https://docs.aws.amazon.com/lambda/latest/dg/lambda-metadata-endpoint.html){target="_blank"}. This can be useful for retrieving information about the Lambda execution environment, such as the Availability Zone ID. +The Metadata utility allows you to fetch data from the [AWS Lambda Metadata Endpoint (LMDS)](https://docs.aws.amazon.com/lambda/latest/dg/configuration-metadata-endpoint.html){target="_blank"}. This can be useful for retrieving information about the Lambda execution environment, such as the Availability Zone ID. ## Key features diff --git a/docs/utilities/parser.md b/docs/utilities/parser.md index b4c7ad749a9..0a142caa18b 100644 --- a/docs/utilities/parser.md +++ b/docs/utilities/parser.md @@ -211,8 +211,8 @@ You can use pre-built envelopes provided by the Parser to extract and parse spec | **EventBridgeEnvelope** | 1. Parses data using `EventBridgeModel`. ``2. Parses `detail` key using your model`` and returns it. | `Model` | | **SqsEnvelope** | 1. Parses data using `SqsModel`. ``2. Parses records in `body` key using your model`` and return them in a list. | `List[Model]` | | **CloudWatchLogsEnvelope** | 1. Parses data using `CloudwatchLogsModel` which will base64 decode and decompress it. ``2. Parses records in `message` key using your model`` and return them in a list. | `List[Model]` | -| **KinesisDataStreamEnvelope** | 1. Parses data using `KinesisDataStreamModel` which will base64 decode it. ``2. Parses records in in `Records` key using your model`` and returns them in a list. | `List[Model]` | -| **KinesisFirehoseEnvelope** | 1. Parses data using `KinesisFirehoseModel` which will base64 decode it. ``2. Parses records in in` Records` key using your model`` and returns them in a list. | `List[Model]` | +| **KinesisDataStreamEnvelope** | 1. Parses data using `KinesisDataStreamModel` which will base64 decode it. ``2. Parses records in the `Records` key using your model`` and returns them in a list. | `List[Model]` | +| **KinesisFirehoseEnvelope** | 1. Parses data using `KinesisFirehoseModel` which will base64 decode it. ``2. Parses records in the `Records` key using your model`` and returns them in a list. | `List[Model]` | | **SnsEnvelope** | 1. Parses data using `SnsModel`. ``2. Parses records in `body` key using your model`` and return them in a list. | `List[Model]` | | **SnsSqsEnvelope** | 1. Parses data using `SqsModel`. `` 2. Parses SNS records in `body` key using `SnsNotificationModel`. `` 3. Parses data in `Message` key using your model and return them in a list. | `List[Model]` | | **ApiGatewayV2Envelope** | 1. Parses data using `APIGatewayProxyEventV2Model`. ``2. Parses `body` key using your model`` and returns it. | `Model` | diff --git a/docs/we_made_this.md b/docs/we_made_this.md index b8a3445cc4f..07370d53d9e 100644 --- a/docs/we_made_this.md +++ b/docs/we_made_this.md @@ -156,6 +156,14 @@ Learn to implement data masking in AWS Lambda with Powertools, protecting sensit [Simplified Data Masking in AWS Lambda with Powertools](https://www.internetkatta.com/simplified-data-masking-in-aws-lambda-with-powertool){target="_blank" rel="nofollow"} +### Stop writing Lambda boilerplate + +Introducing the [AWS Lambda Templates](https://github.com/amrabed/aws-lambda-templates){target="_blank"} repository — a collection of production-ready Python Lambda templates for Bedrock Agent, REST API, GraphQL, DynamoDB Stream, EventBridge, S3, and SQS scenarios, pre-wired with Powertools for AWS Lambda, AWS CDK, Pydantic, and a robust testing infrastructure. + +> **Author: [Amr Abed :material-linkedin:](https://www.linkedin.com/in/amrabed){target="_blank" rel="nofollow"}** + +[Stop Writing Lambda Boilerplate](https://builder.aws.com/content/3CDoe07m8JBNTQyzcrYWTWkfNPz/stop-writing-lambda-boilerplate){target="_blank" rel="nofollow"} + ## Videos #### Building a resilient input handling with Parser 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 d8509e7a1ce..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:32", + 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 b526e04cc2b..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:32", + 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 37b9103eda3..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:32 + - 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 c9aef4c6bcb..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:32 + - 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:32 + - 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_graphql/src/requirements.txt b/examples/event_handler_graphql/src/requirements.txt index 785ab54fc57..ccc5ffb5939 100644 --- a/examples/event_handler_graphql/src/requirements.txt +++ b/examples/event_handler_graphql/src/requirements.txt @@ -1,2 +1,2 @@ aws-lambda-powertools[tracer] -requests>=2.32.0 +requests>=2.34.2 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/async_resolve_all_resolvers.py b/examples/event_handler_rest/src/async_resolve_all_resolvers.py new file mode 100644 index 00000000000..92149e1dc3c --- /dev/null +++ b/examples/event_handler_rest/src/async_resolve_all_resolvers.py @@ -0,0 +1,31 @@ +import asyncio + +from aws_lambda_powertools.event_handler import ( + ALBResolver, + APIGatewayHttpResolver, + APIGatewayRestResolver, +) + +rest_app = APIGatewayRestResolver() # (1)! +http_app = APIGatewayHttpResolver() # (2)! +alb_app = ALBResolver() # (3)! + + +@rest_app.get("/hello") +@http_app.get("/hello") +@alb_app.get("/hello") +async def hello(): + await asyncio.sleep(0) + return {"message": "hello from async"} + + +def rest_handler(event, context): + return asyncio.run(rest_app.resolve_async(event, context)) + + +def http_handler(event, context): + return asyncio.run(http_app.resolve_async(event, context)) + + +def alb_handler(event, context): + return asyncio.run(alb_app.resolve_async(event, context)) diff --git a/examples/event_handler_rest/src/async_resolve_async_middleware.py b/examples/event_handler_rest/src/async_resolve_async_middleware.py new file mode 100644 index 00000000000..e801a58b1f6 --- /dev/null +++ b/examples/event_handler_rest/src/async_resolve_async_middleware.py @@ -0,0 +1,29 @@ +import asyncio +from collections.abc import Callable + +from aws_lambda_powertools import Logger +from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response + +app = APIGatewayRestResolver() +logger = Logger() + + +async def async_inject_correlation_id(app: APIGatewayRestResolver, next_middleware: Callable) -> Response: # (1)! + request_id = app.current_event.request_context.request_id + app.append_context(correlation_id=request_id) + logger.set_correlation_id(request_id) + + result = await next_middleware(app) # (2)! + + result.headers["x-correlation-id"] = request_id + return result + + +@app.get("/todos", middlewares=[async_inject_correlation_id]) +async def get_todos(): + await asyncio.sleep(0) + return {"todos": []} + + +def lambda_handler(event, context): + return asyncio.run(app.resolve_async(event, context)) diff --git a/examples/event_handler_rest/src/async_resolve_concurrent.py b/examples/event_handler_rest/src/async_resolve_concurrent.py new file mode 100644 index 00000000000..868954d51c7 --- /dev/null +++ b/examples/event_handler_rest/src/async_resolve_concurrent.py @@ -0,0 +1,28 @@ +import asyncio + +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver + +app = APIGatewayHttpResolver() + + +async def fetch_profile(user_id: str) -> dict: + await asyncio.sleep(0) # simulate async I/O (e.g., DynamoDB, HTTP call) + return {"user_id": user_id, "name": "John"} + + +async def fetch_orders(user_id: str) -> list: + await asyncio.sleep(0) + return [{"order_id": "123", "total": 99.99}] + + +@app.get("/dashboard/") +async def get_dashboard(user_id: str): + profile, orders = await asyncio.gather( # (1)! + fetch_profile(user_id), + fetch_orders(user_id), + ) + return {"profile": profile, "orders": orders} + + +def lambda_handler(event, context): + return asyncio.run(app.resolve_async(event, context)) diff --git a/examples/event_handler_rest/src/async_resolve_getting_started.py b/examples/event_handler_rest/src/async_resolve_getting_started.py new file mode 100644 index 00000000000..40d9c2b9bec --- /dev/null +++ b/examples/event_handler_rest/src/async_resolve_getting_started.py @@ -0,0 +1,21 @@ +import asyncio + +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver + +app = APIGatewayHttpResolver() + + +@app.get("/todos/") +async def get_todo(todo_id: str): # (1)! + # Async handlers can use await for non-blocking I/O + await asyncio.sleep(0) # simulate async I/O + return {"todo_id": todo_id, "completed": False} + + +@app.get("/health") +def health(): # (2)! + return {"status": "ok"} + + +def lambda_handler(event, context): + return asyncio.run(app.resolve_async(event, context)) # (3)! diff --git a/examples/event_handler_rest/src/async_resolve_middleware.py b/examples/event_handler_rest/src/async_resolve_middleware.py new file mode 100644 index 00000000000..7ace8762ff8 --- /dev/null +++ b/examples/event_handler_rest/src/async_resolve_middleware.py @@ -0,0 +1,29 @@ +import asyncio + +from aws_lambda_powertools import Logger +from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response +from aws_lambda_powertools.event_handler.middlewares import NextMiddleware + +app = APIGatewayRestResolver() +logger = Logger() + + +def inject_correlation_id(app: APIGatewayRestResolver, next_middleware: NextMiddleware) -> Response: # (1)! + request_id = app.current_event.request_context.request_id + app.append_context(correlation_id=request_id) + logger.set_correlation_id(request_id) + + result = next_middleware(app) + + result.headers["x-correlation-id"] = request_id + return result + + +@app.get("/todos", middlewares=[inject_correlation_id]) +async def get_todos(): # (2)! + await asyncio.sleep(0) + return {"todos": []} + + +def lambda_handler(event, context): + return asyncio.run(app.resolve_async(event, context)) diff --git a/examples/event_handler_rest/src/async_resolve_testing.py b/examples/event_handler_rest/src/async_resolve_testing.py new file mode 100644 index 00000000000..8ce0aadeb13 --- /dev/null +++ b/examples/event_handler_rest/src/async_resolve_testing.py @@ -0,0 +1,26 @@ +import asyncio +import json + + +def test_async_handler(): + from async_resolve_getting_started import app # (1)! + + event = { + "httpMethod": "GET", + "path": "/todos/1", + "headers": {}, + "queryStringParameters": None, + "pathParameters": {"todo_id": "1"}, + "body": None, + "isBase64Encoded": False, + "requestContext": {"stage": "dev", "requestId": "test-id", "http": {"method": "GET", "path": "/todos/1"}}, + "rawPath": "/todos/1", + "rawQueryString": "", + "routeKey": "GET /todos/{todo_id}", + "version": "2.0", + } + + response = asyncio.run(app.resolve_async(event, {})) # (2)! + + assert response["statusCode"] == 200 # (3)! + assert json.loads(response["body"]) == {"todo_id": "1", "completed": False} 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 78dee0ac047..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:32 +? 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:32 +? 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 eb6685b800a..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:32", + 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 b7577b5509e..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:32", + ":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 73d023bfc33..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:32 + - !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 e6f8f335f77..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:32 + - 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 aa986baa2ad..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:32"] + 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 24d0108a4a8..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:32 +? 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:32 +? 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 cf774cba370..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:32", + 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 d9aeef246f9..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:32", + ":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 ac3d7545b46..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:32 + - !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 23573959df0..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:32 + - 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 8db1d6ae3ff..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:32"] + 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/idempotency/src/getting_started_with_idempotency_redis_client.py b/examples/idempotency/src/getting_started_with_idempotency_redis_client.py index ac2a20587e8..91b6f5b47c4 100644 --- a/examples/idempotency/src/getting_started_with_idempotency_redis_client.py +++ b/examples/idempotency/src/getting_started_with_idempotency_redis_client.py @@ -21,7 +21,7 @@ max_connections=1000, ) -persistence_layer = CachePersistenceLayer(client=client) +persistence_layer = CachePersistenceLayer(client=client) # type: ignore[arg-type] @dataclass diff --git a/examples/idempotency/src/using_redis_client_with_local_certs.py b/examples/idempotency/src/using_redis_client_with_local_certs.py index 844f5b37e7d..b58571e3bbc 100644 --- a/examples/idempotency/src/using_redis_client_with_local_certs.py +++ b/examples/idempotency/src/using_redis_client_with_local_certs.py @@ -27,7 +27,7 @@ ssl_ca_certs=f"{abs_lambda_path()}/certs/cache_ca.pem", # (4)! ) -persistence_layer = CachePersistenceLayer(client=redis_client) +persistence_layer = CachePersistenceLayer(client=redis_client) # type: ignore[arg-type] config = IdempotencyConfig( expires_after_seconds=2 * 60, # 2 minutes ) diff --git a/examples/logger/sam/template.yaml b/examples/logger/sam/template.yaml index 2bb425009cb..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:32 + - !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 2369678e037..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:32 + - !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 6420c4331bf..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:32 + - !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/parser/src/bring_your_own_envelope.py b/examples/parser/src/bring_your_own_envelope.py index 1fb5dea0045..ae60ac58ee3 100644 --- a/examples/parser/src/bring_your_own_envelope.py +++ b/examples/parser/src/bring_your_own_envelope.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import json -from typing import Any, Dict, Optional, Type, TypeVar, Union +from typing import Any, TypeVar from pydantic import BaseModel @@ -7,11 +9,11 @@ from aws_lambda_powertools.utilities.parser.models import EventBridgeModel from aws_lambda_powertools.utilities.typing import LambdaContext -Model = TypeVar("Model", bound=BaseModel) +T = TypeVar("T") class EventBridgeEnvelope(BaseEnvelope): - def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Type[Model]) -> Optional[Model]: + def parse(self, data: dict[str, Any] | Any | None, model: type[T]) -> T | None: if data is None: return None 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 7ce2bc40715..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:32 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:37 Resources: CaptureLambdaHandlerExample: diff --git a/layer_v3/app.py b/layer_v3/app.py index 25ed2b116ce..b488f640324 100644 --- a/layer_v3/app.py +++ b/layer_v3/app.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import aws_cdk as cdk - from layer.canary_stack import CanaryStack from layer.layer_stack import LayerStack diff --git a/layer_v3/layer/canary/app.py b/layer_v3/layer/canary/app.py index 667d8215636..135356ca730 100644 --- a/layer_v3/layer/canary/app.py +++ b/layer_v3/layer/canary/app.py @@ -66,7 +66,7 @@ def on_event(event, context): def on_create(event): props = event["ResourceProperties"] - logger.info("create new resource with properties %s" % props) + logger.info(f"create new resource with properties {props}") handler(event) diff --git a/layer_v3/poetry.lock b/layer_v3/poetry.lock index 283262858e7..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" @@ -476,14 +451,14 @@ markers = {dev = "python_version == \"3.10\""} [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, - {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, + {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, + {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, ] [package.extras] 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 faae155108c..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.1118.0" + "aws-cdk": "^2.1136.0" } }, "node_modules/aws-cdk": { - "version": "2.1118.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1118.0.tgz", - "integrity": "sha512-Tfd865GRewDTXIbTVtix/l+v8t3rZENvdHcQQZS2wXYVXfHzljULFXe9JKkgZUNDPB1zo9tSBUu8jjiHRm7nWg==", + "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 2820cd2f584..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.1118.0" + "aws-cdk": "^2.1136.0" } } diff --git a/parallel_run_e2e.py b/parallel_run_e2e.py index 1146f66931e..7a56c885705 100755 --- a/parallel_run_e2e.py +++ b/parallel_run_e2e.py @@ -1,4 +1,5 @@ -""" Calculate how many parallel workers are needed to complete E2E infrastructure jobs across available CPU Cores """ +"""Calculate how many parallel workers are needed to complete E2E infrastructure jobs across available CPU Cores""" + import subprocess import sys from pathlib import Path @@ -9,7 +10,7 @@ def main(): workers = len(list(features)) - 1 command = f"poetry run pytest -n {workers} -o log_cli=true tests/e2e" - result = subprocess.run(command.split(), shell=False) + result = subprocess.run(command.split(), shell=False, check=False) sys.exit(result.returncode) diff --git a/poetry.lock b/poetry.lock index 649b08915ab..6234daec8f2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.2 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 = "annotated-types" @@ -11,7 +11,7 @@ files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] -markers = {main = "extra == \"parser\" or extra == \"all\""} +markers = {main = "extra == \"all\" or extra == \"parser\""} [[package]] name = "anyio" @@ -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] @@ -49,6 +50,73 @@ files = [ [package.extras] test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] +[[package]] +name = "ast-serialize" +version = "0.8.0" +description = "Python bindings for mypy AST serialization" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {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]] name = "async-timeout" version = "5.0.1" @@ -73,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" @@ -86,6 +154,7 @@ files = [ {file = "avro-1.12.1-py2.py3-none-any.whl", hash = "sha256:970475dd6457924533966fe761be607c759d5a48390cc8fbed472f7c9a8868f2"}, {file = "avro-1.12.1.tar.gz", hash = "sha256:c5b8dd2dd4c10816f0dc127cc29cfd43b5e405cf7e6840e89460a024bf3d098d"}, ] +markers = {main = "extra == \"kafka-consumer-avro\""} [package.extras] snappy = ["python-snappy"] @@ -93,35 +162,34 @@ zstandard = ["zstandard"] [[package]] name = "aws-cdk-asset-awscli-v1" -version = "2.2.273" +version = "2.2.292" description = "A library that contains the AWS CLI for use in Lambda Layers" optional = false -python-versions = "~=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {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"}, + {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.127.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" -version = "2.1.1" +version = "2.1.2" description = "@aws-cdk/asset-node-proxy-agent-v6" optional = false python-versions = "~=3.9" groups = ["dev"] files = [ - {file = "aws_cdk_asset_node_proxy_agent_v6-2.1.1-py3-none-any.whl", hash = "sha256:a24fab484423fcb7c729fb376817a4dbb6a09c176243fd1dcbdaa05ddf62f037"}, - {file = "aws_cdk_asset_node_proxy_agent_v6-2.1.1.tar.gz", hash = "sha256:37211fea7e308144d20666b939d8763d733000d8d837ba968dacaf4983daf574"}, + {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.126.0,<2.0.0" +jsii = ">=1.129.0,<2.0.0" publication = ">=0.0.3" typeguard = "2.13.3" @@ -157,7 +225,7 @@ files = [ ] [package.dependencies] -"aws-cdk.aws-apigatewayv2-alpha" = "2.114.1.a0" +"aws-cdk.aws-apigatewayv2-alpha" = "2.114.1a0" aws-cdk-lib = ">=2.114.1,<3.0.0" constructs = ">=10.0.0,<11.0.0" jsii = ">=1.92.0,<2.0.0" @@ -177,7 +245,7 @@ files = [ ] [package.dependencies] -"aws-cdk.aws-apigatewayv2-alpha" = "2.114.1.a0" +"aws-cdk.aws-apigatewayv2-alpha" = "2.114.1a0" aws-cdk-lib = ">=2.114.1,<3.0.0" constructs = ">=10.0.0,<11.0.0" jsii = ">=1.92.0,<2.0.0" @@ -205,72 +273,69 @@ typeguard = ">=2.13.3,<2.14.0" [[package]] name = "aws-cdk-aws-lambda-python-alpha" -version = "2.248.0a0" +version = "2.266.0a0" description = "The CDK Construct Library for AWS Lambda in Python" optional = false -python-versions = "~=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "aws_cdk_aws_lambda_python_alpha-2.248.0a0-py3-none-any.whl", hash = "sha256:bf9303515649511fb5299ef36cfcdc042f3422de05321ed30f565dcb3642737f"}, - {file = "aws_cdk_aws_lambda_python_alpha-2.248.0a0.tar.gz", hash = "sha256:2b5f4f3a2ca249355fd86509da800ae36b2e368cfec76372dc3dd25f25ef23af"}, + {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.248.0,<3.0.0" +aws-cdk-lib = ">=2.266.0,<3.0.0" constructs = ">=10.5.0,<11.0.0" -jsii = ">=1.127.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 = "53.13.0" +version = "54.21.0" description = "Schema for the protocol between CDK framework and CDK CLI" optional = false -python-versions = "~=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "aws_cdk_cloud_assembly_schema-53.13.0-py3-none-any.whl", hash = "sha256:b6612da9451a23e1f1ddacfe94dc5359b5ca42bad91a923b04a3d12e14078016"}, - {file = "aws_cdk_cloud_assembly_schema-53.13.0.tar.gz", hash = "sha256:d0f4ed7f8122f57d62983c0274d10480909e4b15362d28ff1f9b505e2bd92a72"}, + {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.127.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.249.0" +version = "2.267.0" description = "Version 2 of the AWS Cloud Development Kit library" optional = false -python-versions = "~=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "aws_cdk_lib-2.249.0-py3-none-any.whl", hash = "sha256:c36a7891027c6252479b26ddb3e21bdc54d1fdf403c7928c8da6e8040e4674fa"}, - {file = "aws_cdk_lib-2.249.0.tar.gz", hash = "sha256:7a4c27b3b22253c099696e54dc6cdd193b718c8d43fd692f91c820921a15dc6e"}, + {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.273" -"aws-cdk.asset-node-proxy-agent-v6" = ">=2.1.1,<3.0.0" -"aws-cdk.cloud-assembly-schema" = ">=53.0.0,<54.0.0" +"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.11.0,<55.0.0" constructs = ">=10.5.0,<11.0.0" -jsii = ">=1.127.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.4" +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.4-py2.py3-none-any.whl", hash = "sha256:29e7ec00aa6f27bb6e4f4f17e51abf3fc58a7fc17882c0e375ee09c97ab84585"}, - {file = "aws_encryption_sdk-4.0.4.tar.gz", hash = "sha256:60b69f19f72fa568d7e69e9d3966fe10e541c9c83b1af5a83724f8a1fe184d43"}, + {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] @@ -299,24 +364,24 @@ requests = ">=0.14.0" [[package]] name = "aws-sam-translator" -version = "1.108.0" +version = "1.110.0" description = "AWS SAM Translator is a library that transform SAM templates into AWS CloudFormation templates" optional = false -python-versions = "!=4.0,<=4.0,>=3.8" +python-versions = "!=4.0,<=4.0,>=3.10" groups = ["dev"] files = [ - {file = "aws_sam_translator-1.108.0-py3-none-any.whl", hash = "sha256:03130421e641bb57ba7978e7db9e49acb32ecb09a87777dca3c28e44b3ea49db"}, - {file = "aws_sam_translator-1.108.0.tar.gz", hash = "sha256:8a21be119caaa64cf85e01b5e0fde804abe117b36fcce934bc1b74f3ccdc2488"}, + {file = "aws_sam_translator-1.110.0-py3-none-any.whl", hash = "sha256:69b09aacf2d305ac747037b7b913224cb8a9d653f47a0306509c1d20e420b670"}, + {file = "aws_sam_translator-1.110.0.tar.gz", hash = "sha256:466ee0e8200992c51b7fd5ede5e56ca2e8dd5473cc551e8495c14f2f4d636127"}, ] [package.dependencies] boto3 = ">=1.34.0,<2.0.0" jsonschema = ">=4.23,<5" -pydantic = {version = ">=2.12.5,<2.13.0", markers = "python_version >= \"3.9\""} +pydantic = ">=2.13.3,<2.14.0" typing_extensions = ">=4.4" [package.extras] -dev = ["black (==24.3.0)", "boto3 (>=1.34.0,<2.0.0)", "boto3-stubs[appconfig,serverlessrepo] (>=1.34.0,<2.0.0)", "coverage (>=5.3,<8)", "dateparser (>=1.1,<2.0)", "mypy (>=1.10.1,<1.11.0)", "parameterized (>=0.7,<1.0)", "pytest (>=6.2,<8)", "pytest-cov (>=2.10,<5)", "pytest-env (>=0.6,<1)", "pytest-rerunfailures (>=9.1,<12)", "pytest-xdist (>=2.5,<4)", "pyyaml (>=6.0,<7.0)", "requests (>=2.28,<3.0)", "ruamel.yaml (==0.17.21)", "ruff (>=0.4.5,<0.5.0)", "tenacity (>=9.0,<10.0)", "types-PyYAML (>=6.0,<7.0)", "types-jsonschema (>=3.2,<4.0)", "types-requests (>=2.28,<3.0)"] +dev = ["black (==26.3.1)", "boto3 (>=1.34.0,<2.0.0)", "boto3-stubs[appconfig,serverlessrepo] (>=1.34.0,<2.0.0)", "coverage (>=5.3,<8)", "dateparser (>=1.1,<2.0)", "mypy (>=1.10.1,<1.11.0)", "parameterized (>=0.7,<1.0)", "pytest (>=6.2,<8)", "pytest-cov (>=2.10,<5)", "pytest-env (>=0.6,<1)", "pytest-rerunfailures (>=9.1,<12)", "pytest-xdist (>=2.5,<4)", "pyyaml (>=6.0,<7.0)", "requests (>=2.28,<3.0)", "ruamel.yaml (==0.17.21)", "ruff (>=0.15.6,<0.16.0)", "tenacity (>=9.0,<10.0)", "types-PyYAML (>=6.0,<7.0)", "types-jsonschema (>=3.2,<4.0)", "types-requests (>=2.28,<3.0)"] [[package]] name = "aws-xray-sdk" @@ -325,7 +390,7 @@ description = "The AWS X-Ray SDK for Python (the SDK) enables Python developers optional = true python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"tracer\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"tracer\"" files = [ {file = "aws_xray_sdk-2.15.0-py2.py3-none-any.whl", hash = "sha256:422d62ad7d52e373eebb90b642eb1bb24657afe03b22a8df4a8b2e5108e278a3"}, {file = "aws_xray_sdk-2.15.0.tar.gz", hash = "sha256:794381b96e835314345068ae1dd3b9120bd8b4e21295066c37e8814dbb341365"}, @@ -442,6 +507,7 @@ files = [ {file = "boto3-1.42.67-py3-none-any.whl", hash = "sha256:aa900216bdc48bbd0115ed7128a4baed5548c6a60673160a38df8a8566df57cd"}, {file = "boto3-1.42.67.tar.gz", hash = "sha256:d4123ceb3be36c5cb7ddccc7a7c43701e1fb6af612ef46e3b5d667daf5447d4b"}, ] +markers = {main = "extra == \"all\" or extra == \"datamasking\" or extra == \"aws-sdk\""} [package.dependencies] botocore = ">=1.42.67,<1.43.0" @@ -453,457 +519,464 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "boto3-stubs" -version = "1.42.84" -description = "Type annotations for boto3 1.42.84 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.42.84-py3-none-any.whl", hash = "sha256:73c3f47fc18e27dfe6f17c1c4d3ee48ab6f926d1b7029d15e6771c8255a6f278"}, - {file = "boto3_stubs-1.42.84.tar.gz", hash = "sha256:c517c254e1d8f00af24f7df55c8b1061d1142405c5ac07e426ee2b5b709f3362"}, + {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] botocore-stubs = "*" -mypy-boto3-appconfig = {version = ">=1.42.0,<1.43.0", optional = true, markers = "extra == \"appconfig\""} -mypy-boto3-appconfigdata = {version = ">=1.42.0,<1.43.0", optional = true, markers = "extra == \"appconfigdata\""} -mypy-boto3-cloudformation = {version = ">=1.42.0,<1.43.0", optional = true, markers = "extra == \"cloudformation\""} -mypy-boto3-cloudwatch = {version = ">=1.42.0,<1.43.0", optional = true, markers = "extra == \"cloudwatch\""} -mypy-boto3-dynamodb = {version = ">=1.42.0,<1.43.0", optional = true, markers = "extra == \"dynamodb\""} -mypy-boto3-lambda = {version = ">=1.42.0,<1.43.0", optional = true, markers = "extra == \"lambda\""} -mypy-boto3-logs = {version = ">=1.42.0,<1.43.0", optional = true, markers = "extra == \"logs\""} -mypy-boto3-s3 = {version = ">=1.42.0,<1.43.0", optional = true, markers = "extra == \"s3\""} -mypy-boto3-secretsmanager = {version = ">=1.42.0,<1.43.0", optional = true, markers = "extra == \"secretsmanager\""} -mypy-boto3-ssm = {version = ">=1.42.0,<1.43.0", optional = true, markers = "extra == \"ssm\""} -mypy-boto3-xray = {version = ">=1.42.0,<1.43.0", optional = true, markers = "extra == \"xray\""} +mypy-boto3-appconfig = {version = ">=1.43.0,<1.44.0", optional = true, markers = "extra == \"appconfig\""} +mypy-boto3-appconfigdata = {version = ">=1.43.0,<1.44.0", optional = true, markers = "extra == \"appconfigdata\""} +mypy-boto3-cloudformation = {version = ">=1.43.0,<1.44.0", optional = true, markers = "extra == \"cloudformation\""} +mypy-boto3-cloudwatch = {version = ">=1.43.0,<1.44.0", optional = true, markers = "extra == \"cloudwatch\""} +mypy-boto3-dynamodb = {version = ">=1.43.0,<1.44.0", optional = true, markers = "extra == \"dynamodb\""} +mypy-boto3-lambda = {version = ">=1.43.0,<1.44.0", optional = true, markers = "extra == \"lambda\""} +mypy-boto3-logs = {version = ">=1.43.0,<1.44.0", optional = true, markers = "extra == \"logs\""} +mypy-boto3-s3 = {version = ">=1.43.0,<1.44.0", optional = true, markers = "extra == \"s3\""} +mypy-boto3-secretsmanager = {version = ">=1.43.0,<1.44.0", optional = true, markers = "extra == \"secretsmanager\""} +mypy-boto3-ssm = {version = ">=1.43.0,<1.44.0", optional = true, markers = "extra == \"ssm\""} +mypy-boto3-xray = {version = ">=1.43.0,<1.44.0", optional = true, markers = "extra == \"xray\""} types-s3transfer = "*" typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} [package.extras] -accessanalyzer = ["mypy-boto3-accessanalyzer (>=1.42.0,<1.43.0)"] -account = ["mypy-boto3-account (>=1.42.0,<1.43.0)"] -acm = ["mypy-boto3-acm (>=1.42.0,<1.43.0)"] -acm-pca = ["mypy-boto3-acm-pca (>=1.42.0,<1.43.0)"] -aiops = ["mypy-boto3-aiops (>=1.42.0,<1.43.0)"] -all = ["mypy-boto3-accessanalyzer (>=1.42.0,<1.43.0)", "mypy-boto3-account (>=1.42.0,<1.43.0)", "mypy-boto3-acm (>=1.42.0,<1.43.0)", "mypy-boto3-acm-pca (>=1.42.0,<1.43.0)", "mypy-boto3-aiops (>=1.42.0,<1.43.0)", "mypy-boto3-amp (>=1.42.0,<1.43.0)", "mypy-boto3-amplify (>=1.42.0,<1.43.0)", "mypy-boto3-amplifybackend (>=1.42.0,<1.43.0)", "mypy-boto3-amplifyuibuilder (>=1.42.0,<1.43.0)", "mypy-boto3-apigateway (>=1.42.0,<1.43.0)", "mypy-boto3-apigatewaymanagementapi (>=1.42.0,<1.43.0)", "mypy-boto3-apigatewayv2 (>=1.42.0,<1.43.0)", "mypy-boto3-appconfig (>=1.42.0,<1.43.0)", "mypy-boto3-appconfigdata (>=1.42.0,<1.43.0)", "mypy-boto3-appfabric (>=1.42.0,<1.43.0)", "mypy-boto3-appflow (>=1.42.0,<1.43.0)", "mypy-boto3-appintegrations (>=1.42.0,<1.43.0)", "mypy-boto3-application-autoscaling (>=1.42.0,<1.43.0)", "mypy-boto3-application-insights (>=1.42.0,<1.43.0)", "mypy-boto3-application-signals (>=1.42.0,<1.43.0)", "mypy-boto3-applicationcostprofiler (>=1.42.0,<1.43.0)", "mypy-boto3-appmesh (>=1.42.0,<1.43.0)", "mypy-boto3-apprunner (>=1.42.0,<1.43.0)", "mypy-boto3-appstream (>=1.42.0,<1.43.0)", "mypy-boto3-appsync (>=1.42.0,<1.43.0)", "mypy-boto3-arc-region-switch (>=1.42.0,<1.43.0)", "mypy-boto3-arc-zonal-shift (>=1.42.0,<1.43.0)", "mypy-boto3-artifact (>=1.42.0,<1.43.0)", "mypy-boto3-athena (>=1.42.0,<1.43.0)", "mypy-boto3-auditmanager (>=1.42.0,<1.43.0)", "mypy-boto3-autoscaling (>=1.42.0,<1.43.0)", "mypy-boto3-autoscaling-plans (>=1.42.0,<1.43.0)", "mypy-boto3-b2bi (>=1.42.0,<1.43.0)", "mypy-boto3-backup (>=1.42.0,<1.43.0)", "mypy-boto3-backup-gateway (>=1.42.0,<1.43.0)", "mypy-boto3-backupsearch (>=1.42.0,<1.43.0)", "mypy-boto3-batch (>=1.42.0,<1.43.0)", "mypy-boto3-bcm-dashboards (>=1.42.0,<1.43.0)", "mypy-boto3-bcm-data-exports (>=1.42.0,<1.43.0)", "mypy-boto3-bcm-pricing-calculator (>=1.42.0,<1.43.0)", "mypy-boto3-bcm-recommended-actions (>=1.42.0,<1.43.0)", "mypy-boto3-bedrock (>=1.42.0,<1.43.0)", "mypy-boto3-bedrock-agent (>=1.42.0,<1.43.0)", "mypy-boto3-bedrock-agent-runtime (>=1.42.0,<1.43.0)", "mypy-boto3-bedrock-agentcore (>=1.42.0,<1.43.0)", "mypy-boto3-bedrock-agentcore-control (>=1.42.0,<1.43.0)", "mypy-boto3-bedrock-data-automation (>=1.42.0,<1.43.0)", "mypy-boto3-bedrock-data-automation-runtime (>=1.42.0,<1.43.0)", "mypy-boto3-bedrock-runtime (>=1.42.0,<1.43.0)", "mypy-boto3-billing (>=1.42.0,<1.43.0)", "mypy-boto3-billingconductor (>=1.42.0,<1.43.0)", "mypy-boto3-braket (>=1.42.0,<1.43.0)", "mypy-boto3-budgets (>=1.42.0,<1.43.0)", "mypy-boto3-ce (>=1.42.0,<1.43.0)", "mypy-boto3-chatbot (>=1.42.0,<1.43.0)", "mypy-boto3-chime (>=1.42.0,<1.43.0)", "mypy-boto3-chime-sdk-identity (>=1.42.0,<1.43.0)", "mypy-boto3-chime-sdk-media-pipelines (>=1.42.0,<1.43.0)", "mypy-boto3-chime-sdk-meetings (>=1.42.0,<1.43.0)", "mypy-boto3-chime-sdk-messaging (>=1.42.0,<1.43.0)", "mypy-boto3-chime-sdk-voice (>=1.42.0,<1.43.0)", "mypy-boto3-cleanrooms (>=1.42.0,<1.43.0)", "mypy-boto3-cleanroomsml (>=1.42.0,<1.43.0)", "mypy-boto3-cloud9 (>=1.42.0,<1.43.0)", "mypy-boto3-cloudcontrol (>=1.42.0,<1.43.0)", "mypy-boto3-clouddirectory (>=1.42.0,<1.43.0)", "mypy-boto3-cloudformation (>=1.42.0,<1.43.0)", "mypy-boto3-cloudfront (>=1.42.0,<1.43.0)", "mypy-boto3-cloudfront-keyvaluestore (>=1.42.0,<1.43.0)", "mypy-boto3-cloudhsm (>=1.42.0,<1.43.0)", "mypy-boto3-cloudhsmv2 (>=1.42.0,<1.43.0)", "mypy-boto3-cloudsearch (>=1.42.0,<1.43.0)", "mypy-boto3-cloudsearchdomain (>=1.42.0,<1.43.0)", "mypy-boto3-cloudtrail (>=1.42.0,<1.43.0)", "mypy-boto3-cloudtrail-data (>=1.42.0,<1.43.0)", "mypy-boto3-cloudwatch (>=1.42.0,<1.43.0)", "mypy-boto3-codeartifact (>=1.42.0,<1.43.0)", "mypy-boto3-codebuild (>=1.42.0,<1.43.0)", "mypy-boto3-codecatalyst (>=1.42.0,<1.43.0)", "mypy-boto3-codecommit (>=1.42.0,<1.43.0)", "mypy-boto3-codeconnections (>=1.42.0,<1.43.0)", "mypy-boto3-codedeploy (>=1.42.0,<1.43.0)", "mypy-boto3-codeguru-reviewer (>=1.42.0,<1.43.0)", "mypy-boto3-codeguru-security (>=1.42.0,<1.43.0)", "mypy-boto3-codeguruprofiler (>=1.42.0,<1.43.0)", "mypy-boto3-codepipeline (>=1.42.0,<1.43.0)", "mypy-boto3-codestar-connections (>=1.42.0,<1.43.0)", "mypy-boto3-codestar-notifications (>=1.42.0,<1.43.0)", "mypy-boto3-cognito-identity (>=1.42.0,<1.43.0)", "mypy-boto3-cognito-idp (>=1.42.0,<1.43.0)", "mypy-boto3-cognito-sync (>=1.42.0,<1.43.0)", "mypy-boto3-comprehend (>=1.42.0,<1.43.0)", "mypy-boto3-comprehendmedical (>=1.42.0,<1.43.0)", "mypy-boto3-compute-optimizer (>=1.42.0,<1.43.0)", "mypy-boto3-compute-optimizer-automation (>=1.42.0,<1.43.0)", "mypy-boto3-config (>=1.42.0,<1.43.0)", "mypy-boto3-connect (>=1.42.0,<1.43.0)", "mypy-boto3-connect-contact-lens (>=1.42.0,<1.43.0)", "mypy-boto3-connectcampaigns (>=1.42.0,<1.43.0)", "mypy-boto3-connectcampaignsv2 (>=1.42.0,<1.43.0)", "mypy-boto3-connectcases (>=1.42.0,<1.43.0)", "mypy-boto3-connecthealth (>=1.42.0,<1.43.0)", "mypy-boto3-connectparticipant (>=1.42.0,<1.43.0)", "mypy-boto3-controlcatalog (>=1.42.0,<1.43.0)", "mypy-boto3-controltower (>=1.42.0,<1.43.0)", "mypy-boto3-cost-optimization-hub (>=1.42.0,<1.43.0)", "mypy-boto3-cur (>=1.42.0,<1.43.0)", "mypy-boto3-customer-profiles (>=1.42.0,<1.43.0)", "mypy-boto3-databrew (>=1.42.0,<1.43.0)", "mypy-boto3-dataexchange (>=1.42.0,<1.43.0)", "mypy-boto3-datapipeline (>=1.42.0,<1.43.0)", "mypy-boto3-datasync (>=1.42.0,<1.43.0)", "mypy-boto3-datazone (>=1.42.0,<1.43.0)", "mypy-boto3-dax (>=1.42.0,<1.43.0)", "mypy-boto3-deadline (>=1.42.0,<1.43.0)", "mypy-boto3-detective (>=1.42.0,<1.43.0)", "mypy-boto3-devicefarm (>=1.42.0,<1.43.0)", "mypy-boto3-devops-agent (>=1.42.0,<1.43.0)", "mypy-boto3-devops-guru (>=1.42.0,<1.43.0)", "mypy-boto3-directconnect (>=1.42.0,<1.43.0)", "mypy-boto3-discovery (>=1.42.0,<1.43.0)", "mypy-boto3-dlm (>=1.42.0,<1.43.0)", "mypy-boto3-dms (>=1.42.0,<1.43.0)", "mypy-boto3-docdb (>=1.42.0,<1.43.0)", "mypy-boto3-docdb-elastic (>=1.42.0,<1.43.0)", "mypy-boto3-drs (>=1.42.0,<1.43.0)", "mypy-boto3-ds (>=1.42.0,<1.43.0)", "mypy-boto3-ds-data (>=1.42.0,<1.43.0)", "mypy-boto3-dsql (>=1.42.0,<1.43.0)", "mypy-boto3-dynamodb (>=1.42.0,<1.43.0)", "mypy-boto3-dynamodbstreams (>=1.42.0,<1.43.0)", "mypy-boto3-ebs (>=1.42.0,<1.43.0)", "mypy-boto3-ec2 (>=1.42.0,<1.43.0)", "mypy-boto3-ec2-instance-connect (>=1.42.0,<1.43.0)", "mypy-boto3-ecr (>=1.42.0,<1.43.0)", "mypy-boto3-ecr-public (>=1.42.0,<1.43.0)", "mypy-boto3-ecs (>=1.42.0,<1.43.0)", "mypy-boto3-efs (>=1.42.0,<1.43.0)", "mypy-boto3-eks (>=1.42.0,<1.43.0)", "mypy-boto3-eks-auth (>=1.42.0,<1.43.0)", "mypy-boto3-elasticache (>=1.42.0,<1.43.0)", "mypy-boto3-elasticbeanstalk (>=1.42.0,<1.43.0)", "mypy-boto3-elb (>=1.42.0,<1.43.0)", "mypy-boto3-elbv2 (>=1.42.0,<1.43.0)", "mypy-boto3-elementalinference (>=1.42.0,<1.43.0)", "mypy-boto3-emr (>=1.42.0,<1.43.0)", "mypy-boto3-emr-containers (>=1.42.0,<1.43.0)", "mypy-boto3-emr-serverless (>=1.42.0,<1.43.0)", "mypy-boto3-entityresolution (>=1.42.0,<1.43.0)", "mypy-boto3-es (>=1.42.0,<1.43.0)", "mypy-boto3-events (>=1.42.0,<1.43.0)", "mypy-boto3-evs (>=1.42.0,<1.43.0)", "mypy-boto3-finspace (>=1.42.0,<1.43.0)", "mypy-boto3-finspace-data (>=1.42.0,<1.43.0)", "mypy-boto3-firehose (>=1.42.0,<1.43.0)", "mypy-boto3-fis (>=1.42.0,<1.43.0)", "mypy-boto3-fms (>=1.42.0,<1.43.0)", "mypy-boto3-forecast (>=1.42.0,<1.43.0)", "mypy-boto3-forecastquery (>=1.42.0,<1.43.0)", "mypy-boto3-frauddetector (>=1.42.0,<1.43.0)", "mypy-boto3-freetier (>=1.42.0,<1.43.0)", "mypy-boto3-fsx (>=1.42.0,<1.43.0)", "mypy-boto3-gamelift (>=1.42.0,<1.43.0)", "mypy-boto3-gameliftstreams (>=1.42.0,<1.43.0)", "mypy-boto3-geo-maps (>=1.42.0,<1.43.0)", "mypy-boto3-geo-places (>=1.42.0,<1.43.0)", "mypy-boto3-geo-routes (>=1.42.0,<1.43.0)", "mypy-boto3-glacier (>=1.42.0,<1.43.0)", "mypy-boto3-globalaccelerator (>=1.42.0,<1.43.0)", "mypy-boto3-glue (>=1.42.0,<1.43.0)", "mypy-boto3-grafana (>=1.42.0,<1.43.0)", "mypy-boto3-greengrass (>=1.42.0,<1.43.0)", "mypy-boto3-greengrassv2 (>=1.42.0,<1.43.0)", "mypy-boto3-groundstation (>=1.42.0,<1.43.0)", "mypy-boto3-guardduty (>=1.42.0,<1.43.0)", "mypy-boto3-health (>=1.42.0,<1.43.0)", "mypy-boto3-healthlake (>=1.42.0,<1.43.0)", "mypy-boto3-iam (>=1.42.0,<1.43.0)", "mypy-boto3-identitystore (>=1.42.0,<1.43.0)", "mypy-boto3-imagebuilder (>=1.42.0,<1.43.0)", "mypy-boto3-importexport (>=1.42.0,<1.43.0)", "mypy-boto3-inspector (>=1.42.0,<1.43.0)", "mypy-boto3-inspector-scan (>=1.42.0,<1.43.0)", "mypy-boto3-inspector2 (>=1.42.0,<1.43.0)", "mypy-boto3-internetmonitor (>=1.42.0,<1.43.0)", "mypy-boto3-invoicing (>=1.42.0,<1.43.0)", "mypy-boto3-iot (>=1.42.0,<1.43.0)", "mypy-boto3-iot-data (>=1.42.0,<1.43.0)", "mypy-boto3-iot-jobs-data (>=1.42.0,<1.43.0)", "mypy-boto3-iot-managed-integrations (>=1.42.0,<1.43.0)", "mypy-boto3-iotdeviceadvisor (>=1.42.0,<1.43.0)", "mypy-boto3-iotevents (>=1.42.0,<1.43.0)", "mypy-boto3-iotevents-data (>=1.42.0,<1.43.0)", "mypy-boto3-iotfleetwise (>=1.42.0,<1.43.0)", "mypy-boto3-iotsecuretunneling (>=1.42.0,<1.43.0)", "mypy-boto3-iotsitewise (>=1.42.0,<1.43.0)", "mypy-boto3-iotthingsgraph (>=1.42.0,<1.43.0)", "mypy-boto3-iottwinmaker (>=1.42.0,<1.43.0)", "mypy-boto3-iotwireless (>=1.42.0,<1.43.0)", "mypy-boto3-ivs (>=1.42.0,<1.43.0)", "mypy-boto3-ivs-realtime (>=1.42.0,<1.43.0)", "mypy-boto3-ivschat (>=1.42.0,<1.43.0)", "mypy-boto3-kafka (>=1.42.0,<1.43.0)", "mypy-boto3-kafkaconnect (>=1.42.0,<1.43.0)", "mypy-boto3-kendra (>=1.42.0,<1.43.0)", "mypy-boto3-kendra-ranking (>=1.42.0,<1.43.0)", "mypy-boto3-keyspaces (>=1.42.0,<1.43.0)", "mypy-boto3-keyspacesstreams (>=1.42.0,<1.43.0)", "mypy-boto3-kinesis (>=1.42.0,<1.43.0)", "mypy-boto3-kinesis-video-archived-media (>=1.42.0,<1.43.0)", "mypy-boto3-kinesis-video-media (>=1.42.0,<1.43.0)", "mypy-boto3-kinesis-video-signaling (>=1.42.0,<1.43.0)", "mypy-boto3-kinesis-video-webrtc-storage (>=1.42.0,<1.43.0)", "mypy-boto3-kinesisanalytics (>=1.42.0,<1.43.0)", "mypy-boto3-kinesisanalyticsv2 (>=1.42.0,<1.43.0)", "mypy-boto3-kinesisvideo (>=1.42.0,<1.43.0)", "mypy-boto3-kms (>=1.42.0,<1.43.0)", "mypy-boto3-lakeformation (>=1.42.0,<1.43.0)", "mypy-boto3-lambda (>=1.42.0,<1.43.0)", "mypy-boto3-launch-wizard (>=1.42.0,<1.43.0)", "mypy-boto3-lex-models (>=1.42.0,<1.43.0)", "mypy-boto3-lex-runtime (>=1.42.0,<1.43.0)", "mypy-boto3-lexv2-models (>=1.42.0,<1.43.0)", "mypy-boto3-lexv2-runtime (>=1.42.0,<1.43.0)", "mypy-boto3-license-manager (>=1.42.0,<1.43.0)", "mypy-boto3-license-manager-linux-subscriptions (>=1.42.0,<1.43.0)", "mypy-boto3-license-manager-user-subscriptions (>=1.42.0,<1.43.0)", "mypy-boto3-lightsail (>=1.42.0,<1.43.0)", "mypy-boto3-location (>=1.42.0,<1.43.0)", "mypy-boto3-logs (>=1.42.0,<1.43.0)", "mypy-boto3-lookoutequipment (>=1.42.0,<1.43.0)", "mypy-boto3-m2 (>=1.42.0,<1.43.0)", "mypy-boto3-machinelearning (>=1.42.0,<1.43.0)", "mypy-boto3-macie2 (>=1.42.0,<1.43.0)", "mypy-boto3-mailmanager (>=1.42.0,<1.43.0)", "mypy-boto3-managedblockchain (>=1.42.0,<1.43.0)", "mypy-boto3-managedblockchain-query (>=1.42.0,<1.43.0)", "mypy-boto3-marketplace-agreement (>=1.42.0,<1.43.0)", "mypy-boto3-marketplace-catalog (>=1.42.0,<1.43.0)", "mypy-boto3-marketplace-deployment (>=1.42.0,<1.43.0)", "mypy-boto3-marketplace-entitlement (>=1.42.0,<1.43.0)", "mypy-boto3-marketplace-reporting (>=1.42.0,<1.43.0)", "mypy-boto3-marketplacecommerceanalytics (>=1.42.0,<1.43.0)", "mypy-boto3-mediaconnect (>=1.42.0,<1.43.0)", "mypy-boto3-mediaconvert (>=1.42.0,<1.43.0)", "mypy-boto3-medialive (>=1.42.0,<1.43.0)", "mypy-boto3-mediapackage (>=1.42.0,<1.43.0)", "mypy-boto3-mediapackage-vod (>=1.42.0,<1.43.0)", "mypy-boto3-mediapackagev2 (>=1.42.0,<1.43.0)", "mypy-boto3-mediastore (>=1.42.0,<1.43.0)", "mypy-boto3-mediastore-data (>=1.42.0,<1.43.0)", "mypy-boto3-mediatailor (>=1.42.0,<1.43.0)", "mypy-boto3-medical-imaging (>=1.42.0,<1.43.0)", "mypy-boto3-memorydb (>=1.42.0,<1.43.0)", "mypy-boto3-meteringmarketplace (>=1.42.0,<1.43.0)", "mypy-boto3-mgh (>=1.42.0,<1.43.0)", "mypy-boto3-mgn (>=1.42.0,<1.43.0)", "mypy-boto3-migration-hub-refactor-spaces (>=1.42.0,<1.43.0)", "mypy-boto3-migrationhub-config (>=1.42.0,<1.43.0)", "mypy-boto3-migrationhuborchestrator (>=1.42.0,<1.43.0)", "mypy-boto3-migrationhubstrategy (>=1.42.0,<1.43.0)", "mypy-boto3-mpa (>=1.42.0,<1.43.0)", "mypy-boto3-mq (>=1.42.0,<1.43.0)", "mypy-boto3-mturk (>=1.42.0,<1.43.0)", "mypy-boto3-mwaa (>=1.42.0,<1.43.0)", "mypy-boto3-mwaa-serverless (>=1.42.0,<1.43.0)", "mypy-boto3-neptune (>=1.42.0,<1.43.0)", "mypy-boto3-neptune-graph (>=1.42.0,<1.43.0)", "mypy-boto3-neptunedata (>=1.42.0,<1.43.0)", "mypy-boto3-network-firewall (>=1.42.0,<1.43.0)", "mypy-boto3-networkflowmonitor (>=1.42.0,<1.43.0)", "mypy-boto3-networkmanager (>=1.42.0,<1.43.0)", "mypy-boto3-networkmonitor (>=1.42.0,<1.43.0)", "mypy-boto3-notifications (>=1.42.0,<1.43.0)", "mypy-boto3-notificationscontacts (>=1.42.0,<1.43.0)", "mypy-boto3-nova-act (>=1.42.0,<1.43.0)", "mypy-boto3-oam (>=1.42.0,<1.43.0)", "mypy-boto3-observabilityadmin (>=1.42.0,<1.43.0)", "mypy-boto3-odb (>=1.42.0,<1.43.0)", "mypy-boto3-omics (>=1.42.0,<1.43.0)", "mypy-boto3-opensearch (>=1.42.0,<1.43.0)", "mypy-boto3-opensearchserverless (>=1.42.0,<1.43.0)", "mypy-boto3-organizations (>=1.42.0,<1.43.0)", "mypy-boto3-osis (>=1.42.0,<1.43.0)", "mypy-boto3-outposts (>=1.42.0,<1.43.0)", "mypy-boto3-panorama (>=1.42.0,<1.43.0)", "mypy-boto3-partnercentral-account (>=1.42.0,<1.43.0)", "mypy-boto3-partnercentral-benefits (>=1.42.0,<1.43.0)", "mypy-boto3-partnercentral-channel (>=1.42.0,<1.43.0)", "mypy-boto3-partnercentral-selling (>=1.42.0,<1.43.0)", "mypy-boto3-payment-cryptography (>=1.42.0,<1.43.0)", "mypy-boto3-payment-cryptography-data (>=1.42.0,<1.43.0)", "mypy-boto3-pca-connector-ad (>=1.42.0,<1.43.0)", "mypy-boto3-pca-connector-scep (>=1.42.0,<1.43.0)", "mypy-boto3-pcs (>=1.42.0,<1.43.0)", "mypy-boto3-personalize (>=1.42.0,<1.43.0)", "mypy-boto3-personalize-events (>=1.42.0,<1.43.0)", "mypy-boto3-personalize-runtime (>=1.42.0,<1.43.0)", "mypy-boto3-pi (>=1.42.0,<1.43.0)", "mypy-boto3-pinpoint (>=1.42.0,<1.43.0)", "mypy-boto3-pinpoint-email (>=1.42.0,<1.43.0)", "mypy-boto3-pinpoint-sms-voice (>=1.42.0,<1.43.0)", "mypy-boto3-pinpoint-sms-voice-v2 (>=1.42.0,<1.43.0)", "mypy-boto3-pipes (>=1.42.0,<1.43.0)", "mypy-boto3-polly (>=1.42.0,<1.43.0)", "mypy-boto3-pricing (>=1.42.0,<1.43.0)", "mypy-boto3-proton (>=1.42.0,<1.43.0)", "mypy-boto3-qapps (>=1.42.0,<1.43.0)", "mypy-boto3-qbusiness (>=1.42.0,<1.43.0)", "mypy-boto3-qconnect (>=1.42.0,<1.43.0)", "mypy-boto3-quicksight (>=1.42.0,<1.43.0)", "mypy-boto3-ram (>=1.42.0,<1.43.0)", "mypy-boto3-rbin (>=1.42.0,<1.43.0)", "mypy-boto3-rds (>=1.42.0,<1.43.0)", "mypy-boto3-rds-data (>=1.42.0,<1.43.0)", "mypy-boto3-redshift (>=1.42.0,<1.43.0)", "mypy-boto3-redshift-data (>=1.42.0,<1.43.0)", "mypy-boto3-redshift-serverless (>=1.42.0,<1.43.0)", "mypy-boto3-rekognition (>=1.42.0,<1.43.0)", "mypy-boto3-repostspace (>=1.42.0,<1.43.0)", "mypy-boto3-resiliencehub (>=1.42.0,<1.43.0)", "mypy-boto3-resource-explorer-2 (>=1.42.0,<1.43.0)", "mypy-boto3-resource-groups (>=1.42.0,<1.43.0)", "mypy-boto3-resourcegroupstaggingapi (>=1.42.0,<1.43.0)", "mypy-boto3-rolesanywhere (>=1.42.0,<1.43.0)", "mypy-boto3-route53 (>=1.42.0,<1.43.0)", "mypy-boto3-route53-recovery-cluster (>=1.42.0,<1.43.0)", "mypy-boto3-route53-recovery-control-config (>=1.42.0,<1.43.0)", "mypy-boto3-route53-recovery-readiness (>=1.42.0,<1.43.0)", "mypy-boto3-route53domains (>=1.42.0,<1.43.0)", "mypy-boto3-route53globalresolver (>=1.42.0,<1.43.0)", "mypy-boto3-route53profiles (>=1.42.0,<1.43.0)", "mypy-boto3-route53resolver (>=1.42.0,<1.43.0)", "mypy-boto3-rtbfabric (>=1.42.0,<1.43.0)", "mypy-boto3-rum (>=1.42.0,<1.43.0)", "mypy-boto3-s3 (>=1.42.0,<1.43.0)", "mypy-boto3-s3control (>=1.42.0,<1.43.0)", "mypy-boto3-s3outposts (>=1.42.0,<1.43.0)", "mypy-boto3-s3tables (>=1.42.0,<1.43.0)", "mypy-boto3-s3vectors (>=1.42.0,<1.43.0)", "mypy-boto3-sagemaker (>=1.42.0,<1.43.0)", "mypy-boto3-sagemaker-a2i-runtime (>=1.42.0,<1.43.0)", "mypy-boto3-sagemaker-edge (>=1.42.0,<1.43.0)", "mypy-boto3-sagemaker-featurestore-runtime (>=1.42.0,<1.43.0)", "mypy-boto3-sagemaker-geospatial (>=1.42.0,<1.43.0)", "mypy-boto3-sagemaker-metrics (>=1.42.0,<1.43.0)", "mypy-boto3-sagemaker-runtime (>=1.42.0,<1.43.0)", "mypy-boto3-savingsplans (>=1.42.0,<1.43.0)", "mypy-boto3-scheduler (>=1.42.0,<1.43.0)", "mypy-boto3-schemas (>=1.42.0,<1.43.0)", "mypy-boto3-sdb (>=1.42.0,<1.43.0)", "mypy-boto3-secretsmanager (>=1.42.0,<1.43.0)", "mypy-boto3-security-ir (>=1.42.0,<1.43.0)", "mypy-boto3-securityagent (>=1.42.0,<1.43.0)", "mypy-boto3-securityhub (>=1.42.0,<1.43.0)", "mypy-boto3-securitylake (>=1.42.0,<1.43.0)", "mypy-boto3-serverlessrepo (>=1.42.0,<1.43.0)", "mypy-boto3-service-quotas (>=1.42.0,<1.43.0)", "mypy-boto3-servicecatalog (>=1.42.0,<1.43.0)", "mypy-boto3-servicecatalog-appregistry (>=1.42.0,<1.43.0)", "mypy-boto3-servicediscovery (>=1.42.0,<1.43.0)", "mypy-boto3-ses (>=1.42.0,<1.43.0)", "mypy-boto3-sesv2 (>=1.42.0,<1.43.0)", "mypy-boto3-shield (>=1.42.0,<1.43.0)", "mypy-boto3-signer (>=1.42.0,<1.43.0)", "mypy-boto3-signer-data (>=1.42.0,<1.43.0)", "mypy-boto3-signin (>=1.42.0,<1.43.0)", "mypy-boto3-simpledbv2 (>=1.42.0,<1.43.0)", "mypy-boto3-simspaceweaver (>=1.42.0,<1.43.0)", "mypy-boto3-snow-device-management (>=1.42.0,<1.43.0)", "mypy-boto3-snowball (>=1.42.0,<1.43.0)", "mypy-boto3-sns (>=1.42.0,<1.43.0)", "mypy-boto3-socialmessaging (>=1.42.0,<1.43.0)", "mypy-boto3-sqs (>=1.42.0,<1.43.0)", "mypy-boto3-ssm (>=1.42.0,<1.43.0)", "mypy-boto3-ssm-contacts (>=1.42.0,<1.43.0)", "mypy-boto3-ssm-guiconnect (>=1.42.0,<1.43.0)", "mypy-boto3-ssm-incidents (>=1.42.0,<1.43.0)", "mypy-boto3-ssm-quicksetup (>=1.42.0,<1.43.0)", "mypy-boto3-ssm-sap (>=1.42.0,<1.43.0)", "mypy-boto3-sso (>=1.42.0,<1.43.0)", "mypy-boto3-sso-admin (>=1.42.0,<1.43.0)", "mypy-boto3-sso-oidc (>=1.42.0,<1.43.0)", "mypy-boto3-stepfunctions (>=1.42.0,<1.43.0)", "mypy-boto3-storagegateway (>=1.42.0,<1.43.0)", "mypy-boto3-sts (>=1.42.0,<1.43.0)", "mypy-boto3-supplychain (>=1.42.0,<1.43.0)", "mypy-boto3-support (>=1.42.0,<1.43.0)", "mypy-boto3-support-app (>=1.42.0,<1.43.0)", "mypy-boto3-sustainability (>=1.42.0,<1.43.0)", "mypy-boto3-swf (>=1.42.0,<1.43.0)", "mypy-boto3-synthetics (>=1.42.0,<1.43.0)", "mypy-boto3-taxsettings (>=1.42.0,<1.43.0)", "mypy-boto3-textract (>=1.42.0,<1.43.0)", "mypy-boto3-timestream-influxdb (>=1.42.0,<1.43.0)", "mypy-boto3-timestream-query (>=1.42.0,<1.43.0)", "mypy-boto3-timestream-write (>=1.42.0,<1.43.0)", "mypy-boto3-tnb (>=1.42.0,<1.43.0)", "mypy-boto3-transcribe (>=1.42.0,<1.43.0)", "mypy-boto3-transfer (>=1.42.0,<1.43.0)", "mypy-boto3-translate (>=1.42.0,<1.43.0)", "mypy-boto3-trustedadvisor (>=1.42.0,<1.43.0)", "mypy-boto3-uxc (>=1.42.0,<1.43.0)", "mypy-boto3-verifiedpermissions (>=1.42.0,<1.43.0)", "mypy-boto3-voice-id (>=1.42.0,<1.43.0)", "mypy-boto3-vpc-lattice (>=1.42.0,<1.43.0)", "mypy-boto3-waf (>=1.42.0,<1.43.0)", "mypy-boto3-waf-regional (>=1.42.0,<1.43.0)", "mypy-boto3-wafv2 (>=1.42.0,<1.43.0)", "mypy-boto3-wellarchitected (>=1.42.0,<1.43.0)", "mypy-boto3-wickr (>=1.42.0,<1.43.0)", "mypy-boto3-wisdom (>=1.42.0,<1.43.0)", "mypy-boto3-workdocs (>=1.42.0,<1.43.0)", "mypy-boto3-workmail (>=1.42.0,<1.43.0)", "mypy-boto3-workmailmessageflow (>=1.42.0,<1.43.0)", "mypy-boto3-workspaces (>=1.42.0,<1.43.0)", "mypy-boto3-workspaces-instances (>=1.42.0,<1.43.0)", "mypy-boto3-workspaces-thin-client (>=1.42.0,<1.43.0)", "mypy-boto3-workspaces-web (>=1.42.0,<1.43.0)", "mypy-boto3-xray (>=1.42.0,<1.43.0)"] -amp = ["mypy-boto3-amp (>=1.42.0,<1.43.0)"] -amplify = ["mypy-boto3-amplify (>=1.42.0,<1.43.0)"] -amplifybackend = ["mypy-boto3-amplifybackend (>=1.42.0,<1.43.0)"] -amplifyuibuilder = ["mypy-boto3-amplifyuibuilder (>=1.42.0,<1.43.0)"] -apigateway = ["mypy-boto3-apigateway (>=1.42.0,<1.43.0)"] -apigatewaymanagementapi = ["mypy-boto3-apigatewaymanagementapi (>=1.42.0,<1.43.0)"] -apigatewayv2 = ["mypy-boto3-apigatewayv2 (>=1.42.0,<1.43.0)"] -appconfig = ["mypy-boto3-appconfig (>=1.42.0,<1.43.0)"] -appconfigdata = ["mypy-boto3-appconfigdata (>=1.42.0,<1.43.0)"] -appfabric = ["mypy-boto3-appfabric (>=1.42.0,<1.43.0)"] -appflow = ["mypy-boto3-appflow (>=1.42.0,<1.43.0)"] -appintegrations = ["mypy-boto3-appintegrations (>=1.42.0,<1.43.0)"] -application-autoscaling = ["mypy-boto3-application-autoscaling (>=1.42.0,<1.43.0)"] -application-insights = ["mypy-boto3-application-insights (>=1.42.0,<1.43.0)"] -application-signals = ["mypy-boto3-application-signals (>=1.42.0,<1.43.0)"] -applicationcostprofiler = ["mypy-boto3-applicationcostprofiler (>=1.42.0,<1.43.0)"] -appmesh = ["mypy-boto3-appmesh (>=1.42.0,<1.43.0)"] -apprunner = ["mypy-boto3-apprunner (>=1.42.0,<1.43.0)"] -appstream = ["mypy-boto3-appstream (>=1.42.0,<1.43.0)"] -appsync = ["mypy-boto3-appsync (>=1.42.0,<1.43.0)"] -arc-region-switch = ["mypy-boto3-arc-region-switch (>=1.42.0,<1.43.0)"] -arc-zonal-shift = ["mypy-boto3-arc-zonal-shift (>=1.42.0,<1.43.0)"] -artifact = ["mypy-boto3-artifact (>=1.42.0,<1.43.0)"] -athena = ["mypy-boto3-athena (>=1.42.0,<1.43.0)"] -auditmanager = ["mypy-boto3-auditmanager (>=1.42.0,<1.43.0)"] -autoscaling = ["mypy-boto3-autoscaling (>=1.42.0,<1.43.0)"] -autoscaling-plans = ["mypy-boto3-autoscaling-plans (>=1.42.0,<1.43.0)"] -b2bi = ["mypy-boto3-b2bi (>=1.42.0,<1.43.0)"] -backup = ["mypy-boto3-backup (>=1.42.0,<1.43.0)"] -backup-gateway = ["mypy-boto3-backup-gateway (>=1.42.0,<1.43.0)"] -backupsearch = ["mypy-boto3-backupsearch (>=1.42.0,<1.43.0)"] -batch = ["mypy-boto3-batch (>=1.42.0,<1.43.0)"] -bcm-dashboards = ["mypy-boto3-bcm-dashboards (>=1.42.0,<1.43.0)"] -bcm-data-exports = ["mypy-boto3-bcm-data-exports (>=1.42.0,<1.43.0)"] -bcm-pricing-calculator = ["mypy-boto3-bcm-pricing-calculator (>=1.42.0,<1.43.0)"] -bcm-recommended-actions = ["mypy-boto3-bcm-recommended-actions (>=1.42.0,<1.43.0)"] -bedrock = ["mypy-boto3-bedrock (>=1.42.0,<1.43.0)"] -bedrock-agent = ["mypy-boto3-bedrock-agent (>=1.42.0,<1.43.0)"] -bedrock-agent-runtime = ["mypy-boto3-bedrock-agent-runtime (>=1.42.0,<1.43.0)"] -bedrock-agentcore = ["mypy-boto3-bedrock-agentcore (>=1.42.0,<1.43.0)"] -bedrock-agentcore-control = ["mypy-boto3-bedrock-agentcore-control (>=1.42.0,<1.43.0)"] -bedrock-data-automation = ["mypy-boto3-bedrock-data-automation (>=1.42.0,<1.43.0)"] -bedrock-data-automation-runtime = ["mypy-boto3-bedrock-data-automation-runtime (>=1.42.0,<1.43.0)"] -bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.42.0,<1.43.0)"] -billing = ["mypy-boto3-billing (>=1.42.0,<1.43.0)"] -billingconductor = ["mypy-boto3-billingconductor (>=1.42.0,<1.43.0)"] -boto3 = ["boto3 (==1.42.84)"] -braket = ["mypy-boto3-braket (>=1.42.0,<1.43.0)"] -budgets = ["mypy-boto3-budgets (>=1.42.0,<1.43.0)"] -ce = ["mypy-boto3-ce (>=1.42.0,<1.43.0)"] -chatbot = ["mypy-boto3-chatbot (>=1.42.0,<1.43.0)"] -chime = ["mypy-boto3-chime (>=1.42.0,<1.43.0)"] -chime-sdk-identity = ["mypy-boto3-chime-sdk-identity (>=1.42.0,<1.43.0)"] -chime-sdk-media-pipelines = ["mypy-boto3-chime-sdk-media-pipelines (>=1.42.0,<1.43.0)"] -chime-sdk-meetings = ["mypy-boto3-chime-sdk-meetings (>=1.42.0,<1.43.0)"] -chime-sdk-messaging = ["mypy-boto3-chime-sdk-messaging (>=1.42.0,<1.43.0)"] -chime-sdk-voice = ["mypy-boto3-chime-sdk-voice (>=1.42.0,<1.43.0)"] -cleanrooms = ["mypy-boto3-cleanrooms (>=1.42.0,<1.43.0)"] -cleanroomsml = ["mypy-boto3-cleanroomsml (>=1.42.0,<1.43.0)"] -cloud9 = ["mypy-boto3-cloud9 (>=1.42.0,<1.43.0)"] -cloudcontrol = ["mypy-boto3-cloudcontrol (>=1.42.0,<1.43.0)"] -clouddirectory = ["mypy-boto3-clouddirectory (>=1.42.0,<1.43.0)"] -cloudformation = ["mypy-boto3-cloudformation (>=1.42.0,<1.43.0)"] -cloudfront = ["mypy-boto3-cloudfront (>=1.42.0,<1.43.0)"] -cloudfront-keyvaluestore = ["mypy-boto3-cloudfront-keyvaluestore (>=1.42.0,<1.43.0)"] -cloudhsm = ["mypy-boto3-cloudhsm (>=1.42.0,<1.43.0)"] -cloudhsmv2 = ["mypy-boto3-cloudhsmv2 (>=1.42.0,<1.43.0)"] -cloudsearch = ["mypy-boto3-cloudsearch (>=1.42.0,<1.43.0)"] -cloudsearchdomain = ["mypy-boto3-cloudsearchdomain (>=1.42.0,<1.43.0)"] -cloudtrail = ["mypy-boto3-cloudtrail (>=1.42.0,<1.43.0)"] -cloudtrail-data = ["mypy-boto3-cloudtrail-data (>=1.42.0,<1.43.0)"] -cloudwatch = ["mypy-boto3-cloudwatch (>=1.42.0,<1.43.0)"] -codeartifact = ["mypy-boto3-codeartifact (>=1.42.0,<1.43.0)"] -codebuild = ["mypy-boto3-codebuild (>=1.42.0,<1.43.0)"] -codecatalyst = ["mypy-boto3-codecatalyst (>=1.42.0,<1.43.0)"] -codecommit = ["mypy-boto3-codecommit (>=1.42.0,<1.43.0)"] -codeconnections = ["mypy-boto3-codeconnections (>=1.42.0,<1.43.0)"] -codedeploy = ["mypy-boto3-codedeploy (>=1.42.0,<1.43.0)"] -codeguru-reviewer = ["mypy-boto3-codeguru-reviewer (>=1.42.0,<1.43.0)"] -codeguru-security = ["mypy-boto3-codeguru-security (>=1.42.0,<1.43.0)"] -codeguruprofiler = ["mypy-boto3-codeguruprofiler (>=1.42.0,<1.43.0)"] -codepipeline = ["mypy-boto3-codepipeline (>=1.42.0,<1.43.0)"] -codestar-connections = ["mypy-boto3-codestar-connections (>=1.42.0,<1.43.0)"] -codestar-notifications = ["mypy-boto3-codestar-notifications (>=1.42.0,<1.43.0)"] -cognito-identity = ["mypy-boto3-cognito-identity (>=1.42.0,<1.43.0)"] -cognito-idp = ["mypy-boto3-cognito-idp (>=1.42.0,<1.43.0)"] -cognito-sync = ["mypy-boto3-cognito-sync (>=1.42.0,<1.43.0)"] -comprehend = ["mypy-boto3-comprehend (>=1.42.0,<1.43.0)"] -comprehendmedical = ["mypy-boto3-comprehendmedical (>=1.42.0,<1.43.0)"] -compute-optimizer = ["mypy-boto3-compute-optimizer (>=1.42.0,<1.43.0)"] -compute-optimizer-automation = ["mypy-boto3-compute-optimizer-automation (>=1.42.0,<1.43.0)"] -config = ["mypy-boto3-config (>=1.42.0,<1.43.0)"] -connect = ["mypy-boto3-connect (>=1.42.0,<1.43.0)"] -connect-contact-lens = ["mypy-boto3-connect-contact-lens (>=1.42.0,<1.43.0)"] -connectcampaigns = ["mypy-boto3-connectcampaigns (>=1.42.0,<1.43.0)"] -connectcampaignsv2 = ["mypy-boto3-connectcampaignsv2 (>=1.42.0,<1.43.0)"] -connectcases = ["mypy-boto3-connectcases (>=1.42.0,<1.43.0)"] -connecthealth = ["mypy-boto3-connecthealth (>=1.42.0,<1.43.0)"] -connectparticipant = ["mypy-boto3-connectparticipant (>=1.42.0,<1.43.0)"] -controlcatalog = ["mypy-boto3-controlcatalog (>=1.42.0,<1.43.0)"] -controltower = ["mypy-boto3-controltower (>=1.42.0,<1.43.0)"] -cost-optimization-hub = ["mypy-boto3-cost-optimization-hub (>=1.42.0,<1.43.0)"] -cur = ["mypy-boto3-cur (>=1.42.0,<1.43.0)"] -customer-profiles = ["mypy-boto3-customer-profiles (>=1.42.0,<1.43.0)"] -databrew = ["mypy-boto3-databrew (>=1.42.0,<1.43.0)"] -dataexchange = ["mypy-boto3-dataexchange (>=1.42.0,<1.43.0)"] -datapipeline = ["mypy-boto3-datapipeline (>=1.42.0,<1.43.0)"] -datasync = ["mypy-boto3-datasync (>=1.42.0,<1.43.0)"] -datazone = ["mypy-boto3-datazone (>=1.42.0,<1.43.0)"] -dax = ["mypy-boto3-dax (>=1.42.0,<1.43.0)"] -deadline = ["mypy-boto3-deadline (>=1.42.0,<1.43.0)"] -detective = ["mypy-boto3-detective (>=1.42.0,<1.43.0)"] -devicefarm = ["mypy-boto3-devicefarm (>=1.42.0,<1.43.0)"] -devops-agent = ["mypy-boto3-devops-agent (>=1.42.0,<1.43.0)"] -devops-guru = ["mypy-boto3-devops-guru (>=1.42.0,<1.43.0)"] -directconnect = ["mypy-boto3-directconnect (>=1.42.0,<1.43.0)"] -discovery = ["mypy-boto3-discovery (>=1.42.0,<1.43.0)"] -dlm = ["mypy-boto3-dlm (>=1.42.0,<1.43.0)"] -dms = ["mypy-boto3-dms (>=1.42.0,<1.43.0)"] -docdb = ["mypy-boto3-docdb (>=1.42.0,<1.43.0)"] -docdb-elastic = ["mypy-boto3-docdb-elastic (>=1.42.0,<1.43.0)"] -drs = ["mypy-boto3-drs (>=1.42.0,<1.43.0)"] -ds = ["mypy-boto3-ds (>=1.42.0,<1.43.0)"] -ds-data = ["mypy-boto3-ds-data (>=1.42.0,<1.43.0)"] -dsql = ["mypy-boto3-dsql (>=1.42.0,<1.43.0)"] -dynamodb = ["mypy-boto3-dynamodb (>=1.42.0,<1.43.0)"] -dynamodbstreams = ["mypy-boto3-dynamodbstreams (>=1.42.0,<1.43.0)"] -ebs = ["mypy-boto3-ebs (>=1.42.0,<1.43.0)"] -ec2 = ["mypy-boto3-ec2 (>=1.42.0,<1.43.0)"] -ec2-instance-connect = ["mypy-boto3-ec2-instance-connect (>=1.42.0,<1.43.0)"] -ecr = ["mypy-boto3-ecr (>=1.42.0,<1.43.0)"] -ecr-public = ["mypy-boto3-ecr-public (>=1.42.0,<1.43.0)"] -ecs = ["mypy-boto3-ecs (>=1.42.0,<1.43.0)"] -efs = ["mypy-boto3-efs (>=1.42.0,<1.43.0)"] -eks = ["mypy-boto3-eks (>=1.42.0,<1.43.0)"] -eks-auth = ["mypy-boto3-eks-auth (>=1.42.0,<1.43.0)"] -elasticache = ["mypy-boto3-elasticache (>=1.42.0,<1.43.0)"] -elasticbeanstalk = ["mypy-boto3-elasticbeanstalk (>=1.42.0,<1.43.0)"] -elb = ["mypy-boto3-elb (>=1.42.0,<1.43.0)"] -elbv2 = ["mypy-boto3-elbv2 (>=1.42.0,<1.43.0)"] -elementalinference = ["mypy-boto3-elementalinference (>=1.42.0,<1.43.0)"] -emr = ["mypy-boto3-emr (>=1.42.0,<1.43.0)"] -emr-containers = ["mypy-boto3-emr-containers (>=1.42.0,<1.43.0)"] -emr-serverless = ["mypy-boto3-emr-serverless (>=1.42.0,<1.43.0)"] -entityresolution = ["mypy-boto3-entityresolution (>=1.42.0,<1.43.0)"] -es = ["mypy-boto3-es (>=1.42.0,<1.43.0)"] -essential = ["mypy-boto3-cloudformation (>=1.42.0,<1.43.0)", "mypy-boto3-dynamodb (>=1.42.0,<1.43.0)", "mypy-boto3-ec2 (>=1.42.0,<1.43.0)", "mypy-boto3-lambda (>=1.42.0,<1.43.0)", "mypy-boto3-rds (>=1.42.0,<1.43.0)", "mypy-boto3-s3 (>=1.42.0,<1.43.0)", "mypy-boto3-sqs (>=1.42.0,<1.43.0)"] -events = ["mypy-boto3-events (>=1.42.0,<1.43.0)"] -evs = ["mypy-boto3-evs (>=1.42.0,<1.43.0)"] -finspace = ["mypy-boto3-finspace (>=1.42.0,<1.43.0)"] -finspace-data = ["mypy-boto3-finspace-data (>=1.42.0,<1.43.0)"] -firehose = ["mypy-boto3-firehose (>=1.42.0,<1.43.0)"] -fis = ["mypy-boto3-fis (>=1.42.0,<1.43.0)"] -fms = ["mypy-boto3-fms (>=1.42.0,<1.43.0)"] -forecast = ["mypy-boto3-forecast (>=1.42.0,<1.43.0)"] -forecastquery = ["mypy-boto3-forecastquery (>=1.42.0,<1.43.0)"] -frauddetector = ["mypy-boto3-frauddetector (>=1.42.0,<1.43.0)"] -freetier = ["mypy-boto3-freetier (>=1.42.0,<1.43.0)"] -fsx = ["mypy-boto3-fsx (>=1.42.0,<1.43.0)"] -full = ["boto3-stubs-full (>=1.42.0,<1.43.0)"] -gamelift = ["mypy-boto3-gamelift (>=1.42.0,<1.43.0)"] -gameliftstreams = ["mypy-boto3-gameliftstreams (>=1.42.0,<1.43.0)"] -geo-maps = ["mypy-boto3-geo-maps (>=1.42.0,<1.43.0)"] -geo-places = ["mypy-boto3-geo-places (>=1.42.0,<1.43.0)"] -geo-routes = ["mypy-boto3-geo-routes (>=1.42.0,<1.43.0)"] -glacier = ["mypy-boto3-glacier (>=1.42.0,<1.43.0)"] -globalaccelerator = ["mypy-boto3-globalaccelerator (>=1.42.0,<1.43.0)"] -glue = ["mypy-boto3-glue (>=1.42.0,<1.43.0)"] -grafana = ["mypy-boto3-grafana (>=1.42.0,<1.43.0)"] -greengrass = ["mypy-boto3-greengrass (>=1.42.0,<1.43.0)"] -greengrassv2 = ["mypy-boto3-greengrassv2 (>=1.42.0,<1.43.0)"] -groundstation = ["mypy-boto3-groundstation (>=1.42.0,<1.43.0)"] -guardduty = ["mypy-boto3-guardduty (>=1.42.0,<1.43.0)"] -health = ["mypy-boto3-health (>=1.42.0,<1.43.0)"] -healthlake = ["mypy-boto3-healthlake (>=1.42.0,<1.43.0)"] -iam = ["mypy-boto3-iam (>=1.42.0,<1.43.0)"] -identitystore = ["mypy-boto3-identitystore (>=1.42.0,<1.43.0)"] -imagebuilder = ["mypy-boto3-imagebuilder (>=1.42.0,<1.43.0)"] -importexport = ["mypy-boto3-importexport (>=1.42.0,<1.43.0)"] -inspector = ["mypy-boto3-inspector (>=1.42.0,<1.43.0)"] -inspector-scan = ["mypy-boto3-inspector-scan (>=1.42.0,<1.43.0)"] -inspector2 = ["mypy-boto3-inspector2 (>=1.42.0,<1.43.0)"] -internetmonitor = ["mypy-boto3-internetmonitor (>=1.42.0,<1.43.0)"] -invoicing = ["mypy-boto3-invoicing (>=1.42.0,<1.43.0)"] -iot = ["mypy-boto3-iot (>=1.42.0,<1.43.0)"] -iot-data = ["mypy-boto3-iot-data (>=1.42.0,<1.43.0)"] -iot-jobs-data = ["mypy-boto3-iot-jobs-data (>=1.42.0,<1.43.0)"] -iot-managed-integrations = ["mypy-boto3-iot-managed-integrations (>=1.42.0,<1.43.0)"] -iotdeviceadvisor = ["mypy-boto3-iotdeviceadvisor (>=1.42.0,<1.43.0)"] -iotevents = ["mypy-boto3-iotevents (>=1.42.0,<1.43.0)"] -iotevents-data = ["mypy-boto3-iotevents-data (>=1.42.0,<1.43.0)"] -iotfleetwise = ["mypy-boto3-iotfleetwise (>=1.42.0,<1.43.0)"] -iotsecuretunneling = ["mypy-boto3-iotsecuretunneling (>=1.42.0,<1.43.0)"] -iotsitewise = ["mypy-boto3-iotsitewise (>=1.42.0,<1.43.0)"] -iotthingsgraph = ["mypy-boto3-iotthingsgraph (>=1.42.0,<1.43.0)"] -iottwinmaker = ["mypy-boto3-iottwinmaker (>=1.42.0,<1.43.0)"] -iotwireless = ["mypy-boto3-iotwireless (>=1.42.0,<1.43.0)"] -ivs = ["mypy-boto3-ivs (>=1.42.0,<1.43.0)"] -ivs-realtime = ["mypy-boto3-ivs-realtime (>=1.42.0,<1.43.0)"] -ivschat = ["mypy-boto3-ivschat (>=1.42.0,<1.43.0)"] -kafka = ["mypy-boto3-kafka (>=1.42.0,<1.43.0)"] -kafkaconnect = ["mypy-boto3-kafkaconnect (>=1.42.0,<1.43.0)"] -kendra = ["mypy-boto3-kendra (>=1.42.0,<1.43.0)"] -kendra-ranking = ["mypy-boto3-kendra-ranking (>=1.42.0,<1.43.0)"] -keyspaces = ["mypy-boto3-keyspaces (>=1.42.0,<1.43.0)"] -keyspacesstreams = ["mypy-boto3-keyspacesstreams (>=1.42.0,<1.43.0)"] -kinesis = ["mypy-boto3-kinesis (>=1.42.0,<1.43.0)"] -kinesis-video-archived-media = ["mypy-boto3-kinesis-video-archived-media (>=1.42.0,<1.43.0)"] -kinesis-video-media = ["mypy-boto3-kinesis-video-media (>=1.42.0,<1.43.0)"] -kinesis-video-signaling = ["mypy-boto3-kinesis-video-signaling (>=1.42.0,<1.43.0)"] -kinesis-video-webrtc-storage = ["mypy-boto3-kinesis-video-webrtc-storage (>=1.42.0,<1.43.0)"] -kinesisanalytics = ["mypy-boto3-kinesisanalytics (>=1.42.0,<1.43.0)"] -kinesisanalyticsv2 = ["mypy-boto3-kinesisanalyticsv2 (>=1.42.0,<1.43.0)"] -kinesisvideo = ["mypy-boto3-kinesisvideo (>=1.42.0,<1.43.0)"] -kms = ["mypy-boto3-kms (>=1.42.0,<1.43.0)"] -lakeformation = ["mypy-boto3-lakeformation (>=1.42.0,<1.43.0)"] -lambda = ["mypy-boto3-lambda (>=1.42.0,<1.43.0)"] -launch-wizard = ["mypy-boto3-launch-wizard (>=1.42.0,<1.43.0)"] -lex-models = ["mypy-boto3-lex-models (>=1.42.0,<1.43.0)"] -lex-runtime = ["mypy-boto3-lex-runtime (>=1.42.0,<1.43.0)"] -lexv2-models = ["mypy-boto3-lexv2-models (>=1.42.0,<1.43.0)"] -lexv2-runtime = ["mypy-boto3-lexv2-runtime (>=1.42.0,<1.43.0)"] -license-manager = ["mypy-boto3-license-manager (>=1.42.0,<1.43.0)"] -license-manager-linux-subscriptions = ["mypy-boto3-license-manager-linux-subscriptions (>=1.42.0,<1.43.0)"] -license-manager-user-subscriptions = ["mypy-boto3-license-manager-user-subscriptions (>=1.42.0,<1.43.0)"] -lightsail = ["mypy-boto3-lightsail (>=1.42.0,<1.43.0)"] -location = ["mypy-boto3-location (>=1.42.0,<1.43.0)"] -logs = ["mypy-boto3-logs (>=1.42.0,<1.43.0)"] -lookoutequipment = ["mypy-boto3-lookoutequipment (>=1.42.0,<1.43.0)"] -m2 = ["mypy-boto3-m2 (>=1.42.0,<1.43.0)"] -machinelearning = ["mypy-boto3-machinelearning (>=1.42.0,<1.43.0)"] -macie2 = ["mypy-boto3-macie2 (>=1.42.0,<1.43.0)"] -mailmanager = ["mypy-boto3-mailmanager (>=1.42.0,<1.43.0)"] -managedblockchain = ["mypy-boto3-managedblockchain (>=1.42.0,<1.43.0)"] -managedblockchain-query = ["mypy-boto3-managedblockchain-query (>=1.42.0,<1.43.0)"] -marketplace-agreement = ["mypy-boto3-marketplace-agreement (>=1.42.0,<1.43.0)"] -marketplace-catalog = ["mypy-boto3-marketplace-catalog (>=1.42.0,<1.43.0)"] -marketplace-deployment = ["mypy-boto3-marketplace-deployment (>=1.42.0,<1.43.0)"] -marketplace-entitlement = ["mypy-boto3-marketplace-entitlement (>=1.42.0,<1.43.0)"] -marketplace-reporting = ["mypy-boto3-marketplace-reporting (>=1.42.0,<1.43.0)"] -marketplacecommerceanalytics = ["mypy-boto3-marketplacecommerceanalytics (>=1.42.0,<1.43.0)"] -mediaconnect = ["mypy-boto3-mediaconnect (>=1.42.0,<1.43.0)"] -mediaconvert = ["mypy-boto3-mediaconvert (>=1.42.0,<1.43.0)"] -medialive = ["mypy-boto3-medialive (>=1.42.0,<1.43.0)"] -mediapackage = ["mypy-boto3-mediapackage (>=1.42.0,<1.43.0)"] -mediapackage-vod = ["mypy-boto3-mediapackage-vod (>=1.42.0,<1.43.0)"] -mediapackagev2 = ["mypy-boto3-mediapackagev2 (>=1.42.0,<1.43.0)"] -mediastore = ["mypy-boto3-mediastore (>=1.42.0,<1.43.0)"] -mediastore-data = ["mypy-boto3-mediastore-data (>=1.42.0,<1.43.0)"] -mediatailor = ["mypy-boto3-mediatailor (>=1.42.0,<1.43.0)"] -medical-imaging = ["mypy-boto3-medical-imaging (>=1.42.0,<1.43.0)"] -memorydb = ["mypy-boto3-memorydb (>=1.42.0,<1.43.0)"] -meteringmarketplace = ["mypy-boto3-meteringmarketplace (>=1.42.0,<1.43.0)"] -mgh = ["mypy-boto3-mgh (>=1.42.0,<1.43.0)"] -mgn = ["mypy-boto3-mgn (>=1.42.0,<1.43.0)"] -migration-hub-refactor-spaces = ["mypy-boto3-migration-hub-refactor-spaces (>=1.42.0,<1.43.0)"] -migrationhub-config = ["mypy-boto3-migrationhub-config (>=1.42.0,<1.43.0)"] -migrationhuborchestrator = ["mypy-boto3-migrationhuborchestrator (>=1.42.0,<1.43.0)"] -migrationhubstrategy = ["mypy-boto3-migrationhubstrategy (>=1.42.0,<1.43.0)"] -mpa = ["mypy-boto3-mpa (>=1.42.0,<1.43.0)"] -mq = ["mypy-boto3-mq (>=1.42.0,<1.43.0)"] -mturk = ["mypy-boto3-mturk (>=1.42.0,<1.43.0)"] -mwaa = ["mypy-boto3-mwaa (>=1.42.0,<1.43.0)"] -mwaa-serverless = ["mypy-boto3-mwaa-serverless (>=1.42.0,<1.43.0)"] -neptune = ["mypy-boto3-neptune (>=1.42.0,<1.43.0)"] -neptune-graph = ["mypy-boto3-neptune-graph (>=1.42.0,<1.43.0)"] -neptunedata = ["mypy-boto3-neptunedata (>=1.42.0,<1.43.0)"] -network-firewall = ["mypy-boto3-network-firewall (>=1.42.0,<1.43.0)"] -networkflowmonitor = ["mypy-boto3-networkflowmonitor (>=1.42.0,<1.43.0)"] -networkmanager = ["mypy-boto3-networkmanager (>=1.42.0,<1.43.0)"] -networkmonitor = ["mypy-boto3-networkmonitor (>=1.42.0,<1.43.0)"] -notifications = ["mypy-boto3-notifications (>=1.42.0,<1.43.0)"] -notificationscontacts = ["mypy-boto3-notificationscontacts (>=1.42.0,<1.43.0)"] -nova-act = ["mypy-boto3-nova-act (>=1.42.0,<1.43.0)"] -oam = ["mypy-boto3-oam (>=1.42.0,<1.43.0)"] -observabilityadmin = ["mypy-boto3-observabilityadmin (>=1.42.0,<1.43.0)"] -odb = ["mypy-boto3-odb (>=1.42.0,<1.43.0)"] -omics = ["mypy-boto3-omics (>=1.42.0,<1.43.0)"] -opensearch = ["mypy-boto3-opensearch (>=1.42.0,<1.43.0)"] -opensearchserverless = ["mypy-boto3-opensearchserverless (>=1.42.0,<1.43.0)"] -organizations = ["mypy-boto3-organizations (>=1.42.0,<1.43.0)"] -osis = ["mypy-boto3-osis (>=1.42.0,<1.43.0)"] -outposts = ["mypy-boto3-outposts (>=1.42.0,<1.43.0)"] -panorama = ["mypy-boto3-panorama (>=1.42.0,<1.43.0)"] -partnercentral-account = ["mypy-boto3-partnercentral-account (>=1.42.0,<1.43.0)"] -partnercentral-benefits = ["mypy-boto3-partnercentral-benefits (>=1.42.0,<1.43.0)"] -partnercentral-channel = ["mypy-boto3-partnercentral-channel (>=1.42.0,<1.43.0)"] -partnercentral-selling = ["mypy-boto3-partnercentral-selling (>=1.42.0,<1.43.0)"] -payment-cryptography = ["mypy-boto3-payment-cryptography (>=1.42.0,<1.43.0)"] -payment-cryptography-data = ["mypy-boto3-payment-cryptography-data (>=1.42.0,<1.43.0)"] -pca-connector-ad = ["mypy-boto3-pca-connector-ad (>=1.42.0,<1.43.0)"] -pca-connector-scep = ["mypy-boto3-pca-connector-scep (>=1.42.0,<1.43.0)"] -pcs = ["mypy-boto3-pcs (>=1.42.0,<1.43.0)"] -personalize = ["mypy-boto3-personalize (>=1.42.0,<1.43.0)"] -personalize-events = ["mypy-boto3-personalize-events (>=1.42.0,<1.43.0)"] -personalize-runtime = ["mypy-boto3-personalize-runtime (>=1.42.0,<1.43.0)"] -pi = ["mypy-boto3-pi (>=1.42.0,<1.43.0)"] -pinpoint = ["mypy-boto3-pinpoint (>=1.42.0,<1.43.0)"] -pinpoint-email = ["mypy-boto3-pinpoint-email (>=1.42.0,<1.43.0)"] -pinpoint-sms-voice = ["mypy-boto3-pinpoint-sms-voice (>=1.42.0,<1.43.0)"] -pinpoint-sms-voice-v2 = ["mypy-boto3-pinpoint-sms-voice-v2 (>=1.42.0,<1.43.0)"] -pipes = ["mypy-boto3-pipes (>=1.42.0,<1.43.0)"] -polly = ["mypy-boto3-polly (>=1.42.0,<1.43.0)"] -pricing = ["mypy-boto3-pricing (>=1.42.0,<1.43.0)"] -proton = ["mypy-boto3-proton (>=1.42.0,<1.43.0)"] -qapps = ["mypy-boto3-qapps (>=1.42.0,<1.43.0)"] -qbusiness = ["mypy-boto3-qbusiness (>=1.42.0,<1.43.0)"] -qconnect = ["mypy-boto3-qconnect (>=1.42.0,<1.43.0)"] -quicksight = ["mypy-boto3-quicksight (>=1.42.0,<1.43.0)"] -ram = ["mypy-boto3-ram (>=1.42.0,<1.43.0)"] -rbin = ["mypy-boto3-rbin (>=1.42.0,<1.43.0)"] -rds = ["mypy-boto3-rds (>=1.42.0,<1.43.0)"] -rds-data = ["mypy-boto3-rds-data (>=1.42.0,<1.43.0)"] -redshift = ["mypy-boto3-redshift (>=1.42.0,<1.43.0)"] -redshift-data = ["mypy-boto3-redshift-data (>=1.42.0,<1.43.0)"] -redshift-serverless = ["mypy-boto3-redshift-serverless (>=1.42.0,<1.43.0)"] -rekognition = ["mypy-boto3-rekognition (>=1.42.0,<1.43.0)"] -repostspace = ["mypy-boto3-repostspace (>=1.42.0,<1.43.0)"] -resiliencehub = ["mypy-boto3-resiliencehub (>=1.42.0,<1.43.0)"] -resource-explorer-2 = ["mypy-boto3-resource-explorer-2 (>=1.42.0,<1.43.0)"] -resource-groups = ["mypy-boto3-resource-groups (>=1.42.0,<1.43.0)"] -resourcegroupstaggingapi = ["mypy-boto3-resourcegroupstaggingapi (>=1.42.0,<1.43.0)"] -rolesanywhere = ["mypy-boto3-rolesanywhere (>=1.42.0,<1.43.0)"] -route53 = ["mypy-boto3-route53 (>=1.42.0,<1.43.0)"] -route53-recovery-cluster = ["mypy-boto3-route53-recovery-cluster (>=1.42.0,<1.43.0)"] -route53-recovery-control-config = ["mypy-boto3-route53-recovery-control-config (>=1.42.0,<1.43.0)"] -route53-recovery-readiness = ["mypy-boto3-route53-recovery-readiness (>=1.42.0,<1.43.0)"] -route53domains = ["mypy-boto3-route53domains (>=1.42.0,<1.43.0)"] -route53globalresolver = ["mypy-boto3-route53globalresolver (>=1.42.0,<1.43.0)"] -route53profiles = ["mypy-boto3-route53profiles (>=1.42.0,<1.43.0)"] -route53resolver = ["mypy-boto3-route53resolver (>=1.42.0,<1.43.0)"] -rtbfabric = ["mypy-boto3-rtbfabric (>=1.42.0,<1.43.0)"] -rum = ["mypy-boto3-rum (>=1.42.0,<1.43.0)"] -s3 = ["mypy-boto3-s3 (>=1.42.0,<1.43.0)"] -s3control = ["mypy-boto3-s3control (>=1.42.0,<1.43.0)"] -s3outposts = ["mypy-boto3-s3outposts (>=1.42.0,<1.43.0)"] -s3tables = ["mypy-boto3-s3tables (>=1.42.0,<1.43.0)"] -s3vectors = ["mypy-boto3-s3vectors (>=1.42.0,<1.43.0)"] -sagemaker = ["mypy-boto3-sagemaker (>=1.42.0,<1.43.0)"] -sagemaker-a2i-runtime = ["mypy-boto3-sagemaker-a2i-runtime (>=1.42.0,<1.43.0)"] -sagemaker-edge = ["mypy-boto3-sagemaker-edge (>=1.42.0,<1.43.0)"] -sagemaker-featurestore-runtime = ["mypy-boto3-sagemaker-featurestore-runtime (>=1.42.0,<1.43.0)"] -sagemaker-geospatial = ["mypy-boto3-sagemaker-geospatial (>=1.42.0,<1.43.0)"] -sagemaker-metrics = ["mypy-boto3-sagemaker-metrics (>=1.42.0,<1.43.0)"] -sagemaker-runtime = ["mypy-boto3-sagemaker-runtime (>=1.42.0,<1.43.0)"] -savingsplans = ["mypy-boto3-savingsplans (>=1.42.0,<1.43.0)"] -scheduler = ["mypy-boto3-scheduler (>=1.42.0,<1.43.0)"] -schemas = ["mypy-boto3-schemas (>=1.42.0,<1.43.0)"] -sdb = ["mypy-boto3-sdb (>=1.42.0,<1.43.0)"] -secretsmanager = ["mypy-boto3-secretsmanager (>=1.42.0,<1.43.0)"] -security-ir = ["mypy-boto3-security-ir (>=1.42.0,<1.43.0)"] -securityagent = ["mypy-boto3-securityagent (>=1.42.0,<1.43.0)"] -securityhub = ["mypy-boto3-securityhub (>=1.42.0,<1.43.0)"] -securitylake = ["mypy-boto3-securitylake (>=1.42.0,<1.43.0)"] -serverlessrepo = ["mypy-boto3-serverlessrepo (>=1.42.0,<1.43.0)"] -service-quotas = ["mypy-boto3-service-quotas (>=1.42.0,<1.43.0)"] -servicecatalog = ["mypy-boto3-servicecatalog (>=1.42.0,<1.43.0)"] -servicecatalog-appregistry = ["mypy-boto3-servicecatalog-appregistry (>=1.42.0,<1.43.0)"] -servicediscovery = ["mypy-boto3-servicediscovery (>=1.42.0,<1.43.0)"] -ses = ["mypy-boto3-ses (>=1.42.0,<1.43.0)"] -sesv2 = ["mypy-boto3-sesv2 (>=1.42.0,<1.43.0)"] -shield = ["mypy-boto3-shield (>=1.42.0,<1.43.0)"] -signer = ["mypy-boto3-signer (>=1.42.0,<1.43.0)"] -signer-data = ["mypy-boto3-signer-data (>=1.42.0,<1.43.0)"] -signin = ["mypy-boto3-signin (>=1.42.0,<1.43.0)"] -simpledbv2 = ["mypy-boto3-simpledbv2 (>=1.42.0,<1.43.0)"] -simspaceweaver = ["mypy-boto3-simspaceweaver (>=1.42.0,<1.43.0)"] -snow-device-management = ["mypy-boto3-snow-device-management (>=1.42.0,<1.43.0)"] -snowball = ["mypy-boto3-snowball (>=1.42.0,<1.43.0)"] -sns = ["mypy-boto3-sns (>=1.42.0,<1.43.0)"] -socialmessaging = ["mypy-boto3-socialmessaging (>=1.42.0,<1.43.0)"] -sqs = ["mypy-boto3-sqs (>=1.42.0,<1.43.0)"] -ssm = ["mypy-boto3-ssm (>=1.42.0,<1.43.0)"] -ssm-contacts = ["mypy-boto3-ssm-contacts (>=1.42.0,<1.43.0)"] -ssm-guiconnect = ["mypy-boto3-ssm-guiconnect (>=1.42.0,<1.43.0)"] -ssm-incidents = ["mypy-boto3-ssm-incidents (>=1.42.0,<1.43.0)"] -ssm-quicksetup = ["mypy-boto3-ssm-quicksetup (>=1.42.0,<1.43.0)"] -ssm-sap = ["mypy-boto3-ssm-sap (>=1.42.0,<1.43.0)"] -sso = ["mypy-boto3-sso (>=1.42.0,<1.43.0)"] -sso-admin = ["mypy-boto3-sso-admin (>=1.42.0,<1.43.0)"] -sso-oidc = ["mypy-boto3-sso-oidc (>=1.42.0,<1.43.0)"] -stepfunctions = ["mypy-boto3-stepfunctions (>=1.42.0,<1.43.0)"] -storagegateway = ["mypy-boto3-storagegateway (>=1.42.0,<1.43.0)"] -sts = ["mypy-boto3-sts (>=1.42.0,<1.43.0)"] -supplychain = ["mypy-boto3-supplychain (>=1.42.0,<1.43.0)"] -support = ["mypy-boto3-support (>=1.42.0,<1.43.0)"] -support-app = ["mypy-boto3-support-app (>=1.42.0,<1.43.0)"] -sustainability = ["mypy-boto3-sustainability (>=1.42.0,<1.43.0)"] -swf = ["mypy-boto3-swf (>=1.42.0,<1.43.0)"] -synthetics = ["mypy-boto3-synthetics (>=1.42.0,<1.43.0)"] -taxsettings = ["mypy-boto3-taxsettings (>=1.42.0,<1.43.0)"] -textract = ["mypy-boto3-textract (>=1.42.0,<1.43.0)"] -timestream-influxdb = ["mypy-boto3-timestream-influxdb (>=1.42.0,<1.43.0)"] -timestream-query = ["mypy-boto3-timestream-query (>=1.42.0,<1.43.0)"] -timestream-write = ["mypy-boto3-timestream-write (>=1.42.0,<1.43.0)"] -tnb = ["mypy-boto3-tnb (>=1.42.0,<1.43.0)"] -transcribe = ["mypy-boto3-transcribe (>=1.42.0,<1.43.0)"] -transfer = ["mypy-boto3-transfer (>=1.42.0,<1.43.0)"] -translate = ["mypy-boto3-translate (>=1.42.0,<1.43.0)"] -trustedadvisor = ["mypy-boto3-trustedadvisor (>=1.42.0,<1.43.0)"] -uxc = ["mypy-boto3-uxc (>=1.42.0,<1.43.0)"] -verifiedpermissions = ["mypy-boto3-verifiedpermissions (>=1.42.0,<1.43.0)"] -voice-id = ["mypy-boto3-voice-id (>=1.42.0,<1.43.0)"] -vpc-lattice = ["mypy-boto3-vpc-lattice (>=1.42.0,<1.43.0)"] -waf = ["mypy-boto3-waf (>=1.42.0,<1.43.0)"] -waf-regional = ["mypy-boto3-waf-regional (>=1.42.0,<1.43.0)"] -wafv2 = ["mypy-boto3-wafv2 (>=1.42.0,<1.43.0)"] -wellarchitected = ["mypy-boto3-wellarchitected (>=1.42.0,<1.43.0)"] -wickr = ["mypy-boto3-wickr (>=1.42.0,<1.43.0)"] -wisdom = ["mypy-boto3-wisdom (>=1.42.0,<1.43.0)"] -workdocs = ["mypy-boto3-workdocs (>=1.42.0,<1.43.0)"] -workmail = ["mypy-boto3-workmail (>=1.42.0,<1.43.0)"] -workmailmessageflow = ["mypy-boto3-workmailmessageflow (>=1.42.0,<1.43.0)"] -workspaces = ["mypy-boto3-workspaces (>=1.42.0,<1.43.0)"] -workspaces-instances = ["mypy-boto3-workspaces-instances (>=1.42.0,<1.43.0)"] -workspaces-thin-client = ["mypy-boto3-workspaces-thin-client (>=1.42.0,<1.43.0)"] -workspaces-web = ["mypy-boto3-workspaces-web (>=1.42.0,<1.43.0)"] -xray = ["mypy-boto3-xray (>=1.42.0,<1.43.0)"] +accessanalyzer = ["mypy-boto3-accessanalyzer (>=1.43.0,<1.44.0)"] +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-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)"] +amplifyuibuilder = ["mypy-boto3-amplifyuibuilder (>=1.43.0,<1.44.0)"] +apigateway = ["mypy-boto3-apigateway (>=1.43.0,<1.44.0)"] +apigatewaymanagementapi = ["mypy-boto3-apigatewaymanagementapi (>=1.43.0,<1.44.0)"] +apigatewayv2 = ["mypy-boto3-apigatewayv2 (>=1.43.0,<1.44.0)"] +appconfig = ["mypy-boto3-appconfig (>=1.43.0,<1.44.0)"] +appconfigdata = ["mypy-boto3-appconfigdata (>=1.43.0,<1.44.0)"] +appfabric = ["mypy-boto3-appfabric (>=1.43.0,<1.44.0)"] +appflow = ["mypy-boto3-appflow (>=1.43.0,<1.44.0)"] +appintegrations = ["mypy-boto3-appintegrations (>=1.43.0,<1.44.0)"] +application-autoscaling = ["mypy-boto3-application-autoscaling (>=1.43.0,<1.44.0)"] +application-insights = ["mypy-boto3-application-insights (>=1.43.0,<1.44.0)"] +application-signals = ["mypy-boto3-application-signals (>=1.43.0,<1.44.0)"] +applicationcostprofiler = ["mypy-boto3-applicationcostprofiler (>=1.43.0,<1.44.0)"] +appmesh = ["mypy-boto3-appmesh (>=1.43.0,<1.44.0)"] +apprunner = ["mypy-boto3-apprunner (>=1.43.0,<1.44.0)"] +appstream = ["mypy-boto3-appstream (>=1.43.0,<1.44.0)"] +appsync = ["mypy-boto3-appsync (>=1.43.0,<1.44.0)"] +arc-region-switch = ["mypy-boto3-arc-region-switch (>=1.43.0,<1.44.0)"] +arc-zonal-shift = ["mypy-boto3-arc-zonal-shift (>=1.43.0,<1.44.0)"] +artifact = ["mypy-boto3-artifact (>=1.43.0,<1.44.0)"] +athena = ["mypy-boto3-athena (>=1.43.0,<1.44.0)"] +auditmanager = ["mypy-boto3-auditmanager (>=1.43.0,<1.44.0)"] +autoscaling = ["mypy-boto3-autoscaling (>=1.43.0,<1.44.0)"] +autoscaling-plans = ["mypy-boto3-autoscaling-plans (>=1.43.0,<1.44.0)"] +b2bi = ["mypy-boto3-b2bi (>=1.43.0,<1.44.0)"] +backup = ["mypy-boto3-backup (>=1.43.0,<1.44.0)"] +backup-gateway = ["mypy-boto3-backup-gateway (>=1.43.0,<1.44.0)"] +backupsearch = ["mypy-boto3-backupsearch (>=1.43.0,<1.44.0)"] +batch = ["mypy-boto3-batch (>=1.43.0,<1.44.0)"] +bcm-dashboards = ["mypy-boto3-bcm-dashboards (>=1.43.0,<1.44.0)"] +bcm-data-exports = ["mypy-boto3-bcm-data-exports (>=1.43.0,<1.44.0)"] +bcm-pricing-calculator = ["mypy-boto3-bcm-pricing-calculator (>=1.43.0,<1.44.0)"] +bcm-recommended-actions = ["mypy-boto3-bcm-recommended-actions (>=1.43.0,<1.44.0)"] +bedrock = ["mypy-boto3-bedrock (>=1.43.0,<1.44.0)"] +bedrock-agent = ["mypy-boto3-bedrock-agent (>=1.43.0,<1.44.0)"] +bedrock-agent-runtime = ["mypy-boto3-bedrock-agent-runtime (>=1.43.0,<1.44.0)"] +bedrock-agentcore = ["mypy-boto3-bedrock-agentcore (>=1.43.0,<1.44.0)"] +bedrock-agentcore-control = ["mypy-boto3-bedrock-agentcore-control (>=1.43.0,<1.44.0)"] +bedrock-data-automation = ["mypy-boto3-bedrock-data-automation (>=1.43.0,<1.44.0)"] +bedrock-data-automation-runtime = ["mypy-boto3-bedrock-data-automation-runtime (>=1.43.0,<1.44.0)"] +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.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)"] +chatbot = ["mypy-boto3-chatbot (>=1.43.0,<1.44.0)"] +chime = ["mypy-boto3-chime (>=1.43.0,<1.44.0)"] +chime-sdk-identity = ["mypy-boto3-chime-sdk-identity (>=1.43.0,<1.44.0)"] +chime-sdk-media-pipelines = ["mypy-boto3-chime-sdk-media-pipelines (>=1.43.0,<1.44.0)"] +chime-sdk-meetings = ["mypy-boto3-chime-sdk-meetings (>=1.43.0,<1.44.0)"] +chime-sdk-messaging = ["mypy-boto3-chime-sdk-messaging (>=1.43.0,<1.44.0)"] +chime-sdk-voice = ["mypy-boto3-chime-sdk-voice (>=1.43.0,<1.44.0)"] +cleanrooms = ["mypy-boto3-cleanrooms (>=1.43.0,<1.44.0)"] +cleanroomsml = ["mypy-boto3-cleanroomsml (>=1.43.0,<1.44.0)"] +cloud9 = ["mypy-boto3-cloud9 (>=1.43.0,<1.44.0)"] +cloudcontrol = ["mypy-boto3-cloudcontrol (>=1.43.0,<1.44.0)"] +clouddirectory = ["mypy-boto3-clouddirectory (>=1.43.0,<1.44.0)"] +cloudformation = ["mypy-boto3-cloudformation (>=1.43.0,<1.44.0)"] +cloudfront = ["mypy-boto3-cloudfront (>=1.43.0,<1.44.0)"] +cloudfront-keyvaluestore = ["mypy-boto3-cloudfront-keyvaluestore (>=1.43.0,<1.44.0)"] +cloudhsm = ["mypy-boto3-cloudhsm (>=1.43.0,<1.44.0)"] +cloudhsmv2 = ["mypy-boto3-cloudhsmv2 (>=1.43.0,<1.44.0)"] +cloudsearch = ["mypy-boto3-cloudsearch (>=1.43.0,<1.44.0)"] +cloudsearchdomain = ["mypy-boto3-cloudsearchdomain (>=1.43.0,<1.44.0)"] +cloudtrail = ["mypy-boto3-cloudtrail (>=1.43.0,<1.44.0)"] +cloudtrail-data = ["mypy-boto3-cloudtrail-data (>=1.43.0,<1.44.0)"] +cloudwatch = ["mypy-boto3-cloudwatch (>=1.43.0,<1.44.0)"] +codeartifact = ["mypy-boto3-codeartifact (>=1.43.0,<1.44.0)"] +codebuild = ["mypy-boto3-codebuild (>=1.43.0,<1.44.0)"] +codecatalyst = ["mypy-boto3-codecatalyst (>=1.43.0,<1.44.0)"] +codecommit = ["mypy-boto3-codecommit (>=1.43.0,<1.44.0)"] +codeconnections = ["mypy-boto3-codeconnections (>=1.43.0,<1.44.0)"] +codedeploy = ["mypy-boto3-codedeploy (>=1.43.0,<1.44.0)"] +codeguru-reviewer = ["mypy-boto3-codeguru-reviewer (>=1.43.0,<1.44.0)"] +codeguru-security = ["mypy-boto3-codeguru-security (>=1.43.0,<1.44.0)"] +codeguruprofiler = ["mypy-boto3-codeguruprofiler (>=1.43.0,<1.44.0)"] +codepipeline = ["mypy-boto3-codepipeline (>=1.43.0,<1.44.0)"] +codestar-connections = ["mypy-boto3-codestar-connections (>=1.43.0,<1.44.0)"] +codestar-notifications = ["mypy-boto3-codestar-notifications (>=1.43.0,<1.44.0)"] +cognito-identity = ["mypy-boto3-cognito-identity (>=1.43.0,<1.44.0)"] +cognito-idp = ["mypy-boto3-cognito-idp (>=1.43.0,<1.44.0)"] +cognito-sync = ["mypy-boto3-cognito-sync (>=1.43.0,<1.44.0)"] +comprehend = ["mypy-boto3-comprehend (>=1.43.0,<1.44.0)"] +comprehendmedical = ["mypy-boto3-comprehendmedical (>=1.43.0,<1.44.0)"] +compute-optimizer = ["mypy-boto3-compute-optimizer (>=1.43.0,<1.44.0)"] +compute-optimizer-automation = ["mypy-boto3-compute-optimizer-automation (>=1.43.0,<1.44.0)"] +config = ["mypy-boto3-config (>=1.43.0,<1.44.0)"] +connect = ["mypy-boto3-connect (>=1.43.0,<1.44.0)"] +connect-contact-lens = ["mypy-boto3-connect-contact-lens (>=1.43.0,<1.44.0)"] +connectcampaigns = ["mypy-boto3-connectcampaigns (>=1.43.0,<1.44.0)"] +connectcampaignsv2 = ["mypy-boto3-connectcampaignsv2 (>=1.43.0,<1.44.0)"] +connectcases = ["mypy-boto3-connectcases (>=1.43.0,<1.44.0)"] +connecthealth = ["mypy-boto3-connecthealth (>=1.43.0,<1.44.0)"] +connectparticipant = ["mypy-boto3-connectparticipant (>=1.43.0,<1.44.0)"] +controlcatalog = ["mypy-boto3-controlcatalog (>=1.43.0,<1.44.0)"] +controltower = ["mypy-boto3-controltower (>=1.43.0,<1.44.0)"] +cost-optimization-hub = ["mypy-boto3-cost-optimization-hub (>=1.43.0,<1.44.0)"] +cur = ["mypy-boto3-cur (>=1.43.0,<1.44.0)"] +customer-profiles = ["mypy-boto3-customer-profiles (>=1.43.0,<1.44.0)"] +databrew = ["mypy-boto3-databrew (>=1.43.0,<1.44.0)"] +dataexchange = ["mypy-boto3-dataexchange (>=1.43.0,<1.44.0)"] +datapipeline = ["mypy-boto3-datapipeline (>=1.43.0,<1.44.0)"] +datasync = ["mypy-boto3-datasync (>=1.43.0,<1.44.0)"] +datazone = ["mypy-boto3-datazone (>=1.43.0,<1.44.0)"] +dax = ["mypy-boto3-dax (>=1.43.0,<1.44.0)"] +deadline = ["mypy-boto3-deadline (>=1.43.0,<1.44.0)"] +detective = ["mypy-boto3-detective (>=1.43.0,<1.44.0)"] +devicefarm = ["mypy-boto3-devicefarm (>=1.43.0,<1.44.0)"] +devops-agent = ["mypy-boto3-devops-agent (>=1.43.0,<1.44.0)"] +devops-guru = ["mypy-boto3-devops-guru (>=1.43.0,<1.44.0)"] +directconnect = ["mypy-boto3-directconnect (>=1.43.0,<1.44.0)"] +discovery = ["mypy-boto3-discovery (>=1.43.0,<1.44.0)"] +dlm = ["mypy-boto3-dlm (>=1.43.0,<1.44.0)"] +dms = ["mypy-boto3-dms (>=1.43.0,<1.44.0)"] +docdb = ["mypy-boto3-docdb (>=1.43.0,<1.44.0)"] +docdb-elastic = ["mypy-boto3-docdb-elastic (>=1.43.0,<1.44.0)"] +drs = ["mypy-boto3-drs (>=1.43.0,<1.44.0)"] +ds = ["mypy-boto3-ds (>=1.43.0,<1.44.0)"] +ds-data = ["mypy-boto3-ds-data (>=1.43.0,<1.44.0)"] +dsql = ["mypy-boto3-dsql (>=1.43.0,<1.44.0)"] +dynamodb = ["mypy-boto3-dynamodb (>=1.43.0,<1.44.0)"] +dynamodbstreams = ["mypy-boto3-dynamodbstreams (>=1.43.0,<1.44.0)"] +ebs = ["mypy-boto3-ebs (>=1.43.0,<1.44.0)"] +ec2 = ["mypy-boto3-ec2 (>=1.43.0,<1.44.0)"] +ec2-instance-connect = ["mypy-boto3-ec2-instance-connect (>=1.43.0,<1.44.0)"] +ecr = ["mypy-boto3-ecr (>=1.43.0,<1.44.0)"] +ecr-public = ["mypy-boto3-ecr-public (>=1.43.0,<1.44.0)"] +ecs = ["mypy-boto3-ecs (>=1.43.0,<1.44.0)"] +efs = ["mypy-boto3-efs (>=1.43.0,<1.44.0)"] +eks = ["mypy-boto3-eks (>=1.43.0,<1.44.0)"] +eks-auth = ["mypy-boto3-eks-auth (>=1.43.0,<1.44.0)"] +elasticache = ["mypy-boto3-elasticache (>=1.43.0,<1.44.0)"] +elasticbeanstalk = ["mypy-boto3-elasticbeanstalk (>=1.43.0,<1.44.0)"] +elb = ["mypy-boto3-elb (>=1.43.0,<1.44.0)"] +elbv2 = ["mypy-boto3-elbv2 (>=1.43.0,<1.44.0)"] +elementalinference = ["mypy-boto3-elementalinference (>=1.43.0,<1.44.0)"] +emr = ["mypy-boto3-emr (>=1.43.0,<1.44.0)"] +emr-containers = ["mypy-boto3-emr-containers (>=1.43.0,<1.44.0)"] +emr-serverless = ["mypy-boto3-emr-serverless (>=1.43.0,<1.44.0)"] +entityresolution = ["mypy-boto3-entityresolution (>=1.43.0,<1.44.0)"] +es = ["mypy-boto3-es (>=1.43.0,<1.44.0)"] +essential = ["mypy-boto3-cloudformation (>=1.43.0,<1.44.0)", "mypy-boto3-dynamodb (>=1.43.0,<1.44.0)", "mypy-boto3-ec2 (>=1.43.0,<1.44.0)", "mypy-boto3-lambda (>=1.43.0,<1.44.0)", "mypy-boto3-rds (>=1.43.0,<1.44.0)", "mypy-boto3-s3 (>=1.43.0,<1.44.0)", "mypy-boto3-sqs (>=1.43.0,<1.44.0)"] +events = ["mypy-boto3-events (>=1.43.0,<1.44.0)"] +evs = ["mypy-boto3-evs (>=1.43.0,<1.44.0)"] +finspace = ["mypy-boto3-finspace (>=1.43.0,<1.44.0)"] +finspace-data = ["mypy-boto3-finspace-data (>=1.43.0,<1.44.0)"] +firehose = ["mypy-boto3-firehose (>=1.43.0,<1.44.0)"] +fis = ["mypy-boto3-fis (>=1.43.0,<1.44.0)"] +fms = ["mypy-boto3-fms (>=1.43.0,<1.44.0)"] +forecast = ["mypy-boto3-forecast (>=1.43.0,<1.44.0)"] +forecastquery = ["mypy-boto3-forecastquery (>=1.43.0,<1.44.0)"] +frauddetector = ["mypy-boto3-frauddetector (>=1.43.0,<1.44.0)"] +freetier = ["mypy-boto3-freetier (>=1.43.0,<1.44.0)"] +fsx = ["mypy-boto3-fsx (>=1.43.0,<1.44.0)"] +full = ["boto3-stubs-full (>=1.43.0,<1.44.0)"] +gamelift = ["mypy-boto3-gamelift (>=1.43.0,<1.44.0)"] +gameliftstreams = ["mypy-boto3-gameliftstreams (>=1.43.0,<1.44.0)"] +geo-maps = ["mypy-boto3-geo-maps (>=1.43.0,<1.44.0)"] +geo-places = ["mypy-boto3-geo-places (>=1.43.0,<1.44.0)"] +geo-routes = ["mypy-boto3-geo-routes (>=1.43.0,<1.44.0)"] +glacier = ["mypy-boto3-glacier (>=1.43.0,<1.44.0)"] +globalaccelerator = ["mypy-boto3-globalaccelerator (>=1.43.0,<1.44.0)"] +glue = ["mypy-boto3-glue (>=1.43.0,<1.44.0)"] +grafana = ["mypy-boto3-grafana (>=1.43.0,<1.44.0)"] +greengrass = ["mypy-boto3-greengrass (>=1.43.0,<1.44.0)"] +greengrassv2 = ["mypy-boto3-greengrassv2 (>=1.43.0,<1.44.0)"] +groundstation = ["mypy-boto3-groundstation (>=1.43.0,<1.44.0)"] +guardduty = ["mypy-boto3-guardduty (>=1.43.0,<1.44.0)"] +health = ["mypy-boto3-health (>=1.43.0,<1.44.0)"] +healthlake = ["mypy-boto3-healthlake (>=1.43.0,<1.44.0)"] +iam = ["mypy-boto3-iam (>=1.43.0,<1.44.0)"] +identitystore = ["mypy-boto3-identitystore (>=1.43.0,<1.44.0)"] +imagebuilder = ["mypy-boto3-imagebuilder (>=1.43.0,<1.44.0)"] +importexport = ["mypy-boto3-importexport (>=1.43.0,<1.44.0)"] +inspector = ["mypy-boto3-inspector (>=1.43.0,<1.44.0)"] +inspector-scan = ["mypy-boto3-inspector-scan (>=1.43.0,<1.44.0)"] +inspector2 = ["mypy-boto3-inspector2 (>=1.43.0,<1.44.0)"] +interconnect = ["mypy-boto3-interconnect (>=1.43.0,<1.44.0)"] +internetmonitor = ["mypy-boto3-internetmonitor (>=1.43.0,<1.44.0)"] +invoicing = ["mypy-boto3-invoicing (>=1.43.0,<1.44.0)"] +iot = ["mypy-boto3-iot (>=1.43.0,<1.44.0)"] +iot-data = ["mypy-boto3-iot-data (>=1.43.0,<1.44.0)"] +iot-jobs-data = ["mypy-boto3-iot-jobs-data (>=1.43.0,<1.44.0)"] +iot-managed-integrations = ["mypy-boto3-iot-managed-integrations (>=1.43.0,<1.44.0)"] +iotdeviceadvisor = ["mypy-boto3-iotdeviceadvisor (>=1.43.0,<1.44.0)"] +iotevents = ["mypy-boto3-iotevents (>=1.43.0,<1.44.0)"] +iotevents-data = ["mypy-boto3-iotevents-data (>=1.43.0,<1.44.0)"] +iotfleetwise = ["mypy-boto3-iotfleetwise (>=1.43.0,<1.44.0)"] +iotsecuretunneling = ["mypy-boto3-iotsecuretunneling (>=1.43.0,<1.44.0)"] +iotsitewise = ["mypy-boto3-iotsitewise (>=1.43.0,<1.44.0)"] +iotthingsgraph = ["mypy-boto3-iotthingsgraph (>=1.43.0,<1.44.0)"] +iottwinmaker = ["mypy-boto3-iottwinmaker (>=1.43.0,<1.44.0)"] +iotwireless = ["mypy-boto3-iotwireless (>=1.43.0,<1.44.0)"] +ivs = ["mypy-boto3-ivs (>=1.43.0,<1.44.0)"] +ivs-realtime = ["mypy-boto3-ivs-realtime (>=1.43.0,<1.44.0)"] +ivschat = ["mypy-boto3-ivschat (>=1.43.0,<1.44.0)"] +kafka = ["mypy-boto3-kafka (>=1.43.0,<1.44.0)"] +kafkaconnect = ["mypy-boto3-kafkaconnect (>=1.43.0,<1.44.0)"] +kendra = ["mypy-boto3-kendra (>=1.43.0,<1.44.0)"] +kendra-ranking = ["mypy-boto3-kendra-ranking (>=1.43.0,<1.44.0)"] +keyspaces = ["mypy-boto3-keyspaces (>=1.43.0,<1.44.0)"] +keyspacesstreams = ["mypy-boto3-keyspacesstreams (>=1.43.0,<1.44.0)"] +kinesis = ["mypy-boto3-kinesis (>=1.43.0,<1.44.0)"] +kinesis-video-archived-media = ["mypy-boto3-kinesis-video-archived-media (>=1.43.0,<1.44.0)"] +kinesis-video-media = ["mypy-boto3-kinesis-video-media (>=1.43.0,<1.44.0)"] +kinesis-video-signaling = ["mypy-boto3-kinesis-video-signaling (>=1.43.0,<1.44.0)"] +kinesis-video-webrtc-storage = ["mypy-boto3-kinesis-video-webrtc-storage (>=1.43.0,<1.44.0)"] +kinesisanalytics = ["mypy-boto3-kinesisanalytics (>=1.43.0,<1.44.0)"] +kinesisanalyticsv2 = ["mypy-boto3-kinesisanalyticsv2 (>=1.43.0,<1.44.0)"] +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)"] +lexv2-models = ["mypy-boto3-lexv2-models (>=1.43.0,<1.44.0)"] +lexv2-runtime = ["mypy-boto3-lexv2-runtime (>=1.43.0,<1.44.0)"] +license-manager = ["mypy-boto3-license-manager (>=1.43.0,<1.44.0)"] +license-manager-linux-subscriptions = ["mypy-boto3-license-manager-linux-subscriptions (>=1.43.0,<1.44.0)"] +license-manager-user-subscriptions = ["mypy-boto3-license-manager-user-subscriptions (>=1.43.0,<1.44.0)"] +lightsail = ["mypy-boto3-lightsail (>=1.43.0,<1.44.0)"] +location = ["mypy-boto3-location (>=1.43.0,<1.44.0)"] +logs = ["mypy-boto3-logs (>=1.43.0,<1.44.0)"] +lookoutequipment = ["mypy-boto3-lookoutequipment (>=1.43.0,<1.44.0)"] +m2 = ["mypy-boto3-m2 (>=1.43.0,<1.44.0)"] +machinelearning = ["mypy-boto3-machinelearning (>=1.43.0,<1.44.0)"] +macie2 = ["mypy-boto3-macie2 (>=1.43.0,<1.44.0)"] +mailmanager = ["mypy-boto3-mailmanager (>=1.43.0,<1.44.0)"] +managedblockchain = ["mypy-boto3-managedblockchain (>=1.43.0,<1.44.0)"] +managedblockchain-query = ["mypy-boto3-managedblockchain-query (>=1.43.0,<1.44.0)"] +marketplace-agreement = ["mypy-boto3-marketplace-agreement (>=1.43.0,<1.44.0)"] +marketplace-catalog = ["mypy-boto3-marketplace-catalog (>=1.43.0,<1.44.0)"] +marketplace-deployment = ["mypy-boto3-marketplace-deployment (>=1.43.0,<1.44.0)"] +marketplace-discovery = ["mypy-boto3-marketplace-discovery (>=1.43.0,<1.44.0)"] +marketplace-entitlement = ["mypy-boto3-marketplace-entitlement (>=1.43.0,<1.44.0)"] +marketplace-reporting = ["mypy-boto3-marketplace-reporting (>=1.43.0,<1.44.0)"] +marketplacecommerceanalytics = ["mypy-boto3-marketplacecommerceanalytics (>=1.43.0,<1.44.0)"] +mediaconnect = ["mypy-boto3-mediaconnect (>=1.43.0,<1.44.0)"] +mediaconvert = ["mypy-boto3-mediaconvert (>=1.43.0,<1.44.0)"] +medialive = ["mypy-boto3-medialive (>=1.43.0,<1.44.0)"] +mediapackage = ["mypy-boto3-mediapackage (>=1.43.0,<1.44.0)"] +mediapackage-vod = ["mypy-boto3-mediapackage-vod (>=1.43.0,<1.44.0)"] +mediapackagev2 = ["mypy-boto3-mediapackagev2 (>=1.43.0,<1.44.0)"] +mediastore = ["mypy-boto3-mediastore (>=1.43.0,<1.44.0)"] +mediastore-data = ["mypy-boto3-mediastore-data (>=1.43.0,<1.44.0)"] +mediatailor = ["mypy-boto3-mediatailor (>=1.43.0,<1.44.0)"] +medical-imaging = ["mypy-boto3-medical-imaging (>=1.43.0,<1.44.0)"] +memorydb = ["mypy-boto3-memorydb (>=1.43.0,<1.44.0)"] +meteringmarketplace = ["mypy-boto3-meteringmarketplace (>=1.43.0,<1.44.0)"] +mgh = ["mypy-boto3-mgh (>=1.43.0,<1.44.0)"] +mgn = ["mypy-boto3-mgn (>=1.43.0,<1.44.0)"] +migration-hub-refactor-spaces = ["mypy-boto3-migration-hub-refactor-spaces (>=1.43.0,<1.44.0)"] +migrationhub-config = ["mypy-boto3-migrationhub-config (>=1.43.0,<1.44.0)"] +migrationhuborchestrator = ["mypy-boto3-migrationhuborchestrator (>=1.43.0,<1.44.0)"] +migrationhubstrategy = ["mypy-boto3-migrationhubstrategy (>=1.43.0,<1.44.0)"] +mpa = ["mypy-boto3-mpa (>=1.43.0,<1.44.0)"] +mq = ["mypy-boto3-mq (>=1.43.0,<1.44.0)"] +mturk = ["mypy-boto3-mturk (>=1.43.0,<1.44.0)"] +mwaa = ["mypy-boto3-mwaa (>=1.43.0,<1.44.0)"] +mwaa-serverless = ["mypy-boto3-mwaa-serverless (>=1.43.0,<1.44.0)"] +neptune = ["mypy-boto3-neptune (>=1.43.0,<1.44.0)"] +neptune-graph = ["mypy-boto3-neptune-graph (>=1.43.0,<1.44.0)"] +neptunedata = ["mypy-boto3-neptunedata (>=1.43.0,<1.44.0)"] +network-firewall = ["mypy-boto3-network-firewall (>=1.43.0,<1.44.0)"] +networkflowmonitor = ["mypy-boto3-networkflowmonitor (>=1.43.0,<1.44.0)"] +networkmanager = ["mypy-boto3-networkmanager (>=1.43.0,<1.44.0)"] +networkmonitor = ["mypy-boto3-networkmonitor (>=1.43.0,<1.44.0)"] +notifications = ["mypy-boto3-notifications (>=1.43.0,<1.44.0)"] +notificationscontacts = ["mypy-boto3-notificationscontacts (>=1.43.0,<1.44.0)"] +nova-act = ["mypy-boto3-nova-act (>=1.43.0,<1.44.0)"] +oam = ["mypy-boto3-oam (>=1.43.0,<1.44.0)"] +observabilityadmin = ["mypy-boto3-observabilityadmin (>=1.43.0,<1.44.0)"] +odb = ["mypy-boto3-odb (>=1.43.0,<1.44.0)"] +omics = ["mypy-boto3-omics (>=1.43.0,<1.44.0)"] +opensearch = ["mypy-boto3-opensearch (>=1.43.0,<1.44.0)"] +opensearchserverless = ["mypy-boto3-opensearchserverless (>=1.43.0,<1.44.0)"] +organizations = ["mypy-boto3-organizations (>=1.43.0,<1.44.0)"] +osis = ["mypy-boto3-osis (>=1.43.0,<1.44.0)"] +outposts = ["mypy-boto3-outposts (>=1.43.0,<1.44.0)"] +panorama = ["mypy-boto3-panorama (>=1.43.0,<1.44.0)"] +partnercentral-account = ["mypy-boto3-partnercentral-account (>=1.43.0,<1.44.0)"] +partnercentral-benefits = ["mypy-boto3-partnercentral-benefits (>=1.43.0,<1.44.0)"] +partnercentral-channel = ["mypy-boto3-partnercentral-channel (>=1.43.0,<1.44.0)"] +partnercentral-selling = ["mypy-boto3-partnercentral-selling (>=1.43.0,<1.44.0)"] +payment-cryptography = ["mypy-boto3-payment-cryptography (>=1.43.0,<1.44.0)"] +payment-cryptography-data = ["mypy-boto3-payment-cryptography-data (>=1.43.0,<1.44.0)"] +pca-connector-ad = ["mypy-boto3-pca-connector-ad (>=1.43.0,<1.44.0)"] +pca-connector-scep = ["mypy-boto3-pca-connector-scep (>=1.43.0,<1.44.0)"] +pcs = ["mypy-boto3-pcs (>=1.43.0,<1.44.0)"] +personalize = ["mypy-boto3-personalize (>=1.43.0,<1.44.0)"] +personalize-events = ["mypy-boto3-personalize-events (>=1.43.0,<1.44.0)"] +personalize-runtime = ["mypy-boto3-personalize-runtime (>=1.43.0,<1.44.0)"] +pi = ["mypy-boto3-pi (>=1.43.0,<1.44.0)"] +pinpoint = ["mypy-boto3-pinpoint (>=1.43.0,<1.44.0)"] +pinpoint-email = ["mypy-boto3-pinpoint-email (>=1.43.0,<1.44.0)"] +pinpoint-sms-voice = ["mypy-boto3-pinpoint-sms-voice (>=1.43.0,<1.44.0)"] +pinpoint-sms-voice-v2 = ["mypy-boto3-pinpoint-sms-voice-v2 (>=1.43.0,<1.44.0)"] +pipes = ["mypy-boto3-pipes (>=1.43.0,<1.44.0)"] +polly = ["mypy-boto3-polly (>=1.43.0,<1.44.0)"] +pricing = ["mypy-boto3-pricing (>=1.43.0,<1.44.0)"] +proton = ["mypy-boto3-proton (>=1.43.0,<1.44.0)"] +qapps = ["mypy-boto3-qapps (>=1.43.0,<1.44.0)"] +qbusiness = ["mypy-boto3-qbusiness (>=1.43.0,<1.44.0)"] +qconnect = ["mypy-boto3-qconnect (>=1.43.0,<1.44.0)"] +quicksight = ["mypy-boto3-quicksight (>=1.43.0,<1.44.0)"] +ram = ["mypy-boto3-ram (>=1.43.0,<1.44.0)"] +rbin = ["mypy-boto3-rbin (>=1.43.0,<1.44.0)"] +rds = ["mypy-boto3-rds (>=1.43.0,<1.44.0)"] +rds-data = ["mypy-boto3-rds-data (>=1.43.0,<1.44.0)"] +redshift = ["mypy-boto3-redshift (>=1.43.0,<1.44.0)"] +redshift-data = ["mypy-boto3-redshift-data (>=1.43.0,<1.44.0)"] +redshift-serverless = ["mypy-boto3-redshift-serverless (>=1.43.0,<1.44.0)"] +rekognition = ["mypy-boto3-rekognition (>=1.43.0,<1.44.0)"] +repostspace = ["mypy-boto3-repostspace (>=1.43.0,<1.44.0)"] +resiliencehub = ["mypy-boto3-resiliencehub (>=1.43.0,<1.44.0)"] +resiliencehubv2 = ["mypy-boto3-resiliencehubv2 (>=1.43.0,<1.44.0)"] +resource-explorer-2 = ["mypy-boto3-resource-explorer-2 (>=1.43.0,<1.44.0)"] +resource-groups = ["mypy-boto3-resource-groups (>=1.43.0,<1.44.0)"] +resourcegroupstaggingapi = ["mypy-boto3-resourcegroupstaggingapi (>=1.43.0,<1.44.0)"] +rolesanywhere = ["mypy-boto3-rolesanywhere (>=1.43.0,<1.44.0)"] +route53 = ["mypy-boto3-route53 (>=1.43.0,<1.44.0)"] +route53-recovery-cluster = ["mypy-boto3-route53-recovery-cluster (>=1.43.0,<1.44.0)"] +route53-recovery-control-config = ["mypy-boto3-route53-recovery-control-config (>=1.43.0,<1.44.0)"] +route53-recovery-readiness = ["mypy-boto3-route53-recovery-readiness (>=1.43.0,<1.44.0)"] +route53domains = ["mypy-boto3-route53domains (>=1.43.0,<1.44.0)"] +route53globalresolver = ["mypy-boto3-route53globalresolver (>=1.43.0,<1.44.0)"] +route53profiles = ["mypy-boto3-route53profiles (>=1.43.0,<1.44.0)"] +route53resolver = ["mypy-boto3-route53resolver (>=1.43.0,<1.44.0)"] +rtbfabric = ["mypy-boto3-rtbfabric (>=1.43.0,<1.44.0)"] +rum = ["mypy-boto3-rum (>=1.43.0,<1.44.0)"] +s3 = ["mypy-boto3-s3 (>=1.43.0,<1.44.0)"] +s3control = ["mypy-boto3-s3control (>=1.43.0,<1.44.0)"] +s3files = ["mypy-boto3-s3files (>=1.43.0,<1.44.0)"] +s3outposts = ["mypy-boto3-s3outposts (>=1.43.0,<1.44.0)"] +s3tables = ["mypy-boto3-s3tables (>=1.43.0,<1.44.0)"] +s3vectors = ["mypy-boto3-s3vectors (>=1.43.0,<1.44.0)"] +sagemaker = ["mypy-boto3-sagemaker (>=1.43.0,<1.44.0)"] +sagemaker-a2i-runtime = ["mypy-boto3-sagemaker-a2i-runtime (>=1.43.0,<1.44.0)"] +sagemaker-edge = ["mypy-boto3-sagemaker-edge (>=1.43.0,<1.44.0)"] +sagemaker-featurestore-runtime = ["mypy-boto3-sagemaker-featurestore-runtime (>=1.43.0,<1.44.0)"] +sagemaker-geospatial = ["mypy-boto3-sagemaker-geospatial (>=1.43.0,<1.44.0)"] +sagemaker-metrics = ["mypy-boto3-sagemaker-metrics (>=1.43.0,<1.44.0)"] +sagemaker-runtime = ["mypy-boto3-sagemaker-runtime (>=1.43.0,<1.44.0)"] +sagemakerjobruntime = ["mypy-boto3-sagemakerjobruntime (>=1.43.0,<1.44.0)"] +savingsplans = ["mypy-boto3-savingsplans (>=1.43.0,<1.44.0)"] +scheduler = ["mypy-boto3-scheduler (>=1.43.0,<1.44.0)"] +schemas = ["mypy-boto3-schemas (>=1.43.0,<1.44.0)"] +sdb = ["mypy-boto3-sdb (>=1.43.0,<1.44.0)"] +secretsmanager = ["mypy-boto3-secretsmanager (>=1.43.0,<1.44.0)"] +security-ir = ["mypy-boto3-security-ir (>=1.43.0,<1.44.0)"] +securityagent = ["mypy-boto3-securityagent (>=1.43.0,<1.44.0)"] +securityhub = ["mypy-boto3-securityhub (>=1.43.0,<1.44.0)"] +securitylake = ["mypy-boto3-securitylake (>=1.43.0,<1.44.0)"] +serverlessrepo = ["mypy-boto3-serverlessrepo (>=1.43.0,<1.44.0)"] +service-quotas = ["mypy-boto3-service-quotas (>=1.43.0,<1.44.0)"] +servicecatalog = ["mypy-boto3-servicecatalog (>=1.43.0,<1.44.0)"] +servicecatalog-appregistry = ["mypy-boto3-servicecatalog-appregistry (>=1.43.0,<1.44.0)"] +servicediscovery = ["mypy-boto3-servicediscovery (>=1.43.0,<1.44.0)"] +ses = ["mypy-boto3-ses (>=1.43.0,<1.44.0)"] +sesv2 = ["mypy-boto3-sesv2 (>=1.43.0,<1.44.0)"] +shield = ["mypy-boto3-shield (>=1.43.0,<1.44.0)"] +signer = ["mypy-boto3-signer (>=1.43.0,<1.44.0)"] +signer-data = ["mypy-boto3-signer-data (>=1.43.0,<1.44.0)"] +signin = ["mypy-boto3-signin (>=1.43.0,<1.44.0)"] +simpledbv2 = ["mypy-boto3-simpledbv2 (>=1.43.0,<1.44.0)"] +simspaceweaver = ["mypy-boto3-simspaceweaver (>=1.43.0,<1.44.0)"] +snow-device-management = ["mypy-boto3-snow-device-management (>=1.43.0,<1.44.0)"] +snowball = ["mypy-boto3-snowball (>=1.43.0,<1.44.0)"] +sns = ["mypy-boto3-sns (>=1.43.0,<1.44.0)"] +socialmessaging = ["mypy-boto3-socialmessaging (>=1.43.0,<1.44.0)"] +sqs = ["mypy-boto3-sqs (>=1.43.0,<1.44.0)"] +ssm = ["mypy-boto3-ssm (>=1.43.0,<1.44.0)"] +ssm-contacts = ["mypy-boto3-ssm-contacts (>=1.43.0,<1.44.0)"] +ssm-guiconnect = ["mypy-boto3-ssm-guiconnect (>=1.43.0,<1.44.0)"] +ssm-incidents = ["mypy-boto3-ssm-incidents (>=1.43.0,<1.44.0)"] +ssm-quicksetup = ["mypy-boto3-ssm-quicksetup (>=1.43.0,<1.44.0)"] +ssm-sap = ["mypy-boto3-ssm-sap (>=1.43.0,<1.44.0)"] +sso = ["mypy-boto3-sso (>=1.43.0,<1.44.0)"] +sso-admin = ["mypy-boto3-sso-admin (>=1.43.0,<1.44.0)"] +sso-oidc = ["mypy-boto3-sso-oidc (>=1.43.0,<1.44.0)"] +stepfunctions = ["mypy-boto3-stepfunctions (>=1.43.0,<1.44.0)"] +storagegateway = ["mypy-boto3-storagegateway (>=1.43.0,<1.44.0)"] +sts = ["mypy-boto3-sts (>=1.43.0,<1.44.0)"] +supplychain = ["mypy-boto3-supplychain (>=1.43.0,<1.44.0)"] +support = ["mypy-boto3-support (>=1.43.0,<1.44.0)"] +support-app = ["mypy-boto3-support-app (>=1.43.0,<1.44.0)"] +sustainability = ["mypy-boto3-sustainability (>=1.43.0,<1.44.0)"] +swf = ["mypy-boto3-swf (>=1.43.0,<1.44.0)"] +synthetics = ["mypy-boto3-synthetics (>=1.43.0,<1.44.0)"] +taxsettings = ["mypy-boto3-taxsettings (>=1.43.0,<1.44.0)"] +textract = ["mypy-boto3-textract (>=1.43.0,<1.44.0)"] +timestream-influxdb = ["mypy-boto3-timestream-influxdb (>=1.43.0,<1.44.0)"] +timestream-query = ["mypy-boto3-timestream-query (>=1.43.0,<1.44.0)"] +timestream-write = ["mypy-boto3-timestream-write (>=1.43.0,<1.44.0)"] +tnb = ["mypy-boto3-tnb (>=1.43.0,<1.44.0)"] +transcribe = ["mypy-boto3-transcribe (>=1.43.0,<1.44.0)"] +transfer = ["mypy-boto3-transfer (>=1.43.0,<1.44.0)"] +translate = ["mypy-boto3-translate (>=1.43.0,<1.44.0)"] +trustedadvisor = ["mypy-boto3-trustedadvisor (>=1.43.0,<1.44.0)"] +uxc = ["mypy-boto3-uxc (>=1.43.0,<1.44.0)"] +verifiedpermissions = ["mypy-boto3-verifiedpermissions (>=1.43.0,<1.44.0)"] +voice-id = ["mypy-boto3-voice-id (>=1.43.0,<1.44.0)"] +vpc-lattice = ["mypy-boto3-vpc-lattice (>=1.43.0,<1.44.0)"] +waf = ["mypy-boto3-waf (>=1.43.0,<1.44.0)"] +waf-regional = ["mypy-boto3-waf-regional (>=1.43.0,<1.44.0)"] +wafv2 = ["mypy-boto3-wafv2 (>=1.43.0,<1.44.0)"] +wellarchitected = ["mypy-boto3-wellarchitected (>=1.43.0,<1.44.0)"] +wickr = ["mypy-boto3-wickr (>=1.43.0,<1.44.0)"] +wisdom = ["mypy-boto3-wisdom (>=1.43.0,<1.44.0)"] +workdocs = ["mypy-boto3-workdocs (>=1.43.0,<1.44.0)"] +workmail = ["mypy-boto3-workmail (>=1.43.0,<1.44.0)"] +workmailmessageflow = ["mypy-boto3-workmailmessageflow (>=1.43.0,<1.44.0)"] +workspaces = ["mypy-boto3-workspaces (>=1.43.0,<1.44.0)"] +workspaces-instances = ["mypy-boto3-workspaces-instances (>=1.43.0,<1.44.0)"] +workspaces-thin-client = ["mypy-boto3-workspaces-thin-client (>=1.43.0,<1.44.0)"] +workspaces-web = ["mypy-boto3-workspaces-web (>=1.43.0,<1.44.0)"] +xray = ["mypy-boto3-xray (>=1.43.0,<1.44.0)"] [[package]] name = "botocore" @@ -916,6 +989,7 @@ files = [ {file = "botocore-1.42.67-py3-none-any.whl", hash = "sha256:a94317d2ce83deae230964beb2729639455de65595d0154f285b0ccfd29780cd"}, {file = "botocore-1.42.67.tar.gz", hash = "sha256:ee307f30fcb798d244fb35a87847b274e1e1f72cd5f7f2e31bd1826df0c45295"}, ] +markers = {main = "extra == \"all\" or extra == \"datamasking\" or extra == \"aws-sdk\" or extra == \"tracer\""} [package.dependencies] jmespath = ">=0.7.1,<2.0.0" @@ -985,40 +1059,40 @@ ujson = ["ujson (>=5.10.0)"] [[package]] name = "cdk-nag" -version = "2.37.55" +version = "2.38.2" description = "Check CDK v2 applications for best practices using a combination on available rule packs." optional = false python-versions = "~=3.9" groups = ["dev"] files = [ - {file = "cdk_nag-2.37.55-py3-none-any.whl", hash = "sha256:bf83bcdeb98ac20bb813cac291af121d91c5a296fa815e01f93886b4f8b38845"}, - {file = "cdk_nag-2.37.55.tar.gz", hash = "sha256:e9dc517070ef5a19deef95e79731e5624bd86cd07b56d210380408ea1314e47b"}, + {file = "cdk_nag-2.38.2-py3-none-any.whl", hash = "sha256:d37f18ae9450f401bcc55d5d82138beee561486806579849cb9be25ff7565904"}, + {file = "cdk_nag-2.38.2.tar.gz", hash = "sha256:a4d419062ea4d64c2892942214b9184b124eb2bc36d087982007b3455e1ac443"}, ] [package.dependencies] aws-cdk-lib = ">=2.176.0,<3.0.0" -constructs = ">=10.0.5,<11.0.0" -jsii = ">=1.116.0,<2.0.0" +constructs = ">=10.5.1,<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 = "cdklabs-generative-ai-cdk-constructs" -version = "0.1.316" +version = "0.1.318" description = "AWS Generative AI CDK Constructs is a library for well-architected generative AI patterns." optional = false -python-versions = "~=3.9" +python-versions = "~=3.10" groups = ["dev"] files = [ - {file = "cdklabs_generative_ai_cdk_constructs-0.1.316-py3-none-any.whl", hash = "sha256:925926882b2978156918536460bbfa03ab1ed0b7641ff0982d7370c8a3f81f83"}, - {file = "cdklabs_generative_ai_cdk_constructs-0.1.316.tar.gz", hash = "sha256:8347018014753f5c99a14f93ea05756ded83f05286eef6fc712b492364fe173b"}, + {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] -aws-cdk-lib = ">=2.233.0,<3.0.0" -cdk-nag = ">=2.37.55,<3.0.0" -constructs = ">=10.3.0,<11.0.0" -jsii = ">=1.127.0,<2.0.0" +aws-cdk-lib = ">=2.254.0,<3.0.0" +cdk-nag = ">=2.38.2,<3.0.0" +constructs = ">=10.6.0,<11.0.0" +jsii = ">=1.130.0,<2.0.0" publication = ">=0.0.3" typeguard = "2.13.3" @@ -1033,6 +1107,7 @@ files = [ {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, ] +markers = {main = "extra == \"datadog\""} [[package]] name = "cffi" @@ -1127,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\""} @@ -1281,6 +1356,7 @@ files = [ {file = "charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0"}, {file = "charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644"}, ] +markers = {main = "extra == \"datadog\""} [[package]] name = "click" @@ -1329,135 +1405,150 @@ development = ["black", "flake8", "mypy", "pytest", "types-colorama"] [[package]] name = "constructs" -version = "10.5.1" +version = "10.6.0" description = "A programming model for software-defined state" optional = false python-versions = "~=3.9" groups = ["dev"] files = [ - {file = "constructs-10.5.1-py3-none-any.whl", hash = "sha256:fc5c14f6b2770c8542a43e298aa29b63dee4b18701763e8c0fdce202624c3a7c"}, - {file = "constructs-10.5.1.tar.gz", hash = "sha256:c0e90bb2b9c2782f292017820b91714321cb78393c8965c9362b0b624bfaf23b"}, + {file = "constructs-10.6.0-py3-none-any.whl", hash = "sha256:ad4ffabdb53c17cde00fb94e441a1ba9fddac57c92ad49d263f8dbd416cec513"}, + {file = "constructs-10.6.0.tar.gz", hash = "sha256:bc55d1d390142424861e5ff5c6b8c243c4bae18fe7302e0939c2003f329ba365"}, ] [package.dependencies] -jsii = ">=1.126.0,<2.0.0" +jsii = ">=1.127.0,<2.0.0" publication = ">=0.0.3" typeguard = "2.13.3" [[package]] name = "coverage" -version = "7.13.5" +version = "7.15.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5"}, - {file = "coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf"}, - {file = "coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8"}, - {file = "coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4"}, - {file = "coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d"}, - {file = "coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930"}, - {file = "coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d"}, - {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40"}, - {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878"}, - {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400"}, - {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0"}, - {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0"}, - {file = "coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58"}, - {file = "coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e"}, - {file = "coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d"}, - {file = "coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587"}, - {file = "coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642"}, - {file = "coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b"}, - {file = "coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686"}, - {file = "coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743"}, - {file = "coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75"}, - {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209"}, - {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a"}, - {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e"}, - {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd"}, - {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8"}, - {file = "coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf"}, - {file = "coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9"}, - {file = "coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028"}, - {file = "coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01"}, - {file = "coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422"}, - {file = "coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f"}, - {file = "coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5"}, - {file = "coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376"}, - {file = "coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256"}, - {file = "coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c"}, - {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5"}, - {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09"}, - {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9"}, - {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf"}, - {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c"}, - {file = "coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf"}, - {file = "coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810"}, - {file = "coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de"}, - {file = "coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1"}, - {file = "coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3"}, - {file = "coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26"}, - {file = "coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3"}, - {file = "coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b"}, - {file = "coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a"}, - {file = "coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969"}, - {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161"}, - {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15"}, - {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1"}, - {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6"}, - {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17"}, - {file = "coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85"}, - {file = "coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b"}, - {file = "coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664"}, - {file = "coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d"}, - {file = "coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0"}, - {file = "coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806"}, - {file = "coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3"}, - {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9"}, - {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd"}, - {file = "coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606"}, - {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e"}, - {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0"}, - {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87"}, - {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479"}, - {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2"}, - {file = "coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a"}, - {file = "coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819"}, - {file = "coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911"}, - {file = "coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f"}, - {file = "coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e"}, - {file = "coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a"}, - {file = "coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510"}, - {file = "coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247"}, - {file = "coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6"}, - {file = "coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0"}, - {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882"}, - {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740"}, - {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16"}, - {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0"}, - {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0"}, - {file = "coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc"}, - {file = "coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633"}, - {file = "coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8"}, - {file = "coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b"}, - {file = "coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c"}, - {file = "coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9"}, - {file = "coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29"}, - {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607"}, - {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90"}, - {file = "coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3"}, - {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab"}, - {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562"}, - {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2"}, - {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea"}, - {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a"}, - {file = "coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215"}, - {file = "coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43"}, - {file = "coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45"}, - {file = "coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61"}, - {file = "coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179"}, + {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] @@ -1468,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" @@ -1666,11 +1747,11 @@ files = [ [package.dependencies] bytecode = [ - {version = ">=0.17.0,<1", markers = "python_version >= \"3.14.0\""}, + {version = ">=0.13.0,<1", markers = "python_version < \"3.11.0\""}, {version = ">=0.16.0,<1", markers = "python_version >= \"3.13.0\" and python_version < \"3.14.0\""}, - {version = ">=0.15.1,<1", markers = "python_version ~= \"3.12.0\""}, + {version = ">=0.17.0,<1", markers = "python_version >= \"3.14.0\""}, {version = ">=0.14.0,<1", markers = "python_version ~= \"3.11.0\""}, - {version = ">=0.13.0,<1", markers = "python_version < \"3.11.0\""}, + {version = ">=0.15.1,<1", markers = "python_version ~= \"3.12.0\""}, ] envier = ">=0.6.1,<0.7.0" opentelemetry-api = ">=1,<2" @@ -1829,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 == \"validation\" or extra == \"all\"" +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] @@ -1845,14 +1926,14 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "filelock" -version = "3.25.2" +version = "3.29.7" description = "A platform independent file lock." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70"}, - {file = "filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694"}, + {file = "filelock-3.29.7-py3-none-any.whl", hash = "sha256:987db6f789a3a2a59f55081801b2b3697cb97e2a736b5f1a9e99b559285fbc51"}, + {file = "filelock-3.29.7.tar.gz", hash = "sha256:5b481979797ae69e72f0b389d89a80bdd585c260c5b3f1fb9c0a5ba9bb3f195d"}, ] [[package]] @@ -1890,55 +1971,22 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.46" +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.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058"}, - {file = "gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f"}, + {file = "gitpython-3.1.58-py3-none-any.whl", hash = "sha256:d331e722577f0fd7fc1f857419b3ecc07af66282b933d2a4d95f84a042fdd50f"}, + {file = "gitpython-3.1.58.tar.gz", hash = "sha256:621416df10ef3fd0e19fabf9172ddeed0fa704d353d04f194eec56a625a95b22"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -doc = ["sphinx (>=7.1.2,<7.2)", "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"}, -] - -[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"}, -] - -[package.dependencies] -colorama = ">=0.4" -griffelib = "2.0.0" +doc = ["sphinx (>=7.4.7,<8)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] +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" @@ -1949,6 +1997,7 @@ python-versions = ">=3.10" groups = ["dev"] files = [ {file = "griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f"}, + {file = "griffelib-2.0.0.tar.gz", hash = "sha256:e504d637a089f5cab9b5daf18f7645970509bf4f53eda8d79ed71cce8bd97934"}, ] [package.extras] @@ -2048,18 +2097,19 @@ parser = ["pyhcl (>=0.4.4,<0.5.0)"] [[package]] name = "idna" -version = "3.11" +version = "3.15" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, - {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, + {file = "idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"}, + {file = "idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"}, ] +markers = {main = "extra == \"valkey\" or extra == \"datadog\""} [package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "ijson" @@ -2191,26 +2241,6 @@ perf = ["ipython"] test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] -[[package]] -name = "importlib-resources" -version = "6.5.2" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -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" @@ -2270,20 +2300,19 @@ files = [ [[package]] name = "jsii" -version = "1.127.0" +version = "1.140.0" description = "Python client for jsii runtime" optional = false -python-versions = "~=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "jsii-1.127.0-py3-none-any.whl", hash = "sha256:92a11f39e461f5168e2467efd53351cc32b118314b95cc6323c2d044eb299eaf"}, - {file = "jsii-1.127.0.tar.gz", hash = "sha256:631a13d73265eaa22c0c0804e77fe59c8fcc3af3a94de9923af65380b6ad267a"}, + {file = "jsii-1.140.0-py3-none-any.whl", hash = "sha256:aa24f27d15d8d7db1ff32a9c56074bc87e728b30330ead97fb149419a3721a50"}, + {file = "jsii-1.140.0.tar.gz", hash = "sha256:454c874d963a862f6b90bc013c45a720e51292659f57249d9cae7cb2a189ec0e"}, ] [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" @@ -2343,7 +2372,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rpds-py = ">=0.25.0" @@ -2368,103 +2397,150 @@ referencing = ">=0.31.0" [[package]] name = "librt" -version = "0.8.1" +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.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc"}, - {file = "librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7"}, - {file = "librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6"}, - {file = "librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0"}, - {file = "librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b"}, - {file = "librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6"}, - {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71"}, - {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7"}, - {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05"}, - {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891"}, - {file = "librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7"}, - {file = "librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2"}, - {file = "librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd"}, - {file = "librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965"}, - {file = "librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da"}, - {file = "librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0"}, - {file = "librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e"}, - {file = "librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3"}, - {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac"}, - {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596"}, - {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99"}, - {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe"}, - {file = "librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb"}, - {file = "librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b"}, - {file = "librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9"}, - {file = "librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a"}, - {file = "librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9"}, - {file = "librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb"}, - {file = "librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d"}, - {file = "librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7"}, - {file = "librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440"}, - {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9"}, - {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972"}, - {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921"}, - {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0"}, - {file = "librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a"}, - {file = "librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444"}, - {file = "librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d"}, - {file = "librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35"}, - {file = "librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583"}, - {file = "librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c"}, - {file = "librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04"}, - {file = "librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363"}, - {file = "librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0"}, - {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012"}, - {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb"}, - {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b"}, - {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d"}, - {file = "librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a"}, - {file = "librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79"}, - {file = "librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0"}, - {file = "librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f"}, - {file = "librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c"}, - {file = "librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc"}, - {file = "librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c"}, - {file = "librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3"}, - {file = "librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14"}, - {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7"}, - {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6"}, - {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071"}, - {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78"}, - {file = "librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023"}, - {file = "librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730"}, - {file = "librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3"}, - {file = "librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1"}, - {file = "librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee"}, - {file = "librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7"}, - {file = "librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040"}, - {file = "librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e"}, - {file = "librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732"}, - {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624"}, - {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4"}, - {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382"}, - {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994"}, - {file = "librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a"}, - {file = "librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4"}, - {file = "librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61"}, - {file = "librt-0.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3dff3d3ca8db20e783b1bc7de49c0a2ab0b8387f31236d6a026597d07fcd68ac"}, - {file = "librt-0.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08eec3a1fc435f0d09c87b6bf1ec798986a3544f446b864e4099633a56fcd9ed"}, - {file = "librt-0.8.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e3f0a41487fd5fad7e760b9e8a90e251e27c2816fbc2cff36a22a0e6bcbbd9dd"}, - {file = "librt-0.8.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bacdb58d9939d95cc557b4dbaa86527c9db2ac1ed76a18bc8d26f6dc8647d851"}, - {file = "librt-0.8.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d7ab1f01aa753188605b09a51faa44a3327400b00b8cce424c71910fc0a128"}, - {file = "librt-0.8.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4998009e7cb9e896569f4be7004f09d0ed70d386fa99d42b6d363f6d200501ac"}, - {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2cc68eeeef5e906839c7bb0815748b5b0a974ec27125beefc0f942715785b551"}, - {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0bf69d79a23f4f40b8673a947a234baeeb133b5078b483b7297c5916539cf5d5"}, - {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:22b46eabd76c1986ee7d231b0765ad387d7673bbd996aa0d0d054b38ac65d8f6"}, - {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:237796479f4d0637d6b9cbcb926ff424a97735e68ade6facf402df4ec93375ed"}, - {file = "librt-0.8.1-cp39-cp39-win32.whl", hash = "sha256:4beb04b8c66c6ae62f8c1e0b2f097c1ebad9295c929a8d5286c05eae7c2fc7dc"}, - {file = "librt-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:64548cde61b692dc0dc379f4b5f59a2f582c2ebe7890d09c1ae3b9e66fa015b7"}, - {file = "librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73"}, + {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]] @@ -2780,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] @@ -2847,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\""} @@ -2912,83 +2988,94 @@ dill = ">=0.4.1" [[package]] name = "mypy" -version = "1.20.1" +version = "2.3.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "mypy-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3ba5d1e712ada9c3b6223dcbc5a31dac334ed62991e5caa17bcf5a4ddc349af0"}, - {file = "mypy-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e731284c117b0987fb1e6c5013a56f33e7faa1fce594066ab83876183ce1c66"}, - {file = "mypy-1.20.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8e945b872a05f4fbefabe2249c0b07b6b194e5e11a86ebee9edf855de09806c"}, - {file = "mypy-1.20.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fc88acef0dc9b15246502b418980478c1bfc9702057a0e1e7598d01a7af8937"}, - {file = "mypy-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:14911a115c73608f155f648b978c5055d16ff974e6b1b5512d7fedf4fa8b15c6"}, - {file = "mypy-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:76d9b4c992cca3331d9793ef197ae360ea44953cf35beb2526e95b9e074f2866"}, - {file = "mypy-1.20.1-cp310-cp310-win_arm64.whl", hash = "sha256:b408722f80be44845da555671a5ef3a0c63f51ca5752b0c20e992dc9c0fbd3cd"}, - {file = "mypy-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c01eb9bac2c6a962d00f9d23421cd2913840e65bba365167d057bd0b4171a92e"}, - {file = "mypy-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55d12ddbd8a9cac5b276878bd534fa39fff5bf543dc6ae18f25d30c8d7d27fca"}, - {file = "mypy-1.20.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0aa322c1468b6cdfc927a44ce130f79bb44bcd34eb4a009eb9f96571fd80955"}, - {file = "mypy-1.20.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f8bc95899cf676b6e2285779a08a998cc3a7b26f1026752df9d2741df3c79e8"}, - {file = "mypy-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:47c2b90191a870a04041e910277494b0d92f0711be9e524d45c074fe60c00b65"}, - {file = "mypy-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:9857dc8d2ec1a392ffbda518075beb00ac58859979c79f9e6bdcb7277082c2f2"}, - {file = "mypy-1.20.1-cp311-cp311-win_arm64.whl", hash = "sha256:09d8df92bb25b6065ab91b178da843dda67b33eb819321679a6e98a907ce0e10"}, - {file = "mypy-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:36ee2b9c6599c230fea89bbd79f401f9f9f8e9fcf0c777827789b19b7da90f51"}, - {file = "mypy-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fba3fb0968a7b48806b0c90f38d39296f10766885a94c83bd21399de1e14eb28"}, - {file = "mypy-1.20.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef1415a637cd3627d6304dfbeddbadd21079dafc2a8a753c477ce4fc0c2af54f"}, - {file = "mypy-1.20.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef3461b1ad5cd446e540016e90b5984657edda39f982f4cc45ca317b628f5a37"}, - {file = "mypy-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:542dd63c9e1339b6092eb25bd515f3a32a1453aee8c9521d2ddb17dacd840237"}, - {file = "mypy-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:1d55c7cd8ca22e31f93af2a01160a9e95465b5878de23dba7e48116052f20a8d"}, - {file = "mypy-1.20.1-cp312-cp312-win_arm64.whl", hash = "sha256:f5b84a79070586e0d353ee07b719d9d0a4aa7c8ee90c0ea97747e98cbe193019"}, - {file = "mypy-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f3886c03e40afefd327bd70b3f634b39ea82e87f314edaa4d0cce4b927ddcc1"}, - {file = "mypy-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e860eb3904f9764e83bafd70c8250bdffdc7dde6b82f486e8156348bf7ceb184"}, - {file = "mypy-1.20.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4b5aac6e785719da51a84f5d09e9e843d473170a9045b1ea7ea1af86225df4b"}, - {file = "mypy-1.20.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f37b6cd0fe2ad3a20f05ace48ca3523fc52ff86940e34937b439613b6854472e"}, - {file = "mypy-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4bbb0f6b54ce7cc350ef4a770650d15fa70edd99ad5267e227133eda9c94218"}, - {file = "mypy-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:c3dc20f8ec76eecd77148cdd2f1542ed496e51e185713bf488a414f862deb8f2"}, - {file = "mypy-1.20.1-cp313-cp313-win_arm64.whl", hash = "sha256:a9d62bbac5d6d46718e2b0330b25e6264463ed832722b8f7d4440ff1be3ca895"}, - {file = "mypy-1.20.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:12927b9c0ed794daedcf1dab055b6c613d9d5659ac511e8d936d96f19c087d12"}, - {file = "mypy-1.20.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:752507dd481e958b2c08fc966d3806c962af5a9433b5bf8f3bdd7175c20e34fe"}, - {file = "mypy-1.20.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c614655b5a065e56274c6cbbe405f7cf7e96c0654db7ba39bc680238837f7b08"}, - {file = "mypy-1.20.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c3f6221a76f34d5100c6d35b3ef6b947054123c3f8d6938a4ba00b1308aa572"}, - {file = "mypy-1.20.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4bdfc06303ac06500af71ea0cdbe995c502b3c9ba32f3f8313523c137a25d1b6"}, - {file = "mypy-1.20.1-cp314-cp314-win_amd64.whl", hash = "sha256:0131edd7eba289973d1ba1003d1a37c426b85cdef76650cd02da6420898a5eb3"}, - {file = "mypy-1.20.1-cp314-cp314-win_arm64.whl", hash = "sha256:33f02904feb2c07e1fdf7909026206396c9deeb9e6f34d466b4cfedb0aadbbe4"}, - {file = "mypy-1.20.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:168472149dd8cc505c98cefd21ad77e4257ed6022cd5ed2fe2999bed56977a5a"}, - {file = "mypy-1.20.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:eb674600309a8f22790cca883a97c90299f948183ebb210fbef6bcee07cb1986"}, - {file = "mypy-1.20.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef2b2e4cc464ba9795459f2586923abd58a0055487cbe558cb538ea6e6bc142a"}, - {file = "mypy-1.20.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee461d396dd46b3f0ed5a098dbc9b8860c81c46ad44fa071afcfbc149f167c9"}, - {file = "mypy-1.20.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e364926308b3e66f1361f81a566fc1b2f8cd47fc8525e8136d4058a65a4b4f02"}, - {file = "mypy-1.20.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a0c17fbd746d38c70cbc42647cfd884f845a9708a4b160a8b4f7e70d41f4d7fa"}, - {file = "mypy-1.20.1-cp314-cp314t-win_arm64.whl", hash = "sha256:db2cb89654626a912efda69c0d5c1d22d948265e2069010d3dde3abf751c7d08"}, - {file = "mypy-1.20.1-py3-none-any.whl", hash = "sha256:1aae28507f253fe82d883790d1c0a0d35798a810117c88184097fe8881052f06"}, - {file = "mypy-1.20.1.tar.gz", hash = "sha256:6fc3f4ecd52de81648fed1945498bf42fa2993ddfad67c9056df36ae5757f804"}, + {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] -librt = {version = ">=0.8.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\""} -typing_extensions = ">=4.6.0" +typing_extensions = [ + {version = ">=4.6.0", markers = "python_version < \"3.15\""}, + {version = ">=4.14.0", markers = "python_version >= \"3.15\""}, +] [package.extras] dmypy = ["psutil (>=4.0)"] faster-cache = ["orjson"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] -native-parser = ["ast-serialize (>=0.1.1,<1.0.0)"] reports = ["lxml"] [[package]] name = "mypy-boto3-appconfig" -version = "1.42.3" -description = "Type annotations for boto3 AppConfig 1.42.3 service generated with mypy-boto3-builder 8.12.0" +version = "1.43.0" +description = "Type annotations for boto3 AppConfig 1.43.0 service generated with mypy-boto3-builder 8.12.0" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "mypy_boto3_appconfig-1.42.3-py3-none-any.whl", hash = "sha256:88857735f2615bcad49e254c12585a29bdf4fbe348d1f72907210569ec97455e"}, - {file = "mypy_boto3_appconfig-1.42.3.tar.gz", hash = "sha256:606d37765259c854a3574eacc3fe5ca3956b5c456b12ff80c8e1cb20bdab9119"}, + {file = "mypy_boto3_appconfig-1.43.0-py3-none-any.whl", hash = "sha256:d9ce0805d58653ec948a9674b53854ef5fcd3318f12b619cfa7052045b7852f9"}, + {file = "mypy_boto3_appconfig-1.43.0.tar.gz", hash = "sha256:25c5e8fdd19dd1a790ceb2450bdb1c3c7288d939daf8f6962d6559c02d7b8a0a"}, ] [package.dependencies] @@ -2996,14 +3083,14 @@ typing-extensions = {version = "*", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-appconfigdata" -version = "1.42.3" -description = "Type annotations for boto3 AppConfigData 1.42.3 service generated with mypy-boto3-builder 8.12.0" +version = "1.43.0" +description = "Type annotations for boto3 AppConfigData 1.43.0 service generated with mypy-boto3-builder 8.12.0" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "mypy_boto3_appconfigdata-1.42.3-py3-none-any.whl", hash = "sha256:3ef47224643a511bd217a92c3360cccf39be8393ed218a199555bc0592ede64f"}, - {file = "mypy_boto3_appconfigdata-1.42.3.tar.gz", hash = "sha256:595e36e9f477205916e1f11d28c6c335d606a20e55bcb793a43a4b48a4b2b32d"}, + {file = "mypy_boto3_appconfigdata-1.43.0-py3-none-any.whl", hash = "sha256:a80c07bc643d9af1f934a4b76fe6ab0304f03d913bc7393eefe527e2072baa92"}, + {file = "mypy_boto3_appconfigdata-1.43.0.tar.gz", hash = "sha256:9570014a955620507743e66b93c5e5e6da07b39b48f146c7abc6b259ab39d562"}, ] [package.dependencies] @@ -3011,14 +3098,14 @@ typing-extensions = {version = "*", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-cloudformation" -version = "1.42.3" -description = "Type annotations for boto3 CloudFormation 1.42.3 service generated with mypy-boto3-builder 8.12.0" +version = "1.43.0" +description = "Type annotations for boto3 CloudFormation 1.43.0 service generated with mypy-boto3-builder 8.12.0" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "mypy_boto3_cloudformation-1.42.3-py3-none-any.whl", hash = "sha256:d4c802dd78844f10e944143b9f40c2c1199ed5f57f3540ab7bfc2281ac5bcaf0"}, - {file = "mypy_boto3_cloudformation-1.42.3.tar.gz", hash = "sha256:3bd3849bc89a371d4c368691535b320244ba00579cddd63bb58b73f28d70e510"}, + {file = "mypy_boto3_cloudformation-1.43.0-py3-none-any.whl", hash = "sha256:bcb2f8b8231f6bd96cc18d17c1c72ea0dfa6dc8156966d8d12495445f5041f4c"}, + {file = "mypy_boto3_cloudformation-1.43.0.tar.gz", hash = "sha256:5be845bc3dc1b9cdbd8b6b071fad7c42d0221d4087ac0cc7c5b9dd219b324606"}, ] [package.dependencies] @@ -3026,14 +3113,14 @@ typing-extensions = {version = "*", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-cloudwatch" -version = "1.42.56" -description = "Type annotations for boto3 CloudWatch 1.42.56 service generated with mypy-boto3-builder 8.12.0" +version = "1.43.2" +description = "Type annotations for boto3 CloudWatch 1.43.2 service generated with mypy-boto3-builder 8.12.0" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "mypy_boto3_cloudwatch-1.42.56-py3-none-any.whl", hash = "sha256:40621e91fbad74a739cdfb76bd5e331059a3d1bc13ae866ab332bf20641c1574"}, - {file = "mypy_boto3_cloudwatch-1.42.56.tar.gz", hash = "sha256:6791ab895dbd2c2871f8c0d686ae5adb39418dbd46515996e2c80a59664d0dcf"}, + {file = "mypy_boto3_cloudwatch-1.43.2-py3-none-any.whl", hash = "sha256:954a9ac4a7d24310aa4df4e3de943fcc1fedb0b1cd0361c51d05951df0ac7918"}, + {file = "mypy_boto3_cloudwatch-1.43.2.tar.gz", hash = "sha256:a08fb826321b88da8043a4175d7dce7a28119ac22aca6e12d938b0ae33228d05"}, ] [package.dependencies] @@ -3041,14 +3128,14 @@ typing-extensions = {version = "*", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-dynamodb" -version = "1.42.55" -description = "Type annotations for boto3 DynamoDB 1.42.55 service generated with mypy-boto3-builder 8.12.0" +version = "1.43.0" +description = "Type annotations for boto3 DynamoDB 1.43.0 service generated with mypy-boto3-builder 8.12.0" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "mypy_boto3_dynamodb-1.42.55-py3-none-any.whl", hash = "sha256:652af33641601d223fb35207b89bd98513a7493d2b95ae4cba47c925b6ec103c"}, - {file = "mypy_boto3_dynamodb-1.42.55.tar.gz", hash = "sha256:a445f439b6bc4532fd592cb7f44444c8fc8f397271c0d9087e712f71f196d2f9"}, + {file = "mypy_boto3_dynamodb-1.43.0-py3-none-any.whl", hash = "sha256:60b64d15e86d406a980d96f734d8c7fb1704668d0234dc8dabd2532c902a3ba6"}, + {file = "mypy_boto3_dynamodb-1.43.0.tar.gz", hash = "sha256:f0cea38e058f1d07361ecb55d8f40665d824b42cf4864724c7fccc8bf3946fcd"}, ] [package.dependencies] @@ -3056,14 +3143,14 @@ typing-extensions = {version = "*", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-lambda" -version = "1.42.37" -description = "Type annotations for boto3 Lambda 1.42.37 service generated with mypy-boto3-builder 8.12.0" +version = "1.43.0" +description = "Type annotations for boto3 Lambda 1.43.0 service generated with mypy-boto3-builder 8.12.0" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "mypy_boto3_lambda-1.42.37-py3-none-any.whl", hash = "sha256:9614518cbe3c300d3d1e2d9c3d857c3829c44a8544c4cd4ca393d35181b22619"}, - {file = "mypy_boto3_lambda-1.42.37.tar.gz", hash = "sha256:94f7f0708f9b5ffa5b8b3eb6d564be1ef402ebb8b8cd96045332b7a3bc1ea0e0"}, + {file = "mypy_boto3_lambda-1.43.0-py3-none-any.whl", hash = "sha256:847b8f12b74f881c743464cd0010a04e2b21201b39ac92b1040c6cd276bac4e6"}, + {file = "mypy_boto3_lambda-1.43.0.tar.gz", hash = "sha256:a58de26b5c13be54deab31723ee9ab7aaa922be1dfbd093dc3a4ca12cc853157"}, ] [package.dependencies] @@ -3071,14 +3158,14 @@ typing-extensions = {version = "*", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-logs" -version = "1.42.60" -description = "Type annotations for boto3 CloudWatchLogs 1.42.60 service generated with mypy-boto3-builder 8.12.0" +version = "1.43.3" +description = "Type annotations for boto3 CloudWatchLogs 1.43.3 service generated with mypy-boto3-builder 8.12.0" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "mypy_boto3_logs-1.42.60-py3-none-any.whl", hash = "sha256:4a34c1224a11d09b883789c47f1bd2910e7b50151fdec63266f7ff543caca3d0"}, - {file = "mypy_boto3_logs-1.42.60.tar.gz", hash = "sha256:08110d32d9332d7aa08c2cba0f5c3813ed1beb74c682e7407519fe4f3d1b2bff"}, + {file = "mypy_boto3_logs-1.43.3-py3-none-any.whl", hash = "sha256:853652fb1fb9de9eb1439c9ebbe578afe080cc7693d12e3ea778bba636aeb836"}, + {file = "mypy_boto3_logs-1.43.3.tar.gz", hash = "sha256:9c7484a6f848e7e5c346a2ea85663c24d282ae78797748321117b262d6ea845c"}, ] [package.dependencies] @@ -3086,14 +3173,14 @@ typing-extensions = {version = "*", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-s3" -version = "1.42.67" -description = "Type annotations for boto3 S3 1.42.67 service generated with mypy-boto3-builder 8.12.0" +version = "1.43.0" +description = "Type annotations for boto3 S3 1.43.0 service generated with mypy-boto3-builder 8.12.0" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "mypy_boto3_s3-1.42.67-py3-none-any.whl", hash = "sha256:93208799734611da4caa5fa8f5ce677b62758ddcd34b737b9f7ae471d179b95e"}, - {file = "mypy_boto3_s3-1.42.67.tar.gz", hash = "sha256:3a3a918a9949f2d6f8071d490b8968ddce634aa19590697537e5189cbdca403e"}, + {file = "mypy_boto3_s3-1.43.0-py3-none-any.whl", hash = "sha256:aaa7991e7ffafcf8ff4fb23c5fb4cc4554ef5724c889ff016b87e60f27405b5b"}, + {file = "mypy_boto3_s3-1.43.0.tar.gz", hash = "sha256:3bfb027b1f3df9316ff72ff29f4b2dc0d7d65ed5032d8bcf4892222994228588"}, ] [package.dependencies] @@ -3101,14 +3188,14 @@ typing-extensions = {version = "*", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-secretsmanager" -version = "1.42.8" -description = "Type annotations for boto3 SecretsManager 1.42.8 service generated with mypy-boto3-builder 8.12.0" +version = "1.43.0" +description = "Type annotations for boto3 SecretsManager 1.43.0 service generated with mypy-boto3-builder 8.12.0" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "mypy_boto3_secretsmanager-1.42.8-py3-none-any.whl", hash = "sha256:50c891a88e725a8dba7444018e47590ea63d8e938abe2b1c0b25e5413f39d51d"}, - {file = "mypy_boto3_secretsmanager-1.42.8.tar.gz", hash = "sha256:5ab42f35ce932765ebb1684146f478a87cc4b83bef950fd1aa0e268b88d59c81"}, + {file = "mypy_boto3_secretsmanager-1.43.0-py3-none-any.whl", hash = "sha256:38415cdecb73dd20e485707a7cf456f6dde54ff4b155e7fb255eb001eb47d5bc"}, + {file = "mypy_boto3_secretsmanager-1.43.0.tar.gz", hash = "sha256:265ee2fddf9d3e42ae39685625fb7861a539110d8e324372847c0e1cbd666b20"}, ] [package.dependencies] @@ -3116,14 +3203,14 @@ typing-extensions = {version = "*", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-ssm" -version = "1.42.54" -description = "Type annotations for boto3 SSM 1.42.54 service generated with mypy-boto3-builder 8.12.0" +version = "1.43.0" +description = "Type annotations for boto3 SSM 1.43.0 service generated with mypy-boto3-builder 8.12.0" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "mypy_boto3_ssm-1.42.54-py3-none-any.whl", hash = "sha256:dfd70aa5f60be70437b53482fa6e183bafe922598a50fc6c51f6ad3bd70d8c04"}, - {file = "mypy_boto3_ssm-1.42.54.tar.gz", hash = "sha256:f4bc19a08635757808b66ef94a5b52c3729da998587745962626e60606a1be2c"}, + {file = "mypy_boto3_ssm-1.43.0-py3-none-any.whl", hash = "sha256:56caee120bdc601aec269b4203e67365db7f1531797d87ff616e318249fc1399"}, + {file = "mypy_boto3_ssm-1.43.0.tar.gz", hash = "sha256:33cb659b6182160141f9598fbdf6ff921dc94247a86f62152abd870b24e4ff62"}, ] [package.dependencies] @@ -3131,14 +3218,14 @@ typing-extensions = {version = "*", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-xray" -version = "1.42.3" -description = "Type annotations for boto3 XRay 1.42.3 service generated with mypy-boto3-builder 8.12.0" +version = "1.43.0" +description = "Type annotations for boto3 XRay 1.43.0 service generated with mypy-boto3-builder 8.12.0" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "mypy_boto3_xray-1.42.3-py3-none-any.whl", hash = "sha256:a8bd87257e3931a415bee6b82892190f3588580dbaf0b54233f348a8f27ebccd"}, - {file = "mypy_boto3_xray-1.42.3.tar.gz", hash = "sha256:8092c41967eed2d0fee096a22b082bb107cfe2bb467a8dd7fbdc392593f1969c"}, + {file = "mypy_boto3_xray-1.43.0-py3-none-any.whl", hash = "sha256:122dd8b99fcd6cbd66314211b692ff32c96a4d9dd02b40d82b5a376faf279a6e"}, + {file = "mypy_boto3_xray-1.43.0.tar.gz", hash = "sha256:68800f2eb955a85d166ad462b5f9563cbd6d0578845807137c93cd3f8e70eb44"}, ] [package.dependencies] @@ -3184,7 +3271,7 @@ description = "Python package for creating and manipulating graphs and networks" optional = false python-versions = ">=3.11" groups = ["dev"] -markers = "python_version >= \"3.14.0\"" +markers = "python_version >= \"3.14.0\" and python_version < \"3.15\"" files = [ {file = "networkx-3.6-py3-none-any.whl", hash = "sha256:cdb395b105806062473d3be36458d8f1459a4e4b98e236a66c3a48996e07684f"}, {file = "networkx-3.6.tar.gz", hash = "sha256:285276002ad1f7f7da0f7b42f004bcba70d381e936559166363707fdad3d72ad"}, @@ -3208,7 +3295,7 @@ description = "Python package for creating and manipulating graphs and networks" optional = false python-versions = "!=3.14.1,>=3.11" groups = ["dev"] -markers = "python_version >= \"3.11.0\" and python_version < \"3.14.0\"" +markers = "python_version >= \"3.11.0\" and python_version < \"3.14.0\" or python_version >= \"3.15\"" files = [ {file = "networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762"}, {file = "networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509"}, @@ -3269,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" @@ -3345,21 +3448,22 @@ 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\""} [[package]] name = "publication" @@ -3374,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]] @@ -3396,24 +3500,24 @@ 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" -version = "2.12.5" +version = "2.13.4" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, - {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, + {file = "pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba"}, + {file = "pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6"}, ] -markers = {main = "extra == \"parser\" or extra == \"all\""} +markers = {main = "extra == \"all\" or extra == \"parser\""} [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.41.5" +pydantic-core = "2.46.4" typing-extensions = ">=4.14.1" typing-inspection = ">=0.4.2" @@ -3423,150 +3527,149 @@ timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.4" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, - {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, -] -markers = {main = "extra == \"parser\" or extra == \"all\""} + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983"}, + {file = "pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1"}, +] +markers = {main = "extra == \"all\" or extra == \"parser\""} [package.dependencies] typing-extensions = ">=4.14.1" [[package]] name = "pydantic-settings" -version = "2.13.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.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237"}, - {file = "pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025"}, + {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] @@ -3575,7 +3678,7 @@ python-dotenv = ">=0.21.0" typing-inspection = ">=0.4.0" [package.extras] -aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"] +aws-secrets-manager = ["boto3 (>=1.35.0)", "types-boto3[secretsmanager]"] azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] toml = ["tomli (>=2.0.1)"] @@ -3598,14 +3701,14 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymdown-extensions" -version = "10.21.2" +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.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638"}, - {file = "pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc"}, + {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] @@ -3617,14 +3720,14 @@ extra = ["pygments (>=2.19.1)"] [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, - {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, + {file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"}, + {file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"}, ] [package.dependencies] @@ -3641,39 +3744,39 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests [[package]] name = "pytest-asyncio" -version = "1.3.0" +version = "1.4.0" description = "Pytest support for asyncio" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5"}, - {file = "pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5"}, + {file = "pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1"}, + {file = "pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42"}, ] [package.dependencies] backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} -pytest = ">=8.2,<10" +pytest = ">=8.4,<10" typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} [package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)", "sphinx-tabs (>=3.5)"] 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] @@ -3721,18 +3824,18 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "pytest-socket" -version = "0.7.0" +version = "0.8.1" description = "Pytest Plugin to disable socket calls during tests" optional = false -python-versions = ">=3.8,<4.0" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pytest_socket-0.7.0-py3-none-any.whl", hash = "sha256:7e0f4642177d55d317bbd58fc68c6bd9048d6eadb2d46a89307fa9221336ce45"}, - {file = "pytest_socket-0.7.0.tar.gz", hash = "sha256:71ab048cbbcb085c15a4423b73b619a8b35d6a307f46f78ea46be51b1b7e11b3"}, + {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] -pytest = ">=6.2.5" +pytest = ">=7.0.0" [[package]] name = "pytest-xdist" @@ -3766,6 +3869,7 @@ files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] +markers = {main = "extra == \"all\" or extra == \"datamasking\" or extra == \"aws-sdk\" or extra == \"tracer\""} [package.dependencies] six = ">=1.5" @@ -3956,14 +4060,14 @@ toml = ["tomli (>=2.0.1)"] [[package]] name = "redis" -version = "7.3.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.3.0-py3-none-any.whl", hash = "sha256:9d4fcb002a12a5e3c3fbe005d59c48a2cc231f87fbb2f6b70c2d89bb64fec364"}, - {file = "redis-7.3.0.tar.gz", hash = "sha256:4d1b768aafcf41b01022410b3cc4f15a07d9b3d6fe0c66fc967da2c88e551034"}, + {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\""} @@ -3973,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)"] @@ -4121,15 +4225,16 @@ 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\""} [package.dependencies] certifi = ">=2023.5.7" @@ -4301,30 +4406,30 @@ files = [ [[package]] name = "ruff" -version = "0.15.10" +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.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f"}, - {file = "ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e"}, - {file = "ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1"}, - {file = "ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e"}, - {file = "ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1"}, - {file = "ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef"}, - {file = "ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158"}, - {file = "ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0"}, - {file = "ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609"}, - {file = "ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f"}, - {file = "ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151"}, - {file = "ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8"}, - {file = "ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07"}, - {file = "ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48"}, - {file = "ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5"}, - {file = "ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed"}, - {file = "ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188"}, - {file = "ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e"}, + {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]] @@ -4338,12 +4443,13 @@ files = [ {file = "s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe"}, {file = "s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920"}, ] +markers = {main = "extra == \"all\" or extra == \"datamasking\" or extra == \"aws-sdk\""} [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 = "scantree" @@ -4363,14 +4469,14 @@ pathspec = ">=0.10.1" [[package]] name = "sentry-sdk" -version = "2.57.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.57.0-py2.py3-none-any.whl", hash = "sha256:812c8bf5ff3d2f0e89c82f5ce80ab3a6423e102729c4706af7413fd1eb480585"}, - {file = "sentry_sdk-2.57.0.tar.gz", hash = "sha256:4be8d1e71c32fb27f79c577a337ac8912137bba4bcbc64a4ec1da4d6d8dc5199"}, + {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] @@ -4436,6 +4542,7 @@ files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] +markers = {main = "extra == \"all\" or extra == \"datamasking\" or extra == \"aws-sdk\" or extra == \"tracer\""} [[package]] name = "smmap" @@ -4462,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]] @@ -4506,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] @@ -4531,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)"] @@ -4541,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"] @@ -4618,31 +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.29" +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.29-py3-none-linux_armv6l.whl", hash = "sha256:b8a40955f7660d3eaceb0d964affc81b790c0765e7052921a5f861ff8a471c30"}, - {file = "ty-0.0.29-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6b6849adae15b00bbe2d3c5b078967dcb62eba37d38936b8eeb4c81a82d2e3b8"}, - {file = "ty-0.0.29-py3-none-macosx_11_0_arm64.whl", hash = "sha256:dcdd9b17209788152f7b7ea815eda07989152325052fe690013537cc7904ce49"}, - {file = "ty-0.0.29-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d8ed4789bae78ffaf94462c0d25589a734cab0366b86f2bbcb1bb90e1a7a169"}, - {file = "ty-0.0.29-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91ec374b8565e0ad0900011c24641ebbef2da51adbd4fb69ff3280c8a7eceb02"}, - {file = "ty-0.0.29-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:298a8d5faa2502d3810bbbb47a030b9455495b9921594206043c785dd61548cf"}, - {file = "ty-0.0.29-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c8fba1a3524c6109d1e020d92301c79d41bf442fa8d335b9fa366239339cb70"}, - {file = "ty-0.0.29-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c48adf88a70d264128c39ee922ed14a947817fced1e93c08c1a89c9244edcde"}, - {file = "ty-0.0.29-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ce0a7a0e96bc7b42518cd3a1a6a6298ef64ff40ca4614355c1aa807059b5c6f"}, - {file = "ty-0.0.29-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6ac86a05b4a3731d45365ab97780acc7b8146fa62fccb3cbe94fe6546c67a97"}, - {file = "ty-0.0.29-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6bbbf53141af0f3150bf288d716263f1a3550054e4b3551ca866d38192ba9891"}, - {file = "ty-0.0.29-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1c9e06b770c1d0ff5efc51e34312390db31d53fcf3088163f413030b42b74f84"}, - {file = "ty-0.0.29-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0307fe37e3f000ef1a4ae230bbaf511508a78d24a5e51b40902a21b09d5e6037"}, - {file = "ty-0.0.29-py3-none-win32.whl", hash = "sha256:7a2a898217960a825f8bc0087e1fdbaf379606175e98f9807187221d53a4a8ed"}, - {file = "ty-0.0.29-py3-none-win_amd64.whl", hash = "sha256:fc1294200226b91615acbf34e0a9ad81caf98c081e9c6a912a31b0a7b603bc3f"}, - {file = "ty-0.0.29-py3-none-win_arm64.whl", hash = "sha256:f9794bbd1bb3ce13f78c191d0c89ae4c63f52c12b6daa0c6fe220b90d019d12c"}, - {file = "ty-0.0.29.tar.gz", hash = "sha256:e7936cca2f691eeda631876c92809688dbbab68687c3473f526cd83b6a9228d8"}, + {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]] @@ -4690,14 +4834,14 @@ types-setuptools = "*" [[package]] name = "types-protobuf" -version = "7.34.1.20260403" +version = "7.34.1.20260518" description = "Typing stubs for protobuf" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "types_protobuf-7.34.1.20260403-py3-none-any.whl", hash = "sha256:16d9bbca52ab0f306279958878567df2520f3f5579059419b0ce149a0ad1e332"}, - {file = "types_protobuf-7.34.1.20260403.tar.gz", hash = "sha256:8d7881867888e667eb9563c08a916fccdc12bdb5f9f34c31d217cce876e36765"}, + {file = "types_protobuf-7.34.1.20260518-py3-none-any.whl", hash = "sha256:a0a5337413347166439c0e07cbc26c6164d091401c6f01b1dfd8cdb966c4dd8f"}, + {file = "types_protobuf-7.34.1.20260518.tar.gz", hash = "sha256:28cfaded25889cb83ebfb63cfb0a43628f0b6f3785767bec17287dc6468795f2"}, ] [[package]] @@ -4718,14 +4862,14 @@ types-cffi = "*" [[package]] name = "types-python-dateutil" -version = "2.9.0.20260408" +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.20260408-py3-none-any.whl", hash = "sha256:473139d514a71c9d1fbd8bb328974bedcb1cc3dba57aad04ffa4157f483c216f"}, - {file = "types_python_dateutil-2.9.0.20260408.tar.gz", hash = "sha256:8b056ec01568674235f64ecbcef928972a5fac412f5aab09c516dfa2acfbb582"}, + {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]] @@ -4746,14 +4890,14 @@ types-pyOpenSSL = "*" [[package]] name = "types-requests" -version = "2.33.0.20260402" +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.20260402-py3-none-any.whl", hash = "sha256:c98372d7124dd5d10af815ee25c013897592ff92af27b27e22c98984102c3254"}, - {file = "types_requests-2.33.0.20260402.tar.gz", hash = "sha256:1bdd3ada9b869741c5c4b887d2c8b4e38284a1449751823b5ebbccba3eefd9da"}, + {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] @@ -4806,111 +4950,127 @@ files = [ {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, ] -markers = {main = "extra == \"parser\" or extra == \"all\""} +markers = {main = "extra == \"all\" or extra == \"parser\""} [package.dependencies] typing-extensions = ">=4.12.0" [[package]] name = "ujson" -version = "5.12.0" +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.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:38051f36423f084b909aaadb3b41c9c6a2958e86956ba21a8489636911e87504"}, - {file = "ujson-5.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:457fabc2700a8e6ddb85bc5a1d30d3345fe0d3ec3ee8161a4e032ec585801dfa"}, - {file = "ujson-5.12.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57930ac9519099b852e190d2c04b1fb5d97ea128db33bce77ed874eccb4c7f09"}, - {file = "ujson-5.12.0-cp310-cp310-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:9b3b86ec3e818f3dd3e13a9de628e88a9990f4af68ecb0b12dd3de81227f0a26"}, - {file = "ujson-5.12.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:460e76a4daff214ae33ab959494962c93918cb44714ea3e3f748b14aa37f8a87"}, - {file = "ujson-5.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e584d0cdd37cac355aca52ed788d1a2d939d6837e2870d3b70e585db24025a50"}, - {file = "ujson-5.12.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0fe9128e75c6aa6e9ae06c1408d6edd9179a2fef0fe6d9cda3166b887eba521d"}, - {file = "ujson-5.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3ed5cb149892141b1e77ef312924a327f2cc718b34247dae346ed66329e1b8be"}, - {file = "ujson-5.12.0-cp310-cp310-win32.whl", hash = "sha256:973b7d7145b1ac553a7466a64afa8b31ec2693d7c7fff6a755059e0a2885dfd2"}, - {file = "ujson-5.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:1d072a403d82aef8090c6d4f728e3a727dfdba1ad3b7fa3a052c3ecbd37e73cb"}, - {file = "ujson-5.12.0-cp310-cp310-win_arm64.whl", hash = "sha256:55ede2a7a051b3b7e71a394978a098d71b3783e6b904702ff45483fad434ae2d"}, - {file = "ujson-5.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:58a11cb49482f1a095a2bd9a1d81dd7c8fb5d2357f959ece85db4e46a825fd00"}, - {file = "ujson-5.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9b3cf13facf6f77c283af0e1713e5e8c47a0fe295af81326cb3cb4380212e797"}, - {file = "ujson-5.12.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb94245a715b4d6e24689de12772b85329a1f9946cbf6187923a64ecdea39e65"}, - {file = "ujson-5.12.0-cp311-cp311-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:0fe6b8b8968e11dd9b2348bd508f0f57cf49ab3512064b36bc4117328218718e"}, - {file = "ujson-5.12.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89e302abd3749f6d6699691747969a5d85f7c73081d5ed7e2624c7bd9721a2ab"}, - {file = "ujson-5.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0727363b05ab05ee737a28f6200dc4078bce6b0508e10bd8aab507995a15df61"}, - {file = "ujson-5.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b62cb9a7501e1f5c9ffe190485501349c33e8862dde4377df774e40b8166871f"}, - {file = "ujson-5.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a6ec5bf6bc361f2f0f9644907a36ce527715b488988a8df534120e5c34eeda94"}, - {file = "ujson-5.12.0-cp311-cp311-win32.whl", hash = "sha256:006428d3813b87477d72d306c40c09f898a41b968e57b15a7d88454ecc42a3fb"}, - {file = "ujson-5.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:40aa43a7a3a8d2f05e79900858053d697a88a605e3887be178b43acbcd781161"}, - {file = "ujson-5.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:561f89cc82deeae82e37d4a4764184926fb432f740a9691563a391b13f7339a4"}, - {file = "ujson-5.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:09b4beff9cc91d445d5818632907b85fb06943b61cb346919ce202668bf6794a"}, - {file = "ujson-5.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ca0c7ce828bb76ab78b3991904b477c2fd0f711d7815c252d1ef28ff9450b052"}, - {file = "ujson-5.12.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d79c6635ccffcbfc1d5c045874ba36b594589be81d50d43472570bb8de9c57"}, - {file = "ujson-5.12.0-cp312-cp312-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:7e07f6f644d2c44d53b7a320a084eef98063651912c1b9449b5f45fcbdc6ccd2"}, - {file = "ujson-5.12.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:085b6ce182cdd6657481c7c4003a417e0655c4f6e58b76f26ee18f0ae21db827"}, - {file = "ujson-5.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:16b4fe9c97dc605f5e1887a9e1224287291e35c56cbc379f8aa44b6b7bcfe2bb"}, - {file = "ujson-5.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0d2e8db5ade3736a163906154ca686203acc7d1d30736cbf577c730d13653d84"}, - {file = "ujson-5.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93bc91fdadcf046da37a214eaa714574e7e9b1913568e93bb09527b2ceb7f759"}, - {file = "ujson-5.12.0-cp312-cp312-win32.whl", hash = "sha256:2a248750abce1c76fbd11b2e1d88b95401e72819295c3b851ec73399d6849b3d"}, - {file = "ujson-5.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:1b5c6ceb65fecd28a1d20d1eba9dbfa992612b86594e4b6d47bb580d2dd6bcb3"}, - {file = "ujson-5.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:9a5fcbe7b949f2e95c47ea8a80b410fcdf2da61c98553b45a4ee875580418b68"}, - {file = "ujson-5.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:15d416440148f3e56b9b244fdaf8a09fcf5a72e4944b8e119f5bf60417a2bfc8"}, - {file = "ujson-5.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0dd3676ea0837cd70ea1879765e9e9f6be063be0436de9b3ea4b775caf83654"}, - {file = "ujson-5.12.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7bbf05c38debc90d1a195b11340cc85cb43ab3e753dc47558a3a84a38cbc72da"}, - {file = "ujson-5.12.0-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:3c2f947e55d3c7cfe124dd4521ee481516f3007d13c6ad4bf6aeb722e190eb1b"}, - {file = "ujson-5.12.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ea6206043385343aff0b7da65cf73677f6f5e50de8f1c879e557f4298cac36a"}, - {file = "ujson-5.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb349dbba57c76eec25e5917e07f35aabaf0a33b9e67fc13d188002500106487"}, - {file = "ujson-5.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:937794042342006f707837f38d721426b11b0774d327a2a45c0bd389eb750a87"}, - {file = "ujson-5.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6ad57654570464eb1b040b5c353dee442608e06cff9102b8fcb105565a44c9ed"}, - {file = "ujson-5.12.0-cp313-cp313-win32.whl", hash = "sha256:76bf3e7406cf23a3e1ca6a23fb1fb9ea82f4f6bd226fe226e09146b0194f85dc"}, - {file = "ujson-5.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:15e555c4caca42411270b2ed2b2ebc7b3a42bb04138cef6c956e1f1d49709fe2"}, - {file = "ujson-5.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bd03472c36fa3a386a6deb887113b9e3fa40efba8203eb4fe786d3c0ccc724f6"}, - {file = "ujson-5.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85833bca01aa5cae326ac759276dc175c5fa3f7b3733b7d543cf27f2df12d1ef"}, - {file = "ujson-5.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d22cad98c2a10bbf6aa083a8980db6ed90d4285a841c4de892890c2b28286ef9"}, - {file = "ujson-5.12.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99cc80facad240b0c2fb5a633044420878aac87a8e7c348b9486450cba93f27c"}, - {file = "ujson-5.12.0-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:d1831c07bd4dce53c4b666fa846c7eba4b7c414f2e641a4585b7f50b72f502dc"}, - {file = "ujson-5.12.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e00cec383eab2406c9e006bd4edb55d284e94bb943fda558326048178d26961"}, - {file = "ujson-5.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f19b3af31d02a2e79c5f9a6deaab0fb3c116456aeb9277d11720ad433de6dfc6"}, - {file = "ujson-5.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bacbd3c69862478cbe1c7ed4325caedec580d8acf31b8ee1b9a1e02a56295cad"}, - {file = "ujson-5.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94c5f1621cbcab83c03be46441f090b68b9f307b6c7ec44d4e3f6d5997383df4"}, - {file = "ujson-5.12.0-cp314-cp314-win32.whl", hash = "sha256:e6369ac293d2cc40d52577e4fa3d75a70c1aae2d01fa3580a34a4e6eff9286b9"}, - {file = "ujson-5.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:31348a0ffbfc815ce78daac569d893349d85a0b57e1cd2cdbba50b7f333784da"}, - {file = "ujson-5.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:6879aed770557f0961b252648d36f6fdaab41079d37a2296b5649fd1b35608e0"}, - {file = "ujson-5.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ddb08b3c2f9213df1f2e3eb2fbea4963d80ec0f8de21f0b59898e34f3b3d96d"}, - {file = "ujson-5.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a3ae28f0b209be5af50b54ca3e2123a3de3a57d87b75f1e5aa3d7961e041983"}, - {file = "ujson-5.12.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30ad4359413c8821cc7b3707f7ca38aa8bc852ba3b9c5a759ee2d7740157315"}, - {file = "ujson-5.12.0-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:02f93da7a4115e24f886b04fd56df1ee8741c2ce4ea491b7ab3152f744ad8f8e"}, - {file = "ujson-5.12.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ff4ede90ed771140caa7e1890de17431763a483c54b3c1f88bd30f0cc1affc0"}, - {file = "ujson-5.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bf9cc97f05048ac8f3e02cd58f0fe62b901453c24345bfde287f4305dcc31c"}, - {file = "ujson-5.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2324d9a0502317ffc35d38e153c1b2fa9610ae03775c9d0f8d0cca7b8572b04e"}, - {file = "ujson-5.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:50524f4f6a1c839714dbaff5386a1afb245d2d5ec8213a01fbc99cea7307811e"}, - {file = "ujson-5.12.0-cp314-cp314t-win32.whl", hash = "sha256:f7a0430d765f9bda043e6aefaba5944d5f21ec43ff4774417d7e296f61917382"}, - {file = "ujson-5.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ccbfd94e59aad4a2566c71912b55f0547ac1680bfac25eb138e6703eb3dd434e"}, - {file = "ujson-5.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:42d875388fbd091c7ea01edfff260f839ba303038ffb23475ef392012e4d63dd"}, - {file = "ujson-5.12.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:bf85a00ac3b56a1e7a19c5be7b02b5180a0895ac4d3c234d717a55e86960691c"}, - {file = "ujson-5.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:64df53eef4ac857eb5816a56e2885ccf0d7dff6333c94065c93b39c51063e01d"}, - {file = "ujson-5.12.0-graalpy312-graalpy250_312_native-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c0aed6a4439994c9666fb8a5b6c4eac94d4ef6ddc95f9b806a599ef83547e3b"}, - {file = "ujson-5.12.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efae5df7a8cc8bdb1037b0f786b044ce281081441df5418c3a0f0e1f86fe7bb3"}, - {file = "ujson-5.12.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:8712b61eb1b74a4478cfd1c54f576056199e9f093659334aeb5c4a6b385338e5"}, - {file = "ujson-5.12.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:871c0e5102e47995b0e37e8df7819a894a6c3da0d097545cd1f9f1f7d7079927"}, - {file = "ujson-5.12.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:56ba3f7abbd6b0bb282a544dc38406d1a188d8bb9164f49fdb9c2fee62cb29da"}, - {file = "ujson-5.12.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c5a52987a990eb1bae55f9000994f1afdb0326c154fb089992f839ab3c30688"}, - {file = "ujson-5.12.0-pp311-pypy311_pp73-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:adf28d13a33f9d750fe7a78fb481cac298fa257d8863d8727b2ea4455ea41235"}, - {file = "ujson-5.12.0-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51acc750ec7a2df786cdc868fb16fa04abd6269a01d58cf59bafc57978773d8e"}, - {file = "ujson-5.12.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ab9056d94e5db513d9313b34394f3a3b83e6301a581c28ad67773434f3faccab"}, - {file = "ujson-5.12.0.tar.gz", hash = "sha256:14b2e1eb528d77bc0f4c5bd1a7ebc05e02b5b41beefb7e8567c9675b8b13bcf4"}, + {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]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, - {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, + {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, + {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, ] +markers = {main = "extra == \"all\" or extra == \"datamasking\" or extra == \"aws-sdk\" or extra == \"tracer\" or extra == \"datadog\""} [package.extras] brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] @@ -4920,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\""} @@ -5124,7 +5277,7 @@ files = [ {file = "wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22"}, {file = "wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0"}, ] -markers = {main = "extra == \"tracer\" or extra == \"all\" or extra == \"datamasking\" or extra == \"datadog\""} +markers = {main = "extra == \"all\" or extra == \"datamasking\" or extra == \"tracer\" or extra == \"datadog\""} [[package]] name = "xenon" @@ -5180,4 +5333,4 @@ valkey = ["valkey-glide"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0.0" -content-hash = "6413dbddcb0a105cd5a278bf10d8e0eaca15eb825ef1d92619b94531c8e76375" +content-hash = "368940a68ae8b3bbb72ab67bbb91e4da4170e07a7faf28011343cfcb809ce84a" diff --git a/pyproject.toml b/pyproject.toml index e75809594be..0510319e5f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aws_lambda_powertools" -version = "3.28.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.1.1,<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 } @@ -86,7 +86,7 @@ boto3 = "^1.26.164" isort = ">=5.13.2,<9.0.0" pytest-cov = ">=5,<8" pytest-mock = "^3.14.0" -pytest-asyncio = ">=0.24,<1.4" +pytest-asyncio = ">=0.24,<1.5" bandit = "^1.7.10" radon = "^6.0.1" xenon = "^0.9.3" @@ -111,25 +111,25 @@ aws-requests-auth = "^0.4.3" urllib3 = ">=1.25.4,!=2.2.0,<3" requests = ">=2.32.0" cfn-lint = "1.48.1" -mypy = "^1.1.1" +mypy = ">=1.1.1,<3.0.0" 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.11" +ruff = ">=0.5.1,<0.16.5" retry2 = "^0.9.5" -pytest-socket = ">=0.6,<0.8" +pytest-socket = ">=0.6,<0.9" types-redis = "^4.6.0.7" 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.30" +ty = ">=0.0.23,<0.0.50" [tool.coverage.run] source = ["aws_lambda_powertools"] @@ -241,5 +241,6 @@ exclude = [ "aws_lambda_powertools/tracing/**", "aws_lambda_powertools/utilities/batch/**", "aws_lambda_powertools/utilities/idempotency/**", + "aws_lambda_powertools/utilities/parameters/**", "aws_lambda_powertools/event_handler/**", ] 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 785d2b8416c..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 @@ -232,7 +232,7 @@ def handler(): schema = app.get_openapi_schema() responses = schema.paths["/"].get.responses - # THE the schema should include a 200 successful response + # The schema should include a 200 successful response # but not a 422 validation error response since validation is disabled assert 200 in responses.keys() assert responses[200].description == "Successful Response" @@ -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 0df8f6a22c5..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,9 +1,9 @@ import json import warnings -from typing import Literal, Optional +from typing import Literal import pytest -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, computed_field from typing_extensions import Annotated from aws_lambda_powertools.event_handler import APIGatewayRestResolver @@ -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 @@ -110,3 +110,79 @@ def create_todo(todo: TodoEnvelope): ... # THEN the schema should be valid assert openapi31_schema(schema) + + +@pytest.mark.usefixtures("pydanticv2_only") +def test_openapi_schema_includes_computed_field(): + # GIVEN a model with a computed_field + class User(BaseModel): + first_name: str + last_name: str + + @computed_field + @property + def full_name(self) -> str: + return f"{self.first_name} {self.last_name}" + + # GIVEN APIGatewayRestResolver with a handler returning that model + app = APIGatewayRestResolver(enable_validation=True) + + @app.get("/user") + def get_user() -> User: + return User(first_name="John", last_name="Doe") + + # WHEN we get the schema + schema = json.loads(app.get_openapi_json_schema()) + + # THEN the computed_field should appear in the response schema + user_schema = schema["components"]["schemas"]["User"] + assert "full_name" in user_schema["properties"] + assert user_schema["properties"]["full_name"]["type"] == "string" + assert user_schema["properties"]["full_name"].get("readOnly") is True + + +@pytest.mark.usefixtures("pydanticv2_only") +def test_openapi_schema_computed_field_not_in_request_body(): + # GIVEN a model with a computed_field used as both request and response + class Item(BaseModel): + price: float + quantity: int + + @computed_field + @property + def total(self) -> float: + return self.price * self.quantity + + # GIVEN APIGatewayRestResolver with handlers using the model + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/items") + def create_item(item: Item) -> Item: + return item + + # WHEN we get the schema + schema = json.loads(app.get_openapi_json_schema()) + + # THEN the request body schema should NOT include computed_field + request_body = schema["paths"]["/items"]["post"]["requestBody"] + request_ref = request_body["content"]["application/json"]["schema"]["$ref"] + request_schema_name = request_ref.split("/")[-1] + + # THEN the response schema SHOULD include computed_field + response_ref = schema["paths"]["/items"]["post"]["responses"]["200"]["content"]["application/json"]["schema"][ + "$ref" + ] + response_schema_name = response_ref.split("/")[-1] + + # When input/output schemas are separate, we expect different schema names + # When they share a schema, computed_field should be present + if request_schema_name == response_schema_name: + # Shared schema - computed_field should be present (serialization mode wins) + item_schema = schema["components"]["schemas"][response_schema_name] + assert "total" in item_schema["properties"] + else: + # Separate schemas + input_schema = schema["components"]["schemas"][request_schema_name] + output_schema = schema["components"]["schemas"][response_schema_name] + assert "total" not in input_schema["properties"] + assert "total" in output_schema["properties"] 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 e7199adc9c5..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) @@ -4227,3 +4319,39 @@ def handler(session_id: Annotated[str, Cookie()]): assert result["statusCode"] == 200 body = json.loads(result["body"]) assert body["session_id"] == "lattice_v1_abc" + + +def test_alb_response_none_body_with_validation(gw_event_alb): + # GIVEN an ALBResolver with validation enabled + app = ALBResolver(enable_validation=True) + + gw_event_alb["path"] = "/no-content" + gw_event_alb["httpMethod"] = "DELETE" + + # WHEN a handler returns Response with body=None and return type is None + @app.delete("/no-content") + def handler() -> None: + return Response(status_code=204, body=None) + + # THEN the response should be 204 with empty body (not 422 validation error) + result = app(gw_event_alb, {}) + assert result["statusCode"] == 204 + assert result["body"] == "" + + +def test_alb_response_typed_none_body_with_validation(gw_event_alb): + # GIVEN an ALBResolver with validation enabled + app = ALBResolver(enable_validation=True) + + gw_event_alb["path"] = "/no-content" + gw_event_alb["httpMethod"] = "DELETE" + + # WHEN a handler returns Response[None] with body=None + @app.delete("/no-content") + def handler() -> Response[None]: + return Response(status_code=204, body=None) + + # THEN the response should be 204 with empty body (not 422 validation error) + result = app(gw_event_alb, {}) + assert result["statusCode"] == 204 + assert result["body"] == "" diff --git a/tests/functional/event_handler/_pydantic/test_resolve_async_validation.py b/tests/functional/event_handler/_pydantic/test_resolve_async_validation.py new file mode 100644 index 00000000000..92b414f72b5 --- /dev/null +++ b/tests/functional/event_handler/_pydantic/test_resolve_async_validation.py @@ -0,0 +1,55 @@ +import asyncio + +from aws_lambda_powertools.event_handler.api_gateway import ( + APIGatewayHttpResolver, + BaseRouter, +) +from tests.functional.utils import load_event + +API_RESTV2_EVENT = load_event("apiGatewayProxyV2Event_GET.json") + + +def _setup_app(app, event): + BaseRouter.current_event = app._to_proxy_event(event) + BaseRouter.lambda_context = {} + + +class TestResolveAsyncValidation: + def test_validation_middleware_created_and_used(self): + # GIVEN a resolver with validation enabled and an async handler + app = APIGatewayHttpResolver(enable_validation=True) + + @app.get("/my/path") + async def get_lambda() -> dict: + await asyncio.sleep(0) + return {"message": "validated"} + + # WHEN calling _resolve_async + _setup_app(app, API_RESTV2_EVENT) + result = asyncio.run(app._resolve_async()) + + # THEN the validation middlewares are created and the response is valid + response = result.build(app.current_event, app._cors) + assert response["statusCode"] == 200 + assert hasattr(app, "_request_validation_middleware") + assert hasattr(app, "_response_validation_middleware") + + def test_validation_middleware_lazy_created_for_per_route_validation(self): + # GIVEN a resolver WITHOUT global validation, but a route WITH enable_validation=True + app = APIGatewayHttpResolver() + assert not hasattr(app, "_request_validation_middleware") + + @app.get("/my/path", enable_validation=True) + async def get_lambda() -> dict: + await asyncio.sleep(0) + return {"message": "lazy validated"} + + # WHEN calling _resolve_async (triggers lazy creation in Route.call_async) + _setup_app(app, API_RESTV2_EVENT) + result = asyncio.run(app._resolve_async()) + + # THEN validation middlewares are lazily created on the app + response = result.build(app.current_event, app._cors) + assert response["statusCode"] == 200 + assert hasattr(app, "_request_validation_middleware") + assert hasattr(app, "_response_validation_middleware") diff --git a/tests/functional/event_handler/required_dependencies/test_async_middleware_frame.py b/tests/functional/event_handler/required_dependencies/test_async_middleware_frame.py new file mode 100644 index 00000000000..6154820454d --- /dev/null +++ b/tests/functional/event_handler/required_dependencies/test_async_middleware_frame.py @@ -0,0 +1,89 @@ +import asyncio + +import pytest + +from aws_lambda_powertools.event_handler import content_types +from aws_lambda_powertools.event_handler.api_gateway import ( + ApiGatewayResolver, + ProxyEventType, + Response, +) +from aws_lambda_powertools.event_handler.middlewares import NextMiddleware +from aws_lambda_powertools.event_handler.middlewares.async_utils import AsyncMiddlewareFrame, wrap_middleware_async +from tests.functional.utils import load_event + +API_REST_EVENT = load_event("apiGatewayProxyEvent.json") + + +def _make_app() -> ApiGatewayResolver: + app = ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent) + app.current_event = app._to_proxy_event(API_REST_EVENT) + app.lambda_context = {} + return app + + +def test_sync_middleware_raising_before_next_does_not_deadlock(): + # GIVEN a sync middleware that raises before calling next() + # This previously caused a deadlock because middleware_called_next was never set + app = _make_app() + + class AuthError(Exception): + pass + + def failing_middleware(app: ApiGatewayResolver, next_middleware: NextMiddleware): + raise AuthError("denied") + + async def next_handler(app: ApiGatewayResolver): + await asyncio.sleep(0) + return Response(200, content_types.TEXT_HTML, "should not reach") + + frame = AsyncMiddlewareFrame(current_middleware=failing_middleware, next_middleware=next_handler) + + # WHEN calling the frame + # THEN the exception propagates without deadlocking + with pytest.raises(AuthError, match="denied"): + asyncio.run(frame(app)) + + +def test_wrap_middleware_async_sync_raising_before_next_does_not_deadlock(): + # GIVEN a sync middleware that raises before calling next(), using wrap_middleware_async + # This exercises _run_sync_middleware_in_thread directly + app = _make_app() + + class AuthError(Exception): + pass + + def failing_middleware(app, next_middleware): + raise AuthError("denied") + + async def next_handler(app): + return Response(200, content_types.TEXT_HTML, "should not reach") + + wrapped = wrap_middleware_async(failing_middleware, next_handler) + + # WHEN calling the wrapped middleware + # THEN the exception propagates without deadlocking + with pytest.raises(AuthError, match="denied"): + asyncio.run(wrapped(app)) + + +def test_async_middleware_raising_before_next_propagates(): + # GIVEN an async middleware that raises before calling next() + app = _make_app() + + class ValidationError(Exception): + pass + + async def failing_middleware(app: ApiGatewayResolver, next_middleware: NextMiddleware): + raise ValidationError("invalid request") + + async def next_handler(app: ApiGatewayResolver): + await asyncio.sleep(0) + return Response(200, content_types.TEXT_HTML, "should not reach") + + frame = AsyncMiddlewareFrame(current_middleware=failing_middleware, next_middleware=next_handler) + + # WHEN calling the frame + # THEN the exception propagates + with pytest.raises(ValidationError, match="invalid request"): + asyncio.run(frame(app)) 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/event_handler/required_dependencies/test_resolve_async.py b/tests/functional/event_handler/required_dependencies/test_resolve_async.py new file mode 100644 index 00000000000..e9b12ce2a2d --- /dev/null +++ b/tests/functional/event_handler/required_dependencies/test_resolve_async.py @@ -0,0 +1,565 @@ +import asyncio +import json + +import pytest + +from aws_lambda_powertools.event_handler import content_types +from aws_lambda_powertools.event_handler.api_gateway import ( + ALBResolver, + APIGatewayHttpResolver, + ApiGatewayResolver, + APIGatewayRestResolver, + BaseRouter, + CORSConfig, + ProxyEventType, + Response, +) +from aws_lambda_powertools.event_handler.middlewares import NextMiddleware +from tests.functional.utils import load_event + +API_REST_EVENT = load_event("apiGatewayProxyEvent.json") +API_RESTV2_EVENT = load_event("apiGatewayProxyV2Event_GET.json") +ALB_EVENT = load_event("albEvent.json") + + +def _setup_app(app, event): + BaseRouter.current_event = app._to_proxy_event(event) + BaseRouter.lambda_context = {} + + +RESOLVER_IDS = ["ApiGatewayResolver", "APIGatewayRestResolver", "APIGatewayHttpResolver", "ALBResolver"] + + +@pytest.fixture( + params=[ + ("apigw_v1", API_REST_EVENT, "/my/path"), + ("apigw_rest", API_REST_EVENT, "/my/path"), + ("apigw_v2", API_RESTV2_EVENT, "/my/path"), + ("alb", ALB_EVENT, "/lambda"), + ], + ids=RESOLVER_IDS, +) +def resolver_and_event(request): + key, event, path = request.param + resolvers = { + "apigw_v1": ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent), + "apigw_rest": APIGatewayRestResolver(), + "apigw_v2": APIGatewayHttpResolver(), + "alb": ALBResolver(), + } + return resolvers[key], event, path + + +class TestResolveAsyncWithAsyncHandlers: + def test_async_handler_through_resolve_chain(self, resolver_and_event): + # GIVEN an async handler registered on the resolver + app, event, path = resolver_and_event + + @app.get(path) + async def get_lambda(): + await asyncio.sleep(0) + return Response(200, content_types.TEXT_HTML, "async works") + + # WHEN calling _resolve_async after setting up context + _setup_app(app, event) + result = asyncio.run(app._resolve_async()) + + # THEN the async handler is awaited and returns a ResponseBuilder + response = result.build(app.current_event, app._cors) + assert response["statusCode"] == 200 + assert response["body"] == "async works" + + def test_async_handler_returning_dict(self, resolver_and_event): + # GIVEN an async handler that returns a dict + app, event, path = resolver_and_event + + @app.get(path) + async def get_lambda(): + await asyncio.sleep(0) + return {"message": "hello"} + + # WHEN calling _resolve_async + _setup_app(app, event) + result = asyncio.run(app._resolve_async()) + + # THEN the dict is normalized into a Response + response = result.build(app.current_event, app._cors) + assert response["statusCode"] == 200 + + def test_async_handler_returning_tuple(self, resolver_and_event): + # GIVEN an async handler that returns a (dict, status_code) tuple + app, event, path = resolver_and_event + + @app.get(path) + async def get_lambda(): + await asyncio.sleep(0) + return {"created": True}, 201 + + # WHEN calling _resolve_async + _setup_app(app, event) + result = asyncio.run(app._resolve_async()) + + # THEN the tuple is normalized with the correct status code + response = result.build(app.current_event, app._cors) + assert response["statusCode"] == 201 + + +class TestResolveAsyncWithSyncHandlers: + def test_sync_handler_works_through_async_chain(self, resolver_and_event): + # GIVEN a sync handler + app, event, path = resolver_and_event + + @app.get(path) + def get_lambda(): + return Response(200, content_types.TEXT_HTML, "sync via async") + + # WHEN calling _resolve_async + _setup_app(app, event) + result = asyncio.run(app._resolve_async()) + + # THEN the sync handler works through the async chain + response = result.build(app.current_event, app._cors) + assert response["statusCode"] == 200 + assert response["body"] == "sync via async" + + +class TestResolveAsyncRouteArguments: + def test_route_args_passed_to_async_handler(self): + # GIVEN an async handler with a path parameter + app = APIGatewayHttpResolver() + + @app.get("/my/") + async def get_lambda(name: str): + await asyncio.sleep(0) + return Response(200, content_types.TEXT_HTML, name) + + # WHEN resolving a matching event + event = load_event("apiGatewayProxyV2Event_GET.json") + event["rawPath"] = "/my/powertools" + event["requestContext"]["http"]["path"] = "/my/powertools" + _setup_app(app, event) + result = asyncio.run(app._resolve_async()) + + # THEN route arguments are passed to the handler + response = result.build(app.current_event, app._cors) + assert response["statusCode"] == 200 + assert response["body"] == "powertools" + + +class TestResolveAsyncNotFound: + def test_not_found_returns_404(self, resolver_and_event): + # GIVEN no matching route + app, event, _path = resolver_and_event + + @app.get("/other/path") + async def get_lambda(): + await asyncio.sleep(0) + return Response(200, content_types.TEXT_HTML, "should not reach") + + # WHEN resolving an event with a non-matching path + _setup_app(app, event) + result = asyncio.run(app._resolve_async()) + + # THEN a 404 response is returned + response = result.build(app.current_event, app._cors) + assert response["statusCode"] == 404 + + def test_custom_not_found_handler(self): + # GIVEN a custom not_found handler + app = APIGatewayRestResolver() + + @app.not_found + def custom_not_found(exc): + return Response(404, content_types.APPLICATION_JSON, '{"error": "custom 404"}') + + @app.get("/other") + def get_lambda(): + return Response(200, content_types.TEXT_HTML, "not reached") + + # WHEN resolving with no matching route + _setup_app(app, API_REST_EVENT) + result = asyncio.run(app._resolve_async()) + + # THEN the custom handler is called + response = result.build(app.current_event, app._cors) + assert response["statusCode"] == 404 + assert response["body"] == '{"error": "custom 404"}' + + def test_cors_preflight_returns_204(self): + # GIVEN a resolver with CORS enabled + app = APIGatewayRestResolver(cors=CORSConfig()) + + @app.get("/my/path") + def get_lambda(): + return Response(200, content_types.TEXT_HTML, "ok") + + # WHEN an OPTIONS request arrives for a non-matching path + event = load_event("apiGatewayProxyEvent.json") + event["httpMethod"] = "OPTIONS" + _setup_app(app, event) + result = asyncio.run(app._resolve_async()) + + # THEN a 204 pre-flight response is returned + response = result.build(app.current_event, app._cors) + assert response["statusCode"] == 204 + + +class TestResolveAsyncExceptionHandling: + def test_exception_handler_catches_async_error(self): + # GIVEN an async handler that raises and an exception handler + app = APIGatewayRestResolver() + + @app.exception_handler(ValueError) + def handle_value_error(exc): + return Response(422, content_types.APPLICATION_JSON, '{"error": "validation failed"}') + + @app.get("/my/path") + async def get_lambda(): + await asyncio.sleep(0) + raise ValueError("bad input") + + # WHEN resolving + _setup_app(app, API_REST_EVENT) + result = asyncio.run(app._resolve_async()) + + # THEN the exception handler catches the error + response = result.build(app.current_event, app._cors) + assert response["statusCode"] == 422 + + +class TestResolveAsyncMiddleware: + def test_sync_middleware_in_async_chain(self): + # GIVEN a sync middleware + app = APIGatewayRestResolver() + + def my_middleware(app: ApiGatewayResolver, next_middleware: NextMiddleware): + app.append_context(sync_mw_called=True) + return next_middleware(app) + + @app.get("/my/path", middlewares=[my_middleware]) + async def get_lambda(): + await asyncio.sleep(0) + return Response(200, content_types.TEXT_HTML, "with middleware") + + # WHEN calling _resolve_async + _setup_app(app, API_REST_EVENT) + result = asyncio.run(app._resolve_async()) + + # THEN the sync middleware runs in the async chain + response = result.build(app.current_event, app._cors) + assert response["statusCode"] == 200 + assert response["body"] == "with middleware" + assert app.context.get("sync_mw_called") is True + + def test_async_middleware_in_async_chain(self): + # GIVEN an async middleware + app = APIGatewayRestResolver() + + async def my_middleware(app: ApiGatewayResolver, next_middleware: NextMiddleware): + app.append_context(async_mw_called=True) + return await next_middleware(app) + + @app.get("/my/path", middlewares=[my_middleware]) + async def get_lambda(): + await asyncio.sleep(0) + return Response(200, content_types.TEXT_HTML, "async mw") + + # WHEN calling _resolve_async + _setup_app(app, API_REST_EVENT) + result = asyncio.run(app._resolve_async()) + + # THEN the async middleware runs correctly + response = result.build(app.current_event, app._cors) + assert response["statusCode"] == 200 + assert app.context.get("async_mw_called") is True + + def test_not_found_goes_through_middleware(self): + # GIVEN a global middleware + middleware_called = [] + + def tracking_middleware(app: ApiGatewayResolver, next_middleware: NextMiddleware): + middleware_called.append(True) + return next_middleware(app) + + app = APIGatewayRestResolver() + app.use([tracking_middleware]) + + @app.get("/other/path") + def get_lambda(): + return Response(200, content_types.TEXT_HTML, "not reached") + + # WHEN resolving with a non-matching path + _setup_app(app, API_REST_EVENT) + result = asyncio.run(app._resolve_async()) + + # THEN the middleware still runs (404 goes through chain) + response = result.build(app.current_event, app._cors) + assert response["statusCode"] == 404 + assert len(middleware_called) > 0 + + +class TestResolveAsyncProcessedStack: + def test_processed_stack_frames_recorded(self): + # GIVEN an async handler + app = APIGatewayRestResolver() + + @app.get("/my/path") + async def get_lambda(): + await asyncio.sleep(0) + return Response(200, content_types.TEXT_HTML, "ok") + + # WHEN calling _resolve_async + _setup_app(app, API_REST_EVENT) + asyncio.run(app._resolve_async()) + + # THEN the processed stack frames are populated + assert len(app.processed_stack_frames) > 0 + assert any("_registered_api_adapter_async" in frame for frame in app.processed_stack_frames) + + +class TestResolveAsyncDebugMode: + def test_debug_mode_prints_middleware_stack(self, capsys): + # GIVEN a resolver with debug=True + app = APIGatewayRestResolver(debug=True) + + @app.get("/my/path") + async def get_lambda(): + await asyncio.sleep(0) + return Response(200, content_types.TEXT_HTML, "debug") + + # WHEN calling _resolve_async + _setup_app(app, API_REST_EVENT) + asyncio.run(app._resolve_async()) + + # THEN the async middleware stack is printed + captured = capsys.readouterr() + assert "Async Middleware Stack:" in captured.out + assert "_registered_api_adapter_async" in captured.out + + +class TestResolveAsyncExceptionNoHandler: + def test_unhandled_exception_reraises(self): + # GIVEN an async handler that raises with no matching exception handler + app = APIGatewayRestResolver() + + @app.get("/my/path") + async def get_lambda(): + await asyncio.sleep(0) + raise RuntimeError("unhandled") + + # WHEN calling _resolve_async + _setup_app(app, API_REST_EVENT) + + # THEN the exception propagates + with pytest.raises(RuntimeError, match="unhandled"): + asyncio.run(app._resolve_async()) + + def test_unhandled_exception_with_debug_returns_traceback(self): + # GIVEN a resolver with debug=True and no exception handler + app = APIGatewayRestResolver(debug=True) + + @app.get("/my/path") + async def get_lambda(): + await asyncio.sleep(0) + raise RuntimeError("debug error") + + # WHEN calling _resolve_async + _setup_app(app, API_REST_EVENT) + result = asyncio.run(app._resolve_async()) + + # THEN a 500 response with traceback is returned + response = result.build(app.current_event, app._cors) + assert response["statusCode"] == 500 + assert "debug error" in response["body"] + + +# ============================================================================ +# Public resolve_async() tests +# ============================================================================ + + +class MockLambdaContext: + function_name = "test-func" + memory_limit_in_mb = 128 + invoked_function_arn = "arn:aws:lambda:eu-west-1:123456789012:function:test-func" + aws_request_id = "52fdfc07-2182-154f-163f-5f0f9a621d72" + + def get_remaining_time_in_millis(self) -> int: + return 1000 + + +RESOLVE_ASYNC_IDS = ["APIGatewayRestResolver", "APIGatewayHttpResolver", "ALBResolver"] + + +@pytest.fixture( + params=[ + ("apigw_rest", API_REST_EVENT, "/my/path"), + ("apigw_v2", API_RESTV2_EVENT, "/my/path"), + ("alb", ALB_EVENT, "/lambda"), + ], + ids=RESOLVE_ASYNC_IDS, +) +def public_resolver_and_event(request): + key, event, path = request.param + resolvers = { + "apigw_rest": APIGatewayRestResolver(), + "apigw_v2": APIGatewayHttpResolver(), + "alb": ALBResolver(), + } + return resolvers[key], event, path + + +class TestResolveAsyncPublic: + def test_resolve_async_returns_dict_response(self, public_resolver_and_event): + # GIVEN an async handler + app, event, path = public_resolver_and_event + + @app.get(path) + async def get_lambda(): + await asyncio.sleep(0) + return Response(200, content_types.TEXT_HTML, "async public") + + # WHEN calling resolve_async with event and context + response = asyncio.run(app.resolve_async(event, MockLambdaContext())) + + # THEN a dict response is returned directly (no need to call .build()) + assert response["statusCode"] == 200 + assert response["body"] == "async public" + + def test_resolve_async_with_sync_handler(self, public_resolver_and_event): + # GIVEN a sync handler + app, event, path = public_resolver_and_event + + @app.get(path) + def get_lambda(): + return Response(200, content_types.TEXT_HTML, "sync via public async") + + # WHEN calling resolve_async + response = asyncio.run(app.resolve_async(event, MockLambdaContext())) + + # THEN sync handlers work through the async chain + assert response["statusCode"] == 200 + assert response["body"] == "sync via public async" + + def test_resolve_async_clears_context(self, public_resolver_and_event): + # GIVEN an async handler + app, event, path = public_resolver_and_event + + @app.get(path) + async def get_lambda(): + app.append_context(custom_key="value") + return Response(200, content_types.TEXT_HTML, "ok") + + # WHEN calling resolve_async + asyncio.run(app.resolve_async(event, MockLambdaContext())) + + # THEN the context is cleared after resolution + assert app.context == {} + + def test_resolve_async_not_found(self, public_resolver_and_event): + # GIVEN no matching route + app, event, _path = public_resolver_and_event + + @app.get("/non/existent/path") + async def get_lambda(): + return Response(200, content_types.TEXT_HTML, "unreachable") + + # WHEN calling resolve_async + response = asyncio.run(app.resolve_async(event, MockLambdaContext())) + + # THEN a 404 response is returned + assert response["statusCode"] == 404 + + def test_resolve_async_with_cors(self): + # GIVEN a resolver with CORS and an async handler + app = APIGatewayRestResolver(cors=CORSConfig()) + + @app.get("/my/path") + async def get_lambda(): + return Response(200, content_types.TEXT_HTML, "cors") + + # WHEN calling resolve_async + response = asyncio.run(app.resolve_async(API_REST_EVENT, MockLambdaContext())) + + # THEN CORS headers are included + assert response["statusCode"] == 200 + assert "Access-Control-Allow-Origin" in response.get("multiValueHeaders", response.get("headers", {})) + + def test_resolve_async_with_middleware(self): + # GIVEN a resolver with a middleware + app = APIGatewayRestResolver() + middleware_order = [] + + def tracking_middleware(app: ApiGatewayResolver, next_middleware: NextMiddleware): + middleware_order.append("before") + result = next_middleware(app) + middleware_order.append("after") + return result + + @app.get("/my/path", middlewares=[tracking_middleware]) + async def get_lambda(): + middleware_order.append("handler") + return Response(200, content_types.TEXT_HTML, "ok") + + # WHEN calling resolve_async + response = asyncio.run(app.resolve_async(API_REST_EVENT, MockLambdaContext())) + + # THEN middleware runs in correct order around the handler + assert response["statusCode"] == 200 + assert middleware_order == ["before", "handler", "after"] + + def test_resolve_async_exception_handler(self): + # GIVEN an async handler that raises with an exception handler registered + app = APIGatewayRestResolver() + + @app.exception_handler(ValueError) + def handle_value_error(exc): + return Response(422, content_types.APPLICATION_JSON, json.dumps({"error": str(exc)})) + + @app.get("/my/path") + async def get_lambda(): + raise ValueError("invalid input") + + # WHEN calling resolve_async + response = asyncio.run(app.resolve_async(API_REST_EVENT, MockLambdaContext())) + + # THEN the exception handler catches the error + assert response["statusCode"] == 422 + assert "invalid input" in response["body"] + + def test_resolve_async_debug_mode(self, capsys): + # GIVEN a resolver with debug=True + app = APIGatewayRestResolver(debug=True) + + @app.get("/my/path") + async def get_lambda(): + return Response(200, content_types.TEXT_HTML, "debug") + + # WHEN calling resolve_async + response = asyncio.run(app.resolve_async(API_REST_EVENT, MockLambdaContext())) + + # THEN debug output includes raw event and middleware stack + captured = capsys.readouterr() + assert response["statusCode"] == 200 + assert "Processed Middlewares:" in captured.out + assert "httpMethod" in captured.out + + def test_resolve_async_with_base_proxy_event(self): + # GIVEN a resolver and a BaseProxyEvent passed directly + from aws_lambda_powertools.utilities.data_classes import APIGatewayProxyEvent + + app = APIGatewayRestResolver() + + @app.get("/my/path") + async def get_lambda(): + return Response(200, content_types.TEXT_HTML, "from proxy event") + + # WHEN calling resolve_async with a data class instead of raw dict + proxy_event = APIGatewayProxyEvent(API_REST_EVENT) + + with pytest.warns(UserWarning, match="You don't need to serialize event"): + response = asyncio.run(app.resolve_async(proxy_event, MockLambdaContext())) + + # THEN it still works after extracting raw_event + assert response["statusCode"] == 200 + assert response["body"] == "from proxy event" 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 c2a0976b0ab..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") @@ -330,6 +409,25 @@ def test_item_to_datarecord_conversion(valid_record): assert record.in_progress_expiry_timestamp == item[layer.in_progress_expiry_attr] +def test_item_to_datarecord_conversion_missing_optional_attributes(persistence_store_standalone_redis): + """ + When data_attr or validation_key_attr is missing from Redis, + response_data and payload_hash should be empty string, not the string "None". + Regression test for: https://github.com/aws-powertools/powertools-lambda-python/issues/8090 + """ + idempotency_key = "test-func#abc123" + item = { + persistence_store_standalone_redis.status_attr: "COMPLETED", + persistence_store_standalone_redis.expiry_attr: 9999999999, + # data_attr and validation_key_attr intentionally absent + } + + record = persistence_store_standalone_redis._item_to_data_record(idempotency_key, item) + + assert record.response_data == "" + assert record.payload_hash == "" + + def test_idempotent_function_and_lambda_handler_redis_basic( persistence_store_standalone_redis: RedisCachePersistenceLayer, lambda_context, 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 1c37b95e202..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.1.1 +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 1c37b95e202..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.1.1 +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 1c37b95e202..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.1.1 +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():