diff --git a/.cfnlintrc.yaml b/.cfnlintrc.yaml new file mode 100644 index 00000000000..3909b9bb437 --- /dev/null +++ b/.cfnlintrc.yaml @@ -0,0 +1,2 @@ +ignore_templates: + - examples/event_handler_appsync_events/sam/getting_started_with_appsync_events.yaml diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile new file mode 100644 index 00000000000..19d3018f3ac --- /dev/null +++ b/.clusterfuzzlite/Dockerfile @@ -0,0 +1,12 @@ +FROM gcr.io/oss-fuzz-base/base-builder-python + +# Copy project source +COPY . $SRC/powertools + +WORKDIR $SRC/powertools + +# Install project dependencies +RUN pip3 install -e ".[all]" + +# Copy build script +COPY .clusterfuzzlite/build.sh $SRC/ diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh new file mode 100644 index 00000000000..e5aadd80335 --- /dev/null +++ b/.clusterfuzzlite/build.sh @@ -0,0 +1,6 @@ +#!/bin/bash -eu + +# Build fuzz targets from tests/fuzz/ +for fuzzer in $(find $SRC/powertools/tests/fuzz -name 'fuzz_*.py'); do + compile_python_fuzzer "$fuzzer" +done diff --git a/.clusterfuzzlite/project.yaml b/.clusterfuzzlite/project.yaml new file mode 100644 index 00000000000..de5f07bb82a --- /dev/null +++ b/.clusterfuzzlite/project.yaml @@ -0,0 +1,4 @@ +language: python +main_repo: https://github.com/aws-powertools/powertools-lambda-python +sanitizers: + - address diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 07fbd55af26..21a8f3b035c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -58,10 +58,11 @@ body: attributes: label: AWS Lambda function runtime options: - - "3.7" - - "3.8" - - "3.9" - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/static_typing.yml b/.github/ISSUE_TEMPLATE/static_typing.yml index 148cf977e95..399fd8bc09a 100644 --- a/.github/ISSUE_TEMPLATE/static_typing.yml +++ b/.github/ISSUE_TEMPLATE/static_typing.yml @@ -25,10 +25,11 @@ body: attributes: label: AWS Lambda function runtime options: - - "3.7" - - "3.8" - - "3.9" - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" validations: required: true - type: input diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5265d390063..234a347f24a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,5 @@ -**Issue number:** +**Issue number:** closes # ## Summary @@ -11,29 +11,17 @@ > Please share what the user experience looks like before and after this change -## Checklist + -* [ ] [Meet tenets criteria](https://docs.powertools.aws.dev/lambda/python/#tenets) -* [ ] I have performed a self-review of this change -* [ ] Changes have been tested -* [ ] Changes are documented -* [ ] PR title follows [conventional commit semantics](https://github.com/aws-powertools/powertools-lambda-python/blob/develop/.github/semantic.yml) - -
-Is this a breaking change? - -**RFC issue number**: - -Checklist: - -* [ ] Migration process documented -* [ ] Implement warnings (if it can live side by side) - -
- -## Acknowledgment +--- By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 9ce63fe3db6..c9597418e3f 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -1,4 +1,3 @@ self-hosted-runner: labels: - - aws-powertools_ubuntu-latest_4-core - aws-powertools_ubuntu-latest_8-core diff --git a/.github/actions/create-pr/action.yml b/.github/actions/create-pr/action.yml index dcf2df738bd..39ba6f60b1f 100644 --- a/.github/actions/create-pr/action.yml +++ b/.github/actions/create-pr/action.yml @@ -64,7 +64,7 @@ runs: name: Git client setup and refresh tip run: | git config user.name "Powertools for AWS Lambda (Python) bot" - git config user.email "aws-lambda-powertools-feedback@amazon.com" + git config user.email "151832416+aws-powertools-bot@users.noreply.github.com" git config pull.rebase true git config remote.origin.url >&- shell: bash diff --git a/.github/actions/download-artifact/action.yml b/.github/actions/download-artifact/action.yml index ef938ddb684..1f1347e4220 100644 --- a/.github/actions/download-artifact/action.yml +++ b/.github/actions/download-artifact/action.yml @@ -38,7 +38,7 @@ runs: using: composite steps: - name: Download artifacts - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: name: ${{ inputs.name }} path: ${{ inputs.path }} diff --git a/.github/actions/seal-restore/action.yml b/.github/actions/seal-restore/action.yml index beadad90cbc..1107414b640 100644 --- a/.github/actions/seal-restore/action.yml +++ b/.github/actions/seal-restore/action.yml @@ -43,7 +43,7 @@ runs: shell: bash - name: Download artifacts - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: name: ${{ inputs.artifact_name }} path: . diff --git a/.github/actions/seal/action.yml b/.github/actions/seal/action.yml index 3438056c872..c3e75a13e92 100644 --- a/.github/actions/seal/action.yml +++ b/.github/actions/seal/action.yml @@ -79,7 +79,7 @@ runs: shell: bash - name: Upload artifacts - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: if-no-files-found: error name: ${{ steps.export_artifact_name.outputs.artifact_name }} diff --git a/.github/actions/upload-artifact/action.yml b/.github/actions/upload-artifact/action.yml index ffa18cc0723..f88ea2475b9 100644 --- a/.github/actions/upload-artifact/action.yml +++ b/.github/actions/upload-artifact/action.yml @@ -68,7 +68,7 @@ runs: shell: bash - name: Upload artifacts - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: if-no-files-found: ${{ inputs.if-no-files-found }} name: ${{ inputs.name }} diff --git a/.github/actions/upload-release-provenance/action.yml b/.github/actions/upload-release-provenance/action.yml index e4c1e52c0d2..d0829efd4f4 100644 --- a/.github/actions/upload-release-provenance/action.yml +++ b/.github/actions/upload-release-provenance/action.yml @@ -42,7 +42,7 @@ runs: - id: download-provenance name: Download newly generated provenance - uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # v3.0.1 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: name: ${{ inputs.provenance_name }} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2ed1322608a..4d53389845e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,78 +1,112 @@ version: 2 updates: - - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" + interval: "weekly" + day: "monday" commit-message: prefix: chore include: scope + groups: + github-actions: + patterns: + - "*" - package-ecosystem: "pip" directory: "/" schedule: - interval: "daily" + interval: "weekly" + day: "monday" target-branch: "develop" commit-message: prefix: chore include: scope ignore: - # 2022-04-23: Ignoring boto3 changes until we need to care about them. - dependency-name: "boto3" + groups: + boto-typing: + patterns: + - "mypy-boto3-*" + dev-dependencies: + patterns: + - "pytest*" + - "ruff" + - "mypy" + - "black" + - "coverage" + - "flake8*" + - "pre-commit" - package-ecosystem: "npm" directory: "/" schedule: - interval: "daily" + interval: "weekly" + day: "monday" target-branch: "develop" commit-message: prefix: chore include: scope allow: - # Allow updates for AWS CDK - - dependency-name: "aws-cdk" - -# - package-ecosystem: "pip" -# directory: "/" -# schedule: -# interval: "daily" -# target-branch: "develop" -# update_types: -# - "semver:major" -# labels: -# - "do-not-merge" -# - "dependencies" -# commit-message: -# prefix: chore -# include: scope + - dependency-name: "aws-cdk*" + groups: + aws-cdk: + patterns: + - "aws-cdk*" - package-ecosystem: pip directory: /benchmark/src/instrumented + commit-message: + prefix: chore + include: scope schedule: - interval: daily + interval: monthly - package-ecosystem: pip directory: /benchmark/src/reference + commit-message: + prefix: chore + include: scope schedule: - interval: daily + interval: monthly - package-ecosystem: docker directory: /docs + commit-message: + prefix: chore + include: scope + schedule: + interval: monthly + + - package-ecosystem: pip + directory: /docs + commit-message: + prefix: chore + include: scope schedule: - interval: daily + interval: weekly + day: "monday" + groups: + docs-dependencies: + patterns: + - "*" - package-ecosystem: pip directory: /examples/event_handler_graphql/src + commit-message: + prefix: chore + include: scope schedule: - interval: daily + interval: monthly - package-ecosystem: gomod directory: /layer/scripts/layer-balancer + commit-message: + prefix: chore + include: scope schedule: - interval: daily - - - package-ecosystem: pip - directory: /docs - schedule: - interval: daily + interval: monthly + groups: + layer-balancer: + patterns: + - "*" diff --git a/.github/dependency-review-config.yml b/.github/dependency-review-config.yml new file mode 100644 index 00000000000..208fd73c9e1 --- /dev/null +++ b/.github/dependency-review-config.yml @@ -0,0 +1,35 @@ +allow-licenses: + - 'Apache-1.1' + - 'Apache-2.0' + - 'MIT' + - 'MIT-0' + - 'MIT-CMU' + - 'MIT-enna' + - 'MIT-feh' + - 'MIT-Festival' + - 'MIT-Modern-Variant' + - 'MIT-open-group' + - 'MIT-testregex' + - 'MIT-Wu' + - 'BSD-1-Clause' + - 'BSD-2-Clause' + - 'BSD-2-Clause-Views' + - 'BSD-3-Clause' + - 'BSD-3-Clause-Attribution' + - 'BSD-3-Clause-Clear' + - 'BSD-3-Clause-flex' + - 'BSD-3-Clause-HP' + - 'BSD-3-Clause-LBNL' + - 'BSD-3-Clause-Modification' + - 'BSD-3-Clause-No-Military-License' + - 'BSD-3-Clause-No-Nuclear-License' + - 'BSD-3-Clause-No-Nuclear-License-2014' + - 'BSD-3-Clause-No-Nuclear-Warranty' + - 'BSD-3-Clause-Open-MPI' + - 'Python-2.0' + - 'Python-2.0.1' + - 'ISC' + - 'MPL-1.1' + - 'MPL-2.0' +comment-summary-in-pr: on-failure +fail-on-scopes: runtime diff --git a/.github/scripts/comment_on_large_pr.js b/.github/scripts/comment_on_large_pr.js deleted file mode 100644 index c17199faf76..00000000000 --- a/.github/scripts/comment_on_large_pr.js +++ /dev/null @@ -1,73 +0,0 @@ -const { - PR_NUMBER, - PR_ACTION, - PR_AUTHOR, - IGNORE_AUTHORS, -} = require("./constants") - - -/** - * Notify PR author to split XXL PR in smaller chunks - * - * @param {object} core - core functions instance from @actions/core - * @param {object} gh_client - Pre-authenticated REST client (Octokit) - * @param {string} owner - GitHub Organization - * @param {string} repository - GitHub repository - */ -const notifyAuthor = async ({ - core, - gh_client, - owner, - repository, -}) => { - core.info(`Commenting on PR ${PR_NUMBER}`) - - let msg = `### ⚠️Large PR detected⚠️ - -Please consider breaking into smaller PRs to avoid significant review delays. Ignore if this PR has naturally grown to this size after reviews. - `; - - try { - await gh_client.rest.issues.createComment({ - owner: owner, - repo: repository, - body: msg, - issue_number: PR_NUMBER, - }); - } catch (error) { - core.setFailed("Failed to notify PR author to split large PR"); - console.error(err); - } -} - -module.exports = async ({github, context, core}) => { - if (IGNORE_AUTHORS.includes(PR_AUTHOR)) { - return core.notice("Author in IGNORE_AUTHORS list; skipping...") - } - - if (PR_ACTION != "labeled") { - return core.notice("Only run on PRs labeling actions; skipping") - } - - - /** @type {string[]} */ - const { data: labels } = await github.rest.issues.listLabelsOnIssue({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: PR_NUMBER, - }) - - // Schema: https://docs.github.com/en/rest/issues/labels#list-labels-for-an-issue - for (const label of labels) { - core.info(`Label: ${label}`) - if (label.name == "size/XXL") { - await notifyAuthor({ - core: core, - gh_client: github, - owner: context.repo.owner, - repository: context.repo.repo, - }) - break; - } - } -} diff --git a/.github/scripts/constants.js b/.github/scripts/constants.js index ce4ed8ddeef..8bfe5571974 100644 --- a/.github/scripts/constants.js +++ b/.github/scripts/constants.js @@ -18,6 +18,9 @@ module.exports = Object.freeze({ /** @type {string} */ "PR_IS_MERGED": process.env.PR_IS_MERGED || "false", + /** @type {string} */ + "PR_LABELS": process.env.PR_LABELS || "", + /** @type {string} */ "LABEL_BLOCK": "do-not-merge", diff --git a/.github/scripts/download_pr_artifact.js b/.github/scripts/download_pr_artifact.js deleted file mode 100644 index 274467c1f1c..00000000000 --- a/.github/scripts/download_pr_artifact.js +++ /dev/null @@ -1,26 +0,0 @@ -module.exports = async ({github, context, core}) => { - const fs = require('fs'); - - const workflowRunId = process.env.WORKFLOW_ID; - core.info(`Listing artifacts for workflow run ${workflowRunId}`); - - const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: workflowRunId, - }); - - const matchArtifact = artifacts.data.artifacts.filter(artifact => artifact.name == "pr")[0]; - - core.info(`Downloading artifacts for workflow run ${workflowRunId}`); - const artifact = await github.rest.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: matchArtifact.id, - archive_format: 'zip', - }); - - core.info("Saving artifact found", artifact); - - fs.writeFileSync('pr.zip', Buffer.from(artifact.data)); -} diff --git a/.github/scripts/enforce_acknowledgment.js b/.github/scripts/enforce_acknowledgment.js deleted file mode 100644 index 3e3be636ede..00000000000 --- a/.github/scripts/enforce_acknowledgment.js +++ /dev/null @@ -1,40 +0,0 @@ -const { -PR_ACTION, -PR_AUTHOR, -PR_BODY, -PR_NUMBER, -IGNORE_AUTHORS, -LABEL_BLOCK, -LABEL_BLOCK_REASON -} = require("./constants") - -module.exports = async ({github, context, core}) => { - if (IGNORE_AUTHORS.includes(PR_AUTHOR)) { - return core.notice("Author in IGNORE_AUTHORS list; skipping...") - } - - if (PR_ACTION != "opened") { - return core.notice("Only newly open PRs are labelled to avoid spam; skipping") - } - - const RELATED_ISSUE_REGEX = /Issue number:[^\d\r\n]+(?\d+)/; - const isMatch = RELATED_ISSUE_REGEX.exec(PR_BODY); - if (isMatch == null) { - core.info(`No related issue found, maybe the author didn't use the template but there is one.`) - - let msg = "No related issues found. Please ensure there is an open issue related to this change to avoid significant delays or closure."; - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - body: msg, - issue_number: PR_NUMBER, - }); - - return await github.rest.issues.addLabels({ - issue_number: PR_NUMBER, - owner: context.repo.owner, - repo: context.repo.repo, - labels: [LABEL_BLOCK, LABEL_BLOCK_REASON] - }) - } -} diff --git a/.github/scripts/label_missing_acknowledgement_section.js b/.github/scripts/label_missing_acknowledgement_section.js deleted file mode 100644 index 12b85241d1d..00000000000 --- a/.github/scripts/label_missing_acknowledgement_section.js +++ /dev/null @@ -1,41 +0,0 @@ -const { - PR_ACTION, - PR_AUTHOR, - PR_BODY, - PR_NUMBER, - IGNORE_AUTHORS, - LABEL_BLOCK, - LABEL_BLOCK_MISSING_LICENSE_AGREEMENT -} = require("./constants") - -module.exports = async ({github, context, core}) => { - if (IGNORE_AUTHORS.includes(PR_AUTHOR)) { - return core.notice("Author in IGNORE_AUTHORS list; skipping...") - } - - if (PR_ACTION != "opened") { - return core.notice("Only newly open PRs are labelled to avoid spam; skipping") - } - - const RELATED_ACK_SECTION_REGEX = /By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice./; - - const isMatch = RELATED_ACK_SECTION_REGEX.exec(PR_BODY); - if (isMatch == null) { - core.info(`No acknowledgement section found, maybe the author didn't use the template but there is one.`) - - let msg = "No acknowledgement section found. Please make sure you used the template to open a PR and didn't remove the acknowledgment section. Check the template here: https://github.com/aws-powertools/powertools-lambda-python/blob/develop/.github/PULL_REQUEST_TEMPLATE.md#acknowledgment"; - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - body: msg, - issue_number: PR_NUMBER, - }); - - return await github.rest.issues.addLabels({ - issue_number: PR_NUMBER, - owner: context.repo.owner, - repo: context.repo.repo, - labels: [LABEL_BLOCK, LABEL_BLOCK_MISSING_LICENSE_AGREEMENT] - }) - } -} diff --git a/.github/scripts/label_missing_related_issue.js b/.github/scripts/label_missing_related_issue.js deleted file mode 100644 index 705e414c47f..00000000000 --- a/.github/scripts/label_missing_related_issue.js +++ /dev/null @@ -1,40 +0,0 @@ -const { - PR_ACTION, - PR_AUTHOR, - PR_BODY, - PR_NUMBER, - IGNORE_AUTHORS, - LABEL_BLOCK, - LABEL_BLOCK_REASON -} = require("./constants") - -module.exports = async ({github, context, core}) => { - if (IGNORE_AUTHORS.includes(PR_AUTHOR)) { - return core.notice("Author in IGNORE_AUTHORS list; skipping...") - } - - if (PR_ACTION != "opened") { - return core.notice("Only newly open PRs are labelled to avoid spam; skipping") - } - - const RELATED_ISSUE_REGEX = /Issue number:[^\d\r\n]+(?\d+)/; - const isMatch = RELATED_ISSUE_REGEX.exec(PR_BODY); - if (isMatch == null) { - core.info(`No related issue found, maybe the author didn't use the template but there is one.`) - - let msg = "No related issues found. Please ensure there is an open issue related to this change to avoid significant delays or closure."; - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - body: msg, - issue_number: PR_NUMBER, - }); - - return await github.rest.issues.addLabels({ - issue_number: PR_NUMBER, - owner: context.repo.owner, - repo: context.repo.repo, - labels: [LABEL_BLOCK, LABEL_BLOCK_REASON] - }) - } -} diff --git a/.github/scripts/label_pr_based_on_title.js b/.github/scripts/label_pr_based_on_title.js deleted file mode 100644 index 95c33841f92..00000000000 --- a/.github/scripts/label_pr_based_on_title.js +++ /dev/null @@ -1,47 +0,0 @@ -const { PR_NUMBER, PR_TITLE } = require("./constants") - -module.exports = async ({github, context, core}) => { - const FEAT_REGEX = /feat(\((.+)\))?(:.+)/ - const BUG_REGEX = /(fix|bug)(\((.+)\))?(:.+)/ - const DOCS_REGEX = /(docs|doc)(\((.+)\))?(:.+)/ - const CHORE_REGEX = /(chore)(\((.+)\))?(:.+)/ - const DEPRECATED_REGEX = /(deprecated)(\((.+)\))?(:.+)/ - const REFACTOR_REGEX = /(refactor)(\((.+)\))?(:.+)/ - - const labels = { - "feature": FEAT_REGEX, - "bug": BUG_REGEX, - "documentation": DOCS_REGEX, - "internal": CHORE_REGEX, - "enhancement": REFACTOR_REGEX, - "deprecated": DEPRECATED_REGEX, - } - - // Maintenance: We should keep track of modified PRs in case their titles change - let miss = 0; - try { - for (const label in labels) { - const matcher = new RegExp(labels[label]) - const matches = matcher.exec(PR_TITLE) - if (matches != null) { - core.info(`Auto-labeling PR ${PR_NUMBER} with ${label}`) - - await github.rest.issues.addLabels({ - issue_number: PR_NUMBER, - owner: context.repo.owner, - repo: context.repo.repo, - labels: [label] - }) - - return; - } else { - core.debug(`'${PR_TITLE}' didn't match '${label}' semantic.`) - miss += 1 - } - } - } finally { - if (miss == Object.keys(labels).length) { - core.notice(`PR ${PR_NUMBER} title '${PR_TITLE}' doesn't follow semantic titles; skipping...`) - } - } -} diff --git a/.github/scripts/label_related_issue.js b/.github/scripts/label_related_issue.js deleted file mode 100644 index 790aac1ced5..00000000000 --- a/.github/scripts/label_related_issue.js +++ /dev/null @@ -1,53 +0,0 @@ -const { - PR_AUTHOR, - PR_BODY, - PR_NUMBER, - IGNORE_AUTHORS, - LABEL_PENDING_RELEASE, - HANDLE_MAINTAINERS_TEAM, - PR_IS_MERGED, -} = require("./constants") - -module.exports = async ({github, context, core}) => { - if (IGNORE_AUTHORS.includes(PR_AUTHOR)) { - return core.notice("Author in IGNORE_AUTHORS list; skipping...") - } - - if (PR_IS_MERGED == "false") { - return core.notice("Only merged PRs to avoid spam; skipping") - } - - const RELATED_ISSUE_REGEX = /Issue number:[^\d\r\n]+(?\d+)/; - - const isMatch = RELATED_ISSUE_REGEX.exec(PR_BODY); - - try { - if (!isMatch) { - core.setFailed(`Unable to find related issue for PR number ${PR_NUMBER}.\n\n Body details: ${PR_BODY}`); - return await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - body: `${HANDLE_MAINTAINERS_TEAM} No related issues found. Please ensure '${LABEL_PENDING_RELEASE}' label is applied before releasing.`, - issue_number: PR_NUMBER, - }); - } - } catch (error) { - core.setFailed(`Unable to create comment on PR number ${PR_NUMBER}.\n\n Error details: ${error}`); - throw new Error(error); - } - - const { groups: {issue} } = isMatch - - try { - core.info(`Auto-labeling related issue ${issue} for release`) - return await github.rest.issues.addLabels({ - issue_number: issue, - owner: context.repo.owner, - repo: context.repo.repo, - labels: [LABEL_PENDING_RELEASE] - }) - } catch (error) { - core.setFailed(`Is this issue number (${issue}) valid? Perhaps a discussion?`); - throw new Error(error); - } -} diff --git a/.github/scripts/save_pr_details.js b/.github/scripts/save_pr_details.js deleted file mode 100644 index 83bd3bf70d4..00000000000 --- a/.github/scripts/save_pr_details.js +++ /dev/null @@ -1,13 +0,0 @@ -module.exports = async ({context, core}) => { - const fs = require('fs'); - const filename = "pr.txt"; - - try { - fs.writeFileSync(`./${filename}`, JSON.stringify(context.payload)); - - return `PR successfully saved ${filename}` - } catch (err) { - core.setFailed("Failed to save PR details"); - console.error(err); - } -} diff --git a/.github/workflows/bootstrap_region.yml b/.github/workflows/bootstrap_region.yml new file mode 100644 index 00000000000..dc8c142efd5 --- /dev/null +++ b/.github/workflows/bootstrap_region.yml @@ -0,0 +1,117 @@ +name: Region Bootstrap + +# bootstraps new regions +# +# PURPOSE +# Ensures new regions are deployable in future releases +# +# JOB 1 PROCESS +# +# 1. Installs CDK +# 2. Bootstraps region +# +# JOB 2 PROCESS +# 1. Sets up Go +# 2. Installs the balance script +# 3. Runs balance script to copy layers between aws regions + +on: + workflow_dispatch: + inputs: + environment: + type: choice + options: + - beta + - prod + description: Deployment environment + region: + type: string + required: true + description: AWS region to bootstrap (i.e. eu-west-1) + +run-name: Region Bootstrap ${{ inputs.region }} + +permissions: + contents: read + +jobs: + cdk: + name: Install CDK + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + environment: layer-${{ inputs.environment }} + steps: + - name: checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.sha }} + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "22" + - name: Setup dependencies + uses: aws-powertools/actions/.github/actions/cached-node-modules@828e78a26eee3554dc2e1d96048004548fbb169f + - id: credentials + name: AWS Credentials + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 + with: + aws-region: ${{ inputs.region }} + role-to-assume: ${{ secrets.REGION_IAM_ROLE }} + mask-aws-account-id: true + - id: workdir + name: Create Workdir + run: | + mkdir -p build/project + - id: cdk-project + name: CDK Project + working-directory: build/project + run: | + npx cdk init app --language=typescript + AWS_REGION="${{ inputs.region }}" npx cdk bootstrap + + copy_layers: + name: Copy Layers + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + strategy: + matrix: + layer: + - AWSLambdaPowertoolsPythonV3-python39-arm64 + - AWSLambdaPowertoolsPythonV3-python310-arm64 + - AWSLambdaPowertoolsPythonV3-python311-arm64 + - AWSLambdaPowertoolsPythonV3-python312-arm64 + - AWSLambdaPowertoolsPythonV3-python313-arm64 + - AWSLambdaPowertoolsPythonV3-python314-arm64 + - AWSLambdaPowertoolsPythonV3-python39-x86_64 + - AWSLambdaPowertoolsPythonV3-python310-x86_64 + - AWSLambdaPowertoolsPythonV3-python311-x86_64 + - AWSLambdaPowertoolsPythonV3-python312-x86_64 + - AWSLambdaPowertoolsPythonV3-python313-x86_64 + - AWSLambdaPowertoolsPythonV3-python314-x86_64 + environment: layer-${{ inputs.environment }} + steps: + - id: credentials + name: AWS Credentials + 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@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: '>=1.23.0' + - id: go-env + name: Go Env + run: go env + - id: go-install-pkg + name: Install + run: go install github.com/aws-powertools/actions/layer-balancer/cmd/balance@29979bc5339bf54f76a11ac36ff67701986bb0f0 + - id: run-balance + name: Run Balance + run: balance -read-region us-east-1 -write-region ${{ inputs.region }} -write-role ${{ secrets.BALANCE_ROLE_ARN }} -layer-name ${{ matrix.layer }} -dry-run=false diff --git a/.github/workflows/build_changelog.yml b/.github/workflows/build_changelog.yml index a167868be05..9509114b423 100644 --- a/.github/workflows/build_changelog.yml +++ b/.github/workflows/build_changelog.yml @@ -9,13 +9,10 @@ name: Build changelog # USAGE # -# Always triggered on PR merge or manually from GitHub UI if we must. +# Triggered manually from GitHub UI when needed (e.g., before a release). on: workflow_dispatch: - push: - branches: - - develop permissions: contents: read diff --git a/.github/workflows/cflite_scheduled.yml b/.github/workflows/cflite_scheduled.yml new file mode 100644 index 00000000000..1c4825b822f --- /dev/null +++ b/.github/workflows/cflite_scheduled.yml @@ -0,0 +1,34 @@ +name: ClusterFuzzLite fuzzing + +on: + schedule: + # Run daily at 8 AM UTC + - cron: "0 8 * * *" + workflow_dispatch: + +permissions: + contents: read + +jobs: + PR: + runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + steps: + - name: Build Fuzzers + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1 + with: + language: python + github-token: ${{ secrets.GITHUB_TOKEN }} + sanitizer: address + + - name: Run Fuzzers + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fuzz-seconds: 30 + mode: code-change + sanitizer: address diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3bbff1988d1..7aeb1c4e2bd 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 1ffd05de307..60ce40982bf 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -2,7 +2,7 @@ # # This Action will scan dependency manifest files that change as part of a Pull Request, # surfacing known-vulnerable versions of the packages declared or updated in the PR. -# Once installed, if the workflow run is marked as required, +# Once installed, if the workflow run is marked as required, # PRs introducing known-vulnerable packages will be blocked from merging. # # Source repository: https://github.com/actions/dependency-review-action @@ -15,8 +15,11 @@ permissions: jobs: dependency-review: runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write steps: - name: 'Checkout Repository' - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: 'Dependency Review' - uses: actions/dependency-review-action@1360a344ccb0ab6e9475edef90ad2f46bf8003b1 # v3.0.6 + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 diff --git a/.github/workflows/dispatch_analytics.yml b/.github/workflows/dispatch_analytics.yml deleted file mode 100644 index b94c7439f7b..00000000000 --- a/.github/workflows/dispatch_analytics.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Dispatch analytics - -# PROCESS -# -# 1. Trade GitHub JWT token with AWS credentials for the analytics account -# 2. Invoke a Lambda function dispatcher synchronously with the read-only scoped JWT token -# 3. The dispatcher function will call GitHub APIs to read data from the last hour and aggregate for operational analytics - -# USAGE -# -# NOTE: meant to use as a scheduled task only (or manually for debugging purposes). - -on: - workflow_dispatch: - - schedule: - - cron: "0 * * * *" - -permissions: - contents: read - - -jobs: - dispatch_token: - if: github.repository == 'aws-powertools/powertools-lambda-python' - concurrency: - group: analytics - runs-on: ubuntu-latest - environment: analytics - permissions: - id-token: write - actions: read - checks: read - contents: read # previously we needed `write` to use GH_TOKEN in our dispatcher (Lambda) - deployments: read - issues: read - discussions: read - packages: read - pages: read - pull-requests: read - repository-projects: read - security-events: read - statuses: read - steps: - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0 - with: - aws-region: eu-central-1 - role-to-assume: ${{ secrets.AWS_ANALYTICS_ROLE_ARN }} - - - name: Invoke Lambda function - run: | - payload=$(echo -n '{"githubToken": "${{ secrets.GITHUB_TOKEN }}"}' | base64) - aws lambda invoke \ - --function-name ${{ secrets.AWS_ANALYTICS_DISPATCHER_ARN }} \ - --payload "$payload" response.json - cat response.json diff --git a/.github/workflows/label_pr_on_title.yml b/.github/workflows/label_pr_on_title.yml deleted file mode 100644 index c69f0f8d7a8..00000000000 --- a/.github/workflows/label_pr_on_title.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Label PR based on title - -# PROCESS -# -# 1. Fetch PR details previously saved from untrusted location -# 2. Parse details for safety -# 3. Label PR based on semantic title (e.g., area, change type) - -# USAGE -# -# NOTE: meant to be used with ./.github/workflows/record_pr.yml -# -# Security Note: -# -# This workflow depends on "Record PR" workflow that runs in an untrusted location (forks) instead of `pull_request_target`. -# This enforces zero trust where "Record PR" workflow always runs on fork with zero permissions on GH_TOKEN. -# When "Record PR" completes, this workflow runs in our repository with the appropriate permissions and sanitize inputs. -# -# Coupled with "Approve GitHub Action to run on forks", we have confidence no privilege can be escalated, -# since any malicious change would need to be approved, and upon social engineering, it'll have zero permissions. - - -on: - workflow_run: - workflows: ["Record PR details"] - types: - - completed - -permissions: - contents: read - -jobs: - get_pr_details: - permissions: - actions: read # download PR artifact - contents: read # checkout code - # Guardrails to only ever run if PR recording workflow was indeed - # run in a PR event and ran successfully - if: ${{ github.event.workflow_run.conclusion == 'success' }} - uses: ./.github/workflows/reusable_export_pr_details.yml - with: - record_pr_workflow_id: ${{ github.event.workflow_run.id }} - workflow_origin: ${{ github.event.repository.full_name }} - secrets: - token: ${{ secrets.GITHUB_TOKEN }} - label_pr: - needs: get_pr_details - runs-on: ubuntu-latest - permissions: - pull-requests: write # label respective PR - steps: - - name: Checkout repository - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: "Label PR based on title" - uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 - env: - PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }} - PR_TITLE: ${{ needs.get_pr_details.outputs.prTitle }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - # This safely runs in our base repo, not on fork - # thus allowing us to provide a write access token to label based on PR title - # and label PR based on semantic title accordingly - script: | - const script = require('.github/scripts/label_pr_based_on_title.js') - await script({github, context, core}) diff --git a/.github/workflows/layer_govcloud.yml b/.github/workflows/layer_govcloud.yml new file mode 100644 index 00000000000..9daf6725808 --- /dev/null +++ b/.github/workflows/layer_govcloud.yml @@ -0,0 +1,224 @@ +name: Layer Deployment (GovCloud) + +# GovCloud Layer Publish +# --- +# This workflow publishes a specific layer version in an AWS account based on the environment input. +# +# Using a matrix, we pull each architecture and python version of the layer and store them as artifacts +# we upload them to each of the GovCloud AWS accounts. +# +# A number of safety checks are performed to ensure safety. + +on: + workflow_dispatch: + inputs: + environment: + description: Deployment environment + type: choice + options: + - Gamma + - Prod + required: true + version: + description: Layer version to duplicate + type: string + required: true + workflow_call: + inputs: + environment: + description: Deployment environment + type: string + required: true + version: + description: Layer version to duplicate + type: string + required: true + +run-name: Layer Deployment (GovCloud) - ${{ inputs.environment }} + +permissions: + contents: read + +jobs: + download: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + strategy: + matrix: + layer: + - AWSLambdaPowertoolsPythonV3-python39 + - AWSLambdaPowertoolsPythonV3-python310 + - AWSLambdaPowertoolsPythonV3-python311 + - AWSLambdaPowertoolsPythonV3-python312 + - AWSLambdaPowertoolsPythonV3-python313 + - AWSLambdaPowertoolsPythonV3-python314 + arch: + - arm64 + - x86_64 + environment: Prod (Readonly) + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + role-to-assume: ${{ secrets.AWS_IAM_ROLE }} + aws-region: us-east-1 + mask-aws-account-id: true + - name: Grab Zip + 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 }} --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@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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ matrix.layer }}_${{ matrix.arch }}.json + path: ${{ matrix.layer }}_${{ matrix.arch }}.json + retention-days: 1 + if-no-files-found: error + + copy_east: + name: Copy (East) + needs: download + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + strategy: + matrix: + layer: + - AWSLambdaPowertoolsPythonV3-python39 + - AWSLambdaPowertoolsPythonV3-python310 + - AWSLambdaPowertoolsPythonV3-python311 + - AWSLambdaPowertoolsPythonV3-python312 + - AWSLambdaPowertoolsPythonV3-python313 + - AWSLambdaPowertoolsPythonV3-python314 + arch: + - arm64 + - x86_64 + environment: GovCloud ${{ inputs.environment }} (East) + steps: + - name: Download Zip + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ matrix.layer }}_${{ matrix.arch }}.zip + - name: Download Metadata + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ matrix.layer }}_${{ matrix.arch }}.json + - name: Verify Layer Signature + run: | + 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@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + role-to-assume: ${{ secrets.AWS_IAM_ROLE }} + aws-region: us-gov-east-1 + mask-aws-account-id: true + - name: Create Layer + id: create-layer + run: | + LAYER_VERSION=$(aws --region us-gov-east-1 lambda publish-layer-version \ + --layer-name ${{ matrix.layer }}-${{ matrix.arch }} \ + --zip-file fileb://./${{ matrix.layer }}_${{ matrix.arch }}.zip \ + --compatible-runtimes "$(jq -r '.CompatibleRuntimes[0]' '${{ matrix.layer }}_${{ matrix.arch }}.json')" \ + --compatible-architectures "$(jq -r '.CompatibleArchitectures[0]' '${{ matrix.layer }}_${{ matrix.arch }}.json')" \ + --license-info "MIT-0" \ + --description "$(jq -r '.Description' '${{ matrix.layer }}_${{ matrix.arch }}.json')" \ + --query 'Version' \ + --output text) + + echo "LAYER_VERSION=$LAYER_VERSION" >> "$GITHUB_OUTPUT" + + aws --region us-gov-east-1 lambda add-layer-version-permission \ + --layer-name '${{ matrix.layer }}-${{ matrix.arch }}' \ + --statement-id 'PublicLayer' \ + --action lambda:GetLayerVersion \ + --principal '*' \ + --version-number "$LAYER_VERSION" + - name: Verify Layer + env: + LAYER_VERSION: ${{ steps.create-layer.outputs.LAYER_VERSION }} + run: | + REMOTE_SHA=$(aws --region us-gov-east-1 lambda get-layer-version-by-arn --arn 'arn:aws-us-gov:lambda:us-gov-east-1:${{ secrets.AWS_ACCOUNT_ID }}:layer:${{ matrix.layer }}-${{ matrix.arch }}:${{ env.LAYER_VERSION }}' --query 'Content.CodeSha256' --output text) + SHA=$(jq -r '.Content.CodeSha256' '${{ matrix.layer }}_${{ matrix.arch }}.json') + test "$REMOTE_SHA" == "$SHA" && echo "SHA OK: ${SHA}" || exit 1 + aws --region us-gov-east-1 lambda get-layer-version-by-arn --arn 'arn:aws-us-gov:lambda:us-gov-east-1:${{ secrets.AWS_ACCOUNT_ID }}:layer:${{ matrix.layer }}-${{ matrix.arch }}:${{ env.LAYER_VERSION }}' --output table + + copy_west: + name: Copy (West) + needs: download + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + strategy: + matrix: + layer: + - AWSLambdaPowertoolsPythonV3-python39 + - AWSLambdaPowertoolsPythonV3-python310 + - AWSLambdaPowertoolsPythonV3-python311 + - AWSLambdaPowertoolsPythonV3-python312 + - AWSLambdaPowertoolsPythonV3-python313 + - AWSLambdaPowertoolsPythonV3-python314 + arch: + - arm64 + - x86_64 + environment: + name: GovCloud ${{ inputs.environment }} (West) + steps: + - name: Download Zip + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ matrix.layer }}_${{ matrix.arch }}.zip + - name: Download Metadata + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ matrix.layer }}_${{ matrix.arch }}.json + - name: Verify Layer Signature + run: | + 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@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + role-to-assume: ${{ secrets.AWS_IAM_ROLE }} + aws-region: us-gov-west-1 + mask-aws-account-id: true + - name: Create Layer + id: create-layer + run: | + LAYER_VERSION=$(aws --region us-gov-west-1 lambda publish-layer-version \ + --layer-name ${{ matrix.layer }}-${{ matrix.arch }} \ + --zip-file fileb://./${{ matrix.layer }}_${{ matrix.arch }}.zip \ + --compatible-runtimes "$(jq -r '.CompatibleRuntimes[0]' '${{ matrix.layer }}_${{ matrix.arch }}.json')" \ + --compatible-architectures "$(jq -r '.CompatibleArchitectures[0]' '${{ matrix.layer }}_${{ matrix.arch }}.json')" \ + --license-info "MIT-0" \ + --description "$(jq -r '.Description' '${{ matrix.layer }}_${{ matrix.arch }}.json')" \ + --query 'Version' \ + --output text) + + echo "LAYER_VERSION=$LAYER_VERSION" >> "$GITHUB_OUTPUT" + + aws --region us-gov-west-1 lambda add-layer-version-permission \ + --layer-name '${{ matrix.layer }}-${{ matrix.arch }}' \ + --statement-id 'PublicLayer' \ + --action lambda:GetLayerVersion \ + --principal '*' \ + --version-number "$LAYER_VERSION" + - name: Verify Layer + env: + LAYER_VERSION: ${{ steps.create-layer.outputs.LAYER_VERSION }} + run: | + REMOTE_SHA=$(aws --region us-gov-west-1 lambda get-layer-version-by-arn --arn 'arn:aws-us-gov:lambda:us-gov-west-1:${{ secrets.AWS_ACCOUNT_ID }}:layer:${{ matrix.layer }}-${{ matrix.arch }}:${{ env.LAYER_VERSION }}' --query 'Content.CodeSha256' --output text) + SHA=$(jq -r '.Content.CodeSha256' '${{ matrix.layer }}_${{ matrix.arch }}.json') + test "$REMOTE_SHA" == "$SHA" && echo "SHA OK: ${SHA}" || exit 1 + aws --region us-gov-west-1 lambda get-layer-version-by-arn --arn 'arn:aws-us-gov:lambda:us-gov-west-1:${{ secrets.AWS_ACCOUNT_ID }}:layer:${{ matrix.layer }}-${{ matrix.arch }}:${{ env.LAYER_VERSION }}' --output table diff --git a/.github/workflows/layer_govcloud_python313.yml b/.github/workflows/layer_govcloud_python313.yml new file mode 100644 index 00000000000..1dc2f4242d2 --- /dev/null +++ b/.github/workflows/layer_govcloud_python313.yml @@ -0,0 +1,209 @@ +name: Layer Deployment (GovCloud) - Temporary for Python 3.13 + +# GovCloud Layer Publish +# --- +# This workflow publishes a specific layer version in an AWS account based on the environment input. +# +# Using a matrix, we pull each architecture and python version of the layer and store them as artifacts +# we upload them to each of the GovCloud AWS accounts. +# +# A number of safety checks are performed to ensure safety. + +on: + workflow_dispatch: + inputs: + environment: + description: Deployment environment + type: choice + options: + - Gamma + - Prod + required: true + version: + description: Layer version to duplicate + type: string + required: true + workflow_call: + inputs: + environment: + description: Deployment environment + type: string + required: true + version: + description: Layer version to duplicate + type: string + required: true + +run-name: Layer Deployment (GovCloud) - ${{ inputs.environment }} + +permissions: + contents: read + +jobs: + download: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + strategy: + matrix: + layer: + - AWSLambdaPowertoolsPythonV3-python313 + arch: + - arm64 + - x86_64 + environment: Prod (Readonly) + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + role-to-assume: ${{ secrets.AWS_IAM_ROLE }} + aws-region: us-east-1 + mask-aws-account-id: true + - name: Grab Zip + 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 }} --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@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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ matrix.layer }}_${{ matrix.arch }}.json + path: ${{ matrix.layer }}_${{ matrix.arch }}.json + retention-days: 1 + if-no-files-found: error + + copy_east: + name: Copy (East) + needs: download + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + strategy: + matrix: + layer: + - AWSLambdaPowertoolsPythonV3-python313 + arch: + - arm64 + - x86_64 + environment: GovCloud ${{ inputs.environment }} (East) + steps: + - name: Download Zip + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ matrix.layer }}_${{ matrix.arch }}.zip + - name: Download Metadata + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ matrix.layer }}_${{ matrix.arch }}.json + - name: Verify Layer Signature + run: | + 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@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + role-to-assume: ${{ secrets.AWS_IAM_ROLE }} + aws-region: us-gov-east-1 + mask-aws-account-id: true + - name: Create Layer + id: create-layer + run: | + LAYER_VERSION=$(aws --region us-gov-east-1 lambda publish-layer-version \ + --layer-name ${{ matrix.layer }}-${{ matrix.arch }} \ + --zip-file fileb://./${{ matrix.layer }}_${{ matrix.arch }}.zip \ + --compatible-runtimes "$(jq -r '.CompatibleRuntimes[0]' '${{ matrix.layer }}_${{ matrix.arch }}.json')" \ + --compatible-architectures "$(jq -r '.CompatibleArchitectures[0]' '${{ matrix.layer }}_${{ matrix.arch }}.json')" \ + --license-info "MIT-0" \ + --description "$(jq -r '.Description' '${{ matrix.layer }}_${{ matrix.arch }}.json')" \ + --query 'Version' \ + --output text) + + echo "LAYER_VERSION=$LAYER_VERSION" >> "$GITHUB_OUTPUT" + + aws --region us-gov-east-1 lambda add-layer-version-permission \ + --layer-name '${{ matrix.layer }}-${{ matrix.arch }}' \ + --statement-id 'PublicLayer' \ + --action lambda:GetLayerVersion \ + --principal '*' \ + --version-number "$LAYER_VERSION" + - name: Verify Layer + env: + LAYER_VERSION: ${{ steps.create-layer.outputs.LAYER_VERSION }} + run: | + REMOTE_SHA=$(aws --region us-gov-east-1 lambda get-layer-version-by-arn --arn 'arn:aws-us-gov:lambda:us-gov-east-1:${{ secrets.AWS_ACCOUNT_ID }}:layer:${{ matrix.layer }}-${{ matrix.arch }}:${{ env.LAYER_VERSION }}' --query 'Content.CodeSha256' --output text) + SHA=$(jq -r '.Content.CodeSha256' '${{ matrix.layer }}_${{ matrix.arch }}.json') + test "$REMOTE_SHA" == "$SHA" && echo "SHA OK: ${SHA}" || exit 1 + aws --region us-gov-east-1 lambda get-layer-version-by-arn --arn 'arn:aws-us-gov:lambda:us-gov-east-1:${{ secrets.AWS_ACCOUNT_ID }}:layer:${{ matrix.layer }}-${{ matrix.arch }}:${{ env.LAYER_VERSION }}' --output table + + copy_west: + name: Copy (West) + needs: download + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + strategy: + matrix: + layer: + - AWSLambdaPowertoolsPythonV3-python313 + arch: + - arm64 + - x86_64 + environment: + name: GovCloud ${{ inputs.environment }} (West) + steps: + - name: Download Zip + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ matrix.layer }}_${{ matrix.arch }}.zip + - name: Download Metadata + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ matrix.layer }}_${{ matrix.arch }}.json + - name: Verify Layer Signature + run: | + 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@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + role-to-assume: ${{ secrets.AWS_IAM_ROLE }} + aws-region: us-gov-west-1 + mask-aws-account-id: true + - name: Create Layer + id: create-layer + run: | + LAYER_VERSION=$(aws --region us-gov-west-1 lambda publish-layer-version \ + --layer-name ${{ matrix.layer }}-${{ matrix.arch }} \ + --zip-file fileb://./${{ matrix.layer }}_${{ matrix.arch }}.zip \ + --compatible-runtimes "$(jq -r '.CompatibleRuntimes[0]' '${{ matrix.layer }}_${{ matrix.arch }}.json')" \ + --compatible-architectures "$(jq -r '.CompatibleArchitectures[0]' '${{ matrix.layer }}_${{ matrix.arch }}.json')" \ + --license-info "MIT-0" \ + --description "$(jq -r '.Description' '${{ matrix.layer }}_${{ matrix.arch }}.json')" \ + --query 'Version' \ + --output text) + + echo "LAYER_VERSION=$LAYER_VERSION" >> "$GITHUB_OUTPUT" + + aws --region us-gov-west-1 lambda add-layer-version-permission \ + --layer-name '${{ matrix.layer }}-${{ matrix.arch }}' \ + --statement-id 'PublicLayer' \ + --action lambda:GetLayerVersion \ + --principal '*' \ + --version-number "$LAYER_VERSION" + - name: Verify Layer + env: + LAYER_VERSION: ${{ steps.create-layer.outputs.LAYER_VERSION }} + run: | + REMOTE_SHA=$(aws --region us-gov-west-1 lambda get-layer-version-by-arn --arn 'arn:aws-us-gov:lambda:us-gov-west-1:${{ secrets.AWS_ACCOUNT_ID }}:layer:${{ matrix.layer }}-${{ matrix.arch }}:${{ env.LAYER_VERSION }}' --query 'Content.CodeSha256' --output text) + SHA=$(jq -r '.Content.CodeSha256' '${{ matrix.layer }}_${{ matrix.arch }}.json') + test "$REMOTE_SHA" == "$SHA" && echo "SHA OK: ${SHA}" || exit 1 + aws --region us-gov-west-1 lambda get-layer-version-by-arn --arn 'arn:aws-us-gov:lambda:us-gov-west-1:${{ secrets.AWS_ACCOUNT_ID }}:layer:${{ matrix.layer }}-${{ matrix.arch }}:${{ env.LAYER_VERSION }}' --output table diff --git a/.github/workflows/layer_govcloud_verify.yml b/.github/workflows/layer_govcloud_verify.yml new file mode 100644 index 00000000000..004f9e091fb --- /dev/null +++ b/.github/workflows/layer_govcloud_verify.yml @@ -0,0 +1,114 @@ +# GovCloud Layer Verification +# --- +# This workflow queries the GovCloud layer info in production only + +on: + workflow_dispatch: + inputs: + version: + description: Layer version to verify information + type: string + required: true + workflow_call: + inputs: + version: + description: Layer version to verify information + type: string + required: true + +name: Layer Verification (GovCloud) +run-name: Layer Verification (GovCloud) + +jobs: + commercial: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + strategy: + matrix: + layer: + - AWSLambdaPowertoolsPythonV3-python39 + - AWSLambdaPowertoolsPythonV3-python310 + - AWSLambdaPowertoolsPythonV3-python311 + - AWSLambdaPowertoolsPythonV3-python312 + - AWSLambdaPowertoolsPythonV3-python313 + - AWSLambdaPowertoolsPythonV3-python314 + arch: + - arm64 + - x86_64 + environment: Prod (Readonly) + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + role-to-assume: ${{ secrets.AWS_IAM_ROLE }} + aws-region: us-east-1 + mask-aws-account-id: true + - name: Output ${{ matrix.layer }}-${{ matrix.arch }} + 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 }}' | jq -r '{"Layer Version Arn": .LayerVersionArn, "Version": .Version, "Description": .Description, "Compatible Runtimes": .CompatibleRuntimes[0], "Compatible Architectures": .CompatibleArchitectures[0], "SHA": .Content.CodeSha256} | keys[] as $k | [$k, .[$k]] | @tsv' | column -t -s $'\t' + + gov_east: + name: Verify (East) + needs: commercial + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + strategy: + matrix: + layer: + - AWSLambdaPowertoolsPythonV3-python39 + - AWSLambdaPowertoolsPythonV3-python310 + - AWSLambdaPowertoolsPythonV3-python311 + - AWSLambdaPowertoolsPythonV3-python312 + - AWSLambdaPowertoolsPythonV3-python313 + - AWSLambdaPowertoolsPythonV3-python314 + arch: + - arm64 + - x86_64 + environment: GovCloud Prod (East) + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + role-to-assume: ${{ secrets.AWS_IAM_ROLE }} + aws-region: us-gov-east-1 + mask-aws-account-id: true + - name: Verify Layer ${{ matrix.layer }}-${{ matrix.arch }} + id: verify-layer + run: | + aws --region us-gov-east-1 lambda get-layer-version-by-arn --arn 'arn:aws-us-gov:lambda:us-gov-east-1:${{ secrets.AWS_ACCOUNT_ID }}:layer:${{ matrix.layer }}-${{ matrix.arch }}:${{ inputs.version }}' | jq -r '{"Layer Version Arn": .LayerVersionArn, "Version": .Version, "Description": .Description, "Compatible Runtimes": .CompatibleRuntimes[0], "Compatible Architectures": .CompatibleArchitectures[0], "SHA": .Content.CodeSha256} | keys[] as $k | [$k, .[$k]] | @tsv' | column -t -s $'\t' + + gov_west: + name: Verify (West) + needs: commercial + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + strategy: + matrix: + layer: + - AWSLambdaPowertoolsPythonV3-python39 + - AWSLambdaPowertoolsPythonV3-python310 + - AWSLambdaPowertoolsPythonV3-python311 + - AWSLambdaPowertoolsPythonV3-python312 + - AWSLambdaPowertoolsPythonV3-python313 + - AWSLambdaPowertoolsPythonV3-python314 + arch: + - arm64 + - x86_64 + environment: GovCloud Prod (West) + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + role-to-assume: ${{ secrets.AWS_IAM_ROLE }} + aws-region: us-gov-east-1 + mask-aws-account-id: true + - name: Verify Layer ${{ matrix.layer }}-${{ matrix.arch }} + id: verify-layer + run: | + aws --region us-gov-west-1 lambda get-layer-version-by-arn --arn 'arn:aws-us-gov:lambda:us-gov-west-1:${{ secrets.AWS_ACCOUNT_ID }}:layer:${{ matrix.layer }}-${{ matrix.arch }}:${{ inputs.version }}' | jq -r '{"Layer Version Arn": .LayerVersionArn, "Version": .Version, "Description": .Description, "Compatible Runtimes": .CompatibleRuntimes[0], "Compatible Architectures": .CompatibleArchitectures[0], "SHA": .Content.CodeSha256} | keys[] as $k | [$k, .[$k]] | @tsv' | column -t -s $'\t' diff --git a/.github/workflows/layers_partition_verify.yml b/.github/workflows/layers_partition_verify.yml new file mode 100644 index 00000000000..d3973e8083d --- /dev/null +++ b/.github/workflows/layers_partition_verify.yml @@ -0,0 +1,158 @@ +# Partition Layer Verification +# --- +# This workflow queries the Partition layer info in production only + +on: + workflow_dispatch: + inputs: + environment: + description: Deployment environment + type: choice + options: + - Gamma + - Prod + required: true + version: + description: Layer version to verify + type: string + required: true + partition_version: + description: Layer version to verify, this is mostly used in Gamma where a version mismatch might exist + type: string + required: false + partition: + description: Partition to deploy to + type: choice + options: + - China + - GovCloud + workflow_call: + inputs: + environment: + description: Deployment environment + type: string + required: true + version: + description: Layer version to verify + type: string + required: true + partition_version: + description: Partition Layer version to verify, this is mostly used in Gamma where a version mismatch might exist + type: string + required: false + +name: Layer Verification (Partition) +run-name: Layer Verification (${{ inputs.partition }}) - ${{ inputs.environment }} / Version - ${{ inputs.version }} + +permissions: {} + +jobs: + setup: + runs-on: ubuntu-latest + outputs: + regions: ${{ format('{0}{1}', steps.regions_china.outputs.regions, steps.regions_govcloud.outputs.regions) }} + partition: ${{ format('{0}{1}', steps.regions_china.outputs.partition, steps.regions_govcloud.outputs.partition) }} + aud: ${{ format('{0}{1}', steps.regions_china.outputs.aud, steps.regions_govcloud.outputs.aud) }} + steps: + - id: regions_china + name: Partition (China) + if: ${{ inputs.partition == 'China' }} + run: | + echo regions='["cn-north-1"]'>> "$GITHUB_OUTPUT" + echo partition='aws-cn'>> "$GITHUB_OUTPUT" + echo aud='sts.amazonaws.com.cn'>> "$GITHUB_OUTPUT" + - id: regions_govcloud + name: Partition (GovCloud) + if: ${{ inputs.partition == 'GovCloud' }} + run: | + echo regions='["us-gov-east-1", "us-gov-west-1"]'>> "$GITHUB_OUTPUT" + echo partition='aws-us-gov'>> "$GITHUB_OUTPUT" + echo aud='sts.amazonaws.com'>> "$GITHUB_OUTPUT" + commercial: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + environment: Prod (Readonly) + strategy: + matrix: + layer: + - AWSLambdaPowertoolsPythonV3-python39 + - AWSLambdaPowertoolsPythonV3-python310 + - AWSLambdaPowertoolsPythonV3-python311 + - AWSLambdaPowertoolsPythonV3-python312 + - AWSLambdaPowertoolsPythonV3-python313 + - AWSLambdaPowertoolsPythonV3-python314 + arch: + - arm64 + - x86_64 + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + role-to-assume: ${{ secrets.AWS_IAM_ROLE }} + aws-region: us-east-1 + mask-aws-account-id: true + - name: Output ${{ matrix.layer }}-${{ matrix.arch }} + # fetch the specific layer version information from the us-east-1 commercial region + 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ matrix.layer }}-${{ matrix.arch }}.json + path: ${{ matrix.layer }}-${{ matrix.arch }}.json + retention-days: 1 + if-no-files-found: error + + verify: + name: Verify + needs: + - setup + - commercial + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + # Environment should interperlate as "GovCloud Prod" or "China Beta" + environment: ${{ inputs.partition }} ${{ inputs.environment }} + strategy: + matrix: + region: ${{ fromJson(needs.setup.outputs.regions) }} + layer: + - AWSLambdaPowertoolsPythonV3-python39 + - AWSLambdaPowertoolsPythonV3-python310 + - AWSLambdaPowertoolsPythonV3-python311 + - AWSLambdaPowertoolsPythonV3-python312 + - AWSLambdaPowertoolsPythonV3-python313 + - AWSLambdaPowertoolsPythonV3-python314 + arch: + - arm64 + - x86_64 + steps: + - name: Download Metadata + 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@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + role-to-assume: ${{ secrets[format('IAM_ROLE_{0}', steps.transform.outputs.CONVERTED_REGION)] }} + aws-region: ${{ matrix.region}} + mask-aws-account-id: true + audience: ${{ needs.setup.outputs.aud }} + - id: partition_version + name: Partition Layer Version + run: | + echo 'partition_version=$([[ -n "${{ inputs.partition_version}}" ]] && echo ${{ inputs.partition_version}} || echo ${{ inputs.version }} )' >> "$GITHUB_OUTPUT" + - name: Verify Layer + run: | + export layer_output='${{ matrix.layer }}-${{ matrix.arch }}-${{matrix.region}}.json' + aws --region ${{ matrix.region}} lambda get-layer-version-by-arn --arn "arn:${{ needs.setup.outputs.partition }}:lambda:${{ matrix.region}}:${{ secrets[format('AWS_ACCOUNT_{0}', steps.transform.outputs.CONVERTED_REGION)] }}:layer:${{ matrix.layer }}-${{ matrix.arch }}:${{ steps.partition_version.outputs.partition_version }}" > $layer_output + REMOTE_SHA=$(jq -r '.Content.CodeSha256' $layer_output) + LOCAL_SHA=$(jq -r '.Content.CodeSha256' ${{ matrix.layer }}-${{ matrix.arch }}.json) + test "$REMOTE_SHA" == "$LOCAL_SHA" && echo "SHA OK: ${LOCAL_SHA}" || exit 1 + 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' diff --git a/.github/workflows/layers_partitions.yml b/.github/workflows/layers_partitions.yml new file mode 100644 index 00000000000..433ac84e357 --- /dev/null +++ b/.github/workflows/layers_partitions.yml @@ -0,0 +1,195 @@ +# Partitioned Layer Publish +# --- +# This workflow publishes a specific layer version in an AWS account based on the environment input. +# +# We pull each the version of the layer and store them as artifacts, the we upload them to each of the Partitioned AWS accounts. +# +# A number of safety checks are performed to ensure safety. + +on: + workflow_dispatch: + inputs: + environment: + description: Deployment environment + type: choice + options: + - Gamma + - Prod + required: true + version: + description: Layer version to duplicate + type: string + required: true + partition: + description: Partition to deploy to + type: choice + options: + - China + - GovCloud + workflow_call: + inputs: + environment: + description: Deployment environment + type: string + required: true + version: + description: Layer version to duplicate + type: string + required: true + +name: Layer Deployment (Partitions) +run-name: Layer Deployment (${{ inputs.partition }}) - ${{ inputs.environment }} / Version - ${{ inputs.version }} + +permissions: + contents: read + +jobs: + setup: + runs-on: ubuntu-latest + outputs: + regions: ${{ format('{0}{1}', steps.regions_china.outputs.regions, steps.regions_govcloud.outputs.regions) }} + partition: ${{ format('{0}{1}', steps.regions_china.outputs.partition, steps.regions_govcloud.outputs.partition) }} + aud: ${{ format('{0}{1}', steps.regions_china.outputs.aud, steps.regions_govcloud.outputs.aud) }} + steps: + - id: regions_china + name: Partition (China) + if: ${{ inputs.partition == 'China' }} + run: | + echo regions='["cn-north-1"]'>> "$GITHUB_OUTPUT" + echo partition='aws-cn'>> "$GITHUB_OUTPUT" + echo aud='sts.amazonaws.com.cn'>> "$GITHUB_OUTPUT" + - id: regions_govcloud + name: Partition (GovCloud) + if: ${{ inputs.partition == 'GovCloud' }} + run: | + echo regions='["us-gov-east-1", "us-gov-west-1"]'>> "$GITHUB_OUTPUT" + echo partition='aws-us-gov'>> "$GITHUB_OUTPUT" + echo aud='sts.amazonaws.com'>> "$GITHUB_OUTPUT" + download: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + environment: Prod (Readonly) + strategy: + matrix: + layer: + - AWSLambdaPowertoolsPythonV3-python39 + - AWSLambdaPowertoolsPythonV3-python310 + - AWSLambdaPowertoolsPythonV3-python311 + - AWSLambdaPowertoolsPythonV3-python312 + - AWSLambdaPowertoolsPythonV3-python313 + - AWSLambdaPowertoolsPythonV3-python314 + arch: + - arm64 + - x86_64 + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + role-to-assume: ${{ secrets.AWS_IAM_ROLE }} + aws-region: us-east-1 + mask-aws-account-id: true + - name: Grab Zip + 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 }} --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@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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ matrix.layer }}-${{ matrix.arch }}.json + path: ${{ matrix.layer }}-${{ matrix.arch }}.json + retention-days: 1 + if-no-files-found: error + + copy: + name: Copy + needs: + - setup + - download + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + # Environment should interperlate as "GovCloud Prod" or "China Beta" + environment: ${{ inputs.partition }} ${{ inputs.environment }} + strategy: + matrix: + region: ${{ fromJson(needs.setup.outputs.regions) }} + layer: + - AWSLambdaPowertoolsPythonV3-python39 + - AWSLambdaPowertoolsPythonV3-python310 + - AWSLambdaPowertoolsPythonV3-python311 + - AWSLambdaPowertoolsPythonV3-python312 + - AWSLambdaPowertoolsPythonV3-python313 + - AWSLambdaPowertoolsPythonV3-python314 + arch: + - arm64 + - x86_64 + steps: + - name: Download Zip + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ matrix.layer }}-${{ matrix.arch }}.zip + - name: Download Metadata + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ matrix.layer }}-${{ matrix.arch }}.json + - name: Verify Layer Signature + run: | + 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 + - 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@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + role-to-assume: ${{ secrets[format('IAM_ROLE_{0}', steps.transform.outputs.CONVERTED_REGION)] }} + aws-region: ${{ matrix.region}} + mask-aws-account-id: true + audience: ${{ needs.setup.outputs.aud }} + - name: Create Layer + 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 \ + --query 'Version' \ + --output text) + + echo "LAYER_VERSION=$LAYER_VERSION" >> "$GITHUB_OUTPUT" + + aws --region ${{ matrix.region}} lambda add-layer-version-permission \ + --layer-name ${{ matrix.layer }}-${{ matrix.arch }} \ + --statement-id 'PublicLayer' \ + --action lambda:GetLayerVersion \ + --principal '*' \ + --version-number "$LAYER_VERSION" + - name: Verify Layer + env: + LAYER_VERSION: ${{ steps.create-layer.outputs.LAYER_VERSION }} + run: | + export layer_output='${{ matrix.layer }}-${{ matrix.arch }}-${{matrix.region}}.json' + aws --region ${{ matrix.region}} lambda get-layer-version-by-arn --arn 'arn:${{ needs.setup.outputs.partition }}:lambda:${{ matrix.region}}:${{ secrets[format('AWS_ACCOUNT_{0}', steps.transform.outputs.CONVERTED_REGION)] }}:layer:${{ matrix.layer }}-${{ matrix.arch }}:${{ env.LAYER_VERSION }}' > $layer_output + REMOTE_SHA=$(jq -r '.Content.CodeSha256' $layer_output) + LOCAL_SHA=$(jq -r '.Content.CodeSha256' '${{ matrix.layer }}-${{ matrix.arch }}.json') + test "$REMOTE_SHA" == "$LOCAL_SHA" && echo "SHA OK: ${LOCAL_SHA}" || exit 1 + 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ matrix.layer }}-${{ matrix.arch }}-${{ matrix.region }}.json + path: ${{ matrix.layer }}-${{ matrix.arch }}-${{ matrix.region }}.json + retention-days: 1 + if-no-files-found: error diff --git a/.github/workflows/on_closed_issues.yml b/.github/workflows/on_closed_issues.yml deleted file mode 100644 index 61a14b028d4..00000000000 --- a/.github/workflows/on_closed_issues.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Closed Issue Message - -# PROCESS -# -# 1. Comment on recently closed issues to warn future responses may not be looked after - -# USAGE -# -# Always triggered upon issue closure -# - -on: - issues: - types: [closed] -permissions: - contents: read - -jobs: - auto_comment: - runs-on: ubuntu-latest - permissions: - issues: write # comment on issues - steps: - - uses: aws-actions/closed-issue-message@8b6324312193476beecf11f8e8539d73a3553bf4 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" - message: | - ### ⚠️COMMENT VISIBILITY WARNING⚠️ - This issue is now closed. Please be mindful that future comments are hard for our team to see. - - If you need more assistance, please either tag a [team member](https://github.com/aws-powertools/powertools-lambda-python/blob/develop/MAINTAINERS.md#current-maintainers) or open a new issue that references this one. - - If you wish to keep having a conversation with other community members under this issue feel free to do so. diff --git a/.github/workflows/on_label_added.yml b/.github/workflows/on_label_added.yml deleted file mode 100644 index 8f7194097e3..00000000000 --- a/.github/workflows/on_label_added.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: On Label added - -# PROCESS -# -# 1. Fetch PR details previously saved from untrusted location -# 2. Parse details for safety -# 3. Comment on PR labels `size/XXL` and suggest splitting into smaller PRs if possible - -# USAGE -# -# NOTE: meant to be used with ./.github/workflows/record_pr.yml -# -# Security Note: -# -# This workflow depends on "Record PR" workflow that runs in an untrusted location (forks) instead of `pull_request_target`. -# This enforces zero trust where "Record PR" workflow always runs on fork with zero permissions on GH_TOKEN. -# When "Record PR" completes, this workflow runs in our repository with the appropriate permissions and sanitize inputs. -# -# Coupled with "Approve GitHub Action to run on forks", we have confidence no privilege can be escalated, -# since any malicious change would need to be approved, and upon social engineering, it'll have zero permissions. - -on: - workflow_run: - workflows: ["Record PR details"] - types: - - completed - -permissions: - contents: read - -jobs: - get_pr_details: - permissions: - actions: read # download PR artifact - contents: read # checkout code - if: ${{ github.event.workflow_run.conclusion == 'success' }} - uses: ./.github/workflows/reusable_export_pr_details.yml - with: - record_pr_workflow_id: ${{ github.event.workflow_run.id }} - workflow_origin: ${{ github.event.repository.full_name }} - secrets: - token: ${{ secrets.GITHUB_TOKEN }} - - split_large_pr: - needs: get_pr_details - runs-on: ubuntu-latest - permissions: - pull-requests: write # comment on PR - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - # Maintenance: Persist state per PR as an artifact to avoid spam on label add - - name: "Suggest split large Pull Request" - uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 - env: - PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }} - PR_ACTION: ${{ needs.get_pr_details.outputs.prAction }} - PR_AUTHOR: ${{ needs.get_pr_details.outputs.prAuthor }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const script = require('.github/scripts/comment_on_large_pr.js'); - await script({github, context, core}); diff --git a/.github/workflows/on_merged_pr.yml b/.github/workflows/on_merged_pr.yml deleted file mode 100644 index e435d59951d..00000000000 --- a/.github/workflows/on_merged_pr.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: On PR merge - -# PROCESS -# -# 1. Fetch PR details previously saved from untrusted location -# 2. Parse details for safety -# 3. Add `pending-release` label for related issue -# 4. Make a comment in PR if related issue is invalid or can't be labeled - -# USAGE -# -# NOTE: meant to be used with ./.github/workflows/record_pr.yml -# -# Security Note: -# -# This workflow depends on "Record PR" workflow that runs in an untrusted location (forks) instead of `pull_request_target`. -# This enforces zero trust where "Record PR" workflow always runs on fork with zero permissions on GH_TOKEN. -# When "Record PR" completes, this workflow runs in our repository with the appropriate permissions and sanitize inputs. -# -# Coupled with "Approve GitHub Action to run on forks", we have confidence no privilege can be escalated, -# since any malicious change would need to be approved, and upon social engineering, it'll have zero permissions. - -on: - workflow_run: - workflows: ["Record PR details"] - types: - - completed - -permissions: - contents: read - -jobs: - get_pr_details: - permissions: - actions: read # download PR artifact - contents: read # checkout code - if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' - uses: ./.github/workflows/reusable_export_pr_details.yml - with: - record_pr_workflow_id: ${{ github.event.workflow_run.id }} - workflow_origin: ${{ github.event.repository.full_name }} - secrets: - token: ${{ secrets.GITHUB_TOKEN }} - release_label_on_merge: - needs: get_pr_details - runs-on: ubuntu-latest - permissions: - pull-requests: write # make a comment in PR if unable to find related issue - issues: write # label issue with pending-release - if: needs.get_pr_details.outputs.prIsMerged == 'true' - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: "Label PR related issue for release" - uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 - env: - PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }} - PR_BODY: ${{ needs.get_pr_details.outputs.prBody }} - PR_IS_MERGED: ${{ needs.get_pr_details.outputs.prIsMerged }} - PR_AUTHOR: ${{ needs.get_pr_details.outputs.prAuthor }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const script = require('.github/scripts/label_related_issue.js') - await script({github, context, core}) diff --git a/.github/workflows/on_opened_pr.yml b/.github/workflows/on_opened_pr.yml deleted file mode 100644 index 1b9cb2f4de2..00000000000 --- a/.github/workflows/on_opened_pr.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: On new PR - -# PROCESS -# -# 1. Fetch PR details previously saved from untrusted location -# 2. Parse details for safety -# 3. Confirm there is a related issue for newly opened PR -# 4. Verify if PR template is used and legal acknowledgement hasn't been removed - -# USAGE -# -# NOTE: meant to be used with ./.github/workflows/record_pr.yml -# -# Security Note: -# -# This workflow depends on "Record PR" workflow that runs in an untrusted location (forks) instead of `pull_request_target`. -# This enforces zero trust where "Record PR" workflow always runs on fork with zero permissions on GH_TOKEN. -# When "Record PR" completes, this workflow runs in our repository with the appropriate permissions and sanitize inputs. -# -# Coupled with "Approve GitHub Action to run on forks", we have confidence no privilege can be escalated, -# since any malicious change would need to be approved, and upon social engineering, it'll have zero permissions. - -on: - workflow_run: - workflows: ["Record PR details"] - types: - - completed - -permissions: - contents: read - -jobs: - get_pr_details: - permissions: - actions: read # download PR artifact - contents: read # checkout code - if: ${{ github.event.workflow_run.conclusion == 'success' }} - uses: ./.github/workflows/reusable_export_pr_details.yml - with: - record_pr_workflow_id: ${{ github.event.workflow_run.id }} - workflow_origin: ${{ github.event.repository.full_name }} - secrets: - token: ${{ secrets.GITHUB_TOKEN }} - check_related_issue: - permissions: - pull-requests: write # label and comment on PR if missing related issue (requirement) - needs: get_pr_details - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: "Ensure related issue is present" - uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 - env: - PR_BODY: ${{ needs.get_pr_details.outputs.prBody }} - PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }} - PR_ACTION: ${{ needs.get_pr_details.outputs.prAction }} - PR_AUTHOR: ${{ needs.get_pr_details.outputs.prAuthor }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const script = require('.github/scripts/label_missing_related_issue.js') - await script({github, context, core}) - check_acknowledge_section: - needs: get_pr_details - runs-on: ubuntu-latest - permissions: - pull-requests: write # label and comment on PR if missing acknowledge section (requirement) - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: "Ensure acknowledgement section is present" - uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 - env: - PR_BODY: ${{ needs.get_pr_details.outputs.prBody }} - PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }} - PR_ACTION: ${{ needs.get_pr_details.outputs.prAction }} - PR_AUTHOR: ${{ needs.get_pr_details.outputs.prAuthor }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const script = require('.github/scripts/label_missing_acknowledgement_section.js') - await script({github, context, core}) diff --git a/.github/workflows/on_pr_updates.yml b/.github/workflows/on_pr_updates.yml index 2663d707399..b81bfe228e5 100644 --- a/.github/workflows/on_pr_updates.yml +++ b/.github/workflows/on_pr_updates.yml @@ -13,8 +13,7 @@ name: PR requirements # NOTES # -# PR requirements are checked async in on_opened_pr.yml and enforced here synchronously -# due to limitations in GH API. +# PR requirements are enforced synchronously here. on: pull_request: @@ -32,5 +31,5 @@ jobs: - name: Block if it doesn't minimum requirements if: contains(github.event.pull_request.labels.*.name, 'do-not-merge') run: | - echo "This PR does not meet minimum requirements (check PR comments)." + echo "PR has 'do-not-merge' label. Please resolve the issues mentioned in the PR comments." exit 1 diff --git a/.github/workflows/on_schedule_monthly_roadmap_reminder.yml b/.github/workflows/on_schedule_monthly_roadmap_reminder.yml new file mode 100644 index 00000000000..a274e2dea08 --- /dev/null +++ b/.github/workflows/on_schedule_monthly_roadmap_reminder.yml @@ -0,0 +1,22 @@ +name: Monthly roadmap reminder + +on: + workflow_dispatch: {} + schedule: + - cron: '0 0 1 * *' # runs first day of the month + +permissions: + contents: read + + +jobs: + call-workflow-passing-data: + permissions: + contents: read + pull-requests: read + issues: write # create monthly roadmap report + + # setting to `@main` until we have releases and governance installed + uses: aws-powertools/actions/.github/workflows/monthly_roadmap_reminder.yml@main + secrets: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ossf_scorecard.yml b/.github/workflows/ossf_scorecard.yml index 4d7d2b6fbe4..3c6e87ab8c7 100644 --- a/.github/workflows/ossf_scorecard.yml +++ b/.github/workflows/ossf_scorecard.yml @@ -15,25 +15,27 @@ jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest + environment: scorecard permissions: security-events: write # update code-scanning dashboard id-token: write # confirm org+repo identity before publish results steps: - name: "Checkout code" - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031 # v2.2.0 + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif publish_results: true # publish to OSSF Scorecard REST API + repo_token: ${{ secrets.SCORECARD_TOKEN }} # read-only fine-grained token to read branch protection settings - name: "Upload results" - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + 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 new file mode 100644 index 00000000000..07cf205ba20 --- /dev/null +++ b/.github/workflows/pre-release.yml @@ -0,0 +1,264 @@ +name: Pre-Release + +# PRE-RELEASE PROCESS +# +# === Automated activities === +# +# 1. [Seal] Bump to release version and export source code with integrity hash +# 2. [Quality check] Restore sealed source code, run tests, linting, security and complexity base line +# 3. [Build] Restore sealed source code, create and export hashed build artifact for PyPi release (wheel, tarball) +# 4. [Provenance] Generates provenance for build, signs attestation with GitHub OIDC claims to confirm it came from this release pipeline, commit, org, repo, branch, hash, etc. +# 5. [Release] Restore built artifact, and publish package to PyPi prod repository +# 6. [PR to bump version] Restore sealed source code, and create a PR to update trunk with latest released project metadata + +# NOTE +# +# See MAINTAINERS.md "Releasing a new version" for release mechanisms +# +# Every job is isolated and starts a new fresh container. + +env: + RELEASE_COMMIT: ${{ github.sha }} + +on: + workflow_dispatch: + inputs: + version: + description: "Pre-release version to publish (e.g., 3.24.0a0)" + required: true + type: string + skip_code_quality: + description: "Skip tests, linting, and baseline. Only use if release fail for reasons beyond our control and you need a quick release." + default: false + type: boolean + required: false + skip_pypi: + description: "Skip publishing to PyPi. Used for testing release steps." + default: false + type: boolean + required: false + +permissions: + contents: read + +jobs: + + # This job bumps the package version to the pre-release version + # creates an integrity hash from the source code + # uploads the artifact with the integrity hash as the key name + # so subsequent jobs can restore from a trusted point in time to prevent tampering + seal: + # ignore forks + if: github.repository == 'aws-powertools/powertools-lambda-python' + + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + integrity_hash: ${{ steps.seal_source_code.outputs.integrity_hash }} + artifact_name: ${{ steps.seal_source_code.outputs.artifact_name }} + RELEASE_VERSION: ${{ steps.release_version.outputs.RELEASE_VERSION }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ env.RELEASE_COMMIT }} + + - name: Set release version + id: release_version + run: echo "RELEASE_VERSION=${{ inputs.version }}" >> "$GITHUB_OUTPUT" + + - name: Verify pre-release version semantics + run: | + if [[ ! "${{ inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+[a-b][0-9]+$ ]]; then + echo "Version ${{ inputs.version }} doesn't look like a pre-release version (e.g., 3.24.0a0); aborting" + exit 1 + fi + + - name: Update version in pyproject.toml + run: sed -i 's/^version = ".*"/version = "${{ inputs.version }}"/' pyproject.toml + + - name: Update version in version.py + run: sed -i 's/^VERSION = ".*"/VERSION = "${{ inputs.version }}"/' aws_lambda_powertools/shared/version.py + + - name: Seal and upload + id: seal_source_code + uses: ./.github/actions/seal + with: + artifact_name_prefix: "source" + + # This job runs our automated test suite, complexity and security baselines + # it ensures previously merged have been tested as part of the pull request process + # + # NOTE + # + # we don't upload the artifact after testing to prevent any tampering of our source code dependencies + quality_check: + needs: seal + runs-on: ubuntu-latest + permissions: + contents: read + steps: + # NOTE: we need actions/checkout to configure git first (pre-commit hooks in make dev) + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ env.RELEASE_COMMIT }} + + - name: Restore sealed source code + uses: ./.github/actions/seal-restore + with: + integrity_hash: ${{ needs.seal.outputs.integrity_hash }} + artifact_name: ${{ needs.seal.outputs.artifact_name }} + + - name: Debug cache restore + run: cat pyproject.toml + + - name: Install poetry + run: pipx install git+https://github.com/python-poetry/poetry@bd500dd3bdfaec3de6894144c9cedb3a9358be84 # v2.0.1 + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.14" + cache: "poetry" + - name: Install dependencies + run: make dev + - name: Run all tests, linting and baselines + run: make pr + + # This job creates a release artifact (tar.gz, wheel) + # it checks out code from release commit for custom actions to work + # then restores the sealed source code (overwrites any potential tampering) + # it's done separately from release job to enforce least privilege. + # We export just the final build artifact for release + build: + runs-on: ubuntu-latest + needs: [quality_check, seal] + permissions: + contents: read + outputs: + integrity_hash: ${{ steps.seal_build.outputs.integrity_hash }} + artifact_name: ${{ steps.seal_build.outputs.artifact_name }} + attestation_hashes: ${{ steps.encoded_hash.outputs.attestation_hashes }} + steps: + # NOTE: we need actions/checkout to configure git first (pre-commit hooks in make dev) + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ env.RELEASE_COMMIT }} + + - name: Restore sealed source code + uses: ./.github/actions/seal-restore + with: + integrity_hash: ${{ needs.seal.outputs.integrity_hash }} + artifact_name: ${{ needs.seal.outputs.artifact_name }} + + - name: Install poetry + run: pipx install git+https://github.com/python-poetry/poetry@bd500dd3bdfaec3de6894144c9cedb3a9358be84 # v2.0.1 + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.14" + cache: "poetry" + + - name: Build python package and wheel + run: poetry build + + - name: Seal and upload + id: seal_build + uses: ./.github/actions/seal + with: + artifact_name_prefix: "build" + files: "dist/" + + # NOTE: SLSA retraces our build to its artifact to ensure it wasn't tampered + # coupled with GitHub OIDC, SLSA can then confidently sign it came from this release pipeline+commit+branch+org+repo+actor+integrity hash + - name: Create attestation encoded hash for provenance + id: encoded_hash + working-directory: dist + run: echo "attestation_hashes=$(sha256sum ./* | base64 -w0)" >> "$GITHUB_OUTPUT" + + # This job creates a provenance file that describes how our release was built (all steps) + # after it verifies our build is reproducible within the same pipeline + # it confirms that its own software and the CI build haven't been tampered with (Trust but verify) + # lastly, it creates and sign an attestation (multiple.intoto.jsonl) that confirms + # this build artifact came from this GitHub org, branch, actor, commit ID, inputs that triggered this pipeline, and matches its integrity hash + # NOTE: supply chain threats review (we protect against all of them now): https://slsa.dev/spec/v1.0/threats-overview + provenance: + needs: [seal, build] + permissions: + contents: write # nested job explicitly require despite upload assets being set to false + actions: read # To read the workflow path. + id-token: write # To sign the provenance. + # NOTE: provenance fails if we use action pinning... it's a Github limitation + # because SLSA needs to trace & attest it came from a given branch; pinning doesn't expose that information + # https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/generic/README.md#referencing-the-slsa-generator + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 + with: + base64-subjects: ${{ needs.build.outputs.attestation_hashes }} + upload-assets: false # we upload its attestation in create_tag job, otherwise it creates a new release + + # This job uses release artifact to publish to PyPi + # it exchanges JWT tokens with GitHub to obtain PyPi credentials + # since it's already registered as a Trusted Publisher. + # It uses the sealed build artifact (.whl, .tar.gz) to release it + release: + needs: [build, seal, provenance] + environment: pre-release + runs-on: ubuntu-latest + permissions: + id-token: write # OIDC for PyPi Trusted Publisher feature + env: + RELEASE_VERSION: ${{ needs.seal.outputs.RELEASE_VERSION }} + steps: + # NOTE: we need actions/checkout in order to use our local actions (e.g., ./.github/actions) + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ env.RELEASE_COMMIT }} + + - name: Restore sealed source code + uses: ./.github/actions/seal-restore + with: + integrity_hash: ${{ needs.build.outputs.integrity_hash }} + artifact_name: ${{ needs.build.outputs.artifact_name }} + + - name: Upload to PyPi prod + if: ${{ !inputs.skip_pypi }} + 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 + bump_version: + needs: [release, seal, provenance] + permissions: + contents: write # create-pr action creates a temporary branch + pull-requests: write # create-pr action creates a PR using the temporary branch + runs-on: ubuntu-latest + steps: + # NOTE: we need actions/checkout to authenticate and configure git first + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ env.RELEASE_COMMIT }} + + - name: Restore sealed source code + uses: ./.github/actions/seal-restore + with: + integrity_hash: ${{ needs.seal.outputs.integrity_hash }} + artifact_name: ${{ needs.seal.outputs.artifact_name }} + + - name: Download provenance + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{needs.provenance.outputs.provenance-name}} + + - name: Update provenance + run: mkdir -p "${PROVENANCE_DIR}" && mv "${PROVENANCE_FILE}" "${PROVENANCE_DIR}/" + env: + PROVENANCE_FILE: ${{ needs.provenance.outputs.provenance-name }} + PROVENANCE_DIR: provenance/${{ needs.seal.outputs.RELEASE_VERSION}} + + - name: Create PR + id: create-pr + uses: ./.github/actions/create-pr + with: + files: "pyproject.toml aws_lambda_powertools/shared/version.py provenance/" + temp_branch_prefix: "ci-bump" + pull_request_title: "chore(ci): new pre-release ${{ needs.seal.outputs.RELEASE_VERSION }}" + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish_v2_layer.yml b/.github/workflows/publish_v3_layer.yml similarity index 69% rename from .github/workflows/publish_v2_layer.yml rename to .github/workflows/publish_v3_layer.yml index d7712d0b912..958402adf98 100644 --- a/.github/workflows/publish_v2_layer.yml +++ b/.github/workflows/publish_v3_layer.yml @@ -1,16 +1,15 @@ -name: Deploy v2 layer to all regions +name: Deploy v3 layer to all regions # PROCESS # -# 1. Compile Layer using cdk-aws-lambda-powertools-layer CDK construct for x86 and ARM (uses custom runner as it's CPU heavy) +# 1. Compile Layer using cdk-aws-lambda-powertools-layer CDK construct for Python3.10-3.14 and x86_64/ARM architectures (uses custom runner as it's CPU heavy) # 2. Kick off pipeline for beta, prod, and canary releases # 3. Create PR to update trunk so staged docs also point to the latest Layer ARN, when merged # 4. Builds and publishes docs with latest Layer ARN using given version (generally coming from release) - # USAGE # -# NOTE: meant to be used with ./.github/workflows/release.yml +# NOTE: meant to be used with ./.github/workflows/release-v3.yml # # publish_layer: # needs: [seal, release, create_tag] @@ -32,7 +31,10 @@ on: workflow_dispatch: inputs: latest_published_version: - description: "Latest PyPi published version to rebuild latest docs for, e.g. 2.0.0, 2.0.0a1 (pre-release)" + description: "Latest PyPi published version to rebuild latest docs for, e.g. 3.0.0, 3.0.0a1 (pre-release)" + required: true + layer_documentation_version: + description: "Version to be updated in our documentation. e.g. if the current layer number is 3, this value must be 4." required: true source_code_artifact_name: description: "Artifact name to restore sealed source code" @@ -43,18 +45,27 @@ on: type: string required: true pre_release: - description: "Publishes documentation using a pre-release tag (2.0.0a1)." + description: "Publishes documentation using a pre-release tag (3.0.0a1)." default: false type: boolean required: false + skip_lambda_layer: + description: "Skip publishing Lambda Layers as it can publish duplicated versions of the same layer. Useful for semi-failed releases" + type: boolean + required: false + workflow_call: inputs: latest_published_version: type: string - description: "Latest PyPi published version to rebuild latest docs for, e.g. 2.0.0, 2.0.0a1 (pre-release)" + description: "Latest PyPi published version to rebuild latest docs for, e.g. 3.0.0, 3.0.0a1 (pre-release)" + required: true + layer_documentation_version: + type: string + description: "Version to be updated in our documentation. e.g. if the current layer number is 3, this value must be 4." required: true pre_release: - description: "Publishes documentation using a pre-release tag (2.0.0a1)." + description: "Publishes documentation using a pre-release tag (3.0.0a1)." default: false type: boolean required: false @@ -66,6 +77,11 @@ on: description: "Sealed source code integrity hash" type: string required: true + skip_lambda_layer: + description: "Skip publishing Lambda Layers as it can publish duplicated versions of the same layer. Useful for semi-failed releases" + default: false + type: boolean + required: false permissions: contents: read @@ -77,18 +93,22 @@ env: jobs: build-layer: permissions: - # lower privilege propagated from parent workflow (release.yml) + # lower privilege propagated from parent workflow (release-v3.yml) contents: read id-token: write pages: none pull-requests: none runs-on: aws-powertools_ubuntu-latest_8-core + strategy: + max-parallel: 5 + matrix: + python-version: ["3.10","3.11","3.12","3.13","3.14"] defaults: run: - working-directory: ./layer + working-directory: ./layer_v3 steps: - name: checkout - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.RELEASE_COMMIT }} @@ -99,15 +119,17 @@ jobs: artifact_name: ${{ inputs.source_code_artifact_name }} - name: Install poetry - run: pipx install git+https://github.com/python-poetry/poetry@68b88e5390720a3dd84f02940ec5200bfce39ac6 # v1.5.0 + run: | + 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@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: "16.12" + node-version: "18.20.4" - name: Setup python - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: "3.10" + python-version: ${{ matrix.python-version }} cache: "pip" - name: Resolve and install project dependencies # CDK spawns system python when compiling stack @@ -115,66 +137,73 @@ jobs: run: | poetry export --format requirements.txt --output requirements.txt pip install --require-hashes -r requirements.txt + - name: Set up QEMU - uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # 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@4c0219f9ac95b02789c1075625400b2acbff50b1 # v2.9.1 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 with: - install: true driver: docker platforms: linux/amd64,linux/arm64 - - name: install cdk and deps + + - name: Install CDK working-directory: ./ run: | npm ci npx cdk --version + + # Baking time for PyPi eventual consistency; 60s seemed more than enough + # https://github.com/aws-powertools/powertools-lambda-python/issues/2491 + - name: Baking time (PyPi) + run: sleep 60 + - name: CDK build - run: npx cdk synth --verbose --context version="${{ inputs.latest_published_version }}" -o cdk.out + 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.out.zip cdk.out + run: zip -r cdk.py${{ matrix.python-version }}.out.zip cdk.out - name: Archive CDK artifacts - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: cdk-layer-artefact - path: layer/cdk.out.zip + name: cdk-layer-artifact-py${{ matrix.python-version }} + path: layer_v3/cdk.py${{ matrix.python-version }}.out.zip beta: needs: build-layer - # lower privilege propagated from parent workflow (release.yml) + # lower privilege propagated from parent workflow (release-v3.yml) permissions: id-token: write contents: read pages: write # docs will be updated with latest Layer ARNs pull-requests: write # creation-action will create a PR with Layer ARN updates - uses: ./.github/workflows/reusable_deploy_v2_layer_stack.yml + uses: ./.github/workflows/reusable_deploy_v3_layer_stack.yml secrets: inherit with: stage: "BETA" - artefact-name: "cdk-layer-artefact" environment: "layer-beta" - latest_published_version: ${{ inputs.latest_published_version }} source_code_artifact_name: ${{ inputs.source_code_artifact_name }} source_code_integrity_hash: ${{ inputs.source_code_integrity_hash }} prod: + if: ${{ !inputs.skip_lambda_layer }} needs: beta - # lower privilege propagated from parent workflow (release.yml) + # lower privilege propagated from parent workflow (release-v3.yml) permissions: id-token: write contents: read pages: write # docs will be updated with latest Layer ARNs pull-requests: write # creation-action will create a PR with Layer ARN updates - uses: ./.github/workflows/reusable_deploy_v2_layer_stack.yml + uses: ./.github/workflows/reusable_deploy_v3_layer_stack.yml secrets: inherit with: stage: "PROD" - artefact-name: "cdk-layer-artefact" environment: "layer-prod" - latest_published_version: ${{ inputs.latest_published_version }} source_code_artifact_name: ${{ inputs.source_code_artifact_name }} source_code_integrity_hash: ${{ inputs.source_code_integrity_hash }} @@ -186,17 +215,15 @@ jobs: contents: read pull-requests: none pages: none - uses: ./.github/workflows/reusable_deploy_v2_sar.yml + uses: ./.github/workflows/reusable_deploy_v3_sar.yml secrets: inherit with: stage: "BETA" - artefact-name: "cdk-layer-artefact" environment: "layer-beta" package-version: ${{ inputs.latest_published_version }} source_code_artifact_name: ${{ inputs.source_code_artifact_name }} source_code_integrity_hash: ${{ inputs.source_code_integrity_hash }} - sar-prod: needs: sar-beta permissions: @@ -205,11 +232,10 @@ jobs: contents: read pull-requests: none pages: none - uses: ./.github/workflows/reusable_deploy_v2_sar.yml + uses: ./.github/workflows/reusable_deploy_v3_sar.yml secrets: inherit with: stage: "PROD" - artefact-name: "cdk-layer-artefact" environment: "layer-prod" package-version: ${{ inputs.latest_published_version }} source_code_artifact_name: ${{ inputs.source_code_artifact_name }} @@ -225,7 +251,7 @@ jobs: # where a new release creates a new doc (2.16.0) while layers are still pointing to 2.15 # because the PR has to be merged while release process is running - update_v2_layer_arn_docs: + update_v3_layer_arn_docs: needs: prod outputs: temp_branch: ${{ steps.create-pr.outputs.temp_branch }} @@ -238,7 +264,7 @@ jobs: pages: none steps: - name: Checkout repository # reusable workflows start clean, so we need to checkout again - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.RELEASE_COMMIT }} @@ -248,15 +274,8 @@ jobs: integrity_hash: ${{ inputs.source_code_integrity_hash }} artifact_name: ${{ inputs.source_code_artifact_name }} - - name: Download CDK layer artifact - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 - with: - name: cdk-layer-stack - path: cdk-layer-stack/ - name: Replace layer versions in documentation - run: | - ls -la cdk-layer-stack/ - ./layer/scripts/update_layer_arn.sh cdk-layer-stack + run: ./layer_v3/scripts/update_layer_arn_v3.sh ${{ inputs.layer_documentation_version }} # NOTE: It felt unnecessary creating yet another PR to update changelog w/ latest tag # since this is the only step in the release where we update docs from a temp branch - name: Update changelog with latest tag @@ -265,12 +284,11 @@ jobs: id: create-pr uses: ./.github/actions/create-pr with: - files: "docs/index.md examples CHANGELOG.md" + files: "docs/index.md docs/includes/_layer_homepage_arm64.md docs/includes/_layer_homepage_x86.md examples CHANGELOG.md" temp_branch_prefix: "ci-layer-docs" pull_request_title: "chore(ci): layer docs update" github_token: ${{ secrets.GITHUB_TOKEN }} - prepare_docs_alias: runs-on: ubuntu-latest permissions: @@ -292,7 +310,7 @@ jobs: echo DOCS_ALIAS="$DOCS_ALIAS" >> "$GITHUB_OUTPUT" release_docs: - needs: [update_v2_layer_arn_docs, prepare_docs_alias] + needs: [update_v3_layer_arn_docs, prepare_docs_alias] permissions: # lower privilege propagated from parent workflow (release.yml) contents: write @@ -304,4 +322,4 @@ jobs: with: version: ${{ inputs.latest_published_version }} alias: ${{ needs.prepare_docs_alias.outputs.DOCS_ALIAS }} - git_ref: ${{ needs.update_v2_layer_arn_docs.outputs.temp_branch }} + git_ref: ${{ needs.update_v3_layer_arn_docs.outputs.temp_branch }} diff --git a/.github/workflows/quality_check.yml b/.github/workflows/quality_check.yml index c51ea493f10..dfbc6528e0d 100644 --- a/.github/workflows/quality_check.yml +++ b/.github/workflows/quality_check.yml @@ -1,4 +1,4 @@ -name: Code quality +name: Quality check - check code # PROCESS # @@ -14,12 +14,12 @@ name: Code quality # # Always triggered on new PRs, PR changes and PR merge. - on: pull_request: paths: - "aws_lambda_powertools/**" - "tests/**" + - "examples/**" - "pyproject.toml" - "poetry.lock" - "mypy.ini" @@ -29,6 +29,7 @@ on: paths: - "aws_lambda_powertools/**" - "tests/**" + - "examples/**" - "pyproject.toml" - "poetry.lock" - "mypy.ini" @@ -42,37 +43,44 @@ jobs: quality_check: runs-on: ubuntu-latest strategy: - max-parallel: 4 + fail-fast: false + max-parallel: 5 matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] env: PYTHON: "${{ matrix.python-version }}" permissions: - contents: read # checkout code only + contents: read # checkout code only steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install poetry run: pipx install poetry - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} - cache: "poetry" - name: Install dependencies - run: make dev + run: make dev-quality-code + - name: Checking and enforcing format + run: make format-check - name: Formatting and Linting run: make lint - - name: Static type checking + - name: Static type checking (mypy) run: make mypy + - name: Static type checking (ty) + run: make ty - name: Test with pytest run: make test + - name: Test dependencies with Nox + run: make test-dependencies - name: Security baseline run: make security-baseline - name: Complexity baseline run: make complexity-baseline - name: Upload coverage to Codecov - uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # 3.1.4 + uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # 6.0.1 with: - file: ./coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml env_vars: PYTHON name: aws-lambda-powertools-python-codecov diff --git a/.github/workflows/quality_check_docs.yml b/.github/workflows/quality_check_docs.yml new file mode 100644 index 00000000000..9a61cf90a23 --- /dev/null +++ b/.github/workflows/quality_check_docs.yml @@ -0,0 +1,50 @@ +name: Quality check - Build docs + +# PROCESS +# +# 1. Install all dependencies required to build the docs +# 2. Build the docs + +# USAGE +# +# Always triggered on new PRs, PR changes and PR merge. + + +on: + pull_request: + paths: + - "docs/**" + - "examples/**" + - "mkdocs.yml" + branches: + - develop + push: + paths: + - "docs/**" + - "examples/**" + - "mkdocs.yml" + branches: + - develop + +permissions: + contents: read + +jobs: + build_docs: + runs-on: ubuntu-latest + permissions: + contents: read # checkout code only + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: 3.14 + - name: Install doc generation dependencies + run: | + cat docs/requirements.txt + pip install --require-hashes -r docs/requirements.txt + - name: Build docs + run: | + rm -rf site + mkdocs build diff --git a/.github/workflows/quality_code_cdk_constructor.yml b/.github/workflows/quality_code_cdk_constructor.yml new file mode 100644 index 00000000000..497d0bea446 --- /dev/null +++ b/.github/workflows/quality_code_cdk_constructor.yml @@ -0,0 +1,71 @@ +name: Code quality - CDK constructor + +# PROCESS +# +# 1. Install all dependencies and spin off containers for all supported Python versions +# 2. Run code formatters and linters (various checks) for code standard +# 3. Run static typing checker for potential bugs +# 4. Run tests + +# USAGE +# +# Always triggered on new PRs, PR changes and PR merge. + + +on: + pull_request: + paths: + - "layer_v3/layer_constructors/**" + branches: + - develop + push: + paths: + - "layer_v3/layer_constructors/**" + branches: + - develop + +permissions: + contents: read + +jobs: + quality_check_cdk: + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: ["3.14"] + env: + PYTHON: "${{ matrix.python-version }}" + permissions: + contents: read # checkout code only + defaults: + run: + working-directory: ./layer_v3/layer_constructors + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Install poetry + run: pipx install poetry + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: ${{ matrix.python-version }} + cache: "poetry" + - name: Set up QEMU + 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@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + with: + driver: docker + platforms: linux/amd64,linux/arm64 + - name: Install dependencies + run: | + pip install --upgrade pip pre-commit poetry + poetry install + - name: Test with pytest + run: poetry run pytest tests + env: + BUILDX_BUILDER: ${{ steps.builder.outputs.name }} diff --git a/.github/workflows/record_pr.yml b/.github/workflows/record_pr.yml deleted file mode 100644 index 0cdcb9bc8ad..00000000000 --- a/.github/workflows/record_pr.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Record PR details - -# PROCESS -# -# 1. Runs in fork location upon PR creation or changes -# 2. Saves GitHub Pull Request Webhook payload -# 3. Uploads as a temporary GitHub Action Artifact with shortest retention - -# USAGE -# -# see .github/workflows/on_merged_pr.yml and related for full example. -# -# on: -# workflow_run: -# workflows: ["Record PR details"] -# types: -# - completed -# -# Security Note: -# -# For security, this is intended to be a 2-step process: (1) collect PR, (2) act on PR. -# Do not ever use `pull_request_target` to "simplify", as it sends a write-token to the fork. Our linter should catch it. -# -# The first step runs in untrusted location (fork), therefore we limit permissions to only check out code. -# -# The second step will be workflows that want to act on a given PR, this time with intended permissions, and -# it runs on its base location (this repo!). -# -# This enforces zero trust where this workflow always runs on fork with zero permissions on GH_TOKEN. -# When this workflow completes, X workflows run in our repository with the appropriate permissions and sanitize inputs. -# -# Coupled with "Approve GitHub Action to run on forks", we have confidence no privilege can be escalated, -# since any malicious change would need to be approved, and upon social engineering, it'll have zero permissions. - - -on: - pull_request: - types: [opened, edited, closed, labeled] - -permissions: - contents: read - -jobs: - record_pr: - runs-on: ubuntu-latest - permissions: - contents: read # NOTE: treat as untrusted location - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: "Extract PR details" - uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 - with: - script: | - const script = require('.github/scripts/save_pr_details.js') - await script({github, context, core}) - - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - with: - name: pr - path: pr.txt - retention-days: 1 diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 8781eba3bef..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@65c5fb495d1e69aa8c08a3317bc44ff8aabe9772 # v5.20.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: release-drafter/release-drafter@c2e2804cc59f45f57076a99af580d0fedb697927 # v7.3.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release-v3.yml similarity index 81% rename from .github/workflows/release.yml rename to .github/workflows/release-v3.yml index c1b2bdb0026..64099e11413 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release-v3.yml @@ -1,4 +1,4 @@ -name: Release +name: Release V3 # RELEASE PROCESS # @@ -11,16 +11,16 @@ name: Release # 5. [Release] Restore built artifact, and publish package to PyPi prod repository # 6. [Create Tag] Restore sealed source code, create a new git tag using released version, uploads provenance to latest draft release # 7. [PR to bump version] Restore sealed source code, and create a PR to update trunk with latest released project metadata -# 8. [Publish Layer] Compile Layer and kick off pipeline for beta, prod, and canary releases -# 9. [Publish Layer] Update docs with latest Layer ARNs and Changelog -# 10. [Publish Layer] Create PR to update trunk so staged docs also point to the latest Layer ARN, when merged +# 8. [Publish Layer v3] Compile Layer in multiple Python versions and kick off pipeline for beta, prod, and canary releases +# 9. [Publish Layer v3] Update docs with latest Layer ARNs and Changelog +# 10. [Publish Layer v3] Create PR to update trunk so staged docs also point to the latest Layer ARN, when merged # 12. [Post release] Close all issues labeled "pending-release" and notify customers about the release # # === Manual activities === # # 1. Kick off this workflow with the intended version # 2. Update draft release notes after this workflow completes -# 3. If not already set, use `v` as a tag, e.g., v1.26.4, and select develop as target branch +# 3. If not already set, use `v` as a tag, e.g., v3.0.0, and select develop as target branch # NOTE # @@ -36,21 +36,30 @@ on: workflow_dispatch: inputs: version_to_publish: - description: "Version to be released in PyPi, Docs, and Lambda Layer, e.g. v2.0.0, v2.0.0a0 (pre-release)" - default: v2.0.0 + description: "Version to be released in PyPi, Docs, and Lambda Layer, e.g. v3.0.0, v3.0.0a0 (pre-release)" + default: v3.0.0 + required: true + layer_documentation_version: + description: "Lambda layer version to be updated in our documentation. e.g. if the current layer number is 3, this value must be 4." + type: string required: true skip_pypi: description: "Skip publishing to PyPi as it can't publish more than once. Useful for semi-failed releases" default: false type: boolean required: false + skip_lambda_layer: + description: "Skip publishing Lambda Layers as it can publish duplicated versions of the same layer. Useful for semi-failed releases" + default: false + type: boolean + required: false skip_code_quality: description: "Skip tests, linting, and baseline. Only use if release fail for reasons beyond our control and you need a quick release." default: false type: boolean required: false pre_release: - description: "Publishes documentation using a pre-release tag (v2.0.0a0). You are still responsible for passing a pre-release version tag to the workflow." + description: "Publishes documentation using a pre-release tag (v3.0.0a0). You are still responsible for passing a pre-release version tag to the workflow." default: false type: boolean required: false @@ -80,15 +89,15 @@ jobs: RELEASE_VERSION="${RELEASE_TAG_VERSION:1}" echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "$GITHUB_OUTPUT" - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.RELEASE_COMMIT }} # We use a pinned version of Poetry to be certain it won't modify source code before we create a hash - name: Install poetry run: | - pipx install git+https://github.com/python-poetry/poetry@68b88e5390720a3dd84f02940ec5200bfce39ac6 # v1.5.0 - pipx inject poetry git+https://github.com/monim67/poetry-bumpversion@315fe3324a699fa12ec20e202eb7375d4327d1c4 # v0.3.1 + pipx install git+https://github.com/python-poetry/poetry@bd500dd3bdfaec3de6894144c9cedb3a9358be84 # v2.0.1 + pipx inject poetry git+https://github.com/monim67/poetry-bumpversion@348de6f247222e2953d649932426e63492e0a6bf # v0.3.3 - name: Bump package version id: versioning @@ -115,7 +124,7 @@ jobs: contents: read steps: # NOTE: we need actions/checkout to configure git first (pre-commit hooks in make dev) - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.RELEASE_COMMIT }} @@ -129,11 +138,11 @@ jobs: run: cat pyproject.toml - name: Install poetry - run: pipx install git+https://github.com/python-poetry/poetry@68b88e5390720a3dd84f02940ec5200bfce39ac6 # v1.5.0 + run: pipx install git+https://github.com/python-poetry/poetry@bd500dd3bdfaec3de6894144c9cedb3a9358be84 # v2.0.1 - name: Set up Python - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: "3.10" + python-version: "3.14" cache: "poetry" - name: Install dependencies run: make dev @@ -156,7 +165,7 @@ jobs: attestation_hashes: ${{ steps.encoded_hash.outputs.attestation_hashes }} steps: # NOTE: we need actions/checkout to configure git first (pre-commit hooks in make dev) - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.RELEASE_COMMIT }} @@ -167,11 +176,11 @@ jobs: artifact_name: ${{ needs.seal.outputs.artifact_name }} - name: Install poetry - run: pipx install git+https://github.com/python-poetry/poetry@68b88e5390720a3dd84f02940ec5200bfce39ac6 # v1.5.0 + run: pipx install git+https://github.com/python-poetry/poetry@bd500dd3bdfaec3de6894144c9cedb3a9358be84 # v2.0.1 - name: Set up Python - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: "3.10" + python-version: "3.14" cache: "poetry" - name: Build python package and wheel @@ -206,7 +215,7 @@ jobs: # NOTE: provenance fails if we use action pinning... it's a Github limitation # because SLSA needs to trace & attest it came from a given branch; pinning doesn't expose that information # https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/generic/README.md#referencing-the-slsa-generator - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.7.0 + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 with: base64-subjects: ${{ needs.build.outputs.attestation_hashes }} upload-assets: false # we upload its attestation in create_tag job, otherwise it creates a new release @@ -225,7 +234,7 @@ jobs: RELEASE_VERSION: ${{ needs.seal.outputs.RELEASE_VERSION }} steps: # NOTE: we need actions/checkout in order to use our local actions (e.g., ./.github/actions) - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.RELEASE_COMMIT }} @@ -237,16 +246,16 @@ jobs: - name: Upload to PyPi prod if: ${{ !inputs.skip_pypi }} - uses: pypa/gh-action-pypi-publish@f8c70e705ffc13c3b4d1221169b84f12a75d6ca8 # v1.8.8 + 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@f8c70e705ffc13c3b4d1221169b84f12a75d6ca8 # v1.8.8 + # uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 # with: # repository-url: https://test.pypi.org/legacy/ - # We create a Git Tag using our release version (e.g., v2.16.0) + # We create a Git Tag using our release version (e.g., v3.16.0) # using our sealed source code we created earlier. # Because we bumped version of our project as part of CI # we need to add this into git before pushing the tag @@ -259,7 +268,7 @@ jobs: contents: write steps: # NOTE: we need actions/checkout to authenticate and configure git first - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.RELEASE_COMMIT }} @@ -273,7 +282,7 @@ jobs: name: Git client setup and refresh tip run: | git config user.name "Powertools for AWS Lambda (Python) bot" - git config user.email "aws-lambda-powertools-feedback@amazon.com" + git config user.email "151832416+aws-powertools-bot@users.noreply.github.com" git config remote.origin.url >&- - name: Create Git Tag @@ -303,7 +312,7 @@ jobs: runs-on: ubuntu-latest steps: # NOTE: we need actions/checkout to authenticate and configure git first - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.RELEASE_COMMIT }} @@ -330,7 +339,7 @@ jobs: # NOTE # # Watch out for the depth limit of 4 nested workflow_calls. - # publish_layer -> publish_v2_layer -> reusable_deploy_v2_layer_stack + # publish_layer -> publish_3_layer -> reusable_deploy_v3_layer_stack publish_layer: needs: [seal, release, create_tag] secrets: inherit @@ -339,12 +348,14 @@ jobs: contents: write pages: write pull-requests: write - uses: ./.github/workflows/publish_v2_layer.yml + uses: ./.github/workflows/publish_v3_layer.yml with: latest_published_version: ${{ needs.seal.outputs.RELEASE_VERSION }} + layer_documentation_version: ${{ inputs.layer_documentation_version }} pre_release: ${{ inputs.pre_release }} source_code_artifact_name: ${{ needs.seal.outputs.artifact_name }} source_code_integrity_hash: ${{ needs.seal.outputs.integrity_hash }} + skip_lambda_layer: ${{ inputs.skip_lambda_layer }} post_release: needs: [seal, release, publish_layer] @@ -357,20 +368,31 @@ jobs: env: RELEASE_VERSION: ${{ needs.seal.outputs.RELEASE_VERSION }} steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.RELEASE_COMMIT }} - - name: Restore sealed source code uses: ./.github/actions/seal-restore with: 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@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const post_release = require('.github/scripts/post_release.js') await post_release({github, context, core}) + + update_ssm: + needs: [seal, release, publish_layer] + secrets: inherit + permissions: + id-token: write + contents: read + uses: ./.github/workflows/update_ssm.yml + with: + environment: "Prod" + write_latest: true + package_version: ${{ needs.seal.outputs.RELEASE_VERSION }} + layer_version: ${{ inputs.layer_documentation_version }} diff --git a/.github/workflows/reusable_deploy_v2_layer_stack.yml b/.github/workflows/reusable_deploy_v3_layer_stack.yml similarity index 56% rename from .github/workflows/reusable_deploy_v2_layer_stack.yml rename to .github/workflows/reusable_deploy_v3_layer_stack.yml index f7d7dc0d409..58b7650e3cb 100644 --- a/.github/workflows/reusable_deploy_v2_layer_stack.yml +++ b/.github/workflows/reusable_deploy_v3_layer_stack.yml @@ -1,15 +1,16 @@ -name: Deploy CDK Layer v2 stack +name: Deploy CDK Layer v3 stack # PROCESS # # 1. Split what AWS regions support ARM vs regions that Lambda support ARM -# 2. Deploy previously built layer for each AWS commercial region -# 3. Export all published Layers as JSON -# 4. Deploy Canaries to every deployed region to test whether Powertools can be imported etc. +# 2. We build the Lambda layer for 3.10 to 3.14 Python runtime and both x86_64 and arm64 (see `matrix` section) +# 3. Deploy previously built layer for each AWS commercial region +# 4. Export all published Layers as JSON +# 5. Deploy Canaries to every deployed region to test whether Powertools can be imported etc. # USAGE # -# NOTE: meant to be used with ./.github/workflows/publish_v2_layer.yml +# NOTE: meant to be used with ./.github/workflows/publish_v3_layer.yml # # beta: # needs: build-layer @@ -19,13 +20,13 @@ name: Deploy CDK Layer v2 stack # contents: read # pages: write # docs will be updated with latest Layer ARNs # pull-requests: write # creation-action will create a PR with Layer ARN updates -# uses: ./.github/workflows/reusable_deploy_v2_layer_stack.yml +# uses: ./.github/workflows/reusable_deploy_v3_layer_stack.yml # secrets: inherit # with: # stage: "BETA" -# artefact-name: "cdk-layer-artefact" # environment: "layer-beta" -# latest_published_version: ${{ inputs.latest_published_version }} +# source_code_artifact_name: code.zip +# source_code_integrity_hash: sha256string on: workflow_call: @@ -34,18 +35,10 @@ on: description: "Deployment stage (BETA, PROD)" required: true type: string - artefact-name: - description: "CDK Layer Artefact name to download" - required: true - type: string environment: description: "GitHub Environment to use for encrypted secrets" required: true type: string - latest_published_version: - description: "Latest version that is published" - required: true - type: string source_code_artifact_name: description: "Artifact name to restore sealed source code" type: string @@ -65,7 +58,7 @@ jobs: deploy-cdk-stack: runs-on: ubuntu-latest environment: ${{ inputs.environment }} - # lower privilege propagated from parent workflow (publish_v2_layer.yml) + # lower privilege propagated from parent workflow (publish_v3_layer.yml) permissions: id-token: write pull-requests: none @@ -73,12 +66,19 @@ jobs: pages: none defaults: run: - working-directory: ./layer + working-directory: ./layer_v3 strategy: fail-fast: false matrix: # To get a list of current regions, use: # aws ec2 describe-regions --all-regions --query "Regions[].RegionName" --output text | tr "\t" "\n" | sort + region: ["af-south-1", "ap-east-1", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", + "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", "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: - region: "af-south-1" has_arm64_support: "true" @@ -93,7 +93,7 @@ jobs: - region: "ap-south-1" has_arm64_support: "true" - region: "ap-south-2" - has_arm64_support: "false" + has_arm64_support: "true" - region: "ap-southeast-1" has_arm64_support: "true" - region: "ap-southeast-2" @@ -101,28 +101,34 @@ jobs: - region: "ap-southeast-3" has_arm64_support: "true" - region: "ap-southeast-4" - has_arm64_support: "false" + has_arm64_support: "true" + - region: "ap-southeast-5" + has_arm64_support: "true" + - region: "ap-southeast-7" + has_arm64_support: "true" - region: "ca-central-1" has_arm64_support: "true" + - region: "ca-west-1" + has_arm64_support: "true" - region: "eu-central-1" has_arm64_support: "true" - region: "eu-central-2" - has_arm64_support: "false" + has_arm64_support: "true" - region: "eu-north-1" has_arm64_support: "true" - region: "eu-south-1" has_arm64_support: "true" - region: "eu-south-2" - has_arm64_support: "false" + has_arm64_support: "true" - region: "eu-west-1" has_arm64_support: "true" - region: "eu-west-2" has_arm64_support: "true" - region: "eu-west-3" has_arm64_support: "true" - - region: "me-central-1" - has_arm64_support: "false" - - region: "me-south-1" + - region: "il-central-1" + has_arm64_support: "true" + - region: "mx-central-1" has_arm64_support: "true" - region: "sa-east-1" has_arm64_support: "true" @@ -136,7 +142,7 @@ jobs: has_arm64_support: "true" steps: - name: checkout - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.RELEASE_COMMIT }} @@ -147,20 +153,23 @@ jobs: artifact_name: ${{ inputs.source_code_artifact_name }} - name: Install poetry - run: pipx install git+https://github.com/python-poetry/poetry@68b88e5390720a3dd84f02940ec5200bfce39ac6 # v1.5.0 - - name: aws credentials - uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0 + run: | + 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@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@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: "16.12" + node-version: "18.20.4" - name: Setup python - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: "3.10" + python-version: ${{ matrix.python-version }} cache: "pip" - name: Resolve and install project dependencies # CDK spawns system python when compiling stack @@ -176,28 +185,35 @@ jobs: - name: install deps run: poetry install - name: Download artifact - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ inputs.artefact-name }} - path: layer + name: cdk-layer-artifact-py${{ matrix.python-version }} + path: layer_v3 - name: unzip artefact - run: unzip cdk.out.zip + run: unzip cdk.py${{ matrix.python-version }}.out.zip + - name: Define constants + id: constants + run: | + PYTHON_VERSION=$(echo ${{ matrix.python-version }} | tr -d '.') + echo "PYTHON_VERSION=${PYTHON_VERSION}" >> "$GITHUB_OUTPUT" + LAYER_VERSION=${{ matrix.region }}-$PYTHON_VERSION-layer-version.txt + echo "LAYER_VERSION=${LAYER_VERSION}" >> "$GITHUB_OUTPUT" - name: CDK Deploy Layer - run: npx cdk deploy --app cdk.out --context region=${{ matrix.region }} --parameters HasARM64Support=${{ matrix.has_arm64_support }} 'LayerV2Stack' --require-approval never --verbose --outputs-file cdk-outputs.json + run: npx cdk deploy --app cdk.out --context region=${{ matrix.region }} --parameters HasARM64Support=${{ matrix.has_arm64_support }} "LayerV3Stack-python${{steps.constants.outputs.PYTHON_VERSION}}" --require-approval never --verbose --outputs-file cdk-outputs.json - name: Store latest Layer ARN if: ${{ inputs.stage == 'PROD' }} run: | mkdir cdk-layer-stack - jq -r -c '.LayerV2Stack.LatestLayerArn' cdk-outputs.json > cdk-layer-stack/${{ matrix.region }}-layer-version.txt - jq -r -c '.LayerV2Stack.LatestLayerArm64Arn' cdk-outputs.json >> cdk-layer-stack/${{ matrix.region }}-layer-version.txt - cat cdk-layer-stack/${{ matrix.region }}-layer-version.txt + jq -r -c ".[\"LayerV3Stack-python${{steps.constants.outputs.PYTHON_VERSION}}\"].LatestLayerArn" cdk-outputs.json > cdk-layer-stack/${{steps.constants.outputs.LAYER_VERSION}} + jq -r -c ".[\"LayerV3Stack-python${{steps.constants.outputs.PYTHON_VERSION}}\"].LatestLayerArm64Arn" cdk-outputs.json >> cdk-layer-stack/${{steps.constants.outputs.LAYER_VERSION}} + cat cdk-layer-stack/${{steps.constants.outputs.LAYER_VERSION}} - name: Save Layer ARN artifact if: ${{ inputs.stage == 'PROD' }} - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: cdk-layer-stack - path: ./layer/cdk-layer-stack/* # NOTE: upload-artifact does not inherit working-directory setting. + name: cdk-layer-stack-${{ matrix.region }}-${{ matrix.python-version }} + path: ./layer_v3/cdk-layer-stack/* # NOTE: upload-artifact does not inherit working-directory setting. if-no-files-found: error retention-days: 1 - name: CDK Deploy Canary - run: npx cdk deploy --app cdk.out --context region=${{ matrix.region }} --parameters DeployStage="${{ inputs.stage }}" --parameters HasARM64Support=${{ matrix.has_arm64_support }} 'CanaryV2Stack' --require-approval never --verbose + run: npx cdk deploy --app cdk.out --context region=${{ matrix.region }} --parameters DeployStage="${{ inputs.stage }}" --parameters HasARM64Support=${{ matrix.has_arm64_support }} "CanaryV3Stack-python${{steps.constants.outputs.PYTHON_VERSION}}" --require-approval never --verbose diff --git a/.github/workflows/reusable_deploy_v2_sar.yml b/.github/workflows/reusable_deploy_v3_sar.yml similarity index 72% rename from .github/workflows/reusable_deploy_v2_sar.yml rename to .github/workflows/reusable_deploy_v3_sar.yml index beab36f24c2..3fc8cbc2fd4 100644 --- a/.github/workflows/reusable_deploy_v2_sar.yml +++ b/.github/workflows/reusable_deploy_v3_sar.yml @@ -1,17 +1,17 @@ -name: Deploy V2 SAR +name: Deploy V3 SAR # PROCESS # -# 1. This workflow starts after the layer artifact is produced on `publish_v2_layer` +# 1. This workflow starts after the layer artifact is produced on `publish_v3_layer` # 2. We use the same layer artifact to ensure the SAR app is consistent with the published Lambda Layer -# 3. We publish the SAR for both x86_64 and arm64 (see `matrix` section) +# 3. We publish the SAR for 3.10 to 3.14 Python runtime and both x86_64 and arm64 (see `matrix` section) # 4. We use `sam package` and `sam publish` to publish the SAR app # 5. We remove the previous Canary stack (if present) and deploy a new one to test the SAR App. We retain the Canary in the account for debugging purposes # 6. Finally the published SAR app is made public on the PROD environment # USAGE # -# NOTE: meant to be used with ./.github/workflows/publish_v2_layer.yml +# NOTE: meant to be used with ./.github/workflows/publish_v3_layer.yml # # sar-beta: # needs: build-layer @@ -21,11 +21,10 @@ name: Deploy V2 SAR # contents: read # pull-requests: none # pages: none -# uses: ./.github/workflows/reusable_deploy_v2_sar.yml +# uses: ./.github/workflows/reusable_deploy_v3_sar.yml # secrets: inherit # with: # stage: "BETA" -# artefact-name: "cdk-layer-artefact" # environment: "layer-beta" # package-version: ${{ inputs.latest_published_version }} # source_code_artifact_name: ${{ inputs.source_code_artifact_name }} @@ -36,10 +35,10 @@ permissions: contents: read env: - NODE_VERSION: 16.12 + NODE_VERSION: 18.20.4 AWS_REGION: eu-west-1 - SAR_NAME: aws-lambda-powertools-python-layer - TEST_STACK_NAME: serverlessrepo-v2-powertools-layer-test-stack + SAR_NAME: aws-lambda-powertools-python-layer-v3 + TEST_STACK_NAME: serverlessrepo-v3-powertools-layer-test-stack RELEASE_COMMIT: ${{ github.sha }} # it gets propagated from the caller for security reasons on: @@ -49,10 +48,6 @@ on: description: "Deployment stage (BETA, PROD)" required: true type: string - artefact-name: - description: "CDK Layer Artefact name to download" - required: true - type: string package-version: description: "The version of the package to deploy" required: true @@ -77,9 +72,10 @@ jobs: strategy: matrix: architecture: ["x86_64", "arm64"] + python-version: ["3.10","3.11","3.12","3.13","3.14"] steps: - name: checkout - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.RELEASE_COMMIT }} @@ -90,18 +86,19 @@ jobs: artifact_name: ${{ inputs.source_code_artifact_name }} - - name: AWS credentials - uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0 + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 with: aws-region: ${{ env.AWS_REGION }} role-to-assume: ${{ secrets.AWS_LAYERS_ROLE_ARN }} + mask-aws-account-id: true # NOTE # We connect to Layers account to log our intent to publish a SAR Layer # 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@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.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,64 +106,69 @@ jobs: aws-session-token: ${{ env.AWS_SESSION_TOKEN }} role-duration-seconds: 1200 aws-region: ${{ env.AWS_REGION }} - role-to-assume: ${{ secrets.AWS_SAR_V2_ROLE_ARN }} + role-to-assume: ${{ secrets.AWS_SAR_V3_ROLE_ARN }} + mask-aws-account-id: true - name: Setup Node.js - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ env.NODE_VERSION }} - name: Download artifact - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ inputs.artefact-name }} + name: cdk-layer-artifact-py${{ matrix.python-version }} - name: Unzip artefact - run: unzip cdk.out.zip + run: unzip cdk.py${{ matrix.python-version }}.out.zip + - name: normalize Python Version + run: | + PYTHON_VERSION=$(echo ${{ matrix.python-version }} | tr -d '.') + echo "PYTHON_VERSION=${PYTHON_VERSION}" >> "$GITHUB_ENV" - name: Configure SAR name run: | if [[ "${{ inputs.stage }}" == "BETA" ]]; then SAR_NAME="test-${SAR_NAME}" fi + ARCH_NAME=$(echo ${{ matrix.architecture }} | tr '_' '-') + SAR_NAME="${SAR_NAME}-python${{env.PYTHON_VERSION}}-${ARCH_NAME}" echo SAR_NAME="${SAR_NAME}" >> "$GITHUB_ENV" - - name: Adds arm64 suffix to SAR name - if: ${{ matrix.architecture == 'arm64' }} - run: echo SAR_NAME="${SAR_NAME}-arm64" >> "$GITHUB_ENV" - - name: Normalize semantic version - id: semantic-version # v2.0.0a0 -> v2.0.0-a0 - env: - VERSION: ${{ inputs.package-version }} - run: | - VERSION="${VERSION/a/-a}" - echo "VERSION=${VERSION}" >> "$GITHUB_OUTPUT" - name: Prepare SAR App - env: - VERSION: ${{ steps.semantic-version.outputs.VERSION }} run: | # From the generated LayerStack cdk.out artifact, find the layer asset path for the correct architecture. # We'll use this as the source directory of our SAR. This way we are re-using the same layer asset for our SAR. - asset=$(jq -jc '.Resources[] | select(.Properties.CompatibleArchitectures == ["${{ matrix.architecture }}"]) | .Metadata."aws:asset:path"' cdk.out/LayerV2Stack.template.json) + PYTHON_VERSION=$(echo ${{ matrix.python-version }} | tr -d '.') + asset_cdk=$(jq -jc '.Resources[] | select(.Properties.CompatibleArchitectures == ["${{ matrix.architecture }}"]) | .Metadata."aws:asset:path"' "cdk.out/LayerV3Stack-python${PYTHON_VERSION}.template.json") + + echo "Normalizing the asset variable" + asset=$(echo $asset_cdk | sed -E 's/^(asset\.[^.]+).*\1/\1/') + + VERSION=$(echo ${{ inputs.package-version }} | sed 's/^v//') + echo $asset + echo $VERSION # fill in the SAR SAM template sed \ -e "s||${VERSION}|g" \ -e "s//${{ env.SAR_NAME }}/g" \ -e "s||./cdk.out/$asset|g" \ - layer/sar/template.txt > template.yml + -e "s||${{ matrix.python-version }}|g" \ + -e "s||${{ matrix.architecture }}|g" \ + layer_v3/sar/template.txt > template.yml # SAR needs a README and a LICENSE, so just copy the ones from the repo cp README.md LICENSE "./cdk.out/$asset/" - - # Debug purposes - cat template.yml - name: Deploy SAR run: | + # Debug purposes + cat template.yml + # Package the SAR to our SAR S3 bucket, and publish it - sam package --template-file template.yml --output-template-file packaged.yml --s3-bucket ${{ secrets.AWS_SAR_S3_BUCKET }} + sam package --template-file template.yml --output-template-file packaged.yml --s3-bucket ${{ secrets.AWS_SAR_S3_BUCKET_V3 }} + cat packaged.yml sam publish --template packaged.yml --region "$AWS_REGION" - name: Deploy BETA canary if: ${{ inputs.stage == 'BETA' }} run: | - if [[ "${{ matrix.architecture }}" == "arm64" ]]; then - TEST_STACK_NAME="${TEST_STACK_NAME}-arm64" - fi + ARCH_NAME=$(echo ${{ matrix.architecture }} | tr -d '_') + TEST_STACK_NAME="${TEST_STACK_NAME}-python${{env.PYTHON_VERSION}}-${ARCH_NAME}" echo "Check if stack does not exist" stack_exists=$(aws cloudformation list-stacks --query "StackSummaries[?(StackName == '$TEST_STACK_NAME' && StackStatus == 'CREATE_COMPLETE')].{StackId:StackId, StackName:StackName, CreationTime:CreationTime, StackStatus:StackStatus}" --output text) @@ -180,7 +182,7 @@ jobs: echo "Creating canary stack" echo "Stack name: $TEST_STACK_NAME" aws serverlessrepo create-cloud-formation-change-set \ - --application-id arn:aws:serverlessrepo:${{ env.AWS_REGION }}:${{ steps.aws-credentials-sar-role.outputs.aws-account-id }}:applications/${{ env.SAR_NAME }} \ + --application-id arn:aws:serverlessrepo:${{ env.AWS_REGION }}:${{ secrets.AWS_SAR_V3_ACCOUNTID }}:applications/${{ env.SAR_NAME }} \ --stack-name "${TEST_STACK_NAME/serverlessrepo-/}" \ --capabilities CAPABILITY_NAMED_IAM @@ -205,5 +207,5 @@ jobs: sleep 15 echo "Make SAR app public" aws serverlessrepo put-application-policy \ - --application-id arn:aws:serverlessrepo:${{ env.AWS_REGION }}:${{ steps.aws-credentials-sar-role.outputs.aws-account-id }}:applications/${{ env.SAR_NAME }} \ + --application-id arn:aws:serverlessrepo:${{ env.AWS_REGION }}:${{ secrets.AWS_SAR_V3_ACCOUNTID }}:applications/${{ env.SAR_NAME }} \ --statements Principals='*',Actions=Deploy diff --git a/.github/workflows/reusable_export_pr_details.yml b/.github/workflows/reusable_export_pr_details.yml deleted file mode 100644 index 30eb9241b08..00000000000 --- a/.github/workflows/reusable_export_pr_details.yml +++ /dev/null @@ -1,108 +0,0 @@ -name: Export previously recorded PR - -# PROCESS -# -# 1. Fetch PR details previously saved from untrusted location -# 2. Parse details for safety -# 3. Export only what's needed for automation, e.g., PR number, title, body, author, action, whether is merged - -# USAGE -# -# see .github/workflows/on_merged_pr.yml and related for full example. -# -# NOTE: meant to be used with workflows that react to a given PR state (labeling, new, merged, etc.) -# done separately to isolate security practices and make it reusable. - - -on: - workflow_call: - inputs: - record_pr_workflow_id: - description: "Record PR workflow execution ID to download PR details" - required: true - type: number - workflow_origin: # see https://github.com/aws-powertools/powertools-lambda-python/issues/1349 - description: "Repository full name for runner integrity" - required: true - type: string - secrets: - token: - description: "GitHub Actions temporary and scoped token" - required: true - # Map the workflow outputs to job outputs - outputs: - prNumber: - description: "PR Number" - value: ${{ jobs.export_pr_details.outputs.prNumber }} - prTitle: - description: "PR Title" - value: ${{ jobs.export_pr_details.outputs.prTitle }} - prBody: - description: "PR Body as string" - value: ${{ jobs.export_pr_details.outputs.prBody }} - prAuthor: - description: "PR author username" - value: ${{ jobs.export_pr_details.outputs.prAuthor }} - prAction: - description: "PR event action" - value: ${{ jobs.export_pr_details.outputs.prAction }} - prIsMerged: - description: "Whether PR is merged" - value: ${{ jobs.export_pr_details.outputs.prIsMerged }} - -permissions: - contents: read - -jobs: - export_pr_details: - permissions: - actions: read # download PR artifact - # see https://github.com/aws-powertools/powertools-lambda-python/issues/1349 - if: inputs.workflow_origin == 'aws-powertools/powertools-lambda-python' - runs-on: ubuntu-latest - env: - FILENAME: pr.txt - # Map the job outputs to step outputs - outputs: - prNumber: ${{ steps.prNumber.outputs.prNumber }} - prTitle: ${{ steps.prTitle.outputs.prTitle }} - prBody: ${{ steps.prBody.outputs.prBody }} - prAuthor: ${{ steps.prAuthor.outputs.prAuthor }} - prAction: ${{ steps.prAction.outputs.prAction }} - prIsMerged: ${{ steps.prIsMerged.outputs.prIsMerged }} - steps: - - name: Checkout repository # in case caller workflow doesn't checkout thus failing with file not found - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: "Download previously saved PR" - uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 - env: - WORKFLOW_ID: ${{ inputs.record_pr_workflow_id }} - # For security, we only download artifacts tied to the successful PR recording workflow - with: - github-token: ${{ secrets.token }} - script: | - const script = require('.github/scripts/download_pr_artifact.js') - await script({github, context, core}) - # NodeJS standard library doesn't provide ZIP capabilities; use system `unzip` command instead - - name: "Unzip PR artifact" - run: unzip pr.zip - # NOTE: We need separate steps for each mapped output and respective IDs - # otherwise the parent caller won't see them regardless on how outputs are set. - - name: "Export Pull Request Number" - id: prNumber - run: echo prNumber="$(jq -c '.number' "${FILENAME}")" >> "$GITHUB_OUTPUT" - - name: "Export Pull Request Title" - id: prTitle - run: echo prTitle="$(jq -c '.pull_request.title' "${FILENAME}")" >> "$GITHUB_OUTPUT" - - name: "Export Pull Request Body" - id: prBody - run: echo prBody="$(jq -c '.pull_request.body' "${FILENAME}")" >> "$GITHUB_OUTPUT" - - name: "Export Pull Request Author" - id: prAuthor - run: echo prAuthor="$(jq -c '.pull_request.user.login' "${FILENAME}")" >> "$GITHUB_OUTPUT" - - name: "Export Pull Request Action" - id: prAction - run: echo prAction="$(jq -c '.action' "${FILENAME}")" >> "$GITHUB_OUTPUT" - - name: "Export Pull Request Merged status" - id: prIsMerged - run: echo prIsMerged="$(jq -c '.pull_request.merged' "${FILENAME}")" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/reusable_publish_changelog.yml b/.github/workflows/reusable_publish_changelog.yml index 1df1ceb5953..adccac305cc 100644 --- a/.github/workflows/reusable_publish_changelog.yml +++ b/.github/workflows/reusable_publish_changelog.yml @@ -15,6 +15,7 @@ permissions: jobs: publish_changelog: + if: github.repository == 'aws-powertools/powertools-lambda-python' # Force Github action to run only a single job at a time (based on the group name) # This is to prevent race-condition and inconsistencies with changelog push concurrency: @@ -25,7 +26,7 @@ jobs: pull-requests: write # create PR steps: - name: Checkout repository # reusable workflows start clean, so we need to checkout again - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: "Generate latest changelog" diff --git a/.github/workflows/reusable_publish_docs.yml b/.github/workflows/reusable_publish_docs.yml index fa89ecff8a5..dd054e98639 100644 --- a/.github/workflows/reusable_publish_docs.yml +++ b/.github/workflows/reusable_publish_docs.yml @@ -32,6 +32,7 @@ permissions: jobs: publish_docs: + if: github.repository == 'aws-powertools/powertools-lambda-python' # Force Github action to run only a single job at a time (based on the group name) # This is to prevent "race-condition" in publishing a new version of doc to `gh-pages` concurrency: @@ -39,23 +40,20 @@ jobs: runs-on: ubuntu-latest environment: "Docs" permissions: - contents: write # push to gh-pages id-token: write # trade JWT token for AWS credentials in AWS Docs account - pages: write # uncomment if mike fails as we migrated to S3 hosting steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 ref: ${{ inputs.git_ref }} - - name: Install poetry - run: pipx install poetry - name: Set up Python - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: "3.10" - cache: "poetry" - - name: Install dependencies - run: make dev + python-version: "3.12" + - name: Install doc generation dependencies + run: | + cat docs/requirements.txt + pip install --require-hashes -r docs/requirements.txt - name: Git client setup run: | git config --global user.name Docs deploy @@ -69,22 +67,19 @@ jobs: git pull origin "$BRANCH" env: BRANCH: ${{ inputs.git_ref }} - - name: Build docs website and API reference - env: - VERSION: ${{ inputs.version }} - ALIAS: ${{ inputs.alias }} - run: | - make release-docs VERSION="$VERSION" ALIAS="$ALIAS" - poetry run mike set-default --push latest - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 with: aws-region: us-east-1 role-to-assume: ${{ secrets.AWS_DOCS_ROLE_ARN }} - - name: Copy API Docs + mask-aws-account-id: true + - name: Build docs + env: + VERSION: ${{ inputs.version }} + ALIAS: ${{ inputs.alias }} run: | - cp -r api site/ + rm -rf site + mkdocs build - name: Deploy Docs (Version) env: VERSION: ${{ inputs.version }} diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml index 5e694731db7..d05239bf089 100644 --- a/.github/workflows/run-e2e-tests.yml +++ b/.github/workflows/run-e2e-tests.yml @@ -19,6 +19,7 @@ on: push: branches: - develop + - v3 paths: - "aws_lambda_powertools/**" - "tests/e2e/**" @@ -47,33 +48,34 @@ jobs: strategy: fail-fast: false # needed so if a version fails, the others will still be able to complete and cleanup matrix: - version: ["3.7", "3.8", "3.9", "3.10"] - if: ${{ github.actor != 'dependabot[bot]' }} + version: ["3.10", "3.11", "3.12","3.13","3.14"] + if: ${{ github.actor != 'dependabot[bot]' && github.repository == 'aws-powertools/powertools-lambda-python' }} steps: - name: "Checkout" - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install poetry run: pipx install poetry - name: "Use Python" - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.version }} architecture: "x64" cache: "poetry" - name: Setup Node.js - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: "16.12" + node-version: "20.10.0" - name: Install CDK CLI run: | npm ci npx cdk --version - name: Install dependencies - run: make dev + run: make dev-quality-code - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.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 }} + mask-aws-account-id: true - name: Test run: make e2e-test diff --git a/.github/workflows/secure_workflows.yml b/.github/workflows/secure_workflows.yml index 418c4c446a5..e2f187a40f2 100644 --- a/.github/workflows/secure_workflows.yml +++ b/.github/workflows/secure_workflows.yml @@ -30,8 +30,10 @@ jobs: contents: read # checkout code and subsequently GitHub action workflows steps: - name: Checkout code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Ensure 3rd party workflows have SHA pinned - uses: zgosalvez/github-actions-ensure-sha-pinned-actions@f32435541e24cd6a4700a7f52bb2ec59e80603b1 # v2.1.4 + uses: zgosalvez/github-actions-ensure-sha-pinned-actions@ca46236c6ce584ae24bc6283ba8dcf4b3ec8a066 # v5.0.4 with: - allowlist: slsa-framework/slsa-github-generator + allowlist: | + slsa-framework/slsa-github-generator + aws-powertools/actions diff --git a/.github/workflows/update_ssm.yml b/.github/workflows/update_ssm.yml new file mode 100644 index 00000000000..f290d9e560b --- /dev/null +++ b/.github/workflows/update_ssm.yml @@ -0,0 +1,131 @@ +name: SSM Parameters + +# SSM Parameters update +# +# PROCESS +# Creates parameters in regional AWS accounts for each layer we create, using the inputs to target specific releases +# * environment: will prefix /beta/ into the parameter +# * write_latest: will create a latest alias instead of a version number in the parameter +# * package_version: semantic version number of the released layer (3.x.y) +# * layer_version: this is sequential layer version from the ARN +# +# A successful parameter would look similar to: +# /aws/service/powertools/python/arm64/python3.14/3.1.0 +# And will have a value of: +# arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:4 + +on: + workflow_dispatch: + inputs: + environment: + description: Environment to deploy to + type: choice + options: + - Beta + - Prod + required: true + + write_latest: + description: Write to the latest path + type: boolean + required: false + + package_version: + description: Semantic Version of published layer + type: string + required: true + + layer_version: + description: Layer version + type: string + required: true + + workflow_call: + inputs: + environment: + description: Environment to deploy to, one of `Prod` or `Beta` + type: string + required: true + + write_latest: + description: Write to the latest path + type: boolean + required: false + default: true + + package_version: + description: Semantic Version of published layer + type: string + required: true + + layer_version: + description: Layer version + type: string + required: true + +run-name: SSM Parameters - Python - Layer version ${{ inputs.layer_version }} - v${{ inputs.package_version }} + +permissions: + contents: read + +jobs: + python: + runs-on: ubuntu-latest + environment: SSM + strategy: + matrix: + region: ["af-south-1", "ap-east-1", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", + "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", "mx-central-1", "sa-east-1", "us-east-1", + "us-east-2", "us-west-1", "us-west-2"] + + permissions: + contents: read + id-token: write + steps: + - id: transform + run: | + echo 'CONVERTED_REGION=${{ matrix.region }}' | tr 'a-z\-' 'A-Z_' >> "$GITHUB_OUTPUT" + - id: creds + 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)] }} + mask-aws-account-id: true + - id: write-version + env: + prefix: ${{ inputs.environment == 'beta' && '/aws/service/powertools/beta' || '/aws/service/powertools' }} + PACKAGE_VERSION: ${{ inputs.package_version }} + LAYER_VERSION: ${{ inputs.layer_version }} + run: | + aws ssm put-parameter --name ${{ env.prefix }}/python/arm64/python3.10/$PACKAGE_VERSION --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:$LAYER_VERSION" --type String --overwrite + aws ssm put-parameter --name ${{ env.prefix }}/python/arm64/python3.11/$PACKAGE_VERSION --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:$LAYER_VERSION" --type String --overwrite + aws ssm put-parameter --name ${{ env.prefix }}/python/arm64/python3.12/$PACKAGE_VERSION --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:$LAYER_VERSION" --type String --overwrite + aws ssm put-parameter --name ${{ env.prefix }}/python/arm64/python3.13/$PACKAGE_VERSION --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:$LAYER_VERSION" --type String --overwrite + aws ssm put-parameter --name ${{ env.prefix }}/python/arm64/python3.14/$PACKAGE_VERSION --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:$LAYER_VERSION" --type String --overwrite + + aws ssm put-parameter --name ${{ env.prefix }}/python/x86_64/python3.10/$PACKAGE_VERSION --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:$LAYER_VERSION" --type String --overwrite + aws ssm put-parameter --name ${{ env.prefix }}/python/x86_64/python3.11/$PACKAGE_VERSION --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:$LAYER_VERSION" --type String --overwrite + aws ssm put-parameter --name ${{ env.prefix }}/python/x86_64/python3.12/$PACKAGE_VERSION --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:$LAYER_VERSION" --type String --overwrite + aws ssm put-parameter --name ${{ env.prefix }}/python/x86_64/python3.13/$PACKAGE_VERSION --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:$LAYER_VERSION" --type String --overwrite + aws ssm put-parameter --name ${{ env.prefix }}/python/x86_64/python3.14/$PACKAGE_VERSION --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:$LAYER_VERSION" --type String --overwrite + + - id: write-latest + if: inputs.write_latest == true + env: + prefix: ${{ inputs.environment == 'beta' && '/aws/service/powertools/beta' || '/aws/service/powertools' }} + LAYER_VERSION: ${{ inputs.layer_version }} + run: | + aws ssm put-parameter --name ${{ env.prefix }}/python/arm64/python3.10/latest --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-arm64:$LAYER_VERSION" --type String --overwrite + aws ssm put-parameter --name ${{ env.prefix }}/python/arm64/python3.11/latest --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:$LAYER_VERSION" --type String --overwrite + aws ssm put-parameter --name ${{ env.prefix }}/python/arm64/python3.12/latest --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-arm64:$LAYER_VERSION" --type String --overwrite + aws ssm put-parameter --name ${{ env.prefix }}/python/arm64/python3.13/latest --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:$LAYER_VERSION" --type String --overwrite + aws ssm put-parameter --name ${{ env.prefix }}/python/arm64/python3.14/latest --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-arm64:$LAYER_VERSION" --type String --overwrite + + aws ssm put-parameter --name ${{ env.prefix }}/python/x86_64/python3.10/latest --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python310-x86_64:$LAYER_VERSION" --type String --overwrite + aws ssm put-parameter --name ${{ env.prefix }}/python/x86_64/python3.11/latest --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:$LAYER_VERSION" --type String --overwrite + aws ssm put-parameter --name ${{ env.prefix }}/python/x86_64/python3.12/latest --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python312-x86_64:$LAYER_VERSION" --type String --overwrite + aws ssm put-parameter --name ${{ env.prefix }}/python/x86_64/python3.13/latest --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:$LAYER_VERSION" --type String --overwrite + aws ssm put-parameter --name ${{ env.prefix }}/python/x86_64/python3.14/latest --value "arn:aws:lambda:${{ matrix.region }}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python314-x86_64:$LAYER_VERSION" --type String --overwrite diff --git a/.gitignore b/.gitignore index a69b4eaf618..2c93b2d1fbe 100644 --- a/.gitignore +++ b/.gitignore @@ -252,6 +252,7 @@ dmypy.json .pyre/ ### VisualStudioCode ### +.vscode .vscode/* !.vscode/tasks.json !.vscode/launch.json @@ -314,3 +315,12 @@ examples/**/sam/.aws-sam cdk.out # NOTE: different accounts will be used for E2E thus creating unnecessary git clutter cdk.context.json + +# vim +*.swp + +# LLMs +.claude +.amazonq +.kiro +.github/instructions diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 00000000000..0622b57c118 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,15 @@ +# Title for the gitleaks configuration file. +title = "Gitleaks" + +[extend] +# useDefault will extend the base configuration with the default gitleaks config: +# https://github.com/zricethezav/gitleaks/blob/master/config/gitleaks.toml +useDefault = true + +[allowlist] +description = "Allow list false positive" + +# Allow list paths to ignore due to false positives. +paths = [ + '''tests/unit/parser/test_kinesis_firehose\.py''', +] diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 00000000000..d501e5cc212 --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1,3 @@ +examples/batch_processing/src/context_manager_access_output_pydantic.txt:aws-access-token:10 +examples/batch_processing/src/context_manager_access_output_pydantic.txt:aws-access-token:15 +examples/batch_processing/src/context_manager_access_output.txt:aws-access-token:10 diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile deleted file mode 100644 index efa414f9ac7..00000000000 --- a/.gitpod.Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -# See here all gitpod images available: https://hub.docker.com/r/gitpod/workspace-python-3.9/tags -# Current python version: 3.9.13 -FROM gitpod/workspace-python-3.9@sha256:de87d4ebffe8daab2e8fef96ec20497ae4f39e8dcb9dec1483d0be61ea78e8cd - -WORKDIR /app -ADD . /app - -# Installing pre-commit as system package and not user package. Git needs this to execute pre-commit hooks. -RUN export PIP_USER=no -# v3.3.3 -RUN python3 -m pip install --require-hashes -r .gitpod_requirements.txt \ No newline at end of file diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index d831f067bdd..00000000000 --- a/.gitpod.yml +++ /dev/null @@ -1,19 +0,0 @@ -image: - file: .gitpod.Dockerfile -tasks: - - init: make dev-gitpod -vscode: - extensions: - - ms-python.python # IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more. - - littlefoxteam.vscode-python-test-adapter # Run your Python tests in the Sidebar of Visual Studio Code - - ms-azuretools.vscode-docker # Makes it easy to create, manage, and debug containerized applications. - - davidanson.vscode-markdownlint # Markdown linting and style checking for Visual Studio Code - - bungcip.better-toml # Better TOML Language support - - oderwat.indent-rainbow # Makes indentation easier to read - - yzhang.markdown-all-in-one # Autoformat, better visualization, snippets, and markdown export to multiple fmts - - bierner.markdown-mermaid # Previews mermaid diagrams when previewing markdown - - matangover.mypy # Highlight mypy issues - - njpwerner.autodocstring # Auto-generate docsstrings in numpy format that we use - - netcorext.uuid-generator # For those helping create code snippets for docs - - streetsidesoftware.code-spell-checker # Spell checker that works with camel case too - - bungcip.better-toml # In case GitPod doesn't have support for TOML pyproject.toml diff --git a/.gitpod_requirements.in b/.gitpod_requirements.in deleted file mode 100644 index e88cdf05e74..00000000000 --- a/.gitpod_requirements.in +++ /dev/null @@ -1 +0,0 @@ -pre-commit==3.3.3 \ No newline at end of file diff --git a/.gitpod_requirements.txt b/.gitpod_requirements.txt deleted file mode 100644 index db6274738d3..00000000000 --- a/.gitpod_requirements.txt +++ /dev/null @@ -1,85 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.9 -# by the following command: -# -# pip-compile --generate-hashes --output-file=.gitpod_requirements.txt .gitpod_requirements.in -# -cfgv==3.3.1 \ - --hash=sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426 \ - --hash=sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736 - # via pre-commit -distlib==0.3.6 \ - --hash=sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46 \ - --hash=sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e - # via virtualenv -filelock==3.12.2 \ - --hash=sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81 \ - --hash=sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec - # via virtualenv -identify==2.5.24 \ - --hash=sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4 \ - --hash=sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d - # via pre-commit -nodeenv==1.8.0 \ - --hash=sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2 \ - --hash=sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec - # via pre-commit -platformdirs==3.8.0 \ - --hash=sha256:b0cabcb11063d21a0b261d557acb0a9d2126350e63b70cdf7db6347baea456dc \ - --hash=sha256:ca9ed98ce73076ba72e092b23d3c93ea6c4e186b3f1c3dad6edd98ff6ffcca2e - # via virtualenv -pre-commit==3.3.3 \ - --hash=sha256:10badb65d6a38caff29703362271d7dca483d01da88f9d7e05d0b97171c136cb \ - --hash=sha256:a2256f489cd913d575c145132ae196fe335da32d91a8294b7afe6622335dd023 - # via -r .gitpod_requirements.in -pyyaml==6.0 \ - --hash=sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf \ - --hash=sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293 \ - --hash=sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b \ - --hash=sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57 \ - --hash=sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b \ - --hash=sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4 \ - --hash=sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07 \ - --hash=sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba \ - --hash=sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9 \ - --hash=sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287 \ - --hash=sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513 \ - --hash=sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0 \ - --hash=sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782 \ - --hash=sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0 \ - --hash=sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92 \ - --hash=sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f \ - --hash=sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 \ - --hash=sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc \ - --hash=sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1 \ - --hash=sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c \ - --hash=sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86 \ - --hash=sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4 \ - --hash=sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c \ - --hash=sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34 \ - --hash=sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b \ - --hash=sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d \ - --hash=sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c \ - --hash=sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb \ - --hash=sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7 \ - --hash=sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737 \ - --hash=sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3 \ - --hash=sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d \ - --hash=sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358 \ - --hash=sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53 \ - --hash=sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78 \ - --hash=sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803 \ - --hash=sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a \ - --hash=sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f \ - --hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \ - --hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5 - # via pre-commit -virtualenv==20.23.1 \ - --hash=sha256:34da10f14fea9be20e0fd7f04aba9732f84e593dac291b757ce42e3368a39419 \ - --hash=sha256:8ff19a38c1021c742148edc4f81cb43d7f8c6816d2ede2ab72af5b84c749ade1 - # via pre-commit - -# WARNING: The following packages were not pinned, but pip requires them to be -# pinned when the requirements file includes hashes and the requirement is not -# satisfied by a package already installed. Consider using the --allow-unsafe flag. -# setuptools diff --git a/.markdownlint.yaml b/.markdownlint.yaml index 4d571206e07..4529480ad19 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -73,8 +73,6 @@ MD013: tables: false # Include headings headings: true - # Include headings - headers: true # Strict length checking strict: false # Stern length checking @@ -107,8 +105,6 @@ MD023: true # MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content MD024: - # Only check sibling headings - allow_different_nesting: false # Only check sibling headings siblings_only: false diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 00000000000..11b6d7ffe29 --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1,2 @@ +docs/core/metrics/index.md +includes/abbreviations.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 319afbad0b4..f0ea1cbf495 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,14 +12,14 @@ repos: - id: check-toml - repo: local hooks: - - id: black - name: formatting::black - entry: poetry run black + - id: ruff + name: formatting::ruff + entry: poetry run ruff format language: system types: [python] - id: ruff - name: linting-format::ruff - entry: poetry run ruff + name: linting::ruff + entry: poetry run ruff check language: system types: [python] - repo: https://github.com/igorshubovych/markdownlint-cli @@ -34,6 +34,7 @@ repos: entry: poetry run cfn-lint language: system types: [yaml] + exclude: examples/build_recipes/* files: examples/.* - repo: https://github.com/rhysd/actionlint rev: "fd7ba3c382e13dcc0248e425b4cbc3f1185fa3ee" # v1.6.24 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d54c049272..f2dc360bea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,4208 @@ # 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)) + + + +## [v3.25.0] - 2026-03-12 +## Bug Fixes + +* **ci:** remove DUB region ([#8031](https://github.com/aws-powertools/powertools-lambda-python/issues/8031)) +* **event-handler:** prevent OpenAPI schema bleed when reusing response dictionaries ([#7952](https://github.com/aws-powertools/powertools-lambda-python/issues/7952)) +* **event_handler:** add middleware validation per route ([#8020](https://github.com/aws-powertools/powertools-lambda-python/issues/8020)) +* **event_handler:** fix bug regression in Annotated field ([#7904](https://github.com/aws-powertools/powertools-lambda-python/issues/7904)) +* **event_handler:** return 415 status_code for unsupported content-type headers ([#7980](https://github.com/aws-powertools/powertools-lambda-python/issues/7980)) +* **event_handler:** sync alias and validation_alias for Pydantic 2.12+ compatibility ([#7901](https://github.com/aws-powertools/powertools-lambda-python/issues/7901)) +* **event_handler:** preserve openapi_examples on Body ([#7862](https://github.com/aws-powertools/powertools-lambda-python/issues/7862)) +* **logger:** preserve percent-style formatting args in flush_buffer ([#8009](https://github.com/aws-powertools/powertools-lambda-python/issues/8009)) +* **parameters:** fix variable shadowing in SSM parameter chunking ([#8006](https://github.com/aws-powertools/powertools-lambda-python/issues/8006)) +* **typing:** resolve ty diagnostics in logging and metrics modules ([#7953](https://github.com/aws-powertools/powertools-lambda-python/issues/7953)) +* **typing:** accept Mapping type in resolve() for event parameter ([#7909](https://github.com/aws-powertools/powertools-lambda-python/issues/7909)) + +## Code Refactoring + +* **batch:** improve type annotation for event parameter ([#7924](https://github.com/aws-powertools/powertools-lambda-python/issues/7924)) + +## Documentation + +* clarify append_context_keys behavior with overlapping keys ([#7846](https://github.com/aws-powertools/powertools-lambda-python/issues/7846)) + +## Features + +* Add a flag to ALBResolver to URL-decode query parameters ([#7940](https://github.com/aws-powertools/powertools-lambda-python/issues/7940)) +* add HttpResolverAlpha resolver ([#7913](https://github.com/aws-powertools/powertools-lambda-python/issues/7913)) +* **decorators:** Support Durable Context in logger and metric decorators ([#7765](https://github.com/aws-powertools/powertools-lambda-python/issues/7765)) +* **event-handler:** add per-route validation support ([#7965](https://github.com/aws-powertools/powertools-lambda-python/issues/7965)) +* **event_source:** add support for S3 IntelligentTiering events ([#7954](https://github.com/aws-powertools/powertools-lambda-python/issues/7954)) +* **metrics:** add support for multiple dimension sets ([#7848](https://github.com/aws-powertools/powertools-lambda-python/issues/7848)) +* **openapi:** add support for micro Lambda pattern ([#7920](https://github.com/aws-powertools/powertools-lambda-python/issues/7920)) + +## Maintenance + +* remove unused PR automation workflows ([#8008](https://github.com/aws-powertools/powertools-lambda-python/issues/8008)) +* adding fuzzing tests ([#7903](https://github.com/aws-powertools/powertools-lambda-python/issues/7903)) +* update swagger ui files ([#7914](https://github.com/aws-powertools/powertools-lambda-python/issues/7914)) +* version bump +* **ci:** new pre-release 3.24.1a1 ([#7926](https://github.com/aws-powertools/powertools-lambda-python/issues/7926)) +* **deps:** bump jmespath from 1.0.1 to 1.1.0 ([#7970](https://github.com/aws-powertools/powertools-lambda-python/issues/7970)) +* **deps:** bump urllib3 from 2.6.0 to 2.6.3 in /docs ([#7921](https://github.com/aws-powertools/powertools-lambda-python/issues/7921)) +* **deps:** bump squidfunk/mkdocs-material from `3bba0a9` to `8f41b60` in /docs ([#8010](https://github.com/aws-powertools/powertools-lambda-python/issues/8010)) +* **deps:** bump the github-actions group with 4 updates ([#8013](https://github.com/aws-powertools/powertools-lambda-python/issues/8013)) +* **deps:** bump protobuf from 6.33.2 to 6.33.4 ([#7948](https://github.com/aws-powertools/powertools-lambda-python/issues/7948)) +* **deps:** bump valkey-glide from 2.2.3 to 2.2.5 ([#7947](https://github.com/aws-powertools/powertools-lambda-python/issues/7947)) +* **deps:** bump actions/dependency-review-action from 4.8.2 to 4.8.3 in the github-actions group ([#8004](https://github.com/aws-powertools/powertools-lambda-python/issues/8004)) +* **deps:** bump cryptography from 46.0.3 to 46.0.5 ([#7991](https://github.com/aws-powertools/powertools-lambda-python/issues/7991)) +* **deps:** bump the github-actions group with 3 updates ([#7960](https://github.com/aws-powertools/powertools-lambda-python/issues/7960)) +* **deps:** bump protobuf from 6.33.4 to 6.33.5 ([#7977](https://github.com/aws-powertools/powertools-lambda-python/issues/7977)) +* **deps:** bump datadog-lambda from 8.120.0 to 8.121.0 ([#8015](https://github.com/aws-powertools/powertools-lambda-python/issues/8015)) +* **deps:** bump the github-actions group with 3 updates ([#7971](https://github.com/aws-powertools/powertools-lambda-python/issues/7971)) +* **deps:** bump the github-actions group with 2 updates ([#7985](https://github.com/aws-powertools/powertools-lambda-python/issues/7985)) +* **deps-dev:** bump the dev-dependencies group with 2 updates ([#7969](https://github.com/aws-powertools/powertools-lambda-python/issues/7969)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.312 to 0.1.314 ([#7973](https://github.com/aws-powertools/powertools-lambda-python/issues/7973)) +* **deps-dev:** bump cfn-lint from 1.43.3 to 1.43.4 ([#7972](https://github.com/aws-powertools/powertools-lambda-python/issues/7972)) +* **deps-dev:** bump aws-cdk-lib from 2.233.0 to 2.236.0 ([#7974](https://github.com/aws-powertools/powertools-lambda-python/issues/7974)) +* **deps-dev:** bump aws-cdk from 2.1103.0 to 2.1105.0 in the aws-cdk group ([#7982](https://github.com/aws-powertools/powertools-lambda-python/issues/7982)) +* **deps-dev:** bump aws-cdk from 2.1101.0 to 2.1103.0 in the aws-cdk group ([#7967](https://github.com/aws-powertools/powertools-lambda-python/issues/7967)) +* **deps-dev:** bump sentry-sdk from 2.48.0 to 2.52.0 ([#7987](https://github.com/aws-powertools/powertools-lambda-python/issues/7987)) +* **deps-dev:** bump testcontainers from 4.14.0 to 4.14.1 ([#7988](https://github.com/aws-powertools/powertools-lambda-python/issues/7988)) +* **deps-dev:** bump cfn-lint from 1.43.2 to 1.43.3 ([#7958](https://github.com/aws-powertools/powertools-lambda-python/issues/7958)) +* **deps-dev:** bump testcontainers from 4.13.3 to 4.14.0 ([#7959](https://github.com/aws-powertools/powertools-lambda-python/issues/7959)) +* **deps-dev:** bump multiprocess from 0.70.18 to 0.70.19 ([#7961](https://github.com/aws-powertools/powertools-lambda-python/issues/7961)) +* **deps-dev:** bump aws-cdk from 2.1100.3 to 2.1101.0 in the aws-cdk group ([#7955](https://github.com/aws-powertools/powertools-lambda-python/issues/7955)) +* **deps-dev:** bump ruff from 0.14.11 to 0.14.13 in the dev-dependencies group ([#7957](https://github.com/aws-powertools/powertools-lambda-python/issues/7957)) +* **deps-dev:** bump bandit from 1.9.2 to 1.9.3 ([#7962](https://github.com/aws-powertools/powertools-lambda-python/issues/7962)) +* **deps-dev:** bump aws-cdk-lib from 2.237.1 to 2.238.0 ([#7986](https://github.com/aws-powertools/powertools-lambda-python/issues/7986)) +* **deps-dev:** bump virtualenv from 20.35.4 to 20.36.1 ([#7950](https://github.com/aws-powertools/powertools-lambda-python/issues/7950)) +* **deps-dev:** bump aws-cdk from 2.1100.2 to 2.1100.3 in the aws-cdk group ([#7942](https://github.com/aws-powertools/powertools-lambda-python/issues/7942)) +* **deps-dev:** bump boto3-stubs from 1.42.21 to 1.42.26 ([#7945](https://github.com/aws-powertools/powertools-lambda-python/issues/7945)) +* **deps-dev:** bump aws-cdk from 2.1105.0 to 2.1106.0 in the aws-cdk group ([#7995](https://github.com/aws-powertools/powertools-lambda-python/issues/7995)) +* **deps-dev:** bump ruff from 0.14.10 to 0.14.11 in the dev-dependencies group ([#7944](https://github.com/aws-powertools/powertools-lambda-python/issues/7944)) +* **deps-dev:** bump filelock from 3.20.2 to 3.20.3 ([#7946](https://github.com/aws-powertools/powertools-lambda-python/issues/7946)) +* **deps-dev:** bump filelock from 3.20.3 to 3.24.2 ([#7999](https://github.com/aws-powertools/powertools-lambda-python/issues/7999)) +* **deps-dev:** bump sentry-sdk from 2.52.0 to 2.53.0 ([#7998](https://github.com/aws-powertools/powertools-lambda-python/issues/7998)) +* **deps-dev:** bump urllib3 from 2.6.2 to 2.6.3 in /layer_v3 ([#7928](https://github.com/aws-powertools/powertools-lambda-python/issues/7928)) +* **deps-dev:** bump types-python-dateutil from 2.9.0.20251115 to 2.9.0.20260124 ([#7989](https://github.com/aws-powertools/powertools-lambda-python/issues/7989)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.233.0a0 to 2.238.0a0 ([#7997](https://github.com/aws-powertools/powertools-lambda-python/issues/7997)) +* **deps-dev:** bump cfn-lint from 1.44.0 to 1.46.0 ([#8018](https://github.com/aws-powertools/powertools-lambda-python/issues/8018)) +* **deps-dev:** bump aws-cdk from 2.1106.0 to 2.1108.0 in the aws-cdk group ([#8011](https://github.com/aws-powertools/powertools-lambda-python/issues/8011)) +* **deps-dev:** bump cfn-lint from 1.43.1 to 1.43.2 ([#7907](https://github.com/aws-powertools/powertools-lambda-python/issues/7907)) +* **deps-dev:** bump aws-cdk from 2.1100.1 to 2.1100.2 in the aws-cdk group ([#7905](https://github.com/aws-powertools/powertools-lambda-python/issues/7905)) +* **deps-dev:** bump filelock from 3.24.2 to 3.25.0 ([#8016](https://github.com/aws-powertools/powertools-lambda-python/issues/8016)) +* **deps-dev:** bump urllib3 from 2.6.2 to 2.6.3 ([#7922](https://github.com/aws-powertools/powertools-lambda-python/issues/7922)) +* **typing:** add ty type checker to CI with baseline exclusions ([#7938](https://github.com/aws-powertools/powertools-lambda-python/issues/7938)) + + + +## [v3.24.0] - 2026-01-05 +## Bug Fixes + +* **batch_processor:** fix batch processor ([#7798](https://github.com/aws-powertools/powertools-lambda-python/issues/7798)) +* **ci:** add missing dollar signs in SSM parameter path variables ([#7695](https://github.com/aws-powertools/powertools-lambda-python/issues/7695)) +* **data-classes:** ensure lazy initialization for Cognito token generation response properties ([#7653](https://github.com/aws-powertools/powertools-lambda-python/issues/7653)) + +## Code Refactoring + +* **event-handler:** remove kwargs from AppSync exception constructor ([#7699](https://github.com/aws-powertools/powertools-lambda-python/issues/7699)) + +## Documentation + +* add EF Education First as customer reference ([#7809](https://github.com/aws-powertools/powertools-lambda-python/issues/7809)) +* clarify BedrockResponse.is_json() always returns True ([#7748](https://github.com/aws-powertools/powertools-lambda-python/issues/7748)) +* **event-handler:** add docstring for serializer in `BedrockAgentFunctionResolver` ([#7808](https://github.com/aws-powertools/powertools-lambda-python/issues/7808)) +* **homepage:** reorganize homepage and create dedicated installation page ([#7896](https://github.com/aws-powertools/powertools-lambda-python/issues/7896)) +* **readme:** update features list and improve readability ([#7889](https://github.com/aws-powertools/powertools-lambda-python/issues/7889)) + +## Features + +* **idempotency:** Allow durable functions to replay ([#7764](https://github.com/aws-powertools/powertools-lambda-python/issues/7764)) +* **parser:** add model for the DynamoDB Stream Lambda invocation record ([#7818](https://github.com/aws-powertools/powertools-lambda-python/issues/7818)) + +## Maintenance + +* version bump +* drop Python3.9 support - WIP ([#7807](https://github.com/aws-powertools/powertools-lambda-python/issues/7807)) +* **ci:** new pre-release 3.23.1a5 ([#7725](https://github.com/aws-powertools/powertools-lambda-python/issues/7725)) +* **ci:** improve dependabot workflow to reduce noise ([#7890](https://github.com/aws-powertools/powertools-lambda-python/issues/7890)) +* **ci:** remove automated pre-release schedule ([#7894](https://github.com/aws-powertools/powertools-lambda-python/issues/7894)) +* **ci:** new pre-release 3.23.1a0 ([#7685](https://github.com/aws-powertools/powertools-lambda-python/issues/7685)) +* **ci:** new pre-release 3.23.1a1 ([#7697](https://github.com/aws-powertools/powertools-lambda-python/issues/7697)) +* **ci:** new pre-release 3.23.1a2 ([#7706](https://github.com/aws-powertools/powertools-lambda-python/issues/7706)) +* **ci:** new pre-release 3.23.1a8 ([#7753](https://github.com/aws-powertools/powertools-lambda-python/issues/7753)) +* **ci:** new pre-release 3.23.1a7 ([#7740](https://github.com/aws-powertools/powertools-lambda-python/issues/7740)) +* **ci:** new pre-release 3.23.1a3 ([#7710](https://github.com/aws-powertools/powertools-lambda-python/issues/7710)) +* **ci:** new pre-release 3.23.1a6 ([#7733](https://github.com/aws-powertools/powertools-lambda-python/issues/7733)) +* **ci:** remove daily changelog rebuild schedule ([#7897](https://github.com/aws-powertools/powertools-lambda-python/issues/7897)) +* **ci:** new pre-release 3.23.1a10 ([#7833](https://github.com/aws-powertools/powertools-lambda-python/issues/7833)) +* **ci:** new pre-release 3.23.1a4 ([#7715](https://github.com/aws-powertools/powertools-lambda-python/issues/7715)) +* **ci:** update layer version for all AWS partitions in docs ([#7757](https://github.com/aws-powertools/powertools-lambda-python/issues/7757)) +* **ci:** new pre-release 3.23.1a9 ([#7828](https://github.com/aws-powertools/powertools-lambda-python/issues/7828)) +* **deps:** bump protobuf from 6.33.0 to 6.33.1 ([#7681](https://github.com/aws-powertools/powertools-lambda-python/issues/7681)) +* **deps:** bump actions/setup-go from 6.0.0 to 6.1.0 ([#7718](https://github.com/aws-powertools/powertools-lambda-python/issues/7718)) +* **deps:** bump mkdocs-llmstxt from 0.4.0 to 0.5.0 in /docs ([#7720](https://github.com/aws-powertools/powertools-lambda-python/issues/7720)) +* **deps:** bump pymdown-extensions from 10.16 to 10.16.1 in /docs ([#7827](https://github.com/aws-powertools/powertools-lambda-python/issues/7827)) +* **deps:** bump squidfunk/mkdocs-material from `980e11f` to `3bba0a9` in /docs ([#7835](https://github.com/aws-powertools/powertools-lambda-python/issues/7835)) +* **deps:** bump mkdocs-material from 9.7.0 to 9.7.1 in /docs ([#7836](https://github.com/aws-powertools/powertools-lambda-python/issues/7836)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 4.0.0 to 4.0.1 ([#7810](https://github.com/aws-powertools/powertools-lambda-python/issues/7810)) +* **deps:** bump actions/checkout from 5.0.1 to 6.0.0 ([#7719](https://github.com/aws-powertools/powertools-lambda-python/issues/7719)) +* **deps:** bump valkey-glide from 2.2.2 to 2.2.3 ([#7837](https://github.com/aws-powertools/powertools-lambda-python/issues/7837)) +* **deps:** bump datadog-lambda from 8.118.0 to 8.120.0 ([#7815](https://github.com/aws-powertools/powertools-lambda-python/issues/7815)) +* **deps:** bump mkdocs-material from 9.7.0 to 9.7.1 ([#7838](https://github.com/aws-powertools/powertools-lambda-python/issues/7838)) +* **deps:** bump datadog-lambda from 8.116.0 to 8.118.0 ([#7785](https://github.com/aws-powertools/powertools-lambda-python/issues/7785)) +* **deps:** bump actions/checkout from 5.0.0 to 5.0.1 ([#7701](https://github.com/aws-powertools/powertools-lambda-python/issues/7701)) +* **deps:** bump docker/setup-buildx-action from 3.11.1 to 3.12.0 ([#7843](https://github.com/aws-powertools/powertools-lambda-python/issues/7843)) +* **deps:** bump actions/download-artifact from 6.0.0 to 7.0.0 ([#7801](https://github.com/aws-powertools/powertools-lambda-python/issues/7801)) +* **deps:** bump actions/checkout from 6.0.0 to 6.0.1 ([#7766](https://github.com/aws-powertools/powertools-lambda-python/issues/7766)) +* **deps:** bump actions/upload-artifact from 5.0.0 to 6.0.0 ([#7800](https://github.com/aws-powertools/powertools-lambda-python/issues/7800)) +* **deps:** bump valkey-glide from 2.1.1 to 2.2.1 ([#7783](https://github.com/aws-powertools/powertools-lambda-python/issues/7783)) +* **deps:** bump redis from 6.4.0 to 7.0.1 ([#7687](https://github.com/aws-powertools/powertools-lambda-python/issues/7687)) +* **deps:** bump valkey-glide from 2.2.1 to 2.2.2 ([#7802](https://github.com/aws-powertools/powertools-lambda-python/issues/7802)) +* **deps:** bump the github-actions group with 2 updates ([#7893](https://github.com/aws-powertools/powertools-lambda-python/issues/7893)) +* **deps:** bump actions/setup-node from 6.0.0 to 6.1.0 ([#7773](https://github.com/aws-powertools/powertools-lambda-python/issues/7773)) +* **deps:** bump protobuf from 6.33.1 to 6.33.2 ([#7786](https://github.com/aws-powertools/powertools-lambda-python/issues/7786)) +* **deps:** bump codecov/codecov-action from 5.5.1 to 5.5.2 ([#7788](https://github.com/aws-powertools/powertools-lambda-python/issues/7788)) +* **deps:** bump urllib3 from 2.5.0 to 2.6.0 in /docs ([#7778](https://github.com/aws-powertools/powertools-lambda-python/issues/7778)) +* **deps-dev:** bump boto3-stubs from 1.41.2 to 1.41.5 ([#7768](https://github.com/aws-powertools/powertools-lambda-python/issues/7768)) +* **deps-dev:** bump ruff from 0.14.6 to 0.14.7 ([#7770](https://github.com/aws-powertools/powertools-lambda-python/issues/7770)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.227.0a0 to 2.231.0a0 ([#7767](https://github.com/aws-powertools/powertools-lambda-python/issues/7767)) +* **deps-dev:** bump aws-cdk-lib from 2.227.0 to 2.231.0 ([#7771](https://github.com/aws-powertools/powertools-lambda-python/issues/7771)) +* **deps-dev:** bump boto3-stubs from 1.42.4 to 1.42.6 ([#7791](https://github.com/aws-powertools/powertools-lambda-python/issues/7791)) +* **deps-dev:** bump types-protobuf from 6.32.1.20251105 to 6.32.1.20251210 ([#7793](https://github.com/aws-powertools/powertools-lambda-python/issues/7793)) +* **deps-dev:** bump aws-cdk from 2.1033.0 to 2.1034.0 ([#7795](https://github.com/aws-powertools/powertools-lambda-python/issues/7795)) +* **deps-dev:** bump boto3-stubs from 1.42.6 to 1.42.7 ([#7796](https://github.com/aws-powertools/powertools-lambda-python/issues/7796)) +* **deps-dev:** bump sentry-sdk from 2.45.0 to 2.47.0 ([#7790](https://github.com/aws-powertools/powertools-lambda-python/issues/7790)) +* **deps-dev:** bump cfn-lint from 1.41.0 to 1.42.0 ([#7784](https://github.com/aws-powertools/powertools-lambda-python/issues/7784)) +* **deps-dev:** bump aws-cdk-lib from 2.232.1 to 2.232.2 ([#7816](https://github.com/aws-powertools/powertools-lambda-python/issues/7816)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.225.0a0 to 2.227.0a0 ([#7730](https://github.com/aws-powertools/powertools-lambda-python/issues/7730)) +* **deps-dev:** bump boto3-stubs from 1.40.76 to 1.41.1 ([#7727](https://github.com/aws-powertools/powertools-lambda-python/issues/7727)) +* **deps-dev:** bump ruff from 0.14.5 to 0.14.6 ([#7728](https://github.com/aws-powertools/powertools-lambda-python/issues/7728)) +* **deps-dev:** bump mypy-boto3-appconfigdata from 1.40.55 to 1.41.0 in the boto-typing group ([#7721](https://github.com/aws-powertools/powertools-lambda-python/issues/7721)) +* **deps-dev:** bump boto3-stubs from 1.42.7 to 1.42.9 ([#7814](https://github.com/aws-powertools/powertools-lambda-python/issues/7814)) +* **deps-dev:** bump aws-cdk from 2.1034.0 to 2.1100.0 ([#7811](https://github.com/aws-powertools/powertools-lambda-python/issues/7811)) +* **deps-dev:** bump cfn-lint from 1.42.0 to 1.43.0 ([#7812](https://github.com/aws-powertools/powertools-lambda-python/issues/7812)) +* **deps-dev:** bump urllib3 from 2.5.0 to 2.6.0 in /layer_v3 ([#7820](https://github.com/aws-powertools/powertools-lambda-python/issues/7820)) +* **deps-dev:** bump pytest-asyncio from 1.2.0 to 1.3.0 ([#7825](https://github.com/aws-powertools/powertools-lambda-python/issues/7825)) +* **deps-dev:** bump ruff from 0.14.8 to 0.14.9 ([#7823](https://github.com/aws-powertools/powertools-lambda-python/issues/7823)) +* **deps-dev:** bump aws-cdk from 2.1032.0 to 2.1033.0 ([#7713](https://github.com/aws-powertools/powertools-lambda-python/issues/7713)) +* **deps-dev:** bump boto3-stubs from 1.40.75 to 1.40.76 ([#7714](https://github.com/aws-powertools/powertools-lambda-python/issues/7714)) +* **deps-dev:** bump pytest from 8.4.2 to 9.0.2 ([#7826](https://github.com/aws-powertools/powertools-lambda-python/issues/7826)) +* **deps-dev:** bump isort from 6.1.0 to 7.0.0 ([#7824](https://github.com/aws-powertools/powertools-lambda-python/issues/7824)) +* **deps-dev:** bump boto3-stubs from 1.42.10 to 1.42.11 ([#7831](https://github.com/aws-powertools/powertools-lambda-python/issues/7831)) +* **deps-dev:** bump cfn-lint from 1.40.4 to 1.41.0 ([#7709](https://github.com/aws-powertools/powertools-lambda-python/issues/7709)) +* **deps-dev:** bump sentry-sdk from 2.44.0 to 2.45.0 ([#7708](https://github.com/aws-powertools/powertools-lambda-python/issues/7708)) +* **deps-dev:** bump aws-cdk from 2.1100.0 to 2.1100.1 ([#7830](https://github.com/aws-powertools/powertools-lambda-python/issues/7830)) +* **deps-dev:** bump ruff from 0.14.9 to 0.14.10 ([#7839](https://github.com/aws-powertools/powertools-lambda-python/issues/7839)) +* **deps-dev:** bump boto3-stubs from 1.40.73 to 1.40.74 ([#7703](https://github.com/aws-powertools/powertools-lambda-python/issues/7703)) +* **deps-dev:** bump types-python-dateutil from 2.9.0.20251108 to 2.9.0.20251115 ([#7702](https://github.com/aws-powertools/powertools-lambda-python/issues/7702)) +* **deps-dev:** bump boto3-stubs from 1.42.11 to 1.42.13 ([#7844](https://github.com/aws-powertools/powertools-lambda-python/issues/7844)) +* **deps-dev:** bump aws-cdk-lib from 2.232.2 to 2.233.0 ([#7845](https://github.com/aws-powertools/powertools-lambda-python/issues/7845)) +* **deps-dev:** bump filelock from 3.20.1 to 3.20.2 ([#7876](https://github.com/aws-powertools/powertools-lambda-python/issues/7876)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.224.0a0 to 2.225.0a0 ([#7704](https://github.com/aws-powertools/powertools-lambda-python/issues/7704)) +* **deps-dev:** bump boto3-stubs from 1.40.72 to 1.40.73 ([#7688](https://github.com/aws-powertools/powertools-lambda-python/issues/7688)) +* **deps-dev:** bump boto3-stubs from 1.42.19 to 1.42.21 ([#7892](https://github.com/aws-powertools/powertools-lambda-python/issues/7892)) +* **deps-dev:** bump coverage from 7.13.0 to 7.13.1 ([#7867](https://github.com/aws-powertools/powertools-lambda-python/issues/7867)) +* **deps-dev:** bump cfn-lint from 1.43.0 to 1.43.1 ([#7869](https://github.com/aws-powertools/powertools-lambda-python/issues/7869)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.232.2a0 to 2.233.0a0 ([#7868](https://github.com/aws-powertools/powertools-lambda-python/issues/7868)) +* **deps-dev:** bump ruff from 0.14.4 to 0.14.5 ([#7682](https://github.com/aws-powertools/powertools-lambda-python/issues/7682)) +* **deps-dev:** bump boto3-stubs from 1.42.17 to 1.42.19 ([#7873](https://github.com/aws-powertools/powertools-lambda-python/issues/7873)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.223.0a0 to 2.224.0a0 ([#7680](https://github.com/aws-powertools/powertools-lambda-python/issues/7680)) +* **deps-dev:** bump aws-cdk from 2.1031.2 to 2.1032.0 ([#7700](https://github.com/aws-powertools/powertools-lambda-python/issues/7700)) +* **maintenance:** remove cloud development environment configurations ([#7887](https://github.com/aws-powertools/powertools-lambda-python/issues/7887)) + + + +## [v3.23.0] - 2025-11-13 +## Bug Fixes + +* **layer:** bump cdk version ([#7677](https://github.com/aws-powertools/powertools-lambda-python/issues/7677)) + +## Documentation + +* **batch:** fix error handling code highlight line number ([#7638](https://github.com/aws-powertools/powertools-lambda-python/issues/7638)) +* **openapi:** Update docstring's openapi default version to match current default version ([#7669](https://github.com/aws-powertools/powertools-lambda-python/issues/7669)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.22.2a0 ([#7642](https://github.com/aws-powertools/powertools-lambda-python/issues/7642)) +* **ci:** new pre-release 3.22.2a1 ([#7646](https://github.com/aws-powertools/powertools-lambda-python/issues/7646)) +* **ci:** adding support for Python 3.14 - WIP ([#7431](https://github.com/aws-powertools/powertools-lambda-python/issues/7431)) +* **ci:** new pre-release 3.22.2a2 ([#7652](https://github.com/aws-powertools/powertools-lambda-python/issues/7652)) +* **ci:** new pre-release 3.22.2a3 ([#7659](https://github.com/aws-powertools/powertools-lambda-python/issues/7659)) +* **ci:** new pre-release 3.22.2a4 ([#7674](https://github.com/aws-powertools/powertools-lambda-python/issues/7674)) +* **deps:** bump actions/dependency-review-action from 4.8.1 to 4.8.2 ([#7662](https://github.com/aws-powertools/powertools-lambda-python/issues/7662)) +* **deps:** bump pydantic from 2.12.3 to 2.12.4 ([#7641](https://github.com/aws-powertools/powertools-lambda-python/issues/7641)) +* **deps:** bump docker/setup-qemu-action from 3.6.0 to 3.7.0 ([#7639](https://github.com/aws-powertools/powertools-lambda-python/issues/7639)) +* **deps:** bump squidfunk/mkdocs-material from `58dee36` to `980e11f` in /docs ([#7661](https://github.com/aws-powertools/powertools-lambda-python/issues/7661)) +* **deps:** bump mkdocstrings-python from 1.18.2 to 1.19.0 in /docs ([#7655](https://github.com/aws-powertools/powertools-lambda-python/issues/7655)) +* **deps:** bump mkdocs-material from 9.6.23 to 9.7.0 in /docs ([#7663](https://github.com/aws-powertools/powertools-lambda-python/issues/7663)) +* **deps-dev:** bump aws-cdk-lib from 2.222.0 to 2.223.0 ([#7656](https://github.com/aws-powertools/powertools-lambda-python/issues/7656)) +* **deps-dev:** bump types-python-dateutil from 2.9.0.20251008 to 2.9.0.20251108 ([#7657](https://github.com/aws-powertools/powertools-lambda-python/issues/7657)) +* **deps-dev:** bump aws-cdk from 2.1031.1 to 2.1031.2 ([#7645](https://github.com/aws-powertools/powertools-lambda-python/issues/7645)) +* **deps-dev:** bump boto3-stubs from 1.40.70 to 1.40.71 ([#7672](https://github.com/aws-powertools/powertools-lambda-python/issues/7672)) +* **deps-dev:** bump sentry-sdk from 2.43.0 to 2.44.0 ([#7665](https://github.com/aws-powertools/powertools-lambda-python/issues/7665)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.222.0a0 to 2.223.0a0 ([#7664](https://github.com/aws-powertools/powertools-lambda-python/issues/7664)) +* **deps-dev:** bump boto3-stubs from 1.40.64 to 1.40.69 ([#7654](https://github.com/aws-powertools/powertools-lambda-python/issues/7654)) +* **deps-dev:** bump types-protobuf from 6.32.1.20250918 to 6.32.1.20251105 ([#7640](https://github.com/aws-powertools/powertools-lambda-python/issues/7640)) +* **deps-dev:** bump pytest-benchmark from 5.2.1 to 5.2.3 ([#7658](https://github.com/aws-powertools/powertools-lambda-python/issues/7658)) +* **deps-dev:** bump nox from 2025.10.16 to 2025.11.12 ([#7671](https://github.com/aws-powertools/powertools-lambda-python/issues/7671)) +* **deps-dev:** bump ruff from 0.14.3 to 0.14.4 ([#7649](https://github.com/aws-powertools/powertools-lambda-python/issues/7649)) +* **docs:** fix broken images ([#7644](https://github.com/aws-powertools/powertools-lambda-python/issues/7644)) + + + +## [v3.22.1] - 2025-11-05 +## Bug Fixes + +* **batch:** ensure Python 3.14 compatibility for async event loop handling ([#7599](https://github.com/aws-powertools/powertools-lambda-python/issues/7599)) +* **event-handler:** preserve metadata constraints in parameter validation ([#7609](https://github.com/aws-powertools/powertools-lambda-python/issues/7609)) +* **typing:** Fix type of Metrics.set_timestamp argument ([#7582](https://github.com/aws-powertools/powertools-lambda-python/issues/7582)) + +## Documentation + +* **tracer:** fix typo in the tracer documentation ([#7617](https://github.com/aws-powertools/powertools-lambda-python/issues/7617)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.22.1a0 ([#7567](https://github.com/aws-powertools/powertools-lambda-python/issues/7567)) +* **ci:** new pre-release 3.22.1a1 ([#7583](https://github.com/aws-powertools/powertools-lambda-python/issues/7583)) +* **ci:** new pre-release 3.22.1a8 ([#7633](https://github.com/aws-powertools/powertools-lambda-python/issues/7633)) +* **ci:** new pre-release 3.22.1a2 ([#7588](https://github.com/aws-powertools/powertools-lambda-python/issues/7588)) +* **ci:** new pre-release 3.22.1a7 ([#7625](https://github.com/aws-powertools/powertools-lambda-python/issues/7625)) +* **ci:** new pre-release 3.22.1a6 ([#7615](https://github.com/aws-powertools/powertools-lambda-python/issues/7615)) +* **ci:** new pre-release 3.22.1a4 ([#7597](https://github.com/aws-powertools/powertools-lambda-python/issues/7597)) +* **ci:** new pre-release 3.22.1a3 ([#7591](https://github.com/aws-powertools/powertools-lambda-python/issues/7591)) +* **ci:** new pre-release 3.22.1a5 ([#7607](https://github.com/aws-powertools/powertools-lambda-python/issues/7607)) +* **deps:** bump mkdocs-material from 9.6.22 to 9.6.23 ([#7630](https://github.com/aws-powertools/powertools-lambda-python/issues/7630)) +* **deps:** bump squidfunk/mkdocs-material from `f5c556a` to `58dee36` in /docs ([#7622](https://github.com/aws-powertools/powertools-lambda-python/issues/7622)) +* **deps:** bump aws-xray-sdk from 2.14.0 to 2.15.0 ([#7603](https://github.com/aws-powertools/powertools-lambda-python/issues/7603)) +* **deps:** bump actions/upload-artifact from 4.6.2 to 5.0.0 ([#7578](https://github.com/aws-powertools/powertools-lambda-python/issues/7578)) +* **deps:** bump redis from 6.4.0 to 7.0.0 ([#7570](https://github.com/aws-powertools/powertools-lambda-python/issues/7570)) +* **deps:** bump actions/download-artifact from 5.0.0 to 6.0.0 ([#7577](https://github.com/aws-powertools/powertools-lambda-python/issues/7577)) +* **deps:** bump mkdocs-material from 9.6.22 to 9.6.23 ([#7624](https://github.com/aws-powertools/powertools-lambda-python/issues/7624)) +* **deps:** bump redis from 7.0.0 to 7.0.1 ([#7586](https://github.com/aws-powertools/powertools-lambda-python/issues/7586)) +* **deps-dev:** bump boto3-stubs from 1.40.60 to 1.40.61 ([#7590](https://github.com/aws-powertools/powertools-lambda-python/issues/7590)) +* **deps-dev:** bump pytest-benchmark from 5.1.0 to 5.2.0 ([#7606](https://github.com/aws-powertools/powertools-lambda-python/issues/7606)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.311 to 0.1.312 ([#7605](https://github.com/aws-powertools/powertools-lambda-python/issues/7605)) +* **deps-dev:** bump cfn-lint from 1.40.2 to 1.40.3 ([#7585](https://github.com/aws-powertools/powertools-lambda-python/issues/7585)) +* **deps-dev:** bump boto3-stubs from 1.40.59 to 1.40.60 ([#7587](https://github.com/aws-powertools/powertools-lambda-python/issues/7587)) +* **deps-dev:** bump aws-cdk-lib from 2.221.1 to 2.222.0 ([#7631](https://github.com/aws-powertools/powertools-lambda-python/issues/7631)) +* **deps-dev:** bump hvac from 2.3.0 to 2.4.0 ([#7602](https://github.com/aws-powertools/powertools-lambda-python/issues/7602)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.221.0a0 to 2.221.1a0 ([#7593](https://github.com/aws-powertools/powertools-lambda-python/issues/7593)) +* **deps-dev:** bump ruff from 0.14.1 to 0.14.2 ([#7575](https://github.com/aws-powertools/powertools-lambda-python/issues/7575)) +* **deps-dev:** bump boto3-stubs from 1.40.55 to 1.40.59 ([#7579](https://github.com/aws-powertools/powertools-lambda-python/issues/7579)) +* **deps-dev:** bump sentry-sdk from 2.42.1 to 2.43.0 ([#7594](https://github.com/aws-powertools/powertools-lambda-python/issues/7594)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.220.0a0 to 2.221.0a0 ([#7580](https://github.com/aws-powertools/powertools-lambda-python/issues/7580)) +* **deps-dev:** bump ruff from 0.14.2 to 0.14.3 ([#7610](https://github.com/aws-powertools/powertools-lambda-python/issues/7610)) +* **deps-dev:** bump boto3-stubs from 1.40.61 to 1.40.64 ([#7611](https://github.com/aws-powertools/powertools-lambda-python/issues/7611)) +* **deps-dev:** bump aws-cdk from 2.1030.0 to 2.1031.0 ([#7569](https://github.com/aws-powertools/powertools-lambda-python/issues/7569)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.221.1a0 to 2.222.0a0 ([#7628](https://github.com/aws-powertools/powertools-lambda-python/issues/7628)) +* **deps-dev:** bump cfn-lint from 1.40.3 to 1.40.4 ([#7632](https://github.com/aws-powertools/powertools-lambda-python/issues/7632)) +* **deps-dev:** bump pytest-benchmark from 5.2.0 to 5.2.1 ([#7629](https://github.com/aws-powertools/powertools-lambda-python/issues/7629)) +* **deps-dev:** bump aws-cdk from 2.1031.0 to 2.1031.1 ([#7601](https://github.com/aws-powertools/powertools-lambda-python/issues/7601)) +* **event_handler:** fix typos in api_gateway.py file ([#7619](https://github.com/aws-powertools/powertools-lambda-python/issues/7619)) + + + +## [v3.22.0] - 2025-10-21 +## Bug Fixes + +* **event_handler:** Preserve `examples` field in OpenAPI schema for BedrockAgentResolver ([#7561](https://github.com/aws-powertools/powertools-lambda-python/issues/7561)) + +## Documentation + +* **idempotency:** removed inexistent css class ([#7539](https://github.com/aws-powertools/powertools-lambda-python/issues/7539)) + +## Features + +* **event_handler:** add support for Pydantic models in Query and Header types ([#7253](https://github.com/aws-powertools/powertools-lambda-python/issues/7253)) + +## Maintenance + +* version bump +* use approved sample names and addresses ([#7542](https://github.com/aws-powertools/powertools-lambda-python/issues/7542)) +* **ci:** new pre-release 3.21.1a5 ([#7538](https://github.com/aws-powertools/powertools-lambda-python/issues/7538)) +* **ci:** new pre-release 3.21.1a4 ([#7528](https://github.com/aws-powertools/powertools-lambda-python/issues/7528)) +* **ci:** new pre-release 3.21.1a7 ([#7563](https://github.com/aws-powertools/powertools-lambda-python/issues/7563)) +* **ci:** new pre-release 3.21.1a0 ([#7500](https://github.com/aws-powertools/powertools-lambda-python/issues/7500)) +* **ci:** update CONTRIBUTING.md ([#7549](https://github.com/aws-powertools/powertools-lambda-python/issues/7549)) +* **ci:** new pre-release 3.21.1a6 ([#7548](https://github.com/aws-powertools/powertools-lambda-python/issues/7548)) +* **ci:** new pre-release 3.21.1a1 ([#7506](https://github.com/aws-powertools/powertools-lambda-python/issues/7506)) +* **ci:** improve message when do-not-merge label is present ([#7505](https://github.com/aws-powertools/powertools-lambda-python/issues/7505)) +* **ci:** new pre-release 3.21.1a3 ([#7519](https://github.com/aws-powertools/powertools-lambda-python/issues/7519)) +* **ci:** new pre-release 3.21.1a2 ([#7514](https://github.com/aws-powertools/powertools-lambda-python/issues/7514)) +* **deps:** bump squidfunk/mkdocs-material from `00f9276` to `f5c556a` in /docs ([#7530](https://github.com/aws-powertools/powertools-lambda-python/issues/7530)) +* **deps:** bump mkdocs-material from 9.6.21 to 9.6.22 in /docs ([#7531](https://github.com/aws-powertools/powertools-lambda-python/issues/7531)) +* **deps:** bump pydantic from 2.12.0 to 2.12.2 ([#7522](https://github.com/aws-powertools/powertools-lambda-python/issues/7522)) +* **deps:** bump mkdocs-material from 9.6.21 to 9.6.22 ([#7534](https://github.com/aws-powertools/powertools-lambda-python/issues/7534)) +* **deps:** bump avro from 1.12.0 to 1.12.1 ([#7546](https://github.com/aws-powertools/powertools-lambda-python/issues/7546)) +* **deps:** bump protobuf from 6.32.1 to 6.33.0 ([#7544](https://github.com/aws-powertools/powertools-lambda-python/issues/7544)) +* **deps:** bump pydantic from 2.12.2 to 2.12.3 ([#7556](https://github.com/aws-powertools/powertools-lambda-python/issues/7556)) +* **deps:** bump actions/setup-node from 5.0.0 to 6.0.0 ([#7521](https://github.com/aws-powertools/powertools-lambda-python/issues/7521)) +* **deps:** bump valkey-glide from 2.1.0 to 2.1.1 ([#7498](https://github.com/aws-powertools/powertools-lambda-python/issues/7498)) +* **deps:** bump actions/dependency-review-action from 4.8.0 to 4.8.1 ([#7516](https://github.com/aws-powertools/powertools-lambda-python/issues/7516)) +* **deps-dev:** bump nox from 2024.10.9 to 2025.10.14 ([#7532](https://github.com/aws-powertools/powertools-lambda-python/issues/7532)) +* **deps-dev:** bump boto3-stubs from 1.40.49 to 1.40.51 ([#7517](https://github.com/aws-powertools/powertools-lambda-python/issues/7517)) +* **deps-dev:** bump cfn-lint from 1.40.1 to 1.40.2 ([#7525](https://github.com/aws-powertools/powertools-lambda-python/issues/7525)) +* **deps-dev:** bump aws-cdk-lib from 2.219.0 to 2.220.0 ([#7524](https://github.com/aws-powertools/powertools-lambda-python/issues/7524)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.219.0a0 to 2.220.0a0 ([#7526](https://github.com/aws-powertools/powertools-lambda-python/issues/7526)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.310 to 0.1.311 ([#7523](https://github.com/aws-powertools/powertools-lambda-python/issues/7523)) +* **deps-dev:** bump boto3-stubs from 1.40.47 to 1.40.49 ([#7503](https://github.com/aws-powertools/powertools-lambda-python/issues/7503)) +* **deps-dev:** bump ijson from 3.4.0 to 3.4.0.post0 ([#7510](https://github.com/aws-powertools/powertools-lambda-python/issues/7510)) +* **deps-dev:** bump aws-cdk from 2.1029.4 to 2.1030.0 ([#7508](https://github.com/aws-powertools/powertools-lambda-python/issues/7508)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.309 to 0.1.310 ([#7509](https://github.com/aws-powertools/powertools-lambda-python/issues/7509)) +* **deps-dev:** bump boto3-stubs from 1.40.51 to 1.40.53 ([#7535](https://github.com/aws-powertools/powertools-lambda-python/issues/7535)) +* **deps-dev:** bump sentry-sdk from 2.41.0 to 2.42.0 ([#7533](https://github.com/aws-powertools/powertools-lambda-python/issues/7533)) +* **deps-dev:** bump cfn-lint from 1.40.0 to 1.40.1 ([#7502](https://github.com/aws-powertools/powertools-lambda-python/issues/7502)) +* **deps-dev:** bump ruff from 0.14.0 to 0.14.1 ([#7545](https://github.com/aws-powertools/powertools-lambda-python/issues/7545)) +* **deps-dev:** bump sentry-sdk from 2.40.0 to 2.41.0 ([#7504](https://github.com/aws-powertools/powertools-lambda-python/issues/7504)) +* **deps-dev:** bump boto3-stubs from 1.40.53 to 1.40.54 ([#7547](https://github.com/aws-powertools/powertools-lambda-python/issues/7547)) +* **deps-dev:** bump boto3-stubs from 1.40.54 to 1.40.55 ([#7555](https://github.com/aws-powertools/powertools-lambda-python/issues/7555)) +* **deps-dev:** bump nox from 2025.10.14 to 2025.10.16 ([#7557](https://github.com/aws-powertools/powertools-lambda-python/issues/7557)) +* **deps-dev:** bump types-python-dateutil from 2.9.0.20250822 to 2.9.0.20251008 ([#7499](https://github.com/aws-powertools/powertools-lambda-python/issues/7499)) +* **deps-dev:** bump sentry-sdk from 2.42.0 to 2.42.1 ([#7562](https://github.com/aws-powertools/powertools-lambda-python/issues/7562)) +* **deps-dev:** bump mypy-boto3-appconfigdata from 1.40.0 to 1.40.55 in the boto-typing group ([#7554](https://github.com/aws-powertools/powertools-lambda-python/issues/7554)) + + + +## [v3.21.0] - 2025-10-08 +## Bug Fixes + +* **docs:** correct build optimization script and docs ([#7367](https://github.com/aws-powertools/powertools-lambda-python/issues/7367)) +* **event_handler:** parse single list items in form data ([#7415](https://github.com/aws-powertools/powertools-lambda-python/issues/7415)) + +## Code Refactoring + +* **parser:** Improve AppSync models with examples and descriptions ([#7330](https://github.com/aws-powertools/powertools-lambda-python/issues/7330)) + +## Documentation + +* **aws:** add AWS docs bootstrap JavaScript ([#7472](https://github.com/aws-powertools/powertools-lambda-python/issues/7472)) +* **event_handler:** remove the wrong CORS warning ([#7385](https://github.com/aws-powertools/powertools-lambda-python/issues/7385)) +* **event_handler:** update test section ([#7374](https://github.com/aws-powertools/powertools-lambda-python/issues/7374)) +* **event_handler:** add info section about types ([#7368](https://github.com/aws-powertools/powertools-lambda-python/issues/7368)) +* **logger:** clarify Advanced Logging Controls interaction with sampling ([#7429](https://github.com/aws-powertools/powertools-lambda-python/issues/7429)) + +## Features + +* **event-handler:** add support for Pydantic Field discriminator in validation ([#7227](https://github.com/aws-powertools/powertools-lambda-python/issues/7227)) +* **event_handler:** enhance OpenAPI response with headers, links, examples and encoding ([#7312](https://github.com/aws-powertools/powertools-lambda-python/issues/7312)) +* **parser:** add field metadata and examples for CloudWatch models ([#7343](https://github.com/aws-powertools/powertools-lambda-python/issues/7343)) + +## Maintenance + +* Fix Discord badge in README ([#7488](https://github.com/aws-powertools/powertools-lambda-python/issues/7488)) +* version bump +* **ci:** new pre-release 3.20.1a12 ([#7492](https://github.com/aws-powertools/powertools-lambda-python/issues/7492)) +* **ci:** new pre-release 3.20.1a5 ([#7418](https://github.com/aws-powertools/powertools-lambda-python/issues/7418)) +* **ci:** new pre-release 3.20.1a0 ([#7362](https://github.com/aws-powertools/powertools-lambda-python/issues/7362)) +* **ci:** new pre-release 3.20.1a11 ([#7485](https://github.com/aws-powertools/powertools-lambda-python/issues/7485)) +* **ci:** new pre-release 3.20.1a4 ([#7404](https://github.com/aws-powertools/powertools-lambda-python/issues/7404)) +* **ci:** new pre-release 3.20.1a10 ([#7479](https://github.com/aws-powertools/powertools-lambda-python/issues/7479)) +* **ci:** new pre-release 3.20.1a3 ([#7393](https://github.com/aws-powertools/powertools-lambda-python/issues/7393)) +* **ci:** new pre-release 3.20.1a1 ([#7372](https://github.com/aws-powertools/powertools-lambda-python/issues/7372)) +* **ci:** new pre-release 3.20.1a9 ([#7469](https://github.com/aws-powertools/powertools-lambda-python/issues/7469)) +* **ci:** new pre-release 3.20.1a8 ([#7456](https://github.com/aws-powertools/powertools-lambda-python/issues/7456)) +* **ci:** new pre-release 3.20.1a6 ([#7442](https://github.com/aws-powertools/powertools-lambda-python/issues/7442)) +* **ci:** new pre-release 3.20.1a2 ([#7383](https://github.com/aws-powertools/powertools-lambda-python/issues/7383)) +* **ci:** new pre-release 3.20.1a7 ([#7450](https://github.com/aws-powertools/powertools-lambda-python/issues/7450)) +* **deps:** bump mkdocs-llmstxt from 0.3.1 to 0.3.2 ([#7436](https://github.com/aws-powertools/powertools-lambda-python/issues/7436)) +* **deps:** bump mkdocs-material from 9.6.20 to 9.6.21 in /docs ([#7453](https://github.com/aws-powertools/powertools-lambda-python/issues/7453)) +* **deps:** bump squidfunk/mkdocs-material from `86d21da` to `00f9276` in /docs ([#7452](https://github.com/aws-powertools/powertools-lambda-python/issues/7452)) +* **deps:** bump ossf/scorecard-action from 2.4.2 to 2.4.3 ([#7458](https://github.com/aws-powertools/powertools-lambda-python/issues/7458)) +* **deps:** bump protobuf from 6.32.0 to 6.32.1 ([#7376](https://github.com/aws-powertools/powertools-lambda-python/issues/7376)) +* **deps:** bump pydantic-settings from 2.10.1 to 2.11.0 ([#7434](https://github.com/aws-powertools/powertools-lambda-python/issues/7434)) +* **deps:** bump mkdocs-material from 9.6.19 to 9.6.20 in /docs ([#7387](https://github.com/aws-powertools/powertools-lambda-python/issues/7387)) +* **deps:** bump mkdocs-llmstxt from 0.3.2 to 0.4.0 in /docs ([#7473](https://github.com/aws-powertools/powertools-lambda-python/issues/7473)) +* **deps:** bump mkdocs-material from 9.6.20 to 9.6.21 ([#7455](https://github.com/aws-powertools/powertools-lambda-python/issues/7455)) +* **deps:** bump mkdocs-material from 9.6.19 to 9.6.20 ([#7389](https://github.com/aws-powertools/powertools-lambda-python/issues/7389)) +* **deps:** bump actions/dependency-review-action from 4.7.3 to 4.8.0 ([#7432](https://github.com/aws-powertools/powertools-lambda-python/issues/7432)) +* **deps:** bump mkdocs-llmstxt from 0.3.2 to 0.4.0 ([#7475](https://github.com/aws-powertools/powertools-lambda-python/issues/7475)) +* **deps:** bump pydantic from 2.11.7 to 2.11.9 ([#7390](https://github.com/aws-powertools/powertools-lambda-python/issues/7390)) +* **deps:** bump valkey-glide from 2.0.1 to 2.1.0 ([#7403](https://github.com/aws-powertools/powertools-lambda-python/issues/7403)) +* **deps:** bump pydantic from 2.11.9 to 2.11.10 ([#7483](https://github.com/aws-powertools/powertools-lambda-python/issues/7483)) +* **deps:** bump mkdocs-material from 9.6.18 to 9.6.19 ([#7359](https://github.com/aws-powertools/powertools-lambda-python/issues/7359)) +* **deps:** bump pydantic from 2.11.10 to 2.12.0 ([#7491](https://github.com/aws-powertools/powertools-lambda-python/issues/7491)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.25 to 4.0.0 ([#7449](https://github.com/aws-powertools/powertools-lambda-python/issues/7449)) +* **deps:** bump mkdocs-llmstxt from 0.3.1 to 0.3.2 in /docs ([#7409](https://github.com/aws-powertools/powertools-lambda-python/issues/7409)) +* **deps:** bump squidfunk/mkdocs-material from `209b62d` to `86d21da` in /docs ([#7386](https://github.com/aws-powertools/powertools-lambda-python/issues/7386)) +* **deps-dev:** bump cfn-lint from 1.39.1 to 1.40.0 ([#7447](https://github.com/aws-powertools/powertools-lambda-python/issues/7447)) +* **deps-dev:** bump pytest-mock from 3.15.0 to 3.15.1 ([#7396](https://github.com/aws-powertools/powertools-lambda-python/issues/7396)) +* **deps-dev:** bump ruff from 0.12.12 to 0.13.2 ([#7427](https://github.com/aws-powertools/powertools-lambda-python/issues/7427)) +* **deps-dev:** bump types-protobuf from 6.30.2.20250822 to 6.32.1.20250918 ([#7406](https://github.com/aws-powertools/powertools-lambda-python/issues/7406)) +* **deps-dev:** bump aws-cdk from 2.1029.2 to 2.1029.3 ([#7420](https://github.com/aws-powertools/powertools-lambda-python/issues/7420)) +* **deps-dev:** bump aws-cdk-lib from 2.215.0 to 2.217.0 ([#7423](https://github.com/aws-powertools/powertools-lambda-python/issues/7423)) +* **deps-dev:** bump aws-cdk from 2.1029.1 to 2.1029.2 ([#7400](https://github.com/aws-powertools/powertools-lambda-python/issues/7400)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.215.0a0 to 2.217.0a0 ([#7424](https://github.com/aws-powertools/powertools-lambda-python/issues/7424)) +* **deps-dev:** bump boto3-stubs from 1.40.31 to 1.40.32 ([#7395](https://github.com/aws-powertools/powertools-lambda-python/issues/7395)) +* **deps-dev:** bump boto3-stubs from 1.40.32 to 1.40.39 ([#7425](https://github.com/aws-powertools/powertools-lambda-python/issues/7425)) +* **deps-dev:** bump coverage from 7.10.6 to 7.10.7 ([#7437](https://github.com/aws-powertools/powertools-lambda-python/issues/7437)) +* **deps-dev:** bump mypy from 1.18.1 to 1.18.2 ([#7435](https://github.com/aws-powertools/powertools-lambda-python/issues/7435)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.214.0a0 to 2.215.0a0 ([#7391](https://github.com/aws-powertools/powertools-lambda-python/issues/7391)) +* **deps-dev:** bump boto3-stubs from 1.40.30 to 1.40.31 ([#7388](https://github.com/aws-powertools/powertools-lambda-python/issues/7388)) +* **deps-dev:** bump boto3-stubs from 1.40.39 to 1.40.40 ([#7433](https://github.com/aws-powertools/powertools-lambda-python/issues/7433)) +* **deps-dev:** bump boto3-stubs from 1.40.27 to 1.40.29 ([#7371](https://github.com/aws-powertools/powertools-lambda-python/issues/7371)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.217.0a0 to 2.218.0a0 ([#7448](https://github.com/aws-powertools/powertools-lambda-python/issues/7448)) +* **deps-dev:** bump boto3-stubs from 1.40.40 to 1.40.41 ([#7445](https://github.com/aws-powertools/powertools-lambda-python/issues/7445)) +* **deps-dev:** bump sentry-sdk from 2.38.0 to 2.39.0 ([#7426](https://github.com/aws-powertools/powertools-lambda-python/issues/7426)) +* **deps-dev:** bump boto3-stubs from 1.40.41 to 1.40.42 ([#7454](https://github.com/aws-powertools/powertools-lambda-python/issues/7454)) +* **deps-dev:** bump boto3-stubs from 1.40.29 to 1.40.30 ([#7378](https://github.com/aws-powertools/powertools-lambda-python/issues/7378)) +* **deps-dev:** bump pytest-asyncio from 1.1.0 to 1.2.0 ([#7377](https://github.com/aws-powertools/powertools-lambda-python/issues/7377)) +* **deps-dev:** bump boto3-stubs from 1.40.42 to 1.40.43 ([#7460](https://github.com/aws-powertools/powertools-lambda-python/issues/7460)) +* **deps-dev:** bump mypy from 1.17.1 to 1.18.1 ([#7375](https://github.com/aws-powertools/powertools-lambda-python/issues/7375)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.218.0a0 to 2.219.0a0 ([#7464](https://github.com/aws-powertools/powertools-lambda-python/issues/7464)) +* **deps-dev:** bump aws-cdk from 2.1029.3 to 2.1029.4 ([#7459](https://github.com/aws-powertools/powertools-lambda-python/issues/7459)) +* **deps-dev:** bump ruff from 0.13.2 to 0.13.3 ([#7465](https://github.com/aws-powertools/powertools-lambda-python/issues/7465)) +* **deps-dev:** bump aws-cdk from 2.1029.0 to 2.1029.1 ([#7370](https://github.com/aws-powertools/powertools-lambda-python/issues/7370)) +* **deps-dev:** bump isort from 6.0.1 to 6.1.0 ([#7461](https://github.com/aws-powertools/powertools-lambda-python/issues/7461)) +* **deps-dev:** bump boto3-stubs from 1.40.44 to 1.40.45 ([#7474](https://github.com/aws-powertools/powertools-lambda-python/issues/7474)) +* **deps-dev:** bump sentry-sdk from 2.39.0 to 2.40.0 ([#7484](https://github.com/aws-powertools/powertools-lambda-python/issues/7484)) +* **deps-dev:** bump boto3-stubs from 1.40.26 to 1.40.27 ([#7358](https://github.com/aws-powertools/powertools-lambda-python/issues/7358)) +* **deps-dev:** bump pytest-cov from 6.3.0 to 7.0.0 ([#7357](https://github.com/aws-powertools/powertools-lambda-python/issues/7357)) +* **deps-dev:** bump ruff from 0.13.3 to 0.14.0 ([#7489](https://github.com/aws-powertools/powertools-lambda-python/issues/7489)) +* **deps-dev:** bump testcontainers from 4.12.0 to 4.13.0 ([#7360](https://github.com/aws-powertools/powertools-lambda-python/issues/7360)) +* **deps-dev:** bump sentry-sdk from 2.37.0 to 2.37.1 ([#7356](https://github.com/aws-powertools/powertools-lambda-python/issues/7356)) +* **deps-dev:** bump boto3-stubs from 1.40.45 to 1.40.47 ([#7490](https://github.com/aws-powertools/powertools-lambda-python/issues/7490)) +* **deps-dev:** bump sentry-sdk from 2.37.1 to 2.38.0 ([#7402](https://github.com/aws-powertools/powertools-lambda-python/issues/7402)) + + + +## [v3.20.0] - 2025-09-09 +## Code Refactoring + +* **event_handler:** allow to pass dict as argument to exception classes ([#7341](https://github.com/aws-powertools/powertools-lambda-python/issues/7341)) +* **parser:** improves S3 models with examples and descriptions ([#7292](https://github.com/aws-powertools/powertools-lambda-python/issues/7292)) +* **parser:** Improve Transfer Family models with examples and descriptions ([#7294](https://github.com/aws-powertools/powertools-lambda-python/issues/7294)) +* **parser:** Improve Kafka models with examples and descriptions ([#7293](https://github.com/aws-powertools/powertools-lambda-python/issues/7293)) +* **parser:** Improve SQS models with examples and descriptions ([#7286](https://github.com/aws-powertools/powertools-lambda-python/issues/7286)) +* **parser:** Improve SNS models with examples and descriptions ([#7287](https://github.com/aws-powertools/powertools-lambda-python/issues/7287)) +* **parser:** Improve VPC Lattice with examples and descriptions ([#7234](https://github.com/aws-powertools/powertools-lambda-python/issues/7234)) +* **parser:** Improve DynamoDB models with examples and descriptions ([#7146](https://github.com/aws-powertools/powertools-lambda-python/issues/7146)) + +## Documentation + +* fix SSM recursive parameter highlighting ([#7316](https://github.com/aws-powertools/powertools-lambda-python/issues/7316)) +* Add AWS copyright footer. ([#7313](https://github.com/aws-powertools/powertools-lambda-python/issues/7313)) +* Add installation snippet for uv package manager ([#7272](https://github.com/aws-powertools/powertools-lambda-python/issues/7272)) +* Fix copy to clipboard button ([#7270](https://github.com/aws-powertools/powertools-lambda-python/issues/7270)) +* **build_recipes:** add troubleshooting page ([#7195](https://github.com/aws-powertools/powertools-lambda-python/issues/7195)) +* **build_recipes:** add cicd page ([#7176](https://github.com/aws-powertools/powertools-lambda-python/issues/7176)) +* **build_recipes:** add initial build recipes structure ([#7163](https://github.com/aws-powertools/powertools-lambda-python/issues/7163)) +* **build_recipes:** add build tools page ([#7201](https://github.com/aws-powertools/powertools-lambda-python/issues/7201)) +* **build_recipes:** add cross build page ([#7199](https://github.com/aws-powertools/powertools-lambda-python/issues/7199)) +* **build_recipes:** add performance optimization page ([#7197](https://github.com/aws-powertools/powertools-lambda-python/issues/7197)) +* **index:** remove customer names ([#7318](https://github.com/aws-powertools/powertools-lambda-python/issues/7318)) +* **mkdocs:** fix docs warnings ([#7211](https://github.com/aws-powertools/powertools-lambda-python/issues/7211)) +* **public_reference:** add QuasiScience as a public reference ([#7228](https://github.com/aws-powertools/powertools-lambda-python/issues/7228)) + +## Features + +* **parser:** add support for sourceIp with ipv6 and port ([#7351](https://github.com/aws-powertools/powertools-lambda-python/issues/7351)) +* **parser:** add support for sourceIp with port ([#7315](https://github.com/aws-powertools/powertools-lambda-python/issues/7315)) + +## Maintenance + +* version bump +* **automation:** update PR template to include closes command ([#7173](https://github.com/aws-powertools/powertools-lambda-python/issues/7173)) +* **ci:** new pre-release 3.19.1a11 ([#7278](https://github.com/aws-powertools/powertools-lambda-python/issues/7278)) +* **ci:** new pre-release 3.19.1a0 ([#7161](https://github.com/aws-powertools/powertools-lambda-python/issues/7161)) +* **ci:** new pre-release 3.19.1a16 ([#7350](https://github.com/aws-powertools/powertools-lambda-python/issues/7350)) +* **ci:** new pre-release 3.19.1a1 ([#7172](https://github.com/aws-powertools/powertools-lambda-python/issues/7172)) +* **ci:** new pre-release 3.19.1a2 ([#7192](https://github.com/aws-powertools/powertools-lambda-python/issues/7192)) +* **ci:** new pre-release 3.19.1a3 ([#7207](https://github.com/aws-powertools/powertools-lambda-python/issues/7207)) +* **ci:** new pre-release 3.19.1a4 ([#7217](https://github.com/aws-powertools/powertools-lambda-python/issues/7217)) +* **ci:** new pre-release 3.19.1a6 ([#7232](https://github.com/aws-powertools/powertools-lambda-python/issues/7232)) +* **ci:** new pre-release 3.19.1a7 ([#7244](https://github.com/aws-powertools/powertools-lambda-python/issues/7244)) +* **ci:** new pre-release 3.19.1a14 ([#7306](https://github.com/aws-powertools/powertools-lambda-python/issues/7306)) +* **ci:** new pre-release 3.19.1a5 ([#7226](https://github.com/aws-powertools/powertools-lambda-python/issues/7226)) +* **ci:** new pre-release 3.19.1a8 ([#7247](https://github.com/aws-powertools/powertools-lambda-python/issues/7247)) +* **ci:** new pre-release 3.19.1a9 ([#7256](https://github.com/aws-powertools/powertools-lambda-python/issues/7256)) +* **ci:** new pre-release 3.19.1a13 ([#7295](https://github.com/aws-powertools/powertools-lambda-python/issues/7295)) +* **ci:** new pre-release 3.19.1a12 ([#7290](https://github.com/aws-powertools/powertools-lambda-python/issues/7290)) +* **ci:** new pre-release 3.19.1a10 ([#7269](https://github.com/aws-powertools/powertools-lambda-python/issues/7269)) +* **ci:** new pre-release 3.19.1a15 ([#7328](https://github.com/aws-powertools/powertools-lambda-python/issues/7328)) +* **deps:** bump mkdocstrings-python from 1.17.0 to 1.18.0 ([#7268](https://github.com/aws-powertools/powertools-lambda-python/issues/7268)) +* **deps:** bump mkdocstrings-python from 1.18.0 to 1.18.2 in /docs ([#7273](https://github.com/aws-powertools/powertools-lambda-python/issues/7273)) +* **deps:** bump squidfunk/mkdocs-material from `1a4e939` to `209b62d` in /docs ([#7344](https://github.com/aws-powertools/powertools-lambda-python/issues/7344)) +* **deps:** bump aws-actions/configure-aws-credentials from aa1f74b81b53cb3adb28afcdb21d7b9f3fceea98 to 209f2a4450bb4b277e1dedaff40ad2fd8d4d0a4c ([#7160](https://github.com/aws-powertools/powertools-lambda-python/issues/7160)) +* **deps:** bump mkdocs-material from 9.6.18 to 9.6.19 ([#7346](https://github.com/aws-powertools/powertools-lambda-python/issues/7346)) +* **deps:** bump actions/setup-go from 5.5.0 to 6.0.0 ([#7323](https://github.com/aws-powertools/powertools-lambda-python/issues/7323)) +* **deps:** bump codecov/codecov-action from 5.5.0 to 5.5.1 ([#7345](https://github.com/aws-powertools/powertools-lambda-python/issues/7345)) +* **deps:** bump aws-actions/configure-aws-credentials from 241c954d319becef88d2022775301737d5eb5e24 to 1b2b73eb6a459c3a91fde76ba4c255e5b4b8e94e ([#7266](https://github.com/aws-powertools/powertools-lambda-python/issues/7266)) +* **deps:** bump aws-actions/configure-aws-credentials from 0eb446ecb2e3f0e1a19f106e12e76c6a98b6bdb2 to 241c954d319becef88d2022775301737d5eb5e24 ([#7260](https://github.com/aws-powertools/powertools-lambda-python/issues/7260)) +* **deps:** bump mkdocstrings-python from 1.18.0 to 1.18.2 ([#7277](https://github.com/aws-powertools/powertools-lambda-python/issues/7277)) +* **deps:** bump actions/setup-python from 5.6.0 to 6.0.0 ([#7321](https://github.com/aws-powertools/powertools-lambda-python/issues/7321)) +* **deps:** bump actions/github-script from 7.0.1 to 8.0.0 ([#7320](https://github.com/aws-powertools/powertools-lambda-python/issues/7320)) +* **deps:** bump aws-powertools/actions from 1.4.0 to 1.5.0 ([#7180](https://github.com/aws-powertools/powertools-lambda-python/issues/7180)) +* **deps:** bump squidfunk/mkdocs-material from `bb7b015` to `405aeb6` in /docs ([#7185](https://github.com/aws-powertools/powertools-lambda-python/issues/7185)) +* **deps:** bump actions/dependency-review-action from 4.7.2 to 4.7.3 ([#7255](https://github.com/aws-powertools/powertools-lambda-python/issues/7255)) +* **deps:** bump mkdocstrings-python from 1.17.0 to 1.18.0 ([#7254](https://github.com/aws-powertools/powertools-lambda-python/issues/7254)) +* **deps:** bump typing-extensions from 4.14.1 to 4.15.0 ([#7251](https://github.com/aws-powertools/powertools-lambda-python/issues/7251)) +* **deps:** bump mkdocs-material from 9.6.16 to 9.6.17 ([#7188](https://github.com/aws-powertools/powertools-lambda-python/issues/7188)) +* **deps:** bump fastjsonschema from 2.21.1 to 2.21.2 ([#7179](https://github.com/aws-powertools/powertools-lambda-python/issues/7179)) +* **deps:** bump aws-actions/configure-aws-credentials from 209f2a4450bb4b277e1dedaff40ad2fd8d4d0a4c to 3821430d177f66b128b701e38ba67c5319b1b0bd ([#7202](https://github.com/aws-powertools/powertools-lambda-python/issues/7202)) +* **deps:** bump aws-actions/configure-aws-credentials from 1b2b73eb6a459c3a91fde76ba4c255e5b4b8e94e to a03048d87541d1d9fcf2ecf528a4a65ba9bd7838 ([#7307](https://github.com/aws-powertools/powertools-lambda-python/issues/7307)) +* **deps:** bump mkdocstrings-python from 1.16.12 to 1.17.0 ([#7206](https://github.com/aws-powertools/powertools-lambda-python/issues/7206)) +* **deps:** bump actions/dependency-review-action from 4.7.1 to 4.7.2 ([#7203](https://github.com/aws-powertools/powertools-lambda-python/issues/7203)) +* **deps:** bump mkdocs-material from 9.6.17 to 9.6.18 ([#7237](https://github.com/aws-powertools/powertools-lambda-python/issues/7237)) +* **deps:** bump protobuf from 6.31.1 to 6.32.0 ([#7208](https://github.com/aws-powertools/powertools-lambda-python/issues/7208)) +* **deps:** bump pypa/gh-action-pypi-publish from 1.12.4 to 1.13.0 ([#7322](https://github.com/aws-powertools/powertools-lambda-python/issues/7322)) +* **deps:** bump aws-actions/configure-aws-credentials from 3821430d177f66b128b701e38ba67c5319b1b0bd to 09a74e37ceda446282c61f1496cdca8d8dca0e57 ([#7213](https://github.com/aws-powertools/powertools-lambda-python/issues/7213)) +* **deps:** bump squidfunk/mkdocs-material from `405aeb6` to `1a4e939` in /docs ([#7236](https://github.com/aws-powertools/powertools-lambda-python/issues/7236)) +* **deps:** bump mkdocs-material from 9.6.16 to 9.6.17 ([#7216](https://github.com/aws-powertools/powertools-lambda-python/issues/7216)) +* **deps:** bump mkdocs-material from 9.6.17 to 9.6.18 in /docs ([#7235](https://github.com/aws-powertools/powertools-lambda-python/issues/7235)) +* **deps:** bump aws-encryption-sdk from 4.0.2 to 4.0.3 ([#7305](https://github.com/aws-powertools/powertools-lambda-python/issues/7305)) +* **deps:** bump actions/setup-node from 4.4.0 to 5.0.0 ([#7319](https://github.com/aws-powertools/powertools-lambda-python/issues/7319)) +* **deps:** bump aws-actions/configure-aws-credentials from 09a74e37ceda446282c61f1496cdca8d8dca0e57 to 0eb446ecb2e3f0e1a19f106e12e76c6a98b6bdb2 ([#7222](https://github.com/aws-powertools/powertools-lambda-python/issues/7222)) +* **deps:** bump codecov/codecov-action from 5.4.3 to 5.5.0 ([#7221](https://github.com/aws-powertools/powertools-lambda-python/issues/7221)) +* **deps:** bump mkdocstrings-python from 1.16.12 to 1.17.0 in /docs ([#7187](https://github.com/aws-powertools/powertools-lambda-python/issues/7187)) +* **deps-dev:** bump boto3-stubs from 1.40.23 to 1.40.25 ([#7334](https://github.com/aws-powertools/powertools-lambda-python/issues/7334)) +* **deps-dev:** bump boto3-stubs from 1.40.14 to 1.40.15 ([#7230](https://github.com/aws-powertools/powertools-lambda-python/issues/7230)) +* **deps-dev:** bump boto3-stubs from 1.40.13 to 1.40.14 ([#7224](https://github.com/aws-powertools/powertools-lambda-python/issues/7224)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.211.0a0 to 2.212.0a0 ([#7223](https://github.com/aws-powertools/powertools-lambda-python/issues/7223)) +* **deps-dev:** bump ruff from 0.12.9 to 0.12.10 ([#7231](https://github.com/aws-powertools/powertools-lambda-python/issues/7231)) +* **deps-dev:** bump sentry-sdk from 2.35.2 to 2.37.0 ([#7335](https://github.com/aws-powertools/powertools-lambda-python/issues/7335)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.213.0a0 to 2.214.0a0 ([#7303](https://github.com/aws-powertools/powertools-lambda-python/issues/7303)) +* **deps-dev:** bump boto3-stubs from 1.40.12 to 1.40.13 ([#7214](https://github.com/aws-powertools/powertools-lambda-python/issues/7214)) +* **deps-dev:** bump requests from 2.32.4 to 2.32.5 ([#7215](https://github.com/aws-powertools/powertools-lambda-python/issues/7215)) +* **deps-dev:** bump types-protobuf from 6.30.2.20250809 to 6.30.2.20250822 ([#7241](https://github.com/aws-powertools/powertools-lambda-python/issues/7241)) +* **deps-dev:** bump pytest-mock from 3.14.1 to 3.15.0 ([#7333](https://github.com/aws-powertools/powertools-lambda-python/issues/7333)) +* **deps-dev:** bump types-python-dateutil from 2.9.0.20250809 to 2.9.0.20250822 ([#7238](https://github.com/aws-powertools/powertools-lambda-python/issues/7238)) +* **deps-dev:** bump cfn-lint from 1.38.3 to 1.39.0 ([#7212](https://github.com/aws-powertools/powertools-lambda-python/issues/7212)) +* **deps-dev:** bump pytest from 8.4.1 to 8.4.2 ([#7325](https://github.com/aws-powertools/powertools-lambda-python/issues/7325)) +* **deps-dev:** bump aws-cdk from 2.1025.0 to 2.1026.0 ([#7239](https://github.com/aws-powertools/powertools-lambda-python/issues/7239)) +* **deps-dev:** bump coverage from 7.10.3 to 7.10.4 ([#7205](https://github.com/aws-powertools/powertools-lambda-python/issues/7205)) +* **deps-dev:** bump boto3-stubs from 1.40.15 to 1.40.16 ([#7240](https://github.com/aws-powertools/powertools-lambda-python/issues/7240)) +* **deps-dev:** bump sentry-sdk from 2.35.1 to 2.35.2 ([#7297](https://github.com/aws-powertools/powertools-lambda-python/issues/7297)) +* **deps-dev:** bump boto3-stubs from 1.40.11 to 1.40.12 ([#7204](https://github.com/aws-powertools/powertools-lambda-python/issues/7204)) +* **deps-dev:** bump aws-cdk-lib from 2.213.0 to 2.214.0 ([#7302](https://github.com/aws-powertools/powertools-lambda-python/issues/7302)) +* **deps-dev:** bump aws-cdk from 2.1028.0 to 2.1029.0 ([#7332](https://github.com/aws-powertools/powertools-lambda-python/issues/7332)) +* **deps-dev:** bump filelock from 3.18.0 to 3.19.1 ([#7177](https://github.com/aws-powertools/powertools-lambda-python/issues/7177)) +* **deps-dev:** bump boto3-stubs from 1.40.9 to 1.40.11 ([#7189](https://github.com/aws-powertools/powertools-lambda-python/issues/7189)) +* **deps-dev:** bump ruff from 0.12.11 to 0.12.12 ([#7324](https://github.com/aws-powertools/powertools-lambda-python/issues/7324)) +* **deps-dev:** bump coverage from 7.10.4 to 7.10.5 ([#7249](https://github.com/aws-powertools/powertools-lambda-python/issues/7249)) +* **deps-dev:** bump aws-cdk from 2.1024.0 to 2.1025.0 ([#7167](https://github.com/aws-powertools/powertools-lambda-python/issues/7167)) +* **deps-dev:** bump sentry-sdk from 2.34.1 to 2.35.0 ([#7181](https://github.com/aws-powertools/powertools-lambda-python/issues/7181)) +* **deps-dev:** bump boto3-stubs from 1.40.16 to 1.40.17 ([#7250](https://github.com/aws-powertools/powertools-lambda-python/issues/7250)) +* **deps-dev:** bump aws-cdk from 2.1027.0 to 2.1028.0 ([#7308](https://github.com/aws-powertools/powertools-lambda-python/issues/7308)) +* **deps-dev:** bump sentry-sdk from 2.35.0 to 2.35.1 ([#7258](https://github.com/aws-powertools/powertools-lambda-python/issues/7258)) +* **deps-dev:** bump cfn-lint from 1.39.0 to 1.39.1 ([#7259](https://github.com/aws-powertools/powertools-lambda-python/issues/7259)) +* **deps-dev:** bump ruff from 0.12.8 to 0.12.9 ([#7182](https://github.com/aws-powertools/powertools-lambda-python/issues/7182)) +* **deps-dev:** bump boto3-stubs from 1.40.21 to 1.40.23 ([#7309](https://github.com/aws-powertools/powertools-lambda-python/issues/7309)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.210.0a0 to 2.211.0a0 ([#7168](https://github.com/aws-powertools/powertools-lambda-python/issues/7168)) +* **deps-dev:** bump cfn-lint from 1.38.2 to 1.38.3 ([#7169](https://github.com/aws-powertools/powertools-lambda-python/issues/7169)) +* **deps-dev:** bump aws-cdk from 2.1026.0 to 2.1027.0 ([#7265](https://github.com/aws-powertools/powertools-lambda-python/issues/7265)) +* **deps-dev:** bump coverage from 7.10.5 to 7.10.6 ([#7282](https://github.com/aws-powertools/powertools-lambda-python/issues/7282)) +* **deps-dev:** bump pytest-cov from 6.2.1 to 6.3.0 ([#7349](https://github.com/aws-powertools/powertools-lambda-python/issues/7349)) +* **deps-dev:** bump boto3-stubs from 1.40.8 to 1.40.9 ([#7170](https://github.com/aws-powertools/powertools-lambda-python/issues/7170)) +* **deps-dev:** bump boto3-stubs from 1.40.25 to 1.40.26 ([#7348](https://github.com/aws-powertools/powertools-lambda-python/issues/7348)) +* **deps-dev:** bump boto3-stubs from 1.40.18 to 1.40.19 ([#7267](https://github.com/aws-powertools/powertools-lambda-python/issues/7267)) +* **deps-dev:** bump boto3-stubs from 1.40.7 to 1.40.8 ([#7159](https://github.com/aws-powertools/powertools-lambda-python/issues/7159)) +* **deps-dev:** bump boto3-stubs from 1.40.19 to 1.40.21 ([#7283](https://github.com/aws-powertools/powertools-lambda-python/issues/7283)) +* **deps-dev:** bump ruff from 0.12.10 to 0.12.11 ([#7275](https://github.com/aws-powertools/powertools-lambda-python/issues/7275)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.212.0a0 to 2.213.0a0 ([#7274](https://github.com/aws-powertools/powertools-lambda-python/issues/7274)) +* **deps-dev:** bump boto3-stubs from 1.40.17 to 1.40.18 ([#7264](https://github.com/aws-powertools/powertools-lambda-python/issues/7264)) + + + +## [v3.19.0] - 2025-08-12 +## Bug Fixes + +* **event_handler:** split OpenAPI validation to respect middleware returns ([#7050](https://github.com/aws-powertools/powertools-lambda-python/issues/7050)) +* **parameters:** fix _transform_and_cache_get_parameters_response ([#7083](https://github.com/aws-powertools/powertools-lambda-python/issues/7083)) + +## Code Refactoring + +* **parser:** Improve ALB models with examples and descriptions ([#7100](https://github.com/aws-powertools/powertools-lambda-python/issues/7100)) +* **parser:** Improve Kinesis models with examples and descriptions ([#7092](https://github.com/aws-powertools/powertools-lambda-python/issues/7092)) +* **parser:** Improve EventBridge models with examples and descriptions ([#7090](https://github.com/aws-powertools/powertools-lambda-python/issues/7090)) + +## Documentation + +* **event_handler:** improve routing rules syntax documentation ([#7094](https://github.com/aws-powertools/powertools-lambda-python/issues/7094)) +* **logger:** fix typo in sampling examples ([#7133](https://github.com/aws-powertools/powertools-lambda-python/issues/7133)) +* **maintainers:** improve release process documentation ([#7088](https://github.com/aws-powertools/powertools-lambda-python/issues/7088)) + +## Features + +* **parameters:** add support for retrieving batch of secrets ([#7058](https://github.com/aws-powertools/powertools-lambda-python/issues/7058)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.18.1a6 ([#7134](https://github.com/aws-powertools/powertools-lambda-python/issues/7134)) +* **ci:** new pre-release 3.18.1a5 ([#7114](https://github.com/aws-powertools/powertools-lambda-python/issues/7114)) +* **ci:** new pre-release 3.18.1a1 ([#7077](https://github.com/aws-powertools/powertools-lambda-python/issues/7077)) +* **ci:** new pre-release 3.18.1a0 ([#7068](https://github.com/aws-powertools/powertools-lambda-python/issues/7068)) +* **ci:** new pre-release 3.18.1a9 ([#7155](https://github.com/aws-powertools/powertools-lambda-python/issues/7155)) +* **ci:** new pre-release 3.18.1a8 ([#7147](https://github.com/aws-powertools/powertools-lambda-python/issues/7147)) +* **ci:** new pre-release 3.18.1a3 ([#7097](https://github.com/aws-powertools/powertools-lambda-python/issues/7097)) +* **ci:** new pre-release 3.18.1a7 ([#7141](https://github.com/aws-powertools/powertools-lambda-python/issues/7141)) +* **ci:** new pre-release 3.18.1a2 ([#7085](https://github.com/aws-powertools/powertools-lambda-python/issues/7085)) +* **ci:** new pre-release 3.18.1a4 ([#7105](https://github.com/aws-powertools/powertools-lambda-python/issues/7105)) +* **deps:** bump mkdocs-llmstxt from 0.3.0 to 0.3.1 ([#7112](https://github.com/aws-powertools/powertools-lambda-python/issues/7112)) +* **deps:** bump squidfunk/mkdocs-material from `0bfdba4` to `bb7b015` in /docs ([#7059](https://github.com/aws-powertools/powertools-lambda-python/issues/7059)) +* **deps:** bump redis from 6.3.0 to 6.4.0 ([#7140](https://github.com/aws-powertools/powertools-lambda-python/issues/7140)) +* **deps:** bump actions/checkout from 4.2.2 to 5.0.0 ([#7154](https://github.com/aws-powertools/powertools-lambda-python/issues/7154)) +* **deps:** bump aws-powertools/actions from 1.3.0 to 1.4.0 ([#7104](https://github.com/aws-powertools/powertools-lambda-python/issues/7104)) +* **deps:** bump actions/download-artifact from 4.3.0 to 5.0.0 ([#7126](https://github.com/aws-powertools/powertools-lambda-python/issues/7126)) +* **deps:** bump aws-powertools/actions from 1.1.0 to 1.3.0 ([#7061](https://github.com/aws-powertools/powertools-lambda-python/issues/7061)) +* **deps:** bump aws-actions/configure-aws-credentials from 4.2.1 to 4.3.0 ([#7103](https://github.com/aws-powertools/powertools-lambda-python/issues/7103)) +* **deps:** bump aws-actions/configure-aws-credentials from 59b441846ad109fa4a1549b73ef4e149c4bfb53b to aa1f74b81b53cb3adb28afcdb21d7b9f3fceea98 ([#7113](https://github.com/aws-powertools/powertools-lambda-python/issues/7113)) +* **deps:** bump redis from 6.2.0 to 6.3.0 ([#7108](https://github.com/aws-powertools/powertools-lambda-python/issues/7108)) +* **deps:** bump mkdocs-material from 9.6.15 to 9.6.16 in /docs ([#7060](https://github.com/aws-powertools/powertools-lambda-python/issues/7060)) +* **deps:** bump mkdocs-llmstxt from 0.3.0 to 0.3.1 ([#7130](https://github.com/aws-powertools/powertools-lambda-python/issues/7130)) +* **deps:** bump mkdocs-material from 9.6.15 to 9.6.16 ([#7065](https://github.com/aws-powertools/powertools-lambda-python/issues/7065)) +* **deps-dev:** bump boto3-stubs from 1.40.2 to 1.40.3 ([#7111](https://github.com/aws-powertools/powertools-lambda-python/issues/7111)) +* **deps-dev:** bump cfn-lint from 1.38.1 to 1.38.2 ([#7109](https://github.com/aws-powertools/powertools-lambda-python/issues/7109)) +* **deps-dev:** bump boto3-stubs from 1.40.1 to 1.40.2 ([#7102](https://github.com/aws-powertools/powertools-lambda-python/issues/7102)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.208.0a0 to 2.210.0a0 ([#7127](https://github.com/aws-powertools/powertools-lambda-python/issues/7127)) +* **deps-dev:** bump boto3-stubs from 1.40.0 to 1.40.1 ([#7093](https://github.com/aws-powertools/powertools-lambda-python/issues/7093)) +* **deps-dev:** bump aws-cdk from 2.1023.0 to 2.1024.0 ([#7125](https://github.com/aws-powertools/powertools-lambda-python/issues/7125)) +* **deps-dev:** bump boto3-stubs from 1.40.3 to 1.40.4 ([#7128](https://github.com/aws-powertools/powertools-lambda-python/issues/7128)) +* **deps-dev:** bump boto3-stubs from 1.40.6 to 1.40.7 ([#7153](https://github.com/aws-powertools/powertools-lambda-python/issues/7153)) +* **deps-dev:** bump mypy from 1.17.0 to 1.17.1 ([#7081](https://github.com/aws-powertools/powertools-lambda-python/issues/7081)) +* **deps-dev:** bump cfn-lint from 1.38.0 to 1.38.1 ([#7080](https://github.com/aws-powertools/powertools-lambda-python/issues/7080)) +* **deps-dev:** bump mypy-boto3-appconfigdata from 1.39.0 to 1.40.0 in the boto-typing group ([#7079](https://github.com/aws-powertools/powertools-lambda-python/issues/7079)) +* **deps-dev:** bump ruff from 0.12.7 to 0.12.8 ([#7138](https://github.com/aws-powertools/powertools-lambda-python/issues/7138)) +* **deps-dev:** bump boto3-stubs from 1.40.4 to 1.40.5 ([#7139](https://github.com/aws-powertools/powertools-lambda-python/issues/7139)) +* **deps-dev:** bump sentry-sdk from 2.34.0 to 2.34.1 ([#7075](https://github.com/aws-powertools/powertools-lambda-python/issues/7075)) +* **deps-dev:** bump ruff from 0.12.5 to 0.12.7 ([#7073](https://github.com/aws-powertools/powertools-lambda-python/issues/7073)) +* **deps-dev:** bump boto3-stubs from 1.39.16 to 1.39.17 ([#7072](https://github.com/aws-powertools/powertools-lambda-python/issues/7072)) +* **deps-dev:** bump boto3-stubs from 1.40.5 to 1.40.6 ([#7143](https://github.com/aws-powertools/powertools-lambda-python/issues/7143)) +* **deps-dev:** bump types-protobuf from 6.30.2.20250703 to 6.30.2.20250809 ([#7150](https://github.com/aws-powertools/powertools-lambda-python/issues/7150)) +* **deps-dev:** bump aws-cdk from 2.1022.0 to 2.1023.0 ([#7067](https://github.com/aws-powertools/powertools-lambda-python/issues/7067)) +* **deps-dev:** bump coverage from 7.10.2 to 7.10.3 ([#7152](https://github.com/aws-powertools/powertools-lambda-python/issues/7152)) +* **deps-dev:** bump sentry-sdk from 2.33.2 to 2.34.0 ([#7064](https://github.com/aws-powertools/powertools-lambda-python/issues/7064)) +* **deps-dev:** bump boto3-stubs from 1.39.14 to 1.39.16 ([#7066](https://github.com/aws-powertools/powertools-lambda-python/issues/7066)) +* **deps-dev:** bump coverage from 7.10.0 to 7.10.1 ([#7063](https://github.com/aws-powertools/powertools-lambda-python/issues/7063)) +* **deps-dev:** bump types-python-dateutil from 2.9.0.20250708 to 2.9.0.20250809 ([#7151](https://github.com/aws-powertools/powertools-lambda-python/issues/7151)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.207.0a0 to 2.208.0a0 ([#7062](https://github.com/aws-powertools/powertools-lambda-python/issues/7062)) +* **deps-dev:** bump coverage from 7.10.1 to 7.10.2 ([#7107](https://github.com/aws-powertools/powertools-lambda-python/issues/7107)) +* **git:** add LLM tools to .gitignore file ([#7137](https://github.com/aws-powertools/powertools-lambda-python/issues/7137)) + + + +## [v3.17.1] - 2025-07-29 +## Maintenance + + + + +## [v3.18.0] - 2025-07-29 +## Documentation + +* **event_handler:** add section about Pydantic serialization ([#7049](https://github.com/aws-powertools/powertools-lambda-python/issues/7049)) +* **event_handler:** enhance documentation on query/header parameters behavior ([#7000](https://github.com/aws-powertools/powertools-lambda-python/issues/7000)) +* **parameters:** fix typo in transform auto instruction ([#7017](https://github.com/aws-powertools/powertools-lambda-python/issues/7017)) +* **parser:** fix a a typo in SqsEnvelope ([#7052](https://github.com/aws-powertools/powertools-lambda-python/issues/7052)) + +## Features + +* **event_handler:** add support for form data in OpenAPI utility ([#7028](https://github.com/aws-powertools/powertools-lambda-python/issues/7028)) +* **metrics:** add runtime validations for the metric name ([#7026](https://github.com/aws-powertools/powertools-lambda-python/issues/7026)) +* **parser:** add AppSync Events models ([#6999](https://github.com/aws-powertools/powertools-lambda-python/issues/6999)) +* **typing:** add tenant_id property ([#6985](https://github.com/aws-powertools/powertools-lambda-python/issues/6985)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.17.1a4 ([#7018](https://github.com/aws-powertools/powertools-lambda-python/issues/7018)) +* **ci:** improve feature flags UA ([#7051](https://github.com/aws-powertools/powertools-lambda-python/issues/7051)) +* **ci:** new pre-release 3.17.1a8 ([#7045](https://github.com/aws-powertools/powertools-lambda-python/issues/7045)) +* **ci:** new pre-release 3.17.1a9 ([#7054](https://github.com/aws-powertools/powertools-lambda-python/issues/7054)) +* **ci:** new pre-release 3.17.1a7 ([#7038](https://github.com/aws-powertools/powertools-lambda-python/issues/7038)) +* **ci:** new pre-release 3.17.1a0 ([#6989](https://github.com/aws-powertools/powertools-lambda-python/issues/6989)) +* **ci:** new pre-release 3.17.1a1 ([#6997](https://github.com/aws-powertools/powertools-lambda-python/issues/6997)) +* **ci:** add a new workflow to build docs in new PRs ([#7002](https://github.com/aws-powertools/powertools-lambda-python/issues/7002)) +* **ci:** new pre-release 3.17.1a2 ([#7008](https://github.com/aws-powertools/powertools-lambda-python/issues/7008)) +* **ci:** new pre-release 3.17.1a6 ([#7029](https://github.com/aws-powertools/powertools-lambda-python/issues/7029)) +* **ci:** new pre-release 3.17.1a3 ([#7014](https://github.com/aws-powertools/powertools-lambda-python/issues/7014)) +* **ci:** new pre-release 3.17.1a5 ([#7025](https://github.com/aws-powertools/powertools-lambda-python/issues/7025)) +* **ci:** remove closed-issue and opened-pr workflows ([#7047](https://github.com/aws-powertools/powertools-lambda-python/issues/7047)) +* **deps:** update aws-powertools/actions requirement to 5ae7c190d6b51491bb14f593c3509c1bcbf7a3c1 ([#7034](https://github.com/aws-powertools/powertools-lambda-python/issues/7034)) +* **deps-dev:** bump pytest-asyncio from 0.26.0 to 1.1.0 ([#6995](https://github.com/aws-powertools/powertools-lambda-python/issues/6995)) +* **deps-dev:** bump boto3-stubs from 1.39.8 to 1.39.9 ([#7011](https://github.com/aws-powertools/powertools-lambda-python/issues/7011)) +* **deps-dev:** bump boto3-stubs from 1.39.11 to 1.39.12 ([#7027](https://github.com/aws-powertools/powertools-lambda-python/issues/7027)) +* **deps-dev:** bump testcontainers from 4.10.0 to 4.12.0 ([#7021](https://github.com/aws-powertools/powertools-lambda-python/issues/7021)) +* **deps-dev:** bump boto3-stubs from 1.39.7 to 1.39.8 ([#7006](https://github.com/aws-powertools/powertools-lambda-python/issues/7006)) +* **deps-dev:** bump ruff from 0.12.3 to 0.12.4 ([#7005](https://github.com/aws-powertools/powertools-lambda-python/issues/7005)) +* **deps-dev:** bump aws-cdk from 2.1021.0 to 2.1022.0 ([#7031](https://github.com/aws-powertools/powertools-lambda-python/issues/7031)) +* **deps-dev:** bump aws-cdk-lib from 2.206.0 to 2.207.0 ([#7035](https://github.com/aws-powertools/powertools-lambda-python/issues/7035)) +* **deps-dev:** bump coverage from 7.9.2 to 7.10.0 ([#7033](https://github.com/aws-powertools/powertools-lambda-python/issues/7033)) +* **deps-dev:** bump sentry-sdk from 2.33.0 to 2.33.2 ([#7023](https://github.com/aws-powertools/powertools-lambda-python/issues/7023)) +* **deps-dev:** bump aws-cdk-lib from 2.205.0 to 2.206.0 ([#6996](https://github.com/aws-powertools/powertools-lambda-python/issues/6996)) +* **deps-dev:** bump boto3-stubs from 1.39.4 to 1.39.7 ([#6994](https://github.com/aws-powertools/powertools-lambda-python/issues/6994)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.205.0a0 to 2.206.0a0 ([#6993](https://github.com/aws-powertools/powertools-lambda-python/issues/6993)) +* **deps-dev:** bump aws-cdk from 2.1020.2 to 2.1021.0 ([#6992](https://github.com/aws-powertools/powertools-lambda-python/issues/6992)) +* **deps-dev:** bump boto3-stubs from 1.39.12 to 1.39.13 ([#7036](https://github.com/aws-powertools/powertools-lambda-python/issues/7036)) +* **deps-dev:** bump ruff from 0.12.4 to 0.12.5 ([#7032](https://github.com/aws-powertools/powertools-lambda-python/issues/7032)) +* **deps-dev:** bump sentry-sdk from 2.32.0 to 2.33.0 ([#6988](https://github.com/aws-powertools/powertools-lambda-python/issues/6988)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.204.0a0 to 2.205.0a0 ([#6986](https://github.com/aws-powertools/powertools-lambda-python/issues/6986)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.206.0a0 to 2.207.0a0 ([#7037](https://github.com/aws-powertools/powertools-lambda-python/issues/7037)) +* **deps-dev:** bump boto3-stubs from 1.39.13 to 1.39.14 ([#7042](https://github.com/aws-powertools/powertools-lambda-python/issues/7042)) +* **deps-dev:** bump boto3-stubs from 1.39.9 to 1.39.11 ([#7022](https://github.com/aws-powertools/powertools-lambda-python/issues/7022)) + + + +## [v3.17.0] - 2025-07-15 +## Bug Fixes + +* enable response compression when using multi-value headers ([#6936](https://github.com/aws-powertools/powertools-lambda-python/issues/6936)) + +## Documentation + +* **event-handler:** remove Amplify transformers section ([#6937](https://github.com/aws-powertools/powertools-lambda-python/issues/6937)) +* **event_handler:** revert deleted file ([#6947](https://github.com/aws-powertools/powertools-lambda-python/issues/6947)) +* **roadmap:** update roadmap items ([#6955](https://github.com/aws-powertools/powertools-lambda-python/issues/6955)) +* **we_made_this:** add MCP server template ([#6851](https://github.com/aws-powertools/powertools-lambda-python/issues/6851)) + +## Features + +* **event_handler:** add support for externalDocs attribute in OpenAPI schema ([#6945](https://github.com/aws-powertools/powertools-lambda-python/issues/6945)) +* **parser:** Added Cognito trigger schemas ([#6737](https://github.com/aws-powertools/powertools-lambda-python/issues/6737)) + +## Maintenance + +* version bump +* **ci:** fix ssm workflow ([#6980](https://github.com/aws-powertools/powertools-lambda-python/issues/6980)) +* **ci:** new pre-release 3.16.1a7 ([#6974](https://github.com/aws-powertools/powertools-lambda-python/issues/6974)) +* **ci:** new pre-release 3.16.1a6 ([#6969](https://github.com/aws-powertools/powertools-lambda-python/issues/6969)) +* **ci:** new pre-release 3.16.1a5 ([#6967](https://github.com/aws-powertools/powertools-lambda-python/issues/6967)) +* **ci:** new pre-release 3.16.1a1 ([#6943](https://github.com/aws-powertools/powertools-lambda-python/issues/6943)) +* **ci:** new pre-release 3.16.1a4 ([#6964](https://github.com/aws-powertools/powertools-lambda-python/issues/6964)) +* **ci:** new pre-release 3.16.1a0 ([#6933](https://github.com/aws-powertools/powertools-lambda-python/issues/6933)) +* **ci:** new pre-release 3.16.1a2 ([#6954](https://github.com/aws-powertools/powertools-lambda-python/issues/6954)) +* **ci:** new pre-release 3.16.1a3 ([#6959](https://github.com/aws-powertools/powertools-lambda-python/issues/6959)) +* **deps:** bump aws-encryption-sdk from 4.0.1 to 4.0.2 ([#6932](https://github.com/aws-powertools/powertools-lambda-python/issues/6932)) +* **deps:** bump mkdocs-llmstxt from 0.2.0 to 0.3.0 in /docs ([#6979](https://github.com/aws-powertools/powertools-lambda-python/issues/6979)) +* **deps:** bump mkdocs-material from 9.6.14 to 9.6.15 ([#6931](https://github.com/aws-powertools/powertools-lambda-python/issues/6931)) +* **deps:** bump mkdocs-llmstxt from 0.2.0 to 0.3.0 ([#6978](https://github.com/aws-powertools/powertools-lambda-python/issues/6978)) +* **deps:** bump typing-extensions from 4.14.0 to 4.14.1 ([#6951](https://github.com/aws-powertools/powertools-lambda-python/issues/6951)) +* **deps-dev:** bump types-python-dateutil from 2.9.0.20250516 to 2.9.0.20250708 ([#6962](https://github.com/aws-powertools/powertools-lambda-python/issues/6962)) +* **deps-dev:** bump aws-cdk-lib from 2.203.1 to 2.204.0 ([#6949](https://github.com/aws-powertools/powertools-lambda-python/issues/6949)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.203.1a0 to 2.204.0a0 ([#6950](https://github.com/aws-powertools/powertools-lambda-python/issues/6950)) +* **deps-dev:** bump bandit from 1.8.5 to 1.8.6 ([#6957](https://github.com/aws-powertools/powertools-lambda-python/issues/6957)) +* **deps-dev:** bump boto3-stubs from 1.39.2 to 1.39.3 ([#6940](https://github.com/aws-powertools/powertools-lambda-python/issues/6940)) +* **deps-dev:** bump coverage from 7.9.1 to 7.9.2 ([#6941](https://github.com/aws-powertools/powertools-lambda-python/issues/6941)) +* **deps-dev:** bump aws-cdk from 2.1020.1 to 2.1020.2 ([#6942](https://github.com/aws-powertools/powertools-lambda-python/issues/6942)) +* **deps-dev:** bump ruff from 0.12.1 to 0.12.2 ([#6938](https://github.com/aws-powertools/powertools-lambda-python/issues/6938)) +* **deps-dev:** bump types-protobuf from 6.30.2.20250516 to 6.30.2.20250703 ([#6939](https://github.com/aws-powertools/powertools-lambda-python/issues/6939)) +* **deps-dev:** bump cfn-lint from 1.37.0 to 1.37.1 ([#6958](https://github.com/aws-powertools/powertools-lambda-python/issues/6958)) +* **deps-dev:** bump cfn-lint from 1.37.1 to 1.37.2 ([#6963](https://github.com/aws-powertools/powertools-lambda-python/issues/6963)) +* **deps-dev:** bump boto3-stubs from 1.39.3 to 1.39.4 ([#6966](https://github.com/aws-powertools/powertools-lambda-python/issues/6966)) +* **deps-dev:** bump ruff from 0.12.2 to 0.12.3 ([#6971](https://github.com/aws-powertools/powertools-lambda-python/issues/6971)) +* **deps-dev:** bump mypy from 1.16.1 to 1.17.0 ([#6977](https://github.com/aws-powertools/powertools-lambda-python/issues/6977)) +* **deps-dev:** bump aws-cdk-lib from 2.203.0 to 2.203.1 ([#6928](https://github.com/aws-powertools/powertools-lambda-python/issues/6928)) +* **deps-dev:** bump boto3-stubs from 1.39.1 to 1.39.2 ([#6930](https://github.com/aws-powertools/powertools-lambda-python/issues/6930)) +* **deps-dev:** bump aws-cdk from 2.1020.0 to 2.1020.1 ([#6927](https://github.com/aws-powertools/powertools-lambda-python/issues/6927)) +* **deps-dev:** bump cfn-lint from 1.37.2 to 1.38.0 ([#6976](https://github.com/aws-powertools/powertools-lambda-python/issues/6976)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.203.0a0 to 2.203.1a0 ([#6929](https://github.com/aws-powertools/powertools-lambda-python/issues/6929)) + + + +## [v3.16.0] - 2025-07-02 +## Bug Fixes + +* **event_source:** fix decode headers with signed bytes ([#6878](https://github.com/aws-powertools/powertools-lambda-python/issues/6878)) +* **logger:** caplog working with parent Logger ([#6847](https://github.com/aws-powertools/powertools-lambda-python/issues/6847)) +* **logger:** fix exception on flush without buffer ([#6794](https://github.com/aws-powertools/powertools-lambda-python/issues/6794)) + +## Documentation + +* **kafka:** refactor kafka documentation ([#6854](https://github.com/aws-powertools/powertools-lambda-python/issues/6854)) + +## Features + +* **ci:** Partition workflow scripts updated to work ([#6900](https://github.com/aws-powertools/powertools-lambda-python/issues/6900)) +* **ci:** Deploy to AWS China partitions ([#6867](https://github.com/aws-powertools/powertools-lambda-python/issues/6867)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.15.2a6 ([#6923](https://github.com/aws-powertools/powertools-lambda-python/issues/6923)) +* **ci:** incorporate SSM update workflow into release pipeline ([#6914](https://github.com/aws-powertools/powertools-lambda-python/issues/6914)) +* **ci:** new pre-release 3.15.2a3 ([#6876](https://github.com/aws-powertools/powertools-lambda-python/issues/6876)) +* **ci:** new pre-release 3.15.2a2 ([#6865](https://github.com/aws-powertools/powertools-lambda-python/issues/6865)) +* **ci:** new pre-release 3.15.2a5 ([#6910](https://github.com/aws-powertools/powertools-lambda-python/issues/6910)) +* **ci:** new pre-release 3.15.2a4 ([#6881](https://github.com/aws-powertools/powertools-lambda-python/issues/6881)) +* **ci:** pinning versions in actions + refactor tests ([#6904](https://github.com/aws-powertools/powertools-lambda-python/issues/6904)) +* **ci:** stop publishing to gh-pages branch ([#6903](https://github.com/aws-powertools/powertools-lambda-python/issues/6903)) +* **ci:** stop publishing to gh-pages branch ([#6902](https://github.com/aws-powertools/powertools-lambda-python/issues/6902)) +* **ci:** stop publishing to gh-pages branch ([#6901](https://github.com/aws-powertools/powertools-lambda-python/issues/6901)) +* **ci:** stop publishing to gh-pages branch ([#6899](https://github.com/aws-powertools/powertools-lambda-python/issues/6899)) +* **ci:** remove v2 deployment files ([#6895](https://github.com/aws-powertools/powertools-lambda-python/issues/6895)) +* **ci:** bump urllib3 version ([#6893](https://github.com/aws-powertools/powertools-lambda-python/issues/6893)) +* **ci:** new pre-release 3.15.2a0 ([#6852](https://github.com/aws-powertools/powertools-lambda-python/issues/6852)) +* **ci:** refactor thread safe tests ([#6889](https://github.com/aws-powertools/powertools-lambda-python/issues/6889)) +* **ci:** new pre-release 3.15.2a1 ([#6860](https://github.com/aws-powertools/powertools-lambda-python/issues/6860)) +* **ci:** fix command to replace layer number ([#6868](https://github.com/aws-powertools/powertools-lambda-python/issues/6868)) +* **deps:** bump docker/setup-buildx-action from 3.10.0 to 3.11.1 ([#6823](https://github.com/aws-powertools/powertools-lambda-python/issues/6823)) +* **deps:** bump pydantic from 2.11.5 to 2.11.7 ([#6844](https://github.com/aws-powertools/powertools-lambda-python/issues/6844)) +* **deps:** bump mkdocs-material from 9.6.14 to 9.6.15 in /docs ([#6920](https://github.com/aws-powertools/powertools-lambda-python/issues/6920)) +* **deps:** bump redis from 5.3.0 to 6.2.0 ([#6827](https://github.com/aws-powertools/powertools-lambda-python/issues/6827)) +* **deps:** bump pydantic-settings from 2.9.1 to 2.10.1 ([#6872](https://github.com/aws-powertools/powertools-lambda-python/issues/6872)) +* **deps:** bump datadog-lambda from 6.110.0 to 6.111.0 ([#6857](https://github.com/aws-powertools/powertools-lambda-python/issues/6857)) +* **deps:** bump valkey-glide from 1.3.5 to 2.0.1 ([#6871](https://github.com/aws-powertools/powertools-lambda-python/issues/6871)) +* **deps:** bump squidfunk/mkdocs-material from `eb04b60` to `0bfdba4` in /docs ([#6915](https://github.com/aws-powertools/powertools-lambda-python/issues/6915)) +* **deps:** bump mkdocs-material from 9.5.50 to 9.6.14 in /docs ([#6908](https://github.com/aws-powertools/powertools-lambda-python/issues/6908)) +* **deps-dev:** bump sentry-sdk from 2.29.1 to 2.31.0 ([#6870](https://github.com/aws-powertools/powertools-lambda-python/issues/6870)) +* **deps-dev:** bump aws-cdk from 2.1019.1 to 2.1019.2 ([#6875](https://github.com/aws-powertools/powertools-lambda-python/issues/6875)) +* **deps-dev:** bump pytest from 8.4.0 to 8.4.1 ([#6874](https://github.com/aws-powertools/powertools-lambda-python/issues/6874)) +* **deps-dev:** bump cfn-lint from 1.35.4 to 1.36.1 ([#6855](https://github.com/aws-powertools/powertools-lambda-python/issues/6855)) +* **deps-dev:** bump boto3-stubs from 1.38.42 to 1.38.43 ([#6864](https://github.com/aws-powertools/powertools-lambda-python/issues/6864)) +* **deps-dev:** bump bandit from 1.8.3 to 1.8.5 ([#6856](https://github.com/aws-powertools/powertools-lambda-python/issues/6856)) +* **deps-dev:** bump boto3-stubs from 1.38.43 to 1.38.44 ([#6873](https://github.com/aws-powertools/powertools-lambda-python/issues/6873)) +* **deps-dev:** bump aws-cdk from 2.1018.1 to 2.1019.1 ([#6837](https://github.com/aws-powertools/powertools-lambda-python/issues/6837)) +* **deps-dev:** bump boto3-stubs from 1.38.41 to 1.38.42 ([#6858](https://github.com/aws-powertools/powertools-lambda-python/issues/6858)) +* **deps-dev:** bump boto3-stubs from 1.38.45 to 1.38.46 ([#6884](https://github.com/aws-powertools/powertools-lambda-python/issues/6884)) +* **deps-dev:** bump sentry-sdk from 2.31.0 to 2.32.0 ([#6885](https://github.com/aws-powertools/powertools-lambda-python/issues/6885)) +* **deps-dev:** bump ruff from 0.11.8 to 0.12.1 ([#6879](https://github.com/aws-powertools/powertools-lambda-python/issues/6879)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.202.0a0 to 2.203.0a0 ([#6919](https://github.com/aws-powertools/powertools-lambda-python/issues/6919)) +* **deps-dev:** bump mypy-boto3-appconfigdata from 1.38.0 to 1.39.0 in the boto-typing group ([#6905](https://github.com/aws-powertools/powertools-lambda-python/issues/6905)) +* **deps-dev:** bump pytest-xdist from 3.7.0 to 3.8.0 ([#6921](https://github.com/aws-powertools/powertools-lambda-python/issues/6921)) +* **deps-dev:** bump mypy from 1.16.0 to 1.16.1 ([#6828](https://github.com/aws-powertools/powertools-lambda-python/issues/6828)) +* **deps-dev:** bump boto3-stubs from 1.39.0 to 1.39.1 ([#6916](https://github.com/aws-powertools/powertools-lambda-python/issues/6916)) +* **deps-dev:** bump boto3-stubs from 1.38.34 to 1.38.41 ([#6845](https://github.com/aws-powertools/powertools-lambda-python/issues/6845)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.200.1a0 to 2.202.0a0 ([#6846](https://github.com/aws-powertools/powertools-lambda-python/issues/6846)) +* **deps-dev:** bump aws-cdk from 2.1019.2 to 2.1020.0 ([#6917](https://github.com/aws-powertools/powertools-lambda-python/issues/6917)) +* **deps-dev:** bump cfn-lint from 1.36.1 to 1.37.0 ([#6918](https://github.com/aws-powertools/powertools-lambda-python/issues/6918)) +* **deps-dev:** bump boto3-stubs from 1.38.44 to 1.38.45 ([#6880](https://github.com/aws-powertools/powertools-lambda-python/issues/6880)) + + + +## [v3.15.1] - 2025-06-20 +## Features + +* **kafka:** add logic to handle protobuf deserialization ([#6841](https://github.com/aws-powertools/powertools-lambda-python/issues/6841)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.15.1a0 ([#6839](https://github.com/aws-powertools/powertools-lambda-python/issues/6839)) + + + +## [v3.15.0] - 2025-06-19 +## Bug Fixes + +* **bedrock_agent:** fix querystring field resolution ([#6777](https://github.com/aws-powertools/powertools-lambda-python/issues/6777)) + +## Documentation + +* **kafka:** add kafka documentation ([#6834](https://github.com/aws-powertools/powertools-lambda-python/issues/6834)) +* **public_reference:** add Instil as a public reference ([#6763](https://github.com/aws-powertools/powertools-lambda-python/issues/6763)) + +## Features + +* **kafka:** add support for Confluence Producers ([#6833](https://github.com/aws-powertools/powertools-lambda-python/issues/6833)) +* **kafka:** New Kafka utility ([#6821](https://github.com/aws-powertools/powertools-lambda-python/issues/6821)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.14.1a6 ([#6830](https://github.com/aws-powertools/powertools-lambda-python/issues/6830)) +* **ci:** new pre-release 3.14.1a5 ([#6820](https://github.com/aws-powertools/powertools-lambda-python/issues/6820)) +* **ci:** new pre-release 3.14.1a0 ([#6773](https://github.com/aws-powertools/powertools-lambda-python/issues/6773)) +* **ci:** new pre-release 3.14.1a4 ([#6812](https://github.com/aws-powertools/powertools-lambda-python/issues/6812)) +* **ci:** new pre-release 3.14.1a3 ([#6797](https://github.com/aws-powertools/powertools-lambda-python/issues/6797)) +* **ci:** new pre-release 3.14.1a1 ([#6778](https://github.com/aws-powertools/powertools-lambda-python/issues/6778)) +* **ci:** new pre-release 3.14.1a2 ([#6788](https://github.com/aws-powertools/powertools-lambda-python/issues/6788)) +* **deps:** bump mkdocstrings-python from 1.16.11 to 1.16.12 in /docs ([#6768](https://github.com/aws-powertools/powertools-lambda-python/issues/6768)) +* **deps:** bump mkdocstrings-python from 1.16.11 to 1.16.12 ([#6765](https://github.com/aws-powertools/powertools-lambda-python/issues/6765)) +* **deps:** bump protobuf from 6.31.0 to 6.31.1 ([#6815](https://github.com/aws-powertools/powertools-lambda-python/issues/6815)) +* **deps-dev:** bump boto3-stubs from 1.38.29 to 1.38.30 ([#6772](https://github.com/aws-powertools/powertools-lambda-python/issues/6772)) +* **deps-dev:** bump aws-cdk from 2.1017.1 to 2.1018.0 ([#6775](https://github.com/aws-powertools/powertools-lambda-python/issues/6775)) +* **deps-dev:** bump boto3-stubs from 1.38.33 to 1.38.35 ([#6796](https://github.com/aws-powertools/powertools-lambda-python/issues/6796)) +* **deps-dev:** bump aws-cdk from 2.1018.0 to 2.1018.1 ([#6803](https://github.com/aws-powertools/powertools-lambda-python/issues/6803)) +* **deps-dev:** bump boto3-stubs from 1.38.30 to 1.38.31 ([#6776](https://github.com/aws-powertools/powertools-lambda-python/issues/6776)) +* **deps-dev:** bump requests from 2.32.3 to 2.32.4 ([#6789](https://github.com/aws-powertools/powertools-lambda-python/issues/6789)) +* **deps-dev:** bump boto3-stubs from 1.38.28 to 1.38.29 ([#6764](https://github.com/aws-powertools/powertools-lambda-python/issues/6764)) +* **deps-dev:** bump ruff from 0.11.12 to 0.11.13 ([#6780](https://github.com/aws-powertools/powertools-lambda-python/issues/6780)) +* **deps-dev:** bump boto3-stubs from 1.38.31 to 1.38.33 ([#6786](https://github.com/aws-powertools/powertools-lambda-python/issues/6786)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.200.0a0 to 2.200.1a0 ([#6766](https://github.com/aws-powertools/powertools-lambda-python/issues/6766)) +* **deps-dev:** bump aws-cdk-lib from 2.200.0 to 2.200.1 ([#6767](https://github.com/aws-powertools/powertools-lambda-python/issues/6767)) +* **deps-dev:** bump pytest-cov from 6.1.1 to 6.2.1 ([#6800](https://github.com/aws-powertools/powertools-lambda-python/issues/6800)) +* **deps-dev:** bump requests from 2.32.3 to 2.32.4 ([#6787](https://github.com/aws-powertools/powertools-lambda-python/issues/6787)) + + + +## [v3.14.0] - 2025-06-03 +## Bug Fixes + +* **event_handler:** fix OpenAPI schema response for disabled validation ([#6720](https://github.com/aws-powertools/powertools-lambda-python/issues/6720)) + +## Features + +* **bedrock_agent:** add new Amazon Bedrock Agents Functions Resolver ([#6564](https://github.com/aws-powertools/powertools-lambda-python/issues/6564)) +* **event_handler:** enable support for custom deserializer to parse the request body ([#6601](https://github.com/aws-powertools/powertools-lambda-python/issues/6601)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.13.1a7 ([#6759](https://github.com/aws-powertools/powertools-lambda-python/issues/6759)) +* **ci:** new pre-release 3.13.1a0 ([#6696](https://github.com/aws-powertools/powertools-lambda-python/issues/6696)) +* **ci:** new pre-release 3.13.1a6 ([#6751](https://github.com/aws-powertools/powertools-lambda-python/issues/6751)) +* **ci:** new pre-release 3.13.1a1 ([#6704](https://github.com/aws-powertools/powertools-lambda-python/issues/6704)) +* **ci:** new pre-release 3.13.1a2 ([#6709](https://github.com/aws-powertools/powertools-lambda-python/issues/6709)) +* **ci:** new pre-release 3.13.1a5 ([#6744](https://github.com/aws-powertools/powertools-lambda-python/issues/6744)) +* **ci:** new pre-release 3.13.1a4 ([#6738](https://github.com/aws-powertools/powertools-lambda-python/issues/6738)) +* **ci:** add missing dependency to build docs ([#6717](https://github.com/aws-powertools/powertools-lambda-python/issues/6717)) +* **ci:** new pre-release 3.13.1a3 ([#6732](https://github.com/aws-powertools/powertools-lambda-python/issues/6732)) +* **deps:** bump pydantic from 2.11.4 to 2.11.5 ([#6711](https://github.com/aws-powertools/powertools-lambda-python/issues/6711)) +* **deps:** bump mkdocstrings-python from 1.16.10 to 1.16.11 ([#6724](https://github.com/aws-powertools/powertools-lambda-python/issues/6724)) +* **deps:** bump redis from 6.1.0 to 6.2.0 ([#6736](https://github.com/aws-powertools/powertools-lambda-python/issues/6736)) +* **deps:** bump ossf/scorecard-action from 2.4.1 to 2.4.2 ([#6746](https://github.com/aws-powertools/powertools-lambda-python/issues/6746)) +* **deps:** bump mkdocstrings-python from 1.16.10 to 1.16.11 in /docs ([#6722](https://github.com/aws-powertools/powertools-lambda-python/issues/6722)) +* **deps:** bump datadog-lambda from 6.109.0 to 6.110.0 ([#6714](https://github.com/aws-powertools/powertools-lambda-python/issues/6714)) +* **deps-dev:** bump pytest-mock from 3.14.0 to 3.14.1 ([#6723](https://github.com/aws-powertools/powertools-lambda-python/issues/6723)) +* **deps-dev:** bump pytest-xdist from 3.6.1 to 3.7.0 ([#6730](https://github.com/aws-powertools/powertools-lambda-python/issues/6730)) +* **deps-dev:** bump aws-cdk-lib from 2.198.0 to 2.199.0 ([#6731](https://github.com/aws-powertools/powertools-lambda-python/issues/6731)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.197.0a0 to 2.198.0a0 ([#6715](https://github.com/aws-powertools/powertools-lambda-python/issues/6715)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.198.0a0 to 2.199.0a0 ([#6729](https://github.com/aws-powertools/powertools-lambda-python/issues/6729)) +* **deps-dev:** bump coverage from 7.8.1 to 7.8.2 ([#6713](https://github.com/aws-powertools/powertools-lambda-python/issues/6713)) +* **deps-dev:** bump boto3-stubs from 1.38.22 to 1.38.23 ([#6712](https://github.com/aws-powertools/powertools-lambda-python/issues/6712)) +* **deps-dev:** bump aws-cdk from 2.1016.1 to 2.1017.0 ([#6734](https://github.com/aws-powertools/powertools-lambda-python/issues/6734)) +* **deps-dev:** bump boto3-stubs from 1.38.23 to 1.38.25 ([#6735](https://github.com/aws-powertools/powertools-lambda-python/issues/6735)) +* **deps-dev:** bump ruff from 0.11.11 to 0.11.12 ([#6741](https://github.com/aws-powertools/powertools-lambda-python/issues/6741)) +* **deps-dev:** bump boto3-stubs from 1.38.25 to 1.38.26 ([#6742](https://github.com/aws-powertools/powertools-lambda-python/issues/6742)) +* **deps-dev:** bump cfn-lint from 1.35.1 to 1.35.3 ([#6708](https://github.com/aws-powertools/powertools-lambda-python/issues/6708)) +* **deps-dev:** bump ruff from 0.11.10 to 0.11.11 ([#6706](https://github.com/aws-powertools/powertools-lambda-python/issues/6706)) +* **deps-dev:** bump boto3-stubs from 1.38.21 to 1.38.22 ([#6707](https://github.com/aws-powertools/powertools-lambda-python/issues/6707)) +* **deps-dev:** bump aws-cdk from 2.1016.0 to 2.1016.1 ([#6703](https://github.com/aws-powertools/powertools-lambda-python/issues/6703)) +* **deps-dev:** bump aws-cdk from 2.1017.0 to 2.1017.1 ([#6748](https://github.com/aws-powertools/powertools-lambda-python/issues/6748)) +* **deps-dev:** bump boto3-stubs from 1.38.26 to 1.38.27 ([#6747](https://github.com/aws-powertools/powertools-lambda-python/issues/6747)) +* **deps-dev:** bump coverage from 7.8.0 to 7.8.1 ([#6701](https://github.com/aws-powertools/powertools-lambda-python/issues/6701)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.196.1a0 to 2.197.0a0 ([#6700](https://github.com/aws-powertools/powertools-lambda-python/issues/6700)) +* **deps-dev:** bump aws-cdk-lib from 2.196.1 to 2.197.0 ([#6699](https://github.com/aws-powertools/powertools-lambda-python/issues/6699)) +* **deps-dev:** bump pytest from 8.3.5 to 8.4.0 ([#6757](https://github.com/aws-powertools/powertools-lambda-python/issues/6757)) +* **deps-dev:** bump cfn-lint from 1.35.3 to 1.35.4 ([#6755](https://github.com/aws-powertools/powertools-lambda-python/issues/6755)) +* **deps-dev:** bump boto3-stubs from 1.38.27 to 1.38.28 ([#6756](https://github.com/aws-powertools/powertools-lambda-python/issues/6756)) +* **deps-dev:** bump aws-cdk-lib from 2.196.0 to 2.196.1 ([#6695](https://github.com/aws-powertools/powertools-lambda-python/issues/6695)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.196.0a0 to 2.196.1a0 ([#6694](https://github.com/aws-powertools/powertools-lambda-python/issues/6694)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.199.0a0 to 2.200.0a0 ([#6758](https://github.com/aws-powertools/powertools-lambda-python/issues/6758)) +* **deps-dev:** bump boto3-stubs from 1.38.19 to 1.38.21 ([#6698](https://github.com/aws-powertools/powertools-lambda-python/issues/6698)) +* **docs:** Add llms.txt to documentation ([#6693](https://github.com/aws-powertools/powertools-lambda-python/issues/6693)) + + + +## [v3.13.0] - 2025-05-20 +## Code Refactoring + +* **idempotency:** replace Redis name with Cache and add valkey-glide support ([#6685](https://github.com/aws-powertools/powertools-lambda-python/issues/6685)) + +## Features + +* **event_source:** add support for tumbling windows in Kinesis and DynamoDB events ([#6658](https://github.com/aws-powertools/powertools-lambda-python/issues/6658)) +* **event_source:** export SQSRecord in data_classes module ([#6639](https://github.com/aws-powertools/powertools-lambda-python/issues/6639)) +* **parser:** add support to decompress Kinesis CloudWatch logs in Kinesis envelope ([#6656](https://github.com/aws-powertools/powertools-lambda-python/issues/6656)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.12.1a2 ([#6638](https://github.com/aws-powertools/powertools-lambda-python/issues/6638)) +* **ci:** include allowed licenses file in dependency review workflow ([#6618](https://github.com/aws-powertools/powertools-lambda-python/issues/6618)) +* **ci:** new pre-release 3.12.1a8 ([#6683](https://github.com/aws-powertools/powertools-lambda-python/issues/6683)) +* **ci:** new pre-release 3.12.1a3 ([#6647](https://github.com/aws-powertools/powertools-lambda-python/issues/6647)) +* **ci:** new pre-release 3.12.1a7 ([#6675](https://github.com/aws-powertools/powertools-lambda-python/issues/6675)) +* **ci:** new pre-release 3.12.1a0 ([#6621](https://github.com/aws-powertools/powertools-lambda-python/issues/6621)) +* **ci:** new pre-release 3.12.1a6 ([#6670](https://github.com/aws-powertools/powertools-lambda-python/issues/6670)) +* **ci:** new pre-release 3.12.1a1 ([#6626](https://github.com/aws-powertools/powertools-lambda-python/issues/6626)) +* **ci:** new pre-release 3.12.1a4 ([#6655](https://github.com/aws-powertools/powertools-lambda-python/issues/6655)) +* **ci:** new pre-release 3.12.1a5 ([#6664](https://github.com/aws-powertools/powertools-lambda-python/issues/6664)) +* **deps:** bump aws-actions/configure-aws-credentials from 4.2.0 to 4.2.1 ([#6667](https://github.com/aws-powertools/powertools-lambda-python/issues/6667)) +* **deps:** bump squidfunk/mkdocs-material from `f6c81d5` to `eb04b60` in /docs ([#6659](https://github.com/aws-powertools/powertools-lambda-python/issues/6659)) +* **deps:** bump datadog-lambda from 6.107.0 to 6.108.0 ([#6634](https://github.com/aws-powertools/powertools-lambda-python/issues/6634)) +* **deps:** bump actions/setup-go from 5.4.0 to 5.5.0 ([#6630](https://github.com/aws-powertools/powertools-lambda-python/issues/6630)) +* **deps:** bump actions/dependency-review-action from 4.7.0 to 4.7.1 ([#6663](https://github.com/aws-powertools/powertools-lambda-python/issues/6663)) +* **deps:** bump redis from 5.2.1 to 6.1.0 ([#6662](https://github.com/aws-powertools/powertools-lambda-python/issues/6662)) +* **deps:** bump actions/dependency-review-action from 4.6.0 to 4.7.0 ([#6629](https://github.com/aws-powertools/powertools-lambda-python/issues/6629)) +* **deps:** bump codecov/codecov-action from 5.4.2 to 5.4.3 ([#6672](https://github.com/aws-powertools/powertools-lambda-python/issues/6672)) +* **deps:** bump squidfunk/mkdocs-material from `95f2ff4` to `f6c81d5` in /docs ([#6650](https://github.com/aws-powertools/powertools-lambda-python/issues/6650)) +* **deps:** bump aws-actions/configure-aws-credentials from 4.1.0 to 4.2.0 ([#6619](https://github.com/aws-powertools/powertools-lambda-python/issues/6619)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.24 to 3.0.25 ([#6686](https://github.com/aws-powertools/powertools-lambda-python/issues/6686)) +* **deps:** bump datadog-lambda from 6.108.0 to 6.109.0 ([#6641](https://github.com/aws-powertools/powertools-lambda-python/issues/6641)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.308 to 0.1.309 ([#6651](https://github.com/aws-powertools/powertools-lambda-python/issues/6651)) +* **deps-dev:** bump boto3-stubs from 1.38.12 to 1.38.13 ([#6644](https://github.com/aws-powertools/powertools-lambda-python/issues/6644)) +* **deps-dev:** bump cfn-lint from 1.35.0 to 1.35.1 ([#6642](https://github.com/aws-powertools/powertools-lambda-python/issues/6642)) +* **deps-dev:** bump ruff from 0.11.8 to 0.11.9 ([#6643](https://github.com/aws-powertools/powertools-lambda-python/issues/6643)) +* **deps-dev:** bump boto3-stubs from 1.38.13 to 1.38.14 ([#6653](https://github.com/aws-powertools/powertools-lambda-python/issues/6653)) +* **deps-dev:** bump sentry-sdk from 2.27.0 to 2.28.0 ([#6652](https://github.com/aws-powertools/powertools-lambda-python/issues/6652)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.194.0a0 to 2.195.0a0 ([#6635](https://github.com/aws-powertools/powertools-lambda-python/issues/6635)) +* **deps-dev:** bump aws-cdk from 2.1013.0 to 2.1014.0 ([#6636](https://github.com/aws-powertools/powertools-lambda-python/issues/6636)) +* **deps-dev:** bump mkdocs-material from 9.6.12 to 9.6.13 ([#6654](https://github.com/aws-powertools/powertools-lambda-python/issues/6654)) +* **deps-dev:** bump boto3-stubs from 1.38.11 to 1.38.12 ([#6633](https://github.com/aws-powertools/powertools-lambda-python/issues/6633)) +* **deps-dev:** bump aws-cdk-lib from 2.194.0 to 2.195.0 ([#6632](https://github.com/aws-powertools/powertools-lambda-python/issues/6632)) +* **deps-dev:** bump boto3-stubs from 1.38.14 to 1.38.15 ([#6660](https://github.com/aws-powertools/powertools-lambda-python/issues/6660)) +* **deps-dev:** bump ijson from 3.3.0 to 3.4.0 ([#6631](https://github.com/aws-powertools/powertools-lambda-python/issues/6631)) +* **deps-dev:** bump mkdocs-material from 9.6.13 to 9.6.14 ([#6661](https://github.com/aws-powertools/powertools-lambda-python/issues/6661)) +* **deps-dev:** bump boto3-stubs from 1.38.15 to 1.38.16 ([#6669](https://github.com/aws-powertools/powertools-lambda-python/issues/6669)) +* **deps-dev:** bump aws-cdk from 2.1014.0 to 2.1015.0 ([#6668](https://github.com/aws-powertools/powertools-lambda-python/issues/6668)) +* **deps-dev:** bump cfn-lint from 1.34.2 to 1.35.0 ([#6623](https://github.com/aws-powertools/powertools-lambda-python/issues/6623)) +* **deps-dev:** bump types-python-dateutil from 2.9.0.20241206 to 2.9.0.20250516 ([#6678](https://github.com/aws-powertools/powertools-lambda-python/issues/6678)) +* **deps-dev:** bump ruff from 0.11.9 to 0.11.10 ([#6673](https://github.com/aws-powertools/powertools-lambda-python/issues/6673)) +* **deps-dev:** bump boto3-stubs from 1.38.16 to 1.38.17 ([#6674](https://github.com/aws-powertools/powertools-lambda-python/issues/6674)) +* **deps-dev:** bump boto3-stubs from 1.38.9 to 1.38.10 ([#6620](https://github.com/aws-powertools/powertools-lambda-python/issues/6620)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.195.0a0 to 2.196.0a0 ([#6677](https://github.com/aws-powertools/powertools-lambda-python/issues/6677)) +* **deps-dev:** bump aws-cdk from 2.1015.0 to 2.1016.0 ([#6680](https://github.com/aws-powertools/powertools-lambda-python/issues/6680)) +* **deps-dev:** bump boto3-stubs from 1.38.18 to 1.38.19 ([#6687](https://github.com/aws-powertools/powertools-lambda-python/issues/6687)) +* **deps-dev:** bump boto3-stubs from 1.38.10 to 1.38.11 ([#6624](https://github.com/aws-powertools/powertools-lambda-python/issues/6624)) + + + +## [v3.12.0] - 2025-05-06 +## Documentation + +* **appsync_events:** improve AppSync events documentation ([#6572](https://github.com/aws-powertools/powertools-lambda-python/issues/6572)) +* **community:** add Ran Isenberg blog post ([#6610](https://github.com/aws-powertools/powertools-lambda-python/issues/6610)) +* **i-made-this:** adding Michael's MCP server ([#6591](https://github.com/aws-powertools/powertools-lambda-python/issues/6591)) + +## Features + +* **bedrock_agents:** add optional fields to response payload ([#6336](https://github.com/aws-powertools/powertools-lambda-python/issues/6336)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.11.1a5 ([#6598](https://github.com/aws-powertools/powertools-lambda-python/issues/6598)) +* **ci:** new pre-release 3.11.1a0 ([#6561](https://github.com/aws-powertools/powertools-lambda-python/issues/6561)) +* **ci:** new pre-release 3.11.1a6 ([#6606](https://github.com/aws-powertools/powertools-lambda-python/issues/6606)) +* **ci:** new pre-release 3.11.1a1 ([#6574](https://github.com/aws-powertools/powertools-lambda-python/issues/6574)) +* **ci:** new pre-release 3.11.1a2 ([#6578](https://github.com/aws-powertools/powertools-lambda-python/issues/6578)) +* **ci:** new pre-release 3.11.1a4 ([#6589](https://github.com/aws-powertools/powertools-lambda-python/issues/6589)) +* **ci:** new pre-release 3.11.1a3 ([#6582](https://github.com/aws-powertools/powertools-lambda-python/issues/6582)) +* **deps:** bump pydantic from 2.11.3 to 2.11.4 ([#6585](https://github.com/aws-powertools/powertools-lambda-python/issues/6585)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.23 to 3.0.24 ([#6611](https://github.com/aws-powertools/powertools-lambda-python/issues/6611)) +* **deps-dev:** bump ruff from 0.11.7 to 0.11.8 ([#6595](https://github.com/aws-powertools/powertools-lambda-python/issues/6595)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.306 to 0.1.307 ([#6580](https://github.com/aws-powertools/powertools-lambda-python/issues/6580)) +* **deps-dev:** bump boto3-stubs from 1.38.4 to 1.38.5 ([#6581](https://github.com/aws-powertools/powertools-lambda-python/issues/6581)) +* **deps-dev:** bump aws-cdk from 2.1012.0 to 2.1013.0 ([#6588](https://github.com/aws-powertools/powertools-lambda-python/issues/6588)) +* **deps-dev:** bump boto3-stubs from 1.38.6 to 1.38.7 ([#6594](https://github.com/aws-powertools/powertools-lambda-python/issues/6594)) +* **deps-dev:** bump boto3-stubs from 1.38.3 to 1.38.4 ([#6577](https://github.com/aws-powertools/powertools-lambda-python/issues/6577)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.307 to 0.1.308 ([#6597](https://github.com/aws-powertools/powertools-lambda-python/issues/6597)) +* **deps-dev:** bump h11 from 0.14.0 to 0.16.0 ([#6575](https://github.com/aws-powertools/powertools-lambda-python/issues/6575)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.192.0a0 to 2.193.0a0 ([#6586](https://github.com/aws-powertools/powertools-lambda-python/issues/6586)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.193.0a0 to 2.194.0a0 ([#6602](https://github.com/aws-powertools/powertools-lambda-python/issues/6602)) +* **deps-dev:** bump boto3-stubs from 1.38.2 to 1.38.3 ([#6569](https://github.com/aws-powertools/powertools-lambda-python/issues/6569)) +* **deps-dev:** bump cfn-lint from 1.34.1 to 1.34.2 ([#6568](https://github.com/aws-powertools/powertools-lambda-python/issues/6568)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.305 to 0.1.306 ([#6567](https://github.com/aws-powertools/powertools-lambda-python/issues/6567)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.191.0a0 to 2.192.0a0 ([#6566](https://github.com/aws-powertools/powertools-lambda-python/issues/6566)) +* **deps-dev:** bump aws-cdk-lib from 2.191.0 to 2.192.0 ([#6565](https://github.com/aws-powertools/powertools-lambda-python/issues/6565)) +* **deps-dev:** bump aws-cdk-lib from 2.193.0 to 2.194.0 ([#6603](https://github.com/aws-powertools/powertools-lambda-python/issues/6603)) +* **deps-dev:** bump boto3-stubs from 1.38.7 to 1.38.9 ([#6612](https://github.com/aws-powertools/powertools-lambda-python/issues/6612)) +* **deps-dev:** bump boto3-stubs from 1.38.5 to 1.38.6 ([#6587](https://github.com/aws-powertools/powertools-lambda-python/issues/6587)) +* **docs:** fix youtube embed link in we made this ([#6593](https://github.com/aws-powertools/powertools-lambda-python/issues/6593)) + + + +## [v3.11.0] - 2025-04-24 +## Bug Fixes + +* **logger:** warn customers when the ALC log level is less verbose than log buffer ([#6509](https://github.com/aws-powertools/powertools-lambda-python/issues/6509)) +* **parser:** make key attribute optional in Kafka model ([#6523](https://github.com/aws-powertools/powertools-lambda-python/issues/6523)) + +## Code Refactoring + +* **batch:** use standard collections for types ([#6475](https://github.com/aws-powertools/powertools-lambda-python/issues/6475)) +* **data_masking:** use standard collections for types ([#6493](https://github.com/aws-powertools/powertools-lambda-python/issues/6493)) +* **e2e-tests:** use standard collections for types + refactor code ([#6505](https://github.com/aws-powertools/powertools-lambda-python/issues/6505)) +* **event_handler:** use standard collections for types + refactor code ([#6495](https://github.com/aws-powertools/powertools-lambda-python/issues/6495)) +* **event_source:** use standard collections for types ([#6479](https://github.com/aws-powertools/powertools-lambda-python/issues/6479)) +* **feature_flags:** use standard collections for type ([#6489](https://github.com/aws-powertools/powertools-lambda-python/issues/6489)) +* **general:** add support for `ruff format` ([#6512](https://github.com/aws-powertools/powertools-lambda-python/issues/6512)) +* **idempotency:** use standard collections for types ([#6487](https://github.com/aws-powertools/powertools-lambda-python/issues/6487)) +* **logger:** use standard collections for types ([#6471](https://github.com/aws-powertools/powertools-lambda-python/issues/6471)) +* **metrics:** use standard collections for types ([#6472](https://github.com/aws-powertools/powertools-lambda-python/issues/6472)) +* **middleware_factory:** use standard collections for types ([#6485](https://github.com/aws-powertools/powertools-lambda-python/issues/6485)) +* **parameters:** use standard collections for types ([#6481](https://github.com/aws-powertools/powertools-lambda-python/issues/6481)) +* **streaming:** use standard collections for types ([#6483](https://github.com/aws-powertools/powertools-lambda-python/issues/6483)) +* **tests:** use standard collections for types + refactor code ([#6497](https://github.com/aws-powertools/powertools-lambda-python/issues/6497)) +* **tracer:** use standard collections for types ([#6473](https://github.com/aws-powertools/powertools-lambda-python/issues/6473)) +* **validation:** use standard collections for types ([#6491](https://github.com/aws-powertools/powertools-lambda-python/issues/6491)) + +## Documentation + +* **bedrock:** fix BedrockServiceRole in template.yaml ([#6436](https://github.com/aws-powertools/powertools-lambda-python/issues/6436)) +* **bedrock_agents:** remove Pydantic v1 recommendation ([#6468](https://github.com/aws-powertools/powertools-lambda-python/issues/6468)) +* **event_handler:** add docs for AppSync event resolver ([#6557](https://github.com/aws-powertools/powertools-lambda-python/issues/6557)) +* **event_handler:** fix typo in api keys swagger url ([#6536](https://github.com/aws-powertools/powertools-lambda-python/issues/6536)) + +## Features + +* **bedrock:** add `openapi_extensions` in BedrockAgentResolver ([#6510](https://github.com/aws-powertools/powertools-lambda-python/issues/6510)) +* **data-masking:** add support for Pydantic models, dataclasses, and standard classes ([#6413](https://github.com/aws-powertools/powertools-lambda-python/issues/6413)) +* **event_handler:** add AppSync events resolver ([#6558](https://github.com/aws-powertools/powertools-lambda-python/issues/6558)) +* **event_handler:** add extras HTTP Error Code Exceptions ([#6454](https://github.com/aws-powertools/powertools-lambda-python/issues/6454)) +* **event_handler:** add route-level custom response validation in OpenAPI utility ([#6341](https://github.com/aws-powertools/powertools-lambda-python/issues/6341)) +* **logger:** add support for exception notes ([#6465](https://github.com/aws-powertools/powertools-lambda-python/issues/6465)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.10.1a7 ([#6518](https://github.com/aws-powertools/powertools-lambda-python/issues/6518)) +* **ci:** new pre-release 3.10.1a0 ([#6431](https://github.com/aws-powertools/powertools-lambda-python/issues/6431)) +* **ci:** new pre-release 3.10.1a1 ([#6437](https://github.com/aws-powertools/powertools-lambda-python/issues/6437)) +* **ci:** new pre-release 3.10.1a2 ([#6446](https://github.com/aws-powertools/powertools-lambda-python/issues/6446)) +* **ci:** new pre-release 3.10.1a10 ([#6538](https://github.com/aws-powertools/powertools-lambda-python/issues/6538)) +* **ci:** new pre-release 3.10.1a3 ([#6455](https://github.com/aws-powertools/powertools-lambda-python/issues/6455)) +* **ci:** new pre-release 3.10.1a4 ([#6463](https://github.com/aws-powertools/powertools-lambda-python/issues/6463)) +* **ci:** new pre-release 3.10.1a9 ([#6533](https://github.com/aws-powertools/powertools-lambda-python/issues/6533)) +* **ci:** new pre-release 3.10.1a5 ([#6498](https://github.com/aws-powertools/powertools-lambda-python/issues/6498)) +* **ci:** new pre-release 3.10.1a11 ([#6546](https://github.com/aws-powertools/powertools-lambda-python/issues/6546)) +* **ci:** new pre-release 3.10.1a8 ([#6526](https://github.com/aws-powertools/powertools-lambda-python/issues/6526)) +* **ci:** new pre-release 3.10.1a6 ([#6506](https://github.com/aws-powertools/powertools-lambda-python/issues/6506)) +* **deps:** bump pydantic-settings from 2.8.1 to 2.9.1 ([#6530](https://github.com/aws-powertools/powertools-lambda-python/issues/6530)) +* **deps:** bump pydantic from 2.11.2 to 2.11.3 ([#6427](https://github.com/aws-powertools/powertools-lambda-python/issues/6427)) +* **deps:** bump squidfunk/mkdocs-material from sha256:23b69789b1dd836c53ea25b32f62ef8e1a23366037acd07c90959a219fd1f285 to sha256:95f2ff42251979c043d6cb5b1c82e6ae8189e57e02105813dd1ce124021a418b in /docs ([#6513](https://github.com/aws-powertools/powertools-lambda-python/issues/6513)) +* **deps:** bump actions/download-artifact from 4.2.1 to 4.3.0 ([#6550](https://github.com/aws-powertools/powertools-lambda-python/issues/6550)) +* **deps:** bump actions/setup-python from 5.5.0 to 5.6.0 ([#6549](https://github.com/aws-powertools/powertools-lambda-python/issues/6549)) +* **deps:** bump typing-extensions from 4.13.1 to 4.13.2 ([#6451](https://github.com/aws-powertools/powertools-lambda-python/issues/6451)) +* **deps:** bump actions/setup-node from 4.3.0 to 4.4.0 ([#6457](https://github.com/aws-powertools/powertools-lambda-python/issues/6457)) +* **deps:** bump codecov/codecov-action from 5.4.0 to 5.4.2 ([#6458](https://github.com/aws-powertools/powertools-lambda-python/issues/6458)) +* **deps-dev:** bump mkdocs-material from 9.6.11 to 9.6.12 ([#6514](https://github.com/aws-powertools/powertools-lambda-python/issues/6514)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.302 to 0.1.304 ([#6531](https://github.com/aws-powertools/powertools-lambda-python/issues/6531)) +* **deps-dev:** bump sentry-sdk from 2.25.1 to 2.26.1 ([#6477](https://github.com/aws-powertools/powertools-lambda-python/issues/6477)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.189.1a0 to 2.190.0a0 ([#6529](https://github.com/aws-powertools/powertools-lambda-python/issues/6529)) +* **deps-dev:** bump boto3-stubs from 1.37.37 to 1.37.38 ([#6537](https://github.com/aws-powertools/powertools-lambda-python/issues/6537)) +* **deps-dev:** bump aws-cdk-lib from 2.189.0 to 2.189.1 ([#6461](https://github.com/aws-powertools/powertools-lambda-python/issues/6461)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.189.0a0 to 2.189.1a0 ([#6462](https://github.com/aws-powertools/powertools-lambda-python/issues/6462)) +* **deps-dev:** bump boto3-stubs from 1.37.33 to 1.37.34 ([#6459](https://github.com/aws-powertools/powertools-lambda-python/issues/6459)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.301 to 0.1.302 ([#6460](https://github.com/aws-powertools/powertools-lambda-python/issues/6460)) +* **deps-dev:** bump cfn-lint from 1.34.0 to 1.34.1 ([#6528](https://github.com/aws-powertools/powertools-lambda-python/issues/6528)) +* **deps-dev:** bump cfn-lint from 1.33.2 to 1.34.0 ([#6502](https://github.com/aws-powertools/powertools-lambda-python/issues/6502)) +* **deps-dev:** bump aws-cdk from 2.1010.0 to 2.1012.0 ([#6540](https://github.com/aws-powertools/powertools-lambda-python/issues/6540)) +* **deps-dev:** bump mypy-boto3-appconfigdata from 1.37.0 to 1.38.0 in the boto-typing group ([#6541](https://github.com/aws-powertools/powertools-lambda-python/issues/6541)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.304 to 0.1.305 ([#6545](https://github.com/aws-powertools/powertools-lambda-python/issues/6545)) +* **deps-dev:** bump cfn-lint from 1.33.1 to 1.33.2 ([#6450](https://github.com/aws-powertools/powertools-lambda-python/issues/6450)) +* **deps-dev:** bump boto3-stubs from 1.37.31 to 1.37.33 ([#6449](https://github.com/aws-powertools/powertools-lambda-python/issues/6449)) +* **deps-dev:** bump boto3-stubs from 1.37.35 to 1.37.37 ([#6521](https://github.com/aws-powertools/powertools-lambda-python/issues/6521)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.190.0a0 to 2.191.0a0 ([#6543](https://github.com/aws-powertools/powertools-lambda-python/issues/6543)) +* **deps-dev:** bump h11 from 0.14.0 to 0.16.0 ([#6548](https://github.com/aws-powertools/powertools-lambda-python/issues/6548)) +* **deps-dev:** bump ruff from 0.11.4 to 0.11.5 ([#6443](https://github.com/aws-powertools/powertools-lambda-python/issues/6443)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.188.0a0 to 2.189.0a0 ([#6444](https://github.com/aws-powertools/powertools-lambda-python/issues/6444)) +* **deps-dev:** bump aws-cdk-lib from 2.188.0 to 2.189.0 ([#6445](https://github.com/aws-powertools/powertools-lambda-python/issues/6445)) +* **deps-dev:** bump cfn-lint from 1.33.0 to 1.33.1 ([#6442](https://github.com/aws-powertools/powertools-lambda-python/issues/6442)) +* **deps-dev:** bump ruff from 0.11.5 to 0.11.6 ([#6515](https://github.com/aws-powertools/powertools-lambda-python/issues/6515)) +* **deps-dev:** bump aws-cdk from 2.1007.0 to 2.1010.0 ([#6501](https://github.com/aws-powertools/powertools-lambda-python/issues/6501)) +* **deps-dev:** bump httpx from 0.25.1 to 0.28.1 ([#6554](https://github.com/aws-powertools/powertools-lambda-python/issues/6554)) +* **deps-dev:** bump boto3-stubs from 1.38.1 to 1.38.2 ([#6556](https://github.com/aws-powertools/powertools-lambda-python/issues/6556)) +* **deps-dev:** bump boto3-stubs from 1.37.29 to 1.37.31 ([#6433](https://github.com/aws-powertools/powertools-lambda-python/issues/6433)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.187.0a0 to 2.188.0a0 ([#6434](https://github.com/aws-powertools/powertools-lambda-python/issues/6434)) +* **deps-dev:** bump ruff from 0.11.3 to 0.11.4 ([#6428](https://github.com/aws-powertools/powertools-lambda-python/issues/6428)) +* **deps-dev:** bump pytest-cov from 6.1.0 to 6.1.1 ([#6429](https://github.com/aws-powertools/powertools-lambda-python/issues/6429)) +* **deps-dev:** bump cfn-lint from 1.32.4 to 1.33.0 ([#6430](https://github.com/aws-powertools/powertools-lambda-python/issues/6430)) +* **deps-dev:** bump multiprocess from 0.70.17 to 0.70.18 ([#6516](https://github.com/aws-powertools/powertools-lambda-python/issues/6516)) +* **deps-dev:** bump ruff from 0.11.6 to 0.11.7 ([#6555](https://github.com/aws-powertools/powertools-lambda-python/issues/6555)) +* **deps-dev:** bump sentry-sdk from 2.26.1 to 2.27.0 ([#6553](https://github.com/aws-powertools/powertools-lambda-python/issues/6553)) +* **deps-dev:** bump boto3-stubs from 1.37.34 to 1.37.35 ([#6504](https://github.com/aws-powertools/powertools-lambda-python/issues/6504)) + + + +## [v3.10.0] - 2025-04-08 +## Bug Fixes + +* **event_source:** Added missing properties in APIGatewayWebSocketEvent class ([#6411](https://github.com/aws-powertools/powertools-lambda-python/issues/6411)) +* **event_source:** fix HomeDirectoryDetails type in TransferFamilyAuthorizerResponse method ([#6403](https://github.com/aws-powertools/powertools-lambda-python/issues/6403)) +* **logger:** improve behavior with `exc_info=True` to prevent errors ([#6417](https://github.com/aws-powertools/powertools-lambda-python/issues/6417)) + +## Documentation + +* **homepage:** add SAR documentation ([#6347](https://github.com/aws-powertools/powertools-lambda-python/issues/6347)) + +## Features + +* **parser:** add AppSyncResolver model ([#6400](https://github.com/aws-powertools/powertools-lambda-python/issues/6400)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.9.1a4 ([#6377](https://github.com/aws-powertools/powertools-lambda-python/issues/6377)) +* **ci:** new pre-release 3.9.1a8 ([#6415](https://github.com/aws-powertools/powertools-lambda-python/issues/6415)) +* **ci:** new pre-release 3.9.1a9 ([#6422](https://github.com/aws-powertools/powertools-lambda-python/issues/6422)) +* **ci:** new pre-release 3.9.1a0 ([#6354](https://github.com/aws-powertools/powertools-lambda-python/issues/6354)) +* **ci:** new pre-release 3.9.1a5 ([#6385](https://github.com/aws-powertools/powertools-lambda-python/issues/6385)) +* **ci:** new pre-release 3.9.1a7 ([#6401](https://github.com/aws-powertools/powertools-lambda-python/issues/6401)) +* **ci:** new pre-release 3.9.1a1 ([#6356](https://github.com/aws-powertools/powertools-lambda-python/issues/6356)) +* **ci:** new pre-release 3.9.1a2 ([#6364](https://github.com/aws-powertools/powertools-lambda-python/issues/6364)) +* **ci:** new pre-release 3.9.1a6 ([#6392](https://github.com/aws-powertools/powertools-lambda-python/issues/6392)) +* **ci:** new pre-release 3.9.1a3 ([#6369](https://github.com/aws-powertools/powertools-lambda-python/issues/6369)) +* **deps:** bump aws-encryption-sdk from 4.0.0 to 4.0.1 ([#6360](https://github.com/aws-powertools/powertools-lambda-python/issues/6360)) +* **deps:** bump pydantic from 2.11.1 to 2.11.2 ([#6395](https://github.com/aws-powertools/powertools-lambda-python/issues/6395)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.22 to 3.0.23 ([#6371](https://github.com/aws-powertools/powertools-lambda-python/issues/6371)) +* **deps:** bump squidfunk/mkdocs-material from `3555052` to `23b6978` in /docs ([#6404](https://github.com/aws-powertools/powertools-lambda-python/issues/6404)) +* **deps:** bump datadog-lambda from 6.106.0 to 6.107.0 ([#6405](https://github.com/aws-powertools/powertools-lambda-python/issues/6405)) +* **deps:** bump squidfunk/mkdocs-material from `f226a2d` to `3555052` in /docs ([#6372](https://github.com/aws-powertools/powertools-lambda-python/issues/6372)) +* **deps:** bump pydantic from 2.10.6 to 2.11.1 ([#6383](https://github.com/aws-powertools/powertools-lambda-python/issues/6383)) +* **deps:** bump typing-extensions from 4.12.2 to 4.13.1 ([#6418](https://github.com/aws-powertools/powertools-lambda-python/issues/6418)) +* **deps:** bump actions/setup-python from 5.4.0 to 5.5.0 ([#6349](https://github.com/aws-powertools/powertools-lambda-python/issues/6349)) +* **deps:** bump actions/dependency-review-action from 4.5.0 to 4.6.0 ([#6380](https://github.com/aws-powertools/powertools-lambda-python/issues/6380)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.186.0a0 to 2.187.0a0 ([#6382](https://github.com/aws-powertools/powertools-lambda-python/issues/6382)) +* **deps-dev:** bump pytest-cov from 6.0.0 to 6.1.0 ([#6381](https://github.com/aws-powertools/powertools-lambda-python/issues/6381)) +* **deps-dev:** bump coverage from 7.7.1 to 7.8.0 ([#6376](https://github.com/aws-powertools/powertools-lambda-python/issues/6376)) +* **deps-dev:** bump mkdocs-material from 9.6.9 to 9.6.10 ([#6375](https://github.com/aws-powertools/powertools-lambda-python/issues/6375)) +* **deps-dev:** bump boto3-stubs from 1.37.23 to 1.37.24 ([#6374](https://github.com/aws-powertools/powertools-lambda-python/issues/6374)) +* **deps-dev:** bump boto3-stubs from 1.37.24 to 1.37.25 ([#6384](https://github.com/aws-powertools/powertools-lambda-python/issues/6384)) +* **deps-dev:** bump sentry-sdk from 2.24.1 to 2.25.0 ([#6373](https://github.com/aws-powertools/powertools-lambda-python/issues/6373)) +* **deps-dev:** bump aws-cdk from 2.1006.0 to 2.1007.0 ([#6387](https://github.com/aws-powertools/powertools-lambda-python/issues/6387)) +* **deps-dev:** bump boto3-stubs from 1.37.25 to 1.37.26 ([#6389](https://github.com/aws-powertools/powertools-lambda-python/issues/6389)) +* **deps-dev:** bump sentry-sdk from 2.25.0 to 2.25.1 ([#6391](https://github.com/aws-powertools/powertools-lambda-python/issues/6391)) +* **deps-dev:** bump boto3-stubs from 1.37.22 to 1.37.23 ([#6366](https://github.com/aws-powertools/powertools-lambda-python/issues/6366)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.298 to 0.1.299 ([#6390](https://github.com/aws-powertools/powertools-lambda-python/issues/6390)) +* **deps-dev:** bump cfn-lint from 1.32.1 to 1.32.3 ([#6388](https://github.com/aws-powertools/powertools-lambda-python/issues/6388)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.185.0a0 to 2.186.0a0 ([#6363](https://github.com/aws-powertools/powertools-lambda-python/issues/6363)) +* **deps-dev:** bump boto3-stubs from 1.37.20 to 1.37.22 ([#6362](https://github.com/aws-powertools/powertools-lambda-python/issues/6362)) +* **deps-dev:** bump testcontainers from 4.9.2 to 4.10.0 ([#6397](https://github.com/aws-powertools/powertools-lambda-python/issues/6397)) +* **deps-dev:** bump mkdocstrings-python from 1.16.8 to 1.16.10 ([#6399](https://github.com/aws-powertools/powertools-lambda-python/issues/6399)) +* **deps-dev:** bump ruff from 0.11.2 to 0.11.3 ([#6398](https://github.com/aws-powertools/powertools-lambda-python/issues/6398)) +* **deps-dev:** bump boto3-stubs from 1.37.26 to 1.37.28 ([#6406](https://github.com/aws-powertools/powertools-lambda-python/issues/6406)) +* **deps-dev:** bump pytest-asyncio from 0.25.3 to 0.26.0 ([#6352](https://github.com/aws-powertools/powertools-lambda-python/issues/6352)) +* **deps-dev:** bump aws-cdk-lib from 2.187.0 to 2.188.0 ([#6407](https://github.com/aws-powertools/powertools-lambda-python/issues/6407)) +* **deps-dev:** bump cfn-lint from 1.32.0 to 1.32.1 ([#6351](https://github.com/aws-powertools/powertools-lambda-python/issues/6351)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.299 to 0.1.300 ([#6408](https://github.com/aws-powertools/powertools-lambda-python/issues/6408)) +* **deps-dev:** bump aws-cdk from 2.1005.0 to 2.1006.0 ([#6350](https://github.com/aws-powertools/powertools-lambda-python/issues/6350)) +* **deps-dev:** bump cfn-lint from 1.32.3 to 1.32.4 ([#6419](https://github.com/aws-powertools/powertools-lambda-python/issues/6419)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.300 to 0.1.301 ([#6420](https://github.com/aws-powertools/powertools-lambda-python/issues/6420)) +* **deps-dev:** bump boto3-stubs from 1.37.28 to 1.37.29 ([#6421](https://github.com/aws-powertools/powertools-lambda-python/issues/6421)) +* **deps-dev:** bump boto3-stubs from 1.37.19 to 1.37.20 ([#6353](https://github.com/aws-powertools/powertools-lambda-python/issues/6353)) + + + +## [v3.9.0] - 2025-03-25 +## Bug Fixes + +* **idempotency:** include sk in error msgs when using composite key ([#6325](https://github.com/aws-powertools/powertools-lambda-python/issues/6325)) +* **metrics:** ensure proper type conversion for `DD_FLUSH_TO_LOG` env var ([#6280](https://github.com/aws-powertools/powertools-lambda-python/issues/6280)) + +## Code Refactoring + +* **data_classes:** Add base class with common code ([#6297](https://github.com/aws-powertools/powertools-lambda-python/issues/6297)) +* **data_classes:** remove duplicated code ([#6288](https://github.com/aws-powertools/powertools-lambda-python/issues/6288)) +* **data_classes:** simplify nested data classes ([#6289](https://github.com/aws-powertools/powertools-lambda-python/issues/6289)) +* **tests:** add LambdaContext type in tests ([#6214](https://github.com/aws-powertools/powertools-lambda-python/issues/6214)) + +## Documentation + +* **homepage:** update layer instructions link ([#6242](https://github.com/aws-powertools/powertools-lambda-python/issues/6242)) +* **public_reference:** add Guild as a public reference ([#6342](https://github.com/aws-powertools/powertools-lambda-python/issues/6342)) + +## Features + +* **data_classes:** add API Gateway Websocket event ([#6287](https://github.com/aws-powertools/powertools-lambda-python/issues/6287)) +* **event_handler:** add custom method for OpenAPI configuration ([#6204](https://github.com/aws-powertools/powertools-lambda-python/issues/6204)) +* **event_handler:** add custom response validation in OpenAPI utility ([#6189](https://github.com/aws-powertools/powertools-lambda-python/issues/6189)) +* **general:** make logger, tracer and metrics utilities aware of provisioned concurrency ([#6324](https://github.com/aws-powertools/powertools-lambda-python/issues/6324)) +* **metrics:** allow change ColdStart function_name dimension ([#6315](https://github.com/aws-powertools/powertools-lambda-python/issues/6315)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.8.1a8 ([#6307](https://github.com/aws-powertools/powertools-lambda-python/issues/6307)) +* **ci:** new pre-release 3.8.1a11 ([#6340](https://github.com/aws-powertools/powertools-lambda-python/issues/6340)) +* **ci:** new pre-release 3.8.1a0 ([#6244](https://github.com/aws-powertools/powertools-lambda-python/issues/6244)) +* **ci:** new pre-release 3.8.1a10 ([#6332](https://github.com/aws-powertools/powertools-lambda-python/issues/6332)) +* **ci:** new pre-release 3.8.1a1 ([#6250](https://github.com/aws-powertools/powertools-lambda-python/issues/6250)) +* **ci:** new pre-release 3.8.1a2 ([#6253](https://github.com/aws-powertools/powertools-lambda-python/issues/6253)) +* **ci:** new pre-release 3.8.1a9 ([#6322](https://github.com/aws-powertools/powertools-lambda-python/issues/6322)) +* **ci:** new pre-release 3.8.1a3 ([#6259](https://github.com/aws-powertools/powertools-lambda-python/issues/6259)) +* **ci:** new pre-release 3.8.1a4 ([#6268](https://github.com/aws-powertools/powertools-lambda-python/issues/6268)) +* **ci:** Fix SAR pipeline ([#6313](https://github.com/aws-powertools/powertools-lambda-python/issues/6313)) +* **ci:** new pre-release 3.8.1a5 ([#6276](https://github.com/aws-powertools/powertools-lambda-python/issues/6276)) +* **ci:** new pre-release 3.8.1a6 ([#6290](https://github.com/aws-powertools/powertools-lambda-python/issues/6290)) +* **ci:** new pre-release 3.8.1a7 ([#6298](https://github.com/aws-powertools/powertools-lambda-python/issues/6298)) +* **deps:** bump actions/setup-go from 5.3.0 to 5.4.0 ([#6304](https://github.com/aws-powertools/powertools-lambda-python/issues/6304)) +* **deps:** bump actions/upload-artifact from 4.6.1 to 4.6.2 ([#6302](https://github.com/aws-powertools/powertools-lambda-python/issues/6302)) +* **deps:** bump squidfunk/mkdocs-material from `047452c` to `479a06a` in /docs ([#6261](https://github.com/aws-powertools/powertools-lambda-python/issues/6261)) +* **deps:** bump squidfunk/mkdocs-material from `479a06a` to `f226a2d` in /docs ([#6279](https://github.com/aws-powertools/powertools-lambda-python/issues/6279)) +* **deps:** bump actions/download-artifact from 4.1.9 to 4.2.0 ([#6294](https://github.com/aws-powertools/powertools-lambda-python/issues/6294)) +* **deps:** bump actions/download-artifact from 4.2.0 to 4.2.1 ([#6303](https://github.com/aws-powertools/powertools-lambda-python/issues/6303)) +* **deps:** bump actions/setup-node from 4.2.0 to 4.3.0 ([#6278](https://github.com/aws-powertools/powertools-lambda-python/issues/6278)) +* **deps-dev:** bump mkdocs-material from 9.6.7 to 9.6.8 ([#6264](https://github.com/aws-powertools/powertools-lambda-python/issues/6264)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.296 to 0.1.297 ([#6281](https://github.com/aws-powertools/powertools-lambda-python/issues/6281)) +* **deps-dev:** bump boto3-stubs from 1.37.12 to 1.37.14 ([#6282](https://github.com/aws-powertools/powertools-lambda-python/issues/6282)) +* **deps-dev:** bump aws-cdk from 2.1004.0 to 2.1005.0 ([#6301](https://github.com/aws-powertools/powertools-lambda-python/issues/6301)) +* **deps-dev:** bump boto3-stubs from 1.37.15 to 1.37.16 ([#6305](https://github.com/aws-powertools/powertools-lambda-python/issues/6305)) +* **deps-dev:** bump mkdocs-material from 9.6.8 to 9.6.9 ([#6285](https://github.com/aws-powertools/powertools-lambda-python/issues/6285)) +* **deps-dev:** bump cfn-lint from 1.31.0 to 1.31.3 ([#6306](https://github.com/aws-powertools/powertools-lambda-python/issues/6306)) +* **deps-dev:** bump ruff from 0.9.10 to 0.11.0 ([#6273](https://github.com/aws-powertools/powertools-lambda-python/issues/6273)) +* **deps-dev:** bump sentry-sdk from 2.24.0 to 2.24.1 ([#6339](https://github.com/aws-powertools/powertools-lambda-python/issues/6339)) +* **deps-dev:** bump aws-cdk-lib from 2.183.0 to 2.184.1 ([#6272](https://github.com/aws-powertools/powertools-lambda-python/issues/6272)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.183.0a0 to 2.184.1a0 ([#6271](https://github.com/aws-powertools/powertools-lambda-python/issues/6271)) +* **deps-dev:** bump filelock from 3.17.0 to 3.18.0 ([#6270](https://github.com/aws-powertools/powertools-lambda-python/issues/6270)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.184.1a0 to 2.185.0a0 ([#6317](https://github.com/aws-powertools/powertools-lambda-python/issues/6317)) +* **deps-dev:** bump boto3-stubs from 1.37.11 to 1.37.12 ([#6266](https://github.com/aws-powertools/powertools-lambda-python/issues/6266)) +* **deps-dev:** bump cfn-lint from 1.31.3 to 1.32.0 ([#6316](https://github.com/aws-powertools/powertools-lambda-python/issues/6316)) +* **deps-dev:** bump cfn-lint from 1.30.0 to 1.31.0 ([#6296](https://github.com/aws-powertools/powertools-lambda-python/issues/6296)) +* **deps-dev:** bump cfn-lint from 1.29.1 to 1.30.0 ([#6263](https://github.com/aws-powertools/powertools-lambda-python/issues/6263)) +* **deps-dev:** bump aws-cdk from 2.1003.0 to 2.1004.0 ([#6262](https://github.com/aws-powertools/powertools-lambda-python/issues/6262)) +* **deps-dev:** bump boto3-stubs from 1.37.14 to 1.37.15 ([#6295](https://github.com/aws-powertools/powertools-lambda-python/issues/6295)) +* **deps-dev:** bump boto3-stubs from 1.37.8 to 1.37.10 ([#6248](https://github.com/aws-powertools/powertools-lambda-python/issues/6248)) +* **deps-dev:** bump mkdocstrings-python from 1.16.6 to 1.16.7 ([#6319](https://github.com/aws-powertools/powertools-lambda-python/issues/6319)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.182.0a0 to 2.183.0a0 ([#6258](https://github.com/aws-powertools/powertools-lambda-python/issues/6258)) +* **deps-dev:** bump aws-cdk-lib from 2.182.0 to 2.183.0 ([#6257](https://github.com/aws-powertools/powertools-lambda-python/issues/6257)) +* **deps-dev:** bump ruff from 0.11.0 to 0.11.1 ([#6320](https://github.com/aws-powertools/powertools-lambda-python/issues/6320)) +* **deps-dev:** bump ruff from 0.11.1 to 0.11.2 ([#6326](https://github.com/aws-powertools/powertools-lambda-python/issues/6326)) +* **deps-dev:** bump boto3-stubs from 1.37.10 to 1.37.11 ([#6252](https://github.com/aws-powertools/powertools-lambda-python/issues/6252)) +* **deps-dev:** bump coverage from 7.7.0 to 7.7.1 ([#6328](https://github.com/aws-powertools/powertools-lambda-python/issues/6328)) +* **deps-dev:** bump cfn-lint from 1.28.0 to 1.29.1 ([#6249](https://github.com/aws-powertools/powertools-lambda-python/issues/6249)) +* **deps-dev:** bump boto3-stubs from 1.37.16 to 1.37.18 ([#6327](https://github.com/aws-powertools/powertools-lambda-python/issues/6327)) +* **deps-dev:** bump sentry-sdk from 2.23.1 to 2.24.0 ([#6329](https://github.com/aws-powertools/powertools-lambda-python/issues/6329)) +* **deps-dev:** bump boto3-stubs from 1.37.18 to 1.37.19 ([#6337](https://github.com/aws-powertools/powertools-lambda-python/issues/6337)) +* **deps-dev:** bump mkdocstrings-python from 1.16.7 to 1.16.8 ([#6338](https://github.com/aws-powertools/powertools-lambda-python/issues/6338)) +* **deps-dev:** bump ruff from 0.9.9 to 0.9.10 ([#6241](https://github.com/aws-powertools/powertools-lambda-python/issues/6241)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.295 to 0.1.296 ([#6240](https://github.com/aws-powertools/powertools-lambda-python/issues/6240)) +* **deps-dev:** bump boto3-stubs from 1.37.7 to 1.37.8 ([#6239](https://github.com/aws-powertools/powertools-lambda-python/issues/6239)) +* **deps-dev:** bump coverage from 7.6.12 to 7.7.0 ([#6284](https://github.com/aws-powertools/powertools-lambda-python/issues/6284)) +* **documentation:** v2 end of support ([#6343](https://github.com/aws-powertools/powertools-lambda-python/issues/6343)) +* **logger:** clear prev request buffers in manual mode ([#6314](https://github.com/aws-powertools/powertools-lambda-python/issues/6314)) + + + +## [v3.8.0] - 2025-03-07 +## Bug Fixes + +* **event_handler:** revert regression when validating response ([#6234](https://github.com/aws-powertools/powertools-lambda-python/issues/6234)) + +## Code Refactoring + +* **tracer:** fix capture_lambda_handler return type annotation ([#6197](https://github.com/aws-powertools/powertools-lambda-python/issues/6197)) + +## Documentation + +* **layer:** Fix SSM parameter name for looking up layer ARN ([#6221](https://github.com/aws-powertools/powertools-lambda-python/issues/6221)) + +## Features + +* **logger:** add logger buffer feature ([#6060](https://github.com/aws-powertools/powertools-lambda-python/issues/6060)) +* **logger:** add new logic to sample debug logs ([#6142](https://github.com/aws-powertools/powertools-lambda-python/issues/6142)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.7.1a2 ([#6186](https://github.com/aws-powertools/powertools-lambda-python/issues/6186)) +* **ci:** new pre-release 3.7.1a0 ([#6166](https://github.com/aws-powertools/powertools-lambda-python/issues/6166)) +* **ci:** new pre-release 3.7.1a6 ([#6229](https://github.com/aws-powertools/powertools-lambda-python/issues/6229)) +* **ci:** new pre-release 3.7.1a7 ([#6233](https://github.com/aws-powertools/powertools-lambda-python/issues/6233)) +* **ci:** new pre-release 3.7.1a1 ([#6178](https://github.com/aws-powertools/powertools-lambda-python/issues/6178)) +* **ci:** enable SAR deployment ([#6104](https://github.com/aws-powertools/powertools-lambda-python/issues/6104)) +* **ci:** new pre-release 3.7.1a5 ([#6219](https://github.com/aws-powertools/powertools-lambda-python/issues/6219)) +* **ci:** new pre-release 3.7.1a3 ([#6201](https://github.com/aws-powertools/powertools-lambda-python/issues/6201)) +* **ci:** new pre-release 3.7.1a4 ([#6211](https://github.com/aws-powertools/powertools-lambda-python/issues/6211)) +* **deps:** bump docker/setup-qemu-action from 3.5.0 to 3.6.0 ([#6190](https://github.com/aws-powertools/powertools-lambda-python/issues/6190)) +* **deps:** bump actions/download-artifact from 4.1.8 to 4.1.9 ([#6174](https://github.com/aws-powertools/powertools-lambda-python/issues/6174)) +* **deps:** bump squidfunk/mkdocs-material from `2615302` to `047452c` in /docs ([#6210](https://github.com/aws-powertools/powertools-lambda-python/issues/6210)) +* **deps:** bump docker/setup-qemu-action from 3.4.0 to 3.5.0 ([#6176](https://github.com/aws-powertools/powertools-lambda-python/issues/6176)) +* **deps:** bump docker/setup-buildx-action from 3.9.0 to 3.10.0 ([#6175](https://github.com/aws-powertools/powertools-lambda-python/issues/6175)) +* **deps:** bump datadog-lambda from 6.105.0 to 6.106.0 ([#6218](https://github.com/aws-powertools/powertools-lambda-python/issues/6218)) +* **deps:** bump codecov/codecov-action from 5.3.1 to 5.4.0 ([#6180](https://github.com/aws-powertools/powertools-lambda-python/issues/6180)) +* **deps:** bump pydantic-settings from 2.8.0 to 2.8.1 ([#6182](https://github.com/aws-powertools/powertools-lambda-python/issues/6182)) +* **deps:** bump jinja2 from 3.1.5 to 3.1.6 in /docs ([#6223](https://github.com/aws-powertools/powertools-lambda-python/issues/6223)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.294 to 0.1.295 ([#6207](https://github.com/aws-powertools/powertools-lambda-python/issues/6207)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.293 to 0.1.294 ([#6193](https://github.com/aws-powertools/powertools-lambda-python/issues/6193)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.181.0a0 to 2.181.1a0 ([#6194](https://github.com/aws-powertools/powertools-lambda-python/issues/6194)) +* **deps-dev:** bump ruff from 0.9.8 to 0.9.9 ([#6195](https://github.com/aws-powertools/powertools-lambda-python/issues/6195)) +* **deps-dev:** bump aws-cdk-lib from 2.181.1 to 2.182.0 ([#6222](https://github.com/aws-powertools/powertools-lambda-python/issues/6222)) +* **deps-dev:** bump testcontainers from 4.9.1 to 4.9.2 ([#6225](https://github.com/aws-powertools/powertools-lambda-python/issues/6225)) +* **deps-dev:** bump cfn-lint from 1.26.1 to 1.27.0 ([#6192](https://github.com/aws-powertools/powertools-lambda-python/issues/6192)) +* **deps-dev:** bump boto3-stubs from 1.37.2 to 1.37.3 ([#6181](https://github.com/aws-powertools/powertools-lambda-python/issues/6181)) +* **deps-dev:** bump isort from 6.0.0 to 6.0.1 ([#6183](https://github.com/aws-powertools/powertools-lambda-python/issues/6183)) +* **deps-dev:** bump boto3-stubs from 1.37.5 to 1.37.6 ([#6227](https://github.com/aws-powertools/powertools-lambda-python/issues/6227)) +* **deps-dev:** bump ruff from 0.9.7 to 0.9.8 ([#6184](https://github.com/aws-powertools/powertools-lambda-python/issues/6184)) +* **deps-dev:** bump boto3-stubs from 1.37.4 to 1.37.5 ([#6217](https://github.com/aws-powertools/powertools-lambda-python/issues/6217)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.181.1a0 to 2.182.0a0 ([#6226](https://github.com/aws-powertools/powertools-lambda-python/issues/6226)) +* **deps-dev:** bump cfn-lint from 1.27.0 to 1.28.0 ([#6228](https://github.com/aws-powertools/powertools-lambda-python/issues/6228)) +* **deps-dev:** bump pytest from 8.3.4 to 8.3.5 ([#6206](https://github.com/aws-powertools/powertools-lambda-python/issues/6206)) +* **deps-dev:** bump boto3-stubs from 1.37.0 to 1.37.1 ([#6170](https://github.com/aws-powertools/powertools-lambda-python/issues/6170)) +* **deps-dev:** bump boto3-stubs from 1.37.3 to 1.37.4 ([#6205](https://github.com/aws-powertools/powertools-lambda-python/issues/6205)) +* **deps-dev:** bump mkdocs-material from 9.6.5 to 9.6.7 ([#6208](https://github.com/aws-powertools/powertools-lambda-python/issues/6208)) +* **deps-dev:** bump aws-cdk from 2.1000.3 to 2.1001.0 ([#6173](https://github.com/aws-powertools/powertools-lambda-python/issues/6173)) +* **deps-dev:** bump cfn-lint from 1.26.0 to 1.26.1 ([#6169](https://github.com/aws-powertools/powertools-lambda-python/issues/6169)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.180.0a0 to 2.181.0a0 ([#6172](https://github.com/aws-powertools/powertools-lambda-python/issues/6172)) +* **deps-dev:** bump jinja2 from 3.1.5 to 3.1.6 ([#6224](https://github.com/aws-powertools/powertools-lambda-python/issues/6224)) +* **deps-dev:** bump aws-cdk from 2.1002.0 to 2.1003.0 ([#6232](https://github.com/aws-powertools/powertools-lambda-python/issues/6232)) +* **deps-dev:** bump cfn-lint from 1.25.1 to 1.26.0 ([#6164](https://github.com/aws-powertools/powertools-lambda-python/issues/6164)) +* **deps-dev:** bump boto3-stubs from 1.36.26 to 1.37.0 ([#6165](https://github.com/aws-powertools/powertools-lambda-python/issues/6165)) +* **deps-dev:** bump mypy-boto3-appconfigdata from 1.36.0 to 1.37.0 in the boto-typing group ([#6163](https://github.com/aws-powertools/powertools-lambda-python/issues/6163)) +* **deps-dev:** bump aws-cdk from 2.1000.2 to 2.1000.3 ([#6162](https://github.com/aws-powertools/powertools-lambda-python/issues/6162)) +* **deps-dev:** bump boto3-stubs from 1.37.6 to 1.37.7 ([#6231](https://github.com/aws-powertools/powertools-lambda-python/issues/6231)) +* **deps-dev:** bump aws-cdk from 2.1001.0 to 2.1002.0 ([#6209](https://github.com/aws-powertools/powertools-lambda-python/issues/6209)) + + + +## [v3.7.0] - 2025-02-25 +## Bug Fixes + +* **logger:** correctly pick powertools or custom handler in custom environments ([#6083](https://github.com/aws-powertools/powertools-lambda-python/issues/6083)) +* **openapi:** validate response serialization when falsy ([#6119](https://github.com/aws-powertools/powertools-lambda-python/issues/6119)) +* **parser:** fix data types for `sourceIPAddress` and `sequencer` fields in S3RecordModel Model ([#6154](https://github.com/aws-powertools/powertools-lambda-python/issues/6154)) +* **parser:** fix EventBridgeModel when working with scheduled events ([#6134](https://github.com/aws-powertools/powertools-lambda-python/issues/6134)) +* **security:** fix encryption_context handling in data masking operations ([#6074](https://github.com/aws-powertools/powertools-lambda-python/issues/6074)) + +## Documentation + +* **roadmap:** update roadmap ([#6077](https://github.com/aws-powertools/powertools-lambda-python/issues/6077)) + +## Features + +* **batch:** raise exception for invalid batch event ([#6088](https://github.com/aws-powertools/powertools-lambda-python/issues/6088)) +* **event_handler:** add support for defining OpenAPI examples in parameters ([#6086](https://github.com/aws-powertools/powertools-lambda-python/issues/6086)) +* **layers:** add new comercial region ap-southeast-7 and mx-central-1 ([#6109](https://github.com/aws-powertools/powertools-lambda-python/issues/6109)) +* **parser:** Event source dataclasses for IoT Core Registry Events ([#6123](https://github.com/aws-powertools/powertools-lambda-python/issues/6123)) +* **parser:** Add IoT registry events models ([#5892](https://github.com/aws-powertools/powertools-lambda-python/issues/5892)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.6.1a9 ([#6157](https://github.com/aws-powertools/powertools-lambda-python/issues/6157)) +* **ci:** new pre-release 3.6.1a8 ([#6152](https://github.com/aws-powertools/powertools-lambda-python/issues/6152)) +* **ci:** new pre-release 3.6.1a4 ([#6120](https://github.com/aws-powertools/powertools-lambda-python/issues/6120)) +* **ci:** new pre-release 3.6.1a3 ([#6107](https://github.com/aws-powertools/powertools-lambda-python/issues/6107)) +* **ci:** new pre-release 3.6.1a0 ([#6084](https://github.com/aws-powertools/powertools-lambda-python/issues/6084)) +* **ci:** new pre-release 3.6.1a5 ([#6124](https://github.com/aws-powertools/powertools-lambda-python/issues/6124)) +* **ci:** new pre-release 3.6.1a7 ([#6139](https://github.com/aws-powertools/powertools-lambda-python/issues/6139)) +* **ci:** new pre-release 3.6.1a1 ([#6090](https://github.com/aws-powertools/powertools-lambda-python/issues/6090)) +* **ci:** new pre-release 3.6.1a6 ([#6132](https://github.com/aws-powertools/powertools-lambda-python/issues/6132)) +* **ci:** new pre-release 3.6.1a2 ([#6098](https://github.com/aws-powertools/powertools-lambda-python/issues/6098)) +* **ci:** remove python3.8 runtime when bootstrapping a new region ([#6101](https://github.com/aws-powertools/powertools-lambda-python/issues/6101)) +* **deps:** bump squidfunk/mkdocs-material from `f5bcec4` to `2615302` in /docs ([#6135](https://github.com/aws-powertools/powertools-lambda-python/issues/6135)) +* **deps:** bump squidfunk/mkdocs-material from `c62453b` to `f5bcec4` in /docs ([#6087](https://github.com/aws-powertools/powertools-lambda-python/issues/6087)) +* **deps:** bump actions/upload-artifact from 4.6.0 to 4.6.1 ([#6144](https://github.com/aws-powertools/powertools-lambda-python/issues/6144)) +* **deps:** bump aws-actions/configure-aws-credentials from 4.0.3 to 4.1.0 ([#6082](https://github.com/aws-powertools/powertools-lambda-python/issues/6082)) +* **deps:** bump pydantic-settings from 2.7.1 to 2.8.0 ([#6147](https://github.com/aws-powertools/powertools-lambda-python/issues/6147)) +* **deps:** bump ossf/scorecard-action from 2.4.0 to 2.4.1 ([#6143](https://github.com/aws-powertools/powertools-lambda-python/issues/6143)) +* **deps:** bump slsa-framework/slsa-github-generator from 2.0.0 to 2.1.0 ([#6155](https://github.com/aws-powertools/powertools-lambda-python/issues/6155)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.21 to 3.0.22 ([#6113](https://github.com/aws-powertools/powertools-lambda-python/issues/6113)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.292 to 0.1.293 ([#6129](https://github.com/aws-powertools/powertools-lambda-python/issues/6129)) +* **deps-dev:** bump sentry-sdk from 2.21.0 to 2.22.0 ([#6114](https://github.com/aws-powertools/powertools-lambda-python/issues/6114)) +* **deps-dev:** bump bandit from 1.8.2 to 1.8.3 ([#6117](https://github.com/aws-powertools/powertools-lambda-python/issues/6117)) +* **deps-dev:** bump mkdocstrings-python from 1.15.0 to 1.16.0 ([#6118](https://github.com/aws-powertools/powertools-lambda-python/issues/6118)) +* **deps-dev:** bump boto3-stubs from 1.36.19 to 1.36.22 ([#6116](https://github.com/aws-powertools/powertools-lambda-python/issues/6116)) +* **deps-dev:** bump cfn-lint from 1.24.0 to 1.25.1 ([#6115](https://github.com/aws-powertools/powertools-lambda-python/issues/6115)) +* **deps-dev:** bump mkdocstrings-python from 1.16.0 to 1.16.1 ([#6128](https://github.com/aws-powertools/powertools-lambda-python/issues/6128)) +* **deps-dev:** bump boto3-stubs from 1.36.22 to 1.36.24 ([#6131](https://github.com/aws-powertools/powertools-lambda-python/issues/6131)) +* **deps-dev:** bump aws-cdk from 2.178.2 to 2.1000.2 ([#6126](https://github.com/aws-powertools/powertools-lambda-python/issues/6126)) +* **deps-dev:** bump sentry-sdk from 2.20.0 to 2.21.0 ([#6096](https://github.com/aws-powertools/powertools-lambda-python/issues/6096)) +* **deps-dev:** bump mkdocs-material from 9.6.3 to 9.6.4 ([#6097](https://github.com/aws-powertools/powertools-lambda-python/issues/6097)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.178.2a0 to 2.179.0a0 ([#6127](https://github.com/aws-powertools/powertools-lambda-python/issues/6127)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.178.1a0 to 2.178.2a0 ([#6095](https://github.com/aws-powertools/powertools-lambda-python/issues/6095)) +* **deps-dev:** bump boto3-stubs from 1.36.17 to 1.36.19 ([#6093](https://github.com/aws-powertools/powertools-lambda-python/issues/6093)) +* **deps-dev:** bump aws-cdk-lib from 2.178.2 to 2.179.0 ([#6130](https://github.com/aws-powertools/powertools-lambda-python/issues/6130)) +* **deps-dev:** bump ruff from 0.9.6 to 0.9.7 ([#6138](https://github.com/aws-powertools/powertools-lambda-python/issues/6138)) +* **deps-dev:** bump aws-cdk from 2.178.1 to 2.178.2 ([#6089](https://github.com/aws-powertools/powertools-lambda-python/issues/6089)) +* **deps-dev:** bump mkdocs-material from 9.6.4 to 9.6.5 ([#6136](https://github.com/aws-powertools/powertools-lambda-python/issues/6136)) +* **deps-dev:** bump boto3-stubs from 1.36.24 to 1.36.25 ([#6137](https://github.com/aws-powertools/powertools-lambda-python/issues/6137)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.179.0a0 to 2.180.0a0 ([#6145](https://github.com/aws-powertools/powertools-lambda-python/issues/6145)) +* **deps-dev:** bump aws-cdk-lib from 2.179.0 to 2.180.0 ([#6148](https://github.com/aws-powertools/powertools-lambda-python/issues/6148)) +* **deps-dev:** bump coverage from 7.6.11 to 7.6.12 ([#6080](https://github.com/aws-powertools/powertools-lambda-python/issues/6080)) +* **deps-dev:** bump mkdocstrings-python from 1.14.6 to 1.15.0 ([#6079](https://github.com/aws-powertools/powertools-lambda-python/issues/6079)) +* **deps-dev:** bump boto3-stubs from 1.36.16 to 1.36.17 ([#6078](https://github.com/aws-powertools/powertools-lambda-python/issues/6078)) +* **deps-dev:** bump boto3-stubs from 1.36.25 to 1.36.26 ([#6146](https://github.com/aws-powertools/powertools-lambda-python/issues/6146)) +* **docs:** enable sitemap generation ([#6103](https://github.com/aws-powertools/powertools-lambda-python/issues/6103)) + + + +## [v3.6.0] - 2025-02-11 +## Bug Fixes + +* **docs:** typo in a service name in Event Handler ([#5944](https://github.com/aws-powertools/powertools-lambda-python/issues/5944)) +* **logger:** child logger must respect log level ([#5950](https://github.com/aws-powertools/powertools-lambda-python/issues/5950)) + +## Code Refactoring + +* **metrics:** Improve type annotations for metrics decorator ([#6000](https://github.com/aws-powertools/powertools-lambda-python/issues/6000)) + +## Documentation + +* **api:** migrating the event handler utility to mkdocstrings ([#6023](https://github.com/aws-powertools/powertools-lambda-python/issues/6023)) +* **api:** migrating the metrics utility to mkdocstrings ([#6022](https://github.com/aws-powertools/powertools-lambda-python/issues/6022)) +* **api:** migrating the logger utility to mkdocstrings ([#6021](https://github.com/aws-powertools/powertools-lambda-python/issues/6021)) +* **api:** migrating the Middleware Factory utility to mkdocstrings ([#6019](https://github.com/aws-powertools/powertools-lambda-python/issues/6019)) +* **api:** migrating the tracer utility to mkdocstrings ([#6017](https://github.com/aws-powertools/powertools-lambda-python/issues/6017)) +* **api:** migrating the batch utility to mkdocstrings ([#6016](https://github.com/aws-powertools/powertools-lambda-python/issues/6016)) +* **api:** migrating the event source data classes utility to mkdocstrings ([#6015](https://github.com/aws-powertools/powertools-lambda-python/issues/6015)) +* **api:** migrating the data masking utility to mkdocstrings ([#6013](https://github.com/aws-powertools/powertools-lambda-python/issues/6013)) +* **api:** migrating the AppConfig utility to mkdocstrings ([#6008](https://github.com/aws-powertools/powertools-lambda-python/issues/6008)) +* **api:** migrating the idempotency utility to mkdocstrings ([#6007](https://github.com/aws-powertools/powertools-lambda-python/issues/6007)) +* **api:** migrating the jmespath utility to mkdocstrings ([#6006](https://github.com/aws-powertools/powertools-lambda-python/issues/6006)) +* **api:** migrating the parameters utility to mkdocstrings ([#6005](https://github.com/aws-powertools/powertools-lambda-python/issues/6005)) +* **api:** migrating the parser utility to mkdocstrings ([#6004](https://github.com/aws-powertools/powertools-lambda-python/issues/6004)) +* **api:** migrating the streaming utility to mkdocstrings ([#6003](https://github.com/aws-powertools/powertools-lambda-python/issues/6003)) +* **api:** migrating the typing utility to mkdocstrings ([#5996](https://github.com/aws-powertools/powertools-lambda-python/issues/5996)) +* **api:** migrating the validation utility to mkdocstrings ([#5972](https://github.com/aws-powertools/powertools-lambda-python/issues/5972)) +* **layer:** update layer version number - v3.5.0 ([#5952](https://github.com/aws-powertools/powertools-lambda-python/issues/5952)) + +## Features + +* **data-masking:** add custom mask functionalities ([#5837](https://github.com/aws-powertools/powertools-lambda-python/issues/5837)) +* **event_source:** add class APIGatewayAuthorizerResponseWebSocket ([#6058](https://github.com/aws-powertools/powertools-lambda-python/issues/6058)) +* **logger:** add clear_state method ([#5956](https://github.com/aws-powertools/powertools-lambda-python/issues/5956)) +* **metrics:** disable metrics flush via environment variables ([#6046](https://github.com/aws-powertools/powertools-lambda-python/issues/6046)) +* **openapi:** enhance support for tuple return type validation ([#5997](https://github.com/aws-powertools/powertools-lambda-python/issues/5997)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.5.1a9 ([#6069](https://github.com/aws-powertools/powertools-lambda-python/issues/6069)) +* **ci:** new pre-release 3.5.1a0 ([#5945](https://github.com/aws-powertools/powertools-lambda-python/issues/5945)) +* **ci:** new pre-release 3.5.1a1 ([#5954](https://github.com/aws-powertools/powertools-lambda-python/issues/5954)) +* **ci:** new pre-release 3.5.1a8 ([#6061](https://github.com/aws-powertools/powertools-lambda-python/issues/6061)) +* **ci:** install & configure mkdocstrings plugin ([#5959](https://github.com/aws-powertools/powertools-lambda-python/issues/5959)) +* **ci:** new pre-release 3.5.1a2 ([#5970](https://github.com/aws-powertools/powertools-lambda-python/issues/5970)) +* **ci:** new pre-release 3.5.1a3 ([#5998](https://github.com/aws-powertools/powertools-lambda-python/issues/5998)) +* **ci:** new pre-release 3.5.1a7 ([#6044](https://github.com/aws-powertools/powertools-lambda-python/issues/6044)) +* **ci:** new pre-release 3.5.1a4 ([#6018](https://github.com/aws-powertools/powertools-lambda-python/issues/6018)) +* **ci:** remove pdoc3 library ([#6024](https://github.com/aws-powertools/powertools-lambda-python/issues/6024)) +* **ci:** new pre-release 3.5.1a5 ([#6026](https://github.com/aws-powertools/powertools-lambda-python/issues/6026)) +* **ci:** add new script to bump Lambda layer version ([#6001](https://github.com/aws-powertools/powertools-lambda-python/issues/6001)) +* **ci:** new pre-release 3.5.1a6 ([#6033](https://github.com/aws-powertools/powertools-lambda-python/issues/6033)) +* **deps:** bump squidfunk/mkdocs-material from `471695f` to `7e841df` in /docs ([#6012](https://github.com/aws-powertools/powertools-lambda-python/issues/6012)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.20 to 3.0.21 ([#6064](https://github.com/aws-powertools/powertools-lambda-python/issues/6064)) +* **deps:** bump actions/setup-python from 5.3.0 to 5.4.0 ([#5960](https://github.com/aws-powertools/powertools-lambda-python/issues/5960)) +* **deps:** bump docker/setup-qemu-action from 3.2.0 to 3.3.0 ([#5961](https://github.com/aws-powertools/powertools-lambda-python/issues/5961)) +* **deps:** bump codecov/codecov-action from 5.1.2 to 5.3.1 ([#5964](https://github.com/aws-powertools/powertools-lambda-python/issues/5964)) +* **deps:** bump squidfunk/mkdocs-material from `7e841df` to `c62453b` in /docs ([#6052](https://github.com/aws-powertools/powertools-lambda-python/issues/6052)) +* **deps:** bump actions/setup-node from 4.1.0 to 4.2.0 ([#5963](https://github.com/aws-powertools/powertools-lambda-python/issues/5963)) +* **deps:** bump actions/upload-artifact from 4.5.0 to 4.6.0 ([#5962](https://github.com/aws-powertools/powertools-lambda-python/issues/5962)) +* **deps:** bump release-drafter/release-drafter from 6.0.0 to 6.1.0 ([#5976](https://github.com/aws-powertools/powertools-lambda-python/issues/5976)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.18 to 3.0.20 ([#5977](https://github.com/aws-powertools/powertools-lambda-python/issues/5977)) +* **deps:** bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4 ([#5980](https://github.com/aws-powertools/powertools-lambda-python/issues/5980)) +* **deps:** bump docker/setup-buildx-action from 3.8.0 to 3.9.0 ([#6042](https://github.com/aws-powertools/powertools-lambda-python/issues/6042)) +* **deps:** bump docker/setup-qemu-action from 3.3.0 to 3.4.0 ([#6043](https://github.com/aws-powertools/powertools-lambda-python/issues/6043)) +* **deps:** bump aws-actions/configure-aws-credentials from 4.0.2 to 4.0.3 ([#5975](https://github.com/aws-powertools/powertools-lambda-python/issues/5975)) +* **deps:** bump squidfunk/mkdocs-material from `41942f7` to `471695f` in /docs ([#5979](https://github.com/aws-powertools/powertools-lambda-python/issues/5979)) +* **deps:** bump actions/setup-go from 5.2.0 to 5.3.0 ([#5978](https://github.com/aws-powertools/powertools-lambda-python/issues/5978)) +* **deps-dev:** bump aws-cdk from 2.178.0 to 2.178.1 ([#6053](https://github.com/aws-powertools/powertools-lambda-python/issues/6053)) +* **deps-dev:** bump mkdocstrings-python from 1.13.0 to 1.14.2 ([#6011](https://github.com/aws-powertools/powertools-lambda-python/issues/6011)) +* **deps-dev:** bump mkdocs-material from 9.6.1 to 9.6.2 ([#6009](https://github.com/aws-powertools/powertools-lambda-python/issues/6009)) +* **deps-dev:** bump aws-cdk-lib from 2.178.0 to 2.178.1 ([#6047](https://github.com/aws-powertools/powertools-lambda-python/issues/6047)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.178.0a0 to 2.178.1a0 ([#6048](https://github.com/aws-powertools/powertools-lambda-python/issues/6048)) +* **deps-dev:** bump boto3-stubs from 1.36.14 to 1.36.15 ([#6049](https://github.com/aws-powertools/powertools-lambda-python/issues/6049)) +* **deps-dev:** bump boto3-stubs from 1.36.10 to 1.36.11 ([#6010](https://github.com/aws-powertools/powertools-lambda-python/issues/6010)) +* **deps-dev:** bump boto3-stubs from 1.36.10 to 1.36.12 ([#6014](https://github.com/aws-powertools/powertools-lambda-python/issues/6014)) +* **deps-dev:** bump ruff from 0.9.5 to 0.9.6 ([#6066](https://github.com/aws-powertools/powertools-lambda-python/issues/6066)) +* **deps-dev:** bump mkdocstrings-python from 1.14.2 to 1.14.4 ([#6025](https://github.com/aws-powertools/powertools-lambda-python/issues/6025)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.177.0a0 to 2.178.0a0 ([#6041](https://github.com/aws-powertools/powertools-lambda-python/issues/6041)) +* **deps-dev:** bump mkdocs-material from 9.5.50 to 9.6.1 ([#5966](https://github.com/aws-powertools/powertools-lambda-python/issues/5966)) +* **deps-dev:** bump black from 24.10.0 to 25.1.0 ([#5968](https://github.com/aws-powertools/powertools-lambda-python/issues/5968)) +* **deps-dev:** bump ruff from 0.9.3 to 0.9.4 ([#5969](https://github.com/aws-powertools/powertools-lambda-python/issues/5969)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.291 to 0.1.292 ([#6051](https://github.com/aws-powertools/powertools-lambda-python/issues/6051)) +* **deps-dev:** bump cfn-lint from 1.22.7 to 1.23.1 ([#5967](https://github.com/aws-powertools/powertools-lambda-python/issues/5967)) +* **deps-dev:** bump mkdocstrings-python from 1.14.5 to 1.14.6 ([#6050](https://github.com/aws-powertools/powertools-lambda-python/issues/6050)) +* **deps-dev:** bump isort from 5.13.2 to 6.0.0 ([#5965](https://github.com/aws-powertools/powertools-lambda-python/issues/5965)) +* **deps-dev:** bump ruff from 0.9.4 to 0.9.5 ([#6039](https://github.com/aws-powertools/powertools-lambda-python/issues/6039)) +* **deps-dev:** bump aws-cdk-lib from 2.177.0 to 2.178.0 ([#6038](https://github.com/aws-powertools/powertools-lambda-python/issues/6038)) +* **deps-dev:** bump mypy from 1.14.1 to 1.15.0 ([#6028](https://github.com/aws-powertools/powertools-lambda-python/issues/6028)) +* **deps-dev:** bump mkdocstrings-python from 1.14.4 to 1.14.5 ([#6032](https://github.com/aws-powertools/powertools-lambda-python/issues/6032)) +* **deps-dev:** bump cfn-lint from 1.23.1 to 1.24.0 ([#6030](https://github.com/aws-powertools/powertools-lambda-python/issues/6030)) +* **deps-dev:** bump boto3-stubs from 1.36.14 to 1.36.16 ([#6057](https://github.com/aws-powertools/powertools-lambda-python/issues/6057)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.290 to 0.1.291 ([#6031](https://github.com/aws-powertools/powertools-lambda-python/issues/6031)) +* **deps-dev:** bump boto3-stubs from 1.36.12 to 1.36.14 ([#6029](https://github.com/aws-powertools/powertools-lambda-python/issues/6029)) +* **deps-dev:** bump mkdocs-material from 9.6.2 to 9.6.3 ([#6065](https://github.com/aws-powertools/powertools-lambda-python/issues/6065)) +* **deps-dev:** bump coverage from 7.6.10 to 7.6.11 ([#6067](https://github.com/aws-powertools/powertools-lambda-python/issues/6067)) +* **deps-dev:** bump aws-cdk from 2.177.0 to 2.178.0 ([#6040](https://github.com/aws-powertools/powertools-lambda-python/issues/6040)) +* **docs:** enable privacy plugin in docs ([#6036](https://github.com/aws-powertools/powertools-lambda-python/issues/6036)) + + + +## [v3.5.0] - 2025-01-28 +## Bug Fixes + +* **event_handler:** fixes typo in variable name `fronzen_openapi_extensions` ([#5929](https://github.com/aws-powertools/powertools-lambda-python/issues/5929)) +* **event_handler:** add tests for PEP 563 compatibility with OpenAPI ([#5886](https://github.com/aws-powertools/powertools-lambda-python/issues/5886)) +* **event_handler:** fix forward references resolution in OpenAPI ([#5885](https://github.com/aws-powertools/powertools-lambda-python/issues/5885)) +* **parser:** make identitySource optional for ApiGatewayAuthorizerRequestV2 model ([#5880](https://github.com/aws-powertools/powertools-lambda-python/issues/5880)) + +## Documentation + +* **data_classes:** improve Event Source Data Classes documentation ([#5916](https://github.com/aws-powertools/powertools-lambda-python/issues/5916)) +* **event_handler:** demonstrate handling optional security routes ([#5895](https://github.com/aws-powertools/powertools-lambda-python/issues/5895)) +* **layer:** update layer version number - v3.4.1 ([#5869](https://github.com/aws-powertools/powertools-lambda-python/issues/5869)) +* **parser:** improve documentation with Pydantic best practices ([#5925](https://github.com/aws-powertools/powertools-lambda-python/issues/5925)) + +## Features + +* **event_source:** add AWS Transfer Family classes ([#5912](https://github.com/aws-powertools/powertools-lambda-python/issues/5912)) +* **idempotency:** add support for custom Idempotency key prefix ([#5898](https://github.com/aws-powertools/powertools-lambda-python/issues/5898)) +* **logger:** add context manager for logger keys ([#5883](https://github.com/aws-powertools/powertools-lambda-python/issues/5883)) +* **parser:** add AWS Transfer Family model ([#5906](https://github.com/aws-powertools/powertools-lambda-python/issues/5906)) + +## Maintenance + +* version bump +* **ci:** adding poetry export plugin to support v2 ([#5941](https://github.com/aws-powertools/powertools-lambda-python/issues/5941)) +* **ci:** adding poetry export plugin to support v2 ([#5938](https://github.com/aws-powertools/powertools-lambda-python/issues/5938)) +* **ci:** adjust token permission ([#5867](https://github.com/aws-powertools/powertools-lambda-python/issues/5867)) +* **ci:** new pre-release 3.4.2a0 ([#5873](https://github.com/aws-powertools/powertools-lambda-python/issues/5873)) +* **ci:** make `pyproject.toml` fully compatible with Poetryv2 ([#5902](https://github.com/aws-powertools/powertools-lambda-python/issues/5902)) +* **ci:** drop support for Python 3.8 ([#5896](https://github.com/aws-powertools/powertools-lambda-python/issues/5896)) +* **ci:** update poetry version to v2 ([#5936](https://github.com/aws-powertools/powertools-lambda-python/issues/5936)) +* **ci:** fix permissions for gh pages ([#5866](https://github.com/aws-powertools/powertools-lambda-python/issues/5866)) +* **deps:** bump pydantic from 2.10.5 to 2.10.6 ([#5918](https://github.com/aws-powertools/powertools-lambda-python/issues/5918)) +* **deps:** bump squidfunk/mkdocs-material from `ba73db5` to `41942f7` in /docs ([#5890](https://github.com/aws-powertools/powertools-lambda-python/issues/5890)) +* **deps-dev:** bump boto3-stubs from 1.36.4 to 1.36.5 ([#5919](https://github.com/aws-powertools/powertools-lambda-python/issues/5919)) +* **deps-dev:** bump boto3-stubs from 1.36.4 to 1.36.6 ([#5923](https://github.com/aws-powertools/powertools-lambda-python/issues/5923)) +* **deps-dev:** bump cfn-lint from 1.22.6 to 1.22.7 ([#5910](https://github.com/aws-powertools/powertools-lambda-python/issues/5910)) +* **deps-dev:** bump testcontainers from 3.7.1 to 4.9.1 ([#5907](https://github.com/aws-powertools/powertools-lambda-python/issues/5907)) +* **deps-dev:** bump pytest-benchmark from 4.0.0 to 5.1.0 ([#5909](https://github.com/aws-powertools/powertools-lambda-python/issues/5909)) +* **deps-dev:** bump aws-cdk from 2.176.0 to 2.177.0 ([#5930](https://github.com/aws-powertools/powertools-lambda-python/issues/5930)) +* **deps-dev:** bump pytest-cov from 5.0.0 to 6.0.0 ([#5908](https://github.com/aws-powertools/powertools-lambda-python/issues/5908)) +* **deps-dev:** bump aws-cdk-lib from 2.176.0 to 2.177.0 ([#5931](https://github.com/aws-powertools/powertools-lambda-python/issues/5931)) +* **deps-dev:** bump cfn-lint from 1.22.5 to 1.22.6 ([#5900](https://github.com/aws-powertools/powertools-lambda-python/issues/5900)) +* **deps-dev:** bump boto3-stubs from 1.36.6 to 1.36.7 ([#5932](https://github.com/aws-powertools/powertools-lambda-python/issues/5932)) +* **deps-dev:** bump boto3-stubs from 1.36.2 to 1.36.3 ([#5894](https://github.com/aws-powertools/powertools-lambda-python/issues/5894)) +* **deps-dev:** bump pytest-asyncio from 0.24.0 to 0.25.2 ([#5920](https://github.com/aws-powertools/powertools-lambda-python/issues/5920)) +* **deps-dev:** bump mkdocs-material from 9.5.49 to 9.5.50 ([#5889](https://github.com/aws-powertools/powertools-lambda-python/issues/5889)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.175.1a0 to 2.176.0a0 ([#5882](https://github.com/aws-powertools/powertools-lambda-python/issues/5882)) +* **deps-dev:** bump boto3-stubs from 1.36.1 to 1.36.2 ([#5881](https://github.com/aws-powertools/powertools-lambda-python/issues/5881)) +* **deps-dev:** bump aws-cdk from 2.175.1 to 2.176.0 ([#5878](https://github.com/aws-powertools/powertools-lambda-python/issues/5878)) +* **deps-dev:** bump ruff from 0.9.1 to 0.9.2 ([#5877](https://github.com/aws-powertools/powertools-lambda-python/issues/5877)) +* **deps-dev:** bump aws-cdk-lib from 2.175.1 to 2.176.0 ([#5876](https://github.com/aws-powertools/powertools-lambda-python/issues/5876)) +* **deps-dev:** bump mypy-boto3-appconfigdata from 1.35.93 to 1.36.0 in the boto-typing group ([#5875](https://github.com/aws-powertools/powertools-lambda-python/issues/5875)) +* **deps-dev:** bump sentry-sdk from 2.19.2 to 2.20.0 ([#5870](https://github.com/aws-powertools/powertools-lambda-python/issues/5870)) +* **deps-dev:** bump boto3-stubs from 1.35.97 to 1.35.99 ([#5874](https://github.com/aws-powertools/powertools-lambda-python/issues/5874)) +* **deps-dev:** bump cfn-lint from 1.22.4 to 1.22.5 ([#5872](https://github.com/aws-powertools/powertools-lambda-python/issues/5872)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.176.0a0 to 2.177.0a0 ([#5933](https://github.com/aws-powertools/powertools-lambda-python/issues/5933)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.289 to 0.1.290 ([#5917](https://github.com/aws-powertools/powertools-lambda-python/issues/5917)) +* **deps-dev:** bump ruff from 0.9.2 to 0.9.3 ([#5911](https://github.com/aws-powertools/powertools-lambda-python/issues/5911)) + + + +## [v3.4.1] - 2025-01-14 +## Bug Fixes + +* **appsync:** enhance consistency for custom resolver field naming in AppSync ([#5801](https://github.com/aws-powertools/powertools-lambda-python/issues/5801)) +* **idempotency:** add support for Optional type when serializing output ([#5590](https://github.com/aws-powertools/powertools-lambda-python/issues/5590)) + +## Documentation + +* **community:** data masking blog post ([#5831](https://github.com/aws-powertools/powertools-lambda-python/issues/5831)) +* **home:** fix date typo and shorten message. ([#5798](https://github.com/aws-powertools/powertools-lambda-python/issues/5798)) +* **layer:** update layer version number - v3.4.0 ([#5785](https://github.com/aws-powertools/powertools-lambda-python/issues/5785)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.4.1a7 ([#5816](https://github.com/aws-powertools/powertools-lambda-python/issues/5816)) +* **ci:** new pre-release 3.4.1a0 ([#5783](https://github.com/aws-powertools/powertools-lambda-python/issues/5783)) +* **ci:** change token permissions ([#5862](https://github.com/aws-powertools/powertools-lambda-python/issues/5862)) +* **ci:** change token permissions / update aws-credentials action ([#5861](https://github.com/aws-powertools/powertools-lambda-python/issues/5861)) +* **ci:** fix dependency resolution ([#5859](https://github.com/aws-powertools/powertools-lambda-python/issues/5859)) +* **ci:** fix dependency resolution ([#5858](https://github.com/aws-powertools/powertools-lambda-python/issues/5858)) +* **ci:** change token permissions ([#5865](https://github.com/aws-powertools/powertools-lambda-python/issues/5865)) +* **ci:** new pre-release 3.4.1a1 ([#5789](https://github.com/aws-powertools/powertools-lambda-python/issues/5789)) +* **ci:** new pre-release 3.4.1a2 ([#5791](https://github.com/aws-powertools/powertools-lambda-python/issues/5791)) +* **ci:** new pre-release 3.4.1a3 ([#5794](https://github.com/aws-powertools/powertools-lambda-python/issues/5794)) +* **ci:** new pre-release 3.4.1a10 ([#5845](https://github.com/aws-powertools/powertools-lambda-python/issues/5845)) +* **ci:** new pre-release 3.4.1a4 ([#5796](https://github.com/aws-powertools/powertools-lambda-python/issues/5796)) +* **ci:** new pre-release 3.4.1a5 ([#5807](https://github.com/aws-powertools/powertools-lambda-python/issues/5807)) +* **ci:** new pre-release 3.4.1a8 ([#5818](https://github.com/aws-powertools/powertools-lambda-python/issues/5818)) +* **ci:** new pre-release 3.4.1a6 ([#5813](https://github.com/aws-powertools/powertools-lambda-python/issues/5813)) +* **ci:** new pre-release 3.4.1a9 ([#5822](https://github.com/aws-powertools/powertools-lambda-python/issues/5822)) +* **deps:** bump pydantic from 2.10.4 to 2.10.5 ([#5848](https://github.com/aws-powertools/powertools-lambda-python/issues/5848)) +* **deps:** bump jinja2 from 3.1.4 to 3.1.5 in /docs ([#5787](https://github.com/aws-powertools/powertools-lambda-python/issues/5787)) +* **deps:** bump pydantic-settings from 2.7.0 to 2.7.1 ([#5815](https://github.com/aws-powertools/powertools-lambda-python/issues/5815)) +* **deps-dev:** bump ruff from 0.8.4 to 0.8.6 ([#5833](https://github.com/aws-powertools/powertools-lambda-python/issues/5833)) +* **deps-dev:** bump boto3-stubs from 1.35.90 to 1.35.92 ([#5827](https://github.com/aws-powertools/powertools-lambda-python/issues/5827)) +* **deps-dev:** bump aws-cdk from 2.173.4 to 2.174.0 ([#5832](https://github.com/aws-powertools/powertools-lambda-python/issues/5832)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.173.2a0 to 2.173.4a0 ([#5811](https://github.com/aws-powertools/powertools-lambda-python/issues/5811)) +* **deps-dev:** bump cfn-lint from 1.22.2 to 1.22.3 ([#5810](https://github.com/aws-powertools/powertools-lambda-python/issues/5810)) +* **deps-dev:** bump boto3-stubs from 1.35.89 to 1.35.90 ([#5809](https://github.com/aws-powertools/powertools-lambda-python/issues/5809)) +* **deps-dev:** bump mypy from 1.14.0 to 1.14.1 ([#5812](https://github.com/aws-powertools/powertools-lambda-python/issues/5812)) +* **deps-dev:** bump boto3-stubs from 1.35.92 to 1.35.93 ([#5835](https://github.com/aws-powertools/powertools-lambda-python/issues/5835)) +* **deps-dev:** bump aws-cdk-lib from 2.173.4 to 2.174.1 ([#5838](https://github.com/aws-powertools/powertools-lambda-python/issues/5838)) +* **deps-dev:** bump mypy-boto3-appconfigdata from 1.35.0 to 1.35.93 in the boto-typing group ([#5840](https://github.com/aws-powertools/powertools-lambda-python/issues/5840)) +* **deps-dev:** bump aws-cdk-lib from 2.173.2 to 2.173.4 ([#5803](https://github.com/aws-powertools/powertools-lambda-python/issues/5803)) +* **deps-dev:** bump aws-cdk from 2.173.2 to 2.173.4 ([#5802](https://github.com/aws-powertools/powertools-lambda-python/issues/5802)) +* **deps-dev:** bump boto3-stubs from 1.35.87 to 1.35.89 ([#5804](https://github.com/aws-powertools/powertools-lambda-python/issues/5804)) +* **deps-dev:** bump jinja2 from 3.1.4 to 3.1.5 ([#5788](https://github.com/aws-powertools/powertools-lambda-python/issues/5788)) +* **deps-dev:** bump aws-cdk from 2.174.0 to 2.174.1 ([#5841](https://github.com/aws-powertools/powertools-lambda-python/issues/5841)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.173.4a0 to 2.174.1a0 ([#5842](https://github.com/aws-powertools/powertools-lambda-python/issues/5842)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.287 to 0.1.288 ([#5793](https://github.com/aws-powertools/powertools-lambda-python/issues/5793)) +* **deps-dev:** bump boto3-stubs from 1.35.93 to 1.35.94 ([#5844](https://github.com/aws-powertools/powertools-lambda-python/issues/5844)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.288 to 0.1.289 ([#5843](https://github.com/aws-powertools/powertools-lambda-python/issues/5843)) +* **deps-dev:** bump boto3-stubs from 1.35.94 to 1.35.95 ([#5847](https://github.com/aws-powertools/powertools-lambda-python/issues/5847)) +* **deps-dev:** bump cfn-lint from 1.22.3 to 1.22.4 ([#5849](https://github.com/aws-powertools/powertools-lambda-python/issues/5849)) +* **deps-dev:** bump boto3-stubs from 1.35.95 to 1.35.96 ([#5850](https://github.com/aws-powertools/powertools-lambda-python/issues/5850)) +* **deps-dev:** bump boto3-stubs from 1.35.96 to 1.35.97 ([#5852](https://github.com/aws-powertools/powertools-lambda-python/issues/5852)) +* **deps-dev:** bump boto3-stubs from 1.35.86 to 1.35.87 ([#5786](https://github.com/aws-powertools/powertools-lambda-python/issues/5786)) +* **deps-dev:** bump aws-cdk from 2.174.1 to 2.175.0 ([#5854](https://github.com/aws-powertools/powertools-lambda-python/issues/5854)) +* **deps-dev:** bump aws-cdk from 2.175.0 to 2.175.1 ([#5863](https://github.com/aws-powertools/powertools-lambda-python/issues/5863)) +* **deps-dev:** bump boto3-stubs from 1.35.85 to 1.35.86 ([#5780](https://github.com/aws-powertools/powertools-lambda-python/issues/5780)) +* **deps-dev:** bump mypy from 1.13.0 to 1.14.0 ([#5779](https://github.com/aws-powertools/powertools-lambda-python/issues/5779)) +* **deps-dev:** bump ruff from 0.8.6 to 0.9.1 ([#5853](https://github.com/aws-powertools/powertools-lambda-python/issues/5853)) +* **deps-dev:** bump aws-cdk-lib from 2.174.1 to 2.175.1 ([#5856](https://github.com/aws-powertools/powertools-lambda-python/issues/5856)) + + + +## [v3.4.0] - 2024-12-20 +## Bug Fixes + +* **ci:** add overwrite to SSM workflow ([#5775](https://github.com/aws-powertools/powertools-lambda-python/issues/5775)) +* **docs:** typo in homepage extra dependencies command ([#5681](https://github.com/aws-powertools/powertools-lambda-python/issues/5681)) +* **openapi:** Allow values of any type in the examples of the Schema Object. ([#5575](https://github.com/aws-powertools/powertools-lambda-python/issues/5575)) +* **parser:** remove AttributeError validation from event_parser function ([#5742](https://github.com/aws-powertools/powertools-lambda-python/issues/5742)) +* **parser:** remove 'aws:' prefix from SelfManagedKafka model ([#5584](https://github.com/aws-powertools/powertools-lambda-python/issues/5584)) + +## Code Refactoring + +* **event_handler:** add type annotations for router decorators ([#5601](https://github.com/aws-powertools/powertools-lambda-python/issues/5601)) +* **event_handler:** add type annotations for `resolve` function ([#5602](https://github.com/aws-powertools/powertools-lambda-python/issues/5602)) + +## Documentation + +* **layer:** update layer version number - v3.3.0 ([#5562](https://github.com/aws-powertools/powertools-lambda-python/issues/5562)) + +## Features + +* **event_handler:** mark API operation as deprecated for OpenAPI documentation ([#5732](https://github.com/aws-powertools/powertools-lambda-python/issues/5732)) +* **event_handler:** add exception handling mechanism for AppSyncResolver ([#5588](https://github.com/aws-powertools/powertools-lambda-python/issues/5588)) +* **event_source:** Extend CodePipeline Artifact Capabilities ([#5448](https://github.com/aws-powertools/powertools-lambda-python/issues/5448)) +* **layer:** add new ap-southeast-5 region ([#5769](https://github.com/aws-powertools/powertools-lambda-python/issues/5769)) +* **metrics:** warn when overwriting dimension ([#5653](https://github.com/aws-powertools/powertools-lambda-python/issues/5653)) +* **parser:** add models for API GW Websockets events ([#5597](https://github.com/aws-powertools/powertools-lambda-python/issues/5597)) +* **ssm:** Parameters for resolving to versioned layers ([#5754](https://github.com/aws-powertools/powertools-lambda-python/issues/5754)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.3.1a14 ([#5713](https://github.com/aws-powertools/powertools-lambda-python/issues/5713)) +* **ci:** new pre-release 3.3.1a21 ([#5773](https://github.com/aws-powertools/powertools-lambda-python/issues/5773)) +* **ci:** new pre-release 3.3.1a0 ([#5565](https://github.com/aws-powertools/powertools-lambda-python/issues/5565)) +* **ci:** new pre-release 3.3.1a1 ([#5577](https://github.com/aws-powertools/powertools-lambda-python/issues/5577)) +* **ci:** disable dry run in layer balancing workflow ([#5768](https://github.com/aws-powertools/powertools-lambda-python/issues/5768)) +* **ci:** new pre-release 3.3.1a20 ([#5766](https://github.com/aws-powertools/powertools-lambda-python/issues/5766)) +* **ci:** new pre-release 3.3.1a10 ([#5679](https://github.com/aws-powertools/powertools-lambda-python/issues/5679)) +* **ci:** add workflow to balance layers per region ([#5752](https://github.com/aws-powertools/powertools-lambda-python/issues/5752)) +* **ci:** new pre-release 3.3.1a9 ([#5668](https://github.com/aws-powertools/powertools-lambda-python/issues/5668)) +* **ci:** new pre-release 3.3.1a19 ([#5757](https://github.com/aws-powertools/powertools-lambda-python/issues/5757)) +* **ci:** new pre-release 3.3.1a8 ([#5663](https://github.com/aws-powertools/powertools-lambda-python/issues/5663)) +* **ci:** adding missing region in matrix ([#5777](https://github.com/aws-powertools/powertools-lambda-python/issues/5777)) +* **ci:** new pre-release 3.3.1a2 ([#5585](https://github.com/aws-powertools/powertools-lambda-python/issues/5585)) +* **ci:** new pre-release 3.3.1a11 ([#5688](https://github.com/aws-powertools/powertools-lambda-python/issues/5688)) +* **ci:** new pre-release 3.3.1a3 ([#5598](https://github.com/aws-powertools/powertools-lambda-python/issues/5598)) +* **ci:** new pre-release 3.3.1a7 ([#5656](https://github.com/aws-powertools/powertools-lambda-python/issues/5656)) +* **ci:** new pre-release 3.3.1a6 ([#5650](https://github.com/aws-powertools/powertools-lambda-python/issues/5650)) +* **ci:** new pre-release 3.3.1a12 ([#5697](https://github.com/aws-powertools/powertools-lambda-python/issues/5697)) +* **ci:** new pre-release 3.3.1a18 ([#5739](https://github.com/aws-powertools/powertools-lambda-python/issues/5739)) +* **ci:** replace closed-issue-message action with powertools action ([#5641](https://github.com/aws-powertools/powertools-lambda-python/issues/5641)) +* **ci:** new pre-release 3.3.1a17 ([#5733](https://github.com/aws-powertools/powertools-lambda-python/issues/5733)) +* **ci:** new pre-release 3.3.1a4 ([#5612](https://github.com/aws-powertools/powertools-lambda-python/issues/5612)) +* **ci:** new pre-release 3.3.1a13 ([#5707](https://github.com/aws-powertools/powertools-lambda-python/issues/5707)) +* **ci:** new pre-release 3.3.1a16 ([#5725](https://github.com/aws-powertools/powertools-lambda-python/issues/5725)) +* **ci:** remove poetry cache in quality check pipeline ([#5626](https://github.com/aws-powertools/powertools-lambda-python/issues/5626)) +* **ci:** revert closed issue action update ([#5637](https://github.com/aws-powertools/powertools-lambda-python/issues/5637)) +* **ci:** new pre-release 3.3.1a15 ([#5720](https://github.com/aws-powertools/powertools-lambda-python/issues/5720)) +* **ci:** new pre-release 3.3.1a5 ([#5639](https://github.com/aws-powertools/powertools-lambda-python/issues/5639)) +* **deps:** bump squidfunk/mkdocs-material from `ef0b45e` to `d063d84` in /docs ([#5649](https://github.com/aws-powertools/powertools-lambda-python/issues/5649)) +* **deps:** bump pydantic from 2.10.0 to 2.10.1 ([#5632](https://github.com/aws-powertools/powertools-lambda-python/issues/5632)) +* **deps:** bump pypa/gh-action-pypi-publish from 1.12.2 to 1.12.3 ([#5709](https://github.com/aws-powertools/powertools-lambda-python/issues/5709)) +* **deps:** bump codecov/codecov-action from 5.0.3 to 5.0.7 ([#5617](https://github.com/aws-powertools/powertools-lambda-python/issues/5617)) +* **deps:** bump actions/dependency-review-action from 4.4.0 to 4.5.0 ([#5616](https://github.com/aws-powertools/powertools-lambda-python/issues/5616)) +* **deps:** bump squidfunk/mkdocs-material from `ce587cb` to `ef0b45e` in /docs ([#5603](https://github.com/aws-powertools/powertools-lambda-python/issues/5603)) +* **deps:** bump squidfunk/mkdocs-material from `d063d84` to `3f571e7` in /docs ([#5678](https://github.com/aws-powertools/powertools-lambda-python/issues/5678)) +* **deps:** bump redis from 5.2.0 to 5.2.1 ([#5701](https://github.com/aws-powertools/powertools-lambda-python/issues/5701)) +* **deps:** bump pydantic-settings from 2.6.1 to 2.7.0 ([#5735](https://github.com/aws-powertools/powertools-lambda-python/issues/5735)) +* **deps:** bump aws-actions/closed-issue-message from 80edfc24bdf1283400eb04d20a8a605ae8bf7d48 to 37548691e7cc75ba58f85c9f873f9eee43590449 ([#5606](https://github.com/aws-powertools/powertools-lambda-python/issues/5606)) +* **deps:** bump pydantic from 2.9.2 to 2.10.0 ([#5611](https://github.com/aws-powertools/powertools-lambda-python/issues/5611)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.17 to 3.0.18 ([#5743](https://github.com/aws-powertools/powertools-lambda-python/issues/5743)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.16 to 3.0.17 ([#5643](https://github.com/aws-powertools/powertools-lambda-python/issues/5643)) +* **deps:** bump squidfunk/mkdocs-material from `3f571e7` to `d485eb6` in /docs ([#5710](https://github.com/aws-powertools/powertools-lambda-python/issues/5710)) +* **deps:** bump codecov/codecov-action from 5.0.7 to 5.1.0 ([#5692](https://github.com/aws-powertools/powertools-lambda-python/issues/5692)) +* **deps:** bump pydantic from 2.10.1 to 2.10.2 ([#5654](https://github.com/aws-powertools/powertools-lambda-python/issues/5654)) +* **deps:** bump squidfunk/mkdocs-material from `d485eb6` to `ba73db5` in /docs ([#5746](https://github.com/aws-powertools/powertools-lambda-python/issues/5746)) +* **deps:** bump docker/setup-buildx-action from 3.7.1 to 3.8.0 ([#5744](https://github.com/aws-powertools/powertools-lambda-python/issues/5744)) +* **deps:** bump datadog-lambda from 6.101.0 to 6.102.0 ([#5570](https://github.com/aws-powertools/powertools-lambda-python/issues/5570)) +* **deps:** bump pydantic from 2.10.2 to 2.10.3 ([#5682](https://github.com/aws-powertools/powertools-lambda-python/issues/5682)) +* **deps:** bump aws-encryption-sdk from 3.3.0 to 4.0.0 ([#5564](https://github.com/aws-powertools/powertools-lambda-python/issues/5564)) +* **deps:** bump pydantic from 2.10.3 to 2.10.4 ([#5760](https://github.com/aws-powertools/powertools-lambda-python/issues/5760)) +* **deps:** bump actions/upload-artifact from 4.4.3 to 4.5.0 ([#5763](https://github.com/aws-powertools/powertools-lambda-python/issues/5763)) +* **deps:** bump codecov/codecov-action from 5.1.1 to 5.1.2 ([#5764](https://github.com/aws-powertools/powertools-lambda-python/issues/5764)) +* **deps:** bump codecov/codecov-action from 4.6.0 to 5.0.2 ([#5567](https://github.com/aws-powertools/powertools-lambda-python/issues/5567)) +* **deps:** bump fastjsonschema from 2.20.0 to 2.21.1 ([#5676](https://github.com/aws-powertools/powertools-lambda-python/issues/5676)) +* **deps:** bump datadog-lambda from 6.102.0 to 6.104.0 ([#5631](https://github.com/aws-powertools/powertools-lambda-python/issues/5631)) +* **deps:** bump codecov/codecov-action from 5.1.0 to 5.1.1 ([#5703](https://github.com/aws-powertools/powertools-lambda-python/issues/5703)) +* **deps:** bump codecov/codecov-action from 5.0.2 to 5.0.3 ([#5592](https://github.com/aws-powertools/powertools-lambda-python/issues/5592)) +* **deps-dev:** bump httpx from 0.27.2 to 0.28.0 ([#5665](https://github.com/aws-powertools/powertools-lambda-python/issues/5665)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.171.0a0 to 2.171.1a0 ([#5666](https://github.com/aws-powertools/powertools-lambda-python/issues/5666)) +* **deps-dev:** bump aws-cdk from 2.171.0 to 2.171.1 ([#5662](https://github.com/aws-powertools/powertools-lambda-python/issues/5662)) +* **deps-dev:** bump aws-cdk-lib from 2.171.0 to 2.171.1 ([#5661](https://github.com/aws-powertools/powertools-lambda-python/issues/5661)) +* **deps-dev:** bump boto3-stubs from 1.35.69 to 1.35.71 ([#5660](https://github.com/aws-powertools/powertools-lambda-python/issues/5660)) +* **deps-dev:** bump cfn-lint from 1.20.0 to 1.20.1 ([#5659](https://github.com/aws-powertools/powertools-lambda-python/issues/5659)) +* **deps-dev:** bump mkdocs-material from 9.5.46 to 9.5.47 ([#5677](https://github.com/aws-powertools/powertools-lambda-python/issues/5677)) +* **deps-dev:** bump cfn-lint from 1.20.1 to 1.20.2 ([#5686](https://github.com/aws-powertools/powertools-lambda-python/issues/5686)) +* **deps-dev:** bump boto3-stubs from 1.35.71 to 1.35.74 ([#5691](https://github.com/aws-powertools/powertools-lambda-python/issues/5691)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.284 to 0.1.285 ([#5642](https://github.com/aws-powertools/powertools-lambda-python/issues/5642)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.170.0a0 to 2.171.0a0 ([#5655](https://github.com/aws-powertools/powertools-lambda-python/issues/5655)) +* **deps-dev:** bump ruff from 0.8.1 to 0.8.2 ([#5693](https://github.com/aws-powertools/powertools-lambda-python/issues/5693)) +* **deps-dev:** bump pytest from 8.3.3 to 8.3.4 ([#5695](https://github.com/aws-powertools/powertools-lambda-python/issues/5695)) +* **deps-dev:** bump mkdocs-material from 9.5.45 to 9.5.46 ([#5645](https://github.com/aws-powertools/powertools-lambda-python/issues/5645)) +* **deps-dev:** bump sentry-sdk from 2.19.0 to 2.19.1 ([#5694](https://github.com/aws-powertools/powertools-lambda-python/issues/5694)) +* **deps-dev:** bump aws-cdk-lib from 2.170.0 to 2.171.0 ([#5647](https://github.com/aws-powertools/powertools-lambda-python/issues/5647)) +* **deps-dev:** bump aws-cdk from 2.170.0 to 2.171.0 ([#5648](https://github.com/aws-powertools/powertools-lambda-python/issues/5648)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.285 to 0.1.287 ([#5685](https://github.com/aws-powertools/powertools-lambda-python/issues/5685)) +* **deps-dev:** bump boto3-stubs from 1.35.67 to 1.35.69 ([#5652](https://github.com/aws-powertools/powertools-lambda-python/issues/5652)) +* **deps-dev:** bump sentry-sdk from 2.19.1 to 2.19.2 ([#5699](https://github.com/aws-powertools/powertools-lambda-python/issues/5699)) +* **deps-dev:** bump ruff from 0.7.4 to 0.8.0 ([#5630](https://github.com/aws-powertools/powertools-lambda-python/issues/5630)) +* **deps-dev:** bump types-python-dateutil from 2.9.0.20241003 to 2.9.0.20241206 ([#5700](https://github.com/aws-powertools/powertools-lambda-python/issues/5700)) +* **deps-dev:** bump httpx from 0.28.0 to 0.28.1 ([#5702](https://github.com/aws-powertools/powertools-lambda-python/issues/5702)) +* **deps-dev:** bump aws-cdk from 2.171.1 to 2.172.0 ([#5712](https://github.com/aws-powertools/powertools-lambda-python/issues/5712)) +* **deps-dev:** bump cfn-lint from 1.20.2 to 1.21.0 ([#5711](https://github.com/aws-powertools/powertools-lambda-python/issues/5711)) +* **deps-dev:** bump boto3-stubs from 1.35.76 to 1.35.77 ([#5716](https://github.com/aws-powertools/powertools-lambda-python/issues/5716)) +* **deps-dev:** bump aws-cdk-lib from 2.171.1 to 2.172.0 ([#5719](https://github.com/aws-powertools/powertools-lambda-python/issues/5719)) +* **deps-dev:** bump cfn-lint from 1.21.0 to 1.22.0 ([#5718](https://github.com/aws-powertools/powertools-lambda-python/issues/5718)) +* **deps-dev:** bump aws-cdk from 2.169.0 to 2.170.0 ([#5628](https://github.com/aws-powertools/powertools-lambda-python/issues/5628)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.167.2a0 to 2.170.0a0 ([#5629](https://github.com/aws-powertools/powertools-lambda-python/issues/5629)) +* **deps-dev:** bump boto3-stubs from 1.35.77 to 1.35.78 ([#5723](https://github.com/aws-powertools/powertools-lambda-python/issues/5723)) +* **deps-dev:** bump sentry-sdk from 2.18.0 to 2.19.0 ([#5633](https://github.com/aws-powertools/powertools-lambda-python/issues/5633)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.171.1a0 to 2.172.0a0 ([#5724](https://github.com/aws-powertools/powertools-lambda-python/issues/5724)) +* **deps-dev:** bump aws-cdk from 2.172.0 to 2.173.0 ([#5727](https://github.com/aws-powertools/powertools-lambda-python/issues/5727)) +* **deps-dev:** bump mkdocs-material from 9.5.44 to 9.5.45 ([#5610](https://github.com/aws-powertools/powertools-lambda-python/issues/5610)) +* **deps-dev:** bump ruff from 0.8.2 to 0.8.3 ([#5728](https://github.com/aws-powertools/powertools-lambda-python/issues/5728)) +* **deps-dev:** bump boto3-stubs from 1.35.64 to 1.35.67 ([#5621](https://github.com/aws-powertools/powertools-lambda-python/issues/5621)) +* **deps-dev:** bump aws-cdk-lib from 2.167.2 to 2.170.0 ([#5622](https://github.com/aws-powertools/powertools-lambda-python/issues/5622)) +* **deps-dev:** bump cfn-lint from 1.22.0 to 1.22.1 ([#5729](https://github.com/aws-powertools/powertools-lambda-python/issues/5729)) +* **deps-dev:** bump aws-cdk from 2.167.2 to 2.169.0 ([#5618](https://github.com/aws-powertools/powertools-lambda-python/issues/5618)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.282 to 0.1.284 ([#5607](https://github.com/aws-powertools/powertools-lambda-python/issues/5607)) +* **deps-dev:** bump boto3-stubs from 1.35.78 to 1.35.80 ([#5730](https://github.com/aws-powertools/powertools-lambda-python/issues/5730)) +* **deps-dev:** bump aws-cdk-lib from 2.172.0 to 2.173.0 ([#5731](https://github.com/aws-powertools/powertools-lambda-python/issues/5731)) +* **deps-dev:** bump mkdocs-material from 9.5.47 to 9.5.48 ([#5717](https://github.com/aws-powertools/powertools-lambda-python/issues/5717)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.172.0a0 to 2.173.0a0 ([#5736](https://github.com/aws-powertools/powertools-lambda-python/issues/5736)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.167.1a0 to 2.167.2a0 ([#5619](https://github.com/aws-powertools/powertools-lambda-python/issues/5619)) +* **deps-dev:** bump boto3-stubs from 1.35.80 to 1.35.81 ([#5750](https://github.com/aws-powertools/powertools-lambda-python/issues/5750)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.281 to 0.1.282 ([#5594](https://github.com/aws-powertools/powertools-lambda-python/issues/5594)) +* **deps-dev:** bump cfn-lint from 1.19.0 to 1.20.0 ([#5595](https://github.com/aws-powertools/powertools-lambda-python/issues/5595)) +* **deps-dev:** bump aws-cdk from 2.167.1 to 2.167.2 ([#5593](https://github.com/aws-powertools/powertools-lambda-python/issues/5593)) +* **deps-dev:** bump cfn-lint from 1.22.1 to 1.22.2 ([#5749](https://github.com/aws-powertools/powertools-lambda-python/issues/5749)) +* **deps-dev:** bump aws-cdk-lib from 2.167.1 to 2.167.2 ([#5596](https://github.com/aws-powertools/powertools-lambda-python/issues/5596)) +* **deps-dev:** bump aws-cdk from 2.173.0 to 2.173.1 ([#5745](https://github.com/aws-powertools/powertools-lambda-python/issues/5745)) +* **deps-dev:** bump boto3-stubs from 1.35.63 to 1.35.64 ([#5582](https://github.com/aws-powertools/powertools-lambda-python/issues/5582)) +* **deps-dev:** bump mkdocs-material from 9.5.48 to 9.5.49 ([#5748](https://github.com/aws-powertools/powertools-lambda-python/issues/5748)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.167.0a0 to 2.167.1a0 ([#5583](https://github.com/aws-powertools/powertools-lambda-python/issues/5583)) +* **deps-dev:** bump aws-cdk-lib from 2.173.0 to 2.173.1 ([#5747](https://github.com/aws-powertools/powertools-lambda-python/issues/5747)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.173.0a0 to 2.173.1a0 ([#5755](https://github.com/aws-powertools/powertools-lambda-python/issues/5755)) +* **deps-dev:** bump aws-cdk from 2.173.1 to 2.173.2 ([#5762](https://github.com/aws-powertools/powertools-lambda-python/issues/5762)) +* **deps-dev:** bump boto3-stubs from 1.35.81 to 1.35.84 ([#5765](https://github.com/aws-powertools/powertools-lambda-python/issues/5765)) +* **deps-dev:** bump boto3-stubs from 1.35.60 to 1.35.63 ([#5581](https://github.com/aws-powertools/powertools-lambda-python/issues/5581)) +* **deps-dev:** bump ruff from 0.8.0 to 0.8.1 ([#5671](https://github.com/aws-powertools/powertools-lambda-python/issues/5671)) +* **deps-dev:** bump aws-cdk from 2.167.0 to 2.167.1 ([#5572](https://github.com/aws-powertools/powertools-lambda-python/issues/5572)) +* **deps-dev:** bump boto3-stubs from 1.35.84 to 1.35.85 ([#5770](https://github.com/aws-powertools/powertools-lambda-python/issues/5770)) +* **deps-dev:** bump ruff from 0.7.3 to 0.7.4 ([#5569](https://github.com/aws-powertools/powertools-lambda-python/issues/5569)) +* **deps-dev:** bump aws-cdk-lib from 2.167.0 to 2.167.1 ([#5568](https://github.com/aws-powertools/powertools-lambda-python/issues/5568)) +* **deps-dev:** bump ruff from 0.8.3 to 0.8.4 ([#5772](https://github.com/aws-powertools/powertools-lambda-python/issues/5772)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.173.1a0 to 2.173.2a0 ([#5771](https://github.com/aws-powertools/powertools-lambda-python/issues/5771)) +* **deps-dev:** bump aws-cdk-lib from 2.173.1 to 2.173.2 ([#5759](https://github.com/aws-powertools/powertools-lambda-python/issues/5759)) +* **layers:** balance Python 3.13 layers in GovCloud partition ([#5579](https://github.com/aws-powertools/powertools-lambda-python/issues/5579)) + + + +## [v3.3.0] - 2024-11-14 +## Bug Fixes + +* **appsync:** make contextual data accessible for async functions ([#5317](https://github.com/aws-powertools/powertools-lambda-python/issues/5317)) +* **ci:** Update output to something easily copy/pasteable ([#5435](https://github.com/aws-powertools/powertools-lambda-python/issues/5435)) +* **ci:** remove space ([#5433](https://github.com/aws-powertools/powertools-lambda-python/issues/5433)) +* **metrics:** add warning for invalid dimension values; prevent their addition to EMF blobs ([#5542](https://github.com/aws-powertools/powertools-lambda-python/issues/5542)) +* **parameters:** fix force_fetch feature when working with get_parameters ([#5515](https://github.com/aws-powertools/powertools-lambda-python/issues/5515)) +* **parser:** support TypeAdapter instances as models ([#5535](https://github.com/aws-powertools/powertools-lambda-python/issues/5535)) + +## Documentation + +* **layer:** update layer version number - v3.2.0 ([#5426](https://github.com/aws-powertools/powertools-lambda-python/issues/5426)) +* **parser:** change parser documentation ([#5262](https://github.com/aws-powertools/powertools-lambda-python/issues/5262)) + +## Features + +* **event_handler:** mutualTLS Security Scheme for OpenAPI ([#5484](https://github.com/aws-powertools/powertools-lambda-python/issues/5484)) +* **layers:** introduce new CDK Python constructor for Powertools Lambda Layer ([#5320](https://github.com/aws-powertools/powertools-lambda-python/issues/5320)) +* **runtime:** add Python 3.13 support ([#5527](https://github.com/aws-powertools/powertools-lambda-python/issues/5527)) + +## Maintenance + +* version bump +* **ci:** Bump CDK version to build layers and fix imports ([#5555](https://github.com/aws-powertools/powertools-lambda-python/issues/5555)) +* **ci:** new pre-release 3.2.1a0 ([#5434](https://github.com/aws-powertools/powertools-lambda-python/issues/5434)) +* **ci:** new pre-release 3.2.1a15 ([#5551](https://github.com/aws-powertools/powertools-lambda-python/issues/5551)) +* **ci:** new pre-release 3.2.1a14 ([#5545](https://github.com/aws-powertools/powertools-lambda-python/issues/5545)) +* **ci:** fix imports to build Lambda layer ([#5557](https://github.com/aws-powertools/powertools-lambda-python/issues/5557)) +* **ci:** new pre-release 3.2.1a1 ([#5443](https://github.com/aws-powertools/powertools-lambda-python/issues/5443)) +* **ci:** bump minimum required pydantic version ([#5446](https://github.com/aws-powertools/powertools-lambda-python/issues/5446)) +* **ci:** new pre-release 3.2.1a2 ([#5456](https://github.com/aws-powertools/powertools-lambda-python/issues/5456)) +* **ci:** new pre-release 3.2.1a12 ([#5524](https://github.com/aws-powertools/powertools-lambda-python/issues/5524)) +* **ci:** new pre-release 3.2.1a3 ([#5465](https://github.com/aws-powertools/powertools-lambda-python/issues/5465)) +* **ci:** new pre-release 3.2.1a4 ([#5470](https://github.com/aws-powertools/powertools-lambda-python/issues/5470)) +* **ci:** new pre-release 3.2.1a5 ([#5473](https://github.com/aws-powertools/powertools-lambda-python/issues/5473)) +* **ci:** new pre-release 3.2.1a11 ([#5517](https://github.com/aws-powertools/powertools-lambda-python/issues/5517)) +* **ci:** new pre-release 3.2.1a6 ([#5480](https://github.com/aws-powertools/powertools-lambda-python/issues/5480)) +* **ci:** new pre-release 3.2.1a7 ([#5488](https://github.com/aws-powertools/powertools-lambda-python/issues/5488)) +* **ci:** new pre-release 3.2.1a10 ([#5509](https://github.com/aws-powertools/powertools-lambda-python/issues/5509)) +* **ci:** new pre-release 3.2.1a8 ([#5497](https://github.com/aws-powertools/powertools-lambda-python/issues/5497)) +* **ci:** new pre-release 3.2.1a9 ([#5504](https://github.com/aws-powertools/powertools-lambda-python/issues/5504)) +* **ci:** new pre-release 3.2.1a13 ([#5537](https://github.com/aws-powertools/powertools-lambda-python/issues/5537)) +* **deps:** bump pypa/gh-action-pypi-publish from 1.10.3 to 1.11.0 ([#5477](https://github.com/aws-powertools/powertools-lambda-python/issues/5477)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.15 to 3.0.16 ([#5499](https://github.com/aws-powertools/powertools-lambda-python/issues/5499)) +* **deps:** bump actions/dependency-review-action from 4.3.4 to 4.3.5 ([#5431](https://github.com/aws-powertools/powertools-lambda-python/issues/5431)) +* **deps:** bump actions/setup-python from 5.2.0 to 5.3.0 ([#5529](https://github.com/aws-powertools/powertools-lambda-python/issues/5529)) +* **deps:** bump datadog-lambda from 6.99.0 to 6.100.0 ([#5491](https://github.com/aws-powertools/powertools-lambda-python/issues/5491)) +* **deps:** bump actions/checkout from 4.2.1 to 4.2.2 ([#5438](https://github.com/aws-powertools/powertools-lambda-python/issues/5438)) +* **deps:** bump actions/checkout from 4.2.0 to 4.2.2 ([#5531](https://github.com/aws-powertools/powertools-lambda-python/issues/5531)) +* **deps:** bump actions/setup-node from 4.0.4 to 4.1.0 ([#5450](https://github.com/aws-powertools/powertools-lambda-python/issues/5450)) +* **deps:** bump squidfunk/mkdocs-material from `2c2802b` to `ce587cb` in /docs ([#5507](https://github.com/aws-powertools/powertools-lambda-python/issues/5507)) +* **deps:** bump actions/setup-python from 5.2.0 to 5.3.0 ([#5449](https://github.com/aws-powertools/powertools-lambda-python/issues/5449)) +* **deps:** bump redis from 5.1.1 to 5.2.0 ([#5454](https://github.com/aws-powertools/powertools-lambda-python/issues/5454)) +* **deps:** bump docker/setup-buildx-action from 2.4.1 to 3.7.1 ([#5530](https://github.com/aws-powertools/powertools-lambda-python/issues/5530)) +* **deps:** bump squidfunk/mkdocs-material from `31eb7f7` to `2c2802b` in /docs ([#5487](https://github.com/aws-powertools/powertools-lambda-python/issues/5487)) +* **deps:** bump docker/setup-qemu-action from 2.1.0 to 3.2.0 ([#5528](https://github.com/aws-powertools/powertools-lambda-python/issues/5528)) +* **deps:** bump actions/dependency-review-action from 4.3.5 to 4.4.0 ([#5469](https://github.com/aws-powertools/powertools-lambda-python/issues/5469)) +* **deps:** bump datadog-lambda from 6.100.0 to 6.101.0 ([#5513](https://github.com/aws-powertools/powertools-lambda-python/issues/5513)) +* **deps:** bump pypa/gh-action-pypi-publish from 1.11.0 to 1.12.1 ([#5514](https://github.com/aws-powertools/powertools-lambda-python/issues/5514)) +* **deps:** bump pypa/gh-action-pypi-publish from 1.12.1 to 1.12.2 ([#5519](https://github.com/aws-powertools/powertools-lambda-python/issues/5519)) +* **deps-dev:** bump sentry-sdk from 2.17.0 to 2.18.0 ([#5502](https://github.com/aws-powertools/powertools-lambda-python/issues/5502)) +* **deps-dev:** bump boto3-stubs from 1.35.51 to 1.35.52 ([#5478](https://github.com/aws-powertools/powertools-lambda-python/issues/5478)) +* **deps-dev:** bump mkdocs-material from 9.5.43 to 9.5.44 ([#5506](https://github.com/aws-powertools/powertools-lambda-python/issues/5506)) +* **deps-dev:** bump cfn-lint from 1.18.2 to 1.18.3 ([#5479](https://github.com/aws-powertools/powertools-lambda-python/issues/5479)) +* **deps-dev:** bump boto3-stubs from 1.35.49 to 1.35.51 ([#5472](https://github.com/aws-powertools/powertools-lambda-python/issues/5472)) +* **deps-dev:** bump aws-cdk from 2.165.0 to 2.166.0 ([#5520](https://github.com/aws-powertools/powertools-lambda-python/issues/5520)) +* **deps-dev:** bump aws-cdk-lib from 2.165.0 to 2.166.0 ([#5522](https://github.com/aws-powertools/powertools-lambda-python/issues/5522)) +* **deps-dev:** bump boto3-stubs from 1.35.52 to 1.35.53 ([#5485](https://github.com/aws-powertools/powertools-lambda-python/issues/5485)) +* **deps-dev:** bump cfn-lint from 1.18.1 to 1.18.2 ([#5468](https://github.com/aws-powertools/powertools-lambda-python/issues/5468)) +* **deps-dev:** bump boto3-stubs from 1.35.54 to 1.35.56 ([#5523](https://github.com/aws-powertools/powertools-lambda-python/issues/5523)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.163.1a0 to 2.164.1a0 ([#5467](https://github.com/aws-powertools/powertools-lambda-python/issues/5467)) +* **deps-dev:** bump mkdocs-material from 9.5.42 to 9.5.43 ([#5486](https://github.com/aws-powertools/powertools-lambda-python/issues/5486)) +* **deps-dev:** bump aws-cdk from 2.164.0 to 2.164.1 ([#5462](https://github.com/aws-powertools/powertools-lambda-python/issues/5462)) +* **deps-dev:** bump boto3-stubs from 1.35.46 to 1.35.49 ([#5460](https://github.com/aws-powertools/powertools-lambda-python/issues/5460)) +* **deps-dev:** bump aws-cdk-lib from 2.164.0 to 2.164.1 ([#5459](https://github.com/aws-powertools/powertools-lambda-python/issues/5459)) +* **deps-dev:** bump ruff from 0.7.0 to 0.7.1 ([#5451](https://github.com/aws-powertools/powertools-lambda-python/issues/5451)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.278 to 0.1.279 ([#5512](https://github.com/aws-powertools/powertools-lambda-python/issues/5512)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.165.0a0 to 2.166.0a0 ([#5533](https://github.com/aws-powertools/powertools-lambda-python/issues/5533)) +* **deps-dev:** bump aws-cdk-lib from 2.163.1 to 2.164.0 ([#5453](https://github.com/aws-powertools/powertools-lambda-python/issues/5453)) +* **deps-dev:** bump aws-cdk from 2.163.1 to 2.164.0 ([#5452](https://github.com/aws-powertools/powertools-lambda-python/issues/5452)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.279 to 0.1.281 ([#5548](https://github.com/aws-powertools/powertools-lambda-python/issues/5548)) +* **deps-dev:** bump aws-cdk-lib from 2.164.1 to 2.165.0 ([#5490](https://github.com/aws-powertools/powertools-lambda-python/issues/5490)) +* **deps-dev:** bump boto3-stubs from 1.35.53 to 1.35.54 ([#5493](https://github.com/aws-powertools/powertools-lambda-python/issues/5493)) +* **deps-dev:** bump aws-cdk from 2.164.1 to 2.165.0 ([#5494](https://github.com/aws-powertools/powertools-lambda-python/issues/5494)) +* **deps-dev:** bump mypy from 1.11.2 to 1.13.0 ([#5440](https://github.com/aws-powertools/powertools-lambda-python/issues/5440)) +* **deps-dev:** bump ruff from 0.7.2 to 0.7.3 ([#5532](https://github.com/aws-powertools/powertools-lambda-python/issues/5532)) +* **deps-dev:** bump boto3-stubs from 1.35.56 to 1.35.58 ([#5540](https://github.com/aws-powertools/powertools-lambda-python/issues/5540)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.162.1a0 to 2.163.1a0 ([#5441](https://github.com/aws-powertools/powertools-lambda-python/issues/5441)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.277 to 0.1.278 ([#5439](https://github.com/aws-powertools/powertools-lambda-python/issues/5439)) +* **deps-dev:** bump cfn-lint from 1.18.3 to 1.18.4 ([#5501](https://github.com/aws-powertools/powertools-lambda-python/issues/5501)) +* **deps-dev:** bump cfn-lint from 1.18.4 to 1.19.0 ([#5544](https://github.com/aws-powertools/powertools-lambda-python/issues/5544)) +* **deps-dev:** bump ruff from 0.7.1 to 0.7.2 ([#5492](https://github.com/aws-powertools/powertools-lambda-python/issues/5492)) +* **deps-dev:** bump aws-cdk-lib from 2.162.1 to 2.163.1 ([#5429](https://github.com/aws-powertools/powertools-lambda-python/issues/5429)) +* **deps-dev:** bump boto3-stubs from 1.35.45 to 1.35.46 ([#5430](https://github.com/aws-powertools/powertools-lambda-python/issues/5430)) +* **deps-dev:** bump aws-cdk from 2.162.1 to 2.163.1 ([#5432](https://github.com/aws-powertools/powertools-lambda-python/issues/5432)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.164.1a0 to 2.165.0a0 ([#5500](https://github.com/aws-powertools/powertools-lambda-python/issues/5500)) +* **deps-dev:** bump xenon from 0.9.1 to 0.9.3 ([#5428](https://github.com/aws-powertools/powertools-lambda-python/issues/5428)) +* **deps-dev:** bump boto3-stubs from 1.35.58 to 1.35.59 ([#5549](https://github.com/aws-powertools/powertools-lambda-python/issues/5549)) +* **layers:** add pydantic-settings package to v3 Layer ([#5516](https://github.com/aws-powertools/powertools-lambda-python/issues/5516)) + + + +## [v3.2.0] - 2024-10-22 +## Bug Fixes + +* test command in verify step ([#5381](https://github.com/aws-powertools/powertools-lambda-python/issues/5381)) +* **ci:** Tables are nicer ([#5416](https://github.com/aws-powertools/powertools-lambda-python/issues/5416)) +* **ci:** GovCloud layer verification ([#5382](https://github.com/aws-powertools/powertools-lambda-python/issues/5382)) +* **ci:** Update partition name ([#5380](https://github.com/aws-powertools/powertools-lambda-python/issues/5380)) +* **layer:** update partition name in the GovCloud workflow ([#5379](https://github.com/aws-powertools/powertools-lambda-python/issues/5379)) + +## Documentation + +* Add GovCloud layer info ([#5414](https://github.com/aws-powertools/powertools-lambda-python/issues/5414)) +* **event_handler:** add Terraform payload info for API Gateway HTTP API ([#5351](https://github.com/aws-powertools/powertools-lambda-python/issues/5351)) +* **examples:** temporarily fix SAR version to v2.x ([#5360](https://github.com/aws-powertools/powertools-lambda-python/issues/5360)) +* **layer:** update layer version number ([#5344](https://github.com/aws-powertools/powertools-lambda-python/issues/5344)) +* **upgrade_guide:** update Lambda layer name ([#5347](https://github.com/aws-powertools/powertools-lambda-python/issues/5347)) + +## Features + +* **ci:** GovCloud Layer Workflow ([#5261](https://github.com/aws-powertools/powertools-lambda-python/issues/5261)) +* **logger:** add thread safe logging keys ([#5141](https://github.com/aws-powertools/powertools-lambda-python/issues/5141)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.1.1a0 ([#5353](https://github.com/aws-powertools/powertools-lambda-python/issues/5353)) +* **ci:** Add dump of govcloud layer info in verify step ([#5415](https://github.com/aws-powertools/powertools-lambda-python/issues/5415)) +* **deps:** bump squidfunk/mkdocs-material from `f9cb76d` to `0d4e687` in /docs ([#5395](https://github.com/aws-powertools/powertools-lambda-python/issues/5395)) +* **deps:** bump actions/upload-artifact from 4.4.1 to 4.4.3 ([#5357](https://github.com/aws-powertools/powertools-lambda-python/issues/5357)) +* **deps:** bump squidfunk/mkdocs-material from `8e8b333` to `f9cb76d` in /docs ([#5366](https://github.com/aws-powertools/powertools-lambda-python/issues/5366)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.14 to 3.0.15 ([#5418](https://github.com/aws-powertools/powertools-lambda-python/issues/5418)) +* **deps:** bump jsonpath-ng from 1.6.1 to 1.7.0 ([#5369](https://github.com/aws-powertools/powertools-lambda-python/issues/5369)) +* **deps:** bump squidfunk/mkdocs-material from `0d4e687` to `31eb7f7` in /docs ([#5417](https://github.com/aws-powertools/powertools-lambda-python/issues/5417)) +* **deps:** bump actions/upload-artifact from 4.4.0 to 4.4.3 ([#5373](https://github.com/aws-powertools/powertools-lambda-python/issues/5373)) +* **deps-dev:** bump boto3-stubs from 1.35.38 to 1.35.39 ([#5370](https://github.com/aws-powertools/powertools-lambda-python/issues/5370)) +* **deps-dev:** bump boto3-stubs from 1.35.39 to 1.35.41 ([#5392](https://github.com/aws-powertools/powertools-lambda-python/issues/5392)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.161.1a0 to 2.162.1a0 ([#5386](https://github.com/aws-powertools/powertools-lambda-python/issues/5386)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.274 to 0.1.275 ([#5406](https://github.com/aws-powertools/powertools-lambda-python/issues/5406)) +* **deps-dev:** bump boto3-stubs from 1.35.43 to 1.35.44 ([#5407](https://github.com/aws-powertools/powertools-lambda-python/issues/5407)) +* **deps-dev:** bump cfn-lint from 1.17.2 to 1.18.1 ([#5423](https://github.com/aws-powertools/powertools-lambda-python/issues/5423)) +* **deps-dev:** bump cfn-lint from 1.17.1 to 1.17.2 ([#5408](https://github.com/aws-powertools/powertools-lambda-python/issues/5408)) +* **deps-dev:** bump aws-cdk-lib from 2.161.1 to 2.162.1 ([#5371](https://github.com/aws-powertools/powertools-lambda-python/issues/5371)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.273 to 0.1.274 ([#5394](https://github.com/aws-powertools/powertools-lambda-python/issues/5394)) +* **deps-dev:** bump aws-cdk from 2.161.1 to 2.162.1 ([#5372](https://github.com/aws-powertools/powertools-lambda-python/issues/5372)) +* **deps-dev:** bump boto3-stubs from 1.35.41 to 1.35.42 ([#5397](https://github.com/aws-powertools/powertools-lambda-python/issues/5397)) +* **deps-dev:** bump cfn-lint from 1.16.1 to 1.17.1 ([#5404](https://github.com/aws-powertools/powertools-lambda-python/issues/5404)) +* **deps-dev:** bump mkdocs-material from 9.5.40 to 9.5.41 ([#5393](https://github.com/aws-powertools/powertools-lambda-python/issues/5393)) +* **deps-dev:** bump cfn-lint from 1.16.0 to 1.16.1 ([#5363](https://github.com/aws-powertools/powertools-lambda-python/issues/5363)) +* **deps-dev:** bump boto3-stubs from 1.35.37 to 1.35.38 ([#5364](https://github.com/aws-powertools/powertools-lambda-python/issues/5364)) +* **deps-dev:** bump mkdocs-material from 9.5.39 to 9.5.40 ([#5365](https://github.com/aws-powertools/powertools-lambda-python/issues/5365)) +* **deps-dev:** bump ruff from 0.6.9 to 0.7.0 ([#5403](https://github.com/aws-powertools/powertools-lambda-python/issues/5403)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.275 to 0.1.277 ([#5419](https://github.com/aws-powertools/powertools-lambda-python/issues/5419)) +* **deps-dev:** bump boto3-stubs from 1.35.42 to 1.35.43 ([#5402](https://github.com/aws-powertools/powertools-lambda-python/issues/5402)) +* **deps-dev:** bump boto3-stubs from 1.35.36 to 1.35.37 ([#5356](https://github.com/aws-powertools/powertools-lambda-python/issues/5356)) +* **deps-dev:** bump nox from 2024.4.15 to 2024.10.9 ([#5355](https://github.com/aws-powertools/powertools-lambda-python/issues/5355)) +* **deps-dev:** bump mkdocs-material from 9.5.41 to 9.5.42 ([#5420](https://github.com/aws-powertools/powertools-lambda-python/issues/5420)) +* **deps-dev:** bump boto3-stubs from 1.35.44 to 1.35.45 ([#5421](https://github.com/aws-powertools/powertools-lambda-python/issues/5421)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.161.0a0 to 2.161.1a0 ([#5349](https://github.com/aws-powertools/powertools-lambda-python/issues/5349)) +* **deps-dev:** bump boto3-stubs from 1.35.35 to 1.35.36 ([#5350](https://github.com/aws-powertools/powertools-lambda-python/issues/5350)) +* **deps-dev:** bump sentry-sdk from 2.15.0 to 2.16.0 ([#5348](https://github.com/aws-powertools/powertools-lambda-python/issues/5348)) +* **deps-dev:** bump sentry-sdk from 2.16.0 to 2.17.0 ([#5400](https://github.com/aws-powertools/powertools-lambda-python/issues/5400)) +* **docs:** remove layer callout from data masking docs ([#5377](https://github.com/aws-powertools/powertools-lambda-python/issues/5377)) + + + +## [v3.1.0] - 2024-10-08 +## Bug Fixes + +* **ci:** Layer Rename Fix ([#5291](https://github.com/aws-powertools/powertools-lambda-python/issues/5291)) +* **ci:** layer rename ([#5283](https://github.com/aws-powertools/powertools-lambda-python/issues/5283)) +* **idempotency:** fix response hook invocation when function returns None ([#5251](https://github.com/aws-powertools/powertools-lambda-python/issues/5251)) +* **layer:** reverting SSM parameter name ([#5340](https://github.com/aws-powertools/powertools-lambda-python/issues/5340)) +* **layers:** rename Lambda layer name from x86 to x86_64 ([#5226](https://github.com/aws-powertools/powertools-lambda-python/issues/5226)) +* **parser:** fallback to `validate_python` when using `type[Model]` and nested models ([#5313](https://github.com/aws-powertools/powertools-lambda-python/issues/5313)) +* **parser:** revert a regression in v3 when raising ValidationError ([#5259](https://github.com/aws-powertools/powertools-lambda-python/issues/5259)) +* **parser:** make size and etag optional for LifecycleExpiration events in S3 ([#5250](https://github.com/aws-powertools/powertools-lambda-python/issues/5250)) + +## Code Refactoring + +* **examples:** fix issues reported by SonarCloud and Scorecard ([#5315](https://github.com/aws-powertools/powertools-lambda-python/issues/5315)) + +## Documentation + +* **idempotency:** fix description in `Advanced` table ([#5191](https://github.com/aws-powertools/powertools-lambda-python/issues/5191)) +* **metrics:** fix test references ([#5265](https://github.com/aws-powertools/powertools-lambda-python/issues/5265)) +* **public_reference:** add Flyweight as a public reference ([#5322](https://github.com/aws-powertools/powertools-lambda-python/issues/5322)) +* **upgrade_guide:** update upgrade guide with Pydantic information ([#5316](https://github.com/aws-powertools/powertools-lambda-python/issues/5316)) +* **v3:** fix small things in the documentation ([#5224](https://github.com/aws-powertools/powertools-lambda-python/issues/5224)) +* **versioning:** add v2 maintainance mode banner ([#5240](https://github.com/aws-powertools/powertools-lambda-python/issues/5240)) + +## Features + +* **event_source:** add CodeDeploy Lifecycle Hook event ([#5219](https://github.com/aws-powertools/powertools-lambda-python/issues/5219)) +* **openapi:** enable direct list input in Examples model ([#5318](https://github.com/aws-powertools/powertools-lambda-python/issues/5318)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.0.1a7 ([#5299](https://github.com/aws-powertools/powertools-lambda-python/issues/5299)) +* **ci:** new pre-release 3.0.1a3 ([#5270](https://github.com/aws-powertools/powertools-lambda-python/issues/5270)) +* **ci:** new pre-release 3.0.1a4 ([#5277](https://github.com/aws-powertools/powertools-lambda-python/issues/5277)) +* **ci:** new pre-release 3.0.1a2 ([#5258](https://github.com/aws-powertools/powertools-lambda-python/issues/5258)) +* **ci:** new pre-release 3.0.1a5 ([#5288](https://github.com/aws-powertools/powertools-lambda-python/issues/5288)) +* **ci:** new pre-release 3.0.1a9 ([#5337](https://github.com/aws-powertools/powertools-lambda-python/issues/5337)) +* **ci:** new pre-release 3.0.1a8 ([#5323](https://github.com/aws-powertools/powertools-lambda-python/issues/5323)) +* **ci:** new pre-release 3.0.1a0 ([#5220](https://github.com/aws-powertools/powertools-lambda-python/issues/5220)) +* **ci:** new pre-release 3.0.1a1 ([#5247](https://github.com/aws-powertools/powertools-lambda-python/issues/5247)) +* **ci:** new pre-release 3.0.1a6 ([#5293](https://github.com/aws-powertools/powertools-lambda-python/issues/5293)) +* **deps:** bump actions/download-artifact from 4.1.7 to 4.1.8 ([#5203](https://github.com/aws-powertools/powertools-lambda-python/issues/5203)) +* **deps:** bump squidfunk/mkdocs-material from `22a429f` to `08fbf58` in /docs ([#5243](https://github.com/aws-powertools/powertools-lambda-python/issues/5243)) +* **deps:** bump docker/setup-buildx-action from 3.6.1 to 3.7.0 ([#5298](https://github.com/aws-powertools/powertools-lambda-python/issues/5298)) +* **deps:** bump actions/checkout from 4.1.7 to 4.2.0 ([#5244](https://github.com/aws-powertools/powertools-lambda-python/issues/5244)) +* **deps:** bump actions/setup-node from 4.0.3 to 4.0.4 ([#5186](https://github.com/aws-powertools/powertools-lambda-python/issues/5186)) +* **deps:** bump docker/setup-buildx-action from 3.7.0 to 3.7.1 ([#5310](https://github.com/aws-powertools/powertools-lambda-python/issues/5310)) +* **deps:** bump pypa/gh-action-pypi-publish from 1.10.2 to 1.10.3 ([#5311](https://github.com/aws-powertools/powertools-lambda-python/issues/5311)) +* **deps:** bump squidfunk/mkdocs-material from `a2e3a31` to `22a429f` in /docs ([#5201](https://github.com/aws-powertools/powertools-lambda-python/issues/5201)) +* **deps:** bump pypa/gh-action-pypi-publish from 1.10.1 to 1.10.2 ([#5202](https://github.com/aws-powertools/powertools-lambda-python/issues/5202)) +* **deps:** bump actions/checkout from 4.2.0 to 4.2.1 ([#5329](https://github.com/aws-powertools/powertools-lambda-python/issues/5329)) +* **deps:** bump squidfunk/mkdocs-material from `08fbf58` to `7aea359` in /docs ([#5253](https://github.com/aws-powertools/powertools-lambda-python/issues/5253)) +* **deps:** bump actions/setup-python from 5.1.0 to 5.2.0 ([#5204](https://github.com/aws-powertools/powertools-lambda-python/issues/5204)) +* **deps:** bump codecov/codecov-action from 4.5.0 to 4.6.0 ([#5287](https://github.com/aws-powertools/powertools-lambda-python/issues/5287)) +* **deps:** bump redis from 5.1.0 to 5.1.1 ([#5331](https://github.com/aws-powertools/powertools-lambda-python/issues/5331)) +* **deps:** bump actions/checkout from 4.1.6 to 4.1.7 ([#5206](https://github.com/aws-powertools/powertools-lambda-python/issues/5206)) +* **deps:** bump actions/upload-artifact from 4.4.0 to 4.4.1 ([#5328](https://github.com/aws-powertools/powertools-lambda-python/issues/5328)) +* **deps:** bump actions/upload-artifact from 4.3.3 to 4.4.0 ([#5217](https://github.com/aws-powertools/powertools-lambda-python/issues/5217)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.12 to 3.0.13 ([#5276](https://github.com/aws-powertools/powertools-lambda-python/issues/5276)) +* **deps:** bump redis from 5.0.8 to 5.1.0 ([#5264](https://github.com/aws-powertools/powertools-lambda-python/issues/5264)) +* **deps:** bump datadog-lambda from 6.98.0 to 6.99.0 ([#5333](https://github.com/aws-powertools/powertools-lambda-python/issues/5333)) +* **deps:** bump squidfunk/mkdocs-material from `7aea359` to `8e8b333` in /docs ([#5272](https://github.com/aws-powertools/powertools-lambda-python/issues/5272)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.13 to 3.0.14 ([#5330](https://github.com/aws-powertools/powertools-lambda-python/issues/5330)) +* **deps:** bump docker/setup-qemu-action from 3.0.0 to 3.2.0 ([#5205](https://github.com/aws-powertools/powertools-lambda-python/issues/5205)) +* **deps-dev:** bump mkdocs-material from 9.5.38 to 9.5.39 ([#5273](https://github.com/aws-powertools/powertools-lambda-python/issues/5273)) +* **deps-dev:** bump cfn-lint from 1.15.1 to 1.15.2 ([#5274](https://github.com/aws-powertools/powertools-lambda-python/issues/5274)) +* **deps-dev:** bump boto3-stubs from 1.35.28 to 1.35.29 ([#5263](https://github.com/aws-powertools/powertools-lambda-python/issues/5263)) +* **deps-dev:** bump boto3-stubs from 1.35.34 to 1.35.35 ([#5334](https://github.com/aws-powertools/powertools-lambda-python/issues/5334)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.270 to 0.1.271 ([#5284](https://github.com/aws-powertools/powertools-lambda-python/issues/5284)) +* **deps-dev:** bump mkdocs-material from 9.5.37 to 9.5.38 ([#5255](https://github.com/aws-powertools/powertools-lambda-python/issues/5255)) +* **deps-dev:** bump ruff from 0.6.7 to 0.6.8 ([#5254](https://github.com/aws-powertools/powertools-lambda-python/issues/5254)) +* **deps-dev:** bump boto3-stubs from 1.35.27 to 1.35.28 ([#5256](https://github.com/aws-powertools/powertools-lambda-python/issues/5256)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.269 to 0.1.270 ([#5257](https://github.com/aws-powertools/powertools-lambda-python/issues/5257)) +* **deps-dev:** bump sentry-sdk from 2.14.0 to 2.15.0 ([#5285](https://github.com/aws-powertools/powertools-lambda-python/issues/5285)) +* **deps-dev:** bump boto3-stubs from 1.35.29 to 1.35.31 ([#5286](https://github.com/aws-powertools/powertools-lambda-python/issues/5286)) +* **deps-dev:** bump boto3-stubs from 1.35.31 to 1.35.32 ([#5292](https://github.com/aws-powertools/powertools-lambda-python/issues/5292)) +* **deps-dev:** bump aws-cdk-lib from 2.161.0 to 2.161.1 ([#5335](https://github.com/aws-powertools/powertools-lambda-python/issues/5335)) +* **deps-dev:** bump boto3-stubs from 1.35.32 to 1.35.33 ([#5295](https://github.com/aws-powertools/powertools-lambda-python/issues/5295)) +* **deps-dev:** bump types-python-dateutil from 2.9.0.20240906 to 2.9.0.20241003 ([#5296](https://github.com/aws-powertools/powertools-lambda-python/issues/5296)) +* **deps-dev:** bump boto3-stubs from 1.35.26 to 1.35.27 ([#5242](https://github.com/aws-powertools/powertools-lambda-python/issues/5242)) +* **deps-dev:** bump mkdocs-material from 9.5.36 to 9.5.37 ([#5241](https://github.com/aws-powertools/powertools-lambda-python/issues/5241)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.271 to 0.1.272 ([#5297](https://github.com/aws-powertools/powertools-lambda-python/issues/5297)) +* **deps-dev:** bump boto3-stubs from 1.35.25 to 1.35.26 ([#5234](https://github.com/aws-powertools/powertools-lambda-python/issues/5234)) +* **deps-dev:** bump aws-cdk from 2.159.1 to 2.160.0 ([#5233](https://github.com/aws-powertools/powertools-lambda-python/issues/5233)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.159.1a0 to 2.160.0a0 ([#5235](https://github.com/aws-powertools/powertools-lambda-python/issues/5235)) +* **deps-dev:** bump aws-cdk-lib from 2.159.1 to 2.160.0 ([#5230](https://github.com/aws-powertools/powertools-lambda-python/issues/5230)) +* **deps-dev:** bump cfn-lint from 1.15.0 to 1.15.1 ([#5232](https://github.com/aws-powertools/powertools-lambda-python/issues/5232)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.158.0a0 to 2.159.1a0 ([#5231](https://github.com/aws-powertools/powertools-lambda-python/issues/5231)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.268 to 0.1.269 ([#5229](https://github.com/aws-powertools/powertools-lambda-python/issues/5229)) +* **deps-dev:** bump aws-cdk-lib from 2.160.0 to 2.161.0 ([#5304](https://github.com/aws-powertools/powertools-lambda-python/issues/5304)) +* **deps-dev:** bump boto3-stubs from 1.35.33 to 1.35.34 ([#5306](https://github.com/aws-powertools/powertools-lambda-python/issues/5306)) +* **deps-dev:** bump types-redis from 4.6.0.20240903 to 4.6.0.20241004 ([#5307](https://github.com/aws-powertools/powertools-lambda-python/issues/5307)) +* **deps-dev:** bump aws-cdk-lib from 2.158.0 to 2.159.1 ([#5208](https://github.com/aws-powertools/powertools-lambda-python/issues/5208)) +* **deps-dev:** bump ruff from 0.6.4 to 0.6.7 ([#5207](https://github.com/aws-powertools/powertools-lambda-python/issues/5207)) +* **deps-dev:** bump aws-cdk from 2.157.0 to 2.159.1 ([#5194](https://github.com/aws-powertools/powertools-lambda-python/issues/5194)) +* **deps-dev:** bump aws-cdk from 2.160.0 to 2.161.0 ([#5309](https://github.com/aws-powertools/powertools-lambda-python/issues/5309)) +* **deps-dev:** bump ruff from 0.6.8 to 0.6.9 ([#5308](https://github.com/aws-powertools/powertools-lambda-python/issues/5308)) +* **deps-dev:** bump cfn-lint from 1.15.2 to 1.16.0 ([#5305](https://github.com/aws-powertools/powertools-lambda-python/issues/5305)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.160.0a0 to 2.161.0a0 ([#5332](https://github.com/aws-powertools/powertools-lambda-python/issues/5332)) +* **deps-dev:** bump aws-cdk from 2.161.0 to 2.161.1 ([#5327](https://github.com/aws-powertools/powertools-lambda-python/issues/5327)) +* **deps-dev:** bump mkdocs-material from 9.5.34 to 9.5.36 ([#5210](https://github.com/aws-powertools/powertools-lambda-python/issues/5210)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.272 to 0.1.273 ([#5336](https://github.com/aws-powertools/powertools-lambda-python/issues/5336)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.264 to 0.1.268 ([#5216](https://github.com/aws-powertools/powertools-lambda-python/issues/5216)) +* **deps-dev:** bump multiprocess from 0.70.16 to 0.70.17 ([#5275](https://github.com/aws-powertools/powertools-lambda-python/issues/5275)) +* **deps-dev:** bump boto3-stubs from 1.35.17 to 1.35.25 ([#5218](https://github.com/aws-powertools/powertools-lambda-python/issues/5218)) +* **deps-dev:** bump bandit from 1.7.9 to 1.7.10 ([#5214](https://github.com/aws-powertools/powertools-lambda-python/issues/5214)) +* **deps-dev:** bump cfn-lint from 1.12.4 to 1.15.0 ([#5215](https://github.com/aws-powertools/powertools-lambda-python/issues/5215)) +* **docs:** recreate requirements.txt file for mkdocs container ([#5246](https://github.com/aws-powertools/powertools-lambda-python/issues/5246)) +* **tests:** fix e2e tests in Idempotency utility ([#5280](https://github.com/aws-powertools/powertools-lambda-python/issues/5280)) + + + +## [v3.0.0] - 2024-09-23 +## Bug Fixes + +* **v3:** revert unnecessary changes that impacts v3 ([#5087](https://github.com/aws-powertools/powertools-lambda-python/issues/5087)) + +## Code Refactoring + +* **batch:** add from __future__ import annotations ([#4993](https://github.com/aws-powertools/powertools-lambda-python/issues/4993)) +* **batch_processing:** mark batch_processor and async_batch_processor as deprecated ([#4910](https://github.com/aws-powertools/powertools-lambda-python/issues/4910)) +* **data_classes:** add from __future__ import annotations ([#4939](https://github.com/aws-powertools/powertools-lambda-python/issues/4939)) +* **data_masking:** add from __future__ import annotations ([#4945](https://github.com/aws-powertools/powertools-lambda-python/issues/4945)) +* **event_handler:** add from __future__ import annotations ([#4992](https://github.com/aws-powertools/powertools-lambda-python/issues/4992)) +* **event_handler:** add from __future__ import annotations in the Middlewares ([#4975](https://github.com/aws-powertools/powertools-lambda-python/issues/4975)) +* **feature_flags:** add from __future__ import annotations ([#4960](https://github.com/aws-powertools/powertools-lambda-python/issues/4960)) +* **general:** drop pydantic v1 ([#4305](https://github.com/aws-powertools/powertools-lambda-python/issues/4305)) +* **idempotency:** add from __future__ import annotations ([#4961](https://github.com/aws-powertools/powertools-lambda-python/issues/4961)) +* **jmespath_utils:** deprecate extract_data_from_envelope in favor of query ([#4907](https://github.com/aws-powertools/powertools-lambda-python/issues/4907)) +* **jmespath_utils:** add from __future__ import annotations ([#4962](https://github.com/aws-powertools/powertools-lambda-python/issues/4962)) +* **logging:** add from __future__ import annotations ([#4940](https://github.com/aws-powertools/powertools-lambda-python/issues/4940)) +* **metrics:** add from __future__ import annotations ([#4944](https://github.com/aws-powertools/powertools-lambda-python/issues/4944)) +* **middleware_factory:** add from __future__ import annotations ([#4941](https://github.com/aws-powertools/powertools-lambda-python/issues/4941)) +* **openapi:** add from __future__ import annotations ([#4990](https://github.com/aws-powertools/powertools-lambda-python/issues/4990)) +* **parameters:** deprecate the config parameter in favor of boto_config ([#4893](https://github.com/aws-powertools/powertools-lambda-python/issues/4893)) +* **parameters:** add top-level get_multiple method in SSMProvider class ([#4785](https://github.com/aws-powertools/powertools-lambda-python/issues/4785)) +* **parameters:** add from __future__ import annotations ([#4976](https://github.com/aws-powertools/powertools-lambda-python/issues/4976)) +* **parameters:** increase default max_age (cache) to 5 minutes ([#4279](https://github.com/aws-powertools/powertools-lambda-python/issues/4279)) +* **parser:** add from __future__ import annotations ([#4977](https://github.com/aws-powertools/powertools-lambda-python/issues/4977)) +* **parser:** add from __future__ import annotations ([#4983](https://github.com/aws-powertools/powertools-lambda-python/issues/4983)) +* **shared:** add from __future__ import annotations ([#4942](https://github.com/aws-powertools/powertools-lambda-python/issues/4942)) +* **streaming:** add from __future__ import annotations ([#4987](https://github.com/aws-powertools/powertools-lambda-python/issues/4987)) +* **tracing:** add from __future__ import annotations ([#4943](https://github.com/aws-powertools/powertools-lambda-python/issues/4943)) +* **typing:** add from __future__ import annotations ([#4985](https://github.com/aws-powertools/powertools-lambda-python/issues/4985)) +* **typing:** enable TCH, UP and FA100 ruff rules ([#5017](https://github.com/aws-powertools/powertools-lambda-python/issues/5017)) +* **typing:** reduce aws_lambda_powertools.shared.types usage ([#4896](https://github.com/aws-powertools/powertools-lambda-python/issues/4896)) +* **typing:** enable boto3 implicit type annotations ([#4692](https://github.com/aws-powertools/powertools-lambda-python/issues/4692)) +* **typing:** move more types into TYPE_CHECKING ([#5088](https://github.com/aws-powertools/powertools-lambda-python/issues/5088)) +* **validation:** add from __future__ import annotations ([#4984](https://github.com/aws-powertools/powertools-lambda-python/issues/4984)) + +## Documentation + +* **upgrade_guide:** create upgrade guide from v2 to v3 ([#5028](https://github.com/aws-powertools/powertools-lambda-python/issues/5028)) + +## Features + +* **data_classes:** return empty dict or list instead of None ([#4606](https://github.com/aws-powertools/powertools-lambda-python/issues/4606)) +* **event_handler:** Ensure Bedrock Agents resolver works with Pydantic v2 ([#5156](https://github.com/aws-powertools/powertools-lambda-python/issues/5156)) +* **idempotency:** simplify access to expiration time in `DataRecord` class ([#5082](https://github.com/aws-powertools/powertools-lambda-python/issues/5082)) +* **lambda-layer:** add pipeline to build Lambda layer in v3 ([#4826](https://github.com/aws-powertools/powertools-lambda-python/issues/4826)) +* **parser:** Adds DDB deserialization to DynamoDBStreamChangedRecordModel ([#4401](https://github.com/aws-powertools/powertools-lambda-python/issues/4401)) +* **parser:** Allow primitive data types to be parsed using TypeAdapter ([#4502](https://github.com/aws-powertools/powertools-lambda-python/issues/4502)) +* **v3:** merging develop into v3 ([#5160](https://github.com/aws-powertools/powertools-lambda-python/issues/5160)) + +## Maintenance + +* version bump +* **ci:** fix bump poetry version ([#5211](https://github.com/aws-powertools/powertools-lambda-python/issues/5211)) +* **ci:** fix working-directory in v3 layer pipeline ([#5199](https://github.com/aws-powertools/powertools-lambda-python/issues/5199)) +* **ci:** fix Redis e2e tests in v3 branch ([#4852](https://github.com/aws-powertools/powertools-lambda-python/issues/4852)) +* **ci:** fix e2e tests in v3 branch ([#4848](https://github.com/aws-powertools/powertools-lambda-python/issues/4848)) +* **ci:** add the aws-encryption-sdk dependency in the Lambda layer ([#4630](https://github.com/aws-powertools/powertools-lambda-python/issues/4630)) +* **ci:** bump pydantic library to 2.0+ and boto3 to 1.34.32 ([#4235](https://github.com/aws-powertools/powertools-lambda-python/issues/4235)) +* **v3:** merging develop into v3 - 15/05/2024 ([#4335](https://github.com/aws-powertools/powertools-lambda-python/issues/4335)) +* **v3:** merging develop into v3 ([#4267](https://github.com/aws-powertools/powertools-lambda-python/issues/4267)) + + + +## [v2.43.1] - 2024-08-12 +## Bug Fixes + +* **event_source:** fix regression when working with zero numbers in DynamoDBStreamEvent ([#4932](https://github.com/aws-powertools/powertools-lambda-python/issues/4932)) + +## Maintenance + +* version bump +* **ci:** new pre-release 2.43.1a0 ([#4920](https://github.com/aws-powertools/powertools-lambda-python/issues/4920)) +* **ci:** new pre-release 2.43.1a1 ([#4926](https://github.com/aws-powertools/powertools-lambda-python/issues/4926)) +* **ci:** new pre-release 2.42.1a9 ([#4912](https://github.com/aws-powertools/powertools-lambda-python/issues/4912)) +* **deps-dev:** bump ruff from 0.5.6 to 0.5.7 ([#4918](https://github.com/aws-powertools/powertools-lambda-python/issues/4918)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.234 to 0.1.238 ([#4917](https://github.com/aws-powertools/powertools-lambda-python/issues/4917)) +* **deps-dev:** bump mypy-boto3-ssm from 1.34.132 to 1.34.158 in the boto-typing group ([#4921](https://github.com/aws-powertools/powertools-lambda-python/issues/4921)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.238 to 0.1.242 ([#4922](https://github.com/aws-powertools/powertools-lambda-python/issues/4922)) +* **deps-dev:** bump cfn-lint from 1.9.6 to 1.9.7 ([#4923](https://github.com/aws-powertools/powertools-lambda-python/issues/4923)) +* **deps-dev:** bump cfn-lint from 1.9.5 to 1.9.6 ([#4916](https://github.com/aws-powertools/powertools-lambda-python/issues/4916)) + + + +## [v2.43.0] - 2024-08-08 +## Bug Fixes + +* **data_class:** ensure DynamoDBStreamEvent conforms to decimal limits ([#4863](https://github.com/aws-powertools/powertools-lambda-python/issues/4863)) + +## Code Refactoring + +* **test:** make CORS test consistent with expected behavior ([#4882](https://github.com/aws-powertools/powertools-lambda-python/issues/4882)) +* **tracer:** make capture_lambda_handler type more generic ([#4796](https://github.com/aws-powertools/powertools-lambda-python/issues/4796)) + +## Documentation + +* fix type vs. field in comment ([#4832](https://github.com/aws-powertools/powertools-lambda-python/issues/4832)) +* **public_reference:** add CHS Inc. as a public reference ([#4885](https://github.com/aws-powertools/powertools-lambda-python/issues/4885)) +* **public_reference:** add LocalStack as a public reference ([#4858](https://github.com/aws-powertools/powertools-lambda-python/issues/4858)) +* **public_reference:** add Caylent as a public reference ([#4822](https://github.com/aws-powertools/powertools-lambda-python/issues/4822)) + +## Features + +* **metrics:** add unit None for CloudWatch EMF Metrics ([#4904](https://github.com/aws-powertools/powertools-lambda-python/issues/4904)) +* **validation:** returns output from validate function ([#4839](https://github.com/aws-powertools/powertools-lambda-python/issues/4839)) + +## Maintenance + +* version bump +* **ci:** new pre-release 2.42.1a5 ([#4868](https://github.com/aws-powertools/powertools-lambda-python/issues/4868)) +* **ci:** new pre-release 2.42.1a8 ([#4903](https://github.com/aws-powertools/powertools-lambda-python/issues/4903)) +* **ci:** new pre-release 2.42.1a0 ([#4827](https://github.com/aws-powertools/powertools-lambda-python/issues/4827)) +* **ci:** new pre-release 2.42.1a7 ([#4894](https://github.com/aws-powertools/powertools-lambda-python/issues/4894)) +* **ci:** new pre-release 2.42.1a1 ([#4837](https://github.com/aws-powertools/powertools-lambda-python/issues/4837)) +* **ci:** new pre-release 2.42.1a3 ([#4856](https://github.com/aws-powertools/powertools-lambda-python/issues/4856)) +* **ci:** new pre-release 2.42.1a4 ([#4864](https://github.com/aws-powertools/powertools-lambda-python/issues/4864)) +* **ci:** new pre-release 2.42.1a6 ([#4884](https://github.com/aws-powertools/powertools-lambda-python/issues/4884)) +* **ci:** new pre-release 2.42.1a2 ([#4847](https://github.com/aws-powertools/powertools-lambda-python/issues/4847)) +* **deps:** bump golang.org/x/sync from 0.7.0 to 0.8.0 in /layer/scripts/layer-balancer in the layer-balancer group ([#4892](https://github.com/aws-powertools/powertools-lambda-python/issues/4892)) +* **deps:** bump actions/upload-artifact from 4.3.5 to 4.3.6 ([#4901](https://github.com/aws-powertools/powertools-lambda-python/issues/4901)) +* **deps:** bump actions/upload-artifact from 4.3.4 to 4.3.5 ([#4871](https://github.com/aws-powertools/powertools-lambda-python/issues/4871)) +* **deps:** bump ossf/scorecard-action from 2.3.3 to 2.4.0 ([#4829](https://github.com/aws-powertools/powertools-lambda-python/issues/4829)) +* **deps:** bump squidfunk/mkdocs-material from `257eca8` to `9919d6e` in /docs ([#4878](https://github.com/aws-powertools/powertools-lambda-python/issues/4878)) +* **deps:** bump docker/setup-buildx-action from 3.5.0 to 3.6.1 ([#4844](https://github.com/aws-powertools/powertools-lambda-python/issues/4844)) +* **deps:** bump redis from 5.0.7 to 5.0.8 ([#4854](https://github.com/aws-powertools/powertools-lambda-python/issues/4854)) +* **deps-dev:** bump ruff from 0.5.5 to 0.5.6 ([#4874](https://github.com/aws-powertools/powertools-lambda-python/issues/4874)) +* **deps-dev:** bump mypy-boto3-cloudwatch from 1.34.83 to 1.34.153 in the boto-typing group ([#4887](https://github.com/aws-powertools/powertools-lambda-python/issues/4887)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.224 to 0.1.228 ([#4867](https://github.com/aws-powertools/powertools-lambda-python/issues/4867)) +* **deps-dev:** bump cfn-lint from 1.9.1 to 1.9.3 ([#4866](https://github.com/aws-powertools/powertools-lambda-python/issues/4866)) +* **deps-dev:** bump sentry-sdk from 2.11.0 to 2.12.0 ([#4861](https://github.com/aws-powertools/powertools-lambda-python/issues/4861)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.228 to 0.1.230 ([#4876](https://github.com/aws-powertools/powertools-lambda-python/issues/4876)) +* **deps-dev:** bump black from 24.4.2 to 24.8.0 ([#4873](https://github.com/aws-powertools/powertools-lambda-python/issues/4873)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.223 to 0.1.224 ([#4855](https://github.com/aws-powertools/powertools-lambda-python/issues/4855)) +* **deps-dev:** bump mypy-boto3-logs from 1.34.66 to 1.34.151 in the boto-typing group ([#4853](https://github.com/aws-powertools/powertools-lambda-python/issues/4853)) +* **deps-dev:** bump coverage from 7.6.0 to 7.6.1 ([#4888](https://github.com/aws-powertools/powertools-lambda-python/issues/4888)) +* **deps-dev:** bump cfn-lint from 1.8.2 to 1.9.1 ([#4851](https://github.com/aws-powertools/powertools-lambda-python/issues/4851)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.150.0a0 to 2.151.0a0 ([#4889](https://github.com/aws-powertools/powertools-lambda-python/issues/4889)) +* **deps-dev:** bump aws-cdk from 2.150.0 to 2.151.0 ([#4872](https://github.com/aws-powertools/powertools-lambda-python/issues/4872)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.219 to 0.1.222 ([#4836](https://github.com/aws-powertools/powertools-lambda-python/issues/4836)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.222 to 0.1.223 ([#4843](https://github.com/aws-powertools/powertools-lambda-python/issues/4843)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.233 to 0.1.234 ([#4909](https://github.com/aws-powertools/powertools-lambda-python/issues/4909)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.230 to 0.1.231 ([#4891](https://github.com/aws-powertools/powertools-lambda-python/issues/4891)) +* **deps-dev:** bump cfn-lint from 1.9.3 to 1.9.5 ([#4890](https://github.com/aws-powertools/powertools-lambda-python/issues/4890)) +* **deps-dev:** bump pytest from 8.3.1 to 8.3.2 ([#4824](https://github.com/aws-powertools/powertools-lambda-python/issues/4824)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.231 to 0.1.233 ([#4900](https://github.com/aws-powertools/powertools-lambda-python/issues/4900)) +* **deps-dev:** bump mkdocs-material from 9.5.30 to 9.5.31 ([#4877](https://github.com/aws-powertools/powertools-lambda-python/issues/4877)) +* **deps-dev:** bump types-redis from 4.6.0.20240425 to 4.6.0.20240726 ([#4831](https://github.com/aws-powertools/powertools-lambda-python/issues/4831)) +* **deps-dev:** bump ruff from 0.5.4 to 0.5.5 ([#4823](https://github.com/aws-powertools/powertools-lambda-python/issues/4823)) +* **deps-dev:** bump aws-cdk-lib from 2.150.0 to 2.151.0 ([#4875](https://github.com/aws-powertools/powertools-lambda-python/issues/4875)) +* **deps-dev:** bump types-redis from 4.6.0.20240726 to 4.6.0.20240806 ([#4899](https://github.com/aws-powertools/powertools-lambda-python/issues/4899)) +* **maintenance:** add Banxware customer refernece ([#4841](https://github.com/aws-powertools/powertools-lambda-python/issues/4841)) + + + +## [v2.42.0] - 2024-07-25 +## Bug Fixes + +* **idempotency:** ensure in_progress_expiration field is set on Lambda timeout. ([#4773](https://github.com/aws-powertools/powertools-lambda-python/issues/4773)) + +## Documentation + +* **idempotency:** improve navigation, wording, and new section on guarantees ([#4613](https://github.com/aws-powertools/powertools-lambda-python/issues/4613)) + +## Features + +* **event_handler:** add OpenAPI extensions ([#4703](https://github.com/aws-powertools/powertools-lambda-python/issues/4703)) + +## Maintenance + +* version bump +* **ci:** new pre-release 2.41.1a4 ([#4772](https://github.com/aws-powertools/powertools-lambda-python/issues/4772)) +* **ci:** new pre-release 2.41.1a0 ([#4749](https://github.com/aws-powertools/powertools-lambda-python/issues/4749)) +* **ci:** new pre-release 2.41.1a1 ([#4756](https://github.com/aws-powertools/powertools-lambda-python/issues/4756)) +* **ci:** new pre-release 2.41.1a2 ([#4758](https://github.com/aws-powertools/powertools-lambda-python/issues/4758)) +* **ci:** new pre-release 2.41.1a9 ([#4808](https://github.com/aws-powertools/powertools-lambda-python/issues/4808)) +* **ci:** new pre-release 2.41.1a3 ([#4766](https://github.com/aws-powertools/powertools-lambda-python/issues/4766)) +* **ci:** new pre-release 2.41.1a8 ([#4802](https://github.com/aws-powertools/powertools-lambda-python/issues/4802)) +* **ci:** new pre-release 2.41.1a5 ([#4777](https://github.com/aws-powertools/powertools-lambda-python/issues/4777)) +* **ci:** new pre-release 2.41.1a6 ([#4783](https://github.com/aws-powertools/powertools-lambda-python/issues/4783)) +* **ci:** new pre-release 2.41.1a7 ([#4792](https://github.com/aws-powertools/powertools-lambda-python/issues/4792)) +* **deps:** bump github.com/aws/aws-sdk-go-v2/config from 1.27.26 to 1.27.27 in /layer/scripts/layer-balancer in the layer-balancer group ([#4779](https://github.com/aws-powertools/powertools-lambda-python/issues/4779)) +* **deps:** bump aws-actions/closed-issue-message from 8b6324312193476beecf11f8e8539d73a3553bf4 to 80edfc24bdf1283400eb04d20a8a605ae8bf7d48 ([#4786](https://github.com/aws-powertools/powertools-lambda-python/issues/4786)) +* **deps:** bump actions/dependency-review-action from 4.3.3 to 4.3.4 ([#4753](https://github.com/aws-powertools/powertools-lambda-python/issues/4753)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#4745](https://github.com/aws-powertools/powertools-lambda-python/issues/4745)) +* **deps:** bump datadog-lambda from 6.96.0 to 6.97.0 ([#4770](https://github.com/aws-powertools/powertools-lambda-python/issues/4770)) +* **deps:** bump docker/setup-buildx-action from 3.4.0 to 3.5.0 ([#4801](https://github.com/aws-powertools/powertools-lambda-python/issues/4801)) +* **deps:** bump docker/setup-qemu-action from 3.1.0 to 3.2.0 ([#4800](https://github.com/aws-powertools/powertools-lambda-python/issues/4800)) +* **deps-dev:** bump cfn-lint from 1.8.1 to 1.8.2 ([#4788](https://github.com/aws-powertools/powertools-lambda-python/issues/4788)) +* **deps-dev:** bump pytest-asyncio from 0.23.7 to 0.23.8 ([#4776](https://github.com/aws-powertools/powertools-lambda-python/issues/4776)) +* **deps-dev:** bump pytest from 8.2.2 to 8.3.1 ([#4799](https://github.com/aws-powertools/powertools-lambda-python/issues/4799)) +* **deps-dev:** bump aws-cdk-lib from 2.148.1 to 2.150.0 ([#4806](https://github.com/aws-powertools/powertools-lambda-python/issues/4806)) +* **deps-dev:** bump ruff from 0.5.3 to 0.5.4 ([#4798](https://github.com/aws-powertools/powertools-lambda-python/issues/4798)) +* **deps-dev:** bump cfn-lint from 1.6.1 to 1.8.1 ([#4780](https://github.com/aws-powertools/powertools-lambda-python/issues/4780)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.211 to 0.1.212 ([#4769](https://github.com/aws-powertools/powertools-lambda-python/issues/4769)) +* **deps-dev:** bump ruff from 0.5.2 to 0.5.3 ([#4781](https://github.com/aws-powertools/powertools-lambda-python/issues/4781)) +* **deps-dev:** bump mkdocs-material from 9.5.28 to 9.5.29 ([#4764](https://github.com/aws-powertools/powertools-lambda-python/issues/4764)) +* **deps-dev:** bump aws-cdk from 2.148.0 to 2.149.0 ([#4765](https://github.com/aws-powertools/powertools-lambda-python/issues/4765)) +* **deps-dev:** bump ruff from 0.5.1 to 0.5.2 ([#4762](https://github.com/aws-powertools/powertools-lambda-python/issues/4762)) +* **deps-dev:** bump sentry-sdk from 2.9.0 to 2.10.0 ([#4763](https://github.com/aws-powertools/powertools-lambda-python/issues/4763)) +* **deps-dev:** bump aws-cdk from 2.149.0 to 2.150.0 ([#4805](https://github.com/aws-powertools/powertools-lambda-python/issues/4805)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.207 to 0.1.211 ([#4760](https://github.com/aws-powertools/powertools-lambda-python/issues/4760)) +* **deps-dev:** bump mypy-boto3-dynamodb from 1.34.131 to 1.34.148 in the boto-typing group ([#4812](https://github.com/aws-powertools/powertools-lambda-python/issues/4812)) +* **deps-dev:** bump sentry-sdk from 2.10.0 to 2.11.0 ([#4815](https://github.com/aws-powertools/powertools-lambda-python/issues/4815)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.212 to 0.1.219 ([#4817](https://github.com/aws-powertools/powertools-lambda-python/issues/4817)) +* **deps-dev:** bump cfn-lint from 1.6.0 to 1.6.1 ([#4751](https://github.com/aws-powertools/powertools-lambda-python/issues/4751)) +* **deps-dev:** bump mkdocs-material from 9.5.29 to 9.5.30 ([#4807](https://github.com/aws-powertools/powertools-lambda-python/issues/4807)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.148.1a0 to 2.150.0a0 ([#4813](https://github.com/aws-powertools/powertools-lambda-python/issues/4813)) +* **deps-dev:** bump cfn-lint from 1.5.3 to 1.6.0 ([#4747](https://github.com/aws-powertools/powertools-lambda-python/issues/4747)) +* **deps-dev:** bump coverage from 7.5.4 to 7.6.0 ([#4746](https://github.com/aws-powertools/powertools-lambda-python/issues/4746)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.206 to 0.1.207 ([#4748](https://github.com/aws-powertools/powertools-lambda-python/issues/4748)) +* **deps-dev:** bump mypy-boto3-secretsmanager from 1.34.128 to 1.34.145 in the boto-typing group ([#4787](https://github.com/aws-powertools/powertools-lambda-python/issues/4787)) +* **docs:** Add lambda layer policy to versioning docs ([#4811](https://github.com/aws-powertools/powertools-lambda-python/issues/4811)) +* **logger:** use package logger over source logger to reduce noise ([#4793](https://github.com/aws-powertools/powertools-lambda-python/issues/4793)) + + + +## [v2.41.0] - 2024-07-11 +## Bug Fixes + +* **event_handler:** make the max_age attribute comply with RFC specification ([#4731](https://github.com/aws-powertools/powertools-lambda-python/issues/4731)) +* **event_handler:** disable allow-credentials header when origin allow_origin is * ([#4638](https://github.com/aws-powertools/powertools-lambda-python/issues/4638)) +* **event_handler:** convert null body to empty string in ALBResolver to avoid HTTP 502 ([#4683](https://github.com/aws-powertools/powertools-lambda-python/issues/4683)) +* **event_handler:** custom serializer recursive values when using data validation ([#4664](https://github.com/aws-powertools/powertools-lambda-python/issues/4664)) + +## Documentation + +* **i-made-this:** Bedrock agents with Powertools for AWS Lambda ([#4705](https://github.com/aws-powertools/powertools-lambda-python/issues/4705)) +* **public_reference:** add BusPatrol as a public reference ([#4713](https://github.com/aws-powertools/powertools-lambda-python/issues/4713)) + +## Features + +* **batch:** add option to not raise `BatchProcessingError` exception when the entire batch fails ([#4719](https://github.com/aws-powertools/powertools-lambda-python/issues/4719)) +* **feature_flags:** allow customers to bring their own boto3 client and session ([#4717](https://github.com/aws-powertools/powertools-lambda-python/issues/4717)) +* **parser:** add support for API Gateway Lambda authorizer events ([#4718](https://github.com/aws-powertools/powertools-lambda-python/issues/4718)) + +## Maintenance + +* version bump +* Add token to codecov action ([#4682](https://github.com/aws-powertools/powertools-lambda-python/issues/4682)) +* **ci:** new pre-release 2.40.2a5 ([#4706](https://github.com/aws-powertools/powertools-lambda-python/issues/4706)) +* **ci:** new pre-release 2.40.2a0 ([#4665](https://github.com/aws-powertools/powertools-lambda-python/issues/4665)) +* **ci:** new pre-release 2.40.2a8 ([#4737](https://github.com/aws-powertools/powertools-lambda-python/issues/4737)) +* **ci:** new pre-release 2.40.2a7 ([#4726](https://github.com/aws-powertools/powertools-lambda-python/issues/4726)) +* **ci:** new pre-release 2.40.2a1 ([#4669](https://github.com/aws-powertools/powertools-lambda-python/issues/4669)) +* **ci:** new pre-release 2.40.2a2 ([#4679](https://github.com/aws-powertools/powertools-lambda-python/issues/4679)) +* **ci:** new pre-release 2.40.2a3 ([#4688](https://github.com/aws-powertools/powertools-lambda-python/issues/4688)) +* **ci:** new pre-release 2.40.2a6 ([#4715](https://github.com/aws-powertools/powertools-lambda-python/issues/4715)) +* **ci:** new pre-release 2.40.2a4 ([#4694](https://github.com/aws-powertools/powertools-lambda-python/issues/4694)) +* **deps:** bump docker/setup-qemu-action from 3.0.0 to 3.1.0 ([#4685](https://github.com/aws-powertools/powertools-lambda-python/issues/4685)) +* **deps:** bump actions/setup-python from 5.1.0 to 5.1.1 ([#4732](https://github.com/aws-powertools/powertools-lambda-python/issues/4732)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#4733](https://github.com/aws-powertools/powertools-lambda-python/issues/4733)) +* **deps:** bump actions/upload-artifact from 4.3.3 to 4.3.4 ([#4698](https://github.com/aws-powertools/powertools-lambda-python/issues/4698)) +* **deps:** bump actions/download-artifact from 4.1.7 to 4.1.8 ([#4699](https://github.com/aws-powertools/powertools-lambda-python/issues/4699)) +* **deps:** bump actions/setup-node from 4.0.2 to 4.0.3 ([#4725](https://github.com/aws-powertools/powertools-lambda-python/issues/4725)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.9 to 3.0.10 ([#4678](https://github.com/aws-powertools/powertools-lambda-python/issues/4678)) +* **deps:** bump docker/setup-buildx-action from 3.3.0 to 3.4.0 ([#4693](https://github.com/aws-powertools/powertools-lambda-python/issues/4693)) +* **deps:** bump zipp from 3.17.0 to 3.19.1 in /docs ([#4720](https://github.com/aws-powertools/powertools-lambda-python/issues/4720)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#4659](https://github.com/aws-powertools/powertools-lambda-python/issues/4659)) +* **deps:** bump certifi from 2024.6.2 to 2024.7.4 ([#4700](https://github.com/aws-powertools/powertools-lambda-python/issues/4700)) +* **deps:** bump github.com/aws/aws-sdk-go-v2/config from 1.27.23 to 1.27.24 in /layer/scripts/layer-balancer in the layer-balancer group ([#4684](https://github.com/aws-powertools/powertools-lambda-python/issues/4684)) +* **deps-dev:** bump mkdocs-material from 9.5.27 to 9.5.28 ([#4676](https://github.com/aws-powertools/powertools-lambda-python/issues/4676)) +* **deps-dev:** bump cfn-lint from 1.4.2 to 1.5.0 ([#4675](https://github.com/aws-powertools/powertools-lambda-python/issues/4675)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.147.3a0 to 2.148.0a0 ([#4722](https://github.com/aws-powertools/powertools-lambda-python/issues/4722)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.200 to 0.1.201 ([#4687](https://github.com/aws-powertools/powertools-lambda-python/issues/4687)) +* **deps-dev:** bump aws-cdk-lib from 2.147.2 to 2.147.3 ([#4674](https://github.com/aws-powertools/powertools-lambda-python/issues/4674)) +* **deps-dev:** bump zipp from 3.17.0 to 3.19.1 in /layer ([#4721](https://github.com/aws-powertools/powertools-lambda-python/issues/4721)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.202 to 0.1.205 ([#4723](https://github.com/aws-powertools/powertools-lambda-python/issues/4723)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.147.2a0 to 2.147.3a0 ([#4686](https://github.com/aws-powertools/powertools-lambda-python/issues/4686)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.199 to 0.1.200 ([#4677](https://github.com/aws-powertools/powertools-lambda-python/issues/4677)) +* **deps-dev:** bump aws-cdk-lib from 2.147.3 to 2.148.0 ([#4710](https://github.com/aws-powertools/powertools-lambda-python/issues/4710)) +* **deps-dev:** bump aws-cdk from 2.147.2 to 2.147.3 ([#4672](https://github.com/aws-powertools/powertools-lambda-python/issues/4672)) +* **deps-dev:** bump mypy-boto3-s3 from 1.34.120 to 1.34.138 in the boto-typing group ([#4673](https://github.com/aws-powertools/powertools-lambda-python/issues/4673)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.201 to 0.1.202 ([#4696](https://github.com/aws-powertools/powertools-lambda-python/issues/4696)) +* **deps-dev:** bump cfn-lint from 1.5.1 to 1.5.2 ([#4724](https://github.com/aws-powertools/powertools-lambda-python/issues/4724)) +* **deps-dev:** bump ruff from 0.5.0 to 0.5.1 ([#4697](https://github.com/aws-powertools/powertools-lambda-python/issues/4697)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.198 to 0.1.199 ([#4668](https://github.com/aws-powertools/powertools-lambda-python/issues/4668)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.147.1a0 to 2.147.2a0 ([#4667](https://github.com/aws-powertools/powertools-lambda-python/issues/4667)) +* **deps-dev:** bump aws-cdk from 2.147.3 to 2.148.0 ([#4708](https://github.com/aws-powertools/powertools-lambda-python/issues/4708)) +* **deps-dev:** bump cfn-lint from 1.5.2 to 1.5.3 ([#4734](https://github.com/aws-powertools/powertools-lambda-python/issues/4734)) +* **deps-dev:** bump sentry-sdk from 2.8.0 to 2.9.0 ([#4735](https://github.com/aws-powertools/powertools-lambda-python/issues/4735)) +* **deps-dev:** bump cfn-lint from 1.4.1 to 1.4.2 ([#4660](https://github.com/aws-powertools/powertools-lambda-python/issues/4660)) +* **deps-dev:** bump aws-cdk-lib from 2.147.1 to 2.147.2 ([#4661](https://github.com/aws-powertools/powertools-lambda-python/issues/4661)) +* **deps-dev:** bump cfn-lint from 1.5.0 to 1.5.1 ([#4711](https://github.com/aws-powertools/powertools-lambda-python/issues/4711)) +* **deps-dev:** bump aws-cdk from 2.147.1 to 2.147.2 ([#4657](https://github.com/aws-powertools/powertools-lambda-python/issues/4657)) +* **deps-dev:** bump ruff from 0.4.10 to 0.5.0 ([#4644](https://github.com/aws-powertools/powertools-lambda-python/issues/4644)) +* **deps-dev:** bump sentry-sdk from 2.7.1 to 2.8.0 ([#4712](https://github.com/aws-powertools/powertools-lambda-python/issues/4712)) +* **layers:** downgrade aws cdk to 2.145.0 ([#4739](https://github.com/aws-powertools/powertools-lambda-python/issues/4739)) + + + +## [v2.40.1] - 2024-06-28 +## Bug Fixes + +* **event_handler:** current_event regression AppSyncResolver Router ([#4652](https://github.com/aws-powertools/powertools-lambda-python/issues/4652)) + +## Maintenance + +* version bump +* **ci:** new pre-release 2.40.1a1 ([#4653](https://github.com/aws-powertools/powertools-lambda-python/issues/4653)) +* **ci:** new pre-release 2.40.1a0 ([#4648](https://github.com/aws-powertools/powertools-lambda-python/issues/4648)) +* **deps-dev:** bump cfn-lint from 1.3.7 to 1.4.1 ([#4646](https://github.com/aws-powertools/powertools-lambda-python/issues/4646)) +* **deps-dev:** bump sentry-sdk from 2.7.0 to 2.7.1 ([#4645](https://github.com/aws-powertools/powertools-lambda-python/issues/4645)) + + + +## [v2.40.0] - 2024-06-27 +## Bug Fixes + +* **event_sources:** change partition and offset field types in KafkaEventRecord ([#4515](https://github.com/aws-powertools/powertools-lambda-python/issues/4515)) + +## Documentation + +* **homepage:** Fix homepage link ([#4587](https://github.com/aws-powertools/powertools-lambda-python/issues/4587)) +* **i-made-this:** add new article about best practices for accelerating serverless development ([#4518](https://github.com/aws-powertools/powertools-lambda-python/issues/4518)) +* **public reference:** add Brsk as a public reference ([#4597](https://github.com/aws-powertools/powertools-lambda-python/issues/4597)) + +## Features + +* **event-handler:** add appsync batch resolvers ([#1998](https://github.com/aws-powertools/powertools-lambda-python/issues/1998)) +* **validation:** support JSON Schema referencing in validation utils ([#4508](https://github.com/aws-powertools/powertools-lambda-python/issues/4508)) + +## Maintenance + +* version bump +* **ci:** add the Metrics feature to nox tests ([#4552](https://github.com/aws-powertools/powertools-lambda-python/issues/4552)) +* **ci:** new pre-release 2.39.2a5 ([#4636](https://github.com/aws-powertools/powertools-lambda-python/issues/4636)) +* **ci:** add the Streaming feature to nox tests ([#4575](https://github.com/aws-powertools/powertools-lambda-python/issues/4575)) +* **ci:** new pre-release 2.39.2a4 ([#4629](https://github.com/aws-powertools/powertools-lambda-python/issues/4629)) +* **ci:** new pre-release 2.39.2a3 ([#4620](https://github.com/aws-powertools/powertools-lambda-python/issues/4620)) +* **ci:** add the Event Handler feature to nox tests ([#4581](https://github.com/aws-powertools/powertools-lambda-python/issues/4581)) +* **ci:** add the Data Class feature to nox tests ([#4583](https://github.com/aws-powertools/powertools-lambda-python/issues/4583)) +* **ci:** add the Parser feature to nox tests ([#4584](https://github.com/aws-powertools/powertools-lambda-python/issues/4584)) +* **ci:** add the Idempotency feature to nox tests ([#4585](https://github.com/aws-powertools/powertools-lambda-python/issues/4585)) +* **ci:** new pre-release 2.39.2a2 ([#4610](https://github.com/aws-powertools/powertools-lambda-python/issues/4610)) +* **ci:** introduce tests with Nox ([#4537](https://github.com/aws-powertools/powertools-lambda-python/issues/4537)) +* **ci:** new pre-release 2.39.2a1 ([#4598](https://github.com/aws-powertools/powertools-lambda-python/issues/4598)) +* **ci:** add the Tracer feature to nox tests ([#4567](https://github.com/aws-powertools/powertools-lambda-python/issues/4567)) +* **ci:** add the Middleware Factory feature to nox tests ([#4568](https://github.com/aws-powertools/powertools-lambda-python/issues/4568)) +* **ci:** add the Parameters feature to nox tests ([#4569](https://github.com/aws-powertools/powertools-lambda-python/issues/4569)) +* **ci:** add the Batch Processor feature to nox tests ([#4586](https://github.com/aws-powertools/powertools-lambda-python/issues/4586)) +* **ci:** add the Feature Flags feature to nox tests ([#4570](https://github.com/aws-powertools/powertools-lambda-python/issues/4570)) +* **ci:** add the Validation feature to nox tests ([#4571](https://github.com/aws-powertools/powertools-lambda-python/issues/4571)) +* **ci:** introduce daily pre-releases ([#4535](https://github.com/aws-powertools/powertools-lambda-python/issues/4535)) +* **ci:** new pre-release 2.39.2a0 ([#4590](https://github.com/aws-powertools/powertools-lambda-python/issues/4590)) +* **ci:** add the Data Masking feature to nox tests ([#4574](https://github.com/aws-powertools/powertools-lambda-python/issues/4574)) +* **ci:** add the Typing feature to nox tests ([#4572](https://github.com/aws-powertools/powertools-lambda-python/issues/4572)) +* **deps:** bump pypa/gh-action-pypi-publish from 1.8.14 to 1.9.0 ([#4592](https://github.com/aws-powertools/powertools-lambda-python/issues/4592)) +* **deps:** bump pydantic from 1.10.16 to 1.10.17 ([#4595](https://github.com/aws-powertools/powertools-lambda-python/issues/4595)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#4565](https://github.com/aws-powertools/powertools-lambda-python/issues/4565)) +* **deps:** bump squidfunk/mkdocs-material from `96abcbb` to `257eca8` in /docs ([#4540](https://github.com/aws-powertools/powertools-lambda-python/issues/4540)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.7 to 3.0.9 ([#4539](https://github.com/aws-powertools/powertools-lambda-python/issues/4539)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#4546](https://github.com/aws-powertools/powertools-lambda-python/issues/4546)) +* **deps:** bump redis from 5.0.5 to 5.0.6 ([#4527](https://github.com/aws-powertools/powertools-lambda-python/issues/4527)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#4580](https://github.com/aws-powertools/powertools-lambda-python/issues/4580)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 2 updates ([#4635](https://github.com/aws-powertools/powertools-lambda-python/issues/4635)) +* **deps:** bump codecov/codecov-action from 4.4.1 to 4.5.0 ([#4514](https://github.com/aws-powertools/powertools-lambda-python/issues/4514)) +* **deps:** bump pypa/gh-action-pypi-publish from 1.8.14 to 1.9.0 ([#4538](https://github.com/aws-powertools/powertools-lambda-python/issues/4538)) +* **deps:** bump fastjsonschema from 2.19.1 to 2.20.0 ([#4543](https://github.com/aws-powertools/powertools-lambda-python/issues/4543)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.189 to 0.1.192 ([#4578](https://github.com/aws-powertools/powertools-lambda-python/issues/4578)) +* **deps-dev:** bump sentry-sdk from 2.5.1 to 2.6.0 ([#4579](https://github.com/aws-powertools/powertools-lambda-python/issues/4579)) +* **deps-dev:** bump cfn-lint from 0.87.7 to 1.3.0 ([#4577](https://github.com/aws-powertools/powertools-lambda-python/issues/4577)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.192 to 0.1.193 ([#4596](https://github.com/aws-powertools/powertools-lambda-python/issues/4596)) +* **deps-dev:** bump ruff from 0.4.9 to 0.4.10 ([#4594](https://github.com/aws-powertools/powertools-lambda-python/issues/4594)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.193 to 0.1.194 ([#4601](https://github.com/aws-powertools/powertools-lambda-python/issues/4601)) +* **deps-dev:** bump aws-cdk from 2.146.0 to 2.147.0 ([#4604](https://github.com/aws-powertools/powertools-lambda-python/issues/4604)) +* **deps-dev:** bump aws-cdk-lib from 2.146.0 to 2.147.0 ([#4603](https://github.com/aws-powertools/powertools-lambda-python/issues/4603)) +* **deps-dev:** bump filelock from 3.15.1 to 3.15.3 ([#4576](https://github.com/aws-powertools/powertools-lambda-python/issues/4576)) +* **deps-dev:** bump hvac from 2.2.0 to 2.3.0 ([#4563](https://github.com/aws-powertools/powertools-lambda-python/issues/4563)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.188 to 0.1.189 ([#4564](https://github.com/aws-powertools/powertools-lambda-python/issues/4564)) +* **deps-dev:** bump cfn-lint from 1.3.0 to 1.3.3 ([#4602](https://github.com/aws-powertools/powertools-lambda-python/issues/4602)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.194 to 0.1.198 ([#4627](https://github.com/aws-powertools/powertools-lambda-python/issues/4627)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.146.0a0 to 2.147.0a0 ([#4619](https://github.com/aws-powertools/powertools-lambda-python/issues/4619)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.184 to 0.1.188 ([#4550](https://github.com/aws-powertools/powertools-lambda-python/issues/4550)) +* **deps-dev:** bump mkdocs-material from 9.5.26 to 9.5.27 ([#4544](https://github.com/aws-powertools/powertools-lambda-python/issues/4544)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.145.0a0 to 2.146.0a0 ([#4542](https://github.com/aws-powertools/powertools-lambda-python/issues/4542)) +* **deps-dev:** bump urllib3 from 1.26.18 to 1.26.19 in /layer ([#4547](https://github.com/aws-powertools/powertools-lambda-python/issues/4547)) +* **deps-dev:** bump aws-cdk-lib from 2.145.0 to 2.146.0 ([#4526](https://github.com/aws-powertools/powertools-lambda-python/issues/4526)) +* **deps-dev:** bump aws-cdk from 2.147.0 to 2.147.1 ([#4614](https://github.com/aws-powertools/powertools-lambda-python/issues/4614)) +* **deps-dev:** bump coverage from 7.5.3 to 7.5.4 ([#4617](https://github.com/aws-powertools/powertools-lambda-python/issues/4617)) +* **deps-dev:** bump aws-cdk-lib from 2.147.0 to 2.147.1 ([#4615](https://github.com/aws-powertools/powertools-lambda-python/issues/4615)) +* **deps-dev:** bump mypy-boto3-secretsmanager from 1.34.125 to 1.34.128 in the boto-typing group ([#4541](https://github.com/aws-powertools/powertools-lambda-python/issues/4541)) +* **deps-dev:** bump pdoc3 from 0.10.0 to 0.11.0 ([#4618](https://github.com/aws-powertools/powertools-lambda-python/issues/4618)) +* **deps-dev:** bump mypy-boto3-secretsmanager from 1.34.109 to 1.34.125 in the boto-typing group ([#4509](https://github.com/aws-powertools/powertools-lambda-python/issues/4509)) +* **deps-dev:** bump mike from 2.1.1 to 2.1.2 ([#4616](https://github.com/aws-powertools/powertools-lambda-python/issues/4616)) +* **deps-dev:** bump mypy from 1.10.0 to 1.10.1 ([#4624](https://github.com/aws-powertools/powertools-lambda-python/issues/4624)) +* **deps-dev:** bump filelock from 3.15.3 to 3.15.4 ([#4626](https://github.com/aws-powertools/powertools-lambda-python/issues/4626)) +* **deps-dev:** bump ruff from 0.4.8 to 0.4.9 ([#4528](https://github.com/aws-powertools/powertools-lambda-python/issues/4528)) +* **deps-dev:** bump cfn-lint from 1.3.3 to 1.3.5 ([#4628](https://github.com/aws-powertools/powertools-lambda-python/issues/4628)) +* **deps-dev:** bump mypy-boto3-ssm from 1.34.91 to 1.34.132 in the boto-typing group ([#4623](https://github.com/aws-powertools/powertools-lambda-python/issues/4623)) +* **deps-dev:** bump aws-cdk from 2.145.0 to 2.146.0 ([#4525](https://github.com/aws-powertools/powertools-lambda-python/issues/4525)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.182 to 0.1.184 ([#4529](https://github.com/aws-powertools/powertools-lambda-python/issues/4529)) +* **deps-dev:** bump bandit from 1.7.8 to 1.7.9 ([#4511](https://github.com/aws-powertools/powertools-lambda-python/issues/4511)) +* **deps-dev:** bump cfn-lint from 0.87.6 to 0.87.7 ([#4513](https://github.com/aws-powertools/powertools-lambda-python/issues/4513)) +* **deps-dev:** bump filelock from 3.14.0 to 3.15.1 ([#4512](https://github.com/aws-powertools/powertools-lambda-python/issues/4512)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.179 to 0.1.182 ([#4510](https://github.com/aws-powertools/powertools-lambda-python/issues/4510)) +* **deps-dev:** bump cfn-lint from 1.3.5 to 1.3.7 ([#4634](https://github.com/aws-powertools/powertools-lambda-python/issues/4634)) +* **deps-dev:** bump mypy-boto3-dynamodb from 1.34.114 to 1.34.131 in the boto-typing group ([#4593](https://github.com/aws-powertools/powertools-lambda-python/issues/4593)) +* **governance:** fix errors when creating Gitpod environment ([#4532](https://github.com/aws-powertools/powertools-lambda-python/issues/4532)) +* **layers:** downgrade aws cdk to 2.145.0 ([#4640](https://github.com/aws-powertools/powertools-lambda-python/issues/4640)) + + + +## [v2.39.1] - 2024-06-13 +## Bug Fixes + +* **event_handler:** regression making pydantic required (it should not) ([#4500](https://github.com/aws-powertools/powertools-lambda-python/issues/4500)) + +## Maintenance + +* version bump + + + +## [v2.39.0] - 2024-06-13 +## Bug Fixes + +* **event_handler:** do not skip middleware and exception handlers on 404 error ([#4492](https://github.com/aws-powertools/powertools-lambda-python/issues/4492)) +* **event_handler:** raise more specific SerializationError exception for unsupported types in data validation ([#4415](https://github.com/aws-powertools/powertools-lambda-python/issues/4415)) +* **event_handler:** security scheme unhashable list when working with router ([#4421](https://github.com/aws-powertools/powertools-lambda-python/issues/4421)) +* **event_handler:** CORS Origin for ALBResolver multi-headers ([#4385](https://github.com/aws-powertools/powertools-lambda-python/issues/4385)) +* **idempotency:** POWERTOOLS_IDEMPOTENCY_DISABLED should respect truthy values ([#4391](https://github.com/aws-powertools/powertools-lambda-python/issues/4391)) + +## Documentation + +* **homepage:** Change installation to CDK v2 ([#4351](https://github.com/aws-powertools/powertools-lambda-python/issues/4351)) +* **public reference:** add Recast as a public reference ([#4491](https://github.com/aws-powertools/powertools-lambda-python/issues/4491)) + +## Features + +* **event_source:** add CloudFormationCustomResourceEvent data class. ([#4342](https://github.com/aws-powertools/powertools-lambda-python/issues/4342)) +* **events:** Update and Add Cognito User Pool Events ([#4423](https://github.com/aws-powertools/powertools-lambda-python/issues/4423)) + +## Maintenance + +* version bump +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#4369](https://github.com/aws-powertools/powertools-lambda-python/issues/4369)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#4468](https://github.com/aws-powertools/powertools-lambda-python/issues/4468)) +* **deps:** bump datadog-lambda from 5.94.0 to 6.95.0 ([#4471](https://github.com/aws-powertools/powertools-lambda-python/issues/4471)) +* **deps:** bump redis from 5.0.4 to 5.0.5 ([#4464](https://github.com/aws-powertools/powertools-lambda-python/issues/4464)) +* **deps:** bump aws-encryption-sdk from 3.2.0 to 3.3.0 ([#4393](https://github.com/aws-powertools/powertools-lambda-python/issues/4393)) +* **deps:** bump codecov/codecov-action from 4.4.0 to 4.4.1 ([#4376](https://github.com/aws-powertools/powertools-lambda-python/issues/4376)) +* **deps:** bump squidfunk/mkdocs-material from `8a87f05` to `96abcbb` in /docs ([#4461](https://github.com/aws-powertools/powertools-lambda-python/issues/4461)) +* **deps:** bump typing-extensions from 4.12.1 to 4.12.2 ([#4470](https://github.com/aws-powertools/powertools-lambda-python/issues/4470)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 2 updates ([#4396](https://github.com/aws-powertools/powertools-lambda-python/issues/4396)) +* **deps:** bump aws-xray-sdk from 2.13.0 to 2.13.1 ([#4379](https://github.com/aws-powertools/powertools-lambda-python/issues/4379)) +* **deps:** bump actions/dependency-review-action from 4.3.2 to 4.3.3 ([#4456](https://github.com/aws-powertools/powertools-lambda-python/issues/4456)) +* **deps:** bump aws-xray-sdk from 2.13.1 to 2.14.0 ([#4453](https://github.com/aws-powertools/powertools-lambda-python/issues/4453)) +* **deps:** bump typing-extensions from 4.11.0 to 4.12.0 ([#4404](https://github.com/aws-powertools/powertools-lambda-python/issues/4404)) +* **deps:** bump squidfunk/mkdocs-material from `5358893` to `8a87f05` in /docs ([#4408](https://github.com/aws-powertools/powertools-lambda-python/issues/4408)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.6 to 3.0.7 ([#4478](https://github.com/aws-powertools/powertools-lambda-python/issues/4478)) +* **deps:** bump squidfunk/mkdocs-material from `48d1914` to `5358893` in /docs ([#4377](https://github.com/aws-powertools/powertools-lambda-python/issues/4377)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#4444](https://github.com/aws-powertools/powertools-lambda-python/issues/4444)) +* **deps:** bump pydantic from 1.10.15 to 1.10.16 ([#4485](https://github.com/aws-powertools/powertools-lambda-python/issues/4485)) +* **deps:** bump datadog-lambda from 6.95.0 to 6.96.0 ([#4489](https://github.com/aws-powertools/powertools-lambda-python/issues/4489)) +* **deps:** bump actions/checkout from 4.1.6 to 4.1.7 ([#4493](https://github.com/aws-powertools/powertools-lambda-python/issues/4493)) +* **deps:** bump typing-extensions from 4.12.0 to 4.12.1 ([#4440](https://github.com/aws-powertools/powertools-lambda-python/issues/4440)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.5 to 3.0.6 ([#4445](https://github.com/aws-powertools/powertools-lambda-python/issues/4445)) +* **deps:** bump requests from 2.31.0 to 2.32.0 ([#4383](https://github.com/aws-powertools/powertools-lambda-python/issues/4383)) +* **deps-dev:** bump aws-cdk from 2.143.1 to 2.144.0 ([#4443](https://github.com/aws-powertools/powertools-lambda-python/issues/4443)) +* **deps-dev:** bump aws-cdk-lib from 2.143.1 to 2.144.0 ([#4441](https://github.com/aws-powertools/powertools-lambda-python/issues/4441)) +* **deps-dev:** bump ruff from 0.4.6 to 0.4.7 ([#4435](https://github.com/aws-powertools/powertools-lambda-python/issues/4435)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.143.0a0 to 2.143.1a0 ([#4433](https://github.com/aws-powertools/powertools-lambda-python/issues/4433)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.164 to 0.1.169 ([#4442](https://github.com/aws-powertools/powertools-lambda-python/issues/4442)) +* **deps-dev:** bump pytest from 8.2.1 to 8.2.2 ([#4450](https://github.com/aws-powertools/powertools-lambda-python/issues/4450)) +* **deps-dev:** bump aws-cdk from 2.143.0 to 2.143.1 ([#4430](https://github.com/aws-powertools/powertools-lambda-python/issues/4430)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.163 to 0.1.164 ([#4428](https://github.com/aws-powertools/powertools-lambda-python/issues/4428)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.161 to 0.1.163 ([#4425](https://github.com/aws-powertools/powertools-lambda-python/issues/4425)) +* **deps-dev:** bump cfn-lint from 0.87.5 to 0.87.6 ([#4486](https://github.com/aws-powertools/powertools-lambda-python/issues/4486)) +* **deps-dev:** bump sentry-sdk from 2.3.1 to 2.4.0 ([#4449](https://github.com/aws-powertools/powertools-lambda-python/issues/4449)) +* **deps-dev:** bump ruff from 0.4.5 to 0.4.6 ([#4417](https://github.com/aws-powertools/powertools-lambda-python/issues/4417)) +* **deps-dev:** bump cfn-lint from 0.87.3 to 0.87.4 ([#4419](https://github.com/aws-powertools/powertools-lambda-python/issues/4419)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.159 to 0.1.161 ([#4420](https://github.com/aws-powertools/powertools-lambda-python/issues/4420)) +* **deps-dev:** bump coverage from 7.5.2 to 7.5.3 ([#4418](https://github.com/aws-powertools/powertools-lambda-python/issues/4418)) +* **deps-dev:** bump mypy-boto3-dynamodb from 1.34.113 to 1.34.114 in the boto-typing group ([#4416](https://github.com/aws-powertools/powertools-lambda-python/issues/4416)) +* **deps-dev:** bump mkdocs-material from 9.5.24 to 9.5.25 ([#4411](https://github.com/aws-powertools/powertools-lambda-python/issues/4411)) +* **deps-dev:** bump aws-cdk-lib from 2.143.0 to 2.143.1 ([#4429](https://github.com/aws-powertools/powertools-lambda-python/issues/4429)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.142.1a0 to 2.143.0a0 ([#4410](https://github.com/aws-powertools/powertools-lambda-python/issues/4410)) +* **deps-dev:** bump mypy-boto3-dynamodb from 1.34.97 to 1.34.113 in the boto-typing group ([#4409](https://github.com/aws-powertools/powertools-lambda-python/issues/4409)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.158 to 0.1.159 ([#4412](https://github.com/aws-powertools/powertools-lambda-python/issues/4412)) +* **deps-dev:** bump coverage from 7.5.1 to 7.5.2 ([#4413](https://github.com/aws-powertools/powertools-lambda-python/issues/4413)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.143.1a0 to 2.144.0a0 ([#4448](https://github.com/aws-powertools/powertools-lambda-python/issues/4448)) +* **deps-dev:** bump mypy-boto3-s3 from 1.34.105 to 1.34.120 in the boto-typing group ([#4452](https://github.com/aws-powertools/powertools-lambda-python/issues/4452)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.169 to 0.1.173 ([#4459](https://github.com/aws-powertools/powertools-lambda-python/issues/4459)) +* **deps-dev:** bump aws-cdk-lib from 2.142.1 to 2.143.0 ([#4403](https://github.com/aws-powertools/powertools-lambda-python/issues/4403)) +* **deps-dev:** bump aws-cdk from 2.142.1 to 2.143.0 ([#4402](https://github.com/aws-powertools/powertools-lambda-python/issues/4402)) +* **deps-dev:** bump ruff from 0.4.4 to 0.4.5 ([#4399](https://github.com/aws-powertools/powertools-lambda-python/issues/4399)) +* **deps-dev:** bump sentry-sdk from 2.2.1 to 2.3.1 ([#4398](https://github.com/aws-powertools/powertools-lambda-python/issues/4398)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.157 to 0.1.158 ([#4397](https://github.com/aws-powertools/powertools-lambda-python/issues/4397)) +* **deps-dev:** bump ruff from 0.4.7 to 0.4.8 ([#4455](https://github.com/aws-powertools/powertools-lambda-python/issues/4455)) +* **deps-dev:** bump sentry-sdk from 2.4.0 to 2.5.0 ([#4462](https://github.com/aws-powertools/powertools-lambda-python/issues/4462)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.155 to 0.1.157 ([#4394](https://github.com/aws-powertools/powertools-lambda-python/issues/4394)) +* **deps-dev:** bump mkdocs-material from 9.5.25 to 9.5.26 ([#4463](https://github.com/aws-powertools/powertools-lambda-python/issues/4463)) +* **deps-dev:** bump mypy-boto3-cloudformation from 1.34.84 to 1.34.111 in the boto-typing group ([#4392](https://github.com/aws-powertools/powertools-lambda-python/issues/4392)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.154 to 0.1.155 ([#4386](https://github.com/aws-powertools/powertools-lambda-python/issues/4386)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.173 to 0.1.174 ([#4466](https://github.com/aws-powertools/powertools-lambda-python/issues/4466)) +* **deps-dev:** bump pytest-asyncio from 0.23.6 to 0.23.7 ([#4387](https://github.com/aws-powertools/powertools-lambda-python/issues/4387)) +* **deps-dev:** bump sentry-sdk from 2.2.0 to 2.2.1 ([#4388](https://github.com/aws-powertools/powertools-lambda-python/issues/4388)) +* **deps-dev:** bump ijson from 3.2.3 to 3.3.0 ([#4465](https://github.com/aws-powertools/powertools-lambda-python/issues/4465)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.152 to 0.1.154 ([#4382](https://github.com/aws-powertools/powertools-lambda-python/issues/4382)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.174 to 0.1.175 ([#4472](https://github.com/aws-powertools/powertools-lambda-python/issues/4472)) +* **deps-dev:** bump mypy-boto3-secretsmanager from 1.34.107 to 1.34.109 in the boto-typing group ([#4378](https://github.com/aws-powertools/powertools-lambda-python/issues/4378)) +* **deps-dev:** bump sentry-sdk from 2.5.0 to 2.5.1 ([#4469](https://github.com/aws-powertools/powertools-lambda-python/issues/4469)) +* **deps-dev:** bump cfn-lint from 0.87.4 to 0.87.5 ([#4479](https://github.com/aws-powertools/powertools-lambda-python/issues/4479)) +* **deps-dev:** bump mkdocs-material from 9.5.23 to 9.5.24 ([#4380](https://github.com/aws-powertools/powertools-lambda-python/issues/4380)) +* **deps-dev:** bump pytest from 8.2.0 to 8.2.1 ([#4381](https://github.com/aws-powertools/powertools-lambda-python/issues/4381)) +* **deps-dev:** bump aws-cdk from 2.144.0 to 2.145.0 ([#4482](https://github.com/aws-powertools/powertools-lambda-python/issues/4482)) +* **deps-dev:** bump aws-cdk-lib from 2.144.0 to 2.145.0 ([#4481](https://github.com/aws-powertools/powertools-lambda-python/issues/4481)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.141.0a0 to 2.142.1a0 ([#4367](https://github.com/aws-powertools/powertools-lambda-python/issues/4367)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.144.0a0 to 2.145.0a0 ([#4487](https://github.com/aws-powertools/powertools-lambda-python/issues/4487)) +* **deps-dev:** bump aws-cdk from 2.142.0 to 2.142.1 ([#4366](https://github.com/aws-powertools/powertools-lambda-python/issues/4366)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.150 to 0.1.152 ([#4368](https://github.com/aws-powertools/powertools-lambda-python/issues/4368)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.176 to 0.1.179 ([#4488](https://github.com/aws-powertools/powertools-lambda-python/issues/4488)) +* **deps-dev:** bump cfn-lint from 0.87.2 to 0.87.3 ([#4370](https://github.com/aws-powertools/powertools-lambda-python/issues/4370)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.175 to 0.1.176 ([#4480](https://github.com/aws-powertools/powertools-lambda-python/issues/4480)) +* **libraries:** add jmespath as a required dependency ([#4422](https://github.com/aws-powertools/powertools-lambda-python/issues/4422)) + + + +## [v2.38.1] - 2024-05-17 +## Bug Fixes + +* **logger:** reverting logger child modification ([#4363](https://github.com/aws-powertools/powertools-lambda-python/issues/4363)) + +## Maintenance + +* version bump + + + +## [v2.38.0] - 2024-05-17 +## Bug Fixes + +* **ci:** apply lessons learned to monthly roadmap reminder cross-repo ([#4078](https://github.com/aws-powertools/powertools-lambda-python/issues/4078)) +* **event-sources:** sane defaults for authorizer v1 and v2 ([#4298](https://github.com/aws-powertools/powertools-lambda-python/issues/4298)) +* **logger:** correctly pick powertools or custom handler in custom environments ([#4295](https://github.com/aws-powertools/powertools-lambda-python/issues/4295)) +* **parser:** make etag optional field on S3 notification events ([#4173](https://github.com/aws-powertools/powertools-lambda-python/issues/4173)) +* **typing:** resolved_headers_field is not Optional ([#4148](https://github.com/aws-powertools/powertools-lambda-python/issues/4148)) + +## Code Refactoring + +* **data-masking:** remove Non-GA comments ([#4334](https://github.com/aws-powertools/powertools-lambda-python/issues/4334)) +* **parser:** only infer type hints when necessary ([#4183](https://github.com/aws-powertools/powertools-lambda-python/issues/4183)) + +## Documentation + +* **general:** update documentation to add info about v3 ([#4234](https://github.com/aws-powertools/powertools-lambda-python/issues/4234)) +* **homepage:** add link to new and official workshop ([#4292](https://github.com/aws-powertools/powertools-lambda-python/issues/4292)) +* **idempotency:** fix highlight and import path ([#4154](https://github.com/aws-powertools/powertools-lambda-python/issues/4154)) +* **roadmap:** april updates ([#4181](https://github.com/aws-powertools/powertools-lambda-python/issues/4181)) + +## Features + +* **event_handler:** add support for persisting authorization session in OpenAPI ([#4312](https://github.com/aws-powertools/powertools-lambda-python/issues/4312)) +* **event_handler:** add decorator for HTTP HEAD verb ([#4275](https://github.com/aws-powertools/powertools-lambda-python/issues/4275)) +* **logger-utils:** preserve log level for discovered third-party top-level loggers ([#4299](https://github.com/aws-powertools/powertools-lambda-python/issues/4299)) + +## Maintenance + +* version bump +* **ci:** bump upload artifact action to v4 ([#4355](https://github.com/aws-powertools/powertools-lambda-python/issues/4355)) +* **ci:** add branch v3 to quality check and e2e actions ([#4232](https://github.com/aws-powertools/powertools-lambda-python/issues/4232)) +* **ci:** bump download artifact action to v4 ([#4358](https://github.com/aws-powertools/powertools-lambda-python/issues/4358)) +* **deps:** bump actions/download-artifact from 4.1.4 to 4.1.5 ([#4161](https://github.com/aws-powertools/powertools-lambda-python/issues/4161)) +* **deps:** bump actions/checkout from 4.1.3 to 4.1.4 ([#4206](https://github.com/aws-powertools/powertools-lambda-python/issues/4206)) +* **deps:** bump ossf/scorecard-action from 2.3.1 to 2.3.3 ([#4315](https://github.com/aws-powertools/powertools-lambda-python/issues/4315)) +* **deps:** bump github.com/aws/aws-sdk-go-v2/config from 1.27.12 to 1.27.13 in /layer/scripts/layer-balancer in the layer-balancer group ([#4319](https://github.com/aws-powertools/powertools-lambda-python/issues/4319)) +* **deps:** bump actions/download-artifact from 4.1.6 to 4.1.7 ([#4205](https://github.com/aws-powertools/powertools-lambda-python/issues/4205)) +* **deps:** bump squidfunk/mkdocs-material from `521644b` to `e309089` in /docs ([#4216](https://github.com/aws-powertools/powertools-lambda-python/issues/4216)) +* **deps:** bump squidfunk/mkdocs-material from `11d7ec0` to `8ef47d7` in /docs ([#4323](https://github.com/aws-powertools/powertools-lambda-python/issues/4323)) +* **deps:** bump datadog-lambda from 5.92.0 to 5.93.0 ([#4211](https://github.com/aws-powertools/powertools-lambda-python/issues/4211)) +* **deps:** bump redis from 5.0.3 to 5.0.4 ([#4187](https://github.com/aws-powertools/powertools-lambda-python/issues/4187)) +* **deps:** bump actions/upload-artifact from 4.3.2 to 4.3.3 ([#4177](https://github.com/aws-powertools/powertools-lambda-python/issues/4177)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 2 updates ([#4302](https://github.com/aws-powertools/powertools-lambda-python/issues/4302)) +* **deps:** bump squidfunk/mkdocs-material from `8ef47d7` to `48d1914` in /docs ([#4336](https://github.com/aws-powertools/powertools-lambda-python/issues/4336)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#4337](https://github.com/aws-powertools/powertools-lambda-python/issues/4337)) +* **deps:** bump squidfunk/mkdocs-material from `e309089` to `98c9809` in /docs ([#4236](https://github.com/aws-powertools/powertools-lambda-python/issues/4236)) +* **deps:** bump actions/dependency-review-action from 4.3.1 to 4.3.2 ([#4244](https://github.com/aws-powertools/powertools-lambda-python/issues/4244)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.4 to 3.0.5 ([#4281](https://github.com/aws-powertools/powertools-lambda-python/issues/4281)) +* **deps:** bump actions/checkout from 4.1.4 to 4.1.5 ([#4282](https://github.com/aws-powertools/powertools-lambda-python/issues/4282)) +* **deps:** bump jinja2 from 3.1.3 to 3.1.4 in /docs ([#4284](https://github.com/aws-powertools/powertools-lambda-python/issues/4284)) +* **deps:** bump codecov/codecov-action from 4.3.0 to 4.3.1 ([#4252](https://github.com/aws-powertools/powertools-lambda-python/issues/4252)) +* **deps:** bump datadog-lambda from 5.93.0 to 5.94.0 ([#4253](https://github.com/aws-powertools/powertools-lambda-python/issues/4253)) +* **deps:** bump actions/checkout from 4.1.2 to 4.1.3 ([#4168](https://github.com/aws-powertools/powertools-lambda-python/issues/4168)) +* **deps:** bump actions/dependency-review-action from 4.2.5 to 4.3.1 ([#4240](https://github.com/aws-powertools/powertools-lambda-python/issues/4240)) +* **deps:** bump actions/checkout from 4.1.5 to 4.1.6 ([#4344](https://github.com/aws-powertools/powertools-lambda-python/issues/4344)) +* **deps:** bump squidfunk/mkdocs-material from `98c9809` to `11d7ec0` in /docs ([#4269](https://github.com/aws-powertools/powertools-lambda-python/issues/4269)) +* **deps:** bump actions/upload-artifact from 4.3.1 to 4.3.2 ([#4162](https://github.com/aws-powertools/powertools-lambda-python/issues/4162)) +* **deps:** bump codecov/codecov-action from 4.3.1 to 4.4.0 ([#4328](https://github.com/aws-powertools/powertools-lambda-python/issues/4328)) +* **deps:** bump slsa-framework/slsa-github-generator from 1.10.0 to 2.0.0 ([#4179](https://github.com/aws-powertools/powertools-lambda-python/issues/4179)) +* **deps:** bump actions/download-artifact from 4.1.5 to 4.1.6 ([#4178](https://github.com/aws-powertools/powertools-lambda-python/issues/4178)) +* **deps-dev:** bump mkdocs-material from 9.5.20 to 9.5.21 ([#4271](https://github.com/aws-powertools/powertools-lambda-python/issues/4271)) +* **deps-dev:** bump mike from 2.1.0 to 2.1.1 ([#4268](https://github.com/aws-powertools/powertools-lambda-python/issues/4268)) +* **deps-dev:** bump cfn-lint from 0.87.0 to 0.87.1 ([#4272](https://github.com/aws-powertools/powertools-lambda-python/issues/4272)) +* **deps-dev:** bump mike from 1.1.2 to 2.1.0 ([#4258](https://github.com/aws-powertools/powertools-lambda-python/issues/4258)) +* **deps-dev:** bump aws-cdk-lib from 2.139.1 to 2.140.0 ([#4259](https://github.com/aws-powertools/powertools-lambda-python/issues/4259)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.139.1a0 to 2.140.0a0 ([#4270](https://github.com/aws-powertools/powertools-lambda-python/issues/4270)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.139.0a0 to 2.139.1a0 ([#4261](https://github.com/aws-powertools/powertools-lambda-python/issues/4261)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.133 to 0.1.134 ([#4260](https://github.com/aws-powertools/powertools-lambda-python/issues/4260)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.134 to 0.1.135 ([#4273](https://github.com/aws-powertools/powertools-lambda-python/issues/4273)) +* **deps-dev:** bump mypy-boto3-dynamodb from 1.34.91 to 1.34.97 in the boto-typing group ([#4257](https://github.com/aws-powertools/powertools-lambda-python/issues/4257)) +* **deps-dev:** bump sentry-sdk from 2.0.1 to 2.1.1 ([#4287](https://github.com/aws-powertools/powertools-lambda-python/issues/4287)) +* **deps-dev:** bump aws-cdk-lib from 2.139.0 to 2.139.1 ([#4248](https://github.com/aws-powertools/powertools-lambda-python/issues/4248)) +* **deps-dev:** bump cfn-lint from 0.86.4 to 0.87.0 ([#4249](https://github.com/aws-powertools/powertools-lambda-python/issues/4249)) +* **deps-dev:** bump pytest-xdist from 3.5.0 to 3.6.1 ([#4247](https://github.com/aws-powertools/powertools-lambda-python/issues/4247)) +* **deps-dev:** bump ruff from 0.4.2 to 0.4.3 ([#4286](https://github.com/aws-powertools/powertools-lambda-python/issues/4286)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.132 to 0.1.133 ([#4246](https://github.com/aws-powertools/powertools-lambda-python/issues/4246)) +* **deps-dev:** bump jinja2 from 3.1.3 to 3.1.4 ([#4283](https://github.com/aws-powertools/powertools-lambda-python/issues/4283)) +* **deps-dev:** bump aws-cdk from 2.139.0 to 2.139.1 ([#4245](https://github.com/aws-powertools/powertools-lambda-python/issues/4245)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.135 to 0.1.136 ([#4285](https://github.com/aws-powertools/powertools-lambda-python/issues/4285)) +* **deps-dev:** bump filelock from 3.13.4 to 3.14.0 ([#4241](https://github.com/aws-powertools/powertools-lambda-python/issues/4241)) +* **deps-dev:** bump hvac from 2.1.0 to 2.2.0 ([#4238](https://github.com/aws-powertools/powertools-lambda-python/issues/4238)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.131 to 0.1.132 ([#4239](https://github.com/aws-powertools/powertools-lambda-python/issues/4239)) +* **deps-dev:** bump mkdocs-material from 9.5.19 to 9.5.20 ([#4242](https://github.com/aws-powertools/powertools-lambda-python/issues/4242)) +* **deps-dev:** bump aws-cdk from 2.139.1 to 2.140.0 ([#4256](https://github.com/aws-powertools/powertools-lambda-python/issues/4256)) +* **deps-dev:** bump pytest from 8.1.1 to 8.2.0 ([#4237](https://github.com/aws-powertools/powertools-lambda-python/issues/4237)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.136 to 0.1.139 ([#4293](https://github.com/aws-powertools/powertools-lambda-python/issues/4293)) +* **deps-dev:** bump aws-cdk-lib from 2.141.0 to 2.142.1 ([#4352](https://github.com/aws-powertools/powertools-lambda-python/issues/4352)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.139 to 0.1.140 ([#4301](https://github.com/aws-powertools/powertools-lambda-python/issues/4301)) +* **deps-dev:** bump sentry-sdk from 1.45.0 to 2.0.1 ([#4223](https://github.com/aws-powertools/powertools-lambda-python/issues/4223)) +* **deps-dev:** bump mkdocs-material from 9.5.18 to 9.5.19 ([#4224](https://github.com/aws-powertools/powertools-lambda-python/issues/4224)) +* **deps-dev:** bump black from 24.4.1 to 24.4.2 ([#4222](https://github.com/aws-powertools/powertools-lambda-python/issues/4222)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.138.0a0 to 2.139.0a0 ([#4225](https://github.com/aws-powertools/powertools-lambda-python/issues/4225)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.130 to 0.1.131 ([#4221](https://github.com/aws-powertools/powertools-lambda-python/issues/4221)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.140 to 0.1.142 ([#4307](https://github.com/aws-powertools/powertools-lambda-python/issues/4307)) +* **deps-dev:** bump ruff from 0.4.1 to 0.4.2 ([#4212](https://github.com/aws-powertools/powertools-lambda-python/issues/4212)) +* **deps-dev:** bump aws-cdk-lib from 2.138.0 to 2.139.0 ([#4213](https://github.com/aws-powertools/powertools-lambda-python/issues/4213)) +* **deps-dev:** bump aws-cdk from 2.138.0 to 2.139.0 ([#4215](https://github.com/aws-powertools/powertools-lambda-python/issues/4215)) +* **deps-dev:** bump aws-cdk from 2.140.0 to 2.141.0 ([#4306](https://github.com/aws-powertools/powertools-lambda-python/issues/4306)) +* **deps-dev:** bump types-redis from 4.6.0.20240423 to 4.6.0.20240425 ([#4214](https://github.com/aws-powertools/powertools-lambda-python/issues/4214)) +* **deps-dev:** bump aws-cdk-lib from 2.140.0 to 2.141.0 ([#4308](https://github.com/aws-powertools/powertools-lambda-python/issues/4308)) +* **deps-dev:** bump the boto-typing group with 2 updates ([#4210](https://github.com/aws-powertools/powertools-lambda-python/issues/4210)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.126 to 0.1.130 ([#4209](https://github.com/aws-powertools/powertools-lambda-python/issues/4209)) +* **deps-dev:** bump ruff from 0.4.3 to 0.4.4 ([#4309](https://github.com/aws-powertools/powertools-lambda-python/issues/4309)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.140.0a0 to 2.141.0a0 ([#4318](https://github.com/aws-powertools/powertools-lambda-python/issues/4318)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.142 to 0.1.144 ([#4316](https://github.com/aws-powertools/powertools-lambda-python/issues/4316)) +* **deps-dev:** bump black from 24.4.0 to 24.4.1 ([#4203](https://github.com/aws-powertools/powertools-lambda-python/issues/4203)) +* **deps-dev:** bump mypy from 1.9.0 to 1.10.0 ([#4202](https://github.com/aws-powertools/powertools-lambda-python/issues/4202)) +* **deps-dev:** bump mypy-boto3-ssm from 1.34.61 to 1.34.91 in the boto-typing group ([#4201](https://github.com/aws-powertools/powertools-lambda-python/issues/4201)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.123 to 0.1.126 ([#4188](https://github.com/aws-powertools/powertools-lambda-python/issues/4188)) +* **deps-dev:** bump cfn-lint from 0.87.1 to 0.87.2 ([#4317](https://github.com/aws-powertools/powertools-lambda-python/issues/4317)) +* **deps-dev:** bump coverage from 7.4.4 to 7.5.0 ([#4186](https://github.com/aws-powertools/powertools-lambda-python/issues/4186)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.144 to 0.1.145 ([#4325](https://github.com/aws-powertools/powertools-lambda-python/issues/4325)) +* **deps-dev:** bump types-redis from 4.6.0.20240417 to 4.6.0.20240423 ([#4185](https://github.com/aws-powertools/powertools-lambda-python/issues/4185)) +* **deps-dev:** bump mkdocs-material from 9.5.21 to 9.5.22 ([#4324](https://github.com/aws-powertools/powertools-lambda-python/issues/4324)) +* **deps-dev:** bump mypy-boto3-s3 from 1.34.91 to 1.34.105 in the boto-typing group ([#4329](https://github.com/aws-powertools/powertools-lambda-python/issues/4329)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.145 to 0.1.146 ([#4330](https://github.com/aws-powertools/powertools-lambda-python/issues/4330)) +* **deps-dev:** bump cfn-lint from 0.86.3 to 0.86.4 ([#4180](https://github.com/aws-powertools/powertools-lambda-python/issues/4180)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.121 to 0.1.123 ([#4176](https://github.com/aws-powertools/powertools-lambda-python/issues/4176)) +* **deps-dev:** bump mkdocs-material from 9.5.22 to 9.5.23 ([#4338](https://github.com/aws-powertools/powertools-lambda-python/issues/4338)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.137.0a0 to 2.138.0a0 ([#4169](https://github.com/aws-powertools/powertools-lambda-python/issues/4169)) +* **deps-dev:** bump aws-cdk from 2.141.0 to 2.142.0 ([#4343](https://github.com/aws-powertools/powertools-lambda-python/issues/4343)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.119 to 0.1.121 ([#4167](https://github.com/aws-powertools/powertools-lambda-python/issues/4167)) +* **deps-dev:** bump ruff from 0.3.7 to 0.4.1 ([#4166](https://github.com/aws-powertools/powertools-lambda-python/issues/4166)) +* **deps-dev:** bump mypy-boto3-secretsmanager from 1.34.72 to 1.34.107 in the boto-typing group ([#4345](https://github.com/aws-powertools/powertools-lambda-python/issues/4345)) +* **deps-dev:** bump aws-cdk from 2.137.0 to 2.138.0 ([#4157](https://github.com/aws-powertools/powertools-lambda-python/issues/4157)) +* **deps-dev:** bump aws-cdk-lib from 2.137.0 to 2.138.0 ([#4160](https://github.com/aws-powertools/powertools-lambda-python/issues/4160)) +* **deps-dev:** bump sentry-sdk from 2.1.1 to 2.2.0 ([#4348](https://github.com/aws-powertools/powertools-lambda-python/issues/4348)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.146 to 0.1.150 ([#4346](https://github.com/aws-powertools/powertools-lambda-python/issues/4346)) +* **deps-dev:** bump coverage from 7.5.0 to 7.5.1 ([#4288](https://github.com/aws-powertools/powertools-lambda-python/issues/4288)) +* **governance:** add FastAPI third party license attribution ([#4297](https://github.com/aws-powertools/powertools-lambda-python/issues/4297)) + + + +## [v2.37.0] - 2024-04-18 +## Bug Fixes + +* **docs:** clarified usage of validation with fine grained responses ([#4101](https://github.com/aws-powertools/powertools-lambda-python/issues/4101)) +* **event_source:** fix typo in physicalname attribute for AmazonMQ events ([#4053](https://github.com/aws-powertools/powertools-lambda-python/issues/4053)) +* **typing:** make the case_sensitive field a boolean only ([#4128](https://github.com/aws-powertools/powertools-lambda-python/issues/4128)) +* **typing:** improve overloads to ensure the return type follows the default_value type ([#4114](https://github.com/aws-powertools/powertools-lambda-python/issues/4114)) + +## Documentation + +* **we-made-this:** new article on how to stream data with AWS Lambda & Powertools for AWS Lambda ([#4068](https://github.com/aws-powertools/powertools-lambda-python/issues/4068)) + +## Features + +* **Idempotency:** add feature for manipulating idempotent responses ([#4037](https://github.com/aws-powertools/powertools-lambda-python/issues/4037)) +* **event_handler:** add support for OpenAPI security schemes ([#4103](https://github.com/aws-powertools/powertools-lambda-python/issues/4103)) +* **logger:** add method to return currently configured keys ([#4033](https://github.com/aws-powertools/powertools-lambda-python/issues/4033)) + +## Maintenance + +* version bump +* **ci:** add monthly roadmap reminder workflow ([#4075](https://github.com/aws-powertools/powertools-lambda-python/issues/4075)) +* **ci:** prevent deprecated custom runner from being used ([#4061](https://github.com/aws-powertools/powertools-lambda-python/issues/4061)) +* **deps:** bump squidfunk/mkdocs-material from `065f3af` to `6b124e1` in /docs ([#4055](https://github.com/aws-powertools/powertools-lambda-python/issues/4055)) +* **deps:** bump squidfunk/mkdocs-material from `3307665` to `065f3af` in /docs ([#4052](https://github.com/aws-powertools/powertools-lambda-python/issues/4052)) +* **deps:** bump idna from 3.6 to 3.7 ([#4121](https://github.com/aws-powertools/powertools-lambda-python/issues/4121)) +* **deps:** bump sqlparse from 0.4.4 to 0.5.0 ([#4138](https://github.com/aws-powertools/powertools-lambda-python/issues/4138)) +* **deps:** bump squidfunk/mkdocs-material from `6b124e1` to `521644b` in /docs ([#4141](https://github.com/aws-powertools/powertools-lambda-python/issues/4141)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#4066](https://github.com/aws-powertools/powertools-lambda-python/issues/4066)) +* **deps:** bump pydantic from 1.10.14 to 1.10.15 ([#4064](https://github.com/aws-powertools/powertools-lambda-python/issues/4064)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#4042](https://github.com/aws-powertools/powertools-lambda-python/issues/4042)) +* **deps:** bump golang.org/x/sync from 0.6.0 to 0.7.0 in /layer/scripts/layer-balancer in the layer-balancer group ([#4071](https://github.com/aws-powertools/powertools-lambda-python/issues/4071)) +* **deps:** bump codecov/codecov-action from 4.1.1 to 4.2.0 ([#4072](https://github.com/aws-powertools/powertools-lambda-python/issues/4072)) +* **deps:** bump datadog-lambda from 5.91.0 to 5.92.0 ([#4038](https://github.com/aws-powertools/powertools-lambda-python/issues/4038)) +* **deps:** bump github.com/aws/aws-sdk-go-v2/config from 1.27.10 to 1.27.11 in /layer/scripts/layer-balancer in the layer-balancer group ([#4079](https://github.com/aws-powertools/powertools-lambda-python/issues/4079)) +* **deps:** bump typing-extensions from 4.10.0 to 4.11.0 ([#4080](https://github.com/aws-powertools/powertools-lambda-python/issues/4080)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.3 to 3.0.4 ([#4099](https://github.com/aws-powertools/powertools-lambda-python/issues/4099)) +* **deps:** bump codecov/codecov-action from 4.2.0 to 4.3.0 ([#4098](https://github.com/aws-powertools/powertools-lambda-python/issues/4098)) +* **deps:** bump docker/setup-buildx-action from 3.2.0 to 3.3.0 ([#4091](https://github.com/aws-powertools/powertools-lambda-python/issues/4091)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.112 to 0.1.113 ([#4136](https://github.com/aws-powertools/powertools-lambda-python/issues/4136)) +* **deps-dev:** bump aws-cdk from 2.135.0 to 2.136.0 ([#4090](https://github.com/aws-powertools/powertools-lambda-python/issues/4090)) +* **deps-dev:** bump types-redis from 4.6.0.20240311 to 4.6.0.20240409 ([#4094](https://github.com/aws-powertools/powertools-lambda-python/issues/4094)) +* **deps-dev:** bump aws-cdk-lib from 2.135.0 to 2.136.0 ([#4092](https://github.com/aws-powertools/powertools-lambda-python/issues/4092)) +* **deps-dev:** bump cfn-lint from 0.86.1 to 0.86.2 ([#4081](https://github.com/aws-powertools/powertools-lambda-python/issues/4081)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.135.0a0 to 2.136.0a0 ([#4095](https://github.com/aws-powertools/powertools-lambda-python/issues/4095)) +* **deps-dev:** bump filelock from 3.13.3 to 3.13.4 ([#4096](https://github.com/aws-powertools/powertools-lambda-python/issues/4096)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.106 to 0.1.107 ([#4082](https://github.com/aws-powertools/powertools-lambda-python/issues/4082)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.107 to 0.1.110 ([#4097](https://github.com/aws-powertools/powertools-lambda-python/issues/4097)) +* **deps-dev:** bump aws-cdk from 2.136.0 to 2.136.1 ([#4106](https://github.com/aws-powertools/powertools-lambda-python/issues/4106)) +* **deps-dev:** bump aws-cdk-lib from 2.136.0 to 2.136.1 ([#4107](https://github.com/aws-powertools/powertools-lambda-python/issues/4107)) +* **deps-dev:** bump sentry-sdk from 1.44.1 to 1.45.0 ([#4108](https://github.com/aws-powertools/powertools-lambda-python/issues/4108)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.110 to 0.1.112 ([#4109](https://github.com/aws-powertools/powertools-lambda-python/issues/4109)) +* **deps-dev:** bump sentry-sdk from 1.44.0 to 1.44.1 ([#4065](https://github.com/aws-powertools/powertools-lambda-python/issues/4065)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.134.0a0 to 2.135.0a0 ([#4063](https://github.com/aws-powertools/powertools-lambda-python/issues/4063)) +* **deps-dev:** bump aws-cdk from 2.136.1 to 2.137.0 ([#4115](https://github.com/aws-powertools/powertools-lambda-python/issues/4115)) +* **deps-dev:** bump the boto-typing group with 2 updates ([#4062](https://github.com/aws-powertools/powertools-lambda-python/issues/4062)) +* **deps-dev:** bump mypy-boto3-cloudwatch from 1.34.75 to 1.34.83 in the boto-typing group ([#4116](https://github.com/aws-powertools/powertools-lambda-python/issues/4116)) +* **deps-dev:** bump ruff from 0.3.5 to 0.3.7 ([#4123](https://github.com/aws-powertools/powertools-lambda-python/issues/4123)) +* **deps-dev:** bump aws-cdk-lib from 2.136.1 to 2.137.0 ([#4119](https://github.com/aws-powertools/powertools-lambda-python/issues/4119)) +* **deps-dev:** bump aws-cdk from 2.134.0 to 2.135.0 ([#4058](https://github.com/aws-powertools/powertools-lambda-python/issues/4058)) +* **deps-dev:** bump aws-cdk-lib from 2.134.0 to 2.135.0 ([#4057](https://github.com/aws-powertools/powertools-lambda-python/issues/4057)) +* **deps-dev:** bump mkdocs-material from 9.5.16 to 9.5.17 ([#4056](https://github.com/aws-powertools/powertools-lambda-python/issues/4056)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.136.0a0 to 2.137.0a0 ([#4124](https://github.com/aws-powertools/powertools-lambda-python/issues/4124)) +* **deps-dev:** bump mypy-boto3-cloudformation from 1.34.77 to 1.34.84 in the boto-typing group ([#4126](https://github.com/aws-powertools/powertools-lambda-python/issues/4126)) +* **deps-dev:** bump ruff from 0.3.4 to 0.3.5 ([#4049](https://github.com/aws-powertools/powertools-lambda-python/issues/4049)) +* **deps-dev:** bump mkdocs-material from 9.5.15 to 9.5.16 ([#4050](https://github.com/aws-powertools/powertools-lambda-python/issues/4050)) +* **deps-dev:** bump the boto-typing group with 1 update ([#4047](https://github.com/aws-powertools/powertools-lambda-python/issues/4047)) +* **deps-dev:** bump black from 24.3.0 to 24.4.0 ([#4135](https://github.com/aws-powertools/powertools-lambda-python/issues/4135)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.105 to 0.1.106 ([#4048](https://github.com/aws-powertools/powertools-lambda-python/issues/4048)) +* **deps-dev:** bump cfn-lint from 0.86.2 to 0.86.3 ([#4137](https://github.com/aws-powertools/powertools-lambda-python/issues/4137)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.113 to 0.1.115 ([#4142](https://github.com/aws-powertools/powertools-lambda-python/issues/4142)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.133.0a0 to 2.134.0a0 ([#4039](https://github.com/aws-powertools/powertools-lambda-python/issues/4039)) +* **deps-dev:** bump mkdocs-material from 9.5.17 to 9.5.18 ([#4143](https://github.com/aws-powertools/powertools-lambda-python/issues/4143)) +* **deps-dev:** bump types-redis from 4.6.0.20240409 to 4.6.0.20240417 ([#4145](https://github.com/aws-powertools/powertools-lambda-python/issues/4145)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.115 to 0.1.119 ([#4150](https://github.com/aws-powertools/powertools-lambda-python/issues/4150)) +* **deps-dev:** bump aws-cdk-lib from 2.133.0 to 2.134.0 ([#4031](https://github.com/aws-powertools/powertools-lambda-python/issues/4031)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.104 to 0.1.105 ([#4030](https://github.com/aws-powertools/powertools-lambda-python/issues/4030)) +* **deps-dev:** bump aws-cdk from 2.133.0 to 2.134.0 ([#4032](https://github.com/aws-powertools/powertools-lambda-python/issues/4032)) +* **deps-dev:** bump the boto-typing group with 1 update ([#4029](https://github.com/aws-powertools/powertools-lambda-python/issues/4029)) +* **deps-dev:** bump sentry-sdk from 1.43.0 to 1.44.0 ([#4040](https://github.com/aws-powertools/powertools-lambda-python/issues/4040)) +* **docs:** update highlighted lines on the Typing examples ([#4131](https://github.com/aws-powertools/powertools-lambda-python/issues/4131)) + + + +## [v2.36.0] - 2024-03-27 +## Bug Fixes + +* **event_handler:** always add 422 response to the schema ([#3995](https://github.com/aws-powertools/powertools-lambda-python/issues/3995)) +* **event_handler:** make decoded_body field optional in ApiGateway resolver ([#3937](https://github.com/aws-powertools/powertools-lambda-python/issues/3937)) +* **tracer:** add name sanitization for X-Ray subsegments ([#4005](https://github.com/aws-powertools/powertools-lambda-python/issues/4005)) + +## Code Refactoring + +* **logger:** add type annotation for append_keys method ([#3988](https://github.com/aws-powertools/powertools-lambda-python/issues/3988)) +* **parameters:** improve typing for get_secret method ([#3910](https://github.com/aws-powertools/powertools-lambda-python/issues/3910)) + +## Documentation + +* **batch:** improved the example demonstrating how to create a custom partial processor. ([#4024](https://github.com/aws-powertools/powertools-lambda-python/issues/4024)) +* **bedrock-agents:** fix type in Bedrock operation example ([#3948](https://github.com/aws-powertools/powertools-lambda-python/issues/3948)) +* **tutorial:** fix "Simplifying with Tracer" section in the tutorial ([#3962](https://github.com/aws-powertools/powertools-lambda-python/issues/3962)) + +## Features + +* **batch:** add flag in SqsFifoProcessor to enable continuous message processing ([#3954](https://github.com/aws-powertools/powertools-lambda-python/issues/3954)) +* **data_classes:** Add CloudWatchAlarmEvent data class ([#3868](https://github.com/aws-powertools/powertools-lambda-python/issues/3868)) +* **event-handler:** add compress option when serving Swagger HTML ([#3946](https://github.com/aws-powertools/powertools-lambda-python/issues/3946)) +* **event_handler:** define exception_handler directly from the router ([#3979](https://github.com/aws-powertools/powertools-lambda-python/issues/3979)) +* **metrics:** allow custom timestamps for metrics ([#4006](https://github.com/aws-powertools/powertools-lambda-python/issues/4006)) +* **parameters:** add feature for creating and updating Parameters and Secrets ([#2858](https://github.com/aws-powertools/powertools-lambda-python/issues/2858)) +* **tracer:** auto-disable tracer when for AWS SAM and Chalice environments ([#3949](https://github.com/aws-powertools/powertools-lambda-python/issues/3949)) + +## Maintenance + +* version bump +* **deps:** bump squidfunk/mkdocs-material from `3678304` to `6c81a89` in /docs ([#3973](https://github.com/aws-powertools/powertools-lambda-python/issues/3973)) +* **deps:** bump datadog-lambda from 5.89.0 to 5.90.0 ([#3941](https://github.com/aws-powertools/powertools-lambda-python/issues/3941)) +* **deps:** bump actions/checkout from 4.1.1 to 4.1.2 ([#3939](https://github.com/aws-powertools/powertools-lambda-python/issues/3939)) +* **deps:** bump redis from 5.0.2 to 5.0.3 ([#3929](https://github.com/aws-powertools/powertools-lambda-python/issues/3929)) +* **deps:** bump slsa-framework/slsa-github-generator from 1.9.0 to 1.10.0 ([#3997](https://github.com/aws-powertools/powertools-lambda-python/issues/3997)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#4001](https://github.com/aws-powertools/powertools-lambda-python/issues/4001)) +* **deps:** bump actions/dependency-review-action from 4.2.3 to 4.2.4 ([#4012](https://github.com/aws-powertools/powertools-lambda-python/issues/4012)) +* **deps:** bump docker/setup-buildx-action from 3.1.0 to 3.2.0 ([#3955](https://github.com/aws-powertools/powertools-lambda-python/issues/3955)) +* **deps:** bump actions/dependency-review-action from 4.1.3 to 4.2.3 ([#3993](https://github.com/aws-powertools/powertools-lambda-python/issues/3993)) +* **deps:** bump datadog-lambda from 5.90.0 to 5.91.0 ([#3958](https://github.com/aws-powertools/powertools-lambda-python/issues/3958)) +* **deps:** bump pypa/gh-action-pypi-publish from 1.8.12 to 1.8.14 ([#3918](https://github.com/aws-powertools/powertools-lambda-python/issues/3918)) +* **deps:** bump squidfunk/mkdocs-material from `6c81a89` to `3307665` in /docs ([#4017](https://github.com/aws-powertools/powertools-lambda-python/issues/4017)) +* **deps:** bump actions/dependency-review-action from 4.2.4 to 4.2.5 ([#4023](https://github.com/aws-powertools/powertools-lambda-python/issues/4023)) +* **deps:** bump aws-encryption-sdk from 3.1.1 to 3.2.0 ([#3983](https://github.com/aws-powertools/powertools-lambda-python/issues/3983)) +* **deps:** bump actions/setup-python from 5.0.0 to 5.1.0 ([#4022](https://github.com/aws-powertools/powertools-lambda-python/issues/4022)) +* **deps:** bump codecov/codecov-action from 4.1.0 to 4.1.1 ([#4021](https://github.com/aws-powertools/powertools-lambda-python/issues/4021)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#3972](https://github.com/aws-powertools/powertools-lambda-python/issues/3972)) +* **deps-dev:** bump filelock from 3.13.1 to 3.13.3 ([#4014](https://github.com/aws-powertools/powertools-lambda-python/issues/4014)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.90 to 0.1.91 ([#3975](https://github.com/aws-powertools/powertools-lambda-python/issues/3975)) +* **deps-dev:** bump types-python-dateutil from 2.9.0.20240315 to 2.9.0.20240316 ([#3977](https://github.com/aws-powertools/powertools-lambda-python/issues/3977)) +* **deps-dev:** bump mkdocs-material from 9.5.13 to 9.5.14 ([#3978](https://github.com/aws-powertools/powertools-lambda-python/issues/3978)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.132.1a0 to 2.133.0a0 ([#3976](https://github.com/aws-powertools/powertools-lambda-python/issues/3976)) +* **deps-dev:** bump the boto-typing group with 2 updates ([#3974](https://github.com/aws-powertools/powertools-lambda-python/issues/3974)) +* **deps-dev:** bump the boto-typing group with 2 updates ([#3982](https://github.com/aws-powertools/powertools-lambda-python/issues/3982)) +* **deps-dev:** bump ruff from 0.3.2 to 0.3.3 ([#3967](https://github.com/aws-powertools/powertools-lambda-python/issues/3967)) +* **deps-dev:** bump aws-cdk-lib from 2.132.1 to 2.133.0 ([#3965](https://github.com/aws-powertools/powertools-lambda-python/issues/3965)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.91 to 0.1.94 ([#3985](https://github.com/aws-powertools/powertools-lambda-python/issues/3985)) +* **deps-dev:** bump black from 24.2.0 to 24.3.0 ([#3968](https://github.com/aws-powertools/powertools-lambda-python/issues/3968)) +* **deps-dev:** bump types-python-dateutil from 2.8.19.20240311 to 2.9.0.20240315 ([#3966](https://github.com/aws-powertools/powertools-lambda-python/issues/3966)) +* **deps-dev:** bump aws-cdk from 2.132.1 to 2.133.0 ([#3963](https://github.com/aws-powertools/powertools-lambda-python/issues/3963)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3964](https://github.com/aws-powertools/powertools-lambda-python/issues/3964)) +* **deps-dev:** bump pytest-asyncio from 0.23.5.post1 to 0.23.6 ([#3984](https://github.com/aws-powertools/powertools-lambda-python/issues/3984)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3991](https://github.com/aws-powertools/powertools-lambda-python/issues/3991)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.89 to 0.1.90 ([#3957](https://github.com/aws-powertools/powertools-lambda-python/issues/3957)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3956](https://github.com/aws-powertools/powertools-lambda-python/issues/3956)) +* **deps-dev:** bump sentry-sdk from 1.42.0 to 1.43.0 ([#3992](https://github.com/aws-powertools/powertools-lambda-python/issues/3992)) +* **deps-dev:** bump coverage from 7.4.3 to 7.4.4 ([#3959](https://github.com/aws-powertools/powertools-lambda-python/issues/3959)) +* **deps-dev:** bump ruff from 0.3.3 to 0.3.4 ([#3996](https://github.com/aws-powertools/powertools-lambda-python/issues/3996)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.88 to 0.1.89 ([#3952](https://github.com/aws-powertools/powertools-lambda-python/issues/3952)) +* **deps-dev:** bump sentry-sdk from 1.41.0 to 1.42.0 ([#3951](https://github.com/aws-powertools/powertools-lambda-python/issues/3951)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3950](https://github.com/aws-powertools/powertools-lambda-python/issues/3950)) +* **deps-dev:** bump pytest-mock from 3.12.0 to 3.13.0 ([#3999](https://github.com/aws-powertools/powertools-lambda-python/issues/3999)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.94 to 0.1.96 ([#4002](https://github.com/aws-powertools/powertools-lambda-python/issues/4002)) +* **deps-dev:** bump the boto-typing group with 2 updates ([#3940](https://github.com/aws-powertools/powertools-lambda-python/issues/3940)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.87 to 0.1.88 ([#3942](https://github.com/aws-powertools/powertools-lambda-python/issues/3942)) +* **deps-dev:** bump pytest from 8.0.2 to 8.1.1 ([#3943](https://github.com/aws-powertools/powertools-lambda-python/issues/3943)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.131.0a0 to 2.132.1a0 ([#3944](https://github.com/aws-powertools/powertools-lambda-python/issues/3944)) +* **deps-dev:** bump cfn-lint from 0.86.0 to 0.86.1 ([#3998](https://github.com/aws-powertools/powertools-lambda-python/issues/3998)) +* **deps-dev:** bump aws-cdk from 2.132.0 to 2.132.1 ([#3938](https://github.com/aws-powertools/powertools-lambda-python/issues/3938)) +* **deps-dev:** bump aws-cdk-lib from 2.131.0 to 2.132.1 ([#3936](https://github.com/aws-powertools/powertools-lambda-python/issues/3936)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.96 to 0.1.99 ([#4008](https://github.com/aws-powertools/powertools-lambda-python/issues/4008)) +* **deps-dev:** bump aws-cdk from 2.131.0 to 2.132.0 ([#3928](https://github.com/aws-powertools/powertools-lambda-python/issues/3928)) +* **deps-dev:** bump types-redis from 4.6.0.20240218 to 4.6.0.20240311 ([#3931](https://github.com/aws-powertools/powertools-lambda-python/issues/3931)) +* **deps-dev:** bump types-python-dateutil from 2.8.19.20240106 to 2.8.19.20240311 ([#3932](https://github.com/aws-powertools/powertools-lambda-python/issues/3932)) +* **deps-dev:** bump pytest-mock from 3.13.0 to 3.14.0 ([#4007](https://github.com/aws-powertools/powertools-lambda-python/issues/4007)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.99 to 0.1.101 ([#4015](https://github.com/aws-powertools/powertools-lambda-python/issues/4015)) +* **deps-dev:** bump ruff from 0.3.0 to 0.3.2 ([#3925](https://github.com/aws-powertools/powertools-lambda-python/issues/3925)) +* **deps-dev:** bump mypy from 1.8.0 to 1.9.0 ([#3921](https://github.com/aws-powertools/powertools-lambda-python/issues/3921)) +* **deps-dev:** bump bandit from 1.7.7 to 1.7.8 ([#3920](https://github.com/aws-powertools/powertools-lambda-python/issues/3920)) +* **deps-dev:** bump pytest-cov from 4.1.0 to 5.0.0 ([#4013](https://github.com/aws-powertools/powertools-lambda-python/issues/4013)) +* **deps-dev:** bump pytest-asyncio from 0.23.5 to 0.23.5.post1 ([#3923](https://github.com/aws-powertools/powertools-lambda-python/issues/3923)) +* **deps-dev:** bump mkdocs-material from 9.5.14 to 9.5.15 ([#4016](https://github.com/aws-powertools/powertools-lambda-python/issues/4016)) +* **deps-dev:** bump the boto-typing group with 2 updates ([#3919](https://github.com/aws-powertools/powertools-lambda-python/issues/3919)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.101 to 0.1.104 ([#4020](https://github.com/aws-powertools/powertools-lambda-python/issues/4020)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.83 to 0.1.87 ([#3930](https://github.com/aws-powertools/powertools-lambda-python/issues/3930)) + + + +## [v2.35.1] - 2024-03-08 +## Bug Fixes + +* **data_sources:** ensure correct types on SQSMessageAttributes ([#3898](https://github.com/aws-powertools/powertools-lambda-python/issues/3898)) +* **event_handler:** validate POST bodies on BedrockAgentResolver ([#3903](https://github.com/aws-powertools/powertools-lambda-python/issues/3903)) +* **internal:** call ruff with correct args ([#3901](https://github.com/aws-powertools/powertools-lambda-python/issues/3901)) + +## Features + +* **event_handler:** use custom serializer during openapi serialization ([#3900](https://github.com/aws-powertools/powertools-lambda-python/issues/3900)) + +## Maintenance + +* version bump +* **deps:** bump aws-xray-sdk from 2.12.1 to 2.13.0 ([#3906](https://github.com/aws-powertools/powertools-lambda-python/issues/3906)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#3911](https://github.com/aws-powertools/powertools-lambda-python/issues/3911)) +* **deps:** bump squidfunk/mkdocs-material from `7be068b` to `3678304` in /docs ([#3894](https://github.com/aws-powertools/powertools-lambda-python/issues/3894)) +* **deps:** bump datadog-lambda from 5.88.0 to 5.89.0 ([#3907](https://github.com/aws-powertools/powertools-lambda-python/issues/3907)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.81 to 0.1.82 ([#3896](https://github.com/aws-powertools/powertools-lambda-python/issues/3896)) +* **deps-dev:** bump sentry-sdk from 1.40.6 to 1.41.0 ([#3905](https://github.com/aws-powertools/powertools-lambda-python/issues/3905)) +* **deps-dev:** bump mkdocs-material from 9.5.12 to 9.5.13 ([#3895](https://github.com/aws-powertools/powertools-lambda-python/issues/3895)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.82 to 0.1.83 ([#3908](https://github.com/aws-powertools/powertools-lambda-python/issues/3908)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3904](https://github.com/aws-powertools/powertools-lambda-python/issues/3904)) + + + +## [v2.35.0] - 2024-03-06 +## Bug Fixes + +* **event_handler:** OpenAPI schema version respects Pydantic version ([#3860](https://github.com/aws-powertools/powertools-lambda-python/issues/3860)) + +## Code Refactoring + +* **logger:** improve typing ([#3869](https://github.com/aws-powertools/powertools-lambda-python/issues/3869)) + +## Documentation + +* **event_handler:** add bedrock agent resolver documentation ([#3602](https://github.com/aws-powertools/powertools-lambda-python/issues/3602)) + +## Maintenance + +* version bump +* **deps:** bump docker/setup-buildx-action from 3.0.0 to 3.1.0 ([#3864](https://github.com/aws-powertools/powertools-lambda-python/issues/3864)) +* **deps:** bump actions/download-artifact from 4.1.3 to 4.1.4 ([#3875](https://github.com/aws-powertools/powertools-lambda-python/issues/3875)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3884](https://github.com/aws-powertools/powertools-lambda-python/issues/3884)) +* **deps:** bump squidfunk/mkdocs-material from `49d1bfd` to `7be068b` in /docs ([#3872](https://github.com/aws-powertools/powertools-lambda-python/issues/3872)) +* **deps:** bump squidfunk/mkdocs-material from `43b898a` to `49d1bfd` in /docs ([#3857](https://github.com/aws-powertools/powertools-lambda-python/issues/3857)) +* **deps:** bump codecov/codecov-action from 4.0.2 to 4.1.0 ([#3856](https://github.com/aws-powertools/powertools-lambda-python/issues/3856)) +* **deps:** bump redis from 5.0.1 to 5.0.2 ([#3867](https://github.com/aws-powertools/powertools-lambda-python/issues/3867)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3887](https://github.com/aws-powertools/powertools-lambda-python/issues/3887)) +* **deps:** bump actions/download-artifact from 4.1.2 to 4.1.3 ([#3862](https://github.com/aws-powertools/powertools-lambda-python/issues/3862)) +* **deps:** bump pypa/gh-action-pypi-publish from 1.8.11 to 1.8.12 ([#3863](https://github.com/aws-powertools/powertools-lambda-python/issues/3863)) +* **deps-dev:** bump aws-cdk-lib from 2.130.0 to 2.131.0 ([#3881](https://github.com/aws-powertools/powertools-lambda-python/issues/3881)) +* **deps-dev:** bump cfn-lint from 0.85.3 to 0.86.0 ([#3882](https://github.com/aws-powertools/powertools-lambda-python/issues/3882)) +* **deps-dev:** bump black from 24.1.1 to 24.2.0 ([#3760](https://github.com/aws-powertools/powertools-lambda-python/issues/3760)) +* **deps-dev:** bump cfn-lint from 0.85.2 to 0.85.3 ([#3861](https://github.com/aws-powertools/powertools-lambda-python/issues/3861)) +* **deps-dev:** bump aws-cdk from 2.130.0 to 2.131.0 ([#3883](https://github.com/aws-powertools/powertools-lambda-python/issues/3883)) +* **deps-dev:** bump mkdocs-material from 9.5.11 to 9.5.12 ([#3870](https://github.com/aws-powertools/powertools-lambda-python/issues/3870)) +* **deps-dev:** bump ruff from 0.2.2 to 0.3.0 ([#3871](https://github.com/aws-powertools/powertools-lambda-python/issues/3871)) +* **docs:** add Bedrock Agents to feature list ([#3889](https://github.com/aws-powertools/powertools-lambda-python/issues/3889)) + + + +## [v2.34.2] - 2024-02-26 +## Bug Fixes + +* **typing:** ensure return type is a str when default_value is set ([#3840](https://github.com/aws-powertools/powertools-lambda-python/issues/3840)) + +## Documentation + +* **install:** make minimum install the default option then extra ([#3834](https://github.com/aws-powertools/powertools-lambda-python/issues/3834)) + +## Features + +* **event-source:** add function to get multi-value query string params by name ([#3846](https://github.com/aws-powertools/powertools-lambda-python/issues/3846)) + +## Maintenance + +* version bump +* **ci:** remove aws-encryption-sdk from Lambda layer due to cffi being tied to python version ([#3853](https://github.com/aws-powertools/powertools-lambda-python/issues/3853)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#3844](https://github.com/aws-powertools/powertools-lambda-python/issues/3844)) +* **deps:** bump cryptography from 42.0.2 to 42.0.4 ([#3827](https://github.com/aws-powertools/powertools-lambda-python/issues/3827)) +* **deps:** bump codecov/codecov-action from 4.0.1 to 4.0.2 ([#3842](https://github.com/aws-powertools/powertools-lambda-python/issues/3842)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#3835](https://github.com/aws-powertools/powertools-lambda-python/issues/3835)) +* **deps-dev:** bump httpx from 0.26.0 to 0.27.0 ([#3828](https://github.com/aws-powertools/powertools-lambda-python/issues/3828)) +* **deps-dev:** bump aws-cdk from 2.128.0 to 2.129.0 ([#3831](https://github.com/aws-powertools/powertools-lambda-python/issues/3831)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3836](https://github.com/aws-powertools/powertools-lambda-python/issues/3836)) +* **deps-dev:** bump aws-cdk from 2.129.0 to 2.130.0 ([#3843](https://github.com/aws-powertools/powertools-lambda-python/issues/3843)) +* **deps-dev:** bump aws-cdk-lib from 2.128.0 to 2.130.0 ([#3838](https://github.com/aws-powertools/powertools-lambda-python/issues/3838)) + + + +## [v2.34.1] - 2024-02-21 +## Bug Fixes + +* **ci:** inject PR_LABELS env for PR Label automation ([#3819](https://github.com/aws-powertools/powertools-lambda-python/issues/3819)) +* **ci:** revert layer version bump write-only back to append ([#3818](https://github.com/aws-powertools/powertools-lambda-python/issues/3818)) +* **event-handler:** return dict on missing multi_value_headers ([#3824](https://github.com/aws-powertools/powertools-lambda-python/issues/3824)) +* **idempotency:** validate before saving to cache ([#3822](https://github.com/aws-powertools/powertools-lambda-python/issues/3822)) + +## Maintenance + +* version bump +* **deps-dev:** bump ruff from 0.2.1 to 0.2.2 ([#3802](https://github.com/aws-powertools/powertools-lambda-python/issues/3802)) +* **deps-dev:** bump the boto-typing group with 2 updates ([#3810](https://github.com/aws-powertools/powertools-lambda-python/issues/3810)) + + + +## [v2.34.0] - 2024-02-21 +## Bug Fixes + +* **ci:** create one layer artifact per region & merge ([#3808](https://github.com/aws-powertools/powertools-lambda-python/issues/3808)) +* **event-handler:** multi-value query string and validation of scalar parameters ([#3795](https://github.com/aws-powertools/powertools-lambda-python/issues/3795)) +* **event-handler:** swagger schema respects api stage ([#3796](https://github.com/aws-powertools/powertools-lambda-python/issues/3796)) +* **event-handler:** handle aliased parameters e.g., Query(alias="categoryType") ([#3766](https://github.com/aws-powertools/powertools-lambda-python/issues/3766)) + +## Code Refactoring + +* **feature-flags:** add intersection tests; structure refinement ([#3775](https://github.com/aws-powertools/powertools-lambda-python/issues/3775)) + +## Documentation + +* **feature_flags:** fix incorrect line markers and envelope name ([#3792](https://github.com/aws-powertools/powertools-lambda-python/issues/3792)) +* **home:** update layer version to 62 for package version 2.33.1 ([#3778](https://github.com/aws-powertools/powertools-lambda-python/issues/3778)) +* **home:** add note about POWERTOOLS_DEV side effects in CloudWatch Logs ([#3770](https://github.com/aws-powertools/powertools-lambda-python/issues/3770)) +* **homepage:** discord flat badge style; remove former devax email ([#3768](https://github.com/aws-powertools/powertools-lambda-python/issues/3768)) +* **homepage:** remove leftover announcement banner ([#3783](https://github.com/aws-powertools/powertools-lambda-python/issues/3783)) +* **roadmap:** latest roadmap update; use new grid to de-clutter homepage ([#3755](https://github.com/aws-powertools/powertools-lambda-python/issues/3755)) +* **we-made-this:** add swagger post ([#3799](https://github.com/aws-powertools/powertools-lambda-python/issues/3799)) +* **we-made-this:** add reinvent 2023 session ([#3790](https://github.com/aws-powertools/powertools-lambda-python/issues/3790)) + +## Features + +* **feature_flags:** add intersect actions for conditions ([#3692](https://github.com/aws-powertools/powertools-lambda-python/issues/3692)) + +## Maintenance + +* version bump +* **deps:** bump actions/dependency-review-action from 4.1.2 to 4.1.3 ([#3813](https://github.com/aws-powertools/powertools-lambda-python/issues/3813)) +* **deps:** bump actions/dependency-review-action from 4.1.0 to 4.1.2 ([#3800](https://github.com/aws-powertools/powertools-lambda-python/issues/3800)) +* **deps:** bump actions/dependency-review-action from 4.0.0 to 4.1.0 ([#3771](https://github.com/aws-powertools/powertools-lambda-python/issues/3771)) +* **deps:** bump squidfunk/mkdocs-material from `62d3668` to `43b898a` in /docs ([#3801](https://github.com/aws-powertools/powertools-lambda-python/issues/3801)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#3764](https://github.com/aws-powertools/powertools-lambda-python/issues/3764)) +* **deps:** bump squidfunk/mkdocs-material from `6a72238` to `62d3668` in /docs ([#3756](https://github.com/aws-powertools/powertools-lambda-python/issues/3756)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 2 updates ([#3814](https://github.com/aws-powertools/powertools-lambda-python/issues/3814)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3784](https://github.com/aws-powertools/powertools-lambda-python/issues/3784)) +* **deps-dev:** bump pytest from 8.0.0 to 8.0.1 ([#3812](https://github.com/aws-powertools/powertools-lambda-python/issues/3812)) +* **deps-dev:** bump aws-cdk from 2.127.0 to 2.128.0 ([#3776](https://github.com/aws-powertools/powertools-lambda-python/issues/3776)) +* **deps-dev:** bump the boto-typing group with 2 updates ([#3797](https://github.com/aws-powertools/powertools-lambda-python/issues/3797)) +* **deps-dev:** bump cfn-lint from 0.85.1 to 0.85.2 ([#3786](https://github.com/aws-powertools/powertools-lambda-python/issues/3786)) +* **deps-dev:** bump pytest-asyncio from 0.21.1 to 0.23.5 ([#3773](https://github.com/aws-powertools/powertools-lambda-python/issues/3773)) +* **deps-dev:** bump aws-cdk-lib from 2.127.0 to 2.128.0 ([#3777](https://github.com/aws-powertools/powertools-lambda-python/issues/3777)) +* **deps-dev:** bump sentry-sdk from 1.40.3 to 1.40.4 ([#3765](https://github.com/aws-powertools/powertools-lambda-python/issues/3765)) +* **deps-dev:** bump sentry-sdk from 1.40.4 to 1.40.5 ([#3805](https://github.com/aws-powertools/powertools-lambda-python/issues/3805)) +* **deps-dev:** bump mkdocs-material from 9.5.9 to 9.5.10 ([#3803](https://github.com/aws-powertools/powertools-lambda-python/issues/3803)) +* **deps-dev:** bump types-redis from 4.6.0.20240106 to 4.6.0.20240218 ([#3804](https://github.com/aws-powertools/powertools-lambda-python/issues/3804)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3757](https://github.com/aws-powertools/powertools-lambda-python/issues/3757)) +* **deps-dev:** bump aws-cdk-lib from 2.126.0 to 2.127.0 ([#3758](https://github.com/aws-powertools/powertools-lambda-python/issues/3758)) +* **deps-dev:** bump aws-cdk from 2.126.0 to 2.127.0 ([#3761](https://github.com/aws-powertools/powertools-lambda-python/issues/3761)) +* **deps-dev:** bump mkdocs-material from 9.5.8 to 9.5.9 ([#3759](https://github.com/aws-powertools/powertools-lambda-python/issues/3759)) +* **deps-dev:** bump sentry-sdk from 1.40.2 to 1.40.3 ([#3750](https://github.com/aws-powertools/powertools-lambda-python/issues/3750)) +* **deps-dev:** bump cfn-lint from 0.85.0 to 0.85.1 ([#3749](https://github.com/aws-powertools/powertools-lambda-python/issues/3749)) +* **deps-dev:** bump coverage from 7.4.1 to 7.4.2 ([#3811](https://github.com/aws-powertools/powertools-lambda-python/issues/3811)) +* **tests:** increase idempotency coverage with nested payload tampering tests ([#3809](https://github.com/aws-powertools/powertools-lambda-python/issues/3809)) + + + +## [v2.33.1] - 2024-02-09 +## Bug Fixes + +* **typing:** make Response headers covariant ([#3745](https://github.com/aws-powertools/powertools-lambda-python/issues/3745)) + +## Documentation + +* Add nathan hanks post community ([#3727](https://github.com/aws-powertools/powertools-lambda-python/issues/3727)) + +## Maintenance + +* version bump +* **ci:** drop support for Python 3.7 ([#3638](https://github.com/aws-powertools/powertools-lambda-python/issues/3638)) +* **ci:** enable Redis e2e tests ([#3718](https://github.com/aws-powertools/powertools-lambda-python/issues/3718)) +* **deps:** bump actions/setup-node from 4.0.1 to 4.0.2 ([#3737](https://github.com/aws-powertools/powertools-lambda-python/issues/3737)) +* **deps:** bump squidfunk/mkdocs-material from `e0d6c67` to `6a72238` in /docs ([#3735](https://github.com/aws-powertools/powertools-lambda-python/issues/3735)) +* **deps:** bump actions/dependency-review-action from 3.1.5 to 4.0.0 ([#3646](https://github.com/aws-powertools/powertools-lambda-python/issues/3646)) +* **deps:** bump release-drafter/release-drafter from 5.25.0 to 6.0.0 ([#3699](https://github.com/aws-powertools/powertools-lambda-python/issues/3699)) +* **deps:** bump actions/download-artifact from 4.1.1 to 4.1.2 ([#3725](https://github.com/aws-powertools/powertools-lambda-python/issues/3725)) +* **deps:** bump squidfunk/mkdocs-material from `a4a2029` to `e0d6c67` in /docs ([#3708](https://github.com/aws-powertools/powertools-lambda-python/issues/3708)) +* **deps:** bump codecov/codecov-action from 3.1.6 to 4.0.1 ([#3700](https://github.com/aws-powertools/powertools-lambda-python/issues/3700)) +* **deps:** bump actions/download-artifact from 3.0.2 to 4.1.1 ([#3612](https://github.com/aws-powertools/powertools-lambda-python/issues/3612)) +* **deps:** revert aws-cdk-lib as a runtime dep ([#3730](https://github.com/aws-powertools/powertools-lambda-python/issues/3730)) +* **deps:** bump actions/upload-artifact from 3.1.3 to 4.3.1 ([#3714](https://github.com/aws-powertools/powertools-lambda-python/issues/3714)) +* **deps-dev:** bump cfn-lint from 0.83.8 to 0.85.0 ([#3724](https://github.com/aws-powertools/powertools-lambda-python/issues/3724)) +* **deps-dev:** bump httpx from 0.24.1 to 0.26.0 ([#3712](https://github.com/aws-powertools/powertools-lambda-python/issues/3712)) +* **deps-dev:** bump pytest from 7.4.4 to 8.0.0 ([#3711](https://github.com/aws-powertools/powertools-lambda-python/issues/3711)) +* **deps-dev:** bump sentry-sdk from 1.40.1 to 1.40.2 ([#3740](https://github.com/aws-powertools/powertools-lambda-python/issues/3740)) +* **deps-dev:** bump coverage from 7.2.7 to 7.4.1 ([#3713](https://github.com/aws-powertools/powertools-lambda-python/issues/3713)) +* **deps-dev:** bump the boto-typing group with 7 updates ([#3709](https://github.com/aws-powertools/powertools-lambda-python/issues/3709)) +* **deps-dev:** bump types-python-dateutil from 2.8.19.14 to 2.8.19.20240106 ([#3720](https://github.com/aws-powertools/powertools-lambda-python/issues/3720)) +* **deps-dev:** bump mypy from 1.4.1 to 1.8.0 ([#3710](https://github.com/aws-powertools/powertools-lambda-python/issues/3710)) +* **deps-dev:** bump ruff from 0.2.0 to 0.2.1 ([#3742](https://github.com/aws-powertools/powertools-lambda-python/issues/3742)) +* **deps-dev:** bump isort from 5.11.5 to 5.13.2 ([#3723](https://github.com/aws-powertools/powertools-lambda-python/issues/3723)) +* **deps-dev:** bump pytest-socket from 0.6.0 to 0.7.0 ([#3721](https://github.com/aws-powertools/powertools-lambda-python/issues/3721)) +* **deps-dev:** bump ruff from 0.1.15 to 0.2.0 ([#3702](https://github.com/aws-powertools/powertools-lambda-python/issues/3702)) +* **deps-dev:** bump aws-cdk from 2.125.0 to 2.126.0 ([#3701](https://github.com/aws-powertools/powertools-lambda-python/issues/3701)) +* **deps-dev:** bump hvac from 1.2.1 to 2.1.0 ([#3738](https://github.com/aws-powertools/powertools-lambda-python/issues/3738)) +* **deps-dev:** bump black from 23.12.1 to 24.1.1 ([#3739](https://github.com/aws-powertools/powertools-lambda-python/issues/3739)) + + + +## [v2.33.0] - 2024-02-02 +## Bug Fixes + +* **data-masking:** fix and improve e2e tests for DataMasking ([#3695](https://github.com/aws-powertools/powertools-lambda-python/issues/3695)) +* **event-handler:** strip whitespace from Content-Type headers during OpenAPI schema validation ([#3677](https://github.com/aws-powertools/powertools-lambda-python/issues/3677)) + +## Documentation + +* **data-masking:** add docs for data masking utility ([#3186](https://github.com/aws-powertools/powertools-lambda-python/issues/3186)) +* **metrics:** fix empty metric warning filter ([#3660](https://github.com/aws-powertools/powertools-lambda-python/issues/3660)) +* **proccess:** add versioning and maintenance policy ([#3682](https://github.com/aws-powertools/powertools-lambda-python/issues/3682)) + +## Features + +* **event_handler:** support Header parameter validation in OpenAPI schema ([#3687](https://github.com/aws-powertools/powertools-lambda-python/issues/3687)) +* **event_handler:** add support for multiValueQueryStringParameters in OpenAPI schema ([#3667](https://github.com/aws-powertools/powertools-lambda-python/issues/3667)) + +## Maintenance + +* version bump +* **deps:** bump codecov/codecov-action from 3.1.5 to 3.1.6 ([#3683](https://github.com/aws-powertools/powertools-lambda-python/issues/3683)) +* **deps:** bump codecov/codecov-action from 3.1.4 to 3.1.5 ([#3674](https://github.com/aws-powertools/powertools-lambda-python/issues/3674)) +* **deps:** bump pydantic from 1.10.13 to 1.10.14 ([#3655](https://github.com/aws-powertools/powertools-lambda-python/issues/3655)) +* **deps:** bump squidfunk/mkdocs-material from `58eef6c` to `9aad7af` in /docs ([#3670](https://github.com/aws-powertools/powertools-lambda-python/issues/3670)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3665](https://github.com/aws-powertools/powertools-lambda-python/issues/3665)) +* **deps:** bump squidfunk/mkdocs-material from `9aad7af` to `a4a2029` in /docs ([#3679](https://github.com/aws-powertools/powertools-lambda-python/issues/3679)) +* **deps-dev:** bump sentry-sdk from 1.39.2 to 1.40.0 ([#3684](https://github.com/aws-powertools/powertools-lambda-python/issues/3684)) +* **deps-dev:** bump ruff from 0.1.14 to 0.1.15 ([#3685](https://github.com/aws-powertools/powertools-lambda-python/issues/3685)) +* **deps-dev:** bump ruff from 0.1.13 to 0.1.14 ([#3656](https://github.com/aws-powertools/powertools-lambda-python/issues/3656)) +* **deps-dev:** bump aws-cdk from 2.122.0 to 2.123.0 ([#3673](https://github.com/aws-powertools/powertools-lambda-python/issues/3673)) +* **deps-dev:** bump aws-cdk from 2.124.0 to 2.125.0 ([#3693](https://github.com/aws-powertools/powertools-lambda-python/issues/3693)) +* **deps-dev:** bump aws-cdk from 2.123.0 to 2.124.0 ([#3678](https://github.com/aws-powertools/powertools-lambda-python/issues/3678)) + + + +## [v2.32.0] - 2024-01-19 +## Bug Fixes + +* **event_handler:** escape OpenAPI schema on Swagger UI ([#3606](https://github.com/aws-powertools/powertools-lambda-python/issues/3606)) + +## Code Refactoring + +* **event-handler:** Inject CSS and JS files into SwaggerUI route when no custom CDN is used. ([#3562](https://github.com/aws-powertools/powertools-lambda-python/issues/3562)) +* **event_handler:** fix BedrockAgentResolver docstring ([#3645](https://github.com/aws-powertools/powertools-lambda-python/issues/3645)) + +## Documentation + +* **homepage:** add banner about Python 3.7 deprecation ([#3618](https://github.com/aws-powertools/powertools-lambda-python/issues/3618)) +* **i-made-this:** added new article on how to create a serverless API with CDK and Powertools ([#3605](https://github.com/aws-powertools/powertools-lambda-python/issues/3605)) + +## Features + +* **event_handler:** add support for additional response models ([#3591](https://github.com/aws-powertools/powertools-lambda-python/issues/3591)) +* **event_handler:** add support to download OpenAPI spec file ([#3571](https://github.com/aws-powertools/powertools-lambda-python/issues/3571)) +* **event_source:** Add support for S3 batch operations ([#3572](https://github.com/aws-powertools/powertools-lambda-python/issues/3572)) +* **event_source:** Add support for policyLevel field in CloudWatch Logs event and parser ([#3624](https://github.com/aws-powertools/powertools-lambda-python/issues/3624)) +* **idempotency:** leverage new DynamoDB Failed conditional writes behavior with ReturnValuesOnConditionCheckFailure ([#3446](https://github.com/aws-powertools/powertools-lambda-python/issues/3446)) +* **idempotency:** adding redis as idempotency backend ([#2567](https://github.com/aws-powertools/powertools-lambda-python/issues/2567)) + +## Maintenance + +* version bump +* **ci:** Disable Redis e2e until we drop Python 3.7 ([#3652](https://github.com/aws-powertools/powertools-lambda-python/issues/3652)) +* **ci:** update boto3 library version to 1.26.164+ ([#3632](https://github.com/aws-powertools/powertools-lambda-python/issues/3632)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3649](https://github.com/aws-powertools/powertools-lambda-python/issues/3649)) +* **deps:** bump jinja2 from 3.1.2 to 3.1.3 in /docs ([#3620](https://github.com/aws-powertools/powertools-lambda-python/issues/3620)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3639](https://github.com/aws-powertools/powertools-lambda-python/issues/3639)) +* **deps:** bump gitpython from 3.1.37 to 3.1.41 in /docs ([#3610](https://github.com/aws-powertools/powertools-lambda-python/issues/3610)) +* **deps:** bump squidfunk/mkdocs-material from `2f29d71` to `58eef6c` in /docs ([#3633](https://github.com/aws-powertools/powertools-lambda-python/issues/3633)) +* **deps:** bump redis from 4.6.0 to 5.0.1 ([#3613](https://github.com/aws-powertools/powertools-lambda-python/issues/3613)) +* **deps-dev:** bump gitpython from 3.1.40 to 3.1.41 ([#3611](https://github.com/aws-powertools/powertools-lambda-python/issues/3611)) +* **deps-dev:** bump sentry-sdk from 1.39.1 to 1.39.2 ([#3614](https://github.com/aws-powertools/powertools-lambda-python/issues/3614)) +* **deps-dev:** bump aws-cdk from 2.120.0 to 2.121.1 ([#3634](https://github.com/aws-powertools/powertools-lambda-python/issues/3634)) +* **deps-dev:** bump jinja2 from 3.1.2 to 3.1.3 ([#3619](https://github.com/aws-powertools/powertools-lambda-python/issues/3619)) +* **deps-dev:** bump cfn-lint from 0.83.7 to 0.83.8 ([#3603](https://github.com/aws-powertools/powertools-lambda-python/issues/3603)) +* **deps-dev:** bump aws-cdk from 2.121.1 to 2.122.0 ([#3648](https://github.com/aws-powertools/powertools-lambda-python/issues/3648)) +* **deps-dev:** bump ruff from 0.1.11 to 0.1.13 ([#3625](https://github.com/aws-powertools/powertools-lambda-python/issues/3625)) +* **deps-dev:** bump aws-cdk from 2.118.0 to 2.120.0 ([#3627](https://github.com/aws-powertools/powertools-lambda-python/issues/3627)) + + + +## [v2.31.0] - 2024-01-05 +## Bug Fixes + +* **ci:** fail dispatch analytics job when Lambda call fails ([#3579](https://github.com/aws-powertools/powertools-lambda-python/issues/3579)) + +## Code Refactoring + +* **parameters:** add overload signatures for get_parameter and get_parameters ([#3534](https://github.com/aws-powertools/powertools-lambda-python/issues/3534)) +* **parser:** Improve error message when parsing models and envelopes ([#3587](https://github.com/aws-powertools/powertools-lambda-python/issues/3587)) + +## Documentation + +* **middleware-factory:** Fix and improve typing ([#3569](https://github.com/aws-powertools/powertools-lambda-python/issues/3569)) + +## Features + +* **event-handler:** add description to request body in OpenAPI schema ([#3548](https://github.com/aws-powertools/powertools-lambda-python/issues/3548)) +* **event_handler:** support richer top level Tags ([#3543](https://github.com/aws-powertools/powertools-lambda-python/issues/3543)) +* **layers:** add new comercial region Canada - ca-west-1 ([#3549](https://github.com/aws-powertools/powertools-lambda-python/issues/3549)) + +## Maintenance + +* version bump +* **ci:** Remove dev dependencies locked to Pydantic v1 within the Pydantic v2 workflow. ([#3582](https://github.com/aws-powertools/powertools-lambda-python/issues/3582)) +* **deps:** bump squidfunk/mkdocs-material from `9af3b7e` to `2f29d71` in /docs ([#3559](https://github.com/aws-powertools/powertools-lambda-python/issues/3559)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 4 updates ([#3593](https://github.com/aws-powertools/powertools-lambda-python/issues/3593)) +* **deps:** bump actions/setup-node from 4.0.0 to 4.0.1 ([#3535](https://github.com/aws-powertools/powertools-lambda-python/issues/3535)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.2 to 3.0.3 ([#3536](https://github.com/aws-powertools/powertools-lambda-python/issues/3536)) +* **deps:** bump actions/dependency-review-action from 3.1.4 to 3.1.5 ([#3592](https://github.com/aws-powertools/powertools-lambda-python/issues/3592)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 2 updates ([#3544](https://github.com/aws-powertools/powertools-lambda-python/issues/3544)) +* **deps:** bump fastjsonschema from 2.19.0 to 2.19.1 ([#3567](https://github.com/aws-powertools/powertools-lambda-python/issues/3567)) +* **deps-dev:** bump ruff from 0.1.8 to 0.1.9 ([#3550](https://github.com/aws-powertools/powertools-lambda-python/issues/3550)) +* **deps-dev:** bump aws-cdk from 2.115.0 to 2.116.1 ([#3553](https://github.com/aws-powertools/powertools-lambda-python/issues/3553)) +* **deps-dev:** bump aws-cdk from 2.117.0 to 2.118.0 ([#3589](https://github.com/aws-powertools/powertools-lambda-python/issues/3589)) +* **deps-dev:** bump cfn-lint from 0.83.6 to 0.83.7 ([#3554](https://github.com/aws-powertools/powertools-lambda-python/issues/3554)) +* **deps-dev:** bump ruff from 0.1.9 to 0.1.10 ([#3583](https://github.com/aws-powertools/powertools-lambda-python/issues/3583)) +* **deps-dev:** bump pytest from 7.4.3 to 7.4.4 ([#3576](https://github.com/aws-powertools/powertools-lambda-python/issues/3576)) +* **deps-dev:** bump aws-cdk from 2.116.1 to 2.117.0 ([#3565](https://github.com/aws-powertools/powertools-lambda-python/issues/3565)) +* **deps-dev:** bump ruff from 0.1.10 to 0.1.11 ([#3588](https://github.com/aws-powertools/powertools-lambda-python/issues/3588)) + + + +## [v2.30.2] - 2023-12-18 +## Bug Fixes + +* **event-handler:** fix operation tags schema generation ([#3528](https://github.com/aws-powertools/powertools-lambda-python/issues/3528)) +* **event-handler:** set default OpenAPI version to 3.0.0 ([#3527](https://github.com/aws-powertools/powertools-lambda-python/issues/3527)) +* **event-handler:** upgrade Swagger UI to fix regressions ([#3526](https://github.com/aws-powertools/powertools-lambda-python/issues/3526)) + +## Maintenance + +* version bump +* **deps-dev:** bump cfn-lint from 0.83.5 to 0.83.6 ([#3521](https://github.com/aws-powertools/powertools-lambda-python/issues/3521)) + + + +## [v2.30.1] - 2023-12-15 +## Bug Fixes + +* **event_handler:** allow responses and metadata when using Router ([#3514](https://github.com/aws-powertools/powertools-lambda-python/issues/3514)) + +## Maintenance + +* version bump +* **deps-dev:** bump aws-cdk from 2.114.1 to 2.115.0 ([#3508](https://github.com/aws-powertools/powertools-lambda-python/issues/3508)) +* **deps-dev:** bump the boto-typing group with 11 updates ([#3509](https://github.com/aws-powertools/powertools-lambda-python/issues/3509)) +* **deps-dev:** bump sentry-sdk from 1.39.0 to 1.39.1 ([#3512](https://github.com/aws-powertools/powertools-lambda-python/issues/3512)) + + + +## [v2.30.0] - 2023-12-14 +## Bug Fixes + +* **docs:** make the Lambda Layer version consistent ([#3498](https://github.com/aws-powertools/powertools-lambda-python/issues/3498)) + +## Documentation + +* **customer-reference:** add Transformity as a customer reference ([#3497](https://github.com/aws-powertools/powertools-lambda-python/issues/3497)) + +## Features + +* **general:** add support for Python 3.12 ([#3304](https://github.com/aws-powertools/powertools-lambda-python/issues/3304)) + +## Maintenance + +* version bump +* **deps:** bump squidfunk/mkdocs-material from `876b39c` to `9af3b7e` in /docs ([#3486](https://github.com/aws-powertools/powertools-lambda-python/issues/3486)) +* **deps-dev:** bump sentry-sdk from 1.38.0 to 1.39.0 ([#3495](https://github.com/aws-powertools/powertools-lambda-python/issues/3495)) +* **deps-dev:** bump cfn-lint from 0.83.4 to 0.83.5 ([#3487](https://github.com/aws-powertools/powertools-lambda-python/issues/3487)) +* **deps-dev:** bump ruff from 0.1.7 to 0.1.8 ([#3501](https://github.com/aws-powertools/powertools-lambda-python/issues/3501)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3500](https://github.com/aws-powertools/powertools-lambda-python/issues/3500)) +* **tests:** temporarily disable E2E parallelism ([#3484](https://github.com/aws-powertools/powertools-lambda-python/issues/3484)) + + + +## [v2.29.1] - 2023-12-11 +## Bug Fixes + +* **logger:** log non-ascii characters as is when JSON stringifying ([#3475](https://github.com/aws-powertools/powertools-lambda-python/issues/3475)) + +## Maintenance + +* version bump +* **deps:** bump squidfunk/mkdocs-material from `8c72011` to `20241c6` in /docs ([#3470](https://github.com/aws-powertools/powertools-lambda-python/issues/3470)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 2 updates ([#3476](https://github.com/aws-powertools/powertools-lambda-python/issues/3476)) +* **deps:** bump actions/setup-python from 4.8.0 to 5.0.0 ([#3465](https://github.com/aws-powertools/powertools-lambda-python/issues/3465)) +* **deps:** bump squidfunk/mkdocs-material from `20241c6` to `876b39c` in /docs ([#3477](https://github.com/aws-powertools/powertools-lambda-python/issues/3477)) +* **deps:** bump datadog-lambda from 5.84.0 to 5.85.0 ([#3466](https://github.com/aws-powertools/powertools-lambda-python/issues/3466)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 2 updates ([#3467](https://github.com/aws-powertools/powertools-lambda-python/issues/3467)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#3469](https://github.com/aws-powertools/powertools-lambda-python/issues/3469)) +* **deps-dev:** bump aws-cdk from 2.113.0 to 2.114.1 ([#3464](https://github.com/aws-powertools/powertools-lambda-python/issues/3464)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3478](https://github.com/aws-powertools/powertools-lambda-python/issues/3478)) + + + +## [v2.29.0] - 2023-12-06 +## Bug Fixes + +* **event_handler:** serialize pydantic/dataclasses in exception handler ([#3455](https://github.com/aws-powertools/powertools-lambda-python/issues/3455)) +* **metrics:** lambda_handler typing, and **kwargs preservation all middlewares ([#3460](https://github.com/aws-powertools/powertools-lambda-python/issues/3460)) + +## Features + +* **event_sources:** add get_context() to standardize API Gateway Lambda Authorizer context in v1 and v2 ([#3454](https://github.com/aws-powertools/powertools-lambda-python/issues/3454)) + +## Maintenance + +* version bump +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#3441](https://github.com/aws-powertools/powertools-lambda-python/issues/3441)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.1 to 3.0.2 ([#3449](https://github.com/aws-powertools/powertools-lambda-python/issues/3449)) +* **deps:** bump datadog-lambda from 5.83.0 to 5.84.0 ([#3438](https://github.com/aws-powertools/powertools-lambda-python/issues/3438)) +* **deps:** bump cryptography from 41.0.4 to 41.0.6 ([#3431](https://github.com/aws-powertools/powertools-lambda-python/issues/3431)) +* **deps:** bump squidfunk/mkdocs-material from `fc42bac` to `8c72011` in /docs ([#3416](https://github.com/aws-powertools/powertools-lambda-python/issues/3416)) +* **deps:** bump actions/dependency-review-action from 3.1.3 to 3.1.4 ([#3426](https://github.com/aws-powertools/powertools-lambda-python/issues/3426)) +* **deps:** bump actions/setup-python from 4.7.1 to 4.8.0 ([#3456](https://github.com/aws-powertools/powertools-lambda-python/issues/3456)) +* **deps:** bump pypa/gh-action-pypi-publish from 1.8.10 to 1.8.11 ([#3433](https://github.com/aws-powertools/powertools-lambda-python/issues/3433)) +* **deps-dev:** bump cfn-lint from 0.83.3 to 0.83.4 ([#3450](https://github.com/aws-powertools/powertools-lambda-python/issues/3450)) +* **deps-dev:** bump ruff from 0.1.6 to 0.1.7 ([#3458](https://github.com/aws-powertools/powertools-lambda-python/issues/3458)) +* **deps-dev:** bump sentry-sdk from 1.36.0 to 1.38.0 ([#3435](https://github.com/aws-powertools/powertools-lambda-python/issues/3435)) +* **deps-dev:** bump aws-cdk-lib from 2.110.1 to 2.111.0 ([#3428](https://github.com/aws-powertools/powertools-lambda-python/issues/3428)) +* **deps-dev:** bump aws-cdk from 2.112.0 to 2.113.0 ([#3448](https://github.com/aws-powertools/powertools-lambda-python/issues/3448)) +* **deps-dev:** bump aws-cdk from 2.110.1 to 2.111.0 ([#3418](https://github.com/aws-powertools/powertools-lambda-python/issues/3418)) +* **deps-dev:** bump the boto-typing group with 11 updates ([#3427](https://github.com/aws-powertools/powertools-lambda-python/issues/3427)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3457](https://github.com/aws-powertools/powertools-lambda-python/issues/3457)) +* **deps-dev:** bump aws-cdk from 2.111.0 to 2.112.0 ([#3440](https://github.com/aws-powertools/powertools-lambda-python/issues/3440)) +* **layers:** Update log retention to 10 years ([#3424](https://github.com/aws-powertools/powertools-lambda-python/issues/3424)) + + + +## [v2.28.1] - 2023-11-28 +## Bug Fixes + +* **event_handler:** fix compress handling ([#3420](https://github.com/aws-powertools/powertools-lambda-python/issues/3420)) + +## Maintenance + +* version bump + + + +## [v2.28.0] - 2023-11-23 +## Bug Fixes + +* **event_handler:** hide error details by default ([#3406](https://github.com/aws-powertools/powertools-lambda-python/issues/3406)) +* **event_handler:** fix format for OpenAPI path templating ([#3399](https://github.com/aws-powertools/powertools-lambda-python/issues/3399)) +* **event_handler:** lazy load Pydantic to improve cold start ([#3397](https://github.com/aws-powertools/powertools-lambda-python/issues/3397)) +* **event_handler:** allow fine grained Response with data validation ([#3394](https://github.com/aws-powertools/powertools-lambda-python/issues/3394)) +* **event_handler:** apply serialization as the last operation for middlewares ([#3392](https://github.com/aws-powertools/powertools-lambda-python/issues/3392)) + +## Documentation + +* **event_handlers:** new data validation and OpenAPI feature ([#3386](https://github.com/aws-powertools/powertools-lambda-python/issues/3386)) + +## Features + +* **event_handler:** allow customers to catch request validation errors ([#3396](https://github.com/aws-powertools/powertools-lambda-python/issues/3396)) + +## Maintenance + +* version bump +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#3389](https://github.com/aws-powertools/powertools-lambda-python/issues/3389)) +* **deps:** bump datadog-lambda from 4.82.0 to 5.83.0 ([#3401](https://github.com/aws-powertools/powertools-lambda-python/issues/3401)) +* **deps-dev:** bump aws-cdk-lib from 2.110.0 to 2.110.1 ([#3402](https://github.com/aws-powertools/powertools-lambda-python/issues/3402)) +* **deps-dev:** bump pytest-xdist from 3.4.0 to 3.5.0 ([#3387](https://github.com/aws-powertools/powertools-lambda-python/issues/3387)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3400](https://github.com/aws-powertools/powertools-lambda-python/issues/3400)) +* **deps-dev:** bump sentry-sdk from 1.35.0 to 1.36.0 ([#3388](https://github.com/aws-powertools/powertools-lambda-python/issues/3388)) +* **deps-dev:** bump aws-cdk from 2.110.0 to 2.110.1 ([#3403](https://github.com/aws-powertools/powertools-lambda-python/issues/3403)) + + + +## [v2.27.1] - 2023-11-21 +## Bug Fixes + +* **logger:** allow custom JMESPath functions to extract correlation ID ([#3382](https://github.com/aws-powertools/powertools-lambda-python/issues/3382)) + +## Documentation + +* **event_handlers:** note that CORS and */* binary mime type don't work in API Gateway ([#3383](https://github.com/aws-powertools/powertools-lambda-python/issues/3383)) +* **logger:** improve ALC messaging in the PT context ([#3359](https://github.com/aws-powertools/powertools-lambda-python/issues/3359)) +* **logger:** Fix ALC link ([#3352](https://github.com/aws-powertools/powertools-lambda-python/issues/3352)) + +## Features + +* **logger:** implement addFilter/removeFilter to address static typing errors ([#3380](https://github.com/aws-powertools/powertools-lambda-python/issues/3380)) + +## Maintenance + +* version bump +* **ci:** lint and type checking removal in Pydantic v2 quality check ([#3360](https://github.com/aws-powertools/powertools-lambda-python/issues/3360)) +* **deps:** bump actions/github-script from 7.0.0 to 7.0.1 ([#3377](https://github.com/aws-powertools/powertools-lambda-python/issues/3377)) +* **deps:** bump squidfunk/mkdocs-material from `2c57e4d` to `fc42bac` in /docs ([#3375](https://github.com/aws-powertools/powertools-lambda-python/issues/3375)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 2 updates ([#3353](https://github.com/aws-powertools/powertools-lambda-python/issues/3353)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3374](https://github.com/aws-powertools/powertools-lambda-python/issues/3374)) +* **deps:** bump squidfunk/mkdocs-material from `f486dc9` to `2c57e4d` in /docs ([#3366](https://github.com/aws-powertools/powertools-lambda-python/issues/3366)) +* **deps-dev:** bump cfn-lint from 0.83.2 to 0.83.3 ([#3363](https://github.com/aws-powertools/powertools-lambda-python/issues/3363)) +* **deps-dev:** bump the boto-typing group with 11 updates ([#3362](https://github.com/aws-powertools/powertools-lambda-python/issues/3362)) +* **deps-dev:** bump aws-cdk-lib from 2.108.1 to 2.110.0 ([#3365](https://github.com/aws-powertools/powertools-lambda-python/issues/3365)) +* **deps-dev:** bump aws-cdk from 2.108.1 to 2.109.0 ([#3354](https://github.com/aws-powertools/powertools-lambda-python/issues/3354)) +* **deps-dev:** bump aws-cdk from 2.109.0 to 2.110.0 ([#3361](https://github.com/aws-powertools/powertools-lambda-python/issues/3361)) +* **deps-dev:** bump the boto-typing group with 2 updates ([#3376](https://github.com/aws-powertools/powertools-lambda-python/issues/3376)) +* **deps-dev:** bump ruff from 0.1.5 to 0.1.6 ([#3364](https://github.com/aws-powertools/powertools-lambda-python/issues/3364)) + + + +## [v2.27.0] - 2023-11-16 +## Features + +* **logger:** Adding support to new env variables ([#3348](https://github.com/aws-powertools/powertools-lambda-python/issues/3348)) + +## Maintenance + +* version bump +* **deps:** bump actions/github-script from 6.4.1 to 7.0.0 ([#3330](https://github.com/aws-powertools/powertools-lambda-python/issues/3330)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 2 updates ([#3340](https://github.com/aws-powertools/powertools-lambda-python/issues/3340)) +* **deps:** bump fastjsonschema from 2.18.1 to 2.19.0 ([#3337](https://github.com/aws-powertools/powertools-lambda-python/issues/3337)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#3345](https://github.com/aws-powertools/powertools-lambda-python/issues/3345)) +* **deps:** bump actions/dependency-review-action from 3.1.2 to 3.1.3 ([#3331](https://github.com/aws-powertools/powertools-lambda-python/issues/3331)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3329](https://github.com/aws-powertools/powertools-lambda-python/issues/3329)) +* **deps:** bump datadog-lambda from 4.81.0 to 4.82.0 ([#3338](https://github.com/aws-powertools/powertools-lambda-python/issues/3338)) +* **deps-dev:** bump cfn-lint from 0.83.1 to 0.83.2 ([#3335](https://github.com/aws-powertools/powertools-lambda-python/issues/3335)) +* **deps-dev:** bump aws-cdk from 2.108.0 to 2.108.1 ([#3344](https://github.com/aws-powertools/powertools-lambda-python/issues/3344)) +* **deps-dev:** bump sentry-sdk from 1.34.0 to 1.35.0 ([#3334](https://github.com/aws-powertools/powertools-lambda-python/issues/3334)) +* **deps-dev:** bump pytest-xdist from 3.3.1 to 3.4.0 ([#3332](https://github.com/aws-powertools/powertools-lambda-python/issues/3332)) +* **deps-dev:** bump aws-cdk-lib from 2.107.0 to 2.108.1 ([#3343](https://github.com/aws-powertools/powertools-lambda-python/issues/3343)) +* **deps-dev:** bump aws-cdk from 2.106.0 to 2.106.1 ([#3328](https://github.com/aws-powertools/powertools-lambda-python/issues/3328)) +* **deps-dev:** bump aws-cdk-lib from 2.105.0 to 2.106.0 ([#3319](https://github.com/aws-powertools/powertools-lambda-python/issues/3319)) +* **deps-dev:** bump aws-cdk from 2.105.0 to 2.106.0 ([#3320](https://github.com/aws-powertools/powertools-lambda-python/issues/3320)) +* **deps-dev:** bump aws-cdk from 2.106.1 to 2.108.0 ([#3341](https://github.com/aws-powertools/powertools-lambda-python/issues/3341)) +* **deps-dev:** bump aws-cdk-lib from 2.106.0 to 2.107.0 ([#3333](https://github.com/aws-powertools/powertools-lambda-python/issues/3333)) + + + +## [v2.26.1] - 2023-11-10 +## Bug Fixes + +* **event-handler:** enable path parameters on Bedrock handler ([#3312](https://github.com/aws-powertools/powertools-lambda-python/issues/3312)) +* **event_handler:** Router prefix mismatch regression after Middleware feat ([#3302](https://github.com/aws-powertools/powertools-lambda-python/issues/3302)) +* **event_source:** kinesis subsequenceNumber str type to int ([#3275](https://github.com/aws-powertools/powertools-lambda-python/issues/3275)) +* **parameters:** Respect POWERTOOLS_PARAMETERS_SSM_DECRYPT environment variable when getting multiple ssm parameters. ([#3241](https://github.com/aws-powertools/powertools-lambda-python/issues/3241)) + +## Documentation + +* **customer-reference:** add Vertex Pharmaceuticals as a customer reference ([#3210](https://github.com/aws-powertools/powertools-lambda-python/issues/3210)) +* **event-handler:** fixed SchemaValidationMiddleware link ([#3247](https://github.com/aws-powertools/powertools-lambda-python/issues/3247)) + +## Features + +* **data_classes:** add support for Bedrock Agents event ([#3262](https://github.com/aws-powertools/powertools-lambda-python/issues/3262)) +* **event_handler:** add Bedrock Agent event handler ([#3285](https://github.com/aws-powertools/powertools-lambda-python/issues/3285)) +* **event_handler:** add ability to expose a Swagger UI ([#3254](https://github.com/aws-powertools/powertools-lambda-python/issues/3254)) +* **event_handler:** generate OpenAPI specifications and validate input/output ([#3109](https://github.com/aws-powertools/powertools-lambda-python/issues/3109)) +* **parser:** add BedrockEventModel parser and envelope ([#3286](https://github.com/aws-powertools/powertools-lambda-python/issues/3286)) + +## Maintenance + +* version bump +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 2.1.5 to 3.0.0 ([#3289](https://github.com/aws-powertools/powertools-lambda-python/issues/3289)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3287](https://github.com/aws-powertools/powertools-lambda-python/issues/3287)) +* **deps:** bump actions/checkout from 4.1.0 to 4.1.1 ([#3220](https://github.com/aws-powertools/powertools-lambda-python/issues/3220)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#3282](https://github.com/aws-powertools/powertools-lambda-python/issues/3282)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 2.1.4 to 2.1.5 ([#3281](https://github.com/aws-powertools/powertools-lambda-python/issues/3281)) +* **deps:** bump release-drafter/release-drafter from 5.24.0 to 5.25.0 ([#3219](https://github.com/aws-powertools/powertools-lambda-python/issues/3219)) +* **deps:** bump squidfunk/mkdocs-material from `cb38dc2` to `df9409b` in /docs ([#3216](https://github.com/aws-powertools/powertools-lambda-python/issues/3216)) +* **deps:** bump urllib3 from 1.26.17 to 1.26.18 ([#3222](https://github.com/aws-powertools/powertools-lambda-python/issues/3222)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 2 updates ([#3298](https://github.com/aws-powertools/powertools-lambda-python/issues/3298)) +* **deps:** bump squidfunk/mkdocs-material from `772e14e` to `f486dc9` in /docs ([#3299](https://github.com/aws-powertools/powertools-lambda-python/issues/3299)) +* **deps:** bump datadog-lambda from 4.80.0 to 4.81.0 ([#3228](https://github.com/aws-powertools/powertools-lambda-python/issues/3228)) +* **deps:** bump actions/setup-node from 3.8.1 to 4.0.0 ([#3244](https://github.com/aws-powertools/powertools-lambda-python/issues/3244)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.0 to 3.0.1 ([#3300](https://github.com/aws-powertools/powertools-lambda-python/issues/3300)) +* **deps:** bump actions/dependency-review-action from 3.1.0 to 3.1.1 ([#3301](https://github.com/aws-powertools/powertools-lambda-python/issues/3301)) +* **deps:** bump squidfunk/mkdocs-material from `df9409b` to `772e14e` in /docs ([#3265](https://github.com/aws-powertools/powertools-lambda-python/issues/3265)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3305](https://github.com/aws-powertools/powertools-lambda-python/issues/3305)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 2 updates ([#3248](https://github.com/aws-powertools/powertools-lambda-python/issues/3248)) +* **deps:** bump actions/dependency-review-action from 3.1.1 to 3.1.2 ([#3308](https://github.com/aws-powertools/powertools-lambda-python/issues/3308)) +* **deps:** bump ossf/scorecard-action from 2.3.0 to 2.3.1 ([#3245](https://github.com/aws-powertools/powertools-lambda-python/issues/3245)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3310](https://github.com/aws-powertools/powertools-lambda-python/issues/3310)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3215](https://github.com/aws-powertools/powertools-lambda-python/issues/3215)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#3313](https://github.com/aws-powertools/powertools-lambda-python/issues/3313)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#3278](https://github.com/aws-powertools/powertools-lambda-python/issues/3278)) +* **deps-dev:** bump pytest from 7.4.2 to 7.4.3 ([#3249](https://github.com/aws-powertools/powertools-lambda-python/issues/3249)) +* **deps-dev:** bump ruff from 0.1.1 to 0.1.2 ([#3250](https://github.com/aws-powertools/powertools-lambda-python/issues/3250)) +* **deps-dev:** bump the boto-typing group with 2 updates ([#3242](https://github.com/aws-powertools/powertools-lambda-python/issues/3242)) +* **deps-dev:** bump aws-cdk-lib from 2.102.0 to 2.103.0 ([#3258](https://github.com/aws-powertools/powertools-lambda-python/issues/3258)) +* **deps-dev:** bump cfn-lint from 0.82.2 to 0.83.0 ([#3243](https://github.com/aws-powertools/powertools-lambda-python/issues/3243)) +* **deps-dev:** bump ruff from 0.1.2 to 0.1.3 ([#3257](https://github.com/aws-powertools/powertools-lambda-python/issues/3257)) +* **deps-dev:** bump aws-cdk from 2.102.0 to 2.103.0 ([#3259](https://github.com/aws-powertools/powertools-lambda-python/issues/3259)) +* **deps-dev:** bump ruff from 0.1.0 to 0.1.1 ([#3235](https://github.com/aws-powertools/powertools-lambda-python/issues/3235)) +* **deps-dev:** bump aws-cdk-lib from 2.103.0 to 2.103.1 ([#3263](https://github.com/aws-powertools/powertools-lambda-python/issues/3263)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3231](https://github.com/aws-powertools/powertools-lambda-python/issues/3231)) +* **deps-dev:** bump aws-cdk from 2.101.1 to 2.102.0 ([#3232](https://github.com/aws-powertools/powertools-lambda-python/issues/3232)) +* **deps-dev:** bump aws-cdk from 2.103.0 to 2.103.1 ([#3264](https://github.com/aws-powertools/powertools-lambda-python/issues/3264)) +* **deps-dev:** bump cfn-lint from 0.82.0 to 0.82.2 ([#3229](https://github.com/aws-powertools/powertools-lambda-python/issues/3229)) +* **deps-dev:** bump cfn-lint from 0.83.0 to 0.83.1 ([#3274](https://github.com/aws-powertools/powertools-lambda-python/issues/3274)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3273](https://github.com/aws-powertools/powertools-lambda-python/issues/3273)) +* **deps-dev:** bump cfn-lint from 0.81.0 to 0.82.0 ([#3224](https://github.com/aws-powertools/powertools-lambda-python/issues/3224)) +* **deps-dev:** bump aws-cdk from 2.101.0 to 2.101.1 ([#3223](https://github.com/aws-powertools/powertools-lambda-python/issues/3223)) +* **deps-dev:** bump sentry-sdk from 1.32.0 to 1.33.1 ([#3277](https://github.com/aws-powertools/powertools-lambda-python/issues/3277)) +* **deps-dev:** bump urllib3 from 1.26.17 to 1.26.18 in /layer ([#3221](https://github.com/aws-powertools/powertools-lambda-python/issues/3221)) +* **deps-dev:** bump aws-cdk from 2.103.1 to 2.104.0 ([#3288](https://github.com/aws-powertools/powertools-lambda-python/issues/3288)) +* **deps-dev:** bump sentry-sdk from 1.33.1 to 1.34.0 ([#3290](https://github.com/aws-powertools/powertools-lambda-python/issues/3290)) +* **deps-dev:** bump aws-cdk-lib from 2.103.1 to 2.104.0 ([#3291](https://github.com/aws-powertools/powertools-lambda-python/issues/3291)) +* **deps-dev:** bump aws-cdk-lib from 2.100.0 to 2.101.1 ([#3217](https://github.com/aws-powertools/powertools-lambda-python/issues/3217)) +* **deps-dev:** bump aws-cdk from 2.100.0 to 2.101.0 ([#3214](https://github.com/aws-powertools/powertools-lambda-python/issues/3214)) +* **deps-dev:** bump aws-cdk from 2.104.0 to 2.105.0 ([#3307](https://github.com/aws-powertools/powertools-lambda-python/issues/3307)) +* **deps-dev:** bump ruff from 0.1.3 to 0.1.4 ([#3297](https://github.com/aws-powertools/powertools-lambda-python/issues/3297)) +* **deps-dev:** bump aws-cdk-lib from 2.104.0 to 2.105.0 ([#3309](https://github.com/aws-powertools/powertools-lambda-python/issues/3309)) +* **deps-dev:** bump the boto-typing group with 2 updates ([#3211](https://github.com/aws-powertools/powertools-lambda-python/issues/3211)) +* **deps-dev:** bump the boto-typing group with 3 updates ([#3314](https://github.com/aws-powertools/powertools-lambda-python/issues/3314)) +* **deps-dev:** bump ruff from 0.1.4 to 0.1.5 ([#3315](https://github.com/aws-powertools/powertools-lambda-python/issues/3315)) +* **deps-dev:** bump ruff from 0.0.292 to 0.1.0 ([#3213](https://github.com/aws-powertools/powertools-lambda-python/issues/3213)) + + + +## [v2.26.0] - 2023-10-13 +## Bug Fixes + +* **logger:** force Logger to use local timezone when UTC flag is not set ([#3168](https://github.com/aws-powertools/powertools-lambda-python/issues/3168)) +* **parameter:** improve AppConfig cached configuration retrieval ([#3195](https://github.com/aws-powertools/powertools-lambda-python/issues/3195)) + +## Code Refactoring + +* **data-masking:** disable e2e tests. ([#3204](https://github.com/aws-powertools/powertools-lambda-python/issues/3204)) +* **data_masking:** move Data Masking utility to a private folder ([#3202](https://github.com/aws-powertools/powertools-lambda-python/issues/3202)) + +## Documentation + +* **contributing:** initial structure for revamped contributing guide ([#3133](https://github.com/aws-powertools/powertools-lambda-python/issues/3133)) +* **event_handler:** add information about case-insensitive header lookup function ([#3183](https://github.com/aws-powertools/powertools-lambda-python/issues/3183)) + +## Features + +* **data_masking:** add new sensitive data masking utility ([#2197](https://github.com/aws-powertools/powertools-lambda-python/issues/2197)) +* **event_handler:** add support to VPC Lattice payload v2 ([#3153](https://github.com/aws-powertools/powertools-lambda-python/issues/3153)) +* **layers:** add arm64 support in more regions ([#3151](https://github.com/aws-powertools/powertools-lambda-python/issues/3151)) +* **logger:** new stack_trace field with rich exception details ([#3147](https://github.com/aws-powertools/powertools-lambda-python/issues/3147)) +* **parser:** infer model from type hint ([#3181](https://github.com/aws-powertools/powertools-lambda-python/issues/3181)) + +## Maintenance + +* version bump +* **deps:** bump squidfunk/mkdocs-material from `cbfecae` to `a4cfa88` in /docs ([#3175](https://github.com/aws-powertools/powertools-lambda-python/issues/3175)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3174](https://github.com/aws-powertools/powertools-lambda-python/issues/3174)) +* **deps:** bump squidfunk/mkdocs-material from `b41ba6d` to `06673a1` in /docs ([#3124](https://github.com/aws-powertools/powertools-lambda-python/issues/3124)) +* **deps:** bump ossf/scorecard-action from 2.2.0 to 2.3.0 ([#3178](https://github.com/aws-powertools/powertools-lambda-python/issues/3178)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#3198](https://github.com/aws-powertools/powertools-lambda-python/issues/3198)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#3177](https://github.com/aws-powertools/powertools-lambda-python/issues/3177)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3127](https://github.com/aws-powertools/powertools-lambda-python/issues/3127)) +* **deps:** bump urllib3 from 1.26.16 to 1.26.17 ([#3162](https://github.com/aws-powertools/powertools-lambda-python/issues/3162)) +* **deps:** bump aws-xray-sdk from 2.12.0 to 2.12.1 ([#3197](https://github.com/aws-powertools/powertools-lambda-python/issues/3197)) +* **deps:** bump fastjsonschema from 2.18.0 to 2.18.1 ([#3159](https://github.com/aws-powertools/powertools-lambda-python/issues/3159)) +* **deps:** bump actions/setup-python from 4.7.0 to 4.7.1 ([#3158](https://github.com/aws-powertools/powertools-lambda-python/issues/3158)) +* **deps:** bump actions/checkout from 4.0.0 to 4.1.0 ([#3128](https://github.com/aws-powertools/powertools-lambda-python/issues/3128)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3156](https://github.com/aws-powertools/powertools-lambda-python/issues/3156)) +* **deps:** bump squidfunk/mkdocs-material from `e5f28aa` to `cbfecae` in /docs ([#3157](https://github.com/aws-powertools/powertools-lambda-python/issues/3157)) +* **deps:** bump squidfunk/mkdocs-material from `06673a1` to `e5f28aa` in /docs ([#3134](https://github.com/aws-powertools/powertools-lambda-python/issues/3134)) +* **deps:** bump squidfunk/mkdocs-material from `a4cfa88` to `cb38dc2` in /docs ([#3189](https://github.com/aws-powertools/powertools-lambda-python/issues/3189)) +* **deps:** bump pydantic from 1.10.12 to 1.10.13 ([#3144](https://github.com/aws-powertools/powertools-lambda-python/issues/3144)) +* **deps:** bump gitpython from 3.1.35 to 3.1.37 in /docs ([#3188](https://github.com/aws-powertools/powertools-lambda-python/issues/3188)) +* **deps-dev:** bump types-requests from 2.31.0.5 to 2.31.0.6 ([#3145](https://github.com/aws-powertools/powertools-lambda-python/issues/3145)) +* **deps-dev:** bump aws-cdk from 2.98.0 to 2.99.0 ([#3148](https://github.com/aws-powertools/powertools-lambda-python/issues/3148)) +* **deps-dev:** bump the boto-typing group with 2 updates ([#3143](https://github.com/aws-powertools/powertools-lambda-python/issues/3143)) +* **deps-dev:** bump aws-cdk from 2.99.1 to 2.100.0 ([#3185](https://github.com/aws-powertools/powertools-lambda-python/issues/3185)) +* **deps-dev:** bump aws-cdk from 2.97.0 to 2.98.0 ([#3139](https://github.com/aws-powertools/powertools-lambda-python/issues/3139)) +* **deps-dev:** bump aws-cdk from 2.96.2 to 2.97.0 ([#3129](https://github.com/aws-powertools/powertools-lambda-python/issues/3129)) +* **deps-dev:** bump types-requests from 2.31.0.3 to 2.31.0.5 ([#3136](https://github.com/aws-powertools/powertools-lambda-python/issues/3136)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3135](https://github.com/aws-powertools/powertools-lambda-python/issues/3135)) +* **deps-dev:** bump ruff from 0.0.291 to 0.0.292 ([#3161](https://github.com/aws-powertools/powertools-lambda-python/issues/3161)) +* **deps-dev:** bump ruff from 0.0.290 to 0.0.291 ([#3126](https://github.com/aws-powertools/powertools-lambda-python/issues/3126)) +* **deps-dev:** bump aws-cdk from 2.99.0 to 2.99.1 ([#3155](https://github.com/aws-powertools/powertools-lambda-python/issues/3155)) +* **deps-dev:** bump sentry-sdk from 1.31.0 to 1.32.0 ([#3192](https://github.com/aws-powertools/powertools-lambda-python/issues/3192)) +* **deps-dev:** bump urllib3 from 1.26.16 to 1.26.17 in /layer ([#3163](https://github.com/aws-powertools/powertools-lambda-python/issues/3163)) +* **deps-dev:** bump cfn-lint from 0.80.3 to 0.80.4 ([#3166](https://github.com/aws-powertools/powertools-lambda-python/issues/3166)) +* **deps-dev:** bump cfn-lint from 0.80.2 to 0.80.3 ([#3125](https://github.com/aws-powertools/powertools-lambda-python/issues/3125)) +* **deps-dev:** bump cfn-lint from 0.80.4 to 0.81.0 ([#3179](https://github.com/aws-powertools/powertools-lambda-python/issues/3179)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3196](https://github.com/aws-powertools/powertools-lambda-python/issues/3196)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3170](https://github.com/aws-powertools/powertools-lambda-python/issues/3170)) + + + +## [v2.25.1] - 2023-09-22 +## Bug Fixes + +* **logger:** add explicit None return type annotations ([#3113](https://github.com/aws-powertools/powertools-lambda-python/issues/3113)) +* **metrics:** support additional arguments in functions wrapped with log_metrics decorator ([#3120](https://github.com/aws-powertools/powertools-lambda-python/issues/3120)) + +## Maintenance + +* version bump +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3108](https://github.com/aws-powertools/powertools-lambda-python/issues/3108)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3115](https://github.com/aws-powertools/powertools-lambda-python/issues/3115)) +* **deps:** bump squidfunk/mkdocs-material from `4ff781e` to `b41ba6d` in /docs ([#3117](https://github.com/aws-powertools/powertools-lambda-python/issues/3117)) +* **deps:** bump squidfunk/mkdocs-material from `c4890ab` to `4ff781e` in /docs ([#3110](https://github.com/aws-powertools/powertools-lambda-python/issues/3110)) +* **deps-dev:** bump ruff from 0.0.289 to 0.0.290 ([#3105](https://github.com/aws-powertools/powertools-lambda-python/issues/3105)) +* **deps-dev:** bump aws-cdk from 2.96.1 to 2.96.2 ([#3102](https://github.com/aws-powertools/powertools-lambda-python/issues/3102)) +* **deps-dev:** bump the boto-typing group with 3 updates ([#3118](https://github.com/aws-powertools/powertools-lambda-python/issues/3118)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3101](https://github.com/aws-powertools/powertools-lambda-python/issues/3101)) +* **deps-dev:** bump cfn-lint from 0.79.11 to 0.80.2 ([#3107](https://github.com/aws-powertools/powertools-lambda-python/issues/3107)) +* **deps-dev:** bump types-requests from 2.31.0.2 to 2.31.0.3 ([#3114](https://github.com/aws-powertools/powertools-lambda-python/issues/3114)) + + + +## [v2.25.0] - 2023-09-15 +## Code Refactoring + +* **parameters:** BaseProvider._get to also support Dict ([#3090](https://github.com/aws-powertools/powertools-lambda-python/issues/3090)) + +## Documentation + +* **event_handler:** fix typing in micro function example ([#3098](https://github.com/aws-powertools/powertools-lambda-python/issues/3098)) +* **event_handler:** add micro function examples ([#3056](https://github.com/aws-powertools/powertools-lambda-python/issues/3056)) +* **we-made-this:** fix broken Twitch video embeds ([#3096](https://github.com/aws-powertools/powertools-lambda-python/issues/3096)) + +## Features + +* **event_source:** add Kinesis Firehose Data Transformation data class ([#3029](https://github.com/aws-powertools/powertools-lambda-python/issues/3029)) +* **event_sources:** add Secrets Manager secret rotation event ([#3061](https://github.com/aws-powertools/powertools-lambda-python/issues/3061)) + +## Maintenance + +* version bump +* **automation:** remove previous labels when PR is updated ([#3066](https://github.com/aws-powertools/powertools-lambda-python/issues/3066)) +* **deps:** bump actions/dependency-review-action from 3.0.8 to 3.1.0 ([#3071](https://github.com/aws-powertools/powertools-lambda-python/issues/3071)) +* **deps:** bump docker/setup-qemu-action from 2.2.0 to 3.0.0 ([#3081](https://github.com/aws-powertools/powertools-lambda-python/issues/3081)) +* **deps:** bump docker/setup-buildx-action from 2.10.0 to 3.0.0 ([#3083](https://github.com/aws-powertools/powertools-lambda-python/issues/3083)) +* **deps:** bump squidfunk/mkdocs-material from `dd1770c` to `c4890ab` in /docs ([#3078](https://github.com/aws-powertools/powertools-lambda-python/issues/3078)) +* **deps-dev:** bump cfn-lint from 0.79.9 to 0.79.10 ([#3077](https://github.com/aws-powertools/powertools-lambda-python/issues/3077)) +* **deps-dev:** bump hvac from 1.2.0 to 1.2.1 ([#3075](https://github.com/aws-powertools/powertools-lambda-python/issues/3075)) +* **deps-dev:** bump ruff from 0.0.288 to 0.0.289 ([#3080](https://github.com/aws-powertools/powertools-lambda-python/issues/3080)) +* **deps-dev:** bump ruff from 0.0.287 to 0.0.288 ([#3076](https://github.com/aws-powertools/powertools-lambda-python/issues/3076)) +* **deps-dev:** bump aws-cdk from 2.95.0 to 2.95.1 ([#3074](https://github.com/aws-powertools/powertools-lambda-python/issues/3074)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3085](https://github.com/aws-powertools/powertools-lambda-python/issues/3085)) +* **deps-dev:** bump aws-cdk from 2.95.1 to 2.96.0 ([#3087](https://github.com/aws-powertools/powertools-lambda-python/issues/3087)) +* **deps-dev:** bump sentry-sdk from 1.30.0 to 1.31.0 ([#3086](https://github.com/aws-powertools/powertools-lambda-python/issues/3086)) +* **deps-dev:** bump aws-cdk from 2.94.0 to 2.95.0 ([#3070](https://github.com/aws-powertools/powertools-lambda-python/issues/3070)) +* **deps-dev:** bump cfn-lint from 0.79.10 to 0.79.11 ([#3088](https://github.com/aws-powertools/powertools-lambda-python/issues/3088)) +* **deps-dev:** bump aws-cdk from 2.96.0 to 2.96.1 ([#3093](https://github.com/aws-powertools/powertools-lambda-python/issues/3093)) +* **typing:** move backwards compat types to shared types ([#3092](https://github.com/aws-powertools/powertools-lambda-python/issues/3092)) + + + +## [v2.24.0] - 2023-09-08 +## Bug Fixes + +* **event_handler:** expanding safe URI characters to include +$& ([#3026](https://github.com/aws-powertools/powertools-lambda-python/issues/3026)) +* **parser:** change ApproximateCreationDateTime field to datetime in DynamoDBStreamChangedRecordModel ([#3049](https://github.com/aws-powertools/powertools-lambda-python/issues/3049)) + +## Code Refactoring + +* **batch:** type response() method ([#3023](https://github.com/aws-powertools/powertools-lambda-python/issues/3023)) + +## Documentation + +* **event_handler:** demonstrate how to combine logger correlation ID and middleware ([#3064](https://github.com/aws-powertools/powertools-lambda-python/issues/3064)) +* **event_handler:** use correct correlation_id for logger in middleware example ([#3063](https://github.com/aws-powertools/powertools-lambda-python/issues/3063)) +* **idempotency:** use tab navigation, improves custom serializer example, and additional explanations ([#3067](https://github.com/aws-powertools/powertools-lambda-python/issues/3067)) + +## Features + +* **event_handler:** add Middleware support for REST Event Handler ([#2917](https://github.com/aws-powertools/powertools-lambda-python/issues/2917)) +* **idempotency:** add support to custom serialization/deserialization on idempotency decorator ([#2951](https://github.com/aws-powertools/powertools-lambda-python/issues/2951)) + +## Maintenance + +* version bump +* **deps:** bump squidfunk/mkdocs-material from `b1f7f94` to `f4764d1` in /docs ([#3031](https://github.com/aws-powertools/powertools-lambda-python/issues/3031)) +* **deps:** bump gitpython from 3.1.32 to 3.1.35 in /docs ([#3059](https://github.com/aws-powertools/powertools-lambda-python/issues/3059)) +* **deps:** bump squidfunk/mkdocs-material from `f4764d1` to `dd1770c` in /docs ([#3044](https://github.com/aws-powertools/powertools-lambda-python/issues/3044)) +* **deps:** bump actions/checkout from 3.6.0 to 4.0.0 ([#3041](https://github.com/aws-powertools/powertools-lambda-python/issues/3041)) +* **deps:** bump squidfunk/mkdocs-material from `97da15b` to `b1f7f94` in /docs ([#3021](https://github.com/aws-powertools/powertools-lambda-python/issues/3021)) +* **deps:** bump docker/setup-buildx-action from 2.9.1 to 2.10.0 ([#3022](https://github.com/aws-powertools/powertools-lambda-python/issues/3022)) +* **deps:** bump actions/upload-artifact from 3.1.2 to 3.1.3 ([#3053](https://github.com/aws-powertools/powertools-lambda-python/issues/3053)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3052](https://github.com/aws-powertools/powertools-lambda-python/issues/3052)) +* **deps-dev:** bump mkdocs-material from 9.2.6 to 9.2.7 ([#3043](https://github.com/aws-powertools/powertools-lambda-python/issues/3043)) +* **deps-dev:** bump cfn-lint from 0.79.7 to 0.79.8 ([#3033](https://github.com/aws-powertools/powertools-lambda-python/issues/3033)) +* **deps-dev:** bump mkdocs-material from 9.2.5 to 9.2.6 ([#3032](https://github.com/aws-powertools/powertools-lambda-python/issues/3032)) +* **deps-dev:** bump ruff from 0.0.286 to 0.0.287 ([#3035](https://github.com/aws-powertools/powertools-lambda-python/issues/3035)) +* **deps-dev:** bump sentry-sdk from 1.29.2 to 1.30.0 ([#3028](https://github.com/aws-powertools/powertools-lambda-python/issues/3028)) +* **deps-dev:** bump the boto-typing group with 11 updates ([#3027](https://github.com/aws-powertools/powertools-lambda-python/issues/3027)) +* **deps-dev:** bump pytest from 7.4.1 to 7.4.2 ([#3057](https://github.com/aws-powertools/powertools-lambda-python/issues/3057)) +* **deps-dev:** bump hvac from 1.1.1 to 1.2.0 ([#3054](https://github.com/aws-powertools/powertools-lambda-python/issues/3054)) +* **deps-dev:** bump cfn-lint from 0.79.8 to 0.79.9 ([#3046](https://github.com/aws-powertools/powertools-lambda-python/issues/3046)) +* **deps-dev:** bump the boto-typing group with 1 update ([#3013](https://github.com/aws-powertools/powertools-lambda-python/issues/3013)) +* **deps-dev:** bump pytest from 7.4.0 to 7.4.1 ([#3042](https://github.com/aws-powertools/powertools-lambda-python/issues/3042)) +* **deps-dev:** bump ruff from 0.0.285 to 0.0.286 ([#3014](https://github.com/aws-powertools/powertools-lambda-python/issues/3014)) +* **deps-dev:** bump gitpython from 3.1.32 to 3.1.35 ([#3060](https://github.com/aws-powertools/powertools-lambda-python/issues/3060)) +* **deps-dev:** bump aws-cdk from 2.93.0 to 2.94.0 ([#3036](https://github.com/aws-powertools/powertools-lambda-python/issues/3036)) + + + +## [v2.23.1] - 2023-08-25 +## Bug Fixes + +* **ci:** revert aws credentials action ([#3010](https://github.com/aws-powertools/powertools-lambda-python/issues/3010)) +* **ci:** change SAR assume role options ([#3005](https://github.com/aws-powertools/powertools-lambda-python/issues/3005)) +* **event_handler:** make invalid chars a raw str to fix DeprecationWarning ([#2982](https://github.com/aws-powertools/powertools-lambda-python/issues/2982)) +* **metrics:** preserve default_tags when metric-specific tag is set in Datadog provider ([#2997](https://github.com/aws-powertools/powertools-lambda-python/issues/2997)) + +## Maintenance + +* version bump +* **deps:** bump squidfunk/mkdocs-material from `cd3a522` to `97da15b` in /docs ([#2987](https://github.com/aws-powertools/powertools-lambda-python/issues/2987)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#2978](https://github.com/aws-powertools/powertools-lambda-python/issues/2978)) +* **deps:** bump aws-actions/configure-aws-credentials from 2.2.0 to 3.0.0 ([#3000](https://github.com/aws-powertools/powertools-lambda-python/issues/3000)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#2983](https://github.com/aws-powertools/powertools-lambda-python/issues/2983)) +* **deps:** bump slsa-framework/slsa-github-generator from 1.8.0 to 1.9.0 ([#2992](https://github.com/aws-powertools/powertools-lambda-python/issues/2992)) +* **deps:** bump actions/checkout from 3.5.3 to 3.6.0 ([#2999](https://github.com/aws-powertools/powertools-lambda-python/issues/2999)) +* **deps-dev:** bump ruff from 0.0.284 to 0.0.285 ([#2977](https://github.com/aws-powertools/powertools-lambda-python/issues/2977)) +* **deps-dev:** bump aws-cdk from 2.92.0 to 2.93.0 ([#2993](https://github.com/aws-powertools/powertools-lambda-python/issues/2993)) +* **deps-dev:** bump mkdocs-material from 9.1.21 to 9.2.0 ([#2984](https://github.com/aws-powertools/powertools-lambda-python/issues/2984)) +* **deps-dev:** bump mkdocs-material from 9.2.0 to 9.2.3 ([#2988](https://github.com/aws-powertools/powertools-lambda-python/issues/2988)) + + + +## [v2.23.0] - 2023-08-18 +## Bug Fixes + +* **logger:** strip xray_trace_id when explicitly disabled ([#2852](https://github.com/aws-powertools/powertools-lambda-python/issues/2852)) +* **metrics:** proxy service and namespace attrs to provider ([#2910](https://github.com/aws-powertools/powertools-lambda-python/issues/2910)) +* **parser:** API Gateway V2 request context scope field should be optional ([#2961](https://github.com/aws-powertools/powertools-lambda-python/issues/2961)) + +## Code Refactoring + +* **e2e:** support fail fast in get_lambda_response ([#2912](https://github.com/aws-powertools/powertools-lambda-python/issues/2912)) +* **metrics:** move from protocol to ABC; split provider tests ([#2934](https://github.com/aws-powertools/powertools-lambda-python/issues/2934)) + +## Documentation + +* **batch:** new visuals and error handling section ([#2857](https://github.com/aws-powertools/powertools-lambda-python/issues/2857)) +* **batch:** explain record type discrepancy in failure and success handler ([#2868](https://github.com/aws-powertools/powertools-lambda-python/issues/2868)) +* **metrics:** update Datadog integration diagram ([#2954](https://github.com/aws-powertools/powertools-lambda-python/issues/2954)) +* **navigation:** remove nofollow attribute for internal links ([#2867](https://github.com/aws-powertools/powertools-lambda-python/issues/2867)) +* **navigation:** add nofollow attribute ([#2842](https://github.com/aws-powertools/powertools-lambda-python/issues/2842)) +* **roadmap:** update roadmap themes ([#2915](https://github.com/aws-powertools/powertools-lambda-python/issues/2915)) +* **roadmap:** add GovCloud and China region item ([#2960](https://github.com/aws-powertools/powertools-lambda-python/issues/2960)) +* **tutorial:** add support for Python 3.11 ([#2860](https://github.com/aws-powertools/powertools-lambda-python/issues/2860)) + +## Features + +* **event_handler:** allow stripping route prefixes using regexes ([#2521](https://github.com/aws-powertools/powertools-lambda-python/issues/2521)) +* **layers:** add new comercial region Israel(Tel Aviv) ([#2907](https://github.com/aws-powertools/powertools-lambda-python/issues/2907)) +* **metrics:** add Datadog observability provider ([#2906](https://github.com/aws-powertools/powertools-lambda-python/issues/2906)) +* **metrics:** support to bring your own metrics provider ([#2194](https://github.com/aws-powertools/powertools-lambda-python/issues/2194)) + +## Maintenance + +* version bump +* **ci:** enable protected branch auditing ([#2913](https://github.com/aws-powertools/powertools-lambda-python/issues/2913)) +* **ci:** group dependabot updates ([#2896](https://github.com/aws-powertools/powertools-lambda-python/issues/2896)) +* **deps:** bump github.com/aws/aws-sdk-go-v2 from 1.19.0 to 1.19.1 in /layer/scripts/layer-balancer ([#2877](https://github.com/aws-powertools/powertools-lambda-python/issues/2877)) +* **deps:** bump pypa/gh-action-pypi-publish from 1.8.8 to 1.8.9 ([#2943](https://github.com/aws-powertools/powertools-lambda-python/issues/2943)) +* **deps:** bump github.com/aws/aws-sdk-go-v2/service/lambda from 1.38.0 to 1.38.1 in /layer/scripts/layer-balancer ([#2876](https://github.com/aws-powertools/powertools-lambda-python/issues/2876)) +* **deps:** bump actions/dependency-review-action from 3.0.6 to 3.0.7 ([#2941](https://github.com/aws-powertools/powertools-lambda-python/issues/2941)) +* **deps:** bump github.com/aws/aws-sdk-go-v2/config from 1.18.29 to 1.18.30 in /layer/scripts/layer-balancer ([#2875](https://github.com/aws-powertools/powertools-lambda-python/issues/2875)) +* **deps:** bump actions/dependency-review-action from 3.0.7 to 3.0.8 ([#2963](https://github.com/aws-powertools/powertools-lambda-python/issues/2963)) +* **deps:** bump gitpython from 3.1.31 to 3.1.32 in /docs ([#2948](https://github.com/aws-powertools/powertools-lambda-python/issues/2948)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 2 updates ([#2904](https://github.com/aws-powertools/powertools-lambda-python/issues/2904)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#2933](https://github.com/aws-powertools/powertools-lambda-python/issues/2933)) +* **deps:** bump pypa/gh-action-pypi-publish from 1.8.9 to 1.8.10 ([#2946](https://github.com/aws-powertools/powertools-lambda-python/issues/2946)) +* **deps:** bump actions/setup-node from 3.7.0 to 3.8.0 ([#2957](https://github.com/aws-powertools/powertools-lambda-python/issues/2957)) +* **deps:** bump slsa-framework/slsa-github-generator from 1.7.0 to 1.8.0 ([#2927](https://github.com/aws-powertools/powertools-lambda-python/issues/2927)) +* **deps:** bump github.com/aws/aws-sdk-go-v2/config from 1.18.28 to 1.18.29 in /layer/scripts/layer-balancer ([#2844](https://github.com/aws-powertools/powertools-lambda-python/issues/2844)) +* **deps:** bump github.com/aws/aws-sdk-go-v2/service/lambda from 1.37.1 to 1.38.0 in /layer/scripts/layer-balancer ([#2843](https://github.com/aws-powertools/powertools-lambda-python/issues/2843)) +* **deps:** bump pydantic from 1.10.11 to 1.10.12 ([#2846](https://github.com/aws-powertools/powertools-lambda-python/issues/2846)) +* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates ([#2971](https://github.com/aws-powertools/powertools-lambda-python/issues/2971)) +* **deps:** bump actions/setup-node from 3.8.0 to 3.8.1 ([#2970](https://github.com/aws-powertools/powertools-lambda-python/issues/2970)) +* **deps:** bump github.com/aws/aws-sdk-go-v2/config from 1.18.30 to 1.18.31 in /layer/scripts/layer-balancer ([#2889](https://github.com/aws-powertools/powertools-lambda-python/issues/2889)) +* **deps:** bump github.com/aws/aws-sdk-go-v2/service/lambda from 1.38.1 to 1.39.0 in /layer/scripts/layer-balancer ([#2890](https://github.com/aws-powertools/powertools-lambda-python/issues/2890)) +* **deps:** bump squidfunk/mkdocs-material from `33e28bd` to `cd3a522` in /docs ([#2859](https://github.com/aws-powertools/powertools-lambda-python/issues/2859)) +* **deps-dev:** bump ruff from 0.0.283 to 0.0.284 ([#2940](https://github.com/aws-powertools/powertools-lambda-python/issues/2940)) +* **deps-dev:** bump cfn-lint from 0.79.5 to 0.79.6 ([#2899](https://github.com/aws-powertools/powertools-lambda-python/issues/2899)) +* **deps-dev:** bump the boto-typing group with 11 updates ([#2901](https://github.com/aws-powertools/powertools-lambda-python/issues/2901)) +* **deps-dev:** bump ruff from 0.0.281 to 0.0.282 ([#2905](https://github.com/aws-powertools/powertools-lambda-python/issues/2905)) +* **deps-dev:** bump aws-cdk from 2.88.0 to 2.89.0 ([#2887](https://github.com/aws-powertools/powertools-lambda-python/issues/2887)) +* **deps-dev:** bump aws-cdk from 2.89.0 to 2.90.0 ([#2932](https://github.com/aws-powertools/powertools-lambda-python/issues/2932)) +* **deps-dev:** bump mkdocs-material from 9.1.19 to 9.1.21 ([#2894](https://github.com/aws-powertools/powertools-lambda-python/issues/2894)) +* **deps-dev:** bump the boto-typing group with 3 updates ([#2967](https://github.com/aws-powertools/powertools-lambda-python/issues/2967)) +* **deps-dev:** bump radon from 5.1.0 to 6.0.1 ([#2964](https://github.com/aws-powertools/powertools-lambda-python/issues/2964)) +* **deps-dev:** bump the boto-typing group with 4 updates ([#2928](https://github.com/aws-powertools/powertools-lambda-python/issues/2928)) +* **deps-dev:** bump mypy-boto3-logs from 1.28.1 to 1.28.15 ([#2880](https://github.com/aws-powertools/powertools-lambda-python/issues/2880)) +* **deps-dev:** bump mypy-boto3-appconfigdata from 1.28.0 to 1.28.15 ([#2879](https://github.com/aws-powertools/powertools-lambda-python/issues/2879)) +* **deps-dev:** bump mypy-boto3-lambda from 1.28.11 to 1.28.15 ([#2878](https://github.com/aws-powertools/powertools-lambda-python/issues/2878)) +* **deps-dev:** bump mypy-boto3-xray from 1.28.0 to 1.28.15 ([#2881](https://github.com/aws-powertools/powertools-lambda-python/issues/2881)) +* **deps-dev:** bump ruff from 0.0.282 to 0.0.283 ([#2937](https://github.com/aws-powertools/powertools-lambda-python/issues/2937)) +* **deps-dev:** bump mypy-boto3-dynamodb from 1.28.0 to 1.28.11 ([#2847](https://github.com/aws-powertools/powertools-lambda-python/issues/2847)) +* **deps-dev:** bump sentry-sdk from 1.28.1 to 1.29.0 ([#2900](https://github.com/aws-powertools/powertools-lambda-python/issues/2900)) +* **deps-dev:** bump cfn-lint from 0.79.4 to 0.79.5 ([#2870](https://github.com/aws-powertools/powertools-lambda-python/issues/2870)) +* **deps-dev:** bump the boto-typing group with 1 update ([#2944](https://github.com/aws-powertools/powertools-lambda-python/issues/2944)) +* **deps-dev:** bump mypy-boto3-cloudformation from 1.28.10 to 1.28.12 ([#2864](https://github.com/aws-powertools/powertools-lambda-python/issues/2864)) +* **deps-dev:** bump mypy-boto3-cloudwatch from 1.28.0 to 1.28.12 ([#2865](https://github.com/aws-powertools/powertools-lambda-python/issues/2865)) +* **deps-dev:** bump cfn-lint from 0.79.3 to 0.79.4 ([#2862](https://github.com/aws-powertools/powertools-lambda-python/issues/2862)) +* **deps-dev:** bump mypy-boto3-appconfig from 1.28.0 to 1.28.12 ([#2861](https://github.com/aws-powertools/powertools-lambda-python/issues/2861)) +* **deps-dev:** bump mypy-boto3-ssm from 1.28.0 to 1.28.12 ([#2863](https://github.com/aws-powertools/powertools-lambda-python/issues/2863)) +* **deps-dev:** bump aws-cdk from 2.90.0 to 2.91.0 ([#2947](https://github.com/aws-powertools/powertools-lambda-python/issues/2947)) +* **deps-dev:** bump xenon from 0.9.0 to 0.9.1 ([#2955](https://github.com/aws-powertools/powertools-lambda-python/issues/2955)) +* **deps-dev:** bump cfn-lint from 0.78.2 to 0.79.3 ([#2854](https://github.com/aws-powertools/powertools-lambda-python/issues/2854)) +* **deps-dev:** bump mypy-boto3-lambda from 1.28.0 to 1.28.11 ([#2845](https://github.com/aws-powertools/powertools-lambda-python/issues/2845)) +* **deps-dev:** bump cfn-lint from 0.79.6 to 0.79.7 ([#2956](https://github.com/aws-powertools/powertools-lambda-python/issues/2956)) +* **deps-dev:** bump aws-cdk from 2.91.0 to 2.92.0 ([#2965](https://github.com/aws-powertools/powertools-lambda-python/issues/2965)) +* **deps-dev:** bump ruff from 0.0.280 to 0.0.281 ([#2891](https://github.com/aws-powertools/powertools-lambda-python/issues/2891)) +* **docs:** include the environment variables section in the utilities documentation ([#2925](https://github.com/aws-powertools/powertools-lambda-python/issues/2925)) +* **docs:** disable line length rule using older syntax ([#2920](https://github.com/aws-powertools/powertools-lambda-python/issues/2920)) +* **maintenance:** enables publishing docs and changelog, running e2e tests only in the main repository ([#2924](https://github.com/aws-powertools/powertools-lambda-python/issues/2924)) + + + +## [v2.22.0] - 2023-07-25 +## Bug Fixes + +* **parameters:** distinct cache key for single vs path with same name ([#2839](https://github.com/aws-powertools/powertools-lambda-python/issues/2839)) + +## Documentation + +* **community:** new batch processing article ([#2828](https://github.com/aws-powertools/powertools-lambda-python/issues/2828)) +* **parameters:** improve readability on error handling get_parameter… ([#2833](https://github.com/aws-powertools/powertools-lambda-python/issues/2833)) + +## Features + +* **general:** add support for Python 3.11 ([#2820](https://github.com/aws-powertools/powertools-lambda-python/issues/2820)) + +## Maintenance + +* version bump +* **ci:** add baking time for layer build ([#2834](https://github.com/aws-powertools/powertools-lambda-python/issues/2834)) +* **ci:** build changelog on a schedule only ([#2832](https://github.com/aws-powertools/powertools-lambda-python/issues/2832)) +* **deps:** bump actions/setup-python from 4.6.1 to 4.7.0 ([#2821](https://github.com/aws-powertools/powertools-lambda-python/issues/2821)) +* **deps-dev:** bump ruff from 0.0.278 to 0.0.279 ([#2822](https://github.com/aws-powertools/powertools-lambda-python/issues/2822)) +* **deps-dev:** bump cfn-lint from 0.78.1 to 0.78.2 ([#2823](https://github.com/aws-powertools/powertools-lambda-python/issues/2823)) +* **deps-dev:** bump ruff from 0.0.279 to 0.0.280 ([#2836](https://github.com/aws-powertools/powertools-lambda-python/issues/2836)) +* **deps-dev:** bump mypy-boto3-cloudformation from 1.28.0 to 1.28.10 ([#2837](https://github.com/aws-powertools/powertools-lambda-python/issues/2837)) + + + +## [v2.21.0] - 2023-07-21 +## Bug Fixes + +* **docs:** remove redundant code ([#2796](https://github.com/aws-powertools/powertools-lambda-python/issues/2796)) + +## Documentation + +* **customer-reference:** add Jit Security as a customer reference ([#2801](https://github.com/aws-powertools/powertools-lambda-python/issues/2801)) + +## Features + +* **parser:** add support for Pydantic v2 ([#2733](https://github.com/aws-powertools/powertools-lambda-python/issues/2733)) + +## Maintenance + +* version bump +* **deps:** bump squidfunk/mkdocs-material from `a28ed81` to `33e28bd` in /docs ([#2797](https://github.com/aws-powertools/powertools-lambda-python/issues/2797)) +* **deps-dev:** bump mypy-boto3-s3 from 1.28.3.post2 to 1.28.8 ([#2808](https://github.com/aws-powertools/powertools-lambda-python/issues/2808)) +* **deps-dev:** bump types-python-dateutil from 2.8.19.13 to 2.8.19.14 ([#2807](https://github.com/aws-powertools/powertools-lambda-python/issues/2807)) +* **deps-dev:** bump mypy-boto3-secretsmanager from 1.28.3.post1 to 1.28.3.post2 ([#2794](https://github.com/aws-powertools/powertools-lambda-python/issues/2794)) +* **deps-dev:** bump types-requests from 2.31.0.1 to 2.31.0.2 ([#2806](https://github.com/aws-powertools/powertools-lambda-python/issues/2806)) +* **deps-dev:** bump mypy-boto3-s3 from 1.28.3.post1 to 1.28.3.post2 ([#2793](https://github.com/aws-powertools/powertools-lambda-python/issues/2793)) +* **deps-dev:** bump aws-cdk from 2.87.0 to 2.88.0 ([#2812](https://github.com/aws-powertools/powertools-lambda-python/issues/2812)) +* **deps-dev:** bump mypy-boto3-secretsmanager from 1.28.3 to 1.28.3.post1 ([#2785](https://github.com/aws-powertools/powertools-lambda-python/issues/2785)) +* **deps-dev:** bump mypy-boto3-s3 from 1.28.3 to 1.28.3.post1 ([#2786](https://github.com/aws-powertools/powertools-lambda-python/issues/2786)) +* **deps-dev:** bump mkdocs-material from 9.1.18 to 9.1.19 ([#2798](https://github.com/aws-powertools/powertools-lambda-python/issues/2798)) +* **security:** improve debugging for provenance script ([#2784](https://github.com/aws-powertools/powertools-lambda-python/issues/2784)) + + ## [v2.20.0] - 2023-07-14 +## Bug Fixes + +* **docs:** ensure alias is applied to versioned releases ([#2644](https://github.com/aws-powertools/powertools-lambda-python/issues/2644)) +* **docs:** ensure version alias is in an array to prevent "you're not viewing the latest version" incorrect message ([#2629](https://github.com/aws-powertools/powertools-lambda-python/issues/2629)) +* **logger:** ensure logs stream to stdout by default, not stderr ([#2736](https://github.com/aws-powertools/powertools-lambda-python/issues/2736)) + +## Code Refactoring + +* **parser:** convert functional tests to unit tests ([#2656](https://github.com/aws-powertools/powertools-lambda-python/issues/2656)) + +## Documentation + +* **batch:** fix custom batch processor example ([#2714](https://github.com/aws-powertools/powertools-lambda-python/issues/2714)) +* **contributing:** add code integration journey graph ([#2685](https://github.com/aws-powertools/powertools-lambda-python/issues/2685)) +* **maintainers:** add cicd pipeline diagram ([#2692](https://github.com/aws-powertools/powertools-lambda-python/issues/2692)) +* **process:** explain our integration automated checks; revamp navigation ([#2764](https://github.com/aws-powertools/powertools-lambda-python/issues/2764)) + +## Features + +* **metrics:** support to set default dimension in EphemeralMetrics ([#2748](https://github.com/aws-powertools/powertools-lambda-python/issues/2748)) + ## Maintenance * version bump +* **ci:** enforce pip --require-hashes to maybe satistify scorecard ([#2679](https://github.com/aws-powertools/powertools-lambda-python/issues/2679)) +* **ci:** prevent merging PRs that do not meet minimum requirements ([#2639](https://github.com/aws-powertools/powertools-lambda-python/issues/2639)) +* **ci:** enforce top-level permission to minimum fail-safe permission as per openssf ([#2638](https://github.com/aws-powertools/powertools-lambda-python/issues/2638)) +* **ci:** propagate checkout permission to nested workflows ([#2642](https://github.com/aws-powertools/powertools-lambda-python/issues/2642)) +* **ci:** improves dependabot based on ossf scorecard recommendations ([#2647](https://github.com/aws-powertools/powertools-lambda-python/issues/2647)) +* **ci:** use deps sha for docs and gitpod images based on ossf findings ([#2662](https://github.com/aws-powertools/powertools-lambda-python/issues/2662)) +* **ci:** use sast on every commit on any supported language ([#2646](https://github.com/aws-powertools/powertools-lambda-python/issues/2646)) +* **ci:** add gitleaks in pre-commit hooks as an extra safety measure ([#2677](https://github.com/aws-powertools/powertools-lambda-python/issues/2677)) +* **ci:** address ossf scorecard findings on npm, pip, and top-level permission leftover ([#2694](https://github.com/aws-powertools/powertools-lambda-python/issues/2694)) +* **ci:** prevent sast codeql to run in forks ([#2711](https://github.com/aws-powertools/powertools-lambda-python/issues/2711)) +* **ci:** introduce provenance and attestation in release ([#2746](https://github.com/aws-powertools/powertools-lambda-python/issues/2746)) +* **deps:** bump pypa/gh-action-pypi-publish from 1.8.7 to 1.8.8 ([#2754](https://github.com/aws-powertools/powertools-lambda-python/issues/2754)) +* **deps:** bump github.com/aws/aws-sdk-go-v2/service/lambda from 1.37.0 to 1.37.1 in /layer/scripts/layer-balancer ([#2769](https://github.com/aws-powertools/powertools-lambda-python/issues/2769)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 2.1.3 to 2.1.4 ([#2738](https://github.com/aws-powertools/powertools-lambda-python/issues/2738)) +* **deps:** bump github.com/aws/aws-sdk-go-v2/config from 1.18.27 to 1.18.28 in /layer/scripts/layer-balancer ([#2770](https://github.com/aws-powertools/powertools-lambda-python/issues/2770)) +* **deps:** bump actions/setup-python from 4.6.1 to 4.7.0 ([#2768](https://github.com/aws-powertools/powertools-lambda-python/issues/2768)) +* **deps:** bump github.com/aws/aws-sdk-go-v2 from 1.16.16 to 1.18.1 in /layer/scripts/layer-balancer ([#2654](https://github.com/aws-powertools/powertools-lambda-python/issues/2654)) +* **deps:** bump golang.org/x/sync from 0.1.0 to 0.3.0 in /layer/scripts/layer-balancer ([#2649](https://github.com/aws-powertools/powertools-lambda-python/issues/2649)) +* **deps:** bump github.com/aws/aws-sdk-go-v2/service/lambda from 1.24.6 to 1.37.0 in /layer/scripts/layer-balancer ([#2653](https://github.com/aws-powertools/powertools-lambda-python/issues/2653)) +* **deps:** bump docker/setup-buildx-action from 2.8.0 to 2.9.0 ([#2718](https://github.com/aws-powertools/powertools-lambda-python/issues/2718)) +* **deps:** bump github.com/aws/aws-sdk-go-v2/config from 1.17.8 to 1.18.27 in /layer/scripts/layer-balancer ([#2651](https://github.com/aws-powertools/powertools-lambda-python/issues/2651)) +* **deps:** bump github.com/aws/aws-sdk-go-v2 from 1.18.1 to 1.19.0 in /layer/scripts/layer-balancer ([#2771](https://github.com/aws-powertools/powertools-lambda-python/issues/2771)) +* **deps:** migrate from retry to retry2 to address CVE-2022-42969 ([#2665](https://github.com/aws-powertools/powertools-lambda-python/issues/2665)) +* **deps:** bump pydantic from 1.10.9 to 1.10.10 ([#2624](https://github.com/aws-powertools/powertools-lambda-python/issues/2624)) +* **deps:** bump squidfunk/mkdocs-material from `3837c0f` to `a28ed81` in /docs ([#2669](https://github.com/aws-powertools/powertools-lambda-python/issues/2669)) +* **deps:** bump pydantic from 1.10.10 to 1.10.11 ([#2671](https://github.com/aws-powertools/powertools-lambda-python/issues/2671)) +* **deps:** bump docker/setup-buildx-action from 2.9.0 to 2.9.1 ([#2755](https://github.com/aws-powertools/powertools-lambda-python/issues/2755)) +* **deps:** bump actions/dependency-review-action from 2.5.1 to 3.0.6 ([#2650](https://github.com/aws-powertools/powertools-lambda-python/issues/2650)) +* **deps:** bump actions/setup-node from 3.6.0 to 3.7.0 ([#2689](https://github.com/aws-powertools/powertools-lambda-python/issues/2689)) +* **deps-dev:** bump mypy-boto3-lambda from 1.27.0 to 1.28.0 ([#2698](https://github.com/aws-powertools/powertools-lambda-python/issues/2698)) +* **deps-dev:** bump sentry-sdk from 1.27.0 to 1.27.1 ([#2701](https://github.com/aws-powertools/powertools-lambda-python/issues/2701)) +* **deps-dev:** bump mypy-boto3-cloudformation from 1.27.0 to 1.28.0 ([#2700](https://github.com/aws-powertools/powertools-lambda-python/issues/2700)) +* **deps-dev:** bump mypy-boto3-appconfigdata from 1.27.0 to 1.28.0 ([#2699](https://github.com/aws-powertools/powertools-lambda-python/issues/2699)) +* **deps-dev:** bump ruff from 0.0.276 to 0.0.277 ([#2682](https://github.com/aws-powertools/powertools-lambda-python/issues/2682)) +* **deps-dev:** bump pytest-asyncio from 0.21.0 to 0.21.1 ([#2756](https://github.com/aws-powertools/powertools-lambda-python/issues/2756)) +* **deps-dev:** bump cfn-lint from 0.77.10 to 0.78.1 ([#2757](https://github.com/aws-powertools/powertools-lambda-python/issues/2757)) +* **deps-dev:** bump aws-cdk from 2.86.0 to 2.87.0 ([#2696](https://github.com/aws-powertools/powertools-lambda-python/issues/2696)) +* **deps-dev:** bump typed-ast from 1.5.4 to 1.5.5 ([#2670](https://github.com/aws-powertools/powertools-lambda-python/issues/2670)) +* **deps-dev:** bump mypy-boto3-cloudwatch from 1.27.0 to 1.28.0 ([#2697](https://github.com/aws-powertools/powertools-lambda-python/issues/2697)) +* **deps-dev:** bump ruff from 0.0.275 to 0.0.276 ([#2655](https://github.com/aws-powertools/powertools-lambda-python/issues/2655)) +* **deps-dev:** bump sentry-sdk from 1.26.0 to 1.27.0 ([#2652](https://github.com/aws-powertools/powertools-lambda-python/issues/2652)) +* **deps-dev:** bump ruff from 0.0.277 to 0.0.278 ([#2758](https://github.com/aws-powertools/powertools-lambda-python/issues/2758)) +* **deps-dev:** bump mypy-boto3-s3 from 1.28.0 to 1.28.3 ([#2774](https://github.com/aws-powertools/powertools-lambda-python/issues/2774)) +* **deps-dev:** bump mypy-boto3-dynamodb from 1.26.158 to 1.26.164 ([#2622](https://github.com/aws-powertools/powertools-lambda-python/issues/2622)) * **deps-dev:** bump mypy-boto3-secretsmanager from 1.28.0 to 1.28.3 ([#2773](https://github.com/aws-powertools/powertools-lambda-python/issues/2773)) +* **deps-dev:** bump sentry-sdk from 1.27.1 to 1.28.0 ([#2741](https://github.com/aws-powertools/powertools-lambda-python/issues/2741)) +* **deps-dev:** bump mypy-boto3-s3 from 1.27.0 to 1.28.0 ([#2721](https://github.com/aws-powertools/powertools-lambda-python/issues/2721)) +* **deps-dev:** bump mypy-boto3-appconfig from 1.27.0 to 1.28.0 ([#2722](https://github.com/aws-powertools/powertools-lambda-python/issues/2722)) +* **deps-dev:** bump mypy-boto3-logs from 1.27.0 to 1.28.1 ([#2723](https://github.com/aws-powertools/powertools-lambda-python/issues/2723)) +* **deps-dev:** bump mypy-boto3-ssm from 1.27.0 to 1.28.0 ([#2724](https://github.com/aws-powertools/powertools-lambda-python/issues/2724)) +* **deps-dev:** bump mypy-boto3-xray from 1.27.0 to 1.28.0 ([#2720](https://github.com/aws-powertools/powertools-lambda-python/issues/2720)) +* **deps-dev:** bump sentry-sdk from 1.28.0 to 1.28.1 ([#2772](https://github.com/aws-powertools/powertools-lambda-python/issues/2772)) +* **deps-dev:** bump mypy-boto3-dynamodb from 1.27.0 to 1.28.0 ([#2740](https://github.com/aws-powertools/powertools-lambda-python/issues/2740)) +* **deps-dev:** bump mypy-boto3-appconfigdata from 1.26.70 to 1.27.0 ([#2636](https://github.com/aws-powertools/powertools-lambda-python/issues/2636)) +* **deps-dev:** bump mypy-boto3-secretsmanager from 1.27.0 to 1.28.0 ([#2739](https://github.com/aws-powertools/powertools-lambda-python/issues/2739)) +* **governance:** update active maintainers list ([#2715](https://github.com/aws-powertools/powertools-lambda-python/issues/2715)) +* **streaming:** replace deprecated Version classes from distutils ([#2752](https://github.com/aws-powertools/powertools-lambda-python/issues/2752)) +* **user-agent:** support patching botocore session ([#2614](https://github.com/aws-powertools/powertools-lambda-python/issues/2614)) @@ -3470,7 +7666,80 @@ * Merge pull request [#5](https://github.com/aws-powertools/powertools-lambda-python/issues/5) from jfuss/feat/python38 -[Unreleased]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.20.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 +[v3.23.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.22.1...v3.23.0 +[v3.22.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.22.0...v3.22.1 +[v3.22.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.21.0...v3.22.0 +[v3.21.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.20.0...v3.21.0 +[v3.20.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.19.0...v3.20.0 +[v3.19.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.17.1...v3.19.0 +[v3.17.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.18.0...v3.17.1 +[v3.18.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.17.0...v3.18.0 +[v3.17.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.16.0...v3.17.0 +[v3.16.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.15.1...v3.16.0 +[v3.15.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.15.0...v3.15.1 +[v3.15.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.14.0...v3.15.0 +[v3.14.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.13.0...v3.14.0 +[v3.13.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.12.0...v3.13.0 +[v3.12.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.11.0...v3.12.0 +[v3.11.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.10.0...v3.11.0 +[v3.10.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.9.0...v3.10.0 +[v3.9.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.8.0...v3.9.0 +[v3.8.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.7.0...v3.8.0 +[v3.7.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.6.0...v3.7.0 +[v3.6.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.5.0...v3.6.0 +[v3.5.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.4.1...v3.5.0 +[v3.4.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.4.0...v3.4.1 +[v3.4.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.3.0...v3.4.0 +[v3.3.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.2.0...v3.3.0 +[v3.2.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.1.0...v3.2.0 +[v3.1.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.0.0...v3.1.0 +[v3.0.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.43.1...v3.0.0 +[v2.43.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.43.0...v2.43.1 +[v2.43.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.42.0...v2.43.0 +[v2.42.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.41.0...v2.42.0 +[v2.41.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.40.1...v2.41.0 +[v2.40.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.40.0...v2.40.1 +[v2.40.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.39.1...v2.40.0 +[v2.39.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.39.0...v2.39.1 +[v2.39.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.38.1...v2.39.0 +[v2.38.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.38.0...v2.38.1 +[v2.38.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.37.0...v2.38.0 +[v2.37.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.36.0...v2.37.0 +[v2.36.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.35.1...v2.36.0 +[v2.35.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.35.0...v2.35.1 +[v2.35.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.34.2...v2.35.0 +[v2.34.2]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.34.1...v2.34.2 +[v2.34.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.34.0...v2.34.1 +[v2.34.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.33.1...v2.34.0 +[v2.33.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.33.0...v2.33.1 +[v2.33.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.32.0...v2.33.0 +[v2.32.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.31.0...v2.32.0 +[v2.31.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.30.2...v2.31.0 +[v2.30.2]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.30.1...v2.30.2 +[v2.30.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.30.0...v2.30.1 +[v2.30.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.29.1...v2.30.0 +[v2.29.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.29.0...v2.29.1 +[v2.29.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.28.1...v2.29.0 +[v2.28.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.28.0...v2.28.1 +[v2.28.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.27.1...v2.28.0 +[v2.27.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.27.0...v2.27.1 +[v2.27.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.26.1...v2.27.0 +[v2.26.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.26.0...v2.26.1 +[v2.26.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.25.1...v2.26.0 +[v2.25.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.25.0...v2.25.1 +[v2.25.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.24.0...v2.25.0 +[v2.24.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.23.1...v2.24.0 +[v2.23.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.23.0...v2.23.1 +[v2.23.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.22.0...v2.23.0 +[v2.22.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.21.0...v2.22.0 +[v2.21.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.20.0...v2.21.0 [v2.20.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.19.0...v2.20.0 [v2.19.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.18.0...v2.19.0 [v2.18.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.17.0...v2.18.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc37371cb88..e42ce8ec09a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,12 +16,17 @@ - [API reference documentation](#api-reference-documentation) - [Licensing](#licensing) + # Contributing Guidelines + Thank you for your interest in contributing to our project. Whether it's a [bug report](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=bug%2Ctriage&projects=&template=bug_report.yml&title=Bug%3A+TITLE), [new feature](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), [correction](https://github.com/aws-powertools/powertools-lambda-python/issues/new/choose), or [additional documentation](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=documentation%2Ctriage&projects=&template=documentation_improvements.yml&title=Docs%3A+TITLE), we greatly value feedback and contributions from our community. +We encourage contributions from the community and we will work with contributors to merge their pull requests. +Rarely, we may close pull requests that do not meet our guidelines specified in CONTRIBUTING.md, or will require unreasonable effort to meet our quality bar. + Please read through this document before submitting any issues or pull requests to ensure we have all the necessary information to effectively respond to your bug report or contribution. @@ -67,7 +72,7 @@ timeline Pre-Pull Request
(make pr) : Code linting : Docs linting : Static typing analysis - : Tests (unit|functional|perf) + : Tests (unit|functional|perf|dependencies) : Security baseline : Complexity baseline : +pre-commit checks @@ -93,13 +98,9 @@ timeline ### Dev setup -[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/from-referrer/) - Firstly, [fork the repository](https://github.com/aws-powertools/powertools-lambda-python/fork). -To setup your development environment, we recommend using our pre-configured Cloud environment: . Replace YOUR_USERNAME with your GitHub username or organization so the Cloud environment can target your fork accordingly. - -Alternatively, you can use `make dev` within your local virtual environment. +You can use `make dev` within your local virtual environment to setup your development environment. To send us a pull request, please follow these steps: diff --git a/Makefile b/Makefile index 5bfbf031949..5c0125090a2 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -.PHONY: target dev format lint test coverage-html pr build build-docs build-docs-api build-docs-website -.PHONY: docs-local docs-api-local security-baseline complexity-baseline release-prod release-test release +.PHONY: target dev format lint test coverage-html pr build build-docs build-docs-website +.PHONY: docs-local security-baseline complexity-baseline release-prod release-test release target: @$(MAKE) pr @@ -7,20 +7,23 @@ target: dev: pip install --upgrade pip pre-commit poetry @$(MAKE) dev-version-plugin - poetry install --extras "all" + poetry install --extras "all redis datamasking valkey" pre-commit install -dev-gitpod: - pip install --upgrade pip poetry +dev-quality-code: + pip install --upgrade pip pre-commit poetry @$(MAKE) dev-version-plugin - poetry install --extras "all" + poetry install --extras "all redis datamasking valkey" pre-commit install +format-check: + poetry run ruff format aws_lambda_powertools tests examples --check + format: - poetry run black aws_lambda_powertools tests examples + poetry run ruff format aws_lambda_powertools tests examples lint: format - poetry run ruff aws_lambda_powertools tests examples + poetry run ruff check aws_lambda_powertools tests examples lint-docs: docker run -v ${PWD}:/markdown 06kellyjac/markdownlint-cli "docs" @@ -32,11 +35,17 @@ test: poetry run pytest -m "not perf" --ignore tests/e2e --cov=aws_lambda_powertools --cov-report=xml poetry run pytest --cache-clear tests/performance +test-dependencies: + poetry run nox --error-on-external-run --reuse-venv=yes --non-interactive + +test-pydanticv2: + poetry run pytest -m "not perf" --ignore tests/e2e + unit-test: poetry run pytest tests/unit e2e-test: - python parallel_run_e2e.py + poetry run pytest tests/e2e coverage-html: poetry run pytest -m "not perf" --ignore tests/e2e --cov=aws_lambda_powertools --cov-report=html @@ -49,20 +58,6 @@ pr: lint lint-docs mypy pre-commit test security-baseline complexity-baseline build: pr poetry build -release-docs: - @echo "Rebuilding docs" - rm -rf site api - @echo "Updating website docs" - poetry run mike deploy --push --update-aliases ${VERSION} ${ALIAS} - @echo "Building API docs" - @$(MAKE) build-docs-api VERSION=${VERSION} - -build-docs-api: - poetry run pdoc --html --output-dir ./api/ ./aws_lambda_powertools --force - mv -f ./api/aws_lambda_powertools/* ./api/ - rm -rf ./api/aws_lambda_powertools - mkdir ${VERSION} && cp -R api ${VERSION} - docs-local: poetry run mkdocs serve @@ -70,9 +65,6 @@ docs-local-docker: docker build -t squidfunk/mkdocs-material ./docs/ docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material -docs-api-local: - poetry run pdoc --http : aws_lambda_powertools - security-baseline: poetry run bandit --baseline bandit.baseline -r aws_lambda_powertools @@ -80,7 +72,7 @@ complexity-baseline: $(info Maintenability index) poetry run radon mi aws_lambda_powertools $(info Cyclomatic complexity index) - poetry run xenon --max-absolute C --max-modules A --max-average A aws_lambda_powertools + poetry run xenon --max-absolute C --max-modules A --max-average A aws_lambda_powertools --exclude aws_lambda_powertools/shared/json_encoder.py,aws_lambda_powertools/utilities/validation/base.py,aws_lambda_powertools/event_handler/api_gateway.py # # Use `poetry version /` for version bump @@ -108,6 +100,8 @@ changelog: mypy: poetry run mypy --pretty aws_lambda_powertools examples +ty: + poetry run ty check . dev-version-plugin: - poetry self add git+https://github.com/monim67/poetry-bumpversion@315fe3324a699fa12ec20e202eb7375d4327d1c4 + poetry self add git+https://github.com/monim67/poetry-bumpversion@348de6f247222e2953d649932426e63492e0a6bf diff --git a/README.md b/README.md index 9ace4a52a2a..34d3cac965c 100644 --- a/README.md +++ b/README.md @@ -3,35 +3,41 @@ [![Build](https://github.com/aws-powertools/powertools-lambda-python/actions/workflows/quality_check.yml/badge.svg)](https://github.com/aws-powertools/powertools-lambda-python/actions/workflows/python_build.yml) [![codecov.io](https://codecov.io/github/aws-powertools/powertools-lambda-python/branch/develop/graphs/badge.svg)](https://app.codecov.io/gh/aws-powertools/powertools-lambda-python) -![PythonSupport](https://img.shields.io/static/v1?label=python&message=%203.7|%203.8|%203.9|%203.10&color=blue?style=flat-square&logo=python) ![PyPI version](https://badge.fury.io/py/aws-lambda-powertools.svg) ![PyPi monthly downloads](https://img.shields.io/pypi/dm/aws-lambda-powertools) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/aws-powertools/powertools-lambda-python/badge)](https://api.securityscorecards.dev/projects/github.com/aws-powertools/powertools-lambda-python) [![Join our Discord](https://dcbadge.vercel.app/api/server/B8zZKbbyET)](https://discord.gg/B8zZKbbyET) +![PythonSupport](https://img.shields.io/static/v1?label=python&message=%203.10|%203.11|%203.12|%203.13|%203.14&color=blue?style=flat-square&logo=python) ![PyPI version](https://badge.fury.io/py/aws-lambda-powertools.svg) ![PyPi monthly downloads](https://img.shields.io/pypi/dm/aws-lambda-powertools) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/aws-powertools/powertools-lambda-python/badge)](https://scorecard.dev/viewer/?uri=github.com/aws-powertools/powertools-lambda-python) +[![Discord](https://img.shields.io/badge/Discord-Join_Community-7289da.svg)](https://discord.gg/B8zZKbbyET) Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless [best practices and increase developer velocity](https://docs.powertools.aws.dev/lambda/python/latest/#features). -> Also available in [Java](https://github.com/aws-powertools/powertools-lambda-java), [Typescript](https://github.com/aws-powertools/powertools-lambda-typescript), and [.NET](https://github.com/aws-powertools/powertools-lambda-dotnet). +Also available in [Java](https://github.com/aws-powertools/powertools-lambda-java), [TypeScript](https://github.com/aws-powertools/powertools-lambda-typescript), and [.NET](https://github.com/aws-powertools/powertools-lambda-dotnet). -**[📜Documentation](https://docs.powertools.aws.dev/lambda/python/)** | **[🐍PyPi](https://pypi.org/project/aws-lambda-powertools/)** | **[Roadmap](https://docs.powertools.aws.dev/lambda/python/latest/roadmap/)** | **[Detailed blog post](https://aws.amazon.com/blogs/opensource/simplifying-serverless-best-practices-with-lambda-powertools/)** - -> **An AWS Developer Acceleration (DevAx) initiative by Specialist Solution Architects | ** +[� Doccumentation](https://docs.powertools.aws.dev/lambda/python/) | [🐍 PyPi](https://pypi.org/project/aws-lambda-powertools/) | [🗺️ Roadmap](https://docs.powertools.aws.dev/lambda/python/latest/roadmap/) | [📰 Blog](https://aws.amazon.com/blogs/opensource/simplifying-serverless-best-practices-with-lambda-powertools/) ![hero-image](https://user-images.githubusercontent.com/3340292/198254617-d0fdb672-86a6-4988-8a40-adf437135e0a.png) ## Features +Core utilities such as Tracing, Logging, Metrics, and Event Handler are available across all Powertools for AWS Lambda languages. Additional utilities are subjective to each language ecosystem and customer demand. + * **[Tracing](https://docs.powertools.aws.dev/lambda/python/latest/core/tracer/)** - Decorators and utilities to trace Lambda function handlers, and both synchronous and asynchronous functions -* **[Logging](https://docs.powertools.aws.dev/lambda/python/latest/core/logger/)** - Structured logging made easier, and decorator to enrich structured logging with key Lambda context details +* **[Logging](https://docs.powertools.aws.dev/lambda/python/latest/core/logger/)** - Structured logging made easier, and target to enrich structured logging with key Lambda context details * **[Metrics](https://docs.powertools.aws.dev/lambda/python/latest/core/metrics/)** - Custom Metrics created asynchronously via CloudWatch Embedded Metric Format (EMF) -* **[Event handler: AppSync](https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/appsync/)** - AWS AppSync event handler for Lambda Direct Resolver and Amplify GraphQL Transformer function -* **[Event handler: API Gateway and ALB](https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/)** - Amazon API Gateway REST/HTTP API and ALB event handler for Lambda functions invoked using Proxy integration -* **[Bring your own middleware](https://docs.powertools.aws.dev/lambda/python/latest/utilities/middleware_factory/)** - Decorator factory to create your own middleware to run logic before, and after each Lambda invocation -* **[Parameters utility](https://docs.powertools.aws.dev/lambda/python/latest/utilities/parameters/)** - Retrieve and cache parameter values from Parameter Store, Secrets Manager, or DynamoDB -* **[Batch processing](https://docs.powertools.aws.dev/lambda/python/latest/utilities/batch/)** - Handle partial failures for AWS SQS batch processing +* **[Event handler: AppSync](https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/appsync/)** - AppSync event handler for Lambda Direct Resolver and Amplify GraphQL Transformer function +* **[Event handler: AppSync Events](https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/appsync_events/)** - AppSync Events handler for real-time WebSocket APIs with pub/sub pattern +* **[Event handler: API Gateway, ALB, Lambda Function URL, VPC Lattice](https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/)** - REST/HTTP API event handler for Lambda functions invoked via Amazon API Gateway, ALB, Lambda Function URL, and VPC Lattice +* **[Event handler: Agents for Amazon Bedrock](https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/bedrock_agents/)** - Create Agents for Amazon Bedrock, automatically generating OpenAPI schemas +* **[Middleware factory](https://docs.powertools.aws.dev/lambda/python/latest/utilities/middleware_factory/)** - Decorator factory to create your own middleware to run logic before, and after each Lambda invocation +* **[Parameters](https://docs.powertools.aws.dev/lambda/python/latest/utilities/parameters/)** - Retrieve and cache parameter values from Parameter Store, Secrets Manager, AppConfig, or DynamoDB +* **[Batch processing](https://docs.powertools.aws.dev/lambda/python/latest/utilities/batch/)** - Handle partial failures for AWS SQS, Kinesis Data Streams, and DynamoDB Streams batch processing * **[Typing](https://docs.powertools.aws.dev/lambda/python/latest/utilities/typing/)** - Static typing classes to speedup development in your IDE * **[Validation](https://docs.powertools.aws.dev/lambda/python/latest/utilities/validation/)** - JSON Schema validator for inbound events and responses * **[Event source data classes](https://docs.powertools.aws.dev/lambda/python/latest/utilities/data_classes/)** - Data classes describing the schema of common Lambda event triggers * **[Parser](https://docs.powertools.aws.dev/lambda/python/latest/utilities/parser/)** - Data parsing and deep validation using Pydantic * **[Idempotency](https://docs.powertools.aws.dev/lambda/python/latest/utilities/idempotency/)** - Convert your Lambda functions into idempotent operations which are safe to retry +* **[Data Masking](https://docs.powertools.aws.dev/lambda/python/latest/utilities/data_masking/)** - Protect confidential data with easy removal or encryption * **[Feature Flags](https://docs.powertools.aws.dev/lambda/python/latest/utilities/feature_flags/)** - A simple rule engine to evaluate when one or multiple features should be enabled depending on the input -* **[Streaming](https://docs.powertools.aws.dev/lambda/python/latest/utilities/streaming/)** - Streams datasets larger than the available memory as streaming data. +* **[Streaming](https://docs.powertools.aws.dev/lambda/python/latest/utilities/streaming/)** - Streams datasets larger than the available memory as streaming data +* **[Kafka](https://docs.powertools.aws.dev/lambda/python/latest/utilities/kafka/)** - Deserialize and validate Kafka events with support for Avro, Protocol Buffers, and JSON Schema +* **[JMESPath Functions](https://docs.powertools.aws.dev/lambda/python/latest/utilities/jmespath_functions/)** - Built-in JMESPath functions to easily deserialize common encoded JSON payloads in Lambda functions ### Installation @@ -53,19 +59,36 @@ Knowing which companies are using this library is important to help prioritize t The following companies, among others, use Powertools: +* [Alma Media](https://www.almamedia.fi/en/) +* [Banxware](https://www.banxware.com/) +* [Brsk](https://www.brsk.co.uk/) +* [BusPatrol](https://buspatrol.com/) * [Capital One](https://www.capitalone.com/) +* [Caylent](https://caylent.com/) +* [CHS Inc.](https://www.chsinc.com/) * [CPQi (Exadel Financial Services)](https://cpqi.com/) * [CloudZero](https://www.cloudzero.com/) * [CyberArk](https://www.cyberark.com/) +* [EF Education First](https://www.ef.com/) +* [Flyweight](https://flyweight.io/) * [globaldatanet](https://globaldatanet.com/) +* [Guild](https://guild.com/) * [IMS](https://ims.tech/) +* [Instil](https://instil.co/) +* [Jit Security](https://www.jit.io/) +* [LocalStack](https://www.localstack.cloud/) * [Propellor.ai](https://www.propellor.ai/) +* [Pushpay](https://pushpay.com/) +* [QuasiScience Limited](https://quasiscience.com/) +* [Recast](https://getrecast.com/) * [TopSport](https://www.topsport.com.au/) +* [Transformity](https://transformity.tech/) * [Trek10](https://www.trek10.com/) +* [Vertex Pharmaceuticals](https://www.vrtx.com/) ### Sharing your work -Share what you did with Powertools for AWS Lambda (Python) 💞💞. Blog post, workshops, presentation, sample apps and others. Check out what the community has already shared about Powertools for AWS Lambda (Python) [here](https://docs.powertools.aws.dev/lambda/python/latest/we_made_this/). +Share what you did with Powertools for AWS Lambda (Python) 💞💞. Blog post, workshops, presentation, sample apps and others. Check out what the community has already shared about Powertools for AWS Lambda (Python) [in this link](https://docs.powertools.aws.dev/lambda/python/latest/we_made_this/). ### Using Lambda Layer or SAR @@ -79,11 +102,11 @@ This helps us understand who uses Powertools for AWS Lambda (Python) in a non-in ## Connect * **Powertools for AWS Lambda on Discord**: `#python` - **[Invite link](https://discord.gg/B8zZKbbyET)** -* **Email**: +* **Email**: ## Security disclosures -If you think you’ve found a potential security issue, please do not post it in the Issues. Instead, please follow the instructions [here](https://aws.amazon.com/security/vulnerability-reporting/) or [email AWS security directly](mailto:aws-security@amazon.com). +If you think you’ve found a potential security issue, please do not post it in the Issues. Instead, please follow the instructions [in this link](https://aws.amazon.com/security/vulnerability-reporting/) or [email AWS security directly](mailto:aws-security@amazon.com). ## License diff --git a/THIRD-PARTY-LICENSES b/THIRD-PARTY-LICENSES index 3712eac88cf..f6d647c54a7 100644 --- a/THIRD-PARTY-LICENSES +++ b/THIRD-PARTY-LICENSES @@ -1,3 +1,27 @@ +** FastAPI - https://github.com/tiangolo/fastapi/ - Used in the OpenAPI feature + + The MIT License (MIT) + + Copyright (c) 2018 Sebastián Ramírez + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ** Tensorflow - https://github.com/tensorflow/tensorflow/ Apache License diff --git a/aws_lambda_powertools/__init__.py b/aws_lambda_powertools/__init__.py index 14237bc7119..dbec3d35635 100644 --- a/aws_lambda_powertools/__init__.py +++ b/aws_lambda_powertools/__init__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Top-level package for Lambda Python Powertools.""" from pathlib import Path diff --git a/aws_lambda_powertools/event_handler/__init__.py b/aws_lambda_powertools/event_handler/__init__.py index 85298cfc15c..98433b2e29b 100644 --- a/aws_lambda_powertools/event_handler/__init__.py +++ b/aws_lambda_powertools/event_handler/__init__.py @@ -11,19 +11,38 @@ Response, ) from aws_lambda_powertools.event_handler.appsync import AppSyncResolver +from aws_lambda_powertools.event_handler.bedrock_agent import BedrockAgentResolver, BedrockResponse +from aws_lambda_powertools.event_handler.bedrock_agent_function import ( + 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.vpc_lattice import VPCLatticeResolver +from aws_lambda_powertools.event_handler.request import Request +from aws_lambda_powertools.event_handler.vpc_lattice import VPCLatticeResolver, VPCLatticeV2Resolver __all__ = [ "AppSyncResolver", + "AppSyncEventsResolver", "APIGatewayRestResolver", "APIGatewayHttpResolver", "ALBResolver", "ApiGatewayResolver", + "BedrockAgentResolver", + "BedrockAgentFunctionResolver", + "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 446b1eca856..a323cf67d56 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import base64 import json import logging @@ -9,22 +11,41 @@ from enum import Enum from functools import partial from http import HTTPStatus -from typing import ( - Any, - Callable, - Dict, - List, - Match, - Optional, - Pattern, - Set, - Tuple, - Type, - Union, -) +from pathlib import Path +from typing import TYPE_CHECKING, Any, Generic, Literal, Match, Pattern, TypeVar, cast + +from typing_extensions import override from aws_lambda_powertools.event_handler import content_types +from aws_lambda_powertools.event_handler.exception_handling import ExceptionHandlerManager from aws_lambda_powertools.event_handler.exceptions import NotFoundError, ServiceError +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, + RequestValidationError, + ResponseValidationError, + SchemaValidationError, +) +from aws_lambda_powertools.event_handler.openapi.types import ( + COMPONENT_REF_PREFIX, + OpenAPIResponse, + response_validation_error_response_definition, +) +from aws_lambda_powertools.event_handler.request import Request +from aws_lambda_powertools.event_handler.util import ( + _FrozenDict, + _FrozenListDict, + _validate_openapi_security_parameters, + extract_origin_header, +) from aws_lambda_powertools.shared.cookies import Cookie from aws_lambda_powertools.shared.functions import powertools_dev_is_set from aws_lambda_powertools.shared.json_encoder import Encoder @@ -32,20 +53,53 @@ ALBEvent, APIGatewayProxyEvent, APIGatewayProxyEventV2, + BedrockAgentEvent, LambdaFunctionUrlEvent, VPCLatticeEvent, + VPCLatticeEventV2, ) from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent -from aws_lambda_powertools.utilities.typing import LambdaContext + +if TYPE_CHECKING: + from collections.abc import Callable, Mapping, Sequence logger = logging.getLogger(__name__) _DYNAMIC_ROUTE_PATTERN = r"(<\w+>)" -_SAFE_URI = "-._~()'!*:@,;=" # https://www.ietf.org/rfc/rfc3986.txt +_SAFE_URI = "-._~()'!*:@,;=+&$" # https://www.ietf.org/rfc/rfc3986.txt # API GW/ALB decode non-safe URI chars; we must support them too -_UNSAFE_URI = "%<> \[\]{}|^" # noqa: W605 +_UNSAFE_URI = r"%<> \[\]{}|^" _NAMED_GROUP_BOUNDARY_PATTERN = rf"(?P\1[{_SAFE_URI}{_UNSAFE_URI}\\w]+)" _ROUTE_REGEX = "^{}$" +_JSON_DUMP_CALL = partial(json.dumps, separators=(",", ":"), cls=Encoder) + +ResponseEventT = TypeVar("ResponseEventT", bound=BaseProxyEvent) +ResponseT = TypeVar("ResponseT") + +if TYPE_CHECKING: + from aws_lambda_powertools.event_handler.openapi.compat import ( + JsonSchemaValue, + ModelField, + ) + from aws_lambda_powertools.event_handler.openapi.models import ( + Contact, + ExternalDocumentation, + License, + OpenAPI, + SecurityScheme, + Server, + Tag, + ) + from aws_lambda_powertools.event_handler.openapi.params import Dependant + from aws_lambda_powertools.event_handler.openapi.swagger_ui.oauth2 import ( + OAuth2Config, + ) + from aws_lambda_powertools.event_handler.openapi.types import ( + TypeModelOrEnum, + ) + from aws_lambda_powertools.shared.cookies import Cookie + from aws_lambda_powertools.shared.types import AnyCallableT + from aws_lambda_powertools.utilities.typing import LambdaContext class ProxyEventType(Enum): @@ -54,30 +108,45 @@ class ProxyEventType(Enum): APIGatewayProxyEvent = "APIGatewayProxyEvent" APIGatewayProxyEventV2 = "APIGatewayProxyEventV2" ALBEvent = "ALBEvent" + BedrockAgentEvent = "BedrockAgentEvent" VPCLatticeEvent = "VPCLatticeEvent" + VPCLatticeEventV2 = "VPCLatticeEventV2" 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 Examples -------- - Simple cors example using the default permissive cors, not this should only be used during early prototyping + Simple CORS example using the default permissive CORS, note that this should only be used during early prototyping. ```python - from aws_lambda_powertools.event_handler import APIGatewayRestResolver + from aws_lambda_powertools.event_handler.api_gateway import ( + APIGatewayRestResolver, CORSConfig + ) - app = APIGatewayRestResolver() + app = APIGatewayRestResolver(cors=CORSConfig()) - @app.get("/my/path", cors=True) + @app.get("/my/path") def with_cors(): return {"message": "Foo"} ``` Using a custom CORSConfig where `with_cors` used the custom provided CORSConfig and `without_cors` - do not include any cors headers. + do not include any CORS headers. ```python from aws_lambda_powertools.event_handler.api_gateway import ( @@ -109,10 +178,10 @@ def without_cors(): def __init__( self, allow_origin: str = "*", - extra_origins: Optional[List[str]] = None, - allow_headers: Optional[List[str]] = None, - expose_headers: Optional[List[str]] = None, - max_age: Optional[int] = None, + extra_origins: list[str] | None = None, + allow_headers: list[str] | None = None, + expose_headers: list[str] | None = None, + max_age: int | None = None, allow_credentials: bool = False, ): """ @@ -121,28 +190,31 @@ def __init__( allow_origin: str The value of the `Access-Control-Allow-Origin` to send in the response. Defaults to "*", but should only be used during development. - extra_origins: Optional[List[str]] + extra_origins: list[str] | None The list of additional allowed origins. - allow_headers: Optional[List[str]] + allow_headers: list[str] | None The list of additional allowed headers. This list is added to list of built-in allowed headers: `Authorization`, `Content-Type`, `X-Amz-Date`, `X-Api-Key`, `X-Amz-Security-Token`. - expose_headers: Optional[List[str]] + expose_headers: list[str] | None A list of values to return for the Access-Control-Expose-Headers - max_age: Optional[int] + max_age: int | None The value for the `Access-Control-Max-Age` allow_credentials: bool A boolean value that sets the value of `Access-Control-Allow-Credentials` """ + self._allowed_origins = [allow_origin] + if extra_origins: self._allowed_origins.extend(extra_origins) + self.allow_headers = set(self._REQUIRED_HEADERS + (allow_headers or [])) self.expose_headers = expose_headers or [] self.max_age = max_age self.allow_credentials = allow_credentials - def to_dict(self, origin: Optional[str]) -> Dict[str, str]: + def to_dict(self, origin: str | None) -> dict[str, str]: """Builds the configured Access-Control http headers""" # If there's no Origin, don't add any CORS headers @@ -155,31 +227,106 @@ def to_dict(self, origin: Optional[str]) -> Dict[str, str]: return {} # The origin matched an allowed origin, so return the CORS headers - headers: Dict[str, str] = { + headers = { "Access-Control-Allow-Origin": origin, - "Access-Control-Allow-Headers": ",".join(sorted(self.allow_headers)), + "Access-Control-Allow-Headers": CORSConfig.build_allow_methods(self.allow_headers), } if self.expose_headers: headers["Access-Control-Expose-Headers"] = ",".join(self.expose_headers) if self.max_age is not None: headers["Access-Control-Max-Age"] = str(self.max_age) - if self.allow_credentials is True: + if origin != "*" and self.allow_credentials is True: headers["Access-Control-Allow-Credentials"] = "true" return headers + def allowed_origin(self, extracted_origin: str) -> str | None: + if extracted_origin in self._allowed_origins: + return extracted_origin + if extracted_origin is not None and "*" in self._allowed_origins: + return "*" + + return None + + @staticmethod + def build_allow_methods(methods: set[str]) -> str: + """Build sorted comma delimited methods for Access-Control-Allow-Methods header + + Parameters + ---------- + methods : set[str] + Set of HTTP Methods + + Returns + ------- + set[str] + Formatted string with all HTTP Methods allowed for CORS e.g., `GET, OPTIONS` + + """ + return ",".join(sorted(methods)) + + +class BedrockResponse(Generic[ResponseT]): + """ + Contains the response body, status code, content type, and optional attributes + for session management and knowledge base configuration. + + Note + ---- + Amazon Bedrock Agents only support TEXT content type in the responseBody according to the + Lambda integration documentation. As a result, all response bodies are automatically serialized + as JSON strings regardless of the content_type parameter. The content_type parameter is maintained + for API consistency but does not affect the actual format sent to Bedrock Agents. + + See: https://docs.aws.amazon.com/bedrock/latest/userguide/agents-lambda.html + """ + + def __init__( + self, + body: Any = None, + 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, + ) -> None: + self.body = body + self.status_code = status_code + self.content_type = content_type + self.session_attributes = session_attributes + self.prompt_session_attributes = prompt_session_attributes + self.knowledge_bases_configuration = knowledge_bases_configuration + + def is_json(self) -> bool: + """ + Returns True if the response is JSON, based on the Content-Type. + + Note + ---- + This method always returns True for BedrockResponse regardless of the content_type parameter. + This is because Amazon Bedrock Agents only support TEXT content type in the responseBody, + and the event handler automatically serializes all response bodies as JSON strings when + sending to Bedrock Agents. + + See: https://docs.aws.amazon.com/bedrock/latest/userguide/agents-lambda.html -class Response: + The content_type parameter in BedrockResponse is maintained for API consistency but does not + affect the actual response format sent to Bedrock Agents. + """ + return True + + +class Response(Generic[ResponseT]): """Response data class that provides greater control over what is returned from the proxy event""" def __init__( self, status_code: int, - content_type: Optional[str] = None, - body: Union[str, bytes, None] = None, - headers: Optional[Dict[str, Union[str, List[str]]]] = None, - cookies: Optional[List[Cookie]] = None, - compress: Optional[bool] = None, + content_type: str | None = None, + body: ResponseT | None = None, + headers: Mapping[str, str | list[str]] | None = None, + cookies: list[Cookie] | None = None, + compress: bool | None = None, ): """ @@ -190,9 +337,9 @@ def __init__( content_type: str Optionally set the Content-Type header, example "application/json". Note this will be merged into any provided http headers - body: Union[str, bytes, None] + body: str | bytes | None Optionally set the response body. Note: bytes body will be automatically base64 encoded - headers: dict[str, Union[str, List[str]]] + headers: Mapping[str, str | list[str]] Optionally set specific http headers. Setting "Content-Type" here would override the `content_type` value. cookies: list[Cookie] Optionally set cookies. @@ -200,12 +347,22 @@ def __init__( self.status_code = status_code self.body = body self.base64_encoded = False - self.headers: Dict[str, Union[str, List[str]]] = headers if headers else {} + self.headers: dict[str, str | list[str]] = dict(headers) if headers else {} self.cookies = cookies or [] self.compress = compress + self.content_type = content_type if content_type: self.headers.setdefault("Content-Type", content_type) + def is_json(self) -> bool: + """ + Returns True if the response is JSON, based on the Content-Type. + """ + content_type = self.headers.get("Content-Type", "") + if isinstance(content_type, list): + content_type = content_type[0] + return content_type.startswith(DEFAULT_CONTENT_TYPE) + class Route: """Internally used Route Configuration""" @@ -213,30 +370,398 @@ class Route: def __init__( self, method: str, + path: str, rule: Pattern, func: Callable, cors: bool, compress: bool, - cache_control: Optional[str], + cache_control: str | None = None, + summary: str | None = None, + description: str | None = None, + responses: dict[int, OpenAPIResponse] | None = None, + response_description: str | None = None, + tags: list[str] | None = None, + operation_id: str | None = None, + include_in_schema: bool = True, + security: list[dict[str, list[str]]] | None = None, + openapi_extensions: dict[str, Any] | None = None, + 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, ): + """ + Internally used Route Configuration + + Parameters + ---------- + method: str + The HTTP method, example "GET" + path: str + The path of the route + rule: Pattern + The route rule, example "/my/path" + func: Callable + The route handler function + cors: bool + Whether or not to enable CORS for this route + compress: bool + Whether or not to enable gzip compression for this route + cache_control: str | None + The cache control header value, example "max-age=3600" + summary: str | None + The OpenAPI summary for this route + description: str | None + The OpenAPI description for this route + responses: dict[int, OpenAPIResponse] | None + The OpenAPI responses for this route + response_description: str | None + The OpenAPI response description for this route + tags: list[str] | None + The list of OpenAPI tags to be used for this route + operation_id: str | None + The OpenAPI operationId for this route + include_in_schema: bool + Whether or not to include this route in the OpenAPI schema + security: list[dict[str, list[str]]], optional + The OpenAPI security for this route + openapi_extensions: dict[str, Any], optional + Additional OpenAPI extensions as a dictionary. + deprecated: bool + Whether or not to mark this route as deprecated in the OpenAPI schema + enable_validation: bool | None, optional + 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. + """ self.method = method.upper() + self.path = path if path.strip() else "/" + + # OpenAPI spec only understands paths with { }. So we'll have to convert Powertools' < >. + # https://swagger.io/specification/#path-templating + self.openapi_path = re.sub(r"<(.*?)>", lambda m: f"{{{''.join(m.group(1))}}}", self.path) # type: ignore[arg-type] + self.rule = rule self.func = func + self._middleware_stack = func self.cors = cors self.compress = compress self.cache_control = cache_control + self.summary = summary + self.description = description + self.responses = responses + self.response_description = response_description + self.tags = tags or [] + self.include_in_schema = include_in_schema + self.security = security + self.openapi_extensions = openapi_extensions + self.middlewares = middlewares or [] + self.operation_id = operation_id or self._generate_operation_id() + self.deprecated = deprecated + self.enable_validation = enable_validation + + # _middleware_stack_built is used to ensure the middleware stack is only built once. + self._middleware_stack_built = False + + # _dependant is used to cache the dependant model for the handler function + self._dependant: Dependant | None = None + + # _body_field is used to cache the dependant model for the body field + 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, + router_middlewares: list[Callable], + app: ApiGatewayResolver, + route_arguments: dict[str, str], + ) -> dict | tuple | Response: + """Calling the Router class instance will trigger the following actions: + 1. If Route Middleware stack has not been built, build it + 2. Call the Route Middleware stack wrapping the original function + handler with the app and route arguments. + + Parameters + ---------- + router_middlewares: list[Callable] + The list of Router Middlewares (assigned to ALL routes) + app: "ApiGatewayResolver" + The ApiGatewayResolver instance to pass into the middleware stack + route_arguments: dict[str, str] + The route arguments to pass to the app function (extracted from the Api Gateway + Lambda Message structure from AWS) + + Returns + ------- + dict | tuple | Response + API Response object in ALL cases, except when the original API route + handler is called which may also return a dict, tuple, or Response. + """ + + # Save CPU cycles by building middleware stack once + if not self._middleware_stack_built: + self._build_middleware_stack(router_middlewares=router_middlewares, app=app) + + # If debug is turned on then output the middleware stack to the console + if app._debug: + print(f"\nProcessing Route:::{self.func.__name__} ({app.context['_path']})") + # Collect ALL middleware for debug printing - include internal _registered_api_adapter + all_middlewares = router_middlewares + self.middlewares + [_registered_api_adapter] + print("\nMiddleware Stack:") + print("=================") + print("\n".join(getattr(item, "__name__", "Unknown") for item in all_middlewares)) + print("=================") + + # Add Route Arguments to app context + app.append_context(_route_args=route_arguments) + + # Call the Middleware Wrapped _call_stack function handler with the app + return self._middleware_stack(app) + + def _build_middleware_stack(self, router_middlewares: list[Callable[..., Any]], app) -> None: + """ + Builds the middleware stack for the handler by wrapping each + handler in an instance of MiddlewareWrapper which is used to contain the state + of each middleware step. + + Middleware is represented by a standard Python Callable construct. Any Middleware + handler wanting to short-circuit the middlware call chain can raise an exception + to force the Python call stack created by the handler call-chain to naturally un-wind. + + This becomes a simple concept for developers to understand and reason with - no additional + gymnastics other than plain old try ... except. + + Notes + ----- + The Route Middleware stack is processed in reverse order. This is so the stack of + middleware handlers is applied in the order of being added to the handler. + """ + # Build middleware stack in the correct order for validation: + # 1. Request validation middleware (first) + # 2. Router middlewares + user middlewares (middle) + # 3. Response validation middleware (before route handler) + # 4. Route handler adapter (last) + + all_middlewares = [] + + # Determine if validation should be enabled for this route + # If route has explicit enable_validation setting, use it; otherwise, use resolver's global setting + route_validation_enabled = ( + self.enable_validation if self.enable_validation is not None else app._enable_validation + ) + + # If route needs validation but resolver didn't create the middlewares, create them now + if route_validation_enabled and not hasattr(app, "_request_validation_middleware"): + from aws_lambda_powertools.event_handler.middlewares.openapi_validation import ( + OpenAPIRequestValidationMiddleware, + OpenAPIResponseValidationMiddleware, + ) + + app._request_validation_middleware = OpenAPIRequestValidationMiddleware() + app._response_validation_middleware = OpenAPIResponseValidationMiddleware( + validation_serializer=app._serializer, + has_response_validation_error=app._has_response_validation_error, + ) + + # Add request validation middleware first if validation is enabled + if route_validation_enabled and hasattr(app, "_request_validation_middleware"): + all_middlewares.append(app._request_validation_middleware) + + # Add user middlewares in the middle + all_middlewares.extend(router_middlewares + self.middlewares) + + # Add response validation middleware before the route handler if validation is enabled + if route_validation_enabled and hasattr(app, "_response_validation_middleware"): + all_middlewares.append(app._response_validation_middleware) + + logger.debug(f"Building middleware stack: {all_middlewares}") + + # IMPORTANT: + # this must be the last middleware in the stack (tech debt for backward + # compatibility purposes) + # + # This adapter will: + # 1. Call the registered API passing only the expected route arguments extracted from the path + # and not the middleware. + # 2. Adapt the response type of the route handler (dict | tuple | Response) + # and normalise into a Response object so middleware will always have a constant signature + all_middlewares.append(_registered_api_adapter) + + # Wrap the original route handler function in the middleware handlers + # using the MiddlewareWrapper class callable construct in reverse order to + # ensure middleware is applied in the order the user defined. + # + # Start with the route function and wrap from last to the first Middleware handler. + for handler in reversed(all_middlewares): + self._middleware_stack = MiddlewareFrame(current_middleware=handler, next_middleware=self._middleware_stack) + + self._middleware_stack_built = True + + async def call_async( + self, + router_middlewares: list[Callable], + app: ApiGatewayResolver, + route_arguments: dict[str, str], + ) -> dict | tuple | Response: + from aws_lambda_powertools.event_handler.middlewares.async_utils import ( + AsyncMiddlewareFrame, + _registered_api_adapter_async, + ) + + all_middlewares: list[Callable[..., Any]] = [] + + route_validation_enabled = ( + self.enable_validation if self.enable_validation is not None else app._enable_validation + ) + + if route_validation_enabled and not hasattr(app, "_request_validation_middleware"): + from aws_lambda_powertools.event_handler.middlewares.openapi_validation import ( + OpenAPIRequestValidationMiddleware, + OpenAPIResponseValidationMiddleware, + ) + + app._request_validation_middleware = OpenAPIRequestValidationMiddleware() + app._response_validation_middleware = OpenAPIResponseValidationMiddleware( + validation_serializer=app._serializer, + has_response_validation_error=app._has_response_validation_error, + ) + + if route_validation_enabled and hasattr(app, "_request_validation_middleware"): + all_middlewares.append(app._request_validation_middleware) + + all_middlewares.extend(router_middlewares + self.middlewares) + + if route_validation_enabled and hasattr(app, "_response_validation_middleware"): + all_middlewares.append(app._response_validation_middleware) + + all_middlewares.append(_registered_api_adapter_async) + + logger.debug(f"Building async middleware stack: {all_middlewares}") + + if app._debug: + print(f"\nProcessing Route (async):::{self.func.__name__} ({app.context['_path']})") + print("\nAsync Middleware Stack:") + print("=================") + print("\n".join(getattr(item, "__name__", "Unknown") for item in all_middlewares)) + print("=================") + + app.append_context(_route_args=route_arguments) + + # Build async chain from inside-out (not cached, avoids state conflicts with sync cache) + next_handler: Callable = self.func + for handler in reversed(all_middlewares): + next_handler = AsyncMiddlewareFrame(current_middleware=handler, next_middleware=next_handler) + + return await next_handler(app) + + @property + def dependant(self) -> Dependant: + if self._dependant is None: + 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 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 + + self._has_dependencies = _has_depends(self.func) + return self._has_dependencies + + @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( + 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. + + Delegates to openapi.schema_generator for the actual generation logic. + """ + 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, + ) + + def _generate_operation_id(self) -> str: + operation_id = self.func.__name__ + self.openapi_path + operation_id = re.sub(r"\W", "_", operation_id) + operation_id = operation_id + "_" + self.method.lower() + return operation_id -class ResponseBuilder: +class ResponseBuilder(Generic[ResponseEventT]): """Internally used Response builder""" - def __init__(self, response: Response, route: Optional[Route] = None): + def __init__( + self, + response: Response, + serializer: Callable[[Any], str] = _JSON_DUMP_CALL, + route: Route | None = None, + ): self.response = response + self.serializer = serializer self.route = route - def _add_cors(self, event: BaseProxyEvent, cors: CORSConfig): + def _add_cors(self, event: ResponseEventT, cors: CORSConfig): """Update headers to include the configured Access-Control headers""" - self.response.headers.update(cors.to_dict(event.get_header_value("Origin"))) + extracted_origin_header = extract_origin_header(event.resolved_headers_field) + + origin = cors.allowed_origin(extracted_origin_header) + if origin is not None: + self.response.headers.update(cors.to_dict(origin)) def _add_cache_control(self, cache_control: str): """Set the specified cache control headers for 200 http responses. For non-200 `no-cache` is used.""" @@ -246,8 +771,8 @@ def _add_cache_control(self, cache_control: str): @staticmethod def _has_compression_enabled( route_compression: bool, - response_compression: Optional[bool], - event: BaseProxyEvent, + response_compression: bool | None, + event: ResponseEventT, ) -> bool: """ Checks if compression is enabled. @@ -260,7 +785,7 @@ def _has_compression_enabled( A boolean indicating whether compression is enabled or not in the route setting. response_compression: bool, optional A boolean indicating whether compression is enabled or not in the response setting. - event: BaseProxyEvent + event: ResponseEventT The event object containing the request details. Returns @@ -268,8 +793,14 @@ def _has_compression_enabled( bool True if compression is enabled and the "gzip" encoding is accepted, False otherwise. """ - encoding: str = event.get_header_value(name="accept-encoding", default_value="", case_sensitive=False) # type: ignore[assignment] # noqa: E501 - if "gzip" in encoding: + encoding = event.resolved_headers_field.get("accept-encoding", "") + gzip_accepted = False + if isinstance(encoding, str): + gzip_accepted = "gzip" in encoding + elif isinstance(encoding, list): + gzip_accepted = "gzip" in ",".join(encoding) + + if gzip_accepted: if response_compression is not None: return response_compression # e.g., Response(compress=False/True)) if route_compression: @@ -286,7 +817,7 @@ def _compress(self): gzip = zlib.compressobj(9, zlib.DEFLATED, zlib.MAX_WBITS | 16) self.response.body = gzip.compress(self.response.body) + gzip.flush() - def _route(self, event: BaseProxyEvent, cors: Optional[CORSConfig]): + def _route(self, event: ResponseEventT, cors: CORSConfig | None): """Optionally handle any of the route's configure response handling""" if self.route is None: return @@ -301,8 +832,14 @@ def _route(self, event: BaseProxyEvent, cors: Optional[CORSConfig]): ): self._compress() - def build(self, event: BaseProxyEvent, cors: Optional[CORSConfig] = None) -> Dict[str, Any]: + def build(self, event: ResponseEventT, cors: CORSConfig | None = None) -> dict[str, Any]: """Build the full response dict to be returned by the lambda""" + + # We only apply the serializer when the content type is JSON and the + # body is not a str, to avoid double encoding + if self.response.is_json() and not isinstance(self.response.body, str): + self.response.body = self.serializer(self.response.body) + self._route(event, cors) if isinstance(self.response.body, bytes): @@ -319,22 +856,101 @@ def build(self, event: BaseProxyEvent, cors: Optional[CORSConfig] = None) -> Dic class BaseRouter(ABC): + """Base class for Routing""" + current_event: BaseProxyEvent lambda_context: LambdaContext context: dict + _router_middlewares: list[Callable] = [] + processed_stack_frames: list[str] = [] @abstractmethod def route( self, rule: str, method: Any, - cors: Optional[bool] = None, + cors: bool | None = None, compress: bool = False, - cache_control: Optional[str] = None, - ): + cache_control: str | None = None, + summary: str | None = None, + description: str | None = None, + responses: dict[int, OpenAPIResponse] | None = None, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + tags: list[str] | None = None, + operation_id: str | None = None, + include_in_schema: bool = True, + security: list[dict[str, list[str]]] | None = None, + openapi_extensions: dict[str, Any] | None = None, + 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() - def get(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): + def use(self, middlewares: list[Callable[..., Response]]) -> None: + """ + Add one or more global middlewares that run before/after route specific middleware. + + NOTE: Middlewares are called in insertion order. + + Parameters + ---------- + middlewares: list[Callable[..., Response]] + List of global middlewares to be used + + Examples + -------- + + Add middlewares to be used for every request processed by the Router. + + ```python + from aws_lambda_powertools import Logger + from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response + from aws_lambda_powertools.event_handler.middlewares import NextMiddleware + + logger = Logger() + app = APIGatewayRestResolver() + + def log_request_response(app: APIGatewayRestResolver, next_middleware: NextMiddleware) -> Response: + logger.info("Incoming request", path=app.current_event.path, request=app.current_event.raw_event) + + result = next_middleware(app) + logger.info("Response received", response=result.__dict__) + + return result + + app.use(middlewares=[log_request_response]) + + + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ + self._router_middlewares = self._router_middlewares + middlewares + + def get( + self, + rule: str, + cors: bool | None = None, + compress: bool = False, + cache_control: str | None = None, + summary: str | None = None, + description: str | None = None, + responses: dict[int, OpenAPIResponse] | None = None, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + tags: list[str] | None = None, + operation_id: str | None = None, + include_in_schema: bool = True, + security: list[dict[str, list[str]]] | None = None, + openapi_extensions: dict[str, Any] | None = None, + 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` Examples @@ -357,9 +973,49 @@ def lambda_handler(event, context): return app.resolve(event, context) ``` """ - return self.route(rule, "GET", cors, compress, cache_control) + return self.route( + rule, + "GET", + cors, + compress, + cache_control, + summary, + description, + responses, + response_description, + tags, + operation_id, + include_in_schema, + security, + openapi_extensions, + deprecated, + enable_validation, + custom_response_validation_http_code, + status_code, + middlewares, + ) - def post(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): + def post( + self, + rule: str, + cors: bool | None = None, + compress: bool = False, + cache_control: str | None = None, + summary: str | None = None, + description: str | None = None, + responses: dict[int, OpenAPIResponse] | None = None, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + tags: list[str] | None = None, + operation_id: str | None = None, + include_in_schema: bool = True, + security: list[dict[str, list[str]]] | None = None, + openapi_extensions: dict[str, Any] | None = None, + 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` Examples @@ -383,9 +1039,49 @@ def lambda_handler(event, context): return app.resolve(event, context) ``` """ - return self.route(rule, "POST", cors, compress, cache_control) + return self.route( + rule, + "POST", + cors, + compress, + cache_control, + summary, + description, + responses, + response_description, + tags, + operation_id, + include_in_schema, + security, + openapi_extensions, + deprecated, + enable_validation, + custom_response_validation_http_code, + status_code, + middlewares, + ) - def put(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): + def put( + self, + rule: str, + cors: bool | None = None, + compress: bool = False, + cache_control: str | None = None, + summary: str | None = None, + description: str | None = None, + responses: dict[int, OpenAPIResponse] | None = None, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + tags: list[str] | None = None, + operation_id: str | None = None, + include_in_schema: bool = True, + security: list[dict[str, list[str]]] | None = None, + openapi_extensions: dict[str, Any] | None = None, + 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` Examples @@ -409,15 +1105,49 @@ def lambda_handler(event, context): return app.resolve(event, context) ``` """ - return self.route(rule, "PUT", cors, compress, cache_control) + return self.route( + rule, + "PUT", + cors, + compress, + cache_control, + summary, + description, + responses, + response_description, + tags, + operation_id, + include_in_schema, + security, + openapi_extensions, + deprecated, + enable_validation, + custom_response_validation_http_code, + status_code, + middlewares, + ) def delete( self, rule: str, - cors: Optional[bool] = None, + cors: bool | None = None, compress: bool = False, - cache_control: Optional[str] = None, - ): + cache_control: str | None = None, + summary: str | None = None, + description: str | None = None, + responses: dict[int, OpenAPIResponse] | None = None, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + tags: list[str] | None = None, + operation_id: str | None = None, + include_in_schema: bool = True, + security: list[dict[str, list[str]]] | None = None, + openapi_extensions: dict[str, Any] | None = None, + 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` Examples @@ -440,15 +1170,49 @@ def lambda_handler(event, context): return app.resolve(event, context) ``` """ - return self.route(rule, "DELETE", cors, compress, cache_control) + return self.route( + rule, + "DELETE", + cors, + compress, + cache_control, + summary, + description, + responses, + response_description, + tags, + operation_id, + include_in_schema, + security, + openapi_extensions, + deprecated, + enable_validation, + custom_response_validation_http_code, + status_code, + middlewares, + ) def patch( self, rule: str, - cors: Optional[bool] = None, + cors: bool | None = None, compress: bool = False, - cache_control: Optional[str] = None, - ): + cache_control: str | None = None, + summary: str | None = None, + description: str | None = None, + responses: dict[int, OpenAPIResponse] | None = None, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + tags: list[str] | None = None, + operation_id: str | None = None, + include_in_schema: bool = True, + security: list[dict[str, list[str]]] | None = None, + openapi_extensions: dict[str, Any] | None = None, + 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` Examples @@ -474,7 +1238,106 @@ def lambda_handler(event, context): return app.resolve(event, context) ``` """ - return self.route(rule, "PATCH", cors, compress, cache_control) + return self.route( + rule, + "PATCH", + cors, + compress, + cache_control, + summary, + description, + responses, + response_description, + tags, + operation_id, + include_in_schema, + security, + openapi_extensions, + deprecated, + enable_validation, + custom_response_validation_http_code, + status_code, + middlewares, + ) + + def head( + self, + rule: str, + cors: bool | None = None, + compress: bool = False, + cache_control: str | None = None, + summary: str | None = None, + description: str | None = None, + responses: dict[int, OpenAPIResponse] | None = None, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + tags: list[str] | None = None, + operation_id: str | None = None, + include_in_schema: bool = True, + security: list[dict[str, list[str]]] | None = None, + openapi_extensions: dict[str, Any] | None = None, + 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` + + Examples + -------- + Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator + + ```python + from aws_lambda_powertools import Tracer + from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response, content_types + + tracer = Tracer() + app = APIGatewayRestResolver() + + @app.head("/head-call") + def simple_head(): + return Response(status_code=200, + content_type=content_types.APPLICATION_JSON, + headers={"Content-Length": "123"}) + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ + return self.route( + rule, + "HEAD", + cors, + compress, + cache_control, + summary, + description, + responses, + response_description, + tags, + operation_id, + include_in_schema, + security, + openapi_extensions, + deprecated, + enable_validation, + custom_response_validation_http_code, + status_code, + middlewares, + ) + + def _push_processed_stack_frame(self, frame: str): + """ + Add Current Middleware to the Middleware Stack Frames + The stack frames will be used when exceptions are thrown and Powertools + debug is enabled by developers. + """ + self.processed_stack_frames.append(frame) + + def _reset_processed_stack(self): + """Reset the Processed Stack Frames""" + self.processed_stack_frames.clear() def append_context(self, **additional_context): """Append key=value data as routing context""" @@ -484,9 +1347,193 @@ 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: + """ + Creates a Middle Stack Wrapper instance to be used as a "Frame" in the overall stack of + middleware functions. Each instance contains the current middleware and the next + middleware function to be called in the stack. + + In this way the middleware stack is constructed in a recursive fashion, with each middleware + calling the next as a simple function call. The actual Python call-stack will contain + each MiddlewareStackWrapper "Frame", meaning any Middleware function can cause the + entire Middleware call chain to be exited early (short-circuited) by raising an exception + or by simply returning early with a custom Response. The decision to short-circuit the middleware + chain is at the user's discretion but instantly available due to the Wrapped nature of the + callable constructs in the Middleware stack and each Middleware function having complete control over + whether the "Next" handler in the stack is called or not. + + 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 + """Current middleware name + + It ensures backward compatibility with view functions being callable. This + improves debugging since we need both current and next middlewares/callable names. + """ + return self.current_middleware.__name__ + + def __str__(self) -> str: + """Identify current middleware identity and call chain for debugging purposes.""" + middleware_name = self.__name__ + return f"[{middleware_name}] next call chain is {middleware_name} -> {self._next_middleware_name}" + + def __call__(self, app: ApiGatewayResolver) -> dict | tuple | Response: + """ + Call the middleware Frame to process the request. + + Parameters + ---------- + app: BaseRouter + The router instance + + Returns + ------- + dict | tuple | Response + (tech-debt for backward compatibility). The response type should be a + Response object in all cases excepting when the original API route handler + is called which will return one of 3 outputs. + + """ + # Do debug printing and push processed stack frame AFTER calling middleware + # else the stack frame text of `current calling next` is confusing. + logger.debug("MiddlewareFrame: %s", self) + app._push_processed_stack_frame(str(self)) + + 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], +) -> dict | tuple | Response | BedrockResponse: + """ + Calls the registered API using the "_route_args" from the Resolver context to ensure the last call + in the chain will match the API route function signature and ensure that Powertools passes the API + route handler the expected arguments. + + **IMPORTANT: This internal middleware ensures the actual API route is called with the correct call signature + and it MUST be the final frame in the middleware stack. This can only be removed when the API Route + function accepts `app: BaseRouter` as the first argument - which is the breaking change. + + 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}") + + # 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)) + class ApiGatewayResolver(BaseRouter): - """API Gateway and ALB proxy resolver + """API Gateway, VPC Lattice, Bedrock and ALB proxy resolver Examples -------- @@ -514,13 +1561,18 @@ def lambda_handler(event, context): ``` """ + _proxy_event_type: Enum = ProxyEventType.APIGatewayProxyEvent + def __init__( self, - proxy_type: Enum = ProxyEventType.APIGatewayProxyEvent, - cors: Optional[CORSConfig] = None, - debug: Optional[bool] = None, - serializer: Optional[Callable[[Dict], str]] = None, - strip_prefixes: Optional[List[str]] = None, + proxy_type: Enum | None = None, + 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, ): """ Parameters @@ -529,50 +1581,899 @@ def __init__( Proxy request type, defaults to API Gateway V1 cors: CORSConfig Optionally configure and enabled CORS. Not each route will need to have to cors=True - debug: Optional[bool] + debug: bool | None Enables debug mode, by default False. Can be also be enabled by "POWERTOOLS_DEV" environment variable - serializer : Callable, optional + serializer: Callable, optional function to serialize `obj` to a JSON formatted `str`, by default json.dumps - strip_prefixes: List[str], optional - optional list of prefixes to be removed from the request path before doing the routing. This is often used - with api gateways with multiple custom mappings. - """ - self._proxy_type = proxy_type - self._dynamic_routes: List[Route] = [] - self._static_routes: List[Route] = [] - self._route_keys: List[str] = [] - self._exception_handlers: Dict[Type, Callable] = {} + strip_prefixes: list[str | Pattern], optional + optional list of prefixes to be removed from the request path before doing the routing. + This is often used with api gateways with multiple custom mappings. + Each prefix can be a static string or a compiled regex pattern + enable_validation: bool | None + Enables validation of the request body against the route schema, by default False. + response_validation_error_http_code + Sets the returned status code if response is not validated. enable_validation must be True. + json_body_deserializer: Callable[[str], dict], optional + 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.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] = [] + self._exception_handlers: dict[type, Callable] = {} self._cors = cors self._cors_enabled: bool = cors is not None - self._cors_methods: Set[str] = {"OPTIONS"} + self._cors_methods: set[str] = {"OPTIONS"} self._debug = self._has_debug(debug) + self._enable_validation = enable_validation self._strip_prefixes = strip_prefixes - self.context: Dict = {} # early init as customers might add context before event resolution + self.context: dict = {} # early init as customers might add context before event resolution + self.processed_stack_frames = [] + self._response_builder_class = ResponseBuilder[BaseProxyEvent] + self.openapi_config = OpenAPIConfig() # starting an empty dataclass + self.exception_handler_manager = ExceptionHandlerManager() + self._has_response_validation_error = response_validation_error_http_code is not None + self._response_validation_error_http_code = self._validate_response_validation_error_http_code( + response_validation_error_http_code, + enable_validation, + ) # Allow for a custom serializer or a concise json serialization self._serializer = serializer or partial(json.dumps, separators=(",", ":"), cls=Encoder) + self._json_body_deserializer = json_body_deserializer - def route( + if self._enable_validation: + from aws_lambda_powertools.event_handler.middlewares.openapi_validation import ( + OpenAPIRequestValidationMiddleware, + OpenAPIResponseValidationMiddleware, + ) + + # Store validation middlewares to be added in the correct order later + self._request_validation_middleware = OpenAPIRequestValidationMiddleware() + self._response_validation_middleware = OpenAPIResponseValidationMiddleware( + validation_serializer=serializer, + has_response_validation_error=self._has_response_validation_error, + ) + + def _validate_response_validation_error_http_code( self, - rule: str, - method: Union[str, Union[List[str], Tuple[str]]], - cors: Optional[bool] = None, + response_validation_error_http_code: HTTPStatus | int | None, + enable_validation: bool, + ) -> HTTPStatus: + if response_validation_error_http_code and not enable_validation: + msg = "'response_validation_error_http_code' cannot be set when enable_validation is False." + raise ValueError(msg) + + if ( + not isinstance(response_validation_error_http_code, HTTPStatus) + and response_validation_error_http_code is not None + ): + try: + response_validation_error_http_code = HTTPStatus(response_validation_error_http_code) + except ValueError: + msg = f"'{response_validation_error_http_code}' must be an integer representing an HTTP status code." + raise ValueError(msg) from None + + return response_validation_error_http_code or HTTPStatus.UNPROCESSABLE_ENTITY + + def _add_resolver_response_validation_error_response_to_route( + self, + route_openapi_path: tuple[dict[str, Any], dict[str, Any]], + ) -> tuple[dict[str, Any], dict[str, Any]]: + """Adds resolver response validation error response to route's operations.""" + path, path_definitions = route_openapi_path + if self._has_response_validation_error and "ResponseValidationError" not in path_definitions: + response_validation_error_response = { + "description": "Response Validation Error", + "content": { + DEFAULT_CONTENT_TYPE: { + "schema": {"$ref": f"{COMPONENT_REF_PREFIX}ResponseValidationError"}, + }, + }, + } + http_code = self._response_validation_error_http_code.value + for operation in path.values(): + operation["responses"][http_code] = response_validation_error_response + return path, path_definitions + + def _generate_schemas(self, definitions: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]: + schemas = {k: definitions[k] for k in sorted(definitions)} + # add response validation error definition + if self._response_validation_error_http_code: + schemas.setdefault("ResponseValidationError", response_validation_error_response_definition) + return schemas + + def get_openapi_schema( + self, + *, + title: str = DEFAULT_OPENAPI_TITLE, + version: str = DEFAULT_API_VERSION, + openapi_version: str = DEFAULT_OPENAPI_VERSION, + summary: str | None = None, + description: str | None = None, + tags: list[Tag | str] | None = None, + servers: list[Server] | None = None, + terms_of_service: str | None = None, + contact: Contact | None = None, + license_info: License | None = None, + security_schemes: dict[str, SecurityScheme] | None = None, + security: list[dict[str, list[str]]] | None = None, + external_documentation: ExternalDocumentation | None = None, + openapi_extensions: dict[str, Any] | None = None, + ) -> OpenAPI: + """ + Returns the OpenAPI schema as a pydantic model. + + Parameters + ---------- + title: str + The title of the application. + version: str + The version of the OpenAPI document (which is distinct from the OpenAPI Specification version or the API + openapi_version: str, default = "3.1.0" + The version of the OpenAPI Specification (which the document uses). + summary: str, optional + A short summary of what the application does. + description: str, optional + A verbose explanation of the application behavior. + tags: list[Tag | str], optional + A list of tags used by the specification with additional metadata. + servers: list[Server], optional + An array of Server Objects, which provide connectivity information to a target server. + terms_of_service: str, optional + A URL to the Terms of Service for the API. MUST be in the format of a URL. + contact: Contact, optional + The contact information for the exposed API. + license_info: License, optional + The license information for the exposed API. + security_schemes: dict[str, SecurityScheme]], optional + A declaration of the security schemes available to be used in the specification. + security: list[dict[str, list[str]]], optional + A declaration of which security mechanisms are applied globally across the API. + external_documentation: ExternalDocumentation, optional + Additional external documentation for the API. + openapi_extensions: Dict[str, Any], optional + Additional OpenAPI extensions as a dictionary. + + Returns + ------- + OpenAPI: pydantic model + The OpenAPI schema as a pydantic model. + """ + + # DEPRECATION: Will be removed in v4.0.0. Use configure_api() instead. + # Maintained for backwards compatibility. + # See: https://github.com/aws-powertools/powertools-lambda-python/issues/6122 + if title == DEFAULT_OPENAPI_TITLE and self.openapi_config.title: + title = self.openapi_config.title + + if version == DEFAULT_API_VERSION and self.openapi_config.version: + version = self.openapi_config.version + + if openapi_version == DEFAULT_OPENAPI_VERSION and self.openapi_config.openapi_version: + openapi_version = self.openapi_config.openapi_version + + summary = summary or self.openapi_config.summary + description = description or self.openapi_config.description + tags = tags or self.openapi_config.tags + servers = servers or self.openapi_config.servers + terms_of_service = terms_of_service or self.openapi_config.terms_of_service + contact = contact or self.openapi_config.contact + license_info = license_info or self.openapi_config.license_info + security_schemes = security_schemes or self.openapi_config.security_schemes + security = security or self.openapi_config.security + external_documentation = external_documentation or self.openapi_config.external_documentation + openapi_extensions = openapi_extensions or self.openapi_config.openapi_extensions + + from pydantic.json_schema import GenerateJsonSchema + + from aws_lambda_powertools.event_handler.openapi.compat import ( + get_compat_model_name_map, + get_definitions, + ) + from aws_lambda_powertools.event_handler.openapi.models import OpenAPI, PathItem, Tag + from aws_lambda_powertools.event_handler.openapi.types import ( + COMPONENT_REF_TEMPLATE, + ) + + openapi_version = self._determine_openapi_version(openapi_version) + + # Start with the bare minimum required for a valid OpenAPI schema + info: dict[str, Any] = {"title": title, "version": version} + + optional_fields = { + "summary": summary, + "description": description, + "termsOfService": terms_of_service, + "contact": contact, + "license": license_info, + } + + info.update({field: value for field, value in optional_fields.items() if value}) + + if not isinstance(openapi_extensions, dict): + openapi_extensions = {} + + output: dict[str, Any] = { + "openapi": openapi_version, + "info": info, + "servers": self._get_openapi_servers(servers), + "security": self._get_openapi_security(security, security_schemes), + **openapi_extensions, + } + + if external_documentation: + output["externalDocs"] = external_documentation + + components: dict[str, dict[str, Any]] = {} + paths: dict[str, dict[str, Any]] = {} + operation_ids: set[str] = set() + + all_routes = self._dynamic_routes + self._static_routes + all_fields = self._get_fields_from_routes(all_routes) + model_name_map = get_compat_model_name_map(all_fields) + + # Collect all models and definitions + schema_generator = GenerateJsonSchema(ref_template=COMPONENT_REF_TEMPLATE) + field_mapping, definitions = get_definitions( + fields=all_fields, + schema_generator=schema_generator, + model_name_map=model_name_map, + ) + + # Add routes to the OpenAPI schema + for route in all_routes: + if route.security and not _validate_openapi_security_parameters( + security=route.security, + security_schemes=security_schemes, + ): + raise SchemaValidationError( + "Security configuration was not found in security_schemas or security_schema was not defined. " + "See: https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/#security-schemes", + ) + + if not route.include_in_schema: + continue + + result = route._get_openapi_path( + dependant=route.dependant, + operation_ids=operation_ids, + model_name_map=model_name_map, + field_mapping=field_mapping, + enable_validation=self._enable_validation, + ) + if result: + path, path_definitions = self._add_resolver_response_validation_error_response_to_route(result) + if path: + paths.setdefault(route.openapi_path, {}).update(path) + if path_definitions: + definitions.update(path_definitions) + + if definitions: + components["schemas"] = self._generate_schemas(definitions) + if security_schemes: + components["securitySchemes"] = security_schemes + if components: + output["components"] = components + if tags: + output["tags"] = [Tag(name=tag) if isinstance(tag, str) else tag for tag in tags] + + output["paths"] = {k: PathItem(**v) for k, v in paths.items()} + + return OpenAPI(**output) + + @staticmethod + def _get_openapi_servers(servers: list[Server] | None) -> list[Server]: + from aws_lambda_powertools.event_handler.openapi.models import Server + + # If the 'servers' property is not provided or is an empty array, + # the default behavior is to return a Server Object with a URL value of "/". + return servers or [Server(url="/")] + + @staticmethod + def _get_openapi_security( + security: list[dict[str, list[str]]] | None, + security_schemes: dict[str, SecurityScheme] | None, + ) -> list[dict[str, list[str]]] | None: + if not security: + return None + + if not _validate_openapi_security_parameters(security=security, security_schemes=security_schemes): + raise SchemaValidationError( + "Security configuration was not found in security_schemas or security_schema was not defined. " + "See: https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/#security-schemes", + ) + + return security + + @staticmethod + def _determine_openapi_version(openapi_version: str): + # Pydantic V2 has no support for OpenAPI schema 3.0 + if not openapi_version.startswith("3.1"): + warnings.warn( + "You are using Pydantic v2, which is incompatible with OpenAPI schema 3.0. Forcing OpenAPI 3.1", + stacklevel=2, + ) + openapi_version = "3.1.0" + return openapi_version + + def get_openapi_json_schema( + self, + *, + title: str = DEFAULT_OPENAPI_TITLE, + version: str = DEFAULT_API_VERSION, + openapi_version: str = DEFAULT_OPENAPI_VERSION, + summary: str | None = None, + description: str | None = None, + tags: list[Tag | str] | None = None, + servers: list[Server] | None = None, + terms_of_service: str | None = None, + contact: Contact | None = None, + license_info: License | None = None, + security_schemes: dict[str, SecurityScheme] | None = None, + security: list[dict[str, list[str]]] | None = None, + external_documentation: ExternalDocumentation | None = None, + openapi_extensions: dict[str, Any] | None = None, + ) -> str: + """ + Returns the OpenAPI schema as a JSON serializable dict + + Parameters + ---------- + title: str + The title of the application. + version: str + The version of the OpenAPI document (which is distinct from the OpenAPI Specification version or the API + openapi_version: str, default = "3.1.0" + The version of the OpenAPI Specification (which the document uses). + summary: str, optional + A short summary of what the application does. + description: str, optional + A verbose explanation of the application behavior. + tags: list[Tag, str], optional + A list of tags used by the specification with additional metadata. + servers: list[Server], optional + An array of Server Objects, which provide connectivity information to a target server. + terms_of_service: str, optional + A URL to the Terms of Service for the API. MUST be in the format of a URL. + contact: Contact, optional + The contact information for the exposed API. + license_info: License, optional + The license information for the exposed API. + security_schemes: dict[str, SecurityScheme]], optional + A declaration of the security schemes available to be used in the specification. + security: list[dict[str, list[str]]], optional + A declaration of which security mechanisms are applied globally across the API. + external_documentation: ExternalDocumentation, optional + Additional external documentation for the API. + openapi_extensions: Dict[str, Any], optional + Additional OpenAPI extensions as a dictionary. + + Returns + ------- + str + The OpenAPI schema as a JSON serializable dict. + """ + + from aws_lambda_powertools.event_handler.openapi.compat import model_json + + return model_json( + self.get_openapi_schema( + title=title, + version=version, + openapi_version=openapi_version, + summary=summary, + description=description, + tags=tags, + servers=servers, + terms_of_service=terms_of_service, + contact=contact, + license_info=license_info, + security_schemes=security_schemes, + security=security, + external_documentation=external_documentation, + openapi_extensions=openapi_extensions, + ), + by_alias=True, + exclude_none=True, + indent=2, + ) + + def configure_openapi( + self, + title: str = DEFAULT_OPENAPI_TITLE, + version: str = DEFAULT_API_VERSION, + openapi_version: str = DEFAULT_OPENAPI_VERSION, + summary: str | None = None, + description: str | None = None, + tags: list[Tag | str] | None = None, + servers: list[Server] | None = None, + terms_of_service: str | None = None, + contact: Contact | None = None, + license_info: License | None = None, + security_schemes: dict[str, SecurityScheme] | None = None, + security: list[dict[str, list[str]]] | None = None, + external_documentation: ExternalDocumentation | None = None, + openapi_extensions: dict[str, Any] | None = None, + ): + """Configure OpenAPI specification settings for the API. + + Sets up the OpenAPI documentation configuration that can be later used + when enabling Swagger UI or generating OpenAPI specifications. + + Parameters + ---------- + title: str + The title of the application. + version: str + The version of the OpenAPI document (which is distinct from the OpenAPI Specification version or the API + openapi_version: str, default = "3.1.0" + The version of the OpenAPI Specification (which the document uses). + summary: str, optional + A short summary of what the application does. + description: str, optional + A verbose explanation of the application behavior. + tags: list[Tag, str], optional + A list of tags used by the specification with additional metadata. + servers: list[Server], optional + An array of Server Objects, which provide connectivity information to a target server. + terms_of_service: str, optional + A URL to the Terms of Service for the API. MUST be in the format of a URL. + contact: Contact, optional + The contact information for the exposed API. + license_info: License, optional + The license information for the exposed API. + security_schemes: dict[str, SecurityScheme]], optional + A declaration of the security schemes available to be used in the specification. + security: list[dict[str, list[str]]], optional + A declaration of which security mechanisms are applied globally across the API. + external_documentation: ExternalDocumentation, optional + A link to external documentation for the API. + openapi_extensions: Dict[str, Any], optional + Additional OpenAPI extensions as a dictionary. + + Example + -------- + >>> api.configure_openapi( + ... title="My API", + ... version="1.0.0", + ... description="API for managing resources", + ... contact=Contact( + ... name="API Support", + ... email="support@example.com" + ... ) + ... ) + + See Also + -------- + enable_swagger : Method to enable Swagger UI using these configurations + OpenAPIConfig : Data class containing all OpenAPI configuration options + """ + self.openapi_config = OpenAPIConfig( + title=title, + version=version, + openapi_version=openapi_version, + summary=summary, + description=description, + tags=tags, + servers=servers, + terms_of_service=terms_of_service, + contact=contact, + license_info=license_info, + security_schemes=security_schemes, + security=security, + external_documentation=external_documentation, + openapi_extensions=openapi_extensions, + ) + + def configure_openapi_merge( + self, + path: str, + pattern: str | list[str] = "handler.py", + exclude: list[str] | None = None, + resolver_name: str = "app", + recursive: bool = False, + title: str = DEFAULT_OPENAPI_TITLE, + version: str = DEFAULT_API_VERSION, + openapi_version: str = DEFAULT_OPENAPI_VERSION, + summary: str | None = None, + description: str | None = None, + tags: list[Tag | str] | None = None, + servers: list[Server] | None = None, + terms_of_service: str | None = None, + contact: Contact | None = None, + license_info: License | None = None, + security_schemes: dict[str, SecurityScheme] | None = None, + security: list[dict[str, list[str]]] | None = None, + external_documentation: ExternalDocumentation | None = None, + openapi_extensions: dict[str, Any] | None = None, + on_conflict: Literal["warn", "error", "first", "last"] = "warn", + ): + """Configure OpenAPI merge to generate a unified schema from multiple Lambda handlers. + + This method discovers resolver instances across multiple Python files and merges + their OpenAPI schemas into a single unified specification. Useful for micro-function + architectures where each Lambda has its own resolver. + + Parameters + ---------- + path : str + Root directory path to search for resolver files. + pattern : str | list[str], optional + Glob pattern(s) to match handler files. Default is "handler.py". + exclude : list[str], optional + Patterns to exclude from search. Default excludes tests, __pycache__, and .venv. + resolver_name : str, optional + Name of the resolver variable in handler files. Default is "app". + recursive : bool, optional + Whether to search recursively in subdirectories. Default is False. + title : str + The title of the unified API. + version : str + The version of the OpenAPI document. + openapi_version : str, default = "3.1.0" + The version of the OpenAPI Specification. + summary : str, optional + A short summary of what the application does. + description : str, optional + A verbose explanation of the application behavior. + tags : list[Tag | str], optional + A list of tags used by the specification with additional metadata. + servers : list[Server], optional + An array of Server Objects for connectivity information. + terms_of_service : str, optional + A URL to the Terms of Service for the API. + contact : Contact, optional + The contact information for the exposed API. + license_info : License, optional + The license information for the exposed API. + security_schemes : dict[str, SecurityScheme], optional + Security schemes available in the specification. + security : list[dict[str, list[str]]], optional + Security mechanisms applied globally across the API. + external_documentation : ExternalDocumentation, optional + A link to external documentation for the API. + openapi_extensions : dict[str, Any], optional + Additional OpenAPI extensions as a dictionary. + on_conflict : str, optional + Strategy for handling conflicts when the same path+method is defined + in multiple schemas. Options: "warn" (default), "error", "first", "last". + + Example + ------- + >>> from aws_lambda_powertools.event_handler import APIGatewayRestResolver + >>> + >>> app = APIGatewayRestResolver() + >>> app.configure_openapi_merge( + ... path="./functions", + ... pattern="handler.py", + ... exclude=["**/tests/**"], + ... resolver_name="app", + ... title="My Unified API", + ... version="1.0.0", + ... ) + + See Also + -------- + configure_openapi : Configure OpenAPI for a single resolver + enable_swagger : Enable Swagger UI + """ + from aws_lambda_powertools.event_handler.openapi.merge import OpenAPIMerge + + if exclude is None: + exclude = ["**/tests/**", "**/__pycache__/**", "**/.venv/**"] + + self._openapi_merge = OpenAPIMerge( + title=title, + version=version, + openapi_version=openapi_version, + summary=summary, + description=description, + tags=tags, + servers=servers, + terms_of_service=terms_of_service, + contact=contact, + license_info=license_info, + security_schemes=security_schemes, + security=security, + external_documentation=external_documentation, + openapi_extensions=openapi_extensions, + on_conflict=on_conflict, + ) + self._openapi_merge.discover( + path=path, + pattern=pattern, + exclude=exclude, + resolver_name=resolver_name, + recursive=recursive, + ) + + def get_openapi_merge_schema(self) -> dict[str, Any]: + """Get the merged OpenAPI schema from multiple Lambda handlers. + + Returns + ------- + dict[str, Any] + The merged OpenAPI schema. + + Raises + ------ + RuntimeError + If configure_openapi_merge has not been called. + """ + if not hasattr(self, "_openapi_merge") or self._openapi_merge is None: + raise RuntimeError("configure_openapi_merge must be called before get_openapi_merge_schema") + return self._openapi_merge.get_openapi_schema() + + def get_openapi_merge_json_schema(self) -> str: + """Get the merged OpenAPI schema as JSON from multiple Lambda handlers. + + Returns + ------- + str + The merged OpenAPI schema as a JSON string. + + Raises + ------ + RuntimeError + If configure_openapi_merge has not been called. + """ + if not hasattr(self, "_openapi_merge") or self._openapi_merge is None: + raise RuntimeError("configure_openapi_merge must be called before get_openapi_merge_json_schema") + return self._openapi_merge.get_openapi_json_schema() + + def enable_swagger( + self, + *, + path: str = "/swagger", + title: str = DEFAULT_OPENAPI_TITLE, + version: str = DEFAULT_API_VERSION, + openapi_version: str = DEFAULT_OPENAPI_VERSION, + summary: str | None = None, + description: str | None = None, + tags: list[Tag | str] | None = None, + servers: list[Server] | None = None, + terms_of_service: str | None = None, + contact: Contact | None = None, + license_info: License | None = None, + swagger_base_url: str | None = None, + middlewares: list[Callable[..., Response]] | None = None, compress: bool = False, - cache_control: Optional[str] = None, + security_schemes: dict[str, SecurityScheme] | None = None, + security: list[dict[str, list[str]]] | None = None, + oauth2_config: OAuth2Config | None = None, + persist_authorization: bool = False, + external_documentation: ExternalDocumentation | None = None, + openapi_extensions: dict[str, Any] | None = None, ): + """ + Returns the OpenAPI schema as a JSON serializable dict + + Parameters + ---------- + path: str, default = "/swagger" + The path to the swagger UI. + title: str + The title of the application. + version: str + The version of the OpenAPI document (which is distinct from the OpenAPI Specification version or the API + openapi_version: str, default = "3.1.0" + The version of the OpenAPI Specification (which the document uses). + summary: str, optional + A short summary of what the application does. + description: str, optional + A verbose explanation of the application behavior. + tags: list[Tag, str], optional + A list of tags used by the specification with additional metadata. + servers: list[Server], optional + An array of Server Objects, which provide connectivity information to a target server. + terms_of_service: str, optional + A URL to the Terms of Service for the API. MUST be in the format of a URL. + contact: Contact, optional + The contact information for the exposed API. + license_info: License, optional + The license information for the exposed API. + swagger_base_url: str, optional + The base url for the swagger UI. If not provided, we will serve a recent version of the Swagger UI. + middlewares: list[Callable[..., Response]], optional + List of middlewares to be used for the swagger route. + compress: bool, default = False + Whether or not to enable gzip compression swagger route. + security_schemes: dict[str, "SecurityScheme"], optional + A declaration of the security schemes available to be used in the specification. + security: list[dict[str, list[str]]], optional + A declaration of which security mechanisms are applied globally across the API. + oauth2_config: OAuth2Config, optional + The OAuth2 configuration for the Swagger UI. + persist_authorization: bool, optional + Whether to persist authorization data on browser close/refresh. + external_documentation: ExternalDocumentation, optional + A link to external documentation for the API. + openapi_extensions: dict[str, Any], optional + Additional OpenAPI extensions as a dictionary. + """ + + from aws_lambda_powertools.event_handler.openapi.compat import model_json + from aws_lambda_powertools.event_handler.openapi.models import Server + from aws_lambda_powertools.event_handler.openapi.swagger_ui import ( + generate_oauth2_redirect_html, + generate_swagger_html, + ) + + @self.get(path, middlewares=middlewares, include_in_schema=False, compress=compress) + def swagger_handler(): + query_params = self.current_event.query_string_parameters or {} + + # Check for query parameters; if "format" is specified as "oauth2-redirect", + # send the oauth2-redirect HTML stanza so OAuth2 can be used + # Source: https://github.com/swagger-api/swagger-ui/blob/master/dist/oauth2-redirect.html + if query_params.get("format") == "oauth2-redirect": + return Response( + status_code=200, + content_type="text/html", + body=generate_oauth2_redirect_html(), + ) + + base_path = self._get_base_path() + + if swagger_base_url: + swagger_js = f"{swagger_base_url}/swagger-ui-bundle.min.js" + swagger_css = f"{swagger_base_url}/swagger-ui.min.css" + else: + # 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() + + openapi_servers = servers or [Server(url=(base_path or "/"))] + + # Use merged schema if configure_openapi_merge was called, otherwise use regular schema + if hasattr(self, "_openapi_merge") and self._openapi_merge is not None: + # Get merged schema as JSON string (already properly serialized) + escaped_spec = self._openapi_merge.get_openapi_json_schema().replace(" or similar tags. Escaping the forward slash in HTTPStatus | None: + if custom_response_validation_http_code and not self._enable_validation: + msg = ( + "'custom_response_validation_http_code' cannot be set for route when enable_validation is False " + "on resolver." + ) + raise ValueError(msg) + + if ( + not isinstance(custom_response_validation_http_code, HTTPStatus) + and custom_response_validation_http_code is not None + ): + try: + custom_response_validation_http_code = HTTPStatus(custom_response_validation_http_code) + except ValueError: + msg = f"'{custom_response_validation_http_code}' must be an integer representing an HTTP status code or an enum of type HTTPStatus." # noqa: E501 + raise ValueError(msg) from None + + return custom_response_validation_http_code + + def route( + self, + rule: str, + method: str | list[str] | tuple[str], + cors: bool | None = None, + compress: bool = False, + cache_control: str | None = None, + summary: str | None = None, + description: str | None = None, + responses: dict[int, OpenAPIResponse] | None = None, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + tags: list[str] | None = None, + operation_id: str | None = None, + include_in_schema: bool = True, + security: list[dict[str, list[str]]] | None = None, + openapi_extensions: dict[str, Any] | None = None, + 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`""" - def register_resolver(func: Callable): + custom_response_validation_http_code = self._validate_route_response_validation_error_http_code( + custom_response_validation_http_code, + ) + + def register_resolver(func: AnyCallableT) -> AnyCallableT: methods = (method,) if isinstance(method, str) else method - logger.debug(f"Adding route using rule {rule} and methods: {','.join((m.upper() for m in methods))}") - if cors is None: - cors_enabled = self._cors_enabled - else: - cors_enabled = cors + logger.debug(f"Adding route using rule {rule} and methods: {','.join(m.upper() for m in methods)}") + + cors_enabled = self._cors_enabled if cors is None else cors for item in methods: - _route = Route(item, self._compile_regex(rule), func, cors_enabled, compress, cache_control) + _route = Route( + item, + rule, + self._compile_regex(rule), + func, + cors_enabled, + compress, + cache_control, + summary, + description, + responses, + response_description, + tags, + operation_id, + include_in_schema, + security, + openapi_extensions, + deprecated, + enable_validation, + custom_response_validation_http_code, + status_code, + middlewares, + ) # The more specific route wins. # We store dynamic (/studies/{studyid}) and static routes (/studies/fetch) separately. @@ -583,26 +2484,52 @@ def register_resolver(func: Callable): else: self._static_routes.append(_route) - route_key = item + rule - if route_key in self._route_keys: - warnings.warn( - f"A route like this was already registered. method: '{item}' rule: '{rule}'", - stacklevel=2, - ) - self._route_keys.append(route_key) + self._create_route_key(item, rule) + if cors_enabled: logger.debug(f"Registering method {item.upper()} to Allow Methods in CORS") self._cors_methods.add(item.upper()) + return func return register_resolver - def resolve(self, event, context) -> Dict[str, Any]: + def resolve(self, event: Mapping[str, Any], context: LambdaContext) -> dict[str, Any]: """Resolves the response based on the provide event and decorator routes + ## Internals + + Request processing chain is triggered by a Route object being called _(`_call_route` -> `__call__`)_: + + 1. **When a route is matched** + 1.1. Exception handlers _(if any exception bubbled up and caught)_ + 1.2. Global middlewares _(before, and after on the way back)_ + 1.3. Path level middleware _(before, and after on the way back)_ + 1.4. Middleware adapter to ensure Response is homogenous (_registered_api_adapter) + 1.5. Run actual route + 2. **When a route is NOT matched** + 2.1. Exception handlers _(if any exception bubbled up and caught)_ + 2.2. Global middlewares _(before, and after on the way back)_ + 2.3. Path level middleware _(before, and after on the way back)_ + 2.4. Middleware adapter to ensure Response is homogenous (_registered_api_adapter) + 2.5. Run 404 route handler + 3. **When a route is a pre-flight CORS (often not matched)** + 3.1. Exception handlers _(if any exception bubbled up and caught)_ + 3.2. Global middlewares _(before, and after on the way back)_ + 3.3. Path level middleware _(before, and after on the way back)_ + 3.4. Middleware adapter to ensure Response is homogenous (_registered_api_adapter) + 3.5. Return 204 with appropriate CORS headers + 4. **When a route is matched with Data Validation enabled** + 4.1. Exception handlers _(if any exception bubbled up and caught)_ + 4.2. Data Validation middleware _(before, and after on the way back)_ + 4.3. Global middlewares _(before, and after on the way back)_ + 4.4. Path level middleware _(before, and after on the way back)_ + 4.5. Middleware adapter to ensure Response is homogenous (_registered_api_adapter) + 4.6. Run actual route + Parameters ---------- - event: Dict[str, Any] + event: dict[str, Any] Event context: LambdaContext Lambda context @@ -620,26 +2547,192 @@ def resolve(self, event, context) -> Dict[str, Any]: event = event.raw_event if self._debug: - print(self._json_dump(event)) + print(self._serializer(cast(dict, event))) # Populate router(s) dependencies without keeping a reference to each registered router - BaseRouter.current_event = self._to_proxy_event(event) + BaseRouter.current_event = self._to_proxy_event(cast(dict, event)) BaseRouter.lambda_context = context response = self._resolve().build(self.current_event, self._cors) + + # Debug print Processed Middlewares + 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, 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) + def _create_route_key(self, item: str, rule: str): + route_key = item + rule + if route_key in self._route_keys: + warnings.warn( + f"A route like this was already registered. method: '{item}' rule: '{rule}'", + stacklevel=2, + ) + self._route_keys.append(route_key) + + def _get_base_path(self) -> str: + raise NotImplementedError() + @staticmethod - def _has_debug(debug: Optional[bool] = None) -> bool: + def _has_debug(debug: bool | None = None) -> bool: # It might have been explicitly switched off (debug=False) - if debug is not None: - return debug - - return powertools_dev_is_set() + return debug if debug is not None else powertools_dev_is_set() @staticmethod def _compile_regex(rule: str, base_regex: str = _ROUTE_REGEX): @@ -674,38 +2767,32 @@ 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: + 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) - if self._proxy_type == ProxyEventType.APIGatewayProxyEventV2: - logger.debug("Converting event to API Gateway HTTP API contract") - return APIGatewayProxyEventV2(event) - if self._proxy_type == ProxyEventType.LambdaFunctionUrlEvent: - logger.debug("Converting event to Lambda Function URL contract") - return LambdaFunctionUrlEvent(event) - if self._proxy_type == ProxyEventType.VPCLatticeEvent: - logger.debug("Converting event to VPC Lattice contract") - return VPCLatticeEvent(event) - logger.debug("Converting event to ALB contract") - return ALBEvent(event) + 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""" method = self.current_event.http_method.upper() path = self._remove_prefix(self.current_event.path) - for route in self._static_routes + self._dynamic_routes: + registered_routes = self._static_routes + self._dynamic_routes + + for route in registered_routes: if method != route.method: continue - match_results: Optional[Match] = route.rule.match(path) + match_results: Match | None = route.rule.match(path) if match_results: logger.debug("Found a registered route. Calling function") - return self._call_route(route, match_results.groupdict()) # pass fn args + # Add matched Route reference into the Resolver context + self.append_context(_route=route, _path=path) - logger.debug(f"No match found for path {path} and method {method}") - return self._not_found(method) + route_keys = self._convert_matches_into_route_keys(match_results) + return self._call_route(route, route_keys) # pass fn args + + return self._handle_not_found(method=method, path=path) def _remove_prefix(self, path: str) -> str: """Remove the configured prefix from the path""" @@ -713,142 +2800,231 @@ def _remove_prefix(self, path: str) -> str: return path for prefix in self._strip_prefixes: - if path == prefix: - return "/" - if self._path_starts_with(path, prefix): - return path[len(prefix) :] + if isinstance(prefix, str): + if path == prefix: + return "/" + + if self._path_starts_with(path, prefix): + return path[len(prefix) :] + + if isinstance(prefix, Pattern): + path = re.sub(prefix, "", path) + + # When using regexes, we might get into a point where everything is removed + # from the string, so we check if it's empty and return /, since there's nothing + # else to strip anymore. + if not path: + return "/" return path + def _convert_matches_into_route_keys(self, match: Match) -> dict[str, str]: + """Converts the regex match into a dict of route keys""" + return match.groupdict() + @staticmethod def _path_starts_with(path: str, prefix: str): """Returns true if the `path` starts with a prefix plus a `/`""" if not isinstance(prefix, str) or prefix == "": return False - return path.startswith(prefix + "/") + return path.startswith(f"{prefix}/") - def _not_found(self, method: str) -> ResponseBuilder: + def _handle_not_found(self, method: str, path: str) -> ResponseBuilder: """Called when no matching route was found and includes support for the cors preflight response""" - headers: Dict[str, Union[str, List[str]]] = {} - if self._cors: - logger.debug("CORS is enabled, updating headers.") - headers.update(self._cors.to_dict(self.current_event.get_header_value("Origin"))) + logger.debug(f"No match found for path {path} and method {method}") - if method == "OPTIONS": - logger.debug("Pre-flight request detected. Returning CORS with null response") - headers["Access-Control-Allow-Methods"] = ",".join(sorted(self._cors_methods)) - return ResponseBuilder(Response(status_code=204, content_type=None, headers=headers, body="")) + def not_found_handler(): + """Route handler for 404s - handler = self._lookup_exception_handler(NotFoundError) - if handler: - return ResponseBuilder(handler(NotFoundError())) + It handles in the following order: + + 1. Pre-flight CORS requests (OPTIONS) + 2. Detects and calls custom HTTP 404 handler + 3. Returns standard 404 along with CORS headers + + Returns + ------- + Response + HTTP 404 response + """ + _headers: dict[str, Any] = {} + + # Pre-flight request? Return immediately to avoid browser error + 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="") + + # Customer registered 404 route? Call it. + custom_not_found_handler = self.exception_handler_manager.lookup_exception_handler(NotFoundError) + if custom_not_found_handler: + return custom_not_found_handler(NotFoundError()) - return ResponseBuilder( - Response( + # No CORS and no custom 404 fn? Default response + return Response( status_code=HTTPStatus.NOT_FOUND.value, content_type=content_types.APPLICATION_JSON, - headers=headers, - body=self._json_dump({"statusCode": HTTPStatus.NOT_FOUND.value, "message": "Not found"}), - ), + headers=_headers, + body={"statusCode": HTTPStatus.NOT_FOUND.value, "message": "Not found"}, + ) + + # We create a route to trigger entire request chain (middleware+exception handlers) + route = Route( + rule=self._compile_regex(r".*"), + method=method, + path=path, + func=not_found_handler, + cors=self._cors_enabled, + compress=False, ) - def _call_route(self, route: Route, args: Dict[str, str]) -> ResponseBuilder: + # Add matched Route reference into the Resolver context + self.append_context(_route=route, _path=path) + + # Kick-off request chain: + # -> exception_handlers() + # --> middlewares() + # ---> not_found_route() + return self._call_route(route=route, route_arguments={}) + + def _call_route(self, route: Route, route_arguments: dict[str, str]) -> ResponseBuilder: """Actually call the matching route with any provided keyword arguments.""" try: - return ResponseBuilder(self._to_response(route.func(**args)), route) + # Reset Processed stack for Middleware (for debugging purposes) + self._reset_processed_stack() + + return self._response_builder_class( + response=self._to_response( # type: ignore[arg-type] + route(router_middlewares=self._router_middlewares, app=self, route_arguments=route_arguments), + ), + serializer=self._serializer, + route=route, + ) except Exception as exc: + # If exception is handled then return the response builder to reduce noise response_builder = self._call_exception_handler(exc, route) if response_builder: return response_builder + logger.exception(exc) if self._debug: # If the user has turned on debug mode, - # we'll let the original exception propagate so + # we'll let the original exception propagate, so # they get more information about what went wrong. - return ResponseBuilder( - Response( + return self._response_builder_class( + response=Response( status_code=500, content_type=content_types.TEXT_PLAIN, body="".join(traceback.format_exc()), ), - route, + serializer=self._serializer, + route=route, ) raise - def not_found(self, func: Optional[Callable] = None): + def not_found(self, func: Callable | None = None): if func is None: return self.exception_handler(NotFoundError) return self.exception_handler(NotFoundError)(func) - def exception_handler(self, exc_class: Union[Type[Exception], List[Type[Exception]]]): - def register_exception_handler(func: Callable): - if isinstance(exc_class, list): - for exp in exc_class: - self._exception_handlers[exp] = func - else: - self._exception_handlers[exc_class] = func - return func - - return register_exception_handler - - def _lookup_exception_handler(self, exp_type: Type) -> Optional[Callable]: - # Use "Method Resolution Order" to allow for matching against a base class - # of an exception - for cls in exp_type.__mro__: - if cls in self._exception_handlers: - return self._exception_handlers[cls] - return None + def exception_handler(self, exc_class: type[Exception] | list[type[Exception]]): + return self.exception_handler_manager.exception_handler(exc_class=exc_class) - def _call_exception_handler(self, exp: Exception, route: Route) -> Optional[ResponseBuilder]: - handler = self._lookup_exception_handler(type(exp)) + def _call_exception_handler(self, exp: Exception, route: Route) -> ResponseBuilder | None: + handler = self.exception_handler_manager.lookup_exception_handler(type(exp)) if handler: try: - return ResponseBuilder(handler(exp), route) + return self._response_builder_class(response=handler(exp), serializer=self._serializer, route=route) except ServiceError as service_error: exp = service_error + if isinstance(exp, RequestValidationError): + # For security reasons, we hide msg details (don't leak Python, Pydantic or file names) + errors = [{"loc": e["loc"], "type": e["type"]} for e in exp.errors()] + + return self._response_builder_class( + response=Response( + status_code=HTTPStatus.UNPROCESSABLE_ENTITY, + content_type=content_types.APPLICATION_JSON, + body={"statusCode": HTTPStatus.UNPROCESSABLE_ENTITY, "detail": errors}, + ), + serializer=self._serializer, + route=route, + ) + + # OpenAPIResponseValidationMiddleware will only raise ResponseValidationError when + # 'self._response_validation_error_http_code' is not None or + # when route has custom_response_validation_http_code + if isinstance(exp, ResponseValidationError): + # route validation must take precedence over app validation + http_code = route.custom_response_validation_http_code or self._response_validation_error_http_code + errors = [{"loc": e["loc"], "type": e["type"]} for e in exp.errors()] + return self._response_builder_class( + response=Response( + status_code=http_code.value, + content_type=content_types.APPLICATION_JSON, + body={"statusCode": http_code, "detail": errors}, + ), + serializer=self._serializer, + route=route, + ) + + if isinstance(exp, RequestUnsupportedContentType): + errors = [{"loc": e["loc"], "type": e["type"]} for e in exp.errors()] + return self._response_builder_class( + response=Response( + status_code=HTTPStatus.UNSUPPORTED_MEDIA_TYPE, + content_type=content_types.APPLICATION_JSON, + body={"statusCode": HTTPStatus.UNSUPPORTED_MEDIA_TYPE, "detail": errors}, + ), + serializer=self._serializer, + route=route, + ) + if isinstance(exp, ServiceError): - return ResponseBuilder( - Response( + return self._response_builder_class( + response=Response( status_code=exp.status_code, content_type=content_types.APPLICATION_JSON, - body=self._json_dump({"statusCode": exp.status_code, "message": exp.msg}), + body={"statusCode": exp.status_code, "message": exp.msg}, ), - route, + serializer=self._serializer, + route=route, ) return None - def _to_response(self, result: Union[Dict, Tuple, Response]) -> Response: + def _to_response(self, result: dict | tuple | Response | BedrockResponse) -> Response | BedrockResponse: """Convert the route's result to a Response 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 + - 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 + - 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): + 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( status_code=status_code, content_type=content_types.APPLICATION_JSON, - body=self._json_dump(result), + body=result, ) - def _json_dump(self, obj: Any) -> str: - return self._serializer(obj) - - def include_router(self, router: "Router", prefix: Optional[str] = None) -> None: + def include_router(self, router: Router, prefix: str | None = None) -> None: """Adds all routes and context defined in a router Parameters @@ -862,11 +3038,19 @@ def include_router(self, router: "Router", prefix: Optional[str] = None) -> None # Add reference to parent ApiGatewayResolver to support use cases where people subclass it to add custom logic router.api_resolver = self - # Merge app and router context + logger.debug("Merging App context with Router context") self.context.update(**router.context) + + logger.debug("Appending Router middlewares into App middlewares.") + self._router_middlewares = self._router_middlewares + router._router_middlewares + + logger.debug("Appending Router exception_handler into App exception_handler.") + self.exception_handler_manager.update_exception_handlers(router._exception_handlers) + # use pointer to allow context clearance after event is processed e.g., resolve(evt, ctx) router.context = self.context + # Iterate through the routes defined in the router to configure and apply middlewares for each route for route, func in router._routes.items(): new_route = route @@ -875,58 +3059,205 @@ def include_router(self, router: "Router", prefix: Optional[str] = None) -> None rule = prefix if rule == "/" else f"{prefix}{rule}" new_route = (rule, *route[1:]) - self.route(*new_route)(func) + # Middlewares are stored by route separately - must grab them to include + # Middleware store the route without prefix, so we must not include prefix when grabbing + middlewares = router._routes_with_middleware.get(route) + + # Need to use "type: ignore" here since mypy does not like a named parameter after + # tuple expansion since may cause duplicate named parameters in the function signature. + # In this case this is not possible since the tuple expansion is from a hashable source + # and the `middlewares` list is a non-hashable structure so will never be included. + # Still need to ignore for mypy checks or will cause failures (false-positive) + self.route(*new_route, middlewares=middlewares)(func) # type: ignore + + @staticmethod + def _get_fields_from_routes(routes: Sequence[Route]) -> list[ModelField]: + """ + Returns a list of fields from the routes + """ + + from aws_lambda_powertools.event_handler.openapi.compat import ModelField + from aws_lambda_powertools.event_handler.openapi.dependant import ( + get_flat_params, + ) + + body_fields_from_routes: list[ModelField] = [] + responses_from_routes: list[ModelField] = [] + request_fields_from_routes: list[ModelField] = [] + + for route in routes: + if route.body_field: + if not isinstance(route.body_field, ModelField): + raise AssertionError("A request body must be a Pydantic Field") + body_fields_from_routes.append(route.body_field) + + params = get_flat_params(route.dependant) + request_fields_from_routes.extend(params) + + if route.dependant.return_param: + responses_from_routes.append(route.dependant.return_param) + + if route.dependant.response_extra_models: + responses_from_routes.extend(route.dependant.response_extra_models) + + return list( + responses_from_routes + request_fields_from_routes + body_fields_from_routes, + ) class Router(BaseRouter): """Router helper class to allow splitting ApiGatewayResolver into multiple files""" def __init__(self): - self._routes: Dict[tuple, Callable] = {} - self.api_resolver: Optional[BaseRouter] = None + self._routes: dict[tuple, Callable] = {} + self._routes_with_middleware: dict[tuple, list[Callable]] = {} + self.api_resolver: BaseRouter | None = None self.context = {} # early init as customers might add context before event resolution + self._exception_handlers: dict[type, Callable] = {} def route( self, rule: str, - method: Union[str, Union[List[str], Tuple[str]]], - cors: Optional[bool] = None, + method: str | list[str] | tuple[str], + cors: bool | None = None, compress: bool = False, - cache_control: Optional[str] = None, - ): - def register_route(func: Callable): - # Convert methods to tuple. It needs to be hashable as its part of the self._routes dict key + cache_control: str | None = None, + summary: str | None = None, + description: str | None = None, + responses: dict[int, OpenAPIResponse] | None = None, + response_description: str | None = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + tags: list[str] | None = None, + operation_id: str | None = None, + include_in_schema: bool = True, + security: list[dict[str, list[str]]] | None = None, + openapi_extensions: dict[str, Any] | None = None, + 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: + # All dict keys needs to be hashable. So we'll need to do some conversions: methods = (method,) if isinstance(method, str) else tuple(method) - self._routes[(rule, methods, cors, compress, cache_control)] = func + frozen_responses = _FrozenDict(responses) if responses else None + frozen_tags = frozenset(tags) if tags else None + frozen_security = _FrozenListDict(security) if security else None + frozen_openapi_extensions = _FrozenDict(openapi_extensions) if openapi_extensions else None + + route_key = ( + rule, + methods, + cors, + compress, + cache_control, + summary, + description, + frozen_responses, + response_description, + frozen_tags, + operation_id, + include_in_schema, + frozen_security, + frozen_openapi_extensions, + deprecated, + enable_validation, + custom_response_validation_http_code, + status_code, + ) + + # Collate Middleware for routes + if middlewares is not None: + for handler in middlewares: + if self._routes_with_middleware.get(route_key) is None: + self._routes_with_middleware[route_key] = [handler] + else: + self._routes_with_middleware[route_key].append(handler) + else: + self._routes_with_middleware[route_key] = [] + + self._routes[route_key] = func + return func return register_route + def exception_handler(self, exc_class: type[Exception] | list[type[Exception]]): + def register_exception_handler(func: Callable): + if isinstance(exc_class, list): + for exp in exc_class: + self._exception_handlers[exp] = func + else: + self._exception_handlers[exc_class] = func + return func + + return register_exception_handler + class APIGatewayRestResolver(ApiGatewayResolver): - current_event: APIGatewayProxyEvent + """Amazon API Gateway REST and HTTP API v1 payload resolver""" - def __init__( - self, - cors: Optional[CORSConfig] = None, - debug: Optional[bool] = None, - serializer: Optional[Callable[[Dict], str]] = None, - strip_prefixes: Optional[List[str]] = None, - ): - """Amazon API Gateway REST and HTTP API v1 payload resolver""" - super().__init__(ProxyEventType.APIGatewayProxyEvent, cors, debug, serializer, strip_prefixes) + current_event: APIGatewayProxyEvent + _proxy_event_type = ProxyEventType.APIGatewayProxyEvent + + def _get_base_path(self) -> str: + # 3 different scenarios: + # + # 1. SAM local: even though a stage variable is sent to the Lambda function, it's not used in the path + # 2. API Gateway REST API: stage variable is used in the path + # 3. API Gateway REST Custom Domain: stage variable is not used in the path + # + # To solve the 3 scenarios, we try to match the beginning of the path with the stage variable + stage = self.current_event.request_context.stage + if stage and stage != "$default" and self.current_event.request_context.path.startswith(f"/{stage}"): + return f"/{stage}" + return "" # override route to ignore trailing "/" in routes for REST API def route( self, rule: str, - method: Union[str, Union[List[str], Tuple[str]]], - cors: Optional[bool] = None, + method: str | list[str] | tuple[str], + cors: bool | None = None, compress: bool = False, - cache_control: Optional[str] = None, - ): + cache_control: str | None = None, + summary: str | None = None, + description: str | None = None, + responses: dict[int, OpenAPIResponse] | None = None, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + tags: list[str] | None = None, + operation_id: str | None = None, + include_in_schema: bool = True, + security: list[dict[str, list[str]]] | None = None, + openapi_extensions: dict[str, Any] | None = None, + 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. - return super().route(rule.rstrip("/"), method, cors, compress, cache_control) + return super().route( + rule.rstrip("/"), + method, + cors, + compress, + cache_control, + summary, + description, + responses, + response_description, + tags, + operation_id, + include_in_schema, + security, + openapi_extensions, + deprecated, + enable_validation, + custom_response_validation_http_code, + status_code, + middlewares, + ) # Override _compile_regex to exclude trailing slashes for route resolution @staticmethod @@ -935,28 +3266,99 @@ def _compile_regex(rule: str, base_regex: str = _ROUTE_REGEX): class APIGatewayHttpResolver(ApiGatewayResolver): - current_event: APIGatewayProxyEventV2 + """Amazon API Gateway HTTP API v2 payload resolver""" - def __init__( - self, - cors: Optional[CORSConfig] = None, - debug: Optional[bool] = None, - serializer: Optional[Callable[[Dict], str]] = None, - strip_prefixes: Optional[List[str]] = None, - ): - """Amazon API Gateway HTTP API v2 payload resolver""" - super().__init__(ProxyEventType.APIGatewayProxyEventV2, cors, debug, serializer, strip_prefixes) + current_event: APIGatewayProxyEventV2 + _proxy_event_type = ProxyEventType.APIGatewayProxyEventV2 + + def _get_base_path(self) -> str: + # 3 different scenarios: + # + # 1. SAM local: even though a stage variable is sent to the Lambda function, it's not used in the path + # 2. API Gateway HTTP API: stage variable is used in the path + # 3. API Gateway HTTP Custom Domain: stage variable is not used in the path + # + # To solve the 3 scenarios, we try to match the beginning of the path with the stage variable + stage = self.current_event.request_context.stage + if stage and stage != "$default" and self.current_event.request_context.http.path.startswith(f"/{stage}"): + return f"/{stage}" + return "" class ALBResolver(ApiGatewayResolver): + """Amazon Application Load Balancer (ALB) resolver""" + current_event: ALBEvent + _proxy_event_type = ProxyEventType.ALBEvent def __init__( self, - cors: Optional[CORSConfig] = None, - debug: Optional[bool] = None, - serializer: Optional[Callable[[Dict], str]] = None, - strip_prefixes: Optional[List[str]] = None, + 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, + decode_query_parameters: bool = False, ): - """Amazon Application Load Balancer (ALB) resolver""" - super().__init__(ProxyEventType.ALBEvent, cors, debug, serializer, strip_prefixes) + """Amazon Application Load Balancer (ALB) resolver + + + Parameters + ---------- + cors: CORSConfig + Optionally configure and enabled CORS. Not each route will need to have to cors=True + debug: bool | None + Enables debug mode, by default False. Can be also be enabled by "POWERTOOLS_DEV" + environment variable + serializer: Callable, optional + function to serialize `obj` to a JSON formatted `str`, by default json.dumps + strip_prefixes: list[str | Pattern], optional + optional list of prefixes to be removed from the request path before doing the routing. + This is often used with api gateways with multiple custom mappings. + Each prefix can be a static string or a compiled regex pattern + enable_validation: bool | None + Enables validation of the request body against the route schema, by default False. + response_validation_error_http_code + Sets the returned status code if response is not validated. enable_validation must be True. + json_body_deserializer: Callable[[str], dict], optional + function to deserialize `str`, `bytes`, `bytearray` containing a JSON document to a Python `dict`, + by default json.loads when integrating with EventSource data class + decode_query_parameters: bool | None + Enables URL-decoding of query parameters (both keys and values), by default False. + """ + super().__init__( + 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 + + def _get_base_path(self) -> str: + # ALB doesn't have a stage variable, so we just return an empty string + return "" + + @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 + """ + # 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 = "" + + return super()._to_response(result) + + @override + def _to_proxy_event(self, event: dict) -> BaseProxyEvent: + proxy_event = super()._to_proxy_event(event) + if isinstance(proxy_event, ALBEvent): + proxy_event.decode_query_parameters = self.decode_query_parameters + return proxy_event diff --git a/aws_lambda_powertools/event_handler/appsync.py b/aws_lambda_powertools/event_handler/appsync.py index fba5681ef6a..29c48d71cb1 100644 --- a/aws_lambda_powertools/event_handler/appsync.py +++ b/aws_lambda_powertools/event_handler/appsync.py @@ -1,96 +1,85 @@ +from __future__ import annotations + +import asyncio import logging -from typing import Any, Callable, Optional, Type, TypeVar +import warnings +from typing import TYPE_CHECKING, Any +from aws_lambda_powertools.event_handler.exception_handling import ExceptionHandlerManager +from aws_lambda_powertools.event_handler.graphql_appsync.exceptions import InvalidBatchResponse, ResolverNotFoundError +from aws_lambda_powertools.event_handler.graphql_appsync.router import Router from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent -from aws_lambda_powertools.utilities.typing import LambdaContext - -logger = logging.getLogger(__name__) -AppSyncResolverEventT = TypeVar("AppSyncResolverEventT", bound=AppSyncResolverEvent) +if TYPE_CHECKING: + from collections.abc import Callable + from aws_lambda_powertools.utilities.typing import LambdaContext -class BaseRouter: - current_event: AppSyncResolverEventT # type: ignore[valid-type] - lambda_context: LambdaContext - context: dict - - def __init__(self): - self._resolvers: dict = {} +from aws_lambda_powertools.warnings import PowertoolsUserWarning - def resolver(self, type_name: str = "*", field_name: Optional[str] = None): - """Registers the resolver for field_name - - Parameters - ---------- - type_name : str - Type name - field_name : str - Field name - """ - - def register_resolver(func): - logger.debug(f"Adding resolver `{func.__name__}` for field `{type_name}.{field_name}`") - self._resolvers[f"{type_name}.{field_name}"] = {"func": func} - return func - - return register_resolver - - def append_context(self, **additional_context): - """Append key=value data as routing context""" - self.context.update(**additional_context) - - def clear_context(self): - """Resets routing context""" - self.context.clear() +logger = logging.getLogger(__name__) -class AppSyncResolver(BaseRouter): +class AppSyncResolver(Router): """ - AppSync resolver decorator + AppSync GraphQL API Resolver Example ------- - - **Sample usage** - - from aws_lambda_powertools.event_handler import AppSyncResolver - - app = AppSyncResolver() - - @app.resolver(type_name="Query", field_name="listLocations") - def list_locations(page: int = 0, size: int = 10) -> list: - # Your logic to fetch locations with arguments passed in - return [{"id": 100, "name": "Smooth Grooves"}] - - @app.resolver(type_name="Merchant", field_name="extraInfo") - def get_extra_info() -> dict: - # Can use "app.current_event.source" to filter within the parent context - account_type = app.current_event.source["accountType"] - method = "BTC" if account_type == "NEW" else "USD" - return {"preferredPaymentMethod": method} - - @app.resolver(field_name="commonField") - def common_field() -> str: - # Would match all fieldNames matching 'commonField' - return str(uuid.uuid4()) + ```python + from aws_lambda_powertools.event_handler import AppSyncResolver + + app = AppSyncResolver() + + @app.resolver(type_name="Query", field_name="listLocations") + def list_locations(page: int = 0, size: int = 10) -> list: + # Your logic to fetch locations with arguments passed in + return [{"id": 100, "name": "Smooth Grooves"}] + + @app.resolver(type_name="Merchant", field_name="extraInfo") + def get_extra_info() -> dict: + # Can use "app.current_event.source" to filter within the parent context + account_type = app.current_event.source["accountType"] + method = "BTC" if account_type == "NEW" else "USD" + return {"preferredPaymentMethod": method} + + @app.resolver(field_name="commonField") + def common_field() -> str: + # Would match all fieldNames matching 'commonField' + return str(uuid.uuid4()) + ``` """ def __init__(self): + """ + Initialize a new instance of the AppSyncResolver. + """ super().__init__() self.context = {} # early init as customers might add context before event resolution + self.exception_handler_manager = ExceptionHandlerManager() + self._exception_handlers: dict[type, Callable] = {} - def resolve( + def __call__( self, event: dict, context: LambdaContext, - data_model: Type[AppSyncResolverEvent] = AppSyncResolverEvent, + data_model: type[AppSyncResolverEvent] = AppSyncResolverEvent, ) -> Any: - """Resolve field_name + """Implicit lambda handler which internally calls `resolve`""" + return self.resolve(event, context, data_model) + + def resolve( + self, + event: dict | list[dict], + context: LambdaContext, + data_model: type[AppSyncResolverEvent] = AppSyncResolverEvent, + ) -> Any: + """Resolves the response based on the provide event and decorator routes Parameters ---------- - event : dict - Lambda event + event : dict | list[Dict] + Lambda event either coming from batch processing endpoint or from standard processing endpoint context : LambdaContext Lambda context data_model: @@ -127,7 +116,7 @@ def handler(event, context: LambdaContext): class MyCustomModel(AppSyncResolverEvent): @property def country_viewer(self) -> str: - return self.request_headers.get("cloudfront-viewer-country") + return self.request_headers.get("cloudfront-viewer-country", "") @app.resolver(field_name="listLocations") @@ -154,47 +143,228 @@ def lambda_handler(event, context): ValueError If we could not find a field resolver """ - # Maintenance: revisit generics/overload to fix [attr-defined] in mypy usage - BaseRouter.current_event = data_model(event) - BaseRouter.lambda_context = context - resolver = self._get_resolver(BaseRouter.current_event.type_name, BaseRouter.current_event.field_name) - response = resolver(**BaseRouter.current_event.arguments) - self.clear_context() + self.lambda_context = context + Router.lambda_context = context + + try: + if isinstance(event, list): + Router.current_batch_event = [data_model(e) for e in event] + response = self._call_batch_resolver(event=event, data_model=data_model) + else: + Router.current_event = data_model(event) + response = self._call_single_resolver(event=event, data_model=data_model) + except Exception as exp: + response_builder = self.exception_handler_manager.lookup_exception_handler(type(exp)) + if response_builder: + return response_builder(exp) + raise + + # We don't clear the context for coroutines because we don't have control over the event loop. + # If we clean the context immediately, it might not be available when the coroutine is actually executed. + # For single async operations, the context should be cleaned up manually after the coroutine completes. + # See: https://github.com/aws-powertools/powertools-lambda-python/issues/5290 + # REVIEW: Review this support in Powertools V4 + if not asyncio.iscoroutine(response): + self.clear_context() return response - def _get_resolver(self, type_name: str, field_name: str) -> Callable: - """Get resolver for field_name + def _call_single_resolver(self, event: dict, data_model: type[AppSyncResolverEvent]) -> Any: + """Call single event resolver Parameters ---------- - type_name : str - Type name - field_name : str - Field name + event : dict + Event + data_model : type[AppSyncResolverEvent] + Data_model to decode AppSync event, by default it is of AppSyncResolverEvent type or subclass of it + """ + + logger.debug("Processing direct resolver event") + + self.current_event = data_model(event) + resolver = self._resolver_registry.find_resolver(self.current_event.type_name, self.current_event.field_name) + if not resolver: + raise ValueError(f"No resolver found for '{self.current_event.type_name}.{self.current_event.field_name}'") + return resolver["func"](**self.current_event.arguments) + + def _call_sync_batch_resolver( + self, + resolver: Callable, + raise_on_error: bool = False, + aggregate: bool = True, + ) -> list[Any]: + """ + Calls a synchronous batch resolver function for each event in the current batch. + + Parameters + ---------- + resolver: Callable + The callable function to resolve events. + raise_on_error: bool + A flag indicating whether to raise an error when processing batches + with failed items. Defaults to False, which means errors are handled without raising exceptions. + aggregate: bool + A flag indicating whether the batch items should be processed at once or individually. + If True (default), the batch resolver will process all items in the batch as a single event. + If False, the batch resolver will process each item in the batch individually. Returns ------- - Callable - callable function and configuration + list[Any] + A list of results corresponding to the resolved events. """ - full_name = f"{type_name}.{field_name}" - resolver = self._resolvers.get(full_name, self._resolvers.get(f"*.{field_name}")) - if not resolver: - raise ValueError(f"No resolver found for '{full_name}'") - return resolver["func"] - def __call__( + logger.debug(f"Graceful error handling flag {raise_on_error=}") + + # Checks whether the entire batch should be processed at once + if aggregate: + # Process the entire batch + response = resolver(event=self.current_batch_event) + + if not isinstance(response, list): + raise InvalidBatchResponse("The response must be a List when using batch resolvers") + + return response + + # Non aggregated events, so we call this event list x times + # Stop on first exception we encounter + if raise_on_error: + return [ + resolver(event=appconfig_event, **appconfig_event.arguments) + for appconfig_event in self.current_batch_event + ] + + # By default, we gracefully append `None` for any records that failed processing + results = [] + for idx, event in enumerate(self.current_batch_event): + try: + results.append(resolver(event=event, **event.arguments)) + except Exception: + logger.debug(f"Failed to process event number {idx} from field '{event.info.field_name}'") + results.append(None) + + return results + + async def _call_async_batch_resolver( self, - event: dict, - context: LambdaContext, - data_model: Type[AppSyncResolverEvent] = AppSyncResolverEvent, - ) -> Any: - """Implicit lambda handler which internally calls `resolve`""" - return self.resolve(event, context, data_model) + resolver: Callable, + raise_on_error: bool = False, + aggregate: bool = True, + ) -> list[Any]: + """ + Asynchronously call a batch resolver for each event in the current batch. + + Parameters + ---------- + resolver: Callable + The asynchronous resolver function. + raise_on_error: bool + A flag indicating whether to raise an error when processing batches + with failed items. Defaults to False, which means errors are handled without raising exceptions. + aggregate: bool + A flag indicating whether the batch items should be processed at once or individually. + If True (default), the batch resolver will process all items in the batch as a single event. + If False, the batch resolver will process each item in the batch individually. + + Returns + ------- + list[Any] + A list of results corresponding to the resolved events. + """ + + logger.debug(f"Graceful error handling flag {raise_on_error=}") - def include_router(self, router: "Router") -> None: + # Checks whether the entire batch should be processed at once + if aggregate: + # Process the entire batch + ret = await resolver(event=self.current_batch_event) + if not isinstance(ret, list): + raise InvalidBatchResponse("The response must be a List when using batch resolvers") + + return ret + + response: list = [] + + # Prime coroutines + tasks = [resolver(event=e, **e.arguments) for e in self.current_batch_event] + + # Aggregate results or raise at first error + if raise_on_error: + response.extend(await asyncio.gather(*tasks)) + return response + + # Aggregate results and exceptions, then filter them out + # Use `None` upon exception for graceful error handling at GraphQL engine level + # + # NOTE: asyncio.gather(return_exceptions=True) catches and includes exceptions in the results + # this will become useful when we support exception handling in AppSync resolver + results = await asyncio.gather(*tasks, return_exceptions=True) + response.extend(None if isinstance(ret, Exception) else ret for ret in results) + + return response + + def _call_batch_resolver(self, event: list[dict], data_model: type[AppSyncResolverEvent]) -> list[Any]: + """Call batch event resolver for sync and async methods + + Parameters + ---------- + event : list[dict] + Batch event + data_model : type[AppSyncResolverEvent] + Data_model to decode AppSync event, by default AppSyncResolverEvent or a subclass + + Returns + ------- + list[Any] + Results of the resolver execution. + + Raises + ------ + InconsistentPayloadError: + When all events in the batch do not have the same fieldName. + + ResolverNotFoundError: + When no resolver is found for the specified type and field. + """ + logger.debug("Processing batch resolver event") + + self.current_batch_event = [data_model(e) for e in event] + type_name, field_name = self.current_batch_event[0].type_name, self.current_batch_event[0].field_name + + resolver = self._batch_resolver_registry.find_resolver(type_name, field_name) + async_resolver = self._async_batch_resolver_registry.find_resolver(type_name, field_name) + + if resolver and async_resolver: + warnings.warn( + f"Both synchronous and asynchronous resolvers found for the same event and field." + f"The synchronous resolver takes precedence. Executing: {resolver['func'].__name__}", + stacklevel=2, + category=PowertoolsUserWarning, + ) + + if resolver: + logger.debug(f"Found sync resolver. {resolver=}, {field_name=}") + return self._call_sync_batch_resolver( + resolver=resolver["func"], + raise_on_error=resolver["raise_on_error"], + aggregate=resolver["aggregate"], + ) + + if async_resolver: + logger.debug(f"Found async resolver. {resolver=}, {field_name=}") + return asyncio.run( + self._call_async_batch_resolver( + resolver=async_resolver["func"], + raise_on_error=async_resolver["raise_on_error"], + aggregate=async_resolver["aggregate"], + ), + ) + + raise ResolverNotFoundError(f"No resolver found for '{type_name}.{field_name}'") + + def include_router(self, router: Router) -> None: """Adds all resolvers defined in a router Parameters @@ -202,15 +372,129 @@ def include_router(self, router: "Router") -> None: router : Router A router containing a dict of field resolvers """ + # Merge app and router context + logger.debug("Merging router and app context") self.context.update(**router.context) + # use pointer to allow context clearance after event is processed e.g., resolve(evt, ctx) router.context = self.context - self._resolvers.update(router._resolvers) + logger.debug("Merging router resolver registries") + self._resolver_registry.merge(router._resolver_registry) + self._batch_resolver_registry.merge(router._batch_resolver_registry) + self._async_batch_resolver_registry.merge(router._async_batch_resolver_registry) + def resolver(self, type_name: str = "*", field_name: str | None = None) -> Callable: + """Registers direct resolver function for GraphQL type and field name. -class Router(BaseRouter): - def __init__(self): - super().__init__() - self.context = {} # early init as customers might add context before event resolution + Parameters + ---------- + type_name : str, optional + GraphQL type e.g., Query, Mutation, by default "*" meaning any + field_name : Optional[str], optional + GraphQL field e.g., getTodo, createTodo, by default None + + Returns + ------- + Callable + Registered resolver + + Example + ------- + + ```python + from aws_lambda_powertools.event_handler import AppSyncResolver + + from typing import TypedDict + + app = AppSyncResolver() + + class Todo(TypedDict, total=False): + id: str + userId: str + title: str + completed: bool + + # resolve any GraphQL `getTodo` queries + # arguments are injected as function arguments as-is + @app.resolver(type_name="Query", field_name="getTodo") + def get_todo(id: str = "", status: str = "open") -> Todo: + todos: Response = requests.get(f"https://jsonplaceholder.typicode.com/todos/{id}") + todos.raise_for_status() + + return todos.json() + + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ + return self._resolver_registry.register(field_name=field_name, type_name=type_name) + + def batch_resolver( + self, + type_name: str = "*", + field_name: str | None = None, + raise_on_error: bool = False, + aggregate: bool = True, + ) -> Callable: + """Registers batch resolver function for GraphQL type and field name. + + By default, we handle errors gracefully by returning `None`. If you want + to short-circuit and fail the entire batch use `raise_on_error=True`. + + Parameters + ---------- + type_name : str, optional + GraphQL type e.g., Query, Mutation, by default "*" meaning any + field_name : Optional[str], optional + GraphQL field e.g., getTodo, createTodo, by default None + raise_on_error : bool, optional + Whether to fail entire batch upon error, or handle errors gracefully (None), by default False + aggregate: bool + A flag indicating whether the batch items should be processed at once or individually. + If True (default), the batch resolver will process all items in the batch as a single event. + If False, the batch resolver will process each item in the batch individually. + + Returns + ------- + Callable + Registered resolver + """ + return self._batch_resolver_registry.register( + field_name=field_name, + type_name=type_name, + raise_on_error=raise_on_error, + aggregate=aggregate, + ) + + def async_batch_resolver( + self, + type_name: str = "*", + field_name: str | None = None, + raise_on_error: bool = False, + aggregate: bool = True, + ) -> Callable: + return self._async_batch_resolver_registry.register( + field_name=field_name, + type_name=type_name, + raise_on_error=raise_on_error, + aggregate=aggregate, + ) + + def exception_handler(self, exc_class: type[Exception] | list[type[Exception]]): + """ + A decorator function that registers a handler for one or more exception types. + + Parameters + ---------- + exc_class (type[Exception] | list[type[Exception]]) + A single exception type or a list of exception types. + + Returns + ------- + Callable: + A decorator function that registers the exception handler. + """ + + return self.exception_handler_manager.exception_handler(exc_class=exc_class) diff --git a/aws_lambda_powertools/event_handler/bedrock_agent.py b/aws_lambda_powertools/event_handler/bedrock_agent.py new file mode 100644 index 00000000000..a49ba38c214 --- /dev/null +++ b/aws_lambda_powertools/event_handler/bedrock_agent.py @@ -0,0 +1,447 @@ +from __future__ import annotations + +import json +from typing import TYPE_CHECKING, Any + +from typing_extensions import override + +from aws_lambda_powertools.event_handler import ApiGatewayResolver +from aws_lambda_powertools.event_handler.api_gateway import ( + BedrockResponse, + ProxyEventType, + ResponseBuilder, +) +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 + from http import HTTPStatus + from re import Match + + from aws_lambda_powertools.event_handler.openapi.models import Contact, License, SecurityScheme, Server, Tag + from aws_lambda_powertools.event_handler.openapi.types import OpenAPIResponse + from aws_lambda_powertools.utilities.data_classes import BedrockAgentEvent + + +class BedrockResponseBuilder(ResponseBuilder): + """ + Bedrock Response Builder. This builds the response dict to be returned by Lambda when using Bedrock Agents. + + Since the payload format is different from the standard API Gateway Proxy event, we override the build method. + """ + + @override + def build(self, event: BedrockAgentEvent, *args) -> dict[str, Any]: + body = self.response.body + if self.response.is_json() and not isinstance(self.response.body, str): + body = self.serializer(self.response.body) + + response = { + "messageVersion": "1.0", + "response": { + "actionGroup": event.action_group, + "apiPath": event.api_path, + "httpMethod": event.http_method, + "httpStatusCode": self.response.status_code, + "responseBody": { + self.response.content_type: { + "body": body, + }, + }, + }, + } + + # Add Bedrock-specific attributes + if isinstance(self.response, BedrockResponse): + if self.response.session_attributes: + response["sessionAttributes"] = self.response.session_attributes + + if self.response.prompt_session_attributes: + response["promptSessionAttributes"] = self.response.prompt_session_attributes + + if self.response.knowledge_bases_configuration: + response["knowledgeBasesConfiguration"] = self.response.knowledge_bases_configuration + + return response + + +class BedrockAgentResolver(ApiGatewayResolver): + """Bedrock Agent Resolver + + See https://aws.amazon.com/bedrock/agents/ for more information. + + Examples + -------- + Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator + + ```python + from aws_lambda_powertools import Tracer + from aws_lambda_powertools.event_handler import BedrockAgentResolver + + tracer = Tracer() + app = BedrockAgentResolver() + + @app.get("/claims") + def simple_get(): + return "You have 3 claims" + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + + """ + + current_event: BedrockAgentEvent + + def __init__(self, debug: bool = False, enable_validation: bool = True): + super().__init__( + proxy_type=ProxyEventType.BedrockAgentEvent, + cors=None, + debug=debug, + serializer=None, + strip_prefixes=None, + enable_validation=enable_validation, + json_body_deserializer=None, + ) + self._response_builder_class = BedrockResponseBuilder + + # Note: we need ignore[override] because we are making the optional `description` field required. + @override + def get( # type: ignore[override] + self, + rule: str, + description: str, + cors: bool | None = None, + compress: bool = False, + cache_control: str | None = None, + summary: str | None = None, + responses: dict[int, OpenAPIResponse] | None = None, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + tags: list[str] | None = None, + operation_id: str | None = None, + include_in_schema: bool = True, + openapi_extensions: dict[str, Any] | None = None, + 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 + + return super().get( + rule, + cors, + compress, + cache_control, + summary, + description, + responses, + response_description, + tags, + operation_id, + include_in_schema, + security, + openapi_extensions, + deprecated, + enable_validation, + custom_response_validation_http_code, + status_code, + middlewares, + ) + + # Note: we need ignore[override] because we are making the optional `description` field required. + @override + def post( # type: ignore[override] + self, + rule: str, + description: str, + cors: bool | None = None, + compress: bool = False, + cache_control: str | None = None, + summary: str | None = None, + responses: dict[int, OpenAPIResponse] | None = None, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + tags: list[str] | None = None, + operation_id: str | None = None, + include_in_schema: bool = True, + openapi_extensions: dict[str, Any] | None = None, + 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 + + return super().post( + rule, + cors, + compress, + cache_control, + summary, + description, + responses, + response_description, + tags, + operation_id, + include_in_schema, + security, + openapi_extensions, + deprecated, + enable_validation, + custom_response_validation_http_code, + status_code, + middlewares, + ) + + # Note: we need ignore[override] because we are making the optional `description` field required. + @override + def put( # type: ignore[override] + self, + rule: str, + description: str, + cors: bool | None = None, + compress: bool = False, + cache_control: str | None = None, + summary: str | None = None, + responses: dict[int, OpenAPIResponse] | None = None, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + tags: list[str] | None = None, + operation_id: str | None = None, + include_in_schema: bool = True, + openapi_extensions: dict[str, Any] | None = None, + 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 + + return super().put( + rule, + cors, + compress, + cache_control, + summary, + description, + responses, + response_description, + tags, + operation_id, + include_in_schema, + security, + openapi_extensions, + deprecated, + enable_validation, + custom_response_validation_http_code, + status_code, + middlewares, + ) + + # Note: we need ignore[override] because we are making the optional `description` field required. + @override + def patch( # type: ignore[override] + self, + rule: str, + description: str, + cors: bool | None = None, + compress: bool = False, + cache_control: str | None = None, + summary: str | None = None, + responses: dict[int, OpenAPIResponse] | None = None, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + tags: list[str] | None = None, + operation_id: str | None = None, + include_in_schema: bool = True, + openapi_extensions: dict[str, Any] | None = None, + 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 + + return super().patch( + rule, + cors, + compress, + cache_control, + summary, + description, + responses, + response_description, + tags, + operation_id, + include_in_schema, + security, + openapi_extensions, + deprecated, + enable_validation, + custom_response_validation_http_code, + status_code, + middlewares, + ) + + # Note: we need ignore[override] because we are making the optional `description` field required. + @override + def delete( # type: ignore[override] + self, + rule: str, + description: str, + cors: bool | None = None, + compress: bool = False, + cache_control: str | None = None, + summary: str | None = None, + responses: dict[int, OpenAPIResponse] | None = None, + response_description: str = DEFAULT_OPENAPI_RESPONSE_DESCRIPTION, + tags: list[str] | None = None, + operation_id: str | None = None, + include_in_schema: bool = True, + openapi_extensions: dict[str, Any] | None = None, + 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 + + return super().delete( + rule, + cors, + compress, + cache_control, + summary, + description, + responses, + response_description, + tags, + operation_id, + include_in_schema, + security, + openapi_extensions, + deprecated, + enable_validation, + custom_response_validation_http_code, + status_code, + middlewares, + ) + + @override + def _convert_matches_into_route_keys(self, match: Match) -> dict[str, str]: + # In Bedrock Agents, all the parameters come inside the "parameters" key, not on the apiPath + # So we have to search for route parameters in the parameters key + parameters: dict[str, str] = {} + if match.groupdict() and self.current_event.parameters: + parameters = {parameter["name"]: parameter["value"] for parameter in self.current_event.parameters} + return parameters + + @override + def get_openapi_json_schema( # type: ignore[override] + self, + *, + title: str = "Powertools API", + version: str = DEFAULT_API_VERSION, + openapi_version: str = DEFAULT_OPENAPI_VERSION, + summary: str | None = None, + description: str | None = None, + tags: list[Tag | str] | None = None, + servers: list[Server] | None = None, + terms_of_service: str | None = None, + contact: Contact | None = None, + license_info: License | None = None, + security_schemes: dict[str, SecurityScheme] | None = None, + security: list[dict[str, list[str]]] | None = None, + openapi_extensions: dict[str, Any] | None = None, + ) -> str: + """ + Returns the OpenAPI schema as a JSON serializable dict. + Since Bedrock Agents only support OpenAPI 3.0.0, we convert OpenAPI 3.1.0 schemas + and enforce 3.0.0 compatibility for seamless integration. + + Parameters + ---------- + title: str + The title of the application. + version: str + The version of the OpenAPI document (which is distinct from the OpenAPI Specification version or the API + openapi_version: str, default = "3.0.0" + The version of the OpenAPI Specification (which the document uses). + summary: str, optional + A short summary of what the application does. + description: str, optional + A verbose explanation of the application behavior. + tags: list[Tag, str], optional + A list of tags used by the specification with additional metadata. + servers: list[Server], optional + An array of Server Objects, which provide connectivity information to a target server. + terms_of_service: str, optional + A URL to the Terms of Service for the API. MUST be in the format of a URL. + contact: Contact, optional + The contact information for the exposed API. + license_info: License, optional + The license information for the exposed API. + security_schemes: dict[str, SecurityScheme]], optional + A declaration of the security schemes available to be used in the specification. + security: list[dict[str, list[str]]], optional + A declaration of which security mechanisms are applied globally across the API. + + Returns + ------- + str + The OpenAPI schema as a JSON serializable dict. + """ + from aws_lambda_powertools.event_handler.openapi.compat import model_json + + schema = super().get_openapi_schema( + title=title, + version=version, + openapi_version=openapi_version, + summary=summary, + description=description, + tags=tags, + servers=servers, + terms_of_service=terms_of_service, + contact=contact, + license_info=license_info, + security_schemes=security_schemes, + security=security, + openapi_extensions=openapi_extensions, + ) + schema.openapi = "3.0.3" + + # Transform OpenAPI 3.1 into 3.0 + def inner(yaml_dict): + if isinstance(yaml_dict, dict): + if "anyOf" in yaml_dict and isinstance((anyOf := yaml_dict["anyOf"]), list): + for i, item in enumerate(anyOf): + if isinstance(item, dict) and item.get("type") == "null": + anyOf.pop(i) + yaml_dict["nullable"] = True + for value in yaml_dict.values(): + inner(value) + elif isinstance(yaml_dict, list): + for item in yaml_dict: + inner(item) + + model = json.loads( + model_json( + schema, + by_alias=True, + exclude_none=True, + indent=2, + ), + ) + + inner(model) + + return json.dumps(model) diff --git a/aws_lambda_powertools/event_handler/bedrock_agent_function.py b/aws_lambda_powertools/event_handler/bedrock_agent_function.py new file mode 100644 index 00000000000..7f51f2b9b1b --- /dev/null +++ b/aws_lambda_powertools/event_handler/bedrock_agent_function.py @@ -0,0 +1,254 @@ +from __future__ import annotations + +import inspect +import json +import logging +import warnings +from collections.abc import Callable +from typing import Any, Literal, TypeVar + +from aws_lambda_powertools.utilities.data_classes import BedrockAgentFunctionEvent +from aws_lambda_powertools.warnings import PowertoolsUserWarning + +# Define a generic type for the function +T = TypeVar("T", bound=Callable[..., Any]) + +logger = logging.getLogger(__name__) + + +class BedrockFunctionResponse: + """Response class for Bedrock Agent Functions. + + Parameters + ---------- + body : Any, optional + Response body to be returned to the caller. + session_attributes : dict[str, str] or None, optional + Session attributes to include in the response for maintaining state. + prompt_session_attributes : dict[str, str] or None, optional + Prompt session attributes to include in the response. + knowledge_bases : list[dict[str, Any]] or None, optional + Knowledge bases to include in the response. + response_state : {"FAILURE", "REPROMPT"} or None, optional + Response state indicating if the function failed or needs reprompting. + + Examples + -------- + >>> @app.tool(description="Function that uses session attributes") + >>> def test_function(): + ... return BedrockFunctionResponse( + ... body="Hello", + ... session_attributes={"userId": "123"}, + ... prompt_session_attributes={"lastAction": "login"} + ... ) + + Notes + ----- + The `response_state` parameter can only be set to "FAILURE" or "REPROMPT". + """ + + def __init__( + self, + body: Any = None, + session_attributes: dict[str, str] | None = None, + prompt_session_attributes: dict[str, str] | None = None, + knowledge_bases: list[dict[str, Any]] | None = None, + response_state: Literal["FAILURE", "REPROMPT"] | None = None, + ) -> None: + if response_state and response_state not in ["FAILURE", "REPROMPT"]: + raise ValueError("responseState must be 'FAILURE' or 'REPROMPT'") + + self.body = body + self.session_attributes = session_attributes + self.prompt_session_attributes = prompt_session_attributes + self.knowledge_bases = knowledge_bases + self.response_state = response_state + + +class BedrockFunctionsResponseBuilder: + """ + Bedrock Functions Response Builder. This builds the response dict to be returned by Lambda + when using Bedrock Agent Functions. + """ + + def __init__(self, result: BedrockFunctionResponse | Any) -> None: + self.result = result + + def build(self, event: BedrockAgentFunctionEvent, serializer: Callable) -> dict[str, Any]: + result_obj = self.result + + # Extract attributes from BedrockFunctionResponse or use defaults + body = getattr(result_obj, "body", result_obj) + session_attributes = getattr(result_obj, "session_attributes", None) + prompt_session_attributes = getattr(result_obj, "prompt_session_attributes", None) + knowledge_bases = getattr(result_obj, "knowledge_bases", None) + response_state = getattr(result_obj, "response_state", None) + + # Build base response structure + # Per AWS Bedrock documentation, currently only "TEXT" is supported as the responseBody content type + # https://docs.aws.amazon.com/bedrock/latest/userguide/agents-lambda.html + response: dict[str, Any] = { + "messageVersion": "1.0", + "response": { + "actionGroup": event.action_group, + "function": event.function, + "functionResponse": { + "responseBody": {"TEXT": {"body": serializer(body if body is not None else "")}}, + }, + }, + "sessionAttributes": session_attributes or event.session_attributes or {}, + "promptSessionAttributes": prompt_session_attributes or event.prompt_session_attributes or {}, + } + + # Add optional fields when present + if response_state: + response["response"]["functionResponse"]["responseState"] = response_state + + if knowledge_bases: + response["knowledgeBasesConfiguration"] = knowledge_bases + + return response + + +class BedrockAgentFunctionResolver: + """Bedrock Agent Function resolver that handles function definitions + + Examples + -------- + ```python + from aws_lambda_powertools.event_handler import BedrockAgentFunctionResolver + + app = BedrockAgentFunctionResolver() + + @app.tool(name="get_current_time", description="Gets the current UTC time") + def get_current_time(): + from datetime import datetime + return datetime.utcnow().isoformat() + + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ + + context: dict + + def __init__(self, serializer: Callable | None = None) -> None: + """ + Parameters + ---------- + serializer: Callable, optional + function to serialize `obj` to a JSON formatted `str`, by default json.dumps + """ + self._tools: dict[str, dict[str, Any]] = {} + self.current_event: BedrockAgentFunctionEvent | None = None + self.context = {} + self._response_builder_class = BedrockFunctionsResponseBuilder + self.serializer = serializer or json.dumps + + def tool( + self, + name: str | None = None, + description: str | None = None, + ) -> Callable[[T], T]: + """Decorator to register a tool function + + Parameters + ---------- + name : str | None + Custom name for the tool. If not provided, uses the function name + description : str | None + Description of what the tool does + + Returns + ------- + Callable + Decorator function that registers and returns the original function + """ + + def decorator(func: T) -> T: + function_name = name or func.__name__ + + logger.debug(f"Registering {function_name} tool") + + if function_name in self._tools: + warnings.warn( + f"Tool '{function_name}' already registered. Overwriting with new definition.", + PowertoolsUserWarning, + stacklevel=2, + ) + + self._tools[function_name] = { + "function": func, + "description": description, + } + return func + + return decorator + + def resolve(self, event: dict[str, Any], context: Any) -> dict[str, Any]: + """Resolves the function call from Bedrock Agent event""" + try: + self.current_event = BedrockAgentFunctionEvent(event) + return self._resolve() + except KeyError as e: + raise ValueError(f"Missing required field: {str(e)}") from e + + def _resolve(self) -> dict[str, Any]: + """Internal resolution logic""" + if self.current_event is None: + raise ValueError("No event to process") + + function_name = self.current_event.function + + logger.debug(f"Resolving {function_name} tool") + + try: + parameters: dict[str, Any] = {} + # Extract parameters from the event + for param in getattr(self.current_event, "parameters", []): + param_type = getattr(param, "type", None) + if param_type == "string": + parameters[param.name] = str(param.value) + elif param_type == "integer": + try: + parameters[param.name] = int(param.value) + except (ValueError, TypeError): + parameters[param.name] = param.value + elif param_type == "number": + try: + parameters[param.name] = float(param.value) + except (ValueError, TypeError): + parameters[param.name] = param.value + elif param_type == "boolean": + if isinstance(param.value, str): + parameters[param.name] = param.value.lower() == "true" + else: + parameters[param.name] = bool(param.value) + else: # "array" or any other type + parameters[param.name] = param.value + + func = self._tools[function_name]["function"] + # Filter parameters to only include those expected by the function + sig = inspect.signature(func) + valid_params = {name: value for name, value in parameters.items() if name in sig.parameters} + + # Call the function with the filtered parameters + result = func(**valid_params) + + self.clear_context() + + # Build and return the response + return BedrockFunctionsResponseBuilder(result).build(self.current_event, serializer=self.serializer) + except Exception as error: + # Return a formatted error response + logger.error(f"Error processing function: {function_name}", exc_info=True) + error_response = BedrockFunctionResponse(body=f"Error: {error.__class__.__name__}: {str(error)}") + return BedrockFunctionsResponseBuilder(error_response).build(self.current_event, serializer=self.serializer) + + def append_context(self, **additional_context): + """Append key=value data as routing context""" + self.context.update(**additional_context) + + def clear_context(self): + """Resets routing context""" + self.context.clear() 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/events_appsync/__init__.py b/aws_lambda_powertools/event_handler/events_appsync/__init__.py new file mode 100644 index 00000000000..64387723526 --- /dev/null +++ b/aws_lambda_powertools/event_handler/events_appsync/__init__.py @@ -0,0 +1,5 @@ +from aws_lambda_powertools.event_handler.events_appsync.appsync_events import AppSyncEventsResolver + +__all__ = [ + "AppSyncEventsResolver", +] diff --git a/aws_lambda_powertools/event_handler/events_appsync/_registry.py b/aws_lambda_powertools/event_handler/events_appsync/_registry.py new file mode 100644 index 00000000000..8c682327706 --- /dev/null +++ b/aws_lambda_powertools/event_handler/events_appsync/_registry.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import logging +import warnings +from typing import TYPE_CHECKING + +from aws_lambda_powertools.event_handler.events_appsync.functions import find_best_route, is_valid_path +from aws_lambda_powertools.warnings import PowertoolsUserWarning + +if TYPE_CHECKING: + from collections.abc import Callable + + from aws_lambda_powertools.event_handler.events_appsync.types import ResolverTypeDef + + +logger = logging.getLogger(__name__) + + +class ResolverEventsRegistry: + def __init__(self, kind_resolver: str): + self.resolvers: dict[str, ResolverTypeDef] = {} + self.kind_resolver = kind_resolver + + def register( + self, + path: str = "/default/*", + aggregate: bool = False, + ) -> Callable | None: + """Registers the resolver for path that includes namespace + channel + + Parameters + ---------- + path : str + Path including namespace + channel + aggregate: bool + A flag indicating whether the batch items should be processed at once or individually. + If True, the resolver will process all items as a single event. + If False (default), the resolver will process each item individually. + + Return + ---------- + Callable + A Callable + """ + + def _register(func) -> Callable | None: + if not is_valid_path(path): + warnings.warn( + f"The path `{path}` registered for `{self.kind_resolver}` is not valid and will be skipped." + f"A path should always have a namespace starting with '/'" + "A path can have multiple namespaces, all separated by '/'." + "Wildcards are allowed only at the end of the path.", + stacklevel=2, + category=PowertoolsUserWarning, + ) + return None + + logger.debug( + f"Adding resolver `{func.__name__}` for path `{path}` and kind_resolver `{self.kind_resolver}`", + ) + self.resolvers[f"{path}"] = { + "func": func, + "aggregate": aggregate, + } + return func + + return _register + + def find_resolver(self, path: str) -> ResolverTypeDef | None: + """Find resolver based on type_name and field_name + + Parameters + ---------- + path : str + Type name + Return + ---------- + dict | None + A dictionary with the resolver and if this is aggregated or not + """ + logger.debug(f"Looking for resolver for path `{path}` and kind_resolver `{self.kind_resolver}`") + return self.resolvers.get(find_best_route(self.resolvers, path)) + + def merge(self, other_registry: ResolverEventsRegistry): + """Update current registry with incoming registry + + Parameters + ---------- + other_registry : ResolverRegistry + Registry to merge from + """ + self.resolvers.update(**other_registry.resolvers) diff --git a/aws_lambda_powertools/event_handler/events_appsync/appsync_events.py b/aws_lambda_powertools/event_handler/events_appsync/appsync_events.py new file mode 100644 index 00000000000..ee03db5c625 --- /dev/null +++ b/aws_lambda_powertools/event_handler/events_appsync/appsync_events.py @@ -0,0 +1,422 @@ +from __future__ import annotations + +import asyncio +import logging +import warnings +from typing import TYPE_CHECKING, Any + +from aws_lambda_powertools.event_handler.events_appsync.exceptions import UnauthorizedException +from aws_lambda_powertools.event_handler.events_appsync.router import Router +from aws_lambda_powertools.utilities.data_classes.appsync_resolver_events_event import AppSyncResolverEventsEvent +from aws_lambda_powertools.warnings import PowertoolsUserWarning + +if TYPE_CHECKING: + from collections.abc import Callable + + from aws_lambda_powertools.event_handler.events_appsync.types import ResolverTypeDef + from aws_lambda_powertools.utilities.typing.lambda_context import LambdaContext + + +logger = logging.getLogger(__name__) + + +class AppSyncEventsResolver(Router): + """ + AppSync Events API Resolver for handling publish and subscribe operations. + + This class extends the Router to process AppSync real-time API events, managing + both synchronous and asynchronous resolvers for event publishing and subscribing. + + Attributes + ---------- + context: dict + Dictionary to store context information accessible across resolvers + lambda_context: LambdaContext + Lambda context from the AWS Lambda function + current_event: AppSyncResolverEventsEvent + Current event being processed + + Examples + -------- + Define a simple AppSync events resolver for a chat application: + + >>> from aws_lambda_powertools.event_handler import AppSyncEventsResolver + >>> app = AppSyncEventsResolver() + >>> + >>> # Using aggregate mode to process multiple messages at once + >>> @app.on_publish(channel_path="/default/*", aggregate=True) + >>> def handle_batch_messages(payload): + >>> processed_messages = [] + >>> for message in payload: + >>> # Process each message + >>> processed_messages.append({ + >>> "messageId": f"msg-{message.get('id')}", + >>> "processed": True + >>> }) + >>> return processed_messages + >>> + >>> # Asynchronous resolver + >>> @app.async_on_publish(channel_path="/default/*") + >>> async def handle_async_messages(event): + >>> # Perform async operations (e.g., DB queries, HTTP calls) + >>> await asyncio.sleep(0.1) # Simulate async work + >>> return { + >>> "messageId": f"async-{event.get('id')}", + >>> "processed": True + >>> } + >>> + >>> # Lambda handler + >>> def lambda_handler(event, context): + >>> return events.resolve(event, context) + """ + + def __init__(self): + """Initialize the AppSyncEventsResolver.""" + super().__init__() + self.context = {} # early init as customers might add context before event resolution + self._exception_handlers: dict[type, Callable] = {} + + def __call__( + self, + event: dict | AppSyncResolverEventsEvent, + context: LambdaContext, + ) -> Any: + """ + Implicit lambda handler which internally calls `resolve`. + + Parameters + ---------- + event: dict or AppSyncResolverEventsEvent + The AppSync event to process + context: LambdaContext + The Lambda context + + Returns + ------- + Any + The resolver's response + """ + return self.resolve(event, context) + + def resolve( + self, + event: dict | AppSyncResolverEventsEvent, + context: LambdaContext, + ) -> Any: + """ + Resolves the response based on the provided event and decorator operation. + + Parameters + ---------- + event: dict or AppSyncResolverEventsEvent + The AppSync event to process + context: LambdaContext + The Lambda context + + Returns + ------- + Any + The resolver's response based on the operation type + + Examples + -------- + >>> events = AppSyncEventsResolver() + >>> + >>> # Explicit call to resolve in Lambda handler + >>> def lambda_handler(event, context): + >>> return events.resolve(event, context) + """ + + self._setup_context(event, context) + + if self.current_event.info.operation == "PUBLISH": + response = self._publish_events(payload=self.current_event.events) + else: + response = self._subscribe_events() + + self.clear_context() + + return response + + def _subscribe_events(self) -> Any: + """ + Handle subscribe events. + + Returns + ------- + Any + Any response + """ + channel_path = self.current_event.info.channel_path + logger.debug(f"Processing subscribe events for path {channel_path}") + + resolver = self._subscribe_registry.find_resolver(channel_path) + if resolver: + try: + resolver["func"]() + return None # Must return None in subscribe events + except UnauthorizedException: + raise + except Exception as error: + return {"error": self._format_error_response(error)} + + self._warn_no_resolver("subscribe", channel_path) + return None + + def _publish_events(self, payload: list[dict[str, Any]]) -> list[dict[str, Any]] | dict[str, Any]: + """ + Handle publish events. + + Parameters + ---------- + payload: list[dict[str, Any]] + The events payload to process + + Returns + ------- + list[dict[str, Any]] or dict[str, Any] + Processed events or error response + """ + + channel_path = self.current_event.info.channel_path + + logger.debug(f"Processing publish events for path {channel_path}") + + resolver = self._publish_registry.find_resolver(channel_path) + async_resolver = self._async_publish_registry.find_resolver(channel_path) + + if resolver and async_resolver: + warnings.warn( + f"Both synchronous and asynchronous resolvers found for the same event and field." + f"The synchronous resolver takes precedence. Executing: {resolver['func'].__name__}", + stacklevel=2, + category=PowertoolsUserWarning, + ) + + if resolver: + logger.debug(f"Found sync resolver: {resolver}") + return self._process_publish_event_sync_resolver(resolver) + + if async_resolver: + logger.debug(f"Found async resolver: {async_resolver}") + return asyncio.run(self._call_publish_event_async_resolver(async_resolver)) + + # No resolver found + # Warning and returning AS IS + self._warn_no_resolver("publish", channel_path, return_as_is=True) + return {"events": payload} + + def _process_publish_event_sync_resolver( + self, + resolver: ResolverTypeDef, + ) -> list[dict[str, Any]] | dict[str, Any]: + """ + Process events using a synchronous resolver. + + Parameters + ---------- + resolver : ResolverTypeDef + The resolver to use for processing events + + Returns + ------- + list[dict[str, Any]] or dict[str, Any] + Processed events or error response + + Notes + ----- + If the resolver is configured with aggregate=True, all events are processed + as a batch. Otherwise, each event is processed individually. + """ + + # Checks whether the entire batch should be processed at once + if resolver["aggregate"]: + try: + # Process the entire batch + response = resolver["func"](payload=self.current_event.events) + + if not isinstance(response, list): + warnings.warn( + "Response must be a list when using aggregate, AppSync will drop those events.", + stacklevel=2, + category=PowertoolsUserWarning, + ) + + return {"events": response} + except UnauthorizedException: + raise + except Exception as error: + return {"error": self._format_error_response(error)} + + # By default, we gracefully append `None` for any records that failed processing + results = [] + for idx, event in enumerate(self.current_event.events): + try: + result_return = resolver["func"](payload=event.get("payload")) + results.append({"id": event.get("id"), "payload": result_return}) + except Exception as error: + logger.debug(f"Failed to process event number {idx}") + error_return = {"id": event.get("id"), "error": self._format_error_response(error)} + results.append(error_return) + + return {"events": results} + + async def _call_publish_event_async_resolver( + self, + resolver: ResolverTypeDef, + ) -> list[dict[str, Any]] | dict[str, Any]: + """ + Process events using an asynchronous resolver. + + Parameters + ---------- + resolver: ResolverTypeDef + The async resolver to use for processing events + + Returns + ------- + list[Any] + Processed events or error responses + + Notes + ----- + If the resolver is configured with aggregate=True, all events are processed + as a batch. Otherwise, each event is processed individually and in parallel. + """ + + # Checks whether the entire batch should be processed at once + if resolver["aggregate"]: + try: + # Process the entire batch + response = await resolver["func"](payload=self.current_event.events) + if not isinstance(response, list): + warnings.warn( + "Response must be a list when using aggregate, AppSync will drop those events.", + stacklevel=2, + category=PowertoolsUserWarning, + ) + + return {"events": response} + except UnauthorizedException: + raise + except Exception as error: + return {"error": self._format_error_response(error)} + + response_async: list = [] + + # Prime coroutines + tasks = [resolver["func"](payload=e.get("payload")) for e in self.current_event.events] + + # Aggregate results and exceptions, then filter them out + # Use `None` upon exception for graceful error handling at GraphQL engine level + # + # NOTE: asyncio.gather(return_exceptions=True) catches and includes exceptions in the results + # this will become useful when we support exception handling in AppSync resolver + # Aggregate results and exceptions, then filter them out + results = await asyncio.gather(*tasks, return_exceptions=True) + response_async.extend( + [ + ( + {"id": e.get("id"), "error": self._format_error_response(ret)} + if isinstance(ret, Exception) + else {"id": e.get("id"), "payload": ret} + ) + for e, ret in zip(self.current_event.events, results) + ], + ) + + return {"events": response_async} + + def include_router(self, router: Router) -> None: + """ + Add all resolvers defined in a router to this resolver. + + Parameters + ---------- + router : Router + A router containing resolvers to include + + Examples + -------- + >>> # Create main resolver and a router + >>> app = AppSyncEventsResolver() + >>> router = Router() + >>> + >>> # Define resolvers in the router + >>> @router.publish(path="/chat/message") + >>> def handle_chat_message(payload): + >>> return {"processed": True, "messageId": payload.get("id")} + >>> + >>> # Include the router in the main resolver + >>> app.include_router(chat_router) + >>> + >>> # Now events can handle "/chat/message" channel_path + """ + + # Merge app and router context + logger.debug("Merging router and app context") + self.context.update(**router.context) + + # use pointer to allow context clearance after event is processed e.g., resolve(evt, ctx) + router.context = self.context + + logger.debug("Merging router resolver registries") + self._publish_registry.merge(router._publish_registry) + self._async_publish_registry.merge(router._async_publish_registry) + self._subscribe_registry.merge(router._subscribe_registry) + + def _format_error_response(self, error=None) -> str: + """ + Format error responses consistently. + + Parameters + ---------- + error: Exception or None + The error to format + + Returns + ------- + str + Formatted error message + """ + if isinstance(error, Exception): + return f"{error.__class__.__name__} - {str(error)}" + return "An unknown error occurred" + + def _warn_no_resolver(self, operation_type: str, path: str, return_as_is: bool = False) -> None: + """ + Generate consistent warning messages for missing resolvers. + + Parameters + ---------- + operation_type : str + Type of operation (e.g., "publish", "subscribe") + path : str + The channel path that's missing a resolver + return_as_is : bool, optional + Whether payload will be returned as is, by default False + """ + message = ( + f"No resolvers were found for {operation_type} operations with path {path}" + f"{'. We will return the entire payload as is' if return_as_is else ''}" + ) + warnings.warn(message, stacklevel=3, category=PowertoolsUserWarning) + + def _setup_context(self, event: dict | AppSyncResolverEventsEvent, context: LambdaContext) -> None: + """ + Set up the context and event for processing. + + Parameters + ---------- + event : dict or AppSyncResolverEventsEvent + The AppSync event to process + context : LambdaContext + The Lambda context + """ + self.lambda_context = context + Router.lambda_context = context + + Router.current_event = ( + event if isinstance(event, AppSyncResolverEventsEvent) else AppSyncResolverEventsEvent(event) + ) + self.current_event = Router.current_event diff --git a/aws_lambda_powertools/event_handler/events_appsync/base.py b/aws_lambda_powertools/event_handler/events_appsync/base.py new file mode 100644 index 00000000000..86a1e140d5d --- /dev/null +++ b/aws_lambda_powertools/event_handler/events_appsync/base.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Callable + +DEFAULT_ROUTE = "/default/*" + + +class BaseRouter(ABC): + """Abstract base class for Router (resolvers)""" + + @abstractmethod + def on_publish( + self, + path: str = DEFAULT_ROUTE, + aggregate: bool = True, + ) -> Callable: + raise NotImplementedError + + @abstractmethod + def async_on_publish( + self, + path: str = DEFAULT_ROUTE, + aggregate: bool = True, + ) -> Callable: + raise NotImplementedError + + @abstractmethod + def on_subscribe( + self, + path: str = DEFAULT_ROUTE, + ) -> Callable: + raise NotImplementedError + + def append_context(self, **additional_context) -> None: + """ + Appends context information available under any route. + + Parameters + ----------- + **additional_context: dict + Additional context key-value pairs to append. + """ + raise NotImplementedError diff --git a/aws_lambda_powertools/event_handler/events_appsync/exceptions.py b/aws_lambda_powertools/event_handler/events_appsync/exceptions.py new file mode 100644 index 00000000000..89c6adcaf27 --- /dev/null +++ b/aws_lambda_powertools/event_handler/events_appsync/exceptions.py @@ -0,0 +1,24 @@ +from __future__ import annotations + + +class UnauthorizedException(Exception): + """ + Error to be thrown to communicate the subscription is unauthorized. + + When this error is raised, the client will receive a 40x error code + and the subscription will be closed. + + Attributes: + message (str): The error message describing the unauthorized access. + """ + + def __init__(self, message: str | None = None, *args): + """ + Initialize the UnauthorizedException. + + Args: + message (str): A descriptive error message. + *args: Variable positional arguments. + """ + super().__init__(message, *args) + self.name = "UnauthorizedException" diff --git a/aws_lambda_powertools/event_handler/events_appsync/functions.py b/aws_lambda_powertools/event_handler/events_appsync/functions.py new file mode 100644 index 00000000000..0d7ddf2518f --- /dev/null +++ b/aws_lambda_powertools/event_handler/events_appsync/functions.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import re +from functools import lru_cache +from typing import Any + +PATH_REGEX = re.compile(r"^\/([^\/\*]+)(\/[^\/\*]+)*(\/\*)?$") + + +def is_valid_path(path: str) -> bool: + """ + Checks if a given path is valid based on specific rules. + + Parameters + ---------- + path: str + The path to validate + + Returns: + -------- + bool: + True if the path is valid, False otherwise + + Examples: + >>> is_valid_path('/*') + True + >>> is_valid_path('/users') + True + >>> is_valid_path('/users/profile') + True + >>> is_valid_path('/users/*/details') + False + >>> is_valid_path('/users/*') + True + >>> is_valid_path('users') + False + """ + return True if path == "/*" else bool(PATH_REGEX.fullmatch(path)) + + +def find_best_route(routes: dict[str, Any], path: str): + """ + Find the most specific matching route for a given path. + + Examples of matches: + Route: /default/v1/* Path: /default/v1/users -> MATCH + Route: /default/v1/* Path: /default/v1/users/students -> MATCH + Route: /default/v1/users/* Path: /default/v1/users/123 -> MATCH (this wins over /default/v1/*) + Route: /* Path: /anything/here -> MATCH (lowest priority) + + Parameters + ---------- + routes: dict[str, Any] + Dictionary containing routes and their handlers + Format: { + 'resolvers': { + '/path/*': {'func': callable, 'aggregate': bool}, + '/path/specific/*': {'func': callable, 'aggregate': bool} + } + } + path: str + Actual path to match (e.g., '/default/v1/users') + + Returns + ------- + str: Most specific matching route or None if no match + """ + + @lru_cache(maxsize=1024) + def pattern_to_regex(route): + """ + Convert a route pattern to a regex pattern with caching. + Examples: + /default/v1/* -> ^/default/v1/[^/]+$ + /default/v1/users/* -> ^/default/v1/users/.*$ + + Parameters + ---------- + route: str + Route pattern with wildcards + + Returns + ------- + Pattern: + Compiled regex pattern + """ + # Escape special regex chars but convert * to regex pattern + pattern = re.escape(route).replace("\\*", "[^/]+") + + # If pattern ends with [^/]+, replace with .* for multi-segment match + if pattern.endswith("[^/]+"): + pattern = pattern[:-6] + ".*" + + # Compile and return the regex pattern + return re.compile(f"^{pattern}$") + + # Find all matching routes + matches = [route for route in routes.keys() if pattern_to_regex(route).match(path)] + + # Return the most specific route (longest length minus wildcards) + # Examples of specificity: + # - '/default/v1/users' -> score: 14 (len=14, wildcards=0) + # - '/default/v1/users/*' -> score: 14 (len=15, wildcards=1) + # - '/default/v1/*' -> score: 8 (len=9, wildcards=1) + # - '/*' -> score: 0 (len=2, wildcards=1) + return max(matches, key=lambda x: len(x) - x.count("*"), default=None) diff --git a/aws_lambda_powertools/event_handler/events_appsync/router.py b/aws_lambda_powertools/event_handler/events_appsync/router.py new file mode 100644 index 00000000000..167403e30fe --- /dev/null +++ b/aws_lambda_powertools/event_handler/events_appsync/router.py @@ -0,0 +1,199 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from aws_lambda_powertools.event_handler.events_appsync._registry import ResolverEventsRegistry +from aws_lambda_powertools.event_handler.events_appsync.base import DEFAULT_ROUTE, BaseRouter + +if TYPE_CHECKING: + from collections.abc import Callable + + from aws_lambda_powertools.utilities.data_classes.appsync_resolver_events_event import AppSyncResolverEventsEvent + from aws_lambda_powertools.utilities.typing.lambda_context import LambdaContext + + +class Router(BaseRouter): + """ + Router for AppSync real-time API event handling. + + This class provides decorators to register resolver functions for publish and subscribe + operations in AppSync real-time APIs. + + Parameters + ---------- + context : dict + Dictionary to store context information accessible across resolvers + current_event : AppSyncResolverEventsEvent + Current event being processed + lambda_context : LambdaContext + Lambda context from the AWS Lambda function + + Examples + -------- + Create a router and define resolvers: + + >>> chat_router = Router() + >>> + >>> # Register a resolver for publish operations + >>> @chat_router.on_publish(path="/chat/message") + >>> def handle_message(payload): + >>> # Process message + >>> return {"success": True, "messageId": payload.get("id")} + >>> + >>> # Register an async resolver for publish operations + >>> @chat_router.async_on_publish(path="/chat/typing") + >>> async def handle_typing(event): + >>> # Process typing indicator + >>> await some_async_operation() + >>> return {"processed": True} + >>> + >>> # Register a resolver for subscribe operations + >>> @chat_router.on_subscribe(path="/chat/room/*") + >>> def handle_subscribe(event): + >>> # Handle subscription setup + >>> return {"allowed": True} + """ + + context: dict + current_event: AppSyncResolverEventsEvent + lambda_context: LambdaContext + + def __init__(self): + """ + Initialize a new Router instance. + + Sets up empty context and registry containers for different types of resolvers. + """ + self.context = {} # early init as customers might add context before event resolution + self._publish_registry = ResolverEventsRegistry(kind_resolver="on_publish") + self._async_publish_registry = ResolverEventsRegistry(kind_resolver="async_on_publish") + self._subscribe_registry = ResolverEventsRegistry(kind_resolver="on_subscribe") + + def on_publish( + self, + path: str = DEFAULT_ROUTE, + aggregate: bool = False, + ) -> Callable: + """ + Register a resolver function for publish operations. + + Parameters + ---------- + path : str, optional + The channel path pattern to match for this resolver, by default "/default/*" + aggregate : bool, optional + Whether to process events in aggregate (batch) mode, by default False + + Returns + ------- + Callable + Decorator function that registers the resolver + + Examples + -------- + >>> router = Router() + >>> + >>> # Basic usage + >>> @router.on_publish(path="/notifications/new") + >>> def handle_notification(payload): + >>> # Process a single notification + >>> return {"processed": True, "notificationId": payload.get("id")} + >>> + >>> # Aggregate mode for batch processing + >>> @router.on_publish(path="/notifications/batch", aggregate=True) + >>> def handle_batch_notifications(payload): + >>> # Process multiple notifications at once + >>> results = [] + >>> for item in payload: + >>> # Process each item + >>> results.append({"processed": True, "id": item.get("id")}) + >>> return results + """ + return self._publish_registry.register(path=path, aggregate=aggregate) + + def async_on_publish( + self, + path: str = DEFAULT_ROUTE, + aggregate: bool = False, + ) -> Callable: + """ + Register an asynchronous resolver function for publish operations. + + Parameters + ---------- + path : str, optional + The channel path pattern to match for this resolver, by default "/default/*" + aggregate : bool, optional + Whether to process events in aggregate (batch) mode, by default False + + Returns + ------- + Callable + Decorator function that registers the async resolver + + Examples + -------- + >>> router = Router() + >>> + >>> # Basic async usage + >>> @router.async_on_publish(path="/messages/send") + >>> async def handle_message(event): + >>> # Perform async operations + >>> result = await database.save_message(event) + >>> return {"saved": True, "messageId": result.id} + >>> + >>> # Aggregate mode for batch processing + >>> @router.async_on_publish(path="/messages/batch", aggregate=True) + >>> async def handle_batch_messages(events): + >>> # Process multiple messages asynchronously + >>> tasks = [database.save_message(e) for e in events] + >>> results = await asyncio.gather(*tasks) + >>> return [{"saved": True, "id": r.id} for r in results] + """ + return self._async_publish_registry.register(path=path, aggregate=aggregate) + + def on_subscribe( + self, + path: str = DEFAULT_ROUTE, + ) -> Callable: + """ + Register a resolver function for subscribe operations. + + Parameters + ---------- + path : str, optional + The channel path pattern to match for this resolver, by default "/default/*" + + Returns + ------- + Callable + Decorator function that registers the resolver + + Examples + -------- + >>> router = Router() + >>> + >>> # Handle subscription request + >>> @router.on_subscribe(path="/chat/room/*") + >>> def authorize_subscription(event): + >>> # Verify if the client can subscribe to this room + >>> room_id = event.info.channel_path.split('/')[-1] + >>> user_id = event.identity.username + >>> + >>> # Check if user is allowed in this room + >>> is_allowed = check_permission(user_id, room_id) + >>> + >>> return { + >>> "allowed": is_allowed, + >>> "roomId": room_id + >>> } + """ + return self._subscribe_registry.register(path=path) + + def append_context(self, **additional_context): + """Append key=value data as routing context""" + self.context.update(**additional_context) + + def clear_context(self): + """Resets routing context""" + self.context.clear() diff --git a/aws_lambda_powertools/event_handler/events_appsync/types.py b/aws_lambda_powertools/event_handler/events_appsync/types.py new file mode 100644 index 00000000000..708e8df8a8c --- /dev/null +++ b/aws_lambda_powertools/event_handler/events_appsync/types.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, TypedDict + +if TYPE_CHECKING: + from collections.abc import Callable + + +class ResolverTypeDef(TypedDict): + """ + Type definition for resolver dictionary + Parameters + ---------- + func: Callable[..., Any] + Resolver function + aggregate: bool + Aggregation flag or method + """ + + func: Callable[..., Any] + aggregate: bool diff --git a/aws_lambda_powertools/event_handler/exception_handling.py b/aws_lambda_powertools/event_handler/exception_handling.py new file mode 100644 index 00000000000..acd8eb95bc6 --- /dev/null +++ b/aws_lambda_powertools/event_handler/exception_handling.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Mapping + +if TYPE_CHECKING: + from collections.abc import Callable + + +class ExceptionHandlerManager: + """ + A class to manage exception handlers for different exception types. + This class allows registering handler functions for specific exception types + and looking up the appropriate handler when an exception occurs. + Example usage: + ------------- + handler_manager = ExceptionHandlerManager() + @handler_manager.exception_handler(ValueError) + def handle_value_error(e): + print(f"Handling ValueError: {e}") + return "Error handled" + # To handle multiple exception types with the same handler: + @handler_manager.exception_handler([KeyError, TypeError]) + def handle_multiple_errors(e): + print(f"Handling {type(e).__name__}: {e}") + return "Multiple error types handled" + # To find and execute a handler: + try: + # some code that might raise an exception + raise ValueError("Invalid value") + except Exception as e: + handler = handler_manager.lookup_exception_handler(type(e)) + if handler: + result = handler(e) + """ + + def __init__(self): + """Initialize an empty dictionary to store exception handlers.""" + self._exception_handlers: dict[type[Exception], Callable] = {} + + def exception_handler(self, exc_class: type[Exception] | list[type[Exception]]): + """ + A decorator function that registers a handler for one or more exception types. + Parameters + ---------- + exc_class : type[Exception] | list[type[Exception]] + A single exception type or a list of exception types. + Returns + ------- + Callable + A decorator function that registers the exception handler. + """ + + def register_exception_handler(func: Callable): + if isinstance(exc_class, list): + for exp in exc_class: + self._exception_handlers[exp] = func + else: + self._exception_handlers[exc_class] = func + return func + + return register_exception_handler + + def lookup_exception_handler(self, exp_type: type) -> Callable | None: + """ + Looks up the registered exception handler for the given exception type or its base classes. + Parameters + ---------- + exp_type : type + The exception type to look up the handler for. + Returns + ------- + Callable | None + The registered exception handler function if found, otherwise None. + """ + for cls in exp_type.__mro__: + if cls in self._exception_handlers: + return self._exception_handlers[cls] + return None + + def update_exception_handlers(self, handlers: Mapping[type[Exception], Callable]) -> None: + """ + Updates the exception handlers dictionary with new handler mappings. + This method allows bulk updates of exception handlers by providing a dictionary + mapping exception types to handler functions. + Parameters + ---------- + handlers : Mapping[Type[Exception], Callable] + A dictionary mapping exception types to handler functions. + Example + ------- + >>> def handle_value_error(e): + ... print(f"Value error: {e}") + ... + >>> def handle_key_error(e): + ... print(f"Key error: {e}") + ... + >>> handler_manager.update_exception_handlers({ + ... ValueError: handle_value_error, + ... KeyError: handle_key_error + ... }) + """ + self._exception_handlers.update(handlers) + + def get_registered_handlers(self) -> dict[type[Exception], Callable]: + """ + Returns all registered exception handlers. + Returns + ------- + Dict[Type[Exception], Callable] + A dictionary mapping exception types to their handler functions. + """ + return self._exception_handlers.copy() + + def clear_handlers(self) -> None: + """ + Clears all registered exception handlers. + """ + self._exception_handlers.clear() diff --git a/aws_lambda_powertools/event_handler/exceptions.py b/aws_lambda_powertools/event_handler/exceptions.py index 4a2838275b1..e870727cfba 100644 --- a/aws_lambda_powertools/event_handler/exceptions.py +++ b/aws_lambda_powertools/event_handler/exceptions.py @@ -1,45 +1,123 @@ +from __future__ import annotations + from http import HTTPStatus class ServiceError(Exception): - """API Gateway and ALB HTTP Service Error""" + """Powertools class HTTP Service Error""" - def __init__(self, status_code: int, msg: str): + def __init__(self, status_code: int, msg: str | dict): """ Parameters ---------- status_code: int Http status code - msg: str - Error message + msg: str | dict + Error message. Can be a string or a dictionary """ self.status_code = status_code self.msg = msg class BadRequestError(ServiceError): - """API Gateway and ALB Bad Request Error (400)""" + """Powertools class Bad Request Error (400)""" - def __init__(self, msg: str): + def __init__(self, msg: str | dict): + """ + Parameters + ---------- + msg : str | dict + Error message. Can be a string or a dictionary. + """ super().__init__(HTTPStatus.BAD_REQUEST, msg) class UnauthorizedError(ServiceError): - """API Gateway and ALB Unauthorized Error (401)""" + """Powertools class Unauthorized Error (401)""" - def __init__(self, msg: str): + def __init__(self, msg: str | dict): + """ + Parameters + ---------- + msg : str | dict + Error message. Can be a string or a dictionary. + """ super().__init__(HTTPStatus.UNAUTHORIZED, msg) +class ForbiddenError(ServiceError): + """Powertools class Forbidden Error (403)""" + + def __init__(self, msg: str | dict): + """ + Parameters + ---------- + msg : str | dict + Error message. Can be a string or a dictionary. + """ + super().__init__(HTTPStatus.FORBIDDEN, msg) + + class NotFoundError(ServiceError): - """API Gateway and ALB Not Found Error (404)""" + """Powertools class Not Found Error (404)""" - def __init__(self, msg: str = "Not found"): + def __init__(self, msg: str | dict = "Not found"): + """ + Parameters + ---------- + msg : str | dict + Error message. Can be a string or a dictionary. + """ super().__init__(HTTPStatus.NOT_FOUND, msg) +class RequestTimeoutError(ServiceError): + """Powertools class Request Timeout Error (408)""" + + def __init__(self, msg: str | dict): + """ + Parameters + ---------- + msg : str | dict + Error message. Can be a string or a dictionary. + """ + super().__init__(HTTPStatus.REQUEST_TIMEOUT, msg) + + +class RequestEntityTooLargeError(ServiceError): + """Powertools class Request Entity Too Large Error (413)""" + + def __init__(self, msg: str | dict): + """ + Parameters + ---------- + msg : str | dict + Error message. Can be a string or a dictionary. + """ + super().__init__(HTTPStatus.REQUEST_ENTITY_TOO_LARGE, msg) + + class InternalServerError(ServiceError): - """API Gateway and ALB Not Found Internal Server Error (500)""" + """Powertools class Internal Server Error (500)""" - def __init__(self, message: str): + def __init__(self, message: str | dict): + """ + Parameters + ---------- + msg : str | dict + Error message. Can be a string or a dictionary. + """ super().__init__(HTTPStatus.INTERNAL_SERVER_ERROR, message) + + +class ServiceUnavailableError(ServiceError): + """Powertools class Service Unavailable Error (503)""" + + def __init__(self, msg: str | dict): + """ + Parameters + ---------- + msg : str | dict + Error message. Can be a string or a dictionary. + """ + super().__init__(HTTPStatus.SERVICE_UNAVAILABLE, msg) diff --git a/layer/layer/__init__.py b/aws_lambda_powertools/event_handler/graphql_appsync/__init__.py similarity index 100% rename from layer/layer/__init__.py rename to aws_lambda_powertools/event_handler/graphql_appsync/__init__.py diff --git a/aws_lambda_powertools/event_handler/graphql_appsync/_registry.py b/aws_lambda_powertools/event_handler/graphql_appsync/_registry.py new file mode 100644 index 00000000000..dc88d904c25 --- /dev/null +++ b/aws_lambda_powertools/event_handler/graphql_appsync/_registry.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Callable + +logger = logging.getLogger(__name__) + + +class ResolverRegistry: + def __init__(self): + self.resolvers: dict[str, dict[str, Any]] = {} + + def register( + self, + type_name: str = "*", + field_name: str | None = None, + raise_on_error: bool = False, + aggregate: bool = True, + ) -> Callable: + """Registers the resolver for field_name + + Parameters + ---------- + type_name : str + Type name + field_name : str + Field name + raise_on_error: bool + A flag indicating whether to raise an error when processing batches + with failed items. Defaults to False, which means errors are handled without raising exceptions. + aggregate: bool + A flag indicating whether the batch items should be processed at once or individually. + If True (default), the batch resolver will process all items in the batch as a single event. + If False, the batch resolver will process each item in the batch individually. + + Return + ---------- + Callable + A Callable + """ + + def _register(func) -> Callable: + logger.debug(f"Adding resolver `{func.__name__}` for field `{type_name}.{field_name}`") + self.resolvers[f"{type_name}.{field_name}"] = { + "func": func, + "raise_on_error": raise_on_error, + "aggregate": aggregate, + } + return func + + return _register + + def find_resolver(self, type_name: str, field_name: str) -> dict | None: + """Find resolver based on type_name and field_name + + Parameters + ---------- + type_name : str + Type name + field_name : str + Field name + Return + ---------- + Optional[Dict] + A dictionary with the resolver and if raise exception on error + """ + logger.debug(f"Looking for resolver for type={type_name}, field={field_name}.") + return self.resolvers.get(f"{type_name}.{field_name}", self.resolvers.get(f"*.{field_name}")) + + def merge(self, other_registry: ResolverRegistry): + """Update current registry with incoming registry + + Parameters + ---------- + other_registry : ResolverRegistry + Registry to merge from + """ + self.resolvers.update(**other_registry.resolvers) diff --git a/aws_lambda_powertools/event_handler/graphql_appsync/base.py b/aws_lambda_powertools/event_handler/graphql_appsync/base.py new file mode 100644 index 00000000000..ea03c44a3b0 --- /dev/null +++ b/aws_lambda_powertools/event_handler/graphql_appsync/base.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable + + +class BaseRouter(ABC): + """Abstract base class for Router (resolvers)""" + + @abstractmethod + def resolver(self, type_name: str = "*", field_name: str | None = None) -> Callable: + """ + Retrieve a resolver function for a specific type and field. + + Parameters + ----------- + type_name: str + The name of the type. + field_name: str, optional + The name of the field (default is None). + + Examples + -------- + ```python + from typing import Optional + + from aws_lambda_powertools.event_handler import AppSyncResolver + from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent + from aws_lambda_powertools.utilities.typing import LambdaContext + + app = AppSyncResolver() + + @app.resolver(type_name="Query", field_name="getPost") + def related_posts(event: AppSyncResolverEvent) -> Optional[list]: + return {"success": "ok"} + + def lambda_handler(event, context: LambdaContext) -> dict: + return app.resolve(event, context) + ``` + + Returns + ------- + Callable + The resolver function. + """ + raise NotImplementedError + + @abstractmethod + def batch_resolver( + self, + type_name: str = "*", + field_name: str | None = None, + raise_on_error: bool = False, + aggregate: bool = True, + ) -> Callable: + """ + Retrieve a batch resolver function for a specific type and field. + + Parameters + ----------- + type_name: str + The name of the type. + field_name: str, optional + The name of the field (default is None). + raise_on_error: bool + A flag indicating whether to raise an error when processing batches + with failed items. Defaults to False, which means errors are handled without raising exceptions. + aggregate: bool + A flag indicating whether the batch items should be processed at once or individually. + If True (default), the batch resolver will process all items in the batch as a single event. + If False, the batch resolver will process each item in the batch individually. + + Examples + -------- + ```python + from typing import Optional + + from aws_lambda_powertools.event_handler import AppSyncResolver + from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent + from aws_lambda_powertools.utilities.typing import LambdaContext + + app = AppSyncResolver() + + @app.batch_resolver(type_name="Query", field_name="getPost") + def related_posts(event: AppSyncResolverEvent, id) -> Optional[list]: + return {"post_id": id} + + def lambda_handler(event, context: LambdaContext) -> dict: + return app.resolve(event, context) + ``` + + Returns + ------- + Callable + The batch resolver function. + """ + raise NotImplementedError + + @abstractmethod + def async_batch_resolver( + self, + type_name: str = "*", + field_name: str | None = None, + raise_on_error: bool = False, + aggregate: bool = True, + ) -> Callable: + """ + Retrieve a batch resolver function for a specific type and field and runs async. + + Parameters + ----------- + type_name: str + The name of the type. + field_name: str, optional + The name of the field (default is None). + raise_on_error: bool + A flag indicating whether to raise an error when processing batches + with failed items. Defaults to False, which means errors are handled without raising exceptions. + aggregate: bool + A flag indicating whether the batch items should be processed at once or individually. + If True (default), the batch resolver will process all items in the batch as a single event. + If False, the batch resolver will process each item in the batch individually. + + Examples + -------- + ```python + from typing import Optional + + from aws_lambda_powertools.event_handler import AppSyncResolver + from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent + from aws_lambda_powertools.utilities.typing import LambdaContext + + app = AppSyncResolver() + + @app.async_batch_resolver(type_name="Query", field_name="getPost") + async def related_posts(event: AppSyncResolverEvent, id) -> Optional[list]: + return {"post_id": id} + + def lambda_handler(event, context: LambdaContext) -> dict: + return app.resolve(event, context) + ``` + + Returns + ------- + Callable + The batch resolver function. + """ + raise NotImplementedError + + @abstractmethod + def append_context(self, **additional_context) -> None: + """ + Appends context information available under any route. + + Parameters + ----------- + **additional_context: dict + Additional context key-value pairs to append. + """ + raise NotImplementedError diff --git a/aws_lambda_powertools/event_handler/graphql_appsync/exceptions.py b/aws_lambda_powertools/event_handler/graphql_appsync/exceptions.py new file mode 100644 index 00000000000..f98a75b6f17 --- /dev/null +++ b/aws_lambda_powertools/event_handler/graphql_appsync/exceptions.py @@ -0,0 +1,10 @@ +class ResolverNotFoundError(Exception): + """ + When a resolver is not found during a lookup. + """ + + +class InvalidBatchResponse(Exception): + """ + When a batch response something different from a List + """ diff --git a/aws_lambda_powertools/event_handler/graphql_appsync/router.py b/aws_lambda_powertools/event_handler/graphql_appsync/router.py new file mode 100644 index 00000000000..b05e6f276f5 --- /dev/null +++ b/aws_lambda_powertools/event_handler/graphql_appsync/router.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from aws_lambda_powertools.event_handler.graphql_appsync._registry import ResolverRegistry +from aws_lambda_powertools.event_handler.graphql_appsync.base import BaseRouter + +if TYPE_CHECKING: + from collections.abc import Callable + + from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import AppSyncResolverEvent + from aws_lambda_powertools.utilities.typing.lambda_context import LambdaContext + + +class Router(BaseRouter): + context: dict + current_batch_event: list[AppSyncResolverEvent] = [] + current_event: AppSyncResolverEvent | None = None + lambda_context: LambdaContext | None = None + + def __init__(self): + self.context = {} # early init as customers might add context before event resolution + self._resolver_registry = ResolverRegistry() + self._batch_resolver_registry = ResolverRegistry() + self._async_batch_resolver_registry = ResolverRegistry() + + def resolver(self, type_name: str = "*", field_name: str | None = None) -> Callable: + return self._resolver_registry.register(field_name=field_name, type_name=type_name) + + def batch_resolver( + self, + type_name: str = "*", + field_name: str | None = None, + raise_on_error: bool = False, + aggregate: bool = True, + ) -> Callable: + return self._batch_resolver_registry.register( + field_name=field_name, + type_name=type_name, + raise_on_error=raise_on_error, + aggregate=aggregate, + ) + + def async_batch_resolver( + self, + type_name: str = "*", + field_name: str | None = None, + raise_on_error: bool = False, + aggregate: bool = True, + ) -> Callable: + return self._async_batch_resolver_registry.register( + field_name=field_name, + type_name=type_name, + raise_on_error=raise_on_error, + aggregate=aggregate, + ) + + def append_context(self, **additional_context): + """Append key=value data as routing context""" + self.context.update(**additional_context) + + def clear_context(self): + """Resets routing context""" + self.context.clear() diff --git a/aws_lambda_powertools/event_handler/http_resolver.py b/aws_lambda_powertools/event_handler/http_resolver.py new file mode 100644 index 00000000000..da72f6fca4d --- /dev/null +++ b/aws_lambda_powertools/event_handler/http_resolver.py @@ -0,0 +1,457 @@ +from __future__ import annotations + +import base64 +import inspect +import warnings +from typing import TYPE_CHECKING, Any, Callable +from urllib.parse import parse_qs + +from aws_lambda_powertools.event_handler.api_gateway import ( + ApiGatewayResolver, + BaseRouter, + ProxyEventType, + Response, + Route, +) +from aws_lambda_powertools.event_handler.middlewares.async_utils import wrap_middleware_async +from aws_lambda_powertools.shared.headers_serializer import BaseHeadersSerializer +from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent + +if TYPE_CHECKING: + from aws_lambda_powertools.shared.cookies import Cookie + + +class HttpHeadersSerializer(BaseHeadersSerializer): + """Headers serializer for native HTTP responses.""" + + def serialize(self, headers: dict[str, str | list[str]], cookies: list[Cookie]) -> dict[str, Any]: + """Serialize headers for HTTP response format.""" + combined_headers: dict[str, str] = {} + for key, values in headers.items(): + if values is None: # pragma: no cover + continue + if isinstance(values, str): + combined_headers[key] = values + else: + combined_headers[key] = ", ".join(values) + + # Add cookies as Set-Cookie headers + cookie_headers = [str(cookie) for cookie in cookies] if cookies else [] + + return {"headers": combined_headers, "cookies": cookie_headers} + + +class HttpProxyEvent(BaseProxyEvent): + """ + A proxy event that wraps native HTTP request data. + + This allows the same route handlers to work with both Lambda and native HTTP servers. + """ + + def __init__( + self, + method: str, + path: str, + headers: dict[str, str] | None = None, + body: str | bytes | None = None, + query_string: str | None = None, + path_parameters: dict[str, str] | None = None, + request_context: dict[str, Any] | None = None, + ): + # Parse query string + query_params: dict[str, str] = {} + multi_query_params: dict[str, list[str]] = {} + + if query_string: + parsed = parse_qs(query_string, keep_blank_values=True) + multi_query_params = parsed + query_params = {k: v[-1] for k, v in parsed.items()} + + # Normalize body to string + body_str = None + if body is not None: + body_str = body.decode("utf-8") if isinstance(body, bytes) else body + + # Build the internal dict structure that BaseProxyEvent expects + data = { + "httpMethod": method.upper(), + "path": path, + "headers": headers or {}, + "body": body_str, + "isBase64Encoded": False, + "queryStringParameters": query_params, + "multiValueQueryStringParameters": multi_query_params, + "pathParameters": path_parameters or {}, + "requestContext": request_context + or { + "stage": "local", + "requestId": "local-request-id", + "http": {"method": method.upper(), "path": path}, + }, + } + + super().__init__(data) + + @classmethod + def _from_dict(cls, data: dict[str, Any]) -> HttpProxyEvent: + """Create HttpProxyEvent directly from a dict (used internally).""" + instance = object.__new__(cls) + BaseProxyEvent.__init__(instance, data) + return instance + + @classmethod + def from_asgi(cls, scope: dict[str, Any], body: bytes | None = None) -> HttpProxyEvent: + """ + Create an HttpProxyEvent from an ASGI scope dict. + + Parameters + ---------- + scope : dict + ASGI scope dictionary + body : bytes, optional + Request body + + Returns + ------- + HttpProxyEvent + Event object compatible with Powertools resolvers + """ + # Extract headers from ASGI format [(b"key", b"value"), ...] + headers: dict[str, str] = {} + for key, value in scope.get("headers", []): + header_name = key.decode("utf-8").lower() + header_value = value.decode("utf-8") + # Handle duplicate headers by joining with comma + if header_name in headers: + headers[header_name] = f"{headers[header_name]}, {header_value}" + else: + headers[header_name] = header_value + + return cls( + method=scope["method"], + path=scope["path"], + headers=headers, + body=body, + query_string=scope.get("query_string", b"").decode("utf-8"), + ) + + def header_serializer(self) -> BaseHeadersSerializer: + """Return the HTTP headers serializer.""" + return HttpHeadersSerializer() + + @property + def resolved_query_string_parameters(self) -> dict[str, list[str]]: + """Return query parameters in the format expected by OpenAPI validation.""" + return self.multi_value_query_string_parameters + + @property + def resolved_headers_field(self) -> dict[str, str]: + """Return headers in the format expected by OpenAPI validation.""" + return self.headers + + +class MockLambdaContext: + """Minimal Lambda context for HTTP adapter.""" + + function_name = "http-resolver" + memory_limit_in_mb = 128 + invoked_function_arn = "arn:aws:lambda:local:000000000000:function:http-resolver" + aws_request_id = "local-request-id" + log_group_name = "/aws/lambda/http-resolver" + log_stream_name = "local" + + def get_remaining_time_in_millis(self) -> int: # pragma: no cover + return 300000 # 5 minutes + + +class HttpResolverLocal(ApiGatewayResolver): + """ + ASGI-compatible HTTP resolver for local development and testing. + + This resolver is designed specifically for local development workflows. + It allows you to run your Powertools application locally with any ASGI server + (uvicorn, hypercorn, daphne, etc.) while maintaining full compatibility with Lambda. + + The same code works in both environments - locally via ASGI and in Lambda via the handler. + + Supports both sync and async route handlers. + + WARNING + ------- + This is intended for local development and testing only. + The API may change in future releases. Do not use in production environments. + + Example + ------- + ```python + from aws_lambda_powertools.event_handler import HttpResolverLocal + + app = HttpResolverLocal() + + @app.get("/hello/") + async def hello(name: str): + # Async handler - can use await + return {"message": f"Hello, {name}!"} + + @app.get("/sync") + def sync_handler(): + # Sync handlers also work + return {"sync": True} + + # Run locally with uvicorn: + # uvicorn app:app --reload + + # Deploy to Lambda (sync only): + # handler = app + ``` + """ + + def __init__( + self, + cors: Any = None, + debug: bool | None = None, + serializer: Callable[[dict], str] | None = None, + strip_prefixes: list[str | Any] | None = None, + enable_validation: bool = False, + ): + warnings.warn( + "HttpResolverLocal is intended for local development and testing only. " + "The API may change in future releases. Do not use in production environments.", + stacklevel=2, + ) + super().__init__( + proxy_type=ProxyEventType.APIGatewayProxyEvent, # Use REST API format internally + cors=cors, + debug=debug, + serializer=serializer, + strip_prefixes=strip_prefixes, + enable_validation=enable_validation, + ) + self._is_async_mode = False + + def _to_proxy_event(self, event: dict) -> BaseProxyEvent: + """Convert event dict to HttpProxyEvent.""" + # Create HttpProxyEvent directly from the dict data + # The dict already has queryStringParameters and multiValueQueryStringParameters + return HttpProxyEvent._from_dict(event) + + def _get_base_path(self) -> str: + """Return the base path for HTTP resolver (no stage prefix).""" + return "" + + async def _resolve_async(self) -> dict: # type: ignore[override] + """Async version of resolve that supports async handlers.""" + method = self.current_event.http_method.upper() + path = self._remove_prefix(self.current_event.path) + + registered_routes = self._static_routes + self._dynamic_routes + + for route in registered_routes: + if method != route.method: + continue + match_results = route.rule.match(path) + if match_results: + self.append_context(_route=route, _path=path) + route_keys = self._convert_matches_into_route_keys(match_results) + return await self._call_route_async(route, route_keys) + + # Handle not found + return await self._handle_not_found_async() + + async def _call_route_async(self, route: Route, route_arguments: dict[str, str]) -> dict: # type: ignore[override] + """Call route handler, supporting both sync and async handlers.""" + from aws_lambda_powertools.event_handler.api_gateway import ResponseBuilder + + try: + self._reset_processed_stack() + + # Get the route args (may be modified by validation middleware) + self.append_context(_route_args=route_arguments) + + # Run middleware chain (sync for now, handlers can be async) + response = await self._run_middleware_chain_async(route) + + response_builder: ResponseBuilder = ResponseBuilder( + response=response, + serializer=self._serializer, + route=route, + ) + + return response_builder.build(self.current_event, self._cors) + + except Exception as exc: + exc_response_builder = self._call_exception_handler(exc, route) + if exc_response_builder: + return exc_response_builder.build(self.current_event, self._cors) + raise + + async def _run_middleware_chain_async(self, route: Route) -> Response: + """Run the middleware chain, awaiting async handlers.""" + # Build middleware list + all_middlewares: list[Callable[..., Any]] = [] + + # Determine if validation should be enabled for this route + # If route has explicit enable_validation setting, use it; otherwise, use resolver's global setting + route_validation_enabled = ( + route.enable_validation if route.enable_validation is not None else self._enable_validation + ) + + if route_validation_enabled and hasattr(self, "_request_validation_middleware"): + all_middlewares.append(self._request_validation_middleware) + + all_middlewares.extend(self._router_middlewares + route.middlewares) + + if route_validation_enabled and hasattr(self, "_response_validation_middleware"): + all_middlewares.append(self._response_validation_middleware) + + # Create the final handler that calls the route function + async def final_handler(app): + route_args = app.context.get("_route_args", {}) + result = route.func(**route_args) + + # Await if coroutine + if inspect.iscoroutine(result): + result = await result + + return self._to_response(result) + + # Build middleware chain from end to start + next_handler = final_handler + + for middleware in reversed(all_middlewares): + next_handler = wrap_middleware_async(middleware, next_handler) + + return await next_handler(self) + + async def _handle_not_found_async(self, method: str = "", path: str = "") -> dict: # type: ignore[override] + """Handle 404 responses, using custom not_found handler if registered.""" + from http import HTTPStatus + + from aws_lambda_powertools.event_handler.api_gateway import ResponseBuilder + from aws_lambda_powertools.event_handler.exceptions import NotFoundError + + # Check for custom not_found handler + custom_not_found_handler = self.exception_handler_manager.lookup_exception_handler(NotFoundError) + if custom_not_found_handler: + response = custom_not_found_handler(NotFoundError()) + else: + response = Response( + status_code=HTTPStatus.NOT_FOUND.value, + content_type="application/json", + body={"statusCode": HTTPStatus.NOT_FOUND.value, "message": "Not found"}, + ) + + response_builder: ResponseBuilder = ResponseBuilder( + response=response, + serializer=self._serializer, + route=None, + ) + + return response_builder.build(self.current_event, self._cors) + + async def asgi_handler(self, scope: dict, receive: Callable, send: Callable) -> None: + """ + ASGI interface - allows running with uvicorn/hypercorn/etc. + + Parameters + ---------- + scope : dict + ASGI connection scope + receive : Callable + ASGI receive function + send : Callable + ASGI send function + """ + if scope["type"] == "lifespan": + # Handle lifespan events (startup/shutdown) + while True: + message = await receive() + if message["type"] == "lifespan.startup": + await send({"type": "lifespan.startup.complete"}) + elif message["type"] == "lifespan.shutdown": + await send({"type": "lifespan.shutdown.complete"}) + return + + if scope["type"] != "http": + return + + # Read request body + body = b"" + while True: + message = await receive() + body += message.get("body", b"") + if not message.get("more_body", False): + break + + # Convert ASGI scope to HttpProxyEvent + event = HttpProxyEvent.from_asgi(scope, body) + + # Create mock Lambda context + context: Any = MockLambdaContext() + + # Set up resolver state (similar to resolve()) + BaseRouter.current_event = self._to_proxy_event(event._data) + BaseRouter.lambda_context = context + + self._is_async_mode = True + + try: + # Use async resolve + response = await self._resolve_async() + finally: + self._is_async_mode = False + self.clear_context() + + # Send HTTP response + await self._send_response(send, response) + + async def __call__( # type: ignore[override] + self, + scope: dict, + receive: Callable, + send: Callable, + ) -> None: + """ASGI interface - allows running with uvicorn/hypercorn/etc.""" + await self.asgi_handler(scope, receive, send) + + async def _send_response(self, send: Callable, response: dict) -> None: + """Send the response via ASGI.""" + status_code = response.get("statusCode", 200) + headers = response.get("headers", {}) + cookies = response.get("cookies", []) + body = response.get("body", "") + is_base64 = response.get("isBase64Encoded", False) + + # Build headers list for ASGI + header_list: list[tuple[bytes, bytes]] = [] + for key, value in headers.items(): + header_list.append((key.lower().encode(), str(value).encode())) + + # Add Set-Cookie headers + for cookie in cookies: + header_list.append((b"set-cookie", str(cookie).encode())) + + # Send response start + await send( + { + "type": "http.response.start", + "status": status_code, + "headers": header_list, + }, + ) + + # Prepare body + if is_base64: + body_bytes = base64.b64decode(body) + elif isinstance(body, str): + body_bytes = body.encode("utf-8") + else: # pragma: no cover + body_bytes = body + + # Send response body + await send( + { + "type": "http.response.body", + "body": body_bytes, + }, + ) diff --git a/aws_lambda_powertools/event_handler/lambda_function_url.py b/aws_lambda_powertools/event_handler/lambda_function_url.py index 6978b29f451..cbd92a00b6e 100644 --- a/aws_lambda_powertools/event_handler/lambda_function_url.py +++ b/aws_lambda_powertools/event_handler/lambda_function_url.py @@ -1,11 +1,14 @@ -from typing import Callable, Dict, List, Optional +from __future__ import annotations + +from typing import TYPE_CHECKING -from aws_lambda_powertools.event_handler import CORSConfig from aws_lambda_powertools.event_handler.api_gateway import ( ApiGatewayResolver, ProxyEventType, ) -from aws_lambda_powertools.utilities.data_classes import LambdaFunctionUrlEvent + +if TYPE_CHECKING: + from aws_lambda_powertools.utilities.data_classes import LambdaFunctionUrlEvent class LambdaFunctionUrlResolver(ApiGatewayResolver): @@ -45,12 +48,10 @@ def lambda_handler(event, context): """ current_event: LambdaFunctionUrlEvent + _proxy_event_type = ProxyEventType.LambdaFunctionUrlEvent - def __init__( - self, - cors: Optional[CORSConfig] = None, - debug: Optional[bool] = None, - serializer: Optional[Callable[[Dict], str]] = None, - strip_prefixes: Optional[List[str]] = None, - ): - super().__init__(ProxyEventType.LambdaFunctionUrlEvent, cors, debug, serializer, strip_prefixes) + def _get_base_path(self) -> str: + stage = self.current_event.request_context.stage + if stage and stage != "$default" and self.current_event.request_context.http.method.startswith(f"/{stage}"): + return f"/{stage}" + return "" diff --git a/aws_lambda_powertools/event_handler/middlewares/__init__.py b/aws_lambda_powertools/event_handler/middlewares/__init__.py new file mode 100644 index 00000000000..068ce9c04b7 --- /dev/null +++ b/aws_lambda_powertools/event_handler/middlewares/__init__.py @@ -0,0 +1,3 @@ +from aws_lambda_powertools.event_handler.middlewares.base import BaseMiddlewareHandler, NextMiddleware + +__all__ = ["BaseMiddlewareHandler", "NextMiddleware"] 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/base.py b/aws_lambda_powertools/event_handler/middlewares/base.py new file mode 100644 index 00000000000..538338abf7c --- /dev/null +++ b/aws_lambda_powertools/event_handler/middlewares/base.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Generic, Protocol + +from aws_lambda_powertools.event_handler.types import EventHandlerInstance + +if TYPE_CHECKING: + from aws_lambda_powertools.event_handler.api_gateway import Response + + +class NextMiddleware(Protocol): + def __call__(self, app: EventHandlerInstance) -> Response: + """Protocol for callback regardless of next_middleware(app), get_response(app) etc""" + ... + + def __name__(self) -> str: # noqa A003 + """Protocol for name of the Middleware""" + ... + + +class BaseMiddlewareHandler(ABC, Generic[EventHandlerInstance]): + """Base implementation for Middlewares to run code before and after in a chain. + + + This is the middleware handler function where middleware logic is implemented. + The next middleware handler is represented by `next_middleware`, returning a Response object. + + Example + -------- + + **Correlation ID Middleware** + + ```python + import requests + + from aws_lambda_powertools import Logger + from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response + from aws_lambda_powertools.event_handler.middlewares import BaseMiddlewareHandler, NextMiddleware + + app = APIGatewayRestResolver() + logger = Logger() + + + class CorrelationIdMiddleware(BaseMiddlewareHandler): + def __init__(self, header: str): + super().__init__() + self.header = header + + def handler(self, app: APIGatewayRestResolver, next_middleware: NextMiddleware) -> Response: + # BEFORE logic + request_id = app.current_event.request_context.request_id + correlation_id = app.current_event.headers.get(self.header, request_id) + + # Call next middleware or route handler ('/todos') + response = next_middleware(app) + + # AFTER logic + response.headers[self.header] = correlation_id + + return response + + + @app.get("/todos", middlewares=[CorrelationIdMiddleware(header="x-correlation-id")]) + def get_todos(): + todos: requests.Response = requests.get("https://jsonplaceholder.typicode.com/todos") + todos.raise_for_status() + + # for brevity, we'll limit to the first 10 only + return {"todos": todos.json()[:10]} + + + @logger.inject_lambda_context + def lambda_handler(event, context): + return app.resolve(event, context) + + ``` + + """ + + @abstractmethod + def handler(self, app: EventHandlerInstance, next_middleware: NextMiddleware) -> Response: + """ + The Middleware Handler + + Parameters + ---------- + app: EventHandlerInstance + An instance of an Event Handler that implements ApiGatewayResolver + next_middleware: NextMiddleware + The next middleware handler in the chain + + Returns + ------- + Response + The response from the next middleware handler in the chain + + """ + raise NotImplementedError() + + @property + def __name__(self) -> str: # noqa A003 + return str(self.__class__.__name__) + + def __call__(self, app: EventHandlerInstance, next_middleware: NextMiddleware) -> Response: + """ + The Middleware handler function. + + Parameters + ---------- + app: ApiGatewayResolver + An instance of an Event Handler that implements ApiGatewayResolver + next_middleware: NextMiddleware + The next middleware handler in the chain + + Returns + ------- + Response + The response from the next middleware handler in the chain + """ + return self.handler(app, next_middleware) diff --git a/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py b/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py new file mode 100644 index 00000000000..470a19e6c54 --- /dev/null +++ b/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py @@ -0,0 +1,755 @@ +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 + +from pydantic import BaseModel +from typing_extensions import get_args, get_origin + +from aws_lambda_powertools.event_handler.middlewares import BaseMiddlewareHandler +from aws_lambda_powertools.event_handler.openapi.compat import ( + _model_dump, + _normalize_errors, + _regenerate_error_with_loc, + field_annotation_is_sequence, + get_missing_field_error, + lenient_issubclass, +) +from aws_lambda_powertools.event_handler.openapi.dependant import is_scalar_field +from aws_lambda_powertools.event_handler.openapi.encoders import jsonable_encoder +from aws_lambda_powertools.event_handler.openapi.exceptions import ( + RequestUnsupportedContentType, + RequestValidationError, + ResponseValidationError, +) +from aws_lambda_powertools.event_handler.openapi.params import Param, UploadFile +from aws_lambda_powertools.event_handler.openapi.types import UnionType + +if TYPE_CHECKING: + from pydantic.fields import FieldInfo + + from aws_lambda_powertools.event_handler import Response + from aws_lambda_powertools.event_handler.api_gateway import Route + from aws_lambda_powertools.event_handler.middlewares import NextMiddleware + from aws_lambda_powertools.event_handler.openapi.compat import ModelField + from aws_lambda_powertools.event_handler.openapi.types import IncEx + from aws_lambda_powertools.event_handler.types import EventHandlerInstance + +logger = logging.getLogger(__name__) + +# Constants +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): + """ + OpenAPI request validation middleware - validates only incoming requests. + + This middleware should be used first in the middleware chain to validate + requests before they reach user middlewares. + """ + + def __init__(self): + """Initialize the request validation middleware.""" + pass + + def handler(self, app: EventHandlerInstance, next_middleware: NextMiddleware) -> Response: + logger.debug("OpenAPIRequestValidationMiddleware handler") + + route: Route = app.context["_route"] + + values: dict[str, Any] = {} + errors: list[Any] = [] + + # Process path values, which can be found on the route_args + path_values, path_errors = _request_params_to_args( + route.dependant.path_params, + app.context["_route_args"], + ) + + # Normalize query values before validate this + query_string = _normalize_multi_params( + app.current_event.resolved_query_string_parameters, + route.dependant.query_params, + ) + + # Process query values + query_values, query_errors = _request_params_to_args( + route.dependant.query_params, + query_string, + ) + + # Normalize header values before validate this + headers = _normalize_multi_params( + app.current_event.resolved_headers_field, + route.dependant.header_params, + ) + + # Process header values + header_values, header_errors = _request_params_to_args( + route.dependant.header_params, + 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) + 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: + (body_values, body_errors) = _request_body_to_args( + required_params=route.dependant.body_params, + received_body=self._get_body(app), + ) + values.update(body_values) + errors.extend(body_errors) + + if errors: + # Raise the validation errors + raise RequestValidationError(_normalize_errors(errors)) + + # Re-write the route_args with the validated values + app.context["_route_args"] = values + + # Call the next middleware + return next_middleware(app) + + def _get_body(self, app: EventHandlerInstance) -> dict[str, Any]: + """ + Get the request body from the event, and parse it according to content type. + """ + content_type = app.current_event.headers.get("content-type", "").strip() + + # Handle JSON content + if not content_type or content_type.startswith(APPLICATION_JSON_CONTENT_TYPE): + return self._parse_json_data(app) + + # Handle URL-encoded form data + 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( + "Unsupported content type", + errors=[ + { + "type": "unsupported_content_type", + "loc": ("body",), + "msg": f"Unsupported content type: {content_type}", + "input": {}, + "ctx": {}, + }, + ], + ) + + def _parse_json_data(self, app: EventHandlerInstance) -> dict[str, Any]: + """Parse JSON data from the request body.""" + try: + return app.current_event.json_body + except json.JSONDecodeError as e: + raise RequestValidationError( + [ + { + "type": "json_invalid", + "loc": ("body", e.pos), + "msg": "JSON decode error", + "input": {}, + "ctx": {"error": e.msg}, + }, + ], + body=e.doc, + ) from e + + def _parse_form_data(self, app: EventHandlerInstance) -> dict[str, Any]: + """Parse URL-encoded form data from the request body.""" + try: + body = app.current_event.decoded_body or "" + # NOTE: Keep values as lists; we'll normalize per-field later based on the expected type. + # This avoids breaking List[...] fields when only a single value is provided. + parsed = parse_qs(body, keep_blank_values=True) + return parsed + + except Exception as e: # pragma: no cover + raise RequestValidationError( # pragma: no cover + [ + { + "type": "form_invalid", + "loc": ("body",), + "msg": "Form data parsing error", + "input": {}, + "ctx": {"error": str(e)}, + }, + ], + ) 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): + """ + OpenAPI response validation middleware - validates only outgoing responses. + + This middleware should be used last in the middleware chain to validate + responses only from route handlers, not from user middlewares. + """ + + def __init__( + self, + validation_serializer: Callable[[Any], str] | None = None, + has_response_validation_error: bool = False, + ): + """ + Initialize the response validation middleware. + + Parameters + ---------- + validation_serializer : Callable, optional + Optional serializer to use when serializing the response for validation. + Use it when you have a custom type that cannot be serialized by the default jsonable_encoder. + + has_response_validation_error: bool, optional + Optional flag used to distinguish between payload and validation errors. + By setting this flag to True, ResponseValidationError will be raised if response could not be validated. + """ + self._validation_serializer = validation_serializer + self._has_response_validation_error = has_response_validation_error + + def handler(self, app: EventHandlerInstance, next_middleware: NextMiddleware) -> Response: + logger.debug("OpenAPIResponseValidationMiddleware handler") + + route: Route = app.context["_route"] + + # Call the next middleware (should be the route handler) + response = next_middleware(app) + + # Process the response + return self._handle_response(route=route, response=response) + + def _handle_response(self, *, route: Route, response: Response): + field = route.dependant.return_param + + if field is None: + if not response.is_json(): + return response + else: + # 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_content, + has_route_custom_response_validation=route.custom_response_validation_http_code is not None, + ) + + return response + + def _serialize_response_with_validation( + self, + *, + field: ModelField, + response_content: Any, + include: IncEx | None = None, + exclude: IncEx | None = None, + by_alias: bool = True, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + has_route_custom_response_validation: bool = False, + ) -> Any: + """ + Serialize the response content according to the field type. + """ + errors: list[dict[str, Any]] = [] + value = _validate_field(field=field, value=response_content, loc=("response",), existing_errors=errors) + if errors: + # route-level validation must take precedence over app-level + if has_route_custom_response_validation: + raise ResponseValidationError( + errors=_normalize_errors(errors), + body=response_content, + source="route", + ) + if self._has_response_validation_error: + raise ResponseValidationError(errors=_normalize_errors(errors), body=response_content, source="app") + + raise RequestValidationError(errors=_normalize_errors(errors), body=response_content) + + if hasattr(field, "serialize"): + return field.serialize( + value, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + return jsonable_encoder( + value, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + custom_serializer=self._validation_serializer, + ) + + def _prepare_response_content( + self, + res: Any, + *, + exclude_unset: bool, + exclude_defaults: bool = False, + exclude_none: bool = False, + ) -> Any: + """ + Prepares the response content for serialization. + """ + if isinstance(res, BaseModel): # pragma: no cover + return _model_dump( # pragma: no cover + res, + by_alias=True, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + elif isinstance(res, list): # pragma: no cover + return [ # pragma: no cover + self._prepare_response_content(item, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults) + for item in res + ] + elif isinstance(res, dict): # pragma: no cover + return { # pragma: no cover + k: self._prepare_response_content(v, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults) + for k, v in res.items() + } + elif dataclasses.is_dataclass(res): # pragma: no cover + return dataclasses.asdict(res) # type: ignore[arg-type] # pragma: no cover + return res # pragma: no cover + + +def _request_params_to_args( + required_params: Sequence[ModelField], + received_params: Mapping[str, Any], +) -> tuple[dict[str, Any], list[dict[str, Any]]]: + """ + Convert the request params to a dictionary of values using validation, and returns a list of errors. + """ + values: dict[str, Any] = {} + errors: list[dict[str, Any]] = [] + + for field in required_params: + field_info = field.field_info + + # To ensure early failure, we check if it's not an instance of Param. + if not isinstance(field_info, Param): + raise AssertionError(f"Expected Param field_info, got {field_info}") + + loc = (field_info.in_.value, field.alias) + value = received_params.get(field.alias) + + # If we don't have a value, see if it's required or has a default + if value is None: + _handle_missing_field_value(field, values, errors, loc) + continue + + # Finally, validate the value + values[field.name] = _validate_field(field=field, value=value, loc=loc, existing_errors=errors) + + return values, errors + + +def _request_body_to_args( + required_params: list[ModelField], + received_body: dict[str, Any] | None, +) -> tuple[dict[str, Any], list[dict[str, Any]]]: + """ + Convert the request body to a dictionary of values using validation, and returns a list of errors. + """ + values: dict[str, Any] = {} + errors: list[dict[str, Any]] = [] + + received_body, field_alias_omitted = _get_embed_body( + field=required_params[0], + required_params=required_params, + received_body=received_body, + ) + + for field in required_params: + loc = _get_body_field_location(field, field_alias_omitted) + value = _extract_field_value_from_body(field, received_body, loc, errors) + + # If we don't have a value, see if it's required or has a default + if value is None: + _handle_missing_field_value(field, values, errors, loc) + continue + + value = _normalize_field_value(value=value, field_info=field.field_info) + + # 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 + + +def _get_body_field_location(field: ModelField, field_alias_omitted: bool) -> tuple[str, ...]: + """Get the location tuple for a body field based on whether the field alias is omitted.""" + if field_alias_omitted: + return ("body",) + return ("body", field.alias) + + +def _extract_field_value_from_body( + field: ModelField, + received_body: dict[str, Any] | None, + loc: tuple[str, ...], + errors: list[dict[str, Any]], +) -> Any | None: + """Extract field value from the received body, handling potential AttributeError.""" + if received_body is None: + return None + + try: + return received_body.get(field.alias) + except AttributeError: + errors.append(get_missing_field_error(loc)) + return None + + +def _handle_missing_field_value( + field: ModelField, + values: dict[str, Any], + errors: list[dict[str, Any]], + loc: tuple[str, ...], +) -> None: + """Handle the case when a field value is missing.""" + if field.required: + errors.append(get_missing_field_error(loc)) + else: + values[field.name] = field.get_default() + + +def _is_or_contains_sequence(annotation: Any) -> bool: + """ + Check if annotation is a sequence or Union/RootModel containing a sequence. + + This function handles complex type annotations like: + - List[Model] - direct sequence + - Union[Model, List[Model]] - checks if any Union member is a sequence + - Optional[List[Model]] - Union[List[Model], None] + - RootModel[List[Model]] - checks if the RootModel wraps a sequence + - Optional[RootModel[List[Model]]] - Union member that is a RootModel + - RootModel[Union[Model, List[Model]]] - RootModel wrapping a Union with a sequence + """ + # Direct sequence check + if field_annotation_is_sequence(annotation): + return True + + # Check Union members — recurse so we catch RootModel inside Union + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + for arg in get_args(annotation): + if _is_or_contains_sequence(arg): + return True + + # Check if it's a RootModel wrapping a sequence (or Union containing a sequence) + if lenient_issubclass(annotation, BaseModel) and getattr(annotation, "__pydantic_root_model__", False): + if hasattr(annotation, "model_fields") and "root" in annotation.model_fields: + root_annotation = annotation.model_fields["root"].annotation + return _is_or_contains_sequence(root_annotation) + + return False + + +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: + return value[0] + + return value + + +def _validate_field( + *, + field: ModelField, + value: Any, + loc: tuple[str, ...], + existing_errors: list[dict[str, Any]], +): + """ + Validate a field, and append any errors to the existing_errors list. + """ + validated_value, errors = field.validate(value=value, loc=loc) + + if isinstance(errors, list): + processed_errors = _regenerate_error_with_loc(errors=errors, loc_prefix=()) + existing_errors.extend(processed_errors) + elif errors: + existing_errors.append(errors) + + return validated_value + + +def _get_embed_body( + *, + field: ModelField, + required_params: list[ModelField], + received_body: dict[str, Any] | None, +) -> tuple[dict[str, Any] | None, bool]: + field_info = field.field_info + embed = getattr(field_info, "embed", None) + + # If the field is an embed, and the field alias is omitted, we need to wrap the received body in the field alias. + field_alias_omitted = len(required_params) == 1 and not embed + if field_alias_omitted: + received_body = {field.alias: received_body} + + return received_body, field_alias_omitted + + +def _normalize_multi_params( + input_dict: MutableMapping[str, Any], + params: Sequence[ModelField], +) -> MutableMapping[str, Any]: + """ + Extract and normalize query string or header parameters with Pydantic model support. + + Parameters + ---------- + input_dict: MutableMapping[str, Any] + A dictionary containing the initial query string or header parameters. + params: Sequence[ModelField] + A sequence of ModelField objects representing parameters. + + Returns + ------- + MutableMapping[str, Any] + A dictionary containing the processed parameters with normalized values. + """ + for param in params: + if is_scalar_field(param): + _process_scalar_param(input_dict, param) + elif lenient_issubclass(param.field_info.annotation, BaseModel): + _process_model_param(input_dict, param) + return input_dict + + +def _process_scalar_param(input_dict: MutableMapping[str, Any], param: ModelField) -> None: + """Process a scalar parameter by normalizing single-item lists.""" + try: + value = input_dict[param.alias] + if isinstance(value, list) and len(value) == 1: + input_dict[param.alias] = value[0] + except KeyError: + pass + + +def _process_model_param(input_dict: MutableMapping[str, Any], param: ModelField) -> None: + """Process a Pydantic model parameter by extracting model fields.""" + model_class = cast(type[BaseModel], param.field_info.annotation) + + model_data = {} + for field_name, field_info in model_class.model_fields.items(): + field_alias = field_info.alias or field_name + value = _get_param_value(input_dict, field_alias, field_name, model_class) + + if value is not None: + model_data[field_alias] = _normalize_field_value(value=value, field_info=field_info) + + input_dict[param.alias] = model_data + + +def _get_param_value( + input_dict: MutableMapping[str, Any], + field_alias: str, + field_name: str, + model_class: type[BaseModel], +) -> Any: + """Get parameter value, checking both alias and field name if needed.""" + value = input_dict.get(field_alias) + if value is not None: + return value + + if model_class.model_config.get("validate_by_name") or model_class.model_config.get("populate_by_name"): + 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/middlewares/schema_validation.py b/aws_lambda_powertools/event_handler/middlewares/schema_validation.py new file mode 100644 index 00000000000..c24fff0cbe0 --- /dev/null +++ b/aws_lambda_powertools/event_handler/middlewares/schema_validation.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +from aws_lambda_powertools.event_handler.exceptions import BadRequestError, InternalServerError +from aws_lambda_powertools.event_handler.middlewares import BaseMiddlewareHandler, NextMiddleware +from aws_lambda_powertools.utilities.validation import validate +from aws_lambda_powertools.utilities.validation.exceptions import InvalidSchemaFormatError, SchemaValidationError + +if TYPE_CHECKING: + from aws_lambda_powertools.event_handler.api_gateway import Response + from aws_lambda_powertools.event_handler.types import EventHandlerInstance + +logger = logging.getLogger(__name__) + + +class SchemaValidationMiddleware(BaseMiddlewareHandler): + """Middleware to validate API request and response against JSON Schema using the [Validation utility](https://docs.powertools.aws.dev/lambda/python/latest/utilities/validation/). + + Example + -------- + **Validating incoming event** + + ```python + import requests + + from aws_lambda_powertools import Logger + from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response + from aws_lambda_powertools.event_handler.middlewares import BaseMiddlewareHandler, NextMiddleware + from aws_lambda_powertools.event_handler.middlewares.schema_validation import SchemaValidationMiddleware + + app = APIGatewayRestResolver() + logger = Logger() + json_schema_validation = SchemaValidationMiddleware(inbound_schema=INCOMING_JSON_SCHEMA) + + + @app.get("/todos", middlewares=[json_schema_validation]) + def get_todos(): + todos: requests.Response = requests.get("https://jsonplaceholder.typicode.com/todos") + todos.raise_for_status() + + # for brevity, we'll limit to the first 10 only + return {"todos": todos.json()[:10]} + + + @logger.inject_lambda_context + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ + + def __init__( + self, + inbound_schema: dict, + inbound_formats: dict | None = None, + outbound_schema: dict | None = None, + outbound_formats: dict | None = None, + ): + """See [Validation utility](https://docs.powertools.aws.dev/lambda/python/latest/utilities/validation/) docs for examples on all parameters. + + Parameters + ---------- + inbound_schema : dict + JSON Schema to validate incoming event + inbound_formats : dict | None, optional + Custom formats containing a key (e.g. int64) and a value expressed as regex or callback returning bool, by default None + JSON Schema to validate outbound event, by default None + outbound_formats : dict | None, optional + Custom formats containing a key (e.g. int64) and a value expressed as regex or callback returning bool, by default None + """ # noqa: E501 + super().__init__() + self.inbound_schema = inbound_schema + self.inbound_formats = inbound_formats + self.outbound_schema = outbound_schema + self.outbound_formats = outbound_formats + + def bad_response(self, error: SchemaValidationError) -> Response: + message: str = f"Bad Response: {error.message}" + logger.debug(message) + raise BadRequestError(message) + + def bad_request(self, error: SchemaValidationError) -> Response: + message: str = f"Bad Request: {error.message}" + logger.debug(message) + raise BadRequestError(message) + + def bad_config(self, error: InvalidSchemaFormatError) -> Response: + logger.debug(f"Invalid Schema Format: {error}") + raise InternalServerError("Internal Server Error") + + def handler(self, app: EventHandlerInstance, next_middleware: NextMiddleware) -> Response: + """Validates incoming JSON payload (body) against JSON Schema provided. + + Parameters + ---------- + app : EventHandlerInstance + An instance of an Event Handler + next_middleware : NextMiddleware + Callable to get response from the next middleware or route handler in the chain + + Returns + ------- + Response + It can return three types of response objects + + - Original response: Propagates HTTP response returned from the next middleware if validation succeeds + - HTTP 400: Payload or response failed JSON Schema validation + - HTTP 500: JSON Schema provided has incorrect format + """ + try: + validate(event=app.current_event.json_body, schema=self.inbound_schema, formats=self.inbound_formats) + except SchemaValidationError as error: + return self.bad_request(error) + except InvalidSchemaFormatError as error: + return self.bad_config(error) + + result = next_middleware(app) + + if self.outbound_formats is not None: + try: + validate(event=result.body, schema=self.inbound_schema, formats=self.inbound_formats) + except SchemaValidationError as error: + return self.bad_response(error) + except InvalidSchemaFormatError as error: + return self.bad_config(error) + + return result diff --git a/aws_lambda_powertools/event_handler/openapi/__init__.py b/aws_lambda_powertools/event_handler/openapi/__init__.py new file mode 100644 index 00000000000..f2881bcc75a --- /dev/null +++ b/aws_lambda_powertools/event_handler/openapi/__init__.py @@ -0,0 +1,9 @@ +"""OpenAPI module for AWS Lambda Powertools.""" + +from aws_lambda_powertools.event_handler.openapi.exceptions import OpenAPIMergeError +from aws_lambda_powertools.event_handler.openapi.merge import OpenAPIMerge + +__all__ = [ + "OpenAPIMerge", + "OpenAPIMergeError", +] diff --git a/aws_lambda_powertools/event_handler/openapi/compat.py b/aws_lambda_powertools/event_handler/openapi/compat.py new file mode 100644 index 00000000000..c37cd2a979a --- /dev/null +++ b/aws_lambda_powertools/event_handler/openapi/compat.py @@ -0,0 +1,367 @@ +# mypy: ignore-errors +from __future__ import annotations + +from collections import deque +from collections.abc import Mapping, Sequence +from copy import copy +from dataclasses import dataclass, is_dataclass +from typing import TYPE_CHECKING, Any, Deque, FrozenSet, List, Set, Tuple, Union + +from pydantic import BaseModel, TypeAdapter, ValidationError, create_model + +# Importing from internal libraries in Pydantic may introduce potential risks, as these internal libraries +# are not part of the public API and may change without notice in future releases. +# We use this for forward reference, as it allows us to handle forward references in type annotations. +from pydantic._internal._typing_extra import eval_type_lenient +from pydantic._internal._utils import lenient_issubclass +from pydantic.fields import FieldInfo as PydanticFieldInfo +from pydantic_core import PydanticUndefined, PydanticUndefinedType +from typing_extensions import Annotated, Literal, get_args, get_origin + +from aws_lambda_powertools.event_handler.openapi.types import UnionType + +if TYPE_CHECKING: + from pydantic.fields import FieldInfo + from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue + + from aws_lambda_powertools.event_handler.openapi.types import IncEx, ModelNameMap + +Undefined = PydanticUndefined +Required = PydanticUndefined +UndefinedType = PydanticUndefinedType + +evaluate_forwardref = eval_type_lenient + +sequence_annotation_to_type = { + Sequence: list, + List: list, + list: list, + Tuple: tuple, + tuple: tuple, + Set: set, + set: set, + FrozenSet: frozenset, + frozenset: frozenset, + Deque: deque, + deque: deque, +} + +sequence_types = tuple(sequence_annotation_to_type.keys()) + +RequestErrorModel: type[BaseModel] = create_model("Request") + + +class ErrorWrapper(Exception): + pass + + +@dataclass +class ModelField: + field_info: FieldInfo + name: str + mode: Literal["validation", "serialization"] = "validation" + + @property + def alias(self) -> str: + value = self.field_info.alias + return value if value is not None else self.name + + @property + def required(self) -> bool: + return self.field_info.is_required() + + @property + def default(self) -> Any: + return self.get_default() + + @property + def type_(self) -> Any: + return self.field_info.annotation + + def __post_init__(self) -> None: + # If the field_info.annotation is already an Annotated type with discriminator metadata, + # use it directly instead of wrapping it again + annotation = self.field_info.annotation + if ( + get_origin(annotation) is Annotated + and hasattr(self.field_info, "discriminator") + and self.field_info.discriminator is not None + ): + self._type_adapter: TypeAdapter[Any] = TypeAdapter(annotation) + else: + self._type_adapter: TypeAdapter[Any] = TypeAdapter( + Annotated[annotation, self.field_info], + ) + + def get_default(self) -> Any: + if self.field_info.is_required(): + return Undefined + return self.field_info.get_default(call_default_factory=True) + + def serialize( + self, + value: Any, + *, + mode: Literal["json", "python"] = "json", + include: IncEx | None = None, + exclude: IncEx | None = None, + by_alias: bool = True, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + ) -> Any: + return self._type_adapter.dump_python( + value, + mode=mode, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + def validate( + self, + value: Any, + *, + loc: tuple[int | str, ...] = (), + ) -> tuple[Any, list[dict[str, Any]] | None]: + try: + return (self._type_adapter.validate_python(value, from_attributes=True), None) + except ValidationError as exc: + return None, _regenerate_error_with_loc(errors=exc.errors(), loc_prefix=loc) + + def __hash__(self) -> int: + # Each ModelField is unique for our purposes + return id(self) + + +def get_schema_from_model_field( + *, + field: ModelField, + model_name_map: ModelNameMap, + field_mapping: dict[ + tuple[ModelField, Literal["validation", "serialization"]], + JsonSchemaValue, + ], +) -> dict[str, Any]: + json_schema = field_mapping[(field, field.mode)] + if "$ref" not in json_schema: + # MAINTENANCE: remove when deprecating Pydantic v1 + # Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207 + json_schema["title"] = field.field_info.title or field.alias.title().replace("_", " ") + return json_schema + + +def get_definitions( + *, + fields: list[ModelField], + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, +) -> tuple[ + dict[ + tuple[ModelField, Literal["validation", "serialization"]], + dict[str, Any], + ], + dict[str, dict[str, Any]], +]: + inputs = [(field, field.mode, field._type_adapter.core_schema) for field in fields] + field_mapping, definitions = schema_generator.generate_definitions(inputs=inputs) + + return field_mapping, definitions + + +def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap: + return {} + + +def get_annotation_from_field_info(annotation: Any, field_info: FieldInfo, field_name: str) -> Any: + return annotation + + +def model_rebuild(model: type[BaseModel]) -> None: + model.model_rebuild() + + +def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: + # Create a shallow copy of the field_info to preserve its type and all attributes + new_field = copy(field_info) + + # Recursively extract all metadata from nested Annotated types + def extract_metadata(ann: Any) -> tuple[Any, list[Any]]: + """Extract base type and all non-FieldInfo metadata from potentially nested Annotated types.""" + if get_origin(ann) is not Annotated: + return ann, [] + + args = get_args(ann) + base_type = args[0] + metadata = list(args[1:]) + + # If base type is also Annotated, recursively extract its metadata + if get_origin(base_type) is Annotated: + inner_base, inner_metadata = extract_metadata(base_type) + all_metadata = [m for m in inner_metadata + metadata if not isinstance(m, PydanticFieldInfo)] + return inner_base, all_metadata + else: + constraint_metadata = [m for m in metadata if not isinstance(m, PydanticFieldInfo)] + return base_type, constraint_metadata + + # Extract base type and constraints + base_type, constraints = extract_metadata(annotation) + + # Set the annotation with base type and all constraint metadata + # Use tuple unpacking for Python 3.10+ compatibility + if constraints: + new_field.annotation = Annotated[(base_type, *constraints)] + else: + new_field.annotation = base_type + + return new_field + + +def get_missing_field_error(loc: tuple[str, ...]) -> dict[str, Any]: + error = ValidationError.from_exception_data( + "Field required", + [{"type": "missing", "loc": loc, "input": {}}], + ).errors()[0] + error["input"] = None + return error + + +def is_scalar_field(field: ModelField) -> bool: + from aws_lambda_powertools.event_handler.openapi.params import Body + + return field_annotation_is_scalar(field.field_info.annotation) and not isinstance(field.field_info, Body) + + +def is_scalar_sequence_field(field: ModelField) -> bool: + return field_annotation_is_scalar_sequence(field.field_info.annotation) + + +def is_sequence_field(field: ModelField) -> bool: + return field_annotation_is_sequence(field.field_info.annotation) + + +def is_bytes_field(field: ModelField) -> bool: + return is_bytes_or_nonable_bytes_annotation(field.type_) + + +def is_bytes_sequence_field(field: ModelField) -> bool: + return is_bytes_sequence_annotation(field.type_) + + +def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: + origin_type = get_origin(field.field_info.annotation) or field.field_info.annotation + if not issubclass(origin_type, sequence_types): # type: ignore[arg-type] + raise AssertionError(f"Expected sequence type, got {origin_type}") + return sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return] + + +def _normalize_errors(errors: Sequence[Any]) -> list[dict[str, Any]]: + return errors # type: ignore[return-value] + + +def create_body_model(*, fields: Sequence[ModelField], model_name: str) -> type[BaseModel]: + field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields} + model: type[BaseModel] = create_model(model_name, **field_params) + return model + + +def _model_dump(model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any) -> Any: + return model.model_dump(mode=mode, **kwargs) + + +def model_json(model: BaseModel, **kwargs: Any) -> Any: + return model.model_dump_json(**kwargs) + + +# Common code for both versions + + +def field_annotation_is_complex(annotation: type[Any] | None) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + return any(field_annotation_is_complex(arg) for arg in get_args(annotation)) + + return ( + _annotation_is_complex(annotation) + or _annotation_is_complex(origin) + or hasattr(origin, "__pydantic_core_schema__") + or hasattr(origin, "__get_pydantic_core_schema__") + ) + + +def field_annotation_is_scalar(annotation: Any) -> bool: + return annotation is Ellipsis or not field_annotation_is_complex(annotation) + + +def field_annotation_is_sequence(annotation: type[Any] | None) -> bool: + return _annotation_is_sequence(annotation) or _annotation_is_sequence(get_origin(annotation)) + + +def field_annotation_is_scalar_sequence(annotation: type[Any] | None) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + at_least_one_scalar_sequence = False + for arg in get_args(annotation): + if field_annotation_is_scalar_sequence(arg): + at_least_one_scalar_sequence = True + continue + elif not field_annotation_is_scalar(arg): + return False + return at_least_one_scalar_sequence + return field_annotation_is_sequence(annotation) and all( + field_annotation_is_scalar(sub_annotation) for sub_annotation in get_args(annotation) + ) + + +def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool: + if lenient_issubclass(annotation, bytes): + return True + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + for arg in get_args(annotation): + if lenient_issubclass(arg, bytes): + return True + return False + + +def is_bytes_sequence_annotation(annotation: Any) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + at_least_one = False + for arg in get_args(annotation): + if is_bytes_sequence_annotation(arg): + at_least_one = True + break + return at_least_one + return field_annotation_is_sequence(annotation) and all( + is_bytes_or_nonable_bytes_annotation(sub_annotation) for sub_annotation in get_args(annotation) + ) + + +def value_is_sequence(value: Any) -> bool: + return isinstance(value, sequence_types) and not isinstance(value, (str, bytes)) # type: ignore[arg-type] + + +def _annotation_is_complex(annotation: type[Any] | None) -> bool: + return ( + lenient_issubclass(annotation, (BaseModel, Mapping)) # Keep it to UploadFile + or _annotation_is_sequence(annotation) + or is_dataclass(annotation) + ) + + +def _annotation_is_sequence(annotation: type[Any] | None) -> bool: + if lenient_issubclass(annotation, (str, bytes)): + return False + return lenient_issubclass(annotation, sequence_types) + + +def _regenerate_error_with_loc(*, errors: Sequence[Any], loc_prefix: tuple[str | int, ...]) -> list[dict[str, Any]]: + updated_loc_errors: list[Any] = [ + {**err, "loc": loc_prefix + err.get("loc", ())} for err in _normalize_errors(errors) + ] + + return updated_loc_errors diff --git a/aws_lambda_powertools/event_handler/openapi/config.py b/aws_lambda_powertools/event_handler/openapi/config.py new file mode 100644 index 00000000000..387388a88d2 --- /dev/null +++ b/aws_lambda_powertools/event_handler/openapi/config.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +from aws_lambda_powertools.event_handler.openapi.constants import ( + DEFAULT_API_VERSION, + DEFAULT_OPENAPI_TITLE, + DEFAULT_OPENAPI_VERSION, +) + +if TYPE_CHECKING: + from aws_lambda_powertools.event_handler.openapi.models import ( + Contact, + ExternalDocumentation, + License, + SecurityScheme, + Server, + Tag, + ) + + +@dataclass +class OpenAPIConfig: + """Configuration class for OpenAPI specification. + + This class holds all the necessary configuration parameters to generate an OpenAPI specification. + + Parameters + ---------- + title: str + The title of the application. + version: str + The version of the OpenAPI document (which is distinct from the OpenAPI Specification version or the API + openapi_version: str, default = "3.1.0" + The version of the OpenAPI Specification (which the document uses). + summary: str, optional + A short summary of what the application does. + description: str, optional + A verbose explanation of the application behavior. + tags: list[Tag, str], optional + A list of tags used by the specification with additional metadata. + servers: list[Server], optional + An array of Server Objects, which provide connectivity information to a target server. + terms_of_service: str, optional + A URL to the Terms of Service for the API. MUST be in the format of a URL. + contact: Contact, optional + The contact information for the exposed API. + license_info: License, optional + The license information for the exposed API. + security_schemes: dict[str, SecurityScheme]], optional + A declaration of the security schemes available to be used in the specification. + security: list[dict[str, list[str]]], optional + A declaration of which security mechanisms are applied globally across the API. + external_documentation: ExternalDocumentation, optional + A link to external documentation for the API. + openapi_extensions: Dict[str, Any], optional + Additional OpenAPI extensions as a dictionary. + + Example + -------- + >>> config = OpenAPIConfig( + ... title="My API", + ... version="1.0.0", + ... description="This is my API description", + ... contact=Contact(name="API Support", email="support@example.com"), + ... servers=[Server(url="https://api.example.com/v1")] + ... ) + """ + + title: str = DEFAULT_OPENAPI_TITLE + version: str = DEFAULT_API_VERSION + openapi_version: str = DEFAULT_OPENAPI_VERSION + summary: str | None = None + description: str | None = None + tags: list[Tag | str] | None = None + servers: list[Server] | None = None + terms_of_service: str | None = None + contact: Contact | None = None + license_info: License | None = None + security_schemes: dict[str, SecurityScheme] | None = None + security: list[dict[str, list[str]]] | None = None + external_documentation: ExternalDocumentation | None = None + openapi_extensions: dict[str, Any] | None = None diff --git a/aws_lambda_powertools/event_handler/openapi/constants.py b/aws_lambda_powertools/event_handler/openapi/constants.py new file mode 100644 index 00000000000..7c5b938920a --- /dev/null +++ b/aws_lambda_powertools/event_handler/openapi/constants.py @@ -0,0 +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 new file mode 100644 index 00000000000..1e7f4327602 --- /dev/null +++ b/aws_lambda_powertools/event_handler/openapi/dependant.py @@ -0,0 +1,405 @@ +from __future__ import annotations + +import inspect +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, + evaluate_forwardref, + is_scalar_field, +) +from aws_lambda_powertools.event_handler.openapi.params import ( + Body, + Dependant, + File, + Form, + Param, + ParamTypes, + 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 + + from pydantic import BaseModel + +""" +This turns the opaque function signature into typed, validated models. + +It relies on Pydantic's typing and validation to achieve this in a declarative way. +This enables traits like autocompletion, validation, and declarative structure vs imperative parsing. + +This code parses an OpenAPI operation handler function signature into Pydantic models. It uses inspect to get the +signature and regex to parse path parameters. Each parameter is analyzed to extract its type annotation and generate +a corresponding Pydantic field, which are added to a Dependant model. Return values are handled similarly. + +This modeling allows for type checking, automatic parameter name/location/type extraction, and input validation - +turning the opaque signature into validated models. It relies on Pydantic's typing and validation for a declarative +approach over imperative parsing, enabling autocompletion, validation and structure. +""" + + +def add_param_to_fields( + *, + field: ModelField, + dependant: Dependant, +) -> None: + """ + Adds a parameter to the list of parameters in the dependant model. + + Parameters + ---------- + field: ModelField + The field to add + dependant: Dependant + The dependant model to add the field to + + """ + field_info = cast(Param, field.field_info) + + # Dictionary to map ParamTypes to their corresponding lists in dependant + param_type_map = { + ParamTypes.path: dependant.path_params, + ParamTypes.query: dependant.query_params, + ParamTypes.header: dependant.header_params, + ParamTypes.cookie: dependant.cookie_params, + } + + # Check if field_info.in_ is a valid key in param_type_map and append the field to the corresponding list + # or raise an exception if it's not a valid key. + if field_info.in_ in param_type_map: + param_type_map[field_info.in_].append(field) + else: + raise AssertionError(f"Unsupported param type: {field_info.in_}") + + +def get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any: + """ + Evaluates a type annotation, which can be a string or a ForwardRef. + """ + if isinstance(annotation, str): + annotation = ForwardRef(annotation) + annotation = evaluate_forwardref(annotation, globalns, globalns) + return annotation + + +def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: + """ + Returns a typed signature for a callable, resolving forward references. + + Parameters + ---------- + call: Callable[..., Any] + The callable to get the signature for + + Returns + ------- + inspect.Signature + The typed signature + """ + signature = inspect.signature(call) + + # Gets the global namespace for the call. This is used to resolve forward references. + globalns = getattr(call, "__globals__", {}) + + typed_params = [ + inspect.Parameter( + name=param.name, + kind=param.kind, + default=param.default, + annotation=get_typed_annotation(param.annotation, globalns), + ) + for param in signature.parameters.values() + ] + + # If the return annotation is not empty, add it to the signature. + if signature.return_annotation is not inspect.Signature.empty: + return_param = inspect.Parameter( + name="Return", + kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, + default=None, + annotation=get_typed_annotation(signature.return_annotation, globalns), + ) + return inspect.Signature(typed_params, return_annotation=return_param.annotation) + else: + return inspect.Signature(typed_params) + + +def get_path_param_names(path: str) -> set[str]: + """ + Returns the path parameter names from a path template. Those are the strings between { and }. + + Parameters + ---------- + path: str + The path template + + Returns + ------- + set[str] + The path parameter names + + """ + return set(re.findall("{(.*?)}", path)) + + +def get_dependant( + *, + path: str, + call: Callable[..., Any], + name: str | None = None, + responses: dict[int, OpenAPIResponse] | None = None, +) -> Dependant: + """ + Returns a dependant model for a handler function. A dependant model is a model that contains + the parameters and return value of a handler function. + + Parameters + ---------- + path: str + The path template + call: Callable[..., Any] + The handler function + name: str, optional + The name of the handler function + responses: list[dict[int, OpenAPIResponse]], optional + The list of extra responses for the handler function + + Returns + ------- + Dependant + The dependant model for the handler function + """ + path_param_names = get_path_param_names(path) + endpoint_signature = get_typed_signature(call) + signature_params = endpoint_signature.parameters + + dependant = Dependant( + call=call, + name=name, + path=path, + ) + + # 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 + + # Analyze the parameter to get the Pydantic field. + param_field = analyze_param( + param_name=param_name, + annotation=param.annotation, + value=param.default, + is_path_param=is_path_param, + is_response_param=False, + ) + if param_field is None: + raise AssertionError(f"Parameter field is None for param: {param_name}") + + if is_body_param(param_field=param_field, is_path_param=is_path_param): + dependant.body_params.append(param_field) + else: + add_param_to_fields(field=param_field, dependant=dependant) + + _add_return_annotation(dependant, endpoint_signature) + _add_extra_responses(dependant, responses) + + return dependant + + +def _add_extra_responses(dependant: Dependant, responses: dict[int, OpenAPIResponse] | None): + # Also add the optional extra responses to the dependant model. + if not responses: + return + + for response in responses.values(): + for schema in response.get("content", {}).values(): + if "model" in schema: + response_field = analyze_param( + param_name="return", + annotation=cast(OpenAPIResponseContentModel, schema)["model"], + value=None, + is_path_param=False, + is_response_param=True, + ) + if response_field is None: + raise AssertionError("Response field is None for response model") + + dependant.response_extra_models.append(response_field) + + +def _add_return_annotation(dependant: Dependant, endpoint_signature: inspect.Signature): + # If the return annotation is not empty, add it to the dependant model. + return_annotation = endpoint_signature.return_annotation + if return_annotation is not inspect.Signature.empty: + param_field = analyze_param( + param_name="return", + annotation=return_annotation, + value=None, + is_path_param=False, + is_response_param=True, + ) + if param_field is None: + raise AssertionError("Param field is None for return annotation") + + dependant.return_param = param_field + + +def is_body_param(*, param_field: ModelField, is_path_param: bool) -> bool: + """ + Returns whether a parameter is a request body parameter, by checking if it is a scalar field or a body field. + + Parameters + ---------- + param_field: ModelField + The parameter field + is_path_param: bool + Whether the parameter is a path parameter + + Returns + ------- + bool + Whether the parameter is a request body parameter + """ + if is_path_param: + if not is_scalar_field(field=param_field): + raise AssertionError("Path params must be of one of the supported types") + return False + elif is_scalar_field(field=param_field): + return False + elif isinstance(param_field.field_info, Param): + return False + else: + if not isinstance(param_field.field_info, Body): + raise AssertionError(f"Param: {param_field.name} can only be a request body, use Body()") + return True + + +def get_flat_params(dependant: Dependant) -> list[ModelField]: + """ + Get a list of all the parameters from a Dependant object. + + Parameters + ---------- + dependant : Dependant + The Dependant object containing the parameters. + + Returns + ------- + list[ModelField] + A list of ModelField objects containing the flat parameters from the Dependant object. + + """ + flat_dependant = get_flat_dependant(dependant) + return ( + flat_dependant.path_params + + flat_dependant.query_params + + flat_dependant.header_params + + flat_dependant.cookie_params + ) + + +def get_body_field(*, dependant: Dependant, name: str) -> ModelField | None: + """ + Get the Body field for a given Dependant object. + """ + + flat_dependant = get_flat_dependant(dependant) + if not flat_dependant.body_params: + return None + + first_param = flat_dependant.body_params[0] + field_info = first_param.field_info + + # Handle the case where there is only one body parameter and it is embedded + embed = getattr(field_info, "embed", None) + body_param_names_set = {param.name for param in flat_dependant.body_params} + if len(body_param_names_set) == 1 and not embed: + return first_param + + # If one field requires to embed, all have to be embedded + for param in flat_dependant.body_params: + setattr(param.field_info, "embed", True) # noqa: B010 + + # Generate a custom body model for this endpoint + model_name = "Body_" + name + body_model = create_body_model(fields=flat_dependant.body_params, model_name=model_name) + + required = any(True for f in flat_dependant.body_params if f.required) + + body_field_info, body_field_info_kwargs = get_body_field_info( + body_model=body_model, + flat_dependant=flat_dependant, + required=required, + ) + + final_field = create_response_field( + name="body", + type_=body_model, + required=required, + alias="body", + field_info=body_field_info(**body_field_info_kwargs), + ) + + return final_field + + +def get_body_field_info( + *, + body_model: type[BaseModel], + flat_dependant: Dependant, + required: bool, +) -> tuple[type[Body], dict[str, Any]]: + """ + Get the Body field info and kwargs for a given body model. + """ + + body_field_info_kwargs: dict[str, Any] = {"annotation": body_model, "alias": "body"} + + if not required: + body_field_info_kwargs["default"] = None + + 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" + else: + body_field_info = Body + + body_param_media_types = [ + f.field_info.media_type for f in flat_dependant.body_params if isinstance(f.field_info, Body) + ] + if len(set(body_param_media_types)) == 1: + body_field_info_kwargs["media_type"] = body_param_media_types[0] + + return body_field_info, body_field_info_kwargs diff --git a/aws_lambda_powertools/event_handler/openapi/encoders.py b/aws_lambda_powertools/event_handler/openapi/encoders.py new file mode 100644 index 00000000000..d1b8861bdde --- /dev/null +++ b/aws_lambda_powertools/event_handler/openapi/encoders.py @@ -0,0 +1,369 @@ +from __future__ import annotations + +import dataclasses +import datetime +from collections import defaultdict, deque +from decimal import Decimal +from enum import Enum +from pathlib import PurePath +from re import Pattern +from types import GeneratorType +from typing import TYPE_CHECKING, Any +from uuid import UUID + +from pydantic import BaseModel +from pydantic.types import SecretBytes, SecretStr + +from aws_lambda_powertools.event_handler.openapi.compat import _model_dump + +if TYPE_CHECKING: + from collections.abc import Callable + + from aws_lambda_powertools.event_handler.openapi.types import IncEx + +from aws_lambda_powertools.event_handler.openapi.exceptions import SerializationError + +""" +This module contains the encoders used by jsonable_encoder to convert Python objects to JSON serializable data types. +""" + + +def jsonable_encoder( # noqa: PLR0911 + obj: Any, + include: IncEx | None = None, + exclude: IncEx | None = None, + by_alias: bool = True, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + custom_serializer: Callable[[Any], str] | None = None, +) -> Any: + """ + JSON encodes an arbitrary Python object into JSON serializable data types. + + This is a modified version of fastapi.encoders.jsonable_encoder that supports + encoding of pydantic.BaseModel objects. + + Parameters + ---------- + obj : Any + The object to encode + include : IncEx | None, optional + A set or dictionary of strings that specifies which properties should be included, by default None, + meaning everything is included + exclude : IncEx | None, optional + A set or dictionary of strings that specifies which properties should be excluded, by default None, + meaning nothing is excluded + by_alias : bool, optional + Whether field aliases should be respected, by default True + exclude_unset : bool, optional + Whether fields that are not set should be excluded, by default False + exclude_defaults : bool, optional + Whether fields that are equal to their default value (as specified in the model) should be excluded, + by default False + exclude_none : bool, optional + Whether fields that are equal to None should be excluded, by default False + custom_serializer : Callable, optional + A custom serializer to use for encoding the object, when everything else fails. + + Returns + ------- + Any + The JSON serializable data types + """ + if include is not None and not isinstance(include, (set, dict)): + include = set(include) + if exclude is not None and not isinstance(exclude, (set, dict)): + exclude = set(exclude) + + try: + # Pydantic models + if isinstance(obj, BaseModel): + return _dump_base_model( + obj=obj, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_none=exclude_none, + exclude_defaults=exclude_defaults, + ) + + # Dataclasses + if dataclasses.is_dataclass(obj): + obj_dict = dataclasses.asdict(obj) # type: ignore[arg-type] + return jsonable_encoder( + obj_dict, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + custom_serializer=custom_serializer, + ) + + # 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) + + 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): + return _dump_dict( + obj=obj, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_none=exclude_none, + custom_serializer=custom_serializer, + ) + + # Sequences + if isinstance(obj, (list, set, frozenset, GeneratorType, tuple, deque)): + return _dump_sequence( + obj=obj, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_none=exclude_none, + exclude_defaults=exclude_defaults, + exclude_unset=exclude_unset, + custom_serializer=custom_serializer, + ) + + # Use custom serializer if present + if custom_serializer: + return custom_serializer(obj) + + # Default + return _dump_other( + obj=obj, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_none=exclude_none, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + custom_serializer=custom_serializer, + ) + except ValueError as exc: + raise SerializationError( + f"Unable to serialize the object {obj} as it is not a supported type. Error details: {exc}", + "See: https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/#serializing-objects", + ) from exc + + +def _dump_base_model( + *, + obj: Any, + include: IncEx | None = None, + exclude: IncEx | None = None, + by_alias: bool = True, + exclude_unset: bool = False, + exclude_none: bool = False, + exclude_defaults: bool = False, +): + """ + Dump a BaseModel object to a dict, using the same parameters as jsonable_encoder + """ + obj_dict = _model_dump( + obj, + mode="json", + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_none=exclude_none, + exclude_defaults=exclude_defaults, + ) + if "__root__" in obj_dict: + obj_dict = obj_dict["__root__"] + + return jsonable_encoder( + obj_dict, + exclude_none=exclude_none, + exclude_defaults=exclude_defaults, + ) + + +def _dump_dict( + *, + obj: Any, + include: IncEx | None = None, + exclude: IncEx | None = None, + by_alias: bool = True, + exclude_unset: bool = False, + exclude_none: bool = False, + custom_serializer: Callable[[Any], str] | None = None, +) -> dict[str, Any]: + """ + Dump a dict to a dict, using the same parameters as jsonable_encoder + + Parameters + ---------- + custom_serializer : Callable, optional + A custom serializer to use for encoding the object, when everything else fails. + """ + encoded_dict = {} + allowed_keys = set(obj.keys()) + if include is not None: + allowed_keys &= set(include) + if exclude is not None: + allowed_keys -= set(exclude) + for key, value in obj.items(): + if ( + (not isinstance(key, str) or not key.startswith("_sa")) + and (value is not None or not exclude_none) + and key in allowed_keys + ): + encoded_key = jsonable_encoder( + key, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_none=exclude_none, + custom_serializer=custom_serializer, + ) + encoded_value = jsonable_encoder( + value, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_none=exclude_none, + custom_serializer=custom_serializer, + ) + encoded_dict[encoded_key] = encoded_value + return encoded_dict + + +def _dump_sequence( + *, + obj: Any, + include: IncEx | None = None, + exclude: IncEx | None = None, + by_alias: bool = True, + exclude_unset: bool = False, + exclude_none: bool = False, + exclude_defaults: bool = False, + custom_serializer: Callable[[Any], str] | None = None, +) -> list[Any]: + """ + Dump a sequence to a list, using the same parameters as jsonable_encoder. + """ + encoded_list = [] + for item in obj: + encoded_list.append( + jsonable_encoder( + item, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + custom_serializer=custom_serializer, + ), + ) + return encoded_list + + +def _dump_other( + *, + obj: Any, + include: IncEx | None = None, + exclude: IncEx | None = None, + by_alias: bool = True, + exclude_unset: bool = False, + exclude_none: bool = False, + exclude_defaults: bool = False, + custom_serializer: Callable[[Any], str] | None = None, +) -> Any: + """ + Dump an object to a hashable object, using the same parameters as jsonable_encoder + """ + try: + data = dict(obj) + except Exception as e: + errors: list[Exception] = [e] + try: + data = vars(obj) + except Exception as e: + errors.append(e) + raise ValueError(errors) from e + return jsonable_encoder( + data, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + custom_serializer=custom_serializer, + ) + + +def iso_format(o: datetime.date | datetime.time) -> str: + """ + ISO format for date and time + """ + return o.isoformat() + + +def decimal_encoder(dec_value: Decimal) -> int | float: + """ + Encodes a Decimal as int of there's no exponent, otherwise float + + This is useful when we use ConstrainedDecimal to represent Numeric(x,0) + where an integer (but not int typed) is used. Encoding this as a float + results in failed round-tripping between encode and parse. + + >>> decimal_encoder(Decimal("1.0")) + 1.0 + + >>> decimal_encoder(Decimal("1")) + 1 + """ + if dec_value.as_tuple().exponent >= 0: # type: ignore[operator] + return int(dec_value) + else: + return float(dec_value) + + +# 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, + datetime.time: iso_format, + datetime.timedelta: lambda td: td.total_seconds(), + Decimal: decimal_encoder, + Enum: lambda o: o.value, + PurePath: str, + Pattern: lambda o: o.pattern, + SecretBytes: str, + SecretStr: str, + UUID: str, +} + + +# Generates a mapping of encoders to a tuple of classes that they can encode +def generate_encoders_by_class_tuples( + type_encoder_map: dict[Any, Callable[[Any], Any]], +) -> dict[Callable[[Any], Any], tuple[Any, ...]]: + encoders: dict[Callable[[Any], Any], tuple[Any, ...]] = defaultdict(tuple) + for type_, encoder in type_encoder_map.items(): + encoders[encoder] += (type_,) + return encoders + + +# Mapping of encoders to a tuple of classes that they can encode +_encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE) diff --git a/aws_lambda_powertools/event_handler/openapi/exceptions.py b/aws_lambda_powertools/event_handler/openapi/exceptions.py new file mode 100644 index 00000000000..593f529ae5f --- /dev/null +++ b/aws_lambda_powertools/event_handler/openapi/exceptions.py @@ -0,0 +1,61 @@ +from collections.abc import Sequence +from typing import Any, Literal + + +class ValidationException(Exception): + """ + Base exception for all validation errors + """ + + def __init__(self, errors: Sequence[Any]) -> None: + self._errors = errors + + def errors(self) -> Sequence[Any]: + return self._errors + + +class RequestValidationError(ValidationException): + """ + Raised when the request body does not match the OpenAPI schema + """ + + def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None: + super().__init__(errors) + self.body = body + + +class ResponseValidationError(ValidationException): + """ + Raised when the response body does not match the OpenAPI schema + """ + + def __init__(self, errors: Sequence[Any], *, body: Any = None, source: Literal["route", "app"] = "app") -> None: + super().__init__(errors) + self.body = body + self.source = source + + +class SerializationError(Exception): + """ + Base exception for all encoding errors + """ + + +class SchemaValidationError(ValidationException): + """ + Raised when the OpenAPI schema validation fails + """ + + +class OpenAPIMergeError(Exception): + """Exception raised when there's a conflict during OpenAPI merge.""" + + +class RequestUnsupportedContentType(NotImplementedError, ValidationException): + """Exception raised when trying to read request body data, with unknown headers""" + + # REVIEW: This inheritance is for backwards compatibility. + # Just inherit from ValidationException in Powertools V4 + def __init__(self, msg: str, errors: Sequence[Any]) -> None: + NotImplementedError.__init__(self, msg) + ValidationException.__init__(self, errors) diff --git a/aws_lambda_powertools/event_handler/openapi/merge.py b/aws_lambda_powertools/event_handler/openapi/merge.py new file mode 100644 index 00000000000..4b7f51cab1c --- /dev/null +++ b/aws_lambda_powertools/event_handler/openapi/merge.py @@ -0,0 +1,543 @@ +"""OpenAPI Merge - Generate unified OpenAPI schema from multiple Lambda handlers.""" + +from __future__ import annotations + +import ast +import fnmatch +import importlib.util +import logging +import sys +import warnings +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal + +from aws_lambda_powertools.event_handler.openapi.config import OpenAPIConfig +from aws_lambda_powertools.event_handler.openapi.constants import ( + DEFAULT_API_VERSION, + DEFAULT_OPENAPI_TITLE, + DEFAULT_OPENAPI_VERSION, +) +from aws_lambda_powertools.event_handler.openapi.exceptions import OpenAPIMergeError + +if TYPE_CHECKING: + from aws_lambda_powertools.event_handler.openapi.models import ( + Contact, + ExternalDocumentation, + License, + SecurityScheme, + Server, + Tag, + ) + +logger = logging.getLogger(__name__) + +ConflictStrategy = Literal["warn", "error", "first", "last"] + +RESOLVER_CLASSES = frozenset( + { + "APIGatewayRestResolver", + "APIGatewayHttpResolver", + "ALBResolver", + "LambdaFunctionUrlResolver", + "VPCLatticeResolver", + "VPCLatticeV2Resolver", + "BedrockAgentResolver", + "ApiGatewayResolver", + }, +) + + +def _is_resolver_call(node: ast.expr) -> bool: + """Check if an AST node is a call to a resolver class.""" + if not isinstance(node, ast.Call): + return False + 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: + return True + return False + + +def _file_has_resolver(file_path: Path, resolver_name: str) -> bool: + """Check if a Python file contains a resolver instance using AST.""" + try: + source = file_path.read_text(encoding="utf-8") + tree = ast.parse(source, filename=str(file_path)) + except (SyntaxError, UnicodeDecodeError): + return False + + for node in ast.walk(tree): + targets: list[ast.expr] = [] + value: ast.expr | None = None + if isinstance(node, ast.Assign): + 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)) + + for pattern in exclude_patterns: + if pattern.startswith("**/"): + sub_pattern = pattern[3:] + if fnmatch.fnmatch(relative_str, pattern) or fnmatch.fnmatch(file_path.name, sub_pattern): + return True + clean_pattern = sub_pattern.replace("/**", "").replace("/*", "") + for part in file_path.relative_to(root).parts: + if fnmatch.fnmatch(part, clean_pattern): + return True + elif fnmatch.fnmatch(relative_str, pattern) or fnmatch.fnmatch(file_path.name, pattern): + return True + return False + + +def _get_glob_pattern(pat: str, recursive: bool) -> str: + """Get the glob pattern based on recursive flag.""" + if recursive and not pat.startswith("**/"): + return f"**/{pat}" + if not recursive and pat.startswith("**/"): + return pat[3:] + return pat + + +def _discover_resolver_files( + path: str | Path, + pattern: str | list[str], + exclude: list[str], + resolver_name: str, + recursive: bool = False, +) -> list[Path]: + """Discover Python files containing resolver instances.""" + root = Path(path).resolve() + if not root.exists(): + raise FileNotFoundError(f"Path does not exist: {root}") + + patterns = [pattern] if isinstance(pattern, str) else pattern + found_files: set[Path] = set() + + for pat in patterns: + glob_pattern = _get_glob_pattern(pat, recursive) + for file_path in root.glob(glob_pattern): + if ( + file_path.is_file() + and not _is_excluded(file_path, root, exclude) + and _file_has_resolver(file_path, resolver_name) + ): + found_files.add(file_path) + + return sorted(found_files) + + +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: + raise ImportError(f"Cannot load module from {file_path}") + + module = importlib.util.module_from_spec(spec) + 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 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 + + +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 + + +class OpenAPIMerge: + """ + Discover and merge OpenAPI schemas from multiple Lambda handlers. + + 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 + + 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__( + self, + *, + title: str = DEFAULT_OPENAPI_TITLE, + version: str = DEFAULT_API_VERSION, + openapi_version: str = DEFAULT_OPENAPI_VERSION, + summary: str | None = None, + description: str | None = None, + tags: list[Tag | str] | None = None, + servers: list[Server] | None = None, + terms_of_service: str | None = None, + contact: Contact | None = None, + license_info: License | None = None, + security_schemes: dict[str, SecurityScheme] | None = None, + security: list[dict[str, list[str]]] | None = None, + external_documentation: ExternalDocumentation | None = None, + openapi_extensions: dict[str, Any] | None = None, + on_conflict: ConflictStrategy = "warn", + ): + self._config = OpenAPIConfig( + title=title, + version=version, + openapi_version=openapi_version, + summary=summary, + description=description, + tags=tags, + servers=servers, + terms_of_service=terms_of_service, + contact=contact, + license_info=license_info, + security_schemes=security_schemes, + security=security, + external_documentation=external_documentation, + openapi_extensions=openapi_extensions, + ) + 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, + path: str | Path, + pattern: str | list[str] = "handler.py", + exclude: list[str] | None = None, + resolver_name: str = "app", + recursive: bool = False, + project_root: str | Path | None = None, + ) -> list[Path]: + """Discover resolver files and their dependent handler files. + + Parameters + ---------- + path : str | Path + Directory to search for resolver files. + pattern : str | list[str] + Glob pattern(s) to match handler files. + 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: + """Add a specific file to be included in the merge. + + Note: Must be called before get_openapi_schema(). Adding files after + schema generation will not affect the cached result. + """ + path = Path(file_path).resolve() + if path not in self._discovered_files: + self._discovered_files.append(path) + if resolver_name: + self._resolver_name = resolver_name + + def add_schema(self, schema: dict[str, Any]) -> None: + """Add a pre-generated OpenAPI schema to be merged. + + Note: Must be called before get_openapi_schema(). Adding schemas after + schema generation will not affect the cached result. + """ + self._schemas.append(_model_to_dict(schema)) + + @property + def discovered_files(self) -> list[Path]: + """Get the list of discovered resolver files.""" + return self._discovered_files.copy() + + @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()} + + def get_openapi_schema(self) -> dict[str, Any]: + """Generate the merged OpenAPI schema.""" + if self._cached_schema is not None: + return self._cached_schema + + for file_path in self._discovered_files: + try: + 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: + 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 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) + + def _merge_schemas(self) -> dict[str, Any]: + """Merge all schemas into a single OpenAPI schema.""" + cfg = self._config + + 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": "/"}], + } + + self._add_optional_info_fields(merged, cfg) + + merged_paths: dict[str, Any] = {} + merged_components: dict[str, dict[str, Any]] = {} + + for schema in self._schemas: + self._merge_paths(schema.get("paths", {}), merged_paths) + self._merge_components(schema.get("components", {}), merged_components) + + if cfg.security_schemes: + merged_components.setdefault("securitySchemes", {}).update(cfg.security_schemes) + + if merged_paths: + merged["paths"] = merged_paths + if merged_components: + merged["components"] = merged_components + + if merged_tags := self._merge_tags(): + merged["tags"] = merged_tags + + return merged + + def _add_optional_info_fields(self, merged: dict[str, Any], cfg: OpenAPIConfig) -> None: + """Add optional fields from config to the merged schema.""" + if cfg.summary: + merged["info"]["summary"] = cfg.summary + if cfg.description: + merged["info"]["description"] = cfg.description + if cfg.terms_of_service: + merged["info"]["termsOfService"] = cfg.terms_of_service + if cfg.contact: + merged["info"]["contact"] = _model_to_dict(cfg.contact) + if cfg.license_info: + merged["info"]["license"] = _model_to_dict(cfg.license_info) + if cfg.security: + merged["security"] = cfg.security + if cfg.external_documentation: + merged["externalDocs"] = _model_to_dict(cfg.external_documentation) + if cfg.openapi_extensions: + merged.update(cfg.openapi_extensions) + + def _merge_paths(self, source_paths: dict[str, Any], target: dict[str, Any]) -> None: + """Merge paths from source into target.""" + for path, path_item in source_paths.items(): + if path not in target: + target[path] = path_item + else: + for method, operation in path_item.items(): + if method not in target[path]: + target[path][method] = operation + else: + self._handle_conflict(method, path, target, operation) + + def _handle_conflict(self, method: str, path: str, target: dict, operation: Any) -> None: + """Handle path/method conflict based on strategy.""" + msg = f"Conflict: {method.upper()} {path} is defined in multiple schemas" + if self._on_conflict == "error": + raise OpenAPIMergeError(msg) + elif self._on_conflict == "warn": + logger.warning(f"{msg}. Keeping first definition.") + elif self._on_conflict == "last": + 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.""" + for component_type, components in source.items(): + target.setdefault(component_type, {}).update(components) + + def _merge_tags(self) -> list[dict[str, Any]]: + """Merge tags from config and schemas.""" + tags_map: dict[str, dict[str, Any]] = {} + + for tag in self._config.tags or []: + if isinstance(tag, str): + tags_map[tag] = {"name": tag} + else: + tag_dict = _model_to_dict(tag) + tags_map[tag_dict["name"]] = tag_dict + + 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} + + return list(tags_map.values()) diff --git a/aws_lambda_powertools/event_handler/openapi/models.py b/aws_lambda_powertools/event_handler/openapi/models.py new file mode 100644 index 00000000000..53becd3f870 --- /dev/null +++ b/aws_lambda_powertools/event_handler/openapi/models.py @@ -0,0 +1,486 @@ +# ruff: noqa: FA100 +from enum import Enum +from typing import Any, Dict, List, Literal, Optional, Set, Union + +from pydantic import AnyUrl, BaseModel, ConfigDict, Field, model_validator +from typing_extensions import Annotated + +from aws_lambda_powertools.event_handler.openapi.compat import model_rebuild +from aws_lambda_powertools.event_handler.openapi.exceptions import SchemaValidationError + +MODEL_CONFIG_ALLOW = ConfigDict(extra="allow") +MODEL_CONFIG_IGNORE = ConfigDict(extra="ignore") + +""" +The code defines Pydantic models for the various OpenAPI objects like OpenAPI, PathItem, Operation, Parameter etc. +These models can be used to parse OpenAPI JSON/YAML files into Python objects, or generate OpenAPI from Python data. +""" + + +class OpenAPIExtensions(BaseModel): + """ + This class serves as a Pydantic proxy model to add OpenAPI extensions. + + OpenAPI extensions are arbitrary fields, so we remove openapi_extensions when dumping + and add only the provided value in the schema. + """ + + openapi_extensions: Optional[Dict[str, Any]] = None + + # If the 'openapi_extensions' field is present in the 'values' dictionary, + # And if the extension starts with x- (must respect the RFC) + # update the 'values' dictionary with the contents of 'openapi_extensions', + # and then remove the 'openapi_extensions' field from the 'values' dictionary + model_config = {"extra": "allow"} + + @model_validator(mode="before") + def serialize_openapi_extension_v2(self): + if isinstance(self, dict) and self.get("openapi_extensions"): + openapi_extension_value = self.get("openapi_extensions") + + for extension_key in openapi_extension_value: + if not str(extension_key).startswith("x-"): + raise SchemaValidationError("An OpenAPI extension key must start with x-") + + self.update(openapi_extension_value) + self.pop("openapi_extensions", None) + + return self + + +# https://swagger.io/specification/#contact-object +class Contact(BaseModel): + name: Optional[str] = None + url: Optional[AnyUrl] = None + email: Optional[str] = None + + model_config = MODEL_CONFIG_ALLOW + + +# https://swagger.io/specification/#license-object +class License(BaseModel): + name: str + identifier: Optional[str] = None + url: Optional[AnyUrl] = None + + model_config = MODEL_CONFIG_ALLOW + + +# https://swagger.io/specification/#info-object +class Info(BaseModel): + title: str + description: Optional[str] = None + termsOfService: Optional[str] = None + contact: Optional[Contact] = None + license: Optional[License] = None # noqa: A003 + version: str + summary: Optional[str] = None + + model_config = MODEL_CONFIG_IGNORE + + +# https://swagger.io/specification/#server-variable-object +class ServerVariable(BaseModel): + enum: Annotated[Optional[List[str]], Field(min_length=1)] = None + default: str + description: Optional[str] = None + + model_config = MODEL_CONFIG_ALLOW + + +# https://swagger.io/specification/#server-object +class Server(OpenAPIExtensions): + url: Union[AnyUrl, str] + description: Optional[str] = None + variables: Optional[Dict[str, ServerVariable]] = None + + model_config = MODEL_CONFIG_ALLOW + + +# https://swagger.io/specification/#reference-object +class Reference(BaseModel): + ref: str = Field(alias="$ref") + + +# https://swagger.io/specification/#discriminator-object +class Discriminator(BaseModel): + propertyName: str + mapping: Optional[Dict[str, str]] = None + + +# https://swagger.io/specification/#xml-object +class XML(BaseModel): + name: Optional[str] = None + namespace: Optional[str] = None + prefix: Optional[str] = None + attribute: Optional[bool] = None + wrapped: Optional[bool] = None + + model_config = MODEL_CONFIG_ALLOW + + +# https://swagger.io/specification/#external-documentation-object +class ExternalDocumentation(BaseModel): + description: Optional[str] = None + url: AnyUrl + + model_config = MODEL_CONFIG_ALLOW + + +# https://swagger.io/specification/#schema-object +class Schema(BaseModel): + # Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-the-json-schema-core-vocabu + # Core Vocabulary + schema_: Optional[str] = Field(default=None, alias="$schema") + vocabulary: Optional[str] = Field(default=None, alias="$vocabulary") + id: Optional[str] = Field(default=None, alias="$id") # noqa: A003 + anchor: Optional[str] = Field(default=None, alias="$anchor") + dynamicAnchor: Optional[str] = Field(default=None, alias="$dynamicAnchor") + ref: Optional[str] = Field(default=None, alias="$ref") + dynamicRef: Optional[str] = Field(default=None, alias="$dynamicRef") + defs: Optional[Dict[str, "SchemaOrBool"]] = Field(default=None, alias="$defs") + comment: Optional[str] = Field(default=None, alias="$comment") + # Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-a-vocabulary-for-applying-s + # A Vocabulary for Applying Subschemas + allOf: Optional[List["SchemaOrBool"]] = None + anyOf: Optional[List["SchemaOrBool"]] = None + oneOf: Optional[List["SchemaOrBool"]] = None + not_: Optional["SchemaOrBool"] = Field(default=None, alias="not") + if_: Optional["SchemaOrBool"] = Field(default=None, alias="if") + then: Optional["SchemaOrBool"] = None + else_: Optional["SchemaOrBool"] = Field(default=None, alias="else") + dependentSchemas: Optional[Dict[str, "SchemaOrBool"]] = None + prefixItems: Optional[List["SchemaOrBool"]] = None + # MAINTENANCE: uncomment and remove below when deprecating Pydantic v1 + # MAINTENANCE: It generates a list of schemas for tuples, before prefixItems was available + # MAINTENANCE: items: Optional["SchemaOrBool"] = None + items: Optional[Union["SchemaOrBool", List["SchemaOrBool"]]] = None + contains: Optional["SchemaOrBool"] = None + properties: Optional[Dict[str, "SchemaOrBool"]] = None + patternProperties: Optional[Dict[str, "SchemaOrBool"]] = None + additionalProperties: Optional["SchemaOrBool"] = None + propertyNames: Optional["SchemaOrBool"] = None + unevaluatedItems: Optional["SchemaOrBool"] = None + unevaluatedProperties: Optional["SchemaOrBool"] = None + # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-structural + # A Vocabulary for Structural Validation + type: Optional[str] = None # noqa: A003 + enum: Optional[List[Any]] = None + const: Optional[Any] = None + multipleOf: Optional[float] = Field(default=None, gt=0) + maximum: Optional[float] = None + exclusiveMaximum: Optional[float] = None + minimum: Optional[float] = None + exclusiveMinimum: Optional[float] = None + maxLength: Optional[int] = Field(default=None, ge=0) + minLength: Optional[int] = Field(default=None, ge=0) + pattern: Optional[str] = None + maxItems: Optional[int] = Field(default=None, ge=0) + minItems: Optional[int] = Field(default=None, ge=0) + uniqueItems: Optional[bool] = None + maxContains: Optional[int] = Field(default=None, ge=0) + minContains: Optional[int] = Field(default=None, ge=0) + maxProperties: Optional[int] = Field(default=None, ge=0) + minProperties: Optional[int] = Field(default=None, ge=0) + required: Optional[List[str]] = None + dependentRequired: Optional[Dict[str, Set[str]]] = None + # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-vocabularies-for-semantic-c + # Vocabularies for Semantic Content With "format" + format: Optional[str] = None # noqa: A003 + # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-the-conten + # A Vocabulary for the Contents of String-Encoded Data + contentEncoding: Optional[str] = None + contentMediaType: Optional[str] = None + contentSchema: Optional["SchemaOrBool"] = None + # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-basic-meta + # A Vocabulary for Basic Meta-Data Annotations + title: Optional[str] = None + description: Optional[str] = None + default: Optional[Any] = None + deprecated: Optional[bool] = None + readOnly: Optional[bool] = None + writeOnly: Optional[bool] = None + examples: Optional[List[Any]] = None + # Ref: OpenAPI 3.0.0: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#schema-object + # Schema Object + discriminator: Optional[Discriminator] = None + xml: Optional[XML] = None + externalDocs: Optional[ExternalDocumentation] = None + + model_config = MODEL_CONFIG_ALLOW + + +# Ref: https://json-schema.org/draft/2020-12/json-schema-core.html#name-json-schema-documents +# A JSON Schema MUST be an object or a boolean. +SchemaOrBool = Union[Schema, bool] + + +# https://swagger.io/specification/#example-object +class Example(BaseModel): + summary: Optional[str] = None + description: Optional[str] = None + value: Optional[Any] = None + externalValue: Optional[AnyUrl] = None + + model_config = MODEL_CONFIG_ALLOW + + +class ParameterInType(Enum): + query = "query" + header = "header" + path = "path" + cookie = "cookie" + + +# https://swagger.io/specification/#encoding-object +class Encoding(BaseModel): + contentType: Optional[str] = None + headers: Optional[Dict[str, Union["Header", Reference]]] = None + style: Optional[str] = None + explode: Optional[bool] = None + allowReserved: Optional[bool] = None + + model_config = MODEL_CONFIG_ALLOW + + +# https://swagger.io/specification/#media-type-object +class MediaType(BaseModel): + schema_: Optional[Union[Schema, Reference]] = Field(default=None, alias="schema") + examples: Optional[Dict[str, Union[Example, Reference]]] = None + encoding: Optional[Dict[str, Encoding]] = None + + model_config = MODEL_CONFIG_ALLOW + + +# https://swagger.io/specification/#parameter-object +class ParameterBase(BaseModel): + description: Optional[str] = None + required: Optional[bool] = None + deprecated: Optional[bool] = None + # Serialization rules for simple scenarios + style: Optional[str] = None + explode: Optional[bool] = None + allowReserved: Optional[bool] = None + schema_: Optional[Union[Schema, Reference]] = Field(default=None, alias="schema") + examples: Optional[Dict[str, Union[Example, Reference]]] = None + # Serialization rules for more complex scenarios + content: Optional[Dict[str, MediaType]] = None + + model_config = MODEL_CONFIG_ALLOW + + +class Parameter(ParameterBase): + name: str + in_: ParameterInType = Field(alias="in") + + +class Header(ParameterBase): + pass + + +# https://swagger.io/specification/#request-body-object +class RequestBody(BaseModel): + description: Optional[str] = None + content: Dict[str, MediaType] + required: Optional[bool] = None + + model_config = MODEL_CONFIG_ALLOW + + +# https://swagger.io/specification/#link-object +class Link(BaseModel): + operationRef: Optional[str] = None + operationId: Optional[str] = None + parameters: Optional[Dict[str, Union[Any, str]]] = None + requestBody: Optional[Union[Any, str]] = None + description: Optional[str] = None + server: Optional[Server] = None + + model_config = MODEL_CONFIG_ALLOW + + +# https://swagger.io/specification/#response-object +class Response(BaseModel): + description: str + headers: Optional[Dict[str, Union[Header, Reference]]] = None + content: Optional[Dict[str, MediaType]] = None + links: Optional[Dict[str, Union[Link, Reference]]] = None + + model_config = MODEL_CONFIG_ALLOW + + +# https://swagger.io/specification/#tag-object +class Tag(BaseModel): + name: str + description: Optional[str] = None + externalDocs: Optional[ExternalDocumentation] = None + + model_config = MODEL_CONFIG_ALLOW + + +# https://swagger.io/specification/#operation-object +class Operation(OpenAPIExtensions): + tags: Optional[List[str]] = None + summary: Optional[str] = None + description: Optional[str] = None + externalDocs: Optional[ExternalDocumentation] = None + operationId: Optional[str] = None + parameters: Optional[List[Union[Parameter, Reference]]] = None + requestBody: Optional[Union[RequestBody, Reference]] = None + # Using Any for Specification Extensions + responses: Optional[Dict[int, Union[Response, Any]]] = None + callbacks: Optional[Dict[str, Union[Dict[str, "PathItem"], Reference]]] = None + deprecated: Optional[bool] = None + security: Optional[List[Dict[str, List[str]]]] = None + servers: Optional[List[Server]] = None + + model_config = MODEL_CONFIG_ALLOW + + +# https://swagger.io/specification/#path-item-object +class PathItem(BaseModel): + ref: Optional[str] = Field(default=None, alias="$ref") + summary: Optional[str] = None + description: Optional[str] = None + get: Optional[Operation] = None + put: Optional[Operation] = None + post: Optional[Operation] = None + delete: Optional[Operation] = None + options: Optional[Operation] = None + head: Optional[Operation] = None + patch: Optional[Operation] = None + trace: Optional[Operation] = None + servers: Optional[List[Server]] = None + parameters: Optional[List[Union[Parameter, Reference]]] = None + + model_config = MODEL_CONFIG_ALLOW + + +# https://swagger.io/specification/#security-scheme-object +class SecuritySchemeType(Enum): + apiKey = "apiKey" + http = "http" + oauth2 = "oauth2" + openIdConnect = "openIdConnect" + mutualTLS = "mutualTLS" + + +class SecurityBase(OpenAPIExtensions): + type_: SecuritySchemeType = Field(alias="type") + description: Optional[str] = None + + model_config = {"extra": "allow", "populate_by_name": True} + + +class APIKeyIn(Enum): + query = "query" + header = "header" + cookie = "cookie" + + +class APIKey(SecurityBase): + type_: SecuritySchemeType = Field(default=SecuritySchemeType.apiKey, alias="type") + in_: APIKeyIn = Field(alias="in") + name: str + + +class HTTPBase(SecurityBase): + type_: SecuritySchemeType = Field(default=SecuritySchemeType.http, alias="type") + scheme: str + + +class HTTPBearer(HTTPBase): # type: ignore[override] + scheme: Literal["bearer"] = "bearer" + bearerFormat: Optional[str] = None + + +class OAuthFlow(BaseModel): + refreshUrl: Optional[str] = None + scopes: Dict[str, str] = {} + + model_config = MODEL_CONFIG_ALLOW + + +class OAuthFlowImplicit(OAuthFlow): + authorizationUrl: str + + +class OAuthFlowPassword(OAuthFlow): + tokenUrl: str + + +class OAuthFlowClientCredentials(OAuthFlow): + tokenUrl: str + + +class OAuthFlowAuthorizationCode(OAuthFlow): + authorizationUrl: str + tokenUrl: str + + +class OAuthFlows(BaseModel): + implicit: Optional[OAuthFlowImplicit] = None + password: Optional[OAuthFlowPassword] = None + clientCredentials: Optional[OAuthFlowClientCredentials] = None + authorizationCode: Optional[OAuthFlowAuthorizationCode] = None + + model_config = MODEL_CONFIG_ALLOW + + +class OAuth2(SecurityBase): + type_: SecuritySchemeType = Field(default=SecuritySchemeType.oauth2, alias="type") + flows: OAuthFlows + + +class OpenIdConnect(SecurityBase): + type_: SecuritySchemeType = Field( + default=SecuritySchemeType.openIdConnect, + alias="type", + ) + openIdConnectUrl: str + + +class MutualTLS(SecurityBase): + type_: SecuritySchemeType = Field(default=SecuritySchemeType.mutualTLS, alias="type") + + +SecurityScheme = Union[APIKey, HTTPBase, OAuth2, OpenIdConnect, HTTPBearer, MutualTLS] + + +# https://swagger.io/specification/#components-object +class Components(BaseModel): + schemas: Optional[Dict[str, Union[Schema, Reference]]] = None + responses: Optional[Dict[str, Union[Response, Reference]]] = None + parameters: Optional[Dict[str, Union[Parameter, Reference]]] = None + examples: Optional[Dict[str, Union[Example, Reference]]] = None + requestBodies: Optional[Dict[str, Union[RequestBody, Reference]]] = None + headers: Optional[Dict[str, Union[Header, Reference]]] = None + securitySchemes: Optional[Dict[str, Union[SecurityScheme, Reference]]] = None + links: Optional[Dict[str, Union[Link, Reference]]] = None + # Using Any for Specification Extensions + callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None + pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None + + model_config = MODEL_CONFIG_ALLOW + + +# https://swagger.io/specification/#openapi-object +class OpenAPI(OpenAPIExtensions): + openapi: str + info: Info + jsonSchemaDialect: Optional[str] = None + servers: Optional[List[Server]] = None + # Using Any for Specification Extensions + paths: Optional[Dict[str, Union[PathItem, Any]]] = None + webhooks: Optional[Dict[str, Union[PathItem, Reference]]] = None + components: Optional[Components] = None + security: Optional[List[Dict[str, List[str]]]] = None + tags: Optional[List[Tag]] = None + externalDocs: Optional[ExternalDocumentation] = None + + model_config = MODEL_CONFIG_ALLOW + + +model_rebuild(Schema) +model_rebuild(Operation) +model_rebuild(Encoding) diff --git a/aws_lambda_powertools/event_handler/openapi/params.py b/aws_lambda_powertools/event_handler/openapi/params.py new file mode 100644 index 00000000000..468e2253b39 --- /dev/null +++ b/aws_lambda_powertools/event_handler/openapi/params.py @@ -0,0 +1,1169 @@ +from __future__ import annotations + +import inspect +from enum import Enum +from typing import TYPE_CHECKING, Any, Literal + +from pydantic import BaseConfig, BaseModel, create_model +from pydantic.fields import FieldInfo +from typing_extensions import Annotated, get_args, get_origin + +from aws_lambda_powertools.event_handler import Response +from aws_lambda_powertools.event_handler.openapi.compat import ( + ModelField, + Required, + Undefined, + UndefinedType, + copy_field_info, + field_annotation_is_scalar, + get_annotation_from_field_info, + lenient_issubclass, +) + +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 + +""" +This turns the low-level function signature into typed, validated Pydantic models for consumption. +""" + + +class ParamTypes(Enum): + query = "query" + header = "header" + path = "path" + cookie = "cookie" + + +# MAINTENANCE: update when deprecating Pydantic v1, remove this alias +_Unset: Any = Undefined + + +class Dependant: + """ + A class used internally to represent a dependency between path operation decorators and the path operation function. + """ + + def __init__( + self, + *, + path_params: list[ModelField] | None = None, + query_params: list[ModelField] | None = None, + header_params: list[ModelField] | None = None, + cookie_params: list[ModelField] | None = None, + body_params: list[ModelField] | None = None, + return_param: ModelField | None = None, + response_extra_models: list[ModelField] | None = None, + name: str | None = None, + call: Callable[..., Any] | None = None, + request_param_name: str | None = None, + websocket_param_name: str | None = None, + 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 [] + self.query_params = query_params or [] + self.header_params = header_params or [] + self.cookie_params = cookie_params or [] + self.body_params = body_params or [] + self.return_param = return_param or None + self.response_extra_models = response_extra_models or [] + self.request_param_name = request_param_name + self.websocket_param_name = websocket_param_name + 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 + self.path = path + # Save the cache key at creation to optimize performance + self.cache_key: CacheKey = self.call + + +class Param(FieldInfo): # type: ignore[misc] + """ + A class used internally to represent a parameter in a path operation. + """ + + in_: ParamTypes + + def __init__( + self, + default: Any = Undefined, + *, + default_factory: Callable[[], Any] | None = _Unset, + annotation: Any | None = None, + alias: str | None = None, + alias_priority: int | None = _Unset, + # MAINTENANCE: update when deprecating Pydantic v1, import these types + # MAINTENANCE: validation_alias: str | AliasPath | AliasChoices | None + validation_alias: str | None = _Unset, + serialization_alias: str | None = None, + title: str | None = None, + description: str | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + min_length: int | None = None, + max_length: int | None = None, + pattern: str | None = None, + discriminator: str | None = None, + strict: bool | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + examples: list[Any] | None = None, + openapi_examples: dict[str, Example] | None = None, + deprecated: bool | None = None, + include_in_schema: bool = True, + json_schema_extra: dict[str, Any] | None = None, + **extra: Any, + ): + """ + Constructs a new 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 + """ + self.deprecated = deprecated + self.include_in_schema = include_in_schema + + kwargs = dict( + default=default, + default_factory=default_factory, + alias=alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + discriminator=discriminator, + multiple_of=multiple_of, + allow_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + **extra, + ) + if examples is not None: + kwargs["examples"] = examples + + if openapi_examples is not None: + kwargs["openapi_examples"] = openapi_examples + + current_json_schema_extra = json_schema_extra or extra + + self.openapi_examples = openapi_examples + + # Pydantic 2.12+ no longer copies alias to validation_alias automatically + # Ensure alias and validation_alias are in sync when only one is provided + if validation_alias is _Unset and alias is not None: + validation_alias = alias + elif alias is None and validation_alias is not _Unset and validation_alias is not None: + alias = validation_alias + kwargs["alias"] = alias + + kwargs.update( + { + "annotation": annotation, + "alias_priority": alias_priority, + "validation_alias": validation_alias, + "serialization_alias": serialization_alias, + "strict": strict, + "json_schema_extra": current_json_schema_extra, + "pattern": pattern, + }, + ) + + use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} + + super().__init__(**use_kwargs) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.default})" + + +class Path(Param): # type: ignore[misc] + """ + A class used internally to represent a path parameter in a path operation. + """ + + in_ = ParamTypes.path + + def __init__( + self, + default: Any = ..., + *, + default_factory: Callable[[], Any] | None = _Unset, + annotation: Any | None = None, + alias: str | None = None, + alias_priority: int | None = _Unset, + # MAINTENANCE: update when deprecating Pydantic v1, import these types + # MAINTENANCE: validation_alias: str | AliasPath | AliasChoices | None + validation_alias: str | None = _Unset, + serialization_alias: str | None = None, + title: str | None = None, + description: str | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + min_length: int | None = None, + max_length: int | None = None, + pattern: str | None = None, + discriminator: str | None = None, + strict: bool | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + examples: list[Any] | None = None, + openapi_examples: dict[str, Example] | None = None, + deprecated: bool | None = None, + include_in_schema: bool = True, + json_schema_extra: dict[str, Any] | None = None, + **extra: Any, + ): + if default is not ...: + raise AssertionError("Path parameters cannot have a default value") + + super().__init__( + default=default, + default_factory=default_factory, + annotation=annotation, + alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + pattern=pattern, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, + examples=examples, + openapi_examples=openapi_examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, + **extra, + ) + + +class Query(Param): # type: ignore[misc] + """ + A class used internally to represent a query parameter in a path operation. + """ + + in_ = ParamTypes.query + + def __init__( + self, + default: Any = _Unset, + *, + default_factory: Callable[[], Any] | None = _Unset, + annotation: Any | None = None, + alias: str | None = None, + alias_priority: int | None = _Unset, + validation_alias: str | None = _Unset, + serialization_alias: str | None = None, + title: str | None = None, + description: str | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + min_length: int | None = None, + max_length: int | None = None, + pattern: str | None = None, + discriminator: str | None = None, + strict: bool | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + examples: list[Any] | None = None, + openapi_examples: dict[str, Example] | None = None, + deprecated: bool | None = None, + include_in_schema: bool = True, + json_schema_extra: dict[str, Any] | None = None, + **extra: Any, + ): + super().__init__( + default=default, + default_factory=default_factory, + annotation=annotation, + alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + pattern=pattern, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, + examples=examples, + openapi_examples=openapi_examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, + **extra, + ) + + +class Header(Param): # type: ignore[misc] + """ + A class used internally to represent a header parameter in a path operation. + """ + + in_ = ParamTypes.header + + def __init__( + self, + default: Any = Undefined, + *, + default_factory: Callable[[], Any] | None = _Unset, + annotation: Any | None = None, + alias: str | None = None, + alias_priority: int | None = _Unset, + # MAINTENANCE: update when deprecating Pydantic v1, import these types + # str | AliasPath | AliasChoices | None + validation_alias: str | None = _Unset, + serialization_alias: str | None = None, + convert_underscores: bool = True, + title: str | None = None, + description: str | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + min_length: int | None = None, + max_length: int | None = None, + pattern: str | None = None, + discriminator: str | None = None, + strict: bool | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + examples: list[Any] | None = None, + openapi_examples: dict[str, Example] | None = None, + deprecated: bool | None = None, + include_in_schema: bool = True, + json_schema_extra: dict[str, Any] | None = None, + **extra: Any, + ): + self.convert_underscores = convert_underscores + self._alias = alias + + super().__init__( + default=default, + default_factory=default_factory, + annotation=annotation, + alias=self._alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + pattern=pattern, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, + examples=examples, + openapi_examples=openapi_examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, + **extra, + ) + + @property + def alias(self): + return self._alias + + @alias.setter + def alias(self, value: str | None = None): + if value is not None: + # Headers are case-insensitive according to RFC 7540 (HTTP/2), so we lower the parameter name + # This ensures that customers can access headers with any casing, as per the RFC guidelines. + # Reference: https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2 + 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. + """ + + def __init__( + self, + default: Any = Undefined, + *, + default_factory: Callable[[], Any] | None = _Unset, + annotation: Any | None = None, + embed: bool = False, + media_type: str = "application/json", + alias: str | None = None, + alias_priority: int | None = _Unset, + # MAINTENANCE: update when deprecating Pydantic v1, import these types + # str | AliasPath | AliasChoices | None + validation_alias: str | None = _Unset, + serialization_alias: str | None = None, + title: str | None = None, + description: str | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + min_length: int | None = None, + max_length: int | None = None, + pattern: str | None = None, + discriminator: str | None = None, + strict: bool | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + examples: list[Any] | None = None, + openapi_examples: dict[str, Example] | None = None, + deprecated: bool | None = None, + include_in_schema: bool = True, + json_schema_extra: dict[str, Any] | None = None, + **extra: Any, + ): + self.embed = embed + self.media_type = media_type + self.deprecated = deprecated + self.include_in_schema = include_in_schema + kwargs = dict( + default=default, + default_factory=default_factory, + alias=alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + discriminator=discriminator, + multiple_of=multiple_of, + allow_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + **extra, + ) + if examples is not None: + kwargs["examples"] = examples + if openapi_examples is not None: + kwargs["openapi_examples"] = openapi_examples + current_json_schema_extra = json_schema_extra or extra + + # Pydantic 2.12+ no longer copies alias to validation_alias automatically + # Ensure alias and validation_alias are in sync when only one is provided + if validation_alias is _Unset and alias is not None: + validation_alias = alias + elif alias is None and validation_alias is not _Unset and validation_alias is not None: + alias = validation_alias + kwargs["alias"] = alias + self.openapi_examples = openapi_examples + + kwargs.update( + { + "annotation": annotation, + "alias_priority": alias_priority, + "validation_alias": validation_alias, + "serialization_alias": serialization_alias, + "strict": strict, + "json_schema_extra": current_json_schema_extra, + "pattern": pattern, + }, + ) + + use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} + + super().__init__(**use_kwargs) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.default})" + + +class Form(Body): # type: ignore[misc] + """ + A class used to represent a form parameter in a path operation. + """ + + def __init__( + self, + default: Any = Undefined, + *, + default_factory: Callable[[], Any] | None = _Unset, + annotation: Any | None = None, + media_type: str = "application/x-www-form-urlencoded", + alias: str | None = None, + alias_priority: int | None = _Unset, + # MAINTENANCE: update when deprecating Pydantic v1, import these types + # str | AliasPath | AliasChoices | None + validation_alias: str | None = _Unset, + serialization_alias: str | None = None, + title: str | None = None, + description: str | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + min_length: int | None = None, + max_length: int | None = None, + pattern: str | None = None, + discriminator: str | None = None, + strict: bool | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + examples: list[Any] | None = None, + deprecated: bool | None = None, + include_in_schema: bool = True, + json_schema_extra: dict[str, Any] | None = None, + **extra: Any, + ): + super().__init__( + default=default, + default_factory=default_factory, + annotation=annotation, + embed=True, + media_type=media_type, + alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + pattern=pattern, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, + examples=examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, + **extra, + ) + + +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. + """ + + def __init__( + self, + default: Any = Undefined, + *, + default_factory: Callable[[], Any] | None = _Unset, + annotation: Any | None = None, + media_type: str = "multipart/form-data", + alias: str | None = None, + alias_priority: int | None = _Unset, + # MAINTENANCE: update when deprecating Pydantic v1, import these types + # str | AliasPath | AliasChoices | None + validation_alias: str | None = None, + serialization_alias: str | None = None, + title: str | None = None, + description: str | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + min_length: int | None = None, + max_length: int | None = None, + pattern: str | None = None, + discriminator: str | None = None, + strict: bool | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + examples: list[Any] | None = None, + deprecated: bool | None = None, + include_in_schema: bool = True, + json_schema_extra: dict[str, Any] | None = None, + **extra: Any, + ): + # For file uploads, ensure the OpenAPI schema has the correct format + # Also we can't test it + file_schema_extra = {"format": "binary"} # pragma: no cover + if json_schema_extra: # pragma: no cover + json_schema_extra.update(file_schema_extra) # pragma: no cover + else: # pragma: no cover + json_schema_extra = file_schema_extra # pragma: no cover + + super().__init__( + default=default, + default_factory=default_factory, + annotation=annotation, + media_type=media_type, + alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + pattern=pattern, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, + examples=examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, + **extra, + ) + + +def get_flat_dependant( + dependant: Dependant, + visited: list[CacheKey] | None = None, +) -> Dependant: + """ + Flatten a recursive Dependant model structure. + + This function recursively concatenates the parameter fields of a Dependant model and its dependencies into a flat + Dependant structure. This is useful for scenarios like parameter validation where the nested structure is not + relevant. + + Parameters + ---------- + dependant: Dependant + The dependant model to flatten + visited: list[CacheKey], optional + Keeps track of visited Dependents to avoid infinite recursion. Defaults to empty list. + + Returns + ------- + Dependant + The flattened Dependant model + """ + if visited is None: + visited = [] + visited.append(dependant.cache_key) + + flat = Dependant( + path_params=dependant.path_params.copy(), + query_params=dependant.query_params.copy(), + header_params=dependant.header_params.copy(), + cookie_params=dependant.cookie_params.copy(), + body_params=dependant.body_params.copy(), + 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( + *, + param_name: str, + annotation: Any, + value: Any, + is_path_param: bool, + is_response_param: bool, +) -> ModelField | None: + """ + Analyze a parameter annotation and value to determine the type and default value of the parameter. + + Parameters + ---------- + param_name: str + The name of the parameter + annotation + The annotation of the parameter + value + The value of the parameter + is_path_param + Whether the parameter is a path parameter + is_response_param + Whether the parameter is the return annotation + + Returns + ------- + ModelField | None + The type annotation and the Pydantic field representing the parameter + """ + field_info, type_annotation = get_field_info_and_type_annotation( + annotation, + value, + is_path_param, + is_response_param, + ) + + # If the value is a FieldInfo, we use it as the FieldInfo for the parameter + if isinstance(value, FieldInfo): + if field_info is not None: + raise AssertionError("Cannot use a FieldInfo as a parameter annotation and pass a FieldInfo as a value") + field_info = value + + field_info.annotation = type_annotation # type: ignore[attr-defined,unused-ignore] + + # If we didn't determine the FieldInfo yet, we create a default one + if field_info is None: + default_value = value if value is not inspect.Signature.empty else Required + + # Check if the parameter is part of the path. Otherwise, defaults to query. + if is_path_param: + field_info = Path(annotation=type_annotation) + elif not field_annotation_is_scalar(annotation=type_annotation): + field_info = Body(annotation=type_annotation, default=default_value) + else: + field_info = Query(annotation=type_annotation, default=default_value) + + # When we have a response field, we need to set the default value to Required + if is_response_param: + field_info.default = Required + + field = _create_model_field(field_info, type_annotation, param_name, is_path_param, is_response_param) + return field + + +def get_field_info_and_type_annotation( + annotation, + value, + is_path_param: bool, + is_response_param: bool, +) -> tuple[FieldInfo | None, Any]: + """ + Get the FieldInfo and type annotation from an annotation and value. + """ + field_info: FieldInfo | None = None + type_annotation: Any = Any + + if annotation is not inspect.Signature.empty: + # If the annotation is an Annotated type, we need to extract the type annotation and the FieldInfo + if get_origin(annotation) is Annotated: + field_info, type_annotation = get_field_info_annotated_type(annotation, value, is_path_param) + # If the annotation is a Response type, we recursively call this function with the inner type + elif get_origin(annotation) is Response: + field_info, type_annotation = get_field_info_response_type(annotation, value) + # If the response param is a tuple with two elements, we use the first element as the type annotation, + # just like we did in the APIGateway._to_response + elif is_response_param and get_origin(annotation) is tuple and len(get_args(annotation)) == 2: + field_info, type_annotation = get_field_info_tuple_type(annotation, value) + # If the annotation is not an Annotated type, we use it as the type annotation + else: + type_annotation = annotation + + return field_info, type_annotation + + +def get_field_info_tuple_type(annotation, value) -> tuple[FieldInfo | None, Any]: + (inner_type, _) = get_args(annotation) + + # If the inner type is an Annotated type, we need to extract the type annotation and the FieldInfo + if get_origin(inner_type) is Annotated: + return get_field_info_annotated_type(inner_type, value, False) + + return None, inner_type + + +def get_field_info_response_type(annotation, value) -> tuple[FieldInfo | None, Any]: + # Example: get_args(Response[inner_type]) == (inner_type,) # noqa: ERA001 + (inner_type,) = get_args(annotation) + + # Recursively resolve the inner type + return get_field_info_and_type_annotation(inner_type, value, False, True) + + +def _has_discriminator(field_info: FieldInfo) -> bool: + """Check if a FieldInfo has a discriminator.""" + return hasattr(field_info, "discriminator") and field_info.discriminator is not None + + +def _handle_discriminator_with_param( + annotations: list[FieldInfo], + annotation: Any, +) -> tuple[FieldInfo | None, Any, bool]: + """ + Handle the special case of Field(discriminator) + Body() combination. + + Returns: + tuple of (powertools_annotation, type_annotation, has_discriminator_with_body) + """ + field_obj = None + body_obj = None + + for ann in annotations: + if isinstance(ann, Body): + body_obj = ann + elif _has_discriminator(ann): + field_obj = ann + + if field_obj and body_obj: + # Use Body as the primary annotation, preserve full annotation for validation + return body_obj, annotation, True + + raise AssertionError("Only one FieldInfo can be used per parameter") + + +def _create_field_info( + powertools_annotation: FieldInfo, + type_annotation: Any, + has_discriminator_with_body: bool, +) -> FieldInfo: + """Create or copy FieldInfo based on the annotation type.""" + field_info: FieldInfo + if has_discriminator_with_body: + # For discriminator + Body case, create a new Body instance directly + field_info = Body() + field_info.annotation = type_annotation + else: + # Copy field_info because we mutate field_info.default later + field_info = copy_field_info( + field_info=powertools_annotation, + annotation=type_annotation, + ) + return field_info + + +def _set_field_default(field_info: FieldInfo, value: Any, is_path_param: bool) -> None: + """Set the default value for a field.""" + if field_info.default not in [Undefined, Required]: + raise AssertionError("FieldInfo needs to have a default value of Undefined or Required") + + if value is not inspect.Signature.empty: + if is_path_param: + raise AssertionError("Cannot use a FieldInfo as a path parameter and pass a value") + field_info.default = value + else: + field_info.default = Required + + +def get_field_info_annotated_type(annotation, value, is_path_param: bool) -> tuple[FieldInfo | None, Any]: + """ + Get the FieldInfo and type annotation from an Annotated type. + """ + annotated_args = get_args(annotation) + type_annotation = annotated_args[0] + + # Handle both FieldInfo instances and FieldInfo subclasses (e.g., Body vs Body()) + powertools_annotations: list[FieldInfo] = [] + for arg in annotated_args[1:]: + if isinstance(arg, FieldInfo): + powertools_annotations.append(arg) + elif isinstance(arg, type) and issubclass(arg, FieldInfo): + # If it's a class (e.g., Body instead of Body()), instantiate it + powertools_annotations.append(arg()) + + # Preserve non-FieldInfo metadata (like annotated_types constraints) + # This is important for constraints like Interval, Gt, Lt, etc. + other_metadata = [ + arg + for arg in annotated_args[1:] + if not isinstance(arg, FieldInfo) and not (isinstance(arg, type) and issubclass(arg, FieldInfo)) + ] + + # Determine which annotation to use + powertools_annotation: FieldInfo | None = None + has_discriminator_with_param = False + + if len(powertools_annotations) == 2: + powertools_annotation, type_annotation, has_discriminator_with_param = _handle_discriminator_with_param( + powertools_annotations, + annotation, + ) + elif len(powertools_annotations) > 1: + raise AssertionError("Only one FieldInfo can be used per parameter") + else: + powertools_annotation = next(iter(powertools_annotations), None) + + # Reconstruct type_annotation with non-FieldInfo metadata if present + # This ensures constraints like Interval are preserved + if other_metadata and not has_discriminator_with_param: + type_annotation = Annotated[(type_annotation, *other_metadata)] + + # Process the annotation if it exists + field_info: FieldInfo | None = None + if isinstance(powertools_annotation, FieldInfo): # pragma: no cover + field_info = _create_field_info(powertools_annotation, type_annotation, has_discriminator_with_param) + _set_field_default(field_info, value, is_path_param) + + # Preserve full annotated type for discriminated unions + if _has_discriminator(powertools_annotation): # pragma: no cover + type_annotation = annotation # pragma: no cover + + return field_info, type_annotation + + +def create_response_field( + name: str, + type_: type[Any], + default: Any | None = Undefined, + required: bool | UndefinedType = Undefined, + model_config: type[BaseConfig] = BaseConfig, + field_info: FieldInfo | None = None, + alias: str | None = None, + mode: Literal["validation", "serialization"] = "validation", +) -> ModelField: + """ + Create a new response field. Raises if type_ is invalid. + """ + field_info = field_info or FieldInfo( + annotation=type_, + default=default, + alias=alias, + ) + + kwargs = {"name": name, "field_info": field_info, "mode": mode} + + return ModelField(**kwargs) # type: ignore[arg-type] + + +def _apply_header_underscore_conversion( + field_info: FieldInfo, + type_annotation: Any, + param_name: str, +) -> tuple[FieldInfo, Any]: + """ + Apply underscore-to-dash conversion for Header parameters. + + For BaseModel: Creates new model with underscore-to-dash alias generator. + Note: If the BaseModel already has an alias generator, it will be replaced + with dash-case conversion since HTTP headers should use dash-case. + For all Header fields: Sets the parameter alias if convert_underscores is True + """ + if not isinstance(field_info, Header) or not field_info.convert_underscores: + return field_info, type_annotation + + # Always set the parameter alias for Header fields (if not already set) + if not field_info.alias: + field_info.alias = param_name.replace("_", "-") + + # Handle BaseModel case - create new model with dash-case alias generator + if lenient_issubclass(type_annotation, BaseModel): + # For HTTP headers, we should use dash-case regardless of existing alias generator + # This ensures consistent header naming conventions + header_aliased_model = create_model( + f"{type_annotation.__name__}WithHeaderAliases", + __base__=type_annotation, + __config__={"alias_generator": lambda name: name.replace("_", "-")}, + ) + + type_annotation = header_aliased_model + field_info.annotation = type_annotation + + return field_info, type_annotation + + +def _create_model_field( + field_info: FieldInfo | None, + 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. + """ + if field_info is None: + return None + + if is_path_param: + if not isinstance(field_info, Path): + raise AssertionError("Path parameters must be of type Path") + elif isinstance(field_info, Param) and getattr(field_info, "in_", None) is None: + field_info.in_ = ParamTypes.query + + # Apply header underscore conversion + field_info, type_annotation = _apply_header_underscore_conversion(field_info, type_annotation, param_name) + + # If the field_info is a Param, we use the `in_` attribute to determine the type annotation + use_annotation = get_annotation_from_field_info(type_annotation, field_info, param_name) + + return create_response_field( + name=param_name, + type_=use_annotation, + default=field_info.default, + 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/pydantic_loader.py b/aws_lambda_powertools/event_handler/openapi/pydantic_loader.py new file mode 100644 index 00000000000..225f7e88096 --- /dev/null +++ b/aws_lambda_powertools/event_handler/openapi/pydantic_loader.py @@ -0,0 +1,6 @@ +try: + from pydantic.version import VERSION as PYDANTIC_VERSION + + PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.") +except ImportError: + PYDANTIC_V2 = False # pragma: no cover # false positive; dropping in v3 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/openapi/swagger_ui/__init__.py b/aws_lambda_powertools/event_handler/openapi/swagger_ui/__init__.py new file mode 100644 index 00000000000..bc6eda8abb3 --- /dev/null +++ b/aws_lambda_powertools/event_handler/openapi/swagger_ui/__init__.py @@ -0,0 +1,13 @@ +from aws_lambda_powertools.event_handler.openapi.swagger_ui.html import ( + generate_swagger_html, +) +from aws_lambda_powertools.event_handler.openapi.swagger_ui.oauth2 import ( + OAuth2Config, + generate_oauth2_redirect_html, +) + +__all__ = [ + "generate_swagger_html", + "generate_oauth2_redirect_html", + "OAuth2Config", +] diff --git a/aws_lambda_powertools/event_handler/openapi/swagger_ui/html.py b/aws_lambda_powertools/event_handler/openapi/swagger_ui/html.py new file mode 100644 index 00000000000..6bcbcff50a4 --- /dev/null +++ b/aws_lambda_powertools/event_handler/openapi/swagger_ui/html.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from aws_lambda_powertools.event_handler.openapi.swagger_ui.oauth2 import OAuth2Config + + +def generate_swagger_html( + spec: str, + swagger_js: str, + swagger_css: str, + swagger_base_url: str, + oauth2_config: OAuth2Config | None, + persist_authorization: bool = False, +) -> str: + """ + Generate Swagger UI HTML page + + Parameters + ---------- + spec: str + The OpenAPI spec + swagger_js: str + Swagger UI JavaScript source code or URL + swagger_css: str + Swagger UI CSS source code or URL + swagger_base_url: str + The base URL for Swagger UI + oauth2_config: OAuth2Config, optional + The OAuth2 configuration. + persist_authorization: bool, optional + Whether to persist authorization data on browser close/refresh. + """ + + # If Swagger base URL is present, generate HTML content with linked CSS and JavaScript files + # If no Swagger base URL is provided, include CSS and JavaScript directly in the HTML + if swagger_base_url: + swagger_css_content = f"" + swagger_js_content = f"" + else: + swagger_css_content = f"" + swagger_js_content = f"" + + # Prepare oauth2 config + oauth2_content = ( + f"ui.initOAuth({oauth2_config.json(exclude_none=True, exclude_unset=True)});" if oauth2_config else "" + ) + + return f""" + + + + + Swagger UI + + {swagger_css_content} + + + +
+ Loading... +
+ + +{swagger_js_content} + + + + """.strip() diff --git a/aws_lambda_powertools/event_handler/openapi/swagger_ui/oauth2.py b/aws_lambda_powertools/event_handler/openapi/swagger_ui/oauth2.py new file mode 100644 index 00000000000..4cafdfe401c --- /dev/null +++ b/aws_lambda_powertools/event_handler/openapi/swagger_ui/oauth2.py @@ -0,0 +1,154 @@ +# ruff: noqa: E501 FA100 +import warnings +from typing import Dict, Optional, Sequence + +from pydantic import BaseModel, Field, field_validator + +from aws_lambda_powertools.event_handler.openapi.models import ( + MODEL_CONFIG_ALLOW, +) +from aws_lambda_powertools.shared.functions import powertools_dev_is_set + + +# Based on https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/ +class OAuth2Config(BaseModel): + """ + OAuth2 configuration for Swagger UI + """ + + # The client ID for the OAuth2 application + clientId: Optional[str] = Field(alias="client_id", default=None) + + # The client secret for the OAuth2 application. This is sensitive information and requires the explicit presence + # of the POWERTOOLS_DEV environment variable. + clientSecret: Optional[str] = Field(alias="client_secret", default=None) + + # The realm in which the OAuth2 application is registered. Optional. + realm: Optional[str] = Field(default=None) + + # The name of the OAuth2 application + appName: str = Field(alias="app_name") + + # The scopes that the OAuth2 application requires. Defaults to an empty list. + scopes: Sequence[str] = Field(default=[]) + + # Additional query string parameters to be included in the OAuth2 request. Defaults to an empty dictionary. + additionalQueryStringParams: Dict[str, str] = Field(alias="additional_query_string_params", default={}) + + # Whether to use basic authentication with the access code grant type. Defaults to False. + useBasicAuthenticationWithAccessCodeGrant: bool = Field( + alias="use_basic_authentication_with_access_code_grant", + default=False, + ) + + # Whether to use PKCE with the authorization code grant type. Defaults to False. + usePkceWithAuthorizationCodeGrant: bool = Field(alias="use_pkce_with_authorization_code_grant", default=False) + + model_config = MODEL_CONFIG_ALLOW + + @field_validator("clientSecret") + def client_secret_only_on_dev(cls, v: Optional[str]) -> Optional[str]: + if not v: + return None + + if not powertools_dev_is_set(): + raise ValueError( + "cannot use client_secret without POWERTOOLS_DEV mode. See " + "https://docs.powertools.aws.dev/lambda/python/latest/#optimizing-for-non-production-environments", + ) + else: + warnings.warn( + "OAuth2Config is using client_secret and POWERTOOLS_DEV is set. This reveals sensitive information. " + "DO NOT USE THIS OUTSIDE LOCAL DEVELOPMENT", + stacklevel=2, + ) + return v + + +def generate_oauth2_redirect_html() -> str: + """ + Generates the HTML content for the OAuth2 redirect page. + + Source: https://github.com/swagger-api/swagger-ui/blob/master/dist/oauth2-redirect.html + """ + return """ + + + + Swagger UI: OAuth2 Redirect + + + + + + """.strip() diff --git a/aws_lambda_powertools/event_handler/openapi/swagger_ui/swagger-ui-bundle.min.js b/aws_lambda_powertools/event_handler/openapi/swagger_ui/swagger-ui-bundle.min.js new file mode 100644 index 00000000000..64a04935ef0 --- /dev/null +++ b/aws_lambda_powertools/event_handler/openapi/swagger_ui/swagger-ui-bundle.min.js @@ -0,0 +1,2 @@ +/*! For license information please see swagger-ui-bundle.js.LICENSE.txt */ +!function webpackUniversalModuleDefinition(s,o){"object"==typeof exports&&"object"==typeof module?module.exports=o():"function"==typeof define&&define.amd?define([],o):"object"==typeof exports?exports.SwaggerUIBundle=o():s.SwaggerUIBundle=o()}(this,(()=>(()=>{var s={251:(s,o)=>{o.read=function(s,o,i,a,u){var _,w,x=8*u-a-1,C=(1<>1,L=-7,B=i?u-1:0,$=i?-1:1,U=s[o+B];for(B+=$,_=U&(1<<-L)-1,U>>=-L,L+=x;L>0;_=256*_+s[o+B],B+=$,L-=8);for(w=_&(1<<-L)-1,_>>=-L,L+=a;L>0;w=256*w+s[o+B],B+=$,L-=8);if(0===_)_=1-j;else{if(_===C)return w?NaN:1/0*(U?-1:1);w+=Math.pow(2,a),_-=j}return(U?-1:1)*w*Math.pow(2,_-a)},o.write=function(s,o,i,a,u,_){var w,x,C,j=8*_-u-1,L=(1<>1,$=23===u?Math.pow(2,-24)-Math.pow(2,-77):0,U=a?0:_-1,V=a?1:-1,z=o<0||0===o&&1/o<0?1:0;for(o=Math.abs(o),isNaN(o)||o===1/0?(x=isNaN(o)?1:0,w=L):(w=Math.floor(Math.log(o)/Math.LN2),o*(C=Math.pow(2,-w))<1&&(w--,C*=2),(o+=w+B>=1?$/C:$*Math.pow(2,1-B))*C>=2&&(w++,C/=2),w+B>=L?(x=0,w=L):w+B>=1?(x=(o*C-1)*Math.pow(2,u),w+=B):(x=o*Math.pow(2,B-1)*Math.pow(2,u),w=0));u>=8;s[i+U]=255&x,U+=V,x/=256,u-=8);for(w=w<0;s[i+U]=255&w,U+=V,w/=256,j-=8);s[i+U-V]|=128*z}},462:(s,o,i)=>{"use strict";var a=i(40975);s.exports=a},659:(s,o,i)=>{var a=i(51873),u=Object.prototype,_=u.hasOwnProperty,w=u.toString,x=a?a.toStringTag:void 0;s.exports=function getRawTag(s){var o=_.call(s,x),i=s[x];try{s[x]=void 0;var a=!0}catch(s){}var u=w.call(s);return a&&(o?s[x]=i:delete s[x]),u}},694:(s,o,i)=>{"use strict";i(91599);var a=i(37257);i(12560),s.exports=a},953:(s,o,i)=>{"use strict";s.exports=i(53375)},1733:s=>{var o=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g;s.exports=function asciiWords(s){return s.match(o)||[]}},1882:(s,o,i)=>{var a=i(72552),u=i(23805);s.exports=function isFunction(s){if(!u(s))return!1;var o=a(s);return"[object Function]"==o||"[object GeneratorFunction]"==o||"[object AsyncFunction]"==o||"[object Proxy]"==o}},1907:(s,o,i)=>{"use strict";var a=i(41505),u=Function.prototype,_=u.call,w=a&&u.bind.bind(_,_);s.exports=a?w:function(s){return function(){return _.apply(s,arguments)}}},2205:function(s,o,i){var a;a=void 0!==i.g?i.g:this,s.exports=function(s){if(s.CSS&&s.CSS.escape)return s.CSS.escape;var cssEscape=function(s){if(0==arguments.length)throw new TypeError("`CSS.escape` requires an argument.");for(var o,i=String(s),a=i.length,u=-1,_="",w=i.charCodeAt(0);++u=1&&o<=31||127==o||0==u&&o>=48&&o<=57||1==u&&o>=48&&o<=57&&45==w?"\\"+o.toString(16)+" ":0==u&&1==a&&45==o||!(o>=128||45==o||95==o||o>=48&&o<=57||o>=65&&o<=90||o>=97&&o<=122)?"\\"+i.charAt(u):i.charAt(u):_+="�";return _};return s.CSS||(s.CSS={}),s.CSS.escape=cssEscape,cssEscape}(a)},2209:(s,o,i)=>{"use strict";var a,u=i(9404),_=function productionTypeChecker(){invariant(!1,"ImmutablePropTypes type checking code is stripped in production.")};_.isRequired=_;var w=function getProductionTypeChecker(){return _};function getPropType(s){var o=typeof s;return Array.isArray(s)?"array":s instanceof RegExp?"object":s instanceof u.Iterable?"Immutable."+s.toSource().split(" ")[0]:o}function createChainableTypeChecker(s){function checkType(o,i,a,u,_,w){for(var x=arguments.length,C=Array(x>6?x-6:0),j=6;j>",null!=i[a]?s.apply(void 0,[i,a,u,_,w].concat(C)):o?new Error("Required "+_+" `"+w+"` was not specified in `"+u+"`."):void 0}var o=checkType.bind(null,!1);return o.isRequired=checkType.bind(null,!0),o}function createIterableSubclassTypeChecker(s,o){return function createImmutableTypeChecker(s,o){return createChainableTypeChecker((function validate(i,a,u,_,w){var x=i[a];if(!o(x)){var C=getPropType(x);return new Error("Invalid "+_+" `"+w+"` of type `"+C+"` supplied to `"+u+"`, expected `"+s+"`.")}return null}))}("Iterable."+s,(function(s){return u.Iterable.isIterable(s)&&o(s)}))}(a={listOf:w,mapOf:w,orderedMapOf:w,setOf:w,orderedSetOf:w,stackOf:w,iterableOf:w,recordOf:w,shape:w,contains:w,mapContains:w,orderedMapContains:w,list:_,map:_,orderedMap:_,set:_,orderedSet:_,stack:_,seq:_,record:_,iterable:_}).iterable.indexed=createIterableSubclassTypeChecker("Indexed",u.Iterable.isIndexed),a.iterable.keyed=createIterableSubclassTypeChecker("Keyed",u.Iterable.isKeyed),s.exports=a},2404:(s,o,i)=>{var a=i(60270);s.exports=function isEqual(s,o){return a(s,o)}},2523:s=>{s.exports=function baseFindIndex(s,o,i,a){for(var u=s.length,_=i+(a?1:-1);a?_--:++_{"use strict";var a=i(45951),u=Object.defineProperty;s.exports=function(s,o){try{u(a,s,{value:o,configurable:!0,writable:!0})}catch(i){a[s]=o}return o}},2694:(s,o,i)=>{"use strict";var a=i(6925);function emptyFunction(){}function emptyFunctionWithReset(){}emptyFunctionWithReset.resetWarningCache=emptyFunction,s.exports=function(){function shim(s,o,i,u,_,w){if(w!==a){var x=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw x.name="Invariant Violation",x}}function getShim(){return shim}shim.isRequired=shim;var s={array:shim,bigint:shim,bool:shim,func:shim,number:shim,object:shim,string:shim,symbol:shim,any:shim,arrayOf:getShim,element:shim,elementType:shim,instanceOf:getShim,node:shim,objectOf:getShim,oneOf:getShim,oneOfType:getShim,shape:getShim,exact:getShim,checkPropTypes:emptyFunctionWithReset,resetWarningCache:emptyFunction};return s.PropTypes=s,s}},2874:s=>{s.exports={}},2875:(s,o,i)=>{"use strict";var a=i(23045),u=i(80376);s.exports=Object.keys||function keys(s){return a(s,u)}},2955:(s,o,i)=>{"use strict";var a,u=i(65606);function _defineProperty(s,o,i){return(o=function _toPropertyKey(s){var o=function _toPrimitive(s,o){if("object"!=typeof s||null===s)return s;var i=s[Symbol.toPrimitive];if(void 0!==i){var a=i.call(s,o||"default");if("object"!=typeof a)return a;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===o?String:Number)(s)}(s,"string");return"symbol"==typeof o?o:String(o)}(o))in s?Object.defineProperty(s,o,{value:i,enumerable:!0,configurable:!0,writable:!0}):s[o]=i,s}var _=i(86238),w=Symbol("lastResolve"),x=Symbol("lastReject"),C=Symbol("error"),j=Symbol("ended"),L=Symbol("lastPromise"),B=Symbol("handlePromise"),$=Symbol("stream");function createIterResult(s,o){return{value:s,done:o}}function readAndResolve(s){var o=s[w];if(null!==o){var i=s[$].read();null!==i&&(s[L]=null,s[w]=null,s[x]=null,o(createIterResult(i,!1)))}}function onReadable(s){u.nextTick(readAndResolve,s)}var U=Object.getPrototypeOf((function(){})),V=Object.setPrototypeOf((_defineProperty(a={get stream(){return this[$]},next:function next(){var s=this,o=this[C];if(null!==o)return Promise.reject(o);if(this[j])return Promise.resolve(createIterResult(void 0,!0));if(this[$].destroyed)return new Promise((function(o,i){u.nextTick((function(){s[C]?i(s[C]):o(createIterResult(void 0,!0))}))}));var i,a=this[L];if(a)i=new Promise(function wrapForNext(s,o){return function(i,a){s.then((function(){o[j]?i(createIterResult(void 0,!0)):o[B](i,a)}),a)}}(a,this));else{var _=this[$].read();if(null!==_)return Promise.resolve(createIterResult(_,!1));i=new Promise(this[B])}return this[L]=i,i}},Symbol.asyncIterator,(function(){return this})),_defineProperty(a,"return",(function _return(){var s=this;return new Promise((function(o,i){s[$].destroy(null,(function(s){s?i(s):o(createIterResult(void 0,!0))}))}))})),a),U);s.exports=function createReadableStreamAsyncIterator(s){var o,i=Object.create(V,(_defineProperty(o={},$,{value:s,writable:!0}),_defineProperty(o,w,{value:null,writable:!0}),_defineProperty(o,x,{value:null,writable:!0}),_defineProperty(o,C,{value:null,writable:!0}),_defineProperty(o,j,{value:s._readableState.endEmitted,writable:!0}),_defineProperty(o,B,{value:function value(s,o){var a=i[$].read();a?(i[L]=null,i[w]=null,i[x]=null,s(createIterResult(a,!1))):(i[w]=s,i[x]=o)},writable:!0}),o));return i[L]=null,_(s,(function(s){if(s&&"ERR_STREAM_PREMATURE_CLOSE"!==s.code){var o=i[x];return null!==o&&(i[L]=null,i[w]=null,i[x]=null,o(s)),void(i[C]=s)}var a=i[w];null!==a&&(i[L]=null,i[w]=null,i[x]=null,a(createIterResult(void 0,!0))),i[j]=!0})),s.on("readable",onReadable.bind(null,i)),i}},3110:(s,o,i)=>{const a=i(5187),u=i(85015),_=i(98023),w=i(53812),x=i(23805),C=i(85105),j=i(86804);class Namespace{constructor(s){this.elementMap={},this.elementDetection=[],this.Element=j.Element,this.KeyValuePair=j.KeyValuePair,s&&s.noDefault||this.useDefault(),this._attributeElementKeys=[],this._attributeElementArrayKeys=[]}use(s){return s.namespace&&s.namespace({base:this}),s.load&&s.load({base:this}),this}useDefault(){return this.register("null",j.NullElement).register("string",j.StringElement).register("number",j.NumberElement).register("boolean",j.BooleanElement).register("array",j.ArrayElement).register("object",j.ObjectElement).register("member",j.MemberElement).register("ref",j.RefElement).register("link",j.LinkElement),this.detect(a,j.NullElement,!1).detect(u,j.StringElement,!1).detect(_,j.NumberElement,!1).detect(w,j.BooleanElement,!1).detect(Array.isArray,j.ArrayElement,!1).detect(x,j.ObjectElement,!1),this}register(s,o){return this._elements=void 0,this.elementMap[s]=o,this}unregister(s){return this._elements=void 0,delete this.elementMap[s],this}detect(s,o,i){return void 0===i||i?this.elementDetection.unshift([s,o]):this.elementDetection.push([s,o]),this}toElement(s){if(s instanceof this.Element)return s;let o;for(let i=0;i{const o=s[0].toUpperCase()+s.substr(1);this._elements[o]=this.elementMap[s]}))),this._elements}get serialiser(){return new C(this)}}C.prototype.Namespace=Namespace,s.exports=Namespace},3121:(s,o,i)=>{"use strict";var a=i(65482),u=Math.min;s.exports=function(s){var o=a(s);return o>0?u(o,9007199254740991):0}},3209:(s,o,i)=>{var a=i(91596),u=i(53320),_=i(36306),w="__lodash_placeholder__",x=128,C=Math.min;s.exports=function mergeData(s,o){var i=s[1],j=o[1],L=i|j,B=L<131,$=j==x&&8==i||j==x&&256==i&&s[7].length<=o[8]||384==j&&o[7].length<=o[8]&&8==i;if(!B&&!$)return s;1&j&&(s[2]=o[2],L|=1&i?0:4);var U=o[3];if(U){var V=s[3];s[3]=V?a(V,U,o[4]):U,s[4]=V?_(s[3],w):o[4]}return(U=o[5])&&(V=s[5],s[5]=V?u(V,U,o[6]):U,s[6]=V?_(s[5],w):o[6]),(U=o[7])&&(s[7]=U),j&x&&(s[8]=null==s[8]?o[8]:C(s[8],o[8])),null==s[9]&&(s[9]=o[9]),s[0]=o[0],s[1]=L,s}},3650:(s,o,i)=>{var a=i(74335)(Object.keys,Object);s.exports=a},3656:(s,o,i)=>{s=i.nmd(s);var a=i(9325),u=i(89935),_=o&&!o.nodeType&&o,w=_&&s&&!s.nodeType&&s,x=w&&w.exports===_?a.Buffer:void 0,C=(x?x.isBuffer:void 0)||u;s.exports=C},4509:(s,o,i)=>{var a=i(12651);s.exports=function mapCacheHas(s){return a(this,s).has(s)}},4640:s=>{"use strict";var o=String;s.exports=function(s){try{return o(s)}catch(s){return"Object"}}},4664:(s,o,i)=>{var a=i(79770),u=i(63345),_=Object.prototype.propertyIsEnumerable,w=Object.getOwnPropertySymbols,x=w?function(s){return null==s?[]:(s=Object(s),a(w(s),(function(o){return _.call(s,o)})))}:u;s.exports=x},4901:(s,o,i)=>{var a=i(72552),u=i(30294),_=i(40346),w={};w["[object Float32Array]"]=w["[object Float64Array]"]=w["[object Int8Array]"]=w["[object Int16Array]"]=w["[object Int32Array]"]=w["[object Uint8Array]"]=w["[object Uint8ClampedArray]"]=w["[object Uint16Array]"]=w["[object Uint32Array]"]=!0,w["[object Arguments]"]=w["[object Array]"]=w["[object ArrayBuffer]"]=w["[object Boolean]"]=w["[object DataView]"]=w["[object Date]"]=w["[object Error]"]=w["[object Function]"]=w["[object Map]"]=w["[object Number]"]=w["[object Object]"]=w["[object RegExp]"]=w["[object Set]"]=w["[object String]"]=w["[object WeakMap]"]=!1,s.exports=function baseIsTypedArray(s){return _(s)&&u(s.length)&&!!w[a(s)]}},4993:(s,o,i)=>{"use strict";var a=i(16946),u=i(74239);s.exports=function(s){return a(u(s))}},5187:s=>{s.exports=function isNull(s){return null===s}},5419:s=>{s.exports=function(s,o,i,a){var u=new Blob(void 0!==a?[a,s]:[s],{type:i||"application/octet-stream"});if(void 0!==window.navigator.msSaveBlob)window.navigator.msSaveBlob(u,o);else{var _=window.URL&&window.URL.createObjectURL?window.URL.createObjectURL(u):window.webkitURL.createObjectURL(u),w=document.createElement("a");w.style.display="none",w.href=_,w.setAttribute("download",o),void 0===w.download&&w.setAttribute("target","_blank"),document.body.appendChild(w),w.click(),setTimeout((function(){document.body.removeChild(w),window.URL.revokeObjectURL(_)}),200)}}},5556:(s,o,i)=>{s.exports=i(2694)()},5861:(s,o,i)=>{var a=i(55580),u=i(68223),_=i(32804),w=i(76545),x=i(28303),C=i(72552),j=i(47473),L="[object Map]",B="[object Promise]",$="[object Set]",U="[object WeakMap]",V="[object DataView]",z=j(a),Y=j(u),Z=j(_),ee=j(w),ie=j(x),ae=C;(a&&ae(new a(new ArrayBuffer(1)))!=V||u&&ae(new u)!=L||_&&ae(_.resolve())!=B||w&&ae(new w)!=$||x&&ae(new x)!=U)&&(ae=function(s){var o=C(s),i="[object Object]"==o?s.constructor:void 0,a=i?j(i):"";if(a)switch(a){case z:return V;case Y:return L;case Z:return B;case ee:return $;case ie:return U}return o}),s.exports=ae},6048:s=>{s.exports=function negate(s){if("function"!=typeof s)throw new TypeError("Expected a function");return function(){var o=arguments;switch(o.length){case 0:return!s.call(this);case 1:return!s.call(this,o[0]);case 2:return!s.call(this,o[0],o[1]);case 3:return!s.call(this,o[0],o[1],o[2])}return!s.apply(this,o)}}},6188:s=>{"use strict";s.exports=Math.max},6205:s=>{s.exports={ROOT:0,GROUP:1,POSITION:2,SET:3,RANGE:4,REPETITION:5,REFERENCE:6,CHAR:7}},6233:(s,o,i)=>{const a=i(6048),u=i(10316),_=i(92340);class ArrayElement extends u{constructor(s,o,i){super(s||[],o,i),this.element="array"}primitive(){return"array"}get(s){return this.content[s]}getValue(s){const o=this.get(s);if(o)return o.toValue()}getIndex(s){return this.content[s]}set(s,o){return this.content[s]=this.refract(o),this}remove(s){const o=this.content.splice(s,1);return o.length?o[0]:null}map(s,o){return this.content.map(s,o)}flatMap(s,o){return this.map(s,o).reduce(((s,o)=>s.concat(o)),[])}compactMap(s,o){const i=[];return this.forEach((a=>{const u=s.bind(o)(a);u&&i.push(u)})),i}filter(s,o){return new _(this.content.filter(s,o))}reject(s,o){return this.filter(a(s),o)}reduce(s,o){let i,a;void 0!==o?(i=0,a=this.refract(o)):(i=1,a="object"===this.primitive()?this.first.value:this.first);for(let o=i;o{s.bind(o)(i,this.refract(a))}))}shift(){return this.content.shift()}unshift(s){this.content.unshift(this.refract(s))}push(s){return this.content.push(this.refract(s)),this}add(s){this.push(s)}findElements(s,o){const i=o||{},a=!!i.recursive,u=void 0===i.results?[]:i.results;return this.forEach(((o,i,_)=>{a&&void 0!==o.findElements&&o.findElements(s,{results:u,recursive:a}),s(o,i,_)&&u.push(o)})),u}find(s){return new _(this.findElements(s,{recursive:!0}))}findByElement(s){return this.find((o=>o.element===s))}findByClass(s){return this.find((o=>o.classes.includes(s)))}getById(s){return this.find((o=>o.id.toValue()===s)).first}includes(s){return this.content.some((o=>o.equals(s)))}contains(s){return this.includes(s)}empty(){return new this.constructor([])}"fantasy-land/empty"(){return this.empty()}concat(s){return new this.constructor(this.content.concat(s.content))}"fantasy-land/concat"(s){return this.concat(s)}"fantasy-land/map"(s){return new this.constructor(this.map(s))}"fantasy-land/chain"(s){return this.map((o=>s(o)),this).reduce(((s,o)=>s.concat(o)),this.empty())}"fantasy-land/filter"(s){return new this.constructor(this.content.filter(s))}"fantasy-land/reduce"(s,o){return this.content.reduce(s,o)}get length(){return this.content.length}get isEmpty(){return 0===this.content.length}get first(){return this.getIndex(0)}get second(){return this.getIndex(1)}get last(){return this.getIndex(this.length-1)}}ArrayElement.empty=function empty(){return new this},ArrayElement["fantasy-land/empty"]=ArrayElement.empty,"undefined"!=typeof Symbol&&(ArrayElement.prototype[Symbol.iterator]=function symbol(){return this.content[Symbol.iterator]()}),s.exports=ArrayElement},6499:(s,o,i)=>{"use strict";var a=i(1907),u=0,_=Math.random(),w=a(1..toString);s.exports=function(s){return"Symbol("+(void 0===s?"":s)+")_"+w(++u+_,36)}},6549:s=>{"use strict";s.exports=Object.getOwnPropertyDescriptor},6925:s=>{"use strict";s.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},7057:(s,o,i)=>{"use strict";var a=i(11470).charAt,u=i(90160),_=i(64932),w=i(60183),x=i(59550),C="String Iterator",j=_.set,L=_.getterFor(C);w(String,"String",(function(s){j(this,{type:C,string:u(s),index:0})}),(function next(){var s,o=L(this),i=o.string,u=o.index;return u>=i.length?x(void 0,!0):(s=a(i,u),o.index+=s.length,x(s,!1))}))},7176:(s,o,i)=>{"use strict";var a,u=i(73126),_=i(75795);try{a=[].__proto__===Array.prototype}catch(s){if(!s||"object"!=typeof s||!("code"in s)||"ERR_PROTO_ACCESS"!==s.code)throw s}var w=!!a&&_&&_(Object.prototype,"__proto__"),x=Object,C=x.getPrototypeOf;s.exports=w&&"function"==typeof w.get?u([w.get]):"function"==typeof C&&function getDunder(s){return C(null==s?s:x(s))}},7309:(s,o,i)=>{var a=i(62006)(i(24713));s.exports=a},7376:s=>{"use strict";s.exports=!0},7463:(s,o,i)=>{"use strict";var a=i(98828),u=i(62250),_=/#|\.prototype\./,isForced=function(s,o){var i=x[w(s)];return i===j||i!==C&&(u(o)?a(o):!!o)},w=isForced.normalize=function(s){return String(s).replace(_,".").toLowerCase()},x=isForced.data={},C=isForced.NATIVE="N",j=isForced.POLYFILL="P";s.exports=isForced},7666:(s,o,i)=>{var a=i(84851),u=i(953);function _extends(){var o;return s.exports=_extends=a?u(o=a).call(o):function(s){for(var o=1;o{const a=i(6205);o.wordBoundary=()=>({type:a.POSITION,value:"b"}),o.nonWordBoundary=()=>({type:a.POSITION,value:"B"}),o.begin=()=>({type:a.POSITION,value:"^"}),o.end=()=>({type:a.POSITION,value:"$"})},8068:s=>{"use strict";var o=(()=>{var s=Object.defineProperty,o=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.getOwnPropertySymbols,u=Object.prototype.hasOwnProperty,_=Object.prototype.propertyIsEnumerable,__defNormalProp=(o,i,a)=>i in o?s(o,i,{enumerable:!0,configurable:!0,writable:!0,value:a}):o[i]=a,__spreadValues=(s,o)=>{for(var i in o||(o={}))u.call(o,i)&&__defNormalProp(s,i,o[i]);if(a)for(var i of a(o))_.call(o,i)&&__defNormalProp(s,i,o[i]);return s},__publicField=(s,o,i)=>__defNormalProp(s,"symbol"!=typeof o?o+"":o,i),w={};((o,i)=>{for(var a in i)s(o,a,{get:i[a],enumerable:!0})})(w,{DEFAULT_OPTIONS:()=>C,DEFAULT_UUID_LENGTH:()=>x,default:()=>B});var x=6,C={dictionary:"alphanum",shuffle:!0,debug:!1,length:x,counter:0},j=class _ShortUniqueId{constructor(s={}){__publicField(this,"counter"),__publicField(this,"debug"),__publicField(this,"dict"),__publicField(this,"version"),__publicField(this,"dictIndex",0),__publicField(this,"dictRange",[]),__publicField(this,"lowerBound",0),__publicField(this,"upperBound",0),__publicField(this,"dictLength",0),__publicField(this,"uuidLength"),__publicField(this,"_digit_first_ascii",48),__publicField(this,"_digit_last_ascii",58),__publicField(this,"_alpha_lower_first_ascii",97),__publicField(this,"_alpha_lower_last_ascii",123),__publicField(this,"_hex_last_ascii",103),__publicField(this,"_alpha_upper_first_ascii",65),__publicField(this,"_alpha_upper_last_ascii",91),__publicField(this,"_number_dict_ranges",{digits:[this._digit_first_ascii,this._digit_last_ascii]}),__publicField(this,"_alpha_dict_ranges",{lowerCase:[this._alpha_lower_first_ascii,this._alpha_lower_last_ascii],upperCase:[this._alpha_upper_first_ascii,this._alpha_upper_last_ascii]}),__publicField(this,"_alpha_lower_dict_ranges",{lowerCase:[this._alpha_lower_first_ascii,this._alpha_lower_last_ascii]}),__publicField(this,"_alpha_upper_dict_ranges",{upperCase:[this._alpha_upper_first_ascii,this._alpha_upper_last_ascii]}),__publicField(this,"_alphanum_dict_ranges",{digits:[this._digit_first_ascii,this._digit_last_ascii],lowerCase:[this._alpha_lower_first_ascii,this._alpha_lower_last_ascii],upperCase:[this._alpha_upper_first_ascii,this._alpha_upper_last_ascii]}),__publicField(this,"_alphanum_lower_dict_ranges",{digits:[this._digit_first_ascii,this._digit_last_ascii],lowerCase:[this._alpha_lower_first_ascii,this._alpha_lower_last_ascii]}),__publicField(this,"_alphanum_upper_dict_ranges",{digits:[this._digit_first_ascii,this._digit_last_ascii],upperCase:[this._alpha_upper_first_ascii,this._alpha_upper_last_ascii]}),__publicField(this,"_hex_dict_ranges",{decDigits:[this._digit_first_ascii,this._digit_last_ascii],alphaDigits:[this._alpha_lower_first_ascii,this._hex_last_ascii]}),__publicField(this,"_dict_ranges",{_number_dict_ranges:this._number_dict_ranges,_alpha_dict_ranges:this._alpha_dict_ranges,_alpha_lower_dict_ranges:this._alpha_lower_dict_ranges,_alpha_upper_dict_ranges:this._alpha_upper_dict_ranges,_alphanum_dict_ranges:this._alphanum_dict_ranges,_alphanum_lower_dict_ranges:this._alphanum_lower_dict_ranges,_alphanum_upper_dict_ranges:this._alphanum_upper_dict_ranges,_hex_dict_ranges:this._hex_dict_ranges}),__publicField(this,"log",((...s)=>{const o=[...s];o[0]="[short-unique-id] ".concat(s[0]),!0!==this.debug||"undefined"==typeof console||null===console||console.log(...o)})),__publicField(this,"_normalizeDictionary",((s,o)=>{let i;if(s&&Array.isArray(s)&&s.length>1)i=s;else{i=[],this.dictIndex=0;const o="_".concat(s,"_dict_ranges"),a=this._dict_ranges[o];let u=0;for(const[,s]of Object.entries(a)){const[o,i]=s;u+=Math.abs(i-o)}i=new Array(u);let _=0;for(const[,s]of Object.entries(a)){this.dictRange=s,this.lowerBound=this.dictRange[0],this.upperBound=this.dictRange[1];const o=this.lowerBound<=this.upperBound,a=this.lowerBound,u=this.upperBound;if(o)for(let s=a;su;s--)i[_++]=String.fromCharCode(s),this.dictIndex=s}i.length=_}if(o){for(let s=i.length-1;s>0;s--){const o=Math.floor(Math.random()*(s+1));[i[s],i[o]]=[i[o],i[s]]}}return i})),__publicField(this,"setDictionary",((s,o)=>{this.dict=this._normalizeDictionary(s,o),this.dictLength=this.dict.length,this.setCounter(0)})),__publicField(this,"seq",(()=>this.sequentialUUID())),__publicField(this,"sequentialUUID",(()=>{const s=this.dictLength,o=this.dict;let i=this.counter;const a=[];do{const u=i%s;i=Math.trunc(i/s),a.push(o[u])}while(0!==i);const u=a.join("");return this.counter+=1,u})),__publicField(this,"rnd",((s=this.uuidLength||x)=>this.randomUUID(s))),__publicField(this,"randomUUID",((s=this.uuidLength||x)=>{if(null==s||s<1)throw new Error("Invalid UUID Length Provided");const o=new Array(s),i=this.dictLength,a=this.dict;for(let u=0;uthis.formattedUUID(s,o))),__publicField(this,"formattedUUID",((s,o)=>{const i={$r:this.randomUUID,$s:this.sequentialUUID,$t:this.stamp};return s.replace(/\$[rs]\d{0,}|\$t0|\$t[1-9]\d{1,}/g,(s=>{const a=s.slice(0,2),u=Number.parseInt(s.slice(2),10);return"$s"===a?i[a]().padStart(u,"0"):"$t"===a&&o?i[a](u,o):i[a](u)}))})),__publicField(this,"availableUUIDs",((s=this.uuidLength)=>Number.parseFloat(([...new Set(this.dict)].length**s).toFixed(0)))),__publicField(this,"_collisionCache",new Map),__publicField(this,"approxMaxBeforeCollision",((s=this.availableUUIDs(this.uuidLength))=>{const o=s,i=this._collisionCache.get(o);if(void 0!==i)return i;const a=Number.parseFloat(Math.sqrt(Math.PI/2*s).toFixed(20));return this._collisionCache.set(o,a),a})),__publicField(this,"collisionProbability",((s=this.availableUUIDs(this.uuidLength),o=this.uuidLength)=>Number.parseFloat((this.approxMaxBeforeCollision(s)/this.availableUUIDs(o)).toFixed(20)))),__publicField(this,"uniqueness",((s=this.availableUUIDs(this.uuidLength))=>{const o=Number.parseFloat((1-this.approxMaxBeforeCollision(s)/s).toFixed(20));return o>1?1:o<0?0:o})),__publicField(this,"getVersion",(()=>this.version)),__publicField(this,"stamp",((s,o)=>{const i=Math.floor(+(o||new Date)/1e3).toString(16);if("number"==typeof s&&0===s)return i;if("number"!=typeof s||s<10)throw new Error(["Param finalLength must be a number greater than or equal to 10,","or 0 if you want the raw hexadecimal timestamp"].join("\n"));const a=s-9,u=Math.round(Math.random()*(a>15?15:a)),_=this.randomUUID(a);return"".concat(_.substring(0,u)).concat(i).concat(_.substring(u)).concat(u.toString(16))})),__publicField(this,"parseStamp",((s,o)=>{if(o&&!/t0|t[1-9]\d{1,}/.test(o))throw new Error("Cannot extract date from a formated UUID with no timestamp in the format");const i=o?o.replace(/\$[rs]\d{0,}|\$t0|\$t[1-9]\d{1,}/g,(s=>{const o={$r:s=>[...Array(s)].map((()=>"r")).join(""),$s:s=>[...Array(s)].map((()=>"s")).join(""),$t:s=>[...Array(s)].map((()=>"t")).join("")},i=s.slice(0,2),a=Number.parseInt(s.slice(2),10);return o[i](a)})).replace(/^(.*?)(t{8,})(.*)$/g,((o,i,a)=>s.substring(i.length,i.length+a.length))):s;if(8===i.length)return new Date(1e3*Number.parseInt(i,16));if(i.length<10)throw new Error("Stamp length invalid");const a=Number.parseInt(i.substring(i.length-1),16);return new Date(1e3*Number.parseInt(i.substring(a,a+8),16))})),__publicField(this,"setCounter",(s=>{this.counter=s})),__publicField(this,"validate",((s,o)=>{const i=o?this._normalizeDictionary(o):this.dict;return s.split("").every((s=>i.includes(s)))}));const o=__spreadValues(__spreadValues({},C),s);this.counter=0,this.debug=!1,this.dict=[],this.version="5.3.2";const{dictionary:i,shuffle:a,length:u,counter:_}=o;this.uuidLength=u,this.setDictionary(i,a),this.setCounter(_),this.debug=o.debug,this.log(this.dict),this.log("Generator instantiated with Dictionary Size ".concat(this.dictLength," and counter set to ").concat(this.counter)),this.log=this.log.bind(this),this.setDictionary=this.setDictionary.bind(this),this.setCounter=this.setCounter.bind(this),this.seq=this.seq.bind(this),this.sequentialUUID=this.sequentialUUID.bind(this),this.rnd=this.rnd.bind(this),this.randomUUID=this.randomUUID.bind(this),this.fmt=this.fmt.bind(this),this.formattedUUID=this.formattedUUID.bind(this),this.availableUUIDs=this.availableUUIDs.bind(this),this.approxMaxBeforeCollision=this.approxMaxBeforeCollision.bind(this),this.collisionProbability=this.collisionProbability.bind(this),this.uniqueness=this.uniqueness.bind(this),this.getVersion=this.getVersion.bind(this),this.stamp=this.stamp.bind(this),this.parseStamp=this.parseStamp.bind(this)}};__publicField(j,"default",j);var L,B=j;return L=w,((a,_,w,x)=>{if(_&&"object"==typeof _||"function"==typeof _)for(let C of i(_))u.call(a,C)||C===w||s(a,C,{get:()=>_[C],enumerable:!(x=o(_,C))||x.enumerable});return a})(s({},"__esModule",{value:!0}),L)})();s.exports=o.default,"undefined"!=typeof window&&(o=o.default)},9325:(s,o,i)=>{var a=i(34840),u="object"==typeof self&&self&&self.Object===Object&&self,_=a||u||Function("return this")();s.exports=_},9404:function(s){s.exports=function(){"use strict";var s=Array.prototype.slice;function createClass(s,o){o&&(s.prototype=Object.create(o.prototype)),s.prototype.constructor=s}function Iterable(s){return isIterable(s)?s:Seq(s)}function KeyedIterable(s){return isKeyed(s)?s:KeyedSeq(s)}function IndexedIterable(s){return isIndexed(s)?s:IndexedSeq(s)}function SetIterable(s){return isIterable(s)&&!isAssociative(s)?s:SetSeq(s)}function isIterable(s){return!(!s||!s[o])}function isKeyed(s){return!(!s||!s[i])}function isIndexed(s){return!(!s||!s[a])}function isAssociative(s){return isKeyed(s)||isIndexed(s)}function isOrdered(s){return!(!s||!s[u])}createClass(KeyedIterable,Iterable),createClass(IndexedIterable,Iterable),createClass(SetIterable,Iterable),Iterable.isIterable=isIterable,Iterable.isKeyed=isKeyed,Iterable.isIndexed=isIndexed,Iterable.isAssociative=isAssociative,Iterable.isOrdered=isOrdered,Iterable.Keyed=KeyedIterable,Iterable.Indexed=IndexedIterable,Iterable.Set=SetIterable;var o="@@__IMMUTABLE_ITERABLE__@@",i="@@__IMMUTABLE_KEYED__@@",a="@@__IMMUTABLE_INDEXED__@@",u="@@__IMMUTABLE_ORDERED__@@",_="delete",w=5,x=1<>>0;if(""+i!==o||4294967295===i)return NaN;o=i}return o<0?ensureSize(s)+o:o}function returnTrue(){return!0}function wholeSlice(s,o,i){return(0===s||void 0!==i&&s<=-i)&&(void 0===o||void 0!==i&&o>=i)}function resolveBegin(s,o){return resolveIndex(s,o,0)}function resolveEnd(s,o){return resolveIndex(s,o,o)}function resolveIndex(s,o,i){return void 0===s?i:s<0?Math.max(0,o+s):void 0===o?s:Math.min(o,s)}var $=0,U=1,V=2,z="function"==typeof Symbol&&Symbol.iterator,Y="@@iterator",Z=z||Y;function Iterator(s){this.next=s}function iteratorValue(s,o,i,a){var u=0===s?o:1===s?i:[o,i];return a?a.value=u:a={value:u,done:!1},a}function iteratorDone(){return{value:void 0,done:!0}}function hasIterator(s){return!!getIteratorFn(s)}function isIterator(s){return s&&"function"==typeof s.next}function getIterator(s){var o=getIteratorFn(s);return o&&o.call(s)}function getIteratorFn(s){var o=s&&(z&&s[z]||s[Y]);if("function"==typeof o)return o}function isArrayLike(s){return s&&"number"==typeof s.length}function Seq(s){return null==s?emptySequence():isIterable(s)?s.toSeq():seqFromValue(s)}function KeyedSeq(s){return null==s?emptySequence().toKeyedSeq():isIterable(s)?isKeyed(s)?s.toSeq():s.fromEntrySeq():keyedSeqFromValue(s)}function IndexedSeq(s){return null==s?emptySequence():isIterable(s)?isKeyed(s)?s.entrySeq():s.toIndexedSeq():indexedSeqFromValue(s)}function SetSeq(s){return(null==s?emptySequence():isIterable(s)?isKeyed(s)?s.entrySeq():s:indexedSeqFromValue(s)).toSetSeq()}Iterator.prototype.toString=function(){return"[Iterator]"},Iterator.KEYS=$,Iterator.VALUES=U,Iterator.ENTRIES=V,Iterator.prototype.inspect=Iterator.prototype.toSource=function(){return this.toString()},Iterator.prototype[Z]=function(){return this},createClass(Seq,Iterable),Seq.of=function(){return Seq(arguments)},Seq.prototype.toSeq=function(){return this},Seq.prototype.toString=function(){return this.__toString("Seq {","}")},Seq.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},Seq.prototype.__iterate=function(s,o){return seqIterate(this,s,o,!0)},Seq.prototype.__iterator=function(s,o){return seqIterator(this,s,o,!0)},createClass(KeyedSeq,Seq),KeyedSeq.prototype.toKeyedSeq=function(){return this},createClass(IndexedSeq,Seq),IndexedSeq.of=function(){return IndexedSeq(arguments)},IndexedSeq.prototype.toIndexedSeq=function(){return this},IndexedSeq.prototype.toString=function(){return this.__toString("Seq [","]")},IndexedSeq.prototype.__iterate=function(s,o){return seqIterate(this,s,o,!1)},IndexedSeq.prototype.__iterator=function(s,o){return seqIterator(this,s,o,!1)},createClass(SetSeq,Seq),SetSeq.of=function(){return SetSeq(arguments)},SetSeq.prototype.toSetSeq=function(){return this},Seq.isSeq=isSeq,Seq.Keyed=KeyedSeq,Seq.Set=SetSeq,Seq.Indexed=IndexedSeq;var ee,ie,ae,ce="@@__IMMUTABLE_SEQ__@@";function ArraySeq(s){this._array=s,this.size=s.length}function ObjectSeq(s){var o=Object.keys(s);this._object=s,this._keys=o,this.size=o.length}function IterableSeq(s){this._iterable=s,this.size=s.length||s.size}function IteratorSeq(s){this._iterator=s,this._iteratorCache=[]}function isSeq(s){return!(!s||!s[ce])}function emptySequence(){return ee||(ee=new ArraySeq([]))}function keyedSeqFromValue(s){var o=Array.isArray(s)?new ArraySeq(s).fromEntrySeq():isIterator(s)?new IteratorSeq(s).fromEntrySeq():hasIterator(s)?new IterableSeq(s).fromEntrySeq():"object"==typeof s?new ObjectSeq(s):void 0;if(!o)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+s);return o}function indexedSeqFromValue(s){var o=maybeIndexedSeqFromValue(s);if(!o)throw new TypeError("Expected Array or iterable object of values: "+s);return o}function seqFromValue(s){var o=maybeIndexedSeqFromValue(s)||"object"==typeof s&&new ObjectSeq(s);if(!o)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+s);return o}function maybeIndexedSeqFromValue(s){return isArrayLike(s)?new ArraySeq(s):isIterator(s)?new IteratorSeq(s):hasIterator(s)?new IterableSeq(s):void 0}function seqIterate(s,o,i,a){var u=s._cache;if(u){for(var _=u.length-1,w=0;w<=_;w++){var x=u[i?_-w:w];if(!1===o(x[1],a?x[0]:w,s))return w+1}return w}return s.__iterateUncached(o,i)}function seqIterator(s,o,i,a){var u=s._cache;if(u){var _=u.length-1,w=0;return new Iterator((function(){var s=u[i?_-w:w];return w++>_?iteratorDone():iteratorValue(o,a?s[0]:w-1,s[1])}))}return s.__iteratorUncached(o,i)}function fromJS(s,o){return o?fromJSWith(o,s,"",{"":s}):fromJSDefault(s)}function fromJSWith(s,o,i,a){return Array.isArray(o)?s.call(a,i,IndexedSeq(o).map((function(i,a){return fromJSWith(s,i,a,o)}))):isPlainObj(o)?s.call(a,i,KeyedSeq(o).map((function(i,a){return fromJSWith(s,i,a,o)}))):o}function fromJSDefault(s){return Array.isArray(s)?IndexedSeq(s).map(fromJSDefault).toList():isPlainObj(s)?KeyedSeq(s).map(fromJSDefault).toMap():s}function isPlainObj(s){return s&&(s.constructor===Object||void 0===s.constructor)}function is(s,o){if(s===o||s!=s&&o!=o)return!0;if(!s||!o)return!1;if("function"==typeof s.valueOf&&"function"==typeof o.valueOf){if((s=s.valueOf())===(o=o.valueOf())||s!=s&&o!=o)return!0;if(!s||!o)return!1}return!("function"!=typeof s.equals||"function"!=typeof o.equals||!s.equals(o))}function deepEqual(s,o){if(s===o)return!0;if(!isIterable(o)||void 0!==s.size&&void 0!==o.size&&s.size!==o.size||void 0!==s.__hash&&void 0!==o.__hash&&s.__hash!==o.__hash||isKeyed(s)!==isKeyed(o)||isIndexed(s)!==isIndexed(o)||isOrdered(s)!==isOrdered(o))return!1;if(0===s.size&&0===o.size)return!0;var i=!isAssociative(s);if(isOrdered(s)){var a=s.entries();return o.every((function(s,o){var u=a.next().value;return u&&is(u[1],s)&&(i||is(u[0],o))}))&&a.next().done}var u=!1;if(void 0===s.size)if(void 0===o.size)"function"==typeof s.cacheResult&&s.cacheResult();else{u=!0;var _=s;s=o,o=_}var w=!0,x=o.__iterate((function(o,a){if(i?!s.has(o):u?!is(o,s.get(a,j)):!is(s.get(a,j),o))return w=!1,!1}));return w&&s.size===x}function Repeat(s,o){if(!(this instanceof Repeat))return new Repeat(s,o);if(this._value=s,this.size=void 0===o?1/0:Math.max(0,o),0===this.size){if(ie)return ie;ie=this}}function invariant(s,o){if(!s)throw new Error(o)}function Range(s,o,i){if(!(this instanceof Range))return new Range(s,o,i);if(invariant(0!==i,"Cannot step a Range by 0"),s=s||0,void 0===o&&(o=1/0),i=void 0===i?1:Math.abs(i),oa?iteratorDone():iteratorValue(s,u,i[o?a-u++:u++])}))},createClass(ObjectSeq,KeyedSeq),ObjectSeq.prototype.get=function(s,o){return void 0===o||this.has(s)?this._object[s]:o},ObjectSeq.prototype.has=function(s){return this._object.hasOwnProperty(s)},ObjectSeq.prototype.__iterate=function(s,o){for(var i=this._object,a=this._keys,u=a.length-1,_=0;_<=u;_++){var w=a[o?u-_:_];if(!1===s(i[w],w,this))return _+1}return _},ObjectSeq.prototype.__iterator=function(s,o){var i=this._object,a=this._keys,u=a.length-1,_=0;return new Iterator((function(){var w=a[o?u-_:_];return _++>u?iteratorDone():iteratorValue(s,w,i[w])}))},ObjectSeq.prototype[u]=!0,createClass(IterableSeq,IndexedSeq),IterableSeq.prototype.__iterateUncached=function(s,o){if(o)return this.cacheResult().__iterate(s,o);var i=getIterator(this._iterable),a=0;if(isIterator(i))for(var u;!(u=i.next()).done&&!1!==s(u.value,a++,this););return a},IterableSeq.prototype.__iteratorUncached=function(s,o){if(o)return this.cacheResult().__iterator(s,o);var i=getIterator(this._iterable);if(!isIterator(i))return new Iterator(iteratorDone);var a=0;return new Iterator((function(){var o=i.next();return o.done?o:iteratorValue(s,a++,o.value)}))},createClass(IteratorSeq,IndexedSeq),IteratorSeq.prototype.__iterateUncached=function(s,o){if(o)return this.cacheResult().__iterate(s,o);for(var i,a=this._iterator,u=this._iteratorCache,_=0;_=a.length){var o=i.next();if(o.done)return o;a[u]=o.value}return iteratorValue(s,u,a[u++])}))},createClass(Repeat,IndexedSeq),Repeat.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},Repeat.prototype.get=function(s,o){return this.has(s)?this._value:o},Repeat.prototype.includes=function(s){return is(this._value,s)},Repeat.prototype.slice=function(s,o){var i=this.size;return wholeSlice(s,o,i)?this:new Repeat(this._value,resolveEnd(o,i)-resolveBegin(s,i))},Repeat.prototype.reverse=function(){return this},Repeat.prototype.indexOf=function(s){return is(this._value,s)?0:-1},Repeat.prototype.lastIndexOf=function(s){return is(this._value,s)?this.size:-1},Repeat.prototype.__iterate=function(s,o){for(var i=0;i=0&&o=0&&ii?iteratorDone():iteratorValue(s,_++,w)}))},Range.prototype.equals=function(s){return s instanceof Range?this._start===s._start&&this._end===s._end&&this._step===s._step:deepEqual(this,s)},createClass(Collection,Iterable),createClass(KeyedCollection,Collection),createClass(IndexedCollection,Collection),createClass(SetCollection,Collection),Collection.Keyed=KeyedCollection,Collection.Indexed=IndexedCollection,Collection.Set=SetCollection;var le="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function imul(s,o){var i=65535&(s|=0),a=65535&(o|=0);return i*a+((s>>>16)*a+i*(o>>>16)<<16>>>0)|0};function smi(s){return s>>>1&1073741824|3221225471&s}function hash(s){if(!1===s||null==s)return 0;if("function"==typeof s.valueOf&&(!1===(s=s.valueOf())||null==s))return 0;if(!0===s)return 1;var o=typeof s;if("number"===o){if(s!=s||s===1/0)return 0;var i=0|s;for(i!==s&&(i^=4294967295*s);s>4294967295;)i^=s/=4294967295;return smi(i)}if("string"===o)return s.length>Se?cachedHashString(s):hashString(s);if("function"==typeof s.hashCode)return s.hashCode();if("object"===o)return hashJSObj(s);if("function"==typeof s.toString)return hashString(s.toString());throw new Error("Value type "+o+" cannot be hashed.")}function cachedHashString(s){var o=Pe[s];return void 0===o&&(o=hashString(s),xe===we&&(xe=0,Pe={}),xe++,Pe[s]=o),o}function hashString(s){for(var o=0,i=0;i0)switch(s.nodeType){case 1:return s.uniqueID;case 9:return s.documentElement&&s.documentElement.uniqueID}}var fe,ye="function"==typeof WeakMap;ye&&(fe=new WeakMap);var be=0,_e="__immutablehash__";"function"==typeof Symbol&&(_e=Symbol(_e));var Se=16,we=255,xe=0,Pe={};function assertNotInfinite(s){invariant(s!==1/0,"Cannot perform this action with an infinite size.")}function Map(s){return null==s?emptyMap():isMap(s)&&!isOrdered(s)?s:emptyMap().withMutations((function(o){var i=KeyedIterable(s);assertNotInfinite(i.size),i.forEach((function(s,i){return o.set(i,s)}))}))}function isMap(s){return!(!s||!s[Re])}createClass(Map,KeyedCollection),Map.of=function(){var o=s.call(arguments,0);return emptyMap().withMutations((function(s){for(var i=0;i=o.length)throw new Error("Missing value for key: "+o[i]);s.set(o[i],o[i+1])}}))},Map.prototype.toString=function(){return this.__toString("Map {","}")},Map.prototype.get=function(s,o){return this._root?this._root.get(0,void 0,s,o):o},Map.prototype.set=function(s,o){return updateMap(this,s,o)},Map.prototype.setIn=function(s,o){return this.updateIn(s,j,(function(){return o}))},Map.prototype.remove=function(s){return updateMap(this,s,j)},Map.prototype.deleteIn=function(s){return this.updateIn(s,(function(){return j}))},Map.prototype.update=function(s,o,i){return 1===arguments.length?s(this):this.updateIn([s],o,i)},Map.prototype.updateIn=function(s,o,i){i||(i=o,o=void 0);var a=updateInDeepMap(this,forceIterator(s),o,i);return a===j?void 0:a},Map.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):emptyMap()},Map.prototype.merge=function(){return mergeIntoMapWith(this,void 0,arguments)},Map.prototype.mergeWith=function(o){return mergeIntoMapWith(this,o,s.call(arguments,1))},Map.prototype.mergeIn=function(o){var i=s.call(arguments,1);return this.updateIn(o,emptyMap(),(function(s){return"function"==typeof s.merge?s.merge.apply(s,i):i[i.length-1]}))},Map.prototype.mergeDeep=function(){return mergeIntoMapWith(this,deepMerger,arguments)},Map.prototype.mergeDeepWith=function(o){var i=s.call(arguments,1);return mergeIntoMapWith(this,deepMergerWith(o),i)},Map.prototype.mergeDeepIn=function(o){var i=s.call(arguments,1);return this.updateIn(o,emptyMap(),(function(s){return"function"==typeof s.mergeDeep?s.mergeDeep.apply(s,i):i[i.length-1]}))},Map.prototype.sort=function(s){return OrderedMap(sortFactory(this,s))},Map.prototype.sortBy=function(s,o){return OrderedMap(sortFactory(this,o,s))},Map.prototype.withMutations=function(s){var o=this.asMutable();return s(o),o.wasAltered()?o.__ensureOwner(this.__ownerID):this},Map.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new OwnerID)},Map.prototype.asImmutable=function(){return this.__ensureOwner()},Map.prototype.wasAltered=function(){return this.__altered},Map.prototype.__iterator=function(s,o){return new MapIterator(this,s,o)},Map.prototype.__iterate=function(s,o){var i=this,a=0;return this._root&&this._root.iterate((function(o){return a++,s(o[1],o[0],i)}),o),a},Map.prototype.__ensureOwner=function(s){return s===this.__ownerID?this:s?makeMap(this.size,this._root,s,this.__hash):(this.__ownerID=s,this.__altered=!1,this)},Map.isMap=isMap;var Te,Re="@@__IMMUTABLE_MAP__@@",$e=Map.prototype;function ArrayMapNode(s,o){this.ownerID=s,this.entries=o}function BitmapIndexedNode(s,o,i){this.ownerID=s,this.bitmap=o,this.nodes=i}function HashArrayMapNode(s,o,i){this.ownerID=s,this.count=o,this.nodes=i}function HashCollisionNode(s,o,i){this.ownerID=s,this.keyHash=o,this.entries=i}function ValueNode(s,o,i){this.ownerID=s,this.keyHash=o,this.entry=i}function MapIterator(s,o,i){this._type=o,this._reverse=i,this._stack=s._root&&mapIteratorFrame(s._root)}function mapIteratorValue(s,o){return iteratorValue(s,o[0],o[1])}function mapIteratorFrame(s,o){return{node:s,index:0,__prev:o}}function makeMap(s,o,i,a){var u=Object.create($e);return u.size=s,u._root=o,u.__ownerID=i,u.__hash=a,u.__altered=!1,u}function emptyMap(){return Te||(Te=makeMap(0))}function updateMap(s,o,i){var a,u;if(s._root){var _=MakeRef(L),w=MakeRef(B);if(a=updateNode(s._root,s.__ownerID,0,void 0,o,i,_,w),!w.value)return s;u=s.size+(_.value?i===j?-1:1:0)}else{if(i===j)return s;u=1,a=new ArrayMapNode(s.__ownerID,[[o,i]])}return s.__ownerID?(s.size=u,s._root=a,s.__hash=void 0,s.__altered=!0,s):a?makeMap(u,a):emptyMap()}function updateNode(s,o,i,a,u,_,w,x){return s?s.update(o,i,a,u,_,w,x):_===j?s:(SetRef(x),SetRef(w),new ValueNode(o,a,[u,_]))}function isLeafNode(s){return s.constructor===ValueNode||s.constructor===HashCollisionNode}function mergeIntoNode(s,o,i,a,u){if(s.keyHash===a)return new HashCollisionNode(o,a,[s.entry,u]);var _,x=(0===i?s.keyHash:s.keyHash>>>i)&C,j=(0===i?a:a>>>i)&C;return new BitmapIndexedNode(o,1<>>=1)w[C]=1&i?o[_++]:void 0;return w[a]=u,new HashArrayMapNode(s,_+1,w)}function mergeIntoMapWith(s,o,i){for(var a=[],u=0;u>1&1431655765))+(s>>2&858993459))+(s>>4)&252645135,s+=s>>8,127&(s+=s>>16)}function setIn(s,o,i,a){var u=a?s:arrCopy(s);return u[o]=i,u}function spliceIn(s,o,i,a){var u=s.length+1;if(a&&o+1===u)return s[o]=i,s;for(var _=new Array(u),w=0,x=0;x=qe)return createNodes(s,C,a,u);var U=s&&s===this.ownerID,V=U?C:arrCopy(C);return $?x?L===B-1?V.pop():V[L]=V.pop():V[L]=[a,u]:V.push([a,u]),U?(this.entries=V,this):new ArrayMapNode(s,V)}},BitmapIndexedNode.prototype.get=function(s,o,i,a){void 0===o&&(o=hash(i));var u=1<<((0===s?o:o>>>s)&C),_=this.bitmap;return _&u?this.nodes[popCount(_&u-1)].get(s+w,o,i,a):a},BitmapIndexedNode.prototype.update=function(s,o,i,a,u,_,x){void 0===i&&(i=hash(a));var L=(0===o?i:i>>>o)&C,B=1<=ze)return expandNodes(s,z,$,L,Z);if(U&&!Z&&2===z.length&&isLeafNode(z[1^V]))return z[1^V];if(U&&Z&&1===z.length&&isLeafNode(Z))return Z;var ee=s&&s===this.ownerID,ie=U?Z?$:$^B:$|B,ae=U?Z?setIn(z,V,Z,ee):spliceOut(z,V,ee):spliceIn(z,V,Z,ee);return ee?(this.bitmap=ie,this.nodes=ae,this):new BitmapIndexedNode(s,ie,ae)},HashArrayMapNode.prototype.get=function(s,o,i,a){void 0===o&&(o=hash(i));var u=(0===s?o:o>>>s)&C,_=this.nodes[u];return _?_.get(s+w,o,i,a):a},HashArrayMapNode.prototype.update=function(s,o,i,a,u,_,x){void 0===i&&(i=hash(a));var L=(0===o?i:i>>>o)&C,B=u===j,$=this.nodes,U=$[L];if(B&&!U)return this;var V=updateNode(U,s,o+w,i,a,u,_,x);if(V===U)return this;var z=this.count;if(U){if(!V&&--z0&&a=0&&s>>o&C;if(a>=this.array.length)return new VNode([],s);var u,_=0===a;if(o>0){var x=this.array[a];if((u=x&&x.removeBefore(s,o-w,i))===x&&_)return this}if(_&&!u)return this;var j=editableVNode(this,s);if(!_)for(var L=0;L>>o&C;if(u>=this.array.length)return this;if(o>0){var _=this.array[u];if((a=_&&_.removeAfter(s,o-w,i))===_&&u===this.array.length-1)return this}var x=editableVNode(this,s);return x.array.splice(u+1),a&&(x.array[u]=a),x};var Xe,Qe,et={};function iterateList(s,o){var i=s._origin,a=s._capacity,u=getTailOffset(a),_=s._tail;return iterateNodeOrLeaf(s._root,s._level,0);function iterateNodeOrLeaf(s,o,i){return 0===o?iterateLeaf(s,i):iterateNode(s,o,i)}function iterateLeaf(s,w){var C=w===u?_&&_.array:s&&s.array,j=w>i?0:i-w,L=a-w;return L>x&&(L=x),function(){if(j===L)return et;var s=o?--L:j++;return C&&C[s]}}function iterateNode(s,u,_){var C,j=s&&s.array,L=_>i?0:i-_>>u,B=1+(a-_>>u);return B>x&&(B=x),function(){for(;;){if(C){var s=C();if(s!==et)return s;C=null}if(L===B)return et;var i=o?--B:L++;C=iterateNodeOrLeaf(j&&j[i],u-w,_+(i<=s.size||o<0)return s.withMutations((function(s){o<0?setListBounds(s,o).set(0,i):setListBounds(s,0,o+1).set(o,i)}));o+=s._origin;var a=s._tail,u=s._root,_=MakeRef(B);return o>=getTailOffset(s._capacity)?a=updateVNode(a,s.__ownerID,0,o,i,_):u=updateVNode(u,s.__ownerID,s._level,o,i,_),_.value?s.__ownerID?(s._root=u,s._tail=a,s.__hash=void 0,s.__altered=!0,s):makeList(s._origin,s._capacity,s._level,u,a):s}function updateVNode(s,o,i,a,u,_){var x,j=a>>>i&C,L=s&&j0){var B=s&&s.array[j],$=updateVNode(B,o,i-w,a,u,_);return $===B?s:((x=editableVNode(s,o)).array[j]=$,x)}return L&&s.array[j]===u?s:(SetRef(_),x=editableVNode(s,o),void 0===u&&j===x.array.length-1?x.array.pop():x.array[j]=u,x)}function editableVNode(s,o){return o&&s&&o===s.ownerID?s:new VNode(s?s.array.slice():[],o)}function listNodeFor(s,o){if(o>=getTailOffset(s._capacity))return s._tail;if(o<1<0;)i=i.array[o>>>a&C],a-=w;return i}}function setListBounds(s,o,i){void 0!==o&&(o|=0),void 0!==i&&(i|=0);var a=s.__ownerID||new OwnerID,u=s._origin,_=s._capacity,x=u+o,j=void 0===i?_:i<0?_+i:u+i;if(x===u&&j===_)return s;if(x>=j)return s.clear();for(var L=s._level,B=s._root,$=0;x+$<0;)B=new VNode(B&&B.array.length?[void 0,B]:[],a),$+=1<<(L+=w);$&&(x+=$,u+=$,j+=$,_+=$);for(var U=getTailOffset(_),V=getTailOffset(j);V>=1<U?new VNode([],a):z;if(z&&V>U&&x<_&&z.array.length){for(var Z=B=editableVNode(B,a),ee=L;ee>w;ee-=w){var ie=U>>>ee&C;Z=Z.array[ie]=editableVNode(Z.array[ie],a)}Z.array[U>>>w&C]=z}if(j<_&&(Y=Y&&Y.removeAfter(a,0,j)),x>=V)x-=V,j-=V,L=w,B=null,Y=Y&&Y.removeBefore(a,0,x);else if(x>u||V>>L&C;if(ae!==V>>>L&C)break;ae&&($+=(1<u&&(B=B.removeBefore(a,L,x-$)),B&&Vu&&(u=x.size),isIterable(w)||(x=x.map((function(s){return fromJS(s)}))),a.push(x)}return u>s.size&&(s=s.setSize(u)),mergeIntoCollectionWith(s,o,a)}function getTailOffset(s){return s>>w<=x&&w.size>=2*_.size?(a=(u=w.filter((function(s,o){return void 0!==s&&C!==o}))).toKeyedSeq().map((function(s){return s[0]})).flip().toMap(),s.__ownerID&&(a.__ownerID=u.__ownerID=s.__ownerID)):(a=_.remove(o),u=C===w.size-1?w.pop():w.set(C,void 0))}else if(L){if(i===w.get(C)[1])return s;a=_,u=w.set(C,[o,i])}else a=_.set(o,w.size),u=w.set(w.size,[o,i]);return s.__ownerID?(s.size=a.size,s._map=a,s._list=u,s.__hash=void 0,s):makeOrderedMap(a,u)}function ToKeyedSequence(s,o){this._iter=s,this._useKeys=o,this.size=s.size}function ToIndexedSequence(s){this._iter=s,this.size=s.size}function ToSetSequence(s){this._iter=s,this.size=s.size}function FromEntriesSequence(s){this._iter=s,this.size=s.size}function flipFactory(s){var o=makeSequence(s);return o._iter=s,o.size=s.size,o.flip=function(){return s},o.reverse=function(){var o=s.reverse.apply(this);return o.flip=function(){return s.reverse()},o},o.has=function(o){return s.includes(o)},o.includes=function(o){return s.has(o)},o.cacheResult=cacheResultThrough,o.__iterateUncached=function(o,i){var a=this;return s.__iterate((function(s,i){return!1!==o(i,s,a)}),i)},o.__iteratorUncached=function(o,i){if(o===V){var a=s.__iterator(o,i);return new Iterator((function(){var s=a.next();if(!s.done){var o=s.value[0];s.value[0]=s.value[1],s.value[1]=o}return s}))}return s.__iterator(o===U?$:U,i)},o}function mapFactory(s,o,i){var a=makeSequence(s);return a.size=s.size,a.has=function(o){return s.has(o)},a.get=function(a,u){var _=s.get(a,j);return _===j?u:o.call(i,_,a,s)},a.__iterateUncached=function(a,u){var _=this;return s.__iterate((function(s,u,w){return!1!==a(o.call(i,s,u,w),u,_)}),u)},a.__iteratorUncached=function(a,u){var _=s.__iterator(V,u);return new Iterator((function(){var u=_.next();if(u.done)return u;var w=u.value,x=w[0];return iteratorValue(a,x,o.call(i,w[1],x,s),u)}))},a}function reverseFactory(s,o){var i=makeSequence(s);return i._iter=s,i.size=s.size,i.reverse=function(){return s},s.flip&&(i.flip=function(){var o=flipFactory(s);return o.reverse=function(){return s.flip()},o}),i.get=function(i,a){return s.get(o?i:-1-i,a)},i.has=function(i){return s.has(o?i:-1-i)},i.includes=function(o){return s.includes(o)},i.cacheResult=cacheResultThrough,i.__iterate=function(o,i){var a=this;return s.__iterate((function(s,i){return o(s,i,a)}),!i)},i.__iterator=function(o,i){return s.__iterator(o,!i)},i}function filterFactory(s,o,i,a){var u=makeSequence(s);return a&&(u.has=function(a){var u=s.get(a,j);return u!==j&&!!o.call(i,u,a,s)},u.get=function(a,u){var _=s.get(a,j);return _!==j&&o.call(i,_,a,s)?_:u}),u.__iterateUncached=function(u,_){var w=this,x=0;return s.__iterate((function(s,_,C){if(o.call(i,s,_,C))return x++,u(s,a?_:x-1,w)}),_),x},u.__iteratorUncached=function(u,_){var w=s.__iterator(V,_),x=0;return new Iterator((function(){for(;;){var _=w.next();if(_.done)return _;var C=_.value,j=C[0],L=C[1];if(o.call(i,L,j,s))return iteratorValue(u,a?j:x++,L,_)}}))},u}function countByFactory(s,o,i){var a=Map().asMutable();return s.__iterate((function(u,_){a.update(o.call(i,u,_,s),0,(function(s){return s+1}))})),a.asImmutable()}function groupByFactory(s,o,i){var a=isKeyed(s),u=(isOrdered(s)?OrderedMap():Map()).asMutable();s.__iterate((function(_,w){u.update(o.call(i,_,w,s),(function(s){return(s=s||[]).push(a?[w,_]:_),s}))}));var _=iterableClass(s);return u.map((function(o){return reify(s,_(o))}))}function sliceFactory(s,o,i,a){var u=s.size;if(void 0!==o&&(o|=0),void 0!==i&&(i===1/0?i=u:i|=0),wholeSlice(o,i,u))return s;var _=resolveBegin(o,u),w=resolveEnd(i,u);if(_!=_||w!=w)return sliceFactory(s.toSeq().cacheResult(),o,i,a);var x,C=w-_;C==C&&(x=C<0?0:C);var j=makeSequence(s);return j.size=0===x?x:s.size&&x||void 0,!a&&isSeq(s)&&x>=0&&(j.get=function(o,i){return(o=wrapIndex(this,o))>=0&&ox)return iteratorDone();var s=u.next();return a||o===U?s:iteratorValue(o,C-1,o===$?void 0:s.value[1],s)}))},j}function takeWhileFactory(s,o,i){var a=makeSequence(s);return a.__iterateUncached=function(a,u){var _=this;if(u)return this.cacheResult().__iterate(a,u);var w=0;return s.__iterate((function(s,u,x){return o.call(i,s,u,x)&&++w&&a(s,u,_)})),w},a.__iteratorUncached=function(a,u){var _=this;if(u)return this.cacheResult().__iterator(a,u);var w=s.__iterator(V,u),x=!0;return new Iterator((function(){if(!x)return iteratorDone();var s=w.next();if(s.done)return s;var u=s.value,C=u[0],j=u[1];return o.call(i,j,C,_)?a===V?s:iteratorValue(a,C,j,s):(x=!1,iteratorDone())}))},a}function skipWhileFactory(s,o,i,a){var u=makeSequence(s);return u.__iterateUncached=function(u,_){var w=this;if(_)return this.cacheResult().__iterate(u,_);var x=!0,C=0;return s.__iterate((function(s,_,j){if(!x||!(x=o.call(i,s,_,j)))return C++,u(s,a?_:C-1,w)})),C},u.__iteratorUncached=function(u,_){var w=this;if(_)return this.cacheResult().__iterator(u,_);var x=s.__iterator(V,_),C=!0,j=0;return new Iterator((function(){var s,_,L;do{if((s=x.next()).done)return a||u===U?s:iteratorValue(u,j++,u===$?void 0:s.value[1],s);var B=s.value;_=B[0],L=B[1],C&&(C=o.call(i,L,_,w))}while(C);return u===V?s:iteratorValue(u,_,L,s)}))},u}function concatFactory(s,o){var i=isKeyed(s),a=[s].concat(o).map((function(s){return isIterable(s)?i&&(s=KeyedIterable(s)):s=i?keyedSeqFromValue(s):indexedSeqFromValue(Array.isArray(s)?s:[s]),s})).filter((function(s){return 0!==s.size}));if(0===a.length)return s;if(1===a.length){var u=a[0];if(u===s||i&&isKeyed(u)||isIndexed(s)&&isIndexed(u))return u}var _=new ArraySeq(a);return i?_=_.toKeyedSeq():isIndexed(s)||(_=_.toSetSeq()),(_=_.flatten(!0)).size=a.reduce((function(s,o){if(void 0!==s){var i=o.size;if(void 0!==i)return s+i}}),0),_}function flattenFactory(s,o,i){var a=makeSequence(s);return a.__iterateUncached=function(a,u){var _=0,w=!1;function flatDeep(s,x){var C=this;s.__iterate((function(s,u){return(!o||x0}function zipWithFactory(s,o,i){var a=makeSequence(s);return a.size=new ArraySeq(i).map((function(s){return s.size})).min(),a.__iterate=function(s,o){for(var i,a=this.__iterator(U,o),u=0;!(i=a.next()).done&&!1!==s(i.value,u++,this););return u},a.__iteratorUncached=function(s,a){var u=i.map((function(s){return s=Iterable(s),getIterator(a?s.reverse():s)})),_=0,w=!1;return new Iterator((function(){var i;return w||(i=u.map((function(s){return s.next()})),w=i.some((function(s){return s.done}))),w?iteratorDone():iteratorValue(s,_++,o.apply(null,i.map((function(s){return s.value}))))}))},a}function reify(s,o){return isSeq(s)?o:s.constructor(o)}function validateEntry(s){if(s!==Object(s))throw new TypeError("Expected [K, V] tuple: "+s)}function resolveSize(s){return assertNotInfinite(s.size),ensureSize(s)}function iterableClass(s){return isKeyed(s)?KeyedIterable:isIndexed(s)?IndexedIterable:SetIterable}function makeSequence(s){return Object.create((isKeyed(s)?KeyedSeq:isIndexed(s)?IndexedSeq:SetSeq).prototype)}function cacheResultThrough(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):Seq.prototype.cacheResult.call(this)}function defaultComparator(s,o){return s>o?1:s=0;i--)o={value:arguments[i],next:o};return this.__ownerID?(this.size=s,this._head=o,this.__hash=void 0,this.__altered=!0,this):makeStack(s,o)},Stack.prototype.pushAll=function(s){if(0===(s=IndexedIterable(s)).size)return this;assertNotInfinite(s.size);var o=this.size,i=this._head;return s.reverse().forEach((function(s){o++,i={value:s,next:i}})),this.__ownerID?(this.size=o,this._head=i,this.__hash=void 0,this.__altered=!0,this):makeStack(o,i)},Stack.prototype.pop=function(){return this.slice(1)},Stack.prototype.unshift=function(){return this.push.apply(this,arguments)},Stack.prototype.unshiftAll=function(s){return this.pushAll(s)},Stack.prototype.shift=function(){return this.pop.apply(this,arguments)},Stack.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):emptyStack()},Stack.prototype.slice=function(s,o){if(wholeSlice(s,o,this.size))return this;var i=resolveBegin(s,this.size);if(resolveEnd(o,this.size)!==this.size)return IndexedCollection.prototype.slice.call(this,s,o);for(var a=this.size-i,u=this._head;i--;)u=u.next;return this.__ownerID?(this.size=a,this._head=u,this.__hash=void 0,this.__altered=!0,this):makeStack(a,u)},Stack.prototype.__ensureOwner=function(s){return s===this.__ownerID?this:s?makeStack(this.size,this._head,s,this.__hash):(this.__ownerID=s,this.__altered=!1,this)},Stack.prototype.__iterate=function(s,o){if(o)return this.reverse().__iterate(s);for(var i=0,a=this._head;a&&!1!==s(a.value,i++,this);)a=a.next;return i},Stack.prototype.__iterator=function(s,o){if(o)return this.reverse().__iterator(s);var i=0,a=this._head;return new Iterator((function(){if(a){var o=a.value;return a=a.next,iteratorValue(s,i++,o)}return iteratorDone()}))},Stack.isStack=isStack;var at,ct="@@__IMMUTABLE_STACK__@@",lt=Stack.prototype;function makeStack(s,o,i,a){var u=Object.create(lt);return u.size=s,u._head=o,u.__ownerID=i,u.__hash=a,u.__altered=!1,u}function emptyStack(){return at||(at=makeStack(0))}function mixin(s,o){var keyCopier=function(i){s.prototype[i]=o[i]};return Object.keys(o).forEach(keyCopier),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(o).forEach(keyCopier),s}lt[ct]=!0,lt.withMutations=$e.withMutations,lt.asMutable=$e.asMutable,lt.asImmutable=$e.asImmutable,lt.wasAltered=$e.wasAltered,Iterable.Iterator=Iterator,mixin(Iterable,{toArray:function(){assertNotInfinite(this.size);var s=new Array(this.size||0);return this.valueSeq().__iterate((function(o,i){s[i]=o})),s},toIndexedSeq:function(){return new ToIndexedSequence(this)},toJS:function(){return this.toSeq().map((function(s){return s&&"function"==typeof s.toJS?s.toJS():s})).__toJS()},toJSON:function(){return this.toSeq().map((function(s){return s&&"function"==typeof s.toJSON?s.toJSON():s})).__toJS()},toKeyedSeq:function(){return new ToKeyedSequence(this,!0)},toMap:function(){return Map(this.toKeyedSeq())},toObject:function(){assertNotInfinite(this.size);var s={};return this.__iterate((function(o,i){s[i]=o})),s},toOrderedMap:function(){return OrderedMap(this.toKeyedSeq())},toOrderedSet:function(){return OrderedSet(isKeyed(this)?this.valueSeq():this)},toSet:function(){return Set(isKeyed(this)?this.valueSeq():this)},toSetSeq:function(){return new ToSetSequence(this)},toSeq:function(){return isIndexed(this)?this.toIndexedSeq():isKeyed(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return Stack(isKeyed(this)?this.valueSeq():this)},toList:function(){return List(isKeyed(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(s,o){return 0===this.size?s+o:s+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+o},concat:function(){return reify(this,concatFactory(this,s.call(arguments,0)))},includes:function(s){return this.some((function(o){return is(o,s)}))},entries:function(){return this.__iterator(V)},every:function(s,o){assertNotInfinite(this.size);var i=!0;return this.__iterate((function(a,u,_){if(!s.call(o,a,u,_))return i=!1,!1})),i},filter:function(s,o){return reify(this,filterFactory(this,s,o,!0))},find:function(s,o,i){var a=this.findEntry(s,o);return a?a[1]:i},forEach:function(s,o){return assertNotInfinite(this.size),this.__iterate(o?s.bind(o):s)},join:function(s){assertNotInfinite(this.size),s=void 0!==s?""+s:",";var o="",i=!0;return this.__iterate((function(a){i?i=!1:o+=s,o+=null!=a?a.toString():""})),o},keys:function(){return this.__iterator($)},map:function(s,o){return reify(this,mapFactory(this,s,o))},reduce:function(s,o,i){var a,u;return assertNotInfinite(this.size),arguments.length<2?u=!0:a=o,this.__iterate((function(o,_,w){u?(u=!1,a=o):a=s.call(i,a,o,_,w)})),a},reduceRight:function(s,o,i){var a=this.toKeyedSeq().reverse();return a.reduce.apply(a,arguments)},reverse:function(){return reify(this,reverseFactory(this,!0))},slice:function(s,o){return reify(this,sliceFactory(this,s,o,!0))},some:function(s,o){return!this.every(not(s),o)},sort:function(s){return reify(this,sortFactory(this,s))},values:function(){return this.__iterator(U)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some((function(){return!0}))},count:function(s,o){return ensureSize(s?this.toSeq().filter(s,o):this)},countBy:function(s,o){return countByFactory(this,s,o)},equals:function(s){return deepEqual(this,s)},entrySeq:function(){var s=this;if(s._cache)return new ArraySeq(s._cache);var o=s.toSeq().map(entryMapper).toIndexedSeq();return o.fromEntrySeq=function(){return s.toSeq()},o},filterNot:function(s,o){return this.filter(not(s),o)},findEntry:function(s,o,i){var a=i;return this.__iterate((function(i,u,_){if(s.call(o,i,u,_))return a=[u,i],!1})),a},findKey:function(s,o){var i=this.findEntry(s,o);return i&&i[0]},findLast:function(s,o,i){return this.toKeyedSeq().reverse().find(s,o,i)},findLastEntry:function(s,o,i){return this.toKeyedSeq().reverse().findEntry(s,o,i)},findLastKey:function(s,o){return this.toKeyedSeq().reverse().findKey(s,o)},first:function(){return this.find(returnTrue)},flatMap:function(s,o){return reify(this,flatMapFactory(this,s,o))},flatten:function(s){return reify(this,flattenFactory(this,s,!0))},fromEntrySeq:function(){return new FromEntriesSequence(this)},get:function(s,o){return this.find((function(o,i){return is(i,s)}),void 0,o)},getIn:function(s,o){for(var i,a=this,u=forceIterator(s);!(i=u.next()).done;){var _=i.value;if((a=a&&a.get?a.get(_,j):j)===j)return o}return a},groupBy:function(s,o){return groupByFactory(this,s,o)},has:function(s){return this.get(s,j)!==j},hasIn:function(s){return this.getIn(s,j)!==j},isSubset:function(s){return s="function"==typeof s.includes?s:Iterable(s),this.every((function(o){return s.includes(o)}))},isSuperset:function(s){return(s="function"==typeof s.isSubset?s:Iterable(s)).isSubset(this)},keyOf:function(s){return this.findKey((function(o){return is(o,s)}))},keySeq:function(){return this.toSeq().map(keyMapper).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(s){return this.toKeyedSeq().reverse().keyOf(s)},max:function(s){return maxFactory(this,s)},maxBy:function(s,o){return maxFactory(this,o,s)},min:function(s){return maxFactory(this,s?neg(s):defaultNegComparator)},minBy:function(s,o){return maxFactory(this,o?neg(o):defaultNegComparator,s)},rest:function(){return this.slice(1)},skip:function(s){return this.slice(Math.max(0,s))},skipLast:function(s){return reify(this,this.toSeq().reverse().skip(s).reverse())},skipWhile:function(s,o){return reify(this,skipWhileFactory(this,s,o,!0))},skipUntil:function(s,o){return this.skipWhile(not(s),o)},sortBy:function(s,o){return reify(this,sortFactory(this,o,s))},take:function(s){return this.slice(0,Math.max(0,s))},takeLast:function(s){return reify(this,this.toSeq().reverse().take(s).reverse())},takeWhile:function(s,o){return reify(this,takeWhileFactory(this,s,o))},takeUntil:function(s,o){return this.takeWhile(not(s),o)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=hashIterable(this))}});var ut=Iterable.prototype;ut[o]=!0,ut[Z]=ut.values,ut.__toJS=ut.toArray,ut.__toStringMapper=quoteString,ut.inspect=ut.toSource=function(){return this.toString()},ut.chain=ut.flatMap,ut.contains=ut.includes,mixin(KeyedIterable,{flip:function(){return reify(this,flipFactory(this))},mapEntries:function(s,o){var i=this,a=0;return reify(this,this.toSeq().map((function(u,_){return s.call(o,[_,u],a++,i)})).fromEntrySeq())},mapKeys:function(s,o){var i=this;return reify(this,this.toSeq().flip().map((function(a,u){return s.call(o,a,u,i)})).flip())}});var pt=KeyedIterable.prototype;function keyMapper(s,o){return o}function entryMapper(s,o){return[o,s]}function not(s){return function(){return!s.apply(this,arguments)}}function neg(s){return function(){return-s.apply(this,arguments)}}function quoteString(s){return"string"==typeof s?JSON.stringify(s):String(s)}function defaultZipper(){return arrCopy(arguments)}function defaultNegComparator(s,o){return so?-1:0}function hashIterable(s){if(s.size===1/0)return 0;var o=isOrdered(s),i=isKeyed(s),a=o?1:0;return murmurHashOfSize(s.__iterate(i?o?function(s,o){a=31*a+hashMerge(hash(s),hash(o))|0}:function(s,o){a=a+hashMerge(hash(s),hash(o))|0}:o?function(s){a=31*a+hash(s)|0}:function(s){a=a+hash(s)|0}),a)}function murmurHashOfSize(s,o){return o=le(o,3432918353),o=le(o<<15|o>>>-15,461845907),o=le(o<<13|o>>>-13,5),o=le((o=o+3864292196^s)^o>>>16,2246822507),o=smi((o=le(o^o>>>13,3266489909))^o>>>16)}function hashMerge(s,o){return s^o+2654435769+(s<<6)+(s>>2)}return pt[i]=!0,pt[Z]=ut.entries,pt.__toJS=ut.toObject,pt.__toStringMapper=function(s,o){return JSON.stringify(o)+": "+quoteString(s)},mixin(IndexedIterable,{toKeyedSeq:function(){return new ToKeyedSequence(this,!1)},filter:function(s,o){return reify(this,filterFactory(this,s,o,!1))},findIndex:function(s,o){var i=this.findEntry(s,o);return i?i[0]:-1},indexOf:function(s){var o=this.keyOf(s);return void 0===o?-1:o},lastIndexOf:function(s){var o=this.lastKeyOf(s);return void 0===o?-1:o},reverse:function(){return reify(this,reverseFactory(this,!1))},slice:function(s,o){return reify(this,sliceFactory(this,s,o,!1))},splice:function(s,o){var i=arguments.length;if(o=Math.max(0|o,0),0===i||2===i&&!o)return this;s=resolveBegin(s,s<0?this.count():this.size);var a=this.slice(0,s);return reify(this,1===i?a:a.concat(arrCopy(arguments,2),this.slice(s+o)))},findLastIndex:function(s,o){var i=this.findLastEntry(s,o);return i?i[0]:-1},first:function(){return this.get(0)},flatten:function(s){return reify(this,flattenFactory(this,s,!1))},get:function(s,o){return(s=wrapIndex(this,s))<0||this.size===1/0||void 0!==this.size&&s>this.size?o:this.find((function(o,i){return i===s}),void 0,o)},has:function(s){return(s=wrapIndex(this,s))>=0&&(void 0!==this.size?this.size===1/0||s{"use strict";i(71340);var a=i(92046);s.exports=a.Object.assign},9957:(s,o,i)=>{"use strict";var a=Function.prototype.call,u=Object.prototype.hasOwnProperty,_=i(66743);s.exports=_.call(a,u)},9999:(s,o,i)=>{var a=i(37217),u=i(83729),_=i(16547),w=i(74733),x=i(43838),C=i(93290),j=i(23007),L=i(92271),B=i(48948),$=i(50002),U=i(83349),V=i(5861),z=i(76189),Y=i(77199),Z=i(35529),ee=i(56449),ie=i(3656),ae=i(87730),ce=i(23805),le=i(38440),pe=i(95950),de=i(37241),fe="[object Arguments]",ye="[object Function]",be="[object Object]",_e={};_e[fe]=_e["[object Array]"]=_e["[object ArrayBuffer]"]=_e["[object DataView]"]=_e["[object Boolean]"]=_e["[object Date]"]=_e["[object Float32Array]"]=_e["[object Float64Array]"]=_e["[object Int8Array]"]=_e["[object Int16Array]"]=_e["[object Int32Array]"]=_e["[object Map]"]=_e["[object Number]"]=_e[be]=_e["[object RegExp]"]=_e["[object Set]"]=_e["[object String]"]=_e["[object Symbol]"]=_e["[object Uint8Array]"]=_e["[object Uint8ClampedArray]"]=_e["[object Uint16Array]"]=_e["[object Uint32Array]"]=!0,_e["[object Error]"]=_e[ye]=_e["[object WeakMap]"]=!1,s.exports=function baseClone(s,o,i,Se,we,xe){var Pe,Te=1&o,Re=2&o,$e=4&o;if(i&&(Pe=we?i(s,Se,we,xe):i(s)),void 0!==Pe)return Pe;if(!ce(s))return s;var qe=ee(s);if(qe){if(Pe=z(s),!Te)return j(s,Pe)}else{var ze=V(s),We=ze==ye||"[object GeneratorFunction]"==ze;if(ie(s))return C(s,Te);if(ze==be||ze==fe||We&&!we){if(Pe=Re||We?{}:Z(s),!Te)return Re?B(s,x(Pe,s)):L(s,w(Pe,s))}else{if(!_e[ze])return we?s:{};Pe=Y(s,ze,Te)}}xe||(xe=new a);var He=xe.get(s);if(He)return He;xe.set(s,Pe),le(s)?s.forEach((function(a){Pe.add(baseClone(a,o,i,a,s,xe))})):ae(s)&&s.forEach((function(a,u){Pe.set(u,baseClone(a,o,i,u,s,xe))}));var Ye=qe?void 0:($e?Re?U:$:Re?de:pe)(s);return u(Ye||s,(function(a,u){Ye&&(a=s[u=a]),_(Pe,u,baseClone(a,o,i,u,s,xe))})),Pe}},10023:(s,o,i)=>{const a=i(6205),INTS=()=>[{type:a.RANGE,from:48,to:57}],WORDS=()=>[{type:a.CHAR,value:95},{type:a.RANGE,from:97,to:122},{type:a.RANGE,from:65,to:90}].concat(INTS()),WHITESPACE=()=>[{type:a.CHAR,value:9},{type:a.CHAR,value:10},{type:a.CHAR,value:11},{type:a.CHAR,value:12},{type:a.CHAR,value:13},{type:a.CHAR,value:32},{type:a.CHAR,value:160},{type:a.CHAR,value:5760},{type:a.RANGE,from:8192,to:8202},{type:a.CHAR,value:8232},{type:a.CHAR,value:8233},{type:a.CHAR,value:8239},{type:a.CHAR,value:8287},{type:a.CHAR,value:12288},{type:a.CHAR,value:65279}];o.words=()=>({type:a.SET,set:WORDS(),not:!1}),o.notWords=()=>({type:a.SET,set:WORDS(),not:!0}),o.ints=()=>({type:a.SET,set:INTS(),not:!1}),o.notInts=()=>({type:a.SET,set:INTS(),not:!0}),o.whitespace=()=>({type:a.SET,set:WHITESPACE(),not:!1}),o.notWhitespace=()=>({type:a.SET,set:WHITESPACE(),not:!0}),o.anyChar=()=>({type:a.SET,set:[{type:a.CHAR,value:10},{type:a.CHAR,value:13},{type:a.CHAR,value:8232},{type:a.CHAR,value:8233}],not:!0})},10043:(s,o,i)=>{"use strict";var a=i(54018),u=String,_=TypeError;s.exports=function(s){if(a(s))return s;throw new _("Can't set "+u(s)+" as a prototype")}},10076:s=>{"use strict";s.exports=Function.prototype.call},10124:(s,o,i)=>{var a=i(9325);s.exports=function(){return a.Date.now()}},10300:(s,o,i)=>{"use strict";var a=i(13930),u=i(82159),_=i(36624),w=i(4640),x=i(73448),C=TypeError;s.exports=function(s,o){var i=arguments.length<2?x(s):o;if(u(i))return _(a(i,s));throw new C(w(s)+" is not iterable")}},10316:(s,o,i)=>{const a=i(2404),u=i(55973),_=i(92340);class Element{constructor(s,o,i){o&&(this.meta=o),i&&(this.attributes=i),this.content=s}freeze(){Object.isFrozen(this)||(this._meta&&(this.meta.parent=this,this.meta.freeze()),this._attributes&&(this.attributes.parent=this,this.attributes.freeze()),this.children.forEach((s=>{s.parent=this,s.freeze()}),this),this.content&&Array.isArray(this.content)&&Object.freeze(this.content),Object.freeze(this))}primitive(){}clone(){const s=new this.constructor;return s.element=this.element,this.meta.length&&(s._meta=this.meta.clone()),this.attributes.length&&(s._attributes=this.attributes.clone()),this.content?this.content.clone?s.content=this.content.clone():Array.isArray(this.content)?s.content=this.content.map((s=>s.clone())):s.content=this.content:s.content=this.content,s}toValue(){return this.content instanceof Element?this.content.toValue():this.content instanceof u?{key:this.content.key.toValue(),value:this.content.value?this.content.value.toValue():void 0}:this.content&&this.content.map?this.content.map((s=>s.toValue()),this):this.content}toRef(s){if(""===this.id.toValue())throw Error("Cannot create reference to an element that does not contain an ID");const o=new this.RefElement(this.id.toValue());return s&&(o.path=s),o}findRecursive(...s){if(arguments.length>1&&!this.isFrozen)throw new Error("Cannot find recursive with multiple element names without first freezing the element. Call `element.freeze()`");const o=s.pop();let i=new _;const append=(s,o)=>(s.push(o),s),checkElement=(s,i)=>{i.element===o&&s.push(i);const a=i.findRecursive(o);return a&&a.reduce(append,s),i.content instanceof u&&(i.content.key&&checkElement(s,i.content.key),i.content.value&&checkElement(s,i.content.value)),s};return this.content&&(this.content.element&&checkElement(i,this.content),Array.isArray(this.content)&&this.content.reduce(checkElement,i)),s.isEmpty||(i=i.filter((o=>{let i=o.parents.map((s=>s.element));for(const o in s){const a=s[o],u=i.indexOf(a);if(-1===u)return!1;i=i.splice(0,u)}return!0}))),i}set(s){return this.content=s,this}equals(s){return a(this.toValue(),s)}getMetaProperty(s,o){if(!this.meta.hasKey(s)){if(this.isFrozen){const s=this.refract(o);return s.freeze(),s}this.meta.set(s,o)}return this.meta.get(s)}setMetaProperty(s,o){this.meta.set(s,o)}get element(){return this._storedElement||"element"}set element(s){this._storedElement=s}get content(){return this._content}set content(s){if(s instanceof Element)this._content=s;else if(s instanceof _)this.content=s.elements;else if("string"==typeof s||"number"==typeof s||"boolean"==typeof s||"null"===s||null==s)this._content=s;else if(s instanceof u)this._content=s;else if(Array.isArray(s))this._content=s.map(this.refract);else{if("object"!=typeof s)throw new Error("Cannot set content to given value");this._content=Object.keys(s).map((o=>new this.MemberElement(o,s[o])))}}get meta(){if(!this._meta){if(this.isFrozen){const s=new this.ObjectElement;return s.freeze(),s}this._meta=new this.ObjectElement}return this._meta}set meta(s){s instanceof this.ObjectElement?this._meta=s:this.meta.set(s||{})}get attributes(){if(!this._attributes){if(this.isFrozen){const s=new this.ObjectElement;return s.freeze(),s}this._attributes=new this.ObjectElement}return this._attributes}set attributes(s){s instanceof this.ObjectElement?this._attributes=s:this.attributes.set(s||{})}get id(){return this.getMetaProperty("id","")}set id(s){this.setMetaProperty("id",s)}get classes(){return this.getMetaProperty("classes",[])}set classes(s){this.setMetaProperty("classes",s)}get title(){return this.getMetaProperty("title","")}set title(s){this.setMetaProperty("title",s)}get description(){return this.getMetaProperty("description","")}set description(s){this.setMetaProperty("description",s)}get links(){return this.getMetaProperty("links",[])}set links(s){this.setMetaProperty("links",s)}get isFrozen(){return Object.isFrozen(this)}get parents(){let{parent:s}=this;const o=new _;for(;s;)o.push(s),s=s.parent;return o}get children(){if(Array.isArray(this.content))return new _(this.content);if(this.content instanceof u){const s=new _([this.content.key]);return this.content.value&&s.push(this.content.value),s}return this.content instanceof Element?new _([this.content]):new _}get recursiveChildren(){const s=new _;return this.children.forEach((o=>{s.push(o),o.recursiveChildren.forEach((o=>{s.push(o)}))})),s}}s.exports=Element},10392:s=>{s.exports=function getValue(s,o){return null==s?void 0:s[o]}},10487:(s,o,i)=>{"use strict";var a=i(96897),u=i(30655),_=i(73126),w=i(12205);s.exports=function callBind(s){var o=_(arguments),i=s.length-(arguments.length-1);return a(o,1+(i>0?i:0),!0)},u?u(s.exports,"apply",{value:w}):s.exports.apply=w},10776:(s,o,i)=>{var a=i(30756),u=i(95950);s.exports=function getMatchData(s){for(var o=u(s),i=o.length;i--;){var _=o[i],w=s[_];o[i]=[_,w,a(w)]}return o}},10866:(s,o,i)=>{const a=i(6048),u=i(92340);class ObjectSlice extends u{map(s,o){return this.elements.map((i=>s.bind(o)(i.value,i.key,i)))}filter(s,o){return new ObjectSlice(this.elements.filter((i=>s.bind(o)(i.value,i.key,i))))}reject(s,o){return this.filter(a(s.bind(o)))}forEach(s,o){return this.elements.forEach(((i,a)=>{s.bind(o)(i.value,i.key,i,a)}))}keys(){return this.map(((s,o)=>o.toValue()))}values(){return this.map((s=>s.toValue()))}}s.exports=ObjectSlice},11002:s=>{"use strict";s.exports=Function.prototype.apply},11042:(s,o,i)=>{"use strict";var a=i(85582),u=i(1907),_=i(24443),w=i(87170),x=i(36624),C=u([].concat);s.exports=a("Reflect","ownKeys")||function ownKeys(s){var o=_.f(x(s)),i=w.f;return i?C(o,i(s)):o}},11091:(s,o,i)=>{"use strict";var a=i(45951),u=i(76024),_=i(92361),w=i(62250),x=i(13846).f,C=i(7463),j=i(92046),L=i(28311),B=i(61626),$=i(49724);i(36128);var wrapConstructor=function(s){var Wrapper=function(o,i,a){if(this instanceof Wrapper){switch(arguments.length){case 0:return new s;case 1:return new s(o);case 2:return new s(o,i)}return new s(o,i,a)}return u(s,this,arguments)};return Wrapper.prototype=s.prototype,Wrapper};s.exports=function(s,o){var i,u,U,V,z,Y,Z,ee,ie,ae=s.target,ce=s.global,le=s.stat,pe=s.proto,de=ce?a:le?a[ae]:a[ae]&&a[ae].prototype,fe=ce?j:j[ae]||B(j,ae,{})[ae],ye=fe.prototype;for(V in o)u=!(i=C(ce?V:ae+(le?".":"#")+V,s.forced))&&de&&$(de,V),Y=fe[V],u&&(Z=s.dontCallGetSet?(ie=x(de,V))&&ie.value:de[V]),z=u&&Z?Z:o[V],(i||pe||typeof Y!=typeof z)&&(ee=s.bind&&u?L(z,a):s.wrap&&u?wrapConstructor(z):pe&&w(z)?_(z):z,(s.sham||z&&z.sham||Y&&Y.sham)&&B(ee,"sham",!0),B(fe,V,ee),pe&&($(j,U=ae+"Prototype")||B(j,U,{}),B(j[U],V,z),s.real&&ye&&(i||!ye[V])&&B(ye,V,z)))}},11287:s=>{s.exports=function getHolder(s){return s.placeholder}},11331:(s,o,i)=>{var a=i(72552),u=i(28879),_=i(40346),w=Function.prototype,x=Object.prototype,C=w.toString,j=x.hasOwnProperty,L=C.call(Object);s.exports=function isPlainObject(s){if(!_(s)||"[object Object]"!=a(s))return!1;var o=u(s);if(null===o)return!0;var i=j.call(o,"constructor")&&o.constructor;return"function"==typeof i&&i instanceof i&&C.call(i)==L}},11470:(s,o,i)=>{"use strict";var a=i(1907),u=i(65482),_=i(90160),w=i(74239),x=a("".charAt),C=a("".charCodeAt),j=a("".slice),createMethod=function(s){return function(o,i){var a,L,B=_(w(o)),$=u(i),U=B.length;return $<0||$>=U?s?"":void 0:(a=C(B,$))<55296||a>56319||$+1===U||(L=C(B,$+1))<56320||L>57343?s?x(B,$):a:s?j(B,$,$+2):L-56320+(a-55296<<10)+65536}};s.exports={codeAt:createMethod(!1),charAt:createMethod(!0)}},11842:(s,o,i)=>{var a=i(82819),u=i(9325);s.exports=function createBind(s,o,i){var _=1&o,w=a(s);return function wrapper(){return(this&&this!==u&&this instanceof wrapper?w:s).apply(_?i:this,arguments)}}},12205:(s,o,i)=>{"use strict";var a=i(66743),u=i(11002),_=i(13144);s.exports=function applyBind(){return _(a,u,arguments)}},12242:(s,o,i)=>{const a=i(10316);s.exports=class BooleanElement extends a{constructor(s,o,i){super(s,o,i),this.element="boolean"}primitive(){return"boolean"}}},12507:(s,o,i)=>{var a=i(28754),u=i(49698),_=i(63912),w=i(13222);s.exports=function createCaseFirst(s){return function(o){o=w(o);var i=u(o)?_(o):void 0,x=i?i[0]:o.charAt(0),C=i?a(i,1).join(""):o.slice(1);return x[s]()+C}}},12560:(s,o,i)=>{"use strict";i(99363);var a=i(19287),u=i(45951),_=i(14840),w=i(93742);for(var x in a)_(u[x],x),w[x]=w.Array},12651:(s,o,i)=>{var a=i(74218);s.exports=function getMapData(s,o){var i=s.__data__;return a(o)?i["string"==typeof o?"string":"hash"]:i.map}},12749:(s,o,i)=>{var a=i(81042),u=Object.prototype.hasOwnProperty;s.exports=function hashHas(s){var o=this.__data__;return a?void 0!==o[s]:u.call(o,s)}},13144:(s,o,i)=>{"use strict";var a=i(66743),u=i(11002),_=i(10076),w=i(47119);s.exports=w||a.call(_,u)},13222:(s,o,i)=>{var a=i(77556);s.exports=function toString(s){return null==s?"":a(s)}},13846:(s,o,i)=>{"use strict";var a=i(39447),u=i(13930),_=i(22574),w=i(75817),x=i(4993),C=i(70470),j=i(49724),L=i(73648),B=Object.getOwnPropertyDescriptor;o.f=a?B:function getOwnPropertyDescriptor(s,o){if(s=x(s),o=C(o),L)try{return B(s,o)}catch(s){}if(j(s,o))return w(!u(_.f,s,o),s[o])}},13930:(s,o,i)=>{"use strict";var a=i(41505),u=Function.prototype.call;s.exports=a?u.bind(u):function(){return u.apply(u,arguments)}},14248:s=>{s.exports=function arraySome(s,o){for(var i=-1,a=null==s?0:s.length;++i{s.exports=function arrayPush(s,o){for(var i=-1,a=o.length,u=s.length;++i{const a=i(10316);s.exports=class RefElement extends a{constructor(s,o,i){super(s||[],o,i),this.element="ref",this.path||(this.path="element")}get path(){return this.attributes.get("path")}set path(s){this.attributes.set("path",s)}}},14744:s=>{"use strict";var o=function isMergeableObject(s){return function isNonNullObject(s){return!!s&&"object"==typeof s}(s)&&!function isSpecial(s){var o=Object.prototype.toString.call(s);return"[object RegExp]"===o||"[object Date]"===o||function isReactElement(s){return s.$$typeof===i}(s)}(s)};var i="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function cloneUnlessOtherwiseSpecified(s,o){return!1!==o.clone&&o.isMergeableObject(s)?deepmerge(function emptyTarget(s){return Array.isArray(s)?[]:{}}(s),s,o):s}function defaultArrayMerge(s,o,i){return s.concat(o).map((function(s){return cloneUnlessOtherwiseSpecified(s,i)}))}function getKeys(s){return Object.keys(s).concat(function getEnumerableOwnPropertySymbols(s){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(s).filter((function(o){return Object.propertyIsEnumerable.call(s,o)})):[]}(s))}function propertyIsOnObject(s,o){try{return o in s}catch(s){return!1}}function mergeObject(s,o,i){var a={};return i.isMergeableObject(s)&&getKeys(s).forEach((function(o){a[o]=cloneUnlessOtherwiseSpecified(s[o],i)})),getKeys(o).forEach((function(u){(function propertyIsUnsafe(s,o){return propertyIsOnObject(s,o)&&!(Object.hasOwnProperty.call(s,o)&&Object.propertyIsEnumerable.call(s,o))})(s,u)||(propertyIsOnObject(s,u)&&i.isMergeableObject(o[u])?a[u]=function getMergeFunction(s,o){if(!o.customMerge)return deepmerge;var i=o.customMerge(s);return"function"==typeof i?i:deepmerge}(u,i)(s[u],o[u],i):a[u]=cloneUnlessOtherwiseSpecified(o[u],i))})),a}function deepmerge(s,i,a){(a=a||{}).arrayMerge=a.arrayMerge||defaultArrayMerge,a.isMergeableObject=a.isMergeableObject||o,a.cloneUnlessOtherwiseSpecified=cloneUnlessOtherwiseSpecified;var u=Array.isArray(i);return u===Array.isArray(s)?u?a.arrayMerge(s,i,a):mergeObject(s,i,a):cloneUnlessOtherwiseSpecified(i,a)}deepmerge.all=function deepmergeAll(s,o){if(!Array.isArray(s))throw new Error("first argument should be an array");return s.reduce((function(s,i){return deepmerge(s,i,o)}),{})};var a=deepmerge;s.exports=a},14792:(s,o,i)=>{var a=i(13222),u=i(55808);s.exports=function capitalize(s){return u(a(s).toLowerCase())}},14840:(s,o,i)=>{"use strict";var a=i(52623),u=i(74284).f,_=i(61626),w=i(49724),x=i(54878),C=i(76264)("toStringTag");s.exports=function(s,o,i,j){var L=i?s:s&&s.prototype;L&&(w(L,C)||u(L,C,{configurable:!0,value:o}),j&&!a&&_(L,"toString",x))}},14974:s=>{s.exports=function safeGet(s,o){if(("constructor"!==o||"function"!=typeof s[o])&&"__proto__"!=o)return s[o]}},15287:(s,o)=>{"use strict";var i=Symbol.for("react.element"),a=Symbol.for("react.portal"),u=Symbol.for("react.fragment"),_=Symbol.for("react.strict_mode"),w=Symbol.for("react.profiler"),x=Symbol.for("react.provider"),C=Symbol.for("react.context"),j=Symbol.for("react.forward_ref"),L=Symbol.for("react.suspense"),B=Symbol.for("react.memo"),$=Symbol.for("react.lazy"),U=Symbol.iterator;var V={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},z=Object.assign,Y={};function E(s,o,i){this.props=s,this.context=o,this.refs=Y,this.updater=i||V}function F(){}function G(s,o,i){this.props=s,this.context=o,this.refs=Y,this.updater=i||V}E.prototype.isReactComponent={},E.prototype.setState=function(s,o){if("object"!=typeof s&&"function"!=typeof s&&null!=s)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,s,o,"setState")},E.prototype.forceUpdate=function(s){this.updater.enqueueForceUpdate(this,s,"forceUpdate")},F.prototype=E.prototype;var Z=G.prototype=new F;Z.constructor=G,z(Z,E.prototype),Z.isPureReactComponent=!0;var ee=Array.isArray,ie=Object.prototype.hasOwnProperty,ae={current:null},ce={key:!0,ref:!0,__self:!0,__source:!0};function M(s,o,a){var u,_={},w=null,x=null;if(null!=o)for(u in void 0!==o.ref&&(x=o.ref),void 0!==o.key&&(w=""+o.key),o)ie.call(o,u)&&!ce.hasOwnProperty(u)&&(_[u]=o[u]);var C=arguments.length-2;if(1===C)_.children=a;else if(1{var a=i(96131);s.exports=function arrayIncludes(s,o){return!!(null==s?0:s.length)&&a(s,o,0)>-1}},15340:()=>{},15377:(s,o,i)=>{"use strict";var a=i(92861).Buffer,u=i(64634),_=i(74372),w=ArrayBuffer.isView||function isView(s){try{return _(s),!0}catch(s){return!1}},x="undefined"!=typeof Uint8Array,C="undefined"!=typeof ArrayBuffer&&"undefined"!=typeof Uint8Array,j=C&&(a.prototype instanceof Uint8Array||a.TYPED_ARRAY_SUPPORT);s.exports=function toBuffer(s,o){if(s instanceof a)return s;if("string"==typeof s)return a.from(s,o);if(C&&w(s)){if(0===s.byteLength)return a.alloc(0);if(j){var i=a.from(s.buffer,s.byteOffset,s.byteLength);if(i.byteLength===s.byteLength)return i}var _=s instanceof Uint8Array?s:new Uint8Array(s.buffer,s.byteOffset,s.byteLength),L=a.from(_);if(L.length===s.byteLength)return L}if(x&&s instanceof Uint8Array)return a.from(s);var B=u(s);if(B)for(var $=0;$255||~~U!==U)throw new RangeError("Array items must be numbers in the range 0-255.")}if(B||a.isBuffer(s)&&s.constructor&&"function"==typeof s.constructor.isBuffer&&s.constructor.isBuffer(s))return a.from(s);throw new TypeError('The "data" argument must be a string, an Array, a Buffer, a Uint8Array, or a DataView.')}},15389:(s,o,i)=>{var a=i(93663),u=i(87978),_=i(83488),w=i(56449),x=i(50583);s.exports=function baseIteratee(s){return"function"==typeof s?s:null==s?_:"object"==typeof s?w(s)?u(s[0],s[1]):a(s):x(s)}},15972:(s,o,i)=>{"use strict";var a=i(49724),u=i(62250),_=i(39298),w=i(92522),x=i(57382),C=w("IE_PROTO"),j=Object,L=j.prototype;s.exports=x?j.getPrototypeOf:function(s){var o=_(s);if(a(o,C))return o[C];var i=o.constructor;return u(i)&&o instanceof i?i.prototype:o instanceof j?L:null}},16038:(s,o,i)=>{var a=i(5861),u=i(40346);s.exports=function baseIsSet(s){return u(s)&&"[object Set]"==a(s)}},16426:s=>{s.exports=function(){var s=document.getSelection();if(!s.rangeCount)return function(){};for(var o=document.activeElement,i=[],a=0;a{var a=i(43360),u=i(75288),_=Object.prototype.hasOwnProperty;s.exports=function assignValue(s,o,i){var w=s[o];_.call(s,o)&&u(w,i)&&(void 0!==i||o in s)||a(s,o,i)}},16708:(s,o,i)=>{"use strict";var a,u=i(65606);function CorkedRequest(s){var o=this;this.next=null,this.entry=null,this.finish=function(){!function onCorkedFinish(s,o,i){var a=s.entry;s.entry=null;for(;a;){var u=a.callback;o.pendingcb--,u(i),a=a.next}o.corkedRequestsFree.next=s}(o,s)}}s.exports=Writable,Writable.WritableState=WritableState;var _={deprecate:i(94643)},w=i(40345),x=i(48287).Buffer,C=(void 0!==i.g?i.g:"undefined"!=typeof window?window:"undefined"!=typeof self?self:{}).Uint8Array||function(){};var j,L=i(75896),B=i(65291).getHighWaterMark,$=i(86048).F,U=$.ERR_INVALID_ARG_TYPE,V=$.ERR_METHOD_NOT_IMPLEMENTED,z=$.ERR_MULTIPLE_CALLBACK,Y=$.ERR_STREAM_CANNOT_PIPE,Z=$.ERR_STREAM_DESTROYED,ee=$.ERR_STREAM_NULL_VALUES,ie=$.ERR_STREAM_WRITE_AFTER_END,ae=$.ERR_UNKNOWN_ENCODING,ce=L.errorOrDestroy;function nop(){}function WritableState(s,o,_){a=a||i(25382),s=s||{},"boolean"!=typeof _&&(_=o instanceof a),this.objectMode=!!s.objectMode,_&&(this.objectMode=this.objectMode||!!s.writableObjectMode),this.highWaterMark=B(this,s,"writableHighWaterMark",_),this.finalCalled=!1,this.needDrain=!1,this.ending=!1,this.ended=!1,this.finished=!1,this.destroyed=!1;var w=!1===s.decodeStrings;this.decodeStrings=!w,this.defaultEncoding=s.defaultEncoding||"utf8",this.length=0,this.writing=!1,this.corked=0,this.sync=!0,this.bufferProcessing=!1,this.onwrite=function(s){!function onwrite(s,o){var i=s._writableState,a=i.sync,_=i.writecb;if("function"!=typeof _)throw new z;if(function onwriteStateUpdate(s){s.writing=!1,s.writecb=null,s.length-=s.writelen,s.writelen=0}(i),o)!function onwriteError(s,o,i,a,_){--o.pendingcb,i?(u.nextTick(_,a),u.nextTick(finishMaybe,s,o),s._writableState.errorEmitted=!0,ce(s,a)):(_(a),s._writableState.errorEmitted=!0,ce(s,a),finishMaybe(s,o))}(s,i,a,o,_);else{var w=needFinish(i)||s.destroyed;w||i.corked||i.bufferProcessing||!i.bufferedRequest||clearBuffer(s,i),a?u.nextTick(afterWrite,s,i,w,_):afterWrite(s,i,w,_)}}(o,s)},this.writecb=null,this.writelen=0,this.bufferedRequest=null,this.lastBufferedRequest=null,this.pendingcb=0,this.prefinished=!1,this.errorEmitted=!1,this.emitClose=!1!==s.emitClose,this.autoDestroy=!!s.autoDestroy,this.bufferedRequestCount=0,this.corkedRequestsFree=new CorkedRequest(this)}function Writable(s){var o=this instanceof(a=a||i(25382));if(!o&&!j.call(Writable,this))return new Writable(s);this._writableState=new WritableState(s,this,o),this.writable=!0,s&&("function"==typeof s.write&&(this._write=s.write),"function"==typeof s.writev&&(this._writev=s.writev),"function"==typeof s.destroy&&(this._destroy=s.destroy),"function"==typeof s.final&&(this._final=s.final)),w.call(this)}function doWrite(s,o,i,a,u,_,w){o.writelen=a,o.writecb=w,o.writing=!0,o.sync=!0,o.destroyed?o.onwrite(new Z("write")):i?s._writev(u,o.onwrite):s._write(u,_,o.onwrite),o.sync=!1}function afterWrite(s,o,i,a){i||function onwriteDrain(s,o){0===o.length&&o.needDrain&&(o.needDrain=!1,s.emit("drain"))}(s,o),o.pendingcb--,a(),finishMaybe(s,o)}function clearBuffer(s,o){o.bufferProcessing=!0;var i=o.bufferedRequest;if(s._writev&&i&&i.next){var a=o.bufferedRequestCount,u=new Array(a),_=o.corkedRequestsFree;_.entry=i;for(var w=0,x=!0;i;)u[w]=i,i.isBuf||(x=!1),i=i.next,w+=1;u.allBuffers=x,doWrite(s,o,!0,o.length,u,"",_.finish),o.pendingcb++,o.lastBufferedRequest=null,_.next?(o.corkedRequestsFree=_.next,_.next=null):o.corkedRequestsFree=new CorkedRequest(o),o.bufferedRequestCount=0}else{for(;i;){var C=i.chunk,j=i.encoding,L=i.callback;if(doWrite(s,o,!1,o.objectMode?1:C.length,C,j,L),i=i.next,o.bufferedRequestCount--,o.writing)break}null===i&&(o.lastBufferedRequest=null)}o.bufferedRequest=i,o.bufferProcessing=!1}function needFinish(s){return s.ending&&0===s.length&&null===s.bufferedRequest&&!s.finished&&!s.writing}function callFinal(s,o){s._final((function(i){o.pendingcb--,i&&ce(s,i),o.prefinished=!0,s.emit("prefinish"),finishMaybe(s,o)}))}function finishMaybe(s,o){var i=needFinish(o);if(i&&(function prefinish(s,o){o.prefinished||o.finalCalled||("function"!=typeof s._final||o.destroyed?(o.prefinished=!0,s.emit("prefinish")):(o.pendingcb++,o.finalCalled=!0,u.nextTick(callFinal,s,o)))}(s,o),0===o.pendingcb&&(o.finished=!0,s.emit("finish"),o.autoDestroy))){var a=s._readableState;(!a||a.autoDestroy&&a.endEmitted)&&s.destroy()}return i}i(56698)(Writable,w),WritableState.prototype.getBuffer=function getBuffer(){for(var s=this.bufferedRequest,o=[];s;)o.push(s),s=s.next;return o},function(){try{Object.defineProperty(WritableState.prototype,"buffer",{get:_.deprecate((function writableStateBufferGetter(){return this.getBuffer()}),"_writableState.buffer is deprecated. Use _writableState.getBuffer instead.","DEP0003")})}catch(s){}}(),"function"==typeof Symbol&&Symbol.hasInstance&&"function"==typeof Function.prototype[Symbol.hasInstance]?(j=Function.prototype[Symbol.hasInstance],Object.defineProperty(Writable,Symbol.hasInstance,{value:function value(s){return!!j.call(this,s)||this===Writable&&(s&&s._writableState instanceof WritableState)}})):j=function realHasInstance(s){return s instanceof this},Writable.prototype.pipe=function(){ce(this,new Y)},Writable.prototype.write=function(s,o,i){var a=this._writableState,_=!1,w=!a.objectMode&&function _isUint8Array(s){return x.isBuffer(s)||s instanceof C}(s);return w&&!x.isBuffer(s)&&(s=function _uint8ArrayToBuffer(s){return x.from(s)}(s)),"function"==typeof o&&(i=o,o=null),w?o="buffer":o||(o=a.defaultEncoding),"function"!=typeof i&&(i=nop),a.ending?function writeAfterEnd(s,o){var i=new ie;ce(s,i),u.nextTick(o,i)}(this,i):(w||function validChunk(s,o,i,a){var _;return null===i?_=new ee:"string"==typeof i||o.objectMode||(_=new U("chunk",["string","Buffer"],i)),!_||(ce(s,_),u.nextTick(a,_),!1)}(this,a,s,i))&&(a.pendingcb++,_=function writeOrBuffer(s,o,i,a,u,_){if(!i){var w=function decodeChunk(s,o,i){s.objectMode||!1===s.decodeStrings||"string"!=typeof o||(o=x.from(o,i));return o}(o,a,u);a!==w&&(i=!0,u="buffer",a=w)}var C=o.objectMode?1:a.length;o.length+=C;var j=o.length-1))throw new ae(s);return this._writableState.defaultEncoding=s,this},Object.defineProperty(Writable.prototype,"writableBuffer",{enumerable:!1,get:function get(){return this._writableState&&this._writableState.getBuffer()}}),Object.defineProperty(Writable.prototype,"writableHighWaterMark",{enumerable:!1,get:function get(){return this._writableState.highWaterMark}}),Writable.prototype._write=function(s,o,i){i(new V("_write()"))},Writable.prototype._writev=null,Writable.prototype.end=function(s,o,i){var a=this._writableState;return"function"==typeof s?(i=s,s=null,o=null):"function"==typeof o&&(i=o,o=null),null!=s&&this.write(s,o),a.corked&&(a.corked=1,this.uncork()),a.ending||function endWritable(s,o,i){o.ending=!0,finishMaybe(s,o),i&&(o.finished?u.nextTick(i):s.once("finish",i));o.ended=!0,s.writable=!1}(this,a,i),this},Object.defineProperty(Writable.prototype,"writableLength",{enumerable:!1,get:function get(){return this._writableState.length}}),Object.defineProperty(Writable.prototype,"destroyed",{enumerable:!1,get:function get(){return void 0!==this._writableState&&this._writableState.destroyed},set:function set(s){this._writableState&&(this._writableState.destroyed=s)}}),Writable.prototype.destroy=L.destroy,Writable.prototype._undestroy=L.undestroy,Writable.prototype._destroy=function(s,o){o(s)}},16946:(s,o,i)=>{"use strict";var a=i(1907),u=i(98828),_=i(45807),w=Object,x=a("".split);s.exports=u((function(){return!w("z").propertyIsEnumerable(0)}))?function(s){return"String"===_(s)?x(s,""):w(s)}:w},16962:(s,o)=>{o.aliasToReal={each:"forEach",eachRight:"forEachRight",entries:"toPairs",entriesIn:"toPairsIn",extend:"assignIn",extendAll:"assignInAll",extendAllWith:"assignInAllWith",extendWith:"assignInWith",first:"head",conforms:"conformsTo",matches:"isMatch",property:"get",__:"placeholder",F:"stubFalse",T:"stubTrue",all:"every",allPass:"overEvery",always:"constant",any:"some",anyPass:"overSome",apply:"spread",assoc:"set",assocPath:"set",complement:"negate",compose:"flowRight",contains:"includes",dissoc:"unset",dissocPath:"unset",dropLast:"dropRight",dropLastWhile:"dropRightWhile",equals:"isEqual",identical:"eq",indexBy:"keyBy",init:"initial",invertObj:"invert",juxt:"over",omitAll:"omit",nAry:"ary",path:"get",pathEq:"matchesProperty",pathOr:"getOr",paths:"at",pickAll:"pick",pipe:"flow",pluck:"map",prop:"get",propEq:"matchesProperty",propOr:"getOr",props:"at",symmetricDifference:"xor",symmetricDifferenceBy:"xorBy",symmetricDifferenceWith:"xorWith",takeLast:"takeRight",takeLastWhile:"takeRightWhile",unapply:"rest",unnest:"flatten",useWith:"overArgs",where:"conformsTo",whereEq:"isMatch",zipObj:"zipObject"},o.aryMethod={1:["assignAll","assignInAll","attempt","castArray","ceil","create","curry","curryRight","defaultsAll","defaultsDeepAll","floor","flow","flowRight","fromPairs","invert","iteratee","memoize","method","mergeAll","methodOf","mixin","nthArg","over","overEvery","overSome","rest","reverse","round","runInContext","spread","template","trim","trimEnd","trimStart","uniqueId","words","zipAll"],2:["add","after","ary","assign","assignAllWith","assignIn","assignInAllWith","at","before","bind","bindAll","bindKey","chunk","cloneDeepWith","cloneWith","concat","conformsTo","countBy","curryN","curryRightN","debounce","defaults","defaultsDeep","defaultTo","delay","difference","divide","drop","dropRight","dropRightWhile","dropWhile","endsWith","eq","every","filter","find","findIndex","findKey","findLast","findLastIndex","findLastKey","flatMap","flatMapDeep","flattenDepth","forEach","forEachRight","forIn","forInRight","forOwn","forOwnRight","get","groupBy","gt","gte","has","hasIn","includes","indexOf","intersection","invertBy","invoke","invokeMap","isEqual","isMatch","join","keyBy","lastIndexOf","lt","lte","map","mapKeys","mapValues","matchesProperty","maxBy","meanBy","merge","mergeAllWith","minBy","multiply","nth","omit","omitBy","overArgs","pad","padEnd","padStart","parseInt","partial","partialRight","partition","pick","pickBy","propertyOf","pull","pullAll","pullAt","random","range","rangeRight","rearg","reject","remove","repeat","restFrom","result","sampleSize","some","sortBy","sortedIndex","sortedIndexOf","sortedLastIndex","sortedLastIndexOf","sortedUniqBy","split","spreadFrom","startsWith","subtract","sumBy","take","takeRight","takeRightWhile","takeWhile","tap","throttle","thru","times","trimChars","trimCharsEnd","trimCharsStart","truncate","union","uniqBy","uniqWith","unset","unzipWith","without","wrap","xor","zip","zipObject","zipObjectDeep"],3:["assignInWith","assignWith","clamp","differenceBy","differenceWith","findFrom","findIndexFrom","findLastFrom","findLastIndexFrom","getOr","includesFrom","indexOfFrom","inRange","intersectionBy","intersectionWith","invokeArgs","invokeArgsMap","isEqualWith","isMatchWith","flatMapDepth","lastIndexOfFrom","mergeWith","orderBy","padChars","padCharsEnd","padCharsStart","pullAllBy","pullAllWith","rangeStep","rangeStepRight","reduce","reduceRight","replace","set","slice","sortedIndexBy","sortedLastIndexBy","transform","unionBy","unionWith","update","xorBy","xorWith","zipWith"],4:["fill","setWith","updateWith"]},o.aryRearg={2:[1,0],3:[2,0,1],4:[3,2,0,1]},o.iterateeAry={dropRightWhile:1,dropWhile:1,every:1,filter:1,find:1,findFrom:1,findIndex:1,findIndexFrom:1,findKey:1,findLast:1,findLastFrom:1,findLastIndex:1,findLastIndexFrom:1,findLastKey:1,flatMap:1,flatMapDeep:1,flatMapDepth:1,forEach:1,forEachRight:1,forIn:1,forInRight:1,forOwn:1,forOwnRight:1,map:1,mapKeys:1,mapValues:1,partition:1,reduce:2,reduceRight:2,reject:1,remove:1,some:1,takeRightWhile:1,takeWhile:1,times:1,transform:2},o.iterateeRearg={mapKeys:[1],reduceRight:[1,0]},o.methodRearg={assignInAllWith:[1,0],assignInWith:[1,2,0],assignAllWith:[1,0],assignWith:[1,2,0],differenceBy:[1,2,0],differenceWith:[1,2,0],getOr:[2,1,0],intersectionBy:[1,2,0],intersectionWith:[1,2,0],isEqualWith:[1,2,0],isMatchWith:[2,1,0],mergeAllWith:[1,0],mergeWith:[1,2,0],padChars:[2,1,0],padCharsEnd:[2,1,0],padCharsStart:[2,1,0],pullAllBy:[2,1,0],pullAllWith:[2,1,0],rangeStep:[1,2,0],rangeStepRight:[1,2,0],setWith:[3,1,2,0],sortedIndexBy:[2,1,0],sortedLastIndexBy:[2,1,0],unionBy:[1,2,0],unionWith:[1,2,0],updateWith:[3,1,2,0],xorBy:[1,2,0],xorWith:[1,2,0],zipWith:[1,2,0]},o.methodSpread={assignAll:{start:0},assignAllWith:{start:0},assignInAll:{start:0},assignInAllWith:{start:0},defaultsAll:{start:0},defaultsDeepAll:{start:0},invokeArgs:{start:2},invokeArgsMap:{start:2},mergeAll:{start:0},mergeAllWith:{start:0},partial:{start:1},partialRight:{start:1},without:{start:1},zipAll:{start:0}},o.mutate={array:{fill:!0,pull:!0,pullAll:!0,pullAllBy:!0,pullAllWith:!0,pullAt:!0,remove:!0,reverse:!0},object:{assign:!0,assignAll:!0,assignAllWith:!0,assignIn:!0,assignInAll:!0,assignInAllWith:!0,assignInWith:!0,assignWith:!0,defaults:!0,defaultsAll:!0,defaultsDeep:!0,defaultsDeepAll:!0,merge:!0,mergeAll:!0,mergeAllWith:!0,mergeWith:!0},set:{set:!0,setWith:!0,unset:!0,update:!0,updateWith:!0}},o.realToAlias=function(){var s=Object.prototype.hasOwnProperty,i=o.aliasToReal,a={};for(var u in i){var _=i[u];s.call(a,_)?a[_].push(u):a[_]=[u]}return a}(),o.remap={assignAll:"assign",assignAllWith:"assignWith",assignInAll:"assignIn",assignInAllWith:"assignInWith",curryN:"curry",curryRightN:"curryRight",defaultsAll:"defaults",defaultsDeepAll:"defaultsDeep",findFrom:"find",findIndexFrom:"findIndex",findLastFrom:"findLast",findLastIndexFrom:"findLastIndex",getOr:"get",includesFrom:"includes",indexOfFrom:"indexOf",invokeArgs:"invoke",invokeArgsMap:"invokeMap",lastIndexOfFrom:"lastIndexOf",mergeAll:"merge",mergeAllWith:"mergeWith",padChars:"pad",padCharsEnd:"padEnd",padCharsStart:"padStart",propertyOf:"get",rangeStep:"range",rangeStepRight:"rangeRight",restFrom:"rest",spreadFrom:"spread",trimChars:"trim",trimCharsEnd:"trimEnd",trimCharsStart:"trimStart",zipAll:"zip"},o.skipFixed={castArray:!0,flow:!0,flowRight:!0,iteratee:!0,mixin:!0,rearg:!0,runInContext:!0},o.skipRearg={add:!0,assign:!0,assignIn:!0,bind:!0,bindKey:!0,concat:!0,difference:!0,divide:!0,eq:!0,gt:!0,gte:!0,isEqual:!0,lt:!0,lte:!0,matchesProperty:!0,merge:!0,multiply:!0,overArgs:!0,partial:!0,partialRight:!0,propertyOf:!0,random:!0,range:!0,rangeRight:!0,subtract:!0,zip:!0,zipObject:!0,zipObjectDeep:!0}},17255:(s,o,i)=>{var a=i(47422);s.exports=function basePropertyDeep(s){return function(o){return a(o,s)}}},17285:s=>{function source(s){return s?"string"==typeof s?s:s.source:null}function lookahead(s){return concat("(?=",s,")")}function concat(...s){return s.map((s=>source(s))).join("")}function either(...s){return"("+s.map((s=>source(s))).join("|")+")"}s.exports=function xml(s){const o=concat(/[A-Z_]/,function optional(s){return concat("(",s,")?")}(/[A-Z0-9_.-]*:/),/[A-Z0-9_.-]*/),i={className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},a={begin:/\s/,contains:[{className:"meta-keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]},u=s.inherit(a,{begin:/\(/,end:/\)/}),_=s.inherit(s.APOS_STRING_MODE,{className:"meta-string"}),w=s.inherit(s.QUOTE_STRING_MODE,{className:"meta-string"}),x={endsWithParent:!0,illegal:/`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin://,relevance:10,contains:[a,w,_,u,{begin:/\[/,end:/\]/,contains:[{className:"meta",begin://,contains:[a,u,w,_]}]}]},s.COMMENT(//,{relevance:10}),{begin://,relevance:10},i,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:/)/,end:/>/,keywords:{name:"style"},contains:[x],starts:{end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:/)/,end:/>/,keywords:{name:"script"},contains:[x],starts:{end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:/<>|<\/>/},{className:"tag",begin:concat(//,/>/,/\s/)))),end:/\/?>/,contains:[{className:"name",begin:o,relevance:0,starts:x}]},{className:"tag",begin:concat(/<\//,lookahead(concat(o,/>/))),contains:[{className:"name",begin:o,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}}},17400:(s,o,i)=>{var a=i(99374),u=1/0;s.exports=function toFinite(s){return s?(s=a(s))===u||s===-1/0?17976931348623157e292*(s<0?-1:1):s==s?s:0:0===s?s:0}},17533:s=>{s.exports=function yaml(s){var o="true false yes no null",i="[\\w#;/?:@&=+$,.~*'()[\\]]+",a={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[s.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},u=s.inherit(a,{variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),_={className:"number",begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"},w={end:",",endsWithParent:!0,excludeEnd:!0,keywords:o,relevance:0},x={begin:/\{/,end:/\}/,contains:[w],illegal:"\\n",relevance:0},C={begin:"\\[",end:"\\]",contains:[w],illegal:"\\n",relevance:0},j=[{className:"attr",variants:[{begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$",relevance:10},{className:"string",begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!\\w+!"+i},{className:"type",begin:"!<"+i+">"},{className:"type",begin:"!"+i},{className:"type",begin:"!!"+i},{className:"meta",begin:"&"+s.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+s.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)",relevance:0},s.HASH_COMMENT_MODE,{beginKeywords:o,keywords:{literal:o}},_,{className:"number",begin:s.C_NUMBER_RE+"\\b",relevance:0},x,C,a],L=[...j];return L.pop(),L.push(u),w.contains=L,{name:"YAML",case_insensitive:!0,aliases:["yml"],contains:j}}},17670:(s,o,i)=>{var a=i(12651);s.exports=function mapCacheDelete(s){var o=a(this,s).delete(s);return this.size-=o?1:0,o}},17965:(s,o,i)=>{"use strict";var a=i(16426),u={"text/plain":"Text","text/html":"Url",default:"Text"};s.exports=function copy(s,o){var i,_,w,x,C,j,L=!1;o||(o={}),i=o.debug||!1;try{if(w=a(),x=document.createRange(),C=document.getSelection(),(j=document.createElement("span")).textContent=s,j.ariaHidden="true",j.style.all="unset",j.style.position="fixed",j.style.top=0,j.style.clip="rect(0, 0, 0, 0)",j.style.whiteSpace="pre",j.style.webkitUserSelect="text",j.style.MozUserSelect="text",j.style.msUserSelect="text",j.style.userSelect="text",j.addEventListener("copy",(function(a){if(a.stopPropagation(),o.format)if(a.preventDefault(),void 0===a.clipboardData){i&&console.warn("unable to use e.clipboardData"),i&&console.warn("trying IE specific stuff"),window.clipboardData.clearData();var _=u[o.format]||u.default;window.clipboardData.setData(_,s)}else a.clipboardData.clearData(),a.clipboardData.setData(o.format,s);o.onCopy&&(a.preventDefault(),o.onCopy(a.clipboardData))})),document.body.appendChild(j),x.selectNodeContents(j),C.addRange(x),!document.execCommand("copy"))throw new Error("copy command was unsuccessful");L=!0}catch(a){i&&console.error("unable to copy using execCommand: ",a),i&&console.warn("trying IE specific stuff");try{window.clipboardData.setData(o.format||"text",s),o.onCopy&&o.onCopy(window.clipboardData),L=!0}catch(a){i&&console.error("unable to copy using clipboardData: ",a),i&&console.error("falling back to prompt"),_=function format(s){var o=(/mac os x/i.test(navigator.userAgent)?"⌘":"Ctrl")+"+C";return s.replace(/#{\s*key\s*}/g,o)}("message"in o?o.message:"Copy to clipboard: #{key}, Enter"),window.prompt(_,s)}}finally{C&&("function"==typeof C.removeRange?C.removeRange(x):C.removeAllRanges()),j&&document.body.removeChild(j),w()}return L}},18073:(s,o,i)=>{var a=i(85087),u=i(54641),_=i(70981);s.exports=function createRecurry(s,o,i,w,x,C,j,L,B,$){var U=8&o;o|=U?32:64,4&(o&=~(U?64:32))||(o&=-4);var V=[s,o,x,U?C:void 0,U?j:void 0,U?void 0:C,U?void 0:j,L,B,$],z=i.apply(void 0,V);return a(s)&&u(z,V),z.placeholder=w,_(z,s,o)}},19123:(s,o,i)=>{var a=i(65606),u=i(31499),_=i(88310).Stream;function resolve(s,o,i){var a,_=function create_indent(s,o){return new Array(o||0).join(s||"")}(o,i=i||0),w=s;if("object"==typeof s&&((w=s[a=Object.keys(s)[0]])&&w._elem))return w._elem.name=a,w._elem.icount=i,w._elem.indent=o,w._elem.indents=_,w._elem.interrupt=w,w._elem;var x,C=[],j=[];function get_attributes(s){Object.keys(s).forEach((function(o){C.push(function attribute(s,o){return s+'="'+u(o)+'"'}(o,s[o]))}))}switch(typeof w){case"object":if(null===w)break;w._attr&&get_attributes(w._attr),w._cdata&&j.push(("/g,"]]]]>")+"]]>"),w.forEach&&(x=!1,j.push(""),w.forEach((function(s){"object"==typeof s?"_attr"==Object.keys(s)[0]?get_attributes(s._attr):j.push(resolve(s,o,i+1)):(j.pop(),x=!0,j.push(u(s)))})),x||j.push(""));break;default:j.push(u(w))}return{name:a,interrupt:!1,attributes:C,content:j,icount:i,indents:_,indent:o}}function format(s,o,i){if("object"!=typeof o)return s(!1,o);var a=o.interrupt?1:o.content.length;function proceed(){for(;o.content.length;){var u=o.content.shift();if(void 0!==u){if(interrupt(u))return;format(s,u)}}s(!1,(a>1?o.indents:"")+(o.name?"":"")+(o.indent&&!i?"\n":"")),i&&i()}function interrupt(o){return!!o.interrupt&&(o.interrupt.append=s,o.interrupt.end=proceed,o.interrupt=!1,s(!0),!0)}if(s(!1,o.indents+(o.name?"<"+o.name:"")+(o.attributes.length?" "+o.attributes.join(" "):"")+(a?o.name?">":"":o.name?"/>":"")+(o.indent&&a>1?"\n":"")),!a)return s(!1,o.indent?"\n":"");interrupt(o)||proceed()}s.exports=function xml(s,o){"object"!=typeof o&&(o={indent:o});var i=o.stream?new _:null,u="",w=!1,x=o.indent?!0===o.indent?" ":o.indent:"",C=!0;function delay(s){C?a.nextTick(s):s()}function append(s,o){if(void 0!==o&&(u+=o),s&&!w&&(i=i||new _,w=!0),s&&w){var a=u;delay((function(){i.emit("data",a)})),u=""}}function add(s,o){format(append,resolve(s,x,x?1:0),o)}function end(){if(i){var s=u;delay((function(){i.emit("data",s),i.emit("end"),i.readable=!1,i.emit("close")}))}}return delay((function(){C=!1})),o.declaration&&function addXmlDeclaration(s){var o={version:"1.0",encoding:s.encoding||"UTF-8"};s.standalone&&(o.standalone=s.standalone),add({"?xml":{_attr:o}}),u=u.replace("/>","?>")}(o.declaration),s&&s.forEach?s.forEach((function(o,i){var a;i+1===s.length&&(a=end),add(o,a)})):add(s,end),i?(i.readable=!0,i):u},s.exports.element=s.exports.Element=function element(){var s={_elem:resolve(Array.prototype.slice.call(arguments)),push:function(s){if(!this.append)throw new Error("not assigned to a parent!");var o=this,i=this._elem.indent;format(this.append,resolve(s,i,this._elem.icount+(i?1:0)),(function(){o.append(!0)}))},close:function(s){void 0!==s&&this.push(s),this.end&&this.end()}};return s}},19219:s=>{s.exports=function cacheHas(s,o){return s.has(o)}},19287:s=>{"use strict";s.exports={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}},19358:(s,o,i)=>{"use strict";var a=i(85582),u=i(49724),_=i(61626),w=i(88280),x=i(79192),C=i(19595),j=i(54829),L=i(34084),B=i(32096),$=i(39259),U=i(85884),V=i(39447),z=i(7376);s.exports=function(s,o,i,Y){var Z="stackTraceLimit",ee=Y?2:1,ie=s.split("."),ae=ie[ie.length-1],ce=a.apply(null,ie);if(ce){var le=ce.prototype;if(!z&&u(le,"cause")&&delete le.cause,!i)return ce;var pe=a("Error"),de=o((function(s,o){var i=B(Y?o:s,void 0),a=Y?new ce(s):new ce;return void 0!==i&&_(a,"message",i),U(a,de,a.stack,2),this&&w(le,this)&&L(a,this,de),arguments.length>ee&&$(a,arguments[ee]),a}));if(de.prototype=le,"Error"!==ae?x?x(de,pe):C(de,pe,{name:!0}):V&&Z in ce&&(j(de,ce,Z),j(de,ce,"prepareStackTrace")),C(de,ce),!z)try{le.name!==ae&&_(le,"name",ae),le.constructor=de}catch(s){}return de}}},19570:(s,o,i)=>{var a=i(37334),u=i(93243),_=i(83488),w=u?function(s,o){return u(s,"toString",{configurable:!0,enumerable:!1,value:a(o),writable:!0})}:_;s.exports=w},19595:(s,o,i)=>{"use strict";var a=i(49724),u=i(11042),_=i(13846),w=i(74284);s.exports=function(s,o,i){for(var x=u(o),C=w.f,j=_.f,L=0;L{"use strict";var a=i(23034);s.exports=a},19846:(s,o,i)=>{"use strict";var a=i(20798),u=i(98828),_=i(45951).String;s.exports=!!Object.getOwnPropertySymbols&&!u((function(){var s=Symbol("symbol detection");return!_(s)||!(Object(s)instanceof Symbol)||!Symbol.sham&&a&&a<41}))},19931:(s,o,i)=>{var a=i(31769),u=i(68090),_=i(68969),w=i(77797);s.exports=function baseUnset(s,o){return o=a(o,s),null==(s=_(s,o))||delete s[w(u(o))]}},20181:(s,o,i)=>{var a=/^\s+|\s+$/g,u=/^[-+]0x[0-9a-f]+$/i,_=/^0b[01]+$/i,w=/^0o[0-7]+$/i,x=parseInt,C="object"==typeof i.g&&i.g&&i.g.Object===Object&&i.g,j="object"==typeof self&&self&&self.Object===Object&&self,L=C||j||Function("return this")(),B=Object.prototype.toString,$=Math.max,U=Math.min,now=function(){return L.Date.now()};function isObject(s){var o=typeof s;return!!s&&("object"==o||"function"==o)}function toNumber(s){if("number"==typeof s)return s;if(function isSymbol(s){return"symbol"==typeof s||function isObjectLike(s){return!!s&&"object"==typeof s}(s)&&"[object Symbol]"==B.call(s)}(s))return NaN;if(isObject(s)){var o="function"==typeof s.valueOf?s.valueOf():s;s=isObject(o)?o+"":o}if("string"!=typeof s)return 0===s?s:+s;s=s.replace(a,"");var i=_.test(s);return i||w.test(s)?x(s.slice(2),i?2:8):u.test(s)?NaN:+s}s.exports=function debounce(s,o,i){var a,u,_,w,x,C,j=0,L=!1,B=!1,V=!0;if("function"!=typeof s)throw new TypeError("Expected a function");function invokeFunc(o){var i=a,_=u;return a=u=void 0,j=o,w=s.apply(_,i)}function shouldInvoke(s){var i=s-C;return void 0===C||i>=o||i<0||B&&s-j>=_}function timerExpired(){var s=now();if(shouldInvoke(s))return trailingEdge(s);x=setTimeout(timerExpired,function remainingWait(s){var i=o-(s-C);return B?U(i,_-(s-j)):i}(s))}function trailingEdge(s){return x=void 0,V&&a?invokeFunc(s):(a=u=void 0,w)}function debounced(){var s=now(),i=shouldInvoke(s);if(a=arguments,u=this,C=s,i){if(void 0===x)return function leadingEdge(s){return j=s,x=setTimeout(timerExpired,o),L?invokeFunc(s):w}(C);if(B)return x=setTimeout(timerExpired,o),invokeFunc(C)}return void 0===x&&(x=setTimeout(timerExpired,o)),w}return o=toNumber(o)||0,isObject(i)&&(L=!!i.leading,_=(B="maxWait"in i)?$(toNumber(i.maxWait)||0,o):_,V="trailing"in i?!!i.trailing:V),debounced.cancel=function cancel(){void 0!==x&&clearTimeout(x),j=0,a=C=u=x=void 0},debounced.flush=function flush(){return void 0===x?w:trailingEdge(now())},debounced}},20317:s=>{s.exports=function mapToArray(s){var o=-1,i=Array(s.size);return s.forEach((function(s,a){i[++o]=[a,s]})),i}},20334:(s,o,i)=>{"use strict";var a=i(48287).Buffer;class NonError extends Error{constructor(s){super(NonError._prepareSuperMessage(s)),Object.defineProperty(this,"name",{value:"NonError",configurable:!0,writable:!0}),Error.captureStackTrace&&Error.captureStackTrace(this,NonError)}static _prepareSuperMessage(s){try{return JSON.stringify(s)}catch{return String(s)}}}const u=[{property:"name",enumerable:!1},{property:"message",enumerable:!1},{property:"stack",enumerable:!1},{property:"code",enumerable:!0}],_=Symbol(".toJSON called"),destroyCircular=({from:s,seen:o,to_:i,forceEnumerable:w,maxDepth:x,depth:C})=>{const j=i||(Array.isArray(s)?[]:{});if(o.push(s),C>=x)return j;if("function"==typeof s.toJSON&&!0!==s[_])return(s=>{s[_]=!0;const o=s.toJSON();return delete s[_],o})(s);for(const[i,u]of Object.entries(s))"function"==typeof a&&a.isBuffer(u)?j[i]="[object Buffer]":"function"!=typeof u&&(u&&"object"==typeof u?o.includes(s[i])?j[i]="[Circular]":(C++,j[i]=destroyCircular({from:s[i],seen:o.slice(),forceEnumerable:w,maxDepth:x,depth:C})):j[i]=u);for(const{property:o,enumerable:i}of u)"string"==typeof s[o]&&Object.defineProperty(j,o,{value:s[o],enumerable:!!w||i,configurable:!0,writable:!0});return j};s.exports={serializeError:(s,o={})=>{const{maxDepth:i=Number.POSITIVE_INFINITY}=o;return"object"==typeof s&&null!==s?destroyCircular({from:s,seen:[],forceEnumerable:!0,maxDepth:i,depth:0}):"function"==typeof s?`[Function: ${s.name||"anonymous"}]`:s},deserializeError:(s,o={})=>{const{maxDepth:i=Number.POSITIVE_INFINITY}=o;if(s instanceof Error)return s;if("object"==typeof s&&null!==s&&!Array.isArray(s)){const o=new Error;return destroyCircular({from:s,seen:[],to_:o,maxDepth:i,depth:0}),o}return new NonError(s)}}},20426:s=>{var o=Object.prototype.hasOwnProperty;s.exports=function baseHas(s,i){return null!=s&&o.call(s,i)}},20575:(s,o,i)=>{"use strict";var a=i(3121);s.exports=function(s){return a(s.length)}},20798:(s,o,i)=>{"use strict";var a,u,_=i(45951),w=i(96794),x=_.process,C=_.Deno,j=x&&x.versions||C&&C.version,L=j&&j.v8;L&&(u=(a=L.split("."))[0]>0&&a[0]<4?1:+(a[0]+a[1])),!u&&w&&(!(a=w.match(/Edge\/(\d+)/))||a[1]>=74)&&(a=w.match(/Chrome\/(\d+)/))&&(u=+a[1]),s.exports=u},20850:(s,o,i)=>{"use strict";s.exports=i(46076)},20999:(s,o,i)=>{var a=i(69302),u=i(36800);s.exports=function createAssigner(s){return a((function(o,i){var a=-1,_=i.length,w=_>1?i[_-1]:void 0,x=_>2?i[2]:void 0;for(w=s.length>3&&"function"==typeof w?(_--,w):void 0,x&&u(i[0],i[1],x)&&(w=_<3?void 0:w,_=1),o=Object(o);++a<_;){var C=i[a];C&&s(o,C,a,w)}return o}))}},21549:(s,o,i)=>{var a=i(22032),u=i(63862),_=i(66721),w=i(12749),x=i(35749);function Hash(s){var o=-1,i=null==s?0:s.length;for(this.clear();++o{var a=i(16547),u=i(43360);s.exports=function copyObject(s,o,i,_){var w=!i;i||(i={});for(var x=-1,C=o.length;++x{var a=i(51873),u=i(37828),_=i(75288),w=i(25911),x=i(20317),C=i(84247),j=a?a.prototype:void 0,L=j?j.valueOf:void 0;s.exports=function equalByTag(s,o,i,a,j,B,$){switch(i){case"[object DataView]":if(s.byteLength!=o.byteLength||s.byteOffset!=o.byteOffset)return!1;s=s.buffer,o=o.buffer;case"[object ArrayBuffer]":return!(s.byteLength!=o.byteLength||!B(new u(s),new u(o)));case"[object Boolean]":case"[object Date]":case"[object Number]":return _(+s,+o);case"[object Error]":return s.name==o.name&&s.message==o.message;case"[object RegExp]":case"[object String]":return s==o+"";case"[object Map]":var U=x;case"[object Set]":var V=1&a;if(U||(U=C),s.size!=o.size&&!V)return!1;var z=$.get(s);if(z)return z==o;a|=2,$.set(s,o);var Y=w(U(s),U(o),a,j,B,$);return $.delete(s),Y;case"[object Symbol]":if(L)return L.call(s)==L.call(o)}return!1}},22032:(s,o,i)=>{var a=i(81042);s.exports=function hashClear(){this.__data__=a?a(null):{},this.size=0}},22225:s=>{var o="\\ud800-\\udfff",i="\\u2700-\\u27bf",a="a-z\\xdf-\\xf6\\xf8-\\xff",u="A-Z\\xc0-\\xd6\\xd8-\\xde",_="\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",w="["+_+"]",x="\\d+",C="["+i+"]",j="["+a+"]",L="[^"+o+_+x+i+a+u+"]",B="(?:\\ud83c[\\udde6-\\uddff]){2}",$="[\\ud800-\\udbff][\\udc00-\\udfff]",U="["+u+"]",V="(?:"+j+"|"+L+")",z="(?:"+U+"|"+L+")",Y="(?:['’](?:d|ll|m|re|s|t|ve))?",Z="(?:['’](?:D|LL|M|RE|S|T|VE))?",ee="(?:[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]|\\ud83c[\\udffb-\\udfff])?",ie="[\\ufe0e\\ufe0f]?",ae=ie+ee+("(?:\\u200d(?:"+["[^"+o+"]",B,$].join("|")+")"+ie+ee+")*"),ce="(?:"+[C,B,$].join("|")+")"+ae,le=RegExp([U+"?"+j+"+"+Y+"(?="+[w,U,"$"].join("|")+")",z+"+"+Z+"(?="+[w,U+V,"$"].join("|")+")",U+"?"+V+"+"+Y,U+"+"+Z,"\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])","\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",x,ce].join("|"),"g");s.exports=function unicodeWords(s){return s.match(le)||[]}},22551:(s,o,i)=>{"use strict";var a=i(96540),u=i(69982);function p(s){for(var o="https://reactjs.org/docs/error-decoder.html?invariant="+s,i=1;i