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