From f267a6696ae76fa43b9e727191df2b07136f67c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Nov 2023 11:03:32 +0100 Subject: [PATCH 01/18] chore(deps): bump the layer-balancer group in /layer/scripts/layer-balancer with 3 updates (#3389) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- layer/scripts/layer-balancer/go.mod | 22 +++++++-------- layer/scripts/layer-balancer/go.sum | 44 ++++++++++++++--------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/layer/scripts/layer-balancer/go.mod b/layer/scripts/layer-balancer/go.mod index d05fe79258c..93058aa1ac7 100644 --- a/layer/scripts/layer-balancer/go.mod +++ b/layer/scripts/layer-balancer/go.mod @@ -3,25 +3,25 @@ module layerbalancer go 1.18 require ( - github.com/aws/aws-sdk-go-v2 v1.23.0 - github.com/aws/aws-sdk-go-v2/config v1.25.3 - github.com/aws/aws-sdk-go-v2/service/lambda v1.48.0 + github.com/aws/aws-sdk-go-v2 v1.23.1 + github.com/aws/aws-sdk-go-v2/config v1.25.5 + github.com/aws/aws-sdk-go-v2/service/lambda v1.48.1 golang.org/x/exp v0.0.0-20230321023759-10a507213a29 golang.org/x/sync v0.5.0 ) require ( github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.16.2 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.16.4 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.4 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.3 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.17.2 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.25.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.17.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.25.4 // indirect github.com/aws/smithy-go v1.17.0 // 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 896fc480654..222e4acf1e5 100644 --- a/layer/scripts/layer-balancer/go.sum +++ b/layer/scripts/layer-balancer/go.sum @@ -1,31 +1,31 @@ -github.com/aws/aws-sdk-go-v2 v1.23.0 h1:PiHAzmiQQr6JULBUdvR8fKlA+UPKLT/8KbiqpFBWiAo= -github.com/aws/aws-sdk-go-v2 v1.23.0/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA= +github.com/aws/aws-sdk-go-v2 v1.23.1 h1:qXaFsOOMA+HsZtX8WoCa+gJnbyW7qyFFBlPqvTSzbaI= +github.com/aws/aws-sdk-go-v2 v1.23.1/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1 h1:ZY3108YtBNq96jNZTICHxN1gSBSbnvIdYwwqnvCV4Mc= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1/go.mod h1:t8PYl/6LzdAqsU4/9tz28V/kU+asFePvpOMkdul0gEQ= -github.com/aws/aws-sdk-go-v2/config v1.25.3 h1:E4m9LbwJOoncDNt3e9MPLbz/saxWcGUlZVBydydD6+8= -github.com/aws/aws-sdk-go-v2/config v1.25.3/go.mod h1:tAByZy03nH5jcq0vZmkcVoo6tRzRHEwSFx3QW4NmDw8= -github.com/aws/aws-sdk-go-v2/credentials v1.16.2 h1:0sdZ5cwfOAipTzZ7eOL0gw4LAhk/RZnTa16cDqIt8tg= -github.com/aws/aws-sdk-go-v2/credentials v1.16.2/go.mod h1:sDdvGhXrSVT5yzBDR7qXz+rhbpiMpUYfF3vJ01QSdrc= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.4 h1:9wKDWEjwSnXZre0/O3+ZwbBl1SmlgWYBbrTV10X/H1s= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.4/go.mod h1:t4i+yGHMCcUNIX1x7YVYa6bH/Do7civ5I6cG/6PMfyA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3 h1:DUwbD79T8gyQ23qVXFUthjzVMTviSHi3y4z58KvghhM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3 h1:AplLJCtIaUZDCbr6+gLYdsYNxne4iuaboJhVt9d+WXI= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3/go.mod h1:ify42Rb7nKeDDPkFjKn7q1bPscVPu/+gmHH8d2c+anU= +github.com/aws/aws-sdk-go-v2/config v1.25.5 h1:UGKm9hpQS2hoK8CEJ1BzAW8NbUpvwDJJ4lyqXSzu8bk= +github.com/aws/aws-sdk-go-v2/config v1.25.5/go.mod h1:Bf4gDvy4ZcFIK0rqDu1wp9wrubNba2DojiPB2rt6nvI= +github.com/aws/aws-sdk-go-v2/credentials v1.16.4 h1:i7UQYYDSJrtc30RSwJwfBKwLFNnBTiICqAJ0pPdum8E= +github.com/aws/aws-sdk-go-v2/credentials v1.16.4/go.mod h1:Kdh/okh+//vQ/AjEt81CjvkTo64+/zIE4OewP7RpfXk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.5 h1:KehRNiVzIfAcj6gw98zotVbb/K67taJE0fkfgM6vzqU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.5/go.mod h1:VhnExhw6uXy9QzetvpXDolo1/hjhx4u9qukBGkuUwjs= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.4 h1:LAm3Ycm9HJfbSCd5I+wqC2S9Ej7FPrgr5CQoOljJZcE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.4/go.mod h1:xEhvbJcyUf/31yfGSQBe01fukXwXJ0gxDp7rLfymWE0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.4 h1:4GV0kKZzUxiWxSVpn/9gwR0g21NF1Jsyduzo9rHgC/Q= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.4/go.mod h1:dYvTNAggxDZy6y1AF7YDwXsPuHFy/VNEpEI/2dWK9IU= github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1 h1:uR9lXYjdPX0xY+NhvaJ4dD8rpSRz5VY81ccIIoNG+lw= github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1 h1:rpkF4n0CyFcrJUG/rNNohoTmhtWlFTRI4BsZOh9PvLs= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1/go.mod h1:l9ymW25HOqymeU2m1gbUQ3rUIsTwKs8gYHXkqDQUhiI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.3 h1:kJOolE8xBAD13xTCgOakByZkyP4D/owNmvEiioeUNAg= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.3/go.mod h1:Owv1I59vaghv1Ax8zz8ELY8DN7/Y0rGS+WWAmjgi950= -github.com/aws/aws-sdk-go-v2/service/lambda v1.48.0 h1:Q1ajPX+B64b/OyxuaSDBjqOMmVrpNLhPfTFghpU783k= -github.com/aws/aws-sdk-go-v2/service/lambda v1.48.0/go.mod h1:80TuTBIg7+OWOOA85SdMfvV393HGXPwqoepFTQn6/qA= -github.com/aws/aws-sdk-go-v2/service/sso v1.17.2 h1:V47N5eKgVZoRSvx2+RQ0EpAEit/pqOhqeSQFiS4OFEQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.17.2/go.mod h1:/pE21vno3q1h4bbhUOEi+6Zu/aT26UK2WKkDXd+TssQ= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.0 h1:/XiEU7VIFcVWRDQLabyrSjBoKIm8UkYgsvWDuFW8Img= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.0/go.mod h1:dWqm5G767qwKPuayKfzm4rjzFmVjiBFbOJrpSPnAMDs= -github.com/aws/aws-sdk-go-v2/service/sts v1.25.3 h1:M2w4kiMGJCCM6Ljmmx/l6mmpfa3gPJVpBencfnsgvqs= -github.com/aws/aws-sdk-go-v2/service/sts v1.25.3/go.mod h1:4EqRHDCKP78hq3zOnmFXu5k0j4bXbRFfCh/zQ6KnEfQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.4 h1:rdovz3rEu0vZKbzoMYPTehp0E8veoE9AyfzqCr5Eeao= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.4/go.mod h1:aYCGNjyUCUelhofxlZyj63srdxWUSsBSGg5l6MCuXuE= +github.com/aws/aws-sdk-go-v2/service/lambda v1.48.1 h1:xVOzP4rFi0kMXUQozqInP+Yy6zldr8WTpHeVEqxMtOY= +github.com/aws/aws-sdk-go-v2/service/lambda v1.48.1/go.mod h1:7dj5Kak6A6QOeZxUgIDUWVG5+7upeEBY1ivtFDRLxSQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.17.3 h1:CdsSOGlFF3Pn+koXOIpTtvX7st0IuGsZ8kJqcWMlX54= +github.com/aws/aws-sdk-go-v2/service/sso v1.17.3/go.mod h1:oA6VjNsLll2eVuUoF2D+CMyORgNzPEW/3PyUdq6WQjI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.1 h1:cbRqFTVnJV+KRpwFl76GJdIZJKKCdTPnjUZ7uWh3pIU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.1/go.mod h1:hHL974p5auvXlZPIjJTblXJpbkfK4klBczlsEaMCGVY= +github.com/aws/aws-sdk-go-v2/service/sts v1.25.4 h1:yEvZ4neOQ/KpUqyR+X0ycUTW/kVRNR4nDZ38wStHGAA= +github.com/aws/aws-sdk-go-v2/service/sts v1.25.4/go.mod h1:feTnm2Tk/pJxdX+eooEsxvlvTWBvDm6CasRZ+JOs2IY= github.com/aws/smithy-go v1.17.0 h1:wWJD7LX6PBV6etBUwO0zElG0nWN9rUhp0WdYeHSHAaI= github.com/aws/smithy-go v1.17.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= From f39770356d8e2ba6346e19667b77c2c84319ecff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:29:52 +0100 Subject: [PATCH 02/18] chore(ci): layer docs update (#3385) Co-authored-by: Powertools for AWS Lambda (Python) bot Co-authored-by: Heitor Lessa --- CHANGELOG.md | 19 ++-- docs/index.md | 140 ++++++++++++++--------------- examples/logger/sam/template.yaml | 2 +- examples/metrics/sam/template.yaml | 2 +- examples/tracer/sam/template.yaml | 2 +- 5 files changed, 80 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f27dd0f88b9..cf726896cde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,22 +4,16 @@ # Unreleased + + +## [v2.27.1] - 2023-11-21 ## Documentation -* **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)) +* **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)) ## Maintenance -* **ci:** lint and type checking removal in Pydantic v2 quality check ([#3360](https://github.com/aws-powertools/powertools-lambda-python/issues/3360)) -* **deps:** bump squidfunk/mkdocs-material from `f486dc9` to `2c57e4d` in /docs ([#3366](https://github.com/aws-powertools/powertools-lambda-python/issues/3366)) -* **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-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 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 cfn-lint from 0.83.2 to 0.83.3 ([#3363](https://github.com/aws-powertools/powertools-lambda-python/issues/3363)) -* **deps-dev:** bump ruff from 0.1.5 to 0.1.6 ([#3364](https://github.com/aws-powertools/powertools-lambda-python/issues/3364)) +* version bump @@ -4023,7 +4017,8 @@ * 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.27.0...HEAD +[Unreleased]: https://github.com/aws-powertools/powertools-lambda-python/compare/v2.27.1...HEAD +[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 diff --git a/docs/index.md b/docs/index.md index 7c8959612c9..5f5cd8e6e5a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,8 +26,8 @@ Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverles You can install Powertools for AWS Lambda (Python) using one of the following options: -* **Lambda Layer (x86_64)**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:48**](# "Replace {region} with your AWS region, e.g., eu-west-1"){: .copyMe}:clipboard: -* **Lambda Layer (arm64)**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48**](# "Replace {region} with your AWS region, e.g., eu-west-1"){: .copyMe}:clipboard: +* **Lambda Layer (x86_64)**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:49**](# "Replace {region} with your AWS region, e.g., eu-west-1"){: .copyMe}:clipboard: +* **Lambda Layer (arm64)**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49**](# "Replace {region} with your AWS region, e.g., eu-west-1"){: .copyMe}:clipboard: * **Pip**: **[`pip install "aws-lambda-powertools"`](#){: .copyMe}:clipboard:** !!! question "Looking for Pip signed releases? [Learn more about verifying signed builds](./security.md#verifying-signed-builds)" @@ -80,66 +80,66 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd | Region | Layer ARN | | ---------------- | ---------------------------------------------------------------------------------------------------------- | - | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `ap-south-2` | [arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | - | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `ap-southeast-4` | [arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | - | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `eu-central-2` | [arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | - | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `eu-south-2` | [arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | - | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `il-central-1` | [arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | - | `me-central-1` | [arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | - | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | - | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:48](#){: .copyMe}:clipboard: | + | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `ap-south-2` | [arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:50](#){: .copyMe}:clipboard: | + | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `ap-southeast-4` | [arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV2:50](#){: .copyMe}:clipboard: | + | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `eu-central-2` | [arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:50](#){: .copyMe}:clipboard: | + | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `eu-south-2` | [arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:50](#){: .copyMe}:clipboard: | + | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `il-central-1` | [arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:50](#){: .copyMe}:clipboard: | + | `me-central-1` | [arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:50](#){: .copyMe}:clipboard: | + | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | + | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:49](#){: .copyMe}:clipboard: | === "arm64" | Region | Layer ARN | | ---------------- | ---------------------------------------------------------------------------------------------------------------- | - | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `ap-south-2` | [arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | - | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `eu-central-2` | [arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | - | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `eu-south-2` | [arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | - | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `il-central-1` | [arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | - | `me-central-1` | [arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | - | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | - | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48](#){: .copyMe}:clipboard: | + | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `ap-south-2` | [arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:50](#){: .copyMe}:clipboard: | + | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `eu-central-2` | [arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:50](#){: .copyMe}:clipboard: | + | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `eu-south-2` | [arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:50](#){: .copyMe}:clipboard: | + | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `il-central-1` | [arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:50](#){: .copyMe}:clipboard: | + | `me-central-1` | [arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:50](#){: .copyMe}:clipboard: | + | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | + | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49](#){: .copyMe}:clipboard: | ??? note "Note: Click to expand and copy code snippets for popular frameworks" @@ -152,7 +152,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd Type: AWS::Serverless::Function Properties: Layers: - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:48 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:49 ``` === "Serverless framework" @@ -162,7 +162,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd hello: handler: lambda_function.lambda_handler layers: - - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:48 + - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:49 ``` === "CDK" @@ -178,7 +178,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd powertools_layer = aws_lambda.LayerVersion.from_layer_version_arn( self, id="lambda-powertools", - layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:48" + layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:49" ) aws_lambda.Function(self, 'sample-app-lambda', @@ -227,7 +227,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd role = aws_iam_role.iam_for_lambda.arn handler = "index.test" runtime = "python3.9" - layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:48"] + layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:49"] source_code_hash = filebase64sha256("lambda_function_payload.zip") } @@ -280,7 +280,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd ? Do you want to configure advanced settings? Yes ... ? Do you want to enable Lambda layers for this function? Yes - ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:48 + ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:49 ❯ amplify push -y @@ -291,7 +291,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd - Name: ? Which setting do you want to update? Lambda layers configuration ? Do you want to enable Lambda layers for this function? Yes - ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:48 + ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:49 ? Do you want to edit the local lambda function now? No ``` @@ -305,7 +305,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd Properties: Architectures: [arm64] Layers: - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49 ``` === "Serverless framework" @@ -316,7 +316,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd handler: lambda_function.lambda_handler architecture: arm64 layers: - - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48 + - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49 ``` === "CDK" @@ -332,7 +332,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd powertools_layer = aws_lambda.LayerVersion.from_layer_version_arn( self, id="lambda-powertools", - layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48" + layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49" ) aws_lambda.Function(self, 'sample-app-lambda', @@ -382,7 +382,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd role = aws_iam_role.iam_for_lambda.arn handler = "index.test" runtime = "python3.9" - layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48"] + layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49"] architectures = ["arm64"] source_code_hash = filebase64sha256("lambda_function_payload.zip") @@ -438,7 +438,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd ? Do you want to configure advanced settings? Yes ... ? Do you want to enable Lambda layers for this function? Yes - ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48 + ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49 ❯ amplify push -y @@ -449,7 +449,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd - Name: ? Which setting do you want to update? Lambda layers configuration ? Do you want to enable Lambda layers for this function? Yes - ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:48 + ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:49 ? Do you want to edit the local lambda function now? No ``` @@ -457,7 +457,7 @@ You can include Powertools for AWS Lambda (Python) Lambda Layer using [AWS Lambd Change {region} to your AWS region, e.g. `eu-west-1` ```bash title="AWS CLI" - aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:48 --region {region} + aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:49 --region {region} ``` The pre-signed URL to download this Lambda Layer will be within `Location` key. diff --git a/examples/logger/sam/template.yaml b/examples/logger/sam/template.yaml index d5410ab9164..10358eec0e8 100644 --- a/examples/logger/sam/template.yaml +++ b/examples/logger/sam/template.yaml @@ -14,7 +14,7 @@ Globals: Layers: # Find the latest Layer version in the official documentation # https://docs.powertools.aws.dev/lambda/python/latest/#lambda-layer - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:48 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:49 Resources: LoggerLambdaHandlerExample: diff --git a/examples/metrics/sam/template.yaml b/examples/metrics/sam/template.yaml index bcf2f805476..65a31816f73 100644 --- a/examples/metrics/sam/template.yaml +++ b/examples/metrics/sam/template.yaml @@ -15,7 +15,7 @@ Globals: Layers: # Find the latest Layer version in the official documentation # https://docs.powertools.aws.dev/lambda/python/latest/#lambda-layer - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:48 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:49 Resources: CaptureLambdaHandlerExample: diff --git a/examples/tracer/sam/template.yaml b/examples/tracer/sam/template.yaml index c61b08dcb94..a3ab1418b63 100644 --- a/examples/tracer/sam/template.yaml +++ b/examples/tracer/sam/template.yaml @@ -13,7 +13,7 @@ Globals: Layers: # Find the latest Layer version in the official documentation # https://docs.powertools.aws.dev/lambda/python/latest/#lambda-layer - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:48 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:49 Resources: CaptureLambdaHandlerExample: From 1c1c446ace34a3085d562a734a8e96ef64bd037a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:32:33 +0100 Subject: [PATCH 03/18] chore(deps-dev): bump sentry-sdk from 1.35.0 to 1.36.0 (#3388) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Heitor Lessa --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index eaa0fb7914b..2159233ddf4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2772,13 +2772,13 @@ pbr = "*" [[package]] name = "sentry-sdk" -version = "1.35.0" +version = "1.36.0" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = "*" files = [ - {file = "sentry-sdk-1.35.0.tar.gz", hash = "sha256:04e392db9a0d59bd49a51b9e3a92410ac5867556820465057c2ef89a38e953e9"}, - {file = "sentry_sdk-1.35.0-py2.py3-none-any.whl", hash = "sha256:a7865952701e46d38b41315c16c075367675c48d049b90a4cc2e41991ebc7efa"}, + {file = "sentry-sdk-1.36.0.tar.gz", hash = "sha256:f32dd16547f2f45e1c71a96fd4a48925e629541f7ddfe3d5d25ef7d5e94eb3c8"}, + {file = "sentry_sdk-1.36.0-py2.py3-none-any.whl", hash = "sha256:25d574f94fdf72199e331c2401fdac60d01b5be8f32822174c51c3ff0fc2f8cb"}, ] [package.dependencies] From f94526f8d1d900aebe7a53592460645ef6c615c0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:58:33 +0100 Subject: [PATCH 04/18] chore(ci): bump version to 2.27.1 (#3384) Co-authored-by: Powertools for AWS Lambda (Python) bot Co-authored-by: Heitor Lessa --- aws_lambda_powertools/shared/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/shared/version.py b/aws_lambda_powertools/shared/version.py index 0566b37f769..fce34232afe 100644 --- a/aws_lambda_powertools/shared/version.py +++ b/aws_lambda_powertools/shared/version.py @@ -1,3 +1,3 @@ """Exposes version constant to avoid circular dependencies.""" -VERSION = "2.27.0" +VERSION = "2.27.1" diff --git a/pyproject.toml b/pyproject.toml index 9f9162acb43..a97f474fe6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aws_lambda_powertools" -version = "2.27.0" +version = "2.27.1" description = "Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity." authors = ["Amazon Web Services"] include = ["aws_lambda_powertools/py.typed", "THIRD-PARTY-LICENSES"] From 6a47ee84474d0441cc248bbc597a398cea5345b3 Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Wed, 22 Nov 2023 15:02:20 +0100 Subject: [PATCH 05/18] fix(event_handler): apply serialization as the last operation for middlewares (#3392) --- .../event_handler/api_gateway.py | 59 +++++++++++-------- .../event_handler/bedrock_agent.py | 6 +- .../middlewares/openapi_validation.py | 6 +- .../event_handler/test_api_gateway.py | 2 +- .../event_handler/test_bedrock_agent.py | 4 +- .../test_openapi_validation_middleware.py | 14 ++--- 6 files changed, 48 insertions(+), 43 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 535d79e5874..a2b81974a21 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -699,8 +699,14 @@ def _generate_operation_id(self) -> str: 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.dumps, + route: Optional[Route] = None, + ): self.response = response + self.serializer = serializer self.route = route def _add_cors(self, event: ResponseEventT, cors: CORSConfig): @@ -783,6 +789,11 @@ def build(self, event: ResponseEventT, cors: Optional[CORSConfig] = None) -> Dic self.response.base64_encoded = True self.response.body = base64.b64encode(self.response.body).decode() + # We only apply the serializer when the content type is JSON and the + # body is not a str, to avoid double encoding + elif self.response.is_json() and not isinstance(self.response.body, str): + self.response.body = self.serializer(self.response.body) + return { "statusCode": self.response.status_code, "body": self.response.body, @@ -1332,14 +1343,6 @@ def __init__( self.use([OpenAPIValidationMiddleware()]) - # When using validation, we need to skip the serializer, as the middleware is doing it automatically. - # However, if the user is using a custom serializer, we need to abort. - if serializer: - raise ValueError("Cannot use a custom serializer when using validation") - - # Install a dummy serializer - self._serializer = lambda args: args # type: ignore - def get_openapi_schema( self, *, @@ -1717,7 +1720,7 @@ def resolve(self, event, context) -> Dict[str, Any]: event = event.raw_event if self._debug: - print(self._json_dump(event)) + print(self._serializer(event)) # Populate router(s) dependencies without keeping a reference to each registered router BaseRouter.current_event = self._to_proxy_event(event) @@ -1881,19 +1884,23 @@ def _not_found(self, method: str) -> ResponseBuilder: 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="")) + return ResponseBuilder( + response=Response(status_code=204, content_type=None, headers=headers, body=""), + serializer=self._serializer, + ) handler = self._lookup_exception_handler(NotFoundError) if handler: - return self._response_builder_class(handler(NotFoundError())) + return self._response_builder_class(response=handler(NotFoundError()), serializer=self._serializer) return self._response_builder_class( - Response( + response=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"}), + body={"statusCode": HTTPStatus.NOT_FOUND.value, "message": "Not found"}, ), + serializer=self._serializer, ) def _call_route(self, route: Route, route_arguments: Dict[str, str]) -> ResponseBuilder: @@ -1903,10 +1910,11 @@ def _call_route(self, route: Route, route_arguments: Dict[str, str]) -> Response self._reset_processed_stack() return self._response_builder_class( - self._to_response( + response=self._to_response( route(router_middlewares=self._router_middlewares, app=self, route_arguments=route_arguments), ), - route, + serializer=self._serializer, + route=route, ) except Exception as exc: # If exception is handled then return the response builder to reduce noise @@ -1920,12 +1928,13 @@ def _call_route(self, route: Route, route_arguments: Dict[str, str]) -> Response # we'll let the original exception propagate, so # they get more information about what went wrong. return self._response_builder_class( - Response( + response=Response( status_code=500, content_type=content_types.TEXT_PLAIN, body="".join(traceback.format_exc()), ), - route, + serializer=self._serializer, + route=route, ) raise @@ -1958,18 +1967,19 @@ def _call_exception_handler(self, exp: Exception, route: Route) -> Optional[Resp handler = self._lookup_exception_handler(type(exp)) if handler: try: - return self._response_builder_class(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, ServiceError): return self._response_builder_class( - Response( + 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 @@ -1995,12 +2005,9 @@ def _to_response(self, result: Union[Dict, Tuple, Response]) -> 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: """Adds all routes and context defined in a router diff --git a/aws_lambda_powertools/event_handler/bedrock_agent.py b/aws_lambda_powertools/event_handler/bedrock_agent.py index f3a21958d60..f292a11519a 100644 --- a/aws_lambda_powertools/event_handler/bedrock_agent.py +++ b/aws_lambda_powertools/event_handler/bedrock_agent.py @@ -23,6 +23,10 @@ def build(self, event: BedrockAgentEvent, *args) -> Dict[str, Any]: """Build the full response dict to be returned by the lambda""" self._route(event, None) + body = self.response.body + if self.response.is_json() and not isinstance(self.response.body, str): + body = self.serializer(self.response.body) + return { "messageVersion": "1.0", "response": { @@ -32,7 +36,7 @@ def build(self, event: BedrockAgentEvent, *args) -> Dict[str, Any]: "httpStatusCode": self.response.status_code, "responseBody": { self.response.content_type: { - "body": self.response.body, + "body": body, }, }, }, diff --git a/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py b/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py index c162eeb4ce1..131f9d267a3 100644 --- a/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py +++ b/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py @@ -112,9 +112,9 @@ def _handle_response(self, *, route: Route, response: Response): if response.body: # Validate and serialize the response, if it's JSON if response.is_json(): - response.body = json.dumps( - self._serialize_response(field=route.dependant.return_param, response_content=response.body), - sort_keys=True, + response.body = self._serialize_response( + field=route.dependant.return_param, + response_content=response.body, ) return response diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 8ad5ac35b18..570de9ec808 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -379,7 +379,7 @@ def handler(event, context): # WHEN calling the event handler result = handler(mock_event, None) - # THEN then the response is not compressed + # THEN the response is not compressed assert result["isBase64Encoded"] is False assert result["body"] == expected_value assert result["multiValueHeaders"].get("Content-Encoding") is None diff --git a/tests/functional/event_handler/test_bedrock_agent.py b/tests/functional/event_handler/test_bedrock_agent.py index f112acf0463..223f9bdbef1 100644 --- a/tests/functional/event_handler/test_bedrock_agent.py +++ b/tests/functional/event_handler/test_bedrock_agent.py @@ -31,7 +31,7 @@ def claims() -> Dict[str, Any]: assert result["response"]["httpStatusCode"] == 200 body = result["response"]["responseBody"]["application/json"]["body"] - assert body == json.dumps({"output": claims_response}) + assert json.loads(body) == {"output": claims_response} def test_bedrock_agent_with_path_params(): @@ -79,7 +79,7 @@ def claims(): assert result["response"]["httpStatusCode"] == 200 body = result["response"]["responseBody"]["application/json"]["body"] - assert body == json.dumps(output) + assert json.loads(body) == output def test_bedrock_agent_event_with_no_matches(): diff --git a/tests/functional/event_handler/test_openapi_validation_middleware.py b/tests/functional/event_handler/test_openapi_validation_middleware.py index 2c86ec0baa6..2e14979acce 100644 --- a/tests/functional/event_handler/test_openapi_validation_middleware.py +++ b/tests/functional/event_handler/test_openapi_validation_middleware.py @@ -4,7 +4,6 @@ from pathlib import PurePath from typing import List, Tuple -import pytest from pydantic import BaseModel from aws_lambda_powertools.event_handler import APIGatewayRestResolver @@ -15,11 +14,6 @@ LOAD_GW_EVENT = load_event("apiGatewayProxyEvent.json") -def test_validate_with_customn_serializer(): - with pytest.raises(ValueError): - APIGatewayRestResolver(enable_validation=True, serializer=json.dumps) - - def test_validate_scalars(): # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -128,7 +122,7 @@ def handler() -> List[int]: # THEN the body must be [123, 234] result = app(LOAD_GW_EVENT, {}) assert result["statusCode"] == 200 - assert result["body"] == "[123, 234]" + assert json.loads(result["body"]) == [123, 234] def test_validate_return_tuple(): @@ -148,7 +142,7 @@ def handler() -> Tuple: # THEN the body must be a tuple result = app(LOAD_GW_EVENT, {}) assert result["statusCode"] == 200 - assert result["body"] == "[1, 2, 3]" + assert json.loads(result["body"]) == [1, 2, 3] def test_validate_return_purepath(): @@ -169,7 +163,7 @@ def handler() -> str: # THEN the body must be a string result = app(LOAD_GW_EVENT, {}) assert result["statusCode"] == 200 - assert result["body"] == json.dumps(sample_path.as_posix()) + assert result["body"] == sample_path.as_posix() def test_validate_return_enum(): @@ -190,7 +184,7 @@ def handler() -> Model: # THEN the body must be a string result = app(LOAD_GW_EVENT, {}) assert result["statusCode"] == 200 - assert result["body"] == '"powertools"' + assert result["body"] == "powertools" def test_validate_return_dataclass(): From 9ce3ed13c8b86582722efc8ff4349427cbeee5e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Nov 2023 15:09:20 +0100 Subject: [PATCH 06/18] chore(deps-dev): bump pytest-xdist from 3.4.0 to 3.5.0 (#3387) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Heitor Lessa --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2159233ddf4..19cfb5fee5d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2428,13 +2428,13 @@ pytest = ">=3.6.3" [[package]] name = "pytest-xdist" -version = "3.4.0" +version = "3.5.0" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-xdist-3.4.0.tar.gz", hash = "sha256:3a94a931dd9e268e0b871a877d09fe2efb6175c2c23d60d56a6001359002b832"}, - {file = "pytest_xdist-3.4.0-py3-none-any.whl", hash = "sha256:e513118bf787677a427e025606f55e95937565e06dfaac8d87f55301e57ae607"}, + {file = "pytest-xdist-3.5.0.tar.gz", hash = "sha256:cbb36f3d67e0c478baa57fa4edc8843887e0f6cfc42d677530a36d7472b32d8a"}, + {file = "pytest_xdist-3.5.0-py3-none-any.whl", hash = "sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24"}, ] [package.dependencies] @@ -3224,4 +3224,4 @@ validation = ["fastjsonschema"] [metadata] lock-version = "2.0" python-versions = "^3.7.4" -content-hash = "21c7697a42537357d74b97fcec11754de4defa04a296362dabc226078869f454" +content-hash = "7ee9fe25b426b28a8615bb8017382362d5652be862adfd4e2fffb632bb929b27" diff --git a/pyproject.toml b/pyproject.toml index a97f474fe6e..7bbfbe64cab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ radon = "^6.0.1" xenon = "^0.9.1" mkdocs-git-revision-date-plugin = "^0.3.2" mike = "^1.1.2" -pytest-xdist = "^3.4.0" +pytest-xdist = "^3.5.0" aws-cdk-lib = "^2.110.0" "aws-cdk.aws-apigatewayv2-alpha" = "^2.38.1-alpha.0" "aws-cdk.aws-apigatewayv2-integrations-alpha" = "^2.38.1-alpha.0" From 270060de13cdb5a96d2d2ccac3125df54d410f40 Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Wed, 22 Nov 2023 15:32:00 +0100 Subject: [PATCH 07/18] fix(event_handler): allow fine grained Response with data validation (#3394) --- .../event_handler/api_gateway.py | 5 +- .../event_handler/openapi/params.py | 12 +++++ .../event_handler/test_openapi_params.py | 20 +++++++- .../test_openapi_validation_middleware.py | 50 ++++++++++++++++++- 4 files changed, 83 insertions(+), 4 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index a2b81974a21..5b7262e5d55 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -66,6 +66,7 @@ _ROUTE_REGEX = "^{}$" ResponseEventT = TypeVar("ResponseEventT", bound=BaseProxyEvent) +ResponseT = TypeVar("ResponseT") if TYPE_CHECKING: from aws_lambda_powertools.event_handler.openapi.compat import ( @@ -207,14 +208,14 @@ def to_dict(self, origin: Optional[str]) -> Dict[str, str]: return headers -class Response: +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: Any = None, + body: Optional[ResponseT] = None, headers: Optional[Dict[str, Union[str, List[str]]]] = None, cookies: Optional[List[Cookie]] = None, compress: Optional[bool] = None, diff --git a/aws_lambda_powertools/event_handler/openapi/params.py b/aws_lambda_powertools/event_handler/openapi/params.py index c8099d20404..28154466ff6 100644 --- a/aws_lambda_powertools/event_handler/openapi/params.py +++ b/aws_lambda_powertools/event_handler/openapi/params.py @@ -5,6 +5,7 @@ from pydantic import BaseConfig from pydantic.fields import FieldInfo +from aws_lambda_powertools.event_handler import Response from aws_lambda_powertools.event_handler.openapi.compat import ( ModelField, Required, @@ -724,6 +725,9 @@ def get_field_info_and_type_annotation(annotation, value, is_path_param: bool) - # 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 annotation is not an Annotated type, we use it as the type annotation else: type_annotation = annotation @@ -731,6 +735,14 @@ def get_field_info_and_type_annotation(annotation, value, is_path_param: bool) - return field_info, type_annotation +def get_field_info_response_type(annotation, value) -> Tuple[Optional[FieldInfo], 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) + + def get_field_info_annotated_type(annotation, value, is_path_param: bool) -> Tuple[Optional[FieldInfo], Any]: """ Get the FieldInfo and type annotation from an Annotated type. diff --git a/tests/functional/event_handler/test_openapi_params.py b/tests/functional/event_handler/test_openapi_params.py index ec31bb14236..6e4f0395aff 100644 --- a/tests/functional/event_handler/test_openapi_params.py +++ b/tests/functional/event_handler/test_openapi_params.py @@ -4,7 +4,7 @@ from pydantic import BaseModel -from aws_lambda_powertools.event_handler.api_gateway import APIGatewayRestResolver +from aws_lambda_powertools.event_handler.api_gateway import APIGatewayRestResolver, Response from aws_lambda_powertools.event_handler.openapi.models import ( Example, Parameter, @@ -153,6 +153,24 @@ def handler() -> str: assert response.schema_.type == "string" +def test_openapi_with_response_returns(): + app = APIGatewayRestResolver() + + @app.get("/") + def handler() -> Response[Annotated[str, Body(title="Response title")]]: + return Response(body="Hello, world", status_code=200) + + schema = app.get_openapi_schema() + assert len(schema.paths.keys()) == 1 + + get = schema.paths["/"].get + assert get.parameters is None + + response = get.responses[200].content[JSON_CONTENT_TYPE] + assert response.schema_.title == "Response title" + assert response.schema_.type == "string" + + def test_openapi_with_omitted_param(): app = APIGatewayRestResolver() diff --git a/tests/functional/event_handler/test_openapi_validation_middleware.py b/tests/functional/event_handler/test_openapi_validation_middleware.py index 2e14979acce..9c7ca371d54 100644 --- a/tests/functional/event_handler/test_openapi_validation_middleware.py +++ b/tests/functional/event_handler/test_openapi_validation_middleware.py @@ -6,7 +6,7 @@ from pydantic import BaseModel -from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response from aws_lambda_powertools.event_handler.openapi.params import Body from aws_lambda_powertools.shared.types import Annotated from tests.functional.utils import load_event @@ -330,3 +330,51 @@ def handler(user: Annotated[Model, Body(embed=True)]) -> Model: LOAD_GW_EVENT["body"] = json.dumps({"user": {"name": "John", "age": 30}}) result = app(LOAD_GW_EVENT, {}) assert result["statusCode"] == 200 + + +def test_validate_response_return(): + # GIVEN an APIGatewayRestResolver with validation enabled + app = APIGatewayRestResolver(enable_validation=True) + + class Model(BaseModel): + name: str + age: int + + # WHEN a handler is defined with a body parameter + @app.post("/") + def handler(user: Model) -> Response[Model]: + return Response(body=user, status_code=200) + + LOAD_GW_EVENT["httpMethod"] = "POST" + LOAD_GW_EVENT["path"] = "/" + LOAD_GW_EVENT["body"] = json.dumps({"name": "John", "age": 30}) + + # THEN the handler should be invoked and return 200 + # THEN the body must be a dict + result = app(LOAD_GW_EVENT, {}) + assert result["statusCode"] == 200 + assert result["body"] == {"name": "John", "age": 30} + + +def test_validate_response_invalid_return(): + # GIVEN an APIGatewayRestResolver with validation enabled + app = APIGatewayRestResolver(enable_validation=True) + + class Model(BaseModel): + name: str + age: int + + # WHEN a handler is defined with a body parameter + @app.post("/") + def handler(user: Model) -> Response[Model]: + return Response(body=user, status_code=200) + + LOAD_GW_EVENT["httpMethod"] = "POST" + LOAD_GW_EVENT["path"] = "/" + LOAD_GW_EVENT["body"] = json.dumps({}) + + # THEN the handler should be invoked and return 422 + # THEN the body should have the word missing + result = app(LOAD_GW_EVENT, {}) + assert result["statusCode"] == 422 + assert "missing" in result["body"] From 481905e7230f5c984c73aec9dec64835e5b2216a Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Wed, 22 Nov 2023 16:38:02 +0100 Subject: [PATCH 08/18] feat(event_handler): allow customers to catch request validation errors (#3396) --- .../event_handler/api_gateway.py | 12 +++ .../middlewares/openapi_validation.py | 73 +++++++++---------- .../event_handler/test_api_gateway.py | 46 ++++++++++++ .../test_openapi_validation_middleware.py | 4 +- 4 files changed, 93 insertions(+), 42 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 5b7262e5d55..05831a2eea5 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -32,6 +32,7 @@ from aws_lambda_powertools.event_handler import content_types from aws_lambda_powertools.event_handler.exceptions import NotFoundError, ServiceError from aws_lambda_powertools.event_handler.openapi.constants import DEFAULT_API_VERSION, DEFAULT_OPENAPI_VERSION +from aws_lambda_powertools.event_handler.openapi.exceptions import RequestValidationError from aws_lambda_powertools.event_handler.openapi.swagger_ui.html import generate_swagger_html from aws_lambda_powertools.event_handler.openapi.types import ( COMPONENT_REF_PREFIX, @@ -1972,6 +1973,17 @@ def _call_exception_handler(self, exp: Exception, route: Route) -> Optional[Resp except ServiceError as service_error: exp = service_error + if isinstance(exp, RequestValidationError): + return self._response_builder_class( + response=Response( + status_code=HTTPStatus.UNPROCESSABLE_ENTITY, + content_type=content_types.APPLICATION_JSON, + body={"statusCode": HTTPStatus.UNPROCESSABLE_ENTITY, "message": exp.errors()}, + ), + serializer=self._serializer, + route=route, + ) + if isinstance(exp, ServiceError): return self._response_builder_class( response=Response( diff --git a/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py b/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py index 131f9d267a3..34011b64384 100644 --- a/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py +++ b/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py @@ -62,50 +62,43 @@ def handler(self, app: EventHandlerInstance, next_middleware: NextMiddleware) -> values: Dict[str, Any] = {} errors: List[Any] = [] - try: - # 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"], + # 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"], + ) + + # Process query values + query_values, query_errors = _request_params_to_args( + route.dependant.query_params, + app.current_event.query_string_parameters or {}, + ) + + values.update(path_values) + values.update(query_values) + errors += path_errors + query_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) - # Process query values - query_values, query_errors = _request_params_to_args( - route.dependant.query_params, - app.current_event.query_string_parameters or {}, - ) - - values.update(path_values) - values.update(query_values) - errors += path_errors + query_errors + if errors: + # Raise the validation errors + raise RequestValidationError(_normalize_errors(errors)) + else: + # Re-write the route_args with the validated values, and call the next middleware + app.context["_route_args"] = values - # 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) + # Call the handler by calling the next middleware + response = next_middleware(app) - if errors: - # Raise the validation errors - raise RequestValidationError(_normalize_errors(errors)) - else: - # Re-write the route_args with the validated values, and call the next middleware - app.context["_route_args"] = values - - # Call the handler by calling the next middleware - response = next_middleware(app) - - # Process the response - return self._handle_response(route=route, response=response) - except RequestValidationError as e: - return Response( - status_code=422, - content_type="application/json", - body=json.dumps({"detail": e.errors()}), - ) + # Process the response + return self._handle_response(route=route, response=response) def _handle_response(self, *, route: Route, response: Response): # Process the response body if it exists diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 570de9ec808..d4c88b541aa 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -30,6 +30,7 @@ ServiceError, UnauthorizedError, ) +from aws_lambda_powertools.event_handler.openapi.exceptions import RequestValidationError from aws_lambda_powertools.shared import constants from aws_lambda_powertools.shared.cookies import Cookie from aws_lambda_powertools.shared.json_encoder import Encoder @@ -1458,6 +1459,51 @@ def get_lambda() -> Response: assert result["body"] == "Foo!" +def test_exception_handler_with_data_validation(): + # GIVEN a resolver with an exception handler defined for RequestValidationError + app = ApiGatewayResolver(enable_validation=True) + + @app.exception_handler(RequestValidationError) + def handle_validation_error(ex: RequestValidationError): + print(f"request path is '{app.current_event.path}'") + return Response( + status_code=422, + content_type=content_types.TEXT_PLAIN, + body=f"Invalid data. Number of errors: {len(ex.errors())}", + ) + + @app.get("/my/path") + def get_lambda(param: int): + ... + + # WHEN calling the event handler + # AND a RequestValidationError is raised + result = app(LOAD_GW_EVENT, {}) + + # THEN call the exception_handler + assert result["statusCode"] == 422 + assert result["multiValueHeaders"]["Content-Type"] == [content_types.TEXT_PLAIN] + assert result["body"] == "Invalid data. Number of errors: 1" + + +def test_data_validation_error(): + # GIVEN a resolver without an exception handler + app = ApiGatewayResolver(enable_validation=True) + + @app.get("/my/path") + def get_lambda(param: int): + ... + + # WHEN calling the event handler + # AND a RequestValidationError is raised + result = app(LOAD_GW_EVENT, {}) + + # THEN call the exception_handler + assert result["statusCode"] == 422 + assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] + assert "missing" in result["body"] + + def test_exception_handler_service_error(): # GIVEN app = ApiGatewayResolver() diff --git a/tests/functional/event_handler/test_openapi_validation_middleware.py b/tests/functional/event_handler/test_openapi_validation_middleware.py index 9c7ca371d54..f558bd23ced 100644 --- a/tests/functional/event_handler/test_openapi_validation_middleware.py +++ b/tests/functional/event_handler/test_openapi_validation_middleware.py @@ -343,7 +343,7 @@ class Model(BaseModel): # WHEN a handler is defined with a body parameter @app.post("/") def handler(user: Model) -> Response[Model]: - return Response(body=user, status_code=200) + return Response(body=user, status_code=200, content_type="application/json") LOAD_GW_EVENT["httpMethod"] = "POST" LOAD_GW_EVENT["path"] = "/" @@ -353,7 +353,7 @@ def handler(user: Model) -> Response[Model]: # THEN the body must be a dict result = app(LOAD_GW_EVENT, {}) assert result["statusCode"] == 200 - assert result["body"] == {"name": "John", "age": 30} + assert json.loads(result["body"]) == {"name": "John", "age": 30} def test_validate_response_invalid_return(): From 0cc687a5ed2070dcb44fa7544b44ff2e150a7418 Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Wed, 22 Nov 2023 18:04:28 +0100 Subject: [PATCH 09/18] fix(event_handler): lazy load Pydantic to improve cold start (#3397) --- aws_lambda_powertools/event_handler/openapi/compat.py | 2 +- aws_lambda_powertools/event_handler/openapi/models.py | 2 +- aws_lambda_powertools/event_handler/openapi/params.py | 3 ++- .../event_handler/openapi/pydantic_loader.py | 6 ++++++ aws_lambda_powertools/event_handler/openapi/types.py | 7 ------- tests/functional/event_handler/test_bedrock_agent.py | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 aws_lambda_powertools/event_handler/openapi/pydantic_loader.py diff --git a/aws_lambda_powertools/event_handler/openapi/compat.py b/aws_lambda_powertools/event_handler/openapi/compat.py index 54b78f7e5f6..bd102aa7b93 100644 --- a/aws_lambda_powertools/event_handler/openapi/compat.py +++ b/aws_lambda_powertools/event_handler/openapi/compat.py @@ -15,9 +15,9 @@ from pydantic import BaseModel, create_model from pydantic.fields import FieldInfo +from aws_lambda_powertools.event_handler.openapi.pydantic_loader import PYDANTIC_V2 from aws_lambda_powertools.event_handler.openapi.types import ( COMPONENT_REF_PREFIX, - PYDANTIC_V2, ModelNameMap, UnionType, ) diff --git a/aws_lambda_powertools/event_handler/openapi/models.py b/aws_lambda_powertools/event_handler/openapi/models.py index bbbc160f1e6..ab97b6dc2e7 100644 --- a/aws_lambda_powertools/event_handler/openapi/models.py +++ b/aws_lambda_powertools/event_handler/openapi/models.py @@ -4,7 +4,7 @@ from pydantic import AnyUrl, BaseModel, Field from aws_lambda_powertools.event_handler.openapi.compat import model_rebuild -from aws_lambda_powertools.event_handler.openapi.types import PYDANTIC_V2 +from aws_lambda_powertools.event_handler.openapi.pydantic_loader import PYDANTIC_V2 from aws_lambda_powertools.shared.types import Annotated, Literal """ diff --git a/aws_lambda_powertools/event_handler/openapi/params.py b/aws_lambda_powertools/event_handler/openapi/params.py index 28154466ff6..f267a4841f5 100644 --- a/aws_lambda_powertools/event_handler/openapi/params.py +++ b/aws_lambda_powertools/event_handler/openapi/params.py @@ -15,7 +15,8 @@ field_annotation_is_scalar, get_annotation_from_field_info, ) -from aws_lambda_powertools.event_handler.openapi.types import PYDANTIC_V2, CacheKey +from aws_lambda_powertools.event_handler.openapi.pydantic_loader import PYDANTIC_V2 +from aws_lambda_powertools.event_handler.openapi.types import CacheKey from aws_lambda_powertools.shared.types import Annotated, Literal, get_args, get_origin """ 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..12f06dad899 --- /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 diff --git a/aws_lambda_powertools/event_handler/openapi/types.py b/aws_lambda_powertools/event_handler/openapi/types.py index 9161d8dc170..0d166de1131 100644 --- a/aws_lambda_powertools/event_handler/openapi/types.py +++ b/aws_lambda_powertools/event_handler/openapi/types.py @@ -16,13 +16,6 @@ COMPONENT_REF_TEMPLATE = "#/components/schemas/{model}" METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"} -try: - from pydantic.version import VERSION as PYDANTIC_VERSION - - PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.") -except ImportError: - PYDANTIC_V2 = False - validation_error_definition = { "title": "ValidationError", diff --git a/tests/functional/event_handler/test_bedrock_agent.py b/tests/functional/event_handler/test_bedrock_agent.py index 223f9bdbef1..df9fb66afc8 100644 --- a/tests/functional/event_handler/test_bedrock_agent.py +++ b/tests/functional/event_handler/test_bedrock_agent.py @@ -2,7 +2,7 @@ from typing import Any, Dict from aws_lambda_powertools.event_handler import BedrockAgentResolver, Response, content_types -from aws_lambda_powertools.event_handler.openapi.types import PYDANTIC_V2 +from aws_lambda_powertools.event_handler.openapi.pydantic_loader import PYDANTIC_V2 from aws_lambda_powertools.utilities.data_classes import BedrockAgentEvent from tests.functional.utils import load_event From 9f593742d9e4a38aa051444fd1f7263ad98886fe Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Wed, 22 Nov 2023 21:39:17 +0100 Subject: [PATCH 10/18] fix(event_handler): fix format for OpenAPI path templating (#3399) --- aws_lambda_powertools/event_handler/api_gateway.py | 13 +++++++++---- .../event_handler/openapi/dependant.py | 4 ++-- .../functional/event_handler/test_openapi_params.py | 6 +++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 05831a2eea5..4263a5132a8 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -316,6 +316,11 @@ def __init__( """ self.method = method.upper() self.path = "/" if path.strip() == "" else path + + # 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) + self.rule = rule self.func = func self._middleware_stack = func @@ -435,7 +440,7 @@ 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.path, call=self.func) + self._dependant = get_dependant(path=self.openapi_path, call=self.func) return self._dependant @@ -542,7 +547,7 @@ def _openapi_operation_summary(self) -> str: Returns the OpenAPI operation summary. If the user has not provided a summary, we generate one based on the route path and method. """ - return self.summary or f"{self.method.upper()} {self.path}" + return self.summary or f"{self.method.upper()} {self.openapi_path}" def _openapi_operation_metadata(self, operation_ids: Set[str]) -> Dict[str, Any]: """ @@ -692,7 +697,7 @@ def _openapi_operation_return( return {"schema": return_schema} def _generate_operation_id(self) -> str: - operation_id = self.func.__name__ + self.path + 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 @@ -1452,7 +1457,7 @@ def get_openapi_schema( if result: path, path_definitions = result if path: - paths.setdefault(route.path, {}).update(path) + paths.setdefault(route.openapi_path, {}).update(path) if path_definitions: definitions.update(path_definitions) diff --git a/aws_lambda_powertools/event_handler/openapi/dependant.py b/aws_lambda_powertools/event_handler/openapi/dependant.py index 87e0c7dfb3d..e22eb535a7e 100644 --- a/aws_lambda_powertools/event_handler/openapi/dependant.py +++ b/aws_lambda_powertools/event_handler/openapi/dependant.py @@ -124,7 +124,7 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: def get_path_param_names(path: str) -> Set[str]: """ - Returns the path parameter names from a path template. Those are the strings between < and >. + Returns the path parameter names from a path template. Those are the strings between { and }. Parameters ---------- @@ -137,7 +137,7 @@ def get_path_param_names(path: str) -> Set[str]: The path parameter names """ - return set(re.findall("<(.*?)>", path)) + return set(re.findall("{(.*?)}", path)) def get_dependant( diff --git a/tests/functional/event_handler/test_openapi_params.py b/tests/functional/event_handler/test_openapi_params.py index 6e4f0395aff..9209cb9decd 100644 --- a/tests/functional/event_handler/test_openapi_params.py +++ b/tests/functional/event_handler/test_openapi_params.py @@ -70,13 +70,13 @@ def handler(user_id: str, include_extra: bool = False): assert schema.info.version == "0.2.2" assert len(schema.paths.keys()) == 1 - assert "/users/" in schema.paths + assert "/users/{user_id}" in schema.paths - path = schema.paths["/users/"] + path = schema.paths["/users/{user_id}"] assert path.get get = path.get - assert get.summary == "GET /users/" + assert get.summary == "GET /users/{user_id}" assert get.operationId == "handler_users__user_id__get" assert len(get.parameters) == 2 From 148ae87f5e00fb33db06172271f6ea580d010d75 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 09:19:48 +0100 Subject: [PATCH 11/18] chore(ci): changelog rebuild (#3404) Co-authored-by: Powertools for AWS Lambda (Python) bot --- CHANGELOG.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf726896cde..853f53fa523 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,16 +4,56 @@ # Unreleased +## Bug Fixes + +* **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)) + +## Features + +* **event_handler:** allow customers to catch request validation errors ([#3396](https://github.com/aws-powertools/powertools-lambda-python/issues/3396)) + +## Maintenance + +* **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-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 sentry-sdk from 1.35.0 to 1.36.0 ([#3388](https://github.com/aws-powertools/powertools-lambda-python/issues/3388)) + ## [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)) From 5a78fd5573e945057f7babf4f4aeccf7410706d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 09:22:12 +0100 Subject: [PATCH 12/18] chore(deps-dev): bump aws-cdk from 2.110.0 to 2.110.1 (#3403) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Heitor Lessa --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index db1e807ca42..ff88df2208e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,13 +11,13 @@ "package-lock.json": "^1.0.0" }, "devDependencies": { - "aws-cdk": "^2.110.0" + "aws-cdk": "^2.110.1" } }, "node_modules/aws-cdk": { - "version": "2.110.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.110.0.tgz", - "integrity": "sha512-ods6/Lh5hWv9qOMmifgg6ur/M6020Yi5mFXUolVSy/0gjzo9wFRcPAxKmQ3++Yz+rf5dadUZmmpc53evvUgR4A==", + "version": "2.110.1", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.110.1.tgz", + "integrity": "sha512-/V0FOgsvD/FkFANrYnSmyb+XK56tm2oE86pUCoEggQ2tka6Zm0z9blKZQV4euMErNSkWz4ReSAKenaqk86Fr5Q==", "dev": true, "bin": { "cdk": "bin/cdk" @@ -51,9 +51,9 @@ }, "dependencies": { "aws-cdk": { - "version": "2.110.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.110.0.tgz", - "integrity": "sha512-ods6/Lh5hWv9qOMmifgg6ur/M6020Yi5mFXUolVSy/0gjzo9wFRcPAxKmQ3++Yz+rf5dadUZmmpc53evvUgR4A==", + "version": "2.110.1", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.110.1.tgz", + "integrity": "sha512-/V0FOgsvD/FkFANrYnSmyb+XK56tm2oE86pUCoEggQ2tka6Zm0z9blKZQV4euMErNSkWz4ReSAKenaqk86Fr5Q==", "dev": true, "requires": { "fsevents": "2.3.2" diff --git a/package.json b/package.json index 13537339cce..c50593c4870 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "aws-lambda-powertools-python-e2e", "version": "1.0.0", "devDependencies": { - "aws-cdk": "^2.110.0" + "aws-cdk": "^2.110.1" }, "dependencies": { "package-lock.json": "^1.0.0" From 365c2dcb1b6847e77d4ba22b833081c633ef558e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 09:47:12 +0100 Subject: [PATCH 13/18] chore(deps): bump datadog-lambda from 4.82.0 to 5.83.0 (#3401) Bumps [datadog-lambda](https://github.com/DataDog/datadog-lambda-python) from 4.82.0 to 5.83.0. - [Release notes](https://github.com/DataDog/datadog-lambda-python/releases) - [Commits](https://github.com/DataDog/datadog-lambda-python/commits) --- updated-dependencies: - dependency-name: datadog-lambda 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: Heitor Lessa --- poetry.lock | 176 ++++++++++++++++++++++++++----------------------- pyproject.toml | 4 +- 2 files changed, 94 insertions(+), 86 deletions(-) diff --git a/poetry.lock b/poetry.lock index 19cfb5fee5d..f0cdbe42f2e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -812,18 +812,18 @@ requests = ">=2.6.0" [[package]] name = "datadog-lambda" -version = "4.82.0" +version = "5.83.0" description = "The Datadog AWS Lambda Library" optional = false python-versions = ">=3.7.0,<4" files = [ - {file = "datadog_lambda-4.82.0-py3-none-any.whl", hash = "sha256:498fd675a3131edf268f006fabe8b9e3eb2e3d2f94a95ad72a26d5c52f5a6624"}, - {file = "datadog_lambda-4.82.0.tar.gz", hash = "sha256:59918dfe20645539558ce4b7b8afcee2895ca8bf2568e594d73d360e6ea871a6"}, + {file = "datadog_lambda-5.83.0-py3-none-any.whl", hash = "sha256:87c9185c1f27b78eba9c053f434b4672199a5613d62ad37bd6c583a54c817513"}, + {file = "datadog_lambda-5.83.0.tar.gz", hash = "sha256:59209f6cdbef4a9b167d8a832368ae2384134214d5d62f6be6b0078ce0d8fffa"}, ] [package.dependencies] datadog = ">=0.41.0,<1.0.0" -ddtrace = "<2.0.0" +ddtrace = ">=2.3.0" importlib_metadata = {version = "*", markers = "python_version < \"3.8\""} typing_extensions = {version = ">=4.0,<5.0", markers = "python_version < \"3.8\""} urllib3 = [ @@ -852,94 +852,86 @@ six = "*" [[package]] name = "ddtrace" -version = "1.20.5" +version = "2.3.1" description = "Datadog APM client library" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "ddtrace-1.20.5-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:8e848f4d4efd02f887633aa6eca284a820e42b316ebbbd9ed25599f777e8090f"}, - {file = "ddtrace-1.20.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:3aff8d055aa57de51a814dc86aaee5602b5ea665e7502d60b8467b07fb5018d2"}, - {file = "ddtrace-1.20.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:ec89b97c50b2ccc27ecef9d9d084a412f174002d665140a5710928909a2d592e"}, - {file = "ddtrace-1.20.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:8216d010c7206544de46053638ec5a9ed1ac3a56908621a958aaa390d8cffb27"}, - {file = "ddtrace-1.20.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:3c16686ed141dc847ed8cd09528649cccd833d896b2d5878f8ee370f15de6de6"}, - {file = "ddtrace-1.20.5-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:834e49122cf1e75c299c0ac54886de7b776f4f292c2679d88c1ea31e866d8514"}, - {file = "ddtrace-1.20.5-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:4f28220ce9cbdd58d02a9851d41b7c2424270ffe3a08fd042299ffc3d92c96f5"}, - {file = "ddtrace-1.20.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7228037265efbd74656e65f8d3f17ea28854c280aa86db1a7adaf642d9ffaa63"}, - {file = "ddtrace-1.20.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f01a2a06974716fb516063369ab973fad38ffb33ca5ca15b3b7b128c06a2490"}, - {file = "ddtrace-1.20.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be2570b7348eb9b561fe148997af3c11a50904870503a3fe7c2b6348e87dac7f"}, - {file = "ddtrace-1.20.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6ca5b6ef30a300518e535a8291c9008d68fc1cde8fc4ed476af1b3c9377ab675"}, - {file = "ddtrace-1.20.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ecd7e3a15e73c1f86cdb6e4c7874e99023f412ba9f63e1b342ecd593313fad2f"}, - {file = "ddtrace-1.20.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86a052d8fe5cb380702bc4ee49857c2f12b019c7a3f4c78f8b2dfd43c72668ca"}, - {file = "ddtrace-1.20.5-cp310-cp310-win32.whl", hash = "sha256:3fd4d252224a74ff72b27b8371a2ac9500dc0ba08d3e009c3052ea63ad514824"}, - {file = "ddtrace-1.20.5-cp310-cp310-win_amd64.whl", hash = "sha256:90a641673b29521ec73ce572896011653cd222152b92ec744b6f162b4ccdb27f"}, - {file = "ddtrace-1.20.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:345c7a2910309547fa9b53d4386912a4cbd9c24c0a9f0b771404c7c2b82f8764"}, - {file = "ddtrace-1.20.5-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:07f97e1d94e3fe3f79d9656baa7283441c5d155adb2a0ca5d2defbdfbb12a28c"}, - {file = "ddtrace-1.20.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e3718c0804cdd2def4b80f9bd51c55b2f44dca914086f647a21356980022668"}, - {file = "ddtrace-1.20.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0acd401c52bf617474c4cac8bca0aa8cf6253d82b31c4e892b30294b397f10d"}, - {file = "ddtrace-1.20.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b36954f49c82e9e2e42615a5b70ba04dfd12d71011f4c9931effb6957c63fbd"}, - {file = "ddtrace-1.20.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:51523c55618a6b26ecdaab1c4540e3679e96723d0f883e1df7ffaa5010231cb2"}, - {file = "ddtrace-1.20.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:61d538c2c6e7a460cbdc19735093c993a62abc3381def43d5940a8facc4a280f"}, - {file = "ddtrace-1.20.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:59635b9fef2e02fefd46876f93e63eaa857503eb64aced6bcffaa81a199cfa4e"}, - {file = "ddtrace-1.20.5-cp311-cp311-win32.whl", hash = "sha256:7c4b6b121ec72736c9004c83d40d4d64155b935fb1c033390370de78af001fdb"}, - {file = "ddtrace-1.20.5-cp311-cp311-win_amd64.whl", hash = "sha256:cdaff774e88feb6d0229609ecdbaecbf50a220428bb29f863734b167d910a658"}, - {file = "ddtrace-1.20.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:ec41eff870ceba145306daab76f003a6977986dd65074b9a74f50cd0a985e431"}, - {file = "ddtrace-1.20.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:89dfaf4133ad8dcb6c63db843624e10175e9813a1f867e24d1c2a8e724088119"}, - {file = "ddtrace-1.20.5-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:7b625af8eb772bc242b850a011ba9294721f85d3f965cecdcc430fb73da1a11a"}, - {file = "ddtrace-1.20.5-cp35-cp35m-win32.whl", hash = "sha256:8cd282d38a394417e65b360ff9fb1d982bb3270ab18239f3ad3c70a1ec9cb9a3"}, - {file = "ddtrace-1.20.5-cp35-cp35m-win_amd64.whl", hash = "sha256:2d1086c5178be1763af7f30a18dd5bf6b77de855bbaa1c6c1d10e98fa7ad0931"}, - {file = "ddtrace-1.20.5-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:af4eac36813863e08cb71f72f86e9c5a060841ce66acdf2a090d831bf6886895"}, - {file = "ddtrace-1.20.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a2a15a3672331ffbdf32f8b8a387979b070510fbc2bc90e2c33c4669cd846bc"}, - {file = "ddtrace-1.20.5-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9f8071a9b4ea0be1fe62e39a68e95592742c56e336fbd0e4e99d18c39eca27"}, - {file = "ddtrace-1.20.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa49ab583816163b4cc133da234c5c2bc75d37cf459004c8319dc4026ac96c9b"}, - {file = "ddtrace-1.20.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a37cc5871b0dc6a18f19228280927d6b22d04fb30522eebcc48371481e447709"}, - {file = "ddtrace-1.20.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e1f4ad4eab735f092d9a23b80d9ffd7d33f7bb8fecba001f222155a7dc03d6ed"}, - {file = "ddtrace-1.20.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:253c75018b412a6f90c26ddfd2f265fc950751dbf76cd8529f8537b4fa4c031d"}, - {file = "ddtrace-1.20.5-cp36-cp36m-win32.whl", hash = "sha256:a61ec2a2874ab9f7228b3cf430a7048803a1ce2db22fde021c1f61ed38a1e5b4"}, - {file = "ddtrace-1.20.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2c6ff4e1118d4530b80fbf9432414e0fad38ef33bc64e39a00648b396c4c44a7"}, - {file = "ddtrace-1.20.5-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:f0eaf682acaed72427c9f05831e44050e6145ca0b227fc25a123c22d1c7d4c89"}, - {file = "ddtrace-1.20.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d952483633e637c9bb6364baf0dd3b805872b121746a7243d9de647c6f2c9417"}, - {file = "ddtrace-1.20.5-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac292a54a8b3b00d424e8c6451d7c21d247ff7e223ab6280ee541319428cd41a"}, - {file = "ddtrace-1.20.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07eae27732d63ac4e7370b575e59ff4f8c1396f8e40992feeca45425b4fce4cc"}, - {file = "ddtrace-1.20.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5678feec55fc77771eae9ff80a34037af14a140b4f6bf68a454cf86204db6401"}, - {file = "ddtrace-1.20.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1b23c154a3ca0f60b9324be0886655e05db40ea98cd33cb8111091ba9adea8f2"}, - {file = "ddtrace-1.20.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d73cb8a34112d80caadd6b088d02f9c6070264abc71733ecb537b2f9a97d6f10"}, - {file = "ddtrace-1.20.5-cp37-cp37m-win32.whl", hash = "sha256:ca8eabcd59c965086de4067475ba8e70895f81f601ce9660c5080cefd66d655c"}, - {file = "ddtrace-1.20.5-cp37-cp37m-win_amd64.whl", hash = "sha256:7ad29dcea031b6ba03f77de4ad6f85d749294fd9e8ba8af871a73811c47d1e5a"}, - {file = "ddtrace-1.20.5-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:67fea1d08dbca04eff521cc013adba1dd0d80c6e5c75a02cb4a7c42c0e973993"}, - {file = "ddtrace-1.20.5-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:79536cfa68338db9c95e728585e1f5f059e321cfcf145fce6b14e8cd1d12892c"}, - {file = "ddtrace-1.20.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c746d3546f69b7718fb61508826552a197fc981bb26c66408e1a20f7ac0952a3"}, - {file = "ddtrace-1.20.5-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8fb3aaf24760d8e4d23d01e497d15ac1b52be202e82a63140afd0c2b799e8571"}, - {file = "ddtrace-1.20.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6602c52359dbf009ef762c4110ee1ca7dfb9a163aa7bc43515c920a3ddf545f"}, - {file = "ddtrace-1.20.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d1cc510f3a4be3df3282415b045cd85dc83a3a2051d219a24546dfb9fdcb39d"}, - {file = "ddtrace-1.20.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:774d48d7e050fc4fec35317348f04326d8f781348e11ff937e739fd7ad555c6b"}, - {file = "ddtrace-1.20.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7094dd13bf2e17dac2f555a2a0ec85f89e330e6d9327122a6e16fac2b6765f34"}, - {file = "ddtrace-1.20.5-cp38-cp38-win32.whl", hash = "sha256:264cadc7caf89f45d225b1323a09c73a2706f38b754266660fc93a19b2ccb1d7"}, - {file = "ddtrace-1.20.5-cp38-cp38-win_amd64.whl", hash = "sha256:815a14133c25ed9a6deb0df043c7095107a2a900faaf458af153352bb329ccd5"}, - {file = "ddtrace-1.20.5-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:3faff82c1c0b809faa6b79ecb37a7402ca2e452241607db3ca4f8d79e30d8695"}, - {file = "ddtrace-1.20.5-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:6a94fc30efcf753cb40f37a1c8d440139ac63db6cc346ecaf3a409f12410297b"}, - {file = "ddtrace-1.20.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a43168df3b30fa228720d7c900a51a4fdaae145efaba6e6f37b678e2714f846"}, - {file = "ddtrace-1.20.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dad47a4058ed2b703843f0615411e5bd8edcaad54b0d8bc3de811252d8dd242e"}, - {file = "ddtrace-1.20.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a415b779b34d4af9880665b25ffcab8463c2e75dc82eed0228756a9fa93ed16"}, - {file = "ddtrace-1.20.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6750dcf7f84895e136452a5c03ff66d4a9e022994b961b1cf7a983fcfc65afba"}, - {file = "ddtrace-1.20.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:76e8f5f07784c8ec26a7bfd5150c359429a3423179776e075735a2e685148095"}, - {file = "ddtrace-1.20.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c4e56bd317578d34663b399b213d8b22801267b8d4faf7a1b645eb1918feaf1b"}, - {file = "ddtrace-1.20.5-cp39-cp39-win32.whl", hash = "sha256:247d8206686ef6c0b1263ce63e37a99eee5fee20ad1456bcb78edcb77519c6f5"}, - {file = "ddtrace-1.20.5-cp39-cp39-win_amd64.whl", hash = "sha256:bafc1c668b8f32a8e980f75c121c33464188270c01ef87430baa489e55fb1590"}, - {file = "ddtrace-1.20.5.tar.gz", hash = "sha256:3a15940d03a4c35d253d93b6c3acf82e0cd70d72f4ec9d8c3d0ca0d0c398dda3"}, -] - -[package.dependencies] -attrs = {version = ">=20", markers = "python_version > \"2.7\""} +python-versions = ">=3.7" +files = [ + {file = "ddtrace-2.3.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:556a046413024cf53ebb0256bbf957692a5e417599e04dac5793e659d08c398c"}, + {file = "ddtrace-2.3.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:6066f1deddb454b8e098e5a0eb53ab36d81344209fdf6bec94767358da190294"}, + {file = "ddtrace-2.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb2b950901845b966a7805ff49a9ad58dcd5e9c27b5b804079977a1309c5b4fb"}, + {file = "ddtrace-2.3.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05b0da47bc98a9802faa2557e83c096868c4ef249c3d9a43f8e5daf91d1c8e4f"}, + {file = "ddtrace-2.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0183c5178112604eb012653fd17d0947e6e2f17325f93b1e32cc6af05ceffd0"}, + {file = "ddtrace-2.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:462eb671cd78780af6e42b43f2bc451537a0d283db054c175348e9b3a1fcaff4"}, + {file = "ddtrace-2.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9b5284786a0912a9739665a33760f561423524e2d250c0b0bb2dedf6edba2da5"}, + {file = "ddtrace-2.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:dbfb1ade5725a63f21945ab8234e64e46645e98a7deb4342eddf6e86d0f9145c"}, + {file = "ddtrace-2.3.1-cp310-cp310-win32.whl", hash = "sha256:1f51732c4181e5b671a5ae3c6c786ce3b9fd2abacad2d4249d53a55564906902"}, + {file = "ddtrace-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f0ae5814fbb51b4aba4d4f4b5c1fd2110790b04d4141cf4a03291566d1d5b0f"}, + {file = "ddtrace-2.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:24f4df55fd952182efe6815748db4675540f6fb674d9838dfa680dec1fdd176f"}, + {file = "ddtrace-2.3.1-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:04b4e476f78389021b50b3ae5c4d494bbbd033a300e93253fe1f873a67611436"}, + {file = "ddtrace-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:711978dd14c0aca7eaf90587b8608c891b82e1767fc6f2be7d82b67d56c8d580"}, + {file = "ddtrace-2.3.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfa6b1b2698029b7b1f8cc351869397c33bff996159660a00ca254d9fcc5b78d"}, + {file = "ddtrace-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dd7295921009ccc61f5325cc3d30fc6182396fc8e598975b372bdf94fd16077"}, + {file = "ddtrace-2.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:94aa6a2e16d05cbb2d7a9a7553ca9b638e5b200e0d80fd027179e6af0faf59a2"}, + {file = "ddtrace-2.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:62f67040ef16149a46c8506d92a2824d7ded39427a51947a3651d572bb7a379f"}, + {file = "ddtrace-2.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:02622c4b8d5497f6367d9ccad38ac8c59d46fc3373034be114474fb01b1a28e6"}, + {file = "ddtrace-2.3.1-cp311-cp311-win32.whl", hash = "sha256:1d13ec5393802a619f922fb37a9f534911f44554bd0434dfd2d8db4e8897649e"}, + {file = "ddtrace-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:36b3427136f61d499f3fd307f97ae168a4d2728887e1922204e509a5aa72a4a3"}, + {file = "ddtrace-2.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac47d141e03c8bea3953fc5f51ac284de9ff4e6325faf2554b003ac906bc4da8"}, + {file = "ddtrace-2.3.1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:dd23e10b4cac1cf26e64d4d1ec1d6e173e609a207f5520469326f5cff6c7e462"}, + {file = "ddtrace-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a31cddf750d7a28c886c194624c6be5a4475de064489002df898731f27f3d16"}, + {file = "ddtrace-2.3.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dedd8097e58519f47f8908fe684f37c8f9722ce4b0614de78d9f39b83621dc7"}, + {file = "ddtrace-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:367aed800b78fb4d2af332c44d07d7126b1dbf758af422299f9a177811ec723d"}, + {file = "ddtrace-2.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca4dea67facdeba44040d9af8eeff96fb9a35a2b1cff93255e33a4d7250881b9"}, + {file = "ddtrace-2.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a661e133d451416741c6c2ad96baa417a1267204975bfb0d247cab748ecc3ed1"}, + {file = "ddtrace-2.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:556f60d6c9bbfc2da6d7f6751625fa3ae597c26bb8bbe74953db0d2d74f93b04"}, + {file = "ddtrace-2.3.1-cp312-cp312-win32.whl", hash = "sha256:261e20b9e9a363ec2dc728f8a009a2b1d3c9de4fbe07438b5600902a285bb179"}, + {file = "ddtrace-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:119be400024efff2f0eb66216b2aa3d2a700cd9b4a07605f7f9c94eb5e4b4cb5"}, + {file = "ddtrace-2.3.1-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:a66d0e0cccfa2fb207fc9a4d9ca6ab235a768f130129d6bb1dd256b7b3d34305"}, + {file = "ddtrace-2.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c465e43b96380f09e1e8f2d0f9cb3b78b4ef2bb211f25b57c925bb79f53cb00c"}, + {file = "ddtrace-2.3.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3eaaf8c5b63e07533822425b3402552c75adf091a1f0a6bf949725fa610c779"}, + {file = "ddtrace-2.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:600551ecd232df060203714dc1acba4809e9194fc91a7c638b68c548e92af171"}, + {file = "ddtrace-2.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:837232d708956a5d595a3618641c188a5844d663e0f77b1461f20c83f74a21c0"}, + {file = "ddtrace-2.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ddf3043581e2424fc3d4271ee00a038651a4ec9d2610eeaa2d6645095c9f4960"}, + {file = "ddtrace-2.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:63c6b28096e273431da923a8dfc0f54f7d472c3c78f0a5c4c99ed7e210b9c855"}, + {file = "ddtrace-2.3.1-cp37-cp37m-win32.whl", hash = "sha256:8b09a42cc975f798bfda9b8d8bf5c8c813022bfcf48b9e0e5e90caf4cf33ee8f"}, + {file = "ddtrace-2.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:66b49153c85423d5e99b1f364cc3b4a3ffedf35be0f3eb840f3bacd7c58100e8"}, + {file = "ddtrace-2.3.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:81f0bd1d50c8fc7d8a96e38f746ca4421fa3b52991f0df44e5e9faeb5a934c2b"}, + {file = "ddtrace-2.3.1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:37d600d582a5046f82cf77ae9247cf15cf62cf23c15739c5f23c30db2aa092c9"}, + {file = "ddtrace-2.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60a62cfa22695cb1392c617910fb389c7240fa9dae0b5792bd87ff3ae82d2c45"}, + {file = "ddtrace-2.3.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1bdf55fa4a842f9786ca30434b31bf6f877e95af86b6fb7a5a540ce592f566b7"}, + {file = "ddtrace-2.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63032c6a76173cab03c021e65c1997a12c0c571263caf00ec18b82c2293c49be"}, + {file = "ddtrace-2.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:516b830e52bc8ac2988f11a06a6c6a5296f73b119e99e8ee55a34e531389acea"}, + {file = "ddtrace-2.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:86e7764759043439c3f672f998f60bb9118fc4a6d7f603c762b125471b17f549"}, + {file = "ddtrace-2.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:87ae203dd8fa3e04f8855786ae4b2f103bc66c9f2368ee2b4e620bccdde9b34d"}, + {file = "ddtrace-2.3.1-cp38-cp38-win32.whl", hash = "sha256:f42fa2fa6f2cd9e3673a3bd7469439f5bea0ee86456706db1b50dc20b10682a6"}, + {file = "ddtrace-2.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:2a3ad8e53c45c3329f939fe921714dfe76f5737e48f5b37a5422b1573a20ce44"}, + {file = "ddtrace-2.3.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:5adff6a5d60239e64062ad5efb72631c47c7fb8310ebea6d817f0208a7585074"}, + {file = "ddtrace-2.3.1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:84012bc7d27dd3c4cd591bbaf0a0cc0413ebc6c838637ca5a76bacb354e2518f"}, + {file = "ddtrace-2.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc2596b26701c9e3a362195f79ddcf54b491a8ea13277ed16697da9ad943646"}, + {file = "ddtrace-2.3.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:986113f7eb4d8a8e87216b55e6cc40b578f84a5730241822af3f34cc61e42710"}, + {file = "ddtrace-2.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd0cdbc6d81e556b6af0875b0bb2ac77d3cf0a0c5da8faa014da1936e1e0adc2"}, + {file = "ddtrace-2.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9e8eb17ef8ca2fc9464216290969cff3bbf8df00860ebb219328804125b43bd1"}, + {file = "ddtrace-2.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fc21e46c5e9d077022b7634ae247d15d2318cbb347f6756607dfd64ff5941797"}, + {file = "ddtrace-2.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:51bf7e3e5c80ef0daadd22c26e7c24c90fc4b4a7662dec1a3d9d8e0db68f3c09"}, + {file = "ddtrace-2.3.1-cp39-cp39-win32.whl", hash = "sha256:2a5f040c0eb101f82a9cd8b8b0279e8583bb0a62fd39b879197d53b71a5d6dbe"}, + {file = "ddtrace-2.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:ff708683becb18771cb31ae5fb5d1430ac5031a082106e0dabac46a1fd6f832e"}, + {file = "ddtrace-2.3.1.tar.gz", hash = "sha256:273a0e98f93e7231708b30067768d80df9bc93a505de93500f30c6da24b70a7b"}, +] + +[package.dependencies] +attrs = ">=20" bytecode = [ {version = ">=0.13.0,<0.14.0", markers = "python_version == \"3.7\""}, {version = "*", markers = "python_version >= \"3.8\""}, ] -cattrs = {version = "*", markers = "python_version >= \"3.7\""} +cattrs = "*" ddsketch = ">=2.0.1" envier = "*" importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -opentelemetry-api = {version = ">=1", markers = "python_version >= \"3.7\""} -protobuf = {version = ">=3", markers = "python_version >= \"3.7\""} +opentelemetry-api = ">=1" +protobuf = ">=3" +setuptools = {version = "*", markers = "python_version >= \"3.12\""} six = ">=1.12.0" typing-extensions = "*" xmltodict = ">=0.12" @@ -2815,6 +2807,22 @@ starlette = ["starlette (>=0.19.1)"] starlite = ["starlite (>=1.48)"] tornado = ["tornado (>=5)"] +[[package]] +name = "setuptools" +version = "69.0.2" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"}, + {file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + [[package]] name = "six" version = "1.16.0" @@ -3224,4 +3232,4 @@ validation = ["fastjsonschema"] [metadata] lock-version = "2.0" python-versions = "^3.7.4" -content-hash = "7ee9fe25b426b28a8615bb8017382362d5652be862adfd4e2fffb632bb929b27" +content-hash = "dcf229fefcf9f9dcfa230a8fb81d6e9348d16fa12cc01120c64f47e26465a899" diff --git a/pyproject.toml b/pyproject.toml index 7bbfbe64cab..2c84e1f575c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ fastjsonschema = { version = "^2.14.5", optional = true } pydantic = { version = "^1.8.2", optional = true } boto3 = { version = "^1.20.32", optional = true } typing-extensions = "^4.6.2" -datadog-lambda = { version = "^4.77.0", optional = true } +datadog-lambda = { version = ">=4.77,<6.0", optional = true } aws-encryption-sdk = { version = "^3.1.1", optional = true } [tool.poetry.dev-dependencies] @@ -88,7 +88,7 @@ ijson = "^3.2.2" typed-ast = { version = "^1.5.5", python = "< 3.8"} hvac = "^1.2.1" aws-requests-auth = "^0.4.3" -datadog-lambda = "^4.82.0" +datadog-lambda = "^5.83.0" [tool.poetry.extras] parser = ["pydantic"] From 716ff9a11dfb14d612ad8eecc34a3734dc2ed534 Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Thu, 23 Nov 2023 11:49:17 +0100 Subject: [PATCH 14/18] fix(event_handler): hide error details by default (#3406) fix(event_handler): hide error details by default --- aws_lambda_powertools/event_handler/api_gateway.py | 5 ++++- tests/functional/event_handler/test_bedrock_agent.py | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 4263a5132a8..2231bc1b400 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -1979,11 +1979,14 @@ def _call_exception_handler(self, exp: Exception, route: Route) -> Optional[Resp 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, "message": exp.errors()}, + body={"statusCode": HTTPStatus.UNPROCESSABLE_ENTITY, "detail": errors}, ), serializer=self._serializer, route=route, diff --git a/tests/functional/event_handler/test_bedrock_agent.py b/tests/functional/event_handler/test_bedrock_agent.py index df9fb66afc8..266edd10de0 100644 --- a/tests/functional/event_handler/test_bedrock_agent.py +++ b/tests/functional/event_handler/test_bedrock_agent.py @@ -121,11 +121,11 @@ def claims() -> Dict[str, Any]: assert result["response"]["httpMethod"] == "GET" assert result["response"]["httpStatusCode"] == 422 - body = result["response"]["responseBody"]["application/json"]["body"] + body = json.loads(result["response"]["responseBody"]["application/json"]["body"]) if PYDANTIC_V2: - assert "should be a valid dictionary" in body + assert body["detail"][0]["type"] == "dict_type" else: - assert "value is not a valid dict" in body + assert body["detail"][0]["type"] == "type_error.dict" def test_bedrock_agent_event_with_exception(): From 334a30b7b2dddf612f8fa625f04a8a7710be3ac5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 12:19:19 +0100 Subject: [PATCH 15/18] chore(deps-dev): bump aws-cdk-lib from 2.110.0 to 2.110.1 (#3402) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Heitor Lessa --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index f0cdbe42f2e..1d6395a73a2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -149,13 +149,13 @@ typeguard = ">=2.13.3,<2.14.0" [[package]] name = "aws-cdk-lib" -version = "2.110.0" +version = "2.110.1" description = "Version 2 of the AWS Cloud Development Kit library" optional = false python-versions = "~=3.7" files = [ - {file = "aws-cdk-lib-2.110.0.tar.gz", hash = "sha256:2f6650e8d365fb2b143e65cf22d91de45c090636d8a2f1ac68efc302187780f7"}, - {file = "aws_cdk_lib-2.110.0-py3-none-any.whl", hash = "sha256:80a8eac6dcc2dd38496d9296efb1d90b45051dd1748743555bf69cfe83b1aa0a"}, + {file = "aws-cdk-lib-2.110.1.tar.gz", hash = "sha256:f9780664b70e11aa886ef42fdb4e45dab180721e42eb8a4575617573a8e46ed0"}, + {file = "aws_cdk_lib-2.110.1-py3-none-any.whl", hash = "sha256:63f234360832f08ae7a767fa1e3f6775ceeef0b8f9a75aa9ec7b79642c1fee21"}, ] [package.dependencies] @@ -3232,4 +3232,4 @@ validation = ["fastjsonschema"] [metadata] lock-version = "2.0" python-versions = "^3.7.4" -content-hash = "dcf229fefcf9f9dcfa230a8fb81d6e9348d16fa12cc01120c64f47e26465a899" +content-hash = "5dd3fc30f5319458ede1e5abd35d67788e0d8471009ce52f123648756d198fe3" diff --git a/pyproject.toml b/pyproject.toml index 2c84e1f575c..18023bd5f98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ xenon = "^0.9.1" mkdocs-git-revision-date-plugin = "^0.3.2" mike = "^1.1.2" pytest-xdist = "^3.5.0" -aws-cdk-lib = "^2.110.0" +aws-cdk-lib = "^2.110.1" "aws-cdk.aws-apigatewayv2-alpha" = "^2.38.1-alpha.0" "aws-cdk.aws-apigatewayv2-integrations-alpha" = "^2.38.1-alpha.0" "aws-cdk.aws-apigatewayv2-authorizers-alpha" = "^2.38.1-alpha.0" From 89a92b7fff2d3a632b879ef450dee97398635a28 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 13:50:34 +0100 Subject: [PATCH 16/18] chore(deps-dev): bump the boto-typing group with 1 update (#3400) 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 1d6395a73a2..7720087d2e6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1918,13 +1918,13 @@ typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-s3" -version = "1.29.3" -description = "Type annotations for boto3.S3 1.29.3 service generated with mypy-boto3-builder 7.20.3" +version = "1.29.5" +description = "Type annotations for boto3.S3 1.29.5 service generated with mypy-boto3-builder 7.20.3" optional = false python-versions = ">=3.7" files = [ - {file = "mypy-boto3-s3-1.29.3.tar.gz", hash = "sha256:683c427ed722cee649443afb1dc65fc00871dac0812caa9d1947d0e3d8a03a37"}, - {file = "mypy_boto3_s3-1.29.3-py3-none-any.whl", hash = "sha256:d951dd748d2717de0a9b642231fe5069e503093ad6cfb7fa5976037e8c0e95de"}, + {file = "mypy-boto3-s3-1.29.5.tar.gz", hash = "sha256:82c9df70b6cfa5e1c3e208a63aaa6edda4fc80696c8718fda4e6ed5bb6501ad3"}, + {file = "mypy_boto3_s3-1.29.5-py3-none-any.whl", hash = "sha256:ce727a57dc1619bad0d4527ec6d74fecf3d2d05f6fef99e884e4a9e7485a1a18"}, ] [package.dependencies] @@ -3232,4 +3232,4 @@ validation = ["fastjsonschema"] [metadata] lock-version = "2.0" python-versions = "^3.7.4" -content-hash = "5dd3fc30f5319458ede1e5abd35d67788e0d8471009ce52f123648756d198fe3" +content-hash = "dbb0e0d3c1ea94802effb1b24095eef86e300a0c28d139818985b38799fd70f9" diff --git a/pyproject.toml b/pyproject.toml index 18023bd5f98..e5ca5eca9e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ mypy-boto3-lambda = "^1.29.2" mypy-boto3-logs = "^1.29.0" mypy-boto3-secretsmanager = "^1.29.0" mypy-boto3-ssm = "^1.29.2" -mypy-boto3-s3 = "^1.29.3" +mypy-boto3-s3 = "^1.29.5" mypy-boto3-xray = "^1.29.0" types-requests = "^2.31.0" typing-extensions = "^4.6.2" From 765781cb16338a72c5c27d292bfbc5ba6f68521b Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Thu, 23 Nov 2023 14:47:41 +0100 Subject: [PATCH 17/18] docs(event_handlers): new data validation and OpenAPI feature (#3386) Co-authored-by: heitorlessa --- .../event_handler/openapi/params.py | 174 ++++++++++ docs/core/event_handler/api_gateway.md | 327 +++++++++++++++++- docs/media/swagger.png | Bin 0 -> 54463 bytes .../sam/template.yaml | 4 +- examples/event_handler_rest/sam/template.yaml | 18 + .../src/customizing_api_metadata.py | 33 ++ .../src/customizing_api_operations.py | 30 ++ .../src/customizing_swagger.py | 29 ++ .../src/customizing_swagger_middlewares.py | 40 +++ .../src/data_validation.json | 36 ++ .../event_handler_rest/src/data_validation.py | 35 ++ .../src/data_validation_error.json | 42 +++ ...a_validation_error_unsanitized_output.json | 9 + .../src/data_validation_output.json | 10 + .../src/data_validation_sanitized_error.py | 46 +++ ...ata_validation_sanitized_error_output.json | 9 + .../src/enabling_swagger.py | 40 +++ .../src/skip_validating_query_strings.py | 40 +++ .../event_handler_rest/src/validating_path.py | 37 ++ .../src/validating_payload_subset.json | 36 ++ .../src/validating_payload_subset.py | 30 ++ .../src/validating_payload_subset_output.json | 10 + .../src/validating_payloads.json | 36 ++ .../src/validating_payloads.py | 43 +++ .../src/validating_payloads_output.json | 10 + .../src/validating_query_strings.py | 42 +++ 26 files changed, 1162 insertions(+), 4 deletions(-) create mode 100644 docs/media/swagger.png create mode 100644 examples/event_handler_rest/src/customizing_api_metadata.py create mode 100644 examples/event_handler_rest/src/customizing_api_operations.py create mode 100644 examples/event_handler_rest/src/customizing_swagger.py create mode 100644 examples/event_handler_rest/src/customizing_swagger_middlewares.py create mode 100644 examples/event_handler_rest/src/data_validation.json create mode 100644 examples/event_handler_rest/src/data_validation.py create mode 100644 examples/event_handler_rest/src/data_validation_error.json create mode 100644 examples/event_handler_rest/src/data_validation_error_unsanitized_output.json create mode 100644 examples/event_handler_rest/src/data_validation_output.json create mode 100644 examples/event_handler_rest/src/data_validation_sanitized_error.py create mode 100644 examples/event_handler_rest/src/data_validation_sanitized_error_output.json create mode 100644 examples/event_handler_rest/src/enabling_swagger.py create mode 100644 examples/event_handler_rest/src/skip_validating_query_strings.py create mode 100644 examples/event_handler_rest/src/validating_path.py create mode 100644 examples/event_handler_rest/src/validating_payload_subset.json create mode 100644 examples/event_handler_rest/src/validating_payload_subset.py create mode 100644 examples/event_handler_rest/src/validating_payload_subset_output.json create mode 100644 examples/event_handler_rest/src/validating_payloads.json create mode 100644 examples/event_handler_rest/src/validating_payloads.py create mode 100644 examples/event_handler_rest/src/validating_payloads_output.json create mode 100644 examples/event_handler_rest/src/validating_query_strings.py diff --git a/aws_lambda_powertools/event_handler/openapi/params.py b/aws_lambda_powertools/event_handler/openapi/params.py index f267a4841f5..bd542ba7932 100644 --- a/aws_lambda_powertools/event_handler/openapi/params.py +++ b/aws_lambda_powertools/event_handler/openapi/params.py @@ -117,6 +117,64 @@ def __init__( json_schema_extra: Union[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 @@ -207,6 +265,64 @@ def __init__( json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): + """ + Constructs a new Path param. + + Parameters + ---------- + default: Any + The default value of the parameter + default_factory: Callable[[], Any], optional + Callable that will be called when a default value is needed for this field + annotation: Any, optional + The type annotation of the parameter + alias: str, optional + The public name of the field + alias_priority: int, optional + Priority of the alias. This affects whether an alias generator is used + validation_alias: str | AliasPath | AliasChoices | None, optional + Alias to be used for validation only + serialization_alias: str | AliasPath | AliasChoices | None, optional + Alias to be used for serialization only + title: str, optional + The title of the parameter + description: str, optional + The description of the parameter + gt: float, optional + Only applies to numbers, required the field to be "greater than" + ge: float, optional + Only applies to numbers, required the field to be "greater than or equal" + lt: float, optional + Only applies to numbers, required the field to be "less than" + le: float, optional + Only applies to numbers, required the field to be "less than or equal" + min_length: int, optional + Only applies to strings, required the field to have a minimum length + max_length: int, optional + Only applies to strings, required the field to have a maximum length + pattern: str, optional + Only applies to strings, requires the field match against a regular expression pattern string + discriminator: str, optional + Parameter field name for discriminating the type in a tagged union + strict: bool, optional + Enables Pydantic's strict mode for the field + multiple_of: float, optional + Only applies to numbers, requires the field to be a multiple of the given value + allow_inf_nan: bool, optional + Only applies to numbers, requires the field to allow infinity and NaN values + max_digits: int, optional + Only applies to Decimals, requires the field to have a maxmium number of digits within the decimal. + decimal_places: int, optional + Only applies to Decimals, requires the field to have at most a number of decimal places + examples: List[Any], optional + A list of examples for the parameter + deprecated: bool, optional + If `True`, the parameter will be marked as deprecated + include_in_schema: bool, optional + If `False`, the parameter will be excluded from the generated OpenAPI schema + json_schema_extra: Dict[str, Any], optional + Extra values to include in the generated OpenAPI schema + """ if default is not ...: raise AssertionError("Path parameters cannot have a default value") @@ -279,6 +395,64 @@ def __init__( json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): + """ + Constructs a new Query param. + + Parameters + ---------- + default: Any + The default value of the parameter + default_factory: Callable[[], Any], optional + Callable that will be called when a default value is needed for this field + annotation: Any, optional + The type annotation of the parameter + alias: str, optional + The public name of the field + alias_priority: int, optional + Priority of the alias. This affects whether an alias generator is used + validation_alias: str | AliasPath | AliasChoices | None, optional + Alias to be used for validation only + serialization_alias: str | AliasPath | AliasChoices | None, optional + Alias to be used for serialization only + title: str, optional + The title of the parameter + description: str, optional + The description of the parameter + gt: float, optional + Only applies to numbers, required the field to be "greater than" + ge: float, optional + Only applies to numbers, required the field to be "greater than or equal" + lt: float, optional + Only applies to numbers, required the field to be "less than" + le: float, optional + Only applies to numbers, required the field to be "less than or equal" + min_length: int, optional + Only applies to strings, required the field to have a minimum length + max_length: int, optional + Only applies to strings, required the field to have a maximum length + pattern: str, optional + Only applies to strings, requires the field match against a regular expression pattern string + discriminator: str, optional + Parameter field name for discriminating the type in a tagged union + strict: bool, optional + Enables Pydantic's strict mode for the field + multiple_of: float, optional + Only applies to numbers, requires the field to be a multiple of the given value + allow_inf_nan: bool, optional + Only applies to numbers, requires the field to allow infinity and NaN values + max_digits: int, optional + Only applies to Decimals, requires the field to have a maxmium number of digits within the decimal. + decimal_places: int, optional + Only applies to Decimals, requires the field to have at most a number of decimal places + examples: List[Any], optional + A list of examples for the parameter + deprecated: bool, optional + If `True`, the parameter will be marked as deprecated + include_in_schema: bool, optional + If `False`, the parameter will be excluded from the generated OpenAPI schema + json_schema_extra: Dict[str, Any], optional + Extra values to include in the generated OpenAPI schema + """ super().__init__( default=default, default_factory=default_factory, diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 6868ce25d46..005ac3a4b7b 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -11,12 +11,21 @@ Event handler for Amazon API Gateway REST and HTTP APIs, Application Loader Bala * Support for CORS, binary and Gzip compression, Decimals JSON encoding and bring your own JSON serializer * Built-in integration with [Event Source Data Classes utilities](../../utilities/data_classes.md){target="_blank"} for self-documented event schema * Works with micro function (one or a few routes) and monolithic functions (all routes) +* Support for OpenAPI and data validation for requests/responses ## Getting started ???+ tip All examples shared in this documentation are available within the [project repository](https://github.com/aws-powertools/powertools-lambda-python/tree/develop/examples){target="_blank"}. +### Install + +!!! info "This is not necessary if you're installing Powertools for AWS Lambda (Python) via [Lambda Layer/SAR](../../index.md#lambda-layer){target="_blank"}." + +**When using the data validation feature**, you need to add `pydantic` as a dependency in your preferred tool _e.g., requirements.txt, pyproject.toml_. + +As of now, both Pydantic V1 and V2 are supported. For a future major version, we will only support Pydantic V2. + ### Required resources @@ -221,6 +230,190 @@ If you need to accept multiple HTTP methods in a single function, you can use th ???+ note It is generally better to have separate functions for each HTTP method, as the functionality tends to differ depending on which method is used. +### Data validation + +!!! note "This changes the authoring experience by relying on Python's type annotations" + It's inspired by [FastAPI framework](https://fastapi.tiangolo.com/){target="_blank" rel="nofollow"} for ergonomics and to ease migrations in either direction. We support both Pydantic models and Python's dataclass. + + For brevity, we'll focus on Pydantic only. + +All resolvers can optionally coerce and validate incoming requests by setting `enable_validation=True`. + +With this feature, we can now express how we expect our incoming data and response to look like. This moves data validation responsibilities to Event Handler resolvers, reducing a ton of boilerplate code. + +Let's rewrite the previous examples to signal our resolver what shape we expect our data to be. + + + +=== "data_validation.py" + + ```python hl_lines="13 16 25 29" + --8<-- "examples/event_handler_rest/src/data_validation.py" + ``` + + 1. This enforces data validation at runtime. Any validation error will return `HTTP 422: Unprocessable Entity error`. + 2. We create a Pydantic model to define how our data looks like. + 3. Defining a route remains exactly as before. + 4. By default, URL Paths will be `str`. Here, we are telling our resolver it should be `int`, so it converts it for us.

Lastly, we're also saying the return should be our `Todo`. This will help us later when we touch OpenAPI auto-documentation. + 5. `todo.json()` returns a dictionary. However, Event Handler knows the response should be `Todo` so it converts and validates accordingly. + +=== "data_validation.json" + + ```json hl_lines="4" + --8<-- "examples/event_handler_rest/src/data_validation.json" + ``` + +=== "data_validation_output.json" + + ```json hl_lines="2-3" + --8<-- "examples/event_handler_rest/src/data_validation_output.json" + ``` + + + +#### Handling validation errors + +!!! info "By default, we hide extended error details for security reasons _(e.g., pydantic url, Pydantic code)_." + +Any incoming request that fails validation will lead to a `HTTP 422: Unprocessable Entity error` response that will look similar to this: + +```json hl_lines="2 3" title="data_validation_error_unsanitized_output.json" +--8<-- "examples/event_handler_rest/src/data_validation_error_unsanitized_output.json" +``` + +You can customize the error message by catching the `RequestValidationError` exception. This is useful when you might have a security policy to return opaque validation errors, or have a company standard for API validation errors. + +Here's an example where we catch validation errors, log all details for further investigation, and return the same `HTTP 422` with an opaque error. + +=== "data_validation_sanitized_error.py" + + Note that Pydantic versions [1](https://docs.pydantic.dev/1.10/usage/models/#error-handling){target="_blank" rel="nofollow"} and [2](https://docs.pydantic.dev/latest/errors/errors/){target="_blank" rel="nofollow"} report validation detailed errors differently. + + ```python hl_lines="8 24-25 31" + --8<-- "examples/event_handler_rest/src/data_validation_sanitized_error.py" + ``` + + 1. We use [exception handler](#exception-handling) decorator to catch **any** request validation errors.

Then, we log the detailed reason as to why it failed while returning a custom `Response` object to hide that from them. + +=== "data_validation_sanitized_error_output.json" + + ```json hl_lines="2 3" + --8<-- "examples/event_handler_rest/src/data_validation_sanitized_error_output.json" + ``` + +#### Validating payloads + +!!! info "We will automatically validate, inject, and convert incoming request payloads based on models via type annotation." + +Let's improve our previous example by handling the creation of todo items via `HTTP POST`. + +What we want is for Event Handler to convert the incoming payload as an instance of our `Todo` model. We handle the creation of that `todo`, and then return the `ID` of the newly created `todo`. + +Even better, we can also let Event Handler validate and convert our response according to type annotations, further reducing boilerplate. + +=== "validating_payloads.py" + + ```python hl_lines="13 16 24 33" + --8<-- "examples/event_handler_rest/src/validating_payloads.py" + ``` + + 1. This enforces data validation at runtime. Any validation error will return `HTTP 422: Unprocessable Entity error`. + 2. We create a Pydantic model to define how our data looks like. + 3. We define `Todo` as our type annotation. Event Handler then uses this model to validate and inject the incoming request as `Todo`. + 4. Lastly, we return the ID of our newly created `todo` item.

Because we specify the return type (`str`), Event Handler will take care of serializing this as a JSON string. + 5. Note that the return type is `List[Todo]`.

Event Handler will take the return (`todo.json`), and validate each list item against `Todo` model before returning the response accordingly. + +=== "validating_payloads.json" + + ```json hl_lines="3 5-6" + --8<-- "examples/event_handler_rest/src/validating_payloads.json" + ``` + +=== "validating_payloads_output.json" + + ```json hl_lines="3" + --8<-- "examples/event_handler_rest/src/validating_payloads_output.json" + ``` + +##### Validating payload subset + +With the addition of the [`Annotated` type starting in Python 3.9](https://docs.python.org/3/library/typing.html#typing.Annotated){target="_blank" rel="nofollow"}, types can contain additional metadata, allowing us to represent anything we want. + +We use the `Annotated` and OpenAPI `Body` type to instruct Event Handler that our payload is located in a particular JSON key. + +!!! note "Event Handler will match the parameter name with the JSON key to validate and inject what you want." + +=== "validating_payload_subset.py" + + ```python hl_lines="7 8 22" + --8<-- "examples/event_handler_rest/src/validating_payload_subset.py" + ``` + + 1. `Body` is a special OpenAPI type that can add additional constraints to a request payload. + 2. `Body(embed=True)` instructs Event Handler to look up inside the payload for a key.

This means Event Handler will look up for a key named `todo`, validate the value against `Todo`, and inject it. + +=== "validating_payload_subset.json" + + ```json hl_lines="3-4 6" + --8<-- "examples/event_handler_rest/src/validating_payload_subset.json" + ``` + +=== "validating_payload_subset_output.json" + + ```json hl_lines="3" + --8<-- "examples/event_handler_rest/src/validating_payload_subset_output.json" + ``` + +#### Validating query strings + +!!! info "We will automatically validate and inject incoming query strings via type annotation." + +We use the `Annotated` type to tell Event Handler that a particular parameter is not only an optional string, but also a query string with constraints. + +In the following example, we use a new `Query` OpenAPI type to add [one out of many possible constraints](#customizing-openapi-parameters), which should read as: + +* `completed` is a query string with a `None` as its default value +* `completed`, when set, should have at minimum 4 characters +* Doesn't match? Event Handler will return a validation error response + + + +=== "validating_query_strings.py" + + ```python hl_lines="8 10 27" + --8<-- "examples/event_handler_rest/src/validating_query_strings.py" + ``` + + 1. If you're not using Python 3.9 or higher, you can install and use [`typing_extensions`](https://pypi.org/project/typing-extensions/){target="_blank" rel="nofollow"} to the same effect + 2. `Query` is a special OpenAPI type that can add constraints to a query string as well as document them + 3. **First time seeing the `Annotated`?**

This special type uses the first argument as the actual type, and subsequent arguments are metadata.

At runtime, static checkers will also see the first argument, but anyone receiving them could inspect them to fetch their metadata. + +=== "skip_validating_query_strings.py" + + If you don't want to validate query strings but simply let Event Handler inject them as parameters, you can omit `Query` type annotation. + + This is merely for your convenience. + + ```python hl_lines="25" + --8<-- "examples/event_handler_rest/src/skip_validating_query_strings.py" + ``` + + 1. `completed` is still the same query string as before, except we simply state it's an string. No `Query` or `Annotated` to validate it. + + + +#### Validating path parameters + +Just like we learned in [query string validation](#validating-query-strings), we can use a new `Path` OpenAPI type to [add constraints](#customizing-openapi-parameters). + +For example, we could validate that `` dynamic path should be no greater than three digits. + +```python hl_lines="8 10 27" title="validating_path.py" +--8<-- "examples/event_handler_rest/src/validating_path.py" +``` + +1. `Path` is a special OpenAPI type that allows us to constrain todo_id to be less than 999. + ### Accessing request details Event Handler integrates with [Event Source Data Classes utilities](../../utilities/data_classes.md){target="_blank"}, and it exposes their respective resolver request details and convenient methods under `app.current_event`. @@ -279,6 +472,30 @@ We provide pre-defined errors for the most popular ones such as HTTP 400, 401, 4 --8<-- "examples/event_handler_rest/src/raising_http_errors.py" ``` +### Enabling SwaggerUI + +!!! note "This feature requires [data validation](#data-validation) feature to be enabled." + +Behind the scenes, the [data validation](#data-validation) feature auto-generates an OpenAPI specification from your routes and type annotations. You can use [Swagger UI](https://swagger.io/tools/swagger-ui/){target="_blank" rel="nofollow"} to visualize and interact with your newly auto-documented API. + +There are some important **caveats** that you should know before enabling it: + +| Caveat | Description | +| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Swagger UI is **publicly accessible by default** | When using `enable_swagger` method, you can [protect sensitive API endpoints by implementing a custom middleware](#customizing-swagger-ui) using your preferred authorization mechanism. | +| **No micro-functions support** yet | Swagger UI is enabled on a per resolver instance which will limit its accuracy here. | +| You need to expose **new routes** | You'll need to expose the following paths to Lambda: `/swagger`, `/swagger.css`, `/swagger.js`; ignore if you're routing all paths already. | + +```python hl_lines="12-13" title="enabling_swagger.py" +--8<-- "examples/event_handler_rest/src/enabling_swagger.py" +``` + +1. `enable_swagger` creates a route to serve Swagger UI and allows quick customizations.

You can also include middlewares to protect or enhance the overall experience. + +Here's an example of what it looks like by default: + +![Swagger UI picture](../../media/swagger.png) + ### Custom Domain API Mappings When using [Custom Domain API Mappings feature](https://docs.aws.amazon.com/apigateway/latest/developerguide/rest-api-mappings.html){target="_blank"}, you must use **`strip_prefixes`** param in the `APIGatewayRestResolver` constructor. @@ -573,8 +790,8 @@ As a practical example, let's refactor our correlation ID middleware so it accep These are native middlewares that may become native features depending on customer demand. -| Middleware | Purpose | -| ------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| Middleware | Purpose | +| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | [SchemaValidationMiddleware](/lambda/python/latest/api/event_handler/middlewares/schema_validation.html){target="_blank"} | Validates API request body and response against JSON Schema, using [Validation utility](../../utilities/validation.md){target="_blank"} | #### Being a good citizen @@ -696,6 +913,112 @@ This will enable full tracebacks errors in the response, print request and respo --8<-- "examples/event_handler_rest/src/debug_mode.py" ``` +### OpenAPI + +When you enable [Data Validation](#data-validation), we use a combination of Pydantic Models and [OpenAPI](https://www.openapis.org/){target="_blank"} type annotations to add constraints to your API's parameters. + +In OpenAPI documentation tools like [SwaggerUI](#enabling-swaggerui), these annotations become readable descriptions, offering a self-explanatory API interface. This reduces boilerplate code while improving functionality and enabling auto-documentation. + +???+ note + We don't have support for files, form data, and header parameters at the moment. If you're interested in this, please [open an issue](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=feature-request%2Ctriage&projects=&template=feature_request.yml&title=Feature+request%3A+TITLE). + +#### Customizing OpenAPI parameters + +Whenever you use OpenAPI parameters to validate [query strings](#validating-query-strings) or [path parameters](#validating-path-parameters), you can enhance validation and OpenAPI documentation by using any of these parameters: + +| Field name | Type | Description | +| --------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `alias` | `str` | Alternative name for a field, used when serializing and deserializing data | +| `validation_alias` | `str` | Alternative name for a field during validation (but not serialization) | +| `serialization_alias` | `str` | Alternative name for a field during serialization (but not during validation) | +| `description` | `str` | Human-readable description | +| `gt` | `float` | Greater than. If set, value must be greater than this. Only applicable to numbers | +| `ge` | `float` | Greater than or equal. If set, value must be greater than or equal to this. Only applicable to numbers | +| `lt` | `float` | Less than. If set, value must be less than this. Only applicable to numbers | +| `le` | `float` | Less than or equal. If set, value must be less than or equal to this. Only applicable to numbers | +| `min_length` | `int` | Minimum length for strings | +| `max_length` | `int` | Maximum length for strings | +| `pattern` | `string` | A regular expression that the string must match. | +| `strict` | `bool` | If `True`, strict validation is applied to the field. See [Strict Mode](https://docs.pydantic.dev/latest/concepts/strict_mode/){target"_blank" rel="nofollow"} for details | +| `multiple_of` | `float` | Value must be a multiple of this. Only applicable to numbers | +| `allow_inf_nan` | `bool` | Allow `inf`, `-inf`, `nan`. Only applicable to numbers | +| `max_digits` | `int` | Maximum number of allow digits for strings | +| `decimal_places` | `int` | Maximum number of decimal places allowed for numbers | +| `examples` | `List\[Any\]` | List of examples of the field | +| `deprecated` | `bool` | Marks the field as deprecated | +| `include_in_schema` | `bool` | If `False` the field will not be part of the exported OpenAPI schema | +| `json_schema_extra` | `JsonDict` | Any additional JSON schema data for the schema property | + +#### Customizing API operations + +Customize your API endpoints by adding metadata to endpoint definitions. This provides descriptive documentation for API consumers and gives extra instructions to the framework. + +Here's a breakdown of various customizable fields: + +| Field Name | Type | Description | +| ---------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `summary` | `str` | A concise overview of the main functionality of the endpoint. This brief introduction is usually displayed in autogenerated API documentation and helps consumers quickly understand what the endpoint does. | +| `description` | `str` | A more detailed explanation of the endpoint, which can include information about the operation's behavior, including side effects, error states, and other operational guidelines. | +| `responses` | `Dict[int, Dict[str, Any]]` | A dictionary that maps each HTTP status code to a Response Object as defined by the [OpenAPI Specification](https://swagger.io/specification/#response-object). This allows you to describe expected responses, including default or error messages, and their corresponding schemas for different status codes. | +| `response_description` | `str` | Provides the default textual description of the response sent by the endpoint when the operation is successful. It is intended to give a human-readable understanding of the result. | +| `tags` | `List[str]` | Tags are a way to categorize and group endpoints within the API documentation. They can help organize the operations by resources or other heuristic. | +| `operation_id` | `str` | A unique identifier for the operation, which can be used for referencing this operation in documentation or code. This ID must be unique across all operations described in the API. | +| `include_in_schema` | `bool` | A boolean value that determines whether or not this operation should be included in the OpenAPI schema. Setting it to `False` can hide the endpoint from generated documentation and schema exports, which might be useful for private or experimental endpoints. | + +To implement these customizations, include extra parameters when defining your routes: + +```python hl_lines="11-20" title="customizing_api_operations.py" +--8<-- "examples/event_handler_rest/src/customizing_api_operations.py" +``` + +#### Customizing Swagger UI + +???+note "Customizing the Swagger metadata" + The `enable_swagger` method accepts the same metadata as described at [Customizing OpenAPI metadata](#customizing-openapi-metadata). + +The Swagger UI appears by default at the `/swagger` path, but you can customize this to serve the documentation from another path and specify the source for Swagger UI assets. + +Below is an example configuration for serving Swagger UI from a custom path or CDN, with assets like CSS and JavaScript loading from a chosen CDN base URL. + +=== "customizing_swagger.py" + + ```python hl_lines="10" + --8<-- "examples/event_handler_rest/src/customizing_swagger.py" + ``` + +=== "customizing_swagger_middlewares.py" + + A Middleware can handle tasks such as adding security headers, user authentication, or other request processing for serving the Swagger UI. + + ```python hl_lines="7 13-18 21" + --8<-- "examples/event_handler_rest/src/customizing_swagger_middlewares.py" + ``` + +#### Customizing OpenAPI metadata + +Defining and customizing OpenAPI metadata gives detailed, top-level information about your API. Here's the method to set and tailor this metadata: + +| Field Name | Type | Description | +| ------------------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `title` | `str` | The title for your API. It should be a concise, specific name that can be used to identify the API in documentation or listings. | +| `version` | `str` | The version of the API you are documenting. This could reflect the release iteration of the API and helps clients understand the evolution of the API. | +| `openapi_version` | `str` | Specifies the version of the OpenAPI Specification on which your API is based. For most contemporary APIs, the default value would be `3.0.0` or higher. | +| `summary` | `str` | A short and informative summary that can provide an overview of what the API does. This can be the same as or different from the title but should add context or information. | +| `description` | `str` | A verbose description that can include Markdown formatting, providing a full explanation of the API's purpose, functionalities, and general usage instructions. | +| `tags` | `List[str]` | A collection of tags that categorize endpoints for better organization and navigation within the documentation. This can group endpoints by their functionality or other criteria. | +| `servers` | `List[Server]` | An array of Server objects, which specify the URL to the server and a description for its environment (production, staging, development, etc.), providing connectivity information. | +| `terms_of_service` | `str` | A URL that points to the terms of service for your API. This could provide legal information and user responsibilities related to the usage of the API. | +| `contact` | `Contact` | A Contact object containing contact details of the organization or individuals maintaining the API. This may include fields such as name, URL, and email. | +| `license_info` | `License` | A License object providing the license details for the API, typically including the name of the license and the URL to the full license text. | + +Include extra parameters when exporting your OpenAPI specification to apply these customizations: + +=== "customizing_api_metadata.py" + + ```python hl_lines="25-31" + --8<-- "examples/event_handler_rest/src/customizing_api_metadata.py" + ``` + ### Custom serializer You can instruct event handler to use a custom serializer to best suit your needs, for example take into account Enums when serializing. diff --git a/docs/media/swagger.png b/docs/media/swagger.png new file mode 100644 index 0000000000000000000000000000000000000000..3db7786886b08e4909f2f73e2e9f4ecc5f16c359 GIT binary patch literal 54463 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

XMDc7Vh@+(d=~5FzW9bXXyu8VC>x=wN zh8SMCVxQ)+T|9roWF;}!(1DYe4Fl@)9v;LI9DXHmZYm9qhq$9}%6bi}xO}V0_w4X)u5IyNJJa zbmvQ)Y+_Abm_EY4&8z|O;sB~*b%RX20u}ci>=!V3?mi}BRLE-b`MYOMl4R6)3R{79 z{2{ik^Qa8OTIxpAH;5|;&GV9tYcNiVEpJ76f{rlw-*@vYd1|Nux%)cyG?}R{0nAlb zaH3EK=Yv3k?uf!ncbsLsP3#4*;5a4hUS)n*`TUcsNNP0{`y>*Mbw zf*1hHurcrsa77%5+pfMuAjP2Ey8ucFP*VdLNPQ^mJ#L?z>)czGR0iXy0}&R8;^aO) zIEGI032mxcXclJ4Q*Pfe1Ho)AL`@RWFT}3WEg1<}Z*vn68p~0p8tw_%ZlAg%}Jc1kBY8QYYjCj5%i;h9$qeEWw_e--?KI-jU$d3sBL3EHzL8 zY(R3?a}<0b6Yx?PN-vH(0LgSsnXpn^P>2m?kwPs5 z0Uk%Wj<%;r12H%ro=Q02QC4;V%&T6#mH@qbr~vdjCEA-oN76p#W8!3q@O2{ZZA*R# zGTo~;T#F-H+ko{PHX_>=x0ih37!q-q3v1L?H&B;QLgAPSxtLz|c*P$$XO1?&kU_}?%p?@alII(jy*J$DMc$3#~5%FL)oAHH@?P#2U7N7|J z0bdBHsA(XqL~aAZfkYUp5Kn^ztc{784`umD~sLE2xFia3>s+a`qPY)e-+eLW%isRofnOq!r`B+Z)r#uHi zs4FWWq!MhT03N+1cGW?6ch~R=1!u?^q`v@h^*#uwq5T!X)UdW$$NdTnV(E09AY^zE z3y(69f|{@;4QP{o#(t8-y*{KE!6(x&QKQzi>Oid)O0CCegLlK?y>zC6$;->N*k)AB zvGEDkcHiU!p07~^Ek&%4Py?gWtesU7k+#M~kh8IW2rYT`LEHJqZSOn)AV%INPQ`Jp z-JsL)vBu0ZunV}LX8rfJHJ9vwb;<+7b;*~j&G(R?m9wHvW^MvJOC6&Yus1(&8>yNr zcIliK1>WCCs~1g{DS&0JVcY?{<8OwWp{^P?fVXj~)^m8#DOgOlQ~p^+3rOvuDand4sh z_LfPGXlU(n$4h_^S2%|qw`Imb&dBaJj<6V(1Ew)P5b#uCF;-V3&KN62U@I(jcrqP; z^tZSm8UvYnS^4!UloBOQ0L57rA5$PnDLp zi|1g4boof1dlF#XuGECX3jnhai3p4F>O?7BH9F6vc_7O3`5F(@F_JZkN zi!Uv!{t-WF0CZC-S#S@tT@dQhAj5SG4if<9(1-ff1~V+5m3sFN4GuqWpVpzQ5IpLQ zLcVYVq~Uv%43N_k43eZR&JEC1asUpU8Uu9*eb@4PRS&^%i{FD3EM)A;1RQiPR>C%j z!J}gHVhpn&*2`l{wb*+kl=DqvPc0qY(mi(B+jisp)3(}6I++1rM9%>vS4FL&L>lB; zTw+wqUR&%wsmKn=2Zs7kt-CX_Qu93Fj5-CjA7>Ihm^?lW;kZ9tgKV2J)cn5b zi`l{L(CNxt0K$O(qXNu-S7!F#!Mgt`pS%g`cK!>T9!lGRI0=%vX8+e|du^QLd-7Fi zCg!cHjiL&-X9Qz1B(=#C1F9Qa{U6MI@c#yLa@1K^dD^^a;)(a~f9ab~x+e<5SCWuL zZVmY}") +def get_todo_title(todo_id: int) -> str: + todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") + todo.raise_for_status() + + return todo.json()["title"] + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) + + +if __name__ == "__main__": + print( + app.get_openapi_json_schema( + title="TODO's API", + version="1.21.3", + summary="API to manage TODOs", + description="This API implements all the CRUD operations for the TODO app", + tags=["todos"], + servers=[Server(url="https://stg.example.org/orders", description="Staging server")], + contact=Contact(name="John Smith", email="john@smith.com"), + ), + ) diff --git a/examples/event_handler_rest/src/customizing_api_operations.py b/examples/event_handler_rest/src/customizing_api_operations.py new file mode 100644 index 00000000000..e455fc7dadd --- /dev/null +++ b/examples/event_handler_rest/src/customizing_api_operations.py @@ -0,0 +1,30 @@ +import requests + +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayRestResolver(enable_validation=True) + + +@app.get( + "/todos/", + summary="Retrieves a todo item", + description="Loads a todo item identified by the `todo_id`", + response_description="The todo object", + responses={ + 200: {"description": "Todo item found"}, + 404: { + "description": "Item not found", + }, + }, + tags=["Todos"], +) +def get_todo_title(todo_id: int) -> str: + todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") + todo.raise_for_status() + + return todo.json()["title"] + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/customizing_swagger.py b/examples/event_handler_rest/src/customizing_swagger.py new file mode 100644 index 00000000000..4903ff25443 --- /dev/null +++ b/examples/event_handler_rest/src/customizing_swagger.py @@ -0,0 +1,29 @@ +from typing import List + +import requests +from pydantic import BaseModel, EmailStr, Field + +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayRestResolver(enable_validation=True) +app.enable_swagger(path="/_swagger", swagger_base_url="https://cdn.example.com/path/to/assets/") + + +class Todo(BaseModel): + userId: int + id_: int = Field(alias="id") + title: str + completed: bool + + +@app.get("/todos") +def get_todos_by_email(email: EmailStr) -> List[Todo]: + todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}") + todos.raise_for_status() + + return todos.json() + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/customizing_swagger_middlewares.py b/examples/event_handler_rest/src/customizing_swagger_middlewares.py new file mode 100644 index 00000000000..49822fecefe --- /dev/null +++ b/examples/event_handler_rest/src/customizing_swagger_middlewares.py @@ -0,0 +1,40 @@ +from typing import List + +import requests +from pydantic import BaseModel, EmailStr, Field + +from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response +from aws_lambda_powertools.event_handler.middlewares import NextMiddleware +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayRestResolver(enable_validation=True) + + +def swagger_middleware(app: APIGatewayRestResolver, next_middleware: NextMiddleware) -> Response: + is_authenticated = ... + if not is_authenticated: + return Response(status_code=400, body="Unauthorized") + + return next_middleware(app) + + +app.enable_swagger(middlewares=[swagger_middleware]) + + +class Todo(BaseModel): + userId: int + id_: int = Field(alias="id") + title: str + completed: bool + + +@app.get("/todos") +def get_todos_by_email(email: EmailStr) -> List[Todo]: + todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}") + todos.raise_for_status() + + return todos.json() + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/data_validation.json b/examples/event_handler_rest/src/data_validation.json new file mode 100644 index 00000000000..f5814ccaa26 --- /dev/null +++ b/examples/event_handler_rest/src/data_validation.json @@ -0,0 +1,36 @@ +{ + "version": "1.0", + "resource": "/todos/1", + "path": "/todos/1", + "httpMethod": "GET", + "headers": { + "Origin": "https://aws.amazon.com" + }, + "multiValueHeaders": {}, + "queryStringParameters": {}, + "multiValueQueryStringParameters": {}, + "requestContext": { + "accountId": "123456789012", + "apiId": "id", + "authorizer": { + "claims": null, + "scopes": null + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "extendedRequestId": "request-id", + "httpMethod": "GET", + "path": "/todos/1", + "protocol": "HTTP/1.1", + "requestId": "id=", + "requestTime": "04/Mar/2020:19:15:17 +0000", + "requestTimeEpoch": 1583349317135, + "resourceId": null, + "resourcePath": "/todos/1", + "stage": "$default" + }, + "pathParameters": null, + "stageVariables": null, + "body": "", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/examples/event_handler_rest/src/data_validation.py b/examples/event_handler_rest/src/data_validation.py new file mode 100644 index 00000000000..1daa9fb2174 --- /dev/null +++ b/examples/event_handler_rest/src/data_validation.py @@ -0,0 +1,35 @@ +from typing import Optional + +import requests +from pydantic import BaseModel, Field + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver(enable_validation=True) # (1)! + + +class Todo(BaseModel): # (2)! + userId: int + id_: Optional[int] = Field(alias="id", default=None) + title: str + completed: bool + + +@app.get("/todos/") # (3)! +@tracer.capture_method +def get_todo_by_id(todo_id: int) -> Todo: # (4)! + todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") + todo.raise_for_status() + + return todo.json() # (5)! + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/data_validation_error.json b/examples/event_handler_rest/src/data_validation_error.json new file mode 100644 index 00000000000..6fc2636ad9c --- /dev/null +++ b/examples/event_handler_rest/src/data_validation_error.json @@ -0,0 +1,42 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/todos/apples", + "rawQueryString": "", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "header1": "value1", + "header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "GET", + "path": "/todos/apples", + "protocol": "HTTP/1.1", + "sourceIp": "192.0.2.1", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "pathParameters": {}, + "isBase64Encoded": false, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } +} diff --git a/examples/event_handler_rest/src/data_validation_error_unsanitized_output.json b/examples/event_handler_rest/src/data_validation_error_unsanitized_output.json new file mode 100644 index 00000000000..46d22c00eef --- /dev/null +++ b/examples/event_handler_rest/src/data_validation_error_unsanitized_output.json @@ -0,0 +1,9 @@ +{ + "statusCode": 422, + "body": "{\"statusCode\": 422, \"detail\": [{\"type\": \"int_parsing\", \"loc\": [\"path\", \"todo_id\"]}]}", + "isBase64Encoded": false, + "headers": { + "Content-Type": "application/json" + }, + "cookies": [] +} \ No newline at end of file diff --git a/examples/event_handler_rest/src/data_validation_output.json b/examples/event_handler_rest/src/data_validation_output.json new file mode 100644 index 00000000000..ec078c87078 --- /dev/null +++ b/examples/event_handler_rest/src/data_validation_output.json @@ -0,0 +1,10 @@ +{ + "statusCode": 200, + "body": "Hello world", + "isBase64Encoded": false, + "multiValueHeaders": { + "Content-Type": [ + "application/json" + ] + } +} \ No newline at end of file diff --git a/examples/event_handler_rest/src/data_validation_sanitized_error.py b/examples/event_handler_rest/src/data_validation_sanitized_error.py new file mode 100644 index 00000000000..71849938f48 --- /dev/null +++ b/examples/event_handler_rest/src/data_validation_sanitized_error.py @@ -0,0 +1,46 @@ +from typing import Optional + +import requests +from pydantic import BaseModel, Field + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response, content_types +from aws_lambda_powertools.event_handler.openapi.exceptions import RequestValidationError +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver(enable_validation=True) + + +class Todo(BaseModel): + userId: int + id_: Optional[int] = Field(alias="id", default=None) + title: str + completed: bool + + +@app.exception_handler(RequestValidationError) # (1)! +def handle_validation_error(ex: RequestValidationError): + logger.error("Request failed validation", path=app.current_event.path, errors=ex.errors()) + + return Response( + status_code=422, + content_type=content_types.APPLICATION_JSON, + body="Invalid data", + ) + + +@app.post("/todos") +def create_todo(todo: Todo) -> int: + response = requests.post("https://jsonplaceholder.typicode.com/todos", json=todo.dict(by_alias=True)) + response.raise_for_status() + + return response.json()["id"] + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/data_validation_sanitized_error_output.json b/examples/event_handler_rest/src/data_validation_sanitized_error_output.json new file mode 100644 index 00000000000..aa6ab7e0d57 --- /dev/null +++ b/examples/event_handler_rest/src/data_validation_sanitized_error_output.json @@ -0,0 +1,9 @@ +{ + "statusCode": 422, + "body": "Invalid data", + "isBase64Encoded": false, + "headers": { + "Content-Type": "application/json" + }, + "cookies": [] +} \ No newline at end of file diff --git a/examples/event_handler_rest/src/enabling_swagger.py b/examples/event_handler_rest/src/enabling_swagger.py new file mode 100644 index 00000000000..b624af77d32 --- /dev/null +++ b/examples/event_handler_rest/src/enabling_swagger.py @@ -0,0 +1,40 @@ +from typing import List + +import requests +from pydantic import BaseModel, Field + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver(enable_validation=True) +app.enable_swagger(path="/swagger") # (1)! + + +class Todo(BaseModel): + userId: int + id_: int = Field(alias="id") + title: str + completed: bool + + +@app.post("/todos") +def create_todo(todo: Todo) -> str: + response = requests.post("https://jsonplaceholder.typicode.com/todos", json=todo.dict(by_alias=True)) + response.raise_for_status() + + return response.json()["id"] + + +@app.get("/todos") +def get_todos() -> List[Todo]: + todo = requests.get("https://jsonplaceholder.typicode.com/todos") + todo.raise_for_status() + + return todo.json() + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/skip_validating_query_strings.py b/examples/event_handler_rest/src/skip_validating_query_strings.py new file mode 100644 index 00000000000..882769239a1 --- /dev/null +++ b/examples/event_handler_rest/src/skip_validating_query_strings.py @@ -0,0 +1,40 @@ +from typing import List, Optional + +import requests +from pydantic import BaseModel, Field + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver(enable_validation=True) + + +class Todo(BaseModel): + userId: int + id_: Optional[int] = Field(alias="id", default=None) + title: str + completed: bool + + +@app.get("/todos") +@tracer.capture_method +def get_todos(completed: Optional[str] = None) -> List[Todo]: # (1)! + url = "https://jsonplaceholder.typicode.com/todos" + + if completed is not None: + url = f"{url}/?completed={completed}" + + todo = requests.get(url) + todo.raise_for_status() + + return todo.json() + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/validating_path.py b/examples/event_handler_rest/src/validating_path.py new file mode 100644 index 00000000000..e892e1c8597 --- /dev/null +++ b/examples/event_handler_rest/src/validating_path.py @@ -0,0 +1,37 @@ +from typing import Optional + +import requests +from pydantic import BaseModel, Field + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.event_handler.openapi.params import Path +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.shared.types import Annotated +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver(enable_validation=True) + + +class Todo(BaseModel): + userId: int + id_: Optional[int] = Field(alias="id", default=None) + title: str + completed: bool + + +@app.get("/todos/") +@tracer.capture_method +def get_todo_by_id(todo_id: Annotated[int, Path(lt=999)]) -> Todo: # (1)! + todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") + todo.raise_for_status() + + return todo.json() + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/validating_payload_subset.json b/examples/event_handler_rest/src/validating_payload_subset.json new file mode 100644 index 00000000000..b786e9287b8 --- /dev/null +++ b/examples/event_handler_rest/src/validating_payload_subset.json @@ -0,0 +1,36 @@ +{ + "version": "1.0", + "body": "{ \"todo\": {\"title\": \"foo\", \"userId\": \"1\", \"completed\": false } }", + "resource": "/todos", + "path": "/todos", + "httpMethod": "POST", + "headers": { + "Origin": "https://aws.amazon.com" + }, + "multiValueHeaders": {}, + "queryStringParameters": {}, + "multiValueQueryStringParameters": {}, + "requestContext": { + "accountId": "123456789012", + "apiId": "id", + "authorizer": { + "claims": null, + "scopes": null + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "extendedRequestId": "request-id", + "httpMethod": "POST", + "path": "/todos", + "protocol": "HTTP/1.1", + "requestId": "id=", + "requestTime": "04/Mar/2020:19:15:17 +0000", + "requestTimeEpoch": 1583349317135, + "resourceId": null, + "resourcePath": "/todos", + "stage": "$default" + }, + "pathParameters": null, + "stageVariables": null, + "isBase64Encoded": false +} \ No newline at end of file diff --git a/examples/event_handler_rest/src/validating_payload_subset.py b/examples/event_handler_rest/src/validating_payload_subset.py new file mode 100644 index 00000000000..ac4ee603853 --- /dev/null +++ b/examples/event_handler_rest/src/validating_payload_subset.py @@ -0,0 +1,30 @@ +from typing import Optional + +import requests +from pydantic import BaseModel, Field + +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.event_handler.openapi.params import Body # (1)! +from aws_lambda_powertools.shared.types import Annotated +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayRestResolver(enable_validation=True) + + +class Todo(BaseModel): + userId: int + id_: Optional[int] = Field(alias="id", default=None) + title: str + completed: bool + + +@app.post("/todos") +def create_todo(todo: Annotated[Todo, Body(embed=True)]) -> int: # (2)! + response = requests.post("https://jsonplaceholder.typicode.com/todos", json=todo.dict(by_alias=True)) + response.raise_for_status() + + return response.json()["id"] + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/validating_payload_subset_output.json b/examples/event_handler_rest/src/validating_payload_subset_output.json new file mode 100644 index 00000000000..754e3a6c128 --- /dev/null +++ b/examples/event_handler_rest/src/validating_payload_subset_output.json @@ -0,0 +1,10 @@ +{ + "statusCode": 200, + "body": "2008822", + "isBase64Encoded": false, + "multiValueHeaders": { + "Content-Type": [ + "application/json" + ] + } +} diff --git a/examples/event_handler_rest/src/validating_payloads.json b/examples/event_handler_rest/src/validating_payloads.json new file mode 100644 index 00000000000..125405e0cf2 --- /dev/null +++ b/examples/event_handler_rest/src/validating_payloads.json @@ -0,0 +1,36 @@ +{ + "version": "1.0", + "body": "{\"title\": \"foo\", \"userId\": \"1\", \"completed\": false}", + "resource": "/todos", + "path": "/todos", + "httpMethod": "POST", + "headers": { + "Origin": "https://aws.amazon.com" + }, + "multiValueHeaders": {}, + "queryStringParameters": {}, + "multiValueQueryStringParameters": {}, + "requestContext": { + "accountId": "123456789012", + "apiId": "id", + "authorizer": { + "claims": null, + "scopes": null + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "extendedRequestId": "request-id", + "httpMethod": "POST", + "path": "/todos", + "protocol": "HTTP/1.1", + "requestId": "id=", + "requestTime": "04/Mar/2020:19:15:17 +0000", + "requestTimeEpoch": 1583349317135, + "resourceId": null, + "resourcePath": "/todos", + "stage": "$default" + }, + "pathParameters": null, + "stageVariables": null, + "isBase64Encoded": false +} \ No newline at end of file diff --git a/examples/event_handler_rest/src/validating_payloads.py b/examples/event_handler_rest/src/validating_payloads.py new file mode 100644 index 00000000000..945cefd8089 --- /dev/null +++ b/examples/event_handler_rest/src/validating_payloads.py @@ -0,0 +1,43 @@ +from typing import List, Optional + +import requests +from pydantic import BaseModel, Field + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver(enable_validation=True) # (1)! + + +class Todo(BaseModel): # (2)! + userId: int + id_: Optional[int] = Field(alias="id", default=None) + title: str + completed: bool + + +@app.post("/todos") +def create_todo(todo: Todo) -> str: # (3)! + response = requests.post("https://jsonplaceholder.typicode.com/todos", json=todo.dict(by_alias=True)) + response.raise_for_status() + + return response.json()["id"] # (4)! + + +@app.get("/todos") +@tracer.capture_method +def get_todos() -> List[Todo]: + todo = requests.get("https://jsonplaceholder.typicode.com/todos") + todo.raise_for_status() + + return todo.json() # (5)! + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/validating_payloads_output.json b/examples/event_handler_rest/src/validating_payloads_output.json new file mode 100644 index 00000000000..9d72764c3c8 --- /dev/null +++ b/examples/event_handler_rest/src/validating_payloads_output.json @@ -0,0 +1,10 @@ +{ + "statusCode": 200, + "body": "2008821", + "isBase64Encoded": false, + "multiValueHeaders": { + "Content-Type": [ + "application/json" + ] + } +} \ No newline at end of file diff --git a/examples/event_handler_rest/src/validating_query_strings.py b/examples/event_handler_rest/src/validating_query_strings.py new file mode 100644 index 00000000000..21d34dbd25a --- /dev/null +++ b/examples/event_handler_rest/src/validating_query_strings.py @@ -0,0 +1,42 @@ +from typing import List, Optional + +import requests +from pydantic import BaseModel, Field + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.event_handler.openapi.params import Query # (2)! +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.shared.types import Annotated # (1)! +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver(enable_validation=True) + + +class Todo(BaseModel): + userId: int + id_: Optional[int] = Field(alias="id", default=None) + title: str + completed: bool + + +@app.get("/todos") +@tracer.capture_method +def get_todos(completed: Annotated[Optional[str], Query(min_length=4)] = None) -> List[Todo]: # (3)! + url = "https://jsonplaceholder.typicode.com/todos" + + if completed is not None: + url = f"{url}/?completed={completed}" + + todo = requests.get(url) + todo.raise_for_status() + + return todo.json() + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) From 547372e730a92ac66de6b938532af2bf01a00992 Mon Sep 17 00:00:00 2001 From: "Powertools for AWS Lambda (Python) bot" Date: Thu, 23 Nov 2023 16:09:55 +0000 Subject: [PATCH 18/18] chore: version bump --- aws_lambda_powertools/shared/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/shared/version.py b/aws_lambda_powertools/shared/version.py index fce34232afe..ef589304f98 100644 --- a/aws_lambda_powertools/shared/version.py +++ b/aws_lambda_powertools/shared/version.py @@ -1,3 +1,3 @@ """Exposes version constant to avoid circular dependencies.""" -VERSION = "2.27.1" +VERSION = "2.28.0" diff --git a/pyproject.toml b/pyproject.toml index e5ca5eca9e0..eafb91f6830 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aws_lambda_powertools" -version = "2.27.1" +version = "2.28.0" description = "Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity." authors = ["Amazon Web Services"] include = ["aws_lambda_powertools/py.typed", "THIRD-PARTY-LICENSES"]