From 3c171ce6ebbb874ef1cf54022103945b54ffbe2a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 10:23:53 +0100 Subject: [PATCH 1/9] chore(deps): bump actions/checkout from 3.6.0 to 4.0.0 (#3041) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.6.0 to 4.0.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/f43a0e5ff2bd294095638e18286ca9a3d1956744...3df4ab11eba7bda6032a0b82a6bb43b11571feac) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Leandro Damascena --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/label_pr_on_title.yml | 2 +- .github/workflows/on_label_added.yml | 2 +- .github/workflows/on_merged_pr.yml | 2 +- .github/workflows/on_opened_pr.yml | 4 ++-- .github/workflows/ossf_scorecard.yml | 2 +- .github/workflows/publish_v2_layer.yml | 4 ++-- .github/workflows/quality_check.yml | 2 +- .github/workflows/quality_check_pydanticv2.yml | 2 +- .github/workflows/record_pr.yml | 2 +- .github/workflows/release.yml | 14 +++++++------- .../workflows/reusable_deploy_v2_layer_stack.yml | 2 +- .github/workflows/reusable_deploy_v2_sar.yml | 2 +- .github/workflows/reusable_export_pr_details.yml | 2 +- .github/workflows/reusable_publish_changelog.yml | 2 +- .github/workflows/reusable_publish_docs.yml | 2 +- .github/workflows/run-e2e-tests.yml | 2 +- .github/workflows/secure_workflows.yml | 2 +- 19 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 92717d2632..f34d71218b 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@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index da83c3abfe..b977111969 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,6 +17,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - name: 'Dependency Review' uses: actions/dependency-review-action@f6fff72a3217f580d5afd49a46826795305b63c7 # v3.0.8 diff --git a/.github/workflows/label_pr_on_title.yml b/.github/workflows/label_pr_on_title.yml index 1b9ae38a11..747946bf4f 100644 --- a/.github/workflows/label_pr_on_title.yml +++ b/.github/workflows/label_pr_on_title.yml @@ -50,7 +50,7 @@ jobs: pull-requests: write # label respective PR steps: - name: Checkout repository - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - name: "Label PR based on title" uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 env: diff --git a/.github/workflows/on_label_added.yml b/.github/workflows/on_label_added.yml index 13f16e8b7e..ed21d851d3 100644 --- a/.github/workflows/on_label_added.yml +++ b/.github/workflows/on_label_added.yml @@ -47,7 +47,7 @@ jobs: permissions: pull-requests: write # comment on PR steps: - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 # 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 diff --git a/.github/workflows/on_merged_pr.yml b/.github/workflows/on_merged_pr.yml index f816ea5a05..0c67cea338 100644 --- a/.github/workflows/on_merged_pr.yml +++ b/.github/workflows/on_merged_pr.yml @@ -49,7 +49,7 @@ jobs: issues: write # label issue with pending-release if: needs.get_pr_details.outputs.prIsMerged == 'true' steps: - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - name: "Label PR related issue for release" uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 env: diff --git a/.github/workflows/on_opened_pr.yml b/.github/workflows/on_opened_pr.yml index 0cd598fa95..f83aa8133d 100644 --- a/.github/workflows/on_opened_pr.yml +++ b/.github/workflows/on_opened_pr.yml @@ -47,7 +47,7 @@ jobs: needs: get_pr_details runs-on: ubuntu-latest steps: - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - name: "Ensure related issue is present" uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 env: @@ -66,7 +66,7 @@ jobs: permissions: pull-requests: write # label and comment on PR if missing acknowledge section (requirement) steps: - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - name: "Ensure acknowledgement section is present" uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 env: diff --git a/.github/workflows/ossf_scorecard.yml b/.github/workflows/ossf_scorecard.yml index f81b647aee..806173fab9 100644 --- a/.github/workflows/ossf_scorecard.yml +++ b/.github/workflows/ossf_scorecard.yml @@ -22,7 +22,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 with: persist-credentials: false diff --git a/.github/workflows/publish_v2_layer.yml b/.github/workflows/publish_v2_layer.yml index 11c13253cc..c93bc3b879 100644 --- a/.github/workflows/publish_v2_layer.yml +++ b/.github/workflows/publish_v2_layer.yml @@ -88,7 +88,7 @@ jobs: working-directory: ./layer steps: - name: checkout - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 with: ref: ${{ env.RELEASE_COMMIT }} @@ -247,7 +247,7 @@ jobs: pages: none steps: - name: Checkout repository # reusable workflows start clean, so we need to checkout again - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 with: ref: ${{ env.RELEASE_COMMIT }} diff --git a/.github/workflows/quality_check.yml b/.github/workflows/quality_check.yml index 226a85902c..575cfa39d9 100644 --- a/.github/workflows/quality_check.yml +++ b/.github/workflows/quality_check.yml @@ -50,7 +50,7 @@ jobs: permissions: contents: read # checkout code only steps: - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - name: Install poetry run: pipx install poetry - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/quality_check_pydanticv2.yml b/.github/workflows/quality_check_pydanticv2.yml index eb44f51707..a4362ec751 100644 --- a/.github/workflows/quality_check_pydanticv2.yml +++ b/.github/workflows/quality_check_pydanticv2.yml @@ -50,7 +50,7 @@ jobs: permissions: contents: read # checkout code only steps: - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - name: Install poetry run: pipx install poetry - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/record_pr.yml b/.github/workflows/record_pr.yml index c22389072c..4966647df7 100644 --- a/.github/workflows/record_pr.yml +++ b/.github/workflows/record_pr.yml @@ -46,7 +46,7 @@ jobs: permissions: contents: read # NOTE: treat as untrusted location steps: - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - name: "Extract PR details" uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 892e39e108..2fcf63cb2f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,7 +80,7 @@ jobs: RELEASE_VERSION="${RELEASE_TAG_VERSION:1}" echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "$GITHUB_OUTPUT" - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 with: ref: ${{ env.RELEASE_COMMIT }} @@ -115,7 +115,7 @@ jobs: contents: read steps: # NOTE: we need actions/checkout to configure git first (pre-commit hooks in make dev) - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 with: ref: ${{ env.RELEASE_COMMIT }} @@ -156,7 +156,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@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 with: ref: ${{ env.RELEASE_COMMIT }} @@ -225,7 +225,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@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 with: ref: ${{ env.RELEASE_COMMIT }} @@ -259,7 +259,7 @@ jobs: contents: write steps: # NOTE: we need actions/checkout to authenticate and configure git first - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 with: ref: ${{ env.RELEASE_COMMIT }} @@ -303,7 +303,7 @@ jobs: runs-on: ubuntu-latest steps: # NOTE: we need actions/checkout to authenticate and configure git first - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 with: ref: ${{ env.RELEASE_COMMIT }} @@ -357,7 +357,7 @@ jobs: env: RELEASE_VERSION: ${{ needs.seal.outputs.RELEASE_VERSION }} steps: - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 with: ref: ${{ env.RELEASE_COMMIT }} diff --git a/.github/workflows/reusable_deploy_v2_layer_stack.yml b/.github/workflows/reusable_deploy_v2_layer_stack.yml index 8e41280303..5819d8caf6 100644 --- a/.github/workflows/reusable_deploy_v2_layer_stack.yml +++ b/.github/workflows/reusable_deploy_v2_layer_stack.yml @@ -138,7 +138,7 @@ jobs: has_arm64_support: "true" steps: - name: checkout - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 with: ref: ${{ env.RELEASE_COMMIT }} diff --git a/.github/workflows/reusable_deploy_v2_sar.yml b/.github/workflows/reusable_deploy_v2_sar.yml index 79b4db4224..0b79b52794 100644 --- a/.github/workflows/reusable_deploy_v2_sar.yml +++ b/.github/workflows/reusable_deploy_v2_sar.yml @@ -79,7 +79,7 @@ jobs: architecture: ["x86_64", "arm64"] steps: - name: checkout - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 with: ref: ${{ env.RELEASE_COMMIT }} diff --git a/.github/workflows/reusable_export_pr_details.yml b/.github/workflows/reusable_export_pr_details.yml index a1f16c9c8f..d942a15695 100644 --- a/.github/workflows/reusable_export_pr_details.yml +++ b/.github/workflows/reusable_export_pr_details.yml @@ -72,7 +72,7 @@ jobs: 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@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - name: "Download previously saved PR" uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 env: diff --git a/.github/workflows/reusable_publish_changelog.yml b/.github/workflows/reusable_publish_changelog.yml index c1bb77389b..7f74b55bf4 100644 --- a/.github/workflows/reusable_publish_changelog.yml +++ b/.github/workflows/reusable_publish_changelog.yml @@ -26,7 +26,7 @@ jobs: pull-requests: write # create PR steps: - name: Checkout repository # reusable workflows start clean, so we need to checkout again - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 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 a826948c89..e84885d037 100644 --- a/.github/workflows/reusable_publish_docs.yml +++ b/.github/workflows/reusable_publish_docs.yml @@ -44,7 +44,7 @@ jobs: 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@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 with: fetch-depth: 0 ref: ${{ inputs.git_ref }} diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml index 1b50b82061..15a62febda 100644 --- a/.github/workflows/run-e2e-tests.yml +++ b/.github/workflows/run-e2e-tests.yml @@ -51,7 +51,7 @@ jobs: if: ${{ github.actor != 'dependabot[bot]' && github.repository == 'aws-powertools/powertools-lambda-python' }} steps: - name: "Checkout" - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - name: Install poetry run: pipx install poetry - name: "Use Python" diff --git a/.github/workflows/secure_workflows.yml b/.github/workflows/secure_workflows.yml index 8c9a523224..23540ef83f 100644 --- a/.github/workflows/secure_workflows.yml +++ b/.github/workflows/secure_workflows.yml @@ -30,7 +30,7 @@ jobs: contents: read # checkout code and subsequently GitHub action workflows steps: - name: Checkout code - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - name: Ensure 3rd party workflows have SHA pinned uses: zgosalvez/github-actions-ensure-sha-pinned-actions@f32435541e24cd6a4700a7f52bb2ec59e80603b1 # v2.1.4 with: From 8534b1630614e57ad1a3f88310abd89433f6dda5 Mon Sep 17 00:00:00 2001 From: aradyaron <59508334+aradyaron@users.noreply.github.com> Date: Tue, 5 Sep 2023 15:23:04 +0300 Subject: [PATCH 2/9] feat(idempotency): add support to custom serialization/deserialization on idempotency decorator (#2951) Co-authored-by: arad.yaron Co-authored-by: Leandro Damascena Co-authored-by: Ruben Fonseca --- .../utilities/idempotency/base.py | 25 +- .../utilities/idempotency/exceptions.py | 12 + .../utilities/idempotency/idempotency.py | 19 +- .../idempotency/serialization/__init__.py | 0 .../idempotency/serialization/base.py | 47 +++ .../idempotency/serialization/custom_dict.py | 23 ++ .../idempotency/serialization/dataclass.py | 43 +++ .../idempotency/serialization/no_op.py | 18 ++ .../idempotency/serialization/pydantic.py | 47 +++ docs/utilities/idempotency.md | 39 +++ ...ith_dataclass_deduced_output_serializer.py | 49 +++ ..._dataclass_explicitly_output_serializer.py | 48 +++ ...otent_function_custom_output_serializer.py | 63 ++++ ...with_pydantic_deduced_output_serializer.py | 45 +++ ...h_pydantic_explicitly_output_serializer.py | 44 +++ .../idempotency/test_idempotency.py | 302 +++++++++++++++++- 16 files changed, 814 insertions(+), 10 deletions(-) create mode 100644 aws_lambda_powertools/utilities/idempotency/serialization/__init__.py create mode 100644 aws_lambda_powertools/utilities/idempotency/serialization/base.py create mode 100644 aws_lambda_powertools/utilities/idempotency/serialization/custom_dict.py create mode 100644 aws_lambda_powertools/utilities/idempotency/serialization/dataclass.py create mode 100644 aws_lambda_powertools/utilities/idempotency/serialization/no_op.py create mode 100644 aws_lambda_powertools/utilities/idempotency/serialization/pydantic.py create mode 100644 examples/idempotency/src/working_with_dataclass_deduced_output_serializer.py create mode 100644 examples/idempotency/src/working_with_dataclass_explicitly_output_serializer.py create mode 100644 examples/idempotency/src/working_with_idempotent_function_custom_output_serializer.py create mode 100644 examples/idempotency/src/working_with_pydantic_deduced_output_serializer.py create mode 100644 examples/idempotency/src/working_with_pydantic_explicitly_output_serializer.py diff --git a/aws_lambda_powertools/utilities/idempotency/base.py b/aws_lambda_powertools/utilities/idempotency/base.py index 46aa5ef896..a8d509b86e 100644 --- a/aws_lambda_powertools/utilities/idempotency/base.py +++ b/aws_lambda_powertools/utilities/idempotency/base.py @@ -18,6 +18,12 @@ BasePersistenceLayer, DataRecord, ) +from aws_lambda_powertools.utilities.idempotency.serialization.base import ( + BaseIdempotencySerializer, +) +from aws_lambda_powertools.utilities.idempotency.serialization.no_op import ( + NoOpSerializer, +) MAX_RETRIES = 2 logger = logging.getLogger(__name__) @@ -51,6 +57,7 @@ def __init__( function_payload: Any, config: IdempotencyConfig, persistence_store: BasePersistenceLayer, + output_serializer: Optional[BaseIdempotencySerializer] = None, function_args: Optional[Tuple] = None, function_kwargs: Optional[Dict] = None, ): @@ -65,12 +72,16 @@ def __init__( Idempotency Configuration persistence_store : BasePersistenceLayer Instance of persistence layer to store idempotency records + output_serializer: Optional[BaseIdempotencySerializer] + Serializer to transform the data to and from a dictionary. + If not supplied, no serialization is done via the NoOpSerializer function_args: Optional[Tuple] Function arguments function_kwargs: Optional[Dict] Function keyword arguments """ self.function = function + self.output_serializer = output_serializer or NoOpSerializer() self.data = deepcopy(_prepare_data(function_payload)) self.fn_args = function_args self.fn_kwargs = function_kwargs @@ -170,7 +181,7 @@ def _get_idempotency_record(self) -> Optional[DataRecord]: return data_record - def _handle_for_status(self, data_record: DataRecord) -> Optional[Dict[Any, Any]]: + def _handle_for_status(self, data_record: DataRecord) -> Optional[Any]: """ Take appropriate action based on data_record's status @@ -180,8 +191,9 @@ def _handle_for_status(self, data_record: DataRecord) -> Optional[Dict[Any, Any] Returns ------- - Optional[Dict[Any, Any] + Optional[Any] Function's response previously used for this idempotency key, if it has successfully executed already. + In case an output serializer is configured, the response is deserialized. Raises ------ @@ -206,8 +218,10 @@ def _handle_for_status(self, data_record: DataRecord) -> Optional[Dict[Any, Any] f"Execution already in progress with idempotency key: " f"{self.persistence_store.event_key_jmespath}={data_record.idempotency_key}", ) - - return data_record.response_json_as_dict() + response_dict: Optional[dict] = data_record.response_json_as_dict() + if response_dict is not None: + return self.output_serializer.from_dict(response_dict) + return None def _get_function_response(self): try: @@ -226,7 +240,8 @@ def _get_function_response(self): else: try: - self.persistence_store.save_success(data=self.data, result=response) + serialized_response: dict = self.output_serializer.to_dict(response) if response else None + self.persistence_store.save_success(data=self.data, result=serialized_response) except Exception as save_exception: raise IdempotencyPersistenceLayerError( "Failed to update record state to success in idempotency store", diff --git a/aws_lambda_powertools/utilities/idempotency/exceptions.py b/aws_lambda_powertools/utilities/idempotency/exceptions.py index 67a8d6721b..6e5930549c 100644 --- a/aws_lambda_powertools/utilities/idempotency/exceptions.py +++ b/aws_lambda_powertools/utilities/idempotency/exceptions.py @@ -71,3 +71,15 @@ class IdempotencyKeyError(BaseError): """ Payload does not contain an idempotent key """ + + +class IdempotencyModelTypeError(BaseError): + """ + Model type does not match expected payload output + """ + + +class IdempotencyNoSerializationModelError(BaseError): + """ + No model was supplied to the serializer + """ diff --git a/aws_lambda_powertools/utilities/idempotency/idempotency.py b/aws_lambda_powertools/utilities/idempotency/idempotency.py index 76d353d205..f38a860a6c 100644 --- a/aws_lambda_powertools/utilities/idempotency/idempotency.py +++ b/aws_lambda_powertools/utilities/idempotency/idempotency.py @@ -4,7 +4,8 @@ import functools import logging import os -from typing import Any, Callable, Dict, Optional, cast +from inspect import isclass +from typing import Any, Callable, Dict, Optional, Type, Union, cast from aws_lambda_powertools.middleware_factory import lambda_handler_decorator from aws_lambda_powertools.shared import constants @@ -14,6 +15,10 @@ from aws_lambda_powertools.utilities.idempotency.persistence.base import ( BasePersistenceLayer, ) +from aws_lambda_powertools.utilities.idempotency.serialization.base import ( + BaseIdempotencyModelSerializer, + BaseIdempotencySerializer, +) from aws_lambda_powertools.utilities.typing import LambdaContext logger = logging.getLogger(__name__) @@ -85,6 +90,7 @@ def idempotent_function( data_keyword_argument: str, persistence_store: BasePersistenceLayer, config: Optional[IdempotencyConfig] = None, + output_serializer: Optional[Union[BaseIdempotencySerializer, Type[BaseIdempotencyModelSerializer]]] = None, ) -> Any: """ Decorator to handle idempotency of any function @@ -99,6 +105,11 @@ def idempotent_function( Instance of BasePersistenceLayer to store data config: IdempotencyConfig Configuration + output_serializer: Optional[Union[BaseIdempotencySerializer, Type[BaseIdempotencyModelSerializer]]] + Serializer to transform the data to and from a dictionary. + If not supplied, no serialization is done via the NoOpSerializer. + In case a serializer of type inheriting BaseIdempotencyModelSerializer is given, + the serializer is derived from the function return type. Examples -------- @@ -124,9 +135,14 @@ def process_order(customer_id: str, order: dict, **kwargs): data_keyword_argument=data_keyword_argument, persistence_store=persistence_store, config=config, + output_serializer=output_serializer, ), ) + if isclass(output_serializer) and issubclass(output_serializer, BaseIdempotencyModelSerializer): + # instantiate an instance of the serializer class + output_serializer = output_serializer.instantiate(function.__annotations__.get("return", None)) + config = config or IdempotencyConfig() @functools.wraps(function) @@ -147,6 +163,7 @@ def decorate(*args, **kwargs): function_payload=payload, config=config, persistence_store=persistence_store, + output_serializer=output_serializer, function_args=args, function_kwargs=kwargs, ) diff --git a/aws_lambda_powertools/utilities/idempotency/serialization/__init__.py b/aws_lambda_powertools/utilities/idempotency/serialization/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aws_lambda_powertools/utilities/idempotency/serialization/base.py b/aws_lambda_powertools/utilities/idempotency/serialization/base.py new file mode 100644 index 0000000000..45317bd031 --- /dev/null +++ b/aws_lambda_powertools/utilities/idempotency/serialization/base.py @@ -0,0 +1,47 @@ +""" +Serialization for supporting idempotency +""" +from abc import ABC, abstractmethod +from typing import Any, Dict + + +class BaseIdempotencySerializer(ABC): + """ + Abstract Base Class for Idempotency serialization layer, supporting dict operations. + """ + + @abstractmethod + def to_dict(self, data: Any) -> Dict: + raise NotImplementedError("Implementation of to_dict is required") + + @abstractmethod + def from_dict(self, data: Dict) -> Any: + raise NotImplementedError("Implementation of from_dict is required") + + +class BaseIdempotencyModelSerializer(BaseIdempotencySerializer): + """ + Abstract Base Class for Idempotency serialization layer, for using a model as data object representation. + """ + + @classmethod + @abstractmethod + def instantiate(cls, model_type: Any) -> BaseIdempotencySerializer: + """ + Creates an instance of a serializer based on a provided model type. + In case the model_type is unknown, None will be sent as `model_type`. + It's on the implementer to verify that: + - None is handled correctly + - A model type not matching the expected types is handled + + Parameters + ---------- + model_type: Any + The model type to instantiate the class for + + Returns + ------- + BaseIdempotencySerializer + Instance of the serializer class + """ + pass diff --git a/aws_lambda_powertools/utilities/idempotency/serialization/custom_dict.py b/aws_lambda_powertools/utilities/idempotency/serialization/custom_dict.py new file mode 100644 index 0000000000..2af8bed08b --- /dev/null +++ b/aws_lambda_powertools/utilities/idempotency/serialization/custom_dict.py @@ -0,0 +1,23 @@ +from typing import Any, Callable, Dict + +from aws_lambda_powertools.utilities.idempotency.serialization.base import BaseIdempotencySerializer + + +class CustomDictSerializer(BaseIdempotencySerializer): + def __init__(self, to_dict: Callable[[Any], Dict], from_dict: Callable[[Dict], Any]): + """ + Parameters + ---------- + to_dict: Callable[[Any], Dict] + A function capable of transforming the saved data object representation into a dictionary + from_dict: Callable[[Dict], Any] + A function capable of transforming the saved dictionary into the original data object representation + """ + self.__to_dict: Callable[[Any], Dict] = to_dict + self.__from_dict: Callable[[Dict], Any] = from_dict + + def to_dict(self, data: Any) -> Dict: + return self.__to_dict(data) + + def from_dict(self, data: Dict) -> Any: + return self.__from_dict(data) diff --git a/aws_lambda_powertools/utilities/idempotency/serialization/dataclass.py b/aws_lambda_powertools/utilities/idempotency/serialization/dataclass.py new file mode 100644 index 0000000000..dac77ed734 --- /dev/null +++ b/aws_lambda_powertools/utilities/idempotency/serialization/dataclass.py @@ -0,0 +1,43 @@ +from dataclasses import asdict, is_dataclass +from typing import Any, Dict, Type + +from aws_lambda_powertools.utilities.idempotency.exceptions import ( + IdempotencyModelTypeError, + IdempotencyNoSerializationModelError, +) +from aws_lambda_powertools.utilities.idempotency.serialization.base import ( + BaseIdempotencyModelSerializer, + BaseIdempotencySerializer, +) + +DataClass = Any + + +class DataclassSerializer(BaseIdempotencyModelSerializer): + """ + A serializer class for transforming data between dataclass objects and dictionaries. + """ + + def __init__(self, model: Type[DataClass]): + """ + Parameters + ---------- + model: Type[DataClass] + A dataclass type to be used for serialization and deserialization + """ + self.__model: Type[DataClass] = model + + def to_dict(self, data: DataClass) -> Dict: + return asdict(data) + + def from_dict(self, data: Dict) -> DataClass: + return self.__model(**data) + + @classmethod + def instantiate(cls, model_type: Any) -> BaseIdempotencySerializer: + if model_type is None: + raise IdempotencyNoSerializationModelError("No serialization model was supplied") + + if not is_dataclass(model_type): + raise IdempotencyModelTypeError("Model type is not inherited of dataclass type") + return cls(model=model_type) diff --git a/aws_lambda_powertools/utilities/idempotency/serialization/no_op.py b/aws_lambda_powertools/utilities/idempotency/serialization/no_op.py new file mode 100644 index 0000000000..59185f704e --- /dev/null +++ b/aws_lambda_powertools/utilities/idempotency/serialization/no_op.py @@ -0,0 +1,18 @@ +from typing import Dict + +from aws_lambda_powertools.utilities.idempotency.serialization.base import BaseIdempotencySerializer + + +class NoOpSerializer(BaseIdempotencySerializer): + def __init__(self): + """ + Parameters + ---------- + Default serializer, does not transform data + """ + + def to_dict(self, data: Dict) -> Dict: + return data + + def from_dict(self, data: Dict) -> Dict: + return data diff --git a/aws_lambda_powertools/utilities/idempotency/serialization/pydantic.py b/aws_lambda_powertools/utilities/idempotency/serialization/pydantic.py new file mode 100644 index 0000000000..0c168233bf --- /dev/null +++ b/aws_lambda_powertools/utilities/idempotency/serialization/pydantic.py @@ -0,0 +1,47 @@ +from typing import Any, Dict, Type + +from pydantic import BaseModel + +from aws_lambda_powertools.utilities.idempotency.exceptions import ( + IdempotencyModelTypeError, + IdempotencyNoSerializationModelError, +) +from aws_lambda_powertools.utilities.idempotency.serialization.base import ( + BaseIdempotencyModelSerializer, + BaseIdempotencySerializer, +) + + +class PydanticSerializer(BaseIdempotencyModelSerializer): + """Pydantic serializer for idempotency models""" + + def __init__(self, model: Type[BaseModel]): + """ + Parameters + ---------- + model: Model + Pydantic model to be used for serialization + """ + self.__model: Type[BaseModel] = model + + def to_dict(self, data: BaseModel) -> Dict: + if callable(getattr(data, "model_dump", None)): + # Support for pydantic V2 + return data.model_dump() # type: ignore[unused-ignore,attr-defined] + return data.dict() + + def from_dict(self, data: Dict) -> BaseModel: + if callable(getattr(self.__model, "model_validate", None)): + # Support for pydantic V2 + return self.__model.model_validate(data) # type: ignore[unused-ignore,attr-defined] + return self.__model.parse_obj(data) + + @classmethod + def instantiate(cls, model_type: Any) -> BaseIdempotencySerializer: + if model_type is None: + raise IdempotencyNoSerializationModelError("No serialization model was supplied") + + if not issubclass(model_type, BaseModel): + raise IdempotencyModelTypeError("Model type is not inherited from pydantic BaseModel") + + return cls(model=model_type) diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md index 6e5c47af6f..3f55b34c25 100644 --- a/docs/utilities/idempotency.md +++ b/docs/utilities/idempotency.md @@ -152,6 +152,45 @@ When using `idempotent_function`, you must tell us which keyword parameter in yo --8<-- "examples/idempotency/src/working_with_idempotent_function_pydantic.py" ``` +#### Output serialization + +The default return of the `idempotent_function` decorator is a JSON object, but you can customize the function's return type by utilizing the `output_serializer` parameter. The output serializer supports any JSON serializable data, **Python Dataclasses** and **Pydantic Models**. + +!!! info "When using the `output_serializer` parameter, the data will continue to be stored in DynamoDB as a JSON object." + +Working with Pydantic Models: + +=== "Explicitly passing the Pydantic model type" + + ```python hl_lines="6 24 25 32 35 44" + --8<-- "examples/idempotency/src/working_with_pydantic_explicitly_output_serializer.py" + ``` +=== "Deducing the Pydantic model type from the return type annotation" + + ```python hl_lines="6 24 25 32 36 45" + --8<-- "examples/idempotency/src/working_with_pydantic_deduced_output_serializer.py" + ``` + +Working with Python Dataclasses: + +=== "Explicitly passing the model type" + + ```python hl_lines="8 27-29 36 39 48" + --8<-- "examples/idempotency/src/working_with_dataclass_explicitly_output_serializer.py" + ``` + +=== "Deducing the model type from the return type annotation" + + ```python hl_lines="8 27-29 36 40 49" + --8<-- "examples/idempotency/src/working_with_dataclass_deduced_output_serializer.py" + ``` + +=== "Using A Custom Type (Dataclasses)" + + ```python hl_lines="9 33 37 41-44 51 54" + --8<-- "examples/idempotency/src/working_with_idempotent_function_custom_output_serializer.py" + ``` + #### Batch integration You can can easily integrate with [Batch utility](batch.md){target="_blank"} via context manager. This ensures that you process each record in an idempotent manner, and guard against a [Lambda timeout](#lambda-timeouts) idempotent situation. diff --git a/examples/idempotency/src/working_with_dataclass_deduced_output_serializer.py b/examples/idempotency/src/working_with_dataclass_deduced_output_serializer.py new file mode 100644 index 0000000000..3feb5153e3 --- /dev/null +++ b/examples/idempotency/src/working_with_dataclass_deduced_output_serializer.py @@ -0,0 +1,49 @@ +from dataclasses import dataclass + +from aws_lambda_powertools.utilities.idempotency import ( + DynamoDBPersistenceLayer, + IdempotencyConfig, + idempotent_function, +) +from aws_lambda_powertools.utilities.idempotency.serialization.dataclass import DataclassSerializer +from aws_lambda_powertools.utilities.typing import LambdaContext + +dynamodb = DynamoDBPersistenceLayer(table_name="IdempotencyTable") +config = IdempotencyConfig(event_key_jmespath="order_id") # see Choosing a payload subset section + + +@dataclass +class OrderItem: + sku: str + description: str + + +@dataclass +class Order: + item: OrderItem + order_id: int + + +@dataclass +class OrderOutput: + order_id: int + + +@idempotent_function( + data_keyword_argument="order", + config=config, + persistence_store=dynamodb, + output_serializer=DataclassSerializer, +) +# order output is deduced from return type +def deduced_order_output_serializer(order: Order) -> OrderOutput: + return OrderOutput(order_id=order.order_id) + + +def lambda_handler(event: dict, context: LambdaContext): + config.register_lambda_context(context) # see Lambda timeouts section + order_item = OrderItem(sku="fake", description="sample") + order = Order(item=order_item, order_id=1) + + # `order` parameter must be called as a keyword argument to work + deduced_order_output_serializer(order=order) diff --git a/examples/idempotency/src/working_with_dataclass_explicitly_output_serializer.py b/examples/idempotency/src/working_with_dataclass_explicitly_output_serializer.py new file mode 100644 index 0000000000..95b65c570e --- /dev/null +++ b/examples/idempotency/src/working_with_dataclass_explicitly_output_serializer.py @@ -0,0 +1,48 @@ +from dataclasses import dataclass + +from aws_lambda_powertools.utilities.idempotency import ( + DynamoDBPersistenceLayer, + IdempotencyConfig, + idempotent_function, +) +from aws_lambda_powertools.utilities.idempotency.serialization.dataclass import DataclassSerializer +from aws_lambda_powertools.utilities.typing import LambdaContext + +dynamodb = DynamoDBPersistenceLayer(table_name="IdempotencyTable") +config = IdempotencyConfig(event_key_jmespath="order_id") # see Choosing a payload subset section + + +@dataclass +class OrderItem: + sku: str + description: str + + +@dataclass +class Order: + item: OrderItem + order_id: int + + +@dataclass +class OrderOutput: + order_id: int + + +@idempotent_function( + data_keyword_argument="order", + config=config, + persistence_store=dynamodb, + output_serializer=DataclassSerializer(model=OrderOutput), +) +def explicit_order_output_serializer(order: Order): + return OrderOutput(order_id=order.order_id) + + +def lambda_handler(event: dict, context: LambdaContext): + config.register_lambda_context(context) # see Lambda timeouts section + order_item = OrderItem(sku="fake", description="sample") + order = Order(item=order_item, order_id=1) + + # `order` parameter must be called as a keyword argument to work + explicit_order_output_serializer(order=order) diff --git a/examples/idempotency/src/working_with_idempotent_function_custom_output_serializer.py b/examples/idempotency/src/working_with_idempotent_function_custom_output_serializer.py new file mode 100644 index 0000000000..f8ef30c7ab --- /dev/null +++ b/examples/idempotency/src/working_with_idempotent_function_custom_output_serializer.py @@ -0,0 +1,63 @@ +from dataclasses import asdict, dataclass +from typing import Any, Dict + +from aws_lambda_powertools.utilities.idempotency import ( + DynamoDBPersistenceLayer, + IdempotencyConfig, + idempotent_function, +) +from aws_lambda_powertools.utilities.idempotency.serialization.custom_dict import CustomDictSerializer +from aws_lambda_powertools.utilities.typing import LambdaContext + +dynamodb = DynamoDBPersistenceLayer(table_name="IdempotencyTable") +config = IdempotencyConfig(event_key_jmespath="order_id") # see Choosing a payload subset section + + +@dataclass +class OrderItem: + sku: str + description: str + + +@dataclass +class Order: + item: OrderItem + order_id: int + + +@dataclass +class OrderOutput: + order_id: int + + +def custom_to_dict(x: Any) -> Dict: + return asdict(x) + + +def custom_from_dict(x: Dict) -> Any: + return OrderOutput(**x) + + +order_output_serializer = CustomDictSerializer( + to_dict=custom_to_dict, + from_dict=custom_from_dict, +) + + +@idempotent_function( + data_keyword_argument="order", + config=config, + persistence_store=dynamodb, + output_serializer=order_output_serializer, +) +def process_order(order: Order) -> OrderOutput: + return OrderOutput(order_id=order.order_id) + + +def lambda_handler(event: dict, context: LambdaContext): + config.register_lambda_context(context) # see Lambda timeouts section + order_item = OrderItem(sku="fake", description="sample") + order = Order(item=order_item, order_id=1) + + # `order` parameter must be called as a keyword argument to work + process_order(order=order) diff --git a/examples/idempotency/src/working_with_pydantic_deduced_output_serializer.py b/examples/idempotency/src/working_with_pydantic_deduced_output_serializer.py new file mode 100644 index 0000000000..98b7ed52bf --- /dev/null +++ b/examples/idempotency/src/working_with_pydantic_deduced_output_serializer.py @@ -0,0 +1,45 @@ +from aws_lambda_powertools.utilities.idempotency import ( + DynamoDBPersistenceLayer, + IdempotencyConfig, + idempotent_function, +) +from aws_lambda_powertools.utilities.idempotency.serialization.pydantic import PydanticSerializer +from aws_lambda_powertools.utilities.parser import BaseModel +from aws_lambda_powertools.utilities.typing import LambdaContext + +dynamodb = DynamoDBPersistenceLayer(table_name="IdempotencyTable") +config = IdempotencyConfig(event_key_jmespath="order_id") # see Choosing a payload subset section + + +class OrderItem(BaseModel): + sku: str + description: str + + +class Order(BaseModel): + item: OrderItem + order_id: int + + +class OrderOutput(BaseModel): + order_id: int + + +@idempotent_function( + data_keyword_argument="order", + config=config, + persistence_store=dynamodb, + output_serializer=PydanticSerializer, +) +# order output is deduced from return type +def deduced_order_output_serializer(order: Order) -> OrderOutput: + return OrderOutput(order_id=order.order_id) + + +def lambda_handler(event: dict, context: LambdaContext): + config.register_lambda_context(context) # see Lambda timeouts section + order_item = OrderItem(sku="fake", description="sample") + order = Order(item=order_item, order_id=1) + + # `order` parameter must be called as a keyword argument to work + deduced_order_output_serializer(order=order) diff --git a/examples/idempotency/src/working_with_pydantic_explicitly_output_serializer.py b/examples/idempotency/src/working_with_pydantic_explicitly_output_serializer.py new file mode 100644 index 0000000000..6219e688e1 --- /dev/null +++ b/examples/idempotency/src/working_with_pydantic_explicitly_output_serializer.py @@ -0,0 +1,44 @@ +from aws_lambda_powertools.utilities.idempotency import ( + DynamoDBPersistenceLayer, + IdempotencyConfig, + idempotent_function, +) +from aws_lambda_powertools.utilities.idempotency.serialization.pydantic import PydanticSerializer +from aws_lambda_powertools.utilities.parser import BaseModel +from aws_lambda_powertools.utilities.typing import LambdaContext + +dynamodb = DynamoDBPersistenceLayer(table_name="IdempotencyTable") +config = IdempotencyConfig(event_key_jmespath="order_id") # see Choosing a payload subset section + + +class OrderItem(BaseModel): + sku: str + description: str + + +class Order(BaseModel): + item: OrderItem + order_id: int + + +class OrderOutput(BaseModel): + order_id: int + + +@idempotent_function( + data_keyword_argument="order", + config=config, + persistence_store=dynamodb, + output_serializer=PydanticSerializer(model=OrderOutput), +) +def explicit_order_output_serializer(order: Order): + return OrderOutput(order_id=order.order_id) + + +def lambda_handler(event: dict, context: LambdaContext): + config.register_lambda_context(context) # see Lambda timeouts section + order_item = OrderItem(sku="fake", description="sample") + order = Order(item=order_item, order_id=1) + + # `order` parameter must be called as a keyword argument to work + explicit_order_output_serializer(order=order) diff --git a/tests/functional/idempotency/test_idempotency.py b/tests/functional/idempotency/test_idempotency.py index 47b2474466..24fcd76b4d 100644 --- a/tests/functional/idempotency/test_idempotency.py +++ b/tests/functional/idempotency/test_idempotency.py @@ -17,6 +17,8 @@ from aws_lambda_powertools.utilities.idempotency import ( DynamoDBPersistenceLayer, IdempotencyConfig, + idempotent, + idempotent_function, ) from aws_lambda_powertools.utilities.idempotency.base import ( MAX_RETRIES, @@ -28,17 +30,18 @@ IdempotencyInconsistentStateError, IdempotencyInvalidStatusError, IdempotencyKeyError, + IdempotencyModelTypeError, + IdempotencyNoSerializationModelError, IdempotencyPersistenceLayerError, IdempotencyValidationError, ) -from aws_lambda_powertools.utilities.idempotency.idempotency import ( - idempotent, - idempotent_function, -) from aws_lambda_powertools.utilities.idempotency.persistence.base import ( BasePersistenceLayer, DataRecord, ) +from aws_lambda_powertools.utilities.idempotency.serialization.custom_dict import CustomDictSerializer +from aws_lambda_powertools.utilities.idempotency.serialization.dataclass import DataclassSerializer +from aws_lambda_powertools.utilities.idempotency.serialization.pydantic import PydanticSerializer from aws_lambda_powertools.utilities.validation import envelopes, validator from tests.functional.idempotency.utils import ( build_idempotency_put_item_stub, @@ -1196,6 +1199,297 @@ def record_handler(record): assert result == expected_result +def test_idempotent_function_serialization_custom_dict(): + # GIVEN + config = IdempotencyConfig(use_local_cache=True) + mock_event = {"customer_id": "fake", "transaction_id": "fake-id"} + idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_serialization_custom_dict..record_handler#{hash_idempotency_key(mock_event)}" # noqa E501 + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + + to_dict_called = False + from_dict_called = False + + def to_dict(data): + nonlocal to_dict_called + to_dict_called = True + return data + + def from_dict(data): + nonlocal from_dict_called + from_dict_called = True + return data + + expected_result = {"message": "Foo"} + output_serializer = CustomDictSerializer( + to_dict=to_dict, + from_dict=from_dict, + ) + + @idempotent_function( + persistence_store=persistence_layer, + data_keyword_argument="record", + config=config, + output_serializer=output_serializer, + ) + def record_handler(record): + return expected_result + + record_handler(record=mock_event) + assert to_dict_called + record_handler(record=mock_event) + assert from_dict_called + + +def test_idempotent_function_serialization_no_response(): + # GIVEN + config = IdempotencyConfig(use_local_cache=True) + mock_event = {"customer_id": "fake", "transaction_id": "fake-id"} + idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_serialization_no_response..record_handler#{hash_idempotency_key(mock_event)}" # noqa E501 + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + + to_dict_called = False + from_dict_called = False + + def to_dict(data): + nonlocal to_dict_called + to_dict_called = True + return data + + def from_dict(data): + nonlocal from_dict_called + from_dict_called = True + return data + + output_serializer = CustomDictSerializer( + to_dict=to_dict, + from_dict=from_dict, + ) + + @idempotent_function( + persistence_store=persistence_layer, + data_keyword_argument="record", + config=config, + output_serializer=output_serializer, + ) + def record_handler(record): + return None + + record_handler(record=mock_event) + assert to_dict_called is False, "in case response is None, to_dict should not be called" + response = record_handler(record=mock_event) + assert response is None + assert from_dict_called is False, "in case response is None, from_dict should not be called" + + +@pytest.mark.parametrize("output_serializer_type", ["explicit", "deduced"]) +def test_idempotent_function_serialization_pydantic(output_serializer_type: str): + # GIVEN + config = IdempotencyConfig(use_local_cache=True) + mock_event = {"customer_id": "fake", "transaction_id": "fake-id"} + idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_serialization_pydantic..collect_payment#{hash_idempotency_key(mock_event)}" # noqa E501 + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + + class PaymentInput(BaseModel): + customer_id: str + transaction_id: str + + class PaymentOutput(BaseModel): + customer_id: str + transaction_id: str + + if output_serializer_type == "explicit": + output_serializer = PydanticSerializer( + model=PaymentOutput, + ) + else: + output_serializer = PydanticSerializer + + @idempotent_function( + data_keyword_argument="payment", + persistence_store=persistence_layer, + config=config, + output_serializer=output_serializer, + ) + def collect_payment(payment: PaymentInput) -> PaymentOutput: + return PaymentOutput(**payment.dict()) + + # WHEN + payment = PaymentInput(**mock_event) + first_call: PaymentOutput = collect_payment(payment=payment) + assert first_call.customer_id == payment.customer_id + assert first_call.transaction_id == payment.transaction_id + assert isinstance(first_call, PaymentOutput) + second_call: PaymentOutput = collect_payment(payment=payment) + assert isinstance(second_call, PaymentOutput) + assert second_call.customer_id == payment.customer_id + assert second_call.transaction_id == payment.transaction_id + + +def test_idempotent_function_serialization_pydantic_failure_no_return_type(): + # GIVEN + config = IdempotencyConfig(use_local_cache=True) + mock_event = {"customer_id": "fake", "transaction_id": "fake-id"} + idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_serialization_pydantic_failure_no_return_type..collect_payment#{hash_idempotency_key(mock_event)}" # noqa E501 + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + + class PaymentInput(BaseModel): + customer_id: str + transaction_id: str + + class PaymentOutput(BaseModel): + customer_id: str + transaction_id: str + + idempotent_function_decorator = idempotent_function( + data_keyword_argument="payment", + persistence_store=persistence_layer, + config=config, + output_serializer=PydanticSerializer, + ) + with pytest.raises(IdempotencyNoSerializationModelError, match="No serialization model was supplied"): + + @idempotent_function_decorator + def collect_payment(payment: PaymentInput): + return PaymentOutput(**payment.dict()) + + +def test_idempotent_function_serialization_pydantic_failure_bad_type(): + # GIVEN + config = IdempotencyConfig(use_local_cache=True) + mock_event = {"customer_id": "fake", "transaction_id": "fake-id"} + idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_serialization_pydantic_failure_no_return_type..collect_payment#{hash_idempotency_key(mock_event)}" # noqa E501 + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + + class PaymentInput(BaseModel): + customer_id: str + transaction_id: str + + class PaymentOutput(BaseModel): + customer_id: str + transaction_id: str + + idempotent_function_decorator = idempotent_function( + data_keyword_argument="payment", + persistence_store=persistence_layer, + config=config, + output_serializer=PydanticSerializer, + ) + with pytest.raises(IdempotencyModelTypeError, match="Model type is not inherited from pydantic BaseModel"): + + @idempotent_function_decorator + def collect_payment(payment: PaymentInput) -> dict: + return PaymentOutput(**payment.dict()) + + +@pytest.mark.parametrize("output_serializer_type", ["explicit", "deduced"]) +def test_idempotent_function_serialization_dataclass(output_serializer_type: str): + # GIVEN + dataclasses = get_dataclasses_lib() + config = IdempotencyConfig(use_local_cache=True) + mock_event = {"customer_id": "fake", "transaction_id": "fake-id"} + idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_serialization_dataclass..collect_payment#{hash_idempotency_key(mock_event)}" # noqa E501 + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + + @dataclasses.dataclass + class PaymentInput: + customer_id: str + transaction_id: str + + @dataclasses.dataclass + class PaymentOutput: + customer_id: str + transaction_id: str + + if output_serializer_type == "explicit": + output_serializer = DataclassSerializer( + model=PaymentOutput, + ) + else: + output_serializer = DataclassSerializer + + @idempotent_function( + data_keyword_argument="payment", + persistence_store=persistence_layer, + config=config, + output_serializer=output_serializer, + ) + def collect_payment(payment: PaymentInput) -> PaymentOutput: + return PaymentOutput(**dataclasses.asdict(payment)) + + # WHEN + payment = PaymentInput(**mock_event) + first_call: PaymentOutput = collect_payment(payment=payment) + assert first_call.customer_id == payment.customer_id + assert first_call.transaction_id == payment.transaction_id + assert isinstance(first_call, PaymentOutput) + second_call: PaymentOutput = collect_payment(payment=payment) + assert isinstance(second_call, PaymentOutput) + assert second_call.customer_id == payment.customer_id + assert second_call.transaction_id == payment.transaction_id + + +def test_idempotent_function_serialization_dataclass_failure_no_return_type(): + # GIVEN + dataclasses = get_dataclasses_lib() + config = IdempotencyConfig(use_local_cache=True) + mock_event = {"customer_id": "fake", "transaction_id": "fake-id"} + idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_serialization_pydantic_failure_no_return_type..collect_payment#{hash_idempotency_key(mock_event)}" # noqa E501 + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + + @dataclasses.dataclass + class PaymentInput: + customer_id: str + transaction_id: str + + @dataclasses.dataclass + class PaymentOutput: + customer_id: str + transaction_id: str + + idempotent_function_decorator = idempotent_function( + data_keyword_argument="payment", + persistence_store=persistence_layer, + config=config, + output_serializer=DataclassSerializer, + ) + with pytest.raises(IdempotencyNoSerializationModelError, match="No serialization model was supplied"): + + @idempotent_function_decorator + def collect_payment(payment: PaymentInput): + return PaymentOutput(**payment.dict()) + + +def test_idempotent_function_serialization_dataclass_failure_bad_type(): + # GIVEN + dataclasses = get_dataclasses_lib() + config = IdempotencyConfig(use_local_cache=True) + mock_event = {"customer_id": "fake", "transaction_id": "fake-id"} + idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_serialization_pydantic_failure_no_return_type..collect_payment#{hash_idempotency_key(mock_event)}" # noqa E501 + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + + @dataclasses.dataclass + class PaymentInput: + customer_id: str + transaction_id: str + + @dataclasses.dataclass + class PaymentOutput: + customer_id: str + transaction_id: str + + idempotent_function_decorator = idempotent_function( + data_keyword_argument="payment", + persistence_store=persistence_layer, + config=config, + output_serializer=PydanticSerializer, + ) + with pytest.raises(IdempotencyModelTypeError, match="Model type is not inherited from pydantic BaseModel"): + + @idempotent_function_decorator + def collect_payment(payment: PaymentInput) -> dict: + return PaymentOutput(**payment.dict()) + + def test_idempotent_function_arbitrary_args_kwargs(): # Scenario to validate we can use idempotent_function with a function # with an arbitrary number of args and kwargs From b836982f6f0461791646c2994e46d35f9f471606 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 23:57:52 +0100 Subject: [PATCH 3/9] chore(deps-dev): bump cfn-lint from 0.79.8 to 0.79.9 (#3046) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index eb38c979a3..cafef7b8bf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -404,17 +404,17 @@ files = [ [[package]] name = "cfn-lint" -version = "0.79.8" +version = "0.79.9" description = "Checks CloudFormation templates for practices and behaviour that could potentially be improved" optional = false python-versions = ">=3.7, <=4.0, !=4.0" files = [ - {file = "cfn-lint-0.79.8.tar.gz", hash = "sha256:d121fe4fac7690247e5124644509e5c8ca9eabb9d7508d28b66c6d3f8c45629e"}, - {file = "cfn_lint-0.79.8-py3-none-any.whl", hash = "sha256:0e2d026ea11b3f1cbab64aaa9df0ec9f31bbf5c40ad84d8a19c17ba8212314b6"}, + {file = "cfn-lint-0.79.9.tar.gz", hash = "sha256:fb8a5fc674ce39469a66d37de19130f4b31fbe4685a19b65ec51c8c8f35e8990"}, + {file = "cfn_lint-0.79.9-py3-none-any.whl", hash = "sha256:e151194ca5b4994d68593f4b0fd3cda3bb28edea2023528ca34ecf82b914eec1"}, ] [package.dependencies] -aws-sam-translator = ">=1.71.0" +aws-sam-translator = ">=1.73.0" jschema-to-python = ">=1.2.3,<1.3.0" jsonpatch = "*" jsonschema = ">=3.0,<4.18" @@ -2992,4 +2992,4 @@ validation = ["fastjsonschema"] [metadata] lock-version = "2.0" python-versions = "^3.7.4" -content-hash = "05d0774e5d3f7bb40b876aed38d34a3b210233289381f965f26fbae8da2f02ad" +content-hash = "ae359f90595dce4d0659a4725d7f8117dd152a7805212194986595d1e2b60bcd" diff --git a/pyproject.toml b/pyproject.toml index 6f2513cbd5..9859a71a1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ aws-sdk = ["boto3"] datadog=["datadog-lambda"] [tool.poetry.group.dev.dependencies] -cfn-lint = "0.79.8" +cfn-lint = "0.79.9" mypy = "^1.1.1" types-python-dateutil = "^2.8.19.6" httpx = ">=0.23.3,<0.25.0" From 0f2870fa25bdd6308763ee19f3c29e71d4edc50b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 08:50:16 +0100 Subject: [PATCH 4/9] chore(ci): changelog rebuild (#3047) Co-authored-by: Powertools for AWS Lambda (Python) bot --- CHANGELOG.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5609e8059d..a545ecad6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,22 +12,28 @@ * **batch:** type response() method ([#3023](https://github.com/aws-powertools/powertools-lambda-python/issues/3023)) +## Features + +* **idempotency:** add support to custom serialization/deserialization on idempotency decorator ([#2951](https://github.com/aws-powertools/powertools-lambda-python/issues/2951)) + ## Maintenance +* **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 squidfunk/mkdocs-material from `b1f7f94` to `f4764d1` in /docs ([#3031](https://github.com/aws-powertools/powertools-lambda-python/issues/3031)) * **deps:** bump squidfunk/mkdocs-material from `f4764d1` to `dd1770c` in /docs ([#3044](https://github.com/aws-powertools/powertools-lambda-python/issues/3044)) -* **deps:** bump squidfunk/mkdocs-material from `97da15b` to `b1f7f94` in /docs ([#3021](https://github.com/aws-powertools/powertools-lambda-python/issues/3021)) -* **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 the boto-typing group with 11 updates ([#3027](https://github.com/aws-powertools/powertools-lambda-python/issues/3027)) -* **deps-dev:** bump aws-cdk from 2.93.0 to 2.94.0 ([#3036](https://github.com/aws-powertools/powertools-lambda-python/issues/3036)) +* **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 `b1f7f94` to `f4764d1` in /docs ([#3031](https://github.com/aws-powertools/powertools-lambda-python/issues/3031)) +* **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 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 cfn-lint from 0.79.7 to 0.79.8 ([#3033](https://github.com/aws-powertools/powertools-lambda-python/issues/3033)) * **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 aws-cdk from 2.93.0 to 2.94.0 ([#3036](https://github.com/aws-powertools/powertools-lambda-python/issues/3036)) * **deps-dev:** bump the boto-typing group with 1 update ([#3013](https://github.com/aws-powertools/powertools-lambda-python/issues/3013)) -* **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.285 to 0.0.286 ([#3014](https://github.com/aws-powertools/powertools-lambda-python/issues/3014)) * **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 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.8 to 0.79.9 ([#3046](https://github.com/aws-powertools/powertools-lambda-python/issues/3046)) From 5304e5c33945239172d269186c3e04b1bfb3d2e8 Mon Sep 17 00:00:00 2001 From: Paul Grillenberger Date: Wed, 6 Sep 2023 16:44:42 +0200 Subject: [PATCH 5/9] fix(parser): change ApproximateCreationDateTime field to datetime in DynamoDBStreamChangedRecordModel (#3049) fix: promote ApproximateCreationDateTime to datetime --- aws_lambda_powertools/utilities/parser/models/dynamodb.py | 4 ++-- tests/events/dynamoStreamEvent.json | 1 + tests/unit/data_classes/test_dynamo_db_stream_event.py | 2 +- tests/unit/parser/test_dynamodb.py | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/aws_lambda_powertools/utilities/parser/models/dynamodb.py b/aws_lambda_powertools/utilities/parser/models/dynamodb.py index 679952a718..4f2de87fad 100644 --- a/aws_lambda_powertools/utilities/parser/models/dynamodb.py +++ b/aws_lambda_powertools/utilities/parser/models/dynamodb.py @@ -1,4 +1,4 @@ -from datetime import date +from datetime import datetime from typing import Any, Dict, List, Optional, Type, Union from pydantic import BaseModel @@ -7,7 +7,7 @@ class DynamoDBStreamChangedRecordModel(BaseModel): - ApproximateCreationDateTime: Optional[date] = None + ApproximateCreationDateTime: Optional[datetime] = None Keys: Dict[str, Dict[str, Any]] NewImage: Optional[Union[Dict[str, Any], Type[BaseModel], BaseModel]] = None OldImage: Optional[Union[Dict[str, Any], Type[BaseModel], BaseModel]] = None diff --git a/tests/events/dynamoStreamEvent.json b/tests/events/dynamoStreamEvent.json index 12c535b005..16009a7a95 100644 --- a/tests/events/dynamoStreamEvent.json +++ b/tests/events/dynamoStreamEvent.json @@ -4,6 +4,7 @@ "eventID": "1", "eventVersion": "1.0", "dynamodb": { + "ApproximateCreationDateTime": 1693997155.0, "Keys": { "Id": { "N": "101" diff --git a/tests/unit/data_classes/test_dynamo_db_stream_event.py b/tests/unit/data_classes/test_dynamo_db_stream_event.py index 798a24f468..f7672abd69 100644 --- a/tests/unit/data_classes/test_dynamo_db_stream_event.py +++ b/tests/unit/data_classes/test_dynamo_db_stream_event.py @@ -33,7 +33,7 @@ def test_dynamodb_stream_trigger_event(): assert record.user_identity is None dynamodb = record.dynamodb assert dynamodb is not None - assert dynamodb.approximate_creation_date_time is None + assert dynamodb.approximate_creation_date_time == record_raw["dynamodb"]["ApproximateCreationDateTime"] keys = dynamodb.keys assert keys is not None assert keys["Id"] == decimal_context.create_decimal(101) diff --git a/tests/unit/parser/test_dynamodb.py b/tests/unit/parser/test_dynamodb.py index 57bd2be5f0..abbcd152d6 100644 --- a/tests/unit/parser/test_dynamodb.py +++ b/tests/unit/parser/test_dynamodb.py @@ -55,7 +55,8 @@ def test_dynamo_db_stream_trigger_event_no_envelope(): dynamodb = record.dynamodb raw_dynamodb = raw_record["dynamodb"] assert dynamodb is not None - assert dynamodb.ApproximateCreationDateTime is None + assert dynamodb.ApproximateCreationDateTime is not None + assert dynamodb.ApproximateCreationDateTime.timestamp() == raw_dynamodb["ApproximateCreationDateTime"] assert dynamodb.OldImage is None assert dynamodb.SequenceNumber == raw_dynamodb["SequenceNumber"] assert dynamodb.SizeBytes == raw_dynamodb["SizeBytes"] From d3fa71370d89c445248b5d1c6b1f6b35a9d79b92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Sep 2023 09:18:28 +0100 Subject: [PATCH 6/9] chore(deps-dev): bump hvac from 1.1.1 to 1.2.0 (#3054) chore(deps-dev): bump hvac from 1.1.1 to 1.2.0 Bumps [hvac](https://github.com/hvac/hvac) from 1.1.1 to 1.2.0. - [Release notes](https://github.com/hvac/hvac/releases) - [Changelog](https://github.com/hvac/hvac/blob/main/CHANGELOG.md) - [Commits](https://github.com/hvac/hvac/compare/v1.1.1...v1.2.0) --- updated-dependencies: - dependency-name: hvac dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index cafef7b8bf..7142fa4114 100644 --- a/poetry.lock +++ b/poetry.lock @@ -990,13 +990,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "hvac" -version = "1.1.1" +version = "1.2.0" description = "HashiCorp Vault API client" optional = false python-versions = ">=3.6.2,<4.0.0" files = [ - {file = "hvac-1.1.1-py3-none-any.whl", hash = "sha256:466e883665b4082933106b292649f9fba3bc0709a1ec1729e9e35b29477164b3"}, - {file = "hvac-1.1.1.tar.gz", hash = "sha256:f9dbcc46b98b250c785eb1050aa11ee34a0c8b6616b75218cf1346a9817992f9"}, + {file = "hvac-1.2.0-py3-none-any.whl", hash = "sha256:95716e0a6c081214d5f6dc74548d6e388aca895cd7be152cfaf177f7520b3d6e"}, + {file = "hvac-1.2.0.tar.gz", hash = "sha256:6f5aa0d6b8138b585d4656d1fe01b5d87616310c80484b909cc84c2cb8f064fd"}, ] [package.dependencies] @@ -2992,4 +2992,4 @@ validation = ["fastjsonschema"] [metadata] lock-version = "2.0" python-versions = "^3.7.4" -content-hash = "ae359f90595dce4d0659a4725d7f8117dd152a7805212194986595d1e2b60bcd" +content-hash = "e9fb58ffd42aabe0876084ba36f74c706452721e785c39194576ea5e17283b9c" diff --git a/pyproject.toml b/pyproject.toml index 9859a71a1e..6cffbbb865 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,7 @@ checksumdir = "^1.2.0" mypy-boto3-appconfigdata = "^1.28.36" ijson = "^3.2.2" typed-ast = { version = "^1.5.5", python = "< 3.8"} -hvac = "^1.1.1" +hvac = "^1.2.0" aws-requests-auth = "^0.4.3" datadog-lambda = "^4.77.0" From 4f83c1964939d381d0a4e96635121a19094761ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Sep 2023 09:19:34 +0100 Subject: [PATCH 7/9] chore(deps): bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update (#3052) chore(deps): bump the layer-balancer group Bumps the layer-balancer group in /layer/scripts/layer-balancer with 1 update: [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2). - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.18.36...config/v1.18.39) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/config dependency-type: direct:production update-type: version-update:semver-patch dependency-group: layer-balancer ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Leandro Damascena --- layer/scripts/layer-balancer/go.mod | 8 ++++---- layer/scripts/layer-balancer/go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/layer/scripts/layer-balancer/go.mod b/layer/scripts/layer-balancer/go.mod index c31053ca0e..de8423585b 100644 --- a/layer/scripts/layer-balancer/go.mod +++ b/layer/scripts/layer-balancer/go.mod @@ -4,7 +4,7 @@ go 1.18 require ( github.com/aws/aws-sdk-go-v2 v1.21.0 - github.com/aws/aws-sdk-go-v2/config v1.18.36 + github.com/aws/aws-sdk-go-v2/config v1.18.39 github.com/aws/aws-sdk-go-v2/service/lambda v1.39.5 golang.org/x/exp v0.0.0-20230321023759-10a507213a29 golang.org/x/sync v0.3.0 @@ -12,14 +12,14 @@ require ( require ( github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.13 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.13.35 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.13.37 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.13.5 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.13.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.6 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.21.5 // indirect github.com/aws/smithy-go v1.14.2 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect diff --git a/layer/scripts/layer-balancer/go.sum b/layer/scripts/layer-balancer/go.sum index 5d66e102a2..9bb97b08b0 100644 --- a/layer/scripts/layer-balancer/go.sum +++ b/layer/scripts/layer-balancer/go.sum @@ -2,10 +2,10 @@ github.com/aws/aws-sdk-go-v2 v1.21.0 h1:gMT0IW+03wtYJhRqTVYn0wLzwdnK9sRMcxmtfGzR github.com/aws/aws-sdk-go-v2 v1.21.0/go.mod h1:/RfNgGmRxI+iFOB1OeJUyxiU+9s88k3pfHvDagGEp0M= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.13 h1:OPLEkmhXf6xFPiz0bLeDArZIDx1NNS4oJyG4nv3Gct0= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.13/go.mod h1:gpAbvyDGQFozTEmlTFO8XcQKHzubdq0LzRyJpG6MiXM= -github.com/aws/aws-sdk-go-v2/config v1.18.36 h1:mLNA12PWU1Y+ueOO79QgQfKIPhc1MYKl44RmvASkJ7Q= -github.com/aws/aws-sdk-go-v2/config v1.18.36/go.mod h1:8AnEFxW9/XGKCbjYDCJy7iltVNyEI9Iu9qC21UzhhgQ= -github.com/aws/aws-sdk-go-v2/credentials v1.13.35 h1:QpsNitYJu0GgvMBLUIYu9H4yryA5kMksjeIVQfgXrt8= -github.com/aws/aws-sdk-go-v2/credentials v1.13.35/go.mod h1:o7rCaLtvK0hUggAGclf76mNGGkaG5a9KWlp+d9IpcV8= +github.com/aws/aws-sdk-go-v2/config v1.18.39 h1:oPVyh6fuu/u4OiW4qcuQyEtk7U7uuNBmHmJSLg1AJsQ= +github.com/aws/aws-sdk-go-v2/config v1.18.39/go.mod h1:+NH/ZigdPckFpgB1TRcRuWCB/Kbbvkxc/iNAKTq5RhE= +github.com/aws/aws-sdk-go-v2/credentials v1.13.37 h1:BvEdm09+ZEh2XtN+PVHPcYwKY3wIeB6pw7vPRM4M9/U= +github.com/aws/aws-sdk-go-v2/credentials v1.13.37/go.mod h1:ACLrdkd4CLZyXOghZ8IYumQbcooAcp2jo/s2xsFH8IM= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 h1:uDZJF1hu0EVT/4bogChk8DyjSF6fof6uL/0Y26Ma7Fg= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11/go.mod h1:TEPP4tENqBGO99KwVpV9MlOX4NSrSLP8u3KRy2CDwA8= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 h1:22dGT7PneFMx4+b3pz7lMTRyN8ZKH7M2cW4GP9yUS2g= @@ -18,10 +18,10 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 h1:CdzPW9kKi github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35/go.mod h1:QGF2Rs33W5MaN9gYdEQOBBFPLwTZkEhRwI33f7KIG0o= github.com/aws/aws-sdk-go-v2/service/lambda v1.39.5 h1:uMvxJFS92hNW6BRX0Ou+5zb9DskgrJQHZ+5yT8FXK5Y= github.com/aws/aws-sdk-go-v2/service/lambda v1.39.5/go.mod h1:ByLHcf0zbHpyLTOy1iPVRPJWmAUPCiJv5k81dt52ID8= -github.com/aws/aws-sdk-go-v2/service/sso v1.13.5 h1:oCvTFSDi67AX0pOX3PuPdGFewvLRU2zzFSrTsgURNo0= -github.com/aws/aws-sdk-go-v2/service/sso v1.13.5/go.mod h1:fIAwKQKBFu90pBxx07BFOMJLpRUGu8VOzLJakeY+0K4= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.5 h1:dnInJb4S0oy8aQuri1mV6ipLlnZPfnsDNB9BGO9PDNY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.5/go.mod h1:yygr8ACQRY2PrEcy3xsUI357stq2AxnFM6DIsR9lij4= +github.com/aws/aws-sdk-go-v2/service/sso v1.13.6 h1:2PylFCfKCEDv6PeSN09pC/VUiRd10wi1VfHG5FrW0/g= +github.com/aws/aws-sdk-go-v2/service/sso v1.13.6/go.mod h1:fIAwKQKBFu90pBxx07BFOMJLpRUGu8VOzLJakeY+0K4= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.6 h1:pSB560BbVj9ZlJZF4WYj5zsytWHWKxg+NgyGV4B2L58= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.6/go.mod h1:yygr8ACQRY2PrEcy3xsUI357stq2AxnFM6DIsR9lij4= github.com/aws/aws-sdk-go-v2/service/sts v1.21.5 h1:CQBFElb0LS8RojMJlxRSo/HXipvTZW2S44Lt9Mk2aYQ= github.com/aws/aws-sdk-go-v2/service/sts v1.21.5/go.mod h1:VC7JDqsqiwXukYEDjoHh9U0fOJtNWh04FPQz4ct4GGU= github.com/aws/smithy-go v1.14.2 h1:MJU9hqBGbvWZdApzpvoF2WAIJDbtjK2NDJSiJP7HblQ= From 19fa45d1fd4182744e8e2764d8770fc103d810de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Sep 2023 09:21:41 +0100 Subject: [PATCH 8/9] chore(deps): bump actions/upload-artifact from 3.1.2 to 3.1.3 (#3053) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3.1.2 to 3.1.3. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/0b7f8abb1508181956e8e162db84b466c27e18ce...a8a3f3ad30e3422c9c7b888a15615d19a852ae32) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Leandro Damascena --- .github/workflows/ossf_scorecard.yml | 2 +- .github/workflows/publish_v2_layer.yml | 2 +- .github/workflows/record_pr.yml | 2 +- .github/workflows/reusable_deploy_v2_layer_stack.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ossf_scorecard.yml b/.github/workflows/ossf_scorecard.yml index 806173fab9..d574655980 100644 --- a/.github/workflows/ossf_scorecard.yml +++ b/.github/workflows/ossf_scorecard.yml @@ -35,7 +35,7 @@ jobs: repo_token: ${{ secrets.SCORECARD_TOKEN }} # read-only fine-grained token to read branch protection settings - name: "Upload results" - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 with: name: SARIF file path: results.sarif diff --git a/.github/workflows/publish_v2_layer.yml b/.github/workflows/publish_v2_layer.yml index c93bc3b879..e875e6eb92 100644 --- a/.github/workflows/publish_v2_layer.yml +++ b/.github/workflows/publish_v2_layer.yml @@ -146,7 +146,7 @@ jobs: - name: zip output run: zip -r cdk.out.zip cdk.out - name: Archive CDK artifacts - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 with: name: cdk-layer-artefact path: layer/cdk.out.zip diff --git a/.github/workflows/record_pr.yml b/.github/workflows/record_pr.yml index 4966647df7..73e2fc3647 100644 --- a/.github/workflows/record_pr.yml +++ b/.github/workflows/record_pr.yml @@ -53,7 +53,7 @@ jobs: script: | const script = require('.github/scripts/save_pr_details.js') await script({github, context, core}) - - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 with: name: pr path: pr.txt diff --git a/.github/workflows/reusable_deploy_v2_layer_stack.yml b/.github/workflows/reusable_deploy_v2_layer_stack.yml index 5819d8caf6..cf544c87e9 100644 --- a/.github/workflows/reusable_deploy_v2_layer_stack.yml +++ b/.github/workflows/reusable_deploy_v2_layer_stack.yml @@ -195,7 +195,7 @@ jobs: cat cdk-layer-stack/${{ matrix.region }}-layer-version.txt - name: Save Layer ARN artifact if: ${{ inputs.stage == 'PROD' }} - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 with: name: cdk-layer-stack path: ./layer/cdk-layer-stack/* # NOTE: upload-artifact does not inherit working-directory setting. From 21fa25dda36b53d459a5c833f3bada985e37e782 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 7 Sep 2023 09:22:48 +0100 Subject: [PATCH 9/9] chore(ci): changelog rebuild (#3055) Co-authored-by: Powertools for AWS Lambda (Python) bot Co-authored-by: Leandro Damascena --- CHANGELOG.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a545ecad6a..988da68ea7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ## 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 @@ -18,22 +19,22 @@ ## Maintenance +* **deps:** bump squidfunk/mkdocs-material from `b1f7f94` to `f4764d1` in /docs ([#3031](https://github.com/aws-powertools/powertools-lambda-python/issues/3031)) +* **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 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 `b1f7f94` to `f4764d1` in /docs ([#3031](https://github.com/aws-powertools/powertools-lambda-python/issues/3031)) -* **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:** 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-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 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.0 to 7.4.1 ([#3042](https://github.com/aws-powertools/powertools-lambda-python/issues/3042)) +* **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 cfn-lint from 0.79.7 to 0.79.8 ([#3033](https://github.com/aws-powertools/powertools-lambda-python/issues/3033)) -* **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 aws-cdk from 2.93.0 to 2.94.0 ([#3036](https://github.com/aws-powertools/powertools-lambda-python/issues/3036)) * **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 aws-cdk from 2.93.0 to 2.94.0 ([#3036](https://github.com/aws-powertools/powertools-lambda-python/issues/3036)) * **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 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.8 to 0.79.9 ([#3046](https://github.com/aws-powertools/powertools-lambda-python/issues/3046)) +* **deps-dev:** bump ruff from 0.0.286 to 0.0.287 ([#3035](https://github.com/aws-powertools/powertools-lambda-python/issues/3035))