From 5c2444c44fcdca49dacaee5569c2fc19a8720c2c Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Thu, 7 Oct 2021 17:23:37 +0200 Subject: [PATCH 01/49] chore: fix var expr --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 69416cb52fe..5a3c246caec 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -54,7 +54,7 @@ jobs: run: | RELEASE_TAG_VERSION=${{ github.event.release.tag_name }} # Replace publishing version if the workflow was triggered manually - test -n $RELEASE_TAG_VERSION && RELEASE_TAG_VERSION=${{ github.event.inputs.publish_version }} + test -n ${RELEASE_TAG_VERSION} && RELEASE_TAG_VERSION=${{ github.event.inputs.publish_version }} echo "RELEASE_TAG_VERSION=${RELEASE_TAG_VERSION:1}" >> $GITHUB_ENV - name: Ensure new version is also set in pyproject and CHANGELOG run: | From 6e8dfc3246d9f986c8977c7a7bf7d98e2cec6a8c Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Thu, 7 Oct 2021 17:39:13 +0200 Subject: [PATCH 02/49] chore: remove Lambda Layer version tag --- docs/index.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/index.md b/docs/index.md index 7f58de4fe8a..8c1248f1a77 100644 --- a/docs/index.md +++ b/docs/index.md @@ -131,29 +131,29 @@ We build, release and distribute packaged Lambda Powertools layers for each regi ``` -??? note "Layer ARN per region" +??? note "Latest Lambda Layers ARN per region" !!! tip "Click to copy to clipboard" - | Region | Version | Layer ARN - |---------------------------| ---------------------------| --------------------------- - | `us-east-1` | `1.21.0` |[arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `us-east-2` | `1.21.0` |[arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `us-west-1` | `1.21.0` |[arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `us-west-2` | `1.21.0` |[arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `ap-south-1` | `1.21.0` |[arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `ap-northeast-1` | `1.21.0` |[arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `ap-northeast-2` | `1.21.0` |[arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `ap-northeast-3` | `1.21.0` |[arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `ap-southeast-1` | `1.21.0` |[arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `ap-southeast-2` | `1.21.0` |[arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `eu-central-1` | `1.21.0` |[arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `eu-west-1` | `1.21.0` |[arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `eu-west-2` | `1.21.0` |[arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `eu-west-3` | `1.21.0` |[arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `eu-north-1` | `1.21.0` |[arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `ca-central-1` | `1.21.0` |[arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `sa-east-1` | `1.21.0` |[arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} + | Region | Layer ARN + |--------------------------- | --------------------------- + | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} + | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} + | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} + | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} + | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} + | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} + | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} + | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} + | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} + | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} + | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} + | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} + | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} + | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} + | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} + | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} + | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} #### SAR From 6fcedeca10e54312ddc9e00eb1ca8fe27064f7dc Mon Sep 17 00:00:00 2001 From: BVMiko Date: Thu, 7 Oct 2021 11:55:17 -0500 Subject: [PATCH 03/49] feat(apigateway): add Router to allow large routing composition (#645) --- .../event_handler/api_gateway.py | 74 +++++++++- .../event_handler/test_api_gateway.py | 134 ++++++++++++++++++ 2 files changed, 206 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 754cc24710d..d3a79761556 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -6,9 +6,9 @@ import traceback import zlib from enum import Enum -from functools import partial +from functools import partial, wraps from http import HTTPStatus -from typing import Any, Callable, Dict, List, Optional, Set, Union +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union from aws_lambda_powertools.event_handler import content_types from aws_lambda_powertools.event_handler.exceptions import ServiceError @@ -630,3 +630,73 @@ def _to_response(self, result: Union[Dict, Response]) -> Response: 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 defined in a router""" + router._app = self + for route, func in router.api.items(): + if prefix and route[0] == "/": + route = (prefix, *route[1:]) + elif prefix: + route = (f"{prefix}{route[0]}", *route[1:]) + self.route(*route)(func()) + + +class Router: + """Router helper class to allow splitting ApiGatewayResolver into multiple files""" + + _app: ApiGatewayResolver + + def __init__(self): + self.api: Dict[tuple, Callable] = {} + + @property + def current_event(self) -> BaseProxyEvent: + return self._app.current_event + + @property + def lambda_context(self) -> LambdaContext: + return self._app.lambda_context + + def route( + self, + rule: str, + method: Union[str, Tuple[str], List[str]], + cors: Optional[bool] = None, + compress: bool = False, + cache_control: Optional[str] = None, + ): + def actual_decorator(func: Callable): + @wraps(func) + def wrapper(): + def inner_wrapper(**kwargs): + return func(**kwargs) + + return inner_wrapper + + if isinstance(method, (list, tuple)): + for item in method: + self.api[(rule, item, cors, compress, cache_control)] = wrapper + else: + self.api[(rule, method, cors, compress, cache_control)] = wrapper + + return actual_decorator + + def get(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): + return self.route(rule, "GET", cors, compress, cache_control) + + def post(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): + return self.route(rule, "POST", cors, compress, cache_control) + + def put(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): + return self.route(rule, "PUT", cors, compress, cache_control) + + def delete( + self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None + ): + return self.route(rule, "DELETE", cors, compress, cache_control) + + def patch( + self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None + ): + return self.route(rule, "PATCH", cors, compress, cache_control) diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 21700ec09dd..afc979065f8 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -17,6 +17,7 @@ ProxyEventType, Response, ResponseBuilder, + Router, ) from aws_lambda_powertools.event_handler.exceptions import ( BadRequestError, @@ -860,3 +861,136 @@ def base(): # THEN process event correctly assert result["statusCode"] == 200 assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + + +def test_api_gateway_app_router(): + # GIVEN a Router with registered routes + app = ApiGatewayResolver() + router = Router() + + @router.get("/my/path") + def foo(): + return {} + + app.include_router(router) + # WHEN calling the event handler after applying routes from router object + result = app(LOAD_GW_EVENT, {}) + + # THEN process event correctly + assert result["statusCode"] == 200 + assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + + +def test_api_gateway_app_router_with_params(): + # GIVEN a Router with registered routes + app = ApiGatewayResolver() + router = Router() + req = "foo" + event = deepcopy(LOAD_GW_EVENT) + event["resource"] = "/accounts/{account_id}" + event["path"] = f"/accounts/{req}" + lambda_context = {} + + @router.route(rule="/accounts/", method=["GET", "POST"]) + def foo(account_id): + assert router.current_event.raw_event == event + assert router.lambda_context == lambda_context + assert account_id == f"{req}" + return {} + + app.include_router(router) + # WHEN calling the event handler after applying routes from router object + result = app(event, lambda_context) + + # THEN process event correctly + assert result["statusCode"] == 200 + assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + + +def test_api_gateway_app_router_with_prefix(): + # GIVEN a Router with registered routes + # AND a prefix is defined during the registration + app = ApiGatewayResolver() + router = Router() + + @router.get(rule="/path") + def foo(): + return {} + + app.include_router(router, prefix="/my") + # WHEN calling the event handler after applying routes from router object + result = app(LOAD_GW_EVENT, {}) + + # THEN process event correctly + assert result["statusCode"] == 200 + assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + + +def test_api_gateway_app_router_with_prefix_equals_path(): + # GIVEN a Router with registered routes + # AND a prefix is defined during the registration + app = ApiGatewayResolver() + router = Router() + + @router.get(rule="/") + def foo(): + return {} + + app.include_router(router, prefix="/my/path") + # WHEN calling the event handler after applying routes from router object + # WITH the request path matching the registration prefix + result = app(LOAD_GW_EVENT, {}) + + # THEN process event correctly + assert result["statusCode"] == 200 + assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + + +def test_api_gateway_app_router_with_different_methods(): + # GIVEN a Router with all the possible HTTP methods + app = ApiGatewayResolver() + router = Router() + + @router.get("/not_matching_get") + def get_func(): + raise RuntimeError() + + @router.post("/no_matching_post") + def post_func(): + raise RuntimeError() + + @router.put("/no_matching_put") + def put_func(): + raise RuntimeError() + + @router.delete("/no_matching_delete") + def delete_func(): + raise RuntimeError() + + @router.patch("/no_matching_patch") + def patch_func(): + raise RuntimeError() + + app.include_router(router) + + # Also check check the route configurations + routes = app._routes + assert len(routes) == 5 + for route in routes: + if route.func == get_func: + assert route.method == "GET" + elif route.func == post_func: + assert route.method == "POST" + elif route.func == put_func: + assert route.method == "PUT" + elif route.func == delete_func: + assert route.method == "DELETE" + elif route.func == patch_func: + assert route.method == "PATCH" + + # WHEN calling the handler + # THEN return a 404 + result = app(LOAD_GW_EVENT, None) + assert result["statusCode"] == 404 + # AND cors headers are not returned + assert "Access-Control-Allow-Origin" not in result["headers"] From a3b7b14b4b876a91810f5833b94003df8e39ff2d Mon Sep 17 00:00:00 2001 From: AlessandroVol23 Date: Fri, 8 Oct 2021 19:13:10 +0200 Subject: [PATCH 04/49] docs: add amplify-cli instructions for public layer (#754) --- docs/index.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/index.md b/docs/index.md index 7f58de4fe8a..2bd9053a183 100644 --- a/docs/index.md +++ b/docs/index.md @@ -131,6 +131,32 @@ We build, release and distribute packaged Lambda Powertools layers for each regi ``` +=== "Amplify" + + ```zsh + # Create a new one with the layer + ❯ amplify add function + ? Select which capability you want to add: Lambda function (serverless function) + ? Provide an AWS Lambda function name: + ? Choose the runtime that you want to use: Python + ? 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:AWSLambdaPowertoolsPython:3 + ❯ amplify push -y + + + # Updating an existing function and add the layer + ❯ amplify update function + ? Select the Lambda function you want to update test2 + General information + - Name: + ? Which setting do you want to update? Lambda layers configuration + ? Do you want to enable Lambda layers for this function? Yes + ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:3 + ? Do you want to edit the local lambda function now? No + ``` + ??? note "Layer ARN per region" !!! tip "Click to copy to clipboard" From 02eb1520f277eda4487ffa1631e3f4a000014b70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 11:55:16 +0000 Subject: [PATCH 05/49] chore(deps): bump boto3 from 1.18.56 to 1.18.58 (#755) Bumps [boto3](https://github.com/boto/boto3) from 1.18.56 to 1.18.58.
Changelog

Sourced from boto3's changelog.

1.18.58

  • api-change:lexv2-runtime: [botocore] Update lexv2-runtime client to latest version
  • api-change:lexv2-models: [botocore] Update lexv2-models client to latest version
  • api-change:secretsmanager: [botocore] Documentation updates for Secrets Manager
  • api-change:securityhub: [botocore] Added new resource details objects to ASFF, including resources for WAF rate-based rules, EC2 VPC endpoints, ECR repositories, EKS clusters, X-Ray encryption, and OpenSearch domains. Added additional details for CloudFront distributions, CodeBuild projects, ELB V2 load balancers, and S3 buckets.
  • api-change:mediaconvert: [botocore] AWS Elemental MediaConvert has added the ability to set account policies which control access restrictions for HTTP, HTTPS, and S3 content sources.
  • api-change:ec2: [botocore] This release removes a requirement for filters on SearchLocalGatewayRoutes operations.

1.18.57

  • api-change:kendra: [botocore] Amazon Kendra now supports indexing and querying documents in different languages.
  • api-change:grafana: [botocore] Initial release of the SDK for Amazon Managed Grafana API.
  • api-change:firehose: [botocore] Allow support for Amazon Opensearch Service(successor to Amazon Elasticsearch Service) as a Kinesis Data Firehose delivery destination.
  • api-change:backup: [botocore] Launch of AWS Backup Vault Lock, which protects your backups from malicious and accidental actions, works with existing backup policies, and helps you meet compliance requirements.
  • api-change:schemas: [botocore] Removing unused request/response objects.
  • api-change:chime: [botocore] This release enables customers to configure Chime MediaCapturePipeline via API.
Commits
  • 7f4cb35 Merge branch 'release-1.18.58'
  • 1ad7bb3 Bumping version to 1.18.58
  • 471384e Add changelog entries from botocore
  • 7a10e21 Merge pull request #2988 from nateprewitt/pre-commit
  • 5ecd069 Merge branch 'release-1.18.57'
  • 2af248f Merge branch 'release-1.18.57' into develop
  • b8a6c55 Bumping version to 1.18.57
  • f2b9fdb Add changelog entries from botocore
  • 8feeb37 Merge pull request #3030 from boto/kdaily-bump-ancient-min-reactions
  • bea14ac Update stale_issue.yml
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=boto3&package-manager=pip&previous-version=1.18.56&new-version=1.18.58)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- poetry.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 1d1e5366e12..d914ab6e5d5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -81,14 +81,14 @@ d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] name = "boto3" -version = "1.18.56" +version = "1.18.58" description = "The AWS SDK for Python" category = "main" optional = false python-versions = ">= 3.6" [package.dependencies] -botocore = ">=1.21.56,<1.22.0" +botocore = ">=1.21.58,<1.22.0" jmespath = ">=0.7.1,<1.0.0" s3transfer = ">=0.5.0,<0.6.0" @@ -97,7 +97,7 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.21.56" +version = "1.21.58" description = "Low-level, data-driven core of boto 3." category = "main" optional = false @@ -1082,12 +1082,12 @@ black = [ {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] boto3 = [ - {file = "boto3-1.18.56-py3-none-any.whl", hash = "sha256:42828d83acddfa2361411b13683eedf7c1c0a15e896b45960ed04a26efe7adfb"}, - {file = "boto3-1.18.56.tar.gz", hash = "sha256:d43e3651ad1b0b5de6f77df82df27e0f1e6cd854f725c808c70a1fb956f0b699"}, + {file = "boto3-1.18.58-py3-none-any.whl", hash = "sha256:7309552da6ef9e610f8a9712c8abe29a8d3d45b514188cc3efad47bd1774bf77"}, + {file = "boto3-1.18.58.tar.gz", hash = "sha256:f680dee9c670d42ab4a6da5539ca3691d1ccbbcbf041e7021025029776864156"}, ] botocore = [ - {file = "botocore-1.21.56-py3-none-any.whl", hash = "sha256:d712f572022670916bd77fbe421155dcf575398b9dced88035ed3658679883bd"}, - {file = "botocore-1.21.56.tar.gz", hash = "sha256:43fab79905e3dfe56f92a137314ef1afbf040f7c06516a003351c24322cbfd7c"}, + {file = "botocore-1.21.58-py3-none-any.whl", hash = "sha256:9d84a97015c0565a81c0b6d55e60af5ab1a2da28039ad93976388014a14186da"}, + {file = "botocore-1.21.58.tar.gz", hash = "sha256:87e881569c32b218a1b82ecb607a4dddb4dca3b80a5d1016571b99b51cef3158"}, ] certifi = [ {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, From 2582679b77c3ba6b4dec02166f766bfdbedcf343 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 11:55:20 +0000 Subject: [PATCH 06/49] chore(deps-dev): bump coverage from 6.0 to 6.0.1 (#751) Bumps [coverage](https://github.com/nedbat/coveragepy) from 6.0 to 6.0.1.
Changelog

Sourced from coverage's changelog.

Version 6.0.1 --- 2021-10-06

  • In 6.0, the coverage.py exceptions moved from coverage.misc to coverage.exceptions. These exceptions are not part of the public supported API, CoverageException is. But a number of other third-party packages were importing the exceptions from coverage.misc, so they are now available from there again (issue 1226_).

  • Changed an internal detail of how tomli is imported, so that tomli can use coverage.py for their own test suite (issue 1228_).

  • Defend against an obscure possibility under code obfuscation, where a function can have an argument called "self", but no local named "self" (pull request 1210_). Thanks, Ben Carlsson.

.. _pull request 1210: nedbat/coveragepy#1210 .. _issue 1226: nedbat/coveragepy#1226 .. _issue 1228: nedbat/coveragepy#1228

.. _changes_60:

Commits
  • 78a9c68 build: prep for 6.0.1
  • 72200e2 docs: this document isn't in a toc, and that's ok
  • a309f08 fix: make exceptions importable from coverage.misc again. #1226
  • f33b733 docs: note #1210 in the changelog
  • 19545b7 Fix an incompatibility with pyarmor
  • 613446c fix: pretend we didn't import third-party packages we use. #1228
  • 6211680 build: 3.10.0 is out
  • fb21f8e docs: the 5.0 change summary shouldn't be so prominent anymore
  • a5da277 build: bump version
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=coverage&package-manager=pip&previous-version=6.0&new-version=6.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- poetry.lock | 70 ++++++++++++++++++++++++++--------------------------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/poetry.lock b/poetry.lock index d914ab6e5d5..39f06d28142 100644 --- a/poetry.lock +++ b/poetry.lock @@ -149,7 +149,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.0" +version = "6.0.1" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -1106,41 +1106,39 @@ colorama = [ {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] coverage = [ - {file = "coverage-6.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:3dfb23cc180b674a11a559183dff9655beb9da03088f3fe3c4f3a6d200c86f05"}, - {file = "coverage-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5dd5ae0a9cd55d71f1335c331e9625382239b8cede818fb62d8d2702336dbf8"}, - {file = "coverage-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8426fec5ad5a6e8217921716b504e9b6e1166dc147e8443b4855e329db686282"}, - {file = "coverage-6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aa5d4d43fa18cc9d0c6e02a83de0b9729b5451a9066574bd276481474f0a53ab"}, - {file = "coverage-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78dd3eeb8f5ff26d2113c41836bac04a9ea91be54c346826b54a373133c8c53"}, - {file = "coverage-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:581fddd2f883379bd5af51da9233e0396b6519f3d3eeae4fb88867473be6d56e"}, - {file = "coverage-6.0-cp310-cp310-win32.whl", hash = "sha256:43bada49697a62ffa0283c7f01bbc76aac562c37d4bb6c45d56dd008d841194e"}, - {file = "coverage-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:fa816e97cfe1f691423078dffa39a18106c176f28008db017b3ce3e947c34aa5"}, - {file = "coverage-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5c191e01b23e760338f19d8ba2470c0dad44c8b45e41ac043b2db84efc62f695"}, - {file = "coverage-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:274a612f67f931307706b60700f1e4cf80e1d79dff6c282fc9301e4565e78724"}, - {file = "coverage-6.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9dbfcbc56d8de5580483cf2caff6a59c64d3e88836cbe5fb5c20c05c29a8808"}, - {file = "coverage-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e63490e8a6675cee7a71393ee074586f7eeaf0e9341afd006c5d6f7eec7c16d7"}, - {file = "coverage-6.0-cp36-cp36m-win32.whl", hash = "sha256:72f8c99f1527c5a8ee77c890ea810e26b39fd0b4c2dffc062e20a05b2cca60ef"}, - {file = "coverage-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:88f1810eb942e7063d051d87aaaa113eb5fd5a7fd2cda03a972de57695b8bb1a"}, - {file = "coverage-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:befb5ffa9faabef6dadc42622c73de168001425258f0b7e402a2934574e7a04b"}, - {file = "coverage-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7dbda34e8e26bd86606ba8a9c13ccb114802e01758a3d0a75652ffc59a573220"}, - {file = "coverage-6.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b4ee5815c776dfa3958ba71c7cd4cdd8eb40d79358a18352feb19562fe4408c4"}, - {file = "coverage-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d82cbef1220703ce56822be7fbddb40736fc1a928ac893472df8aff7421ae0aa"}, - {file = "coverage-6.0-cp37-cp37m-win32.whl", hash = "sha256:d795a2c92fe8cb31f6e9cd627ee4f39b64eb66bf47d89d8fcf7cb3d17031c887"}, - {file = "coverage-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6e216e4021c934246c308fd3e0d739d9fa8a3f4ea414f584ab90ef9c1592f282"}, - {file = "coverage-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8305e14112efb74d0b5fec4df6e41cafde615c2392a7e51c84013cafe945842c"}, - {file = "coverage-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4865dc4a7a566147cbdc2b2f033a6cccc99a7dcc89995137765c384f6c73110b"}, - {file = "coverage-6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:25df2bc53a954ba2ccf230fa274d1de341f6aa633d857d75e5731365f7181749"}, - {file = "coverage-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:08fd55d2e00dac4c18a2fa26281076035ec86e764acdc198b9185ce749ada58f"}, - {file = "coverage-6.0-cp38-cp38-win32.whl", hash = "sha256:11ce082eb0f7c2bbfe96f6c8bcc3a339daac57de4dc0f3186069ec5c58da911c"}, - {file = "coverage-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:7844a8c6a0fee401edbf578713c2473e020759267c40261b294036f9d3eb6a2d"}, - {file = "coverage-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bea681309bdd88dd1283a8ba834632c43da376d9bce05820826090aad80c0126"}, - {file = "coverage-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e735ab8547d8a1fe8e58dd765d6f27ac539b395f52160d767b7189f379f9be7a"}, - {file = "coverage-6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7593a49300489d064ebb6c58539f52cbbc4a2e6a4385de5e92cae1563f88a425"}, - {file = "coverage-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:adb0f4c3c8ba8104378518a1954cbf3d891a22c13fd0e0bf135391835f44f288"}, - {file = "coverage-6.0-cp39-cp39-win32.whl", hash = "sha256:8da0c4a26a831b392deaba5fdd0cd7838d173b47ce2ec3d0f37be630cb09ef6e"}, - {file = "coverage-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:7af2f8e7bb54ace984de790e897f858e88068d8fbc46c9490b7c19c59cf51822"}, - {file = "coverage-6.0-pp36-none-any.whl", hash = "sha256:82b58d37c47d93a171be9b5744bcc96a0012cbf53d5622b29a49e6be2097edd7"}, - {file = "coverage-6.0-pp37-none-any.whl", hash = "sha256:fff04bfefb879edcf616f1ce5ea6f4a693b5976bdc5e163f8464f349c25b59f0"}, - {file = "coverage-6.0.tar.gz", hash = "sha256:17983f6ccc47f4864fd16d20ff677782b23d1207bf222d10e4d676e4636b0872"}, + {file = "coverage-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:abe8207dfb8a61ded9cd830d26c1073c8218fc0ae17eb899cfe8ec0fafae6e22"}, + {file = "coverage-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83faa3692e8306b20293889714fdf573d10ef5efc5843bd7c7aea6971487bd6a"}, + {file = "coverage-6.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f82a17f2a77958f3eef40ad385fc82d4c6ba9a77a51a174efe03ce75daebbc16"}, + {file = "coverage-6.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b06f4f1729e2963281d9cd6e65e6976bf27b44d4c07ac5b47223ce45f822cec"}, + {file = "coverage-6.0.1-cp310-cp310-win32.whl", hash = "sha256:7600fac458f74c68b097379f76f3a6e3a630493fc7fc94b6508fedd9d498c194"}, + {file = "coverage-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:2c5f39d1556e75fc3c4fb071f9e7cfa618895a999a0de763a541d730775d0d5f"}, + {file = "coverage-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3edbb3ec580c73e5a264f5d04f30245bc98eff1a26765d46c5c65134f0a0e2f7"}, + {file = "coverage-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea452a2d83964d08232ade470091015e7ab9b8f53acbec10f2210fbab4ce7e43"}, + {file = "coverage-6.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1770d24f45f1f2daeae34cfa3b33fcb29702153544cd2ad40d58399dd4ff53b5"}, + {file = "coverage-6.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ad7182a82843f9f85487f44567c8c688f16c906bdb8d0e44ae462aed61cb8f1b"}, + {file = "coverage-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:9d242a2434801ef5125330deddb4cddba8990c9a49b3dec99dca17dd7eefba5a"}, + {file = "coverage-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e66c50f0ab445fec920a9f084914ea1776a809e3016c3738519048195f851bbb"}, + {file = "coverage-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5b1ceacb86e0a9558061dcc6baae865ed25933ea57effea644f21657cdce19bc"}, + {file = "coverage-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e15ab5afbee34abf716fece80ea33ea09a82e7450512f022723b1a82ec9a4e"}, + {file = "coverage-6.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6873f3f954d3e3ab8b1881f4e5307cc19f70c9f931c41048d9f7e6fd946eabe7"}, + {file = "coverage-6.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0898d6948b31df13391cd40568de8f35fa5901bc922c5ae05cf070587cb9c666"}, + {file = "coverage-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9c416ba03844608f45661a5b48dc59c6b5e89956efe388564dd138ca8caf540b"}, + {file = "coverage-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:66fe33e9e0df58675e08e83fe257f89e7f625e7633ea93d0872154e09cce2724"}, + {file = "coverage-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:353a50f123f0185cdb7a1e1e3e2cfb9d1fd7e293cfaf68eedaf5bd8e02e3ec32"}, + {file = "coverage-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b81a4e667c45b13658b84f9b8f1d32ef86d5405fabcbd181b76b9e51d295f397"}, + {file = "coverage-6.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:17426808e8e0824f864876312d41961223bf5e503bf8f1f846735279a60ea345"}, + {file = "coverage-6.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e11cca9eb5c9b3eaad899728ee2ce916138399ee8cbbccaadc1871fecb750827"}, + {file = "coverage-6.0.1-cp38-cp38-win32.whl", hash = "sha256:0a7e55cc9f7efa22d5cc9966276ec7a40a8803676f6ccbfdc06a486fba9aa9ee"}, + {file = "coverage-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:4eb9cd910ca8e243f930243a9940ea1a522e32435d15668445753d087c30ee12"}, + {file = "coverage-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:83682b73785d2e078e0b5f63410b8125b122e1a22422640c57edd4011c950f3e"}, + {file = "coverage-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b45f89a8ef65c29195f8f28dbe215f44ccb29d934f3e862d2a5c12e38698a793"}, + {file = "coverage-6.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:73880a80fad0597eca43e213e5e1711bf6c0fcdb7eb6b01b3b17841ebe5a7f8d"}, + {file = "coverage-6.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f398d38e6ebc2637863db1d7be3d4f9c5174e7d24bb3b0716cdb1f204669cbcf"}, + {file = "coverage-6.0.1-cp39-cp39-win32.whl", hash = "sha256:1864bdf9b2ccb43e724051bc23a1c558daf101ad4488ede1945f2a8be1facdad"}, + {file = "coverage-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:c9c413c4397d4cdc7ca89286158d240ce524f9667b52c9a64dd7e13d16cf8815"}, + {file = "coverage-6.0.1-pp36-none-any.whl", hash = "sha256:65da6e3e8325291f012921bbf71fea0a97824e1c573981871096aac6e2cf0ec5"}, + {file = "coverage-6.0.1-pp37-none-any.whl", hash = "sha256:07efe1fbd72e67df026ad5109bcd216acbbd4a29d5208b3dab61779bae6b7b26"}, + {file = "coverage-6.0.1.tar.gz", hash = "sha256:3490ff6dbf3f7accf0750136ed60ae1f487bccc1f097740e3b21262bc9c89854"}, ] dataclasses = [ {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, From 32be054b3e5e9a2b879d42b4ec64c8931ec9f297 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Oct 2021 08:44:20 +0000 Subject: [PATCH 07/49] chore(deps): bump boto3 from 1.18.58 to 1.18.59 (#760) Bumps [boto3](https://github.com/boto/boto3) from 1.18.58 to 1.18.59.
Changelog

Sourced from boto3's changelog.

1.18.59

  • api-change:elbv2: [botocore] Update elbv2 client to latest version
  • bugfix:Signing: [botocore] SigV4QueryAuth and CrtSigV4QueryAuth now properly respect AWSRequest.params while signing boto/botocore[#2521](https://github.com/boto/boto3/issues/2521) <https://github.com/boto/botocore/issues/2521>__
  • api-change:medialive: [botocore] This release adds support for Transport Stream files as an input type to MediaLive encoders.
  • api-change:ec2: [botocore] Documentation update for Amazon EC2.
  • api-change:frauddetector: [botocore] New model type: Transaction Fraud Insights, which is optimized for online transaction fraud. Stored Events, which allows customers to send and store data directly within Amazon Fraud Detector. Batch Import, which allows customers to upload a CSV file of historic event data for processing and storage
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=boto3&package-manager=pip&previous-version=1.18.58&new-version=1.18.59)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- poetry.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 39f06d28142..480350dfe8b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -81,14 +81,14 @@ d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] name = "boto3" -version = "1.18.58" +version = "1.18.59" description = "The AWS SDK for Python" category = "main" optional = false python-versions = ">= 3.6" [package.dependencies] -botocore = ">=1.21.58,<1.22.0" +botocore = ">=1.21.59,<1.22.0" jmespath = ">=0.7.1,<1.0.0" s3transfer = ">=0.5.0,<0.6.0" @@ -97,7 +97,7 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.21.58" +version = "1.21.59" description = "Low-level, data-driven core of boto 3." category = "main" optional = false @@ -1082,12 +1082,12 @@ black = [ {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] boto3 = [ - {file = "boto3-1.18.58-py3-none-any.whl", hash = "sha256:7309552da6ef9e610f8a9712c8abe29a8d3d45b514188cc3efad47bd1774bf77"}, - {file = "boto3-1.18.58.tar.gz", hash = "sha256:f680dee9c670d42ab4a6da5539ca3691d1ccbbcbf041e7021025029776864156"}, + {file = "boto3-1.18.59-py3-none-any.whl", hash = "sha256:daa721ccad79ed8e23a4b662eedce59ba0585e5b336bd6a9cd8e4fec40cd2db6"}, + {file = "boto3-1.18.59.tar.gz", hash = "sha256:40e948276010e5eb23f0625afe9b323146e16a45dbeade0acd558eababd8b8ce"}, ] botocore = [ - {file = "botocore-1.21.58-py3-none-any.whl", hash = "sha256:9d84a97015c0565a81c0b6d55e60af5ab1a2da28039ad93976388014a14186da"}, - {file = "botocore-1.21.58.tar.gz", hash = "sha256:87e881569c32b218a1b82ecb607a4dddb4dca3b80a5d1016571b99b51cef3158"}, + {file = "botocore-1.21.59-py3-none-any.whl", hash = "sha256:ebcb8f0b0e9b56d275073746ea9bb8b60597517b2bfa4d6ebdc64b9bcf4422e0"}, + {file = "botocore-1.21.59.tar.gz", hash = "sha256:c947215f45653609682dc448195a3bfcf0f7da193fd7b17b739656db5442221a"}, ] certifi = [ {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, From b20d88251d198c739372654443afd9de04b1e3f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Oct 2021 08:46:25 +0000 Subject: [PATCH 08/49] chore(deps-dev): bump flake8-comprehensions from 3.6.1 to 3.7.0 (#759) Bumps [flake8-comprehensions](https://github.com/adamchainz/flake8-comprehensions) from 3.6.1 to 3.7.0.
Changelog

Sourced from flake8-comprehensions's changelog.

3.7.0 (2021-10-11)

  • Support Flake8 4.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=flake8-comprehensions&package-manager=pip&previous-version=3.6.1&new-version=3.7.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 480350dfe8b..b97f5be57f5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -273,14 +273,14 @@ test = ["coverage", "coveralls", "mock", "pytest", "pytest-cov"] [[package]] name = "flake8-comprehensions" -version = "3.6.1" +version = "3.7.0" description = "A flake8 plugin to help you write better list/set/dict comprehensions." category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -flake8 = ">=3.0,<3.2.0 || >3.2.0,<4" +flake8 = ">=3.0,<3.2.0 || >3.2.0,<5" importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] @@ -1055,7 +1055,7 @@ pydantic = ["pydantic", "email-validator"] [metadata] lock-version = "1.1" python-versions = "^3.6.1" -content-hash = "829128c92690e9cfa6ed3387bb6927fcca65a5478baadb59db5c489b99f71bfd" +content-hash = "bb9ac21fcba8853ce6db9eb384c9c441c0a0ffc3212cea2a04c956f8cd381046" [metadata.files] appdirs = [ @@ -1176,8 +1176,8 @@ flake8-builtins = [ {file = "flake8_builtins-1.5.3-py2.py3-none-any.whl", hash = "sha256:7706babee43879320376861897e5d1468e396a40b8918ed7bccf70e5f90b8687"}, ] flake8-comprehensions = [ - {file = "flake8-comprehensions-3.6.1.tar.gz", hash = "sha256:4888de89248b7f7535159189ff693c77f8354f6d37a02619fa28c9921a913aa0"}, - {file = "flake8_comprehensions-3.6.1-py3-none-any.whl", hash = "sha256:e9a010b99aa90c05790d45281ad9953df44a4a08a1a8f6cd41f98b4fc6a268a0"}, + {file = "flake8-comprehensions-3.7.0.tar.gz", hash = "sha256:6b3218b2dde8ac5959c6476cde8f41a79e823c22feb656be2710cd2a3232cef9"}, + {file = "flake8_comprehensions-3.7.0-py3-none-any.whl", hash = "sha256:a5d7aea6315bbbd6fbcb2b4e80bff6a54d1600155e26236e555d0c6fe1d62522"}, ] flake8-debugger = [ {file = "flake8-debugger-4.0.0.tar.gz", hash = "sha256:e43dc777f7db1481db473210101ec2df2bd39a45b149d7218a618e954177eda6"}, diff --git a/pyproject.toml b/pyproject.toml index 19797484c8f..775bd85b3ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ black = "^20.8b1" flake8 = "^3.9.0" flake8-black = "^0.2.3" flake8-builtins = "^1.5.3" -flake8-comprehensions = "^3.6.1" +flake8-comprehensions = "^3.7.0" flake8-debugger = "^4.0.0" flake8-fixme = "^1.1.1" flake8-isort = "^4.0.0" From dce7432b771e310bc2ba353ed99053a5d0be41c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Oct 2021 08:48:32 +0000 Subject: [PATCH 09/49] chore(deps-dev): bump mkdocs-material from 7.3.2 to 7.3.3 (#758) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.3.2 to 7.3.3.
Release notes

Sourced from mkdocs-material's releases.

mkdocs-material-7.3.3

  • Rewrite of entire documentation
  • Adjusted height of new content tabs to match single line code blocks
  • Fixed new content tabs missing right padding in some browsers on overflow
  • Fixed new content tabs bleeding out of flex container on overflow
  • Fixed new content tabs overflow scrolling bugs on some browsers
  • Fixed new content tabs stealing keyboard access when active
  • Fixed some spacings issues for right-to-left languages
Changelog

Sourced from mkdocs-material's changelog.

mkdocs-material-7.3.3 (2021-10-11)

  • Rewrite of entire documentation
  • Adjusted height of new content tabs to match single line code blocks
  • Fixed new content tabs missing right padding in some browsers on overflow
  • Fixed new content tabs bleeding out of flex container on overflow
  • Fixed new content tabs overflow scrolling bugs on some browsers
  • Fixed new content tabs stealing keyboard access when active
  • Fixed some spacings issues for right-to-left languages

mkdocs-material-7.3.2+insiders-3.1.2 (2021-10-06)

  • Fixed incorrect path separators for social cards on Windows

mkdocs-material-7.3.2 (2021-10-06)

  • Deprecated prebuilding of search index
  • Improved graceful handling of broken search for file://
  • Added minimum Jinja version to list of requirements
  • Fixed #3071: section index pages render empty directories
  • Fixed margin issues when using navigation tabs (7.3.1 regression)
  • Fixed search placeholder sometimes being shown too early

mkdocs-material-7.3.1 (2021-10-02)

  • Added new experimental content tabs implementation
  • Fixed #3069: GitHub stats broken for users/orgs (7.1.0 regression)
  • Fixed #3070: Sections not linking to index page
  • Fixed title not linking to index page when using tabs
  • Fixed Disqus integration when using instant loading
  • Fixed some spacing issues for right-to-left languages
  • Fixed syntax error in Serbian translations

mkdocs-material-7.3.0+insiders-3.1.1 (2021-09-26)

  • Fixed ordering bug in search exclusion logic

mkdocs-material-7.3.0+insiders-3.1.0 (2021-09-26)

  • Added support for excluding pages, sections, and elements from search
  • Fixed #2803: Code block annotations not visible when printing

mkdocs-material-7.3.0 (2021-09-23)

  • Added support for sticky navigation tabs
  • Added support for section index pages
  • Added support for removing generator notice

mkdocs-material-7.2.8 (2021-09-20)

... (truncated)

Commits
  • be9d7b5 Prepare 7.3.3 release
  • de98c57 Adjusted height of content tabs to match single line code blocks
  • b655863 Fixed right padding on some browser for overflowing content tabs
  • 49f4677 Removed stale deprecations file
  • 53b32a1 Updated documentation
  • 98bc87b Downgraded ESlint
  • 969c713 Fixed overflow for alternate style when more than 10 tabs are present
  • 6fab1e3 Fixed buggy content tabs overflow scrolling + updated documentation
  • c4cf616 Updated documentation
  • babc28f Updated dependencies
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=mkdocs-material&package-manager=pip&previous-version=7.3.2&new-version=7.3.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index b97f5be57f5..90d888b7cd0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -577,7 +577,7 @@ mkdocs = ">=0.17" [[package]] name = "mkdocs-material" -version = "7.3.2" +version = "7.3.3" description = "A Material Design theme for MkDocs" category = "dev" optional = false @@ -1055,7 +1055,7 @@ pydantic = ["pydantic", "email-validator"] [metadata] lock-version = "1.1" python-versions = "^3.6.1" -content-hash = "bb9ac21fcba8853ce6db9eb384c9c441c0a0ffc3212cea2a04c956f8cd381046" +content-hash = "dc363cbaa0be00b64c588301bebd0e83dd7c34a48132711cdd5d573a08d0a05b" [metadata.files] appdirs = [ @@ -1326,8 +1326,8 @@ mkdocs-git-revision-date-plugin = [ {file = "mkdocs_git_revision_date_plugin-0.3.1-py3-none-any.whl", hash = "sha256:8ae50b45eb75d07b150a69726041860801615aae5f4adbd6b1cf4d51abaa03d5"}, ] mkdocs-material = [ - {file = "mkdocs-material-7.3.2.tar.gz", hash = "sha256:02aeb2f9d9826b5c5ba4e320b4008bdc89f7b30ca00ded72ee43385a1690eaa4"}, - {file = "mkdocs_material-7.3.2-py2.py3-none-any.whl", hash = "sha256:a9b7c6432ebadd0e192e3b341b25bd41bd1c1b167061ea629a9887a1e4129176"}, + {file = "mkdocs-material-7.3.3.tar.gz", hash = "sha256:3444b681f47e62c0ec7166bfb6f12360a26c751224cd6d3b3816f7310827073f"}, + {file = "mkdocs_material-7.3.3-py2.py3-none-any.whl", hash = "sha256:56bcc4e43356b97caa07706b0f446a3d9c39eb9aa1ad64131686d555270fc65e"}, ] mkdocs-material-extensions = [ {file = "mkdocs-material-extensions-1.0.1.tar.gz", hash = "sha256:6947fb7f5e4291e3c61405bad3539d81e0b3cd62ae0d66ced018128af509c68f"}, diff --git a/pyproject.toml b/pyproject.toml index 775bd85b3ef..53dfa42b912 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ radon = "^5.1.0" xenon = "^0.8.0" flake8-eradicate = "^1.1.0" flake8-bugbear = "^21.9.2" -mkdocs-material = "^7.3.2" +mkdocs-material = "^7.3.3" mkdocs-git-revision-date-plugin = "^0.3.1" mike = "^0.6.0" mypy = "^0.910" From 0764521f518492b4aa19a14e484fc62f0ecfe288 Mon Sep 17 00:00:00 2001 From: Heitor Lessa Date: Tue, 12 Oct 2021 16:24:45 +0200 Subject: [PATCH 10/49] docs: improve public lambda layer wording, clipboard buttons (#762) --- docs/index.md | 117 ++++++++++++++++++++++++++------------------------ 1 file changed, 62 insertions(+), 55 deletions(-) diff --git a/docs/index.md b/docs/index.md index 2bd9053a183..c67ef2fc6fe 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,27 +21,21 @@ This project separates core utilities that will be available in other runtimes v ## Install -Powertools is available in PyPi. You can use your favourite dependency management tool to install it +Powertools is available in the following formats: -* [poetry](https://python-poetry.org/): `poetry add aws-lambda-powertools` -* [pip](https://pip.pypa.io/en/latest/index.html): `pip install aws-lambda-powertools` +??? info "Lambda Layer is a .zip file archive with Lambda Powertools pre-packaged in every AWS region. See what's inside!" + Change {region} to your AWS region, e.g. `eu-west-1` -**Quick hello world example using SAM CLI** + **`aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:3 --region {region}`** -=== "shell" - - ```bash - sam init --location https://github.com/aws-samples/cookiecutter-aws-sam-python - ``` +* **Lambda Layer**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:3**](#){: .copyMe} :clipboard: +* **PyPi**: **`pip install aws-lambda-powertools`** ### Lambda Layer -Powertools is also available as a Lambda Layer with public ARNs in each region or distributed via the [AWS Serverless Application Repository (SAR)](https://docs.aws.amazon.com/serverlessrepo/latest/devguide/what-is-serverlessrepo.html) to support semantic versioning. - -#### Public ARNs - -We build, release and distribute packaged Lambda Powertools layers for each region. This means you can copy a specific ARN and use it in your Lambda deployment. The layer region must be equal to the region of your lambda function. The public layers do not contain the `pydantic` library that is required for the `parser` utility. +Include Lambda Powertools in your function using the [AWS Lambda Console](https://console.aws.amazon.com/lambda){target="_blank"} or your preferred deployment framework. +!!! note "The public layers do not contain the `pydantic` library that is required for the `parser` utility; See [SAR](#sar) option instead." === "SAM" @@ -50,7 +44,7 @@ We build, release and distribute packaged Lambda Powertools layers for each regi Type: AWS::Serverless::Function Properties: Layers: - - arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPython:3 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:3 ``` === "Serverless framework" @@ -60,7 +54,7 @@ We build, release and distribute packaged Lambda Powertools layers for each regi main: handler: lambda_function.lambda_handler layers: - - arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPython:3 + - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPython:3 ``` === "CDK" @@ -70,16 +64,16 @@ We build, release and distribute packaged Lambda Powertools layers for each regi class SampleApp(core.Construct): - def __init__(self, scope: core.Construct, id_: str) -> None: + def __init__(self, scope: core.Construct, id_: str, env: core.Environment) -> None: super().__init__(scope, id_) aws_lambda.Function(self, 'sample-app-lambda', - runtime=aws_lambda.Runtime.PYTHON_3_8, + runtime=aws_lambda.Runtime.PYTHON_3_9, function_name='sample-lambda', code=aws_lambda.Code.asset('./src'), handler='app.handler', - layers: ["arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPython:3"] + layers: [f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPython:3"] ) ``` @@ -94,7 +88,7 @@ We build, release and distribute packaged Lambda Powertools layers for each regi } provider "aws" { - region = "us-east-1" + region = "{region}" } resource "aws_iam_role" "iam_for_lambda" { @@ -109,21 +103,20 @@ We build, release and distribute packaged Lambda Powertools layers for each regi "Principal": { "Service": "lambda.amazonaws.com" }, - "Effect": "Allow", - "Sid": "" + "Effect": "Allow" } ] } EOF - } + } resource "aws_lambda_function" "test_lambda" { filename = "lambda_function_payload.zip" function_name = "lambda_function_name" role = aws_iam_role.iam_for_lambda.arn handler = "index.test" - runtime = "python3.8" - layers = ["arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPython:3"] + runtime = "python3.9" + layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:3"] source_code_hash = filebase64sha256("lambda_function_payload.zip") } @@ -145,7 +138,7 @@ We build, release and distribute packaged Lambda Powertools layers for each regi ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:3 ❯ amplify push -y - + # Updating an existing function and add the layer ❯ amplify update function ? Select the Lambda function you want to update test2 @@ -157,40 +150,44 @@ We build, release and distribute packaged Lambda Powertools layers for each regi ? Do you want to edit the local lambda function now? No ``` -??? note "Layer ARN per region" - - !!! tip "Click to copy to clipboard" - - | Region | Version | Layer ARN - |---------------------------| ---------------------------| --------------------------- - | `us-east-1` | `1.21.0` |[arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `us-east-2` | `1.21.0` |[arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `us-west-1` | `1.21.0` |[arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `us-west-2` | `1.21.0` |[arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `ap-south-1` | `1.21.0` |[arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `ap-northeast-1` | `1.21.0` |[arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `ap-northeast-2` | `1.21.0` |[arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `ap-northeast-3` | `1.21.0` |[arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `ap-southeast-1` | `1.21.0` |[arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `ap-southeast-2` | `1.21.0` |[arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `eu-central-1` | `1.21.0` |[arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `eu-west-1` | `1.21.0` |[arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `eu-west-2` | `1.21.0` |[arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `eu-west-3` | `1.21.0` |[arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `eu-north-1` | `1.21.0` |[arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `ca-central-1` | `1.21.0` |[arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} - | `sa-east-1` | `1.21.0` |[arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#) {: .copyMe} +??? note "Expand to copy any regional Lambda Layer ARN" + + | Region | Layer ARN + |--------------------------- | --------------------------- + | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: #### SAR +Serverless Application Repository (SAR) App deploys a CloudFormation stack with a copy of our Lambda Layer in your AWS account and region. + +Despite having more steps compared to the [public Layer ARN](#lambda-layer) option, the benefit is that you can specify a semantic version you want to use. + | App | ARN | Description |----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- -| [aws-lambda-powertools-python-layer](https://serverlessrepo.aws.amazon.com/applications/eu-west-1/057560766410/aws-lambda-powertools-python-layer) | arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer | Core dependencies only; sufficient for nearly all utilities. -| [aws-lambda-powertools-python-layer-extras](https://serverlessrepo.aws.amazon.com/applications/eu-west-1/057560766410/aws-lambda-powertools-python-layer-extras) | arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer-extras | Core plus extra dependencies such as `pydantic` that is required by `parser` utility. +| [aws-lambda-powertools-python-layer](https://serverlessrepo.aws.amazon.com/applications/eu-west-1/057560766410/aws-lambda-powertools-python-layer) | [arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer](#){: .copyMe} :clipboard: | Core dependencies only; sufficient for nearly all utilities. +| [aws-lambda-powertools-python-layer-extras](https://serverlessrepo.aws.amazon.com/applications/eu-west-1/057560766410/aws-lambda-powertools-python-layer-extras) | [arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer-extras](#){: .copyMe} :clipboard: | Core plus extra dependencies such as `pydantic` that is required by `parser` utility. !!! warning **Layer-extras** does not support Python 3.6 runtime. This layer also includes all extra dependencies: `22.4MB zipped`, `~155MB unzipped`. +!!! tip "You can create a shared Lambda Layers stack and make this along with other account level layers stack." + If using SAM, you can include this SAR App as part of your shared Layers stack, and lock to a specific semantic version. Once deployed, it'll be available across the account this is deployed to. === "SAM" @@ -201,7 +198,7 @@ If using SAM, you can include this SAR App as part of your shared Layers stack, Properties: Location: ApplicationId: arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer - SemanticVersion: 1.17.0 # change to latest semantic version available in SAR + SemanticVersion: 1.21.1 # change to latest semantic version available in SAR MyLambdaFunction: Type: AWS::Serverless::Function @@ -222,14 +219,14 @@ If using SAM, you can include this SAR App as part of your shared Layers stack, resources: Transform: AWS::Serverless-2016-10-31 - Resources: + Resources:**** AwsLambdaPowertoolsPythonLayer: Type: AWS::Serverless::Application Properties: Location: ApplicationId: arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer # Find latest from github.com/awslabs/aws-lambda-powertools-python/releases - SemanticVersion: 1.17.0 + SemanticVersion: 1.21.1 ``` === "CDK" @@ -239,7 +236,7 @@ If using SAM, you can include this SAR App as part of your shared Layers stack, POWERTOOLS_BASE_NAME = 'AWSLambdaPowertools' # Find latest from github.com/awslabs/aws-lambda-powertools-python/releases - POWERTOOLS_VER = '1.17.0' + POWERTOOLS_VER = '1.21.1' POWERTOOLS_ARN = 'arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer' class SampleApp(core.Construct): @@ -391,6 +388,16 @@ You can fetch available versions via SAR API with: --application-id arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer ``` +## Quick getting started + +**Quick hello world example using SAM CLI** + +=== "shell" + + ```bash + sam init --location https://github.com/aws-samples/cookiecutter-aws-sam-python + ``` + ## Features | Utility | Description From 5ced73c37ffefc421a6340396babd319a4780ad8 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 12 Oct 2021 18:03:32 +0200 Subject: [PATCH 11/49] chore: conditional to publish docs only --- .github/workflows/publish.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5a3c246caec..4f00d9bdab2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -38,6 +38,10 @@ on: publish_version: description: 'Version to publish, e.g. v1.13.0' required: true + publish_docs_only: + description: 'Build and publish docs only' + required: false + default: 'false' jobs: release: @@ -51,32 +55,39 @@ jobs: with: python-version: "3.8" - name: Set release notes tag + if: ${{ github.event.inputs.publish_docs_only == "false" }} run: | RELEASE_TAG_VERSION=${{ github.event.release.tag_name }} # Replace publishing version if the workflow was triggered manually test -n ${RELEASE_TAG_VERSION} && RELEASE_TAG_VERSION=${{ github.event.inputs.publish_version }} echo "RELEASE_TAG_VERSION=${RELEASE_TAG_VERSION:1}" >> $GITHUB_ENV - name: Ensure new version is also set in pyproject and CHANGELOG + if: ${{ github.event.inputs.publish_docs_only == "false" }} run: | grep --regexp "${RELEASE_TAG_VERSION}" CHANGELOG.md grep --regexp "version \= \"${RELEASE_TAG_VERSION}\"" pyproject.toml - name: Install dependencies run: make dev - name: Run all tests, linting and baselines + if: ${{ github.event.inputs.publish_docs_only == "false" }} run: make pr - name: Build python package and wheel + if: ${{ github.event.inputs.publish_docs_only == "false" }} run: poetry build - name: Upload to PyPi test + if: ${{ github.event.inputs.publish_docs_only == "false" }} run: make release-test env: PYPI_USERNAME: __token__ PYPI_TEST_TOKEN: ${{ secrets.PYPI_TEST_TOKEN }} - name: Upload to PyPi prod + if: ${{ github.event.inputs.publish_docs_only == "false" }} run: make release-prod env: PYPI_USERNAME: __token__ PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} - name: publish lambda layer in SAR by triggering the internal codepipeline + if: ${{ github.event.inputs.publish_docs_only == "false" }} run: | aws ssm put-parameter --name "powertools-python-release-version" --value $RELEASE_TAG_VERSION --overwrite aws codepipeline start-pipeline-execution --name ${{ secrets.CODEPIPELINE_NAME }} @@ -88,7 +99,7 @@ jobs: - name: Setup doc deploy run: | git config --global user.name Docs deploy - git config --global user.email docs@dummy.bot.com + git config --global user.email aws-devax-open-source@amazon.com - name: Build docs website and API reference run: | make release-docs VERSION=${RELEASE_TAG_VERSION} ALIAS="latest" @@ -113,6 +124,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + if: ${{ github.event.inputs.publish_docs_only == "false" }} - name: Sync master from detached head # If version matches CHANGELOG and pyproject.toml # If it passes all checks, successfully releases to test and prod From 33841151ad986304d613203e3e0d6cbb5f820fd4 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 12 Oct 2021 18:28:31 +0200 Subject: [PATCH 12/49] chore: conditional to publish docs only attempt 2 --- .github/workflows/publish.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4f00d9bdab2..20fe0ad3abe 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -55,39 +55,39 @@ jobs: with: python-version: "3.8" - name: Set release notes tag - if: ${{ github.event.inputs.publish_docs_only == "false" }} + if: ${{ github.event.inputs.publish_docs_only == false }} run: | RELEASE_TAG_VERSION=${{ github.event.release.tag_name }} # Replace publishing version if the workflow was triggered manually test -n ${RELEASE_TAG_VERSION} && RELEASE_TAG_VERSION=${{ github.event.inputs.publish_version }} echo "RELEASE_TAG_VERSION=${RELEASE_TAG_VERSION:1}" >> $GITHUB_ENV - name: Ensure new version is also set in pyproject and CHANGELOG - if: ${{ github.event.inputs.publish_docs_only == "false" }} + if: ${{ github.event.inputs.publish_docs_only == false }} run: | grep --regexp "${RELEASE_TAG_VERSION}" CHANGELOG.md grep --regexp "version \= \"${RELEASE_TAG_VERSION}\"" pyproject.toml - name: Install dependencies run: make dev - name: Run all tests, linting and baselines - if: ${{ github.event.inputs.publish_docs_only == "false" }} + if: ${{ github.event.inputs.publish_docs_only == false }} run: make pr - name: Build python package and wheel - if: ${{ github.event.inputs.publish_docs_only == "false" }} + if: ${{ github.event.inputs.publish_docs_only == false }} run: poetry build - name: Upload to PyPi test - if: ${{ github.event.inputs.publish_docs_only == "false" }} + if: ${{ github.event.inputs.publish_docs_only == false }} run: make release-test env: PYPI_USERNAME: __token__ PYPI_TEST_TOKEN: ${{ secrets.PYPI_TEST_TOKEN }} - name: Upload to PyPi prod - if: ${{ github.event.inputs.publish_docs_only == "false" }} + if: ${{ github.event.inputs.publish_docs_only == false }} run: make release-prod env: PYPI_USERNAME: __token__ PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} - name: publish lambda layer in SAR by triggering the internal codepipeline - if: ${{ github.event.inputs.publish_docs_only == "false" }} + if: ${{ github.event.inputs.publish_docs_only == false }} run: | aws ssm put-parameter --name "powertools-python-release-version" --value $RELEASE_TAG_VERSION --overwrite aws codepipeline start-pipeline-execution --name ${{ secrets.CODEPIPELINE_NAME }} @@ -124,7 +124,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - if: ${{ github.event.inputs.publish_docs_only == "false" }} + if: ${{ github.event.inputs.publish_docs_only == false }} - name: Sync master from detached head # If version matches CHANGELOG and pyproject.toml # If it passes all checks, successfully releases to test and prod From 905cbadafe124a65cadd5cd41c7225f6aec92107 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 12 Oct 2021 18:33:00 +0200 Subject: [PATCH 13/49] chore: conditional to publish docs only attempt 3 --- .github/workflows/publish.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 20fe0ad3abe..5a54eee2d7c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -55,7 +55,6 @@ jobs: with: python-version: "3.8" - name: Set release notes tag - if: ${{ github.event.inputs.publish_docs_only == false }} run: | RELEASE_TAG_VERSION=${{ github.event.release.tag_name }} # Replace publishing version if the workflow was triggered manually From 9a85973c3557fff81f06569cf053518c54f3f274 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 15 Oct 2021 10:42:38 +0200 Subject: [PATCH 14/49] fix(ci): skip sync master on docs hotfix --- .github/workflows/publish.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5a54eee2d7c..f49fcc5ceff 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -30,6 +30,14 @@ name: Publish to PyPi # 2. Use the version released under Releases e.g. v1.13.0 # +# +# === Documentation hotfix === +# +# 1. Trigger "Publish to PyPi" workflow manually: https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow +# 2. Use the latest version released under Releases e.g. v1.21.1 +# 3. Set `Build and publish docs only` field to `true` + + on: release: types: [published] @@ -121,9 +129,9 @@ jobs: sync_master: needs: release runs-on: ubuntu-latest + if: ${{ github.event.inputs.publish_docs_only == false }} steps: - uses: actions/checkout@v2 - if: ${{ github.event.inputs.publish_docs_only == false }} - name: Sync master from detached head # If version matches CHANGELOG and pyproject.toml # If it passes all checks, successfully releases to test and prod From 1f78df85f624e0de96dd6897973c20fce4e198fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Oct 2021 14:46:54 +0000 Subject: [PATCH 15/49] chore(deps): bump boto3 from 1.18.59 to 1.18.61 (#766) Bumps [boto3](https://github.com/boto/boto3) from 1.18.59 to 1.18.61.
Changelog

Sourced from boto3's changelog.

1.18.61

  • api-change:config: [botocore] Adding Config support for AWS::OpenSearch::Domain
  • api-change:ec2: [botocore] This release adds support for additional VPC Flow Logs delivery options to S3, such as Apache Parquet formatted files, Hourly partitions and Hive-compatible S3 prefixes
  • api-change:storagegateway: [botocore] Adding support for Audit Logs on NFS shares and Force Closing Files on SMB shares.
  • api-change:workmail: [botocore] This release adds APIs for adding, removing and retrieving details of mail domains
  • api-change:kinesisanalyticsv2: [botocore] Support for Apache Flink 1.13 in Kinesis Data Analytics. Changed the required status of some Update properties to better fit the corresponding Create properties.

1.18.60

  • api-change:cloudsearch: [botocore] Adds an additional validation exception for Amazon CloudSearch configuration APIs for better error handling.
  • api-change:ecs: [botocore] Documentation only update to address tickets.
  • api-change:mediatailor: [botocore] MediaTailor now supports ad prefetching.
  • api-change:ec2: [botocore] EncryptionSupport for InstanceStorageInfo added to DescribeInstanceTypes API
Commits
  • 5670e64 Merge branch 'release-1.18.61'
  • b77bb65 Bumping version to 1.18.61
  • a9d9172 Add changelog entries from botocore
  • ef35d5d Merge branch 'release-1.18.60'
  • 868088c Merge branch 'release-1.18.60' into develop
  • 5734837 Bumping version to 1.18.60
  • e766ff1 Add changelog entries from botocore
  • 62ceb05 Move from 3.10-dev to 3.10 (#3036)
  • 4f667ba Update documentation cookie preferences (#3035)
  • 31531e1 Merge branch 'release-1.18.59' into develop
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=boto3&package-manager=pip&previous-version=1.18.59&new-version=1.18.61)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- poetry.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 90d888b7cd0..ab8669c3c04 100644 --- a/poetry.lock +++ b/poetry.lock @@ -81,14 +81,14 @@ d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] name = "boto3" -version = "1.18.59" +version = "1.18.61" description = "The AWS SDK for Python" category = "main" optional = false python-versions = ">= 3.6" [package.dependencies] -botocore = ">=1.21.59,<1.22.0" +botocore = ">=1.21.61,<1.22.0" jmespath = ">=0.7.1,<1.0.0" s3transfer = ">=0.5.0,<0.6.0" @@ -97,7 +97,7 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.21.59" +version = "1.21.61" description = "Low-level, data-driven core of boto 3." category = "main" optional = false @@ -1082,12 +1082,12 @@ black = [ {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] boto3 = [ - {file = "boto3-1.18.59-py3-none-any.whl", hash = "sha256:daa721ccad79ed8e23a4b662eedce59ba0585e5b336bd6a9cd8e4fec40cd2db6"}, - {file = "boto3-1.18.59.tar.gz", hash = "sha256:40e948276010e5eb23f0625afe9b323146e16a45dbeade0acd558eababd8b8ce"}, + {file = "boto3-1.18.61-py3-none-any.whl", hash = "sha256:d2a8f8cd0a53093b2f1ab5a4b233533f1aa04e751209266597a05f50a46f6683"}, + {file = "boto3-1.18.61.tar.gz", hash = "sha256:08dc897299ecc9eef6df3cd296864154dfbc9f78edb8995b93258b59d747cbdc"}, ] botocore = [ - {file = "botocore-1.21.59-py3-none-any.whl", hash = "sha256:ebcb8f0b0e9b56d275073746ea9bb8b60597517b2bfa4d6ebdc64b9bcf4422e0"}, - {file = "botocore-1.21.59.tar.gz", hash = "sha256:c947215f45653609682dc448195a3bfcf0f7da193fd7b17b739656db5442221a"}, + {file = "botocore-1.21.61-py3-none-any.whl", hash = "sha256:eda50985c229b01c7de6f0a0ccb561dfa52ded06c8c59044f624133a33357b94"}, + {file = "botocore-1.21.61.tar.gz", hash = "sha256:92ae0ca56a6582bddf42aca199ac3f67579910860bcd86166ae10f7a83f8841b"}, ] certifi = [ {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, From a9e206774f6fa4310c71d9ef6d25cc4cfce21f33 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Oct 2021 14:46:59 +0000 Subject: [PATCH 16/49] chore(deps-dev): bump coverage from 6.0.1 to 6.0.2 (#764) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [coverage](https://github.com/nedbat/coveragepy) from 6.0.1 to 6.0.2.
Changelog

Sourced from coverage's changelog.

Version 6.0.2 — 2021-10-11

  • Namespace packages being measured weren't properly handled by the new code that ignores third-party packages. If the namespace package was installed, it was ignored as a third-party package. That problem (issue 1231_) is now fixed.

  • Packages named as "source packages" (with source, or source_pkgs, or pytest-cov's --cov) might have been only partially measured. Their top-level statements could be marked as unexecuted, because they were imported by coverage.py before measurement began (issue 1232_). This is now fixed, but the package will be imported twice, once by coverage.py, then again by your test suite. This could cause problems if importing the package has side effects.

  • The :meth:.CoverageData.contexts_by_lineno method was documented to return a dict, but was returning a defaultdict. Now it returns a plain dict. It also no longer returns negative numbered keys.

.. _issue 1231: nedbat/coveragepy#1231 .. _issue 1232: nedbat/coveragepy#1232

.. _changes_601:

Commits
  • a3921d2 build: prep for 6.0.2
  • 19bb1f8 docs: sample HTML from 6.0.2
  • 2603597 fix: source modules need to be re-imported. #1232
  • fdaa822 test: add more tests of run_python_file
  • cedd319 build: clean up the Makefile a bit
  • d3f46d2 test: add a test of hash-based pyc files
  • bcff84f refactor: remove qualname code that was only for Python 2
  • 9b54389 fix: make third-party detection work with namespace packages. #1231
  • 27db7b4 style: the name of the matchers don't need quotes in the reprs
  • a05710e refactor: remove some left over test prints
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=coverage&package-manager=pip&previous-version=6.0.1&new-version=6.0.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- poetry.lock | 68 ++++++++++++++++++++++++++--------------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/poetry.lock b/poetry.lock index ab8669c3c04..e13747793c1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -149,7 +149,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.0.1" +version = "6.0.2" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -1106,39 +1106,39 @@ colorama = [ {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] coverage = [ - {file = "coverage-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:abe8207dfb8a61ded9cd830d26c1073c8218fc0ae17eb899cfe8ec0fafae6e22"}, - {file = "coverage-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83faa3692e8306b20293889714fdf573d10ef5efc5843bd7c7aea6971487bd6a"}, - {file = "coverage-6.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f82a17f2a77958f3eef40ad385fc82d4c6ba9a77a51a174efe03ce75daebbc16"}, - {file = "coverage-6.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b06f4f1729e2963281d9cd6e65e6976bf27b44d4c07ac5b47223ce45f822cec"}, - {file = "coverage-6.0.1-cp310-cp310-win32.whl", hash = "sha256:7600fac458f74c68b097379f76f3a6e3a630493fc7fc94b6508fedd9d498c194"}, - {file = "coverage-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:2c5f39d1556e75fc3c4fb071f9e7cfa618895a999a0de763a541d730775d0d5f"}, - {file = "coverage-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3edbb3ec580c73e5a264f5d04f30245bc98eff1a26765d46c5c65134f0a0e2f7"}, - {file = "coverage-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea452a2d83964d08232ade470091015e7ab9b8f53acbec10f2210fbab4ce7e43"}, - {file = "coverage-6.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1770d24f45f1f2daeae34cfa3b33fcb29702153544cd2ad40d58399dd4ff53b5"}, - {file = "coverage-6.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ad7182a82843f9f85487f44567c8c688f16c906bdb8d0e44ae462aed61cb8f1b"}, - {file = "coverage-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:9d242a2434801ef5125330deddb4cddba8990c9a49b3dec99dca17dd7eefba5a"}, - {file = "coverage-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e66c50f0ab445fec920a9f084914ea1776a809e3016c3738519048195f851bbb"}, - {file = "coverage-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5b1ceacb86e0a9558061dcc6baae865ed25933ea57effea644f21657cdce19bc"}, - {file = "coverage-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e15ab5afbee34abf716fece80ea33ea09a82e7450512f022723b1a82ec9a4e"}, - {file = "coverage-6.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6873f3f954d3e3ab8b1881f4e5307cc19f70c9f931c41048d9f7e6fd946eabe7"}, - {file = "coverage-6.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0898d6948b31df13391cd40568de8f35fa5901bc922c5ae05cf070587cb9c666"}, - {file = "coverage-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9c416ba03844608f45661a5b48dc59c6b5e89956efe388564dd138ca8caf540b"}, - {file = "coverage-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:66fe33e9e0df58675e08e83fe257f89e7f625e7633ea93d0872154e09cce2724"}, - {file = "coverage-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:353a50f123f0185cdb7a1e1e3e2cfb9d1fd7e293cfaf68eedaf5bd8e02e3ec32"}, - {file = "coverage-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b81a4e667c45b13658b84f9b8f1d32ef86d5405fabcbd181b76b9e51d295f397"}, - {file = "coverage-6.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:17426808e8e0824f864876312d41961223bf5e503bf8f1f846735279a60ea345"}, - {file = "coverage-6.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e11cca9eb5c9b3eaad899728ee2ce916138399ee8cbbccaadc1871fecb750827"}, - {file = "coverage-6.0.1-cp38-cp38-win32.whl", hash = "sha256:0a7e55cc9f7efa22d5cc9966276ec7a40a8803676f6ccbfdc06a486fba9aa9ee"}, - {file = "coverage-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:4eb9cd910ca8e243f930243a9940ea1a522e32435d15668445753d087c30ee12"}, - {file = "coverage-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:83682b73785d2e078e0b5f63410b8125b122e1a22422640c57edd4011c950f3e"}, - {file = "coverage-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b45f89a8ef65c29195f8f28dbe215f44ccb29d934f3e862d2a5c12e38698a793"}, - {file = "coverage-6.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:73880a80fad0597eca43e213e5e1711bf6c0fcdb7eb6b01b3b17841ebe5a7f8d"}, - {file = "coverage-6.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f398d38e6ebc2637863db1d7be3d4f9c5174e7d24bb3b0716cdb1f204669cbcf"}, - {file = "coverage-6.0.1-cp39-cp39-win32.whl", hash = "sha256:1864bdf9b2ccb43e724051bc23a1c558daf101ad4488ede1945f2a8be1facdad"}, - {file = "coverage-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:c9c413c4397d4cdc7ca89286158d240ce524f9667b52c9a64dd7e13d16cf8815"}, - {file = "coverage-6.0.1-pp36-none-any.whl", hash = "sha256:65da6e3e8325291f012921bbf71fea0a97824e1c573981871096aac6e2cf0ec5"}, - {file = "coverage-6.0.1-pp37-none-any.whl", hash = "sha256:07efe1fbd72e67df026ad5109bcd216acbbd4a29d5208b3dab61779bae6b7b26"}, - {file = "coverage-6.0.1.tar.gz", hash = "sha256:3490ff6dbf3f7accf0750136ed60ae1f487bccc1f097740e3b21262bc9c89854"}, + {file = "coverage-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1549e1d08ce38259de2bc3e9a0d5f3642ff4a8f500ffc1b2df73fd621a6cdfc0"}, + {file = "coverage-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcae10fccb27ca2a5f456bf64d84110a5a74144be3136a5e598f9d9fb48c0caa"}, + {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:53a294dc53cfb39c74758edaa6305193fb4258a30b1f6af24b360a6c8bd0ffa7"}, + {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8251b37be1f2cd9c0e5ccd9ae0380909c24d2a5ed2162a41fcdbafaf59a85ebd"}, + {file = "coverage-6.0.2-cp310-cp310-win32.whl", hash = "sha256:db42baa892cba723326284490283a68d4de516bfb5aaba369b4e3b2787a778b7"}, + {file = "coverage-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:bbffde2a68398682623d9dd8c0ca3f46fda074709b26fcf08ae7a4c431a6ab2d"}, + {file = "coverage-6.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:60e51a3dd55540bec686d7fff61b05048ca31e804c1f32cbb44533e6372d9cc3"}, + {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a6a9409223a27d5ef3cca57dd7cd4dfcb64aadf2fad5c3b787830ac9223e01a"}, + {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4b34ae4f51bbfa5f96b758b55a163d502be3dcb24f505d0227858c2b3f94f5b9"}, + {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3bbda1b550e70fa6ac40533d3f23acd4f4e9cb4e6e77251ce77fdf41b3309fb2"}, + {file = "coverage-6.0.2-cp36-cp36m-win32.whl", hash = "sha256:4e28d2a195c533b58fc94a12826f4431726d8eb029ac21d874345f943530c122"}, + {file = "coverage-6.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a82d79586a0a4f5fd1cf153e647464ced402938fbccb3ffc358c7babd4da1dd9"}, + {file = "coverage-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3be1206dc09fb6298de3fce70593e27436862331a85daee36270b6d0e1c251c4"}, + {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cd3828bbe1a40070c11fe16a51df733fd2f0cb0d745fb83b7b5c1f05967df7"}, + {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d036dc1ed8e1388e995833c62325df3f996675779541f682677efc6af71e96cc"}, + {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04560539c19ec26995ecfb3d9307ff154fbb9a172cb57e3b3cfc4ced673103d1"}, + {file = "coverage-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:e4fb7ced4d9dec77d6cf533acfbf8e1415fe799430366affb18d69ee8a3c6330"}, + {file = "coverage-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:77b1da5767ed2f44611bc9bc019bc93c03fa495728ec389759b6e9e5039ac6b1"}, + {file = "coverage-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61b598cbdbaae22d9e34e3f675997194342f866bb1d781da5d0be54783dce1ff"}, + {file = "coverage-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36e9040a43d2017f2787b28d365a4bb33fcd792c7ff46a047a04094dc0e2a30d"}, + {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9f1627e162e3864a596486774876415a7410021f4b67fd2d9efdf93ade681afc"}, + {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7a0b42db2a47ecb488cde14e0f6c7679a2c5a9f44814393b162ff6397fcdfbb"}, + {file = "coverage-6.0.2-cp38-cp38-win32.whl", hash = "sha256:a1b73c7c4d2a42b9d37dd43199c5711d91424ff3c6c22681bc132db4a4afec6f"}, + {file = "coverage-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:1db67c497688fd4ba85b373b37cc52c50d437fd7267520ecd77bddbd89ea22c9"}, + {file = "coverage-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f2f184bf38e74f152eed7f87e345b51f3ab0b703842f447c22efe35e59942c24"}, + {file = "coverage-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1cf1deb3d5544bd942356364a2fdc8959bad2b6cf6eb17f47d301ea34ae822"}, + {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad9b8c1206ae41d46ec7380b78ba735ebb77758a650643e841dd3894966c31d0"}, + {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:381d773d896cc7f8ba4ff3b92dee4ed740fb88dfe33b6e42efc5e8ab6dfa1cfe"}, + {file = "coverage-6.0.2-cp39-cp39-win32.whl", hash = "sha256:424c44f65e8be58b54e2b0bd1515e434b940679624b1b72726147cfc6a9fc7ce"}, + {file = "coverage-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:abbff240f77347d17306d3201e14431519bf64495648ca5a49571f988f88dee9"}, + {file = "coverage-6.0.2-pp36-none-any.whl", hash = "sha256:7092eab374346121805fb637572483270324407bf150c30a3b161fc0c4ca5164"}, + {file = "coverage-6.0.2-pp37-none-any.whl", hash = "sha256:30922626ce6f7a5a30bdba984ad21021529d3d05a68b4f71ea3b16bda35b8895"}, + {file = "coverage-6.0.2.tar.gz", hash = "sha256:6807947a09510dc31fa86f43595bf3a14017cd60bf633cc746d52141bfa6b149"}, ] dataclasses = [ {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, From 7a7ba0a2ce7531d93cacfe36afb42bbad7ecc4be Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Fri, 15 Oct 2021 08:00:23 -0700 Subject: [PATCH 17/49] refactor(apigateway): Add BaseRouter and duplicate route check (#757) --- .../event_handler/api_gateway.py | 240 +++++++++--------- .../utilities/validation/exceptions.py | 4 +- .../event_handler/test_api_gateway.py | 27 ++ tests/functional/test_logger.py | 7 +- 4 files changed, 149 insertions(+), 129 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index d3a79761556..dce520c147d 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -4,11 +4,13 @@ import os import re import traceback +import warnings import zlib +from abc import ABC, abstractmethod from enum import Enum -from functools import partial, wraps +from functools import partial from http import HTTPStatus -from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Set, Union from aws_lambda_powertools.event_handler import content_types from aws_lambda_powertools.event_handler.exceptions import ServiceError @@ -227,78 +229,20 @@ def build(self, event: BaseProxyEvent, cors: Optional[CORSConfig] = None) -> Dic } -class ApiGatewayResolver: - """API Gateway and ALB proxy resolver - - Examples - -------- - Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator - - ```python - from aws_lambda_powertools import Tracer - from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver - - tracer = Tracer() - app = ApiGatewayResolver() - - @app.get("/get-call") - def simple_get(): - return {"message": "Foo"} - - @app.post("/post-call") - def simple_post(): - post_data: dict = app.current_event.json_body - return {"message": post_data["value"]} - - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) - ``` - """ - +class BaseRouter(ABC): current_event: BaseProxyEvent lambda_context: LambdaContext - def __init__( + @abstractmethod + def route( self, - proxy_type: Enum = ProxyEventType.APIGatewayProxyEvent, - cors: Optional[CORSConfig] = None, - debug: Optional[bool] = None, - serializer: Optional[Callable[[Dict], str]] = None, - strip_prefixes: Optional[List[str]] = None, + rule: str, + method: Any, + cors: Optional[bool] = None, + compress: bool = False, + cache_control: Optional[str] = None, ): - """ - Parameters - ---------- - proxy_type: ProxyEventType - Proxy request type, defaults to API Gateway V1 - cors: CORSConfig - Optionally configure and enabled CORS. Not each route will need to have to cors=True - debug: Optional[bool] - Enables debug mode, by default False. Can be also be enabled by "POWERTOOLS_EVENT_HANDLER_DEBUG" - environment variable - serializer : Callable, optional - function to serialize `obj` to a JSON formatted `str`, by default json.dumps - strip_prefixes: List[str], optional - optional list of prefixes to be removed from the request path before doing the routing. This is often used - with api gateways with multiple custom mappings. - """ - self._proxy_type = proxy_type - self._routes: List[Route] = [] - self._cors = cors - self._cors_enabled: bool = cors is not None - self._cors_methods: Set[str] = {"OPTIONS"} - self._debug = resolve_truthy_env_var_choice( - env=os.getenv(constants.EVENT_HANDLER_DEBUG_ENV, "false"), choice=debug - ) - self._strip_prefixes = strip_prefixes - - # Allow for a custom serializer or a concise json serialization - self._serializer = serializer or partial(json.dumps, separators=(",", ":"), cls=Encoder) - - if self._debug: - # Always does a pretty print when in debug mode - self._serializer = partial(json.dumps, indent=4, cls=Encoder) + raise NotImplementedError() def get(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): """Get route decorator with GET `method` @@ -434,6 +378,78 @@ def lambda_handler(event, context): """ return self.route(rule, "PATCH", cors, compress, cache_control) + +class ApiGatewayResolver(BaseRouter): + """API Gateway and ALB proxy resolver + + Examples + -------- + Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator + + ```python + from aws_lambda_powertools import Tracer + from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver + + tracer = Tracer() + app = ApiGatewayResolver() + + @app.get("/get-call") + def simple_get(): + return {"message": "Foo"} + + @app.post("/post-call") + def simple_post(): + post_data: dict = app.current_event.json_body + return {"message": post_data["value"]} + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ + + def __init__( + self, + proxy_type: Enum = ProxyEventType.APIGatewayProxyEvent, + cors: Optional[CORSConfig] = None, + debug: Optional[bool] = None, + serializer: Optional[Callable[[Dict], str]] = None, + strip_prefixes: Optional[List[str]] = None, + ): + """ + Parameters + ---------- + proxy_type: ProxyEventType + Proxy request type, defaults to API Gateway V1 + cors: CORSConfig + Optionally configure and enabled CORS. Not each route will need to have to cors=True + debug: Optional[bool] + Enables debug mode, by default False. Can be also be enabled by "POWERTOOLS_EVENT_HANDLER_DEBUG" + environment variable + serializer : Callable, optional + function to serialize `obj` to a JSON formatted `str`, by default json.dumps + strip_prefixes: List[str], optional + optional list of prefixes to be removed from the request path before doing the routing. This is often used + with api gateways with multiple custom mappings. + """ + self._proxy_type = proxy_type + self._routes: List[Route] = [] + self._route_keys: List[str] = [] + self._cors = cors + self._cors_enabled: bool = cors is not None + self._cors_methods: Set[str] = {"OPTIONS"} + self._debug = resolve_truthy_env_var_choice( + env=os.getenv(constants.EVENT_HANDLER_DEBUG_ENV, "false"), choice=debug + ) + self._strip_prefixes = strip_prefixes + + # Allow for a custom serializer or a concise json serialization + self._serializer = serializer or partial(json.dumps, separators=(",", ":"), cls=Encoder) + + if self._debug: + # Always does a pretty print when in debug mode + self._serializer = partial(json.dumps, indent=4, cls=Encoder) + def route( self, rule: str, @@ -451,6 +467,10 @@ def register_resolver(func: Callable): else: cors_enabled = cors self._routes.append(Route(method, self._compile_regex(rule), func, cors_enabled, compress, cache_control)) + route_key = method + rule + if route_key in self._route_keys: + warnings.warn(f"A route like this was already registered. method: '{method}' rule: '{rule}'") + self._route_keys.append(route_key) if cors_enabled: logger.debug(f"Registering method {method.upper()} to Allow Methods in CORS") self._cors_methods.add(method.upper()) @@ -474,8 +494,8 @@ def resolve(self, event, context) -> Dict[str, Any]: """ if self._debug: print(self._json_dump(event)) - self.current_event = self._to_proxy_event(event) - self.lambda_context = context + BaseRouter.current_event = self._to_proxy_event(event) + BaseRouter.lambda_context = context return self._resolve().build(self.current_event, self._cors) def __call__(self, event, context) -> Any: @@ -632,71 +652,41 @@ 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 defined in a router""" - router._app = self - for route, func in router.api.items(): - if prefix and route[0] == "/": - route = (prefix, *route[1:]) - elif prefix: - route = (f"{prefix}{route[0]}", *route[1:]) - self.route(*route)(func()) - + """Adds all routes defined in a router -class Router: - """Router helper class to allow splitting ApiGatewayResolver into multiple files""" + Parameters + ---------- + router : Router + The Router containing a list of routes to be registered after the existing routes + prefix : str, optional + An optional prefix to be added to the originally defined rule + """ + for route, func in router._routes.items(): + if prefix: + rule = route[0] + rule = prefix if rule == "/" else f"{prefix}{rule}" + route = (rule, *route[1:]) - _app: ApiGatewayResolver + self.route(*route)(func) - def __init__(self): - self.api: Dict[tuple, Callable] = {} - @property - def current_event(self) -> BaseProxyEvent: - return self._app.current_event +class Router(BaseRouter): + """Router helper class to allow splitting ApiGatewayResolver into multiple files""" - @property - def lambda_context(self) -> LambdaContext: - return self._app.lambda_context + def __init__(self): + self._routes: Dict[tuple, Callable] = {} def route( self, rule: str, - method: Union[str, Tuple[str], List[str]], + method: Union[str, List[str]], cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None, ): - def actual_decorator(func: Callable): - @wraps(func) - def wrapper(): - def inner_wrapper(**kwargs): - return func(**kwargs) - - return inner_wrapper - - if isinstance(method, (list, tuple)): - for item in method: - self.api[(rule, item, cors, compress, cache_control)] = wrapper - else: - self.api[(rule, method, cors, compress, cache_control)] = wrapper - - return actual_decorator - - def get(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): - return self.route(rule, "GET", cors, compress, cache_control) - - def post(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): - return self.route(rule, "POST", cors, compress, cache_control) - - def put(self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None): - return self.route(rule, "PUT", cors, compress, cache_control) - - def delete( - self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None - ): - return self.route(rule, "DELETE", cors, compress, cache_control) + def register_route(func: Callable): + methods = method if isinstance(method, list) else [method] + for item in methods: + self._routes[(rule, item, cors, compress, cache_control)] = func - def patch( - self, rule: str, cors: Optional[bool] = None, compress: bool = False, cache_control: Optional[str] = None - ): - return self.route(rule, "PATCH", cors, compress, cache_control) + return register_route diff --git a/aws_lambda_powertools/utilities/validation/exceptions.py b/aws_lambda_powertools/utilities/validation/exceptions.py index 7c719ca3119..2f13ff64188 100644 --- a/aws_lambda_powertools/utilities/validation/exceptions.py +++ b/aws_lambda_powertools/utilities/validation/exceptions.py @@ -8,7 +8,7 @@ class SchemaValidationError(Exception): def __init__( self, - message: str, + message: Optional[str] = None, validation_message: Optional[str] = None, name: Optional[str] = None, path: Optional[List] = None, @@ -21,7 +21,7 @@ def __init__( Parameters ---------- - message : str + message : str, optional Powertools formatted error message validation_message : str, optional Containing human-readable information what is wrong diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index afc979065f8..f4543fa300c 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -994,3 +994,30 @@ def patch_func(): assert result["statusCode"] == 404 # AND cors headers are not returned assert "Access-Control-Allow-Origin" not in result["headers"] + + +def test_duplicate_routes(): + # GIVEN a duplicate routes + app = ApiGatewayResolver() + router = Router() + + @router.get("/my/path") + def get_func_duplicate(): + raise RuntimeError() + + @app.get("/my/path") + def get_func(): + return {} + + @router.get("/my/path") + def get_func_another_duplicate(): + raise RuntimeError() + + app.include_router(router) + + # WHEN calling the handler + result = app(LOAD_GW_EVENT, None) + + # THEN only execute the first registered route + # AND print warnings + assert result["statusCode"] == 200 diff --git a/tests/functional/test_logger.py b/tests/functional/test_logger.py index a8d92c05257..3c9a8a54189 100644 --- a/tests/functional/test_logger.py +++ b/tests/functional/test_logger.py @@ -537,11 +537,11 @@ def format(self, record: logging.LogRecord) -> str: # noqa: A003 logger = Logger(service=service_name, stream=stdout, logger_formatter=custom_formatter) # WHEN a lambda function is decorated with logger - @logger.inject_lambda_context + @logger.inject_lambda_context(correlation_id_path="foo") def handler(event, context): logger.info("Hello") - handler({}, lambda_context) + handler({"foo": "value"}, lambda_context) lambda_context_keys = ( "function_name", @@ -554,8 +554,11 @@ def handler(event, context): # THEN custom key should always be present # and lambda contextual info should also be in the logs + # and get_correlation_id should return None assert "my_default_key" in log assert all(k in log for k in lambda_context_keys) + assert log["correlation_id"] == "value" + assert logger.get_correlation_id() is None def test_logger_custom_handler(lambda_context, service_name, tmp_path): From dc45fd87b005a3a2aa733d4a7d009eaaf138174b Mon Sep 17 00:00:00 2001 From: Steve Cook Date: Sat, 23 Oct 2021 15:58:11 +1000 Subject: [PATCH 18/49] Removed unused import, added typing imports, fixed typo in example. (#774) --- docs/utilities/parser.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/utilities/parser.md b/docs/utilities/parser.md index 9f1bed3c0cb..7c9af95896f 100644 --- a/docs/utilities/parser.md +++ b/docs/utilities/parser.md @@ -57,8 +57,9 @@ Use the decorator for fail fast scenarios where you want your Lambda function to === "event_parser_decorator.py" ```python hl_lines="18" - from aws_lambda_powertools.utilities.parser import event_parser, BaseModel, ValidationError + from aws_lambda_powertools.utilities.parser import event_parser, BaseModel from aws_lambda_powertools.utilities.typing import LambdaContext + from typing import List, Optional import json @@ -80,7 +81,7 @@ Use the decorator for fail fast scenarios where you want your Lambda function to print(event.description) print(event.items) - order_items = [items for item in event.items] + order_items = [item for item in event.items] ... payload = { @@ -107,6 +108,7 @@ Use this standalone function when you want more control over the data validation ```python hl_lines="21 30" from aws_lambda_powertools.utilities.parser import parse, BaseModel, ValidationError + from typing import List, Optional class OrderItem(BaseModel): id: int From c3769f3a11f7bc81ee5ead8c2a111efdbf506379 Mon Sep 17 00:00:00 2001 From: Arthur Freund Date: Sat, 23 Oct 2021 07:59:15 +0200 Subject: [PATCH 19/49] Fix middleware sample (#772) --- docs/utilities/middleware_factory.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/utilities/middleware_factory.md b/docs/utilities/middleware_factory.md index 366ae7eda66..253bf6157c3 100644 --- a/docs/utilities/middleware_factory.md +++ b/docs/utilities/middleware_factory.md @@ -47,9 +47,8 @@ You can also have your own keyword arguments after the mandatory arguments. # Obfuscate email before calling Lambda handler if fields: for field in fields: - field = event.get(field, "") if field in event: - event[field] = obfuscate(field) + event[field] = obfuscate(event[field]) return handler(event, context) From 4c41ec5c0b8f4864819561bc71494029131135c9 Mon Sep 17 00:00:00 2001 From: Jonas Neubert Date: Sun, 24 Oct 2021 23:21:50 -0700 Subject: [PATCH 20/49] docs: fix indentation of SAM snippets in install section (#778) --- docs/index.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/index.md b/docs/index.md index c67ef2fc6fe..70a35fb5506 100644 --- a/docs/index.md +++ b/docs/index.md @@ -43,8 +43,8 @@ Include Lambda Powertools in your function using the [AWS Lambda Console](https: MyLambdaFunction: Type: AWS::Serverless::Function Properties: - Layers: - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:3 + Layers: + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:3 ``` === "Serverless framework" @@ -196,16 +196,16 @@ If using SAM, you can include this SAR App as part of your shared Layers stack, AwsLambdaPowertoolsPythonLayer: Type: AWS::Serverless::Application Properties: - Location: - ApplicationId: arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer - SemanticVersion: 1.21.1 # change to latest semantic version available in SAR + Location: + ApplicationId: arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer + SemanticVersion: 1.21.1 # change to latest semantic version available in SAR MyLambdaFunction: Type: AWS::Serverless::Function Properties: - Layers: - # fetch Layer ARN from SAR App stack output - - !GetAtt AwsLambdaPowertoolsPythonLayer.Outputs.LayerVersionArn + Layers: + # fetch Layer ARN from SAR App stack output + - !GetAtt AwsLambdaPowertoolsPythonLayer.Outputs.LayerVersionArn ``` === "Serverless framework" @@ -223,10 +223,10 @@ If using SAM, you can include this SAR App as part of your shared Layers stack, AwsLambdaPowertoolsPythonLayer: Type: AWS::Serverless::Application Properties: - Location: - ApplicationId: arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer - # Find latest from github.com/awslabs/aws-lambda-powertools-python/releases - SemanticVersion: 1.21.1 + Location: + ApplicationId: arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer + # Find latest from github.com/awslabs/aws-lambda-powertools-python/releases + SemanticVersion: 1.21.1 ``` === "CDK" From bbd8b6370803b91d481e70af99a6a5de842eb016 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Oct 2021 09:39:51 +0200 Subject: [PATCH 21/49] chore(deps-dev): bump pytest-asyncio from 0.15.1 to 0.16.0 (#782) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 23 +++++++++++++++++++---- pyproject.toml | 2 +- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index e13747793c1..63f11b1dd0e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -772,7 +772,7 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm [[package]] name = "pytest-asyncio" -version = "0.15.1" +version = "0.16.0" description = "Pytest support for asyncio." category = "dev" optional = false @@ -1055,7 +1055,7 @@ pydantic = ["pydantic", "email-validator"] [metadata] lock-version = "1.1" python-versions = "^3.6.1" -content-hash = "dc363cbaa0be00b64c588301bebd0e83dd7c34a48132711cdd5d573a08d0a05b" +content-hash = "2f666f603e288e7c556b1dbe3b101e513c8054f2ec8e0e46373f37eb1d119630" [metadata.files] appdirs = [ @@ -1255,6 +1255,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, @@ -1266,6 +1269,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -1277,6 +1283,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, @@ -1289,6 +1298,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -1301,6 +1313,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -1434,8 +1449,8 @@ pytest = [ {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] pytest-asyncio = [ - {file = "pytest-asyncio-0.15.1.tar.gz", hash = "sha256:2564ceb9612bbd560d19ca4b41347b54e7835c2f792c504f698e05395ed63f6f"}, - {file = "pytest_asyncio-0.15.1-py3-none-any.whl", hash = "sha256:3042bcdf1c5d978f6b74d96a151c4cfb9dcece65006198389ccd7e6c60eb1eea"}, + {file = "pytest-asyncio-0.16.0.tar.gz", hash = "sha256:7496c5977ce88c34379df64a66459fe395cd05543f0a2f837016e7144391fcfb"}, + {file = "pytest_asyncio-0.16.0-py3-none-any.whl", hash = "sha256:5f2a21273c47b331ae6aa5b36087047b4899e40f03f18397c0e65fa5cca54e9b"}, ] pytest-cov = [ {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, diff --git a/pyproject.toml b/pyproject.toml index 53dfa42b912..450731bbfdc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ isort = "^5.9.3" pytest-cov = "^3.0.0" pytest-mock = "^3.5.1" pdoc3 = "^0.10.0" -pytest-asyncio = "^0.15.1" +pytest-asyncio = "^0.16.0" bandit = "^1.7.0" radon = "^5.1.0" xenon = "^0.8.0" From 1816f660e2e7b1a0378591ed113d724e1da13749 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Oct 2021 07:40:27 +0000 Subject: [PATCH 22/49] chore(deps): bump boto3 from 1.18.61 to 1.19.6 (#783) Bumps [boto3](https://github.com/boto/boto3) from 1.18.61 to 1.19.6.
Changelog

Sourced from boto3's changelog.

1.19.6

  • api-change:gamelift: [botocore] Added support for Arm-based AWS Graviton2 instances, such as M6g, C6g, and R6g.
  • api-change:ecs: [botocore] Amazon ECS now supports running Fargate tasks on Windows Operating Systems Families which includes Windows Server 2019 Core and Windows Server 2019 Full.
  • api-change:sagemaker: [botocore] This release adds support for RStudio on SageMaker.
  • api-change:connectparticipant: [botocore] This release adds a new boolean attribute - Connect Participant - to the CreateParticipantConnection API, which can be used to mark the participant as connected.
  • api-change:ec2: [botocore] Added new read-only DenyAllIGWTraffic network interface attribute. Added support for DL1 24xlarge instances powered by Habana Gaudi Accelerators for deep learning model training workloads
  • api-change:ssm-incidents: [botocore] Updating documentation, adding new field to ConflictException to indicate earliest retry timestamp for some operations, increase maximum length of nextToken fields

1.19.5

  • api-change:autoscaling: [botocore] This release adds support for attribute-based instance type selection, a new EC2 Auto Scaling feature that lets customers express their instance requirements as a set of attributes, such as vCPU, memory, and storage.
  • api-change:ec2: [botocore] This release adds: attribute-based instance type selection for EC2 Fleet, Spot Fleet, a feature that lets customers express instance requirements as attributes like vCPU, memory, and storage; and Spot placement score, a feature that helps customers identify an optimal location to run Spot workloads.
  • enhancement:Session: Added get_partition_for_region to lookup partition for a given region_name
  • api-change:eks: [botocore] EKS managed node groups now support BOTTLEROCKET_x86_64 and BOTTLEROCKET_ARM_64 AMI types.
  • api-change:sagemaker: [botocore] This release allows customers to describe one or more versioned model packages through BatchDescribeModelPackage, update project via UpdateProject, modify and read customer metadata properties using Create, Update and Describe ModelPackage and enables cross account registration of model packages.
  • enhancement:Session: [botocore] Added get_partition_for_region allowing partition lookup by region name.
  • api-change:textract: [botocore] This release adds support for asynchronously analyzing invoice and receipt documents through two new APIs: StartExpenseAnalysis and GetExpenseAnalysis
  • enchancement:s3: TransferConfig now supports the max_bandwidth argument.

1.19.4

  • api-change:emr-containers: [botocore] This feature enables auto-generation of certificate to secure the managed-endpoint and removes the need for customer provided certificate-arn during managed-endpoint setup.
  • api-change:chime-sdk-messaging: [botocore] The Amazon Chime SDK now supports push notifications through Amazon Pinpoint
  • api-change:chime-sdk-identity: [botocore] The Amazon Chime SDK now supports push notifications through Amazon Pinpoint

1.19.3

  • api-change:rds: [botocore] This release adds support for Amazon RDS Custom, which is a new RDS management type that gives you full access to your database and operating system. For more information, see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-custom.html
  • api-change:auditmanager: [botocore] This release introduces a new feature for Audit Manager: Custom framework sharing. You can now share your custom frameworks with another AWS account, or replicate them into another AWS Region under your own account.
  • api-change:ec2: [botocore] This release adds support to create a VPN Connection that is not attached to a Gateway at the time of creation. Use this to create VPNs associated with Core Networks, or modify your VPN and attach a gateway using the modify API after creation.
  • api-change:route53resolver: [botocore] New API for ResolverConfig, which allows autodefined rules for reverse DNS resolution to be disabled for a VPC

1.19.2

  • api-change:quicksight: [botocore] Added QSearchBar option for GenerateEmbedUrlForRegisteredUser ExperienceConfiguration to support Q search bar embedding
  • api-change:auditmanager: [botocore] This release introduces character restrictions for ControlSet names. We updated regex patterns for the following attributes: ControlSet, CreateAssessmentFrameworkControlSet, and UpdateAssessmentFrameworkControlSet.
  • api-change:chime: [botocore] Chime VoiceConnector and VoiceConnectorGroup APIs will now return an ARN.

1.19.1

... (truncated)

Commits
  • 4e94e99 Merge branch 'release-1.19.6'
  • 43a8076 Bumping version to 1.19.6
  • 27de205 Add changelog entries from botocore
  • 527297d Merge branch 'release-1.19.5'
  • e25b889 Merge branch 'release-1.19.5' into develop
  • 6967a2d Bumping version to 1.19.5
  • 5d20ca0 Add changelog entries from botocore
  • 371f6d3 Add get_partition_for_region to Session (#3060)
  • c5487b5 Add max_bandwidth to TransferConfig (#3059)
  • a426ea0 Merge branch 'release-1.19.4'
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=boto3&package-manager=pip&previous-version=1.18.61&new-version=1.19.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- poetry.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index 63f11b1dd0e..f4ea5457bdd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -81,14 +81,14 @@ d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] name = "boto3" -version = "1.18.61" +version = "1.19.6" description = "The AWS SDK for Python" category = "main" optional = false python-versions = ">= 3.6" [package.dependencies] -botocore = ">=1.21.61,<1.22.0" +botocore = ">=1.22.6,<1.23.0" jmespath = ">=0.7.1,<1.0.0" s3transfer = ">=0.5.0,<0.6.0" @@ -97,7 +97,7 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.21.61" +version = "1.22.6" description = "Low-level, data-driven core of boto 3." category = "main" optional = false @@ -109,7 +109,7 @@ python-dateutil = ">=2.1,<3.0.0" urllib3 = ">=1.25.4,<1.27" [package.extras] -crt = ["awscrt (==0.11.24)"] +crt = ["awscrt (==0.12.5)"] [[package]] name = "certifi" @@ -1082,12 +1082,12 @@ black = [ {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] boto3 = [ - {file = "boto3-1.18.61-py3-none-any.whl", hash = "sha256:d2a8f8cd0a53093b2f1ab5a4b233533f1aa04e751209266597a05f50a46f6683"}, - {file = "boto3-1.18.61.tar.gz", hash = "sha256:08dc897299ecc9eef6df3cd296864154dfbc9f78edb8995b93258b59d747cbdc"}, + {file = "boto3-1.19.6-py3-none-any.whl", hash = "sha256:79e40de74faaf4b36fc885b10a2a39adb242ab499ba6d9506c6f91d0054b5338"}, + {file = "boto3-1.19.6.tar.gz", hash = "sha256:e8e20230fd8dfd991fa8459696ff2da96b15ba720f05f96d43174b2f04b328a9"}, ] botocore = [ - {file = "botocore-1.21.61-py3-none-any.whl", hash = "sha256:eda50985c229b01c7de6f0a0ccb561dfa52ded06c8c59044f624133a33357b94"}, - {file = "botocore-1.21.61.tar.gz", hash = "sha256:92ae0ca56a6582bddf42aca199ac3f67579910860bcd86166ae10f7a83f8841b"}, + {file = "botocore-1.22.6-py3-none-any.whl", hash = "sha256:5fa5de2deef817d0ef52b97dec2bc6e4489a3145afa3f5f4ec0ad6a3b213a019"}, + {file = "botocore-1.22.6.tar.gz", hash = "sha256:9210881ee5eef6fff30a2e1acfe9dea068a0885a13731ab2034e35775379637f"}, ] certifi = [ {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, From 6e29ce74c9258978e420210047837706fbf3c9fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Oct 2021 07:46:17 +0000 Subject: [PATCH 23/49] chore(deps-dev): bump flake8-eradicate from 1.1.0 to 1.2.0 (#784) Bumps [flake8-eradicate](https://github.com/wemake-services/flake8-eradicate) from 1.1.0 to 1.2.0.
Release notes

Sourced from flake8-eradicate's releases.

Version 1.2.0

  • Adds flake8@4.0.0 support
  • Adds python3.10 support
Changelog

Sourced from flake8-eradicate's changelog.

1.2.0

Features

  • Adds flake8@4.0.0 support
  • Adds python3.10 support
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=flake8-eradicate&package-manager=pip&previous-version=1.1.0&new-version=1.2.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index f4ea5457bdd..5e390f15008 100644 --- a/poetry.lock +++ b/poetry.lock @@ -298,7 +298,7 @@ six = "*" [[package]] name = "flake8-eradicate" -version = "1.1.0" +version = "1.2.0" description = "Flake8 plugin to find commented out code" category = "dev" optional = false @@ -307,7 +307,7 @@ python-versions = ">=3.6,<4.0" [package.dependencies] attrs = "*" eradicate = ">=2.0,<3.0" -flake8 = ">=3.5,<4.0" +flake8 = ">=3.5,<5" [[package]] name = "flake8-fixme" @@ -1055,7 +1055,7 @@ pydantic = ["pydantic", "email-validator"] [metadata] lock-version = "1.1" python-versions = "^3.6.1" -content-hash = "2f666f603e288e7c556b1dbe3b101e513c8054f2ec8e0e46373f37eb1d119630" +content-hash = "f927582047e5798e1add412681ae3e5f9388d38841455164f24a5d8104463664" [metadata.files] appdirs = [ @@ -1184,8 +1184,8 @@ flake8-debugger = [ {file = "flake8_debugger-4.0.0-py3-none-any.whl", hash = "sha256:82e64faa72e18d1bdd0000407502ebb8ecffa7bc027c62b9d4110ce27c091032"}, ] flake8-eradicate = [ - {file = "flake8-eradicate-1.1.0.tar.gz", hash = "sha256:f5917d6dbca352efcd10c15fdab9c55c48f0f26f6a8d47898b25d39101f170a8"}, - {file = "flake8_eradicate-1.1.0-py3-none-any.whl", hash = "sha256:d8e39b684a37c257a53cda817d86e2d96c9ba3450ddc292742623a5dfee04d9e"}, + {file = "flake8-eradicate-1.2.0.tar.gz", hash = "sha256:acaa1b6839ff00d284b805c432fdfa6047262bd15a5504ec945797e87b4de1fa"}, + {file = "flake8_eradicate-1.2.0-py3-none-any.whl", hash = "sha256:51dc660d0c1c1ed93af0f813540bbbf72ab2d3466c14e3f3bac371c618b6042f"}, ] flake8-fixme = [ {file = "flake8-fixme-1.1.1.tar.gz", hash = "sha256:50cade07d27a4c30d4f12351478df87339e67640c83041b664724bda6d16f33a"}, diff --git a/pyproject.toml b/pyproject.toml index 450731bbfdc..e72f6b5a3ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ pytest-asyncio = "^0.16.0" bandit = "^1.7.0" radon = "^5.1.0" xenon = "^0.8.0" -flake8-eradicate = "^1.1.0" +flake8-eradicate = "^1.2.0" flake8-bugbear = "^21.9.2" mkdocs-material = "^7.3.3" mkdocs-git-revision-date-plugin = "^0.3.1" From 9bc3a286cdc60adfb08066b42655729ff61fd5fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Oct 2021 07:46:22 +0000 Subject: [PATCH 24/49] chore(deps): bump urllib3 from 1.26.4 to 1.26.5 (#787) Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.4 to 1.26.5.
Release notes

Sourced from urllib3's releases.

1.26.5

:warning: IMPORTANT: urllib3 v2.0 will drop support for Python 2: Read more in the v2.0 Roadmap

  • Fixed deprecation warnings emitted in Python 3.10.
  • Updated vendored six library to 1.16.0.
  • Improved performance of URL parser when splitting the authority component.

If you or your organization rely on urllib3 consider supporting us via GitHub Sponsors

Changelog

Sourced from urllib3's changelog.

1.26.5 (2021-05-26)

  • Fixed deprecation warnings emitted in Python 3.10.
  • Updated vendored six library to 1.16.0.
  • Improved performance of URL parser when splitting the authority component.
Commits
  • d161647 Release 1.26.5
  • 2d4a3fe Improve performance of sub-authority splitting in URL
  • 2698537 Update vendored six to 1.16.0
  • 07bed79 Fix deprecation warnings for Python 3.10 ssl module
  • d725a9b Add Python 3.10 to GitHub Actions
  • 339ad34 Use pytest==6.2.4 on Python 3.10+
  • f271c9c Apply latest Black formatting
  • 1884878 [1.26] Properly proxy EOF on the SSLTransport test suite
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=urllib3&package-manager=pip&previous-version=1.26.4&new-version=1.26.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/awslabs/aws-lambda-powertools-python/network/alerts).
--- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5e390f15008..53d306be248 100644 --- a/poetry.lock +++ b/poetry.lock @@ -994,16 +994,16 @@ python-versions = "*" [[package]] name = "urllib3" -version = "1.26.4" +version = "1.26.5" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] +brotli = ["brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] -brotli = ["brotlipy (>=0.6.0)"] [[package]] name = "watchdog" @@ -1653,8 +1653,8 @@ typing-extensions = [ {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, ] urllib3 = [ - {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, - {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, + {file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"}, + {file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"}, ] watchdog = [ {file = "watchdog-2.1.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9628f3f85375a17614a2ab5eac7665f7f7be8b6b0a2a228e6f6a2e91dd4bfe26"}, From 1fa6a7c8b9194303e901c2d786bbf15fc5e90a4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Oct 2021 07:48:15 +0000 Subject: [PATCH 25/49] chore(deps-dev): bump flake8-isort from 4.0.0 to 4.1.1 (#785) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps [flake8-isort](https://github.com/gforcada/flake8-isort) from 4.0.0 to 4.1.1.
Changelog

Sourced from flake8-isort's changelog.

4.1.1 (2021-10-14)

  • Release py3 only wheels..

4.1.0 (2021-10-14)

  • Support flake8 4.x [g-as]

  • Switch from travis-ci to github actions. [g-as]

  • Drop python 2.7 support and 3.5 as well [g-as]

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=flake8-isort&package-manager=pip&previous-version=4.0.0&new-version=4.1.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- poetry.lock | 12 ++++++------ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 53d306be248..0bb99d068ae 100644 --- a/poetry.lock +++ b/poetry.lock @@ -319,19 +319,19 @@ python-versions = "*" [[package]] name = "flake8-isort" -version = "4.0.0" +version = "4.1.1" description = "flake8 plugin that integrates isort ." category = "dev" optional = false python-versions = "*" [package.dependencies] -flake8 = ">=3.2.1,<4" +flake8 = ">=3.2.1,<5" isort = ">=4.3.5,<6" testfixtures = ">=6.8.0,<7" [package.extras] -test = ["pytest (>=4.0.2,<6)", "toml"] +test = ["pytest-cov"] [[package]] name = "flake8-variables-names" @@ -1055,7 +1055,7 @@ pydantic = ["pydantic", "email-validator"] [metadata] lock-version = "1.1" python-versions = "^3.6.1" -content-hash = "f927582047e5798e1add412681ae3e5f9388d38841455164f24a5d8104463664" +content-hash = "faa554f1cb4eafe6a4308fc71d3a64443d6cb65088c0937b4c90a4af10f31487" [metadata.files] appdirs = [ @@ -1192,8 +1192,8 @@ flake8-fixme = [ {file = "flake8_fixme-1.1.1-py2.py3-none-any.whl", hash = "sha256:226a6f2ef916730899f29ac140bed5d4a17e5aba79f00a0e3ae1eff1997cb1ac"}, ] flake8-isort = [ - {file = "flake8-isort-4.0.0.tar.gz", hash = "sha256:2b91300f4f1926b396c2c90185844eb1a3d5ec39ea6138832d119da0a208f4d9"}, - {file = "flake8_isort-4.0.0-py2.py3-none-any.whl", hash = "sha256:729cd6ef9ba3659512dee337687c05d79c78e1215fdf921ed67e5fe46cce2f3c"}, + {file = "flake8-isort-4.1.1.tar.gz", hash = "sha256:d814304ab70e6e58859bc5c3e221e2e6e71c958e7005239202fee19c24f82717"}, + {file = "flake8_isort-4.1.1-py3-none-any.whl", hash = "sha256:c4e8b6dcb7be9b71a02e6e5d4196cefcef0f3447be51e82730fb336fff164949"}, ] flake8-variables-names = [ {file = "flake8_variables_names-0.0.4.tar.gz", hash = "sha256:d6fa0571a807c72940b5773827c5760421ea6f8206595ff0a8ecfa01e42bf2cf"}, diff --git a/pyproject.toml b/pyproject.toml index e72f6b5a3ed..007eac4242e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ flake8-builtins = "^1.5.3" flake8-comprehensions = "^3.7.0" flake8-debugger = "^4.0.0" flake8-fixme = "^1.1.1" -flake8-isort = "^4.0.0" +flake8-isort = "^4.1.1" flake8-variables-names = "^0.0.4" isort = "^5.9.3" pytest-cov = "^3.0.0" From bb8e3b62d9ee0a43ad198a8635bd68d3449b557d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Oct 2021 07:52:19 +0000 Subject: [PATCH 26/49] chore(deps-dev): bump mkdocs-material from 7.3.3 to 7.3.5 (#781) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.3.3 to 7.3.5.
Release notes

Sourced from mkdocs-material's releases.

mkdocs-material-7.3.5

  • Added support for setting table of contents title via mkdocs.yml
  • Fixed back-to-top button position for right-to-left languages

mkdocs-material-7.3.4

  • Bumped MkDocs version to 1.2.3 to mitigate CVE-2021-40978
  • Fixed spacing issues when using integrate table of contents with tabs
  • Fixed some spacings issues for right-to-left languages
  • Fixed race condition in search initialization
Changelog

Sourced from mkdocs-material's changelog.

mkdocs-material-7.3.5 (2021-10-27)

  • Added support for setting table of contents title via mkdocs.yml
  • Fixed back-to-top button position for right-to-left languages

mkdocs-material-7.3.4+insiders-3.1.4 (2021-10-17)

  • Fixed #2974: Text cropped with other fonts than Roboto in social plugin
  • Fixed #3099: Encoding problems with non-latin character in social plugin
  • Fixed #3112: Japanese segmenter not executed as part of new tokenizer
  • Fixed tags (front matter) appearing in search with disabled tags plugin

mkdocs-material-7.3.4 (2021-10-17)

  • Bumped MkDocs version to 1.2.3 to mitigate CVE-2021-40978
  • Fixed spacing issues when using integrate table of contents with tabs
  • Fixed some spacings issues for right-to-left languages
  • Fixed race condition in search initialization

mkdocs-material-7.3.3+insiders-3.1.3 (2021-10-12)

  • Added warnings to search plugin for unsupported options and syntax
  • Fixed #3503: Search sometimes returns entire page
  • Fixed #3089: Single-line code annotations disappear when printing

mkdocs-material-7.3.3 (2021-10-11)

  • Rewrite of entire documentation
  • Adjusted height of new content tabs to match single line code blocks
  • Fixed new content tabs missing right padding in some browsers on overflow
  • Fixed new content tabs bleeding out of flex container on overflow
  • Fixed new content tabs overflow scrolling bugs on some browsers
  • Fixed new content tabs stealing keyboard access when active
  • Fixed some spacings issues for right-to-left languages

mkdocs-material-7.3.2+insiders-3.1.2 (2021-10-06)

  • Fixed incorrect path separators for social cards on Windows

mkdocs-material-7.3.2 (2021-10-06)

  • Deprecated prebuilding of search index
  • Improved graceful handling of broken search for file://
  • Added minimum Jinja version to list of requirements
  • Fixed #3071: Section index pages render empty directories
  • Fixed margin issues when using navigation tabs (7.3.1 regression)
  • Fixed search placeholder sometimes being shown too early

mkdocs-material-7.3.1 (2021-10-02)

... (truncated)

Commits
  • cda30e8 Prepare 7.3.5 release
  • 3daf8e2 Improved social cards documentation
  • 403580b Fixed back-to-top button position for right-to-left languages
  • cffb592 Updated custom translations example
  • 541c51e Documentation
  • 25c91b9 Formatting
  • 4bdfef4 Added support for setting table of contents title via mkdocs.yml
  • 8f382d9 Added documentation for permanent link title
  • 9d1f5c9 Switched to numeric values
  • db1739e Enabled feedback for testing
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=mkdocs-material&package-manager=pip&previous-version=7.3.3&new-version=7.3.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- poetry.lock | 16 ++++++++-------- pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0bb99d068ae..bfda332dd0a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -541,7 +541,7 @@ test = ["coverage", "flake8 (>=3.0)"] [[package]] name = "mkdocs" -version = "1.2.2" +version = "1.2.3" description = "Project documentation with Markdown." category = "dev" optional = false @@ -577,7 +577,7 @@ mkdocs = ">=0.17" [[package]] name = "mkdocs-material" -version = "7.3.3" +version = "7.3.5" description = "A Material Design theme for MkDocs" category = "dev" optional = false @@ -586,7 +586,7 @@ python-versions = "*" [package.dependencies] jinja2 = ">=2.11.1" markdown = ">=3.2" -mkdocs = ">=1.2.2" +mkdocs = ">=1.2.3" mkdocs-material-extensions = ">=1.0" pygments = ">=2.4" pymdown-extensions = ">=9.0" @@ -1055,7 +1055,7 @@ pydantic = ["pydantic", "email-validator"] [metadata] lock-version = "1.1" python-versions = "^3.6.1" -content-hash = "faa554f1cb4eafe6a4308fc71d3a64443d6cb65088c0937b4c90a4af10f31487" +content-hash = "6eb7e379d0b2b14d0a9f374b66c6b6d2f236f0d23cffc0b4375835ba853ac5ae" [metadata.files] appdirs = [ @@ -1333,16 +1333,16 @@ mike = [ {file = "mike-0.6.0.tar.gz", hash = "sha256:6d6239de2a60d733da2f34617e9b9a14c4b5437423b47e524f14dc96d6ce5f2f"}, ] mkdocs = [ - {file = "mkdocs-1.2.2-py3-none-any.whl", hash = "sha256:d019ff8e17ec746afeb54eb9eb4112b5e959597aebc971da46a5c9486137f0ff"}, - {file = "mkdocs-1.2.2.tar.gz", hash = "sha256:a334f5bd98ec960638511366eb8c5abc9c99b9083a0ed2401d8791b112d6b078"}, + {file = "mkdocs-1.2.3-py3-none-any.whl", hash = "sha256:a1fa8c2d0c1305d7fc2b9d9f607c71778572a8b110fb26642aa00296c9e6d072"}, + {file = "mkdocs-1.2.3.tar.gz", hash = "sha256:89f5a094764381cda656af4298727c9f53dc3e602983087e1fe96ea1df24f4c1"}, ] mkdocs-git-revision-date-plugin = [ {file = "mkdocs-git-revision-date-plugin-0.3.1.tar.gz", hash = "sha256:4abaef720763a64c952bed6829dcc180f67c97c60dd73914e90715e05d1cfb23"}, {file = "mkdocs_git_revision_date_plugin-0.3.1-py3-none-any.whl", hash = "sha256:8ae50b45eb75d07b150a69726041860801615aae5f4adbd6b1cf4d51abaa03d5"}, ] mkdocs-material = [ - {file = "mkdocs-material-7.3.3.tar.gz", hash = "sha256:3444b681f47e62c0ec7166bfb6f12360a26c751224cd6d3b3816f7310827073f"}, - {file = "mkdocs_material-7.3.3-py2.py3-none-any.whl", hash = "sha256:56bcc4e43356b97caa07706b0f446a3d9c39eb9aa1ad64131686d555270fc65e"}, + {file = "mkdocs-material-7.3.5.tar.gz", hash = "sha256:5edbf86b4249f413d54b8e2f1f2f8f1d2a9345f45e928303f4541e1d51f362c0"}, + {file = "mkdocs_material-7.3.5-py2.py3-none-any.whl", hash = "sha256:9fc09d2636d3249a7d75813c16360f3aa6987dff792c0e19f218fc80d48c2296"}, ] mkdocs-material-extensions = [ {file = "mkdocs-material-extensions-1.0.1.tar.gz", hash = "sha256:6947fb7f5e4291e3c61405bad3539d81e0b3cd62ae0d66ced018128af509c68f"}, diff --git a/pyproject.toml b/pyproject.toml index 007eac4242e..c177e4b5820 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ radon = "^5.1.0" xenon = "^0.8.0" flake8-eradicate = "^1.2.0" flake8-bugbear = "^21.9.2" -mkdocs-material = "^7.3.3" +mkdocs-material = "^7.3.5" mkdocs-git-revision-date-plugin = "^0.3.1" mike = "^0.6.0" mypy = "^0.910" From 8b01fc5f02390d4cab2cd64b72381e2d785aec4f Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Fri, 29 Oct 2021 02:58:57 -0700 Subject: [PATCH 27/49] feat(appsync): add Router to allow large resolver composition (#776) --- .../event_handler/appsync.py | 75 ++++++++++++------- .../functional/event_handler/test_appsync.py | 27 +++++++ 2 files changed, 75 insertions(+), 27 deletions(-) diff --git a/aws_lambda_powertools/event_handler/appsync.py b/aws_lambda_powertools/event_handler/appsync.py index 69b90c4cbb6..6a4bf989169 100644 --- a/aws_lambda_powertools/event_handler/appsync.py +++ b/aws_lambda_powertools/event_handler/appsync.py @@ -1,4 +1,5 @@ import logging +from abc import ABC from typing import Any, Callable, Optional, Type, TypeVar from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent @@ -9,7 +10,33 @@ AppSyncResolverEventT = TypeVar("AppSyncResolverEventT", bound=AppSyncResolverEvent) -class AppSyncResolver: +class BaseRouter(ABC): + current_event: AppSyncResolverEventT # type: ignore[valid-type] + lambda_context: LambdaContext + + def __init__(self): + self._resolvers: dict = {} + + def resolver(self, type_name: str = "*", field_name: Optional[str] = None): + """Registers the resolver for field_name + + Parameters + ---------- + type_name : str + Type name + field_name : str + Field name + """ + + def register_resolver(func): + logger.debug(f"Adding resolver `{func.__name__}` for field `{type_name}.{field_name}`") + self._resolvers[f"{type_name}.{field_name}"] = {"func": func} + return func + + return register_resolver + + +class AppSyncResolver(BaseRouter): """ AppSync resolver decorator @@ -40,29 +67,8 @@ def common_field() -> str: return str(uuid.uuid4()) """ - current_event: AppSyncResolverEventT # type: ignore[valid-type] - lambda_context: LambdaContext - def __init__(self): - self._resolvers: dict = {} - - def resolver(self, type_name: str = "*", field_name: Optional[str] = None): - """Registers the resolver for field_name - - Parameters - ---------- - type_name : str - Type name - field_name : str - Field name - """ - - def register_resolver(func): - logger.debug(f"Adding resolver `{func.__name__}` for field `{type_name}.{field_name}`") - self._resolvers[f"{type_name}.{field_name}"] = {"func": func} - return func - - return register_resolver + super().__init__() def resolve( self, event: dict, context: LambdaContext, data_model: Type[AppSyncResolverEvent] = AppSyncResolverEvent @@ -136,10 +142,10 @@ def lambda_handler(event, context): ValueError If we could not find a field resolver """ - self.current_event = data_model(event) - self.lambda_context = context - resolver = self._get_resolver(self.current_event.type_name, self.current_event.field_name) - return resolver(**self.current_event.arguments) + BaseRouter.current_event = data_model(event) + BaseRouter.lambda_context = context + resolver = self._get_resolver(BaseRouter.current_event.type_name, BaseRouter.current_event.field_name) + return resolver(**BaseRouter.current_event.arguments) def _get_resolver(self, type_name: str, field_name: str) -> Callable: """Get resolver for field_name @@ -167,3 +173,18 @@ def __call__( ) -> Any: """Implicit lambda handler which internally calls `resolve`""" return self.resolve(event, context, data_model) + + def include_router(self, router: "Router") -> None: + """Adds all resolvers defined in a router + + Parameters + ---------- + router : Router + A router containing a dict of field resolvers + """ + self._resolvers.update(router._resolvers) + + +class Router(BaseRouter): + def __init__(self): + super().__init__() diff --git a/tests/functional/event_handler/test_appsync.py b/tests/functional/event_handler/test_appsync.py index 26a3ffdcb1f..79173e55825 100644 --- a/tests/functional/event_handler/test_appsync.py +++ b/tests/functional/event_handler/test_appsync.py @@ -4,6 +4,7 @@ import pytest from aws_lambda_powertools.event_handler import AppSyncResolver +from aws_lambda_powertools.event_handler.appsync import Router from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.utils import load_event @@ -161,3 +162,29 @@ def create_something(id: str): # noqa AA03 VNE003 assert result == "my identifier" assert app.current_event.country_viewer == "US" + + +def test_resolver_include_resolver(): + # GIVEN + app = AppSyncResolver() + router = Router() + + @router.resolver(type_name="Query", field_name="listLocations") + def get_locations(name: str): + return "get_locations#" + name + + @app.resolver(field_name="listLocations2") + def get_locations2(name: str): + return "get_locations2#" + name + + app.include_router(router) + + # WHEN + mock_event1 = {"typeName": "Query", "fieldName": "listLocations", "arguments": {"name": "value"}} + mock_event2 = {"typeName": "Query", "fieldName": "listLocations2", "arguments": {"name": "value"}} + result1 = app.resolve(mock_event1, LambdaContext()) + result2 = app.resolve(mock_event2, LambdaContext()) + + # THEN + assert result1 == "get_locations#value" + assert result2 == "get_locations2#value" From 1bcd4ff7202ab31c33f4d5a6ec26d497ad5433a1 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Fri, 29 Oct 2021 02:59:31 -0700 Subject: [PATCH 28/49] feat(data-classes): ActiveMQ and RabbitMQ support (#770) Co-authored-by: heitorlessa --- .../utilities/data_classes/active_mq_event.py | 125 ++++++++++++++++++ .../utilities/data_classes/rabbit_mq_event.py | 121 +++++++++++++++++ docs/utilities/data_classes.md | 54 ++++++++ tests/events/activeMQEvent.json | 45 +++++++ tests/events/rabbitMQEvent.json | 51 +++++++ .../functional/data_classes/test_amazon_mq.py | 69 ++++++++++ 6 files changed, 465 insertions(+) create mode 100644 aws_lambda_powertools/utilities/data_classes/active_mq_event.py create mode 100644 aws_lambda_powertools/utilities/data_classes/rabbit_mq_event.py create mode 100644 tests/events/activeMQEvent.json create mode 100644 tests/events/rabbitMQEvent.json create mode 100644 tests/functional/data_classes/test_amazon_mq.py diff --git a/aws_lambda_powertools/utilities/data_classes/active_mq_event.py b/aws_lambda_powertools/utilities/data_classes/active_mq_event.py new file mode 100644 index 00000000000..058a6a6ecf4 --- /dev/null +++ b/aws_lambda_powertools/utilities/data_classes/active_mq_event.py @@ -0,0 +1,125 @@ +import base64 +import json +from typing import Any, Iterator, Optional + +from aws_lambda_powertools.utilities.data_classes.common import DictWrapper + + +class ActiveMQMessage(DictWrapper): + @property + def message_id(self) -> str: + """Unique identifier for the message""" + return self["messageID"] + + @property + def message_type(self) -> str: + return self["messageType"] + + @property + def data(self) -> str: + return self["data"] + + @property + def decoded_data(self) -> str: + """Decodes the data as a str""" + return base64.b64decode(self.data.encode()).decode() + + @property + def json_data(self) -> Any: + """Parses the data as json""" + return json.loads(self.decoded_data) + + @property + def connection_id(self) -> str: + return self["connectionId"] + + @property + def redelivered(self) -> bool: + """true if the message is being resent to the consumer""" + return self["redelivered"] + + @property + def timestamp(self) -> int: + """Time in milliseconds.""" + return self["timestamp"] + + @property + def broker_in_time(self) -> int: + """Time stamp (in milliseconds) for when the message arrived at the broker.""" + return self["brokerInTime"] + + @property + def broker_out_time(self) -> int: + """Time stamp (in milliseconds) for when the message left the broker.""" + return self["brokerOutTime"] + + @property + def destination_physicalname(self) -> str: + return self["destination"]["physicalname"] + + @property + def delivery_mode(self) -> Optional[int]: + """persistent or non-persistent delivery""" + return self.get("deliveryMode") + + @property + def correlation_id(self) -> Optional[str]: + """User defined correlation id""" + return self.get("correlationID") + + @property + def reply_to(self) -> Optional[str]: + """User defined reply to""" + return self.get("replyTo") + + @property + def get_type(self) -> Optional[str]: + """User defined message type""" + return self.get("type") + + @property + def expiration(self) -> Optional[int]: + """Expiration attribute whose value is given in milliseconds""" + return self.get("expiration") + + @property + def priority(self) -> Optional[int]: + """ + JMS defines a ten-level priority value, with 0 as the lowest priority and 9 + as the highest. In addition, clients should consider priorities 0-4 as + gradations of normal priority and priorities 5-9 as gradations of expedited + priority. + + JMS does not require that a provider strictly implement priority ordering + of messages; however, it should do its best to deliver expedited messages + ahead of normal messages. + """ + return self.get("priority") + + +class ActiveMQEvent(DictWrapper): + """Represents an Active MQ event sent to Lambda + + Documentation: + -------------- + - https://docs.aws.amazon.com/lambda/latest/dg/with-mq.html + - https://aws.amazon.com/blogs/compute/using-amazon-mq-as-an-event-source-for-aws-lambda/ + """ + + @property + def event_source(self) -> str: + return self["eventSource"] + + @property + def event_source_arn(self) -> str: + """The Amazon Resource Name (ARN) of the event source""" + return self["eventSourceArn"] + + @property + def messages(self) -> Iterator[ActiveMQMessage]: + for record in self["messages"]: + yield ActiveMQMessage(record) + + @property + def message(self) -> ActiveMQMessage: + return next(self.messages) diff --git a/aws_lambda_powertools/utilities/data_classes/rabbit_mq_event.py b/aws_lambda_powertools/utilities/data_classes/rabbit_mq_event.py new file mode 100644 index 00000000000..7676e6ff9b5 --- /dev/null +++ b/aws_lambda_powertools/utilities/data_classes/rabbit_mq_event.py @@ -0,0 +1,121 @@ +import base64 +import json +from typing import Any, Dict, List + +from aws_lambda_powertools.utilities.data_classes.common import DictWrapper + + +class BasicProperties(DictWrapper): + @property + def content_type(self) -> str: + return self["contentType"] + + @property + def content_encoding(self) -> str: + return self["contentEncoding"] + + @property + def headers(self) -> Dict[str, Any]: + return self["headers"] + + @property + def delivery_mode(self) -> int: + return self["deliveryMode"] + + @property + def priority(self) -> int: + return self["priority"] + + @property + def correlation_id(self) -> str: + return self["correlationId"] + + @property + def reply_to(self) -> str: + return self["replyTo"] + + @property + def expiration(self) -> str: + return self["expiration"] + + @property + def message_id(self) -> str: + return self["messageId"] + + @property + def timestamp(self) -> str: + return self["timestamp"] + + @property + def get_type(self) -> str: + return self["type"] + + @property + def user_id(self) -> str: + return self["userId"] + + @property + def app_id(self) -> str: + return self["appId"] + + @property + def cluster_id(self) -> str: + return self["clusterId"] + + @property + def body_size(self) -> int: + return self["bodySize"] + + +class RabbitMessage(DictWrapper): + @property + def basic_properties(self) -> BasicProperties: + return BasicProperties(self["basicProperties"]) + + @property + def redelivered(self) -> bool: + return self["redelivered"] + + @property + def data(self) -> str: + return self["data"] + + @property + def decoded_data(self) -> str: + """Decodes the data as a str""" + return base64.b64decode(self.data.encode()).decode() + + @property + def json_data(self) -> Any: + """Parses the data as json""" + return json.loads(self.decoded_data) + + +class RabbitMQEvent(DictWrapper): + """Represents a Rabbit MQ event sent to Lambda + + Documentation: + -------------- + - https://docs.aws.amazon.com/lambda/latest/dg/with-mq.html + - https://aws.amazon.com/blogs/compute/using-amazon-mq-for-rabbitmq-as-an-event-source-for-lambda/ + """ + + def __init__(self, data: Dict[str, Any]): + super().__init__(data) + self._rmq_messages_by_queue = { + key: [RabbitMessage(message) for message in messages] + for key, messages in self["rmqMessagesByQueue"].items() + } + + @property + def event_source(self) -> str: + return self["eventSource"] + + @property + def event_source_arn(self) -> str: + """The Amazon Resource Name (ARN) of the event source""" + return self["eventSourceArn"] + + @property + def rmq_messages_by_queue(self) -> Dict[str, List[RabbitMessage]]: + return self._rmq_messages_by_queue diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index e05193c7702..cbe874d4b94 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -58,6 +58,7 @@ Same example as above, but using the `event_source` decorator Event Source | Data_class ------------------------------------------------- | --------------------------------------------------------------------------------- +[Active MQ](#active-mq) | `ActiveMQEvent` [API Gateway Authorizer](#api-gateway-authorizer) | `APIGatewayAuthorizerRequestEvent` [API Gateway Authorizer V2](#api-gateway-authorizer-v2) | `APIGatewayAuthorizerEventV2` [API Gateway Proxy](#api-gateway-proxy) | `APIGatewayProxyEvent` @@ -72,6 +73,7 @@ Event Source | Data_class [DynamoDB streams](#dynamodb-streams) | `DynamoDBStreamEvent`, `DynamoDBRecordEventName` [EventBridge](#eventbridge) | `EventBridgeEvent` [Kinesis Data Stream](#kinesis-streams) | `KinesisStreamEvent` +[Rabbit MQ](#rabbit-mq) | `RabbitMQEvent` [S3](#s3) | `S3Event` [S3 Object Lambda](#s3-object-lambda) | `S3ObjectLambdaEvent` [SES](#ses) | `SESEvent` @@ -82,6 +84,31 @@ Event Source | Data_class The examples provided below are far from exhaustive - the data classes themselves are designed to provide a form of documentation inherently (via autocompletion, types and docstrings). +### Active MQ + +It is used for [Active MQ payloads](https://docs.aws.amazon.com/lambda/latest/dg/with-mq.html){target="_blank"}, also see +the [AWS blog post](https://aws.amazon.com/blogs/compute/using-amazon-mq-as-an-event-source-for-aws-lambda/){target="_blank"} +for more details. + +=== "app.py" + + ```python hl_lines="4-5 9-10" + from typing import Dict + + from aws_lambda_powertools import Logger + from aws_lambda_powertools.utilities.data_classes import event_source + from aws_lambda_powertools.utilities.data_classes.active_mq_event import ActiveMQEvent + + logger = Logger() + + @event_source(data_class=ActiveMQEvent) + def lambda_handler(event: ActiveMQEvent, context): + for message in event.messages: + logger.debug(f"MessageID: {message.message_id}") + data: Dict = message.json_data + logger.debug("Process json in base64 encoded data str", data) + ``` + ### API Gateway Authorizer > New in 1.20.0 @@ -810,6 +837,33 @@ or plain text, depending on the original payload. do_something_with(data) ``` +### Rabbit MQ + +It is used for [Rabbit MQ payloads](https://docs.aws.amazon.com/lambda/latest/dg/with-mq.html){target="_blank"}, also see +the [blog post](https://aws.amazon.com/blogs/compute/using-amazon-mq-for-rabbitmq-as-an-event-source-for-lambda/){target="_blank"} +for more details. + +=== "app.py" + + ```python hl_lines="4-5 9-10" + from typing import Dict + + from aws_lambda_powertools import Logger + from aws_lambda_powertools.utilities.data_classes import event_source + from aws_lambda_powertools.utilities.data_classes.rabbit_mq_event import RabbitMQEvent + + logger = Logger() + + @event_source(data_class=RabbitMQEvent) + def lambda_handler(event: RabbitMQEvent, context): + for queue_name, messages in event.rmq_messages_by_queue.items(): + logger.debug(f"Messages for queue: {queue_name}") + for message in messages: + logger.debug(f"MessageID: {message.basic_properties.message_id}") + data: Dict = message.json_data + logger.debug("Process json in base64 encoded data str", data) + ``` + ### S3 === "app.py" diff --git a/tests/events/activeMQEvent.json b/tests/events/activeMQEvent.json new file mode 100644 index 00000000000..290ada184c9 --- /dev/null +++ b/tests/events/activeMQEvent.json @@ -0,0 +1,45 @@ +{ + "eventSource": "aws:amq", + "eventSourceArn": "arn:aws:mq:us-west-2:112556298976:broker:test:b-9bcfa592-423a-4942-879d-eb284b418fc8", + "messages": [ + { + "messageID": "ID:b-9bcfa592-423a-4942-879d-eb284b418fc8-1.mq.us-west-2.amazonaws.com-37557-1234520418293-4:1:1:1:1", + "messageType": "jms/text-message", + "data": "QUJDOkFBQUE=", + "connectionId": "myJMSCoID", + "redelivered": false, + "destination": { + "physicalname": "testQueue" + }, + "timestamp": 1598827811958, + "brokerInTime": 1598827811958, + "brokerOutTime": 1598827811959 + }, + { + "messageID": "ID:b-9bcfa592-423a-4942-879d-eb284b418fc8-1.mq.us-west-2.amazonaws.com-37557-1234520418293-4:1:1:1:1", + "messageType": "jms/text-message", + "data": "eyJ0aW1lb3V0IjowLCJkYXRhIjoiQ1pybWYwR3c4T3Y0YnFMUXhENEUifQ==", + "connectionId": "myJMSCoID2", + "redelivered": false, + "destination": { + "physicalname": "testQueue" + }, + "timestamp": 1598827811958, + "brokerInTime": 1598827811958, + "brokerOutTime": 1598827811959 + }, + { + "messageID": "ID:b-9bcfa592-423a-4942-879d-eb284b418fc8-1.mq.us-west-2.amazonaws.com-37557-1234520418293-4:1:1:1:1", + "messageType": "jms/bytes-message", + "data": "3DTOOW7crj51prgVLQaGQ82S48k=", + "connectionId": "myJMSCoID1", + "persistent": false, + "destination": { + "physicalname": "testQueue" + }, + "timestamp": 1598827811958, + "brokerInTime": 1598827811958, + "brokerOutTime": 1598827811959 + } + ] +} diff --git a/tests/events/rabbitMQEvent.json b/tests/events/rabbitMQEvent.json new file mode 100644 index 00000000000..e4259555a8b --- /dev/null +++ b/tests/events/rabbitMQEvent.json @@ -0,0 +1,51 @@ +{ + "eventSource": "aws:rmq", + "eventSourceArn": "arn:aws:mq:us-west-2:112556298976:broker:pizzaBroker:b-9bcfa592-423a-4942-879d-eb284b418fc8", + "rmqMessagesByQueue": { + "pizzaQueue::/": [ + { + "basicProperties": { + "contentType": "text/plain", + "contentEncoding": null, + "headers": { + "header1": { + "bytes": [ + 118, + 97, + 108, + 117, + 101, + 49 + ] + }, + "header2": { + "bytes": [ + 118, + 97, + 108, + 117, + 101, + 50 + ] + }, + "numberInHeader": 10 + }, + "deliveryMode": 1, + "priority": 34, + "correlationId": null, + "replyTo": null, + "expiration": "60000", + "messageId": null, + "timestamp": "Jan 1, 1970, 12:33:41 AM", + "type": null, + "userId": "AIDACKCEVSQ6C2EXAMPLE", + "appId": null, + "clusterId": null, + "bodySize": 80 + }, + "redelivered": false, + "data": "eyJ0aW1lb3V0IjowLCJkYXRhIjoiQ1pybWYwR3c4T3Y0YnFMUXhENEUifQ==" + } + ] + } +} diff --git a/tests/functional/data_classes/test_amazon_mq.py b/tests/functional/data_classes/test_amazon_mq.py new file mode 100644 index 00000000000..0f4f5079565 --- /dev/null +++ b/tests/functional/data_classes/test_amazon_mq.py @@ -0,0 +1,69 @@ +from typing import Dict + +from aws_lambda_powertools.utilities.data_classes.active_mq_event import ActiveMQEvent, ActiveMQMessage +from aws_lambda_powertools.utilities.data_classes.rabbit_mq_event import BasicProperties, RabbitMessage, RabbitMQEvent +from tests.functional.utils import load_event + + +def test_active_mq_event(): + event = ActiveMQEvent(load_event("activeMQEvent.json")) + + assert event.event_source == "aws:amq" + assert event.event_source_arn is not None + assert len(list(event.messages)) == 3 + + message = event.message + assert isinstance(message, ActiveMQMessage) + assert message.message_id is not None + assert message.message_type is not None + assert message.data is not None + assert message.decoded_data is not None + assert message.connection_id is not None + assert message.redelivered is False + assert message.timestamp is not None + assert message.broker_in_time is not None + assert message.broker_out_time is not None + assert message.destination_physicalname is not None + assert message.delivery_mode is None + assert message.correlation_id is None + assert message.reply_to is None + assert message.get_type is None + assert message.expiration is None + assert message.priority is None + + messages = list(event.messages) + message = messages[1] + assert message.json_data["timeout"] == 0 + + +def test_rabbit_mq_event(): + event = RabbitMQEvent(load_event("rabbitMQEvent.json")) + + assert event.event_source == "aws:rmq" + assert event.event_source_arn is not None + + message = event.rmq_messages_by_queue["pizzaQueue::/"][0] + assert message.redelivered is False + assert message.data is not None + assert message.decoded_data is not None + assert message.json_data["timeout"] == 0 + + assert isinstance(message, RabbitMessage) + properties = message.basic_properties + assert isinstance(properties, BasicProperties) + assert properties.content_type == "text/plain" + assert properties.content_encoding is None + assert isinstance(properties.headers, Dict) + assert properties.headers["header1"] is not None + assert properties.delivery_mode == 1 + assert properties.priority == 34 + assert properties.correlation_id is None + assert properties.reply_to is None + assert properties.expiration == "60000" + assert properties.message_id is None + assert properties.timestamp is not None + assert properties.get_type is None + assert properties.user_id is not None + assert properties.app_id is None + assert properties.cluster_id is None + assert properties.body_size == 80 From 750f482090a1b190a9512ad6fab72f0ba1df79d8 Mon Sep 17 00:00:00 2001 From: Steve Cook Date: Fri, 29 Oct 2021 23:21:51 +1000 Subject: [PATCH 29/49] docs: updated Lambda Layers definition & limitations. (#775) Co-authored-by: heitorlessa --- docs/index.md | 71 ++++++++++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/docs/index.md b/docs/index.md index 70a35fb5506..e513f2ba8ed 100644 --- a/docs/index.md +++ b/docs/index.md @@ -23,19 +23,36 @@ This project separates core utilities that will be available in other runtimes v Powertools is available in the following formats: -??? info "Lambda Layer is a .zip file archive with Lambda Powertools pre-packaged in every AWS region. See what's inside!" - Change {region} to your AWS region, e.g. `eu-west-1` - - **`aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:3 --region {region}`** - * **Lambda Layer**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:3**](#){: .copyMe} :clipboard: * **PyPi**: **`pip install aws-lambda-powertools`** ### Lambda Layer -Include Lambda Powertools in your function using the [AWS Lambda Console](https://console.aws.amazon.com/lambda){target="_blank"} or your preferred deployment framework. +[Lambda Layer](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html){target="_blank"} is a .zip file archive that can contain additional code, pre-packaged dependencies, data, or configuration files. Layers promote code sharing and separation of responsibilities so that you can iterate faster on writing business logic. + +You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https://docs.aws.amazon.com/lambda/latest/dg/invocation-layers.html#invocation-layers-using){target="_blank"}, or your preferred deployment framework. + +??? note "Expand to copy any regional Lambda Layer ARN" -!!! note "The public layers do not contain the `pydantic` library that is required for the `parser` utility; See [SAR](#sar) option instead." + | Region | Layer ARN + |--------------------------- | --------------------------- + | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: + | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: === "SAM" @@ -50,11 +67,11 @@ Include Lambda Powertools in your function using the [AWS Lambda Console](https: === "Serverless framework" ```yaml hl_lines="5" - functions: - main: - handler: lambda_function.lambda_handler - layers: - - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPython:3 + functions: + hello: + handler: lambda_function.lambda_handler + layers: + - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPython:3 ``` === "CDK" @@ -150,27 +167,17 @@ Include Lambda Powertools in your function using the [AWS Lambda Console](https: ? Do you want to edit the local lambda function now? No ``` -??? note "Expand to copy any regional Lambda Layer ARN" +=== "Get the Layer .zip contents" + Change {region} to your AWS region, e.g. `eu-west-1` + + **`aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:3 --region {region}`** + +!!! warning "Limitations" + + Container Image deployment (OCI) or inline Lambda functions do not support Lambda Layers. + + Lambda Powertools Lambda Layer do not include `pydantic` library - required dependency for the `parser` utility. See [SAR](#sar) option instead. - | Region | Layer ARN - |--------------------------- | --------------------------- - | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: - | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: - | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: - | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: - | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: - | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: - | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: - | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: - | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: - | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: - | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: - | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: - | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: - | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: - | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: - | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: - | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPython:3](#){: .copyMe} :clipboard: #### SAR From 6c451f783e46d65838db0ed9cb2d0382cb1c2a8e Mon Sep 17 00:00:00 2001 From: Adam Tankanow Date: Tue, 9 Nov 2021 09:26:06 -0500 Subject: [PATCH 30/49] improv: Support a composite primary key in DynamoDBPersistenceLayer (#740) * ISSUE-694: add sort key to DynamoDBPersistenceLayer * ISSUE-694: change default pk; revert test changes for now * ISSUE-694: PR Updates - remove test-func default name * ISSUE-694: Change key_attr_value based on PR feedback --- .../idempotency/persistence/dynamodb.py | 28 +++++++++++++++---- .../idempotency/test_idempotency.py | 6 ++-- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/aws_lambda_powertools/utilities/idempotency/persistence/dynamodb.py b/aws_lambda_powertools/utilities/idempotency/persistence/dynamodb.py index 0ce307ab503..8a470c0f910 100644 --- a/aws_lambda_powertools/utilities/idempotency/persistence/dynamodb.py +++ b/aws_lambda_powertools/utilities/idempotency/persistence/dynamodb.py @@ -1,10 +1,12 @@ import datetime import logging +import os from typing import Any, Dict, Optional import boto3 from botocore.config import Config +from aws_lambda_powertools.shared import constants from aws_lambda_powertools.utilities.idempotency import BasePersistenceLayer from aws_lambda_powertools.utilities.idempotency.exceptions import ( IdempotencyItemAlreadyExistsError, @@ -20,6 +22,8 @@ def __init__( self, table_name: str, key_attr: str = "id", + static_pk_value: str = f"idempotency#{os.getenv(constants.LAMBDA_FUNCTION_NAME_ENV, '')}", + sort_key_attr: Optional[str] = None, expiry_attr: str = "expiration", status_attr: str = "status", data_attr: str = "data", @@ -35,7 +39,12 @@ def __init__( table_name: str Name of the table to use for storing execution records key_attr: str, optional - DynamoDB attribute name for key, by default "id" + DynamoDB attribute name for partition key, by default "id" + static_pk_value: str, optional + DynamoDB attribute value for partition key, by default "idempotency#". + This will be used if the sort_key_attr is set. + sort_key_attr: str, optional + DynamoDB attribute name for the sort key expiry_attr: str, optional DynamoDB attribute name for expiry timestamp, by default "expiration" status_attr: str, optional @@ -64,10 +73,14 @@ def __init__( self._boto_config = boto_config or Config() self._boto3_session = boto3_session or boto3.session.Session() + if sort_key_attr == key_attr: + raise ValueError(f"key_attr [{key_attr}] and sort_key_attr [{sort_key_attr}] cannot be the same!") self._table = None self.table_name = table_name self.key_attr = key_attr + self.static_pk_value = static_pk_value + self.sort_key_attr = sort_key_attr self.expiry_attr = expiry_attr self.status_attr = status_attr self.data_attr = data_attr @@ -93,6 +106,11 @@ def table(self, table): """ self._table = table + def _get_key(self, idempotency_key: str) -> dict: + if self.sort_key_attr: + return {self.key_attr: self.static_pk_value, self.sort_key_attr: idempotency_key} + return {self.key_attr: idempotency_key} + def _item_to_data_record(self, item: Dict[str, Any]) -> DataRecord: """ Translate raw item records from DynamoDB to DataRecord @@ -117,7 +135,7 @@ def _item_to_data_record(self, item: Dict[str, Any]) -> DataRecord: ) def _get_record(self, idempotency_key) -> DataRecord: - response = self.table.get_item(Key={self.key_attr: idempotency_key}, ConsistentRead=True) + response = self.table.get_item(Key=self._get_key(idempotency_key), ConsistentRead=True) try: item = response["Item"] @@ -127,7 +145,7 @@ def _get_record(self, idempotency_key) -> DataRecord: def _put_record(self, data_record: DataRecord) -> None: item = { - self.key_attr: data_record.idempotency_key, + **self._get_key(data_record.idempotency_key), self.expiry_attr: data_record.expiry_timestamp, self.status_attr: data_record.status, } @@ -168,7 +186,7 @@ def _update_record(self, data_record: DataRecord): expression_attr_names["#validation_key"] = self.validation_key_attr kwargs = { - "Key": {self.key_attr: data_record.idempotency_key}, + "Key": self._get_key(data_record.idempotency_key), "UpdateExpression": update_expression, "ExpressionAttributeValues": expression_attr_values, "ExpressionAttributeNames": expression_attr_names, @@ -178,4 +196,4 @@ def _update_record(self, data_record: DataRecord): def _delete_record(self, data_record: DataRecord) -> None: logger.debug(f"Deleting record for idempotency key: {data_record.idempotency_key}") - self.table.delete_item(Key={self.key_attr: data_record.idempotency_key}) + self.table.delete_item(Key=self._get_key(data_record.idempotency_key)) diff --git a/tests/functional/idempotency/test_idempotency.py b/tests/functional/idempotency/test_idempotency.py index b1d0914d181..043fb06a04a 100644 --- a/tests/functional/idempotency/test_idempotency.py +++ b/tests/functional/idempotency/test_idempotency.py @@ -783,11 +783,11 @@ def test_jmespath_with_powertools_json( # GIVEN an event_key_jmespath with powertools_json custom function persistence_store.configure(idempotency_config) sub_attr_value = "cognito_user" - key_attr_value = "some_key" - expected_value = [sub_attr_value, key_attr_value] + static_pk_value = "some_key" + expected_value = [sub_attr_value, static_pk_value] api_gateway_proxy_event = { "requestContext": {"authorizer": {"claims": {"sub": sub_attr_value}}}, - "body": serialize({"id": key_attr_value}), + "body": serialize({"id": static_pk_value}), } # WHEN calling _get_hashed_idempotency_key From d45ebfbf5f7eb1c73b273ba5594b12a54c92eaca Mon Sep 17 00:00:00 2001 From: Tom McCarthy Date: Wed, 10 Nov 2021 12:07:39 +0100 Subject: [PATCH 31/49] fix: change supported python version from 3.6.1 to 3.6.2, bump black (#807) --- poetry.lock | 73 +++++++++++++++++++++++++++++--------------------- pyproject.toml | 4 +-- 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/poetry.lock b/poetry.lock index bfda332dd0a..86aaa71ffa1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,11 +1,3 @@ -[[package]] -name = "appdirs" -version = "1.4.4" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "atomicwrites" version = "1.4.0" @@ -58,26 +50,32 @@ stevedore = ">=1.20.0" [[package]] name = "black" -version = "20.8b1" +version = "21.10b0" description = "The uncompromising code formatter." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.6.2" [package.dependencies] -appdirs = "*" click = ">=7.1.2" dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} mypy-extensions = ">=0.4.3" -pathspec = ">=0.6,<1" +pathspec = ">=0.9.0,<1" +platformdirs = ">=2" regex = ">=2020.1.8" -toml = ">=0.10.1" -typed-ast = ">=1.4.0" -typing-extensions = ">=3.7.4" +tomli = ">=0.2.6,<2.0.0" +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\""} +typing-extensions = [ + {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, + {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, +] [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +python2 = ["typed-ast (>=1.4.3)"] +uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "boto3" @@ -641,11 +639,11 @@ pyparsing = ">=2.0.2" [[package]] name = "pathspec" -version = "0.8.1" +version = "0.9.0" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "pbr" @@ -667,6 +665,18 @@ python-versions = ">= 3.6" mako = "*" markdown = ">=3.0" +[[package]] +name = "platformdirs" +version = "2.4.0" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + [[package]] name = "pluggy" version = "0.13.1" @@ -986,7 +996,7 @@ python-versions = "*" [[package]] name = "typing-extensions" -version = "3.10.0.0" +version = "3.10.0.2" description = "Backported and Experimental Type Hints for Python 3.5+" category = "main" optional = false @@ -1054,14 +1064,10 @@ pydantic = ["pydantic", "email-validator"] [metadata] lock-version = "1.1" -python-versions = "^3.6.1" -content-hash = "6eb7e379d0b2b14d0a9f374b66c6b6d2f236f0d23cffc0b4375835ba853ac5ae" +python-versions = "^3.6.2" +content-hash = "4585adda3e000d274de915286b570d232c3659e788548e7d46187a796b6cd73c" [metadata.files] -appdirs = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, @@ -1079,7 +1085,8 @@ bandit = [ {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"}, ] black = [ - {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, + {file = "black-21.10b0-py3-none-any.whl", hash = "sha256:6eb7448da9143ee65b856a5f3676b7dda98ad9abe0f87fce8c59291f15e82a5b"}, + {file = "black-21.10b0.tar.gz", hash = "sha256:a9952229092e325fe5f3dae56d81f639b23f7131eb840781947e4b2886030f33"}, ] boto3 = [ {file = "boto3-1.19.6-py3-none-any.whl", hash = "sha256:79e40de74faaf4b36fc885b10a2a39adb242ab499ba6d9506c6f91d0054b5338"}, @@ -1382,8 +1389,8 @@ packaging = [ {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, ] pathspec = [ - {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, - {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] pbr = [ {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"}, @@ -1392,6 +1399,10 @@ pbr = [ pdoc3 = [ {file = "pdoc3-0.10.0.tar.gz", hash = "sha256:5f22e7bcb969006738e1aa4219c75a32f34c2d62d46dc9d2fb2d3e0b0287e4b7"}, ] +platformdirs = [ + {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, + {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, +] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, @@ -1648,9 +1659,9 @@ typed-ast = [ {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, ] typing-extensions = [ - {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, - {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, - {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, + {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, + {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, + {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, ] urllib3 = [ {file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"}, diff --git a/pyproject.toml b/pyproject.toml index c177e4b5820..79905822ee0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ keywords = ["aws_lambda_powertools", "aws", "tracing", "logging", "lambda", "pow license = "MIT-0" [tool.poetry.dependencies] -python = "^3.6.1" +python = "^3.6.2" aws-xray-sdk = "^2.8.0" fastjsonschema = "^2.14.5" boto3 = "^1.18" @@ -31,7 +31,7 @@ email-validator = {version = "*", optional = true } [tool.poetry.dev-dependencies] coverage = {extras = ["toml"], version = "^6.0"} pytest = "^6.2.5" -black = "^20.8b1" +black = "^21.10.b0" flake8 = "^3.9.0" flake8-black = "^0.2.3" flake8-builtins = "^1.5.3" From 64689ef68548191d3d2d29258711267b78acbdee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Nov 2021 09:29:41 +0000 Subject: [PATCH 32/49] chore(deps-dev): bump mkdocs-material from 7.3.5 to 7.3.6 (#791) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.3.5 to 7.3.6.
Release notes

Sourced from mkdocs-material's releases.

mkdocs-material-7.3.6

  • Added support for adding titles to code blocks
Changelog

Sourced from mkdocs-material's changelog.

mkdocs-material-7.3.6+insiders-3.2.0 (2021-10-31)

  • Added support for feedback widget (Was this page helpful?)

mkdocs-material-7.3.6 (2021-10-30)

  • Added support for adding titles to code blocks

mkdocs-material-7.3.5+insiders-3.1.5 (2021-10-28)

  • Fixed #3144: Rogue link when using tags with auto-populated navigation
  • Fixed #3147: Code block line numbers appear in search results
  • Fixed #3158: Social cards do not strip HTML tags from title

mkdocs-material-7.3.5 (2021-10-27)

  • Added support for setting table of contents title via mkdocs.yml
  • Fixed back-to-top button position for right-to-left languages

mkdocs-material-7.3.4+insiders-3.1.4 (2021-10-17)

  • Fixed #2974: Text cropped with other fonts than Roboto in social plugin
  • Fixed #3099: Encoding problems with non-latin character in social plugin
  • Fixed #3112: Japanese segmenter not executed as part of new tokenizer
  • Fixed tags (front matter) appearing in search with disabled tags plugin

mkdocs-material-7.3.4 (2021-10-17)

  • Bumped MkDocs version to 1.2.3 to mitigate CVE-2021-40978
  • Fixed spacing issues when using integrate table of contents with tabs
  • Fixed some spacings issues for right-to-left languages
  • Fixed race condition in search initialization

mkdocs-material-7.3.3+insiders-3.1.3 (2021-10-12)

  • Added warnings to search plugin for unsupported options and syntax
  • Fixed #3503: Search sometimes returns entire page
  • Fixed #3089: Single-line code annotations disappear when printing

mkdocs-material-7.3.3 (2021-10-11)

  • Rewrite of entire documentation
  • Adjusted height of new content tabs to match single line code blocks
  • Fixed new content tabs missing right padding in some browsers on overflow
  • Fixed new content tabs bleeding out of flex container on overflow
  • Fixed new content tabs overflow scrolling bugs on some browsers
  • Fixed new content tabs stealing keyboard access when active
  • Fixed some spacings issues for right-to-left languages

mkdocs-material-7.3.2+insiders-3.1.2 (2021-10-06)

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=mkdocs-material&package-manager=pip&previous-version=7.3.5&new-version=7.3.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- poetry.lock | 16 ++++++++-------- pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/poetry.lock b/poetry.lock index 86aaa71ffa1..cdd26d8021c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -575,7 +575,7 @@ mkdocs = ">=0.17" [[package]] name = "mkdocs-material" -version = "7.3.5" +version = "7.3.6" description = "A Material Design theme for MkDocs" category = "dev" optional = false @@ -586,7 +586,7 @@ jinja2 = ">=2.11.1" markdown = ">=3.2" mkdocs = ">=1.2.3" mkdocs-material-extensions = ">=1.0" -pygments = ">=2.4" +pygments = ">=2.10" pymdown-extensions = ">=9.0" [[package]] @@ -733,7 +733,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" -version = "2.9.0" +version = "2.10.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false @@ -1065,7 +1065,7 @@ pydantic = ["pydantic", "email-validator"] [metadata] lock-version = "1.1" python-versions = "^3.6.2" -content-hash = "4585adda3e000d274de915286b570d232c3659e788548e7d46187a796b6cd73c" +content-hash = "30c52844bfc6deb473d20069996a09cf8ea5a9ce5c6ff6d84e844b0e041343c6" [metadata.files] atomicwrites = [ @@ -1348,8 +1348,8 @@ mkdocs-git-revision-date-plugin = [ {file = "mkdocs_git_revision_date_plugin-0.3.1-py3-none-any.whl", hash = "sha256:8ae50b45eb75d07b150a69726041860801615aae5f4adbd6b1cf4d51abaa03d5"}, ] mkdocs-material = [ - {file = "mkdocs-material-7.3.5.tar.gz", hash = "sha256:5edbf86b4249f413d54b8e2f1f2f8f1d2a9345f45e928303f4541e1d51f362c0"}, - {file = "mkdocs_material-7.3.5-py2.py3-none-any.whl", hash = "sha256:9fc09d2636d3249a7d75813c16360f3aa6987dff792c0e19f218fc80d48c2296"}, + {file = "mkdocs-material-7.3.6.tar.gz", hash = "sha256:1b1dbd8ef2508b358d93af55a5c5db3f141c95667fad802301ec621c40c7c217"}, + {file = "mkdocs_material-7.3.6-py2.py3-none-any.whl", hash = "sha256:1b6b3e9e09f922c2d7f1160fe15c8f43d4adc0d6fb81aa6ff0cbc7ef5b78ec75"}, ] mkdocs-material-extensions = [ {file = "mkdocs-material-extensions-1.0.1.tar.gz", hash = "sha256:6947fb7f5e4291e3c61405bad3539d81e0b3cd62ae0d66ced018128af509c68f"}, @@ -1444,8 +1444,8 @@ pyflakes = [ {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] pygments = [ - {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, - {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, + {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, + {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, ] pymdown-extensions = [ {file = "pymdown-extensions-9.0.tar.gz", hash = "sha256:01e4bec7f4b16beaba0087a74496401cf11afd69e3a11fe95cb593e5c698ef40"}, diff --git a/pyproject.toml b/pyproject.toml index 79905822ee0..3d3f11f1995 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ radon = "^5.1.0" xenon = "^0.8.0" flake8-eradicate = "^1.2.0" flake8-bugbear = "^21.9.2" -mkdocs-material = "^7.3.5" +mkdocs-material = "^7.3.6" mkdocs-git-revision-date-plugin = "^0.3.1" mike = "^0.6.0" mypy = "^0.910" From 56ee00a3f6f4952501f2af438290eb47af0dfda8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Nov 2021 09:30:15 +0000 Subject: [PATCH 33/49] chore(deps): bump boto3 from 1.19.6 to 1.20.3 (#809) Bumps [boto3](https://github.com/boto/boto3) from 1.19.6 to 1.20.3.
Changelog

Sourced from boto3's changelog.

1.20.3

  • api-change:backup: [botocore] AWS Backup SDK provides new options when scheduling backups: select supported services and resources that are assigned to a particular tag, linked to a combination of tags, or can be identified by a partial tag value, and exclude resources from their assignments.
  • api-change:ecs: [botocore] This release adds support for container instance health.
  • api-change:resiliencehub: [botocore] Initial release of AWS Resilience Hub, a managed service that enables you to define, validate, and track the resilience of your applications on AWS

1.20.2

  • api-change:batch: [botocore] Adds support for scheduling policy APIs.
  • api-change:health: [botocore] Documentation updates for AWS Health.
  • api-change:greengrassv2: [botocore] This release adds support for Greengrass core devices running Windows. You can now specify name of a Windows user to run a component.

1.20.1

  • bugfix:urllib3: [botocore] Fix NO_OP_TICKET import bug in older versions of urllib3

1.20.0

  • feature:EndpointResolver: [botocore] Adding support for resolving modeled FIPS and Dualstack endpoints.
  • feature:six: [botocore] Updated vendored version of six from 1.10.0 to 1.16.0
  • api-change:sagemaker: [botocore] SageMaker CreateEndpoint and UpdateEndpoint APIs now support additional deployment configuration to manage traffic shifting options and automatic rollback monitoring. DescribeEndpoint now shows new in-progress deployment details with stage status.
  • api-change:chime-sdk-meetings: [botocore] Updated format validation for ids and regions.
  • api-change:wafv2: [botocore] You can now configure rules to run a CAPTCHA check against web requests and, as needed, send a CAPTCHA challenge to the client.
  • api-change:ec2: [botocore] This release adds internal validation on the GatewayAssociationState field

1.19.12

  • api-change:ec2: [botocore] DescribeInstances now returns customer-owned IP addresses for instances running on an AWS Outpost.
  • api-change:translate: [botocore] This release enable customers to use their own KMS keys to encrypt output files when they submit a batch transform job.
  • api-change:resourcegroupstaggingapi: [botocore] Documentation updates and improvements.

1.19.11

  • api-change:chime-sdk-meetings: [botocore] The Amazon Chime SDK Meetings APIs allow software developers to create meetings and attendees for interactive audio, video, screen and content sharing in custom meeting applications which use the Amazon Chime SDK.
  • api-change:sagemaker: [botocore] ListDevices and DescribeDevice now show Edge Manager agent version.
  • api-change:connect: [botocore] This release adds CRUD operation support for Security profile resource in Amazon Connect
  • api-change:iotwireless: [botocore] Adding APIs for the FUOTA (firmware update over the air) and multicast for LoRaWAN devices and APIs to support event notification opt-in feature for Sidewalk related events. A few existing APIs need to be modified for this new feature.
  • api-change:ec2: [botocore] This release adds a new instance replacement strategy for EC2 Fleet, Spot Fleet. Now you can select an action to perform when your instance gets a rebalance notification. EC2 Fleet, Spot Fleet can launch a replacement then terminate the instance that received notification after a termination delay

... (truncated)

Commits
  • 2dd5562 Merge branch 'release-1.20.3'
  • e57f5ec Bumping version to 1.20.3
  • e8722f6 Add changelog entries from botocore
  • 3c1049b Merge branch 'release-1.20.2'
  • 359aa31 Merge branch 'release-1.20.2' into develop
  • 9c50d99 Bumping version to 1.20.2
  • 3d9ec64 Add changelog entries from botocore
  • c344cff Merge branch 'release-1.20.1'
  • 4ad4409 Merge branch 'release-1.20.1' into develop
  • 066a466 Bumping version to 1.20.1
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=boto3&package-manager=pip&previous-version=1.19.6&new-version=1.20.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- poetry.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index cdd26d8021c..7e9a28aa8ab 100644 --- a/poetry.lock +++ b/poetry.lock @@ -79,14 +79,14 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "boto3" -version = "1.19.6" +version = "1.20.3" description = "The AWS SDK for Python" category = "main" optional = false python-versions = ">= 3.6" [package.dependencies] -botocore = ">=1.22.6,<1.23.0" +botocore = ">=1.23.3,<1.24.0" jmespath = ">=0.7.1,<1.0.0" s3transfer = ">=0.5.0,<0.6.0" @@ -95,7 +95,7 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.22.6" +version = "1.23.3" description = "Low-level, data-driven core of boto 3." category = "main" optional = false @@ -1089,12 +1089,12 @@ black = [ {file = "black-21.10b0.tar.gz", hash = "sha256:a9952229092e325fe5f3dae56d81f639b23f7131eb840781947e4b2886030f33"}, ] boto3 = [ - {file = "boto3-1.19.6-py3-none-any.whl", hash = "sha256:79e40de74faaf4b36fc885b10a2a39adb242ab499ba6d9506c6f91d0054b5338"}, - {file = "boto3-1.19.6.tar.gz", hash = "sha256:e8e20230fd8dfd991fa8459696ff2da96b15ba720f05f96d43174b2f04b328a9"}, + {file = "boto3-1.20.3-py3-none-any.whl", hash = "sha256:21853231cc8545eb996a31c73d23a52e967888a70e4a949769a05f8f5cfb461f"}, + {file = "boto3-1.20.3.tar.gz", hash = "sha256:67059fd703a74c7c68ccb2afdc8de741b7635f2949a03b19df521c50adc5581d"}, ] botocore = [ - {file = "botocore-1.22.6-py3-none-any.whl", hash = "sha256:5fa5de2deef817d0ef52b97dec2bc6e4489a3145afa3f5f4ec0ad6a3b213a019"}, - {file = "botocore-1.22.6.tar.gz", hash = "sha256:9210881ee5eef6fff30a2e1acfe9dea068a0885a13731ab2034e35775379637f"}, + {file = "botocore-1.23.3-py3-none-any.whl", hash = "sha256:203be511cfc166c7ae1ef64621a40cfbe11e4572e6b1734b49bb00ee6563dc39"}, + {file = "botocore-1.23.3.tar.gz", hash = "sha256:8938d464c94e5f02ea6446722fc8024b45283fe3c465283ac7132f2cab9d1678"}, ] certifi = [ {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, From e09fe44b980c809b3a843ae98953b05eccd68898 Mon Sep 17 00:00:00 2001 From: Tom McCarthy Date: Fri, 12 Nov 2021 09:23:30 +0100 Subject: [PATCH 34/49] docs(idempotency): add support for DynamoDB composite keys (#808) --- docs/utilities/idempotency.md | 68 +++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md index 43eb1ac3a0b..18a99b53999 100644 --- a/docs/utilities/idempotency.md +++ b/docs/utilities/idempotency.md @@ -289,16 +289,40 @@ The client was successful in receiving the result after the retry. Since the Lam ### Handling exceptions -**The record in the persistence layer will be deleted** if your Lambda handler returns an exception. This means that new invocations will execute again despite having the same payload. +If you are using the `idempotent` decorator on your Lambda handler, any unhandled exceptions that are raised during the code execution will cause **the record in the persistence layer to be deleted**. +This means that new invocations will execute your code again despite having the same payload. If you don't want the record to be deleted, you need to catch exceptions within the idempotent function and return a successful response. -If you don't want the record to be deleted, you need to catch exceptions within the handler and return a successful response. ![Idempotent sequence exception](../media/idempotent_sequence_exception.png) +If you are using `idempotent_function`, any unhandled exceptions that are raised _inside_ the decorated function will cause the record in the persistence layer to be deleted, and allow the function to be executed again if retried. +If an Exception is raised _outside_ the scope of the decorated function and after your function has been called, the persistent record will not be affected. In this case, idempotency will be maintained for your decorated function. Example: + +=== "app.py" + +```python hl_lines="2-4 8-10" +def lambda_handler(event, context): + # If an exception is raised here, no idempotent record will ever get created as the + # idempotent function does not get called + do_some_stuff() + + result = call_external_service(data={"user": "user1", "id": 5}) + + # This exception will not cause the idempotent record to be deleted, since it + # happens after the decorated function has been successfully called + raise Exception + + +@idempotent_function(data_keyword_argument="data", config=config, persistence_store=dynamodb) +def call_external_service(data: dict, **kwargs): + result = requests.post('http://example.com', json={"user": data['user'], "transaction_id": data['id']} + return result.json() +``` + !!! warning - **We will raise `IdempotencyPersistenceLayerError`** if any of the calls to the persistence layer fail unexpectedly. + **We will raise `IdempotencyPersistenceLayerError`** if any of the calls to the persistence layer fail unexpectedly. - As this happens outside the scope of your Lambda handler, you are not going to be able to catch it. + As this happens outside the scope of your decorated function, you are not able to catch it if you're using the `idempotent` decorator on your Lambda handler. ### Persistence layers @@ -321,16 +345,18 @@ This persistence layer is built-in, and you can either use an existing DynamoDB ) ``` -These are knobs you can use when using DynamoDB as a persistence layer: +When using DynamoDB as a persistence layer, you can alter the attribute names by passing these parameters when initializing the persistence layer: Parameter | Required | Default | Description ------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------- **table_name** | :heavy_check_mark: | | Table name to store state -**key_attr** | | `id` | Primary key of the table. Hashed representation of the payload +**key_attr** | | `id` | Partition key of the table. Hashed representation of the payload (unless **sort_key_attr** is specified) **expiry_attr** | | `expiration` | Unix timestamp of when record expires **status_attr** | | `status` | Stores status of the lambda execution during and after invocation **data_attr** | | `data` | Stores results of successfully executed Lambda handlers **validation_key_attr** | | `validation` | Hashed representation of the parts of the event used for validation +**sort_key_attr** | | | Sort key of the table (if table is configured with a sort key). +**static_pk_value** | | `idempotency#{LAMBDA_FUNCTION_NAME}` | Static value to use as the partition key. Only used when **sort_key_attr** is set. ## Advanced @@ -590,6 +616,36 @@ The **`boto_config`** and **`boto3_session`** parameters enable you to pass in a ... ``` +### Using a DynamoDB table with a composite primary key + +If you wish to use this utility with a DynamoDB table that is configured with a composite primary key (uses both partition key and sort key), you +should set the `sort_key_attr` parameter when initializing your persistence layer. When this parameter is set, the partition key value for all idempotency entries +will be the same, with the idempotency key being saved as the sort key instead of the partition key. You can optionally set a static value for the partition +key using the `static_pk_value` parameter. If not specified, it will default to `idempotency#{LAMBDA_FUNCTION_NAME}`. + +=== "MyLambdaFunction" + + ```python hl_lines="5" + from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, idempotent + + persistence_layer = DynamoDBPersistenceLayer( + table_name="IdempotencyTable", + sort_key_attr='sort_key') + + + @idempotent(persistence_store=persistence_layer) + def handler(event, context): + return {"message": "success": "id": event['body']['id]} + ``` + +The example function above would cause data to be stored in DynamoDB like this: + +| id | sort_key | expiration | status | data | +|------------------------------|----------------------------------|------------|-------------|-------------------------------------| +| idempotency#MyLambdaFunction | 1e956ef7da78d0cb890be999aecc0c9e | 1636549553 | COMPLETED | {"id": 12391, "message": "success"} | +| idempotency#MyLambdaFunction | 2b2cdb5f86361e97b4383087c1ffdf27 | 1636549571 | COMPLETED | {"id": 527212, "message": "success"}| +| idempotency#MyLambdaFunction | f091d2527ad1c78f05d54cc3f363be80 | 1636549585 | IN_PROGRESS | | + ### Bring your own persistent store This utility provides an abstract base class (ABC), so that you can implement your choice of persistent storage layer. From d62b0a07ad25f9d60bfaac2937c126e475b77af7 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Fri, 12 Nov 2021 09:50:00 -0800 Subject: [PATCH 35/49] docs(api-gateway): add support for new router feature (#767) Co-authored-by: Dani Comnea Co-authored-by: Steve Cook Co-authored-by: Arthur Freund Co-authored-by: heitorlessa --- docs/core/event_handler/api_gateway.md | 136 +++++++++++++++++++++++-- 1 file changed, 130 insertions(+), 6 deletions(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index aeaa75e0d2a..70fdc92f3ae 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -12,6 +12,7 @@ Event handler for Amazon API Gateway REST/HTTP APIs and Application Loader Balan * Integrates with [Data classes utilities](../../utilities/data_classes.md){target="_blank"} to easily access event and identity information * Built-in support for Decimals JSON encoding * Support for dynamic path expressions +* Router to allow for splitting up the handler accross multiple files ## Getting started @@ -75,12 +76,11 @@ This is the sample infrastructure for API Gateway we are using for the examples Outputs: HelloWorldApigwURL: - Description: "API Gateway endpoint URL for Prod environment for Hello World Function" - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello" - - HelloWorldFunction: - Description: "Hello World Lambda Function ARN" - Value: !GetAtt HelloWorldFunction.Arn + Description: "API Gateway endpoint URL for Prod environment for Hello World Function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn ``` ### API Gateway decorator @@ -853,6 +853,130 @@ You can instruct API Gateway handler to use a custom serializer to best suit you } ``` +### Split routes with Router + +As you grow the number of routes a given Lambda function should handle, it is natural to split routes into separate files to ease maintenance - That's where the `Router` feature is useful. + +Let's assume you have `app.py` as your Lambda function entrypoint and routes in `users.py`, this is how you'd use the `Router` feature. + +=== "users.py" + + We import **Router** instead of **ApiGatewayResolver**; syntax wise is exactly the same. + + ```python hl_lines="4 8 12 15 21" + import itertools + from typing import Dict + + from aws_lambda_powertools import Logger + from aws_lambda_powertools.event_handler.api_gateway import Router + + logger = Logger(child=True) + router = Router() + USERS = {"user1": "details_here", "user2": "details_here", "user3": "details_here"} + + + @router.get("/users") + def get_users() -> Dict: + # /users?limit=1 + pagination_limit = router.current_event.get_query_string_value(name="limit", default_value=10) + + logger.info(f"Fetching the first {pagination_limit} users...") + ret = dict(itertools.islice(USERS.items(), int(pagination_limit))) + return {"items": [ret]} + + @router.get("/users/") + def get_user(username: str) -> Dict: + logger.info(f"Fetching username {username}") + return {"details": USERS.get(username, {})} + + # many other related /users routing + ``` + +=== "app.py" + + We use `include_router` method and include all user routers registered in the `router` global object. + + ```python hl_lines="6 8-9" + from typing import Dict + + from aws_lambda_powertools.event_handler import ApiGatewayResolver + from aws_lambda_powertools.utilities.typing import LambdaContext + + import users + + app = ApiGatewayResolver() + app.include_router(users.router) + + + def lambda_handler(event: Dict, context: LambdaContext): + return app.resolve(event, context) + ``` + +#### Route prefix + +In the previous example, `users.py` routes had a `/users` prefix. This might grow over time and become repetitive. + +When necessary, you can set a prefix when including a router object. This means you could remove `/users` prefix in `users.py` altogether. + +=== "app.py" + + ```python hl_lines="9" + from typing import Dict + + from aws_lambda_powertools.event_handler import ApiGatewayResolver + from aws_lambda_powertools.utilities.typing import LambdaContext + + import users + + app = ApiGatewayResolver() + app.include_router(users.router, prefix="/users") # prefix '/users' to any route in `users.router` + + + def lambda_handler(event: Dict, context: LambdaContext): + return app.resolve(event, context) + ``` + +=== "users.py" + + ```python hl_lines="11 15" + from typing import Dict + + from aws_lambda_powertools import Logger + from aws_lambda_powertools.event_handler.api_gateway import Router + + logger = Logger(child=True) + router = Router() + USERS = {"user1": "details", "user2": "details", "user3": "details"} + + + @router.get("/") # /users, when we set the prefix in app.py + def get_users() -> Dict: + ... + + @router.get("/") + def get_user(username: str) -> Dict: + ... + + # many other related /users routing + ``` + + +#### Trade-offs + +!!! tip "TL;DR. Balance your latency requirements, cognitive overload, least privilege, and operational overhead to decide between one, few, or many single purpose functions." + +Route splitting feature helps accommodate customers familiar with popular frameworks and practices found in the Python community. + +It can help better organize your code and reason + +This can also quickly lead to discussions whether it facilitates a monolithic vs single-purpose function. To this end, these are common trade-offs you'll encounter as you grow your Serverless service, specifically synchronous functions. + +**Least privilege**. Start with a monolithic function, then split them as their data access & boundaries become clearer. Treat Lambda functions as separate logical resources to more easily scope permissions. + +**Package size**. Consider Lambda Layers for third-party dependencies and service-level shared code. Treat third-party dependencies as dev dependencies, and Lambda Layers as a mechanism to speed up build and deployments. + +**Cold start**. High load can diminish the benefit of monolithic functions depending on your latency requirements. Always load test to pragmatically balance between your customer experience and development cognitive load. + ## Testing your code You can test your routes by passing a proxy event request where `path` and `httpMethod`. From 6c2f270d193a01b68f4705ea105be5374776d2b5 Mon Sep 17 00:00:00 2001 From: Heitor Lessa Date: Fri, 12 Nov 2021 18:56:43 +0100 Subject: [PATCH 36/49] feat(logger): add ALB correlation ID support (#816) --- aws_lambda_powertools/logging/correlation_paths.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aws_lambda_powertools/logging/correlation_paths.py b/aws_lambda_powertools/logging/correlation_paths.py index b6926f08591..bd889f9df44 100644 --- a/aws_lambda_powertools/logging/correlation_paths.py +++ b/aws_lambda_powertools/logging/correlation_paths.py @@ -2,6 +2,7 @@ API_GATEWAY_REST = "requestContext.requestId" API_GATEWAY_HTTP = API_GATEWAY_REST +ALB = "headers.x-amzn-trace-id" APPSYNC_AUTHORIZER = "requestContext.requestId" APPSYNC_RESOLVER = 'request.headers."x-amzn-trace-id"' APPLICATION_LOAD_BALANCER = 'headers."x-amzn-trace-id"' From e34469a2a6642bc055d1aa4a306ddca227b5b0c9 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Sat, 13 Nov 2021 17:14:37 +0100 Subject: [PATCH 37/49] revert "feat(logger): add ALB correlation ID support (#816)" This reverts commit 6c2f270d193a01b68f4705ea105be5374776d2b5. --- aws_lambda_powertools/logging/correlation_paths.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aws_lambda_powertools/logging/correlation_paths.py b/aws_lambda_powertools/logging/correlation_paths.py index bd889f9df44..b6926f08591 100644 --- a/aws_lambda_powertools/logging/correlation_paths.py +++ b/aws_lambda_powertools/logging/correlation_paths.py @@ -2,7 +2,6 @@ API_GATEWAY_REST = "requestContext.requestId" API_GATEWAY_HTTP = API_GATEWAY_REST -ALB = "headers.x-amzn-trace-id" APPSYNC_AUTHORIZER = "requestContext.requestId" APPSYNC_RESOLVER = 'request.headers."x-amzn-trace-id"' APPLICATION_LOAD_BALANCER = 'headers."x-amzn-trace-id"' From 0728aa28dd682e3367df990ea9135fff0f8203b1 Mon Sep 17 00:00:00 2001 From: Heitor Lessa Date: Sat, 13 Nov 2021 19:06:44 +0100 Subject: [PATCH 38/49] fix(parser): body/QS can be null or omitted in apigw v1/v2 (#820) --- .../utilities/parser/models/apigw.py | 2 +- .../utilities/parser/models/apigwv2.py | 4 ++-- tests/functional/parser/test_apigw.py | 8 +++++++- tests/functional/parser/test_apigwv2.py | 15 ++++++++++++++- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/aws_lambda_powertools/utilities/parser/models/apigw.py b/aws_lambda_powertools/utilities/parser/models/apigw.py index 44ddda6e4f1..283a73da9c3 100644 --- a/aws_lambda_powertools/utilities/parser/models/apigw.py +++ b/aws_lambda_powertools/utilities/parser/models/apigw.py @@ -89,4 +89,4 @@ class APIGatewayProxyEventModel(BaseModel): pathParameters: Optional[Dict[str, str]] stageVariables: Optional[Dict[str, str]] isBase64Encoded: bool - body: str + body: Optional[str] diff --git a/aws_lambda_powertools/utilities/parser/models/apigwv2.py b/aws_lambda_powertools/utilities/parser/models/apigwv2.py index 4243315bb21..36dd85b907e 100644 --- a/aws_lambda_powertools/utilities/parser/models/apigwv2.py +++ b/aws_lambda_powertools/utilities/parser/models/apigwv2.py @@ -63,9 +63,9 @@ class APIGatewayProxyEventV2Model(BaseModel): rawQueryString: str cookies: Optional[List[str]] headers: Dict[str, str] - queryStringParameters: Dict[str, str] + queryStringParameters: Optional[Dict[str, str]] pathParameters: Optional[Dict[str, str]] stageVariables: Optional[Dict[str, str]] requestContext: RequestContextV2 - body: str + body: Optional[str] isBase64Encoded: bool diff --git a/tests/functional/parser/test_apigw.py b/tests/functional/parser/test_apigw.py index d657a0dbe4d..35b2fdb1926 100644 --- a/tests/functional/parser/test_apigw.py +++ b/tests/functional/parser/test_apigw.py @@ -1,7 +1,7 @@ import pytest from pydantic import ValidationError -from aws_lambda_powertools.utilities.parser import envelopes, event_parser +from aws_lambda_powertools.utilities.parser import envelopes, event_parser, parse from aws_lambda_powertools.utilities.parser.models import APIGatewayProxyEventModel from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyApiGatewayBusiness @@ -144,3 +144,9 @@ def test_apigw_event_with_invalid_websocket_request(): expected_msg = "messageId is available only when the `eventType` is `MESSAGE`" assert errors[0]["msg"] == expected_msg assert expected_msg in str(err.value) + + +def test_apigw_event_empty_body(): + event = load_event("apiGatewayProxyEvent.json") + event["body"] = None + parse(event=event, model=APIGatewayProxyEventModel) diff --git a/tests/functional/parser/test_apigwv2.py b/tests/functional/parser/test_apigwv2.py index ee6a4790cd4..d3510b185dd 100644 --- a/tests/functional/parser/test_apigwv2.py +++ b/tests/functional/parser/test_apigwv2.py @@ -1,4 +1,4 @@ -from aws_lambda_powertools.utilities.parser import envelopes, event_parser +from aws_lambda_powertools.utilities.parser import envelopes, event_parser, parse from aws_lambda_powertools.utilities.parser.models import ( APIGatewayProxyEventV2Model, RequestContextV2, @@ -90,3 +90,16 @@ def test_api_gateway_proxy_v2_event_iam_authorizer(): assert iam.principalOrgId == "AwsOrgId" assert iam.userArn == "arn:aws:iam::1234567890:user/Admin" assert iam.userId == "AROA2ZJZYVRE7Y3TUXHH6" + + +def test_apigw_event_empty_body(): + event = load_event("apiGatewayProxyV2Event.json") + event.pop("body") # API GW v2 removes certain keys when no data is passed + parse(event=event, model=APIGatewayProxyEventV2Model) + + +def test_apigw_event_empty_query_strings(): + event = load_event("apiGatewayProxyV2Event.json") + event["rawQueryString"] = "" + event.pop("queryStringParameters") # API GW v2 removes certain keys when no data is passed + parse(event=event, model=APIGatewayProxyEventV2Model) From 93c3a91136f13a56176b29588829deb9c5f83c99 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Sat, 13 Nov 2021 19:55:04 +0100 Subject: [PATCH 39/49] docs: use higher contrast font --- mkdocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index fc51acb8b47..54a0fa50a67 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -30,6 +30,8 @@ nav: theme: name: material + font: + text: Ubuntu palette: - scheme: default primary: deep purple From cb5465de6017c82a3d5ddd06c01748a7b5f2c207 Mon Sep 17 00:00:00 2001 From: Heitor Lessa Date: Sat, 13 Nov 2021 19:57:22 +0100 Subject: [PATCH 40/49] docs: use higher contrast font (#822) --- mkdocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index fc51acb8b47..54a0fa50a67 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -30,6 +30,8 @@ nav: theme: name: material + font: + text: Ubuntu palette: - scheme: default primary: deep purple From 22469c29d08a6277bc76043b2cd6ccbeaac1a129 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Sat, 13 Nov 2021 19:59:36 +0100 Subject: [PATCH 41/49] docs: Idiomatic tenet updated to Progressive --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index e513f2ba8ed..86b91635163 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,7 +17,7 @@ This project separates core utilities that will be available in other runtimes v * **Keep it lean**. Additional dependencies are carefully considered for security and ease of maintenance, and prevent negatively impacting startup time. * **We strive for backwards compatibility**. New features and changes should keep backwards compatibility. If a breaking change cannot be avoided, the deprecation and migration process should be clearly defined. * **We work backwards from the community**. We aim to strike a balance of what would work best for 80% of customers. Emerging practices are considered and discussed via Requests for Comment (RFCs) -* **Idiomatic**. Utilities follow programming language idioms and language-specific best practices. +* **Progressive**. Utilities are designed to be incrementally adoptable for customers at any stage of their Serverless journey. They follow language idioms and their community’s common practices. ## Install From f07bc9a0806d2eca52a09eb1863bed8c5cc53063 Mon Sep 17 00:00:00 2001 From: Heitor Lessa Date: Sat, 13 Nov 2021 20:03:36 +0100 Subject: [PATCH 42/49] docs(tenets): update Idiomatic tenet to Progressive (#823) --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index e513f2ba8ed..86b91635163 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,7 +17,7 @@ This project separates core utilities that will be available in other runtimes v * **Keep it lean**. Additional dependencies are carefully considered for security and ease of maintenance, and prevent negatively impacting startup time. * **We strive for backwards compatibility**. New features and changes should keep backwards compatibility. If a breaking change cannot be avoided, the deprecation and migration process should be clearly defined. * **We work backwards from the community**. We aim to strike a balance of what would work best for 80% of customers. Emerging practices are considered and discussed via Requests for Comment (RFCs) -* **Idiomatic**. Utilities follow programming language idioms and language-specific best practices. +* **Progressive**. Utilities are designed to be incrementally adoptable for customers at any stage of their Serverless journey. They follow language idioms and their community’s common practices. ## Install From c2aaddcaf11c0f7d75871d99acc2c5d324603126 Mon Sep 17 00:00:00 2001 From: Heitor Lessa Date: Sat, 13 Nov 2021 20:04:50 +0100 Subject: [PATCH 43/49] docs(appsync): add new router feature (#821) --- docs/core/event_handler/appsync.md | 58 ++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/docs/core/event_handler/appsync.md b/docs/core/event_handler/appsync.md index 93bb7bf69a5..396955b0e61 100644 --- a/docs/core/event_handler/appsync.md +++ b/docs/core/event_handler/appsync.md @@ -711,6 +711,64 @@ You can subclass `AppSyncResolverEvent` to bring your own set of methods to hand } ``` +### Split operations with Router + +As you grow the number of related GraphQL operations a given Lambda function should handle, it is natural to split them into separate files to ease maintenance - That's where the `Router` feature is useful. + +Let's assume you have `app.py` as your Lambda function entrypoint and routes in `users.py`, this is how you'd use the `Router` feature. + +=== "resolvers/location.py" + + We import **Router** instead of **AppSyncResolver**; syntax wise is exactly the same. + + ```python hl_lines="4 7 10 15" + from typing import Any, Dict, List + + from aws_lambda_powertools import Logger + from aws_lambda_powertools.event_handler.appsync import Router + + logger = Logger(child=True) + router = Router() + + + @router.resolver(type_name="Query", field_name="listLocations") + def list_locations(merchant_id: str) -> List[Dict[str, Any]]: + return [{"name": "Location name", "merchant_id": merchant_id}] + + + @router.resolver(type_name="Location", field_name="status") + def resolve_status(merchant_id: str) -> str: + logger.debug(f"Resolve status for merchant_id: {merchant_id}") + return "FOO" + ``` + +=== "app.py" + + We use `include_router` method and include all location operations registered in the `router` global object. + + ```python hl_lines="8 13" + from typing import Dict + + from aws_lambda_powertools import Logger, Tracer + from aws_lambda_powertools.event_handler import AppSyncResolver + from aws_lambda_powertools.logging.correlation_paths import APPSYNC_RESOLVER + from aws_lambda_powertools.utilities.typing import LambdaContext + + from resolvers import location + + tracer = Tracer() + logger = Logger() + app = AppSyncResolver() + app.include_router(location.router) + + + @tracer.capture_lambda_handler + @logger.inject_lambda_context(correlation_id_path=APPSYNC_RESOLVER) + def lambda_handler(event: Dict, context: LambdaContext): + app.resolve(event, context) + ``` + + ## Testing your code You can test your resolvers by passing a mocked or actual AppSync Lambda event that you're expecting. From bda42a887f613f9157a983338a6f00dcb28af416 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Nov 2021 13:09:11 +0000 Subject: [PATCH 44/49] chore(deps): bump boto3 from 1.20.3 to 1.20.5 (#817) Bumps [boto3](https://github.com/boto/boto3) from 1.20.3 to 1.20.5.
Changelog

Sourced from boto3's changelog.

1.20.5

  • api-change:ec2: [botocore] C6i instances are powered by a third-generation Intel Xeon Scalable processor (Ice Lake) delivering all-core turbo frequency of 3.5 GHz. G5 instances feature up to 8 NVIDIA A10G Tensor Core GPUs and second generation AMD EPYC processors.
  • api-change:ssm: [botocore] This Patch Manager release supports creating Patch Baselines for RaspberryPi OS (formerly Raspbian)
  • api-change:devops-guru: [botocore] Add support for cross account APIs.
  • api-change:connect: [botocore] This release adds APIs for creating and managing scheduled tasks. Additionally, adds APIs to describe and update a contact and list associated references.
  • api-change:mediaconvert: [botocore] AWS Elemental MediaConvert SDK has added automatic modes for GOP configuration and added the ability to ingest screen recordings generated by Safari on MacOS 12 Monterey.

1.20.4

  • api-change:dynamodb: [botocore] Updated Help section for "dynamodb update-contributor-insights" API
  • api-change:ec2: [botocore] This release provides an additional route target for the VPC route table.
  • api-change:translate: [botocore] This release enables customers to import Multi-Directional Custom Terminology and use Multi-Directional Custom Terminology in both real-time translation and asynchronous batch translation.
Commits
  • 28305a8 Merge branch 'release-1.20.5'
  • 80cb11b Bumping version to 1.20.5
  • 982f5c3 Add changelog entries from botocore
  • ca177e6 Merge branch 'release-1.20.4'
  • 9780362 Merge branch 'release-1.20.4' into develop
  • f4233c1 Bumping version to 1.20.4
  • bca7e02 Add changelog entries from botocore
  • 684da62 Add maintenance notice to README (#3068)
  • 70e6f6c Merge branch 'release-1.20.3' into develop
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=boto3&package-manager=pip&previous-version=1.20.3&new-version=1.20.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- poetry.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7e9a28aa8ab..f9a61b2c440 100644 --- a/poetry.lock +++ b/poetry.lock @@ -79,14 +79,14 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "boto3" -version = "1.20.3" +version = "1.20.5" description = "The AWS SDK for Python" category = "main" optional = false python-versions = ">= 3.6" [package.dependencies] -botocore = ">=1.23.3,<1.24.0" +botocore = ">=1.23.5,<1.24.0" jmespath = ">=0.7.1,<1.0.0" s3transfer = ">=0.5.0,<0.6.0" @@ -95,7 +95,7 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.23.3" +version = "1.23.5" description = "Low-level, data-driven core of boto 3." category = "main" optional = false @@ -1089,12 +1089,12 @@ black = [ {file = "black-21.10b0.tar.gz", hash = "sha256:a9952229092e325fe5f3dae56d81f639b23f7131eb840781947e4b2886030f33"}, ] boto3 = [ - {file = "boto3-1.20.3-py3-none-any.whl", hash = "sha256:21853231cc8545eb996a31c73d23a52e967888a70e4a949769a05f8f5cfb461f"}, - {file = "boto3-1.20.3.tar.gz", hash = "sha256:67059fd703a74c7c68ccb2afdc8de741b7635f2949a03b19df521c50adc5581d"}, + {file = "boto3-1.20.5-py3-none-any.whl", hash = "sha256:81ca80fbb3d551819c35c809cb159fd0bec6701d3d8f0e5906a22da7558d098e"}, + {file = "boto3-1.20.5.tar.gz", hash = "sha256:cc620c289b12d7bf7c2706b517c9f8950f9be4622aacc9e7580b8b4ee0d3bc73"}, ] botocore = [ - {file = "botocore-1.23.3-py3-none-any.whl", hash = "sha256:203be511cfc166c7ae1ef64621a40cfbe11e4572e6b1734b49bb00ee6563dc39"}, - {file = "botocore-1.23.3.tar.gz", hash = "sha256:8938d464c94e5f02ea6446722fc8024b45283fe3c465283ac7132f2cab9d1678"}, + {file = "botocore-1.23.5-py3-none-any.whl", hash = "sha256:c8eaeee0bac356396386aa9165043808fe736fb9e03ac0dedb1dfd82f41ad1a3"}, + {file = "botocore-1.23.5.tar.gz", hash = "sha256:49d1f012dc8467577a5fe603fc87cc13af816dd926b2bc2e28a3b2999ab14d36"}, ] certifi = [ {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, From c92c1c0120dd25e5df136915e1722dac0d129493 Mon Sep 17 00:00:00 2001 From: Heitor Lessa Date: Wed, 17 Nov 2021 10:53:34 +0100 Subject: [PATCH 45/49] docs(apigateway): re-add sample layout, add considerations (#826) --- docs/core/event_handler/api_gateway.md | 218 ++++++++++++++++++++++++- docs/core/event_handler/appsync.md | 2 + docs/media/micro-function.png | Bin 0 -> 86833 bytes docs/media/monolithic-function.png | Bin 0 -> 81725 bytes 4 files changed, 211 insertions(+), 9 deletions(-) create mode 100644 docs/media/micro-function.png create mode 100644 docs/media/monolithic-function.png diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 70fdc92f3ae..f9482edaacf 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -896,14 +896,16 @@ Let's assume you have `app.py` as your Lambda function entrypoint and routes in We use `include_router` method and include all user routers registered in the `router` global object. - ```python hl_lines="6 8-9" + ```python hl_lines="7 10-11" from typing import Dict + from aws_lambda_powertools import Logger from aws_lambda_powertools.event_handler import ApiGatewayResolver from aws_lambda_powertools.utilities.typing import LambdaContext import users + logger = Logger() app = ApiGatewayResolver() app.include_router(users.router) @@ -960,22 +962,220 @@ When necessary, you can set a prefix when including a router object. This means # many other related /users routing ``` +#### Sample layout + +!!! info "We use ALB to demonstrate that the UX remains the same" + +This sample project contains an Users function with two distinct set of routes, `/users` and `/health`. The layout optimizes for code sharing, no custom build tooling, and it uses [Lambda Layers](../../index.md#lambda-layer) to install Lambda Powertools. + +=== "Project layout" + + + ```python hl_lines="6 8 10-13" + . + ├── Pipfile # project app & dev dependencies; poetry, pipenv, etc. + ├── Pipfile.lock + ├── mypy.ini # namespace_packages = True + ├── .env # VSCode only. PYTHONPATH="users:${PYTHONPATH}" + ├── users + │ ├── requirements.txt # sam build detect it automatically due to CodeUri: users, e.g. pipenv lock -r > users/requirements.txt + │ ├── lambda_function.py # this will be our users Lambda fn; it could be split in folders if we want separate fns same code base + │ ├── constants.py + │ └── routers # routers module + │ ├── __init__.py + │ ├── users.py # /users routes, e.g. from routers import users; users.router + │ ├── health.py # /health routes, e.g. from routers import health; health.router + ├── template.yaml # SAM template.yml, CodeUri: users, Handler: users.main.lambda_handler + └── tests + ├── __init__.py + ├── unit + │ ├── __init__.py + │ └── test_users.py # unit tests for the users router + │ └── test_health.py # unit tests for the health router + └── functional + ├── __init__.py + ├── conftest.py # pytest fixtures for the functional tests + └── test_lambda_function.py # functional tests for the main lambda handler + ``` + +=== "template.yml" + + ```yaml hl_lines="20-21" + AWSTemplateFormatVersion: '2010-09-09' + Transform: AWS::Serverless-2016-10-31 + Description: Example service with multiple routes + Globals: + Function: + Timeout: 10 + MemorySize: 512 + Runtime: python3.9 + Tracing: Active + Environment: + Variables: + LOG_LEVEL: INFO + POWERTOOLS_LOGGER_LOG_EVENT: true + POWERTOOLS_METRICS_NAMESPACE: MyServerlessApplication + POWERTOOLS_SERVICE_NAME: users + Resources: + UsersService: + Type: AWS::Serverless::Function + Properties: + Handler: lambda_function.lambda_handler + CodeUri: users + Layers: + # Latest version: https://awslabs.github.io/aws-lambda-powertools-python/latest/#lambda-layer + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:3 + Events: + ByUser: + Type: Api + Properties: + Path: /users/{name} + Method: GET + AllUsers: + Type: Api + Properties: + Path: /users + Method: GET + HealthCheck: + Type: Api + Properties: + Path: /status + Method: GET + Outputs: + UsersApiEndpoint: + Description: "API Gateway endpoint URL for Prod environment for Users Function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod" + AllUsersURL: + Description: "URL to fetch all registered users" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/users" + ByUserURL: + Description: "URL to retrieve details by user" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/users/test" + UsersServiceFunctionArn: + Description: "Users Lambda Function ARN" + Value: !GetAtt UsersService.Arn + ``` + +=== "users/lambda_function.py" + + ```python hl_lines="9 15-16" + from typing import Dict + + from aws_lambda_powertools import Logger, Tracer + from aws_lambda_powertools.event_handler import ApiGatewayResolver + from aws_lambda_powertools.event_handler.api_gateway import ProxyEventType + from aws_lambda_powertools.logging.correlation_paths import APPLICATION_LOAD_BALANCER + from aws_lambda_powertools.utilities.typing import LambdaContext + + from routers import health, users + + tracer = Tracer() + logger = Logger() + app = ApiGatewayResolver(proxy_type=ProxyEventType.ALBEvent) + + app.include_router(health.router) + app.include_router(users.router) + + + @logger.inject_lambda_context(correlation_id_path=APPLICATION_LOAD_BALANCER) + @tracer.capture_lambda_handler + def lambda_handler(event: Dict, context: LambdaContext): + return app.resolve(event, context) + ``` + +=== "users/routers/health.py" + + ```python hl_lines="4 6-7 10" + from typing import Dict + + from aws_lambda_powertools import Logger + from aws_lambda_powertools.event_handler.api_gateway import Router + + router = Router() + logger = Logger(child=True) + + + @router.get("/status") + def health() -> Dict: + logger.debug("Health check called") + return {"status": "OK"} + ``` + +=== "tests/functional/test_users.py" + + ```python hl_lines="3" + import json + + from users import main # follows namespace package from root + + + def test_lambda_handler(apigw_event, lambda_context): + ret = main.lambda_handler(apigw_event, lambda_context) + expected = json.dumps({"message": "hello universe"}, separators=(",", ":")) + + assert ret["statusCode"] == 200 + assert ret["body"] == expected + ``` + +=== ".env" + + > Note: It is not needed for PyCharm (select folder as source). + + This is necessary for Visual Studio Code, so integrated tooling works without failing import. + + ```bash + PYTHONPATH="users:${PYTHONPATH}" + ``` + +### Considerations + +This utility is optimized for fast startup, minimal feature set, and to quickly on-board customers familiar with frameworks like Flask — it's not meant to be a fully fledged framework. + +Event Handler naturally leads to a single Lambda function handling multiple routes for a given service, which can be eventually broken into multiple functions. + +Both single (monolithic) and multiple functions (micro) offer different set of trade-offs worth knowing. + +!!! tip "TL;DR. Start with a monolithic function, add additional functions with new handlers, and possibly break into micro functions if necessary." + +#### Monolithic function + +![Monolithic function sample](./../../media/monolithic-function.png) + +A monolithic function means that your final code artifact will be deployed to a single function. This is generally the best approach to start. + +**Benefits** + +* **Code reuse**. It's easier to reason about your service, modularize it and reuse code as it grows. Eventually, it can be turned into a standalone library. +* **No custom tooling**. Monolithic functions are treated just like normal Python packages; no upfront investment in tooling. +* **Faster deployment and debugging**. Whether you use all-at-once, linear, or canary deployments, a monolithic function is a single deployable unit. IDEs like PyCharm and VSCode have tooling to quickly profile, visualize, and step through debug any Python package. + +**Downsides** -#### Trade-offs +* **Cold starts**. Frequent deployments and/or high load can diminish the benefit of monolithic functions depending on your latency requirements, due to [Lambda scaling model](https://docs.aws.amazon.com/lambda/latest/dg/invocation-scaling.html){target="_blank"}. Always load test to pragmatically balance between your customer experience and development cognitive load. +* **Granular security permissions**. The micro function approach enables you to use fine-grained permissions & access controls, separate external dependencies & code signing at the function level. Conversely, you could have multiple functions while duplicating the final code artifact in a monolithic approach. + - Regardless, least privilege can be applied to either approaches. +* **Higher risk per deployment**. A misconfiguration or invalid import can cause disruption if not caught earlier in automated testing. Multiple functions can mitigate misconfigurations but they would still share the same code artifact. You can further minimize risks with multiple environments in your CI/CD pipeline. -!!! tip "TL;DR. Balance your latency requirements, cognitive overload, least privilege, and operational overhead to decide between one, few, or many single purpose functions." +#### Micro function -Route splitting feature helps accommodate customers familiar with popular frameworks and practices found in the Python community. +![Micro function sample](./../../media/micro-function.png) -It can help better organize your code and reason +A micro function means that your final code artifact will be different to each function deployed. This is generally the approach to start if you're looking for fine-grain control and/or high load on certain parts of your service. -This can also quickly lead to discussions whether it facilitates a monolithic vs single-purpose function. To this end, these are common trade-offs you'll encounter as you grow your Serverless service, specifically synchronous functions. +**Benefits** -**Least privilege**. Start with a monolithic function, then split them as their data access & boundaries become clearer. Treat Lambda functions as separate logical resources to more easily scope permissions. +* **Granular scaling**. A micro function can benefit from the [Lambda scaling model](https://docs.aws.amazon.com/lambda/latest/dg/invocation-scaling.html){target="_blank"} to scale differently depending on each part of your application. Concurrency controls and provisioned concurrency can also be used at a granular level for capacity management. +* **Discoverability**. Micro functions are easier do visualize when using distributed tracing. Their high-level architectures can be self-explanatory, and complexity is highly visible — assuming each function is named to the business purpose it serves. +* **Package size**. An independent function can be significant smaller (KB vs MB) depending on external dependencies it require to perform its purpose. Conversely, a monolithic approach can benefit from [Lambda Layers](https://docs.aws.amazon.com/lambda/latest/dg/invocation-layers.html){target="_blank"} to optimize builds for external dependencies. -**Package size**. Consider Lambda Layers for third-party dependencies and service-level shared code. Treat third-party dependencies as dev dependencies, and Lambda Layers as a mechanism to speed up build and deployments. +**Downsides** -**Cold start**. High load can diminish the benefit of monolithic functions depending on your latency requirements. Always load test to pragmatically balance between your customer experience and development cognitive load. +* **Upfront investment**. Python ecosystem doesn't use a bundler — you need a custom build tooling to ensure each function only has what it needs and account for [C bindings for runtime compatibility](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html){target="_blank"}. Operations become more elaborate — you need to standardize tracing labels/annotations, structured logging, and metrics to pinpoint root causes. + - Engineering discipline is necessary for both approaches. Micro-function approach however requires further attention in consistency as the number of functions grow, just like any distributed system. +* **Harder to share code**. Shared code must be carefully evaluated to avoid unnecessary deployments when that changes. Equally, if shared code isn't a library, +your development, building, deployment tooling need to accommodate the distinct layout. +* **Slower safe deployments**. Safely deploying multiple functions require coordination — AWS CodeDeploy deploys and verifies each function sequentially. This increases lead time substantially (minutes to hours) depending on the deployment strategy you choose. You can mitigate it by selectively enabling it in prod-like environments only, and where the risk profile is applicable. + - Automated testing, operational and security reviews are essential to stability in either approaches. ## Testing your code diff --git a/docs/core/event_handler/appsync.md b/docs/core/event_handler/appsync.md index 396955b0e61..ce9150113b6 100644 --- a/docs/core/event_handler/appsync.md +++ b/docs/core/event_handler/appsync.md @@ -713,6 +713,8 @@ You can subclass `AppSyncResolverEvent` to bring your own set of methods to hand ### Split operations with Router +!!! tip "Read the **[considerations section for trade-offs between monolithic and micro functions](./api_gateway.md#considerations){target="_blank"}**, as it's also applicable here." + As you grow the number of related GraphQL operations a given Lambda function should handle, it is natural to split them into separate files to ease maintenance - That's where the `Router` feature is useful. Let's assume you have `app.py` as your Lambda function entrypoint and routes in `users.py`, this is how you'd use the `Router` feature. diff --git a/docs/media/micro-function.png b/docs/media/micro-function.png new file mode 100644 index 0000000000000000000000000000000000000000..74887bc77269e2fd6111f28201ef650f472cdeeb GIT binary patch literal 86833 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 z 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±w?X?p7^INTE->C-bgKQsOQJ)Y3X$F4__uj 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 Date: Wed, 17 Nov 2021 11:01:58 +0100 Subject: [PATCH 46/49] chore: bump to 1.22.0 --- CHANGELOG.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41065b6c722..adc3a14aad1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,62 @@ This project follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) fo ## [Unreleased] +## 1.22.0 - 2021-11-17 + +Tenet update! We've updated **Idiomatic** tenet to **Progressive** to reflect the new Router feature in Event Handler, and more importantly the new wave of customers coming from SRE, Data Analysis, and Data Science background. + +* BEFORE: **Idiomatic**. Utilities follow programming language idioms and language-specific best practices. +* AFTER: **Progressive**. Utilities are designed to be incrementally adoptable for customers at any stage of their Serverless journey. They follow language idioms and their community’s common practices. +### Bug Fixes + +* **ci:** change supported python version from 3.6.1 to 3.6.2, bump black ([#807](https://github.com/awslabs/aws-lambda-powertools-python/issues/807)) +* **ci:** skip sync master on docs hotfix +* **parser:** body and query strings can be null or omitted in ApiGatewayProxyEventModel and ApiGatewayProxyEventV2Model ([#820](https://github.com/awslabs/aws-lambda-powertools-python/issues/820)) + +### Code Refactoring + +* **apigateway:** Add BaseRouter and duplicate route check ([#757](https://github.com/awslabs/aws-lambda-powertools-python/issues/757)) + +### Documentation + +* **docs:** updated Lambda Layers definition & limitations. ([#775](https://github.com/awslabs/aws-lambda-powertools-python/issues/775)) +* **docs:** Idiomatic tenet updated to Progressive +* **docs:** use higher contrast font to improve accessibility ([#822](https://github.com/awslabs/aws-lambda-powertools-python/issues/822)) +* **docs:** fix indentation of SAM snippets in install section ([#778](https://github.com/awslabs/aws-lambda-powertools-python/issues/778)) +* **docs:** improve public lambda layer wording, add clipboard buttons to improve UX ([#762](https://github.com/awslabs/aws-lambda-powertools-python/issues/762)) +* **docs:** add amplify-cli instructions for public layer ([#754](https://github.com/awslabs/aws-lambda-powertools-python/issues/754)) +* **api-gateway:** add new router feature to allow route splitting in API Gateway and ALB ([#767](https://github.com/awslabs/aws-lambda-powertools-python/issues/767)) +* **apigateway:** re-add sample layout, add considerations ([#826](https://github.com/awslabs/aws-lambda-powertools-python/issues/826)) +* **appsync:** add new router feature to allow GraphQL Resolver composition ([#821](https://github.com/awslabs/aws-lambda-powertools-python/issues/821)) +* **idempotency:** add support for DynamoDB composite keys ([#808](https://github.com/awslabs/aws-lambda-powertools-python/issues/808)) +* **tenets:** update Idiomatic tenet to Progressive ([#823](https://github.com/awslabs/aws-lambda-powertools-python/issues/823)) +* **docs:** remove Lambda Layer version tag +### Features + +* **apigateway:** add Router to allow large routing composition ([#645](https://github.com/awslabs/aws-lambda-powertools-python/issues/645)) +* **appsync:** add Router to allow large resolver composition ([#776](https://github.com/awslabs/aws-lambda-powertools-python/issues/776)) +* **data-classes:** ActiveMQ and RabbitMQ support ([#770](https://github.com/awslabs/aws-lambda-powertools-python/issues/770)) +* **logger:** add ALB correlation ID support ([#816](https://github.com/awslabs/aws-lambda-powertools-python/issues/816)) + +### Maintenance + +* **deps:** bump boto3 from 1.19.6 to 1.20.3 ([#809](https://github.com/awslabs/aws-lambda-powertools-python/issues/809)) +* **deps:** bump boto3 from 1.18.58 to 1.18.59 ([#760](https://github.com/awslabs/aws-lambda-powertools-python/issues/760)) +* **deps:** bump urllib3 from 1.26.4 to 1.26.5 ([#787](https://github.com/awslabs/aws-lambda-powertools-python/issues/787)) +* **deps:** bump boto3 from 1.18.61 to 1.19.6 ([#783](https://github.com/awslabs/aws-lambda-powertools-python/issues/783)) +* **deps:** bump boto3 from 1.18.56 to 1.18.58 ([#755](https://github.com/awslabs/aws-lambda-powertools-python/issues/755)) +* **deps:** bump boto3 from 1.18.59 to 1.18.61 ([#766](https://github.com/awslabs/aws-lambda-powertools-python/issues/766)) +* **deps:** bump boto3 from 1.20.3 to 1.20.5 ([#817](https://github.com/awslabs/aws-lambda-powertools-python/issues/817)) +* **deps-dev:** bump coverage from 6.0.1 to 6.0.2 ([#764](https://github.com/awslabs/aws-lambda-powertools-python/issues/764)) +* **deps-dev:** bump pytest-asyncio from 0.15.1 to 0.16.0 ([#782](https://github.com/awslabs/aws-lambda-powertools-python/issues/782)) +* **deps-dev:** bump flake8-eradicate from 1.1.0 to 1.2.0 ([#784](https://github.com/awslabs/aws-lambda-powertools-python/issues/784)) +* **deps-dev:** bump flake8-comprehensions from 3.6.1 to 3.7.0 ([#759](https://github.com/awslabs/aws-lambda-powertools-python/issues/759)) +* **deps-dev:** bump flake8-isort from 4.0.0 to 4.1.1 ([#785](https://github.com/awslabs/aws-lambda-powertools-python/issues/785)) +* **deps-dev:** bump coverage from 6.0 to 6.0.1 ([#751](https://github.com/awslabs/aws-lambda-powertools-python/issues/751)) +* **deps-dev:** bump mkdocs-material from 7.3.3 to 7.3.5 ([#781](https://github.com/awslabs/aws-lambda-powertools-python/issues/781)) +* **deps-dev:** bump mkdocs-material from 7.3.5 to 7.3.6 ([#791](https://github.com/awslabs/aws-lambda-powertools-python/issues/791)) +* **deps-dev:** bump mkdocs-material from 7.3.2 to 7.3.3 ([#758](https://github.com/awslabs/aws-lambda-powertools-python/issues/758)) + ## 1.21.1 - 2021-10-07 ### Regression diff --git a/pyproject.toml b/pyproject.toml index 3d3f11f1995..6cdbec13de3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aws_lambda_powertools" -version = "1.21.1" +version = "1.22.0" description = "A suite of utilities for AWS Lambda functions to ease adopting best practices such as tracing, structured logging, custom metrics, batching, idempotency, feature flags, and more." authors = ["Amazon Web Services"] include = ["aws_lambda_powertools/py.typed", "THIRD-PARTY-LICENSES"] From 02561887409db062afd064101c3b5e8e34784046 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Nov 2021 10:04:41 +0000 Subject: [PATCH 47/49] chore(deps-dev): bump coverage from 6.0.2 to 6.1.2 (#810) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps [coverage](https://github.com/nedbat/coveragepy) from 6.0.2 to 6.1.2.
Commits
  • 2078c2f docs: sample HTML report for 6.1.2
  • e403155 docs: update the man page with --quiet
  • 27426bf build: prep for 6.1.2
  • 9765493 fix: CPython 3.11 support. #1241
  • dfa9774 style: make these macros more bullet-proof
  • f3a70c9 fix: warn about more source file problems
  • 23f567f build: use this setup.py command because it shows compiler errors that might ...
  • 049844a docs: fix typo in source docs (#1275)
  • d58e136 build: one pypy wheel to rule them all
  • 2afc907 refactor(test): convert eight tests to one parametrized test
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=coverage&package-manager=pip&previous-version=6.0.2&new-version=6.1.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- poetry.lock | 84 +++++++++++++++++++++++++++++--------------------- pyproject.toml | 2 +- 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/poetry.lock b/poetry.lock index f9a61b2c440..29f6666c866 100644 --- a/poetry.lock +++ b/poetry.lock @@ -147,7 +147,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.0.2" +version = "6.1.2" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -1065,7 +1065,7 @@ pydantic = ["pydantic", "email-validator"] [metadata] lock-version = "1.1" python-versions = "^3.6.2" -content-hash = "30c52844bfc6deb473d20069996a09cf8ea5a9ce5c6ff6d84e844b0e041343c6" +content-hash = "bb81da6430a9c274d18c474dffde03de826069f136404f4ebc59b0b0af743564" [metadata.files] atomicwrites = [ @@ -1113,39 +1113,53 @@ colorama = [ {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] coverage = [ - {file = "coverage-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1549e1d08ce38259de2bc3e9a0d5f3642ff4a8f500ffc1b2df73fd621a6cdfc0"}, - {file = "coverage-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcae10fccb27ca2a5f456bf64d84110a5a74144be3136a5e598f9d9fb48c0caa"}, - {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:53a294dc53cfb39c74758edaa6305193fb4258a30b1f6af24b360a6c8bd0ffa7"}, - {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8251b37be1f2cd9c0e5ccd9ae0380909c24d2a5ed2162a41fcdbafaf59a85ebd"}, - {file = "coverage-6.0.2-cp310-cp310-win32.whl", hash = "sha256:db42baa892cba723326284490283a68d4de516bfb5aaba369b4e3b2787a778b7"}, - {file = "coverage-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:bbffde2a68398682623d9dd8c0ca3f46fda074709b26fcf08ae7a4c431a6ab2d"}, - {file = "coverage-6.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:60e51a3dd55540bec686d7fff61b05048ca31e804c1f32cbb44533e6372d9cc3"}, - {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a6a9409223a27d5ef3cca57dd7cd4dfcb64aadf2fad5c3b787830ac9223e01a"}, - {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4b34ae4f51bbfa5f96b758b55a163d502be3dcb24f505d0227858c2b3f94f5b9"}, - {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3bbda1b550e70fa6ac40533d3f23acd4f4e9cb4e6e77251ce77fdf41b3309fb2"}, - {file = "coverage-6.0.2-cp36-cp36m-win32.whl", hash = "sha256:4e28d2a195c533b58fc94a12826f4431726d8eb029ac21d874345f943530c122"}, - {file = "coverage-6.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a82d79586a0a4f5fd1cf153e647464ced402938fbccb3ffc358c7babd4da1dd9"}, - {file = "coverage-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3be1206dc09fb6298de3fce70593e27436862331a85daee36270b6d0e1c251c4"}, - {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cd3828bbe1a40070c11fe16a51df733fd2f0cb0d745fb83b7b5c1f05967df7"}, - {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d036dc1ed8e1388e995833c62325df3f996675779541f682677efc6af71e96cc"}, - {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04560539c19ec26995ecfb3d9307ff154fbb9a172cb57e3b3cfc4ced673103d1"}, - {file = "coverage-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:e4fb7ced4d9dec77d6cf533acfbf8e1415fe799430366affb18d69ee8a3c6330"}, - {file = "coverage-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:77b1da5767ed2f44611bc9bc019bc93c03fa495728ec389759b6e9e5039ac6b1"}, - {file = "coverage-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61b598cbdbaae22d9e34e3f675997194342f866bb1d781da5d0be54783dce1ff"}, - {file = "coverage-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36e9040a43d2017f2787b28d365a4bb33fcd792c7ff46a047a04094dc0e2a30d"}, - {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9f1627e162e3864a596486774876415a7410021f4b67fd2d9efdf93ade681afc"}, - {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7a0b42db2a47ecb488cde14e0f6c7679a2c5a9f44814393b162ff6397fcdfbb"}, - {file = "coverage-6.0.2-cp38-cp38-win32.whl", hash = "sha256:a1b73c7c4d2a42b9d37dd43199c5711d91424ff3c6c22681bc132db4a4afec6f"}, - {file = "coverage-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:1db67c497688fd4ba85b373b37cc52c50d437fd7267520ecd77bddbd89ea22c9"}, - {file = "coverage-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f2f184bf38e74f152eed7f87e345b51f3ab0b703842f447c22efe35e59942c24"}, - {file = "coverage-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1cf1deb3d5544bd942356364a2fdc8959bad2b6cf6eb17f47d301ea34ae822"}, - {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad9b8c1206ae41d46ec7380b78ba735ebb77758a650643e841dd3894966c31d0"}, - {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:381d773d896cc7f8ba4ff3b92dee4ed740fb88dfe33b6e42efc5e8ab6dfa1cfe"}, - {file = "coverage-6.0.2-cp39-cp39-win32.whl", hash = "sha256:424c44f65e8be58b54e2b0bd1515e434b940679624b1b72726147cfc6a9fc7ce"}, - {file = "coverage-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:abbff240f77347d17306d3201e14431519bf64495648ca5a49571f988f88dee9"}, - {file = "coverage-6.0.2-pp36-none-any.whl", hash = "sha256:7092eab374346121805fb637572483270324407bf150c30a3b161fc0c4ca5164"}, - {file = "coverage-6.0.2-pp37-none-any.whl", hash = "sha256:30922626ce6f7a5a30bdba984ad21021529d3d05a68b4f71ea3b16bda35b8895"}, - {file = "coverage-6.0.2.tar.gz", hash = "sha256:6807947a09510dc31fa86f43595bf3a14017cd60bf633cc746d52141bfa6b149"}, + {file = "coverage-6.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:675adb3b3380967806b3cbb9c5b00ceb29b1c472692100a338730c1d3e59c8b9"}, + {file = "coverage-6.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95a58336aa111af54baa451c33266a8774780242cab3704b7698d5e514840758"}, + {file = "coverage-6.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d0a595a781f8e186580ff8e3352dd4953b1944289bec7705377c80c7e36c4d6c"}, + {file = "coverage-6.1.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d3c5f49ce6af61154060640ad3b3281dbc46e2e0ef2fe78414d7f8a324f0b649"}, + {file = "coverage-6.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:310c40bed6b626fd1f463e5a83dba19a61c4eb74e1ac0d07d454ebbdf9047e9d"}, + {file = "coverage-6.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a4d48e42e17d3de212f9af44f81ab73b9378a4b2b8413fd708d0d9023f2bbde4"}, + {file = "coverage-6.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ffa545230ca2ad921ad066bf8fd627e7be43716b6e0fcf8e32af1b8188ccb0ab"}, + {file = "coverage-6.1.2-cp310-cp310-win32.whl", hash = "sha256:cd2d11a59afa5001ff28073ceca24ae4c506da4355aba30d1e7dd2bd0d2206dc"}, + {file = "coverage-6.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:96129e41405887a53a9cc564f960d7f853cc63d178f3a182fdd302e4cab2745b"}, + {file = "coverage-6.1.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1de9c6f5039ee2b1860b7bad2c7bc3651fbeb9368e4c4d93e98a76358cdcb052"}, + {file = "coverage-6.1.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:80cb70264e9a1d04b519cdba3cd0dc42847bf8e982a4d55c769b9b0ee7cdce1e"}, + {file = "coverage-6.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:ba6125d4e55c0b8e913dad27b22722eac7abdcb1f3eab1bd090eee9105660266"}, + {file = "coverage-6.1.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8492d37acdc07a6eac6489f6c1954026f2260a85a4c2bb1e343fe3d35f5ee21a"}, + {file = "coverage-6.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66af99c7f7b64d050d37e795baadf515b4561124f25aae6e1baa482438ecc388"}, + {file = "coverage-6.1.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ebcc03e1acef4ff44f37f3c61df478d6e469a573aa688e5a162f85d7e4c3860d"}, + {file = "coverage-6.1.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98d44a8136eebbf544ad91fef5bd2b20ef0c9b459c65a833c923d9aa4546b204"}, + {file = "coverage-6.1.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:c18725f3cffe96732ef96f3de1939d81215fd6d7d64900dcc4acfe514ea4fcbf"}, + {file = "coverage-6.1.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c8e9c4bcaaaa932be581b3d8b88b677489975f845f7714efc8cce77568b6711c"}, + {file = "coverage-6.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:06d009e8a29483cbc0520665bc46035ffe9ae0e7484a49f9782c2a716e37d0a0"}, + {file = "coverage-6.1.2-cp36-cp36m-win32.whl", hash = "sha256:e5432d9c329b11c27be45ee5f62cf20a33065d482c8dec1941d6670622a6fb8f"}, + {file = "coverage-6.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:82fdcb64bf08aa5db881db061d96db102c77397a570fbc112e21c48a4d9cb31b"}, + {file = "coverage-6.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:94f558f8555e79c48c422045f252ef41eb43becdd945e9c775b45ebfc0cbd78f"}, + {file = "coverage-6.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:046647b96969fda1ae0605f61288635209dd69dcd27ba3ec0bf5148bc157f954"}, + {file = "coverage-6.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cc799916b618ec9fd00135e576424165691fec4f70d7dc12cfaef09268a2478c"}, + {file = "coverage-6.1.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62646d98cf0381ffda301a816d6ac6c35fc97aa81b09c4c52d66a15c4bef9d7c"}, + {file = "coverage-6.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:27a3df08a855522dfef8b8635f58bab81341b2fb5f447819bc252da3aa4cf44c"}, + {file = "coverage-6.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:610c0ba11da8de3a753dc4b1f71894f9f9debfdde6559599f303286e70aeb0c2"}, + {file = "coverage-6.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:35b246ae3a2c042dc8f410c94bcb9754b18179cdb81ff9477a9089dbc9ecc186"}, + {file = "coverage-6.1.2-cp37-cp37m-win32.whl", hash = "sha256:0cde7d9fe2fb55ff68ebe7fb319ef188e9b88e0a3d1c9c5db7dd829cd93d2193"}, + {file = "coverage-6.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:958ac66272ff20e63d818627216e3d7412fdf68a2d25787b89a5c6f1eb7fdd93"}, + {file = "coverage-6.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a300b39c3d5905686c75a369d2a66e68fd01472ea42e16b38c948bd02b29e5bd"}, + {file = "coverage-6.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d3855d5d26292539861f5ced2ed042fc2aa33a12f80e487053aed3bcb6ced13"}, + {file = "coverage-6.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:586d38dfc7da4a87f5816b203ff06dd7c1bb5b16211ccaa0e9788a8da2b93696"}, + {file = "coverage-6.1.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a34fccb45f7b2d890183a263578d60a392a1a218fdc12f5bce1477a6a68d4373"}, + {file = "coverage-6.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bc1ee1318f703bc6c971da700d74466e9b86e0c443eb85983fb2a1bd20447263"}, + {file = "coverage-6.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3f546f48d5d80a90a266769aa613bc0719cb3e9c2ef3529d53f463996dd15a9d"}, + {file = "coverage-6.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd92ece726055e80d4e3f01fff3b91f54b18c9c357c48fcf6119e87e2461a091"}, + {file = "coverage-6.1.2-cp38-cp38-win32.whl", hash = "sha256:24ed38ec86754c4d5a706fbd5b52b057c3df87901a8610d7e5642a08ec07087e"}, + {file = "coverage-6.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:97ef6e9119bd39d60ef7b9cd5deea2b34869c9f0b9777450a7e3759c1ab09b9b"}, + {file = "coverage-6.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e5a8c947a2a89c56655ecbb789458a3a8e3b0cbf4c04250331df8f647b3de59"}, + {file = "coverage-6.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a39590d1e6acf6a3c435c5d233f72f5d43b585f5be834cff1f21fec4afda225"}, + {file = "coverage-6.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9d2c2e3ce7b8cc932a2f918186964bd44de8c84e2f9ef72dc616f5bb8be22e71"}, + {file = "coverage-6.1.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3348865798c077c695cae00da0924136bb5cc501f236cfd6b6d9f7a3c94e0ec4"}, + {file = "coverage-6.1.2-cp39-cp39-win32.whl", hash = "sha256:fae3fe111670e51f1ebbc475823899524e3459ea2db2cb88279bbfb2a0b8a3de"}, + {file = "coverage-6.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:af45eea024c0e3a25462fade161afab4f0d9d9e0d5a5d53e86149f74f0a35ecc"}, + {file = "coverage-6.1.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:eab14fdd410500dae50fd14ccc332e65543e7b39f6fc076fe90603a0e5d2f929"}, + {file = "coverage-6.1.2.tar.gz", hash = "sha256:d9a635114b88c0ab462e0355472d00a180a5fbfd8511e7f18e4ac32652e7d972"}, ] dataclasses = [ {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, diff --git a/pyproject.toml b/pyproject.toml index 6cdbec13de3..79b89fca410 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ pydantic = {version = "^1.8.2", optional = true } email-validator = {version = "*", optional = true } [tool.poetry.dev-dependencies] -coverage = {extras = ["toml"], version = "^6.0"} +coverage = {extras = ["toml"], version = "^6.1"} pytest = "^6.2.5" black = "^21.10.b0" flake8 = "^3.9.0" From 8be9af1eddcfce7b1badc4ec65b94f057e32255d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Nov 2021 10:09:52 +0000 Subject: [PATCH 48/49] chore(deps-dev): bump isort from 5.9.3 to 5.10.1 (#811) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps [isort](https://github.com/pycqa/isort) from 5.9.3 to 5.10.1.
Release notes

Sourced from isort's releases.

5.10.1 November 8 2021

  • Fixed #1819: Occasional inconsistency with multiple src paths.
  • Fixed #1840: skip_file ignored when on the first docstring line

5.10.0

Implemented [#1796](https://github.com/pycqa/isort/issues/1796): Switch to tomli for pyproject.toml configuration loader.
Fixed [#1801](https://github.com/pycqa/isort/issues/1801): CLI bug (--exend-skip-glob, overrides instead of extending).
Fixed [#1802](https://github.com/pycqa/isort/issues/1802): respect PATH customization in nested calls to git.
Fixed [#1838](https://github.com/pycqa/isort/issues/1838): Append only with certain code snippets incorrectly adds imports.
Added official support for Python 3.10
Changelog

Sourced from isort's changelog.

5.10.1 November 8 2021

  • Fixed #1819: Occasional inconsistency with multiple src paths.
  • Fixed #1840: skip_file ignored when on the first docstring line

5.10.0 November 3 2021

  • Implemented #1796: Switch to tomli for pyproject.toml configuration loader.
  • Fixed #1801: CLI bug (--exend-skip-glob, overrides instead of extending).
  • Fixed #1802: respect PATH customization in nested calls to git.
  • Fixed #1838: Append only with certain code snippets incorrectly adds imports.
  • Added official support for Python 3.10

Potentially breaking changes:

  • Fixed #1785: _ast module incorrectly excluded from stdlib definition.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=isort&package-manager=pip&previous-version=5.9.3&new-version=5.10.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 29f6666c866..fea9831cd5f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -418,7 +418,7 @@ python-versions = "*" [[package]] name = "isort" -version = "5.9.3" +version = "5.10.1" description = "A Python utility / library to sort Python imports." category = "dev" optional = false @@ -1065,7 +1065,7 @@ pydantic = ["pydantic", "email-validator"] [metadata] lock-version = "1.1" python-versions = "^3.6.2" -content-hash = "bb81da6430a9c274d18c474dffde03de826069f136404f4ebc59b0b0af743564" +content-hash = "2873198da6ba0fc9487a838f4bb5e3f7c7d35fa31cf7a6a412733927cfed5c5f" [metadata.files] atomicwrites = [ @@ -1247,8 +1247,8 @@ iniconfig = [ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ - {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, - {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] jinja2 = [ {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, diff --git a/pyproject.toml b/pyproject.toml index 79b89fca410..cea8a4abbdc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ flake8-debugger = "^4.0.0" flake8-fixme = "^1.1.1" flake8-isort = "^4.1.1" flake8-variables-names = "^0.0.4" -isort = "^5.9.3" +isort = "^5.10.1" pytest-cov = "^3.0.0" pytest-mock = "^3.5.1" pdoc3 = "^0.10.0" From f198ca8462680f8e294d8b0069db6115a69d2d44 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 17 Nov 2021 11:20:30 +0100 Subject: [PATCH 49/49] fix(ci): comment custom publish version checker --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f49fcc5ceff..5a7c1a3110c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -66,7 +66,7 @@ jobs: run: | RELEASE_TAG_VERSION=${{ github.event.release.tag_name }} # Replace publishing version if the workflow was triggered manually - test -n ${RELEASE_TAG_VERSION} && RELEASE_TAG_VERSION=${{ github.event.inputs.publish_version }} + # test -n ${RELEASE_TAG_VERSION} && RELEASE_TAG_VERSION=${{ github.event.inputs.publish_version }} echo "RELEASE_TAG_VERSION=${RELEASE_TAG_VERSION:1}" >> $GITHUB_ENV - name: Ensure new version is also set in pyproject and CHANGELOG if: ${{ github.event.inputs.publish_docs_only == false }}
Changelog

Sourced from coverage's changelog.

Version 6.1.2 — 2021-11-10

  • Python 3.11 is supported (tested with 3.11.0a2). One still-open issue has to do with exits through with-statements <issue 1270_>_.

  • Fix: When remapping file paths through the [paths] setting while combining, the [run] relative_files setting was ignored, resulting in absolute paths for remapped file names (issue 1147_). This is now fixed.

  • Fix: Complex conditionals over excluded lines could have incorrectly reported a missing branch (issue 1271_). This is now fixed.

  • Fix: More exceptions are now handled when trying to parse source files for reporting. Problems that used to terminate coverage.py can now be handled with [report] ignore_errors. This helps with plugins failing to read files (django_coverage_plugin issue 78_).

  • Fix: Removed another vestige of jQuery from the source tarball (issue 840_).

  • Fix: Added a default value for a new-to-6.x argument of an internal class. This unsupported class is being used by coveralls (issue 1273_). Although I'd rather not "fix" unsupported interfaces, it's actually nicer with a default value.

.. _django_coverage_plugin issue 78: nedbat/django_coverage_plugin#78 .. _issue 1147: nedbat/coveragepy#1147 .. _issue 1270: nedbat/coveragepy#1270 .. _issue 1271: nedbat/coveragepy#1271 .. _issue 1273: nedbat/coveragepy#1273

.. _changes_611:

Version 6.1.1 — 2021-10-31

  • Fix: The sticky header on the HTML report didn't work unless you had branch coverage enabled. This is now fixed: the sticky header works for everyone. (Do people still use coverage without branch measurement!? j/k)

  • Fix: When using explicitly declared namespace packages, the "already imported a file that will be measured" warning would be issued (issue 888_). This is now fixed.

.. _issue 888: nedbat/coveragepy#888

.. _changes_61:

... (truncated)