diff --git a/.github/workflows/bootstrap_region.yml b/.github/workflows/bootstrap_region.yml index b46c4020c0c..dc8c142efd5 100644 --- a/.github/workflows/bootstrap_region.yml +++ b/.github/workflows/bootstrap_region.yml @@ -48,14 +48,14 @@ jobs: with: ref: ${{ github.sha }} - name: Setup Node.js - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "22" - name: Setup dependencies - uses: aws-powertools/actions/.github/actions/cached-node-modules@3b5b8e2e58b7af07994be982e83584a94e8c76c5 + uses: aws-powertools/actions/.github/actions/cached-node-modules@828e78a26eee3554dc2e1d96048004548fbb169f - id: credentials name: AWS Credentials - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 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@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 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@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: '>=1.23.0' - id: go-env diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 62612b20fa7..60ce40982bf 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -22,4 +22,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: 'Dependency Review' - uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3 + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 diff --git a/.github/workflows/layer_govcloud.yml b/.github/workflows/layer_govcloud.yml index aacdd4399a9..9daf6725808 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@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 with: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} aws-region: us-east-1 @@ -70,14 +70,14 @@ jobs: aws --region us-east-1 lambda get-layer-version-by-arn --arn arn:aws:lambda:us-east-1:017000801446:layer:${{ matrix.layer }}-${{ matrix.arch }}:${{ inputs.version }} --query 'Content.Location' | xargs curl -L -o ${{ matrix.layer }}_${{ matrix.arch }}.zip aws --region us-east-1 lambda get-layer-version-by-arn --arn arn:aws:lambda:us-east-1:017000801446:layer:${{ matrix.layer }}-${{ matrix.arch }}:${{ inputs.version }} > ${{ matrix.layer }}_${{ matrix.arch }}.json - name: Store Zip - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ matrix.layer }}_${{ matrix.arch }}.zip path: ${{ matrix.layer }}_${{ matrix.arch }}.zip retention-days: 1 if-no-files-found: error - name: Store Metadata - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ matrix.layer }}_${{ matrix.arch }}.json path: ${{ matrix.layer }}_${{ matrix.arch }}.json @@ -106,11 +106,11 @@ jobs: environment: GovCloud ${{ inputs.environment }} (East) steps: - name: Download Zip - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ matrix.layer }}_${{ matrix.arch }}.zip - name: Download Metadata - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ matrix.layer }}_${{ matrix.arch }}.json - name: Verify Layer Signature @@ -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@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 with: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} aws-region: us-gov-east-1 @@ -176,11 +176,11 @@ jobs: name: GovCloud ${{ inputs.environment }} (West) steps: - name: Download Zip - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ matrix.layer }}_${{ matrix.arch }}.zip - name: Download Metadata - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ matrix.layer }}_${{ matrix.arch }}.json - name: Verify Layer Signature @@ -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@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 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 88cbd692333..1dc2f4242d2 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@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 with: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} aws-region: us-east-1 @@ -65,14 +65,14 @@ jobs: aws --region us-east-1 lambda get-layer-version-by-arn --arn arn:aws:lambda:us-east-1:017000801446:layer:${{ matrix.layer }}-${{ matrix.arch }}:${{ inputs.version }} --query 'Content.Location' | xargs curl -L -o ${{ matrix.layer }}_${{ matrix.arch }}.zip aws --region us-east-1 lambda get-layer-version-by-arn --arn arn:aws:lambda:us-east-1:017000801446:layer:${{ matrix.layer }}-${{ matrix.arch }}:${{ inputs.version }} > ${{ matrix.layer }}_${{ matrix.arch }}.json - name: Store Zip - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ matrix.layer }}_${{ matrix.arch }}.zip path: ${{ matrix.layer }}_${{ matrix.arch }}.zip retention-days: 1 if-no-files-found: error - name: Store Metadata - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ matrix.layer }}_${{ matrix.arch }}.json path: ${{ matrix.layer }}_${{ matrix.arch }}.json @@ -96,11 +96,11 @@ jobs: environment: GovCloud ${{ inputs.environment }} (East) steps: - name: Download Zip - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ matrix.layer }}_${{ matrix.arch }}.zip - name: Download Metadata - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ matrix.layer }}_${{ matrix.arch }}.json - name: Verify Layer Signature @@ -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@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 with: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} aws-region: us-gov-east-1 @@ -161,11 +161,11 @@ jobs: name: GovCloud ${{ inputs.environment }} (West) steps: - name: Download Zip - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ matrix.layer }}_${{ matrix.arch }}.zip - name: Download Metadata - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ matrix.layer }}_${{ matrix.arch }}.json - name: Verify Layer Signature @@ -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@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 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 b6434ab026c..004f9e091fb 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@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 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@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 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@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 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 4b06da595ee..d3973e8083d 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@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 with: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} aws-region: us-east-1 @@ -98,7 +98,7 @@ jobs: run: | aws --region us-east-1 lambda get-layer-version-by-arn --arn 'arn:aws:lambda:us-east-1:017000801446:layer:${{ matrix.layer }}-${{ matrix.arch }}:${{ inputs.version }}' > '${{ matrix.layer }}-${{ matrix.arch }}.json' - name: Store Metadata - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ matrix.layer }}-${{ matrix.arch }}.json path: ${{ matrix.layer }}-${{ matrix.arch }}.json @@ -107,7 +107,7 @@ jobs: verify: name: Verify - needs: + needs: - setup - commercial runs-on: ubuntu-latest @@ -131,14 +131,14 @@ jobs: - x86_64 steps: - name: Download Metadata - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ matrix.layer }}-${{ matrix.arch }}.json - id: transform run: | echo 'CONVERTED_REGION=${{ matrix.region }}' | tr 'a-z\-' 'A-Z_' >> "$GITHUB_OUTPUT" - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 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 5c7560525fe..433ac84e357 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@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 with: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} aws-region: us-east-1 @@ -95,14 +95,14 @@ jobs: aws --region us-east-1 lambda get-layer-version-by-arn --arn arn:aws:lambda:us-east-1:017000801446:layer:${{ matrix.layer }}-${{ matrix.arch }}:${{ inputs.version }} --query 'Content.Location' | xargs curl -L -o ${{ matrix.layer }}-${{ matrix.arch }}.zip aws --region us-east-1 lambda get-layer-version-by-arn --arn arn:aws:lambda:us-east-1:017000801446:layer:${{ matrix.layer }}-${{ matrix.arch }}:${{ inputs.version }} > ${{ matrix.layer }}-${{ matrix.arch }}.json - name: Store Zip - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ matrix.layer }}-${{ matrix.arch }}.zip path: ${{ matrix.layer }}-${{ matrix.arch }}.zip retention-days: 1 if-no-files-found: error - name: Store Metadata - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ matrix.layer }}-${{ matrix.arch }}.json path: ${{ matrix.layer }}-${{ matrix.arch }}.json @@ -111,7 +111,7 @@ jobs: copy: name: Copy - needs: + needs: - setup - download runs-on: ubuntu-latest @@ -135,11 +135,11 @@ jobs: - x86_64 steps: - name: Download Zip - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ matrix.layer }}-${{ matrix.arch }}.zip - name: Download Metadata - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ matrix.layer }}-${{ matrix.arch }}.json - name: Verify Layer Signature @@ -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@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 with: role-to-assume: ${{ secrets[format('IAM_ROLE_{0}', steps.transform.outputs.CONVERTED_REGION)] }} aws-region: ${{ matrix.region}} @@ -160,7 +160,7 @@ jobs: id: create-layer run: | cat '${{ matrix.layer }}-${{ matrix.arch }}.json' | jq '{"LayerName": "${{ matrix.layer }}-${{ matrix.arch }}", "Description": .Description, "CompatibleRuntimes": .CompatibleRuntimes, "CompatibleArchitectures": .CompatibleArchitectures, "LicenseInfo": .LicenseInfo}' > input.json - + LAYER_VERSION=$(aws --region ${{ matrix.region}} lambda publish-layer-version \ --zip-file 'fileb://./${{ matrix.layer }}-${{ matrix.arch }}.zip' \ --cli-input-json file://./input.json \ @@ -187,7 +187,7 @@ jobs: jq -s -r '["Layer Arn", "Runtimes", "Version", "Description", "SHA256"], ([.[0], .[1]] | .[] | [.LayerArn, (.CompatibleRuntimes | join("/")), .Version, .Description, .Content.CodeSha256]) |@tsv' '${{ matrix.layer }}-${{ matrix.arch }}.json' $layer_output | column -t -s $'\t' - name: Store Metadata - ${{ matrix.region }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ matrix.layer }}-${{ matrix.arch }}-${{ matrix.region }}.json path: ${{ matrix.layer }}-${{ matrix.arch }}-${{ matrix.region }}.json diff --git a/.github/workflows/ossf_scorecard.yml b/.github/workflows/ossf_scorecard.yml index 5045dc11327..3c6e87ab8c7 100644 --- a/.github/workflows/ossf_scorecard.yml +++ b/.github/workflows/ossf_scorecard.yml @@ -35,7 +35,7 @@ jobs: repo_token: ${{ secrets.SCORECARD_TOKEN }} # read-only fine-grained token to read branch protection settings - name: "Upload results" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: SARIF file path: results.sarif diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index c8889221bca..07cf205ba20 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -221,7 +221,7 @@ jobs: - name: Upload to PyPi prod if: ${{ !inputs.skip_pypi }} - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 # Creates a PR with the latest version we've just released # since our trunk is protected against any direct pushes from automation @@ -244,7 +244,7 @@ jobs: artifact_name: ${{ needs.seal.outputs.artifact_name }} - name: Download provenance - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{needs.provenance.outputs.provenance-name}} diff --git a/.github/workflows/publish_v3_layer.yml b/.github/workflows/publish_v3_layer.yml index a1ac4e57208..958402adf98 100644 --- a/.github/workflows/publish_v3_layer.yml +++ b/.github/workflows/publish_v3_layer.yml @@ -123,7 +123,7 @@ 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@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "18.20.4" - name: Setup python @@ -139,16 +139,15 @@ jobs: pip install --require-hashes -r requirements.txt - name: Set up QEMU - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v2.0.0 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.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@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 with: - install: true driver: docker platforms: linux/amd64,linux/arm64 @@ -165,10 +164,12 @@ jobs: - name: CDK build run: npx cdk synth --verbose --context version="${{ inputs.latest_published_version }}" --context pythonVersion="python${{ matrix.python-version }}" -o cdk.out + env: + BUILDX_BUILDER: ${{ steps.builder.outputs.name }} - name: zip output run: zip -r cdk.py${{ matrix.python-version }}.out.zip cdk.out - name: Archive CDK artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: cdk-layer-artifact-py${{ matrix.python-version }} path: layer_v3/cdk.py${{ matrix.python-version }}.out.zip diff --git a/.github/workflows/quality_check.yml b/.github/workflows/quality_check.yml index f1213e7a351..dfbc6528e0d 100644 --- a/.github/workflows/quality_check.yml +++ b/.github/workflows/quality_check.yml @@ -78,7 +78,7 @@ jobs: - name: Complexity baseline run: make complexity-baseline - name: Upload coverage to Codecov - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # 5.5.2 + uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # 6.0.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml diff --git a/.github/workflows/quality_code_cdk_constructor.yml b/.github/workflows/quality_code_cdk_constructor.yml index e6f2f431c8f..497d0bea446 100644 --- a/.github/workflows/quality_code_cdk_constructor.yml +++ b/.github/workflows/quality_code_cdk_constructor.yml @@ -51,15 +51,14 @@ jobs: python-version: ${{ matrix.python-version }} cache: "poetry" - name: Set up QEMU - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v2.0.0 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.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@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 with: - install: true driver: docker platforms: linux/amd64,linux/arm64 - name: Install dependencies @@ -68,3 +67,5 @@ jobs: poetry install - name: Test with pytest run: poetry run pytest tests + env: + BUILDX_BUILDER: ${{ steps.builder.outputs.name }} diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 4f70494ec37..e08dd925efe 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -27,6 +27,4 @@ jobs: permissions: contents: write # create release in draft mode steps: - - uses: release-drafter/release-drafter@6db134d15f3909ccc9eefd369f02bd1e9cffdf97 # v5.20.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: release-drafter/release-drafter@c2e2804cc59f45f57076a99af580d0fedb697927 # v7.3.0 diff --git a/.github/workflows/release-v3.yml b/.github/workflows/release-v3.yml index 5b36d1018fd..64099e11413 100644 --- a/.github/workflows/release-v3.yml +++ b/.github/workflows/release-v3.yml @@ -246,12 +246,12 @@ jobs: - name: Upload to PyPi prod if: ${{ !inputs.skip_pypi }} - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 # 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@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + # uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 # with: # repository-url: https://test.pypi.org/legacy/ @@ -377,7 +377,7 @@ jobs: integrity_hash: ${{ needs.seal.outputs.integrity_hash }} artifact_name: ${{ needs.seal.outputs.artifact_name }} - name: Close issues related to this release - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/reusable_deploy_v3_layer_stack.yml b/.github/workflows/reusable_deploy_v3_layer_stack.yml index d657f891644..58b7650e3cb 100644 --- a/.github/workflows/reusable_deploy_v3_layer_stack.yml +++ b/.github/workflows/reusable_deploy_v3_layer_stack.yml @@ -76,7 +76,7 @@ jobs: "ap-south-1", "ap-south-2", "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", "ap-southeast-7", "ca-central-1", "ca-west-1", "eu-central-1", "eu-central-2", "eu-north-1", "eu-south-1", "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", - "il-central-1", "me-south-1", "mx-central-1", "sa-east-1", "us-east-1", + "il-central-1", "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", "us-west-1", "us-west-2"] python-version: ["3.10","3.11","3.12","3.13","3.14"] include: @@ -128,8 +128,6 @@ jobs: has_arm64_support: "true" - region: "il-central-1" has_arm64_support: "true" - - region: "me-south-1" - has_arm64_support: "true" - region: "mx-central-1" has_arm64_support: "true" - region: "sa-east-1" @@ -159,13 +157,13 @@ 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@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 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@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "18.20.4" - name: Setup python @@ -187,7 +185,7 @@ jobs: - name: install deps run: poetry install - name: Download artifact - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: cdk-layer-artifact-py${{ matrix.python-version }} path: layer_v3 @@ -211,7 +209,7 @@ jobs: cat cdk-layer-stack/${{steps.constants.outputs.LAYER_VERSION}} - name: Save Layer ARN artifact if: ${{ inputs.stage == 'PROD' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: cdk-layer-stack-${{ matrix.region }}-${{ matrix.python-version }} path: ./layer_v3/cdk-layer-stack/* # NOTE: upload-artifact does not inherit working-directory setting. diff --git a/.github/workflows/reusable_deploy_v3_sar.yml b/.github/workflows/reusable_deploy_v3_sar.yml index 3d6f302e260..3fc8cbc2fd4 100644 --- a/.github/workflows/reusable_deploy_v3_sar.yml +++ b/.github/workflows/reusable_deploy_v3_sar.yml @@ -87,7 +87,7 @@ jobs: - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 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@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 id: aws-credentials-sar-role with: aws-access-key-id: ${{ env.AWS_ACCESS_KEY_ID }} @@ -109,11 +109,11 @@ jobs: role-to-assume: ${{ secrets.AWS_SAR_V3_ROLE_ARN }} mask-aws-account-id: true - name: Setup Node.js - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ env.NODE_VERSION }} - name: Download artifact - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: cdk-layer-artifact-py${{ matrix.python-version }} - name: Unzip artefact diff --git a/.github/workflows/reusable_publish_docs.yml b/.github/workflows/reusable_publish_docs.yml index 83771abb145..dd054e98639 100644 --- a/.github/workflows/reusable_publish_docs.yml +++ b/.github/workflows/reusable_publish_docs.yml @@ -68,7 +68,7 @@ jobs: env: BRANCH: ${{ inputs.git_ref }} - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 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 17d2c8637ea..d05239bf089 100644 --- a/.github/workflows/run-e2e-tests.yml +++ b/.github/workflows/run-e2e-tests.yml @@ -62,7 +62,7 @@ jobs: architecture: "x64" cache: "poetry" - name: Setup Node.js - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.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@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 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 e087b98fa5c..e2f187a40f2 100644 --- a/.github/workflows/secure_workflows.yml +++ b/.github/workflows/secure_workflows.yml @@ -32,7 +32,7 @@ jobs: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Ensure 3rd party workflows have SHA pinned - uses: zgosalvez/github-actions-ensure-sha-pinned-actions@70c4af2ed5282c51ba40566d026d6647852ffa3e # v5.0.1 + uses: zgosalvez/github-actions-ensure-sha-pinned-actions@ca46236c6ce584ae24bc6283ba8dcf4b3ec8a066 # v5.0.4 with: allowlist: | slsa-framework/slsa-github-generator diff --git a/.github/workflows/update_ssm.yml b/.github/workflows/update_ssm.yml index 53a3851a7f1..f290d9e560b 100644 --- a/.github/workflows/update_ssm.yml +++ b/.github/workflows/update_ssm.yml @@ -78,7 +78,7 @@ jobs: "ap-south-1", "ap-south-2", "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", "ap-southeast-7", "ca-central-1", "ca-west-1", "eu-central-1", "eu-central-2", "eu-north-1", "eu-south-1", "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", - "il-central-1", "me-south-1", "mx-central-1", "sa-east-1", "us-east-1", + "il-central-1", "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", "us-west-1", "us-west-2"] permissions: @@ -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@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v4.3.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 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 30278b30259..f2dc360bea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,130 @@ # Unreleased + +## [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)) + +## Maintenance + +* version bump + + + +## [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)) + + + +## [v3.27.0] - 2026-04-06 +## Bug Fixes + +* **data_classes:** support {proxy+} and path parameters in authorizer response ([#8092](https://github.com/aws-powertools/powertools-lambda-python/issues/8092)) +* **event_handler:** sync middleware receives real response in async ASGI context ([#8089](https://github.com/aws-powertools/powertools-lambda-python/issues/8089)) +* **event_handler:** support finding type annotated resolver when merging schemas ([#8074](https://github.com/aws-powertools/powertools-lambda-python/issues/8074)) +* **event_handler:** normalize Union and RootModel sequences in body validation ([#8067](https://github.com/aws-powertools/powertools-lambda-python/issues/8067)) +* **idempotency:** serialize Pydantic models with mode='json' for UUID/date support ([#8075](https://github.com/aws-powertools/powertools-lambda-python/issues/8075)) + +## Documentation + +* adding docs to Request object ([#8105](https://github.com/aws-powertools/powertools-lambda-python/issues/8105)) +* fix ranthebuilder link in Update we_made_this.md ([#8084](https://github.com/aws-powertools/powertools-lambda-python/issues/8084)) + +## Features + +* **event_handler:** add File parameter support for multipart/form-data uploads ([#8093](https://github.com/aws-powertools/powertools-lambda-python/issues/8093)) +* **event_handler:** add Cookie parameter support for OpenAPI utility ([#8095](https://github.com/aws-powertools/powertools-lambda-python/issues/8095)) +* **event_handler:** add Request object for middleware access to resolved route and args ([#8036](https://github.com/aws-powertools/powertools-lambda-python/issues/8036)) + +## Maintenance + +* version bump +* **deps:** bump valkey-glide from 2.2.7 to 2.3.0 ([#8080](https://github.com/aws-powertools/powertools-lambda-python/issues/8080)) +* **deps:** bump protobuf from 6.33.5 to 7.34.0 ([#8046](https://github.com/aws-powertools/powertools-lambda-python/issues/8046)) +* **deps:** bump mkdocs-material from 9.7.1 to 9.7.5 ([#8045](https://github.com/aws-powertools/powertools-lambda-python/issues/8045)) +* **deps:** bump requests from 2.32.4 to 2.33.0 in /docs ([#8070](https://github.com/aws-powertools/powertools-lambda-python/issues/8070)) +* **deps:** bump cryptography from 46.0.5 to 46.0.6 ([#8072](https://github.com/aws-powertools/powertools-lambda-python/issues/8072)) +* **deps:** bump squidfunk/mkdocs-material from `8f41b60` to `868ad4d` in /docs ([#8083](https://github.com/aws-powertools/powertools-lambda-python/issues/8083)) +* **deps:** bump the github-actions group across 1 directory with 10 updates ([#8081](https://github.com/aws-powertools/powertools-lambda-python/issues/8081)) +* **deps:** bump mkdocs-material from 9.7.5 to 9.7.6 ([#8079](https://github.com/aws-powertools/powertools-lambda-python/issues/8079)) +* **deps-dev:** bump aws-cdk from 2.1111.0 to 2.1113.0 in the aws-cdk group ([#8058](https://github.com/aws-powertools/powertools-lambda-python/issues/8058)) +* **deps-dev:** bump sentry-sdk from 2.54.0 to 2.56.0 ([#8082](https://github.com/aws-powertools/powertools-lambda-python/issues/8082)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.243.0a0 to 2.248.0a0 ([#8103](https://github.com/aws-powertools/powertools-lambda-python/issues/8103)) +* **deps-dev:** bump the dev-dependencies group across 1 directory with 4 updates ([#8086](https://github.com/aws-powertools/powertools-lambda-python/issues/8086)) +* **deps-dev:** bump pygments from 2.19.2 to 2.20.0 ([#8077](https://github.com/aws-powertools/powertools-lambda-python/issues/8077)) +* **deps-dev:** bump requests from 2.32.5 to 2.33.0 ([#8069](https://github.com/aws-powertools/powertools-lambda-python/issues/8069)) +* **deps-dev:** bump ty from 0.0.23 to 0.0.26 ([#8078](https://github.com/aws-powertools/powertools-lambda-python/issues/8078)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.315 to 0.1.316 ([#8061](https://github.com/aws-powertools/powertools-lambda-python/issues/8061)) +* **deps-dev:** bump boto3-stubs from 1.42.73 to 1.42.74 ([#8062](https://github.com/aws-powertools/powertools-lambda-python/issues/8062)) +* **deps-dev:** bump requests from 2.33.0 to 2.33.1 ([#8104](https://github.com/aws-powertools/powertools-lambda-python/issues/8104)) +* **deps-dev:** bump boto3-stubs from 1.42.67 to 1.42.68 ([#8043](https://github.com/aws-powertools/powertools-lambda-python/issues/8043)) +* **deps-dev:** bump nox from 2025.11.12 to 2026.2.9 ([#8044](https://github.com/aws-powertools/powertools-lambda-python/issues/8044)) +* **deps-dev:** bump aws-cdk from 2.1110.0 to 2.1111.0 in the aws-cdk group ([#8039](https://github.com/aws-powertools/powertools-lambda-python/issues/8039)) +* **deps-dev:** bump ruff from 0.15.8 to 0.15.9 in the dev-dependencies group ([#8100](https://github.com/aws-powertools/powertools-lambda-python/issues/8100)) +* **deps-dev:** bump isort from 7.0.0 to 8.0.1 ([#8101](https://github.com/aws-powertools/powertools-lambda-python/issues/8101)) +* **deps-dev:** bump types-requests from 2.32.4.20260107 to 2.33.0.20260402 ([#8102](https://github.com/aws-powertools/powertools-lambda-python/issues/8102)) + + ## [v3.26.0] - 2026-03-20 +## Bug Fixes + +* **ci:** add ty check to dataclasses utility ([#8038](https://github.com/aws-powertools/powertools-lambda-python/issues/8038)) +* **ci:** add ty check to parser folder ([#8037](https://github.com/aws-powertools/powertools-lambda-python/issues/8037)) +* **ci:** add ty check to parameters folder ([#8035](https://github.com/aws-powertools/powertools-lambda-python/issues/8035)) +* **openapi:** correct response validation for falsy objects ([#7990](https://github.com/aws-powertools/powertools-lambda-python/issues/7990)) + ## Features * add ldms feature ([#8051](https://github.com/aws-powertools/powertools-lambda-python/issues/8051)) +* **batch:** add Kafka/MSK batch processing support ([#7941](https://github.com/aws-powertools/powertools-lambda-python/issues/7941)) +* **buffer-handler:** add buffering support for external loggers ([#7994](https://github.com/aws-powertools/powertools-lambda-python/issues/7994)) ## Maintenance * version bump +* **deps:** bump aws-encryption-sdk from 4.0.3 to 4.0.4 ([#8027](https://github.com/aws-powertools/powertools-lambda-python/issues/8027)) +* **deps:** bump valkey-glide from 2.2.5 to 2.2.7 ([#8030](https://github.com/aws-powertools/powertools-lambda-python/issues/8030)) +* **deps-dev:** bump types-python-dateutil from 2.9.0.20260124 to 2.9.0.20260305 ([#8029](https://github.com/aws-powertools/powertools-lambda-python/issues/8029)) +* **deps-dev:** bump ijson from 3.4.0.post0 to 3.5.0 ([#8028](https://github.com/aws-powertools/powertools-lambda-python/issues/8028)) +* **deps-dev:** bump aws-cdk from 2.1108.0 to 2.1110.0 in the aws-cdk group ([#8023](https://github.com/aws-powertools/powertools-lambda-python/issues/8023)) @@ -7551,7 +7666,10 @@ * 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.26.0...HEAD +[Unreleased]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.29.0...HEAD +[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 [v3.25.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.24.0...v3.25.0 [v3.24.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.23.0...v3.24.0 diff --git a/aws_lambda_powertools/event_handler/__init__.py b/aws_lambda_powertools/event_handler/__init__.py index 6b926e6248a..98433b2e29b 100644 --- a/aws_lambda_powertools/event_handler/__init__.py +++ b/aws_lambda_powertools/event_handler/__init__.py @@ -16,11 +16,13 @@ BedrockAgentFunctionResolver, BedrockFunctionResponse, ) +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.lambda_function_url import ( LambdaFunctionUrlResolver, ) +from aws_lambda_powertools.event_handler.request import Request from aws_lambda_powertools.event_handler.vpc_lattice import VPCLatticeResolver, VPCLatticeV2Resolver __all__ = [ @@ -35,8 +37,11 @@ "BedrockResponse", "BedrockFunctionResponse", "CORSConfig", + "Depends", + "DependencyResolutionError", "HttpResolverLocal", "LambdaFunctionUrlResolver", + "Request", "Response", "VPCLatticeResolver", "VPCLatticeV2Resolver", diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index b1e0c9ff16d..a323cf67d56 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -1,7 +1,6 @@ from __future__ import annotations import base64 -import copy import json import logging import re @@ -23,8 +22,11 @@ from aws_lambda_powertools.event_handler.openapi.config import OpenAPIConfig from aws_lambda_powertools.event_handler.openapi.constants import ( DEFAULT_API_VERSION, + DEFAULT_CONTENT_TYPE, + DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, DEFAULT_OPENAPI_TITLE, DEFAULT_OPENAPI_VERSION, + DEFAULT_STATUS_CODE, ) from aws_lambda_powertools.event_handler.openapi.exceptions import ( RequestUnsupportedContentType, @@ -34,14 +36,10 @@ ) from aws_lambda_powertools.event_handler.openapi.types import ( COMPONENT_REF_PREFIX, - METHODS_WITH_BODY, OpenAPIResponse, - OpenAPIResponseContentModel, - OpenAPIResponseContentSchema, response_validation_error_response_definition, - validation_error_definition, - validation_error_response_definition, ) +from aws_lambda_powertools.event_handler.request import Request from aws_lambda_powertools.event_handler.util import ( _FrozenDict, _FrozenListDict, @@ -72,10 +70,8 @@ # API GW/ALB decode non-safe URI chars; we must support them too _UNSAFE_URI = r"%<> \[\]{}|^" _NAMED_GROUP_BOUNDARY_PATTERN = rf"(?P\1[{_SAFE_URI}{_UNSAFE_URI}\\w]+)" -_DEFAULT_OPENAPI_RESPONSE_DESCRIPTION = "Successful Response" _ROUTE_REGEX = "^{}$" _JSON_DUMP_CALL = partial(json.dumps, separators=(",", ":"), cls=Encoder) -_DEFAULT_CONTENT_TYPE = "application/json" ResponseEventT = TypeVar("ResponseEventT", bound=BaseProxyEvent) ResponseT = TypeVar("ResponseT") @@ -94,7 +90,7 @@ Server, Tag, ) - from aws_lambda_powertools.event_handler.openapi.params import Dependant, Param + from aws_lambda_powertools.event_handler.openapi.params import Dependant from aws_lambda_powertools.event_handler.openapi.swagger_ui.oauth2 import ( OAuth2Config, ) @@ -118,6 +114,17 @@ class ProxyEventType(Enum): LambdaFunctionUrlEvent = "LambdaFunctionUrlEvent" +_PROXY_EVENT_MAP: dict[Enum, tuple[type[BaseProxyEvent], str]] = { + ProxyEventType.APIGatewayProxyEvent: (APIGatewayProxyEvent, "API Gateway REST API"), + ProxyEventType.APIGatewayProxyEventV2: (APIGatewayProxyEventV2, "API Gateway HTTP API"), + ProxyEventType.BedrockAgentEvent: (BedrockAgentEvent, "Bedrock Agent"), + ProxyEventType.LambdaFunctionUrlEvent: (LambdaFunctionUrlEvent, "Lambda Function URL"), + ProxyEventType.VPCLatticeEvent: (VPCLatticeEvent, "VPC Lattice"), + ProxyEventType.VPCLatticeEventV2: (VPCLatticeEventV2, "VPC LatticeV2"), + ProxyEventType.ALBEvent: (ALBEvent, "ALB"), +} + + class CORSConfig: """CORS Config @@ -277,8 +284,8 @@ class BedrockResponse(Generic[ResponseT]): def __init__( self, body: Any = None, - status_code: int = 200, - content_type: str = _DEFAULT_CONTENT_TYPE, + status_code: int = DEFAULT_STATUS_CODE, + content_type: str = DEFAULT_CONTENT_TYPE, session_attributes: dict[str, Any] | None = None, prompt_session_attributes: dict[str, Any] | None = None, knowledge_bases_configuration: list[dict[str, Any]] | None = None, @@ -354,7 +361,7 @@ def is_json(self) -> bool: content_type = self.headers.get("Content-Type", "") if isinstance(content_type, list): content_type = content_type[0] - return content_type.startswith(_DEFAULT_CONTENT_TYPE) + return content_type.startswith(DEFAULT_CONTENT_TYPE) class Route: @@ -381,6 +388,7 @@ def __init__( deprecated: bool = False, enable_validation: bool | None = None, custom_response_validation_http_code: HTTPStatus | None = None, + status_code: int = DEFAULT_STATUS_CODE, middlewares: list[Callable[..., Response]] | None = None, ): """ @@ -426,6 +434,9 @@ def __init__( Enable or disable validation for this specific route. If None, inherits from resolver setting. custom_response_validation_http_code: int | HTTPStatus | None, optional Whether to have custom http status code for this route if response validation fails + status_code: int + The default HTTP status code for successful responses. Used in both the OpenAPI schema + and the actual response when the handler returns a dict. Defaults to 200. middlewares: list[Callable[..., Response]] | None The list of route middlewares to be called in order. """ @@ -465,6 +476,15 @@ def __init__( self._body_field: ModelField | None = None self.custom_response_validation_http_code = custom_response_validation_http_code + self.status_code = status_code + + # Cache whether this route's handler declares Depends() parameters + self._has_dependencies: bool | None = None + + # Caches the name of any Request-typed parameter in the handler. + # Avoids re-scanning the signature on every invocation. + self.request_param_name: str | None = None + self.request_param_name_checked: bool = False def __call__( self, @@ -593,422 +613,128 @@ def _build_middleware_stack(self, router_middlewares: list[Callable[..., Any]], self._middleware_stack_built = True - @property - def dependant(self) -> Dependant: - if self._dependant is None: - from aws_lambda_powertools.event_handler.openapi.dependant import get_dependant - - self._dependant = get_dependant(path=self.openapi_path, call=self.func, responses=self.responses) - - return self._dependant - - @property - def body_field(self) -> ModelField | None: - if self._body_field is None: - from aws_lambda_powertools.event_handler.openapi.dependant import get_body_field - - self._body_field = get_body_field(dependant=self.dependant, name=self.operation_id) - - return self._body_field - - def _get_openapi_path( # noqa PLR0912 + async def call_async( self, - *, - dependant: Dependant, - operation_ids: set[str], - model_name_map: dict[TypeModelOrEnum, str], - field_mapping: dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], - enable_validation: bool = False, - ) -> tuple[dict[str, Any], dict[str, Any]]: - """ - Returns the OpenAPI path and definitions for the route. - """ - from aws_lambda_powertools.event_handler.openapi.dependant import get_flat_params + 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, + ) - definitions: dict[str, Any] = {} + all_middlewares: list[Callable[..., Any]] = [] - # Gather all the route parameters - operation = self._openapi_operation_metadata(operation_ids=operation_ids) - parameters: list[dict[str, Any]] = [] - all_route_params = get_flat_params(dependant) - operation_params = self._openapi_operation_parameters( - all_route_params=all_route_params, - model_name_map=model_name_map, - field_mapping=field_mapping, + route_validation_enabled = ( + self.enable_validation if self.enable_validation is not None else app._enable_validation ) - parameters.extend(operation_params) - - # Add security if present - if self.security: - operation["security"] = self.security - - # Add OpenAPI extensions if present - if self.openapi_extensions: - operation.update(self.openapi_extensions) - - # Add the parameters to the OpenAPI operation - if parameters: - all_parameters = {(param["in"], param["name"]): param for param in parameters} - required_parameters = {(param["in"], param["name"]): param for param in parameters if param.get("required")} - all_parameters.update(required_parameters) - operation["parameters"] = list(all_parameters.values()) - - # Add the request body to the OpenAPI operation, if applicable - if self.method.upper() in METHODS_WITH_BODY: - request_body_oai = self._openapi_operation_request_body( - body_field=self.body_field, - model_name_map=model_name_map, - field_mapping=field_mapping, - ) - if request_body_oai: - operation["requestBody"] = request_body_oai - - operation_responses: dict[int, OpenAPIResponse] = {} - - if enable_validation: - # Validation failure response (422) is added only if Enable Validation feature is true - operation_responses = { - 422: { - "description": "Validation Error", - "content": { - _DEFAULT_CONTENT_TYPE: {"schema": {"$ref": f"{COMPONENT_REF_PREFIX}HTTPValidationError"}}, - }, - }, - } - # Add custom response validation response, if exists - if self.custom_response_validation_http_code: - http_code = self.custom_response_validation_http_code.value - operation_responses[http_code] = { - "description": "Response Validation Error", - "content": { - _DEFAULT_CONTENT_TYPE: {"schema": {"$ref": f"{COMPONENT_REF_PREFIX}ResponseValidationError"}}, - }, - } - # Add model definition - definitions["ResponseValidationError"] = response_validation_error_response_definition - - # Add the response to the OpenAPI operation - if self.responses: - for status_code in list(self.responses): - # Create a deep copy to prevent mutation of the shared dictionary - response = copy.deepcopy(self.responses[status_code]) - - # Case 1: there is not 'content' key - if "content" not in response: - response["content"] = { - _DEFAULT_CONTENT_TYPE: self._openapi_operation_return( - param=dependant.return_param, - model_name_map=model_name_map, - field_mapping=field_mapping, - ), - } - - # Case 2: there is a 'content' key - else: - # Need to iterate to transform any 'model' into a 'schema' - for content_type, payload in response["content"].items(): - # Case 2.1: the 'content' has a model - if "model" in payload: - # Find the model in the dependant's extra models - model_payload_typed = cast(OpenAPIResponseContentModel, payload) - return_field = next( - filter( - lambda model: model.type_ is model_payload_typed["model"], - self.dependant.response_extra_models, - ), - ) - if not return_field: - raise AssertionError("Model declared in custom responses was not found") - - model_payload = self._openapi_operation_return( - param=return_field, - model_name_map=model_name_map, - field_mapping=field_mapping, - ) - - # Preserve existing fields like examples, encoding, etc. - new_payload: OpenAPIResponseContentSchema = {} - for key, value in payload.items(): - if key != "model": - new_payload[key] = value # type: ignore[literal-required] - new_payload.update(model_payload) # Add/override with model schema - - # Case 2.2: the 'content' has a schema - else: - # Do nothing! We already have what we need! - new_payload = cast(OpenAPIResponseContentSchema, payload) - - response["content"][content_type] = new_payload - - # Merge the user provided response with the default responses - operation_responses[status_code] = response - else: - # Set the default 200 response - response_schema = self._openapi_operation_return( - param=dependant.return_param, - model_name_map=model_name_map, - field_mapping=field_mapping, + if route_validation_enabled and not hasattr(app, "_request_validation_middleware"): + from aws_lambda_powertools.event_handler.middlewares.openapi_validation import ( + OpenAPIRequestValidationMiddleware, + OpenAPIResponseValidationMiddleware, ) - # Add the response schema to the OpenAPI 200 response - operation_responses[200] = { - "description": self.response_description or _DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, - "content": {_DEFAULT_CONTENT_TYPE: response_schema}, - } - - operation["responses"] = operation_responses - path = {self.method.lower(): operation} - # Add the validation error schema to the definitions, but only if it hasn't been added yet - if "ValidationError" not in definitions: - definitions.update( - { - "ValidationError": validation_error_definition, - "HTTPValidationError": validation_error_response_definition, - }, + app._request_validation_middleware = OpenAPIRequestValidationMiddleware() + app._response_validation_middleware = OpenAPIResponseValidationMiddleware( + validation_serializer=app._serializer, + has_response_validation_error=app._has_response_validation_error, ) - # Generate the response schema - return path, definitions + if route_validation_enabled and hasattr(app, "_request_validation_middleware"): + all_middlewares.append(app._request_validation_middleware) - def _openapi_operation_summary(self) -> str: - """ - Returns the OpenAPI operation summary. If the user has not provided a summary, we - generate one based on the route path and method. - """ - return self.summary or f"{self.method.upper()} {self.openapi_path}" + all_middlewares.extend(router_middlewares + self.middlewares) - def _openapi_operation_metadata(self, operation_ids: set[str]) -> dict[str, Any]: - """ - Returns the OpenAPI operation metadata. If the user has not provided a description, we - generate one based on the route path and method. - """ - operation: dict[str, Any] = {} + if route_validation_enabled and hasattr(app, "_response_validation_middleware"): + all_middlewares.append(app._response_validation_middleware) - # Ensure tags is added to the operation - if self.tags: - operation["tags"] = self.tags + all_middlewares.append(_registered_api_adapter_async) - # Ensure summary is added to the operation - operation["summary"] = self._openapi_operation_summary() + logger.debug(f"Building async middleware stack: {all_middlewares}") - # Ensure description is added to the operation - if self.description: - operation["description"] = self.description + 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("=================") - # Ensure operationId is unique - if self.operation_id in operation_ids: - message = f"Duplicate Operation ID {self.operation_id} for function {self.func.__name__}" - file_name = getattr(self.func, "__globals__", {}).get("__file__") - if file_name: - message += f" in {file_name}" - warnings.warn(message, stacklevel=1) + app.append_context(_route_args=route_arguments) - # Adds the operation - operation_ids.add(self.operation_id) - operation["operationId"] = self.operation_id + # 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) - # Mark as deprecated if necessary - operation["deprecated"] = self.deprecated or None + return await next_handler(app) - return operation + @property + def dependant(self) -> Dependant: + if self._dependant is None: + from aws_lambda_powertools.event_handler.openapi.dependant import get_dependant - @staticmethod - def _openapi_operation_request_body( - *, - body_field: ModelField | None, - model_name_map: dict[TypeModelOrEnum, str], - field_mapping: dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], - ) -> dict[str, Any] | None: - """ - Returns the OpenAPI operation request body. - """ - from aws_lambda_powertools.event_handler.openapi.compat import ModelField, get_schema_from_model_field - from aws_lambda_powertools.event_handler.openapi.params import Body + self._dependant = get_dependant(path=self.openapi_path, call=self.func, responses=self.responses) - # Check that there is a body field and it's a Pydantic's model field - if not body_field: - return None + return self._dependant - if not isinstance(body_field, ModelField): - raise AssertionError(f"Expected ModelField, got {body_field}") + @property + def has_dependencies(self) -> bool: + """Check if handler declares Depends() parameters without triggering full dependant computation.""" + if self._has_dependencies is None: + from aws_lambda_powertools.event_handler.depends import _has_depends - # Generate the request body schema - body_schema = get_schema_from_model_field( - field=body_field, - model_name_map=model_name_map, - field_mapping=field_mapping, - ) + self._has_dependencies = _has_depends(self.func) + return self._has_dependencies - field_info = cast(Body, body_field.field_info) - request_media_type = field_info.media_type - required = body_field.required - request_body_oai: dict[str, Any] = {} - if required: - request_body_oai["required"] = required + @property + def body_field(self) -> ModelField | None: + if self._body_field is None: + from aws_lambda_powertools.event_handler.openapi.dependant import get_body_field - if field_info.description: - request_body_oai["description"] = field_info.description + self._body_field = get_body_field(dependant=self.dependant, name=self.operation_id) - # Generate the request body media type - request_media_content: dict[str, Any] = {"schema": body_schema} - if field_info.openapi_examples: - request_media_content["examples"] = field_info.openapi_examples - request_body_oai["content"] = {request_media_type: request_media_content} - return request_body_oai + return self._body_field - @staticmethod - def _openapi_operation_parameters( + def _get_openapi_path( + self, *, - all_route_params: Sequence[ModelField], - model_name_map: dict[TypeModelOrEnum, str], - field_mapping: dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], - ) -> list[dict[str, Any]]: - """ - Returns the OpenAPI operation parameters. - """ - from aws_lambda_powertools.event_handler.openapi.params import Param - - parameters: list[dict[str, Any]] = [] - - for param in all_route_params: - field_info = cast(Param, param.field_info) - if not field_info.include_in_schema: - continue - - # Check if this is a Pydantic model that should be expanded - if Route._is_pydantic_model_param(field_info): - parameters.extend(Route._expand_pydantic_model_parameters(field_info)) - else: - parameters.append(Route._create_regular_parameter(param, model_name_map, field_mapping)) - - return parameters - - @staticmethod - def _is_pydantic_model_param(field_info: Param) -> bool: - """Check if the field info represents a Pydantic model parameter.""" - from pydantic import BaseModel - - from aws_lambda_powertools.event_handler.openapi.compat import lenient_issubclass - - return lenient_issubclass(field_info.annotation, BaseModel) - - @staticmethod - def _expand_pydantic_model_parameters(field_info: Param) -> list[dict[str, Any]]: - """Expand a Pydantic model into individual OpenAPI parameters.""" - from pydantic import BaseModel - - model_class = cast(type[BaseModel], field_info.annotation) - parameters: list[dict[str, Any]] = [] - - for field_name, field_def in model_class.model_fields.items(): - param_name = field_def.alias or field_name - individual_param = Route._create_pydantic_field_parameter( - param_name=param_name, - field_def=field_def, - param_location=field_info.in_.value, - ) - parameters.append(individual_param) - - return parameters - - @staticmethod - def _create_pydantic_field_parameter( - param_name: str, - field_def: Any, - param_location: str, - ) -> dict[str, Any]: - """Create an OpenAPI parameter from a Pydantic field definition.""" - individual_param: dict[str, Any] = { - "name": param_name, - "in": param_location, - "required": field_def.is_required() if hasattr(field_def, "is_required") else field_def.default is ..., - "schema": Route._get_basic_type_schema(field_def.annotation or type(None)), - } - - if field_def.description: - individual_param["description"] = field_def.description - - return individual_param - - @staticmethod - def _create_regular_parameter( - param: ModelField, + dependant: Dependant, + operation_ids: set[str], model_name_map: dict[TypeModelOrEnum, str], field_mapping: dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], - ) -> dict[str, Any]: - """Create an OpenAPI parameter from a regular ModelField.""" - from aws_lambda_powertools.event_handler.openapi.compat import get_schema_from_model_field - from aws_lambda_powertools.event_handler.openapi.params import Param - - field_info = cast(Param, param.field_info) - param_schema = get_schema_from_model_field( - field=param, - model_name_map=model_name_map, - field_mapping=field_mapping, - ) - - parameter: dict[str, Any] = { - "name": param.alias, - "in": field_info.in_.value, - "required": param.required, - "schema": param_schema, - } - - # Add optional attributes if present - if field_info.description: - parameter["description"] = field_info.description - if field_info.openapi_examples: - parameter["examples"] = field_info.openapi_examples - if field_info.deprecated: - parameter["deprecated"] = field_info.deprecated - - return parameter - - @staticmethod - def _get_basic_type_schema(param_type: type) -> dict[str, str]: - """ - Get basic OpenAPI schema for simple types + enable_validation: bool = False, + ) -> tuple[dict[str, Any], dict[str, Any]]: """ - try: - # Check bool before int, since bool is a subclass of int in Python - if issubclass(param_type, bool): - return {"type": "boolean"} - elif issubclass(param_type, int): - return {"type": "integer"} - elif issubclass(param_type, float): - return {"type": "number"} - else: - return {"type": "string"} - except TypeError: - # param_type may not be a type (e.g., typing.Optional[int]), fallback to string - return {"type": "string"} + Returns the OpenAPI path and definitions for the route. - @staticmethod - def _openapi_operation_return( - *, - param: ModelField | None, - model_name_map: dict[TypeModelOrEnum, str], - field_mapping: dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], - ) -> OpenAPIResponseContentSchema: - """ - Returns the OpenAPI operation return. + Delegates to openapi.schema_generator for the actual generation logic. """ - if param is None: - return {} - - from aws_lambda_powertools.event_handler.openapi.compat import ( - get_schema_from_model_field, - ) - - return_schema = get_schema_from_model_field( - field=param, + from aws_lambda_powertools.event_handler.openapi.schema_generator import generate_openapi_path + + return generate_openapi_path( + method=self.method, + operation_id=self.operation_id, + summary=self.summary, + description=self.description, + openapi_path=self.openapi_path, + tags=self.tags, + deprecated=self.deprecated, + security=self.security, + openapi_extensions=self.openapi_extensions, + responses=self.responses, + response_description=self.response_description, + body_field=self.body_field, + custom_response_validation_http_code=self.custom_response_validation_http_code, + status_code=self.status_code, + dependant=dependant, + operation_ids=operation_ids, model_name_map=model_name_map, field_mapping=field_mapping, + enable_validation=enable_validation, ) - return {"schema": return_schema} - def _generate_operation_id(self) -> str: operation_id = self.func.__name__ + self.openapi_path operation_id = re.sub(r"\W", "_", operation_id) @@ -1149,7 +875,7 @@ def route( summary: str | None = None, description: str | None = None, responses: dict[int, OpenAPIResponse] | None = None, - response_description: str = _DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, @@ -1158,6 +884,7 @@ def route( deprecated: bool = False, enable_validation: bool | None = None, custom_response_validation_http_code: int | HTTPStatus | None = None, + status_code: int = DEFAULT_STATUS_CODE, middlewares: list[Callable[..., Any]] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: raise NotImplementedError() @@ -1212,7 +939,7 @@ def get( summary: str | None = None, description: str | None = None, responses: dict[int, OpenAPIResponse] | None = None, - response_description: str = _DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, @@ -1221,6 +948,7 @@ def get( deprecated: bool = False, enable_validation: bool | None = None, custom_response_validation_http_code: int | HTTPStatus | None = None, + status_code: int = DEFAULT_STATUS_CODE, middlewares: list[Callable[..., Any]] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: """Get route decorator with GET `method` @@ -1263,6 +991,7 @@ def lambda_handler(event, context): deprecated, enable_validation, custom_response_validation_http_code, + status_code, middlewares, ) @@ -1275,7 +1004,7 @@ def post( summary: str | None = None, description: str | None = None, responses: dict[int, OpenAPIResponse] | None = None, - response_description: str = _DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, @@ -1284,6 +1013,7 @@ def post( deprecated: bool = False, enable_validation: bool | None = None, custom_response_validation_http_code: int | HTTPStatus | None = None, + status_code: int = DEFAULT_STATUS_CODE, middlewares: list[Callable[..., Any]] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: """Post route decorator with POST `method` @@ -1327,6 +1057,7 @@ def lambda_handler(event, context): deprecated, enable_validation, custom_response_validation_http_code, + status_code, middlewares, ) @@ -1339,7 +1070,7 @@ def put( summary: str | None = None, description: str | None = None, responses: dict[int, OpenAPIResponse] | None = None, - response_description: str = _DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, @@ -1348,6 +1079,7 @@ def put( deprecated: bool = False, enable_validation: bool | None = None, custom_response_validation_http_code: int | HTTPStatus | None = None, + status_code: int = DEFAULT_STATUS_CODE, middlewares: list[Callable[..., Any]] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: """Put route decorator with PUT `method` @@ -1391,6 +1123,7 @@ def lambda_handler(event, context): deprecated, enable_validation, custom_response_validation_http_code, + status_code, middlewares, ) @@ -1403,7 +1136,7 @@ def delete( summary: str | None = None, description: str | None = None, responses: dict[int, OpenAPIResponse] | None = None, - response_description: str = _DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, @@ -1412,6 +1145,7 @@ def delete( deprecated: bool = False, enable_validation: bool | None = None, custom_response_validation_http_code: int | HTTPStatus | None = None, + status_code: int = DEFAULT_STATUS_CODE, middlewares: list[Callable[..., Any]] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: """Delete route decorator with DELETE `method` @@ -1454,6 +1188,7 @@ def lambda_handler(event, context): deprecated, enable_validation, custom_response_validation_http_code, + status_code, middlewares, ) @@ -1466,7 +1201,7 @@ def patch( summary: str | None = None, description: str | None = None, responses: dict[int, OpenAPIResponse] | None = None, - response_description: str = _DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, @@ -1475,6 +1210,7 @@ def patch( deprecated: bool = False, enable_validation: bool | None = None, custom_response_validation_http_code: int | HTTPStatus | None = None, + status_code: int = DEFAULT_STATUS_CODE, middlewares: list[Callable] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: """Patch route decorator with PATCH `method` @@ -1520,6 +1256,7 @@ def lambda_handler(event, context): deprecated, enable_validation, custom_response_validation_http_code, + status_code, middlewares, ) @@ -1532,7 +1269,7 @@ def head( summary: str | None = None, description: str | None = None, responses: dict[int, OpenAPIResponse] | None = None, - response_description: str = _DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, @@ -1541,6 +1278,7 @@ def head( deprecated: bool = False, enable_validation: bool | None = None, custom_response_validation_http_code: int | HTTPStatus | None = None, + status_code: int = DEFAULT_STATUS_CODE, middlewares: list[Callable] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: """Head route decorator with HEAD `method` @@ -1585,6 +1323,7 @@ def lambda_handler(event, context): deprecated, enable_validation, custom_response_validation_http_code, + status_code, middlewares, ) @@ -1608,6 +1347,48 @@ def clear_context(self): """Resets routing context""" self.context.clear() + @property + def request(self) -> Request: + """Current resolved :class:`Request` object. + + Available inside middleware and in route handlers that declare a parameter + typed as :class:`Request `. + + Raises + ------ + RuntimeError + When accessed before route resolution (i.e. outside of middleware / handler scope). + + Examples + -------- + **Middleware** + + ```python + def my_middleware(app, next_middleware): + req = app.request + print(req.route, req.method, req.path_parameters) + return next_middleware(app) + ``` + """ + cached: Request | None = self.context.get("_request") + if cached is not None: + return cached + + route: Route | None = self.context.get("_route") + if route is None: + raise RuntimeError( + "app.request is only available after route resolution. Use it inside middleware or a route handler.", + ) + + request = Request( + route_path=route.openapi_path, + path_parameters=self.context.get("_route_args", {}), + current_event=self.current_event, + context=self.context, + ) + self.context["_request"] = request + return request + class MiddlewareFrame: """ @@ -1680,6 +1461,24 @@ def __call__(self, app: ApiGatewayResolver) -> dict | tuple | Response: return self.current_middleware(app, self.next_middleware) +def _find_request_param_name(func: Callable) -> str | None: + """Return the name of the first parameter annotated as ``Request``, or ``None``.""" + from typing import get_type_hints + + try: + # get_type_hints resolves string annotations from ``from __future__ import annotations`` + # using the function's own module globals. + hints = get_type_hints(func) + except Exception: + hints = {} + + for param_name, annotation in hints.items(): + if annotation is Request: + return param_name + + return None + + def _registered_api_adapter( app: ApiGatewayResolver, next_middleware: Callable[..., Any], @@ -1708,6 +1507,28 @@ def _registered_api_adapter( """ route_args: dict = app.context.get("_route_args", {}) logger.debug(f"Calling API Route Handler: {route_args}") + + # Inject a Request object when the handler declares a parameter typed as Request. + # Lookup is cached on the Route object to avoid repeated signature inspection. + route: Route | None = app.context.get("_route") + if route is not None: + if not route.request_param_name_checked: + route.request_param_name = _find_request_param_name(next_middleware) + route.request_param_name_checked = True + if route.request_param_name: + route_args = {**route_args, route.request_param_name: app.request} + + # Resolve Depends() parameters + if route.has_dependencies: + from aws_lambda_powertools.event_handler.depends import build_dependency_tree, solve_dependencies + + dep_values = solve_dependencies( + dependant=build_dependency_tree(route.func), + request=app.request, + dependency_overrides=app.dependency_overrides or None, + ) + route_args.update(dep_values) + return app._to_response(next_middleware(**route_args)) @@ -1740,9 +1561,11 @@ def lambda_handler(event, context): ``` """ + _proxy_event_type: Enum = ProxyEventType.APIGatewayProxyEvent + def __init__( self, - proxy_type: Enum = ProxyEventType.APIGatewayProxyEvent, + proxy_type: Enum | None = None, cors: CORSConfig | None = None, debug: bool | None = None, serializer: Callable[[dict], str] | None = None, @@ -1775,7 +1598,8 @@ def __init__( function to deserialize `str`, `bytes`, `bytearray` containing a JSON document to a Python `dict`, by default json.loads when integrating with EventSource data class """ - self._proxy_type = proxy_type + self.dependency_overrides: dict[Callable, Callable] = {} + self._proxy_type = proxy_type or self._proxy_event_type self._dynamic_routes: list[Route] = [] self._static_routes: list[Route] = [] self._route_keys: list[str] = [] @@ -1845,7 +1669,7 @@ def _add_resolver_response_validation_error_response_to_route( response_validation_error_response = { "description": "Response Validation Error", "content": { - _DEFAULT_CONTENT_TYPE: { + DEFAULT_CONTENT_TYPE: { "schema": {"$ref": f"{COMPONENT_REF_PREFIX}ResponseValidationError"}, }, }, @@ -2502,8 +2326,12 @@ def swagger_handler(): # We now inject CSS and JS into the SwaggerUI file swagger_js = Path.open( Path(__file__).parent / "openapi" / "swagger_ui" / "swagger-ui-bundle.min.js", + encoding="utf-8", + ).read() + swagger_css = Path.open( + Path(__file__).parent / "openapi" / "swagger_ui" / "swagger-ui.min.css", + encoding="utf-8", ).read() - swagger_css = Path.open(Path(__file__).parent / "openapi" / "swagger_ui" / "swagger-ui.min.css").read() openapi_servers = servers or [Server(url=(base_path or "/"))] @@ -2546,7 +2374,7 @@ def swagger_handler(): if query_params.get("format") == "json": return Response( status_code=200, - content_type=_DEFAULT_CONTENT_TYPE, + content_type=DEFAULT_CONTENT_TYPE, body=escaped_spec, ) @@ -2598,7 +2426,7 @@ def route( summary: str | None = None, description: str | None = None, responses: dict[int, OpenAPIResponse] | None = None, - response_description: str = _DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, @@ -2607,6 +2435,7 @@ def route( deprecated: bool = False, enable_validation: bool | None = None, custom_response_validation_http_code: int | HTTPStatus | None = None, + status_code: int = DEFAULT_STATUS_CODE, middlewares: list[Callable[..., Any]] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: """Route decorator includes parameter `method`""" @@ -2642,6 +2471,7 @@ def register_resolver(func: AnyCallableT) -> AnyCallableT: deprecated, enable_validation, custom_response_validation_http_code, + status_code, middlewares, ) @@ -2736,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) @@ -2789,28 +2767,11 @@ def _compile_regex(rule: str, base_regex: str = _ROUTE_REGEX): rule_regex: str = re.sub(_DYNAMIC_ROUTE_PATTERN, _NAMED_GROUP_BOUNDARY_PATTERN, rule) return re.compile(base_regex.format(rule_regex)) - def _to_proxy_event(self, event: dict) -> BaseProxyEvent: # noqa: PLR0911 # ignore many returns + def _to_proxy_event(self, event: dict) -> BaseProxyEvent: """Convert the event dict to the corresponding data class""" - if self._proxy_type == ProxyEventType.APIGatewayProxyEvent: - logger.debug("Converting event to API Gateway REST API contract") - return APIGatewayProxyEvent(event, self._json_body_deserializer) - if self._proxy_type == ProxyEventType.APIGatewayProxyEventV2: - logger.debug("Converting event to API Gateway HTTP API contract") - return APIGatewayProxyEventV2(event, self._json_body_deserializer) - if self._proxy_type == ProxyEventType.BedrockAgentEvent: - logger.debug("Converting event to Bedrock Agent contract") - return BedrockAgentEvent(event, self._json_body_deserializer) - if self._proxy_type == ProxyEventType.LambdaFunctionUrlEvent: - logger.debug("Converting event to Lambda Function URL contract") - return LambdaFunctionUrlEvent(event, self._json_body_deserializer) - if self._proxy_type == ProxyEventType.VPCLatticeEvent: - logger.debug("Converting event to VPC Lattice contract") - return VPCLatticeEvent(event, self._json_body_deserializer) - if self._proxy_type == ProxyEventType.VPCLatticeEventV2: - logger.debug("Converting event to VPC LatticeV2 contract") - return VPCLatticeEventV2(event, self._json_body_deserializer) - logger.debug("Converting event to ALB contract") - return ALBEvent(event, self._json_body_deserializer) + event_class, label = _PROXY_EVENT_MAP.get(self._proxy_type, (ALBEvent, "ALB")) + logger.debug("Converting event to %s contract", label) + return event_class(event, self._json_body_deserializer) def _resolve(self) -> ResponseBuilder: """Resolves the response or return the not found response""" @@ -3046,12 +3007,15 @@ def _to_response(self, result: dict | tuple | Response | BedrockResponse) -> Res - 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 """ - status_code = HTTPStatus.OK if isinstance(result, (Response, BedrockResponse)): return result elif isinstance(result, tuple) and len(result) == 2: # Unpack result dict and status code from tuple result, status_code = result + else: + # Use the route's status_code if available, otherwise default to 200 + route: Route | None = self.context.get("_route") + status_code = route.status_code if route else HTTPStatus.OK logger.debug("Simple response detected, serializing return before constructing final response") return Response( @@ -3161,7 +3125,7 @@ def route( summary: str | None = None, description: str | None = None, responses: dict[int, OpenAPIResponse] | None = None, - response_description: str | None = _DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + response_description: str | None = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, @@ -3170,6 +3134,7 @@ def route( deprecated: bool = False, enable_validation: bool | None = None, custom_response_validation_http_code: int | HTTPStatus | None = None, + status_code: int = DEFAULT_STATUS_CODE, middlewares: list[Callable[..., Any]] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: def register_route(func: AnyCallableT) -> AnyCallableT: @@ -3198,6 +3163,7 @@ def register_route(func: AnyCallableT) -> AnyCallableT: deprecated, enable_validation, custom_response_validation_http_code, + status_code, ) # Collate Middleware for routes @@ -3232,28 +3198,7 @@ class APIGatewayRestResolver(ApiGatewayResolver): """Amazon API Gateway REST and HTTP API v1 payload resolver""" current_event: APIGatewayProxyEvent - - def __init__( - self, - cors: CORSConfig | None = None, - debug: bool | None = None, - serializer: Callable[[dict], str] | None = None, - strip_prefixes: list[str | Pattern] | None = None, - enable_validation: bool = False, - response_validation_error_http_code: HTTPStatus | int | None = None, - json_body_deserializer: Callable[[str], dict] | None = None, - ): - """Amazon API Gateway REST and HTTP API v1 payload resolver""" - super().__init__( - ProxyEventType.APIGatewayProxyEvent, - cors, - debug, - serializer, - strip_prefixes, - enable_validation, - response_validation_error_http_code, - json_body_deserializer=json_body_deserializer, - ) + _proxy_event_type = ProxyEventType.APIGatewayProxyEvent def _get_base_path(self) -> str: # 3 different scenarios: @@ -3279,7 +3224,7 @@ def route( summary: str | None = None, description: str | None = None, responses: dict[int, OpenAPIResponse] | None = None, - response_description: str = _DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, @@ -3288,6 +3233,7 @@ def route( deprecated: bool = False, enable_validation: bool | None = None, custom_response_validation_http_code: int | HTTPStatus | None = None, + status_code: int = DEFAULT_STATUS_CODE, middlewares: list[Callable[..., Any]] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: # NOTE: see #1552 for more context. @@ -3309,6 +3255,7 @@ def route( deprecated, enable_validation, custom_response_validation_http_code, + status_code, middlewares, ) @@ -3322,28 +3269,7 @@ class APIGatewayHttpResolver(ApiGatewayResolver): """Amazon API Gateway HTTP API v2 payload resolver""" current_event: APIGatewayProxyEventV2 - - def __init__( - self, - cors: CORSConfig | None = None, - debug: bool | None = None, - serializer: Callable[[dict], str] | None = None, - strip_prefixes: list[str | Pattern] | None = None, - enable_validation: bool = False, - response_validation_error_http_code: HTTPStatus | int | None = None, - json_body_deserializer: Callable[[str], dict] | None = None, - ): - """Amazon API Gateway HTTP API v2 payload resolver""" - super().__init__( - ProxyEventType.APIGatewayProxyEventV2, - cors, - debug, - serializer, - strip_prefixes, - enable_validation, - response_validation_error_http_code, - json_body_deserializer=json_body_deserializer, - ) + _proxy_event_type = ProxyEventType.APIGatewayProxyEventV2 def _get_base_path(self) -> str: # 3 different scenarios: @@ -3363,6 +3289,7 @@ class ALBResolver(ApiGatewayResolver): """Amazon Application Load Balancer (ALB) resolver""" current_event: ALBEvent + _proxy_event_type = ProxyEventType.ALBEvent def __init__( self, @@ -3402,13 +3329,12 @@ def __init__( Enables URL-decoding of query parameters (both keys and values), by default False. """ super().__init__( - ProxyEventType.ALBEvent, - cors, - debug, - serializer, - strip_prefixes, - enable_validation, - response_validation_error_http_code, + cors=cors, + debug=debug, + serializer=serializer, + strip_prefixes=strip_prefixes, + enable_validation=enable_validation, + response_validation_error_http_code=response_validation_error_http_code, json_body_deserializer=json_body_deserializer, ) self.decode_query_parameters = decode_query_parameters @@ -3417,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/bedrock_agent.py b/aws_lambda_powertools/event_handler/bedrock_agent.py index 4593715e88d..a49ba38c214 100644 --- a/aws_lambda_powertools/event_handler/bedrock_agent.py +++ b/aws_lambda_powertools/event_handler/bedrock_agent.py @@ -7,12 +7,16 @@ from aws_lambda_powertools.event_handler import ApiGatewayResolver from aws_lambda_powertools.event_handler.api_gateway import ( - _DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, BedrockResponse, ProxyEventType, ResponseBuilder, ) -from aws_lambda_powertools.event_handler.openapi.constants import DEFAULT_API_VERSION, DEFAULT_OPENAPI_VERSION +from aws_lambda_powertools.event_handler.openapi.constants import ( + DEFAULT_API_VERSION, + DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + DEFAULT_OPENAPI_VERSION, + DEFAULT_STATUS_CODE, +) if TYPE_CHECKING: from collections.abc import Callable @@ -118,7 +122,7 @@ def get( # type: ignore[override] cache_control: str | None = None, summary: str | None = None, responses: dict[int, OpenAPIResponse] | None = None, - response_description: str = _DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, @@ -126,6 +130,7 @@ def get( # type: ignore[override] deprecated: bool = False, enable_validation: bool | None = None, custom_response_validation_http_code: int | HTTPStatus | None = None, + status_code: int = DEFAULT_STATUS_CODE, middlewares: list[Callable[..., Any]] | None = None, ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: security = None @@ -147,6 +152,7 @@ def get( # type: ignore[override] deprecated, enable_validation, custom_response_validation_http_code, + status_code, middlewares, ) @@ -161,7 +167,7 @@ def post( # type: ignore[override] cache_control: str | None = None, summary: str | None = None, responses: dict[int, OpenAPIResponse] | None = None, - response_description: str = _DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, @@ -169,6 +175,7 @@ def post( # type: ignore[override] deprecated: bool = False, enable_validation: bool | None = None, custom_response_validation_http_code: int | HTTPStatus | None = None, + status_code: int = DEFAULT_STATUS_CODE, middlewares: list[Callable[..., Any]] | None = None, ): security = None @@ -190,6 +197,7 @@ def post( # type: ignore[override] deprecated, enable_validation, custom_response_validation_http_code, + status_code, middlewares, ) @@ -204,7 +212,7 @@ def put( # type: ignore[override] cache_control: str | None = None, summary: str | None = None, responses: dict[int, OpenAPIResponse] | None = None, - response_description: str = _DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, @@ -212,6 +220,7 @@ def put( # type: ignore[override] deprecated: bool = False, enable_validation: bool | None = None, custom_response_validation_http_code: int | HTTPStatus | None = None, + status_code: int = DEFAULT_STATUS_CODE, middlewares: list[Callable[..., Any]] | None = None, ): security = None @@ -233,6 +242,7 @@ def put( # type: ignore[override] deprecated, enable_validation, custom_response_validation_http_code, + status_code, middlewares, ) @@ -247,7 +257,7 @@ def patch( # type: ignore[override] cache_control: str | None = None, summary: str | None = None, responses: dict[int, OpenAPIResponse] | None = None, - response_description: str = _DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, @@ -255,6 +265,7 @@ def patch( # type: ignore[override] deprecated: bool = False, enable_validation: bool | None = None, custom_response_validation_http_code: int | HTTPStatus | None = None, + status_code: int = DEFAULT_STATUS_CODE, middlewares: list[Callable] | None = None, ): security = None @@ -276,6 +287,7 @@ def patch( # type: ignore[override] deprecated, enable_validation, custom_response_validation_http_code, + status_code, middlewares, ) @@ -290,7 +302,7 @@ def delete( # type: ignore[override] cache_control: str | None = None, summary: str | None = None, responses: dict[int, OpenAPIResponse] | None = None, - response_description: str = _DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, @@ -298,6 +310,7 @@ def delete( # type: ignore[override] deprecated: bool = False, enable_validation: bool | None = None, custom_response_validation_http_code: int | HTTPStatus | None = None, + status_code: int = DEFAULT_STATUS_CODE, middlewares: list[Callable[..., Any]] | None = None, ): security = None @@ -319,6 +332,7 @@ def delete( # type: ignore[override] deprecated, enable_validation, custom_response_validation_http_code, + status_code, middlewares, ) diff --git a/aws_lambda_powertools/event_handler/depends.py b/aws_lambda_powertools/event_handler/depends.py new file mode 100644 index 00000000000..f05167c63d9 --- /dev/null +++ b/aws_lambda_powertools/event_handler/depends.py @@ -0,0 +1,222 @@ +"""Lightweight dependency injection primitives — no pydantic import.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Annotated, Any, get_args, get_origin, get_type_hints + +if TYPE_CHECKING: + from collections.abc import Callable + + from aws_lambda_powertools.event_handler.openapi.params import Dependant + from aws_lambda_powertools.event_handler.request import Request + + +class DependencyResolutionError(Exception): + """Raised when a dependency cannot be resolved.""" + + +class Depends: + """ + Declares a dependency for a route handler parameter. + + Dependencies are resolved automatically before the handler is called. The return value + of the dependency callable is injected as the parameter value. + + Parameters + ---------- + dependency: Callable[..., Any] + A callable whose return value will be injected into the handler parameter. + The callable can itself declare ``Depends()`` parameters to form a dependency tree. + use_cache: bool + If ``True`` (default), the dependency result is cached per invocation so that + the same dependency used multiple times is only called once. + + Examples + -------- + + ```python + from typing import Annotated + + from aws_lambda_powertools.event_handler import APIGatewayHttpResolver, Depends + + app = APIGatewayHttpResolver() + + def get_tenant() -> str: + return "default-tenant" + + @app.get("/orders") + def list_orders(tenant_id: Annotated[str, Depends(get_tenant)]): + return {"tenant": tenant_id} + ``` + """ + + def __init__(self, dependency: Callable[..., Any], *, use_cache: bool = True) -> None: + if not callable(dependency): + raise DependencyResolutionError( + f"Depends() requires a callable, got {type(dependency).__name__}: {dependency!r}", + ) + self.dependency = dependency + self.use_cache = use_cache + + +class _DependencyNode: + """Lightweight node in a dependency tree — used by ``build_dependency_tree``.""" + + def __init__(self, *, param_name: str, depends: Depends, sub_tree: DependencyTree) -> None: + self.param_name = param_name + self.depends = depends + self.dependant = sub_tree + + +class DependencyTree: + """Lightweight dependency tree — no pydantic required. + + This mirrors the shape that ``solve_dependencies`` expects (a ``.dependencies`` + attribute containing nodes with ``.param_name``, ``.depends``, and ``.dependant``), + but can be built without importing pydantic. + """ + + def __init__(self, *, dependencies: list[_DependencyNode] | None = None) -> None: + self.dependencies: list[_DependencyNode] = dependencies or [] + + +class DependencyParam: + """Holds a dependency's parameter name and its resolved Dependant sub-tree (OpenAPI path).""" + + def __init__(self, *, param_name: str, depends: Depends, dependant: Dependant) -> None: + self.param_name = param_name + self.depends = depends + self.dependant = dependant + + +def _get_depends_from_annotation(annotation: Any) -> Depends | None: + """Extract a Depends instance from an Annotated[Type, Depends(...)] annotation.""" + if get_origin(annotation) is Annotated: + for arg in get_args(annotation)[1:]: + if isinstance(arg, Depends): + return arg + return None + + +def _has_depends(func: Callable[..., Any]) -> bool: + """Check if a callable has any Depends() parameters, without importing pydantic.""" + try: + hints = get_type_hints(func, include_extras=True) + except Exception: + return False + + for annotation in hints.values(): + if _get_depends_from_annotation(annotation) is not None: + return True + return False + + +def build_dependency_tree(func: Callable[..., Any]) -> DependencyTree: + """Build a lightweight dependency tree from a callable's signature. + + This inspects the function parameters for ``Annotated[Type, Depends(...)]`` + annotations and recursively builds the tree — all without importing pydantic. + """ + try: + hints = get_type_hints(func, include_extras=True) + except Exception: + return DependencyTree() + + dependencies: list[_DependencyNode] = [] + + for param_name, annotation in hints.items(): + if param_name == "return": + continue + + depends_instance = _get_depends_from_annotation(annotation) + if depends_instance is not None: + sub_tree = build_dependency_tree(depends_instance.dependency) + dependencies.append( + _DependencyNode( + param_name=param_name, + depends=depends_instance, + sub_tree=sub_tree, + ), + ) + + return DependencyTree(dependencies=dependencies) + + +def solve_dependencies( + *, + dependant: Dependant | DependencyTree, + request: Request | None = None, + dependency_overrides: dict[Callable[..., Any], Callable[..., Any]] | None = None, + dependency_cache: dict[Callable[..., Any], Any] | None = None, +) -> dict[str, Any]: + """ + Recursively resolve all ``Depends()`` parameters for a given dependant. + + Parameters + ---------- + dependant: Dependant + The dependant model containing dependency declarations + request: Request, optional + The current request object, injected into dependencies that declare a Request parameter + dependency_overrides: dict, optional + Mapping of original dependency callable to override callable (for testing) + dependency_cache: dict, optional + Per-invocation cache of resolved dependency values + + Returns + ------- + dict[str, Any] + Mapping of parameter name to resolved dependency value + """ + from aws_lambda_powertools.event_handler.request import Request as RequestClass + + if dependency_cache is None: + dependency_cache = {} + + values: dict[str, Any] = {} + + for dep in dependant.dependencies: + use_fn = dep.depends.dependency + + # Apply overrides (for testing) + if dependency_overrides and use_fn in dependency_overrides: + use_fn = dependency_overrides[use_fn] + + # Check cache + if dep.depends.use_cache and use_fn in dependency_cache: + values[dep.param_name] = dependency_cache[use_fn] + continue + + # Recursively resolve sub-dependencies + sub_values = solve_dependencies( + dependant=dep.dependant, + request=request, + dependency_overrides=dependency_overrides, + dependency_cache=dependency_cache, + ) + + # Inject Request if the dependency declares it + if request is not None: + try: + hints = get_type_hints(use_fn) + except Exception: # pragma: no cover - defensive for broken annotations + hints = {} + for param_name, annotation in hints.items(): + if annotation is RequestClass: + sub_values[param_name] = request + + try: + solved = use_fn(**sub_values) + except Exception as exc: + dep_name = getattr(use_fn, "__name__", repr(use_fn)) + raise DependencyResolutionError( + f"Failed to resolve dependency '{dep_name}' for parameter '{dep.param_name}': {exc}", + ) from exc + + # Cache result + if dep.depends.use_cache: + dependency_cache[use_fn] = solved + + values[dep.param_name] = solved + + return values diff --git a/aws_lambda_powertools/event_handler/http_resolver.py b/aws_lambda_powertools/event_handler/http_resolver.py index 0be443bd200..da72f6fca4d 100644 --- a/aws_lambda_powertools/event_handler/http_resolver.py +++ b/aws_lambda_powertools/event_handler/http_resolver.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio import base64 import inspect import warnings @@ -14,6 +13,7 @@ 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 @@ -239,7 +239,7 @@ def _get_base_path(self) -> str: """Return the base path for HTTP resolver (no stage prefix).""" return "" - async def _resolve_async(self) -> dict: + async def _resolve_async(self) -> dict: # type: ignore[override] """Async version of resolve that supports async handlers.""" method = self.current_event.http_method.upper() path = self._remove_prefix(self.current_event.path) @@ -258,7 +258,7 @@ async def _resolve_async(self) -> dict: # Handle not found return await self._handle_not_found_async() - async def _call_route_async(self, route: Route, route_arguments: dict[str, str]) -> dict: + async def _call_route_async(self, route: Route, route_arguments: dict[str, str]) -> dict: # type: ignore[override] """Call route handler, supporting both sync and async handlers.""" from aws_lambda_powertools.event_handler.api_gateway import ResponseBuilder @@ -319,45 +319,11 @@ async def final_handler(app): next_handler = final_handler for middleware in reversed(all_middlewares): - next_handler = self._wrap_middleware_async(middleware, next_handler) + next_handler = wrap_middleware_async(middleware, next_handler) return await next_handler(self) - def _wrap_middleware_async(self, middleware: Callable, next_handler: Callable) -> Callable: - """Wrap a middleware to work in async context.""" - - async def wrapped(app): - # Create a next_middleware that the sync middleware can call - def sync_next(app): - # This will be called by sync middleware - # We need to run the async next_handler - loop = asyncio.get_event_loop() - if loop.is_running(): - # We're in an async context, create a task - future = asyncio.ensure_future(next_handler(app)) - # Store for later await - app.context["_async_next_result"] = future - return Response(status_code=200, body="") # Placeholder - else: # pragma: no cover - return loop.run_until_complete(next_handler(app)) - - # Check if middleware is async - if inspect.iscoroutinefunction(middleware): - result = await middleware(app, next_handler) - else: - # Sync middleware - need special handling - result = middleware(app, sync_next) - - # Check if we stored an async result - if "_async_next_result" in app.context: - future = app.context.pop("_async_next_result") - result = await future - - return result - - return wrapped - - async def _handle_not_found_async(self) -> dict: + async def _handle_not_found_async(self, method: str = "", path: str = "") -> dict: # type: ignore[override] """Handle 404 responses, using custom not_found handler if registered.""" from http import HTTPStatus diff --git a/aws_lambda_powertools/event_handler/lambda_function_url.py b/aws_lambda_powertools/event_handler/lambda_function_url.py index 279899b645e..cbd92a00b6e 100644 --- a/aws_lambda_powertools/event_handler/lambda_function_url.py +++ b/aws_lambda_powertools/event_handler/lambda_function_url.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Pattern +from typing import TYPE_CHECKING from aws_lambda_powertools.event_handler.api_gateway import ( ApiGatewayResolver, @@ -8,10 +8,6 @@ ) if TYPE_CHECKING: - from collections.abc import Callable - from http import HTTPStatus - - from aws_lambda_powertools.event_handler import CORSConfig from aws_lambda_powertools.utilities.data_classes import LambdaFunctionUrlEvent @@ -52,27 +48,7 @@ def lambda_handler(event, context): """ current_event: LambdaFunctionUrlEvent - - def __init__( - self, - cors: CORSConfig | None = None, - debug: bool | None = None, - serializer: Callable[[dict], str] | None = None, - strip_prefixes: list[str | Pattern] | None = None, - enable_validation: bool = False, - response_validation_error_http_code: HTTPStatus | int | None = None, - json_body_deserializer: Callable[[str], dict] | None = None, - ): - super().__init__( - ProxyEventType.LambdaFunctionUrlEvent, - cors, - debug, - serializer, - strip_prefixes, - enable_validation, - response_validation_error_http_code, - json_body_deserializer=json_body_deserializer, - ) + _proxy_event_type = ProxyEventType.LambdaFunctionUrlEvent def _get_base_path(self) -> str: stage = self.current_event.request_context.stage diff --git a/aws_lambda_powertools/event_handler/middlewares/async_utils.py b/aws_lambda_powertools/event_handler/middlewares/async_utils.py new file mode 100644 index 00000000000..4f375bc9b0b --- /dev/null +++ b/aws_lambda_powertools/event_handler/middlewares/async_utils.py @@ -0,0 +1,224 @@ +"""Async middleware utilities for bridging sync and async middleware execution.""" + +from __future__ import annotations + +import asyncio +import inspect +import logging +import threading +from typing import TYPE_CHECKING, Any + +logger = logging.getLogger(__name__) + +if TYPE_CHECKING: + from collections.abc import Callable + + from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver, BedrockResponse, Response + + +def wrap_middleware_async(middleware: Callable, next_handler: Callable) -> Callable: + """Wrap a middleware to work in an async context. + + For async middlewares, delegates directly with ``await``. + + For sync middlewares, runs the middleware in a background thread and uses + ``asyncio.Event`` / ``threading.Event`` to coordinate the ``next()`` call + so the async handler can be awaited on the main event-loop while the sync + middleware blocks its own thread waiting for the result. + + Parameters + ---------- + middleware : Callable + A sync or async middleware ``(app, next_middleware) -> Response``. + next_handler : Callable + The next (async) handler in the chain. + + Returns + ------- + Callable + An async callable ``(app) -> Response`` that executes *middleware* + followed by *next_handler*. + """ + + async def wrapped(app: ApiGatewayResolver) -> Response: + if inspect.iscoroutinefunction(middleware): + return await middleware(app, next_handler) + + return await _run_sync_middleware_in_thread(middleware, next_handler, app) + + return wrapped + + +async def _run_sync_middleware_in_thread( + middleware: Callable, + next_handler: Callable, + app: Any, +) -> Any: + """Execute a **sync** middleware inside a daemon thread. + + The sync middleware calls ``sync_next(app)`` which: + + 1. Signals the async side that the middleware is ready for the next handler. + 2. Blocks the thread until the async handler has produced a response. + 3. Returns the response so the middleware can do post-processing. + + Meanwhile the async side awaits *next_handler*, feeds the response back, + and waits for the thread to finish. + """ + middleware_called_next = asyncio.Event() + next_app_holder: list = [] + real_response_holder: list = [] + middleware_result_holder: list = [] + middleware_error_holder: list = [] + + def sync_next(app: Any) -> Any: + next_app_holder.append(app) + middleware_called_next.set() + # Block this thread until the async handler resolves + event = threading.Event() + next_app_holder.append(event) + event.wait() + return real_response_holder[0] + + def run_middleware() -> None: + try: + result = middleware(app, sync_next) + 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() 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) + + # Unblock the middleware thread + threading_event = next_app_holder[1] + threading_event.set() + + # Wait for the middleware thread to complete post-processing + thread.join() + + if middleware_error_holder: + raise middleware_error_holder[0] + + 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], +) -> dict | tuple | Response | BedrockResponse: + """ + Async version of _registered_api_adapter. + + Detects if the route handler is a coroutine and awaits it. + _to_response() stays sync (CPU-bound — no async benefit). + + IMPORTANT: This is an internal building block only. + Nothing calls it in the resolve chain yet. It will be used + by resolve_async() (see issue #8137). + + Parameters + ---------- + app: ApiGatewayResolver + The API Gateway resolver + next_middleware: Callable[..., Any] + The function to handle the API + + Returns + ------- + Response + The API Response Object + """ + route_args: dict = app.context.get("_route_args", {}) + logger.debug(f"Calling API Route Handler: {route_args}") + + route = app.context.get("_route") + if route is not None: + if not route.request_param_name_checked: + from aws_lambda_powertools.event_handler.api_gateway import _find_request_param_name + + route.request_param_name = _find_request_param_name(next_middleware) + route.request_param_name_checked = True + if route.request_param_name: + route_args = {**route_args, route.request_param_name: app.request} + + if route.has_dependencies: + from aws_lambda_powertools.event_handler.depends import build_dependency_tree, solve_dependencies + + dep_values = solve_dependencies( + dependant=build_dependency_tree(route.func), + request=app.request, + dependency_overrides=app.dependency_overrides or None, + ) + route_args.update(dep_values) + + # Call handler — detect if result is a coroutine and await it + result = next_middleware(**route_args) + if inspect.iscoroutine(result): + result = await result + + return app._to_response(result) diff --git a/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py b/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py index dbfc0a6f9d7..470a19e6c54 100644 --- a/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py +++ b/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py @@ -1,8 +1,10 @@ from __future__ import annotations +import base64 import dataclasses import json import logging +import warnings from typing import TYPE_CHECKING, Any, Callable, Mapping, MutableMapping, Sequence, Union, cast from urllib.parse import parse_qs @@ -25,7 +27,7 @@ RequestValidationError, ResponseValidationError, ) -from aws_lambda_powertools.event_handler.openapi.params import Param +from aws_lambda_powertools.event_handler.openapi.params import Param, UploadFile from aws_lambda_powertools.event_handler.openapi.types import UnionType if TYPE_CHECKING: @@ -44,6 +46,7 @@ CONTENT_DISPOSITION_NAME_PARAM = "name=" APPLICATION_JSON_CONTENT_TYPE = "application/json" APPLICATION_FORM_CONTENT_TYPE = "application/x-www-form-urlencoded" +MULTIPART_FORM_DATA_CONTENT_TYPE = "multipart/form-data" class OpenAPIRequestValidationMiddleware(BaseMiddlewareHandler): @@ -96,10 +99,17 @@ def handler(self, app: EventHandlerInstance, next_middleware: NextMiddleware) -> headers, ) + # Process cookie values + cookie_values, cookie_errors = _request_params_to_args( + route.dependant.cookie_params, + app.current_event.resolved_cookies_field, + ) + values.update(path_values) values.update(query_values) values.update(header_values) - errors += path_errors + query_errors + header_errors + values.update(cookie_values) + errors += path_errors + query_errors + header_errors + cookie_errors # Process the request body, if it exists if route.dependant.body_params: @@ -134,14 +144,18 @@ def _get_body(self, app: EventHandlerInstance) -> dict[str, Any]: elif content_type.startswith(APPLICATION_FORM_CONTENT_TYPE): return self._parse_form_data(app) + # Handle multipart/form-data (file uploads) + elif content_type.startswith(MULTIPART_FORM_DATA_CONTENT_TYPE): + return self._parse_multipart_data(app, content_type) + else: raise RequestUnsupportedContentType( - "Only JSON body or Form() are supported", + "Unsupported content type", errors=[ { "type": "unsupported_content_type", "loc": ("body",), - "msg": "Only JSON body or Form() are supported", + "msg": f"Unsupported content type: {content_type}", "input": {}, "ctx": {}, }, @@ -188,6 +202,49 @@ def _parse_form_data(self, app: EventHandlerInstance) -> dict[str, Any]: ], ) from e + def _parse_multipart_data(self, app: EventHandlerInstance, content_type: str) -> dict[str, Any]: + """Parse multipart/form-data from the request body (file uploads).""" + try: + # Extract the boundary from the content-type header + boundary = _extract_multipart_boundary(content_type) + if not boundary: + raise ValueError("Missing boundary in multipart/form-data content-type header") + + # Get raw body bytes + raw_body = app.current_event.body or "" + if app.current_event.is_base64_encoded: + body_bytes = base64.b64decode(raw_body) + else: + warnings.warn( + "Received multipart/form-data without base64 encoding. " + "Binary file uploads may be corrupted. " + "If using API Gateway REST API (v1), configure Binary Media Types " + "to include 'multipart/form-data'. " + "See: https://docs.aws.amazon.com/apigateway/latest/developerguide/" + "api-gateway-payload-encodings.html", + stacklevel=2, + ) + # Use latin-1 to preserve all byte values (0-255) since the body + # may contain raw binary data that isn't valid UTF-8 + body_bytes = raw_body.encode("latin-1") + + return _parse_multipart_body(body_bytes, boundary) + + except ValueError: + raise + except Exception as e: + raise RequestValidationError( + [ + { + "type": "multipart_invalid", + "loc": ("body",), + "msg": "Multipart form data parsing error", + "input": {}, + "ctx": {"error": str(e)}, + }, + ], + ) from e + class OpenAPIResponseValidationMiddleware(BaseMiddlewareHandler): """ @@ -239,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, ) @@ -391,7 +452,12 @@ def _request_body_to_args( continue value = _normalize_field_value(value=value, field_info=field.field_info) - values[field.name] = _validate_field(field=field, value=value, loc=loc, existing_errors=errors) + + # UploadFile objects bypass Pydantic validation — they're already constructed + if isinstance(value, UploadFile): + values[field.name] = value + else: + values[field.name] = _validate_field(field=field, value=value, loc=loc, existing_errors=errors) return values, errors @@ -467,6 +533,10 @@ def _is_or_contains_sequence(annotation: Any) -> bool: def _normalize_field_value(value: Any, field_info: FieldInfo) -> Any: """Normalize field value, converting lists to single values for non-sequence fields.""" + # When annotation is bytes but value is UploadFile, extract raw content + if isinstance(value, UploadFile) and field_info.annotation is bytes: + return value.content + if _is_or_contains_sequence(field_info.annotation): return value elif isinstance(value, list) and value: @@ -580,3 +650,106 @@ def _get_param_value( value = input_dict.get(field_name) return value + + +def _extract_multipart_boundary(content_type: str) -> str | None: + """Extract the boundary string from a multipart/form-data content-type header.""" + for segment in content_type.split(";"): + stripped = segment.strip() + if stripped.startswith("boundary="): + boundary = stripped[len("boundary=") :] + # Remove optional quotes around boundary + if boundary.startswith('"') and boundary.endswith('"'): + boundary = boundary[1:-1] + return boundary + return None + + +def _parse_multipart_body(body: bytes, boundary: str) -> dict[str, Any]: + """ + Parse a multipart/form-data body into a dict of field names to values. + + File fields get bytes values; regular form fields get string values. + Multiple values for the same field name are collected into lists. + """ + delimiter = f"--{boundary}".encode() + end_delimiter = f"--{boundary}--".encode() + + result: dict[str, Any] = {} + + # Split body by the boundary delimiter + raw_parts = body.split(delimiter) + + for raw_part in raw_parts: + # Skip the preamble (before first boundary) and epilogue (after closing boundary) + if not raw_part or raw_part.strip() == b"" or raw_part.strip() == b"--": + continue + + # Remove the end delimiter marker if present + chunk = raw_part + if chunk.endswith(end_delimiter): + chunk = chunk[: -len(end_delimiter)] + + # Strip leading \r\n + if chunk.startswith(b"\r\n"): + chunk = chunk[2:] + + # Strip trailing \r\n + if chunk.endswith(b"\r\n"): + chunk = chunk[:-2] + + # Split headers from body at the double CRLF + header_end = chunk.find(b"\r\n\r\n") + if header_end == -1: + continue + + header_section = chunk[:header_end].decode("utf-8") + body_section = chunk[header_end + 4 :] + + # Parse Content-Disposition to get the field name and optional filename + field_name = None + filename = None + content_type_header = None + + for header_line in header_section.split("\r\n"): + header_lower = header_line.lower() + if header_lower.startswith("content-disposition:"): + field_name = _extract_header_param(header_line, "name") + filename = _extract_header_param(header_line, "filename") + elif header_lower.startswith("content-type:"): + content_type_header = header_line.split(":", 1)[1].strip() + + if field_name is None: + continue + + # If it has a filename, it's a file upload — wrap as UploadFile + # Otherwise it's a regular form field — decode to string + if filename is not None: + value: Any = UploadFile(content=body_section, filename=filename, content_type=content_type_header) + else: + value = body_section.decode("utf-8") + + # Collect multiple values for same field name into a list + if field_name in result: + existing = result[field_name] + if isinstance(existing, list): + existing.append(value) + else: + result[field_name] = [existing, value] + else: + result[field_name] = value + + return result + + +def _extract_header_param(header_line: str, param_name: str) -> str | None: + """Extract a parameter value from a header line (e.g., name="file" from Content-Disposition).""" + search = f'{param_name}="' + idx = header_line.find(search) + if idx == -1: + return None + start = idx + len(search) + end = header_line.find('"', start) + if end == -1: + return None + return header_line[start:end] diff --git a/aws_lambda_powertools/event_handler/openapi/constants.py b/aws_lambda_powertools/event_handler/openapi/constants.py index debe1d56736..7c5b938920a 100644 --- a/aws_lambda_powertools/event_handler/openapi/constants.py +++ b/aws_lambda_powertools/event_handler/openapi/constants.py @@ -1,3 +1,6 @@ DEFAULT_API_VERSION = "1.0.0" DEFAULT_OPENAPI_VERSION = "3.1.0" DEFAULT_OPENAPI_TITLE = "Powertools for AWS Lambda (Python) API" +DEFAULT_CONTENT_TYPE = "application/json" +DEFAULT_OPENAPI_RESPONSE_DESCRIPTION = "Successful Response" +DEFAULT_STATUS_CODE = 200 diff --git a/aws_lambda_powertools/event_handler/openapi/dependant.py b/aws_lambda_powertools/event_handler/openapi/dependant.py index 310cab68e66..1e7f4327602 100644 --- a/aws_lambda_powertools/event_handler/openapi/dependant.py +++ b/aws_lambda_powertools/event_handler/openapi/dependant.py @@ -4,6 +4,7 @@ import re from typing import TYPE_CHECKING, Any, ForwardRef, cast +from aws_lambda_powertools.event_handler.depends import DependencyParam, _get_depends_from_annotation from aws_lambda_powertools.event_handler.openapi.compat import ( ModelField, create_body_model, @@ -13,15 +14,16 @@ from aws_lambda_powertools.event_handler.openapi.params import ( Body, Dependant, + File, Form, Param, ParamTypes, - _File, analyze_param, create_response_field, get_flat_dependant, ) from aws_lambda_powertools.event_handler.openapi.types import OpenAPIResponse, OpenAPIResponseContentModel +from aws_lambda_powertools.event_handler.request import Request if TYPE_CHECKING: from collections.abc import Callable @@ -187,6 +189,27 @@ def get_dependant( # Add each parameter to the dependant model for param_name, param in signature_params.items(): + # Request-typed parameters are injected by the resolver at call time; + # they carry no OpenAPI meaning and must be excluded from schema generation. + if param.annotation is Request: + continue + + # Depends() parameters (via Annotated[Type, Depends(fn)]) are resolved at call time. + depends_instance = _get_depends_from_annotation(param.annotation) + if depends_instance is not None: + sub_dependant = get_dependant( + path=path, + call=depends_instance.dependency, + ) + dependant.dependencies.append( + DependencyParam( + param_name=param_name, + depends=depends_instance, + dependant=sub_dependant, + ), + ) + continue + # If the parameter is a path parameter, we need to set the in_ field to "path". is_path_param = param_name in path_param_names @@ -364,9 +387,9 @@ def get_body_field_info( if not required: body_field_info_kwargs["default"] = None - if any(isinstance(f.field_info, _File) for f in flat_dependant.body_params): - # MAINTENANCE: body_field_info: type[Body] = _File - raise NotImplementedError("_File fields are not supported in request bodies") + if any(isinstance(f.field_info, File) for f in flat_dependant.body_params): + body_field_info = Body + body_field_info_kwargs["media_type"] = "multipart/form-data" elif any(isinstance(f.field_info, Form) for f in flat_dependant.body_params): body_field_info = Body body_field_info_kwargs["media_type"] = "application/x-www-form-urlencoded" diff --git a/aws_lambda_powertools/event_handler/openapi/encoders.py b/aws_lambda_powertools/event_handler/openapi/encoders.py index 59ce47ebc1d..d1b8861bdde 100644 --- a/aws_lambda_powertools/event_handler/openapi/encoders.py +++ b/aws_lambda_powertools/event_handler/openapi/encoders.py @@ -5,7 +5,7 @@ from collections import defaultdict, deque from decimal import Decimal from enum import Enum -from pathlib import Path, PurePath +from pathlib import PurePath from re import Pattern from types import GeneratorType from typing import TYPE_CHECKING, Any @@ -103,17 +103,14 @@ def jsonable_encoder( # noqa: PLR0911 custom_serializer=custom_serializer, ) - # Enums - if isinstance(obj, Enum): - return obj.value + # Simple type dispatch (exact type match, then isinstance for subclasses) + encoder = ENCODERS_BY_TYPE.get(type(obj)) + if encoder is not None: + return encoder(obj) - # Paths - if isinstance(obj, PurePath): - return str(obj) - - # Scalars - if isinstance(obj, (str, int, float, type(None))): - return obj + for encoder_fn, classes_tuple in _encoders_by_class_tuples.items(): + if isinstance(obj, classes_tuple): + return encoder_fn(obj) # Dictionaries if isinstance(obj, dict): @@ -140,14 +137,6 @@ def jsonable_encoder( # noqa: PLR0911 custom_serializer=custom_serializer, ) - # Other types - if type(obj) in ENCODERS_BY_TYPE: - return ENCODERS_BY_TYPE[type(obj)](obj) - - for encoder, classes_tuple in encoders_by_class_tuples.items(): - if isinstance(obj, classes_tuple): - return encoder(obj) - # Use custom serializer if present if custom_serializer: return custom_serializer(obj) @@ -346,6 +335,11 @@ def decimal_encoder(dec_value: Decimal) -> int | float: # Encoders for types that are not JSON serializable ENCODERS_BY_TYPE: dict[type[Any], Callable[[Any], Any]] = { + bool: lambda o: o, + int: lambda o: o, + float: lambda o: o, + str: lambda o: o, + type(None): lambda o: o, bytes: lambda o: o.decode(), datetime.date: iso_format, datetime.datetime: iso_format, @@ -353,14 +347,10 @@ def decimal_encoder(dec_value: Decimal) -> int | float: datetime.timedelta: lambda td: td.total_seconds(), Decimal: decimal_encoder, Enum: lambda o: o.value, - frozenset: list, - deque: list, - GeneratorType: list, - Path: str, + PurePath: str, Pattern: lambda o: o.pattern, SecretBytes: str, SecretStr: str, - set: list, UUID: str, } @@ -376,4 +366,4 @@ def generate_encoders_by_class_tuples( # Mapping of encoders to a tuple of classes that they can encode -encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE) +_encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE) diff --git a/aws_lambda_powertools/event_handler/openapi/merge.py b/aws_lambda_powertools/event_handler/openapi/merge.py index 38b80914df3..4b7f51cab1c 100644 --- a/aws_lambda_powertools/event_handler/openapi/merge.py +++ b/aws_lambda_powertools/event_handler/openapi/merge.py @@ -7,6 +7,7 @@ import importlib.util import logging import sys +import warnings from pathlib import Path from typing import TYPE_CHECKING, Any, Literal @@ -53,9 +54,9 @@ def _is_resolver_call(node: ast.expr) -> bool: func = node.func if isinstance(func, ast.Name) and func.id in RESOLVER_CLASSES: return True - if isinstance(func, ast.Attribute) and func.attr in RESOLVER_CLASSES: # pragma: no cover + if isinstance(func, ast.Attribute) and func.attr in RESOLVER_CLASSES: return True - return False # pragma: no cover + return False def _file_has_resolver(file_path: Path, resolver_name: str) -> bool: @@ -67,14 +68,80 @@ def _file_has_resolver(file_path: Path, resolver_name: str) -> bool: return False for node in ast.walk(tree): + targets: list[ast.expr] = [] + value: ast.expr | None = None if isinstance(node, ast.Assign): - for target in node.targets: - if isinstance(target, ast.Name) and target.id == resolver_name: - if _is_resolver_call(node.value): + targets = node.targets + value = node.value + elif isinstance(node, ast.AnnAssign): + targets = [node.target] + value = node.value + for target in targets: + if isinstance(target, ast.Name) and target.id == resolver_name: + if value is not None and _is_resolver_call(value): + return True + return False + + +def _file_imports_resolver(file_path: Path, resolver_file: Path, resolver_name: str, root: Path) -> bool: + """Check if a Python file imports the resolver from the resolver file.""" + try: + source = file_path.read_text(encoding="utf-8") + tree = ast.parse(source, filename=str(file_path)) + except (SyntaxError, UnicodeDecodeError): + return False + + # Get the module path of the resolver file relative to root + # e.g., "service/handlers/utils/rest_api_resolver.py" -> "service.handlers.utils.rest_api_resolver" + resolver_relative = resolver_file.relative_to(root).with_suffix("") + resolver_module = ".".join(resolver_relative.parts) + + for node in ast.walk(tree): + # Check "from X import app" or "from X import app as something" + if isinstance(node, ast.ImportFrom) and node.module: + for alias in node.names: + if alias.name == resolver_name: + # Check if the import module matches the resolver module + if node.module == resolver_module: return True return False +def _find_dependent_files( + search_path: Path, + resolver_file: Path, + resolver_name: str, + exclude: list[str], + project_root: Path, +) -> list[Path]: + """Find all Python files that import the resolver. + + Parameters + ---------- + search_path : Path + Directory to search for dependent files. + resolver_file : Path + The resolver file that dependents import from. + resolver_name : str + Variable name of the resolver. + exclude : list[str] + Patterns to exclude. + project_root : Path + Root directory for resolving Python imports. + """ + dependent_files: list[Path] = [] + + for file_path in search_path.rglob("*.py"): + if file_path == resolver_file: + continue + if _is_excluded(file_path, search_path, exclude): + continue + if _file_imports_resolver(file_path, resolver_file, resolver_name, project_root): + dependent_files.append(file_path) + + return sorted(dependent_files) + + def _is_excluded(file_path: Path, root: Path, exclude_patterns: list[str]) -> bool: """Check if a file matches any exclusion pattern.""" relative_str = str(file_path.relative_to(root)) @@ -84,12 +151,11 @@ def _is_excluded(file_path: Path, root: Path, exclude_patterns: list[str]) -> bo sub_pattern = pattern[3:] if fnmatch.fnmatch(relative_str, pattern) or fnmatch.fnmatch(file_path.name, sub_pattern): return True - # Check directory parts - remove trailing glob patterns clean_pattern = sub_pattern.replace("/**", "").replace("/*", "") for part in file_path.relative_to(root).parts: - if fnmatch.fnmatch(part, clean_pattern): # pragma: no cover + if fnmatch.fnmatch(part, clean_pattern): return True - elif fnmatch.fnmatch(relative_str, pattern) or fnmatch.fnmatch(file_path.name, pattern): # pragma: no cover + elif fnmatch.fnmatch(relative_str, pattern) or fnmatch.fnmatch(file_path.name, pattern): return True return False @@ -99,7 +165,7 @@ def _get_glob_pattern(pat: str, recursive: bool) -> str: if recursive and not pat.startswith("**/"): return f"**/{pat}" if not recursive and pat.startswith("**/"): - return pat[3:] # Strip **/ prefix + return pat[3:] return pat @@ -131,133 +197,87 @@ def _discover_resolver_files( return sorted(found_files) -def _load_resolver(file_path: Path, resolver_name: str) -> Any: - """Load a resolver instance from a Python file.""" - file_path = Path(file_path).resolve() - module_name = f"_powertools_openapi_merge_{file_path.stem}_{id(file_path)}" - +def _load_module(file_path: Path, module_name: str) -> Any: + """Load a Python module from file.""" spec = importlib.util.spec_from_file_location(module_name, file_path) - if spec is None or spec.loader is None: # pragma: no cover + if spec is None or spec.loader is None: raise ImportError(f"Cannot load module from {file_path}") module = importlib.util.module_from_spec(spec) - module_dir = str(file_path.parent) + sys.modules[module_name] = module + spec.loader.exec_module(module) + return module + + +def _load_resolver_with_dependencies( + file_path: Path, + resolver_name: str, + dependent_files: list[Path], + root: Path, +) -> Any: + """Load a resolver instance, first loading all dependent files that register routes.""" + file_path = Path(file_path).resolve() + + # Add root to sys.path if not already there + root_str = str(root) original_path = sys.path.copy() try: - if module_dir not in sys.path: - sys.path.insert(0, module_dir) - sys.modules[module_name] = module - spec.loader.exec_module(module) + if root_str not in sys.path: + sys.path.insert(0, root_str) + + # First, load all dependent files (they will import the resolver and register routes) + for dep_file in dependent_files: + dep_module_name = f"_powertools_dep_{dep_file.stem}_{id(dep_file)}" + try: + _load_module(dep_file, dep_module_name) + logger.debug(f"Loaded dependent file: {dep_file}") + except Exception as e: + warnings.warn( + f"Failed to load dependent file {dep_file}: {e}. " + "If your handler module has side effects at import time " + "(e.g. environment variable validation, database connections), " + "consider deferring them to runtime.", + stacklevel=2, + ) + + # Now get the resolver - it should already be loaded by the dependent files + # Try to get it from the module that was loaded by dependents + resolver_relative = file_path.relative_to(root).with_suffix("") + resolver_module_name = ".".join(resolver_relative.parts) + + if resolver_module_name in sys.modules: + module = sys.modules[resolver_module_name] + else: + # Fallback: load the resolver file directly + module_name = f"_powertools_openapi_merge_{file_path.stem}_{id(file_path)}" + module = _load_module(file_path, module_name) if not hasattr(module, resolver_name): raise AttributeError(f"Resolver '{resolver_name}' not found in {file_path}.") return getattr(module, resolver_name) finally: sys.path = original_path - sys.modules.pop(module_name, None) def _model_to_dict(obj: Any) -> Any: """Convert Pydantic model to dict if needed.""" if hasattr(obj, "model_dump"): return obj.model_dump(by_alias=True, exclude_none=True) - return obj # pragma: no cover + return obj class OpenAPIMerge: """ Discover and merge OpenAPI schemas from multiple Lambda handlers. - This class is designed for micro-functions architectures where you have multiple - Lambda functions, each with its own resolver, and need to generate a unified - OpenAPI specification. It's particularly useful for: - - - CI/CD pipelines to generate and publish unified API documentation - - Build-time schema generation for API Gateway imports - - Creating a dedicated Lambda that serves the consolidated OpenAPI spec + This class supports two patterns: + 1. Standard pattern: Each handler file defines its own resolver with routes + 2. Shared resolver pattern: A central resolver file is imported by multiple handler files + that register routes on it - The class uses AST analysis to detect resolver instances without importing modules, - making discovery fast and safe. - - Parameters - ---------- - title : str - The title of the unified API. - version : str - The version of the API (e.g., "1.0.0"). - openapi_version : str, default "3.1.0" - The OpenAPI specification version. - summary : str, optional - A short summary of the API. - description : str, optional - A detailed description of the API. - tags : list[Tag | str], optional - Tags for API documentation organization. - servers : list[Server], optional - Server objects for API connectivity information. - terms_of_service : str, optional - URL to the Terms of Service. - contact : Contact, optional - Contact information for the API. - license_info : License, optional - License information for the API. - security_schemes : dict[str, SecurityScheme], optional - Security scheme definitions. - security : list[dict[str, list[str]]], optional - Global security requirements. - external_documentation : ExternalDocumentation, optional - Link to external documentation. - openapi_extensions : dict[str, Any], optional - OpenAPI specification extensions (x-* fields). - on_conflict : Literal["warn", "error", "first", "last"], default "warn" - Strategy when the same path+method is defined in multiple handlers: - - "warn": Log warning and keep first definition - - "error": Raise OpenAPIMergeError - - "first": Silently keep first definition - - "last": Use last definition (override) - - Example - ------- - **CI/CD Pipeline - Generate unified schema at build time:** - - >>> from aws_lambda_powertools.event_handler.openapi import OpenAPIMerge - >>> - >>> merge = OpenAPIMerge( - ... title="My Unified API", - ... version="1.0.0", - ... description="Consolidated API from multiple Lambda functions", - ... ) - >>> merge.discover( - ... path="./src/functions", - ... pattern="**/handler.py", - ... exclude=["**/tests/**"], - ... ) - >>> schema_json = merge.get_openapi_json_schema() - >>> - >>> # Write to file for API Gateway import or documentation - >>> with open("openapi.json", "w") as f: - ... f.write(schema_json) - - **Dedicated OpenAPI Lambda - Serve unified spec at runtime:** - - >>> from aws_lambda_powertools.event_handler import APIGatewayRestResolver - >>> - >>> app = APIGatewayRestResolver() - >>> app.configure_openapi_merge( - ... path="./functions", - ... pattern="**/handler.py", - ... title="My API", - ... version="1.0.0", - ... ) - >>> app.enable_swagger(path="/docs") # Swagger UI with merged schema - >>> - >>> def handler(event, context): - ... return app.resolve(event, context) - - See Also - -------- - OpenAPIMergeError : Exception raised on merge conflicts when on_conflict="error" + For the shared resolver pattern, this class automatically discovers files that import + the resolver and loads them before extracting the schema, ensuring all routes are registered. """ def __init__( @@ -297,9 +317,12 @@ def __init__( ) self._schemas: list[dict[str, Any]] = [] self._discovered_files: list[Path] = [] + self._dependent_files: dict[Path, list[Path]] = {} self._resolver_name: str = "app" self._on_conflict = on_conflict self._cached_schema: dict[str, Any] | None = None + self._root: Path | None = None + self._exclude: list[str] = [] def discover( self, @@ -308,45 +331,41 @@ def discover( exclude: list[str] | None = None, resolver_name: str = "app", recursive: bool = False, + project_root: str | Path | None = None, ) -> list[Path]: - """ - Discover resolver files in the specified path using glob patterns. - - This method scans the directory tree for Python files matching the pattern, - then uses AST analysis to identify files containing resolver instances. + """Discover resolver files and their dependent handler files. Parameters ---------- path : str | Path - Root directory to search for handler files. - pattern : str | list[str], default "handler.py" + Directory to search for resolver files. + pattern : str | list[str] Glob pattern(s) to match handler files. - exclude : list[str], optional - Patterns to exclude. Defaults to ["**/tests/**", "**/__pycache__/**", "**/.venv/**"]. - resolver_name : str, default "app" - Variable name of the resolver instance in handler files. - recursive : bool, default False - Whether to search recursively in subdirectories. - - Returns - ------- - list[Path] - List of discovered files containing resolver instances. - - Example - ------- - >>> merge = OpenAPIMerge(title="API", version="1.0.0") - >>> files = merge.discover( - ... path="./src", - ... pattern=["handler.py", "api.py"], - ... exclude=["**/tests/**", "**/legacy/**"], - ... recursive=True, - ... ) - >>> print(f"Found {len(files)} handlers") + exclude : list[str] | None + Patterns to exclude. + resolver_name : str + Variable name of the resolver instance. + recursive : bool + Whether to search recursively. + project_root : str | Path | None + Root directory for resolving Python imports. If None, uses current working directory. + This is needed when handlers import the resolver using absolute imports like + 'from service.handlers.utils.resolver import app'. """ exclude = exclude or ["**/tests/**", "**/__pycache__/**", "**/.venv/**"] + self._exclude = exclude self._resolver_name = resolver_name + self._search_path = Path(path).resolve() + self._root = Path(project_root).resolve() if project_root else self._search_path + self._discovered_files = _discover_resolver_files(path, pattern, exclude, resolver_name, recursive) + + # For each resolver file, find files that import it (search within path, resolve imports with project_root) + for resolver_file in self._discovered_files: + dependent = _find_dependent_files(self._search_path, resolver_file, resolver_name, exclude, self._root) + self._dependent_files[resolver_file] = dependent + logger.debug(f"Found {len(dependent)} dependent files for {resolver_file}") + return self._discovered_files def add_file(self, file_path: str | Path, resolver_name: str | None = None) -> None: @@ -369,87 +388,75 @@ def add_schema(self, schema: dict[str, Any]) -> None: """ self._schemas.append(_model_to_dict(schema)) - def get_openapi_schema(self) -> dict[str, Any]: - """ - Generate the merged OpenAPI schema as a dictionary. - - Loads all discovered resolver files, extracts their OpenAPI schemas, - and merges them into a single unified specification. - - The schema is cached after the first generation for performance. + @property + def discovered_files(self) -> list[Path]: + """Get the list of discovered resolver files.""" + return self._discovered_files.copy() - Returns - ------- - dict[str, Any] - The merged OpenAPI schema. + @property + def dependent_files(self) -> dict[Path, list[Path]]: + """Get the mapping of resolver files to their dependent handler files.""" + return {k: v.copy() for k, v in self._dependent_files.items()} - Raises - ------ - OpenAPIMergeError - If on_conflict="error" and duplicate path+method combinations are found. - """ + def get_openapi_schema(self) -> dict[str, Any]: + """Generate the merged OpenAPI schema.""" if self._cached_schema is not None: return self._cached_schema - # Load schemas from discovered files for file_path in self._discovered_files: try: - resolver = _load_resolver(file_path, self._resolver_name) + dependent = self._dependent_files.get(file_path, []) + root = self._root or file_path.parent + resolver = _load_resolver_with_dependencies( + file_path, + self._resolver_name, + dependent, + root, + ) if hasattr(resolver, "get_openapi_schema"): self._schemas.append(_model_to_dict(resolver.get_openapi_schema())) - except (ImportError, AttributeError, FileNotFoundError) as e: # pragma: no cover - logger.warning(f"Failed to load resolver from {file_path}: {e}") + except (ImportError, AttributeError, FileNotFoundError) as e: + warnings.warn( + f"Failed to load resolver from {file_path}: {e}. " + "If your handler module has side effects at import time " + "(e.g. environment variable validation, database connections), " + "consider deferring them to runtime.", + stacklevel=1, + ) self._cached_schema = self._merge_schemas() + + if self._discovered_files and not self._cached_schema.get("paths"): + warnings.warn( + f"OpenAPIMerge discovered {len(self._discovered_files)} handler file(s) " + "but the final schema has no paths. " + "Check if your handler modules have side effects at import time " + "that prevent route registration.", + stacklevel=1, + ) + return self._cached_schema def get_openapi_json_schema(self) -> str: - """ - Generate the merged OpenAPI schema as a JSON string. - - This is the recommended method for CI/CD pipelines and build-time - schema generation, as the output can be directly written to a file - or used for API Gateway imports. - - Returns - ------- - str - The merged OpenAPI schema as formatted JSON. - - Example - ------- - >>> merge = OpenAPIMerge(title="API", version="1.0.0") - >>> merge.discover(path="./functions", pattern="**/handler.py") - >>> json_schema = merge.get_openapi_json_schema() - >>> with open("openapi.json", "w") as f: - ... f.write(json_schema) - """ + """Generate the merged OpenAPI schema as JSON string.""" from aws_lambda_powertools.event_handler.openapi.compat import model_json from aws_lambda_powertools.event_handler.openapi.models import OpenAPI schema = self.get_openapi_schema() return model_json(OpenAPI(**schema), by_alias=True, exclude_none=True, indent=2) - @property - def discovered_files(self) -> list[Path]: - """Get the list of discovered resolver files.""" - return self._discovered_files.copy() - def _merge_schemas(self) -> dict[str, Any]: """Merge all schemas into a single OpenAPI schema.""" cfg = self._config - # Build base schema merged: dict[str, Any] = { "openapi": cfg.openapi_version, "info": {"title": cfg.title, "version": cfg.version}, "servers": [_model_to_dict(s) for s in cfg.servers] if cfg.servers else [{"url": "/"}], } - # Add optional info fields self._add_optional_info_fields(merged, cfg) - # Merge paths and components merged_paths: dict[str, Any] = {} merged_components: dict[str, dict[str, Any]] = {} @@ -457,7 +464,6 @@ def _merge_schemas(self) -> dict[str, Any]: self._merge_paths(schema.get("paths", {}), merged_paths) self._merge_components(schema.get("components", {}), merged_components) - # Add security schemes from config if cfg.security_schemes: merged_components.setdefault("securitySchemes", {}).update(cfg.security_schemes) @@ -466,7 +472,6 @@ def _merge_schemas(self) -> dict[str, Any]: if merged_components: merged["components"] = merged_components - # Merge tags if merged_tags := self._merge_tags(): merged["tags"] = merged_tags @@ -514,12 +519,7 @@ def _handle_conflict(self, method: str, path: str, target: dict, operation: Any) target[path][method] = operation def _merge_components(self, source: dict[str, Any], target: dict[str, dict[str, Any]]) -> None: - """Merge components from source into target. - - Note: Components with the same name are silently overwritten (last wins). - This is intentional as component conflicts are typically user errors - (e.g., two handlers defining different 'User' schemas). - """ + """Merge components from source into target.""" for component_type, components in source.items(): target.setdefault(component_type, {}).update(components) @@ -527,7 +527,6 @@ def _merge_tags(self) -> list[dict[str, Any]]: """Merge tags from config and schemas.""" tags_map: dict[str, dict[str, Any]] = {} - # Config tags first for tag in self._config.tags or []: if isinstance(tag, str): tags_map[tag] = {"name": tag} @@ -535,11 +534,10 @@ def _merge_tags(self) -> list[dict[str, Any]]: tag_dict = _model_to_dict(tag) tags_map[tag_dict["name"]] = tag_dict - # Schema tags (don't override config) for schema in self._schemas: for tag in schema.get("tags", []): name = tag["name"] if isinstance(tag, dict) else tag if name not in tags_map: - tags_map[name] = tag if isinstance(tag, dict) else {"name": tag} # pragma: no cover + tags_map[name] = tag if isinstance(tag, dict) else {"name": tag} return list(tags_map.values()) diff --git a/aws_lambda_powertools/event_handler/openapi/params.py b/aws_lambda_powertools/event_handler/openapi/params.py index 8b70b7cb074..468e2253b39 100644 --- a/aws_lambda_powertools/event_handler/openapi/params.py +++ b/aws_lambda_powertools/event_handler/openapi/params.py @@ -23,6 +23,7 @@ if TYPE_CHECKING: from collections.abc import Callable + from aws_lambda_powertools.event_handler.depends import DependencyParam from aws_lambda_powertools.event_handler.openapi.models import Example from aws_lambda_powertools.event_handler.openapi.types import CacheKey @@ -64,6 +65,7 @@ def __init__( http_connection_param_name: str | None = None, response_param_name: str | None = None, background_tasks_param_name: str | None = None, + dependencies: list[DependencyParam] | None = None, path: str | None = None, ) -> None: self.path_params = path_params or [] @@ -78,6 +80,7 @@ def __init__( self.http_connection_param_name = http_connection_param_name self.response_param_name = response_param_name self.background_tasks_param_name = background_tasks_param_name + self.dependencies = dependencies or [] self.name = name self.call = call # Store the path to be able to re-generate a dependable from it in overrides @@ -286,64 +289,6 @@ def __init__( json_schema_extra: dict[str, Any] | None = None, **extra: Any, ): - """ - Constructs a new Path param. - - Parameters - ---------- - default: Any - The default value of the parameter - default_factory: Callable[[], Any], optional - Callable that will be called when a default value is needed for this field - annotation: Any, optional - The type annotation of the parameter - alias: str, optional - The public name of the field - alias_priority: int, optional - Priority of the alias. This affects whether an alias generator is used - validation_alias: str | AliasPath | AliasChoices | None, optional - Alias to be used for validation only - serialization_alias: str | AliasPath | AliasChoices | None, optional - Alias to be used for serialization only - title: str, optional - The title of the parameter - description: str, optional - The description of the parameter - gt: float, optional - Only applies to numbers, required the field to be "greater than" - ge: float, optional - Only applies to numbers, required the field to be "greater than or equal" - lt: float, optional - Only applies to numbers, required the field to be "less than" - le: float, optional - Only applies to numbers, required the field to be "less than or equal" - min_length: int, optional - Only applies to strings, required the field to have a minimum length - max_length: int, optional - Only applies to strings, required the field to have a maximum length - pattern: str, optional - Only applies to strings, requires the field match against a regular expression pattern string - discriminator: str, optional - Parameter field name for discriminating the type in a tagged union - strict: bool, optional - Enables Pydantic's strict mode for the field - multiple_of: float, optional - Only applies to numbers, requires the field to be a multiple of the given value - allow_inf_nan: bool, optional - Only applies to numbers, requires the field to allow infinity and NaN values - max_digits: int, optional - Only applies to Decimals, requires the field to have a maxmium number of digits within the decimal. - decimal_places: int, optional - Only applies to Decimals, requires the field to have at most a number of decimal places - examples: list[Any], optional - A list of examples for the parameter - deprecated: bool, optional - If `True`, the parameter will be marked as deprecated - include_in_schema: bool, optional - If `False`, the parameter will be excluded from the generated OpenAPI schema - json_schema_extra: dict[str, Any], optional - Extra values to include in the generated OpenAPI schema - """ if default is not ...: raise AssertionError("Path parameters cannot have a default value") @@ -418,64 +363,6 @@ def __init__( json_schema_extra: dict[str, Any] | None = None, **extra: Any, ): - """ - Constructs a new Query param. - - Parameters - ---------- - default: Any - The default value of the parameter - default_factory: Callable[[], Any], optional - Callable that will be called when a default value is needed for this field - annotation: Any, optional - The type annotation of the parameter - alias: str, optional - The public name of the field - alias_priority: int, optional - Priority of the alias. This affects whether an alias generator is used - validation_alias: str | AliasPath | AliasChoices | None, optional - Alias to be used for validation only - serialization_alias: str | AliasPath | AliasChoices | None, optional - Alias to be used for serialization only - title: str, optional - The title of the parameter - description: str, optional - The description of the parameter - gt: float, optional - Only applies to numbers, required the field to be "greater than" - ge: float, optional - Only applies to numbers, required the field to be "greater than or equal" - lt: float, optional - Only applies to numbers, required the field to be "less than" - le: float, optional - Only applies to numbers, required the field to be "less than or equal" - min_length: int, optional - Only applies to strings, required the field to have a minimum length - max_length: int, optional - Only applies to strings, required the field to have a maximum length - pattern: str, optional - Only applies to strings, requires the field match against a regular expression pattern string - discriminator: str, optional - Parameter field name for discriminating the type in a tagged union - strict: bool, optional - Enables Pydantic's strict mode for the field - multiple_of: float, optional - Only applies to numbers, requires the field to be a multiple of the given value - allow_inf_nan: bool, optional - Only applies to numbers, requires the field to allow infinity and NaN values - max_digits: int, optional - Only applies to Decimals, requires the field to have a maxmium number of digits within the decimal. - decimal_places: int, optional - Only applies to Decimals, requires the field to have at most a number of decimal places - examples: list[Any], optional - A list of examples for the parameter - deprecated: bool, optional - If `True`, the parameter will be marked as deprecated - include_in_schema: bool, optional - If `False`, the parameter will be excluded from the generated OpenAPI schema - json_schema_extra: dict[str, Any], optional - Extra values to include in the generated OpenAPI schema - """ super().__init__( default=default, default_factory=default_factory, @@ -550,67 +437,6 @@ def __init__( json_schema_extra: dict[str, Any] | None = None, **extra: Any, ): - """ - Constructs a new Query param. - - Parameters - ---------- - default: Any - The default value of the parameter - default_factory: Callable[[], Any], optional - Callable that will be called when a default value is needed for this field - annotation: Any, optional - The type annotation of the parameter - alias: str, optional - The public name of the field - alias_priority: int, optional - Priority of the alias. This affects whether an alias generator is used - validation_alias: str | AliasPath | AliasChoices | None, optional - Alias to be used for validation only - serialization_alias: str | AliasPath | AliasChoices | None, optional - Alias to be used for serialization only - convert_underscores: bool - If true convert "_" to "-" - See RFC: https://www.rfc-editor.org/rfc/rfc9110.html#name-field-name-registry - title: str, optional - The title of the parameter - description: str, optional - The description of the parameter - gt: float, optional - Only applies to numbers, required the field to be "greater than" - ge: float, optional - Only applies to numbers, required the field to be "greater than or equal" - lt: float, optional - Only applies to numbers, required the field to be "less than" - le: float, optional - Only applies to numbers, required the field to be "less than or equal" - min_length: int, optional - Only applies to strings, required the field to have a minimum length - max_length: int, optional - Only applies to strings, required the field to have a maximum length - pattern: str, optional - Only applies to strings, requires the field match against a regular expression pattern string - discriminator: str, optional - Parameter field name for discriminating the type in a tagged union - strict: bool, optional - Enables Pydantic's strict mode for the field - multiple_of: float, optional - Only applies to numbers, requires the field to be a multiple of the given value - allow_inf_nan: bool, optional - Only applies to numbers, requires the field to allow infinity and NaN values - max_digits: int, optional - Only applies to Decimals, requires the field to have a maxmium number of digits within the decimal. - decimal_places: int, optional - Only applies to Decimals, requires the field to have at most a number of decimal places - examples: list[Any], optional - A list of examples for the parameter - deprecated: bool, optional - If `True`, the parameter will be marked as deprecated - include_in_schema: bool, optional - If `False`, the parameter will be excluded from the generated OpenAPI schema - json_schema_extra: dict[str, Any], optional - Extra values to include in the generated OpenAPI schema - """ self.convert_underscores = convert_underscores self._alias = alias @@ -658,6 +484,14 @@ def alias(self, value: str | None = None): self._alias = value.lower() +class Cookie(Param): # type: ignore[misc] + """ + A class used internally to represent a cookie parameter in a path operation. + """ + + in_ = ParamTypes.cookie + + class Body(FieldInfo): # type: ignore[misc] """ A class used internally to represent a body parameter in a path operation. @@ -829,7 +663,57 @@ def __init__( ) -class _File(Form): # type: ignore[misc] +class UploadFile: + """ + Represents an uploaded file with its metadata. + + Use with ``Annotated[UploadFile, File()]`` to receive file content along with + filename and content type. For raw bytes only, use ``Annotated[bytes, File()]``. + + Attributes + ---------- + filename : str | None + The original filename from the upload. + content_type : str | None + The MIME type declared by the client (e.g. ``image/jpeg``). + content : bytes + The raw file content. + """ + + __slots__ = ("content", "content_type", "filename") + + def __init__(self, *, content: bytes, filename: str | None = None, content_type: str | None = None): + self.content = content + self.filename = filename + self.content_type = content_type + + def __len__(self) -> int: + return len(self.content) + + def __repr__(self) -> str: + return f"UploadFile(filename={self.filename!r}, content_type={self.content_type!r}, size={len(self.content)})" + + @classmethod + def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: Any) -> Any: + from pydantic_core import core_schema + + return core_schema.no_info_plain_validator_function( + cls._validate, + serialization=core_schema.plain_serializer_function_ser_schema(lambda v: v, info_arg=False), + ) + + @classmethod + def _validate(cls, v: Any) -> UploadFile: + if isinstance(v, cls): + return v + raise ValueError(f"Expected UploadFile, got {type(v).__name__}") + + @classmethod + def __get_pydantic_json_schema__(cls, _schema: Any, handler: Any) -> dict[str, Any]: + return {"type": "string", "format": "binary"} + + +class File(Form): # type: ignore[misc] """ A class used to represent a file parameter in a path operation. """ @@ -935,7 +819,7 @@ def get_flat_dependant( visited = [] visited.append(dependant.cache_key) - return Dependant( + flat = Dependant( path_params=dependant.path_params.copy(), query_params=dependant.query_params.copy(), header_params=dependant.header_params.copy(), @@ -944,6 +828,18 @@ def get_flat_dependant( path=dependant.path, ) + # Flatten sub-dependencies that declare HTTP params (query, header, etc.) + for dep in dependant.dependencies: + if dep.dependant.cache_key not in visited: + sub_flat = get_flat_dependant(dep.dependant, visited=visited) + flat.path_params.extend(sub_flat.path_params) + flat.query_params.extend(sub_flat.query_params) + flat.header_params.extend(sub_flat.header_params) + flat.cookie_params.extend(sub_flat.cookie_params) + flat.body_params.extend(sub_flat.body_params) + + return flat + def analyze_param( *, @@ -1005,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 @@ -1242,6 +1138,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. @@ -1268,4 +1165,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 new file mode 100644 index 00000000000..6334a3b53cc --- /dev/null +++ b/aws_lambda_powertools/event_handler/openapi/schema_generator.py @@ -0,0 +1,553 @@ +""" +OpenAPI schema generation for individual routes. + +Extracted from Route to keep route configuration and schema generation +as separate concerns. All functions here are internal. +""" + +from __future__ import annotations + +import copy +import warnings +from typing import TYPE_CHECKING, Any, Literal, cast + +from aws_lambda_powertools.event_handler.openapi.types import ( + COMPONENT_REF_PREFIX, + METHODS_WITH_BODY, + OpenAPIResponse, + OpenAPIResponseContentModel, + OpenAPIResponseContentSchema, + response_validation_error_response_definition, + validation_error_definition, + validation_error_response_definition, +) + +if TYPE_CHECKING: + from collections.abc import Sequence + from http import HTTPStatus + + from aws_lambda_powertools.event_handler.openapi.compat import ( + JsonSchemaValue, + ModelField, + ) + from aws_lambda_powertools.event_handler.openapi.params import Dependant, Param + from aws_lambda_powertools.event_handler.openapi.types import TypeModelOrEnum + +from aws_lambda_powertools.event_handler.openapi.constants import ( + DEFAULT_CONTENT_TYPE, + DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + DEFAULT_STATUS_CODE, +) + + +def generate_openapi_path( + *, + method: str, + operation_id: str, + summary: str | None, + description: str | None, + openapi_path: str, + tags: list[str], + deprecated: bool, + security: list[dict[str, list[str]]] | None, + openapi_extensions: dict[str, Any] | None, + responses: dict[int, OpenAPIResponse] | None, + response_description: str | None, + body_field: ModelField | None, + custom_response_validation_http_code: HTTPStatus | None, + status_code: int = DEFAULT_STATUS_CODE, + dependant: Dependant, + operation_ids: set[str], + model_name_map: dict[TypeModelOrEnum, str], + field_mapping: dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], + enable_validation: bool = False, +) -> tuple[dict[str, Any], dict[str, Any]]: + """ + Generate the OpenAPI path spec and definitions for a single route. + """ + from aws_lambda_powertools.event_handler.openapi.dependant import get_flat_params + + definitions: dict[str, Any] = {} + + # Build operation metadata + operation = _build_operation_metadata( + method=method, + operation_id=operation_id, + summary=summary, + description=description, + openapi_path=openapi_path, + tags=tags, + deprecated=deprecated, + operation_ids=operation_ids, + func_name=dependant.call.__name__ if dependant.call else "", + func_file=getattr(dependant.call, "__globals__", {}).get("__file__") if dependant.call else None, + ) + + _apply_optional_fields(operation, security=security, openapi_extensions=openapi_extensions) + + # Build parameters + all_route_params = get_flat_params(dependant) + parameters = _build_operation_parameters( + all_route_params=all_route_params, + model_name_map=model_name_map, + field_mapping=field_mapping, + ) + + if parameters: + operation["parameters"] = _deduplicate_parameters(parameters) + + # Build request body + _apply_request_body( + operation, + method=method, + body_field=body_field, + model_name_map=model_name_map, + field_mapping=field_mapping, + ) + + # Build responses + operation_responses, response_definitions = _build_responses( + responses=responses, + response_description=response_description, + custom_response_validation_http_code=custom_response_validation_http_code, + status_code=status_code, + dependant=dependant, + model_name_map=model_name_map, + field_mapping=field_mapping, + enable_validation=enable_validation, + ) + definitions.update(response_definitions) + + operation["responses"] = operation_responses + path = {method.lower(): operation} + + _add_validation_error_definitions(definitions) + + return path, definitions + + +def _build_operation_metadata( + *, + method: str, + operation_id: str, + summary: str | None, + description: str | None, + openapi_path: str, + tags: list[str], + deprecated: bool, + operation_ids: set[str], + func_name: str, + func_file: str | None, +) -> dict[str, Any]: + """Build the OpenAPI operation metadata (tags, summary, operationId, etc.).""" + _warn_duplicate_operation_id(operation_id, operation_ids, func_name, func_file) + operation_ids.add(operation_id) + + operation: dict[str, Any] = { + "summary": summary or f"{method.upper()} {openapi_path}", + "operationId": operation_id, + "deprecated": deprecated or None, + } + + if tags: + operation["tags"] = tags + if description: + operation["description"] = description + + return operation + + +def _build_operation_parameters( + *, + all_route_params: Sequence[ModelField], + model_name_map: dict[TypeModelOrEnum, str], + field_mapping: dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], +) -> list[dict[str, Any]]: + """Build the list of OpenAPI operation parameters.""" + from aws_lambda_powertools.event_handler.openapi.params import Param + + parameters: list[dict[str, Any]] = [] + + for param in all_route_params: + field_info = cast(Param, param.field_info) + if not field_info.include_in_schema: + continue + + if _is_pydantic_model_param(field_info): + parameters.extend(_expand_pydantic_model_parameters(field_info)) + else: + parameters.append(_create_regular_parameter(param, model_name_map, field_mapping)) + + return parameters + + +def _build_request_body( + *, + body_field: ModelField | None, + model_name_map: dict[TypeModelOrEnum, str], + field_mapping: dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], +) -> dict[str, Any] | None: + """Build the OpenAPI request body spec.""" + from aws_lambda_powertools.event_handler.openapi.compat import ModelField as ModelFieldClass + from aws_lambda_powertools.event_handler.openapi.compat import get_schema_from_model_field + from aws_lambda_powertools.event_handler.openapi.params import Body + + if not body_field: + return None + + if not isinstance(body_field, ModelFieldClass): + raise AssertionError(f"Expected ModelField, got {body_field}") + + body_schema = get_schema_from_model_field( + field=body_field, + model_name_map=model_name_map, + field_mapping=field_mapping, + ) + + field_info = cast(Body, body_field.field_info) + + request_body_oai: dict[str, Any] = {} + if body_field.required: + request_body_oai["required"] = body_field.required + if field_info.description: + request_body_oai["description"] = field_info.description + + request_body_oai["content"] = { + field_info.media_type: _build_media_content(body_schema, field_info.openapi_examples), + } + return request_body_oai + + +def _build_responses( + *, + responses: dict[int, OpenAPIResponse] | None, + response_description: str | None, + custom_response_validation_http_code: HTTPStatus | None, + status_code: int = DEFAULT_STATUS_CODE, + dependant: Dependant, + model_name_map: dict[TypeModelOrEnum, str], + field_mapping: dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], + enable_validation: bool, +) -> tuple[dict[int, OpenAPIResponse], dict[str, Any]]: + """Build the OpenAPI response specs and any extra definitions.""" + definitions: dict[str, Any] = {} + operation_responses: dict[int, OpenAPIResponse] = {} + + _add_validation_responses(operation_responses, enable_validation=enable_validation) + _add_response_validation_error( + operation_responses, + definitions, + custom_response_validation_http_code=custom_response_validation_http_code, + ) + + if responses: + for resp_code in list(responses): + operation_responses[resp_code] = _build_custom_response( + response=copy.deepcopy(responses[resp_code]), + dependant=dependant, + model_name_map=model_name_map, + field_mapping=field_mapping, + ) + else: + response_schema = _build_return_schema( + param=dependant.return_param, + model_name_map=model_name_map, + field_mapping=field_mapping, + ) + + operation_responses[status_code] = { + "description": response_description or DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + "content": {DEFAULT_CONTENT_TYPE: response_schema}, + } + + return operation_responses, definitions + + +def _build_return_schema( + *, + param: ModelField | None, + model_name_map: dict[TypeModelOrEnum, str], + field_mapping: dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], +) -> OpenAPIResponseContentSchema: + """Build the response schema for a return parameter.""" + if param is None: + return {} + + from aws_lambda_powertools.event_handler.openapi.compat import get_schema_from_model_field + + return_schema = get_schema_from_model_field( + field=param, + model_name_map=model_name_map, + field_mapping=field_mapping, + ) + + return {"schema": return_schema} + + +def _is_pydantic_model_param(field_info: Param) -> bool: + """Check if the field info represents a Pydantic model parameter.""" + from pydantic import BaseModel + + from aws_lambda_powertools.event_handler.openapi.compat import lenient_issubclass + + return lenient_issubclass(field_info.annotation, BaseModel) + + +def _expand_pydantic_model_parameters(field_info: Param) -> list[dict[str, Any]]: + """Expand a Pydantic model into individual OpenAPI parameters.""" + from pydantic import BaseModel + + model_class = cast(type[BaseModel], field_info.annotation) + parameters: list[dict[str, Any]] = [] + + for field_name, field_def in model_class.model_fields.items(): + param_name = field_def.alias or field_name + individual_param = _create_pydantic_field_parameter( + param_name=param_name, + field_def=field_def, + param_location=field_info.in_.value, + ) + parameters.append(individual_param) + + return parameters + + +def _create_pydantic_field_parameter( + param_name: str, + field_def: Any, + param_location: str, +) -> dict[str, Any]: + """Create an OpenAPI parameter from a Pydantic field definition.""" + individual_param: dict[str, Any] = { + "name": param_name, + "in": param_location, + "required": field_def.is_required() if hasattr(field_def, "is_required") else field_def.default is ..., + "schema": _get_basic_type_schema(field_def.annotation or type(None)), + } + + if field_def.description: + individual_param["description"] = field_def.description + + return individual_param + + +def _create_regular_parameter( + param: ModelField, + model_name_map: dict[TypeModelOrEnum, str], + field_mapping: dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], +) -> dict[str, Any]: + """Create an OpenAPI parameter from a regular ModelField.""" + from aws_lambda_powertools.event_handler.openapi.compat import get_schema_from_model_field + from aws_lambda_powertools.event_handler.openapi.params import Param + + field_info = cast(Param, param.field_info) + param_schema = get_schema_from_model_field( + field=param, + model_name_map=model_name_map, + field_mapping=field_mapping, + ) + + parameter: dict[str, Any] = { + "name": param.alias, + "in": field_info.in_.value, + "required": param.required, + "schema": param_schema, + } + + if field_info.description: + parameter["description"] = field_info.description + if field_info.openapi_examples: + parameter["examples"] = field_info.openapi_examples + if field_info.deprecated: + parameter["deprecated"] = field_info.deprecated + + return parameter + + +def _get_basic_type_schema(param_type: type) -> dict[str, str]: + """Get basic OpenAPI schema for simple types.""" + type_map: dict[type, str] = {bool: "boolean", int: "integer", float: "number"} + try: + for base_type, schema_type in type_map.items(): + if issubclass(param_type, base_type): + return {"type": schema_type} + return {"type": "string"} + except TypeError: + return {"type": "string"} + + +def _apply_optional_fields( + operation: dict[str, Any], + *, + security: list[dict[str, list[str]]] | None, + openapi_extensions: dict[str, Any] | None, +) -> None: + """Apply optional security and extension fields to the operation.""" + if security: + operation["security"] = security + if openapi_extensions: + operation.update(openapi_extensions) + + +def _apply_request_body( + operation: dict[str, Any], + *, + method: str, + body_field: ModelField | None, + model_name_map: dict[TypeModelOrEnum, str], + field_mapping: dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], +) -> None: + """Build and apply request body to operation if applicable.""" + if method.upper() not in METHODS_WITH_BODY: + return + + request_body_oai = _build_request_body( + body_field=body_field, + model_name_map=model_name_map, + field_mapping=field_mapping, + ) + if request_body_oai: + operation["requestBody"] = request_body_oai + + +def _add_validation_responses( + operation_responses: dict[int, OpenAPIResponse], + *, + enable_validation: bool, +) -> None: + """Add 422 validation error response if validation is enabled.""" + if not enable_validation: + return + + operation_responses[422] = { + "description": "Validation Error", + "content": { + DEFAULT_CONTENT_TYPE: {"schema": {"$ref": f"{COMPONENT_REF_PREFIX}HTTPValidationError"}}, + }, + } + + +def _add_response_validation_error( + operation_responses: dict[int, OpenAPIResponse], + definitions: dict[str, Any], + *, + custom_response_validation_http_code: HTTPStatus | None, +) -> None: + """Add response validation error if a custom HTTP code is configured.""" + if not custom_response_validation_http_code: + return + + http_code = custom_response_validation_http_code.value + operation_responses[http_code] = { + "description": "Response Validation Error", + "content": { + DEFAULT_CONTENT_TYPE: {"schema": {"$ref": f"{COMPONENT_REF_PREFIX}ResponseValidationError"}}, + }, + } + definitions["ResponseValidationError"] = response_validation_error_response_definition + + +def _deduplicate_parameters(parameters: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Deduplicate parameters, giving priority to required ones.""" + all_parameters = {(param["in"], param["name"]): param for param in parameters} + required_parameters = {(param["in"], param["name"]): param for param in parameters if param.get("required")} + all_parameters.update(required_parameters) + return list(all_parameters.values()) + + +def _add_validation_error_definitions(definitions: dict[str, Any]) -> None: + """Add standard validation error schema definitions if not already present.""" + if "ValidationError" not in definitions: + definitions["ValidationError"] = validation_error_definition + definitions["HTTPValidationError"] = validation_error_response_definition + + +def _warn_duplicate_operation_id( + operation_id: str, + operation_ids: set[str], + func_name: str, + func_file: str | None, +) -> None: + """Warn if an operationId has already been used.""" + if operation_id not in operation_ids: + return + + message = f"Duplicate Operation ID {operation_id} for function {func_name}" + if func_file: + message += f" in {func_file}" + warnings.warn(message, stacklevel=1) + + +def _build_media_content( + body_schema: dict[str, Any], + openapi_examples: dict[str, Any] | None, +) -> dict[str, Any]: + """Build the media content dict for a request body.""" + content: dict[str, Any] = {"schema": body_schema} + if openapi_examples: + content["examples"] = openapi_examples + return content + + +def _build_custom_response( + *, + response: OpenAPIResponse, + dependant: Dependant, + model_name_map: dict[TypeModelOrEnum, str], + field_mapping: dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], +) -> OpenAPIResponse: + """Build a single custom response, resolving model references in content.""" + if "content" not in response: + response["content"] = { + DEFAULT_CONTENT_TYPE: _build_return_schema( + param=dependant.return_param, + model_name_map=model_name_map, + field_mapping=field_mapping, + ), + } + return response + + for content_type, payload in response["content"].items(): + response["content"][content_type] = _resolve_response_payload( + payload=payload, + dependant=dependant, + model_name_map=model_name_map, + field_mapping=field_mapping, + ) + + return response + + +def _resolve_response_payload( + *, + payload: OpenAPIResponseContentSchema | OpenAPIResponseContentModel, + dependant: Dependant, + model_name_map: dict[TypeModelOrEnum, str], + field_mapping: dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], +) -> OpenAPIResponseContentSchema: + """Resolve a single response content payload, replacing model refs with schemas.""" + if "model" not in payload: + return cast(OpenAPIResponseContentSchema, payload) + + model_payload_typed = cast(OpenAPIResponseContentModel, payload) + return_field = next( + filter( + lambda model: model.type_ is model_payload_typed["model"], + dependant.response_extra_models, + ), + ) + if not return_field: + raise AssertionError("Model declared in custom responses was not found") + + model_payload = _build_return_schema( + param=return_field, + model_name_map=model_name_map, + field_mapping=field_mapping, + ) + + new_payload: OpenAPIResponseContentSchema = {} + for key, value in payload.items(): + if key != "model": + new_payload[key] = value # type: ignore[literal-required] + new_payload.update(model_payload) + return new_payload diff --git a/aws_lambda_powertools/event_handler/request.py b/aws_lambda_powertools/event_handler/request.py new file mode 100644 index 00000000000..59d1f84d962 --- /dev/null +++ b/aws_lambda_powertools/event_handler/request.py @@ -0,0 +1,170 @@ +"""Resolved HTTP Request object for Event Handler.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent + + +class Request: + """Represents the resolved HTTP request. + + Provides structured access to the matched route pattern, extracted path parameters, + HTTP method, headers, query parameters, body, the full Powertools proxy event + (``resolved_event``), and the shared resolver context (``context``). + + Available via ``app.request`` inside middleware and, when added as a type-annotated + parameter, inside ``Depends()`` dependency functions and route handlers. + + Examples + -------- + **Dependency injection with Depends()** + + ```python + from typing import Annotated + from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Request, Depends + + app = APIGatewayRestResolver() + + def get_auth_user(request: Request) -> str: + # Full event access via resolved_event + token = request.resolved_event.get_header_value("authorization", default_value="") + user = validate_token(token) + # Bridge with middleware via shared context + request.context["user"] = user + return user + + @app.get("/orders") + def list_orders(user: Annotated[str, Depends(get_auth_user)]): + return {"user": user} + ``` + + **Middleware usage** + + ```python + from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Request, Response + from aws_lambda_powertools.event_handler.middlewares import NextMiddleware + + app = APIGatewayRestResolver() + + def auth_middleware(app: APIGatewayRestResolver, next_middleware: NextMiddleware) -> Response: + request: Request = app.request + + route = request.route # "/applications/{application_id}" + path_params = request.path_parameters # {"application_id": "4da715ee-..."} + method = request.method # "PUT" + + if not is_authorized(route, method, path_params): + return Response(status_code=403, body="Forbidden") + + return next_middleware(app) + + app.use(middlewares=[auth_middleware]) + ``` + """ + + __slots__ = ("_context", "_current_event", "_path_parameters", "_route_path") + + def __init__( + self, + route_path: str, + path_parameters: dict[str, Any], + current_event: BaseProxyEvent, + context: dict[str, Any] | None = None, + ) -> None: + self._route_path = route_path + self._path_parameters = path_parameters + self._current_event = current_event + self._context = context if context is not None else {} + + @property + def route(self) -> str: + """Matched route pattern in OpenAPI path-template format. + + Examples + -------- + For a route registered as ``/applications/`` the value is + ``/applications/{application_id}``. + """ + return self._route_path + + @property + def path_parameters(self) -> dict[str, Any]: + """Extracted path parameters for the matched route. + + Examples + -------- + For a request to ``/applications/4da715ee``, matched against + ``/applications/``, the value is + ``{"application_id": "4da715ee"}``. + """ + return self._path_parameters + + @property + def method(self) -> str: + """HTTP method in upper-case, e.g. ``"GET"``, ``"PUT"``.""" + return self._current_event.http_method.upper() + + @property + def headers(self) -> dict[str, str]: + """Request headers dict (lower-cased keys may vary by event source).""" + return self._current_event.headers or {} + + @property + def query_parameters(self) -> dict[str, str] | None: + """Query string parameters, or ``None`` when none are present.""" + return self._current_event.query_string_parameters + + @property + def body(self) -> str | None: + """Raw request body string, or ``None`` when the request has no body.""" + return self._current_event.body + + @property + def json_body(self) -> Any: + """Request body deserialized as a Python object (dict / list), or ``None``.""" + return self._current_event.json_body + + @property + def resolved_event(self) -> BaseProxyEvent: + """Full Powertools proxy event with all helpers and properties. + + Provides access to the complete ``BaseProxyEvent`` (or subclass) that + Powertools resolved for the current invocation. This includes cookies, + request context, path, and event-source-specific properties that are not + available through the convenience properties on :class:`Request`. + + Examples + -------- + ```python + def get_request_details(request: Request) -> dict: + event = request.resolved_event + return { + "path": event.path, + "cookies": event.cookies, + "request_context": event.request_context, + } + ``` + """ + return self._current_event + + @property + def context(self) -> dict[str, Any]: + """Shared resolver context (``app.context``) for this invocation. + + Provides read/write access to the same ``dict`` that middleware and + ``app.append_context()`` populate. This enables incremental migration + from middleware-based data sharing to ``Depends()``-based injection: + middleware writes to ``app.context``, dependencies read from + ``request.context``. + + Examples + -------- + ```python + def get_current_user(request: Request) -> dict: + return request.context["user"] + ``` + """ + return self._context diff --git a/aws_lambda_powertools/event_handler/vpc_lattice.py b/aws_lambda_powertools/event_handler/vpc_lattice.py index 40eafc01d01..9c3efc03792 100644 --- a/aws_lambda_powertools/event_handler/vpc_lattice.py +++ b/aws_lambda_powertools/event_handler/vpc_lattice.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Pattern +from typing import TYPE_CHECKING from aws_lambda_powertools.event_handler.api_gateway import ( ApiGatewayResolver, @@ -8,10 +8,6 @@ ) if TYPE_CHECKING: - from collections.abc import Callable - from http import HTTPStatus - - from aws_lambda_powertools.event_handler import CORSConfig from aws_lambda_powertools.utilities.data_classes import VPCLatticeEvent, VPCLatticeEventV2 @@ -48,28 +44,7 @@ def lambda_handler(event, context): """ current_event: VPCLatticeEvent - - def __init__( - self, - cors: CORSConfig | None = None, - debug: bool | None = None, - serializer: Callable[[dict], str] | None = None, - strip_prefixes: list[str | Pattern] | None = None, - enable_validation: bool = False, - response_validation_error_http_code: HTTPStatus | int | None = None, - json_body_deserializer: Callable[[str], dict] | None = None, - ): - """Amazon VPC Lattice resolver""" - super().__init__( - ProxyEventType.VPCLatticeEvent, - cors, - debug, - serializer, - strip_prefixes, - enable_validation, - response_validation_error_http_code, - json_body_deserializer=json_body_deserializer, - ) + _proxy_event_type = ProxyEventType.VPCLatticeEvent def _get_base_path(self) -> str: return "" @@ -108,28 +83,7 @@ def lambda_handler(event, context): """ current_event: VPCLatticeEventV2 - - def __init__( - self, - cors: CORSConfig | None = None, - debug: bool | None = None, - serializer: Callable[[dict], str] | None = None, - strip_prefixes: list[str | Pattern] | None = None, - enable_validation: bool = False, - response_validation_error_http_code: HTTPStatus | int | None = None, - json_body_deserializer: Callable[[str], dict] | None = None, - ): - """Amazon VPC Lattice resolver""" - super().__init__( - ProxyEventType.VPCLatticeEventV2, - cors, - debug, - serializer, - strip_prefixes, - enable_validation, - response_validation_error_http_code, - json_body_deserializer=json_body_deserializer, - ) + _proxy_event_type = ProxyEventType.VPCLatticeEventV2 def _get_base_path(self) -> str: return "" diff --git a/aws_lambda_powertools/shared/version.py b/aws_lambda_powertools/shared/version.py index b2c4ff3ba90..242e0b4ae14 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.26.0" +VERSION = "3.29.0" diff --git a/aws_lambda_powertools/utilities/data_classes/alb_event.py b/aws_lambda_powertools/utilities/data_classes/alb_event.py index 7d1541455d5..b9525e6a9f0 100644 --- a/aws_lambda_powertools/utilities/data_classes/alb_event.py +++ b/aws_lambda_powertools/utilities/data_classes/alb_event.py @@ -44,7 +44,17 @@ def request_context(self) -> ALBEventRequestContext: @property def resolved_query_string_parameters(self) -> dict[str, list[str]]: - params = self.multi_value_query_string_parameters or super().resolved_query_string_parameters + multi_value = self.multi_value_query_string_parameters + single_value = super().resolved_query_string_parameters + + if not multi_value: + params = single_value + elif not single_value: + params = multi_value + else: + # Merge both: multi_value takes precedence, single_value fills missing keys + params = {**single_value, **multi_value} + if not self.decode_query_parameters: return params diff --git a/aws_lambda_powertools/utilities/data_classes/api_gateway_authorizer_event.py b/aws_lambda_powertools/utilities/data_classes/api_gateway_authorizer_event.py index f1b9c4f5103..6d05984128e 100644 --- a/aws_lambda_powertools/utilities/data_classes/api_gateway_authorizer_event.py +++ b/aws_lambda_powertools/utilities/data_classes/api_gateway_authorizer_event.py @@ -437,7 +437,7 @@ class APIGatewayAuthorizerResponse: - https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html """ - path_regex = r"^[/.a-zA-Z0-9-_\*]+$" + path_regex = r"^[/.a-zA-Z0-9\-_\*\{\}\+]+$" """The regular expression used to validate resource paths for the policy""" def __init__( @@ -668,7 +668,7 @@ def from_route_arn( # Note: we need ignore[override] because we are removing the http_method field @override - def _add_route(self, effect: str, resource: str, conditions: list[dict] | None = None): # type: ignore[override] + def _add_route(self, effect: str, resource: str, conditions: list[dict] | None = None): # type: ignore[override] # ty: ignore[invalid-method-override] """Adds a route to the internal lists of allowed or denied routes. Each object in the internal list contains a resource ARN and a condition statement. The condition statement can be null.""" @@ -703,7 +703,7 @@ def deny_all_routes(self, http_method: str = HttpVerb.ALL.value): # type: ignor # Note: we need ignore[override] because we are removing the http_method field @override - def allow_route(self, resource: str, conditions: list[dict] | None = None): # type: ignore[override] + def allow_route(self, resource: str, conditions: list[dict] | None = None): # type: ignore[override] # ty: ignore[invalid-method-override] """ Add an API Gateway Websocket method to the list of allowed methods for the policy. @@ -732,7 +732,7 @@ def allow_route(self, resource: str, conditions: list[dict] | None = None): # t # Note: we need ignore[override] because we are removing the http_method field @override - def deny_route(self, resource: str, conditions: list[dict] | None = None): # type: ignore[override] + def deny_route(self, resource: str, conditions: list[dict] | None = None): # type: ignore[override] # ty: ignore[invalid-method-override] """ Add an API Gateway Websocket method to the list of allowed methods for the policy. diff --git a/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py b/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py index 540e86a5c51..88f6cb8fa54 100644 --- a/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py +++ b/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py @@ -120,7 +120,17 @@ def multi_value_headers(self) -> dict[str, list[str]]: @property def resolved_query_string_parameters(self) -> dict[str, list[str]]: - return self.multi_value_query_string_parameters or super().resolved_query_string_parameters + multi_value = self.multi_value_query_string_parameters + single_value = super().resolved_query_string_parameters + + if not multi_value: + return single_value + + if not single_value: + return multi_value + + # Merge both: multi_value takes precedence, single_value fills missing keys + return {**single_value, **multi_value} @property def resolved_headers_field(self) -> dict[str, Any]: @@ -280,6 +290,17 @@ def raw_query_string(self) -> str: def cookies(self) -> list[str]: return self.get("cookies") or [] + @property + def resolved_cookies_field(self) -> dict[str, str]: + """ + Parse cookies from the dedicated ``cookies`` field in API Gateway HTTP API v2 format. + + The ``cookies`` field contains a list of strings like ``["session=abc", "theme=dark"]``. + """ + from aws_lambda_powertools.utilities.data_classes.common import _parse_cookie_string + + return _parse_cookie_string("; ".join(self.cookies)) + @property def request_context(self) -> RequestContextV2: return RequestContextV2(self["requestContext"]) diff --git a/aws_lambda_powertools/utilities/data_classes/common.py b/aws_lambda_powertools/utilities/data_classes/common.py index 08fd60f0679..c35a02cc3ce 100644 --- a/aws_lambda_powertools/utilities/data_classes/common.py +++ b/aws_lambda_powertools/utilities/data_classes/common.py @@ -29,6 +29,17 @@ ) +def _parse_cookie_string(cookie_string: str) -> dict[str, str]: + """Parse a cookie string (``key=value; key2=value2``) into a dict.""" + cookies: dict[str, str] = {} + for segment in cookie_string.split(";"): + stripped = segment.strip() + if "=" in stripped: + name, _, value = stripped.partition("=") + cookies[name.strip()] = value.strip() + return cookies + + class CaseInsensitiveDict(dict): """Case insensitive dict implementation. Assumes string keys only.""" @@ -125,7 +136,7 @@ def _str_helper(self) -> dict[str, Any]: properties = self._properties() sensitive_properties = ["raw_event"] if hasattr(self, "_sensitive_properties"): - sensitive_properties.extend(self._sensitive_properties) # pyright: ignore # type: ignore[arg-type] + sensitive_properties.extend(self._sensitive_properties) # pyright: ignore # type: ignore[arg-type] # ty: ignore[invalid-argument-type] result: dict[str, Any] = {} for property_key in properties: @@ -203,6 +214,36 @@ def resolved_headers_field(self) -> dict[str, str]: """ return self.headers + @property + def resolved_cookies_field(self) -> dict[str, str]: + """ + This property extracts cookies from the request as a dict of name-value pairs. + + By default, cookies are parsed from the ``Cookie`` header. + Uses ``self.headers`` (CaseInsensitiveDict) first for reliable case-insensitive + lookup, then falls back to ``resolved_headers_field`` for proxies that only + populate multi-value headers (e.g., ALB without single-value headers). + Subclasses may override this for event formats that provide cookies + in a dedicated field (e.g., API Gateway HTTP API v2). + """ + # Primary: self.headers is CaseInsensitiveDict — case-insensitive lookup + cookie_value: str | list[str] = self.headers.get("cookie") or "" + + # Fallback: resolved_headers_field covers ALB/REST v1 multi-value headers + # where the event may not have a single-value 'headers' dict at all + if not cookie_value: + headers = self.resolved_headers_field or {} + cookie_value = headers.get("cookie") or headers.get("Cookie") or "" + + # Multi-value headers (ALB, REST v1) may return a list + if isinstance(cookie_value, list): + cookie_value = "; ".join(cookie_value) + + if not cookie_value: + return {} + + return _parse_cookie_string(cookie_value) + @property def is_base64_encoded(self) -> bool | None: return self.get("isBase64Encoded") diff --git a/aws_lambda_powertools/utilities/data_masking/base.py b/aws_lambda_powertools/utilities/data_masking/base.py index 0c58ee2a861..f76f990b842 100644 --- a/aws_lambda_powertools/utilities/data_masking/base.py +++ b/aws_lambda_powertools/utilities/data_masking/base.py @@ -406,7 +406,7 @@ def _apply_action_to_fields( json_parse.update( data_parsed, - lambda field_value, fields, field_name: update_callback(field_value, fields, field_name), # type: ignore[misc] # noqa: B023 + update_callback, # type: ignore[misc] # noqa: B023 ) return data_parsed 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/feature_flags/feature_flags.py b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py index d62dbfc625f..19e96a8641d 100644 --- a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py +++ b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py @@ -23,7 +23,7 @@ from aws_lambda_powertools.utilities.feature_flags.types import JSONType, P, T -RULE_ACTION_MAPPING = { +RULE_ACTION_MAPPING: dict[str, Callable[..., bool]] = { schema.RuleAction.EQUALS.value: lambda a, b: a == b, schema.RuleAction.NOT_EQUALS.value: lambda a, b: a != b, schema.RuleAction.KEY_GREATER_THAN_VALUE.value: lambda a, b: a > b, @@ -38,13 +38,13 @@ schema.RuleAction.KEY_NOT_IN_VALUE.value: lambda a, b: a not in b, schema.RuleAction.VALUE_IN_KEY.value: lambda a, b: b in a, schema.RuleAction.VALUE_NOT_IN_KEY.value: lambda a, b: b not in a, - schema.RuleAction.ALL_IN_VALUE.value: lambda a, b: compare_all_in_list(a, b), - schema.RuleAction.ANY_IN_VALUE.value: lambda a, b: compare_any_in_list(a, b), - schema.RuleAction.NONE_IN_VALUE.value: lambda a, b: compare_none_in_list(a, b), - schema.RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value: lambda a, b: compare_time_range(a, b), - schema.RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value: lambda a, b: compare_datetime_range(a, b), - schema.RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value: lambda a, b: compare_days_of_week(a, b), - schema.RuleAction.MODULO_RANGE.value: lambda a, b: compare_modulo_range(a, b), + schema.RuleAction.ALL_IN_VALUE.value: compare_all_in_list, + schema.RuleAction.ANY_IN_VALUE.value: compare_any_in_list, + schema.RuleAction.NONE_IN_VALUE.value: compare_none_in_list, + schema.RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value: compare_time_range, + schema.RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value: compare_datetime_range, + schema.RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value: compare_days_of_week, + schema.RuleAction.MODULO_RANGE.value: compare_modulo_range, } @@ -432,5 +432,5 @@ def _lookup_exception_handler(self, exc: BaseException) -> Callable | None: # of an exception for cls in type(exc).__mro__: if cls in self._exception_handlers: - return self._exception_handlers[cls] # type: ignore[index] # index is correct + return self._exception_handlers[cls] # type: ignore[index] # ty: ignore[invalid-argument-type] return None diff --git a/aws_lambda_powertools/utilities/idempotency/base.py b/aws_lambda_powertools/utilities/idempotency/base.py index f93b9097611..bee109ef842 100644 --- a/aws_lambda_powertools/utilities/idempotency/base.py +++ b/aws_lambda_powertools/utilities/idempotency/base.py @@ -60,7 +60,7 @@ def _prepare_data(data: Any) -> Any: # Convert from Pydantic model if callable(getattr(data, "model_dump", None)): - return data.model_dump() + return data.model_dump(mode="json") # Convert from event source data class if callable(getattr(data, "dict", None)): @@ -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 2803e6f0f3a..3d54a01f018 100644 --- a/aws_lambda_powertools/utilities/idempotency/persistence/base.py +++ b/aws_lambda_powertools/utilities/idempotency/persistence/base.py @@ -111,7 +111,10 @@ def _get_hashed_idempotency_key(self, data: dict[str, Any]) -> str | None: """ if self.event_key_jmespath: - data = self.event_key_compiled_jmespath.search(data, options=jmespath.Options(**self.jmespath_options)) + data = self.event_key_compiled_jmespath.search( + data, + options=jmespath.Options(**(self.jmespath_options or {})), + ) if self.is_missing_idempotency_key(data=data): if self.raise_on_no_idempotency_key: diff --git a/aws_lambda_powertools/utilities/idempotency/persistence/redis.py b/aws_lambda_powertools/utilities/idempotency/persistence/redis.py index d1c490ee0f3..82a44e079de 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"), ) diff --git a/aws_lambda_powertools/utilities/kafka/deserializer/protobuf.py b/aws_lambda_powertools/utilities/kafka/deserializer/protobuf.py index 16bb3bbc6ec..683b8432023 100644 --- a/aws_lambda_powertools/utilities/kafka/deserializer/protobuf.py +++ b/aws_lambda_powertools/utilities/kafka/deserializer/protobuf.py @@ -3,7 +3,9 @@ import logging from typing import Any -from google.protobuf.internal.decoder import _DecodeSignedVarint # type: ignore[attr-defined] +from google.protobuf.internal.decoder import ( # type: ignore[attr-defined] + _DecodeSignedVarint, # ty: ignore[unresolved-import] +) from google.protobuf.json_format import MessageToDict from aws_lambda_powertools.utilities.kafka.deserializer.base import DeserializerBase diff --git a/aws_lambda_powertools/utilities/metadata/lambda_metadata.py b/aws_lambda_powertools/utilities/metadata/lambda_metadata.py index 71bbece3aac..ee9e06dd8c3 100644 --- a/aws_lambda_powertools/utilities/metadata/lambda_metadata.py +++ b/aws_lambda_powertools/utilities/metadata/lambda_metadata.py @@ -9,6 +9,7 @@ import logging import os +import urllib.error import urllib.request from dataclasses import dataclass, field from json import JSONDecodeError diff --git a/aws_lambda_powertools/utilities/parameters/base.py b/aws_lambda_powertools/utilities/parameters/base.py index 2daf4bb5642..99475edb3ea 100644 --- a/aws_lambda_powertools/utilities/parameters/base.py +++ b/aws_lambda_powertools/utilities/parameters/base.py @@ -271,7 +271,7 @@ def get_transform_method(value: str, transform: TransformOptions = None) -> Call transform_method = TRANSFORM_METHOD_MAPPING.get(transform) if transform == "auto": - key_suffix = value.rsplit(".")[-1] + key_suffix = value.rsplit(".", maxsplit=1)[-1] transform_method = TRANSFORM_METHOD_MAPPING.get(key_suffix, TRANSFORM_METHOD_MAPPING[None]) return cast(Callable, transform_method) # https://github.com/python/mypy/issues/10740 diff --git a/aws_lambda_powertools/utilities/parameters/dynamodb.py b/aws_lambda_powertools/utilities/parameters/dynamodb.py index d80fd1b985a..9fe95518b8e 100644 --- a/aws_lambda_powertools/utilities/parameters/dynamodb.py +++ b/aws_lambda_powertools/utilities/parameters/dynamodb.py @@ -202,7 +202,7 @@ def _get(self, name: str, **sdk_options) -> str: # maintenance: look for better ways to correctly type DynamoDB multiple return types # without a breaking change within ABC return type - return self.table.get_item(**sdk_options)["Item"][self.value_attr] # type: ignore[return-value] + return self.table.get_item(**sdk_options)["Item"][self.value_attr] # type: ignore[return-value] # ty: ignore[invalid-return-type] def _get_multiple(self, path: str, **sdk_options) -> dict[str, str]: """ @@ -230,4 +230,4 @@ def _get_multiple(self, path: str, **sdk_options) -> dict[str, str]: # maintenance: look for better ways to correctly type DynamoDB multiple return types # without a breaking change within ABC return type - return {item[self.sort_attr]: item[self.value_attr] for item in items} # type: ignore[misc] + return {item[self.sort_attr]: item[self.value_attr] for item in items} # type: ignore[misc] # ty: ignore[invalid-return-type] diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 0f80f003920..eff4e745738 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -130,7 +130,7 @@ def _get(self, name: str, **sdk_options) -> str | bytes: return secret_value["SecretBinary"] - def _get_multiple(self, names: list[str], **sdk_options) -> dict[str, Any]: # type: ignore[override] + def _get_multiple(self, names: list[str], **sdk_options) -> dict[str, Any]: # type: ignore[override] # ty: ignore[invalid-method-override] """ Retrieve multiple secrets using AWS Secrets Manager batch_get_secret_value API @@ -200,7 +200,7 @@ def _get_multiple(self, names: list[str], **sdk_options) -> dict[str, Any]: # t return secrets - def get_multiple( # type: ignore[override] + def get_multiple( # type: ignore[override] # ty: ignore[invalid-method-override] self, names: list[str], max_age: int | None = None, diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index b4e0de011c4..fde8d980494 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -133,7 +133,7 @@ def __init__( super().__init__(client=self.client) - def get_multiple( # type: ignore[override] + def get_multiple( # type: ignore[override] # ty: ignore[invalid-method-override] self, path: str, max_age: int | None = None, @@ -192,7 +192,7 @@ def get_multiple( # type: ignore[override] # We break Liskov substitution principle due to differences in signatures of this method and superclass get method # We ignore mypy error, as changes to the signature here or in a superclass is a breaking change to users - def get( # type: ignore[override] + def get( # type: ignore[override] # ty: ignore[invalid-method-override] self, name: str, max_age: int | None = None, 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/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/Dockerfile b/docs/Dockerfile index 7c4cd4e4a71..fc49b3a319a 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -1,5 +1,5 @@ # v9.1.18 -FROM squidfunk/mkdocs-material@sha256:8f41b6089700e1c32212c3857936f14e88a3306a35be4ffd1826420e2f3e4197 +FROM squidfunk/mkdocs-material@sha256:868ad4d39fb5865b72d00173ade00f4eae2b38dde7ff790a011cc44ce4a8ff8e # pip-compile --generate-hashes --output-file=requirements.txt requirements.in COPY requirements.txt /tmp/ RUN pip install --require-hashes -r /tmp/requirements.txt diff --git a/docs/core/event_handler/_openapi_customization_operations.md b/docs/core/event_handler/_openapi_customization_operations.md index 0072ec1fae4..dbea969c99f 100644 --- a/docs/core/event_handler/_openapi_customization_operations.md +++ b/docs/core/event_handler/_openapi_customization_operations.md @@ -13,4 +13,5 @@ Here's a breakdown of various customizable fields: | `tags` | `List[str]` | Tags are a way to categorize and group endpoints within the API documentation. They can help organize the operations by resources or other heuristic. | | `operation_id` | `str` | A unique identifier for the operation, which can be used for referencing this operation in documentation or code. This ID must be unique across all operations described in the API. | | `include_in_schema` | `bool` | A boolean value that determines whether or not this operation should be included in the OpenAPI schema. Setting it to `False` can hide the endpoint from generated documentation and schema exports, which might be useful for private or experimental endpoints. | -| `deprecated` | `bool` | A boolean value that determines whether or not this operation should be marked as deprecated in the OpenAPI schema. | +| `deprecated` | `bool` | A boolean value that determines whether or not this operation should be marked as deprecated in the OpenAPI schema. | +| `status_code` | `int` | The default HTTP status code for successful responses. Defaults to `200`. This value is used both in the OpenAPI schema and as the actual response status code when the handler returns a dictionary or plain value (not a `Response` object or tuple). | diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index e262613046c..076e2aa3c11 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 @@ -485,7 +486,7 @@ This value will override the value of the failed response validation http code s We use the `Annotated` type to tell the Event Handler that a particular parameter is not only an optional string, but also a query string with constraints. -In the following example, we use a new `Query` OpenAPI type to add [one out of many possible constraints](#customizing-openapi-parameters), which should read as: +In the following example, we use a new `Query` OpenAPI type to add [one out of many possible constraints](openapi.md#customizing-parameters), which should read as: * `completed` is a query string with a `None` as its default value * `completed`, when set, should have at minimum 4 characters @@ -539,7 +540,7 @@ In the following example, we use a new `Query` OpenAPI type to add [one out of m #### Validating path parameters -Just like we learned in [query string validation](#validating-query-strings), we can use a new `Path` OpenAPI type to [add constraints](#customizing-openapi-parameters). +Just like we learned in [query string validation](#validating-query-strings), we can use a new `Path` OpenAPI type to [add constraints](openapi.md#customizing-parameters). For example, we could validate that `` dynamic path should be no greater than three digits. @@ -555,7 +556,7 @@ For example, we could validate that `` dynamic path should be no greate We use the `Annotated` type to tell the Event Handler that a particular parameter is a header that needs to be validated. Also, we adhere to [HTTP RFC standards](https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2){target="_blank" rel="nofollow"}, which means we treat HTTP headers as case-insensitive. -In the following example, we use a new `Header` OpenAPI type to add [one out of many possible constraints](#customizing-openapi-parameters), which should read as: +In the following example, we use a new `Header` OpenAPI type to add [one out of many possible constraints](openapi.md#customizing-parameters), which should read as: * `correlation_id` is a header that must be present in the request * `correlation_id` should have 16 characters @@ -605,6 +606,57 @@ You can use the `Form` type to tell the Event Handler that a parameter expects f --8<-- "examples/event_handler_rest/src/working_with_form_data.py" ``` +#### Handling file uploads + +!!! info "You must set `enable_validation=True` to handle file uploads via type annotation." + +You can use the `File` type to accept `multipart/form-data` file uploads. This automatically sets the correct OpenAPI schema, and Swagger UI will render a file picker for each `File()` parameter. + +There are two ways to receive uploaded files: + +* **`bytes`** — receive raw file content only +* **`UploadFile`** — receive file content along with metadata (filename, content type) + +=== "working_with_file_uploads.py" + + ```python hl_lines="4 12" + --8<-- "examples/event_handler_rest/src/working_with_file_uploads.py" + ``` + + 1. `File` is a special OpenAPI type for `multipart/form-data` file uploads. When annotated as `bytes`, you receive the raw file content. + +=== "working_with_file_uploads_metadata.py" + + ```python hl_lines="4 11 15-16" + --8<-- "examples/event_handler_rest/src/working_with_file_uploads_metadata.py" + ``` + + 1. Using `UploadFile` instead of `bytes` gives you access to file metadata. + 2. `filename` and `content_type` come from the multipart headers sent by the client. + +=== "working_with_file_uploads_mixed.py" + + You can combine `File()` and `Form()` parameters in the same route to accept file uploads with additional form fields. + + ```python hl_lines="6 14-15" + --8<-- "examples/event_handler_rest/src/working_with_file_uploads_mixed.py" + ``` + + 1. File upload parameter — receives the uploaded file with metadata. + 2. Regular form field — receives a string value from the same multipart request. + +!!! warning "API Gateway REST API (v1) requires Binary Media Types configuration" + When using API Gateway REST API (v1), you must configure Binary Media Types to include `multipart/form-data`, otherwise binary file content will be corrupted. + + ```yaml title="SAM template.yaml" + Globals: + Api: + BinaryMediaTypes: + - "multipart~1form-data" + ``` + + API Gateway HTTP API (v2), Lambda Function URL, and ALB handle binary encoding automatically — no extra configuration needed. + #### Supported types for response serialization With data validation enabled, we natively support serializing the following data types to JSON: @@ -716,28 +768,15 @@ We provide pre-defined errors for the most popular ones based on [AWS Lambda API ### Enabling SwaggerUI -!!! note "This feature requires [data validation](#data-validation) feature to be enabled." - -Behind the scenes, the [data validation](#data-validation) feature auto-generates an OpenAPI specification from your routes and type annotations. You can use [Swagger UI](https://swagger.io/tools/swagger-ui/){target="_blank" rel="nofollow"} to visualize and interact with your newly auto-documented API. - -There are some important **caveats** that you should know before enabling it: +???+ tip "OpenAPI documentation has moved" + For complete OpenAPI documentation including Swagger UI customization, security schemes, and OpenAPI Merge for micro-functions, see the dedicated [OpenAPI documentation](openapi.md). -| Caveat | Description | -| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Swagger UI is **publicly accessible by default** | When using `enable_swagger` method, you can [protect sensitive API endpoints by implementing a custom middleware](#customizing-swagger-ui) using your preferred authorization mechanism. | -| **No micro-functions support** yet | Swagger UI is enabled on a per resolver instance which will limit its accuracy here. | -| You need to expose a **new route** | You'll need to expose the following path to Lambda: `/swagger`; ignore if you're routing this path already. | -| JS and CSS files are **embedded within Swagger HTML** | If you are not using an external CDN to serve Swagger UI assets, we embed JS and CSS directly into the HTML. To enhance performance, please consider enabling the `compress` option to minimize the size of HTTP requests. | -| Authorization data is **lost** on browser close/refresh | Use `enable_swagger(persist_authorization=True)` to persist authorization data, like OAuath 2.0 access tokens. | +Use `enable_swagger()` to serve interactive API documentation: ```python hl_lines="12-13" title="enabling_swagger.py" --8<-- "examples/event_handler_rest/src/enabling_swagger.py" ``` -1. `enable_swagger` creates a route to serve Swagger UI and allows quick customizations.

You can also include middlewares to protect or enhance the overall experience. - -Here's an example of what it looks like by default: - ![Swagger UI picture](../../media/swagger.png) ### Custom Domain API Mappings @@ -892,6 +931,50 @@ Here's a sample middleware that extracts and injects correlation ID, using `APIG --8<-- "examples/event_handler_rest/src/middleware_getting_started_output.json" ``` +#### Accessing the Request object + +After route resolution, Event Handler creates a `Request` object with the **resolved** route context. You can access it in two ways: + +1. **`app.request`** - available in middleware functions. +2. **`request: Request` type annotation** - injected automatically into route handlers as a parameter. + +`Request` gives you the resolved route context that `app.current_event` doesn't have: + +| Property | Example | Description | +| --------------------- | ---------------------------------------- | ----------------------------------------------------- | +| `route` | `/todos/{todo_id}` | Matched route pattern in OpenAPI path-template format | +| `path_parameters` | `{"todo_id": "123"}` | Powertools-resolved path parameters | +| `method` | `GET` | HTTP method (upper-case) | +| `headers` | `{"content-type": "application/json"}` | Request headers | +| `query_parameters` | `{"page": "1"}` | Query string parameters | +| `body` | `'{"name": "task"}'` | Raw request body | +| `json_body` | `{"name": "task"}` | Deserialized request body | + +=== "Using `app.request` in middleware" + + ```python hl_lines="10 13-15 21" title="Accessing Request via app.request" + --8<-- "examples/event_handler_rest/src/middleware_request_object.py" + ``` + + 1. Access the resolved `Request` object from the app instance. + 2. `request.route` returns the matched route pattern, e.g. `/todos/{todo_id}`. + 3. `request.path_parameters` returns the Powertools-resolved parameters, e.g. `{"todo_id": "123"}`. + 4. You can include route metadata in the response headers. + +=== "Using `request: Request` in route handlers" + + ```python hl_lines="7 10" title="Accessing Request via type annotation" + --8<-- "examples/event_handler_rest/src/middleware_request_handler_injection.py" + ``` + + 1. Add `request: Request` as a parameter - Event Handler injects it automatically. + 2. Access resolved route, path parameters, headers, query parameters, and body. + +???+ note "When to use `Request` vs `app.current_event`" + Use `Request` for **route-aware** logic like authorization, logging, and metrics - it gives you the matched route pattern and Powertools-resolved path parameters. + + Use `app.current_event` when you need the **raw event** data like request context, stage variables, or authorizer context that is available from the start of the request, before route resolution. + #### Global middlewares
@@ -1061,7 +1144,7 @@ Keep the following in mind when authoring middlewares for Event Handler: 2. **Call the next middleware**. Return the result of `next_middleware(app)`, or a [Response object](#fine-grained-responses) when you want to [return early](#returning-early). 3. **Keep a lean scope**. Focus on a single task per middleware to ease composability and maintenance. In [debug mode](#debug-mode), we also print out the order middlewares will be triggered to ease operations. 4. **Catch your own exceptions**. Catch and handle known exceptions to your logic. Unless you want to raise [HTTP Errors](#raising-http-errors), or propagate specific exceptions to the client. To catch all and any exceptions, we recommend you use the [exception_handler](#exception-handling) decorator. -5. **Use context to share data**. Use `app.append_context` to [share contextual data](#sharing-contextual-data) between middlewares and route handlers, and `app.context.get(key)` to fetch them. We clear all contextual data at the end of every request. +5. **Use context to share data**. Use `app.append_context` to [share contextual data](#sharing-contextual-data) between middlewares and route handlers, and `app.context.get(key)` to fetch them. We clear all contextual data at the end of every request. For route-aware request data, use [`app.request`](#accessing-the-request-object) instead. ### Fine grained responses @@ -1179,128 +1262,8 @@ This will enable full tracebacks errors in the response, print request and respo ### OpenAPI -When you enable [Data Validation](#data-validation), we use a combination of Pydantic Models and [OpenAPI](https://www.openapis.org/){target="_blank" rel="nofollow"} type annotations to add constraints to your API's parameters. - -???+ warning "OpenAPI schema version depends on the installed version of Pydantic" - Pydantic v1 generates [valid OpenAPI 3.0.3 schemas](https://docs.pydantic.dev/1.10/usage/schema/){target="_blank" rel="nofollow"}, and Pydantic v2 generates [valid OpenAPI 3.1.0 schemas](https://docs.pydantic.dev/latest/why/#json-schema){target="_blank" rel="nofollow"}. - -In OpenAPI documentation tools like [SwaggerUI](#enabling-swaggerui), these annotations become readable descriptions, offering a self-explanatory API interface. This reduces boilerplate code while improving functionality and enabling auto-documentation. - -???+ note - We don't have support for files, form data, and header parameters at the moment. If you're interested in this, please [open an issue](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=feature-request%2Ctriage&projects=&template=feature_request.yml&title=Feature+request%3A+TITLE). - -#### Customizing OpenAPI parameters - ---8<-- "docs/core/event_handler/_openapi_customization_parameters.md" - -#### Customizing API operations - ---8<-- "docs/core/event_handler/_openapi_customization_operations.md" - -To implement these customizations, include extra parameters when defining your routes: - -```python hl_lines="11-20" title="customizing_api_operations.py" ---8<-- "examples/event_handler_rest/src/customizing_api_operations.py" -``` - -#### Customizing OpenAPI metadata - ---8<-- "docs/core/event_handler/_openapi_customization_metadata.md" - -Include extra parameters when exporting your OpenAPI specification to apply these customizations: - -=== "customizing_api_metadata.py" - - ```python hl_lines="8-16" - --8<-- "examples/event_handler_rest/src/customizing_api_metadata.py" - ``` - -#### Customizing Swagger UI - -???+note "Customizing the Swagger metadata" - The `enable_swagger` method accepts the same metadata as described at [Customizing OpenAPI metadata](#customizing-openapi-metadata). - -The Swagger UI appears by default at the `/swagger` path, but you can customize this to serve the documentation from another path and specify the source for Swagger UI assets. - -Below is an example configuration for serving Swagger UI from a custom path or CDN, with assets like CSS and JavaScript loading from a chosen CDN base URL. - -=== "customizing_swagger.py" - - ```python hl_lines="10" - --8<-- "examples/event_handler_rest/src/customizing_swagger.py" - ``` - -=== "customizing_swagger_middlewares.py" - - A Middleware can handle tasks such as adding security headers, user authentication, or other request processing for serving the Swagger UI. - - ```python hl_lines="7 13-18 21" - --8<-- "examples/event_handler_rest/src/customizing_swagger_middlewares.py" - ``` - -#### Security schemes - -???-info "Does Powertools implement any of the security schemes?" - No. Powertools adds support for generating OpenAPI documentation with [security schemes](https://swagger.io/docs/specification/authentication/), but it doesn't implement any of the security schemes itself, so you must implement the security mechanisms separately. - -Security schemes are declared at the top-level first. You can reference them globally or on a per path _(operation)_ level. **However**, if you reference security schemes that are not defined at the top-level it will lead to a `SchemaValidationError` _(invalid OpenAPI spec)_. - -=== "Global OpenAPI security schemes" - - ```python title="security_schemes_global.py" hl_lines="17-27" - --8<-- "examples/event_handler_rest/src/security_schemes_global.py" - ``` - - 1. Using the oauth security scheme defined earlier, scoped to the "admin" role. - -=== "Per Operation security" - - ```python title="security_schemes_per_operation.py" hl_lines="17-26 30" - --8<-- "examples/event_handler_rest/src/security_schemes_per_operation.py" - ``` - - 1. Using the oauth security scheme defined bellow, scoped to the "admin" role. - -=== "Global security schemes and optional security per route" - - ```python title="security_schemes_global_and_optional.py" hl_lines="17-26 35" - --8<-- "examples/event_handler_rest/src/security_schemes_global_and_optional.py" - ``` - - 1. To make security optional in a specific route, an empty security requirement ({}) can be included in the array. - -OpenAPI 3 lets you describe APIs protected using the following security schemes: - -| Security Scheme | Type | Description | -| --------------------------------------------------------------------------------------------------------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [HTTP auth](https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml){target="_blank"} | `HTTPBase` | HTTP authentication schemes using the Authorization header (e.g: [Basic auth](https://swagger.io/docs/specification/authentication/basic-authentication/){target="_blank"}, [Bearer](https://swagger.io/docs/specification/authentication/bearer-authentication/){target="_blank"}) | -| [API keys](https://swagger.io/docs/specification/authentication/api-keys/){target="_blank"} (e.g: query strings, cookies) | `APIKey` | API keys in headers, query strings or [cookies](https://swagger.io/docs/specification/authentication/cookie-authentication/){target="_blank"}. | -| [OAuth 2](https://swagger.io/docs/specification/authentication/oauth2/){target="_blank"} | `OAuth2` | Authorization protocol that gives an API client limited access to user data on a web server. | -| [OpenID Connect Discovery](https://swagger.io/docs/specification/authentication/openid-connect-discovery/){target="_blank"} | `OpenIdConnect` | Identity layer built [on top of the OAuth 2.0 protocol](https://openid.net/developers/how-connect-works/){target="_blank"} and supported by some OAuth 2.0. | -| [Mutual TLS](https://swagger.io/specification/#security-scheme-object){target="_blank"}. | `MutualTLS` | Client/server certificate mutual authentication scheme. | - -???-note "Using OAuth2 with the Swagger UI?" - You can use the `OAuth2Config` option to configure a default OAuth2 app on the generated Swagger UI. - - ```python hl_lines="10 15-18 22" - --8<-- "examples/event_handler_rest/src/swagger_with_oauth2.py" - ``` - -#### OpenAPI extensions - -For a better experience when working with Lambda and Amazon API Gateway, customers can define extensions using the `openapi_extensions` parameter. We support defining OpenAPI extensions at the following levels of the OpenAPI JSON Schema: Root, Servers, Operation, and Security Schemes. - -???+ warning - We do not support the `x-amazon-apigateway-any-method` and `x-amazon-apigateway-integrations` extensions. - -```python hl_lines="9 15 25 28" title="Adding OpenAPI extensions" ---8<-- "examples/event_handler_rest/src/working_with_openapi_extensions.py" -``` - -1. Server level -2. Operation level -3. Security scheme level -4. Root level +???+ tip "OpenAPI documentation has moved" + For complete OpenAPI documentation including customization, security schemes, extensions, and OpenAPI Merge for micro-functions, see the dedicated [OpenAPI documentation](openapi.md). ### Custom serializer @@ -1434,6 +1397,167 @@ This is a sample project layout for a monolithic function with routes split in d └── test_main.py # functional tests for the main lambda handler ``` +### Dependency injection + +You can use `Depends()` to declare dependencies that are automatically resolved and injected into your route handlers. This provides type-safe, composable, and testable dependency injection. + +#### Basic usage + +Use `Annotated[Type, Depends(fn)]` to declare a dependency. The return value of `fn` is injected into the parameter automatically. + +```python hl_lines="5 8 20 25" +--8<-- "examples/event_handler_rest/src/dependency_injection.py" +``` + +#### Nested dependencies + +Dependencies can depend on other dependencies, forming a composable tree. Shared sub-dependencies are resolved once per invocation and cached automatically. + +```python hl_lines="18 22 29-30" +--8<-- "examples/event_handler_rest/src/dependency_injection_nested.py" +``` + +#### Accessing the request + +Dependencies that need access to the current request can declare a parameter typed as `Request`. It will be injected automatically. + +The `Request` object provides: + +* **`headers`**, **`query_parameters`**, **`body`**, **`json_body`**: common request data +* **`resolved_event`**: the full Powertools proxy event with all helpers, cookies, request context, and path +* **`context`**: shared resolver context (`app.context`) for bridging data between middleware and dependencies + +```python hl_lines="5-6 14 20" +--8<-- "examples/event_handler_rest/src/dependency_injection_with_request.py" +``` + +#### Combining middleware and Depends() + +Middleware and `Depends()` are **complementary patterns**. Use middleware for request interception (auth gates, redirects, response modification) and `Depends()` for typed data injection. + +The bridge between them is `request.context`: middleware writes to `app.context`, and dependencies read from `request.context`: + +```python hl_lines="12-18 22-23 27" +--8<-- "examples/event_handler_rest/src/dependency_injection_with_middleware.py" +``` + +???+ tip "When to use middleware vs Depends()" + | Use case | Middleware | Depends() | + | --- | --- | --- | + | Return custom HTTP responses (redirects, 401s) | **Yes** | No, can only return values or raise exceptions | + | Short-circuit the request pipeline | **Yes** | No | + | Pre/post-process responses (add headers, compress) | **Yes** | No | + | Inject typed, testable data into handlers | No | **Yes** | + | Compose a dependency tree with caching | No | **Yes** | + | Override dependencies in tests | No | **Yes**, via `dependency_overrides` | + +#### Testing with dependency overrides + +Use `dependency_overrides` to replace any dependency with a mock or stub during testing - no monkeypatching needed. + +```python hl_lines="3 12 26" +--8<-- "examples/event_handler_rest/src/dependency_injection_testing.py" +``` + +???+ 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. @@ -1516,6 +1640,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/core/event_handler/openapi.md b/docs/core/event_handler/openapi.md new file mode 100644 index 00000000000..d32797db1d7 --- /dev/null +++ b/docs/core/event_handler/openapi.md @@ -0,0 +1,452 @@ +--- +title: OpenAPI +description: Core utility - OpenAPI documentation and schema generation +--- + + + +Powertools for AWS Lambda supports automatic OpenAPI schema generation from your route definitions and type annotations. This includes Swagger UI integration, schema customization, and OpenAPI Merge for micro-functions architectures. + +## Key features + +* **Automatic schema generation** from Pydantic models and type annotations +* **Swagger UI** for interactive API documentation +* **OpenAPI Merge** for generating unified schemas from multiple Lambda handlers +* **Security schemes** support (OAuth2, API Key, HTTP auth, etc.) +* **Customizable** metadata, operations, and parameters + +## Swagger UI + +Behind the scenes, the [data validation](api_gateway.md#data-validation) feature auto-generates an OpenAPI specification from your routes and type annotations. You can use [Swagger UI](https://swagger.io/tools/swagger-ui/){target="_blank" rel="nofollow"} to visualize and interact with your API. + +!!! note "This feature requires [data validation](api_gateway.md#data-validation) to be enabled." + +???+ warning "Important caveats" + | Caveat | Description | + | ------ | ----------- | + | Swagger UI is **publicly accessible by default** | Implement a [custom middleware](#customizing-swagger-ui) for authorization | + | You need to expose a **new route** | Expose `/swagger` path to Lambda | + | JS and CSS files are **embedded within Swagger HTML** | Consider enabling `compress` option for better performance | + | Authorization data is **lost** on browser close/refresh | Use `enable_swagger(persist_authorization=True)` to persist | + +=== "enabling_swagger.py" + + ```python hl_lines="12-13" + --8<-- "examples/event_handler_rest/src/enabling_swagger.py" + ``` + + 1. `enable_swagger` creates a route to serve Swagger UI and allows quick customizations. + +Here's an example of what it looks like by default: + +![Swagger UI picture](../../media/swagger.png) + +### Customizing Swagger UI + +The Swagger UI appears by default at the `/swagger` path, but you can customize this to serve the documentation from another path and specify the source for Swagger UI assets. + +=== "customizing_swagger.py" + + ```python hl_lines="10" + --8<-- "examples/event_handler_rest/src/customizing_swagger.py" + ``` + +=== "customizing_swagger_middlewares.py" + + Use middleware for security headers, authentication, or other request processing. + + ```python hl_lines="7 13-18 21" + --8<-- "examples/event_handler_rest/src/customizing_swagger_middlewares.py" + ``` + +## Customization + +### Customizing parameters + +--8<-- "docs/core/event_handler/_openapi_customization_parameters.md" + +### Customizing operations + +--8<-- "docs/core/event_handler/_openapi_customization_operations.md" + +To implement these customizations, include extra parameters when defining your routes: + +=== "customizing_api_operations.py" + + ```python hl_lines="11-20 29-36" + --8<-- "examples/event_handler_rest/src/customizing_api_operations.py" + ``` + +### Customizing metadata + +--8<-- "docs/core/event_handler/_openapi_customization_metadata.md" + +Include extra parameters when exporting your OpenAPI specification: + +=== "customizing_api_metadata.py" + + ```python hl_lines="8-16" + --8<-- "examples/event_handler_rest/src/customizing_api_metadata.py" + ``` + +### Security schemes + +???- info "Does Powertools implement any of the security schemes?" + No. Powertools adds support for generating OpenAPI documentation with [security schemes](https://swagger.io/docs/specification/authentication/), but you must implement the security mechanisms separately. + +Security schemes are declared at the top-level first, then referenced globally or per operation. + +=== "Global security schemes" + + ```python hl_lines="17-27" + --8<-- "examples/event_handler_rest/src/security_schemes_global.py" + ``` + + 1. Using the oauth security scheme defined earlier, scoped to the "admin" role. + +=== "Per operation security" + + ```python hl_lines="17-26 30" + --8<-- "examples/event_handler_rest/src/security_schemes_per_operation.py" + ``` + + 1. Using the oauth security scheme scoped to the "admin" role. + +=== "Optional security per route" + + ```python hl_lines="17-26 35" + --8<-- "examples/event_handler_rest/src/security_schemes_global_and_optional.py" + ``` + + 1. An empty security requirement ({}) makes security optional for this route. + +OpenAPI 3 supports these security schemes: + +| Security Scheme | Type | Description | +| --------------- | ---- | ----------- | +| [HTTP auth](https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml){target="_blank"} | `HTTPBase` | HTTP authentication (Basic, Bearer) | +| [API keys](https://swagger.io/docs/specification/authentication/api-keys/){target="_blank"} | `APIKey` | API keys in headers, query strings or cookies | +| [OAuth 2](https://swagger.io/docs/specification/authentication/oauth2/){target="_blank"} | `OAuth2` | OAuth 2.0 authorization | +| [OpenID Connect](https://swagger.io/docs/specification/authentication/openid-connect-discovery/){target="_blank"} | `OpenIdConnect` | OpenID Connect Discovery | +| [Mutual TLS](https://swagger.io/specification/#security-scheme-object){target="_blank"} | `MutualTLS` | Client/server certificate authentication | + +???- note "Using OAuth2 with Swagger UI?" + Use `OAuth2Config` to configure a default OAuth2 app: + + ```python hl_lines="10 15-18 22" + --8<-- "examples/event_handler_rest/src/swagger_with_oauth2.py" + ``` + +### OpenAPI extensions + +Define extensions using `openapi_extensions` parameter at Root, Servers, Operation, and Security Schemes levels. + +???+ warning + We do not support `x-amazon-apigateway-any-method` and `x-amazon-apigateway-integrations` extensions. + +=== "working_with_openapi_extensions.py" + + ```python hl_lines="9 15 25 28" + --8<-- "examples/event_handler_rest/src/working_with_openapi_extensions.py" + ``` + + 1. Server level + 2. Operation level + 3. Security scheme level + 4. Root level + +## OpenAPI Merge + +OpenAPI Merge generates a unified OpenAPI schema from multiple Lambda handlers. This is designed for micro-functions architectures where each Lambda has its own resolver. + +### Why OpenAPI Merge? + +In a micro-functions architecture, each Lambda function handles a specific domain (users, orders, payments). Each has its own resolver with routes, but you need a single OpenAPI specification for documentation and API Gateway imports. + +```mermaid +graph LR + A[Users Lambda] --> D[OpenAPI Merge] + B[Orders Lambda] --> D + C[Payments Lambda] --> D + D --> E[Unified OpenAPI Schema] + E --> F[Swagger UI] + E --> G[API Gateway Import] +``` + +### How it works + +OpenAPI Merge uses AST (Abstract Syntax Tree) analysis to detect resolver instances in your handler files. **No code is executed during discovery** - it's pure static analysis. This means: + +* No side effects from importing handler code +* No Lambda cold starts +* No security concerns from arbitrary code execution +* Fast discovery across many files + +???+ warning "Handler modules must be side-effect-free at import time" + While discovery uses static analysis (AST), **schema generation requires importing your handler modules** to extract route definitions. If a handler module runs code at import time - such as validating environment variables, opening database connections, or calling external services — the import will fail silently and its routes will be missing from the final schema. + + If your schema is unexpectedly empty, check whether your handler files have decorators or top-level code that depends on runtime state. Move these to the handler function body or guard them with `if __name__ == "__main__"`. + +### Discovery parameters + +The `discover()` method accepts the following parameters: + +| Parameter | Type | Default | Description | +| --------- | ---- | ------- | ----------- | +| `path` | `str` or `Path` | required | Root directory to search for handler files | +| `pattern` | `str` or `list[str]` | `"handler.py"` | Glob pattern(s) to match handler files | +| `exclude` | `list[str]` | `["**/tests/**", "**/__pycache__/**", "**/.venv/**"]` | Patterns to exclude from discovery | +| `resolver_name` | `str` | `"app"` | Variable name of the resolver instance in handler files | +| `recursive` | `bool` | `False` | Whether to search recursively in subdirectories | +| `project_root` | `str` or `Path` | Same as `path` | Root directory for resolving Python imports | + +#### Pattern examples + +Patterns use glob syntax: + +| Pattern | Matches | +| ------- | ------- | +| `handler.py` | Files named exactly `handler.py` in the root directory | +| `*_handler.py` | Files ending with `_handler.py` (e.g., `users_handler.py`) | +| `**/*.py` | All Python files recursively (requires `recursive=True`) | +| `["handler.py", "api.py"]` | Multiple patterns | + +#### Recursive search + +By default, `recursive=False` searches only in the specified `path` directory. Set `recursive=True` to search subdirectories: + +```python +# Only searches in ./src (not subdirectories) +merge.discover(path="./src", pattern="handler.py") + +# Searches ./src and all subdirectories +merge.discover(path="./src", pattern="handler.py", recursive=True) + +# Pattern with **/ also searches recursively +merge.discover(path="./src", pattern="**/handler.py") +``` + +#### Project root for imports + +When handler files use absolute imports (e.g., `from myapp.utils.resolver import app`), set `project_root` to the directory that serves as the Python package root: + +```python +merge.discover( + path="./src/myapp/handlers", + pattern="*.py", + project_root="./src", # Allows resolving "from myapp.x import y" +) +``` + +### Getting started example + +Here's a typical micro-functions project structure and how to configure OpenAPI Merge: + +```text +my-api/ +├── functions/ +│ ├── users/ +│ │ └── handler.py # app = APIGatewayRestResolver() with /users routes +│ ├── orders/ +│ │ └── handler.py # app = APIGatewayRestResolver() with /orders routes +│ ├── payments/ +│ │ └── handler.py # app = APIGatewayRestResolver() with /payments routes +│ └── docs/ +│ └── handler.py # Dedicated Lambda to serve unified OpenAPI docs +├── scripts/ +│ └── generate_openapi.py # CI/CD script to generate openapi.json +└── template.yaml # SAM/CloudFormation template +``` + +Each handler file defines its own resolver with domain-specific routes: + +=== "functions/users/handler.py" + + ```python + from aws_lambda_powertools.event_handler import APIGatewayRestResolver + + app = APIGatewayRestResolver(enable_validation=True) + + @app.get("/users") + def list_users(): + return {"users": []} + + @app.get("/users/") + def get_user(user_id: str): + return {"id": user_id, "name": "John"} + + def handler(event, context): + return app.resolve(event, context) + ``` + +=== "functions/orders/handler.py" + + ```python + from aws_lambda_powertools.event_handler import APIGatewayRestResolver + + app = APIGatewayRestResolver(enable_validation=True) + + @app.get("/orders") + def list_orders(): + return {"orders": []} + + @app.post("/orders") + def create_order(): + return {"id": "order-123"} + + def handler(event, context): + return app.resolve(event, context) + ``` + +To generate a unified OpenAPI schema, you have two options: + +=== "Option 1: CI/CD script" + + Generate `openapi.json` at build time: + + ```python + # scripts/generate_openapi.py + from pathlib import Path + from aws_lambda_powertools.event_handler.openapi import OpenAPIMerge + + merge = OpenAPIMerge( + title="My API", + version="1.0.0", + description="Unified API documentation", + ) + + merge.discover( + path="./functions", + pattern="handler.py", + exclude=["**/docs/**"], # Exclude the docs Lambda + recursive=True, + ) + + output = Path("openapi.json") + output.write_text(merge.get_openapi_json_schema()) + print(f"Generated {output}") + ``` + +=== "Option 2: Dedicated docs Lambda" + + Serve Swagger UI from a dedicated Lambda: + + ```python + # functions/docs/handler.py + from aws_lambda_powertools.event_handler import APIGatewayRestResolver + + app = APIGatewayRestResolver() + + app.configure_openapi_merge( + path="../", # Parent directory containing other handlers + pattern="handler.py", + exclude=["**/docs/**"], + recursive=True, + title="My API", + version="1.0.0", + ) + + app.enable_swagger(path="/") + + def handler(event, context): + return app.resolve(event, context) + ``` + +### Standalone class + +Use `OpenAPIMerge` class to generate schemas. This is pure Python code where you control the paths and output. + +=== "openapi_merge_standalone.py" + + ```python + --8<-- "examples/event_handler_rest/src/openapi_merge_standalone.py" + ``` + +=== "openapi_merge_with_exclusions.py" + + ```python + --8<-- "examples/event_handler_rest/src/openapi_merge_with_exclusions.py" + ``` + +=== "openapi_merge_multiple_patterns.py" + + ```python + --8<-- "examples/event_handler_rest/src/openapi_merge_multiple_patterns.py" + ``` + +### Resolver integration + +Use `configure_openapi_merge()` on any resolver to serve merged schemas via Swagger UI. This is useful when you want a dedicated Lambda to serve the unified documentation. + +=== "openapi_merge_resolver.py" + + ```python + --8<-- "examples/event_handler_rest/src/openapi_merge_resolver.py" + ``` + +???+ warning "Routes from other Lambdas are documentation only" + The merged schema includes routes from all discovered handlers for documentation purposes. However, only routes defined in the current Lambda are actually executable. Other routes exist only in the OpenAPI spec - unless you configure API Gateway to route them to their respective Lambdas. + +### Shared resolver pattern + +In some architectures, instead of each handler file defining its own resolver, you have a central resolver file that is imported by multiple route files. Each route file registers its routes on the shared resolver instance. + +```text +src/ +├── myapp/ +│ ├── resolver.py # Defines: app = APIGatewayRestResolver() +│ ├── users_routes.py # Imports app, registers /users routes +│ ├── orders_routes.py # Imports app, registers /orders routes +│ └── payments_routes.py # Imports app, registers /payments routes +``` + +OpenAPI Merge automatically detects this pattern. When you point `discover()` to the resolver file, it finds all files that import from it and loads them to ensure all routes are registered before extracting the schema. + +=== "shared_resolver.py" + + ```python + --8<-- "examples/event_handler_rest/src/openapi_merge_shared_resolver.py" + ``` + +=== "shared_users_routes.py" + + ```python + --8<-- "examples/event_handler_rest/src/openapi_merge_shared_users_routes.py" + ``` + +=== "shared_orders_routes.py" + + ```python + --8<-- "examples/event_handler_rest/src/openapi_merge_shared_orders_routes.py" + ``` + +=== "Discovery" + + ```python + --8<-- "examples/event_handler_rest/src/openapi_merge_shared_discovery.py" + ``` + +### Conflict handling + +When the same path+method is defined in multiple handlers, use `on_conflict` to control behavior: + +| Strategy | Behavior | +| -------- | -------- | +| `warn` (default) | Log warning, keep first definition | +| `error` | Raise `OpenAPIMergeError` | +| `first` | Silently keep first definition | +| `last` | Use last definition (override) | + +=== "openapi_merge_conflict.py" + + ```python + --8<-- "examples/event_handler_rest/src/openapi_merge_conflict.py" + ``` + +### Full configuration + +=== "openapi_merge_full_config.py" + + ```python + --8<-- "examples/event_handler_rest/src/openapi_merge_full_config.py" + ``` diff --git a/docs/includes/_layer_homepage_arm64.md b/docs/includes/_layer_homepage_arm64.md index 8c6437a47ca..e40a04078ec 100644 --- a/docs/includes/_layer_homepage_arm64.md +++ b/docs/includes/_layer_homepage_arm64.md @@ -6,178 +6,168 @@ | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`me-central-1`** | **arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`me-south-1`** | **arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:30**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:33**{: .copyMe} | === "Python 3.11" | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`me-central-1`** | **arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`me-south-1`** | **arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:30**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:33**{: .copyMe} | === "Python 3.12" | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`me-central-1`** | **arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`me-south-1`** | **arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:30**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33**{: .copyMe} | === "Python 3.13" | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`me-central-1`** | **arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`me-south-1`** | **arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:30**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:33**{: .copyMe} | === "Python 3.14" | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`me-central-1`** | **arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`me-south-1`** | **arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:30**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:33**{: .copyMe} | diff --git a/docs/includes/_layer_homepage_x86.md b/docs/includes/_layer_homepage_x86.md index 7160f26f385..907788332f7 100644 --- a/docs/includes/_layer_homepage_x86.md +++ b/docs/includes/_layer_homepage_x86.md @@ -5,183 +5,173 @@ | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`me-central-1`** | **arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`me-south-1`** | **arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:30**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:33**{: .copyMe} | === "Python 3.11" | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`me-central-1`** | **arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`me-south-1`** | **arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:30**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:33**{: .copyMe} | === "Python 3.12" | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`me-central-1`** | **arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`me-south-1`** | **arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:30**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33**{: .copyMe} | === "Python 3.13" | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`me-central-1`** | **arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`me-south-1`** | **arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:30**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33**{: .copyMe} | === "Python 3.14" | Region | Layer ARN | | -------------------- | --------------------------------------------------------------------------------------------------------- | - | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`me-central-1`** | **arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`me-south-1`** | **arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | - | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:30**{: .copyMe} | + | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`ap-southeast-5`** | **arn:aws:lambda:ap-southeast-5:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`ap-southeast-7`** | **arn:aws:lambda:ap-southeast-7:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`mx-central-1`** | **arn:aws:lambda:mx-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | + | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:33**{: .copyMe} | diff --git a/docs/lambda-features/durable-functions.md b/docs/lambda-features/durable-functions.md new file mode 100644 index 00000000000..9a2e6f9b831 --- /dev/null +++ b/docs/lambda-features/durable-functions.md @@ -0,0 +1,151 @@ +--- +title: Durable Functions +description: Using Powertools for AWS Lambda (Python) with Lambda Durable Functions +--- + + + +[Lambda Durable Functions](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html){target="_blank" rel="nofollow"} enable you to build resilient multi-step workflows that can execute for up to one year. They use checkpoints to track progress and automatically recover from failures through replay. + +## Key concepts + +| Concept | Description | +| --------------------- | ------------------------------------------------------------------ | +| **Durable execution** | Complete lifecycle of a durable function, from start to completion | +| **Checkpoint** | Saved state that tracks progress through the workflow | +| **Replay** | Re-execution from the beginning, skipping completed checkpoints | +| **Step** | Business logic with built-in retries and progress tracking | +| **Wait** | Suspend execution without incurring compute charges | + +## How it works + +Durable functions use a **checkpoint/replay mechanism**: + +1. Your code runs always from the beginning +2. Completed operations are skipped using stored results +3. Execution of new steps continues from where it left off +4. State is automatically managed by the SDK + +## Powertools integration + +Powertools for AWS Lambda (Python) works seamlessly with Durable Functions. The [Durable Execution SDK](https://github.com/aws/aws-durable-execution-sdk-python){target="_blank" rel="nofollow"} has native integration with Logger via `context.set_logger()`. + +???+ note "Found an issue?" + If you encounter any issues using Powertools for AWS Lambda (Python) with Durable Functions, please [open an issue](https://github.com/aws-powertools/powertools-lambda-python/issues/new?template=bug_report.yml){target="_blank"}. + +### Logger + +The Durable Execution SDK provides a `context.logger` instance that automatically handles **log deduplication during replays**. You can integrate Logger to get structured JSON logging while keeping the deduplication benefits. + +For the best experience, set the Logger on the durable context. This gives you structured JSON logging with automatic log deduplication during replays: + +```python hl_lines="5 8 12 15" title="Integrating Logger with Durable Functions" +--8<-- "examples/lambda_features/durable_functions/src/using_logger.py" +``` + +This gives you: + +- **JSON structured logging** from Powertools for AWS Lambda (Python) +- **Log deduplication** during replays (logs from completed operations don't repeat) +- **Automatic SDK enrichment** (execution_arn, parent_id, name, attempt) +- **Lambda context injection** (request_id, function_name, etc.) + +???+ warning "Direct logger usage" + If you use the Logger directly (not through `context.logger`), logs will be emitted on every replay: + + ```python + # Logs will duplicate during replays + logger.info("This appears on every replay") + + # Use context.logger instead for deduplication + context.logger.info("This appears only once") + ``` + +### Tracer + +Tracer works with Durable Functions. Each execution creates trace segments. + +???+ note "Trace continuity" + Due to the replay mechanism, traces may be interleaved. Each execution (including replays) creates separate trace segments. Use the `execution_arn` to correlate traces. + +```python hl_lines="5-6 9-10" title="Using Tracer with Durable Functions" +--8<-- "examples/lambda_features/durable_functions/src/using_tracer.py" +``` + +### Metrics + +Metrics work with Durable Functions, but be aware that **metrics may be emitted multiple times** during replay if not handled carefully. Emit metrics at workflow completion rather than during intermediate steps to avoid counting replays as new executions. + +```python hl_lines="6 9 18 19 20 21" title="Using Metrics with Durable Functions" +--8<-- "examples/lambda_features/durable_functions/src/best_practice_metrics.py" +``` + +### Idempotency + +The `@idempotent` decorator integrates with Durable Functions and is **replay-aware**. It's useful for protecting the Lambda handler entry point, especially for Event Source Mapping (ESM) invocations like SQS, Kinesis, or DynamoDB Streams. + +```python hl_lines="8 15" title="Using Idempotency with Durable Functions" +--8<-- "examples/lambda_features/durable_functions/src/using_idempotency.py" +``` + +???+ warning "Decorator ordering matters" + The `@idempotent` decorator must be placed **above** `@durable_execution`. This ensures the idempotency check runs first, preventing duplicate executions before the durable workflow begins. Reversing the order would cause the durable execution to start before the idempotency check, defeating its purpose. + +**When to use Powertools Idempotency:** + +- Protecting the Lambda handler entry point from duplicate invocations +- Methods you don't want to convert into steps but need idempotency guarantees +- Event Source Mapping triggers (SQS, Kinesis, DynamoDB Streams) + +**When you don't need it:** + +- Steps within a durable function are already idempotent via the checkpoint mechanism + +### Parameters + +Parameters work normally with Durable Functions. + +```python hl_lines="13" title="Using Parameters with Durable Functions" +--8<-- "examples/lambda_features/durable_functions/src/using_parameters.py" +``` + +???+ note "Parameter freshness" + If the replay or execution happens within the cache TTL on the same execution environment, the parameter value may come from cache. For long-running workflows (hours/days), parameters fetched at the start may become stale. Consider fetching parameters within steps that need the latest values, and customize the caching behavior with `max_age` to control freshness. + +## Best practices + +### Use Idempotency for ESM triggers + +When your durable function is triggered by Event Source Mappings (SQS, Kinesis, DynamoDB Streams), use the `@idempotent` decorator to protect against duplicate invocations. + +```python title="Idempotency for ESM" +--8<-- "examples/lambda_features/durable_functions/src/best_practice_idempotency.py" +``` + +## FAQ + +### Do I need Idempotency utility with Durable Functions? + +It depends on your use case. Steps within a durable function are already idempotent via checkpoints. However, the `@idempotent` decorator is useful for protecting the Lambda handler entry point, especially for Event Source Mapping invocations (SQS, Kinesis, DynamoDB Streams) where the same event might trigger multiple invocations. + +### Why do I see duplicate logs? + +If you're using the logger directly instead of `context.logger`, logs will be emitted on every replay. Use `context.set_logger(logger)` and then `context.logger.info()` to get automatic log deduplication. + +### How do I correlate logs across replays? + +Use the `execution_arn` field that's automatically added to every log entry when using `context.logger`: + +```sql +fields @timestamp, @message, execution_arn +| filter execution_arn = "arn:aws:lambda:us-east-1:123456789012:function:my-function:execution-id" +| sort @timestamp asc +``` + +### Can I use Tracer with Durable Functions? + +Yes, but be aware that each execution (including replays) creates separate trace segments. Use the `execution_arn` as a correlation identifier for end-to-end visibility. + +### How should I emit metrics without duplicates? + +Emit metrics at workflow completion rather than during intermediate steps. This ensures you count completed workflows, not replay attempts. diff --git a/docs/lambda-features/index.md b/docs/lambda-features/index.md new file mode 100644 index 00000000000..30d791c8601 --- /dev/null +++ b/docs/lambda-features/index.md @@ -0,0 +1,28 @@ +--- +title: Lambda Features +description: Using Powertools with advanced Lambda features +--- + + + +This section covers how to use Powertools for AWS Lambda (Python) with advanced Lambda features like Lambda Managed Instances and Durable Functions. + +
+ +- :material-server:{ .lg .middle } __Lambda Managed Instances__ + + --- + + Run Lambda functions on EC2 instances with multi-concurrent invocations + + [:octicons-arrow-right-24: Getting started](./managed-instances.md) + +- :material-state-machine:{ .lg .middle } __Durable Functions__ + + --- + + Build resilient multi-step workflows that can execute for up to one year + + [:octicons-arrow-right-24: Getting started](./durable-functions.md) + +
diff --git a/docs/lambda-features/managed-instances.md b/docs/lambda-features/managed-instances.md new file mode 100644 index 00000000000..38779673a7f --- /dev/null +++ b/docs/lambda-features/managed-instances.md @@ -0,0 +1,102 @@ +--- +title: Lambda Managed Instances +description: Using Powertools for AWS Lambda (Python) with Lambda Managed Instances +--- + + + +[Lambda Managed Instances](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html){target="_blank" rel="nofollow"} enables you to run Lambda functions on Amazon EC2 instances without managing infrastructure. It supports multi-concurrent invocations, EC2 pricing models, and specialized compute options like Graviton4. + +## Key differences from Lambda On Demand + +| Aspect | Lambda On Demand | Lambda Managed Instances | +| ---------------- | ------------------------------------------- | ----------------------------------------------- | +| **Concurrency** | Single invocation per execution environment | Multiple concurrent invocations per environment | +| **Python model** | One process, one request | Multiple processes, one request each | +| **Pricing** | Per-request duration | EC2-based with Savings Plans support | +| **Scaling** | Scale on demand with cold starts | Async scaling based on CPU | +| **Isolation** | Firecracker microVMs | Containers on EC2 Nitro | + +## How Lambda Python runtime handles concurrency + +The **Lambda Python runtime uses multiple processes** for concurrent requests. Each request runs in a separate process, which provides natural isolation between requests. + +This means: + +- **Each process has its own memory** - global variables are isolated per process +- **`/tmp` directory is shared** across all processes - use caution with file operations + +For more details on the isolation model, see [Lambda Managed Instances documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html){target="_blank" rel="nofollow"}. + +## Powertools integration + +Powertools for AWS Lambda (Python) works seamlessly with Lambda Managed Instances. All utilities are compatible with the multi-process concurrency model used by Python. + +### Logger, Tracer, and Metrics + +Core utilities work without any changes. Each process has its own instances, so correlation IDs and traces are naturally isolated per request. + +???+ note "VPC connectivity required" + Lambda Managed Instances run in your VPC. Ensure you have [network connectivity](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances-networking.html){target="_blank" rel="nofollow"} to send logs to CloudWatch, traces to X-Ray, and metrics to CloudWatch. + +```python hl_lines="5 6 7 10 11 12 20 25" title="Using Logger, Tracer, and Metrics with Managed Instances" +--8<-- "examples/lambda_features/managed_instances/src/using_tracer.py" +``` + +### Parameters + +The Parameters utility works as expected, but be aware that **caching is per-process**. + +```python hl_lines="9" title="Using Parameters with Managed Instances" +--8<-- "examples/lambda_features/managed_instances/src/using_parameters.py" +``` + +???+ tip "Cache behavior" + Since each process has its own cache, you might see more calls to SSM/Secrets Manager during initial warm-up. Once each process has cached the value, subsequent requests within that process use the cache. You can customize the caching behavior with `max_age` to control the TTL. + +### Idempotency + +Idempotency works without any changes. It uses DynamoDB for state management, which is external to the process. + +```python hl_lines="7 10" title="Using Idempotency with Managed Instances" +--8<-- "examples/lambda_features/managed_instances/src/using_idempotency.py" +``` + +## VPC connectivity + +Lambda Managed Instances require VPC configuration for: + +- Sending logs to CloudWatch Logs +- Sending traces to X-Ray +- Accessing AWS services (SSM, Secrets Manager, DynamoDB, etc.) + +Configure connectivity using one of these options: + +1. **VPC Endpoints** - Private connectivity without internet access +2. **NAT Gateway** - Internet access from private subnets +3. **Public subnet with Internet Gateway** - Direct internet access +4. **Egress-only Internet Gateway** - IPv6 outbound connectivity without inbound access ([learn more](https://docs.aws.amazon.com/vpc/latest/userguide/egress-only-internet-gateway.html){target="_blank" rel="nofollow"}) + +See [Networking for Lambda Managed Instances](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances-networking.html){target="_blank" rel="nofollow"} for detailed setup instructions. + +## FAQ + +### Does Powertools for AWS Lambda (Python) work with Lambda Managed Instances? + +Yes, all Powertools for AWS Lambda (Python) utilities work seamlessly with Lambda Managed Instances. The multi-process model in Python provides natural isolation between concurrent requests. + +### Is my code thread-safe? + +Lambda Managed Instances uses **multiple processes**, instead of threads. Each request runs in its own process with isolated memory. If you implement multi-threading within your handler, you are responsible for thread safety. + +### Why is my cache not shared between requests? + +Each process maintains its own cache (for Parameters, Feature Flags, etc.). This is expected behavior. The cache will warm up independently per process, which may result in slightly more calls to backend services during initial warm-up. + +### Can I use global variables? + +Yes, but remember they are **per-process**, not shared across concurrent requests. This is actually safer than shared state. + +### Do I need to change my existing Powertools for AWS Lambda (Python) code? + +No changes are required if you are running Powertools for AWS Lambda (Python) version **3.4.0** or later. Your existing code will work as-is with Lambda Managed Instances. diff --git a/docs/requirements.txt b/docs/requirements.txt index d1113254b7d..a7b731a7d5a 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.50 \ + --hash=sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc \ + --hash=sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9 # 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 docs/requirements.in + # via -r requirements.in mkdocs-llmstxt==0.5.0 \ --hash=sha256:753c699913d2d619a9072604b26b6dc9f5fb6d257d9b107857f80c8a0b787533 \ --hash=sha256:b2fa9e6d68df41d7467e948a4745725b6c99434a36b36204857dbd7bb3dfe041 - # via -r docs/requirements.in + # via -r requirements.in mkdocs-material==9.7.6 \ --hash=sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69 \ --hash=sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba - # via -r docs/requirements.in + # via -r 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 docs/requirements.in + # via -r 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==10.21.3 \ + --hash=sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354 \ + --hash=sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6 # via # mkdocs-material # mkdocstrings @@ -397,9 +397,9 @@ pyyaml-env-tag==1.1 \ --hash=sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04 \ --hash=sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff # via mkdocs -requests==2.32.4 \ - --hash=sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c \ - --hash=sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422 +requests==2.33.0 \ + --hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b \ + --hash=sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652 # via mkdocs-material six==1.17.0 \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ @@ -415,64 +415,13 @@ soupsieve==2.7 \ --hash=sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4 \ --hash=sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a # via beautifulsoup4 -tomli==2.4.0 \ - --hash=sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729 \ - --hash=sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b \ - --hash=sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d \ - --hash=sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df \ - --hash=sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576 \ - --hash=sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d \ - --hash=sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1 \ - --hash=sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a \ - --hash=sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e \ - --hash=sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc \ - --hash=sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702 \ - --hash=sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6 \ - --hash=sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd \ - --hash=sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4 \ - --hash=sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776 \ - --hash=sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a \ - --hash=sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66 \ - --hash=sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87 \ - --hash=sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2 \ - --hash=sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f \ - --hash=sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475 \ - --hash=sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f \ - --hash=sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95 \ - --hash=sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9 \ - --hash=sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3 \ - --hash=sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9 \ - --hash=sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76 \ - --hash=sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da \ - --hash=sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8 \ - --hash=sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51 \ - --hash=sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86 \ - --hash=sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8 \ - --hash=sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0 \ - --hash=sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b \ - --hash=sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1 \ - --hash=sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e \ - --hash=sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d \ - --hash=sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c \ - --hash=sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867 \ - --hash=sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a \ - --hash=sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c \ - --hash=sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0 \ - --hash=sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4 \ - --hash=sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614 \ - --hash=sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132 \ - --hash=sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa \ - --hash=sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087 - # via mdformat typing-extensions==4.14.0 \ --hash=sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4 \ --hash=sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af - # via - # beautifulsoup4 - # mkdocstrings-python -urllib3==2.6.3 \ - --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ - --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 + # via beautifulsoup4 +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via requests watchdog==6.0.0 \ --hash=sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a \ 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/we_made_this.md b/docs/we_made_this.md index db3dc8a23b1..07370d53d9e 100644 --- a/docs/we_made_this.md +++ b/docs/we_made_this.md @@ -35,29 +35,29 @@ GitHub: [https://github.com/serverless-dna/powertools-mcp](https://github.com/se A collection of articles explaining in detail how Powertools for AWS Lambda helps with a Serverless adoption strategy and its challenges. -* [Part 1 - Logging](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-elevate-your-handler-s-code-part-1-logging){target="_blank" rel="nofollow"} +* [Part 1 - Logging](https://ranthebuilder.cloud/blog/aws-lambda-cookbook-elevate-your-handler-s-code-part-1-logging/){target="_blank" rel="nofollow"} -* [Part 2 - Observability: monitoring and tracing](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-elevate-your-handler-s-code-part-2-observability){target="_blank" rel="nofollow"} +* [Part 2 - Observability: monitoring and tracing](https://ranthebuilder.cloud/blog/aws-lambda-cookbook-elevate-your-handler-s-code-part-2-observability/){target="_blank" rel="nofollow"} -* [Part 3 - Business Domain Observability](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-elevate-your-handler-s-code-part-3-business-domain-observability){target="_blank" rel="nofollow"} +* [Part 3 - Business Domain Observability](https://ranthebuilder.cloud/blog/aws-lambda-cookbook-elevate-your-handler-s-code-part-3-business-domain-observability/){target="_blank" rel="nofollow"} -* [Part 4 - Environment Variables](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-environment-variables){target="_blank" rel="nofollow"} +* [Part 4 - Environment Variables](https://ranthebuilder.cloud/blog/aws-lambda-cookbook-environment-variables/){target="_blank" rel="nofollow"} -* [Part 5 - Input Validation](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-elevate-your-handler-s-code-part-5-input-validation){target="_blank" rel="nofollow"} +* [Part 5 - Input Validation](https://ranthebuilder.cloud/blog/aws-lambda-cookbook-elevate-your-handler-s-code-part-5-input-validation/){target="_blank" rel="nofollow"} -* [Part 6 - Configuration & Feature Flags](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-part-6-feature-flags-configuration-best-practices){target="_blank" rel="nofollow"} +* [Part 6 - Configuration & Feature Flags](https://ranthebuilder.cloud/blog/aws-lambda-cookbook-part-6-feature-flags-configuration-best-practices/){target="_blank" rel="nofollow"} -* [Serverless API Idempotency with AWS Powertools for AWS Lambda and CDK](https://www.ranthebuilder.cloud/post/serverless-api-idempotency-with-aws-lambda-powertools-and-cdk){target="_blank" rel="nofollow"} +* [Serverless API Idempotency with AWS Powertools for AWS Lambda and CDK](https://ranthebuilder.cloud/blog/serverless-api-idempotency-with-aws-lambda-powertools-and-cdk/){target="_blank" rel="nofollow"} -* [Effective Amazon SQS Batch Handling with Powertools for AWS Lambda (Python)](https://www.ranthebuilder.cloud/post/effective-amazon-sqs-batch-handling-with-aws-lambda-powertools){target="_blank" rel="nofollow"} +* [Effective Amazon SQS Batch Handling with Powertools for AWS Lambda (Python)](https://ranthebuilder.cloud/blog/effective-amazon-sqs-batch-handling-with-aws-lambda-powertools/){target="_blank" rel="nofollow"} -* [Serverless API Documentation with Powertools for AWS Lambda](https://www.ranthebuilder.cloud/post/serverless-open-api-documentation-with-aws-powertools){:target="_blank"} +* [Serverless API Documentation with Powertools for AWS Lambda](https://ranthebuilder.cloud/blog/serverless-open-api-documentation-with-aws-powertools/){:target="_blank"} * [Best practices for accelerating development with serverless blueprints](https://aws.amazon.com/blogs/infrastructure-and-automation/best-practices-for-accelerating-development-with-serverless-blueprints/){target="_blank" rel="nofollow"} -* [Build a Chatbot with Amazon Bedrock: Automate API Calls Using Powertools for AWS Lambda and CDK](https://www.ranthebuilder.cloud/post/automating-api-calls-with-agents-for-amazon-bedrock-with-powertools){target="_blank" rel="nofollow"} +* [Build a Chatbot with Amazon Bedrock: Automate API Calls Using Powertools for AWS Lambda and CDK](https://ranthebuilder.cloud/blog/automating-api-calls-with-agents-for-amazon-bedrock-with-powertools/){target="_blank" rel="nofollow"} -* [Build Serverless WebSockets with AWS AppSync Events and Powertools for AWS Lambda](https://www.ranthebuilder.cloud/post/aws-appsync-events-and-powertools-for-aws-lambda){target="_blank" rel="nofollow"} +* [Build Serverless WebSockets with AWS AppSync Events and Powertools for AWS Lambda](https://ranthebuilder.cloud/blog/aws-appsync-events-and-powertools-for-aws-lambda/){target="_blank" rel="nofollow"} #### Lambda MCP Server Cookbook @@ -138,7 +138,7 @@ This article will guide you through personalizing observability by integrating C > **Author: [Nathan Hanks](https://www.linkedin.com/in/nathan-hanks-25151815/){target="_blank" rel="nofollow"}** :material-linkedin: -[Creating a serverless API using Powertools for AWS Lambda and CDK](https://www.ranthebuilder.cloud/post/boost-app-engagement-with-aws-cloudwatch-metrics-powertools-for-aws){target="_blank" rel="nofollow"} +[Creating a serverless API using Powertools for AWS Lambda and CDK](https://ranthebuilder.cloud/blog/boost-app-engagement-with-aws-cloudwatch-metrics-powertools-for-aws/){target="_blank" rel="nofollow"} ### Streaming data with AWS Lambda & Powertools for AWS Lambda @@ -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 @@ -192,7 +200,7 @@ Are you developing AWS Lambda functions with Python? Always looking for tools to This session covers an opinionated approach to Python project setup, testing, profiling, deployments, and operations. Learn about many open source tools, including Powertools for AWS Lambda—a toolkit that can help you implement serverless best practices and increase developer velocity. -Join to discover tools and patterns for effective serverless development with Python. To maximize your learning experience, the session includes a sample application that implements what’s described. +Join to discover tools and patterns for effective serverless development with Python. To maximize your learning experience, the session includes a sample application that implements what's described. diff --git a/examples/build_recipes/cdk/basic/app.py b/examples/build_recipes/cdk/basic/app.py index f5d3d759ed8..6268d8e49c0 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:30", + layer_version_arn="arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33", ) # 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 7bb1f3f578f..3c7a480adbe 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:30", + layer_version_arn="arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33", ) def _create_dynamodb_table(self) -> dynamodb.Table: diff --git a/examples/build_recipes/sam/multi-env/template.yaml b/examples/build_recipes/sam/multi-env/template.yaml index 636beea0d67..8babe94f382 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:30 + - arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33 - !Ref DependenciesLayer Events: ApiEvent: diff --git a/examples/build_recipes/sam/with-layers/template.yaml b/examples/build_recipes/sam/with-layers/template.yaml index 9fac52a8127..fc9803b5a8a 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:30 + - arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33 - !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:30 + - arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:33 - !Ref DependenciesLayer Events: SQSEvent: diff --git a/examples/event_handler_graphql/src/requirements.txt b/examples/event_handler_graphql/src/requirements.txt index 785ab54fc57..67f59e04d75 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.33.1 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_api_operations.py b/examples/event_handler_rest/src/customizing_api_operations.py index e455fc7dadd..d7e9afeba6d 100644 --- a/examples/event_handler_rest/src/customizing_api_operations.py +++ b/examples/event_handler_rest/src/customizing_api_operations.py @@ -26,5 +26,17 @@ def get_todo_title(todo_id: int) -> str: return todo.json()["title"] +@app.post( + "/todos", + summary="Creates a new todo item", + description="Creates a new todo item and returns it", + response_description="The created todo object", + status_code=201, + tags=["Todos"], +) +def create_todo(title: str) -> dict: + return {"id": 1, "title": title} + + def lambda_handler(event: dict, context: LambdaContext) -> dict: return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/dependency_injection.py b/examples/event_handler_rest/src/dependency_injection.py new file mode 100644 index 00000000000..664bc56951d --- /dev/null +++ b/examples/event_handler_rest/src/dependency_injection.py @@ -0,0 +1,32 @@ +import os +from typing import Any + +import boto3 +from typing_extensions import Annotated + +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler.depends import Depends +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayHttpResolver() + + +def get_dynamodb_table(): + dynamodb = boto3.resource("dynamodb") + return dynamodb.Table(os.environ["TABLE_NAME"]) + + +@app.get("/orders") +def list_orders(table: Annotated[Any, Depends(get_dynamodb_table)]): + return table.scan()["Items"] + + +@app.post("/orders") +def create_order(table: Annotated[Any, Depends(get_dynamodb_table)]): + order = app.current_event.json_body + table.put_item(Item=order) + return {"message": "Order created"} + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/dependency_injection_nested.py b/examples/event_handler_rest/src/dependency_injection_nested.py new file mode 100644 index 00000000000..f8245439538 --- /dev/null +++ b/examples/event_handler_rest/src/dependency_injection_nested.py @@ -0,0 +1,38 @@ +import os +from typing import Any + +import boto3 +from typing_extensions import Annotated + +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler.depends import Depends +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayHttpResolver() + + +def get_dynamodb_resource(): + return boto3.resource("dynamodb") + + +def get_orders_table(dynamodb: Annotated[Any, Depends(get_dynamodb_resource)]): + return dynamodb.Table(os.environ["ORDERS_TABLE"]) + + +def get_users_table(dynamodb: Annotated[Any, Depends(get_dynamodb_resource)]): + return dynamodb.Table(os.environ["USERS_TABLE"]) + + +@app.get("/orders/") +def get_user_orders( + user_id: str, + orders_table: Annotated[Any, Depends(get_orders_table)], + users_table: Annotated[Any, Depends(get_users_table)], +): + user = users_table.get_item(Key={"pk": user_id})["Item"] + orders = orders_table.query(KeyConditionExpression="pk = :uid", ExpressionAttributeValues={":uid": user_id}) + return {"user": user["name"], "orders": orders["Items"]} + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/dependency_injection_testing.py b/examples/event_handler_rest/src/dependency_injection_testing.py new file mode 100644 index 00000000000..3b9f41c5330 --- /dev/null +++ b/examples/event_handler_rest/src/dependency_injection_testing.py @@ -0,0 +1,26 @@ +from unittest.mock import MagicMock + +from dependency_injection import app, get_dynamodb_table + + +def test_list_orders(): + # Create a mock table + mock_table = MagicMock() + mock_table.scan.return_value = {"Items": [{"id": "order-1"}]} + + # Override the dependency with a lambda that returns the mock + app.dependency_overrides[get_dynamodb_table] = lambda: mock_table + + result = app( + { + "requestContext": {"http": {"method": "GET", "path": "/orders"}, "stage": "$default"}, + "rawPath": "/orders", + "headers": {}, + }, + {}, + ) + + assert result["statusCode"] == 200 + + # Clean up overrides after testing + app.dependency_overrides.clear() diff --git a/examples/event_handler_rest/src/dependency_injection_with_middleware.py b/examples/event_handler_rest/src/dependency_injection_with_middleware.py new file mode 100644 index 00000000000..63ee419c345 --- /dev/null +++ b/examples/event_handler_rest/src/dependency_injection_with_middleware.py @@ -0,0 +1,36 @@ +from typing_extensions import Annotated + +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver, Response +from aws_lambda_powertools.event_handler.depends import Depends +from aws_lambda_powertools.event_handler.request import Request +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayHttpResolver() + + +# Middleware handles auth — it can return HTTP responses (redirects, 401s) +def auth_middleware(app, next_middleware): + token = app.current_event.headers.get("authorization", "") + if not token: + return Response(status_code=401, body="Unauthorized") + + # Middleware writes to app.context + app.append_context(user={"id": "user-123", "role": "admin"}) + return next_middleware(app) + + +app.use(middlewares=[auth_middleware]) + + +# Depends() reads what middleware wrote via request.context — typed and testable +def get_current_user(request: Request) -> dict: + return request.context["user"] + + +@app.get("/admin/dashboard") +def admin_dashboard(user: Annotated[dict, Depends(get_current_user)]): + return {"message": f"Welcome {user['id']}", "role": user["role"]} + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/dependency_injection_with_request.py b/examples/event_handler_rest/src/dependency_injection_with_request.py new file mode 100644 index 00000000000..e8586294f32 --- /dev/null +++ b/examples/event_handler_rest/src/dependency_injection_with_request.py @@ -0,0 +1,26 @@ +from typing_extensions import Annotated + +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler.depends import Depends +from aws_lambda_powertools.event_handler.exceptions import UnauthorizedError +from aws_lambda_powertools.event_handler.request import Request +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayHttpResolver() + + +def get_authenticated_user(request: Request) -> str: + # Use resolved_event for full Powertools event access (cookies, request_context, path, etc.) + user_id = request.resolved_event.headers.get("x-user-id", "") + if not user_id: + raise UnauthorizedError("Missing authentication") + return user_id + + +@app.get("/profile") +def get_profile(user_id: Annotated[str, Depends(get_authenticated_user)]): + return {"user_id": user_id} + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/middleware_request_handler_injection.py b/examples/event_handler_rest/src/middleware_request_handler_injection.py new file mode 100644 index 00000000000..9304fcb1021 --- /dev/null +++ b/examples/event_handler_rest/src/middleware_request_handler_injection.py @@ -0,0 +1,16 @@ +from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Request + +app = APIGatewayRestResolver() + + +@app.get("/todos/") +def get_todo(todo_id: str, request: Request): # (1)! + return { + "id": todo_id, + "route": request.route, # (2)! + "user_agent": request.headers.get("user-agent", ""), + } + + +def lambda_handler(event, context): + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/middleware_request_object.py b/examples/event_handler_rest/src/middleware_request_object.py new file mode 100644 index 00000000000..9f58460888d --- /dev/null +++ b/examples/event_handler_rest/src/middleware_request_object.py @@ -0,0 +1,30 @@ +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 request_context_middleware(app: APIGatewayRestResolver, next_middleware: NextMiddleware) -> Response: + request = app.request # (1)! + + logger.append_keys( + route=request.route, # (2)! + method=request.method, + path_parameters=request.path_parameters, # (3)! + ) + + response = next_middleware(app) + + response.headers["x-route-pattern"] = request.route # (4)! + return response + + +@app.get("/todos/", middlewares=[request_context_middleware]) +def get_todo(todo_id: str): + return {"id": todo_id} + + +def lambda_handler(event, context): + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/openapi_merge_conflict.py b/examples/event_handler_rest/src/openapi_merge_conflict.py new file mode 100644 index 00000000000..f7b90c7a946 --- /dev/null +++ b/examples/event_handler_rest/src/openapi_merge_conflict.py @@ -0,0 +1,9 @@ +from aws_lambda_powertools.event_handler.openapi import OpenAPIMerge + +merge = OpenAPIMerge( + title="API", + version="1.0.0", + on_conflict="error", # Raise OpenAPIMergeError on conflicts +) + +merge.discover(path="./src", pattern="**/handler.py") diff --git a/examples/event_handler_rest/src/openapi_merge_full_config.py b/examples/event_handler_rest/src/openapi_merge_full_config.py new file mode 100644 index 00000000000..a52549c6aab --- /dev/null +++ b/examples/event_handler_rest/src/openapi_merge_full_config.py @@ -0,0 +1,18 @@ +from aws_lambda_powertools.event_handler.openapi import OpenAPIMerge +from aws_lambda_powertools.event_handler.openapi.models import Contact, License, Server, Tag + +merge = OpenAPIMerge( + title="My API", + version="1.0.0", + summary="API summary", + description="Full API description", + terms_of_service="https://example.com/tos", + contact=Contact(name="Support", email="support@example.com"), + license_info=License(name="MIT"), + servers=[Server(url="https://api.example.com")], + tags=[Tag(name="users", description="User operations")], + on_conflict="warn", +) + +merge.discover(path="./src", pattern="**/handler.py", recursive=True) +schema = merge.get_openapi_json_schema() diff --git a/examples/event_handler_rest/src/openapi_merge_multiple_patterns.py b/examples/event_handler_rest/src/openapi_merge_multiple_patterns.py new file mode 100644 index 00000000000..9cf2a46fc8e --- /dev/null +++ b/examples/event_handler_rest/src/openapi_merge_multiple_patterns.py @@ -0,0 +1,9 @@ +from aws_lambda_powertools.event_handler.openapi import OpenAPIMerge + +merge = OpenAPIMerge(title="API", version="1.0.0") + +merge.discover( + path="./src", + pattern=["handler.py", "api.py", "*_routes.py"], + recursive=True, +) diff --git a/examples/event_handler_rest/src/openapi_merge_resolver.py b/examples/event_handler_rest/src/openapi_merge_resolver.py new file mode 100644 index 00000000000..cc8cccbf2a5 --- /dev/null +++ b/examples/event_handler_rest/src/openapi_merge_resolver.py @@ -0,0 +1,18 @@ +from aws_lambda_powertools.event_handler import APIGatewayRestResolver + +app = APIGatewayRestResolver() + +# Configure merge - discovers handlers but doesn't execute them +app.configure_openapi_merge( + path="./functions", + pattern="**/handler.py", + title="My API", + version="1.0.0", +) + +# Swagger UI will show the merged schema +app.enable_swagger(path="/docs") + + +def handler(event, context): + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/openapi_merge_shared_discovery.py b/examples/event_handler_rest/src/openapi_merge_shared_discovery.py new file mode 100644 index 00000000000..13cdfc3cef8 --- /dev/null +++ b/examples/event_handler_rest/src/openapi_merge_shared_discovery.py @@ -0,0 +1,13 @@ +from aws_lambda_powertools.event_handler.openapi import OpenAPIMerge + +merge = OpenAPIMerge(title="API", version="1.0.0") + +# Use project_root to resolve absolute imports like "from myapp.shared_resolver import app" +merge.discover( + path="./src/myapp", + pattern="shared_resolver.py", + project_root="./src", # Root for import resolution +) + +# Automatically finds users_routes.py and orders_routes.py +# that import from shared_resolver.py diff --git a/examples/event_handler_rest/src/openapi_merge_shared_orders_routes.py b/examples/event_handler_rest/src/openapi_merge_shared_orders_routes.py new file mode 100644 index 00000000000..2682a9c6ab1 --- /dev/null +++ b/examples/event_handler_rest/src/openapi_merge_shared_orders_routes.py @@ -0,0 +1,7 @@ +# Imports and registers routes on shared resolver - orders_routes.py +from myapp.shared_resolver import app # type: ignore[import-not-found] + + +@app.get("/orders") +def get_orders(): + return [] diff --git a/examples/event_handler_rest/src/openapi_merge_shared_resolver.py b/examples/event_handler_rest/src/openapi_merge_shared_resolver.py new file mode 100644 index 00000000000..decaaa3c829 --- /dev/null +++ b/examples/event_handler_rest/src/openapi_merge_shared_resolver.py @@ -0,0 +1,4 @@ +# Central resolver definition - shared_resolver.py +from aws_lambda_powertools.event_handler import APIGatewayRestResolver + +app = APIGatewayRestResolver() diff --git a/examples/event_handler_rest/src/openapi_merge_shared_users_routes.py b/examples/event_handler_rest/src/openapi_merge_shared_users_routes.py new file mode 100644 index 00000000000..de4c87069de --- /dev/null +++ b/examples/event_handler_rest/src/openapi_merge_shared_users_routes.py @@ -0,0 +1,12 @@ +# Imports and registers routes on shared resolver - users_routes.py +from myapp.shared_resolver import app # type: ignore[import-not-found] + + +@app.get("/users") +def get_users(): + return [] + + +@app.get("/users/") +def get_user(user_id: str): + return {"id": user_id} diff --git a/examples/event_handler_rest/src/openapi_merge_standalone.py b/examples/event_handler_rest/src/openapi_merge_standalone.py new file mode 100644 index 00000000000..ef056974901 --- /dev/null +++ b/examples/event_handler_rest/src/openapi_merge_standalone.py @@ -0,0 +1,23 @@ +from pathlib import Path + +from aws_lambda_powertools.event_handler.openapi import OpenAPIMerge + +merge = OpenAPIMerge( + title="My Unified API", + version="1.0.0", + description="Consolidated API from multiple Lambda functions", +) + +# Discover handlers +merge.discover( + path="./src/functions", + pattern="*_handler.py", + recursive=True, +) + +# Generate schema +schema_json = merge.get_openapi_json_schema() + +# Write to file +output = Path("openapi.json") +output.write_text(schema_json) diff --git a/examples/event_handler_rest/src/openapi_merge_with_exclusions.py b/examples/event_handler_rest/src/openapi_merge_with_exclusions.py new file mode 100644 index 00000000000..b781857ddc5 --- /dev/null +++ b/examples/event_handler_rest/src/openapi_merge_with_exclusions.py @@ -0,0 +1,10 @@ +from aws_lambda_powertools.event_handler.openapi import OpenAPIMerge + +merge = OpenAPIMerge(title="API", version="1.0.0") + +merge.discover( + path="./src", + pattern="**/*_handler.py", + exclude=["**/tests/**", "**/legacy/**"], + recursive=True, +) diff --git a/examples/event_handler_rest/src/working_with_file_uploads.py b/examples/event_handler_rest/src/working_with_file_uploads.py new file mode 100644 index 00000000000..bebf72939fe --- /dev/null +++ b/examples/event_handler_rest/src/working_with_file_uploads.py @@ -0,0 +1,17 @@ +from typing import Annotated + +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.event_handler.openapi.params import File + +app = APIGatewayRestResolver(enable_validation=True) + + +@app.post("/upload") +def upload_file( + file_data: Annotated[bytes, File(description="File to upload")], # (1)! +): + return {"file_size": len(file_data)} + + +def lambda_handler(event, context): + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/working_with_file_uploads_metadata.py b/examples/event_handler_rest/src/working_with_file_uploads_metadata.py new file mode 100644 index 00000000000..07da798f350 --- /dev/null +++ b/examples/event_handler_rest/src/working_with_file_uploads_metadata.py @@ -0,0 +1,21 @@ +from typing import Annotated + +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.event_handler.openapi.params import File, UploadFile + +app = APIGatewayRestResolver(enable_validation=True) + + +@app.post("/upload") +def upload_file( + file_data: Annotated[UploadFile, File(description="File to upload")], # (1)! +): + return { + "filename": file_data.filename, # (2)! + "content_type": file_data.content_type, + "file_size": len(file_data), + } + + +def lambda_handler(event, context): + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/working_with_file_uploads_mixed.py b/examples/event_handler_rest/src/working_with_file_uploads_mixed.py new file mode 100644 index 00000000000..e0c3859c58e --- /dev/null +++ b/examples/event_handler_rest/src/working_with_file_uploads_mixed.py @@ -0,0 +1,29 @@ +import csv +import io +from typing import Annotated + +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.event_handler.openapi.params import File, Form, UploadFile + +app = APIGatewayRestResolver(enable_validation=True) + + +@app.post("/upload-csv") +def upload_csv( + file_data: Annotated[UploadFile, File(description="CSV file to parse")], # (1)! + separator: Annotated[str, Form(description="CSV separator")] = ",", # (2)! +): + text = file_data.content.decode("utf-8") + reader = csv.DictReader(io.StringIO(text), delimiter=separator) + rows = list(reader) + + return { + "filename": file_data.filename, + "total_rows": len(rows), + "columns": list(rows[0].keys()) if rows else [], + "data": rows, + } + + +def lambda_handler(event, context): + return app.resolve(event, context) diff --git a/examples/homepage/install/arm64/amplify.txt b/examples/homepage/install/arm64/amplify.txt index 4c426a23c01..8aadd5ecbba 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:30 +? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33 ❯ 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:30 +? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33 ? 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 a9b6ed9d01f..2b7d084a199 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:30", + layer_version_arn=f"arn:aws:lambda:{Aws.REGION}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33", ) aws_lambda.Function( self, diff --git a/examples/homepage/install/arm64/pulumi_arm64.py b/examples/homepage/install/arm64/pulumi_arm64.py index 99115287ef0..1659d2362ad 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:30", + ":017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33", ), ], tracing_config={"mode": "Active"}, diff --git a/examples/homepage/install/arm64/sam.yaml b/examples/homepage/install/arm64/sam.yaml index fa1833c7be1..8d77abd0e02 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:30 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33 diff --git a/examples/homepage/install/arm64/serverless.yml b/examples/homepage/install/arm64/serverless.yml index 4ef384f4d99..c3812016a42 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:30 + - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33 diff --git a/examples/homepage/install/arm64/terraform.tf b/examples/homepage/install/arm64/terraform.tf index 679c422939f..dc46db6df87 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:30"] + layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:33"] 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 63cdcc42b41..9a1223e9280 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:30 +? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33 ❯ 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:30 +? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33 ? 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 b70637672c2..30d82a497dd 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:30", + layer_version_arn=f"arn:aws:lambda:{Aws.REGION}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33", ) 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 067a6036ae7..1130c67b644 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:30", + ":017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33", ), ], tracing_config={"mode": "Active"}, diff --git a/examples/homepage/install/x86_64/sam.yaml b/examples/homepage/install/x86_64/sam.yaml index b26723ca830..d6beb66ea52 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:30 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33 diff --git a/examples/homepage/install/x86_64/serverless.yml b/examples/homepage/install/x86_64/serverless.yml index d55c246c4c6..56deaf1b0fa 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:30 + - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33 diff --git a/examples/homepage/install/x86_64/terraform.tf b/examples/homepage/install/x86_64/terraform.tf index 53f7088f950..56e73fe45b6 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:30"] + layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33"] source_code_hash = filebase64sha256("lambda_function_payload.zip") } 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/lambda_features/__init__.py b/examples/lambda_features/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/lambda_features/durable_functions/__init__.py b/examples/lambda_features/durable_functions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/lambda_features/durable_functions/src/__init__.py b/examples/lambda_features/durable_functions/src/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/lambda_features/durable_functions/src/best_practice_idempotency.py b/examples/lambda_features/durable_functions/src/best_practice_idempotency.py new file mode 100644 index 00000000000..6d412b6127b --- /dev/null +++ b/examples/lambda_features/durable_functions/src/best_practice_idempotency.py @@ -0,0 +1,21 @@ +from aws_durable_execution_sdk_python import DurableContext, durable_execution # type: ignore[import-not-found] + +from aws_lambda_powertools.utilities.idempotency import ( + DynamoDBPersistenceLayer, + idempotent, +) + +persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable") + + +@idempotent(persistence_store=persistence_layer) +@durable_execution +def handler(event: dict, context: DurableContext) -> str: + # Protected against duplicate SQS/Kinesis/DynamoDB triggers + + result: str = context.step( + lambda _: "processed", + name="process", + ) + + return result diff --git a/examples/lambda_features/durable_functions/src/best_practice_metrics.py b/examples/lambda_features/durable_functions/src/best_practice_metrics.py new file mode 100644 index 00000000000..59bd2c037ad --- /dev/null +++ b/examples/lambda_features/durable_functions/src/best_practice_metrics.py @@ -0,0 +1,23 @@ +from aws_durable_execution_sdk_python import DurableContext, durable_execution # type: ignore[import-not-found] + +from aws_lambda_powertools import Metrics +from aws_lambda_powertools.metrics import MetricUnit + +metrics = Metrics() + + +@metrics.log_metrics +@durable_execution +def handler(event: dict, context: DurableContext) -> str: + result: str = context.step( + lambda _: "processed", + name="process", + ) + + # Emit metrics in a dedicated step to ensure they are only counted once + context.step( + lambda _: metrics.add_metric(name="WorkflowCompleted", unit=MetricUnit.Count, value=1), + name="emit_completion_metric", + ) + + return result diff --git a/examples/lambda_features/durable_functions/src/using_idempotency.py b/examples/lambda_features/durable_functions/src/using_idempotency.py new file mode 100644 index 00000000000..04654dd5c9d --- /dev/null +++ b/examples/lambda_features/durable_functions/src/using_idempotency.py @@ -0,0 +1,26 @@ +from aws_durable_execution_sdk_python import DurableContext, durable_execution # type: ignore[import-not-found] + +from aws_lambda_powertools.utilities.idempotency import ( + DynamoDBPersistenceLayer, + idempotent, +) + +persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable") + + +def process_order(event: dict) -> str: + return f"processed-{event.get('order_id')}" + + +@idempotent(persistence_store=persistence_layer) +@durable_execution +def handler(event: dict, context: DurableContext) -> str: + # Idempotency protects against duplicate ESM invocations + # Steps within the workflow are already idempotent via checkpoints + + result: str = context.step( + lambda _: process_order(event), + name="process_order", + ) + + return result diff --git a/examples/lambda_features/durable_functions/src/using_logger.py b/examples/lambda_features/durable_functions/src/using_logger.py new file mode 100644 index 00000000000..577a15eb583 --- /dev/null +++ b/examples/lambda_features/durable_functions/src/using_logger.py @@ -0,0 +1,25 @@ +from aws_durable_execution_sdk_python import DurableContext, durable_execution # type: ignore[import-not-found] + +from aws_lambda_powertools import Logger + +logger = Logger(service="order-processing") + + +@logger.inject_lambda_context +@durable_execution +def handler(event: dict, context: DurableContext) -> str: + # Set Logger on the context for automatic deduplication + context.set_logger(logger) + + # Logs via context.logger appear only once, even during replays + context.logger.info("Starting workflow", extra={"order_id": event.get("order_id")}) + + result: str = context.step( + lambda _: "processed", + name="process_order", + ) + + # This log won't repeat when the function replays after completing the step above + context.logger.info("Workflow completed", extra={"result": result}) + + return result diff --git a/examples/lambda_features/durable_functions/src/using_parameters.py b/examples/lambda_features/durable_functions/src/using_parameters.py new file mode 100644 index 00000000000..92f2c28256a --- /dev/null +++ b/examples/lambda_features/durable_functions/src/using_parameters.py @@ -0,0 +1,20 @@ +from aws_durable_execution_sdk_python import DurableContext, durable_execution # type: ignore[import-not-found] + +from aws_lambda_powertools.utilities import parameters + + +def call_api(api_key: str) -> str: + return f"called-with-{api_key[:4]}..." + + +@durable_execution +def handler(event: dict, context: DurableContext) -> str: + # Parameters may come from cache if replay hits the same execution environment within the TTL + api_key = parameters.get_secret("api-key") + + result: str = context.step( + lambda _: call_api(api_key), + name="call_api", + ) + + return result diff --git a/examples/lambda_features/durable_functions/src/using_tracer.py b/examples/lambda_features/durable_functions/src/using_tracer.py new file mode 100644 index 00000000000..a70b6f46277 --- /dev/null +++ b/examples/lambda_features/durable_functions/src/using_tracer.py @@ -0,0 +1,27 @@ +from aws_durable_execution_sdk_python import DurableContext, durable_execution # type: ignore[import-not-found] + +from aws_lambda_powertools import Logger, Tracer + +tracer = Tracer() +logger = Logger() + + +@logger.inject_lambda_context +@tracer.capture_lambda_handler +@durable_execution +def handler(event: dict, context: DurableContext) -> str: + context.set_logger(logger) + + result: str = context.step( + lambda _: process_data(), + name="process_data", + ) + + return result + + +@tracer.capture_method +def process_data() -> str: + # This is traced on first execution + # On replay, the cached result is used + return "processed" diff --git a/examples/lambda_features/managed_instances/__init__.py b/examples/lambda_features/managed_instances/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/lambda_features/managed_instances/src/__init__.py b/examples/lambda_features/managed_instances/src/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/lambda_features/managed_instances/src/using_idempotency.py b/examples/lambda_features/managed_instances/src/using_idempotency.py new file mode 100644 index 00000000000..e0e054d07f6 --- /dev/null +++ b/examples/lambda_features/managed_instances/src/using_idempotency.py @@ -0,0 +1,14 @@ +from aws_lambda_powertools.utilities.idempotency import ( + DynamoDBPersistenceLayer, + idempotent, +) +from aws_lambda_powertools.utilities.typing import LambdaContext + +persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable") + + +@idempotent(persistence_store=persistence_layer) +def lambda_handler(event: dict, context: LambdaContext) -> dict: + # Idempotency is guaranteed across all concurrent requests + # DynamoDB handles the distributed locking + return {"statusCode": 200, "body": "Order processed"} diff --git a/examples/lambda_features/managed_instances/src/using_parameters.py b/examples/lambda_features/managed_instances/src/using_parameters.py new file mode 100644 index 00000000000..36ded9cc9c6 --- /dev/null +++ b/examples/lambda_features/managed_instances/src/using_parameters.py @@ -0,0 +1,11 @@ +from aws_lambda_powertools.utilities import parameters +from aws_lambda_powertools.utilities.typing import LambdaContext + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + # Cache is per-process, not shared across concurrent requests + # Each process maintains its own cache + # This is generally fine - cache will warm up per process + api_key = parameters.get_secret("my-api-key", max_age=300) # noqa: F841 + + return {"statusCode": 200} diff --git a/examples/lambda_features/managed_instances/src/using_tracer.py b/examples/lambda_features/managed_instances/src/using_tracer.py new file mode 100644 index 00000000000..3cb4ee6f7fd --- /dev/null +++ b/examples/lambda_features/managed_instances/src/using_tracer.py @@ -0,0 +1,28 @@ +from aws_lambda_powertools import Logger, Metrics, Tracer +from aws_lambda_powertools.metrics import MetricUnit +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +metrics = Metrics() + + +@tracer.capture_lambda_handler +@metrics.log_metrics +@logger.inject_lambda_context +def lambda_handler(event: dict, context: LambdaContext) -> dict: + order_id = event.get("order_id", "unknown") + logger.append_keys(order_id=order_id) + + result = process_order(order_id) + + # Metrics are flushed per request + metrics.add_metric(name="OrderProcessed", unit=MetricUnit.Count, value=1) + + return {"statusCode": 200, "body": result} + + +@tracer.capture_method +def process_order(order_id: str) -> str: + logger.info("Processing order") + return f"Processed order {order_id}" diff --git a/examples/logger/sam/template.yaml b/examples/logger/sam/template.yaml index 9fc1ee20500..092fb4166d3 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:30 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33 Resources: LoggerLambdaHandlerExample: diff --git a/examples/metrics/sam/template.yaml b/examples/metrics/sam/template.yaml index 0e080111506..dc7d0258fee 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:30 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33 Resources: CaptureLambdaHandlerExample: diff --git a/examples/metrics_datadog/sam/template.yaml b/examples/metrics_datadog/sam/template.yaml index cc89c955374..d43669484f8 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:30 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33 # 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/tracer/sam/template.yaml b/examples/tracer/sam/template.yaml index 67a7e7ead41..465ac3e2507 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:30 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:33 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/layer_constructors/layer_stack.py b/layer_v3/layer_constructors/layer_stack.py index a718fba5e9a..66e03b82218 100644 --- a/layer_v3/layer_constructors/layer_stack.py +++ b/layer_v3/layer_constructors/layer_stack.py @@ -1,5 +1,6 @@ from __future__ import annotations +# trigger CI: validate docker/setup-buildx-action v4 compatibility from pathlib import Path from typing import TYPE_CHECKING diff --git a/layer_v3/poetry.lock b/layer_v3/poetry.lock index 7ab9c82dc68..79d0e9ccd06 100644 --- a/layer_v3/poetry.lock +++ b/layer_v3/poetry.lock @@ -305,28 +305,44 @@ files = [ {file = "publication-0.0.3.tar.gz", hash = "sha256:68416a0de76dddcdd2930d1c8ef853a743cc96c82416c4e4d3b5d901c6276dc4"}, ] +[[package]] +name = "pygments" +version = "2.20.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pytest" -version = "7.4.4" +version = "9.0.3" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, + {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, + {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, ] [package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] name = "python-dateutil" @@ -460,14 +476,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] @@ -479,4 +495,4 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "3dc6a5fee955e5a822626957f89f37c55b1e0adc6fbf61267357edb26a944d7c" +content-hash = "55e0062a4839c65fd80947421f94e712530dda1c9dd1aed9954d75a73b882fd9" diff --git a/layer_v3/pyproject.toml b/layer_v3/pyproject.toml index 5dffa424144..fd67c31419d 100644 --- a/layer_v3/pyproject.toml +++ b/layer_v3/pyproject.toml @@ -11,7 +11,7 @@ python = "^3.10" aws-cdk-lib = "^2.223.0" [tool.poetry.group.dev.dependencies] -pytest = "^7.1.2" +pytest = ">=7.1.2,<10.0.0" boto3 = "^1.24.46" urllib3 = ">=1.25.4,!=2.2.0,<3" diff --git a/mkdocs.yml b/mkdocs.yml index cc51089fab0..6bea356bac6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,6 +21,7 @@ nav: - Datadog: core/metrics/datadog.md - Event Handler: - core/event_handler/api_gateway.md + - core/event_handler/openapi.md - core/event_handler/appsync.md - core/event_handler/appsync_events.md - core/event_handler/bedrock_agents.md @@ -39,6 +40,10 @@ nav: - utilities/middleware_factory.md - utilities/jmespath_functions.md - CloudFormation Custom Resources: https://github.com/aws-cloudformation/custom-resource-helper" target="_blank + - Lambda Features: + - lambda-features/index.md + - lambda-features/managed-instances.md + - lambda-features/durable-functions.md - Build recipes: - build_recipes/index.md - Getting started: build_recipes/getting-started.md diff --git a/package-lock.json b/package-lock.json index dfaf20a09cb..3ef587c3ca1 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.1111.0" + "aws-cdk": "^2.1124.1" } }, "node_modules/aws-cdk": { - "version": "2.1111.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1111.0.tgz", - "integrity": "sha512-69AVF04cxbAhYzmeJYtUF5bs6ruNnH05EQZJfjadk5Lpg+HVaJY2NjG/2qgsMmKrfRgvLet73Ir8ehVlAaaGng==", + "version": "2.1124.1", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1124.1.tgz", + "integrity": "sha512-sRYdPMdkX+02EHaT946AFV0w0CMfbHKWpLZPv525xTCkaVu1eYu6DzHFuTdimxdSN0uGQ2D4LHrD1sr94tRhow==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 3bccdebc669..e862df39fb7 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.1111.0" + "aws-cdk": "^2.1124.1" } } 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 242e8d71083..fdc75dcbcac 100644 --- a/poetry.lock +++ b/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 = "annotated-types" @@ -49,6 +49,49 @@ files = [ [package.extras] test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] +[[package]] +name = "ast-serialize" +version = "0.5.0" +description = "Python bindings for mypy AST serialization" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c"}, + {file = "ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb"}, + {file = "ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101"}, + {file = "ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a"}, + {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211"}, + {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf"}, + {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9"}, + {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee"}, + {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809"}, + {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43"}, + {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934"}, + {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759"}, + {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887"}, + {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27"}, + {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d"}, + {file = "ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a"}, + {file = "ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590"}, + {file = "ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642"}, + {file = "ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6"}, +] + [[package]] name = "async-timeout" version = "5.0.1" @@ -86,6 +129,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,18 +137,18 @@ zstandard = ["zstandard"] [[package]] name = "aws-cdk-asset-awscli-v1" -version = "2.2.263" +version = "2.2.273" description = "A library that contains the AWS CLI for use in Lambda Layers" optional = false python-versions = "~=3.9" groups = ["dev"] files = [ - {file = "aws_cdk_asset_awscli_v1-2.2.263-py3-none-any.whl", hash = "sha256:185150757d4216ea982d7b35596de5b3d767776be00cd78cacce02eaa08f8851"}, - {file = "aws_cdk_asset_awscli_v1-2.2.263.tar.gz", hash = "sha256:657605260ace055fac4ae30a6fb84a80504a6e24ce0b1d278913d4db4bafa266"}, + {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.125.0,<2.0.0" +jsii = ">=1.127.0,<2.0.0" publication = ">=0.0.3" typeguard = "2.13.3" @@ -157,7 +201,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 +221,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,18 +249,18 @@ typeguard = ">=2.13.3,<2.14.0" [[package]] name = "aws-cdk-aws-lambda-python-alpha" -version = "2.243.0a0" +version = "2.251.0a0" description = "The CDK Construct Library for AWS Lambda in Python" optional = false python-versions = "~=3.9" groups = ["dev"] files = [ - {file = "aws_cdk_aws_lambda_python_alpha-2.243.0a0-py3-none-any.whl", hash = "sha256:96f9d7281dd84f1bff3cc8b8e0d403589f957845c6ba2a34a570dd8d7b776e0e"}, - {file = "aws_cdk_aws_lambda_python_alpha-2.243.0a0.tar.gz", hash = "sha256:d97718daedab795f9652ab7799ac3d206a6dd33efa5e622b60c8fa5cc6a526d2"}, + {file = "aws_cdk_aws_lambda_python_alpha-2.251.0a0-py3-none-any.whl", hash = "sha256:c5780e06890582166932269ef594f4f2050961c2a1431429821ea45ea7a338fc"}, + {file = "aws_cdk_aws_lambda_python_alpha-2.251.0a0.tar.gz", hash = "sha256:04f2b8edb36cb2eb3494169100d3eaad54f7acb577bd870a5343f6d95a5480da"}, ] [package.dependencies] -aws-cdk-lib = ">=2.243.0,<3.0.0" +aws-cdk-lib = ">=2.251.0,<3.0.0" constructs = ">=10.5.0,<11.0.0" jsii = ">=1.127.0,<2.0.0" publication = ">=0.0.3" @@ -224,53 +268,53 @@ typeguard = "2.13.3" [[package]] name = "aws-cdk-cloud-assembly-schema" -version = "52.2.0" +version = "53.24.0" description = "Schema for the protocol between CDK framework and CDK CLI" optional = false python-versions = "~=3.9" groups = ["dev"] files = [ - {file = "aws_cdk_cloud_assembly_schema-52.2.0-py3-none-any.whl", hash = "sha256:87b918589f7d627f45e330726f592ec0b39056e1403558f3a1ba8a2134dacd6f"}, - {file = "aws_cdk_cloud_assembly_schema-52.2.0.tar.gz", hash = "sha256:737309e2c7c7e4b46bd669cb9fe8799a36424c9a174523b54833cf1cd12b5e3f"}, + {file = "aws_cdk_cloud_assembly_schema-53.24.0-py3-none-any.whl", hash = "sha256:360c4804f3073601ac320d1773432bc45b34201d7c8fb85aff7ed536801efb0c"}, + {file = "aws_cdk_cloud_assembly_schema-53.24.0.tar.gz", hash = "sha256:f999f4c777deaca6631c61993bf5583022ff57c3a94a65930e2fc6b68cb7c407"}, ] [package.dependencies] -jsii = ">=1.121.0,<2.0.0" +jsii = ">=1.129.0,<2.0.0" publication = ">=0.0.3" typeguard = "2.13.3" [[package]] name = "aws-cdk-lib" -version = "2.243.0" +version = "2.254.0" description = "Version 2 of the AWS Cloud Development Kit library" optional = false python-versions = "~=3.9" groups = ["dev"] files = [ - {file = "aws_cdk_lib-2.243.0-py3-none-any.whl", hash = "sha256:9de49529fbca82680c7e4fa28040bf3895efd4250a32c812d31133c2274708b5"}, - {file = "aws_cdk_lib-2.243.0.tar.gz", hash = "sha256:ae39b90d0eef422293d22c6f2c67c5e1edfbfce7a5135a463f29e7c23538318c"}, + {file = "aws_cdk_lib-2.254.0-py3-none-any.whl", hash = "sha256:626095eaa742e0b9d894c3a1bf06a6e7d1245a986165a72408871d41886c6d07"}, + {file = "aws_cdk_lib-2.254.0.tar.gz", hash = "sha256:ddbef134cad91f8985444f77f052f0337af5f132101ad7aea2215b4775ff7828"}, ] [package.dependencies] -"aws-cdk.asset-awscli-v1" = "2.2.263" +"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" = ">=52.1.0,<53.0.0" +"aws-cdk.cloud-assembly-schema" = ">=53.21.0,<54.0.0" constructs = ">=10.5.0,<11.0.0" -jsii = ">=1.127.0,<2.0.0" +jsii = ">=1.129.0,<2.0.0" publication = ">=0.0.3" typeguard = "2.13.3" [[package]] name = "aws-encryption-sdk" -version = "4.0.4" +version = "4.0.5" 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.5-py2.py3-none-any.whl", hash = "sha256:3e6b76afb94c28730487dee71fa1bfc217fcdbab061733c220ff87b88630e40e"}, + {file = "aws_encryption_sdk-4.0.5.tar.gz", hash = "sha256:a36136181a4d63cbf0d7347d29786c80a4d74a07c79c88a8799e27b27a9c3fc1"}, ] [package.dependencies] @@ -299,14 +343,14 @@ requests = ">=0.14.0" [[package]] name = "aws-sam-translator" -version = "1.107.0" +version = "1.108.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" groups = ["dev"] files = [ - {file = "aws_sam_translator-1.107.0-py3-none-any.whl", hash = "sha256:95b2a03a87fb61d9a9e9e431a18e6221c4780b32792eed4b239e72ef7366d63b"}, - {file = "aws_sam_translator-1.107.0.tar.gz", hash = "sha256:e6462c85309a4cabcc9559edf12f164c67a74a1208feb6350ab8aa1b620c9365"}, + {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"}, ] [package.dependencies] @@ -316,7 +360,7 @@ pydantic = {version = ">=2.12.5,<2.13.0", markers = "python_version >= \"3.9\""} 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)"] +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)"] [[package]] name = "aws-xray-sdk" @@ -442,6 +486,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,453 +498,460 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "boto3-stubs" -version = "1.42.74" -description = "Type annotations for boto3 1.42.74 generated with mypy-boto3-builder 8.12.0" +version = "1.43.3" +description = "Type annotations for boto3 1.43.3 generated with mypy-boto3-builder 8.12.0" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "boto3_stubs-1.42.74-py3-none-any.whl", hash = "sha256:63b7ba180b3fe361dcae0a50dd57e1ac676149cf0c90be420fa067189bafa7c6"}, - {file = "boto3_stubs-1.42.74.tar.gz", hash = "sha256:781078235e61c78000035ece0a92befaaf846762b6a91becf6b2887331fd010d"}, + {file = "boto3_stubs-1.43.3-py3-none-any.whl", hash = "sha256:dd43fb68fe1d6db450588609a96013a9baf86d1cfc45bbbee7fd97bac971a3c0"}, + {file = "boto3_stubs-1.43.3.tar.gz", hash = "sha256:1c17fb4003c8d3ac324385f1d7a366721436eab3b6dc7838239dea53137ba9de"}, ] [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-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-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-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-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.74)"] -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-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)"] -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)"] -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)"] -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-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-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-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.3)"] +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)"] +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)"] +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)"] +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" @@ -912,6 +964,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" @@ -981,40 +1034,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.317" 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.317-py3-none-any.whl", hash = "sha256:86359377bbb56c946a460b0b7244ad5f9b8be3ab290201ad2e197fc7a6628dfb"}, + {file = "cdklabs_generative_ai_cdk_constructs-0.1.317.tar.gz", hash = "sha256:e04ed66168736f65cc4d13449a719dbb8d84c7ffb054d22a034865918315d157"}, ] [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" @@ -1029,6 +1082,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" @@ -1130,18 +1184,18 @@ pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} [[package]] name = "cfn-lint" -version = "1.46.0" +version = "1.48.1" description = "Checks CloudFormation templates for practices and behaviour that could potentially be improved" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "cfn_lint-1.46.0-py3-none-any.whl", hash = "sha256:1dfca1993af3159411e4a4f79466617ccdca48eddaf424e216297167c500aa3b"}, - {file = "cfn_lint-1.46.0.tar.gz", hash = "sha256:fa7cb76db683109133241baf1e1734b1d61b46d3900ba2a309db8f3d0e5d3994"}, + {file = "cfn_lint-1.48.1-py3-none-any.whl", hash = "sha256:73785acd4fcb71ed68183589f7dcf9e46fd0ad9fb01f704243cb07993490a40e"}, + {file = "cfn_lint-1.48.1.tar.gz", hash = "sha256:1855dce6b97528ff532e3f5a3aa5b659f40e51c338192a0ba82c1af88882b6f7"}, ] [package.dependencies] -aws-sam-translator = ">=1.97.0" +aws-sam-translator = ">=1.108.0" jsonpatch = "*" networkx = ">=2.4,<4" pyyaml = ">5.4" @@ -1277,6 +1331,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" @@ -1325,135 +1380,135 @@ 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.4" +version = "7.14.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415"}, - {file = "coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9"}, - {file = "coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf"}, - {file = "coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95"}, - {file = "coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053"}, - {file = "coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9"}, - {file = "coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9"}, - {file = "coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f"}, - {file = "coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f"}, - {file = "coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459"}, - {file = "coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0"}, - {file = "coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246"}, - {file = "coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126"}, - {file = "coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d"}, - {file = "coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9"}, - {file = "coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a"}, - {file = "coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d"}, - {file = "coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd"}, - {file = "coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af"}, - {file = "coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d"}, - {file = "coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b"}, - {file = "coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9"}, - {file = "coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd"}, - {file = "coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997"}, - {file = "coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601"}, - {file = "coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0"}, - {file = "coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb"}, - {file = "coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505"}, - {file = "coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2"}, - {file = "coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056"}, - {file = "coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0"}, - {file = "coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea"}, - {file = "coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932"}, - {file = "coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b"}, - {file = "coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0"}, - {file = "coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91"}, + {file = "coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075"}, + {file = "coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85"}, + {file = "coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323"}, + {file = "coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a"}, + {file = "coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480"}, + {file = "coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c"}, + {file = "coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3"}, + {file = "coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1"}, + {file = "coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627"}, + {file = "coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5"}, + {file = "coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595"}, + {file = "coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27"}, + {file = "coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2"}, + {file = "coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d"}, + {file = "coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef"}, + {file = "coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe"}, + {file = "coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae"}, + {file = "coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e"}, + {file = "coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96"}, + {file = "coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90"}, + {file = "coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477"}, + {file = "coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab"}, + {file = "coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917"}, + {file = "coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8"}, + {file = "coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d"}, + {file = "coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca"}, + {file = "coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828"}, + {file = "coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d"}, + {file = "coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9"}, + {file = "coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1"}, + {file = "coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db"}, + {file = "coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2"}, + {file = "coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644"}, + {file = "coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b"}, + {file = "coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1"}, + {file = "coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74"}, ] [package.dependencies] @@ -1464,61 +1519,61 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" -version = "46.0.5" +version = "46.0.7" 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" groups = ["main", "dev"] files = [ - {file = "cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731"}, - {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82"}, - {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1"}, - {file = "cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48"}, - {file = "cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4"}, - {file = "cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663"}, - {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826"}, - {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d"}, - {file = "cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a"}, - {file = "cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4"}, - {file = "cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c"}, - {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4"}, - {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9"}, - {file = "cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72"}, - {file = "cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7"}, - {file = "cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d"}, + {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"}, ] markers = {main = "extra == \"all\" or extra == \"datamasking\""} @@ -1533,7 +1588,7 @@ 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.5)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +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]] @@ -1662,11 +1717,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" @@ -1841,14 +1896,14 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "filelock" -version = "3.25.2" +version = "3.29.0" 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.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258"}, + {file = "filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90"}, ] [[package]] @@ -1886,21 +1941,21 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.46" +version = "3.1.50" 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.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9"}, + {file = "gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] +doc = ["sphinx (>=7.4.7,<8)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy (==1.18.2) ; python_version >= \"3.9\"", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] [[package]] @@ -2047,18 +2102,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 == \"datadog\" or extra == \"valkey\""} [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" @@ -2224,19 +2280,18 @@ files = [ [[package]] name = "isort" -version = "7.0.0" +version = "8.0.1" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.10.0" groups = ["dev"] files = [ - {file = "isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1"}, - {file = "isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187"}, + {file = "isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75"}, + {file = "isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d"}, ] [package.extras] colors = ["colorama"] -plugins = ["setuptools"] [[package]] name = "jinja2" @@ -2270,14 +2325,14 @@ files = [ [[package]] name = "jsii" -version = "1.127.0" +version = "1.130.0" description = "Python client for jsii runtime" optional = false python-versions = "~=3.9" 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.130.0-py3-none-any.whl", hash = "sha256:ce50e11ea588fe6b2d0766d90edaf4c78b9e97e2e1f075fbd8bc29349c6503c8"}, + {file = "jsii-1.130.0.tar.gz", hash = "sha256:7436ae382e2de27970b34a4ccfef953a45980c5070241c1bd610bf3af68a2d6b"}, ] [package.dependencies] @@ -2343,7 +2398,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 +2423,103 @@ referencing = ">=0.31.0" [[package]] name = "librt" -version = "0.8.1" +version = "0.11.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.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f"}, + {file = "librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45"}, + {file = "librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c"}, + {file = "librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33"}, + {file = "librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884"}, + {file = "librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280"}, + {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c"}, + {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb"}, + {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783"}, + {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0"}, + {file = "librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89"}, + {file = "librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4"}, + {file = "librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29"}, + {file = "librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9"}, + {file = "librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5"}, + {file = "librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b"}, + {file = "librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89"}, + {file = "librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc"}, + {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5"}, + {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7"}, + {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d"}, + {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412"}, + {file = "librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d"}, + {file = "librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73"}, + {file = "librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c"}, + {file = "librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46"}, + {file = "librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3"}, + {file = "librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67"}, + {file = "librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a"}, + {file = "librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a"}, + {file = "librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f"}, + {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b"}, + {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766"}, + {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d"}, + {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8"}, + {file = "librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a"}, + {file = "librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9"}, + {file = "librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c"}, + {file = "librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894"}, + {file = "librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c"}, + {file = "librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea"}, + {file = "librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230"}, + {file = "librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2"}, + {file = "librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3"}, + {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21"}, + {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930"}, + {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be"}, + {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e"}, + {file = "librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e"}, + {file = "librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47"}, + {file = "librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44"}, + {file = "librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd"}, + {file = "librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4"}, + {file = "librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8"}, + {file = "librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b"}, + {file = "librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175"}, + {file = "librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03"}, + {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c"}, + {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3"}, + {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96"}, + {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe"}, + {file = "librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f"}, + {file = "librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7"}, + {file = "librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1"}, + {file = "librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72"}, + {file = "librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa"}, + {file = "librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548"}, + {file = "librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2"}, + {file = "librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f"}, + {file = "librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51"}, + {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2"}, + {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085"}, + {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3"}, + {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd"}, + {file = "librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8"}, + {file = "librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c"}, + {file = "librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253"}, + {file = "librt-0.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6bd72d903911d995ab666dbd1871f8b1e80925a699af8063fbf50053329fb05f"}, + {file = "librt-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ef69ac715f3cd8e5cd252cb2aebfa72c015492aacc339d5d7bf8fef3c62c677"}, + {file = "librt-0.11.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:624a40c4a4ad7773315c287276cd024509b2c66ff5904f504bfc08d2c70293ab"}, + {file = "librt-0.11.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:41dc19fe150b69716c8ece4f76773a9e8813fe3e35e032a58b4d46423fb8d7c0"}, + {file = "librt-0.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e8bd98ea9c47ae90b319a087ab28dac493f1ffbc1ecd1f28fcdbf3b7e1108d1"}, + {file = "librt-0.11.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84308fc49423ce6475d1c5d1985cd69a8ca9f0325fc7d5f81bb690a3f3625d4e"}, + {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ff0fbaf5f44a21beeb0110f2ab64f45135a9536a834b79c0d1ef018f2786bbfa"}, + {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9c028a9442a18e266955d364ce42259136e79a7ba14d773e0d778d5f70cd56f1"}, + {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9f1692105a02bcf853f355032a5fdc5494358ef83d8fd22d16de375c85cec3f5"}, + {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7a80a71e1fda83cc752a9141e87aae7fef279538597564d670e9ce513f286192"}, + {file = "librt-0.11.0-cp39-cp39-win32.whl", hash = "sha256:140695816ddf3c86eb972981a26f35efd871c44b0c3aed44c8cd01749386617f"}, + {file = "librt-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:92f7ff819c197fc30473190a12c2856f325ac90aabfccbeb2072d28cc2e234e3"}, + {file = "librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1"}, ] [[package]] @@ -2780,14 +2835,14 @@ mdformat = ">=0.7.21" [[package]] name = "mkdocs-material" -version = "9.7.5" +version = "9.7.6" description = "Documentation that simply works" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "mkdocs_material-9.7.5-py3-none-any.whl", hash = "sha256:7cf9df2ff121fd098ff6e05c732b0be3699afca9642e2dfe4926c40eb5873eec"}, - {file = "mkdocs_material-9.7.5.tar.gz", hash = "sha256:f76bdab532bad1d9c57ca7187b37eccf64dd12e1586909307f8856db3be384ea"}, + {file = "mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba"}, + {file = "mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69"}, ] [package.dependencies] @@ -2912,58 +2967,68 @@ dill = ">=0.4.1" [[package]] name = "mypy" -version = "1.19.1" +version = "2.1.0" description = "Optional static typing for Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, - {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, - {file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"}, - {file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"}, - {file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"}, - {file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"}, - {file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"}, - {file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"}, - {file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"}, - {file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"}, - {file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"}, - {file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"}, - {file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"}, - {file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"}, - {file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"}, - {file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"}, - {file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"}, - {file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"}, - {file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"}, - {file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"}, - {file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"}, - {file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"}, - {file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"}, - {file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"}, - {file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"}, - {file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"}, - {file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"}, - {file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"}, - {file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"}, - {file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"}, - {file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"}, - {file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"}, - {file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"}, - {file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"}, - {file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"}, - {file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"}, - {file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"}, - {file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"}, + {file = "mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a6beb180257a805961aea9ec591bbd0bd17f1e18d35b8456d57aee5bedfedc"}, + {file = "mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ef78c1d306bbf9a8a12f526c44902c9c28dffd6c52c52bf6a72641ce18d3849"}, + {file = "mypy-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c209a90853081ff01d01ee895cafe10f7db1474e0d95beaeef0f6c1db9119bbd"}, + {file = "mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47cebf61abde7c088a4e27718a8b13a81655686b2e9c251f5c0915a802248166"}, + {file = "mypy-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d57a90ae5e872138a425ec328edbc9b235d1934c4377881a33ec05b341acc9a8"}, + {file = "mypy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aea7f7a8a55b459c34275fc468ada6ca7c173a5e43a68f5dbe588a563d8a06b8"}, + {file = "mypy-2.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c989640253f0d76843e9c6c1bbf4bd48c5e85ada61bde4beb37cb3eca035685e"}, + {file = "mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41"}, + {file = "mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca"}, + {file = "mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538"}, + {file = "mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398"}, + {file = "mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563"}, + {file = "mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389"}, + {file = "mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666"}, + {file = "mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af"}, + {file = "mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6"}, + {file = "mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211"}, + {file = "mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b"}, + {file = "mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22"}, + {file = "mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b"}, + {file = "mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8"}, + {file = "mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5"}, + {file = "mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e"}, + {file = "mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e"}, + {file = "mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285"}, + {file = "mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5"}, + {file = "mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65"}, + {file = "mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d"}, + {file = "mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2"}, + {file = "mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f"}, + {file = "mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4"}, + {file = "mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef"}, + {file = "mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135"}, + {file = "mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21"}, + {file = "mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57"}, + {file = "mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e"}, + {file = "mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780"}, + {file = "mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd"}, + {file = "mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08"}, + {file = "mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081"}, + {file = "mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7"}, + {file = "mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6"}, + {file = "mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289"}, + {file = "mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633"}, ] [package.dependencies] -librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""} +ast-serialize = ">=0.3.0,<1.0.0" +librt = {version = ">=0.11.0", markers = "platform_python_implementation != \"PyPy\""} mypy_extensions = ">=1.0.0" -pathspec = ">=0.9.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)"] @@ -2974,14 +3039,14 @@ 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] @@ -2989,14 +3054,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] @@ -3004,14 +3069,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] @@ -3019,14 +3084,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] @@ -3034,14 +3099,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] @@ -3049,14 +3114,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] @@ -3064,14 +3129,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] @@ -3079,14 +3144,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] @@ -3094,14 +3159,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] @@ -3109,14 +3174,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] @@ -3124,14 +3189,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] @@ -3177,7 +3242,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"}, @@ -3201,7 +3266,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"}, @@ -3220,14 +3285,14 @@ test-extras = ["pytest-mpl", "pytest-randomly"] [[package]] name = "nox" -version = "2026.2.9" +version = "2026.4.10" description = "Flexible test automation." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "nox-2026.2.9-py3-none-any.whl", hash = "sha256:1b7143bc8ecdf25f2353201326152c5303ae4ae56ca097b1fb6179ad75164c47"}, - {file = "nox-2026.2.9.tar.gz", hash = "sha256:1bc8a202ee8cd69be7aaada63b2a7019126899a06fc930a7aee75585bf8ee41b"}, + {file = "nox-2026.4.10-py3-none-any.whl", hash = "sha256:082c117627590d9b90aa21f86df89b310b07c5842539524203bcb3c719f116c1"}, + {file = "nox-2026.4.10.tar.gz", hash = "sha256:2d0af5374f3f37a295428c927d1b04a8182aa01762897d172446dda2f1ce9692"}, ] [package.dependencies] @@ -3242,7 +3307,7 @@ virtualenv = {version = ">=20.15", markers = "python_version >= \"3.10\""} [package.extras] pbs = ["pbs-installer[all] (>=2025.1.6)"] -tox-to-nox = ["importlib-resources ; python_version < \"3.9\"", "jinja2", "tox (>=4)"] +tox-to-nox = ["jinja2", "tox (>=4)"] uv = ["uv (>=0.1.6)"] [[package]] @@ -3353,6 +3418,7 @@ files = [ {file = "protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11"}, {file = "protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280"}, ] +markers = {main = "extra == \"kafka-consumer-protobuf\" or extra == \"valkey\""} [[package]] name = "publication" @@ -3551,15 +3617,15 @@ typing-extensions = ">=4.14.1" [[package]] name = "pydantic-settings" -version = "2.13.1" +version = "2.14.1" 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.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de"}, + {file = "pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa"}, ] [package.dependencies] @@ -3568,7 +3634,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)"] @@ -3576,14 +3642,14 @@ yaml = ["pyyaml (>=6.0.1)"] [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, ] [package.extras] @@ -3591,14 +3657,14 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymdown-extensions" -version = "10.21" +version = "10.21.3" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f"}, - {file = "pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5"}, + {file = "pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6"}, + {file = "pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354"}, ] [package.dependencies] @@ -3610,14 +3676,14 @@ extra = ["pygments (>=2.19.1)"] [[package]] name = "pytest" -version = "9.0.2" +version = "9.0.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"}, - {file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"}, + {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, + {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, ] [package.dependencies] @@ -3676,14 +3742,14 @@ histogram = ["pygal", "pygaljs", "setuptools"] [[package]] name = "pytest-cov" -version = "7.0.0" +version = "7.1.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, - {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, + {file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"}, + {file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"}, ] [package.dependencies] @@ -3714,18 +3780,18 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "pytest-socket" -version = "0.7.0" +version = "0.8.0" 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.0-py3-none-any.whl", hash = "sha256:81821ba59f07d7600fe2b551d8714f40b068bd46e8b6704c48664e9d60cdacb8"}, + {file = "pytest_socket-0.8.0.tar.gz", hash = "sha256:af9bb5f487da72be63573a6194cfac033b6c7a1c1561e150521105970f9e99f2"}, ] [package.dependencies] -pytest = ">=6.2.5" +pytest = ">=7.0.0" [[package]] name = "pytest-xdist" @@ -3759,6 +3825,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" @@ -3949,14 +4016,14 @@ toml = ["tomli (>=2.0.1)"] [[package]] name = "redis" -version = "7.3.0" +version = "7.4.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-7.4.0-py3-none-any.whl", hash = "sha256:a9c74a5c893a5ef8455a5adb793a31bb70feb821c86eccb62eebef5a19c429ec"}, + {file = "redis-7.4.0.tar.gz", hash = "sha256:64a6ea7bf567ad43c964d2c30d82853f8df927c5c9017766c55a1d1ed95d18ad"}, ] markers = {main = "extra == \"redis\""} @@ -4114,25 +4181,26 @@ files = [ [[package]] name = "requests" -version = "2.32.5" +version = "2.33.1" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, + {file = "requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a"}, + {file = "requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517"}, ] +markers = {main = "extra == \"datadog\""} [package.dependencies] -certifi = ">=2017.4.17" +certifi = ">=2023.5.7" charset_normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" +urllib3 = ">=1.26,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] name = "retry2" @@ -4294,31 +4362,30 @@ files = [ [[package]] name = "ruff" -version = "0.14.14" +version = "0.15.14" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed"}, - {file = "ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c"}, - {file = "ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66"}, - {file = "ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13"}, - {file = "ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412"}, - {file = "ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3"}, - {file = "ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b"}, - {file = "ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167"}, - {file = "ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd"}, - {file = "ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c"}, - {file = "ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b"}, + {file = "ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108"}, + {file = "ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b"}, + {file = "ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f"}, + {file = "ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d"}, + {file = "ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4"}, + {file = "ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542"}, + {file = "ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f"}, + {file = "ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf"}, + {file = "ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba"}, + {file = "ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f"}, + {file = "ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581"}, + {file = "ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93"}, + {file = "ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61"}, + {file = "ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553"}, + {file = "ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6"}, + {file = "ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902"}, + {file = "ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826"}, + {file = "ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f"}, ] [[package]] @@ -4332,12 +4399,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" @@ -4357,14 +4425,14 @@ pathspec = ">=0.10.1" [[package]] name = "sentry-sdk" -version = "2.54.0" +version = "2.57.0" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" groups = ["dev"] files = [ - {file = "sentry_sdk-2.54.0-py2.py3-none-any.whl", hash = "sha256:fd74e0e281dcda63afff095d23ebcd6e97006102cdc8e78a29f19ecdf796a0de"}, - {file = "sentry_sdk-2.54.0.tar.gz", hash = "sha256:2620c2575128d009b11b20f7feb81e4e4e8ae08ec1d36cbc845705060b45cc1b"}, + {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"}, ] [package.dependencies] @@ -4375,6 +4443,7 @@ urllib3 = ">=1.26.11" aiohttp = ["aiohttp (>=3.5)"] anthropic = ["anthropic (>=0.16)"] arq = ["arq (>=0.23)"] +asyncio = ["httpcore[asyncio] (==1.*)"] asyncpg = ["asyncpg (>=0.23)"] beam = ["apache-beam (>=2.12)"] bottle = ["bottle (>=0.12.13)"] @@ -4395,7 +4464,7 @@ huggingface-hub = ["huggingface_hub (>=0.22)"] langchain = ["langchain (>=0.0.210)"] langgraph = ["langgraph (>=0.6.6)"] launchdarkly = ["launchdarkly-server-sdk (>=9.8.0)"] -litellm = ["litellm (>=1.77.5)"] +litellm = ["litellm (>=1.77.5,!=1.82.7,!=1.82.8)"] litestar = ["litestar (>=2.0.0)"] loguru = ["loguru (>=0.5)"] mcp = ["mcp (>=1.15.0)"] @@ -4429,6 +4498,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" @@ -4499,58 +4569,59 @@ dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"] [[package]] name = "testcontainers" -version = "4.14.1" +version = "4.14.2" 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.1-py3-none-any.whl", hash = "sha256:03dfef4797b31c82e7b762a454b6afec61a2a512ad54af47ab41e4fa5415f891"}, - {file = "testcontainers-4.14.1.tar.gz", hash = "sha256:316f1bb178d829c003acd650233e3ff3c59a833a08d8661c074f58a4fbd42a64"}, + {file = "testcontainers-4.14.2-py3-none-any.whl", hash = "sha256:0d0522c3cd8f8d9627cda41f7a6b51b639fa57bdc492923c045117933c668d68"}, + {file = "testcontainers-4.14.2.tar.gz", hash = "sha256:1340ccf16fe3acd9389a6c9e1d9ab21d9fe99a8afdf8165f89c3e69c1967d239"}, ] [package.dependencies] docker = "*" python-dotenv = "*" -redis = {version = ">=7,<8", optional = true, markers = "extra == \"generic\" or extra == \"redis\""} +redis = {version = ">=7", optional = true, markers = "extra == \"redis\""} typing-extensions = "*" urllib3 = "*" wrapt = "*" [package.extras] -arangodb = ["python-arango (>=8,<9)"] -aws = ["boto3 (>=1,<2)", "httpx"] -azurite = ["azure-storage-blob (>=12,<13)"] -chroma = ["chromadb-client (>=1,<2)"] -cosmosdb = ["azure-cosmos (>=4,<5)"] -db2 = ["ibm_db_sa ; platform_machine != \"aarch64\" and platform_machine != \"arm64\"", "sqlalchemy (>=2,<3)"] -generic = ["httpx", "redis (>=7,<8)"] -google = ["google-cloud-datastore (>=2,<3)", "google-cloud-pubsub (>=2,<3)"] -influxdb = ["influxdb (>=5,<6)", "influxdb-client (>=1,<2)"] +arangodb = ["python-arango (>=8)"] +aws = ["boto3 (>=1)", "httpx"] +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)"] +google = ["google-cloud-datastore (>=2)", "google-cloud-pubsub (>=2)"] +influxdb = ["influxdb (>=5)", "influxdb-client (>=1)"] k3s = ["kubernetes", "pyyaml (>=6.0.3)"] -keycloak = ["python-keycloak (>=6,<7) ; python_version < \"4.0\""] -localstack = ["boto3 (>=1,<2)"] +keycloak = ["python-keycloak (>=6) ; python_version < \"4.0\""] +localstack = ["boto3 (>=1)"] mailpit = ["cryptography"] -minio = ["minio (>=7,<8)"] -mongodb = ["pymongo (>=4,<5)"] -mssql = ["pymssql (>=2,<3)", "sqlalchemy (>=2,<3)"] -mysql = ["pymysql[rsa] (>=1,<2)", "sqlalchemy (>=2,<3)"] -nats = ["nats-py (>=2,<3)"] -neo4j = ["neo4j (>=6,<7)"] +minio = ["minio (>=7)"] +mongodb = ["pymongo (>=4)"] +mssql = ["pymssql (>=2)", "sqlalchemy (>=2)"] +mysql = ["pymysql[rsa] (>=1)", "sqlalchemy (>=2)"] +nats = ["nats-py (>=2)"] +neo4j = ["neo4j (>=6)"] openfga = ["openfga-sdk"] -opensearch = ["opensearch-py (>=3,<4) ; python_version < \"4.0\""] -oracle = ["oracledb (>=3,<4)", "sqlalchemy (>=2,<3)"] -oracle-free = ["oracledb (>=3,<4)", "sqlalchemy (>=2,<3)"] -qdrant = ["qdrant-client (>=1,<2)"] -rabbitmq = ["pika (>=1,<2)"] -redis = ["redis (>=7,<8)"] -registry = ["bcrypt (>=5,<6)"] -scylla = ["cassandra-driver (>=3,<4)"] -selenium = ["selenium (>=4,<5)"] +opensearch = ["opensearch-py (>=3) ; python_version < \"4.0\""] +oracle = ["oracledb (>=3)", "sqlalchemy (>=2)"] +oracle-free = ["oracledb (>=3)", "sqlalchemy (>=2)"] +qdrant = ["qdrant-client (>=1)"] +rabbitmq = ["pika (>=1)"] +redis = ["redis (>=7)"] +registry = ["bcrypt (>=5)"] +scylla = ["cassandra-driver (>=3)"] +selenium = ["selenium (>=4)"] sftp = ["cryptography"] test-module-import = ["httpx"] trino = ["trino"] -weaviate = ["weaviate-client (>=4,<5)"] +weaviate = ["weaviate-client (>=4)"] [[package]] name = "tomli" @@ -4612,29 +4683,30 @@ files = [ [[package]] name = "ty" -version = "0.0.23" +version = "0.0.35" description = "An extremely fast Python type checker, written in Rust." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "ty-0.0.23-py3-none-linux_armv6l.whl", hash = "sha256:e810eef1a5f1cfc0731a58af8d2f334906a96835829767aed00026f1334a8dd7"}, - {file = "ty-0.0.23-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e43d36bd89a151ddcad01acaeff7dcc507cb73ff164c1878d2d11549d39a061c"}, - {file = "ty-0.0.23-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bd6a340969577b4645f231572c4e46012acba2d10d4c0c6570fe1ab74e76ae00"}, - {file = "ty-0.0.23-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:341441783e626eeb7b1ec2160432956aed5734932ab2d1c26f94d0c98b229937"}, - {file = "ty-0.0.23-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ce1dc66c26d4167e2c78d12fa870ef5a7ec9cc344d2baaa6243297cfa88bd52"}, - {file = "ty-0.0.23-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bae1e7a294bf8528836f7617dc5c360ea2dddb63789fc9471ae6753534adca05"}, - {file = "ty-0.0.23-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2b162768764d9dc177c83fb497a51532bb67cbebe57b8fa0f2668436bf53f3c"}, - {file = "ty-0.0.23-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d28384e48ca03b34e4e2beee0e230c39bbfb68994bb44927fec61ef3642900da"}, - {file = "ty-0.0.23-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:559d9a299df793cb7a7902caed5eda8a720ff69164c31c979673e928f02251ee"}, - {file = "ty-0.0.23-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:32a7b8a14a98e1d20a9d8d2af23637ed7efdb297ac1fa2450b8e465d05b94482"}, - {file = "ty-0.0.23-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6f803b9b9cca87af793467973b9abdd4b83e6b96d9b5e749d662cff7ead70b6d"}, - {file = "ty-0.0.23-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4a0bf086ec8e2197b7ea7ebfcf4be36cb6a52b235f8be61647ef1b2d99d6ffd3"}, - {file = "ty-0.0.23-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:252539c3fcd7aeb9b8d5c14e2040682c3e1d7ff640906d63fd2c4ce35865a4ba"}, - {file = "ty-0.0.23-py3-none-win32.whl", hash = "sha256:51b591d19eef23bbc3807aef77d38fa1f003c354e1da908aa80ea2dca0993f77"}, - {file = "ty-0.0.23-py3-none-win_amd64.whl", hash = "sha256:1e137e955f05c501cfbb81dd2190c8fb7d01ec037c7e287024129c722a83c9ad"}, - {file = "ty-0.0.23-py3-none-win_arm64.whl", hash = "sha256:a0399bd13fd2cd6683fd0a2d59b9355155d46546d8203e152c556ddbdeb20842"}, - {file = "ty-0.0.23.tar.gz", hash = "sha256:5fb05db58f202af366f80ef70f806e48f5237807fe424ec787c9f289e3f3a4ef"}, + {file = "ty-0.0.35-py3-none-linux_armv6l.whl", hash = "sha256:85ae1e59b9fb0b40e9d84fe61b29653c5f2f5e78b487ece371a7a38c20c781cf"}, + {file = "ty-0.0.35-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:709dbb7af4fcadb1196863c00b8791bbbbcc9dacbe15a0ff17f0af82b35d415b"}, + {file = "ty-0.0.35-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2cb0877419ab0c8708b6925cb0c2800b263842bd3c425113f200538772f3a0cc"}, + {file = "ty-0.0.35-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7afbcfc61904b7e82e7fe1a1db832a40d8f01e69dee1775f6594e552980536c"}, + {file = "ty-0.0.35-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b61498cc3e4178031c079951257fbdb209a891b4feb10ad6c40f615a51846f41"}, + {file = "ty-0.0.35-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:573b1eacda349fc8dba0d767b41631c3a6f66412363127c5bf2b1b40a1d898d2"}, + {file = "ty-0.0.35-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7209746158d6393c1040aa64b3ca29622e212ea7d8bae22ba50dbcbb4f96f0a"}, + {file = "ty-0.0.35-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4466a1470aa4418d49a9aa45d9da7de42033addd0a2837c5b2b0eb71d3c2bcd3"}, + {file = "ty-0.0.35-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb44bb742d52c309dcaa6598bcf4d82eb4bf1241b9e4940461e522e30093fe8b"}, + {file = "ty-0.0.35-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:34b219250736c989b2670a03782c61315f523f3a2be37f1f90b1207e2212c188"}, + {file = "ty-0.0.35-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:88e2ac497decc0940ef1a07571dee8a746112a93a09cdc7f8bca0099752e2e05"}, + {file = "ty-0.0.35-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:02cae51b53e6ec17d5d827ff1a3a76fd119705b56a92156e04399eda6e911596"}, + {file = "ty-0.0.35-py3-none-musllinux_1_2_i686.whl", hash = "sha256:11871d730c9400d899ac0b9f3d660ed2e7e433377c8725549f8250a36a7f2620"}, + {file = "ty-0.0.35-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ad0a2f0530d0933dcc99ad36ac556c63e384ea72ab9a18d23ad2e2c9fd61c73"}, + {file = "ty-0.0.35-py3-none-win32.whl", hash = "sha256:0e25d63ec4ab116e7f6757e44d16ca9216bca679d19ecc36d119cf80faada61a"}, + {file = "ty-0.0.35-py3-none-win_amd64.whl", hash = "sha256:6a0a6d259f6f2f8f2f954c6f013d4e0b5eba68af6b353bf19a47d59ec254a3d5"}, + {file = "ty-0.0.35-py3-none-win_arm64.whl", hash = "sha256:619c52c0fb2aa21961a848a1995135ad3b6d0a9aa54da0194e60f679cc200e13"}, + {file = "ty-0.0.35.tar.gz", hash = "sha256:8375c240ab38138a19db07996c9808fb7a92047c1492e1ce587c2ef5112ad3a9"}, ] [[package]] @@ -4682,14 +4754,14 @@ types-setuptools = "*" [[package]] name = "types-protobuf" -version = "6.32.1.20260221" +version = "7.34.1.20260518" description = "Typing stubs for protobuf" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "types_protobuf-6.32.1.20260221-py3-none-any.whl", hash = "sha256:da7cdd947975964a93c30bfbcc2c6841ee646b318d3816b033adc2c4eb6448e4"}, - {file = "types_protobuf-6.32.1.20260221.tar.gz", hash = "sha256:6d5fb060a616bfb076cbb61b4b3c3969f5fc8bec5810f9a2f7e648ee5cbcbf6e"}, + {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]] @@ -4710,14 +4782,14 @@ types-cffi = "*" [[package]] name = "types-python-dateutil" -version = "2.9.0.20260305" +version = "2.9.0.20260508" description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "types_python_dateutil-2.9.0.20260305-py3-none-any.whl", hash = "sha256:a3be9ca444d38cadabd756cfbb29780d8b338ae2a3020e73c266a83cc3025dd7"}, - {file = "types_python_dateutil-2.9.0.20260305.tar.gz", hash = "sha256:389717c9f64d8f769f36d55a01873915b37e97e52ce21928198d210fbd393c8b"}, + {file = "types_python_dateutil-2.9.0.20260508-py3-none-any.whl", hash = "sha256:bfc6fd2d81aa86e5ac97206a64304f6bd247426eedbca9b98619bbc48c6a1c10"}, + {file = "types_python_dateutil-2.9.0.20260508.tar.gz", hash = "sha256:596a6d63d81f587bf04c8254fb78df9d2344e915ce67948d7400512e3a6206d5"}, ] [[package]] @@ -4738,14 +4810,14 @@ types-pyOpenSSL = "*" [[package]] name = "types-requests" -version = "2.32.4.20260107" +version = "2.33.0.20260518" description = "Typing stubs for requests" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d"}, - {file = "types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f"}, + {file = "types_requests-2.33.0.20260518-py3-none-any.whl", hash = "sha256:626d697d1adaaff76e2044dc8c5c051d8f21abc157bdfe204a75558076fe0bf0"}, + {file = "types_requests-2.33.0.20260518.tar.gz", hash = "sha256:df7bd3bfe0ca8402dfb841e7d9be714bb5578203283d66d7dc4ef69343449a5e"}, ] [package.dependencies] @@ -4805,104 +4877,103 @@ typing-extensions = ">=4.12.0" [[package]] name = "ujson" -version = "5.12.0" +version = "5.12.1" 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.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:71bdb5d10c6d7e710cfa78e743d9fb79a37c7c66fa916cd287bffbaa520f5abe"}, + {file = "ujson-5.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:558673c6c3a2309775683ca96d5f1e4cd99889f71b1ba5cb6be8aa37ae67f9e0"}, + {file = "ujson-5.12.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4b0c9f6a56aa94bb98b403e1f57a866f0b43abaa89757b24d4a4b3cd8643ced"}, + {file = "ujson-5.12.1-cp310-cp310-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:7bba5ab7965619db7d6f5503133b8e2d8bfce9bb6754224ca64d19261cc52f7c"}, + {file = "ujson-5.12.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:191d2077fd53441599a2efd3dcc205b9cc5f3a4d685a76e9f73f4b6c19aee0c9"}, + {file = "ujson-5.12.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d90d27953716ef206c42f166932b3dbb264dc638bbf32acae81b216ae35f566d"}, + {file = "ujson-5.12.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b6afa86c117b66034004ee83c5149c6dccf7cb88941f9d3a1640c7076577f2d4"}, + {file = "ujson-5.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9523d67d45334f9a1d62e423bd72be62b58d2289a50420ffffa9363763eab73f"}, + {file = "ujson-5.12.1-cp310-cp310-win32.whl", hash = "sha256:757f2026bef09d231d63a2250a2c7ad21ea1c9cb1ded6480659d202c4e2ef09e"}, + {file = "ujson-5.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:7e31afad20cd6837a5ac6965d95b44b0ff06e42a82b01a8d3dc606a07f0b7a2a"}, + {file = "ujson-5.12.1-cp310-cp310-win_arm64.whl", hash = "sha256:80f58ae2be100da0f525330ee274accd8892d1c125fea75076f60539d9a5f9cd"}, + {file = "ujson-5.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:26dcb43869057373048cbd2678293c5b0f962d5774cc76fc9488564a209bcbf2"}, + {file = "ujson-5.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bca3f04b2f590a8211acdc3ca06649b65a7ed1e999437dccf095310be9d3ba4e"}, + {file = "ujson-5.12.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29d1d64ed2c3c17666f4f0e15462800f3477255dc53667ad5d099277866c5666"}, + {file = "ujson-5.12.1-cp311-cp311-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:2cfbd6b0c677d5d053964b8f98d8bb1af10c591c8c24454bcd40006ac8ba18db"}, + {file = "ujson-5.12.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f75caed5b6d1fc271bb720a780c4199914267f7b865f9bf17826c4feccea582c"}, + {file = "ujson-5.12.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b21b4c680594c8686bcd4cdda0fd3ea2567b9d42bcf1d1e3d92d39bcdb02e8f1"}, + {file = "ujson-5.12.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:50d07e79ec70d32b4fbe18ab706ed0b172be08710d5901b9d067d7951bfaa164"}, + {file = "ujson-5.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:080bc65ac7c0a6314d45d55b6171d3a48b1aeaf89895654d625b291cfe46309f"}, + {file = "ujson-5.12.1-cp311-cp311-win32.whl", hash = "sha256:251ba8229e19b4b0b3efb5e7e3ddfa67c5c466aa492707bc3f6568bf714604dc"}, + {file = "ujson-5.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:46315b82505c99101dcab3bd979f15fecfde85c02df7efbb4e428fa357665290"}, + {file = "ujson-5.12.1-cp311-cp311-win_arm64.whl", hash = "sha256:12e99e49c62322ed0394c914aff15403ba7ede0b74f05a0faa4ec12c7d17a139"}, + {file = "ujson-5.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:10f44bd08ae52ee23ca6e8b472692e5da1768af2d53ff1bad6f40b532e0bc7ee"}, + {file = "ujson-5.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cc6ea753b7303fa5629fa9ac9257ea4b001c4d72583b2bb36ff1855a07db49f"}, + {file = "ujson-5.12.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:289f13095764d03734adfa10107da9b530ceb64dc1b02a5f507588d978d5b7df"}, + {file = "ujson-5.12.1-cp312-cp312-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:427893168d074e59214b0ee058337c57f5bb80175cdd5b4799a9c931aae22022"}, + {file = "ujson-5.12.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7a81724d5d90a2da7155d15d8b156ce57eaed7cdd622df813f36a8e612fd4c8"}, + {file = "ujson-5.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3a6efff7dc6515416366819de4a1bc449b77107c5b48508b101fd40f7f8bec08"}, + {file = "ujson-5.12.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77a71fe53427a0cf49d56eafd801d9f7e203b784b7f99cc717783fd6f6f7b732"}, + {file = "ujson-5.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ea3bed53d2ea8e5642e814a9e41f3e29420a8067874ba03ace8c0462e160490c"}, + {file = "ujson-5.12.1-cp312-cp312-win32.whl", hash = "sha256:758e5c8fbe4e6d483041e03b307b01fb5d2f2dd4452d4d4b927ab902e188939e"}, + {file = "ujson-5.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:f6074d3d3267ba1914c624b6e1fa3d8152648ff36b0ab77ddf83b92db488c30d"}, + {file = "ujson-5.12.1-cp312-cp312-win_arm64.whl", hash = "sha256:7642a41520ac1b2bc25ea282b66b8da522cc43424442e6fb5e039be4d4f96530"}, + {file = "ujson-5.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c4bdc052a5d097f0a2e56d93aed97355f9f7a62ef9baa4f8517e43245434af9c"}, + {file = "ujson-5.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5dc91fa06ea35920b704fd9d70871897680145998071cfbf5ee3e19f2c9fc242"}, + {file = "ujson-5.12.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5db0849c0e3da54822a5834f2dc51d7c51072d7f7d665014ee34600dc10889b"}, + {file = "ujson-5.12.1-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:949cb4863a5d4847edeb47c5364b334e8cadf23a7cbdaa547d86098a4b093106"}, + {file = "ujson-5.12.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8aa731138d6dfca4ab84501b72384e6c544bfb48cb87a0dd4d304df3246cac25"}, + {file = "ujson-5.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:727e983ef27892d86ee2d28fd517eeb02b2c1165aafcbe929dce988aeee81bfe"}, + {file = "ujson-5.12.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d57d731ecf492d3d011e65369f8330654f0875b19f646be5270d478e843d3b81"}, + {file = "ujson-5.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a09636220f26c66f80c6c6283023cb53120e843825f890be92696cd1aa43f39"}, + {file = "ujson-5.12.1-cp313-cp313-win32.whl", hash = "sha256:ee83fbac03a0896faf190177c938f94eb610b798d495a19d50997242c4eca685"}, + {file = "ujson-5.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:e08d9e096c416ddc34519241f97c201258b42639f2012d9547d8ae32921800dd"}, + {file = "ujson-5.12.1-cp313-cp313-win_arm64.whl", hash = "sha256:963287e4b1bc463735c4056968a2dfa59bb831b6daba68bddd14f451191fe9e5"}, + {file = "ujson-5.12.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f19e9a407a24230df0cc1ec1c0f5999872ba526b14a780f80ad6479f5eed9bc"}, + {file = "ujson-5.12.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8b657e870c77aaacdeea86cfad3e6d2ef9b52517e45988c9c367f7ee764fe4dd"}, + {file = "ujson-5.12.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:984b5a99d1e0a037c2046c3c4b34cec832565d62d5017be0a035bf3cbfab72dc"}, + {file = "ujson-5.12.1-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:f48ef8a16f1d85bd7982beac7adfd3fb704058631db84c1c61c8a1b7072b1508"}, + {file = "ujson-5.12.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f39ba3b65cc637b59731532f7e7c807786bff1d0332ab2d5b96a04d2584d78f"}, + {file = "ujson-5.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:07f307780f85b49cba93f291718421b6f5f3b627a323b431fad937a18f6587cb"}, + {file = "ujson-5.12.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1c335caea51c31494e514b82d50763b9792d3960d2c7d9fdb6b6fb8ed50ebdd0"}, + {file = "ujson-5.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:19ea07e29a45d199f926aadf93a9974128438c01b83141fba32477c0ee604b33"}, + {file = "ujson-5.12.1-cp314-cp314-win32.whl", hash = "sha256:c8e626b6bc9bdd2e8f7393b7d99f3daa2ca4022e6203662e70de7bb3604b21b9"}, + {file = "ujson-5.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:c6d3bdd020333688ee60559437021ed68a98a28fdd609b5af16de5dd58f90cba"}, + {file = "ujson-5.12.1-cp314-cp314-win_arm64.whl", hash = "sha256:e3c9c894971f4ada3ded16a804ed4640e1f2b3e5239beaeec7c48296f39f4232"}, + {file = "ujson-5.12.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:49dd9c378e1c8e676785ff2b62cb490074229f15ab54abf45b623713cb2c36b5"}, + {file = "ujson-5.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d8827904358d7da59ccf2e1fd8de59e78248036d17fecc0462e62c6721f1102"}, + {file = "ujson-5.12.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc26caebea90425662ef0b979f945f6ac832651881107d6ec9a3c4d4a4ba929c"}, + {file = "ujson-5.12.1-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:45022aae09ac3d45bda6fbfc631088d1aff9a0465542d40bd6d295ced378c430"}, + {file = "ujson-5.12.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b22aa0f644516d3d5b29464949e4b23fe784f84b4a1030ab9ac3cb42aaedabb1"}, + {file = "ujson-5.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7dc5cf44ea42365cd1b66e6ed3fc6ca040c86587b024a6659b98e99d31cff2cd"}, + {file = "ujson-5.12.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8df5d984ff4ac1ef292d70f30da03417038a7e1e0bc272d28ca9d34f02f41682"}, + {file = "ujson-5.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:485f0182a0c0b54c304061cdc826d8343ce595c4055f7a24e72772a8520e5f7b"}, + {file = "ujson-5.12.1-cp314-cp314t-win32.whl", hash = "sha256:4e12ca368b397aed7fa1eec534ea1ba8d94977b376f9df3e93ae1acfd004ec40"}, + {file = "ujson-5.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:cec6b9b539539affc1f01a795c99574592a635ce22331b64f2b42e0af570659e"}, + {file = "ujson-5.12.1-cp314-cp314t-win_arm64.whl", hash = "sha256:696224d4cfb8883fa5c0285dff31e5ce924704dd9ccd38e9ea8b5bf4a42b12fc"}, + {file = "ujson-5.12.1-graalpy312-graalpy250_312_native-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c419bf42ae40963fc27f70c59e24e9a97f5cf168dbce2c572f3c0ce3595912"}, + {file = "ujson-5.12.1-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0be2b4f2f547b9f0f3d902640e410e5a2fc851576cbe033c88445a23e3e7aef1"}, + {file = "ujson-5.12.1-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:4ea0c490c702c20495e97345acfcf0c2f3153e658ef537ff111929c48b89e10a"}, + {file = "ujson-5.12.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3e30fa6bc7156ed709e13f8b52e917db08fbfd611ba61346b62630974ec0ba8e"}, + {file = "ujson-5.12.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f67c5f0d64eba0fbbd6d2d6a79b0c43c5bc06f27564378fd5d716e0d40360068"}, + {file = "ujson-5.12.1-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8416bb724db9accfa97bdb77245952494b1800c23e42defd46afb5c661c9af19"}, + {file = "ujson-5.12.1-pp311-pypy311_pp73-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:66005b49c753a1b9f2f8853919dc58e1e6bd66846ea341a33afa76c6d7602485"}, + {file = "ujson-5.12.1-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdc6b277dcd27663f7fb76b6a5088424c66e0407c23e9884f80cd733f7d71b19"}, + {file = "ujson-5.12.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7957b64583793042521f7f7c71c01626b3d32a17528eaab980eb8cdc3d4eec68"}, + {file = "ujson-5.12.1.tar.gz", hash = "sha256:5b7e96406c301a1366534479a7352ec40ec68bb327c0c119091635acd5925e35"}, ] [[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\""] @@ -4912,50 +4983,50 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "valkey-glide" -version = "2.2.7" +version = "2.3.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.2.7-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:701b6ee036a54598ba63d7e6ecdee8f6ddd5b460cef67491f29414447deb7407"}, - {file = "valkey_glide-2.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:556dd3a906f61ff2d53f540fa782eee5c67a2048ed434f87089bb4f62cbd2564"}, - {file = "valkey_glide-2.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6622536445b7c78ae3f0f497ae449efac6a627f7c607b92c9ef934c5dd046c4b"}, - {file = "valkey_glide-2.2.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9bd390f66dc324ce3e937a6ac7592bfbd4e6cf9eb5d4c28838fc766645f149b"}, - {file = "valkey_glide-2.2.7-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:e39a1db18d08f5a9995d87158b070af1a625a612dc7e57e27a9becee40f6144c"}, - {file = "valkey_glide-2.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:180aa1ee0cdfbcf34ae7322838fd063a720a6dae9e97a8e9462b8a12b1f65138"}, - {file = "valkey_glide-2.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44a9a6e85e8320220604468c35e0a84bea392dddbab2dcdf9cce9ece01b4a041"}, - {file = "valkey_glide-2.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7905c5f3efb67058c5f52b7906aa2d114288eff4aa76a5379107b312af6b8ec8"}, - {file = "valkey_glide-2.2.7-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:4db4ff570c0a63cc8a4551b780dd00069d61c8841a6e6eeaf2dda05d89ec0221"}, - {file = "valkey_glide-2.2.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05f5ebe701f18b22d331a12af120e1250927391665b66fd78c273d563b2523c6"}, - {file = "valkey_glide-2.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca7aab86a175c678bb0573db29050d49d692adcf87c7dd01e2ff9da94bdac68f"}, - {file = "valkey_glide-2.2.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c81c7cb8bbae7a75c3efcfe9b05ebd97db6f332128606e5464e518ba5a7b8e02"}, - {file = "valkey_glide-2.2.7-cp313-cp313-macosx_10_7_x86_64.whl", hash = "sha256:1d40da535a77ce318367ac255b1d5de95cf0ca669b8cac79a158f678feed9fb3"}, - {file = "valkey_glide-2.2.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f435ed9c14d7de72df04322300034931aba528d1183770b2f7624dd8fc18d7c"}, - {file = "valkey_glide-2.2.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cad26daa0775ab6dd7ad5a1d8300c4b97ed4b39401c1f130200456f9f9b5234"}, - {file = "valkey_glide-2.2.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:340a9bdf31e811121e9ea7d95cb75161125c78690334581d4be08aae9c824f29"}, - {file = "valkey_glide-2.2.7-cp314-cp314-macosx_10_7_x86_64.whl", hash = "sha256:085c81403600555a7672cf45d68f2c786d1fac12d5759d8e6e3a3f7d5a79d8b7"}, - {file = "valkey_glide-2.2.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d2470a704f463600a0c12000b48adbcc888210be38fbb39fd33c7f36fe84bd66"}, - {file = "valkey_glide-2.2.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d1985f7c579c7b37bf7fc42125b141295dded29257d7b811d318bb5343343c8"}, - {file = "valkey_glide-2.2.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba5149f2019164024958778e5b314f05dc61187731e2c23411498cb884a9181b"}, - {file = "valkey_glide-2.2.7-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:20b586d2702a71cd90bb7c85380155f92585129f9534396450e2a64896e5b00c"}, - {file = "valkey_glide-2.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8316673b56632ae92b4cf22a990b8fc510fe87cbb29d3aac242496cf7a44d96c"}, - {file = "valkey_glide-2.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3c5ae19adbb299c212c0011c1934ea3769b1dc364126a6fb5b443842678c2ec"}, - {file = "valkey_glide-2.2.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec85da03bd00402df90152c5e647cade29c0e539311839c844e135e945f84dbc"}, - {file = "valkey_glide-2.2.7-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:47949c900e08de0e64fb5b59abfa069e09a62a9a4db2ba6756ca3a6b440f012a"}, - {file = "valkey_glide-2.2.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8d6ba5b86d8910545dcd8429807780bae705def558ce38ca8f2a10ee13aa7021"}, - {file = "valkey_glide-2.2.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ce02ce683b42687b72fc21a70b7dfe3597c79cb1594c6e707b464fa37e8f3a3"}, - {file = "valkey_glide-2.2.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b02900b8e6ea539a5a158c0a74e63d92043b4487dd43f33cc1b0bb03a0aeac0"}, - {file = "valkey_glide-2.2.7-pp311-pypy311_pp73-macosx_10_7_x86_64.whl", hash = "sha256:33e6a21430580499943f29d30c3d74bc9b53f421bb76ea190e43cead428fc832"}, - {file = "valkey_glide-2.2.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c2eff5bf9e30bb2e2efb4bad09ecf2568a7ca722e39b37f8a10d5244a512b3a"}, - {file = "valkey_glide-2.2.7-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:589e52f909bc7e7736e35af6e4b3d91e7dfcbf26b3bf13fca79668ad633d9ed4"}, - {file = "valkey_glide-2.2.7-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8c9beaff220439b10906e8b84c5a141d4b6515ea28db38f076191777e26c05"}, - {file = "valkey_glide-2.2.7-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:1e353efd6b7d6b511be246e0376be0176869b2a7bde4ba7c4d8d0e25c3bda07b"}, - {file = "valkey_glide-2.2.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ba90316717570f550ffbacdad36bc023ca404468c35c997f2ee4bbd8b1cbb634"}, - {file = "valkey_glide-2.2.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:170ab03fa9fb958bb1c9ed467a4e173444d7b23886d5be01b8719d7c4d8ced8d"}, - {file = "valkey_glide-2.2.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6b1ad67ff44d23850713c10191a701c19b8bd4d800ca3ef1a442267563ad92f"}, - {file = "valkey_glide-2.2.7.tar.gz", hash = "sha256:2cd05b8c871c7878cb89679ac34f294f100481b64f79d797cde325a1d051cdc9"}, + {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"}, ] [package.dependencies] @@ -5172,4 +5243,4 @@ valkey = ["valkey-glide"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0.0" -content-hash = "ade91e6e8ba781806bb5af43cda5f48fa585acd3005f449e182394d238625deb" +content-hash = "ec4397857f1745105717c60a48f9791c37387457cba3aca337c2afa55b29d77d" diff --git a/pyproject.toml b/pyproject.toml index 8a5e4cec44d..5ca4ee98915 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aws_lambda_powertools" -version = "3.26.0" +version = "3.29.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"] @@ -52,7 +52,7 @@ pydantic-settings = {version = "^2.6.1", optional = true} boto3 = { version = "^1.34.32", optional = true } redis = { version = ">=4.4,<8.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 = ">=3.3.1,!=4.0.0,!=4.0.1,!=4.0.2,!=4.0.3,!=4.0.4,<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 } @@ -83,7 +83,7 @@ kafka-consumer-protobuf = ["protobuf"] coverage = { extras = ["toml"], version = "^7.6" } pytest = ">=8.3.4,<10.0.0" boto3 = "^1.26.164" -isort = ">=5.13.2,<8.0.0" +isort = ">=5.13.2,<9.0.0" pytest-cov = ">=5,<8" pytest-mock = "^3.14.0" pytest-asyncio = ">=0.24,<1.4" @@ -110,15 +110,15 @@ hvac = "^2.3.0" aws-requests-auth = "^0.4.3" urllib3 = ">=1.25.4,!=2.2.0,<3" requests = ">=2.32.0" -cfn-lint = "1.46.0" -mypy = "^1.1.1" +cfn-lint = "1.48.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.14.15" +ruff = ">=0.5.1,<0.15.15" 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" @@ -128,8 +128,8 @@ mkdocstrings-python = "^1.13.0" mkdocs-llmstxt = ">=0.2,<0.5" avro = "^1.12.0" protobuf = ">=6.30.2,<8.0.0" -types-protobuf = "^6.30.2.20250516" -ty = "^0.0.23" +types-protobuf = ">=6.30.2.20250516,<8.0.0.0" +ty = ">=0.0.23,<0.0.36" [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/tests/functional/event_handler/_pydantic/merge_handlers/shared/__init__.py b/tests/functional/event_handler/_pydantic/merge_handlers/shared/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/event_handler/_pydantic/merge_handlers/shared/categories_routes.py b/tests/functional/event_handler/_pydantic/merge_handlers/shared/categories_routes.py new file mode 100644 index 00000000000..7029ad332c7 --- /dev/null +++ b/tests/functional/event_handler/_pydantic/merge_handlers/shared/categories_routes.py @@ -0,0 +1,11 @@ +"""Categories routes - imports shared resolver and registers routes.""" + +from __future__ import annotations + +from tests.functional.event_handler._pydantic.merge_handlers.shared.resolver import app + + +@app.get("/categories") +def get_categories() -> list[dict]: + """Get all categories.""" + return [] diff --git a/tests/functional/event_handler/_pydantic/merge_handlers/shared/products_routes.py b/tests/functional/event_handler/_pydantic/merge_handlers/shared/products_routes.py new file mode 100644 index 00000000000..84d9d4bbde5 --- /dev/null +++ b/tests/functional/event_handler/_pydantic/merge_handlers/shared/products_routes.py @@ -0,0 +1,17 @@ +"""Products routes - imports shared resolver and registers routes.""" + +from __future__ import annotations + +from tests.functional.event_handler._pydantic.merge_handlers.shared.resolver import app + + +@app.get("/products") +def get_products() -> list[dict]: + """Get all products.""" + return [] + + +@app.get("/products/") +def get_product(product_id: str) -> dict: + """Get a product by ID.""" + return {"id": product_id} diff --git a/tests/functional/event_handler/_pydantic/merge_handlers/shared/resolver.py b/tests/functional/event_handler/_pydantic/merge_handlers/shared/resolver.py new file mode 100644 index 00000000000..8a7a472b216 --- /dev/null +++ b/tests/functional/event_handler/_pydantic/merge_handlers/shared/resolver.py @@ -0,0 +1,5 @@ +"""Shared resolver - routes are registered by other files that import this.""" + +from aws_lambda_powertools.event_handler import APIGatewayRestResolver + +app = APIGatewayRestResolver() diff --git a/tests/functional/event_handler/_pydantic/merge_handlers/typed_handler.py b/tests/functional/event_handler/_pydantic/merge_handlers/typed_handler.py new file mode 100644 index 00000000000..53c2b4e0a12 --- /dev/null +++ b/tests/functional/event_handler/_pydantic/merge_handlers/typed_handler.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from pydantic import BaseModel + +from aws_lambda_powertools.event_handler import APIGatewayRestResolver + +app: APIGatewayRestResolver = APIGatewayRestResolver(enable_validation=True) + + +class Product(BaseModel): + id: int + name: str + price: float + + +@app.get("/products") +def get_products() -> list[Product]: + return [ + Product(id=1, name="Widget", price=9.99), + ] + + +def handler(event, context): + return app.resolve(event, context) diff --git a/tests/functional/event_handler/_pydantic/test_api_gateway.py b/tests/functional/event_handler/_pydantic/test_api_gateway.py index ce3fd89e864..32a6e3c549d 100644 --- a/tests/functional/event_handler/_pydantic/test_api_gateway.py +++ b/tests/functional/event_handler/_pydantic/test_api_gateway.py @@ -80,3 +80,69 @@ def get_lambda(param: int): ... assert result["statusCode"] == 422 assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] assert "missing" in result["body"] + + +def test_route_custom_status_code_with_dict(): + # GIVEN a route with a custom status_code returning a dict + app = ApiGatewayResolver(enable_validation=True) + + @app.post("/my/path", status_code=201) + def create_item(): + return {"name": "test"} + + event = {"httpMethod": "POST", "path": "/my/path", "body": "{}"} + + # WHEN calling the event handler + result = app(event, {}) + + # THEN the response should use the route's custom status code + assert result["statusCode"] == 201 + + +def test_route_custom_status_code_tuple_override(): + # GIVEN a route with status_code=201 but handler returns a tuple with 202 + app = ApiGatewayResolver(enable_validation=True) + + @app.post("/my/path", status_code=201) + def create_item(): + return {"name": "test"}, 202 + + event = {"httpMethod": "POST", "path": "/my/path", "body": "{}"} + + # WHEN calling the event handler + result = app(event, {}) + + # THEN the tuple status code should override the route's status code + assert result["statusCode"] == 202 + + +def test_route_custom_status_code_response_object_override(): + # GIVEN a route with status_code=201 but handler returns a Response with 204 + app = ApiGatewayResolver(enable_validation=True) + + @app.post("/my/path", status_code=201) + def create_item(): + return Response(status_code=204, content_type=content_types.APPLICATION_JSON, body="{}") + + event = {"httpMethod": "POST", "path": "/my/path", "body": "{}"} + + # WHEN calling the event handler + result = app(event, {}) + + # THEN the Response object's status code should take precedence + assert result["statusCode"] == 204 + + +def test_route_default_status_code_with_dict(): + # GIVEN a route without custom status_code returning a dict + app = ApiGatewayResolver(enable_validation=True) + + @app.get("/my/path") + def get_items(): + return {"items": []} + + # WHEN calling the event handler + result = app(LOAD_GW_EVENT, {}) + + # THEN the response should default to 200 + assert result["statusCode"] == 200 diff --git a/tests/functional/event_handler/_pydantic/test_depends.py b/tests/functional/event_handler/_pydantic/test_depends.py new file mode 100644 index 00000000000..0cbc05b83c7 --- /dev/null +++ b/tests/functional/event_handler/_pydantic/test_depends.py @@ -0,0 +1,216 @@ +"""Tests for Depends() with OpenAPI schema generation and validation.""" + +import json +from typing import Annotated + +from pydantic import BaseModel + +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver, Depends +from aws_lambda_powertools.event_handler.request import Request +from tests.functional.utils import load_event + +API_GW_V2_EVENT = load_event("apiGatewayProxyV2Event.json") + + +# --- Fixtures --- + + +class AppConfig(BaseModel): + region: str = "us-east-1" + debug: bool = False + + +def get_config() -> AppConfig: + return AppConfig(region="eu-west-1", debug=True) + + +def get_tenant() -> str: + return "tenant-abc" + + +# --- OpenAPI schema tests --- + + +def test_depends_excluded_from_openapi_schema(): + """Depends() parameters must NOT appear in the OpenAPI schema.""" + app = APIGatewayHttpResolver(enable_validation=True) + + @app.get("/orders") + def handler(tenant: Annotated[str, Depends(get_tenant)], status: str = "active"): + return {"tenant": tenant, "status": status} + + schema = app.get_openapi_schema() + get_op = schema.paths["/orders"].get + param_names = [p.name for p in (get_op.parameters or [])] + + assert "tenant" not in param_names + assert "status" in param_names + + +def test_depends_with_pydantic_model_excluded_from_schema(): + """Depends() returning a Pydantic model must NOT appear as a body param in the schema.""" + app = APIGatewayHttpResolver(enable_validation=True) + + @app.get("/info") + def handler(config: Annotated[AppConfig, Depends(get_config)]): + return {"region": config.region} + + schema = app.get_openapi_schema() + get_op = schema.paths["/info"].get + param_names = [p.name for p in (get_op.parameters or [])] + + assert "config" not in param_names + # Should have no request body either + assert get_op.requestBody is None + + +def test_depends_nested_excluded_from_openapi_schema(): + """Nested Depends() parameters must NOT appear in the OpenAPI schema.""" + app = APIGatewayHttpResolver(enable_validation=True) + + def get_prefix() -> str: + return "Hello" + + def get_greeting(prefix: Annotated[str, Depends(get_prefix)]) -> str: + return f"{prefix}, world!" + + @app.get("/greet") + def handler(greeting: Annotated[str, Depends(get_greeting)]): + return {"greeting": greeting} + + schema = app.get_openapi_schema() + get_op = schema.paths["/greet"].get + param_names = [p.name for p in (get_op.parameters or [])] + + assert "greeting" not in param_names + assert "prefix" not in param_names + + +# --- Validation + Depends integration tests --- + + +def test_depends_with_validation_resolves_and_validates(): + """Depends() values are injected alongside validated query params.""" + app = APIGatewayHttpResolver(enable_validation=True) + + @app.get("/orders") + def handler(tenant: Annotated[str, Depends(get_tenant)], limit: int = 10): + return {"tenant": tenant, "limit": limit} + + event = {**API_GW_V2_EVENT} + event["rawPath"] = "/orders" + event["requestContext"] = { + **event["requestContext"], + "http": {"method": "GET", "path": "/orders"}, + } + event["queryStringParameters"] = {"limit": "5"} + + result = app(event, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["tenant"] == "tenant-abc" + assert body["limit"] == 5 + + +def test_depends_pydantic_model_with_validation(): + """Depends() returning a Pydantic model works with enable_validation.""" + app = APIGatewayHttpResolver(enable_validation=True) + + @app.get("/config") + def handler(config: Annotated[AppConfig, Depends(get_config)]): + return {"region": config.region, "debug": config.debug} + + event = {**API_GW_V2_EVENT} + event["rawPath"] = "/config" + event["requestContext"] = { + **event["requestContext"], + "http": {"method": "GET", "path": "/config"}, + } + + result = app(event, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["region"] == "eu-west-1" + assert body["debug"] is True + + +def test_depends_with_request_and_validation(): + """Depends() with Request injection works alongside validation.""" + app = APIGatewayHttpResolver(enable_validation=True) + + def get_method(request: Request) -> str: + return request.method + + @app.post("/my/path") + def handler(method: Annotated[str, Depends(get_method)], name: str = "world"): + return {"method": method, "name": name} + + event = {**API_GW_V2_EVENT, "queryStringParameters": {"name": "Lambda"}} + result = app(event, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["method"] == "POST" + assert body["name"] == "Lambda" + + +def test_depends_override_with_validation(): + """dependency_overrides works with enable_validation.""" + app = APIGatewayHttpResolver(enable_validation=True) + + @app.get("/orders") + def handler(tenant: Annotated[str, Depends(get_tenant)]): + return {"tenant": tenant} + + app.dependency_overrides[get_tenant] = lambda: "test-tenant" + + event = {**API_GW_V2_EVENT} + event["rawPath"] = "/orders" + event["requestContext"] = { + **event["requestContext"], + "http": {"method": "GET", "path": "/orders"}, + } + + result = app(event, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"tenant": "test-tenant"} + + app.dependency_overrides.clear() + + +def test_depends_with_path_params_and_validation(): + """Depends() works with path parameters and validation.""" + app = APIGatewayHttpResolver(enable_validation=True) + + @app.get("/orders/") + def handler(order_id: str, tenant: Annotated[str, Depends(get_tenant)]): + return {"order_id": order_id, "tenant": tenant} + + event = {**API_GW_V2_EVENT} + event["rawPath"] = "/orders/abc-123" + event["requestContext"] = { + **event["requestContext"], + "http": {"method": "GET", "path": "/orders/abc-123"}, + } + + result = app(event, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["order_id"] == "abc-123" + assert body["tenant"] == "tenant-abc" + + +def test_depends_with_regular_params_and_validation(): + """Depends() works alongside regular handler parameters with validation.""" + app = APIGatewayHttpResolver(enable_validation=True) + + def get_greeting() -> str: + return "hello" + + @app.post("/my/path") + def handler(name: str = "world", greeting: Annotated[str, Depends(get_greeting)] = ""): + return {"message": f"{greeting}, {name}!"} + + event = {**API_GW_V2_EVENT, "queryStringParameters": {"name": "Lambda"}} + result = app(event, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"message": "hello, Lambda!"} 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 d31185f3239..7cab58a1b70 100644 --- a/tests/functional/event_handler/_pydantic/test_http_resolver_pydantic.py +++ b/tests/functional/event_handler/_pydantic/test_http_resolver_pydantic.py @@ -209,7 +209,6 @@ def search( # ============================================================================= -@pytest.mark.skip("Due to issue #7981.") @pytest.mark.asyncio async def test_async_handler_with_validation(): # GIVEN an app with async handler and validation @@ -241,6 +240,91 @@ async def create_user(user: UserModel) -> UserResponse: assert body["user"]["name"] == "AsyncUser" +@pytest.mark.asyncio +async def test_async_handler_invalid_response_returns_422(): + # GIVEN an app with async handler and validation + app = HttpResolverLocal(enable_validation=True) + + @app.get("/user") + async def get_user() -> UserResponse: + await asyncio.sleep(0.001) + return {"name": "John"} # type: ignore # Missing required fields + + scope = { + "type": "http", + "method": "GET", + "path": "/user", + "query_string": b"", + "headers": [(b"content-type", b"application/json")], + } + + receive = make_asgi_receive() + send, captured = make_asgi_send() + + # WHEN called via ASGI interface + await app(scope, receive, send) + + # THEN it returns 422 for invalid response + assert captured["status_code"] == 422 + + +@pytest.mark.asyncio +async def test_sync_handler_with_validation_via_asgi(): + # GIVEN an app with a sync handler and validation, called via ASGI + app = HttpResolverLocal(enable_validation=True) + + @app.post("/users") + def create_user(user: UserModel) -> UserResponse: + return UserResponse(id="sync-123", user=user) + + scope = { + "type": "http", + "method": "POST", + "path": "/users", + "query_string": b"", + "headers": [(b"content-type", b"application/json")], + } + + receive = make_asgi_receive(b'{"name": "SyncUser", "age": 30}') + send, captured = make_asgi_send() + + # WHEN called via ASGI interface + await app(scope, receive, send) + + # THEN validation works with sync handler + assert captured["status_code"] == 200 + body = json.loads(captured["body"]) + assert body["id"] == "sync-123" + assert body["user"]["name"] == "SyncUser" + + +@pytest.mark.asyncio +async def test_sync_handler_invalid_response_returns_422_via_asgi(): + # GIVEN an app with a sync handler and validation, called via ASGI + app = HttpResolverLocal(enable_validation=True) + + @app.get("/user") + def get_user() -> UserResponse: + return {"name": "John"} # type: ignore # Missing required fields + + scope = { + "type": "http", + "method": "GET", + "path": "/user", + "query_string": b"", + "headers": [(b"content-type", b"application/json")], + } + + receive = make_asgi_receive() + send, captured = make_asgi_send() + + # WHEN called via ASGI interface + await app(scope, receive, send) + + # THEN it returns 422 for invalid response + assert captured["status_code"] == 422 + + # ============================================================================= # OpenAPI Tests # ============================================================================= diff --git a/tests/functional/event_handler/_pydantic/test_openapi_merge.py b/tests/functional/event_handler/_pydantic/test_openapi_merge.py index b4dc1d70232..88834667727 100644 --- a/tests/functional/event_handler/_pydantic/test_openapi_merge.py +++ b/tests/functional/event_handler/_pydantic/test_openapi_merge.py @@ -367,3 +367,52 @@ def test_openapi_merge_schema_is_cached(): # AND paths should not be duplicated assert len([p for p in schema1["paths"] if p == "/users"]) == 1 + + +def test_openapi_merge_shared_resolver_pattern(): + # GIVEN a shared resolver pattern where: + # - resolver.py defines the resolver + # - products_routes.py and categories_routes.py import it and register routes + merge = OpenAPIMerge(title="Shared Resolver API", version="1.0.0") + + # WHEN discovering with project_root set to allow absolute imports + shared_path = MERGE_HANDLERS_PATH / "shared" + project_root = Path(__file__).parent.parent.parent.parent.parent # repo root + + files = merge.discover( + path=shared_path, + pattern="resolver.py", + project_root=project_root, + ) + + # THEN it should find the resolver file + assert len(files) == 1 + assert files[0].name == "resolver.py" + + # AND it should find dependent files that import the resolver + dependent = merge.dependent_files.get(files[0], []) + dependent_names = [f.name for f in dependent] + assert "products_routes.py" in dependent_names + assert "categories_routes.py" in dependent_names + + # AND the merged schema should include routes from all dependent files + schema = merge.get_openapi_schema() + assert "/products" in schema["paths"] + assert "/products/{product_id}" in schema["paths"] + assert "/categories" in schema["paths"] + + +def test_openapi_merge_discover_type_annotated_resolver(): + # GIVEN an OpenAPIMerge instance + merge = OpenAPIMerge(title="Typed API", version="1.0.0") + + # WHEN discovering a handler with a type-annotated resolver (app: Resolver = Resolver()) + merge.discover( + path=MERGE_HANDLERS_PATH, + pattern="**/typed_handler.py", + resolver_name="app", + ) + + # THEN it should find the resolver and include its routes in the schema + schema = merge.get_openapi_schema() + assert "/products" in schema["paths"] diff --git a/tests/functional/event_handler/_pydantic/test_openapi_responses.py b/tests/functional/event_handler/_pydantic/test_openapi_responses.py index 71c7d186cbe..785d2b8416c 100644 --- a/tests/functional/event_handler/_pydantic/test_openapi_responses.py +++ b/tests/functional/event_handler/_pydantic/test_openapi_responses.py @@ -370,3 +370,89 @@ def handler() -> UserResponse: assert "example2" in examples assert examples["example2"].summary == "Example 2" assert examples["example2"].value["id"] == 2 + + +def test_openapi_custom_status_code(): + # GIVEN a route with a custom status_code + app = APIGatewayRestResolver(enable_validation=True) + + class Item(BaseModel): + name: str + + @app.post("/items", status_code=201) + def create_item() -> Item: + return Item(name="test") + + # WHEN we retrieve the OpenAPI schema + schema = app.get_openapi_schema() + responses = schema.paths["/items"].post.responses + + # THEN the schema should use 201 as the success response code instead of 200 + assert 201 in responses + assert responses[201].description == "Successful Response" + assert 200 not in responses + + +def test_openapi_custom_status_code_with_description(): + # GIVEN a route with a custom status_code and response_description + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/items", status_code=201, response_description="Item created") + def create_item(): + return {"name": "test"} + + # WHEN we retrieve the OpenAPI schema + schema = app.get_openapi_schema() + responses = schema.paths["/items"].post.responses + + # THEN the schema should use 201 with the custom description + assert 201 in responses + assert responses[201].description == "Item created" + assert 200 not in responses + + +def test_openapi_default_status_code(): + # GIVEN a route without a custom status_code + app = APIGatewayRestResolver(enable_validation=True) + + @app.get("/items") + def get_items(): + return {"items": []} + + # WHEN we retrieve the OpenAPI schema + schema = app.get_openapi_schema() + responses = schema.paths["/items"].get.responses + + # THEN the schema should default to 200 + assert 200 in responses + assert responses[200].description == "Successful Response" + + +def test_openapi_custom_status_code_all_methods(): + # GIVEN routes with custom status_code on different HTTP methods + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/items", status_code=201) + def create(): + return {} + + @app.put("/items", status_code=204) + def update(): + return {} + + @app.delete("/items", status_code=204) + def delete(): + return {} + + @app.patch("/items", status_code=202) + def patch(): + return {} + + # WHEN we retrieve the OpenAPI schema + schema = app.get_openapi_schema() + + # THEN each method should have the correct custom status code + assert 201 in schema.paths["/items"].post.responses + assert 204 in schema.paths["/items"].put.responses + assert 204 in schema.paths["/items"].delete.responses + assert 202 in schema.paths["/items"].patch.responses 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..d25811d24ae 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 @@ -3,7 +3,7 @@ from typing import Literal, Optional 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 @@ -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_validation_middleware.py b/tests/functional/event_handler/_pydantic/test_openapi_validation_middleware.py index 21bc9b26e0a..0da092f8aea 100644 --- a/tests/functional/event_handler/_pydantic/test_openapi_validation_middleware.py +++ b/tests/functional/event_handler/_pydantic/test_openapi_validation_middleware.py @@ -1,6 +1,7 @@ import base64 import datetime import json +import warnings from dataclasses import dataclass from enum import Enum from pathlib import PurePath @@ -3327,3 +3328,938 @@ def handler(items: Annotated[Union[_Item, List[_Item]], Body()]) -> Dict[str, An status, body = _post_json(app, "/items", big_payload) assert status == 200 assert body["count"] == 100 + + +# ---------- File upload (multipart/form-data) ---------- + + +def _build_multipart_body(fields: List[Dict], boundary: str = "----TestBoundary") -> Tuple[str, str]: + """ + Build a multipart/form-data body and return (base64_body, content_type). + + Each field dict can have: + - name: field name (required) + - value: str or bytes (required) + - filename: optional filename (makes it a file part) + - content_type: optional content type for the part + """ + parts = [] + for field in fields: + headers = f'Content-Disposition: form-data; name="{field["name"]}"' + if "filename" in field: + headers += f'; filename="{field["filename"]}"' + if "content_type" in field: + headers += f"\r\nContent-Type: {field['content_type']}" + value = field["value"] + if isinstance(value, str): + value = value.encode("utf-8") + parts.append((headers, value)) + + body = b"" + for headers, value in parts: + body += f"--{boundary}\r\n".encode() + body += f"{headers}\r\n\r\n".encode() + body += value + body += b"\r\n" + body += f"--{boundary}--\r\n".encode() + + content_type = f"multipart/form-data; boundary={boundary}" + return base64.b64encode(body).decode("utf-8"), content_type + + +def test_file_upload_basic(gw_event): + """Test basic file upload with File() parameter.""" + from aws_lambda_powertools.event_handler.openapi.params import File + + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/upload") + def upload(file_data: Annotated[bytes, File()]): + return {"size": len(file_data)} + + body, content_type = _build_multipart_body( + [ + {"name": "file_data", "value": b"hello world", "filename": "test.txt"}, + ], + ) + + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/upload" + gw_event["headers"]["content-type"] = content_type + gw_event["body"] = body + gw_event["isBase64Encoded"] = True + + result = app(gw_event, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"size": 11} + + +def test_file_upload_with_form_field(gw_event): + """Test file upload mixed with a regular form field.""" + from aws_lambda_powertools.event_handler.openapi.params import File + + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/upload") + def upload( + description: Annotated[str, Form()], + file_data: Annotated[bytes, File()], + ): + return {"description": description, "size": len(file_data)} + + body, content_type = _build_multipart_body( + [ + {"name": "description", "value": "my file"}, + {"name": "file_data", "value": b"\x89PNG\r\n\x1a\n", "filename": "image.png", "content_type": "image/png"}, + ], + ) + + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/upload" + gw_event["headers"]["content-type"] = content_type + gw_event["body"] = body + gw_event["isBase64Encoded"] = True + + result = app(gw_event, {}) + assert result["statusCode"] == 200 + parsed = json.loads(result["body"]) + assert parsed["description"] == "my file" + assert parsed["size"] == 8 + + +def test_file_upload_missing_required(gw_event): + """Test that missing required File() parameter returns 422.""" + from aws_lambda_powertools.event_handler.openapi.params import File + + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/upload") + def upload(file_data: Annotated[bytes, File()]): + return {"size": len(file_data)} + + # Send empty multipart body (no file_data field) + body, content_type = _build_multipart_body( + [ + {"name": "other_field", "value": "some value"}, + ], + ) + + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/upload" + gw_event["headers"]["content-type"] = content_type + gw_event["body"] = body + gw_event["isBase64Encoded"] = True + + result = app(gw_event, {}) + assert result["statusCode"] == 422 + assert "missing" in result["body"] + + +def test_file_upload_openapi_schema(): + """Test that File() parameters generate correct OpenAPI schema.""" + from aws_lambda_powertools.event_handler.openapi.params import File + + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/upload") + def upload(file_data: Annotated[bytes, File(description="The file to upload")]): + return {"size": len(file_data)} + + schema = app.get_openapi_schema() + path = schema.paths["/upload"] + post_op = path.post + + # Should have a request body with multipart/form-data + assert post_op.requestBody is not None + content = post_op.requestBody.content + assert "multipart/form-data" in content + + # The schema should reference a binary format field + multipart_schema = content["multipart/form-data"].schema_ + assert multipart_schema is not None + + +def test_file_upload_non_base64(gw_event): + """Test file upload when body is not base64-encoded (edge case).""" + from aws_lambda_powertools.event_handler.openapi.params import File + + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/upload") + def upload(file_data: Annotated[bytes, File()]): + return {"size": len(file_data)} + + # Build multipart body without base64 encoding + boundary = "----TestBoundary" + raw_body = ( + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="file_data"; filename="test.txt"\r\n' + f"\r\n" + f"hello world\r\n" + f"--{boundary}--\r\n" + ) + + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/upload" + gw_event["headers"]["content-type"] = f"multipart/form-data; boundary={boundary}" + gw_event["body"] = raw_body + gw_event["isBase64Encoded"] = False + + result = app(gw_event, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"size": 11} + + +def test_file_upload_non_base64_emits_warning(gw_event): + """Test that non-base64 multipart body emits a warning about API Gateway config.""" + from aws_lambda_powertools.event_handler.openapi.params import File + + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/upload") + def upload(file_data: Annotated[bytes, File()]): + return {"size": len(file_data)} + + boundary = "----TestBoundary" + raw_body = ( + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="file_data"; filename="test.txt"\r\n' + f"\r\n" + f"hello world\r\n" + f"--{boundary}--\r\n" + ) + + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/upload" + gw_event["headers"]["content-type"] = f"multipart/form-data; boundary={boundary}" + gw_event["body"] = raw_body + gw_event["isBase64Encoded"] = False + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = app(gw_event, {}) + + assert result["statusCode"] == 200 + assert len(w) == 1 + assert "Binary Media Types" in str(w[0].message) + + +def test_file_upload_non_base64_binary_content(gw_event): + """Test file upload with raw binary bytes (e.g. JPEG) without base64 encoding.""" + from aws_lambda_powertools.event_handler.openapi.params import File + + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/upload") + def upload(file_data: Annotated[bytes, File()]): + return {"size": len(file_data)} + + # Simulate binary content with bytes that are NOT valid UTF-8 (like JPEG header 0xFF 0xD8) + binary_content = b"\xff\xd8\xff\xe0\x00\x10JFIF\x00" + boundary = "----TestBoundary" + raw_bytes = ( + ( + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="file_data"; filename="photo.jpg"\r\n' + f"Content-Type: image/jpeg\r\n" + f"\r\n" + ).encode("latin-1") + + binary_content + + f"\r\n--{boundary}--\r\n".encode("latin-1") + ) + + # Without binary mode, API Gateway passes body as latin-1 compatible string + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/upload" + gw_event["headers"]["content-type"] = f"multipart/form-data; boundary={boundary}" + gw_event["body"] = raw_bytes.decode("latin-1") + gw_event["isBase64Encoded"] = False + + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + result = app(gw_event, {}) + + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"size": len(binary_content)} + + +def test_upload_file_with_metadata(gw_event): + """Test UploadFile annotation provides filename and content_type.""" + from aws_lambda_powertools.event_handler.openapi.params import File, UploadFile + + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/upload") + def upload(file_data: Annotated[UploadFile, File()]): + return { + "filename": file_data.filename, + "content_type": file_data.content_type, + "size": len(file_data), + } + + body, content_type = _build_multipart_body( + [ + {"name": "file_data", "value": b"fake jpeg", "filename": "photo.jpg", "content_type": "image/jpeg"}, + ], + ) + + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/upload" + gw_event["headers"]["content-type"] = content_type + gw_event["body"] = body + gw_event["isBase64Encoded"] = True + + result = app(gw_event, {}) + assert result["statusCode"] == 200 + parsed = json.loads(result["body"]) + assert parsed["filename"] == "photo.jpg" + assert parsed["content_type"] == "image/jpeg" + assert parsed["size"] == 9 + + +def test_upload_file_mixed_with_form(gw_event): + """Test UploadFile + Form fields together.""" + from aws_lambda_powertools.event_handler.openapi.params import File, UploadFile + + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/upload") + def upload( + file_data: Annotated[UploadFile, File()], + title: Annotated[str, Form()], + ): + return { + "title": title, + "filename": file_data.filename, + "size": len(file_data), + } + + body, content_type = _build_multipart_body( + [ + {"name": "title", "value": "My Document"}, + { + "name": "file_data", + "value": b"pdf content here", + "filename": "doc.pdf", + "content_type": "application/pdf", + }, + ], + ) + + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/upload" + gw_event["headers"]["content-type"] = content_type + gw_event["body"] = body + gw_event["isBase64Encoded"] = True + + result = app(gw_event, {}) + assert result["statusCode"] == 200 + parsed = json.loads(result["body"]) + assert parsed["title"] == "My Document" + assert parsed["filename"] == "doc.pdf" + assert parsed["size"] == 16 + + +def test_upload_file_openapi_schema(): + """Test UploadFile generates correct OpenAPI schema.""" + from aws_lambda_powertools.event_handler.openapi.params import File, UploadFile + + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/upload") + def upload(file_data: Annotated[UploadFile, File(description="A file")]): + return {} + + schema = app.get_openapi_schema() + schema_dict = schema.model_dump(exclude_none=True, by_alias=True) + upload_path = schema_dict["paths"]["/upload"]["post"] + content = upload_path["requestBody"]["content"] + assert "multipart/form-data" in content + + # Resolve $ref to get the actual schema + ref = content["multipart/form-data"]["schema"]["$ref"] + schema_name = ref.split("/")[-1] + props = schema_dict["components"]["schemas"][schema_name]["properties"] + assert props["file_data"]["type"] == "string" + assert props["file_data"]["format"] == "binary" + + +def test_multipart_missing_boundary(gw_event): + """Test that missing boundary in content-type raises ValueError.""" + from aws_lambda_powertools.event_handler.openapi.params import File + + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/upload") + def upload(file_data: Annotated[bytes, File()]): + return {"size": len(file_data)} + + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/upload" + gw_event["headers"]["content-type"] = "multipart/form-data" # no boundary + gw_event["body"] = base64.b64encode(b"some data").decode() + gw_event["isBase64Encoded"] = True + + with pytest.raises(ValueError, match="Missing boundary"): + app(gw_event, {}) + + +def test_multipart_quoted_boundary(gw_event): + """Test that boundary with quotes is parsed correctly.""" + from aws_lambda_powertools.event_handler.openapi.params import File + + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/upload") + def upload(file_data: Annotated[bytes, File()]): + return {"size": len(file_data)} + + boundary = "----TestBoundary" + body, _ = _build_multipart_body( + [ + {"name": "file_data", "value": b"hello", "filename": "test.txt"}, + ], + boundary=boundary, + ) + + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/upload" + # Use quoted boundary + gw_event["headers"]["content-type"] = f'multipart/form-data; boundary="{boundary}"' + gw_event["body"] = body + gw_event["isBase64Encoded"] = True + + result = app(gw_event, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"size": 5} + + +def test_multipart_multiple_values_same_field(gw_event): + """Test multiple values for the same field name are collected as list.""" + from aws_lambda_powertools.event_handler.openapi.params import File, UploadFile + + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/upload") + def upload(file_data: Annotated[List[UploadFile], File()]): + return {"count": len(file_data), "filenames": [f.filename for f in file_data]} + + # Build body with two parts having the same field name + boundary = "----TestBoundary" + raw = ( + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="file_data"; filename="a.txt"\r\n' + f"\r\n" + f"content a\r\n" + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="file_data"; filename="b.txt"\r\n' + f"\r\n" + f"content b\r\n" + f"--{boundary}--\r\n" + ).encode() + + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/upload" + gw_event["headers"]["content-type"] = f"multipart/form-data; boundary={boundary}" + gw_event["body"] = base64.b64encode(raw).decode() + gw_event["isBase64Encoded"] = True + + result = app(gw_event, {}) + assert result["statusCode"] == 200 + parsed = json.loads(result["body"]) + assert parsed["count"] == 2 + assert parsed["filenames"] == ["a.txt", "b.txt"] + + +def test_multipart_three_values_same_field(gw_event): + """Test three or more values for same field name builds onto existing list.""" + from aws_lambda_powertools.event_handler.openapi.params import File, UploadFile + + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/upload") + def upload(file_data: Annotated[List[UploadFile], File()]): + return {"count": len(file_data), "filenames": [f.filename for f in file_data]} + + boundary = "----TestBoundary" + raw = ( + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="file_data"; filename="a.txt"\r\n' + f"\r\n" + f"aaa\r\n" + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="file_data"; filename="b.txt"\r\n' + f"\r\n" + f"bbb\r\n" + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="file_data"; filename="c.txt"\r\n' + f"\r\n" + f"ccc\r\n" + f"--{boundary}--\r\n" + ).encode() + + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/upload" + gw_event["headers"]["content-type"] = f"multipart/form-data; boundary={boundary}" + gw_event["body"] = base64.b64encode(raw).decode() + gw_event["isBase64Encoded"] = True + + result = app(gw_event, {}) + assert result["statusCode"] == 200 + parsed = json.loads(result["body"]) + assert parsed["count"] == 3 + assert parsed["filenames"] == ["a.txt", "b.txt", "c.txt"] + + +def test_multipart_part_without_headers_separator(gw_event): + """Test that a malformed part missing the header/body separator is skipped.""" + from aws_lambda_powertools.event_handler.openapi.params import File, UploadFile + + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/upload") + def upload(file_data: Annotated[UploadFile, File()]): + return {"filename": file_data.filename} + + # Build a body with one malformed part (no \r\n\r\n) and one valid part + boundary = "----TestBoundary" + raw = ( + f"--{boundary}\r\n" + f"This part has no header separator at all\r\n" + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="file_data"; filename="good.txt"\r\n' + f"\r\n" + f"good content\r\n" + f"--{boundary}--\r\n" + ).encode() + + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/upload" + gw_event["headers"]["content-type"] = f"multipart/form-data; boundary={boundary}" + gw_event["body"] = base64.b64encode(raw).decode() + gw_event["isBase64Encoded"] = True + + result = app(gw_event, {}) + assert result["statusCode"] == 200 + parsed = json.loads(result["body"]) + assert parsed["filename"] == "good.txt" + + +def test_multipart_part_without_field_name(gw_event): + """Test that a part missing the name parameter in Content-Disposition is skipped.""" + from aws_lambda_powertools.event_handler.openapi.params import File, UploadFile + + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/upload") + def upload(file_data: Annotated[UploadFile, File()]): + return {"filename": file_data.filename} + + # Build a body with one part that has no name= param and one valid part + boundary = "----TestBoundary" + raw = ( + f"--{boundary}\r\n" + f"Content-Disposition: form-data\r\n" + f"\r\n" + f"orphan content\r\n" + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="file_data"; filename="valid.txt"\r\n' + f"\r\n" + f"valid content\r\n" + f"--{boundary}--\r\n" + ).encode() + + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/upload" + gw_event["headers"]["content-type"] = f"multipart/form-data; boundary={boundary}" + gw_event["body"] = base64.b64encode(raw).decode() + gw_event["isBase64Encoded"] = True + + result = app(gw_event, {}) + assert result["statusCode"] == 200 + parsed = json.loads(result["body"]) + assert parsed["filename"] == "valid.txt" + + +def test_upload_file_validate_error(): + """Test UploadFile._validate raises ValueError for non-UploadFile values.""" + from aws_lambda_powertools.event_handler.openapi.params import UploadFile + + with pytest.raises(ValueError, match="Expected UploadFile, got str"): + UploadFile._validate("not an upload file") + + with pytest.raises(ValueError, match="Expected UploadFile, got int"): + UploadFile._validate(42) + + +def test_multipart_unclosed_quote_in_header(): + """Test that _extract_header_param returns None when quote is unclosed.""" + from aws_lambda_powertools.event_handler.middlewares.openapi_validation import _extract_header_param + + # name=" is present but closing quote is missing + result = _extract_header_param('Content-Disposition: form-data; name="broken', "name") + assert result is None + + +def test_multipart_generic_parse_error(gw_event): + """Test that non-ValueError exceptions during multipart parsing produce 422.""" + from unittest.mock import patch + + from aws_lambda_powertools.event_handler.openapi.params import File, UploadFile + + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/upload") + def upload(file_data: Annotated[UploadFile, File()]): + return {"filename": file_data.filename} + + body_b64, content_type = _build_multipart_body( + [{"name": "file_data", "value": b"data", "filename": "test.txt"}], + ) + + gw_event["httpMethod"] = "POST" + gw_event["path"] = "/upload" + gw_event["headers"]["content-type"] = content_type + gw_event["body"] = body_b64 + gw_event["isBase64Encoded"] = True + + # Patch _parse_multipart_body to raise a non-ValueError (e.g. TypeError) + with patch( + "aws_lambda_powertools.event_handler.middlewares.openapi_validation._parse_multipart_body", + side_effect=TypeError("unexpected type"), + ): + result = app(gw_event, {}) + assert result["statusCode"] == 422 + body = json.loads(result["body"]) + assert body["detail"][0]["type"] == "multipart_invalid" + + +# ---------- Cookie parameter tests ---------- + + +def test_cookie_param_basic(gw_event): + """Test basic cookie parameter extraction from REST API v1 (Cookie header).""" + from aws_lambda_powertools.event_handler.openapi.params import Cookie + + app = APIGatewayRestResolver(enable_validation=True) + + @app.get("/me") + def handler(session_id: Annotated[str, Cookie()]): + return {"session_id": session_id} + + gw_event["path"] = "/me" + gw_event["headers"]["cookie"] = "session_id=abc123; theme=dark" + # Clear multiValueHeaders to avoid interference + gw_event.pop("multiValueHeaders", None) + + result = app(gw_event, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["session_id"] == "abc123" + + +def test_cookie_param_missing_required(gw_event): + """Test that a missing required cookie returns 422.""" + from aws_lambda_powertools.event_handler.openapi.params import Cookie + + app = APIGatewayRestResolver(enable_validation=True) + + @app.get("/me") + def handler(session_id: Annotated[str, Cookie()]): + return {"session_id": session_id} + + gw_event["path"] = "/me" + gw_event["headers"]["cookie"] = "theme=dark" + gw_event.pop("multiValueHeaders", None) + + result = app(gw_event, {}) + assert result["statusCode"] == 422 + + +def test_cookie_param_with_default(gw_event): + """Test cookie parameter with a default value when cookie is absent.""" + from aws_lambda_powertools.event_handler.openapi.params import Cookie + + app = APIGatewayRestResolver(enable_validation=True) + + @app.get("/me") + def handler(theme: Annotated[str, Cookie()] = "light"): + return {"theme": theme} + + gw_event["path"] = "/me" + gw_event["headers"].pop("cookie", None) + gw_event.pop("multiValueHeaders", None) + + result = app(gw_event, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["theme"] == "light" + + +def test_cookie_param_multiple_cookies(gw_event): + """Test extracting multiple cookie parameters.""" + from aws_lambda_powertools.event_handler.openapi.params import Cookie + + app = APIGatewayRestResolver(enable_validation=True) + + @app.get("/me") + def handler( + session_id: Annotated[str, Cookie()], + theme: Annotated[str, Cookie()] = "light", + ): + return {"session_id": session_id, "theme": theme} + + gw_event["path"] = "/me" + gw_event["headers"]["cookie"] = "session_id=abc123; theme=dark" + gw_event.pop("multiValueHeaders", None) + + result = app(gw_event, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["session_id"] == "abc123" + assert body["theme"] == "dark" + + +def test_cookie_param_int_validation(gw_event): + """Test cookie parameter with int type validation.""" + from aws_lambda_powertools.event_handler.openapi.params import Cookie + + app = APIGatewayRestResolver(enable_validation=True) + + @app.get("/me") + def handler(visits: Annotated[int, Cookie()]): + return {"visits": visits} + + gw_event["path"] = "/me" + gw_event["headers"]["cookie"] = "visits=42" + gw_event.pop("multiValueHeaders", None) + + result = app(gw_event, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["visits"] == 42 + + # Invalid int + gw_event["headers"]["cookie"] = "visits=not_a_number" + result = app(gw_event, {}) + assert result["statusCode"] == 422 + + +def test_cookie_param_http_api_v2(gw_event_http): + """Test cookie parameter with HTTP API v2 (dedicated cookies field).""" + from aws_lambda_powertools.event_handler.openapi.params import Cookie + + app = APIGatewayHttpResolver(enable_validation=True) + + @app.get("/me") + def handler(session_id: Annotated[str, Cookie()]): + return {"session_id": session_id} + + gw_event_http["rawPath"] = "/me" + gw_event_http["requestContext"]["http"]["method"] = "GET" + gw_event_http["cookies"] = ["session_id=xyz789", "theme=dark"] + + result = app(gw_event_http, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["session_id"] == "xyz789" + + +def test_cookie_param_lambda_function_url(gw_event_lambda_url): + """Test cookie parameter with Lambda Function URL (v2 format).""" + from aws_lambda_powertools.event_handler.openapi.params import Cookie + + app = LambdaFunctionUrlResolver(enable_validation=True) + + @app.get("/me") + def handler(session_id: Annotated[str, Cookie()]): + return {"session_id": session_id} + + gw_event_lambda_url["rawPath"] = "/me" + gw_event_lambda_url["requestContext"]["http"]["method"] = "GET" + gw_event_lambda_url["cookies"] = ["session_id=fn_url_abc"] + + result = app(gw_event_lambda_url, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["session_id"] == "fn_url_abc" + + +def test_cookie_param_alb(gw_event_alb): + """Test cookie parameter with ALB (Cookie header in multiValueHeaders).""" + from aws_lambda_powertools.event_handler.openapi.params import Cookie + + app = ALBResolver(enable_validation=True) + + @app.get("/me") + def handler(session_id: Annotated[str, Cookie()]): + return {"session_id": session_id} + + gw_event_alb["path"] = "/me" + gw_event_alb["httpMethod"] = "GET" + gw_event_alb["multiValueHeaders"]["cookie"] = ["session_id=alb_abc"] + + result = app(gw_event_alb, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["session_id"] == "alb_abc" + + +def test_cookie_param_openapi_schema(): + """Test that Cookie() generates correct OpenAPI schema with in=cookie.""" + from aws_lambda_powertools.event_handler.openapi.params import Cookie + + app = APIGatewayRestResolver(enable_validation=True) + + @app.get("/me") + def handler( + session_id: Annotated[str, Cookie(description="Session identifier")], + theme: Annotated[str, Cookie(description="UI theme")] = "light", + ): + return {"session_id": session_id} + + schema = app.get_openapi_schema() + schema_dict = schema.model_dump(mode="json", by_alias=True, exclude_none=True) + + path = schema_dict["paths"]["/me"]["get"] + params = path["parameters"] + + cookie_params = [p for p in params if p["in"] == "cookie"] + assert len(cookie_params) == 2 + + session_param = next(p for p in cookie_params if p["name"] == "session_id") + assert session_param["required"] is True + assert session_param["description"] == "Session identifier" + + theme_param = next(p for p in cookie_params if p["name"] == "theme") + assert theme_param.get("required") is not True + assert theme_param["description"] == "UI theme" + + +def test_cookie_param_with_query_and_header(gw_event): + """Test that Cookie(), Query(), and Header() work together.""" + from aws_lambda_powertools.event_handler.openapi.params import Cookie + + app = APIGatewayRestResolver(enable_validation=True) + + @app.get("/me") + def handler( + user_id: Annotated[str, Query()], + x_request_id: Annotated[str, Header()], + session_id: Annotated[str, Cookie()], + ): + return { + "user_id": user_id, + "x_request_id": x_request_id, + "session_id": session_id, + } + + gw_event["path"] = "/me" + gw_event["queryStringParameters"] = {"user_id": "u123"} + gw_event["multiValueQueryStringParameters"] = {"user_id": ["u123"]} + gw_event["headers"]["x-request-id"] = "req-456" + gw_event["multiValueHeaders"] = {"x-request-id": ["req-456"], "cookie": ["session_id=sess-789"]} + gw_event["headers"]["cookie"] = "session_id=sess-789" + + result = app(gw_event, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["user_id"] == "u123" + assert body["x_request_id"] == "req-456" + assert body["session_id"] == "sess-789" + + +def test_cookie_param_no_cookies_in_request(gw_event): + """Test that empty cookies dict is handled gracefully.""" + from aws_lambda_powertools.event_handler.openapi.params import Cookie + + app = APIGatewayRestResolver(enable_validation=True) + + @app.get("/me") + def handler(theme: Annotated[str, Cookie()] = "light"): + return {"theme": theme} + + gw_event["path"] = "/me" + gw_event["headers"] = {} + gw_event.pop("multiValueHeaders", None) + + result = app(gw_event, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["theme"] == "light" + + +def test_cookie_param_vpc_lattice_v2(gw_event_vpc_lattice): + """Test cookie parameter with VPC Lattice v2 (headers are lists).""" + from aws_lambda_powertools.event_handler.openapi.params import Cookie + + app = VPCLatticeV2Resolver(enable_validation=True) + + @app.get("/me") + def handler(session_id: Annotated[str, Cookie()]): + return {"session_id": session_id} + + gw_event_vpc_lattice["method"] = "GET" + gw_event_vpc_lattice["path"] = "/me" + gw_event_vpc_lattice["headers"]["cookie"] = ["session_id=lattice_abc"] + + result = app(gw_event_vpc_lattice, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["session_id"] == "lattice_abc" + + +def test_cookie_param_vpc_lattice_v1(gw_event_vpc_lattice_v1): + """Test cookie parameter with VPC Lattice v1 (comma-separated headers).""" + from aws_lambda_powertools.event_handler.openapi.params import Cookie + + app = VPCLatticeResolver(enable_validation=True) + + @app.get("/me") + def handler(session_id: Annotated[str, Cookie()]): + return {"session_id": session_id} + + gw_event_vpc_lattice_v1["method"] = "GET" + gw_event_vpc_lattice_v1["raw_path"] = "/me" + gw_event_vpc_lattice_v1["headers"]["cookie"] = "session_id=lattice_v1_abc" + + result = app(gw_event_vpc_lattice_v1, {}) + 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_depends.py b/tests/functional/event_handler/required_dependencies/test_depends.py new file mode 100644 index 00000000000..d5e49e07cdd --- /dev/null +++ b/tests/functional/event_handler/required_dependencies/test_depends.py @@ -0,0 +1,509 @@ +"""Tests for the Depends() dependency injection feature using Annotated.""" + +import json + +import pytest +from typing_extensions import Annotated + +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler.depends import DependencyResolutionError, Depends +from aws_lambda_powertools.event_handler.request import Request +from tests.functional.utils import load_event + +API_GW_V2_EVENT = load_event("apiGatewayProxyV2Event.json") + + +def test_depends_simple(): + """A simple dependency is resolved and injected into the handler.""" + app = APIGatewayHttpResolver() + + def get_greeting() -> str: + return "hello" + + @app.post("/my/path") + def handler(greeting: Annotated[str, Depends(get_greeting)]): + return {"greeting": greeting} + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"greeting": "hello"} + + +def test_depends_nested(): + """Dependencies can depend on other dependencies.""" + app = APIGatewayHttpResolver() + + def get_prefix() -> str: + return "Hello" + + def get_greeting(prefix: Annotated[str, Depends(get_prefix)]) -> str: + return f"{prefix}, world!" + + @app.post("/my/path") + def handler(greeting: Annotated[str, Depends(get_greeting)]): + return {"greeting": greeting} + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"greeting": "Hello, world!"} + + +def test_depends_cache_per_invocation(): + """Same dependency used twice in one invocation is only resolved once (use_cache=True).""" + app = APIGatewayHttpResolver() + call_count = 0 + + def get_config() -> dict: + nonlocal call_count + call_count += 1 + return {"key": "value"} + + def get_a(config: Annotated[dict, Depends(get_config)]) -> str: + return config["key"] + + def get_b(config: Annotated[dict, Depends(get_config)]) -> str: + return config["key"] + + @app.post("/my/path") + def handler(a: Annotated[str, Depends(get_a)], b: Annotated[str, Depends(get_b)]): + return {"a": a, "b": b} + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert call_count == 1 # get_config called once despite being used by both get_a and get_b + + +def test_depends_no_cache(): + """use_cache=False resolves every time.""" + app = APIGatewayHttpResolver() + call_count = 0 + + def get_value() -> int: + nonlocal call_count + call_count += 1 + return call_count + + @app.post("/my/path") + def handler( + a: Annotated[int, Depends(get_value, use_cache=False)], + b: Annotated[int, Depends(get_value, use_cache=False)], + ): + return {"a": a, "b": b} + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert call_count == 2 + + +def test_depends_with_request(): + """A dependency can receive the Request object.""" + app = APIGatewayHttpResolver() + + def get_method(request: Request) -> str: + return request.method + + @app.post("/my/path") + def handler(method: Annotated[str, Depends(get_method)]): + return {"method": method} + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"method": "POST"} + + +def test_depends_override(): + """dependency_overrides replaces a dependency callable for testing.""" + app = APIGatewayHttpResolver() + + def get_tenant() -> str: + return "real-tenant" + + @app.post("/my/path") + def handler(tenant: Annotated[str, Depends(get_tenant)]): + return {"tenant": tenant} + + app.dependency_overrides[get_tenant] = lambda: "test-tenant" + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"tenant": "test-tenant"} + + app.dependency_overrides.clear() + + +def test_depends_override_nested(): + """dependency_overrides works for nested dependencies too.""" + app = APIGatewayHttpResolver() + + def get_db_client(): + return "real-db" + + def get_table(db: Annotated[str, Depends(get_db_client)]) -> str: + return f"table-from-{db}" + + @app.post("/my/path") + def handler(table: Annotated[str, Depends(get_table)]): + return {"table": table} + + app.dependency_overrides[get_db_client] = lambda: "mock-db" + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"table": "table-from-mock-db"} + + app.dependency_overrides.clear() + + +def test_depends_multiple_handlers(): + """Dependencies work across different route handlers.""" + app = APIGatewayHttpResolver() + + def get_user() -> str: + return "user-123" + + @app.get("/my/path") + def get_handler(user: Annotated[str, Depends(get_user)]): + return {"user": user, "action": "get"} + + @app.post("/my/path") + def post_handler(user: Annotated[str, Depends(get_user)]): + return {"user": user, "action": "post"} + + # Test POST (matches the event) + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"user": "user-123", "action": "post"} + + +def test_depends_reusable_type_alias(): + """Annotated type aliases can be reused across handlers.""" + app = APIGatewayHttpResolver() + + def get_tenant() -> str: + return "tenant-abc" + + TenantId = Annotated[str, Depends(get_tenant)] + + @app.post("/my/path") + def handler(tenant: TenantId): + return {"tenant": tenant} + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"tenant": "tenant-abc"} + + +def test_handler_without_depends_works_normally(): + """A plain handler with no Depends() params is not affected by DI.""" + app = APIGatewayHttpResolver() + + @app.post("/my/path") + def handler(): + return {"ok": True} + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"ok": True} + + +def test_depends_not_cached_across_invocations(): + """Each app() call resolves dependencies fresh — no cross-request leakage.""" + app = APIGatewayHttpResolver() + call_count = 0 + + def get_counter() -> int: + nonlocal call_count + call_count += 1 + return call_count + + @app.post("/my/path") + def handler(c: Annotated[int, Depends(get_counter)]): + return {"c": c} + + result1 = app(API_GW_V2_EVENT, {}) + result2 = app(API_GW_V2_EVENT, {}) + + assert json.loads(result1["body"]) == {"c": 1} + assert json.loads(result2["body"]) == {"c": 2} + assert call_count == 2 + + +def test_depends_deeply_nested(): + """Three-level dependency chain resolves correctly.""" + app = APIGatewayHttpResolver() + + def get_url() -> str: + return "postgres://localhost" + + def get_conn(url: Annotated[str, Depends(get_url)]) -> str: + return f"conn({url})" + + def get_session(conn: Annotated[str, Depends(get_conn)]) -> str: + return f"session({conn})" + + @app.post("/my/path") + def handler(session: Annotated[str, Depends(get_session)]): + return {"session": session} + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"session": "session(conn(postgres://localhost))"} + + +def test_depends_with_request_reads_headers(): + """A dependency using Request can read actual request headers.""" + app = APIGatewayHttpResolver() + + def get_user_agent(request: Request) -> str: + return request.headers.get("user-agent", "unknown") + + @app.post("/my/path") + def handler(ua: Annotated[str, Depends(get_user_agent)]): + return {"ua": ua} + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert isinstance(json.loads(result["body"])["ua"], str) + + +def test_depends_returning_none(): + """A dependency can return None without breaking.""" + app = APIGatewayHttpResolver() + + def get_nothing() -> None: + return None + + @app.post("/my/path") + def handler(val: Annotated[None, Depends(get_nothing)]): + return {"val": val} + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"val": None} + + +def test_depends_exception_raises_dependency_resolution_error(): + """If a dependency raises, a DependencyResolutionError wraps the original exception.""" + app = APIGatewayHttpResolver() + + def broken() -> str: + raise ValueError("boom") + + @app.post("/my/path") + def handler(val: Annotated[str, Depends(broken)]): + return {"val": val} + + with pytest.raises(DependencyResolutionError, match="broken.*boom"): + app(API_GW_V2_EVENT, {}) + + +def test_depends_non_callable_raises_dependency_resolution_error(): + """Passing a non-callable to Depends() raises DependencyResolutionError immediately.""" + with pytest.raises(DependencyResolutionError, match="requires a callable"): + Depends("not_a_function") # type: ignore + + with pytest.raises(DependencyResolutionError, match="requires a callable"): + Depends(42) # type: ignore + + with pytest.raises(DependencyResolutionError, match="requires a callable"): + Depends(None) # type: ignore + + +def test_depends_accepts_lambda(): + """Depends() works with a lambda as the dependency.""" + app = APIGatewayHttpResolver() + + @app.post("/my/path") + def handler(val: Annotated[str, Depends(lambda: "from-lambda")]): + return {"val": val} + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"val": "from-lambda"} + + +def test_depends_accepts_class_with_call(): + """Depends() works with a class that implements __call__.""" + app = APIGatewayHttpResolver() + + class TenantProvider: + def __call__(self) -> str: + return "tenant-from-class" + + @app.post("/my/path") + def handler(tenant: Annotated[str, Depends(TenantProvider())]): + return {"tenant": tenant} + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"tenant": "tenant-from-class"} + + +def test_depends_accepts_class_as_factory(): + """Depends() works with a class itself (constructor as callable).""" + app = APIGatewayHttpResolver() + + class Config: + def __init__(self): + self.region = "us-east-1" + + @app.post("/my/path") + def handler(config: Annotated[Config, Depends(Config)]): + return {"region": config.region} + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"region": "us-east-1"} + + +def test_depends_with_unresolvable_annotations_is_ignored(): + """A handler whose annotations cannot be resolved by get_type_hints is treated as having no deps.""" + app = APIGatewayHttpResolver() + + # Build a function with broken annotations that get_type_hints cannot resolve. + # The param has a default so the handler can still be called without it. + def make_handler(): + def handler(x: "CompletelyBogusType" = None): # noqa: F821 + return {"ok": True} + + return handler + + app.post("/my/path")(make_handler()) + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"ok": True} + + +def test_depends_without_request_does_not_inject(): + """A dependency that does NOT declare Request still works when request is available.""" + app = APIGatewayHttpResolver() + + def get_static() -> str: + return "no-request-needed" + + @app.post("/my/path") + def handler(val: Annotated[str, Depends(get_static)]): + return {"val": val} + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"val": "no-request-needed"} + + +def test_depends_with_broken_type_hints_on_dependency(): + """A dependency callable with broken annotations still resolves (get_type_hints fails gracefully).""" + app = APIGatewayHttpResolver() + + # Create a callable whose annotations reference a nonexistent type + # so get_type_hints() will raise inside solve_dependencies + broken_dep = type( + "BrokenDep", + (), + { + "__call__": lambda self: "it-works", + "__annotations__": {"x": "NonExistentType"}, + "__module__": __name__, + }, + )() + + @app.post("/my/path") + def handler(val: Annotated[str, Depends(broken_dep)]): + return {"val": val} + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"val": "it-works"} + + +# --------------------------------------------------------------------------- +# request.context — bridge between middleware and Depends() +# --------------------------------------------------------------------------- + + +def test_depends_request_context_writable(): + """Dependencies can write to request.context and handlers can read it.""" + app = APIGatewayHttpResolver() + + def set_tenant(request: Request) -> str: + tenant = request.headers.get("x-tenant-id", "default") + request.context["tenant"] = tenant + return tenant + + @app.post("/my/path") + def handler(tenant: Annotated[str, Depends(set_tenant)], request: Request): + return {"tenant": tenant, "from_context": request.context.get("tenant")} + + event = {**API_GW_V2_EVENT, "headers": {**API_GW_V2_EVENT.get("headers", {}), "x-tenant-id": "acme-corp"}} + result = app(event, {}) + + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["tenant"] == "acme-corp" + assert body["from_context"] == "acme-corp" + + +def test_depends_request_context_bridges_middleware(): + """Middleware writes to app.context, Depends() reads via request.context.""" + app = APIGatewayHttpResolver() + + def auth_middleware(app, next_middleware): + app.append_context(user="admin-user") + return next_middleware(app) + + app.use(middlewares=[auth_middleware]) + + def get_current_user(request: Request) -> str: + return request.context["user"] + + @app.post("/my/path") + def handler(user: Annotated[str, Depends(get_current_user)]): + return {"user": user} + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"user": "admin-user"} + + +def test_depends_request_context_with_router(): + """request.context works when routes come from an included Router.""" + from aws_lambda_powertools.event_handler.api_gateway import Router + + app = APIGatewayHttpResolver() + router = Router() + + def mw(app, next_middleware): + app.append_context(role="admin") + return next_middleware(app) + + app.use(middlewares=[mw]) + + def get_role(request: Request) -> str: + return request.context["role"] + + @router.post("/my/path") + def handler(role: Annotated[str, Depends(get_role)]): + return {"role": role} + + app.include_router(router) + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"role": "admin"} + + +def test_depends_request_resolved_event(): + """Dependencies can access the full event via request.resolved_event.""" + app = APIGatewayHttpResolver() + + def get_path(request: Request) -> str: + return request.resolved_event.path + + @app.post("/my/path") + def handler(path: Annotated[str, Depends(get_path)]): + return {"path": path} + + result = app(API_GW_V2_EVENT, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["path"] == "/my/path" diff --git a/tests/functional/event_handler/required_dependencies/test_registered_api_adapter_async.py b/tests/functional/event_handler/required_dependencies/test_registered_api_adapter_async.py new file mode 100644 index 00000000000..10d5b4602f0 --- /dev/null +++ b/tests/functional/event_handler/required_dependencies/test_registered_api_adapter_async.py @@ -0,0 +1,335 @@ +import asyncio +import re +from typing import cast + +import pytest +from typing_extensions import Annotated + +from aws_lambda_powertools.event_handler import content_types +from aws_lambda_powertools.event_handler.api_gateway import ( + APIGatewayHttpResolver, + ApiGatewayResolver, + APIGatewayRestResolver, + BaseRouter, + ProxyEventType, + Response, + Route, +) +from aws_lambda_powertools.event_handler.depends import Depends +from aws_lambda_powertools.event_handler.middlewares.async_utils import _registered_api_adapter_async +from aws_lambda_powertools.event_handler.request import Request +from tests.functional.utils import load_event + +API_REST_EVENT = load_event("apiGatewayProxyEvent.json") +API_RESTV2_EVENT = load_event("apiGatewayProxyV2Event_GET.json") + + +def _setup_resolver_context(app: ApiGatewayResolver, event: dict) -> None: + """Populate the resolver context the same way resolve() does, without calling the full chain.""" + BaseRouter.current_event = app._to_proxy_event(cast(dict, event)) + BaseRouter.lambda_context = {} + + +@pytest.mark.parametrize( + "app, event", + [ + (ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent), API_REST_EVENT), + (APIGatewayRestResolver(), API_REST_EVENT), + (APIGatewayHttpResolver(), API_RESTV2_EVENT), + ], +) +def test_sync_handler_returns_response(app: ApiGatewayResolver, event): + # GIVEN a sync route handler + @app.get("/my/path") + def get_lambda(): + return Response(200, content_types.TEXT_HTML, "sync response") + + # WHEN resolving the event through the normal chain + result = app(event, {}) + + # THEN the sync handler is called and returns correctly + assert result["statusCode"] == 200 + assert result["body"] == "sync response" + + +@pytest.mark.parametrize( + "app, event", + [ + (ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent), API_REST_EVENT), + (APIGatewayRestResolver(), API_REST_EVENT), + (APIGatewayHttpResolver(), API_RESTV2_EVENT), + ], +) +def test_async_handler_is_awaited(app: ApiGatewayResolver, event): + # GIVEN an async route handler registered on the resolver + @app.get("/my/path") + async def get_lambda(): + return Response(200, content_types.TEXT_HTML, "async response") + + # WHEN populating context and calling the async adapter directly + _setup_resolver_context(app, event) + app.append_context(_route_args={}) + + result = asyncio.run( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN the async handler is awaited and returns correctly + assert result.status_code == 200 + assert result.body == "async response" + + +@pytest.mark.parametrize( + "app, event", + [ + (ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent), API_REST_EVENT), + (APIGatewayRestResolver(), API_REST_EVENT), + (APIGatewayHttpResolver(), API_RESTV2_EVENT), + ], +) +def test_sync_handler_through_adapter(app: ApiGatewayResolver, event): + # GIVEN a sync route handler + @app.get("/my/path") + def get_lambda(): + return Response(200, content_types.TEXT_HTML, "sync via adapter") + + # WHEN calling _registered_api_adapter_async with a sync handler + _setup_resolver_context(app, event) + app.append_context(_route_args={}) + + result = asyncio.run( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN sync handler works through the async adapter without issue + assert result.status_code == 200 + assert result.body == "sync via adapter" + + +@pytest.mark.parametrize( + "app, event", + [ + (ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent), API_REST_EVENT), + (APIGatewayRestResolver(), API_REST_EVENT), + (APIGatewayHttpResolver(), API_RESTV2_EVENT), + ], +) +def test_adapter_passes_route_args_to_async_handler(app: ApiGatewayResolver, event): + # GIVEN an async handler that expects route arguments + async def get_lambda(name: str): + return Response(200, content_types.TEXT_HTML, name) + + # WHEN route_args are set in the context + _setup_resolver_context(app, event) + app.append_context(_route_args={"name": "powertools"}) + + result = asyncio.run( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN the route args are passed to the handler + assert result.status_code == 200 + assert result.body == "powertools" + + +@pytest.mark.parametrize( + "app, event", + [ + (ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent), API_REST_EVENT), + (APIGatewayRestResolver(), API_REST_EVENT), + (APIGatewayHttpResolver(), API_RESTV2_EVENT), + ], +) +def test_adapter_passes_route_args_to_sync_handler(app: ApiGatewayResolver, event): + # GIVEN a sync handler that expects route arguments + def get_lambda(name: str): + return Response(200, content_types.TEXT_HTML, name) + + # WHEN route_args are set in the context + _setup_resolver_context(app, event) + app.append_context(_route_args={"name": "powertools"}) + + result = asyncio.run( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN the route args are passed to the sync handler + assert result.status_code == 200 + assert result.body == "powertools" + + +def test_adapter_converts_dict_response_from_async_handler(): + # GIVEN an async handler that returns a dict (not a Response object) + app = ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent) + + async def get_lambda(): + return {"message": "hello"} + + # WHEN calling through the async adapter + _setup_resolver_context(app, API_REST_EVENT) + app.append_context(_route_args={}) + + result = asyncio.run( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN _to_response normalizes the dict into a Response object + assert result.status_code == 200 + assert result.body is not None + + +def test_adapter_converts_tuple_response_from_async_handler(): + # GIVEN an async handler that returns a (dict, status_code) tuple + app = ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent) + + async def get_lambda(): + return {"created": True}, 201 + + # WHEN calling through the async adapter + _setup_resolver_context(app, API_REST_EVENT) + app.append_context(_route_args={}) + + result = asyncio.run( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN _to_response normalizes the tuple into a Response object + assert result.status_code == 201 + + +def test_adapter_with_no_route_in_context(): + # GIVEN a handler and no _route in context + app = ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent) + + async def get_lambda(): + return Response(200, content_types.TEXT_HTML, "no route") + + # WHEN _route is None in context (default) + _setup_resolver_context(app, API_REST_EVENT) + app.append_context(_route_args={}) + + result = asyncio.run( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN the adapter skips request injection and dependency resolution + assert result.status_code == 200 + assert result.body == "no route" + + +def test_adapter_injects_request_param(): + # GIVEN an async handler that declares a Request parameter + app = APIGatewayHttpResolver() + + async def get_lambda(request: Request): + return Response(200, content_types.TEXT_HTML, request.method) + + # WHEN a Route is present in context with request_param_name not yet checked + _setup_resolver_context(app, API_RESTV2_EVENT) + route = Route( + method="GET", + path="/my/path", + rule=re.compile(r"^/my/path$"), + func=get_lambda, + cors=False, + compress=False, + ) + app.append_context(_route=route, _route_args={}) + + result = asyncio.run( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN the Request object is injected and request_param_name is cached + assert result.status_code == 200 + assert route.request_param_name_checked is True + assert route.request_param_name == "request" + + +def test_adapter_uses_cached_request_param_name(): + # GIVEN a Route where request_param_name was already resolved + app = APIGatewayHttpResolver() + + async def get_lambda(req: Request): + return Response(200, content_types.TEXT_HTML, req.method) + + _setup_resolver_context(app, API_RESTV2_EVENT) + route = Route( + method="GET", + path="/my/path", + rule=re.compile(r"^/my/path$"), + func=get_lambda, + cors=False, + compress=False, + ) + route.request_param_name = "req" + route.request_param_name_checked = True + app.append_context(_route=route, _route_args={}) + + # WHEN calling the adapter a second time (cache hit) + result = asyncio.run( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN it still injects the Request using the cached param name + assert result.status_code == 200 + + +def test_adapter_resolves_dependencies(): + # GIVEN an async handler with Depends() parameters + app = APIGatewayHttpResolver() + + def get_greeting() -> str: + return "hello" + + async def get_lambda(greeting: Annotated[str, Depends(get_greeting)]): + return {"greeting": greeting} + + _setup_resolver_context(app, API_RESTV2_EVENT) + route = Route( + method="GET", + path="/my/path", + rule=re.compile(r"^/my/path$"), + func=get_lambda, + cors=False, + compress=False, + ) + app.append_context(_route=route, _route_args={}) + + # WHEN calling the adapter + result = asyncio.run( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN dependencies are resolved and injected + assert result.status_code == 200 + + +def test_adapter_resolves_dependencies_with_sync_handler(): + # GIVEN a sync handler with Depends() parameters + app = APIGatewayHttpResolver() + + def get_greeting() -> str: + return "hello" + + def get_lambda(greeting: Annotated[str, Depends(get_greeting)]): + return {"greeting": greeting} + + _setup_resolver_context(app, API_RESTV2_EVENT) + route = Route( + method="GET", + path="/my/path", + rule=re.compile(r"^/my/path$"), + func=get_lambda, + cors=False, + compress=False, + ) + app.append_context(_route=route, _route_args={}) + + # WHEN calling the adapter with a sync handler that has dependencies + result = asyncio.run( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN dependencies are resolved and injected for sync handler too + assert result.status_code == 200 diff --git a/tests/functional/event_handler/required_dependencies/test_request.py b/tests/functional/event_handler/required_dependencies/test_request.py new file mode 100644 index 00000000000..b00ae6659ba --- /dev/null +++ b/tests/functional/event_handler/required_dependencies/test_request.py @@ -0,0 +1,669 @@ +"""Tests for the Request object feature (GH #7992). + +Covers: +- ``app.request`` availability in global and route-level middleware +- ``Request`` type-annotation injection in route handlers +- ``Request`` properties: route, path_parameters, method, headers, query_parameters, body +- ``RuntimeError`` when ``app.request`` is accessed outside of resolution +- Backward compatibility: routes without ``Request`` continue to work unchanged +- ``APIGatewayHttpResolver`` and ``ALBResolver`` variants +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from aws_lambda_powertools.event_handler import ( + ALBResolver, + APIGatewayHttpResolver, + APIGatewayRestResolver, + Request, + Response, +) +from tests.functional.utils import load_event + +if TYPE_CHECKING: + from aws_lambda_powertools.event_handler.middlewares import NextMiddleware + +# --------------------------------------------------------------------------- +# Shared test events +# --------------------------------------------------------------------------- + +API_REST_EVENT = load_event("apiGatewayProxyEvent.json") # GET /my/path +API_RESTV2_EVENT = load_event("apiGatewayProxyV2Event_GET.json") + + +def _make_rest_event(path: str, method: str = "GET", path_parameters: dict | None = None, body: str | None = None): + """Build a minimal API Gateway REST (v1) proxy event.""" + return { + "httpMethod": method, + "path": path, + "pathParameters": path_parameters, + "queryStringParameters": None, + "multiValueQueryStringParameters": None, + "headers": {"Content-Type": "application/json", "user-agent": "pytest"}, + "multiValueHeaders": {}, + "body": body, + "isBase64Encoded": False, + "requestContext": {"httpMethod": method, "resourcePath": path}, + "resource": path, + "stageVariables": None, + } + + +# --------------------------------------------------------------------------- +# app.request in global middleware +# --------------------------------------------------------------------------- + + +def test_request_available_in_global_middleware(): + app = APIGatewayRestResolver() + captured: list[Request] = [] + + def capture_middleware(app: APIGatewayRestResolver, next_middleware: NextMiddleware) -> Response: + captured.append(app.request) + return next_middleware(app) + + app.use(middlewares=[capture_middleware]) + + @app.get("/my/path") + def handler(): + return {} + + app(API_REST_EVENT, {}) + + assert len(captured) == 1 + req = captured[0] + assert isinstance(req, Request) + assert req.route == "/my/path" + assert req.method == "GET" + + +def test_request_route_pattern_uses_openapi_format(): + """route property should use {param} OpenAPI notation, not Powertools notation.""" + app = APIGatewayRestResolver() + captured: list[Request] = [] + + def mw(app: APIGatewayRestResolver, next_middleware: NextMiddleware) -> Response: + captured.append(app.request) + return next_middleware(app) + + app.use(middlewares=[mw]) + + @app.get("/applications/") + def handler(application_id: str): + return {} + + event = _make_rest_event( + "/applications/42", + path_parameters={"application_id": "42"}, + ) + app(event, {}) + + assert captured[0].route == "/applications/{application_id}" + + +def test_request_path_parameters_in_middleware(): + app = APIGatewayRestResolver() + captured: list[dict] = [] + + def mw(app: APIGatewayRestResolver, next_middleware: NextMiddleware) -> Response: + captured.append(app.request.path_parameters) + return next_middleware(app) + + app.use(middlewares=[mw]) + + @app.get("/applications/") + def handler(application_id: str): + return {} + + event = _make_rest_event( + "/applications/4da715ee", + path_parameters={"application_id": "4da715ee"}, + ) + app(event, {}) + + assert captured == [{"application_id": "4da715ee"}] + + +def test_request_method_in_middleware(): + app = APIGatewayRestResolver() + methods_seen: list[str] = [] + + def mw(app: APIGatewayRestResolver, next_middleware: NextMiddleware) -> Response: + methods_seen.append(app.request.method) + return next_middleware(app) + + app.use(middlewares=[mw]) + + @app.put("/items/") + def handler(item_id: str): + return {} + + event = _make_rest_event("/items/99", method="PUT", path_parameters={"item_id": "99"}) + app(event, {}) + + assert methods_seen == ["PUT"] + + +def test_request_headers_in_middleware(): + app = APIGatewayRestResolver() + headers_seen: list[dict] = [] + + def mw(app: APIGatewayRestResolver, next_middleware: NextMiddleware) -> Response: + headers_seen.append(app.request.headers) + return next_middleware(app) + + app.use(middlewares=[mw]) + + @app.get("/my/path") + def handler(): + return {} + + app(API_REST_EVENT, {}) + + assert len(headers_seen) == 1 + # headers is a dict (may have varying casing depending on event source) + assert isinstance(headers_seen[0], dict) + + +def test_request_query_parameters_in_middleware(): + app = APIGatewayRestResolver() + captured: list = [] + + def mw(app: APIGatewayRestResolver, next_middleware: NextMiddleware) -> Response: + captured.append(app.request.query_parameters) + return next_middleware(app) + + app.use(middlewares=[mw]) + + @app.get("/search") + def handler(): + return {} + + event = _make_rest_event("/search") + event["queryStringParameters"] = {"q": "powertools"} + app(event, {}) + + assert captured == [{"q": "powertools"}] + + +def test_request_body_in_middleware(): + app = APIGatewayRestResolver() + bodies_seen: list = [] + + def mw(app: APIGatewayRestResolver, next_middleware: NextMiddleware) -> Response: + bodies_seen.append(app.request.body) + return next_middleware(app) + + app.use(middlewares=[mw]) + + @app.post("/items") + def handler(): + return {} + + event = _make_rest_event("/items", method="POST", body='{"name": "widget"}') + event["httpMethod"] = "POST" + app(event, {}) + + assert bodies_seen == ['{"name": "widget"}'] + + +# --------------------------------------------------------------------------- +# Request injection in route handlers via type annotation +# --------------------------------------------------------------------------- + + +def test_request_injected_into_handler(): + app = APIGatewayRestResolver() + + received: list[Request] = [] + + @app.get("/my/path") + def handler(request: Request): + received.append(request) + return {} + + app(API_REST_EVENT, {}) + + assert len(received) == 1 + assert isinstance(received[0], Request) + assert received[0].route == "/my/path" + assert received[0].method == "GET" + + +def test_request_injected_alongside_path_params(): + app = APIGatewayRestResolver() + + received: list[tuple] = [] + + @app.get("/users/") + def handler(user_id: str, request: Request): + received.append((user_id, request)) + return {} + + event = _make_rest_event("/users/123", path_parameters={"user_id": "123"}) + app(event, {}) + + assert len(received) == 1 + user_id, req = received[0] + assert user_id == "123" + assert isinstance(req, Request) + assert req.path_parameters == {"user_id": "123"} + assert req.route == "/users/{user_id}" + + +def test_request_injection_parameter_name_is_flexible(): + """The parameter can be named anything as long as it is annotated as Request.""" + app = APIGatewayRestResolver() + + received: list[Request] = [] + + @app.get("/my/path") + def handler(req: Request): + received.append(req) + return {} + + app(API_REST_EVENT, {}) + + assert received[0].route == "/my/path" + + +def test_handler_without_request_annotation_unaffected(): + """Existing handlers with no Request annotation continue to work identically.""" + app = APIGatewayRestResolver() + + @app.get("/my/path") + def handler(): + return {"ok": True} + + result = app(API_REST_EVENT, {}) + assert result["statusCode"] == 200 + + +def test_handler_with_path_params_only_unaffected(): + """Handlers that only use path params continue to work identically.""" + app = APIGatewayRestResolver() + + @app.get("/users/") + def handler(user_id: str): + return {"id": user_id} + + event = _make_rest_event("/users/42", path_parameters={"user_id": "42"}) + result = app(event, {}) + assert result["statusCode"] == 200 + + +# --------------------------------------------------------------------------- +# Request injection caching (idempotency across multiple calls) +# --------------------------------------------------------------------------- + + +def test_request_injection_works_across_multiple_invocations(): + """Injection must work correctly on repeated calls (cached param name must stay valid).""" + app = APIGatewayRestResolver() + call_count = 0 + + @app.get("/counters/") + def handler(counter_id: str, request: Request): + nonlocal call_count + call_count += 1 + assert request.path_parameters["counter_id"] == counter_id + return {} + + for i in range(3): + event = _make_rest_event(f"/counters/{i}", path_parameters={"counter_id": str(i)}) + result = app(event, {}) + assert result["statusCode"] == 200 + + assert call_count == 3 + + +# --------------------------------------------------------------------------- +# RuntimeError when accessed outside of request resolution +# --------------------------------------------------------------------------- + + +def test_request_raises_before_resolution(): + app = APIGatewayRestResolver() + with pytest.raises(RuntimeError, match="app.request is only available after route resolution"): + _ = app.request + + +# --------------------------------------------------------------------------- +# Route-level middleware also gets app.request +# --------------------------------------------------------------------------- + + +def test_request_available_in_route_level_middleware(): + app = APIGatewayRestResolver() + captured: list[Request] = [] + + def route_mw(app: APIGatewayRestResolver, next_middleware: NextMiddleware) -> Response: + captured.append(app.request) + return next_middleware(app) + + @app.get("/protected/", middlewares=[route_mw]) + def handler(resource_id: str): + return {} + + event = _make_rest_event("/protected/abc", path_parameters={"resource_id": "abc"}) + app(event, {}) + + assert len(captured) == 1 + assert captured[0].route == "/protected/{resource_id}" + assert captured[0].path_parameters == {"resource_id": "abc"} + + +# --------------------------------------------------------------------------- +# Other resolver types +# --------------------------------------------------------------------------- + + +def test_request_available_in_http_resolver_middleware(): + app = APIGatewayHttpResolver() + captured: list[Request] = [] + + def mw(app, next_middleware): + captured.append(app.request) + return next_middleware(app) + + app.use(middlewares=[mw]) + + @app.get("/my/path") + def handler(): + return {} + + app(API_RESTV2_EVENT, {}) + + assert len(captured) == 1 + assert captured[0].method == "GET" + + +def test_request_available_in_alb_middleware(): + alb_event = load_event("albEvent.json") + app = ALBResolver() + captured: list[Request] = [] + + def mw(app, next_middleware): + captured.append(app.request) + return next_middleware(app) + + app.use(middlewares=[mw]) + + # Register a route that matches the ALB event's path + path = alb_event.get("path", "/lambda") + + @app.get(path) + def handler(): + return {} + + app(alb_event, {}) + + assert len(captured) == 1 + assert isinstance(captured[0], Request) + + +# --------------------------------------------------------------------------- +# Router / include_router pattern +# --------------------------------------------------------------------------- + + +def test_request_available_in_middleware_with_include_router(): + """app.request must work in middleware when routes come from an included Router.""" + from aws_lambda_powertools.event_handler.api_gateway import Router + + app = APIGatewayRestResolver() + router = Router() + captured: list[Request] = [] + + def mw(app, next_middleware): + captured.append(app.request) + return next_middleware(app) + + app.use(middlewares=[mw]) + + @router.get("/users/") + def get_user(user_id: str): + return {"id": user_id} + + app.include_router(router) + + event = _make_rest_event("/users/abc", path_parameters={"user_id": "abc"}) + result = app(event, {}) + + assert result["statusCode"] == 200 + assert len(captured) == 1 + assert captured[0].route == "/users/{user_id}" + assert captured[0].path_parameters == {"user_id": "abc"} + + +def test_request_injected_in_handler_with_include_router(): + """Request injection via type annotation must work when routes come from an included Router.""" + from aws_lambda_powertools.event_handler.api_gateway import Router + + app = APIGatewayRestResolver() + router = Router() + received: list[Request] = [] + + @router.get("/items/") + def get_item(item_id: str, request: Request): + received.append(request) + return {"id": item_id} + + app.include_router(router) + + event = _make_rest_event("/items/xyz", path_parameters={"item_id": "xyz"}) + result = app(event, {}) + + assert result["statusCode"] == 200 + assert len(received) == 1 + assert received[0].route == "/items/{item_id}" + assert received[0].path_parameters == {"item_id": "xyz"} + + +# --------------------------------------------------------------------------- +# Proxy+ use case (the original issue scenario) +# --------------------------------------------------------------------------- + + +def test_request_resolves_path_params_from_proxy_plus_event(): + """When API GW uses {proxy+}, app.current_event.pathParameters only has 'proxy'. + But app.request.path_parameters should have the *resolved* params from Powertools routing.""" + app = APIGatewayRestResolver() + captured: list[Request] = [] + + def auth_middleware(app, next_middleware): + captured.append(app.request) + return next_middleware(app) + + app.use(middlewares=[auth_middleware]) + + @app.get("/applications/") + def get_application(application_id: str): + return {"id": application_id} + + @app.put("/applications/") + def put_application(application_id: str): + return {"updated": application_id} + + # Simulate a proxy+ event where API GW only knows about {proxy+} + event = { + "httpMethod": "PUT", + "path": "/applications/4da715ee-79d4-4e52-81cb-1ecc464708fb", + "pathParameters": {"proxy": "4da715ee-79d4-4e52-81cb-1ecc464708fb"}, + "queryStringParameters": None, + "multiValueQueryStringParameters": None, + "headers": {"Content-Type": "application/json"}, + "multiValueHeaders": {}, + "body": None, + "isBase64Encoded": False, + "requestContext": {"httpMethod": "PUT", "resourcePath": "/applications/{proxy+}"}, + "resource": "/applications/{proxy+}", + "stageVariables": None, + } + + result = app(event, {}) + + assert result["statusCode"] == 200 + assert len(captured) == 1 + + req = captured[0] + # Middleware sees the resolved route, NOT the proxy+ pattern + assert req.route == "/applications/{application_id}" + assert req.path_parameters == {"application_id": "4da715ee-79d4-4e52-81cb-1ecc464708fb"} + assert req.method == "PUT" + + +# --------------------------------------------------------------------------- +# Missing coverage: json_body, query_parameters=None, request caching +# --------------------------------------------------------------------------- + + +def test_request_json_body_in_middleware(): + app = APIGatewayRestResolver() + bodies_seen: list = [] + + def mw(app: APIGatewayRestResolver, next_middleware): + bodies_seen.append(app.request.json_body) + return next_middleware(app) + + app.use(middlewares=[mw]) + + @app.post("/items") + def handler(): + return {} + + event = _make_rest_event("/items", method="POST", body='{"name": "widget"}') + app(event, {}) + + assert bodies_seen == [{"name": "widget"}] + + +def test_request_query_parameters_empty(): + """When no query string parameters are present, query_parameters returns empty or None.""" + app = APIGatewayRestResolver() + captured: list = [] + + def mw(app: APIGatewayRestResolver, next_middleware): + captured.append(app.request.query_parameters) + return next_middleware(app) + + app.use(middlewares=[mw]) + + @app.get("/my/path") + def handler(): + return {} + + event = _make_rest_event("/my/path") + app(event, {}) + + # No query params present — should be falsy (empty dict or None depending on event source) + assert not captured[0] + + +def test_request_is_cached_across_multiple_accesses(): + """Accessing app.request multiple times in the same invocation returns the same object.""" + app = APIGatewayRestResolver() + ids_seen: list[int] = [] + + def mw(app: APIGatewayRestResolver, next_middleware): + ids_seen.append(id(app.request)) + ids_seen.append(id(app.request)) + return next_middleware(app) + + app.use(middlewares=[mw]) + + @app.get("/my/path") + def handler(request: Request): + ids_seen.append(id(request)) + return {} + + app(API_REST_EVENT, {}) + + # All accesses should return the same cached instance + assert len(ids_seen) == 3 + assert ids_seen[0] == ids_seen[1] == ids_seen[2] + + +# --------------------------------------------------------------------------- +# resolved_event — full Powertools proxy event access +# --------------------------------------------------------------------------- + + +def test_request_resolved_event_exposes_full_event(): + """resolved_event should return the full BaseProxyEvent with all helpers.""" + app = APIGatewayRestResolver() + captured: list[Request] = [] + + def mw(app: APIGatewayRestResolver, next_middleware): + captured.append(app.request) + return next_middleware(app) + + app.use(middlewares=[mw]) + + @app.get("/my/path") + def handler(): + return {} + + app(API_REST_EVENT, {}) + + req = captured[0] + resolved = req.resolved_event + + # resolved_event should be the same object as app.current_event + assert resolved is not None + assert resolved.http_method == "GET" + # Should have helper methods not available on Request directly + assert hasattr(resolved, "get_header_value") + assert hasattr(resolved, "get_query_string_value") + + +def test_request_resolved_event_provides_cookies_and_path(): + """resolved_event gives access to path and properties not on Request.""" + app = APIGatewayRestResolver() + captured: list[Request] = [] + + def mw(app: APIGatewayRestResolver, next_middleware): + captured.append(app.request) + return next_middleware(app) + + app.use(middlewares=[mw]) + + @app.get("/items/") + def handler(item_id: str): + return {} + + event = _make_rest_event("/items/42", path_parameters={"item_id": "42"}) + app(event, {}) + + resolved = captured[0].resolved_event + assert resolved.path == "/items/42" + + +# --------------------------------------------------------------------------- +# context — shared resolver context (app.context) +# --------------------------------------------------------------------------- + + +def test_request_context_shares_app_context(): + """request.context should be the same dict as app.context.""" + app = APIGatewayRestResolver() + + def mw(app: APIGatewayRestResolver, next_middleware): + app.append_context(user="test-user") + return next_middleware(app) + + app.use(middlewares=[mw]) + + @app.get("/my/path") + def handler(request: Request): + return {"user": request.context.get("user")} + + result = app(API_REST_EVENT, {}) + assert result["statusCode"] == 200 + import json + + assert json.loads(result["body"]) == {"user": "test-user"} 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/_pydantic/test_idempotency_pydantic_json_serialization.py b/tests/functional/idempotency/_pydantic/test_idempotency_pydantic_json_serialization.py new file mode 100644 index 00000000000..624e4239e98 --- /dev/null +++ b/tests/functional/idempotency/_pydantic/test_idempotency_pydantic_json_serialization.py @@ -0,0 +1,185 @@ +""" +Test for issue #8065: @idempotent_function fails with non-JSON-serializable types in Pydantic models + +Bug: _prepare_data() calls model_dump() without mode="json", which doesn't +serialize UUIDs, dates, datetimes, Decimals, and Paths to JSON-compatible strings. +""" + +from datetime import date, datetime +from decimal import Decimal +from pathlib import PurePosixPath +from uuid import UUID + +from pydantic import BaseModel + +from aws_lambda_powertools.utilities.idempotency import ( + IdempotencyConfig, + idempotent_function, +) +from aws_lambda_powertools.utilities.idempotency.persistence.base import ( + BasePersistenceLayer, + DataRecord, +) +from tests.functional.idempotency.utils import hash_idempotency_key + +TESTS_MODULE_PREFIX = "test-func.tests.functional.idempotency._pydantic.test_idempotency_pydantic_json_serialization" + + +class MockPersistenceLayer(BasePersistenceLayer): + def __init__(self, expected_idempotency_key: str): + self.expected_idempotency_key = expected_idempotency_key + super().__init__() + + def _put_record(self, data_record: DataRecord) -> None: + assert data_record.idempotency_key == self.expected_idempotency_key + + def _update_record(self, data_record: DataRecord) -> None: + assert data_record.idempotency_key == self.expected_idempotency_key + + def _get_record(self, idempotency_key) -> DataRecord: ... + + def _delete_record(self, data_record: DataRecord) -> None: ... + + +# --- Models --- + + +class PaymentWithUUID(BaseModel): + payment_id: UUID + customer_id: str + + +class EventWithDate(BaseModel): + event_id: str + event_date: date + + +class OrderWithDatetime(BaseModel): + order_id: str + created_at: datetime + + +class InvoiceWithDecimal(BaseModel): + invoice_id: str + amount: Decimal + + +class ConfigWithPath(BaseModel): + config_id: str + file_path: PurePosixPath + + +def test_idempotent_function_with_uuid(): + # GIVEN + config = IdempotencyConfig(use_local_cache=True) + payment_uuid = UUID("12345678-1234-5678-1234-567812345678") + mock_event = {"payment_id": str(payment_uuid), "customer_id": "c-456"} + idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_with_uuid..collect_payment#{hash_idempotency_key(mock_event)}" # noqa E501 + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + + @idempotent_function( + data_keyword_argument="payment", + persistence_store=persistence_layer, + config=config, + ) + def collect_payment(payment: PaymentWithUUID) -> dict: + return {"status": "ok"} + + # WHEN + payment = PaymentWithUUID(payment_id=payment_uuid, customer_id="c-456") + result = collect_payment(payment=payment) + + # THEN + assert result == {"status": "ok"} + + +def test_idempotent_function_with_date(): + # GIVEN + config = IdempotencyConfig(use_local_cache=True) + mock_event = {"event_id": "evt-001", "event_date": "2024-03-20"} + idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_with_date..process_event#{hash_idempotency_key(mock_event)}" # noqa E501 + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + + @idempotent_function( + data_keyword_argument="event", + persistence_store=persistence_layer, + config=config, + ) + def process_event(event: EventWithDate) -> dict: + return {"status": "ok"} + + # WHEN + event = EventWithDate(event_id="evt-001", event_date=date(2024, 3, 20)) + result = process_event(event=event) + + # THEN + assert result == {"status": "ok"} + + +def test_idempotent_function_with_datetime(): + # GIVEN + config = IdempotencyConfig(use_local_cache=True) + mock_event = {"order_id": "ord-001", "created_at": "2024-03-20T14:30:00"} + idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_with_datetime..process_order#{hash_idempotency_key(mock_event)}" # noqa E501 + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + + @idempotent_function( + data_keyword_argument="order", + persistence_store=persistence_layer, + config=config, + ) + def process_order(order: OrderWithDatetime) -> dict: + return {"status": "ok"} + + # WHEN + order = OrderWithDatetime(order_id="ord-001", created_at=datetime(2024, 3, 20, 14, 30, 0)) + result = process_order(order=order) + + # THEN + assert result == {"status": "ok"} + + +def test_idempotent_function_with_decimal(): + # GIVEN + config = IdempotencyConfig(use_local_cache=True) + mock_event = {"invoice_id": "inv-001", "amount": "199.99"} + idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_with_decimal..process_invoice#{hash_idempotency_key(mock_event)}" # noqa E501 + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + + @idempotent_function( + data_keyword_argument="invoice", + persistence_store=persistence_layer, + config=config, + ) + def process_invoice(invoice: InvoiceWithDecimal) -> dict: + return {"status": "ok"} + + # WHEN + invoice = InvoiceWithDecimal(invoice_id="inv-001", amount=Decimal("199.99")) + result = process_invoice(invoice=invoice) + + # THEN + assert result == {"status": "ok"} + + +def test_idempotent_function_with_path(): + # GIVEN + config = IdempotencyConfig(use_local_cache=True) + mock_event = {"config_id": "cfg-001", "file_path": "/etc/app/config.yaml"} + idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_with_path..process_config#{hash_idempotency_key(mock_event)}" # noqa E501 + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + + @idempotent_function( + data_keyword_argument="config", + persistence_store=persistence_layer, + config=config, + ) + def process_config(config: ConfigWithPath) -> dict: + return {"status": "ok"} + + # WHEN + cfg = ConfigWithPath(config_id="cfg-001", file_path=PurePosixPath("/etc/app/config.yaml")) + result = process_config(config=cfg) + + # THEN + assert result == {"status": "ok"} diff --git a/tests/functional/idempotency/_redis/test_redis_layer.py b/tests/functional/idempotency/_redis/test_redis_layer.py index c2a0976b0ab..22c3b9a6d83 100644 --- a/tests/functional/idempotency/_redis/test_redis_layer.py +++ b/tests/functional/idempotency/_redis/test_redis_layer.py @@ -330,6 +330,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/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..397c11ba436 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>=3.3.1,!=4.0.0,!=4.0.1,!=4.0.2,!=4.0.3,!=4.0.4 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..397c11ba436 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>=3.3.1,!=4.0.0,!=4.0.1,!=4.0.2,!=4.0.3,!=4.0.4 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..397c11ba436 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>=3.3.1,!=4.0.0,!=4.0.1,!=4.0.2,!=4.0.3,!=4.0.4 diff --git a/tests/unit/data_classes/required_dependencies/test_alb_event.py b/tests/unit/data_classes/required_dependencies/test_alb_event.py index 13d8b5907be..23ab7af6365 100644 --- a/tests/unit/data_classes/required_dependencies/test_alb_event.py +++ b/tests/unit/data_classes/required_dependencies/test_alb_event.py @@ -52,3 +52,17 @@ def test_alb_event_decode_multi_value_query_parameters(): # With decode_query_parameters, the key and value are not decoded parsed_event.decode_query_parameters = True assert parsed_event.resolved_query_string_parameters == {expected_key: expected_values} + + +def test_alb_event_merged_query_string_parameters(): + """When both multiValueQueryStringParameters and queryStringParameters are present, + resolved_query_string_parameters should merge them (GH #7993).""" + raw_event = load_event("albMultiValueQueryStringEvent.json") + raw_event["multiValueQueryStringParameters"] = {"ids": ["1", "2", "3"]} + raw_event["queryStringParameters"] = {"status": "fizzbuzz"} + + parsed_event = ALBEvent(raw_event) + resolved = parsed_event.resolved_query_string_parameters + + assert resolved["ids"] == ["1", "2", "3"] + assert resolved["status"] == ["fizzbuzz"] diff --git a/tests/unit/data_classes/required_dependencies/test_api_gateway_authorizer.py b/tests/unit/data_classes/required_dependencies/test_api_gateway_authorizer.py index 1fad5176672..c26c1a417e7 100644 --- a/tests/unit/data_classes/required_dependencies/test_api_gateway_authorizer.py +++ b/tests/unit/data_classes/required_dependencies/test_api_gateway_authorizer.py @@ -200,6 +200,40 @@ def test_authorizer_response_allow_route_with_underscore(builder: APIGatewayAuth } +def test_authorizer_response_allow_route_with_proxy_plus(builder: APIGatewayAuthorizerResponse): + builder.allow_route(http_method="GET", resource="/{proxy+}") + assert builder.asdict() == { + "principalId": "foo", + "policyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "execute-api:Invoke", + "Effect": "Allow", + "Resource": ["arn:aws:execute-api:us-west-1:123456789:fantom/dev/GET/{proxy+}"], + }, + ], + }, + } + + +def test_authorizer_response_allow_route_with_path_parameter(builder: APIGatewayAuthorizerResponse): + builder.allow_route(http_method="GET", resource="/users/{user_id}") + assert builder.asdict() == { + "principalId": "foo", + "policyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "execute-api:Invoke", + "Effect": "Allow", + "Resource": ["arn:aws:execute-api:us-west-1:123456789:fantom/dev/GET/users/{user_id}"], + }, + ], + }, + } + + def test_parse_api_gateway_arn_with_resource(): mock_event = { "type": "TOKEN", diff --git a/tests/unit/data_classes/required_dependencies/test_api_gateway_proxy_event.py b/tests/unit/data_classes/required_dependencies/test_api_gateway_proxy_event.py index ec71d815a7c..fd9ca1cca76 100644 --- a/tests/unit/data_classes/required_dependencies/test_api_gateway_proxy_event.py +++ b/tests/unit/data_classes/required_dependencies/test_api_gateway_proxy_event.py @@ -241,3 +241,53 @@ def test_api_gateway_proxy_v2_iam_event(): assert iam.principal_org_id == iam_raw["principalOrgId"] assert iam.user_arn == iam_raw["userArn"] assert iam.user_id == iam_raw["userId"] + + +def test_api_gateway_proxy_event_merged_query_string_parameters(): + """When both multiValueQueryStringParameters and queryStringParameters are present, + resolved_query_string_parameters should merge them (GH #7993).""" + raw_event = load_event("apiGatewayProxyEvent.json") + raw_event["multiValueQueryStringParameters"] = {"ids": ["1", "2", "3"]} + raw_event["queryStringParameters"] = {"status": "fizzbuzz"} + + parsed_event = APIGatewayProxyEvent(raw_event) + resolved = parsed_event.resolved_query_string_parameters + + assert resolved["ids"] == ["1", "2", "3"] + assert resolved["status"] == ["fizzbuzz"] + + +def test_api_gateway_proxy_event_multi_value_takes_precedence(): + """When the same key exists in both, multiValueQueryStringParameters wins.""" + raw_event = load_event("apiGatewayProxyEvent.json") + raw_event["multiValueQueryStringParameters"] = {"key": ["a", "b"]} + raw_event["queryStringParameters"] = {"key": "c"} + + parsed_event = APIGatewayProxyEvent(raw_event) + resolved = parsed_event.resolved_query_string_parameters + + assert resolved["key"] == ["a", "b"] + + +def test_api_gateway_proxy_event_only_single_value_query_params(): + """When only queryStringParameters is present, it should still work.""" + raw_event = load_event("apiGatewayProxyEvent.json") + raw_event["multiValueQueryStringParameters"] = None + raw_event["queryStringParameters"] = {"status": "active"} + + parsed_event = APIGatewayProxyEvent(raw_event) + resolved = parsed_event.resolved_query_string_parameters + + assert resolved["status"] == ["active"] + + +def test_api_gateway_proxy_event_only_multi_value_query_params(): + """When only multiValueQueryStringParameters is present, it should still work.""" + raw_event = load_event("apiGatewayProxyEvent.json") + raw_event["multiValueQueryStringParameters"] = {"ids": ["1", "2"]} + raw_event["queryStringParameters"] = None + + parsed_event = APIGatewayProxyEvent(raw_event) + resolved = parsed_event.resolved_query_string_parameters + + assert resolved["ids"] == ["1", "2"] diff --git a/tests/unit/event_handler/openapi/test_openapi_merge.py b/tests/unit/event_handler/openapi/test_openapi_merge.py index 21500145b35..bce18b62dea 100644 --- a/tests/unit/event_handler/openapi/test_openapi_merge.py +++ b/tests/unit/event_handler/openapi/test_openapi_merge.py @@ -10,7 +10,7 @@ _discover_resolver_files, _file_has_resolver, _is_excluded, - _load_resolver, + _load_resolver_with_dependencies, ) MERGE_HANDLERS_PATH = Path(__file__).parents[3] / "functional/event_handler/_pydantic/merge_handlers" @@ -71,7 +71,7 @@ def test_is_excluded_with_file_pattern(): def test_load_resolver_file_not_found(): with pytest.raises(FileNotFoundError): - _load_resolver(Path("/non/existent/file.py"), "app") + _load_resolver_with_dependencies(Path("/non/existent/file.py"), "app", [], Path("/")) def test_load_resolver_not_found_in_module(tmp_path: Path): @@ -79,7 +79,7 @@ def test_load_resolver_not_found_in_module(tmp_path: Path): handler_file.write_text("x = 1") with pytest.raises(AttributeError, match="Resolver 'app' not found"): - _load_resolver(handler_file, "app") + _load_resolver_with_dependencies(handler_file, "app", [], tmp_path) def test_load_resolver_success(tmp_path: Path): @@ -93,6 +93,6 @@ def test_endpoint(): return {"test": True} """) - resolver = _load_resolver(handler_file, "app") + resolver = _load_resolver_with_dependencies(handler_file, "app", [], tmp_path) assert resolver is not None assert hasattr(resolver, "get_openapi_schema")