diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml
new file mode 100644
index 00000000000..e96446a551a
--- /dev/null
+++ b/.github/actionlint.yaml
@@ -0,0 +1,5 @@
+self-hosted-runner:
+ labels:
+ - aws-lambda-powertools_ubuntu-latest_4-core
+ - aws-lambda-powertools_ubuntu-latest_8-core
+ - aws-lambda-powertools_ubuntu-latest_16-core
diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml
index 829d005734d..8402ff66d2e 100644
--- a/.github/workflows/auto-merge.yml
+++ b/.github/workflows/auto-merge.yml
@@ -14,7 +14,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
- uses: dependabot/fetch-metadata@v1.3.4
+ uses: dependabot/fetch-metadata@v1.3.5
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Enable auto-merge for mypy-boto3 stubs Dependabot PRs
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index f8a7849e7ea..d70a5c024e7 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -2,7 +2,10 @@ name: "CodeQL"
on:
push:
- branches: [develop, v2]
+ paths:
+ - "aws_lambda_powertools/**"
+ branches:
+ - develop
jobs:
analyze:
diff --git a/.github/workflows/publish_v2_layer.yml b/.github/workflows/publish_v2_layer.yml
index 6d91d908ac0..5e5739ef34b 100644
--- a/.github/workflows/publish_v2_layer.yml
+++ b/.github/workflows/publish_v2_layer.yml
@@ -32,7 +32,7 @@ jobs:
build-layer:
permissions:
contents: read
- runs-on: ubuntu-latest
+ runs-on: aws-lambda-powertools_ubuntu-latest_4-core
defaults:
run:
working-directory: ./layer
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0d58dd6c05b..77050bc25fd 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -21,6 +21,7 @@ name: Release
env:
BRANCH: develop
+ ORIGIN: awslabs/aws-lambda-powertools-python
on:
workflow_dispatch:
@@ -48,9 +49,9 @@ on:
jobs:
release:
environment: release
- runs-on: ubuntu-latest
+ runs-on: aws-lambda-powertools_ubuntu-latest_4-core
permissions:
- contents: read
+ contents: write
outputs:
RELEASE_VERSION: ${{ steps.release_version.outputs.RELEASE_VERSION }}
env:
@@ -78,25 +79,40 @@ jobs:
- name: Run all tests, linting and baselines
if: ${{ !inputs.skip_code_quality }}
run: make pr
+ - name: Git client setup and refresh tip
+ run: |
+ git config user.name "Release bot"
+ git config user.email "aws-devax-open-source@amazon.com"
+ git config pull.rebase true
+ git config remote.origin.url >&- || git remote add origin https://github.com/"${ORIGIN}" # Git Detached mode (release notes) doesn't have origin
+ git pull origin "${BRANCH}"
- name: Bump package version
+ id: versioning
run: poetry version "${RELEASE_VERSION}"
- name: Build python package and wheel
if: ${{ !inputs.skip_pypi }}
run: poetry build
- # NOTE: TestPyPi is undergoing a CDN migration https://status.python.org/#
- # re-enable for the next release, when degraded status changes
- # - name: Upload to PyPi test
- # if: ${{ !inputs.skip_pypi }}
- # run: make release-test
- # env:
- # PYPI_USERNAME: __token__
- # PYPI_TEST_TOKEN: ${{ secrets.PYPI_TEST_TOKEN }}
+ - name: Upload to PyPi test
+ if: ${{ !inputs.skip_pypi }}
+ run: make release-test
+ env:
+ PYPI_USERNAME: __token__
+ PYPI_TEST_TOKEN: ${{ secrets.PYPI_TEST_TOKEN }}
- name: Upload to PyPi prod
if: ${{ !inputs.skip_pypi }}
run: make release-prod
env:
PYPI_USERNAME: __token__
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
+ - name: Update version in trunk
+ if: steps.versioning.outcome == 'success'
+ run: |
+ HAS_CHANGE=$(git status --porcelain)
+ test -z "${HAS_CHANGE}" && echo "Nothing to update" && exit 0
+ git add pyproject.toml
+ git commit -m "bump version to ${RELEASE_VERSION}" --no-verify
+ git pull origin "${BRANCH}" # prevents concurrent branch update failing push
+ git push origin HEAD:refs/heads/"${BRANCH}"
changelog:
needs: release
diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml
index e60aaf391ec..06196b97f92 100644
--- a/.github/workflows/run-e2e-tests.yml
+++ b/.github/workflows/run-e2e-tests.yml
@@ -21,13 +21,14 @@ env:
jobs:
run:
- runs-on: ubuntu-latest
+ runs-on: aws-lambda-powertools_ubuntu-latest_8-core
permissions:
id-token: write # needed to request JWT with GitHub's OIDC Token endpoint. docs: https://bit.ly/3MNgQO9
contents: read
strategy:
matrix:
version: ["3.7", "3.8", "3.9"]
+ if: ${{ github.actor != 'dependabot[bot]' }}
steps:
- name: "Checkout"
uses: actions/checkout@v3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 106d0ada40c..0d7780c2b45 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,68 @@
# Unreleased
+## Bug Fixes
+
+* **apigateway:** support nested router decorators ([#1709](https://github.com/awslabs/aws-lambda-powertools-python/issues/1709))
+* **ci:** increase permission to allow version sync back to repo
+* **ci:** disable pre-commit hook download from version bump
+* **ci:** setup git client earlier to prevent dirty stash error
+* **parameters:** get_secret correctly return SecretBinary value ([#1717](https://github.com/awslabs/aws-lambda-powertools-python/issues/1717))
+
+## Documentation
+
+* project name consistency
+* **apigateway:** add all resolvers in testing your code section for accuracy ([#1688](https://github.com/awslabs/aws-lambda-powertools-python/issues/1688))
+* **examples:** linting unnecessary whitespace
+* **homepage:** update default value for `POWERTOOLS_DEV` ([#1695](https://github.com/awslabs/aws-lambda-powertools-python/issues/1695))
+* **idempotency:** add missing Lambda Context; note on thread-safe ([#1732](https://github.com/awslabs/aws-lambda-powertools-python/issues/1732))
+* **logger:** update uncaught exception message value
+
+## Features
+
+* **apigateway:** multiple exceptions in exception_handler ([#1707](https://github.com/awslabs/aws-lambda-powertools-python/issues/1707))
+* **event_sources:** extract CloudWatch Logs in Kinesis streams ([#1710](https://github.com/awslabs/aws-lambda-powertools-python/issues/1710))
+* **logger:** log uncaught exceptions via system's exception hook ([#1727](https://github.com/awslabs/aws-lambda-powertools-python/issues/1727))
+* **parser:** export Pydantic.errors through escape hatch ([#1728](https://github.com/awslabs/aws-lambda-powertools-python/issues/1728))
+* **parser:** extract CloudWatch Logs in Kinesis streams ([#1726](https://github.com/awslabs/aws-lambda-powertools-python/issues/1726))
+
+## Maintenance
+
+* apigw test event wrongly set with base64
+* **ci:** bump hardware for build steps
+* **ci:** try bigger hardware for e2e test
+* **ci:** uncomment test pypi, fix version bump sync
+* **ci:** limit to src only to prevent dependabot failures
+* **ci:** use new custom hw for E2E
+* **ci:** prevent dependabot updates to trigger E2E
+* **ci:** revert custom hw for E2E due to lack of hw
+* **deps:** bump dependabot/fetch-metadata from 1.3.4 to 1.3.5 ([#1689](https://github.com/awslabs/aws-lambda-powertools-python/issues/1689))
+* **deps-dev:** bump mypy-boto3-ssm from 1.25.0 to 1.26.0.post1 ([#1690](https://github.com/awslabs/aws-lambda-powertools-python/issues/1690))
+* **deps-dev:** bump mypy-boto3-s3 from 1.25.0 to 1.26.0.post1 ([#1716](https://github.com/awslabs/aws-lambda-powertools-python/issues/1716))
+* **deps-dev:** bump mypy-boto3-appconfigdata from 1.25.0 to 1.26.0.post1 ([#1704](https://github.com/awslabs/aws-lambda-powertools-python/issues/1704))
+* **deps-dev:** bump mypy-boto3-xray from 1.25.0 to 1.26.0.post1 ([#1703](https://github.com/awslabs/aws-lambda-powertools-python/issues/1703))
+* **deps-dev:** bump mypy-boto3-cloudwatch from 1.25.0 to 1.26.0.post1 ([#1714](https://github.com/awslabs/aws-lambda-powertools-python/issues/1714))
+* **deps-dev:** bump flake8-bugbear from 22.10.25 to 22.10.27 ([#1665](https://github.com/awslabs/aws-lambda-powertools-python/issues/1665))
+* **deps-dev:** bump mypy-boto3-lambda from 1.25.0 to 1.26.0.post1 ([#1705](https://github.com/awslabs/aws-lambda-powertools-python/issues/1705))
+* **deps-dev:** bump types-requests from 2.28.11.3 to 2.28.11.4 ([#1701](https://github.com/awslabs/aws-lambda-powertools-python/issues/1701))
+* **deps-dev:** bump mypy-boto3-logs from 1.25.0 to 1.26.3 ([#1702](https://github.com/awslabs/aws-lambda-powertools-python/issues/1702))
+* **deps-dev:** bump mypy-boto3-xray from 1.26.0.post1 to 1.26.9 ([#1720](https://github.com/awslabs/aws-lambda-powertools-python/issues/1720))
+* **deps-dev:** bump mypy-boto3-ssm from 1.26.0.post1 to 1.26.4 ([#1721](https://github.com/awslabs/aws-lambda-powertools-python/issues/1721))
+* **deps-dev:** bump mypy-boto3-appconfig from 1.25.0 to 1.26.0.post1 ([#1722](https://github.com/awslabs/aws-lambda-powertools-python/issues/1722))
+* **deps-dev:** bump pytest-asyncio from 0.20.1 to 0.20.2 ([#1723](https://github.com/awslabs/aws-lambda-powertools-python/issues/1723))
+* **deps-dev:** bump pytest-xdist from 2.5.0 to 3.0.2 ([#1655](https://github.com/awslabs/aws-lambda-powertools-python/issues/1655))
+* **deps-dev:** bump mkdocs-material from 8.5.7 to 8.5.9 ([#1697](https://github.com/awslabs/aws-lambda-powertools-python/issues/1697))
+* **deps-dev:** bump flake8-comprehensions from 3.10.0 to 3.10.1 ([#1699](https://github.com/awslabs/aws-lambda-powertools-python/issues/1699))
+* **deps-dev:** bump types-requests from 2.28.11.2 to 2.28.11.3 ([#1698](https://github.com/awslabs/aws-lambda-powertools-python/issues/1698))
+* **deps-dev:** bump pytest-benchmark from 3.4.1 to 4.0.0 ([#1659](https://github.com/awslabs/aws-lambda-powertools-python/issues/1659))
+* **deps-dev:** bump mypy-boto3-secretsmanager from 1.25.0 to 1.26.0.post1 ([#1691](https://github.com/awslabs/aws-lambda-powertools-python/issues/1691))
+* **deps-dev:** bump flake8-builtins from 2.0.0 to 2.0.1 ([#1715](https://github.com/awslabs/aws-lambda-powertools-python/issues/1715))
+* **logger:** overload inject_lambda_context with generics ([#1583](https://github.com/awslabs/aws-lambda-powertools-python/issues/1583))
+* **logger:** uncaught exception to use exception value as message
+
+
+
+## [v2.2.0] - 2022-11-07
## Documentation
* **homepage:** remove v1 layer limitation on pydantic not being included
@@ -16,6 +78,8 @@
## Maintenance
+* update v2 layer ARN on documentation
+* **deps:** bump package to 2.2.0
* **deps-dev:** bump aws-cdk-lib from 2.49.0 to 2.50.0 ([#1683](https://github.com/awslabs/aws-lambda-powertools-python/issues/1683))
* **deps-dev:** bump mypy-boto3-dynamodb from 1.25.0 to 1.26.0.post1 ([#1682](https://github.com/awslabs/aws-lambda-powertools-python/issues/1682))
* **deps-dev:** bump mypy-boto3-cloudformation from 1.25.0 to 1.26.0.post1 ([#1679](https://github.com/awslabs/aws-lambda-powertools-python/issues/1679))
@@ -2552,7 +2616,8 @@
* Merge pull request [#5](https://github.com/awslabs/aws-lambda-powertools-python/issues/5) from jfuss/feat/python38
-[Unreleased]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v2.1.0...HEAD
+[Unreleased]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v2.2.0...HEAD
+[v2.2.0]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v2.1.0...v2.2.0
[v2.1.0]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v2.0.0...v2.1.0
[v2.0.0]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v1.31.1...v2.0.0
[v1.31.1]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v1.31.0...v1.31.1
diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py
index 112bcd92dfe..5b91a1583d4 100644
--- a/aws_lambda_powertools/event_handler/api_gateway.py
+++ b/aws_lambda_powertools/event_handler/api_gateway.py
@@ -689,9 +689,14 @@ def not_found(self, func: Optional[Callable] = None):
return self.exception_handler(NotFoundError)
return self.exception_handler(NotFoundError)(func)
- def exception_handler(self, exc_class: Type[Exception]):
+ def exception_handler(self, exc_class: Union[Type[Exception], List[Type[Exception]]]):
def register_exception_handler(func: Callable):
- self._exception_handlers[exc_class] = func
+ if isinstance(exc_class, list):
+ for exp in exc_class:
+ self._exception_handlers[exp] = func
+ else:
+ self._exception_handlers[exc_class] = func
+ return func
return register_exception_handler
@@ -793,6 +798,7 @@ def register_route(func: Callable):
# Convert methods to tuple. It needs to be hashable as its part of the self._routes dict key
methods = (method,) if isinstance(method, str) else tuple(method)
self._routes[(rule, methods, cors, compress, cache_control)] = func
+ return func
return register_route
diff --git a/aws_lambda_powertools/logging/logger.py b/aws_lambda_powertools/logging/logger.py
index b82c510036a..bafde28e65c 100644
--- a/aws_lambda_powertools/logging/logger.py
+++ b/aws_lambda_powertools/logging/logger.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import functools
import inspect
import io
@@ -18,12 +20,14 @@
Optional,
TypeVar,
Union,
+ overload,
)
import jmespath
from ..shared import constants
from ..shared.functions import resolve_env_var_choice, resolve_truthy_env_var_choice
+from ..shared.types import AnyCallableT
from .exceptions import InvalidLoggerSamplingRateError
from .filters import SuppressFilter
from .formatter import (
@@ -94,6 +98,11 @@ class Logger(logging.Logger): # lgtm [py/missing-call-to-init]
custom logging formatter that implements PowertoolsFormatter
logger_handler: logging.Handler, optional
custom logging handler e.g. logging.FileHandler("file.log")
+ log_uncaught_exceptions: bool, by default False
+ logs uncaught exception using sys.excepthook
+
+ See: https://docs.python.org/3/library/sys.html#sys.excepthook
+
Parameters propagated to LambdaPowertoolsFormatter
--------------------------------------------------
@@ -201,6 +210,7 @@ def __init__(
stream: Optional[IO[str]] = None,
logger_formatter: Optional[PowertoolsFormatter] = None,
logger_handler: Optional[logging.Handler] = None,
+ log_uncaught_exceptions: bool = False,
json_serializer: Optional[Callable[[Dict], str]] = None,
json_deserializer: Optional[Callable[[Union[Dict, str, bool, int, float]], str]] = None,
json_default: Optional[Callable[[Any], Any]] = None,
@@ -220,6 +230,8 @@ def __init__(
self.child = child
self.logger_formatter = logger_formatter
self.logger_handler = logger_handler or logging.StreamHandler(stream)
+ self.log_uncaught_exceptions = log_uncaught_exceptions
+
self.log_level = self._get_log_level(level)
self._is_deduplication_disabled = resolve_truthy_env_var_choice(
env=os.getenv(constants.LOGGER_LOG_DEDUPLICATION_ENV, "false")
@@ -242,6 +254,10 @@ def __init__(
self._init_logger(formatter_options=formatter_options, **kwargs)
+ if self.log_uncaught_exceptions:
+ logger.debug("Replacing exception hook")
+ sys.excepthook = functools.partial(log_uncaught_exception_hook, logger=self)
+
# Prevent __getattr__ from shielding unknown attribute errors in type checkers
# https://github.com/awslabs/aws-lambda-powertools-python/issues/1660
if not TYPE_CHECKING:
@@ -314,13 +330,33 @@ def _configure_sampling(self):
f"Please review POWERTOOLS_LOGGER_SAMPLE_RATE environment variable."
)
+ @overload
def inject_lambda_context(
self,
- lambda_handler: Optional[Callable[[Dict, Any], Any]] = None,
+ lambda_handler: AnyCallableT,
log_event: Optional[bool] = None,
correlation_id_path: Optional[str] = None,
clear_state: Optional[bool] = False,
- ):
+ ) -> AnyCallableT:
+ ...
+
+ @overload
+ def inject_lambda_context(
+ self,
+ lambda_handler: None = None,
+ log_event: Optional[bool] = None,
+ correlation_id_path: Optional[str] = None,
+ clear_state: Optional[bool] = False,
+ ) -> Callable[[AnyCallableT], AnyCallableT]:
+ ...
+
+ def inject_lambda_context(
+ self,
+ lambda_handler: Optional[AnyCallableT] = None,
+ log_event: Optional[bool] = None,
+ correlation_id_path: Optional[str] = None,
+ clear_state: Optional[bool] = False,
+ ) -> Any:
"""Decorator to capture Lambda contextual info and inject into logger
Parameters
@@ -713,3 +749,8 @@ def _is_internal_frame(frame): # pragma: no cover
"""Signal whether the frame is a CPython or logging module internal."""
filename = os.path.normcase(frame.f_code.co_filename)
return filename == logging._srcfile or ("importlib" in filename and "_bootstrap" in filename)
+
+
+def log_uncaught_exception_hook(exc_type, exc_value, exc_traceback, logger: Logger):
+ """Callback function for sys.excepthook to use Logger to log uncaught exceptions"""
+ logger.exception(exc_value, exc_info=(exc_type, exc_value, exc_traceback)) # pragma: no cover
diff --git a/aws_lambda_powertools/utilities/data_classes/kinesis_stream_event.py b/aws_lambda_powertools/utilities/data_classes/kinesis_stream_event.py
index ec45bfbd0b2..06eaedb8904 100644
--- a/aws_lambda_powertools/utilities/data_classes/kinesis_stream_event.py
+++ b/aws_lambda_powertools/utilities/data_classes/kinesis_stream_event.py
@@ -1,7 +1,11 @@
import base64
import json
-from typing import Iterator
+import zlib
+from typing import Iterator, List
+from aws_lambda_powertools.utilities.data_classes.cloud_watch_logs_event import (
+ CloudWatchLogsDecodedData,
+)
from aws_lambda_powertools.utilities.data_classes.common import DictWrapper
@@ -43,6 +47,11 @@ def data_as_json(self) -> dict:
"""Decode binary encoded data as json"""
return json.loads(self.data_as_text())
+ def data_zlib_compressed_as_json(self) -> dict:
+ """Decode binary encoded data as bytes"""
+ decompressed = zlib.decompress(self.data_as_bytes(), zlib.MAX_WBITS | 32)
+ return json.loads(decompressed)
+
class KinesisStreamRecord(DictWrapper):
@property
@@ -98,3 +107,11 @@ class KinesisStreamEvent(DictWrapper):
def records(self) -> Iterator[KinesisStreamRecord]:
for record in self["Records"]:
yield KinesisStreamRecord(record)
+
+
+def extract_cloudwatch_logs_from_event(event: KinesisStreamEvent) -> List[CloudWatchLogsDecodedData]:
+ return [CloudWatchLogsDecodedData(record.kinesis.data_zlib_compressed_as_json()) for record in event.records]
+
+
+def extract_cloudwatch_logs_from_record(record: KinesisStreamRecord) -> CloudWatchLogsDecodedData:
+ return CloudWatchLogsDecodedData(data=record.kinesis.data_zlib_compressed_as_json())
diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py
index affdaf2e4dd..4a616fcf9f9 100644
--- a/aws_lambda_powertools/utilities/parameters/secrets.py
+++ b/aws_lambda_powertools/utilities/parameters/secrets.py
@@ -96,7 +96,12 @@ def _get(self, name: str, **sdk_options) -> str:
# Explicit arguments will take precedence over keyword arguments
sdk_options["SecretId"] = name
- return self.client.get_secret_value(**sdk_options)["SecretString"]
+ secret_value = self.client.get_secret_value(**sdk_options)
+
+ if "SecretString" in secret_value:
+ return secret_value["SecretString"]
+
+ return secret_value["SecretBinary"]
def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]:
"""
diff --git a/aws_lambda_powertools/utilities/parser/models/kinesis.py b/aws_lambda_powertools/utilities/parser/models/kinesis.py
index ffc89bcbdaa..6fb9a7076b5 100644
--- a/aws_lambda_powertools/utilities/parser/models/kinesis.py
+++ b/aws_lambda_powertools/utilities/parser/models/kinesis.py
@@ -1,8 +1,13 @@
-from typing import List, Type, Union
+import json
+import zlib
+from typing import Dict, List, Type, Union
from pydantic import BaseModel, validator
from aws_lambda_powertools.shared.functions import base64_decode
+from aws_lambda_powertools.utilities.parser.models.cloudwatch import (
+ CloudWatchLogsDecode,
+)
from aws_lambda_powertools.utilities.parser.types import Literal
@@ -28,6 +33,21 @@ class KinesisDataStreamRecord(BaseModel):
eventSourceARN: str
kinesis: KinesisDataStreamRecordPayload
+ def decompress_zlib_record_data_as_json(self) -> Dict:
+ """Decompress Kinesis Record bytes data zlib compressed to JSON"""
+ if not isinstance(self.kinesis.data, bytes):
+ raise ValueError("We can only decompress bytes data, not custom models.")
+
+ return json.loads(zlib.decompress(self.kinesis.data, zlib.MAX_WBITS | 32))
+
class KinesisDataStreamModel(BaseModel):
Records: List[KinesisDataStreamRecord]
+
+
+def extract_cloudwatch_logs_from_event(event: KinesisDataStreamModel) -> List[CloudWatchLogsDecode]:
+ return [CloudWatchLogsDecode(**record.decompress_zlib_record_data_as_json()) for record in event.Records]
+
+
+def extract_cloudwatch_logs_from_record(record: KinesisDataStreamRecord) -> CloudWatchLogsDecode:
+ return CloudWatchLogsDecode(**record.decompress_zlib_record_data_as_json())
diff --git a/aws_lambda_powertools/utilities/parser/pydantic.py b/aws_lambda_powertools/utilities/parser/pydantic.py
index d2551928979..3d8eb2da4e1 100644
--- a/aws_lambda_powertools/utilities/parser/pydantic.py
+++ b/aws_lambda_powertools/utilities/parser/pydantic.py
@@ -6,3 +6,4 @@
# to use `from aws_lambda_powertools.utilities.parser.pydantic import `
from pydantic import * # noqa: F403,F401
+from pydantic.errors import * # noqa: F403,F401
diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md
index ec6116403e3..ca092e30c04 100644
--- a/docs/core/event_handler/api_gateway.md
+++ b/docs/core/event_handler/api_gateway.md
@@ -226,6 +226,9 @@ You can use **`exception_handler`** decorator with any Python exception. This al
--8<-- "examples/event_handler_rest/src/exception_handling.py"
```
+???+ info
+ The `exception_handler` also supports passing a list of exception types you wish to handle with one handler.
+
### Raising HTTP errors
You can easily raise any HTTP Error back to the client using `ServiceError` exception. This ensures your Lambda function doesn't fail but return the correct HTTP response signalling the error.
@@ -562,19 +565,63 @@ your development, building, deployment tooling need to accommodate the distinct
## Testing your code
-You can test your routes by passing a proxy event request where `path` and `httpMethod`.
+You can test your routes by passing a proxy event request with required params.
-=== "assert_http_response.py"
+=== "API Gateway REST API"
- ```python hl_lines="21-24"
- --8<-- "examples/event_handler_rest/src/assert_http_response.py"
- ```
+ === "assert_rest_api_resolver_response.py"
-=== "assert_http_response_module.py"
+ ```python hl_lines="21-24"
+ --8<-- "examples/event_handler_rest/src/assert_rest_api_resolver_response.py"
+ ```
- ```python
- --8<-- "examples/event_handler_rest/src/assert_http_response_module.py"
- ```
+ === "assert_rest_api_response_module.py"
+
+ ```python
+ --8<-- "examples/event_handler_rest/src/assert_rest_api_response_module.py"
+ ```
+
+=== "API Gateway HTTP API"
+
+ === "assert_http_api_resolver_response.py"
+
+ ```python hl_lines="21-29"
+ --8<-- "examples/event_handler_rest/src/assert_http_api_resolver_response.py"
+ ```
+
+ === "assert_http_api_response_module.py"
+
+ ```python
+ --8<-- "examples/event_handler_rest/src/assert_http_api_response_module.py"
+ ```
+
+=== "Application Load Balancer"
+
+ === "assert_alb_api_resolver_response.py"
+
+ ```python hl_lines="21-24"
+ --8<-- "examples/event_handler_rest/src/assert_alb_api_resolver_response.py"
+ ```
+
+ === "assert_alb_api_response_module.py"
+
+ ```python
+ --8<-- "examples/event_handler_rest/src/assert_alb_api_response_module.py"
+ ```
+
+=== "Lambda Function URL"
+
+ === "assert_function_url_api_resolver_response.py"
+
+ ```python hl_lines="21-29"
+ --8<-- "examples/event_handler_rest/src/assert_function_url_api_resolver_response.py"
+ ```
+
+ === "assert_function_url_api_response_module.py"
+
+ ```python
+ --8<-- "examples/event_handler_rest/src/assert_function_url_api_response_module.py"
+ ```
## FAQ
diff --git a/docs/core/logger.md b/docs/core/logger.md
index 471186cba5b..0fa112e564b 100644
--- a/docs/core/logger.md
+++ b/docs/core/logger.md
@@ -291,6 +291,30 @@ Use `logger.exception` method to log contextual information about exceptions. Lo
--8<-- "examples/logger/src/logging_exceptions_output.json"
```
+#### Uncaught exceptions
+
+Logger can optionally log uncaught exceptions by setting `log_uncaught_exceptions=True` at initialization.
+
+!!! info "Logger will replace any exception hook previously registered via [sys.excepthook](https://docs.python.org/3/library/sys.html#sys.excepthook){target='_blank'}."
+
+??? question "What are uncaught exceptions?"
+
+ It's any raised exception that wasn't handled by the [`except` statement](https://docs.python.org/3.9/tutorial/errors.html#handling-exceptions){target="_blank"}, leading a Python program to a non-successful exit.
+
+ They are typically raised intentionally to signal a problem (`raise ValueError`), or a propagated exception from elsewhere in your code that you didn't handle it willingly or not (`KeyError`, `jsonDecoderError`, etc.).
+
+=== "logging_uncaught_exceptions.py"
+
+ ```python hl_lines="7"
+ --8<-- "examples/logger/src/logging_uncaught_exceptions.py"
+ ```
+
+=== "logging_uncaught_exceptions_output.json"
+
+ ```json hl_lines="7-8"
+ --8<-- "examples/logger/src/logging_uncaught_exceptions_output.json"
+ ```
+
### Date formatting
Logger uses Python's standard logging date format with the addition of timezone: `2021-05-03 11:47:12,494+0200`.
diff --git a/docs/index.md b/docs/index.md
index fb52c187e91..2118b4e5c46 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -18,16 +18,16 @@ A suite of utilities for AWS Lambda functions to ease adopting best practices su
2) [**Share your work**](https://github.com/awslabs/aws-lambda-powertools-python/issues/new?assignees=&labels=community-content&template=share_your_work.yml&title=%5BI+Made+This%5D%3A+%3CTITLE%3E). Blog posts, video, sample projects you used Powertools!
- 3) Use [**Lambda Layers**](#lambda-layer) or [**SAR**](#sar), if possible. This helps us understand who uses Powertools in a non-intrusive way, and helps us gain future investments for other Lambda Powertools languages.
+ 3) Use [**Lambda Layers**](#lambda-layer) or [**SAR**](#sar), if possible. This helps us understand who uses Powertools in a non-intrusive way, and helps us gain future investments for other Powertools languages.
- When using Layers, you can add Lambda Powertools as a dev dependency (or as part of your virtual env) to not impact the development process.
+ When using Layers, you can add Powertools as a dev dependency (or as part of your virtual env) to not impact the development process.
## Install
Powertools is available in the following formats:
-* **Lambda Layer (x86_64)**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:13**](#){: .copyMe}:clipboard:
-* **Lambda Layer (arm64)**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13**](#){: .copyMe}:clipboard:
+* **Lambda Layer (x86_64)**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:14**](#){: .copyMe}:clipboard:
+* **Lambda Layer (arm64)**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14**](#){: .copyMe}:clipboard:
* **PyPi**: **`pip install "aws-lambda-powertools"`**
???+ info "Some utilities require additional dependencies"
@@ -59,7 +59,7 @@ This means you need to add AWS SDK as a development dependency (not as a product
[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.
+You can include 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 "Note: Click to expand and copy any regional Lambda Layer ARN"
@@ -67,55 +67,55 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
| Region | Layer ARN |
| ---------------- | ---------------------------------------------------------------------------------------------------------- |
- | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
- | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:13](#){: .copyMe}:clipboard: |
+ | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
+ | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:14](#){: .copyMe}:clipboard: |
=== "arm64"
| Region | Layer ARN |
| ---------------- | ---------------------------------------------------------------------------------------------------------------- |
- | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
- | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13](#){: .copyMe}:clipboard: |
+ | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
+ | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14](#){: .copyMe}:clipboard: |
??? note "Note: Click to expand and copy code snippets for popular frameworks"
@@ -128,7 +128,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
Type: AWS::Serverless::Function
Properties:
Layers:
- - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:13
+ - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:14
```
=== "Serverless framework"
@@ -138,7 +138,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
hello:
handler: lambda_function.lambda_handler
layers:
- - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:13
+ - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:14
```
=== "CDK"
@@ -154,7 +154,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
powertools_layer = aws_lambda.LayerVersion.from_layer_version_arn(
self,
id="lambda-powertools",
- layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:13"
+ layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:14"
)
aws_lambda.Function(self,
'sample-app-lambda',
@@ -203,7 +203,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
role = aws_iam_role.iam_for_lambda.arn
handler = "index.test"
runtime = "python3.9"
- layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:13"]
+ layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:14"]
source_code_hash = filebase64sha256("lambda_function_payload.zip")
}
@@ -256,7 +256,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
? Do you want to configure advanced settings? Yes
...
? Do you want to enable Lambda layers for this function? Yes
- ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:13
+ ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:14
❯ amplify push -y
@@ -267,7 +267,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
- Name:
? Which setting do you want to update? Lambda layers configuration
? Do you want to enable Lambda layers for this function? Yes
- ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:13
+ ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:14
? Do you want to edit the local lambda function now? No
```
@@ -276,7 +276,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
Change {region} to your AWS region, e.g. `eu-west-1`
```bash title="AWS CLI"
- aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:13 --region {region}
+ aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:14 --region {region}
```
The pre-signed URL to download this Lambda Layer will be within `Location` key.
@@ -291,7 +291,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
Properties:
Architectures: [arm64]
Layers:
- - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13
+ - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14
```
=== "Serverless framework"
@@ -302,7 +302,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
handler: lambda_function.lambda_handler
architecture: arm64
layers:
- - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13
+ - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14
```
=== "CDK"
@@ -318,7 +318,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
powertools_layer = aws_lambda.LayerVersion.from_layer_version_arn(
self,
id="lambda-powertools",
- layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13"
+ layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14"
)
aws_lambda.Function(self,
'sample-app-lambda',
@@ -368,7 +368,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
role = aws_iam_role.iam_for_lambda.arn
handler = "index.test"
runtime = "python3.9"
- layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13"]
+ layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14"]
architectures = ["arm64"]
source_code_hash = filebase64sha256("lambda_function_payload.zip")
@@ -424,7 +424,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
? Do you want to configure advanced settings? Yes
...
? Do you want to enable Lambda layers for this function? Yes
- ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13
+ ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14
❯ amplify push -y
@@ -435,7 +435,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
- Name:
? Which setting do you want to update? Lambda layers configuration
? Do you want to enable Lambda layers for this function? Yes
- ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13
+ ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14
? Do you want to edit the local lambda function now? No
```
@@ -443,7 +443,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
Change {region} to your AWS region, e.g. `eu-west-1`
```bash title="AWS CLI"
- aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:13 --region {region}
+ aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:14 --region {region}
```
The pre-signed URL to download this Lambda Layer will be within `Location` key.
@@ -585,7 +585,7 @@ Compared with the [public Layer ARN](#lambda-layer) option, SAR allows you to ch
value = data.aws_serverlessapplicationrepository_application.sar_app.semantic_version
}
- # Fetch Lambda Powertools Layer ARN from deployed SAR App
+ # Fetch Powertools Layer ARN from deployed SAR App
output "aws_lambda_powertools_layer_arn" {
value = aws_serverlessapplicationrepository_cloudformation_stack.deploy_sar_stack.outputs.LayerVersionArn
}
@@ -673,7 +673,7 @@ sam init --location https://github.com/aws-samples/cookiecutter-aws-sam-python
## Features
-Core utilities such as Tracing, Logging, Metrics, and Event Handler will be available across all Lambda Powertools languages. Additional utilities are subjective to each language ecosystem and customer demand.
+Core utilities such as Tracing, Logging, Metrics, and Event Handler will be available across all Powertools languages. Additional utilities are subjective to each language ecosystem and customer demand.
| Utility | Description |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -708,7 +708,7 @@ Core utilities such as Tracing, Logging, Metrics, and Event Handler will be avai
| **POWERTOOLS_LOGGER_LOG_EVENT** | Logs incoming event | [Logging](./core/logger) | `false` |
| **POWERTOOLS_LOGGER_SAMPLE_RATE** | Debug log sampling | [Logging](./core/logger) | `0` |
| **POWERTOOLS_LOG_DEDUPLICATION_DISABLED** | Disables log deduplication filter protection to use Pytest Live Log feature | [Logging](./core/logger) | `false` |
-| **POWERTOOLS_DEV** | Increases verbosity across utilities | Multiple; see [POWERTOOLS_DEV effect below](#increasing-verbosity-across-utilities) | `0` |
+| **POWERTOOLS_DEV** | Increases verbosity across utilities | Multiple; see [POWERTOOLS_DEV effect below](#increasing-verbosity-across-utilities) | `false` |
| **LOG_LEVEL** | Sets logging level | [Logging](./core/logger) | `INFO` |
### Optimizing for non-production environments
@@ -728,7 +728,7 @@ When `POWERTOOLS_DEV` is set to a truthy value (`1`, `true`), it'll have the fol
## Debug mode
-As a best practice for libraries, AWS Lambda Powertools module logging statements are suppressed.
+As a best practice for libraries, Powertools module logging statements are suppressed.
When necessary, you can use `POWERTOOLS_DEBUG` environment variable to enable debugging. This will provide additional information on every internal operation.
diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md
index e6f7cbfed29..8ae49544419 100644
--- a/docs/tutorial/index.md
+++ b/docs/tutorial/index.md
@@ -3,6 +3,8 @@ title: Tutorial
description: Powertools introduction
---
+
+
This tutorial progressively introduces Lambda Powertools core utilities by using one feature at a time.
## Requirements
@@ -343,7 +345,7 @@ Let's include Lambda Powertools as a dependency in `requirement.txt`, and use Ev
=== "requirements.txt"
```bash
- aws-lambda-powertools
+ aws-lambda-powertools[tracer] # Tracer requires AWS X-Ray SDK dependency
```
Use `sam build && sam local start-api` and try run it locally again.
diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md
index 9981978ebc9..85c58e7ce72 100644
--- a/docs/utilities/data_classes.md
+++ b/docs/utilities/data_classes.md
@@ -58,33 +58,33 @@ Same example as above, but using the `event_source` decorator
## Supported event sources
-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`
-[API Gateway Proxy V2](#api-gateway-proxy-v2) | `APIGatewayProxyEventV2`
-[Application Load Balancer](#application-load-balancer) | `ALBEvent`
-[AppSync Authorizer](#appsync-authorizer) | `AppSyncAuthorizerEvent`
-[AppSync Resolver](#appsync-resolver) | `AppSyncResolverEvent`
-[CloudWatch Dashboard Custom Widget](#cloudwatch-dashboard-custom-widget) | `CloudWatchDashboardCustomWidgetEvent`
-[CloudWatch Logs](#cloudwatch-logs) | `CloudWatchLogsEvent`
-[CodePipeline Job Event](#codepipeline-job) | `CodePipelineJobEvent`
-[Cognito User Pool](#cognito-user-pool) | Multiple available under `cognito_user_pool_event`
-[Connect Contact Flow](#connect-contact-flow) | `ConnectContactFlowEvent`
-[DynamoDB streams](#dynamodb-streams) | `DynamoDBStreamEvent`, `DynamoDBRecordEventName`
-[EventBridge](#eventbridge) | `EventBridgeEvent`
-[Kafka](#kafka) | `KafkaEvent`
-[Kinesis Data Stream](#kinesis-streams) | `KinesisStreamEvent`
-[Kinesis Firehose Delivery Stream](#kinesis-firehose-delivery-stream) | `KinesisFirehoseEvent`
-[Lambda Function URL](#lambda-function-url) | `LambdaFunctionUrlEvent`
-[Rabbit MQ](#rabbit-mq) | `RabbitMQEvent`
-[S3](#s3) | `S3Event`
-[S3 Object Lambda](#s3-object-lambda) | `S3ObjectLambdaEvent`
-[SES](#ses) | `SESEvent`
-[SNS](#sns) | `SNSEvent`
-[SQS](#sqs) | `SQSEvent`
+| 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` |
+| [API Gateway Proxy V2](#api-gateway-proxy-v2) | `APIGatewayProxyEventV2` |
+| [Application Load Balancer](#application-load-balancer) | `ALBEvent` |
+| [AppSync Authorizer](#appsync-authorizer) | `AppSyncAuthorizerEvent` |
+| [AppSync Resolver](#appsync-resolver) | `AppSyncResolverEvent` |
+| [CloudWatch Dashboard Custom Widget](#cloudwatch-dashboard-custom-widget) | `CloudWatchDashboardCustomWidgetEvent` |
+| [CloudWatch Logs](#cloudwatch-logs) | `CloudWatchLogsEvent` |
+| [CodePipeline Job Event](#codepipeline-job) | `CodePipelineJobEvent` |
+| [Cognito User Pool](#cognito-user-pool) | Multiple available under `cognito_user_pool_event` |
+| [Connect Contact Flow](#connect-contact-flow) | `ConnectContactFlowEvent` |
+| [DynamoDB streams](#dynamodb-streams) | `DynamoDBStreamEvent`, `DynamoDBRecordEventName` |
+| [EventBridge](#eventbridge) | `EventBridgeEvent` |
+| [Kafka](#kafka) | `KafkaEvent` |
+| [Kinesis Data Stream](#kinesis-streams) | `KinesisStreamEvent` |
+| [Kinesis Firehose Delivery Stream](#kinesis-firehose-delivery-stream) | `KinesisFirehoseEvent` |
+| [Lambda Function URL](#lambda-function-url) | `LambdaFunctionUrlEvent` |
+| [Rabbit MQ](#rabbit-mq) | `RabbitMQEvent` |
+| [S3](#s3) | `S3Event` |
+| [S3 Object Lambda](#s3-object-lambda) | `S3ObjectLambdaEvent` |
+| [SES](#ses) | `SESEvent` |
+| [SNS](#sns) | `SNSEvent` |
+| [SQS](#sqs) | `SQSEvent` |
???+ info
The examples provided below are far from exhaustive - the data classes themselves are designed to provide a form of
@@ -456,9 +456,9 @@ In this example, we also use the new Logger `correlation_id` and built-in `corre
A simple echo script. Anything passed in \`\`\`echo\`\`\` parameter is returned as the content of custom widget.
### Widget parameters
- Param | Description
- ---|---
- **echo** | The content to echo back
+ | Param | Description |
+ | -------- | ------------------------ |
+ | **echo** | The content to echo back |
### Example parameters
\`\`\` yaml
@@ -497,6 +497,53 @@ decompress and parse json data from the event.
do_something_with(event.timestamp, event.message)
```
+#### Kinesis integration
+
+[When streaming CloudWatch Logs to a Kinesis Data Stream](https://aws.amazon.com/premiumsupport/knowledge-center/streaming-cloudwatch-logs/){target="_blank"} (cross-account or not), you can use `extract_cloudwatch_logs_from_event` to decode, decompress and extract logs as `CloudWatchLogsDecodedData` to ease log processing.
+
+=== "app.py"
+
+ ```python hl_lines="5-6 11"
+ from typing import List
+
+ from aws_lambda_powertools.utilities.data_classes import event_source
+ from aws_lambda_powertools.utilities.data_classes.cloud_watch_logs_event import CloudWatchLogsDecodedData
+ from aws_lambda_powertools.utilities.data_classes.kinesis_stream_event import (
+ KinesisStreamEvent, extract_cloudwatch_logs_from_event)
+
+
+ @event_source(data_class=KinesisStreamEvent)
+ def simple_handler(event: KinesisStreamEvent, context):
+ logs: List[CloudWatchLogsDecodedData] = extract_cloudwatch_logs_from_event(event)
+ for log in logs:
+ if log.message_type == "DATA_MESSAGE":
+ return "success"
+ return "nothing to be processed"
+ ```
+
+Alternatively, you can use `extract_cloudwatch_logs_from_record` to seamless integrate with the [Batch utility](./batch.md) for more robust log processing.
+
+=== "app.py"
+
+ ```python hl_lines="3-4 10"
+ from aws_lambda_powertools.utilities.batch import (BatchProcessor, EventType,
+ batch_processor)
+ from aws_lambda_powertools.utilities.data_classes.kinesis_stream_event import (
+ KinesisStreamRecord, extract_cloudwatch_logs_from_record)
+
+ processor = BatchProcessor(event_type=EventType.KinesisDataStreams)
+
+
+ def record_handler(record: KinesisStreamRecord):
+ log = extract_cloudwatch_logs_from_record(record)
+ return log.message_type == "DATA_MESSAGE"
+
+
+ @batch_processor(record_handler=record_handler, processor=processor)
+ def lambda_handler(event, context):
+ return processor.response()
+ ```
+
### CodePipeline Job
Data classes and utility functions to help create continuous delivery pipelines tasks with AWS Lambda
@@ -553,18 +600,18 @@ Data classes and utility functions to help create continuous delivery pipelines
Cognito User Pools have several [different Lambda trigger sources](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html#cognito-user-identity-pools-working-with-aws-lambda-trigger-sources), all of which map to a different data class, which
can be imported from `aws_lambda_powertools.data_classes.cognito_user_pool_event`:
-Trigger/Event Source | Data Class
-------------------------------------------------- | -------------------------------------------------
-Custom message event | `data_classes.cognito_user_pool_event.CustomMessageTriggerEvent`
-Post authentication | `data_classes.cognito_user_pool_event.PostAuthenticationTriggerEvent`
-Post confirmation | `data_classes.cognito_user_pool_event.PostConfirmationTriggerEvent`
-Pre authentication | `data_classes.cognito_user_pool_event.PreAuthenticationTriggerEvent`
-Pre sign-up | `data_classes.cognito_user_pool_event.PreSignUpTriggerEvent`
-Pre token generation | `data_classes.cognito_user_pool_event.PreTokenGenerationTriggerEvent`
-User migration | `data_classes.cognito_user_pool_event.UserMigrationTriggerEvent`
-Define Auth Challenge | `data_classes.cognito_user_pool_event.DefineAuthChallengeTriggerEvent`
-Create Auth Challenge | `data_classes.cognito_user_pool_event.CreateAuthChallengeTriggerEvent`
-Verify Auth Challenge | `data_classes.cognito_user_pool_event.VerifyAuthChallengeResponseTriggerEvent`
+| Trigger/Event Source | Data Class |
+| --------------------- | ------------------------------------------------------------------------------ |
+| Custom message event | `data_classes.cognito_user_pool_event.CustomMessageTriggerEvent` |
+| Post authentication | `data_classes.cognito_user_pool_event.PostAuthenticationTriggerEvent` |
+| Post confirmation | `data_classes.cognito_user_pool_event.PostConfirmationTriggerEvent` |
+| Pre authentication | `data_classes.cognito_user_pool_event.PreAuthenticationTriggerEvent` |
+| Pre sign-up | `data_classes.cognito_user_pool_event.PreSignUpTriggerEvent` |
+| Pre token generation | `data_classes.cognito_user_pool_event.PreTokenGenerationTriggerEvent` |
+| User migration | `data_classes.cognito_user_pool_event.UserMigrationTriggerEvent` |
+| Define Auth Challenge | `data_classes.cognito_user_pool_event.DefineAuthChallengeTriggerEvent` |
+| Create Auth Challenge | `data_classes.cognito_user_pool_event.CreateAuthChallengeTriggerEvent` |
+| Verify Auth Challenge | `data_classes.cognito_user_pool_event.VerifyAuthChallengeResponseTriggerEvent` |
#### Post Confirmation Example
diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md
index f35dd88106b..ec4c28699e7 100644
--- a/docs/utilities/feature_flags.md
+++ b/docs/utilities/feature_flags.md
@@ -53,7 +53,7 @@ The following sample infrastructure will be used throughout this documentation:
```yaml hl_lines="5 11 18 25 31-50 54"
AWSTemplateFormatVersion: "2010-09-09"
- Description: Lambda Powertools Feature flags sample template
+ Description: Lambda Powertools for Python Feature flags sample template
Resources:
FeatureStoreApp:
Type: AWS::AppConfig::Application
@@ -580,20 +580,20 @@ The `conditions` block is a list of conditions that contain `action`, `key`, and
The `action` configuration can have the following values, where the expressions **`a`** is the `key` and **`b`** is the `value` above:
-Action | Equivalent expression
-------------------------------------------------- | ---------------------------------------------------------------------------------
-**EQUALS** | `lambda a, b: a == b`
-**NOT_EQUALS** | `lambda a, b: a != b`
-**KEY_GREATER_THAN_VALUE** | `lambda a, b: a > b`
-**KEY_GREATER_THAN_OR_EQUAL_VALUE** | `lambda a, b: a >= b`
-**KEY_LESS_THAN_VALUE** | `lambda a, b: a < b`
-**KEY_LESS_THAN_OR_EQUAL_VALUE** | `lambda a, b: a <= b`
-**STARTSWITH** | `lambda a, b: a.startswith(b)`
-**ENDSWITH** | `lambda a, b: a.endswith(b)`
-**KEY_IN_VALUE** | `lambda a, b: a in b`
-**KEY_NOT_IN_VALUE** | `lambda a, b: a not in b`
-**VALUE_IN_KEY** | `lambda a, b: b in a`
-**VALUE_NOT_IN_KEY** | `lambda a, b: b not in a`
+| Action | Equivalent expression |
+| ----------------------------------- | ------------------------------ |
+| **EQUALS** | `lambda a, b: a == b` |
+| **NOT_EQUALS** | `lambda a, b: a != b` |
+| **KEY_GREATER_THAN_VALUE** | `lambda a, b: a > b` |
+| **KEY_GREATER_THAN_OR_EQUAL_VALUE** | `lambda a, b: a >= b` |
+| **KEY_LESS_THAN_VALUE** | `lambda a, b: a < b` |
+| **KEY_LESS_THAN_OR_EQUAL_VALUE** | `lambda a, b: a <= b` |
+| **STARTSWITH** | `lambda a, b: a.startswith(b)` |
+| **ENDSWITH** | `lambda a, b: a.endswith(b)` |
+| **KEY_IN_VALUE** | `lambda a, b: a in b` |
+| **KEY_NOT_IN_VALUE** | `lambda a, b: a not in b` |
+| **VALUE_IN_KEY** | `lambda a, b: b in a` |
+| **VALUE_NOT_IN_KEY** | `lambda a, b: b not in a` |
???+ info
The `**key**` and `**value**` will be compared to the input from the `**context**` parameter.
@@ -667,16 +667,16 @@ AppConfig store provider fetches any JSON document from AWS AppConfig.
These are the available options for further customization.
-Parameter | Default | Description
-------------------------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------
-**environment** | `""` | AWS AppConfig Environment, e.g. `test`
-**application** | `""` | AWS AppConfig Application
-**name** | `""` | AWS AppConfig Configuration name
-**envelope** | `None` | JMESPath expression to use to extract feature flags configuration from AWS AppConfig configuration
-**max_age** | `5` | Number of seconds to cache feature flags configuration fetched from AWS AppConfig
-**sdk_config** | `None` | [Botocore Config object](https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html){target="_blank"}
-**jmespath_options** | `None` | For advanced use cases when you want to bring your own [JMESPath functions](https://github.com/jmespath/jmespath.py#custom-functions){target="_blank"}
-**logger** | `logging.Logger` | Logger to use for debug. You can optionally supply an instance of Powertools Logger.
+| Parameter | Default | Description |
+| -------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| **environment** | `""` | AWS AppConfig Environment, e.g. `test` |
+| **application** | `""` | AWS AppConfig Application |
+| **name** | `""` | AWS AppConfig Configuration name |
+| **envelope** | `None` | JMESPath expression to use to extract feature flags configuration from AWS AppConfig configuration |
+| **max_age** | `5` | Number of seconds to cache feature flags configuration fetched from AWS AppConfig |
+| **sdk_config** | `None` | [Botocore Config object](https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html){target="_blank"} |
+| **jmespath_options** | `None` | For advanced use cases when you want to bring your own [JMESPath functions](https://github.com/jmespath/jmespath.py#custom-functions){target="_blank"} |
+| **logger** | `logging.Logger` | Logger to use for debug. You can optionally supply an instance of Powertools Logger. |
```python hl_lines="21-27" title="AppConfigStore sample"
from botocore.config import Config
@@ -771,17 +771,17 @@ def test_flags_condition_match(mocker):
## Feature flags vs Parameters vs env vars
-Method | When to use | Requires new deployment on changes | Supported services
-------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------- | -------------------------------------------------
-**[Environment variables](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html){target="_blank"}** | Simple configuration that will rarely if ever change, because changing it requires a Lambda function deployment. | Yes | Lambda
-**[Parameters utility](parameters.md)** | Access to secrets, or fetch parameters in different formats from AWS System Manager Parameter Store or Amazon DynamoDB. | No | Parameter Store, DynamoDB, Secrets Manager, AppConfig
-**Feature flags utility** | Rule engine to define when one or multiple features should be enabled depending on the input. | No | AppConfig
+| Method | When to use | Requires new deployment on changes | Supported services |
+| --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ----------------------------------------------------- |
+| **[Environment variables](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html){target="_blank"}** | Simple configuration that will rarely if ever change, because changing it requires a Lambda function deployment. | Yes | Lambda |
+| **[Parameters utility](parameters.md)** | Access to secrets, or fetch parameters in different formats from AWS System Manager Parameter Store or Amazon DynamoDB. | No | Parameter Store, DynamoDB, Secrets Manager, AppConfig |
+| **Feature flags utility** | Rule engine to define when one or multiple features should be enabled depending on the input. | No | AppConfig |
## Deprecation list when GA
-Breaking change | Recommendation
-------------------------------------------------- | ---------------------------------------------------------------------------------
-`IN` RuleAction | Use `KEY_IN_VALUE` instead
-`NOT_IN` RuleAction | Use `KEY_NOT_IN_VALUE` instead
-`get_enabled_features` | Return type changes from `List[str]` to `Dict[str, Any]`. New return will contain a list of features enabled and their values. List of enabled features will be in `enabled_features` key to keep ease of assertion we have in Beta.
-`boolean_type` Schema | This **might** not be necessary anymore before we go GA. We will return either the `default` value when there are no rules as well as `when_match` value. This will simplify on-boarding if we can keep the same set of validations already offered.
+| Breaking change | Recommendation |
+| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `IN` RuleAction | Use `KEY_IN_VALUE` instead |
+| `NOT_IN` RuleAction | Use `KEY_NOT_IN_VALUE` instead |
+| `get_enabled_features` | Return type changes from `List[str]` to `Dict[str, Any]`. New return will contain a list of features enabled and their values. List of enabled features will be in `enabled_features` key to keep ease of assertion we have in Beta. |
+| `boolean_type` Schema | This **might** not be necessary anymore before we go GA. We will return either the `default` value when there are no rules as well as `when_match` value. This will simplify on-boarding if we can keep the same set of validations already offered. |
diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md
index b246e2cebc1..096262a5d52 100644
--- a/docs/utilities/idempotency.md
+++ b/docs/utilities/idempotency.md
@@ -125,8 +125,12 @@ When using `idempotent_function`, you must tell us which keyword parameter in yo
!!! info "We support JSON serializable data, [Python Dataclasses](https://docs.python.org/3.7/library/dataclasses.html){target="_blank"}, [Parser/Pydantic Models](parser.md){target="_blank"}, and our [Event Source Data Classes](./data_classes.md){target="_blank"}."
-???+ warning
- Make sure to call your decorated function using keyword arguments
+???+ warning "Limitations"
+ Make sure to call your decorated function using keyword arguments.
+
+ Decorated functions with `idempotent_function` are not thread-safe, if the caller uses threading, not the function computation itself.
+
+ DynamoDB Persistency layer uses a Resource client [which is not thread-safe](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/resources.html?highlight=multithreading#multithreading-or-multiprocessing-with-resources){target="_blank"}.
=== "batch_sample.py"
@@ -1018,12 +1022,34 @@ with a truthy value. If you prefer setting this for specific tests, and are usin
=== "tests.py"
- ```python hl_lines="2 3"
- def test_idempotent_lambda_handler(monkeypatch):
+ ```python hl_lines="24-25"
+ from dataclasses import dataclass
+
+ import pytest
+
+ import app
+
+
+ @pytest.fixture
+ def lambda_context():
+ @dataclass
+ class LambdaContext:
+ function_name: str = "test"
+ memory_limit_in_mb: int = 128
+ invoked_function_arn: str = "arn:aws:lambda:eu-west-1:809313241:function:test"
+ aws_request_id: str = "52fdfc07-2182-154f-163f-5f0f9a621d72"
+
+ def get_remaining_time_in_millis(self) -> int:
+ return 5
+
+ return LambdaContext()
+
+
+ def test_idempotent_lambda_handler(monkeypatch, lambda_context):
# Set POWERTOOLS_IDEMPOTENCY_DISABLED before calling decorated functions
monkeypatch.setenv("POWERTOOLS_IDEMPOTENCY_DISABLED", 1)
- result = handler()
+ result = handler({}, lambda_context)
...
```
=== "app.py"
@@ -1051,18 +1077,36 @@ To test with [DynamoDB Local](https://docs.aws.amazon.com/amazondynamodb/latest/
=== "tests.py"
- ```python hl_lines="6 7 8"
+ ```python hl_lines="24-27"
+ from dataclasses import dataclass
+
import boto3
+ import pytest
import app
- def test_idempotent_lambda():
+
+ @pytest.fixture
+ def lambda_context():
+ @dataclass
+ class LambdaContext:
+ function_name: str = "test"
+ memory_limit_in_mb: int = 128
+ invoked_function_arn: str = "arn:aws:lambda:eu-west-1:809313241:function:test"
+ aws_request_id: str = "52fdfc07-2182-154f-163f-5f0f9a621d72"
+
+ def get_remaining_time_in_millis(self) -> int:
+ return 5
+
+ return LambdaContext()
+
+ def test_idempotent_lambda(lambda_context):
# Create our own Table resource using the endpoint for our DynamoDB Local instance
resource = boto3.resource("dynamodb", endpoint_url='http://localhost:8000')
table = resource.Table(app.persistence_layer.table_name)
app.persistence_layer.table = table
- result = app.handler({'testkey': 'testvalue'}, {})
+ result = app.handler({'testkey': 'testvalue'}, lambda_context)
assert result['payment_id'] == 12345
```
@@ -1092,15 +1136,35 @@ This means it is possible to pass a mocked Table resource, or stub various metho
=== "tests.py"
- ```python hl_lines="6 7 8 9"
+ ```python hl_lines="26-29"
+ from dataclasses import dataclass
from unittest.mock import MagicMock
+ import boto3
+ import pytest
+
import app
- def test_idempotent_lambda():
+
+ @pytest.fixture
+ def lambda_context():
+ @dataclass
+ class LambdaContext:
+ function_name: str = "test"
+ memory_limit_in_mb: int = 128
+ invoked_function_arn: str = "arn:aws:lambda:eu-west-1:809313241:function:test"
+ aws_request_id: str = "52fdfc07-2182-154f-163f-5f0f9a621d72"
+
+ def get_remaining_time_in_millis(self) -> int:
+ return 5
+
+ return LambdaContext()
+
+
+ def test_idempotent_lambda(lambda_context):
table = MagicMock()
app.persistence_layer.table = table
- result = app.handler({'testkey': 'testvalue'}, {})
+ result = app.handler({'testkey': 'testvalue'}, lambda_context)
table.put_item.assert_called()
...
```
diff --git a/docs/we_made_this.md b/docs/we_made_this.md
index ae6d516c2c8..1aa52df5cce 100644
--- a/docs/we_made_this.md
+++ b/docs/we_made_this.md
@@ -1,11 +1,11 @@
---
title: We Made This (Community)
-description: Blog posts, tutorials, and videos about Lambda Powertools created by the Powertools Community.
+description: Blog posts, tutorials, and videos about AWS Lambda Powertools created by the Powertools Community.
---
-This space is dedicated to highlight our awesome community content featuring Lambda Powertools 🙏!
+This space is dedicated to highlight our awesome community content featuring Powertools 🙏!
!!! info "[Get your content featured here](https://github.com/awslabs/aws-lambda-powertools-python/issues/new?assignees=&labels=community-content&template=share_your_work.yml&title=%5BI+Made+This%5D%3A+%3CTITLE%3E){target="_blank"}!"
diff --git a/examples/event_handler_rest/src/assert_alb_api_resolver_response.py b/examples/event_handler_rest/src/assert_alb_api_resolver_response.py
new file mode 100644
index 00000000000..f6bd54facee
--- /dev/null
+++ b/examples/event_handler_rest/src/assert_alb_api_resolver_response.py
@@ -0,0 +1,30 @@
+from dataclasses import dataclass
+
+import assert_alb_api_response_module
+import pytest
+
+
+@pytest.fixture
+def lambda_context():
+ @dataclass
+ class LambdaContext:
+ function_name: str = "test"
+ memory_limit_in_mb: int = 128
+ invoked_function_arn: str = "arn:aws:lambda:eu-west-1:123456789012:function:test"
+ aws_request_id: str = "da658bd3-2d6f-4e7b-8ec2-937234644fdc"
+
+ return LambdaContext()
+
+
+def test_lambda_handler(lambda_context):
+ minimal_event = {
+ "path": "/todos",
+ "httpMethod": "GET",
+ "headers": {"x-amzn-trace-id": "b25827e5-0e30-4d52-85a8-4df449ee4c5a"},
+ }
+ # Example of Application Load Balancer request event:
+ # https://docs.aws.amazon.com/lambda/latest/dg/services-alb.html
+
+ ret = assert_alb_api_response_module.lambda_handler(minimal_event, lambda_context)
+ assert ret["statusCode"] == 200
+ assert ret["body"] != ""
diff --git a/examples/event_handler_rest/src/assert_alb_api_response_module.py b/examples/event_handler_rest/src/assert_alb_api_response_module.py
new file mode 100644
index 00000000000..828787179d6
--- /dev/null
+++ b/examples/event_handler_rest/src/assert_alb_api_response_module.py
@@ -0,0 +1,27 @@
+import requests
+from requests import Response
+
+from aws_lambda_powertools import Logger, Tracer
+from aws_lambda_powertools.event_handler import ALBResolver
+from aws_lambda_powertools.logging import correlation_paths
+from aws_lambda_powertools.utilities.typing import LambdaContext
+
+tracer = Tracer()
+logger = Logger()
+app = ALBResolver()
+
+
+@app.get("/todos")
+@tracer.capture_method
+def get_todos():
+ todos: Response = requests.get("https://jsonplaceholder.typicode.com/todos")
+ todos.raise_for_status()
+
+ return {"todos": todos.json()[:10]}
+
+
+# You can continue to use other utilities just as before
+@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPLICATION_LOAD_BALANCER)
+@tracer.capture_lambda_handler
+def lambda_handler(event: dict, context: LambdaContext) -> dict:
+ return app.resolve(event, context)
diff --git a/examples/event_handler_rest/src/assert_function_url_api_resolver_response.py b/examples/event_handler_rest/src/assert_function_url_api_resolver_response.py
new file mode 100644
index 00000000000..865f26b70a3
--- /dev/null
+++ b/examples/event_handler_rest/src/assert_function_url_api_resolver_response.py
@@ -0,0 +1,35 @@
+from dataclasses import dataclass
+
+import assert_function_url_api_response_module
+import pytest
+
+
+@pytest.fixture
+def lambda_context():
+ @dataclass
+ class LambdaContext:
+ function_name: str = "test"
+ memory_limit_in_mb: int = 128
+ invoked_function_arn: str = "arn:aws:lambda:eu-west-1:123456789012:function:test"
+ aws_request_id: str = "da658bd3-2d6f-4e7b-8ec2-937234644fdc"
+
+ return LambdaContext()
+
+
+def test_lambda_handler(lambda_context):
+ minimal_event = {
+ "rawPath": "/todos",
+ "requestContext": {
+ "requestContext": {"requestId": "227b78aa-779d-47d4-a48e-ce62120393b8"}, # correlation ID
+ "http": {
+ "method": "GET",
+ },
+ "stage": "$default",
+ },
+ }
+ # Example of Lambda Function URL request event:
+ # https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads
+
+ ret = assert_function_url_api_response_module.lambda_handler(minimal_event, lambda_context)
+ assert ret["statusCode"] == 200
+ assert ret["body"] != ""
diff --git a/examples/event_handler_rest/src/assert_function_url_api_response_module.py b/examples/event_handler_rest/src/assert_function_url_api_response_module.py
new file mode 100644
index 00000000000..921e066fc78
--- /dev/null
+++ b/examples/event_handler_rest/src/assert_function_url_api_response_module.py
@@ -0,0 +1,27 @@
+import requests
+from requests import Response
+
+from aws_lambda_powertools import Logger, Tracer
+from aws_lambda_powertools.event_handler import LambdaFunctionUrlResolver
+from aws_lambda_powertools.logging import correlation_paths
+from aws_lambda_powertools.utilities.typing import LambdaContext
+
+tracer = Tracer()
+logger = Logger()
+app = LambdaFunctionUrlResolver()
+
+
+@app.get("/todos")
+@tracer.capture_method
+def get_todos():
+ todos: Response = requests.get("https://jsonplaceholder.typicode.com/todos")
+ todos.raise_for_status()
+
+ return {"todos": todos.json()[:10]}
+
+
+# You can continue to use other utilities just as before
+@logger.inject_lambda_context(correlation_id_path=correlation_paths.LAMBDA_FUNCTION_URL)
+@tracer.capture_lambda_handler
+def lambda_handler(event: dict, context: LambdaContext) -> dict:
+ return app.resolve(event, context)
diff --git a/examples/event_handler_rest/src/assert_http_api_resolver_response.py b/examples/event_handler_rest/src/assert_http_api_resolver_response.py
new file mode 100644
index 00000000000..af294fbc3bc
--- /dev/null
+++ b/examples/event_handler_rest/src/assert_http_api_resolver_response.py
@@ -0,0 +1,35 @@
+from dataclasses import dataclass
+
+import assert_http_api_response_module
+import pytest
+
+
+@pytest.fixture
+def lambda_context():
+ @dataclass
+ class LambdaContext:
+ function_name: str = "test"
+ memory_limit_in_mb: int = 128
+ invoked_function_arn: str = "arn:aws:lambda:eu-west-1:123456789012:function:test"
+ aws_request_id: str = "da658bd3-2d6f-4e7b-8ec2-937234644fdc"
+
+ return LambdaContext()
+
+
+def test_lambda_handler(lambda_context):
+ minimal_event = {
+ "rawPath": "/todos",
+ "requestContext": {
+ "requestContext": {"requestId": "227b78aa-779d-47d4-a48e-ce62120393b8"}, # correlation ID
+ "http": {
+ "method": "GET",
+ },
+ "stage": "$default",
+ },
+ }
+ # Example of API Gateway HTTP API request event:
+ # https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
+
+ ret = assert_http_api_response_module.lambda_handler(minimal_event, lambda_context)
+ assert ret["statusCode"] == 200
+ assert ret["body"] != ""
diff --git a/examples/event_handler_rest/src/assert_http_api_response_module.py b/examples/event_handler_rest/src/assert_http_api_response_module.py
new file mode 100644
index 00000000000..852212272bb
--- /dev/null
+++ b/examples/event_handler_rest/src/assert_http_api_response_module.py
@@ -0,0 +1,27 @@
+import requests
+from requests import Response
+
+from aws_lambda_powertools import Logger, Tracer
+from aws_lambda_powertools.event_handler import APIGatewayHttpResolver
+from aws_lambda_powertools.logging import correlation_paths
+from aws_lambda_powertools.utilities.typing import LambdaContext
+
+tracer = Tracer()
+logger = Logger()
+app = APIGatewayHttpResolver()
+
+
+@app.get("/todos")
+@tracer.capture_method
+def get_todos():
+ todos: Response = requests.get("https://jsonplaceholder.typicode.com/todos")
+ todos.raise_for_status()
+
+ return {"todos": todos.json()[:10]}
+
+
+# You can continue to use other utilities just as before
+@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP)
+@tracer.capture_lambda_handler
+def lambda_handler(event: dict, context: LambdaContext) -> dict:
+ return app.resolve(event, context)
diff --git a/examples/event_handler_rest/src/assert_http_response.py b/examples/event_handler_rest/src/assert_rest_api_resolver_response.py
similarity index 70%
rename from examples/event_handler_rest/src/assert_http_response.py
rename to examples/event_handler_rest/src/assert_rest_api_resolver_response.py
index 95d56599288..4422022ae5f 100644
--- a/examples/event_handler_rest/src/assert_http_response.py
+++ b/examples/event_handler_rest/src/assert_rest_api_resolver_response.py
@@ -1,6 +1,6 @@
from dataclasses import dataclass
-import assert_http_response_module
+import assert_rest_api_resolver_response
import pytest
@@ -22,7 +22,8 @@ def test_lambda_handler(lambda_context):
"httpMethod": "GET",
"requestContext": {"requestId": "227b78aa-779d-47d4-a48e-ce62120393b8"}, # correlation ID
}
-
- ret = assert_http_response_module.lambda_handler(minimal_event, lambda_context)
+ # Example of API Gateway REST API request event:
+ # https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html#apigateway-example-event
+ ret = assert_rest_api_resolver_response.lambda_handler(minimal_event, lambda_context)
assert ret["statusCode"] == 200
assert ret["body"] != ""
diff --git a/examples/event_handler_rest/src/assert_http_response_module.py b/examples/event_handler_rest/src/assert_rest_api_response_module.py
similarity index 100%
rename from examples/event_handler_rest/src/assert_http_response_module.py
rename to examples/event_handler_rest/src/assert_rest_api_response_module.py
diff --git a/examples/logger/src/logging_uncaught_exceptions.py b/examples/logger/src/logging_uncaught_exceptions.py
new file mode 100644
index 00000000000..1b43c67914a
--- /dev/null
+++ b/examples/logger/src/logging_uncaught_exceptions.py
@@ -0,0 +1,16 @@
+import requests
+
+from aws_lambda_powertools import Logger
+from aws_lambda_powertools.utilities.typing import LambdaContext
+
+ENDPOINT = "http://httpbin.org/status/500"
+logger = Logger(log_uncaught_exceptions=True)
+
+
+def handler(event: dict, context: LambdaContext) -> str:
+ ret = requests.get(ENDPOINT)
+ # HTTP 4xx/5xx status will lead to requests.HTTPError
+ # Logger will log this exception before this program exits non-successfully
+ ret.raise_for_status()
+
+ return "hello world"
diff --git a/examples/logger/src/logging_uncaught_exceptions_output.json b/examples/logger/src/logging_uncaught_exceptions_output.json
new file mode 100644
index 00000000000..c8ff16e55b5
--- /dev/null
+++ b/examples/logger/src/logging_uncaught_exceptions_output.json
@@ -0,0 +1,9 @@
+{
+ "level": "ERROR",
+ "location": "log_uncaught_exception_hook:756",
+ "message": "500 Server Error: INTERNAL SERVER ERROR for url: http://httpbin.org/status/500",
+ "timestamp": "2022-11-16 13:51:29,198+0100",
+ "service": "payment",
+ "exception": "Traceback (most recent call last):\n File \"\", line 52, in \n handler({}, {})\n File \"\", line 17, in handler\n ret.raise_for_status()\n File \"/lib/python3.9/site-packages/requests/models.py\", line 1021, in raise_for_status\n raise HTTPError(http_error_msg, response=self)\nrequests.exceptions.HTTPError: 500 Server Error: INTERNAL SERVER ERROR for url: http://httpbin.org/status/500",
+ "exception_name": "HTTPError"
+}
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
index 8ca59c6145a..19f13020db2 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -353,7 +353,7 @@ tomli = "*"
[[package]]
name = "flake8-bugbear"
-version = "22.10.25"
+version = "22.10.27"
description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle."
category = "dev"
optional = false
@@ -368,11 +368,11 @@ dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "tox"]
[[package]]
name = "flake8-builtins"
-version = "2.0.0"
+version = "2.0.1"
description = "Check for python builtins being used as variables or parameters."
category = "dev"
optional = false
-python-versions = "*"
+python-versions = ">=3.7"
[package.dependencies]
flake8 = "*"
@@ -382,7 +382,7 @@ test = ["pytest"]
[[package]]
name = "flake8-comprehensions"
-version = "3.10.0"
+version = "3.10.1"
description = "A flake8 plugin to help you write better list/set/dict comprehensions."
category = "dev"
optional = false
@@ -770,7 +770,7 @@ mkdocs = ">=0.17"
[[package]]
name = "mkdocs-material"
-version = "8.5.7"
+version = "8.5.9"
description = "Documentation that simply works"
category = "dev"
optional = false
@@ -780,7 +780,7 @@ python-versions = ">=3.7"
jinja2 = ">=3.0.2"
markdown = ">=3.2"
mkdocs = ">=1.4.0"
-mkdocs-material-extensions = ">=1.0.3"
+mkdocs-material-extensions = ">=1.1"
pygments = ">=2.12"
pymdown-extensions = ">=9.4"
requests = ">=2.26"
@@ -814,8 +814,8 @@ reports = ["lxml"]
[[package]]
name = "mypy-boto3-appconfig"
-version = "1.25.0"
-description = "Type annotations for boto3.AppConfig 1.25.0 service generated with mypy-boto3-builder 7.11.10"
+version = "1.26.0.post1"
+description = "Type annotations for boto3.AppConfig 1.26.0 service generated with mypy-boto3-builder 7.11.10"
category = "dev"
optional = false
python-versions = ">=3.7"
@@ -825,8 +825,8 @@ typing-extensions = ">=4.1.0"
[[package]]
name = "mypy-boto3-appconfigdata"
-version = "1.25.0"
-description = "Type annotations for boto3.AppConfigData 1.25.0 service generated with mypy-boto3-builder 7.11.10"
+version = "1.26.0.post1"
+description = "Type annotations for boto3.AppConfigData 1.26.0 service generated with mypy-boto3-builder 7.11.10"
category = "dev"
optional = false
python-versions = ">=3.7"
@@ -847,8 +847,8 @@ typing-extensions = ">=4.1.0"
[[package]]
name = "mypy-boto3-cloudwatch"
-version = "1.25.0"
-description = "Type annotations for boto3.CloudWatch 1.25.0 service generated with mypy-boto3-builder 7.11.10"
+version = "1.26.0.post1"
+description = "Type annotations for boto3.CloudWatch 1.26.0 service generated with mypy-boto3-builder 7.11.10"
category = "dev"
optional = false
python-versions = ">=3.7"
@@ -869,8 +869,8 @@ typing-extensions = ">=4.1.0"
[[package]]
name = "mypy-boto3-lambda"
-version = "1.25.0"
-description = "Type annotations for boto3.Lambda 1.25.0 service generated with mypy-boto3-builder 7.11.10"
+version = "1.26.0.post1"
+description = "Type annotations for boto3.Lambda 1.26.0 service generated with mypy-boto3-builder 7.11.10"
category = "dev"
optional = false
python-versions = ">=3.7"
@@ -880,8 +880,8 @@ typing-extensions = ">=4.1.0"
[[package]]
name = "mypy-boto3-logs"
-version = "1.25.0"
-description = "Type annotations for boto3.CloudWatchLogs 1.25.0 service generated with mypy-boto3-builder 7.11.10"
+version = "1.26.3"
+description = "Type annotations for boto3.CloudWatchLogs 1.26.3 service generated with mypy-boto3-builder 7.11.10"
category = "dev"
optional = false
python-versions = ">=3.7"
@@ -891,8 +891,8 @@ typing-extensions = ">=4.1.0"
[[package]]
name = "mypy-boto3-s3"
-version = "1.25.0"
-description = "Type annotations for boto3.S3 1.25.0 service generated with mypy-boto3-builder 7.11.10"
+version = "1.26.0.post1"
+description = "Type annotations for boto3.S3 1.26.0 service generated with mypy-boto3-builder 7.11.10"
category = "dev"
optional = false
python-versions = ">=3.7"
@@ -902,8 +902,8 @@ typing-extensions = ">=4.1.0"
[[package]]
name = "mypy-boto3-secretsmanager"
-version = "1.25.0"
-description = "Type annotations for boto3.SecretsManager 1.25.0 service generated with mypy-boto3-builder 7.11.10"
+version = "1.26.0.post1"
+description = "Type annotations for boto3.SecretsManager 1.26.0 service generated with mypy-boto3-builder 7.11.10"
category = "dev"
optional = false
python-versions = ">=3.7"
@@ -913,8 +913,8 @@ typing-extensions = ">=4.1.0"
[[package]]
name = "mypy-boto3-ssm"
-version = "1.25.0"
-description = "Type annotations for boto3.SSM 1.25.0 service generated with mypy-boto3-builder 7.11.10"
+version = "1.26.4"
+description = "Type annotations for boto3.SSM 1.26.4 service generated with mypy-boto3-builder 7.11.10"
category = "dev"
optional = false
python-versions = ">=3.7"
@@ -924,8 +924,8 @@ typing-extensions = ">=4.1.0"
[[package]]
name = "mypy-boto3-xray"
-version = "1.25.0"
-description = "Type annotations for boto3.XRay 1.25.0 service generated with mypy-boto3-builder 7.11.10"
+version = "1.26.9"
+description = "Type annotations for boto3.XRay 1.26.9 service generated with mypy-boto3-builder 7.11.10"
category = "dev"
optional = false
python-versions = ">=3.7"
@@ -1141,7 +1141,7 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.
[[package]]
name = "pytest-asyncio"
-version = "0.20.1"
+version = "0.20.2"
description = "Pytest support for asyncio"
category = "dev"
optional = false
@@ -1156,11 +1156,11 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy
[[package]]
name = "pytest-benchmark"
-version = "3.4.1"
+version = "4.0.0"
description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer."
category = "dev"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = ">=3.7"
[package.dependencies]
py-cpuinfo = "*"
@@ -1186,18 +1186,6 @@ pytest = ">=4.6"
[package.extras]
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
-[[package]]
-name = "pytest-forked"
-version = "1.4.0"
-description = "run tests in isolated forked subprocesses"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-py = "*"
-pytest = ">=3.10"
-
[[package]]
name = "pytest-mock"
version = "3.10.0"
@@ -1214,7 +1202,7 @@ dev = ["pre-commit", "pytest-asyncio", "tox"]
[[package]]
name = "pytest-xdist"
-version = "2.5.0"
+version = "3.0.2"
description = "pytest xdist plugin for distributed testing and loop-on-failing modes"
category = "dev"
optional = false
@@ -1223,7 +1211,6 @@ python-versions = ">=3.6"
[package.dependencies]
execnet = ">=1.1"
pytest = ">=6.2.0"
-pytest-forked = "*"
[package.extras]
psutil = ["psutil (>=3.0)"]
@@ -1408,7 +1395,7 @@ test = ["mypy", "pytest", "typing-extensions"]
[[package]]
name = "types-requests"
-version = "2.28.11.2"
+version = "2.28.11.4"
description = "Typing stubs for requests"
category = "dev"
optional = false
@@ -1511,7 +1498,7 @@ validation = ["fastjsonschema"]
[metadata]
lock-version = "1.1"
python-versions = "^3.7.4"
-content-hash = "071841d4883c874f0f7ea8a8bc9b4b9cce97eaf8fce1122e2304d74879305b3d"
+content-hash = "a2a1ac8fa45ea0c52ef620431988b1a4639aad98ebc86f672acc5eb22ff22eab"
[metadata.files]
attrs = [
@@ -1691,16 +1678,16 @@ flake8-black = [
{file = "flake8_black-0.3.3-py3-none-any.whl", hash = "sha256:7d667d0059fd1aa468de1669d77cc934b7f1feeac258d57bdae69a8e73c4cd90"},
]
flake8-bugbear = [
- {file = "flake8-bugbear-22.10.25.tar.gz", hash = "sha256:89e51284eb929fbb7f23fbd428491e7427f7cdc8b45a77248daffe86a039d696"},
- {file = "flake8_bugbear-22.10.25-py3-none-any.whl", hash = "sha256:584631b608dc0d7d3f9201046d5840a45502da4732d5e8df6c7ac1694a91cb9e"},
+ {file = "flake8-bugbear-22.10.27.tar.gz", hash = "sha256:a6708608965c9e0de5fff13904fed82e0ba21ac929fe4896459226a797e11cd5"},
+ {file = "flake8_bugbear-22.10.27-py3-none-any.whl", hash = "sha256:6ad0ab754507319060695e2f2be80e6d8977cfcea082293089a9226276bd825d"},
]
flake8-builtins = [
- {file = "flake8-builtins-2.0.0.tar.gz", hash = "sha256:98833fa16139a75cd4913003492a9bd9a61c6f8ac146c3db12a2ebaf420dade3"},
- {file = "flake8_builtins-2.0.0-py3-none-any.whl", hash = "sha256:39bfa3badb5e8d22f92baf4e0ea1b816707245233846932d6b13e81fc6f673e8"},
+ {file = "flake8-builtins-2.0.1.tar.gz", hash = "sha256:5aeb420130efe8acbdaf8708a582492413293a3ca25653518f687937879650a5"},
+ {file = "flake8_builtins-2.0.1-py3-none-any.whl", hash = "sha256:a5b9ca9cbc921c4455ea02e2e9963c990ac66d028c15b654625e012a1e3bbb4d"},
]
flake8-comprehensions = [
- {file = "flake8-comprehensions-3.10.0.tar.gz", hash = "sha256:181158f7e7aa26a63a0a38e6017cef28c6adee71278ce56ce11f6ec9c4905058"},
- {file = "flake8_comprehensions-3.10.0-py3-none-any.whl", hash = "sha256:dad454fd3d525039121e98fa1dd90c46bc138708196a4ebbc949ad3c859adedb"},
+ {file = "flake8-comprehensions-3.10.1.tar.gz", hash = "sha256:412052ac4a947f36b891143430fef4859705af11b2572fbb689f90d372cf26ab"},
+ {file = "flake8_comprehensions-3.10.1-py3-none-any.whl", hash = "sha256:d763de3c74bc18a79c039a7ec732e0a1985b0c79309ceb51e56401ad0a2cd44e"},
]
flake8-debugger = [
{file = "flake8-debugger-4.1.2.tar.gz", hash = "sha256:52b002560941e36d9bf806fca2523dc7fb8560a295d5f1a6e15ac2ded7a73840"},
@@ -1858,8 +1845,8 @@ mkdocs-git-revision-date-plugin = [
{file = "mkdocs_git_revision_date_plugin-0.3.2-py3-none-any.whl", hash = "sha256:2e67956cb01823dd2418e2833f3623dee8604cdf223bddd005fe36226a56f6ef"},
]
mkdocs-material = [
- {file = "mkdocs_material-8.5.7-py3-none-any.whl", hash = "sha256:07fc70dfa325a8019b99a124751c43e4c1c2a739ed1b0b82c00f823f31c9a1e2"},
- {file = "mkdocs_material-8.5.7.tar.gz", hash = "sha256:ff4c7851b2e5f9a6cfa0a8b247e973ebae753b9836a53bd68742827541ab73e5"},
+ {file = "mkdocs_material-8.5.9-py3-none-any.whl", hash = "sha256:143ea55843b3747b640e1110824d91e8a4c670352380e166e64959f9abe98862"},
+ {file = "mkdocs_material-8.5.9.tar.gz", hash = "sha256:45eeabb23d2caba8fa3b85c91d9ec8e8b22add716e9bba8faf16d56af8aa5622"},
]
mkdocs-material-extensions = [
{file = "mkdocs_material_extensions-1.1-py3-none-any.whl", hash = "sha256:bcc2e5fc70c0ec50e59703ee6e639d87c7e664c0c441c014ea84461a90f1e902"},
@@ -1892,48 +1879,48 @@ mypy = [
{file = "mypy-0.982.tar.gz", hash = "sha256:85f7a343542dc8b1ed0a888cdd34dca56462654ef23aa673907305b260b3d746"},
]
mypy-boto3-appconfig = [
- {file = "mypy-boto3-appconfig-1.25.0.tar.gz", hash = "sha256:a4674c2c616d67a4a5b6cd722a32e24bdd05149c195e66a986657d500dc821c8"},
- {file = "mypy_boto3_appconfig-1.25.0-py3-none-any.whl", hash = "sha256:2137d81caa379db120ded09cbd9263da7e504f83696fa24e3cbb58c10471e1ee"},
+ {file = "mypy-boto3-appconfig-1.26.0.post1.tar.gz", hash = "sha256:eb1473f3e3f6b78f1a1a747a1373dd2e574db016a6b9bc5259f721c3482f2e56"},
+ {file = "mypy_boto3_appconfig-1.26.0.post1-py3-none-any.whl", hash = "sha256:749daf4ed2494a899ccfee1e9f564985fb163ede9c82778847276604a6783c15"},
]
mypy-boto3-appconfigdata = [
- {file = "mypy-boto3-appconfigdata-1.25.0.tar.gz", hash = "sha256:1ce2bb2bace41a3c8641547b55276a61360b53d38e7572f98cd838657baabee2"},
- {file = "mypy_boto3_appconfigdata-1.25.0-py3-none-any.whl", hash = "sha256:21a332c85080ce2c5416b751f4fc4870e057af85d1aedc33516bde2a86330caa"},
+ {file = "mypy-boto3-appconfigdata-1.26.0.post1.tar.gz", hash = "sha256:9f28686ba800c9c5cdbf42b4385164df2de7084758e9ab0850959c7c0a058935"},
+ {file = "mypy_boto3_appconfigdata-1.26.0.post1-py3-none-any.whl", hash = "sha256:bacf16229bfdf0c001107cde3c1a2c8219cf708b8cc69b6cb34ad52a29518917"},
]
mypy-boto3-cloudformation = [
{file = "mypy-boto3-cloudformation-1.26.0.post1.tar.gz", hash = "sha256:9e8dce3149c5f5dee5ab05850ec9cae0925abb9da3aa63397b098219709db077"},
{file = "mypy_boto3_cloudformation-1.26.0.post1-py3-none-any.whl", hash = "sha256:e0dd01030209b77c3159a299a04a5c6353a6feb0dd49bff9f5acec9e0274264c"},
]
mypy-boto3-cloudwatch = [
- {file = "mypy-boto3-cloudwatch-1.25.0.tar.gz", hash = "sha256:d5323ffeafe5144a232e27242c5d2f334f5e7ff10d0733145328888783ffcf12"},
- {file = "mypy_boto3_cloudwatch-1.25.0-py3-none-any.whl", hash = "sha256:e4934d92972f8ea531959593e476a5967b16aed223dc3c076e7e123acc8a2e77"},
+ {file = "mypy-boto3-cloudwatch-1.26.0.post1.tar.gz", hash = "sha256:4798903afa6eb0b1a4a24f2c84ff64bd00810e2e59928e00cfc240cf790aaa1f"},
+ {file = "mypy_boto3_cloudwatch-1.26.0.post1-py3-none-any.whl", hash = "sha256:aeef90abeed13d6bddf7f878ec68a78903347fbd53022ad1133ef86b6a5dabc4"},
]
mypy-boto3-dynamodb = [
{file = "mypy-boto3-dynamodb-1.26.0.post1.tar.gz", hash = "sha256:731141ff962033b77603a8a02626d64eb8575a0070e865aff31fe7443e4be6e3"},
{file = "mypy_boto3_dynamodb-1.26.0.post1-py3-none-any.whl", hash = "sha256:abe06c4c819ef2faa4b2f5cea127549f4c50e83a9869be80c9e77893f682a11b"},
]
mypy-boto3-lambda = [
- {file = "mypy-boto3-lambda-1.25.0.tar.gz", hash = "sha256:441ea9b9a6aa94a70e4e69dd9c7148434e7e501decb5cd8e278f8ca878ef77d3"},
- {file = "mypy_boto3_lambda-1.25.0-py3-none-any.whl", hash = "sha256:2564695a40b962a026f6fd642544df7c76ca6ea664d76b13f400e216f09bd78c"},
+ {file = "mypy-boto3-lambda-1.26.0.post1.tar.gz", hash = "sha256:3e6dedf959aa13fc58e6721d56c93f91068c468659b3d0e22580830f3b39aebc"},
+ {file = "mypy_boto3_lambda-1.26.0.post1-py3-none-any.whl", hash = "sha256:c198f5a3749c945cb66eecaf225657d9e4ac23d825ca923827df8f771fab91c4"},
]
mypy-boto3-logs = [
- {file = "mypy-boto3-logs-1.25.0.tar.gz", hash = "sha256:21769777f8ae286d9232ec7ebde0c4074de2253f7658df43dcb6835a8aada2e3"},
- {file = "mypy_boto3_logs-1.25.0-py3-none-any.whl", hash = "sha256:fddc937f53a25bb3849e3bbe5b07d0baae504a87f19060308dacee99b76c8f7f"},
+ {file = "mypy-boto3-logs-1.26.3.tar.gz", hash = "sha256:45f6616118ef758eb899fff206f4c835b443845af5d77b16f39c19c1a257dcec"},
+ {file = "mypy_boto3_logs-1.26.3-py3-none-any.whl", hash = "sha256:676355cc513a3a38ae83ba7d69a7aa62ce45522e298d510cbbb3011045be6931"},
]
mypy-boto3-s3 = [
- {file = "mypy-boto3-s3-1.25.0.tar.gz", hash = "sha256:68977f744ce3b9c42088467ff66e33e791ca3f27b0dc55f3b550298f03ea1a6f"},
- {file = "mypy_boto3_s3-1.25.0-py3-none-any.whl", hash = "sha256:a597db46fef02232417f6e4c2bd5d4960af0b7dc330b8a5a91ad0538405d46c6"},
+ {file = "mypy-boto3-s3-1.26.0.post1.tar.gz", hash = "sha256:6d7079f8c739dc993cbedad0736299c413b297814b73795a3855a79169ecc938"},
+ {file = "mypy_boto3_s3-1.26.0.post1-py3-none-any.whl", hash = "sha256:7de2792ff0cc541b84cd46ff3a6aa2b6e5f267217f2203f27f6e4016bddc644d"},
]
mypy-boto3-secretsmanager = [
- {file = "mypy-boto3-secretsmanager-1.25.0.tar.gz", hash = "sha256:8f61d60fbe8a662f2a16a936615724d6d6e0aee7792f613e3be8a397d91de988"},
- {file = "mypy_boto3_secretsmanager-1.25.0-py3-none-any.whl", hash = "sha256:a11c40f1e89273107fdba4b6f219e728a8bfea19e7a46a0aaff9c3b3fe095211"},
+ {file = "mypy-boto3-secretsmanager-1.26.0.post1.tar.gz", hash = "sha256:20fde0ef5c1f023aa6cb6f5376e3bcd1c9d1e66f3217e61581aee079763201c6"},
+ {file = "mypy_boto3_secretsmanager-1.26.0.post1-py3-none-any.whl", hash = "sha256:4fb63d1322b5731676425590ad6109b78fd8087418ac757c746d636aa9390a89"},
]
mypy-boto3-ssm = [
- {file = "mypy-boto3-ssm-1.25.0.tar.gz", hash = "sha256:8162d6677c3a1432131ef41b85f339fcf37c106a1916dcb1c54dc0621353be3b"},
- {file = "mypy_boto3_ssm-1.25.0-py3-none-any.whl", hash = "sha256:3fc7fb8b59a57330c7546190002717e094ff57532b6f286707f57f54e9156d03"},
+ {file = "mypy-boto3-ssm-1.26.4.tar.gz", hash = "sha256:b23c65a33a0ad40276f25d19d28091dc415cf7f4cdf8b4550f10399928815f1e"},
+ {file = "mypy_boto3_ssm-1.26.4-py3-none-any.whl", hash = "sha256:e4a5d54086501a4216dfc604cf27dc7c4e626b1ec76613be333a4bb9b40d1da1"},
]
mypy-boto3-xray = [
- {file = "mypy-boto3-xray-1.25.0.tar.gz", hash = "sha256:db95b35c7075e610d6e0b4031e8dbdd08314cfc87f5634e14a1981f094a3a9d0"},
- {file = "mypy_boto3_xray-1.25.0-py3-none-any.whl", hash = "sha256:c5e50565bdd4de516b80fc3b3343642e83eeed6ae4779bb199f07af238677eeb"},
+ {file = "mypy-boto3-xray-1.26.9.tar.gz", hash = "sha256:1b4a66f5d4aafa1f2ba516a8d4a4584aa8c1a6f98bd92709c508152fcd36febb"},
+ {file = "mypy_boto3_xray-1.26.9-py3-none-any.whl", hash = "sha256:5128669234525bd8e5b6071b098711b4845db974d0168d3c64de9cfc70944b22"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
@@ -2064,28 +2051,24 @@ pytest = [
{file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
]
pytest-asyncio = [
- {file = "pytest-asyncio-0.20.1.tar.gz", hash = "sha256:626699de2a747611f3eeb64168b3575f70439b06c3d0206e6ceaeeb956e65519"},
- {file = "pytest_asyncio-0.20.1-py3-none-any.whl", hash = "sha256:2c85a835df33fda40fe3973b451e0c194ca11bc2c007eabff90bb3d156fc172b"},
+ {file = "pytest-asyncio-0.20.2.tar.gz", hash = "sha256:32a87a9836298a881c0ec637ebcc952cfe23a56436bdc0d09d1511941dd8a812"},
+ {file = "pytest_asyncio-0.20.2-py3-none-any.whl", hash = "sha256:07e0abf9e6e6b95894a39f688a4a875d63c2128f76c02d03d16ccbc35bcc0f8a"},
]
pytest-benchmark = [
- {file = "pytest-benchmark-3.4.1.tar.gz", hash = "sha256:40e263f912de5a81d891619032983557d62a3d85843f9a9f30b98baea0cd7b47"},
- {file = "pytest_benchmark-3.4.1-py2.py3-none-any.whl", hash = "sha256:36d2b08c4882f6f997fd3126a3d6dfd70f3249cde178ed8bbc0b73db7c20f809"},
+ {file = "pytest-benchmark-4.0.0.tar.gz", hash = "sha256:fb0785b83efe599a6a956361c0691ae1dbb5318018561af10f3e915caa0048d1"},
+ {file = "pytest_benchmark-4.0.0-py3-none-any.whl", hash = "sha256:fdb7db64e31c8b277dff9850d2a2556d8b60bcb0ea6524e36e28ffd7c87f71d6"},
]
pytest-cov = [
{file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"},
{file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"},
]
-pytest-forked = [
- {file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"},
- {file = "pytest_forked-1.4.0-py3-none-any.whl", hash = "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"},
-]
pytest-mock = [
{file = "pytest-mock-3.10.0.tar.gz", hash = "sha256:fbbdb085ef7c252a326fd8cdcac0aa3b1333d8811f131bdcc701002e1be7ed4f"},
{file = "pytest_mock-3.10.0-py3-none-any.whl", hash = "sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b"},
]
pytest-xdist = [
- {file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"},
- {file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"},
+ {file = "pytest-xdist-3.0.2.tar.gz", hash = "sha256:688da9b814370e891ba5de650c9327d1a9d861721a524eb917e620eec3e90291"},
+ {file = "pytest_xdist-3.0.2-py3-none-any.whl", hash = "sha256:9feb9a18e1790696ea23e1434fa73b325ed4998b0e9fcb221f16fd1945e6df1b"},
]
python-dateutil = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
@@ -2258,8 +2241,8 @@ typeguard = [
{file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"},
]
types-requests = [
- {file = "types-requests-2.28.11.2.tar.gz", hash = "sha256:fdcd7bd148139fb8eef72cf4a41ac7273872cad9e6ada14b11ff5dfdeee60ed3"},
- {file = "types_requests-2.28.11.2-py3-none-any.whl", hash = "sha256:14941f8023a80b16441b3b46caffcbfce5265fd14555844d6029697824b5a2ef"},
+ {file = "types-requests-2.28.11.4.tar.gz", hash = "sha256:d4f342b0df432262e9e326d17638eeae96a5881e78e7a6aae46d33870d73952e"},
+ {file = "types_requests-2.28.11.4-py3-none-any.whl", hash = "sha256:bdb1f9811e53d0642c8347b09137363eb25e1a516819e190da187c29595a1df3"},
]
types-urllib3 = [
{file = "types-urllib3-1.26.25.1.tar.gz", hash = "sha256:a948584944b2412c9a74b9cf64f6c48caf8652cb88b38361316f6d15d8a184cd"},
diff --git a/pyproject.toml b/pyproject.toml
index 57a330be504..77881ba1027 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "aws_lambda_powertools"
-version = "2.2.0"
+version = "2.3.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"]
@@ -31,8 +31,8 @@ coverage = {extras = ["toml"], version = "^6.2"}
pytest = "^7.0.1"
black = "^22.8"
boto3 = "^1.18"
-flake8-builtins = "^2.0.0"
-flake8-comprehensions = "^3.7.0"
+flake8-builtins = "^2.0.1"
+flake8-comprehensions = "^3.10.1"
flake8-debugger = "^4.0.0"
flake8-fixme = "^1.1.1"
flake8-variables-names = "^0.0.5"
@@ -41,37 +41,37 @@ isort = "^5.10.1"
pytest-cov = "^4.0.0"
pytest-mock = "^3.5.1"
pdoc3 = "^0.10.0"
-pytest-asyncio = "^0.20.1"
+pytest-asyncio = "^0.20.2"
bandit = "^1.7.1"
radon = "^5.1.0"
xenon = "^0.9.0"
flake8-eradicate = "^1.2.1"
-flake8-bugbear = "^22.9.23"
+flake8-bugbear = "^22.10.27"
mkdocs-git-revision-date-plugin = "^0.3.2"
mike = "^1.1.2"
retry = "^0.9.2"
-pytest-xdist = "^2.5.0"
+pytest-xdist = "^3.0.2"
aws-cdk-lib = "^2.50.0"
"aws-cdk.aws-apigatewayv2-alpha" = "^2.38.1-alpha.0"
"aws-cdk.aws-apigatewayv2-integrations-alpha" = "^2.38.1-alpha.0"
-pytest-benchmark = "^3.4.1"
+pytest-benchmark = "^4.0.0"
python-snappy = "^0.6.1"
-mypy-boto3-appconfig = "^1.24.29"
+mypy-boto3-appconfig = "^1.26.0"
mypy-boto3-cloudformation = "^1.26.0"
-mypy-boto3-cloudwatch = "^1.24.35"
+mypy-boto3-cloudwatch = "^1.26.0"
mypy-boto3-dynamodb = "^1.26.0"
-mypy-boto3-lambda = "^1.24.0"
-mypy-boto3-logs = "^1.24.0"
-mypy-boto3-secretsmanager = "^1.24.11"
-mypy-boto3-ssm = "^1.24.0"
-mypy-boto3-s3 = "^1.24.0"
-mypy-boto3-xray = "^1.24.0"
+mypy-boto3-lambda = "^1.26.0"
+mypy-boto3-logs = "^1.26.3"
+mypy-boto3-secretsmanager = "^1.26.0"
+mypy-boto3-ssm = "^1.26.4"
+mypy-boto3-s3 = "^1.26.0"
+mypy-boto3-xray = "^1.26.9"
types-requests = "^2.28.11"
typing-extensions = "^4.4.0"
-mkdocs-material = "^8.5.4"
+mkdocs-material = "^8.5.9"
filelock = "^3.8.0"
checksumdir = "^1.2.0"
-mypy-boto3-appconfigdata = "^1.24.36"
+mypy-boto3-appconfigdata = "^1.26.0"
importlib-metadata = "^4.13"
[tool.poetry.extras]
diff --git a/tests/events/apiGatewayProxyEvent.json b/tests/events/apiGatewayProxyEvent.json
index 8bc72b7ce78..11833d21f2c 100644
--- a/tests/events/apiGatewayProxyEvent.json
+++ b/tests/events/apiGatewayProxyEvent.json
@@ -76,5 +76,5 @@
"pathParameters": null,
"stageVariables": null,
"body": "Hello from Lambda!",
- "isBase64Encoded": true
-}
\ No newline at end of file
+ "isBase64Encoded": false
+}
diff --git a/tests/events/apiGatewayProxyEventAnotherPath.json b/tests/events/apiGatewayProxyEventAnotherPath.json
new file mode 100644
index 00000000000..d8f43e46266
--- /dev/null
+++ b/tests/events/apiGatewayProxyEventAnotherPath.json
@@ -0,0 +1,80 @@
+{
+ "version": "1.0",
+ "resource": "/my/anotherPath",
+ "path": "/my/anotherPath",
+ "httpMethod": "GET",
+ "headers": {
+ "Header1": "value1",
+ "Header2": "value2"
+ },
+ "multiValueHeaders": {
+ "Header1": [
+ "value1"
+ ],
+ "Header2": [
+ "value1",
+ "value2"
+ ]
+ },
+ "queryStringParameters": {
+ "parameter1": "value1",
+ "parameter2": "value"
+ },
+ "multiValueQueryStringParameters": {
+ "parameter1": [
+ "value1",
+ "value2"
+ ],
+ "parameter2": [
+ "value"
+ ]
+ },
+ "requestContext": {
+ "accountId": "123456789012",
+ "apiId": "id",
+ "authorizer": {
+ "claims": null,
+ "scopes": null
+ },
+ "domainName": "id.execute-api.us-east-1.amazonaws.com",
+ "domainPrefix": "id",
+ "extendedRequestId": "request-id",
+ "httpMethod": "GET",
+ "identity": {
+ "accessKey": null,
+ "accountId": null,
+ "caller": null,
+ "cognitoAuthenticationProvider": null,
+ "cognitoAuthenticationType": null,
+ "cognitoIdentityId": null,
+ "cognitoIdentityPoolId": null,
+ "principalOrgId": null,
+ "sourceIp": "192.168.0.1/32",
+ "user": null,
+ "userAgent": "user-agent",
+ "userArn": null,
+ "clientCert": {
+ "clientCertPem": "CERT_CONTENT",
+ "subjectDN": "www.example.com",
+ "issuerDN": "Example issuer",
+ "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1",
+ "validity": {
+ "notBefore": "May 28 12:30:02 2019 GMT",
+ "notAfter": "Aug 5 09:36:04 2021 GMT"
+ }
+ }
+ },
+ "path": "/my/anotherPath",
+ "protocol": "HTTP/1.1",
+ "requestId": "id=",
+ "requestTime": "04/Mar/2020:19:15:17 +0000",
+ "requestTimeEpoch": 1583349317135,
+ "resourceId": null,
+ "resourcePath": "/my/anotherPath",
+ "stage": "$default"
+ },
+ "pathParameters": null,
+ "stageVariables": null,
+ "body": "Hello from Lambda!",
+ "isBase64Encoded": true
+}
\ No newline at end of file
diff --git a/tests/events/kinesisStreamCloudWatchLogsEvent.json b/tests/events/kinesisStreamCloudWatchLogsEvent.json
new file mode 100644
index 00000000000..a9a6959f907
--- /dev/null
+++ b/tests/events/kinesisStreamCloudWatchLogsEvent.json
@@ -0,0 +1,36 @@
+{
+ "Records": [
+ {
+ "kinesis": {
+ "kinesisSchemaVersion": "1.0",
+ "partitionKey": "da10bf66b1f54bff5d96eae99149ad1f",
+ "sequenceNumber": "49635052289529725553291405521504870233219489715332317186",
+ "data": "H4sIAAAAAAAAAK2Sa2vbMBSG/4ox+xg3Oror39IlvaztVmJv7WjCUGwl8+ZLZstts5L/vuOsZYUyWGEgJHiP9J7nvOghLF3b2rVLthsXjsLJOBl/uZjG8fh4Gg7C+q5yDcqUAWcSONHEoFzU6+Om7jZYGdq7dljYcpnZ4cZHwLWOJl1Zbs/r9cR6e9RVqc/rKlpXV9eXt+fy27vt8W+L2DfOlr07oXQIMAQyvHlzPk6mcbKgciktF5lQfMU5dZZqzrShLF2uFC60aLtlmzb5prc/ygvvmjYc3YRPFG+LusuurE+/Ikqb1Gd55dq8jV+8isT6+317Rk42J5PTcLFnm966yvd2D2GeISJTYIwCJSQ1BE9OtWZCABWaKMIJAMdDMyU5MYZLhmkxBhQxfY4Re1tiWiAlBsgIVQTE4Cl6tI+T8SwJZu5Hh1dPs1FApOMSDI9WVKmIC+4irTMWQZYpx7QkztrgE06MU4yCx9DmVbgbvABmQJTGtkYAB0NwEwyYQUBpqEFuSbkGrThTRKi/AlP+HHj6fvJa3P9Ap/+Rbja9/PD6POd+0jXW7xM1B8CDsp37w7woXBb8qQDZ6xeurJttEOc/HWpUBxeHKNr74LHwsXXYlsm9flrl/rmFIQeS7m3m1fVs/DlIGpu6nhMiyWQGXNKIMbcCIgkhElKbaZnZpYJUz33s1iV+z/6+StMlR3yphHNcCyxiNEXf2zed6xuEu8XuF2wb6krnAwAA",
+ "approximateArrivalTimestamp": 1668093033.744
+ },
+ "eventSource": "aws:kinesis",
+ "eventVersion": "1.0",
+ "eventID": "shardId-000000000000:49635052289529725553291405521504870233219489715332317186",
+ "eventName": "aws:kinesis:record",
+ "invokeIdentityArn": "arn:aws:iam::231436140809:role/pt-1488-CloudWatchKinesisLogsFunctionRole-1M4G2TIWIE49",
+ "awsRegion": "eu-west-1",
+ "eventSourceARN": "arn:aws:kinesis:eu-west-1:231436140809:stream/pt-1488-KinesisStreamCloudWatchLogs-D8tHs0im0aJG"
+ },
+ {
+ "kinesis": {
+ "kinesisSchemaVersion": "1.0",
+ "partitionKey": "cf4c4c2c9a49bdfaf58d7dbbc2b06081",
+ "sequenceNumber": "49635052289529725553291405520881064510298312199003701250",
+ "data": "H4sIAAAAAAAAAK2SW2/TQBCF/4pl8ViTvc7u5i0laVraQhUbWtREaG1PgsGXYK/bhqr/nXVoBRIgUYnXc2bPfHO092GFXWc3mOy2GI7D6SSZfDyfxfFkPgsPwua2xtbLjFPBgQqiifFy2WzmbdNvvTOyt92otFWa29HWRVRoHU37qtqdNZupdfaorzNXNHW0qS+vLm7O4PPr3fxHROxatNWQThgbUTqiZHT94mySzOJkBUqYLOWY8ZQLbaTRkEvDciUYzWzKfETXp13WFtsh/qgoHbZdOL4OnyhelU2fX1qXffIoXdKcFjV2RRf/9iqSmy933Sk53h5PT8LVnm12g7Ub4u7DIveIXFFjFNGUKUlAaMY0EUJKLjkQbxhKGCWeknMKoAGUkYoJ7TFd4St2tvJtDRYxDAg3VB08Ve/j42SySIIFfu396Ek+DkS+xkwAiYhM00isgUV6jXmEMrM5EmMsh+C9v9hfMQ4eS1vW4cPBH4CZVpoTJkEIAp5RUMo8vGFae3JNCCdUccMVgPw7sP4VePZm+lzc/0AH/0i3mF28fX6fSzftW+v2jZKXRgVVt3SHRVliHvx06F4+x6ppd0FcfEMvMR2cH3rR3gWPxrsO/Vau9vqyvlpMPgRJazMcYGgEHHLKBhLGJaBA0JLxNc0JppoS9Cwxbir/B4d5QDBAQSnfFFGp8aa/vxw2uLbHYUH4sHr4Dj5RJxfMAwAA",
+ "approximateArrivalTimestamp": 1668092612.992
+ },
+ "eventSource": "aws:kinesis",
+ "eventVersion": "1.0",
+ "eventID": "shardId-000000000000:49635052289529725553291405520881064510298312199003701250",
+ "eventName": "aws:kinesis:record",
+ "invokeIdentityArn": "arn:aws:iam::231436140809:role/pt-1488-CloudWatchKinesisLogsFunctionRole-1M4G2TIWIE49",
+ "awsRegion": "eu-west-1",
+ "eventSourceARN": "arn:aws:kinesis:eu-west-1:231436140809:stream/pt-1488-KinesisStreamCloudWatchLogs-D8tHs0im0aJG"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py
index 6b343dd1f0f..76174909b01 100644
--- a/tests/functional/event_handler/test_api_gateway.py
+++ b/tests/functional/event_handler/test_api_gateway.py
@@ -1388,6 +1388,65 @@ def get_lambda() -> Response:
assert result["body"] == json_dump(expected)
+def test_exception_handler_supports_list(json_dump):
+ # GIVEN a resolver with an exception handler defined for a multiple exceptions in a list
+ app = ApiGatewayResolver()
+ event = deepcopy(LOAD_GW_EVENT)
+
+ @app.exception_handler([ValueError, NotFoundError])
+ def multiple_error(ex: Exception):
+ raise BadRequestError("Bad request")
+
+ @app.get("/path/a")
+ def path_a() -> Response:
+ raise ValueError("foo")
+
+ @app.get("/path/b")
+ def path_b() -> Response:
+ raise NotFoundError
+
+ # WHEN calling the app generating each exception
+ for route in ["/path/a", "/path/b"]:
+ event["path"] = route
+ result = app(event, {})
+
+ # THEN call the exception handler in the same way for both exceptions
+ assert result["statusCode"] == 400
+ assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON]
+ expected = {"statusCode": 400, "message": "Bad request"}
+ assert result["body"] == json_dump(expected)
+
+
+def test_exception_handler_supports_multiple_decorators(json_dump):
+ # GIVEN a resolver with an exception handler defined with multiple decorators
+ app = ApiGatewayResolver()
+ event = deepcopy(LOAD_GW_EVENT)
+
+ @app.exception_handler(ValueError)
+ @app.exception_handler(NotFoundError)
+ def multiple_error(ex: Exception):
+ raise BadRequestError("Bad request")
+
+ @app.get("/path/a")
+ def path_a() -> Response:
+ raise ValueError("foo")
+
+ @app.get("/path/b")
+ def path_b() -> Response:
+ raise NotFoundError
+
+ # WHEN calling the app generating each exception
+ for route in ["/path/a", "/path/b"]:
+ event["path"] = route
+ result = app(event, {})
+
+ # THEN call the exception handler in the same way for both exceptions
+ assert result["statusCode"] == 400
+ assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON]
+ expected = {"statusCode": 400, "message": "Bad request"}
+ assert result["body"] == json_dump(expected)
+
+
def test_event_source_compatibility():
# GIVEN
app = APIGatewayHttpResolver()
@@ -1476,3 +1535,46 @@ def test_include_router_merges_context():
app.include_router(router)
assert app.context == router.context
+
+
+def test_nested_app_decorator():
+ # GIVEN a Http API V1 proxy type event
+ # with a function registered with two distinct routes
+ app = APIGatewayRestResolver()
+
+ @app.get("/my/path")
+ @app.get("/my/anotherPath")
+ def get_lambda() -> Response:
+ return Response(200, content_types.APPLICATION_JSON, json.dumps({"foo": "value"}))
+
+ # WHEN calling the event handler
+ result = app(LOAD_GW_EVENT, {})
+ result2 = app(load_event("apiGatewayProxyEventAnotherPath.json"), {})
+
+ # THEN process event correctly
+ # AND set the current_event type as APIGatewayProxyEvent
+ assert result["statusCode"] == 200
+ assert result2["statusCode"] == 200
+
+
+def test_nested_router_decorator():
+ # GIVEN a Http API V1 proxy type event
+ # with a function registered with two distinct routes
+ app = APIGatewayRestResolver()
+ router = Router()
+
+ @router.get("/my/path")
+ @router.get("/my/anotherPath")
+ def get_lambda() -> Response:
+ return Response(200, content_types.APPLICATION_JSON, json.dumps({"foo": "value"}))
+
+ app.include_router(router)
+
+ # WHEN calling the event handler
+ result = app(LOAD_GW_EVENT, {})
+ result2 = app(load_event("apiGatewayProxyEventAnotherPath.json"), {})
+
+ # THEN process event correctly
+ # AND set the current_event type as APIGatewayProxyEvent
+ assert result["statusCode"] == 200
+ assert result2["statusCode"] == 200
diff --git a/tests/functional/parser/test_kinesis.py b/tests/functional/parser/test_kinesis.py
index 13f1e55b479..6b23bd214a6 100644
--- a/tests/functional/parser/test_kinesis.py
+++ b/tests/functional/parser/test_kinesis.py
@@ -3,6 +3,7 @@
import pytest
from aws_lambda_powertools.utilities.parser import (
+ BaseModel,
ValidationError,
envelopes,
event_parser,
@@ -11,6 +12,13 @@
KinesisDataStreamModel,
KinesisDataStreamRecordPayload,
)
+from aws_lambda_powertools.utilities.parser.models.cloudwatch import (
+ CloudWatchLogsDecode,
+)
+from aws_lambda_powertools.utilities.parser.models.kinesis import (
+ extract_cloudwatch_logs_from_event,
+ extract_cloudwatch_logs_from_record,
+)
from aws_lambda_powertools.utilities.typing import LambdaContext
from tests.functional.parser.schemas import MyKinesisBusiness
from tests.functional.utils import load_event
@@ -111,3 +119,35 @@ def test_validate_event_does_not_conform_with_model():
event_dict: Any = {"hello": "s"}
with pytest.raises(ValidationError):
handle_kinesis(event_dict, LambdaContext())
+
+
+def test_kinesis_stream_event_cloudwatch_logs_data_extraction():
+ # GIVEN a KinesisDataStreamModel is instantiated with CloudWatch Logs compressed data
+ event_dict = load_event("kinesisStreamCloudWatchLogsEvent.json")
+ stream_data = KinesisDataStreamModel(**event_dict)
+ single_record = stream_data.Records[0]
+
+ # WHEN we try to extract CloudWatch Logs from KinesisDataStreamRecordPayload model
+ extracted_logs = extract_cloudwatch_logs_from_event(stream_data)
+ individual_logs = [extract_cloudwatch_logs_from_record(record) for record in stream_data.Records]
+ single_log = extract_cloudwatch_logs_from_record(single_record)
+
+ # THEN we should have extracted any potential logs as CloudWatchLogsDecode models
+ assert len(extracted_logs) == len(individual_logs)
+ assert isinstance(single_log, CloudWatchLogsDecode)
+
+
+def test_kinesis_stream_event_cloudwatch_logs_data_extraction_fails_with_custom_model():
+ # GIVEN a custom model replaces Kinesis Record Data bytes
+ class DummyModel(BaseModel):
+ ...
+
+ event_dict = load_event("kinesisStreamCloudWatchLogsEvent.json")
+ stream_data = KinesisDataStreamModel(**event_dict)
+
+ # WHEN decompress_zlib_record_data_as_json is used
+ # THEN ValueError should be raised
+ with pytest.raises(ValueError, match="We can only decompress bytes data"):
+ for record in stream_data.Records:
+ record.kinesis.data = DummyModel()
+ record.decompress_zlib_record_data_as_json()
diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py
index a0113b62486..916e9b61e0d 100644
--- a/tests/functional/test_data_classes.py
+++ b/tests/functional/test_data_classes.py
@@ -83,6 +83,10 @@
StreamViewType,
)
from aws_lambda_powertools.utilities.data_classes.event_source import event_source
+from aws_lambda_powertools.utilities.data_classes.kinesis_stream_event import (
+ extract_cloudwatch_logs_from_event,
+ extract_cloudwatch_logs_from_record,
+)
from aws_lambda_powertools.utilities.data_classes.s3_object_event import (
S3ObjectLambdaEvent,
)
@@ -1267,6 +1271,14 @@ def test_kinesis_stream_event_json_data():
assert record.kinesis.data_as_json() == json_value
+def test_kinesis_stream_event_cloudwatch_logs_data_extraction():
+ event = KinesisStreamEvent(load_event("kinesisStreamCloudWatchLogsEvent.json"))
+ extracted_logs = extract_cloudwatch_logs_from_event(event)
+ individual_logs = [extract_cloudwatch_logs_from_record(record) for record in event.records]
+
+ assert len(extracted_logs) == len(individual_logs)
+
+
def test_alb_event():
event = ALBEvent(load_event("albEvent.json"))
assert event.request_context.elb_target_group_arn == event["requestContext"]["elb"]["targetGroupArn"]
diff --git a/tests/functional/test_logger.py b/tests/functional/test_logger.py
index f171ba7ee5b..0e576752508 100644
--- a/tests/functional/test_logger.py
+++ b/tests/functional/test_logger.py
@@ -1,3 +1,4 @@
+import functools
import inspect
import io
import json
@@ -5,6 +6,7 @@
import random
import re
import string
+import sys
import warnings
from ast import Dict
from collections import namedtuple
@@ -892,3 +894,15 @@ def test_powertools_debug_env_var_warning(monkeypatch: pytest.MonkeyPatch):
set_package_logger_handler()
assert len(w) == 1
assert str(w[0].message) == warning_message
+
+
+def test_logger_log_uncaught_exceptions(service_name, stdout):
+ # GIVEN an initialized Logger is set with log_uncaught_exceptions
+ logger = Logger(service=service_name, stream=stdout, log_uncaught_exceptions=True)
+
+ # WHEN Python's exception hook is inspected
+ exception_hook = sys.excepthook
+
+ # THEN it should contain our custom exception hook with a copy of our logger
+ assert isinstance(exception_hook, functools.partial)
+ assert exception_hook.keywords.get("logger") == logger
diff --git a/tests/functional/test_utilities_parameters.py b/tests/functional/test_utilities_parameters.py
index c5e65c158be..f3d326fcd58 100644
--- a/tests/functional/test_utilities_parameters.py
+++ b/tests/functional/test_utilities_parameters.py
@@ -46,6 +46,11 @@ def config():
return Config(region_name="us-east-1")
+@pytest.fixture
+def mock_binary_value() -> str:
+ return "ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnpkV0lpT2lJeE1qTTBOVFkzT0Rrd0lpd2libUZ0WlNJNklrcHZhRzRnUkc5bElpd2lhV0YwSWpveE5URTJNak01TURJeWZRLlNmbEt4d1JKU01lS0tGMlFUNGZ3cE1lSmYzNlBPazZ5SlZfYWRRc3N3NWMK" # noqa: E501
+
+
def build_get_parameters_stub(params: Dict[str, Any], invalid_parameters: List[str] | None = None) -> Dict[str, List]:
invalid_parameters = invalid_parameters or []
version = random.randrange(1, 1000)
@@ -1186,6 +1191,31 @@ def test_secrets_provider_get(mock_name, mock_value, config):
stubber.deactivate()
+def test_secrets_provider_get_binary_secret(mock_name, mock_binary_value, config):
+ # GIVEN a new provider
+ provider = parameters.SecretsProvider(config=config)
+ expected_params = {"SecretId": mock_name}
+ expected_response = {
+ "ARN": f"arn:aws:secretsmanager:us-east-1:132456789012:secret/{mock_name}",
+ "Name": mock_name,
+ "VersionId": "edc66e31-3d5f-4276-aaa1-95ed44cfed72",
+ "SecretBinary": mock_binary_value,
+ "CreatedDate": datetime(2015, 1, 1),
+ }
+
+ stubber = stub.Stubber(provider.client)
+ stubber.add_response("get_secret_value", expected_response, expected_params)
+ stubber.activate()
+
+ try:
+ value = provider.get(mock_name)
+ stubber.assert_no_pending_responses()
+ finally:
+ stubber.deactivate()
+
+ assert value == mock_binary_value
+
+
def test_secrets_provider_get_with_custom_client(mock_name, mock_value, config):
"""
Test SecretsProvider.get() with a non-cached value