diff --git a/.github/boring-cyborg.yml b/.github/boring-cyborg.yml index 38760c85de6..325cd7c4ebc 100644 --- a/.github/boring-cyborg.yml +++ b/.github/boring-cyborg.yml @@ -1,48 +1,48 @@ ##### Labeler ########################################################################################################## labelPRBasedOnFilePath: - area/logger: + logger: - aws_lambda_powertools/logging/* - aws_lambda_powertools/logging/**/* - aws_lambda_powertools/package_logger.py - area/tracer: + tracer: - aws_lambda_powertools/tracing/* - aws_lambda_powertools/tracing/**/* - area/metrics: + metrics: - aws_lambda_powertools/metrics/* - aws_lambda_powertools/metrics/**/* - area/event_handlers: + event_handlers: - aws_lambda_powertools/event_handler/* - aws_lambda_powertools/event_handler/**/* - area/middleware_factory: + middleware_factory: - aws_lambda_powertools/middleware_factory/* - aws_lambda_powertools/middleware_factory/**/* - area/parameters: + parameters: - aws_lambda_powertools/parameters/* - aws_lambda_powertools/parameters/**/* - area/batch: + batch: - aws_lambda_powertools/batch/* - aws_lambda_powertools/batch/**/* - area/validator: + validator: - aws_lambda_powertools/validation/* - aws_lambda_powertools/validation/**/* - area/event_sources: + event_sources: - aws_lambda_powertools/data_classes/* - aws_lambda_powertools/data_classes/**/* - area/parser: + parser: - aws_lambda_powertools/parser/* - aws_lambda_powertools/parser/**/* - area/idempotency: + idempotency: - aws_lambda_powertools/idempotency/* - aws_lambda_powertools/idempotency/**/* - area/feature_flags: + feature_flags: - aws_lambda_powertools/feature_flags/* - aws_lambda_powertools/feature_flags/**/* - area/jmespath: + jmespath: - aws_lambda_powertools/utilities/jmespath_utils/* - area/typing: + typing: - aws_lambda_powertools/utilities/typing/* - mypy.ini - area/commons: + commons: - aws_lambda_powertools/shared/* documentation: diff --git a/.github/workflows/publish_v2_layer.yml b/.github/workflows/publish_v2_layer.yml index 653dc4178b2..6d91d908ac0 100644 --- a/.github/workflows/publish_v2_layer.yml +++ b/.github/workflows/publish_v2_layer.yml @@ -59,7 +59,7 @@ jobs: poetry export --format requirements.txt --output requirements.txt pip install -r requirements.txt - name: Set up QEMU - uses: docker/setup-qemu-action@8b122486cedac8393e77aa9734c3528886e4a1a8 # v2.0.0 + uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18 # v2.0.0 with: platforms: arm64 # NOTE: we need QEMU to build Layer against a different architecture (e.g., ARM) diff --git a/.github/workflows/rebuild_latest_docs.yml b/.github/workflows/rebuild_latest_docs.yml index 1e8333d4540..aa3b2216289 100644 --- a/.github/workflows/rebuild_latest_docs.yml +++ b/.github/workflows/rebuild_latest_docs.yml @@ -4,14 +4,14 @@ name: Rebuild latest docs # === Documentation hotfix === # # 1. Trigger "Rebuild latest docs" workflow manually: https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow -# 2. Use the latest version released under Releases e.g. v1.22.0 +# 2. Use the latest version released under Releases e.g. 2.0.0 on: workflow_dispatch: inputs: latest_published_version: - description: "Latest PyPi published version to rebuild latest docs for, e.g. v2.0.0" - default: "v2.0.0" + description: "Latest PyPi published version to rebuild latest docs for, e.g. 2.0.0" + default: "2.0.0" required: true jobs: diff --git a/.github/workflows/reusable_publish_docs.yml b/.github/workflows/reusable_publish_docs.yml index a157783abf4..6023961a510 100644 --- a/.github/workflows/reusable_publish_docs.yml +++ b/.github/workflows/reusable_publish_docs.yml @@ -8,7 +8,7 @@ on: workflow_call: inputs: version: - description: "Version to build and publish docs (v1.28.0, develop)" + description: "Version to build and publish docs (1.28.0, develop)" required: true type: string alias: @@ -65,7 +65,7 @@ jobs: poetry run mike set-default --push latest # Maintenance: Migrate to new gh-pages action - name: Release API docs - uses: peaceiris/actions-gh-pages@068dc23d9710f1ba62e86896f84735d869951305 # v3.8.0 + uses: peaceiris/actions-gh-pages@de7ea6f8efb354206b205ef54722213d99067935 # v3.9.0 env: VERSION: ${{ inputs.version }} with: @@ -75,7 +75,7 @@ jobs: destination_dir: ${{ env.VERSION }}/api - name: Release API docs to latest if: ${{ inputs.alias == 'latest' }} - uses: peaceiris/actions-gh-pages@068dc23d9710f1ba62e86896f84735d869951305 # v3.8.0 + uses: peaceiris/actions-gh-pages@de7ea6f8efb354206b205ef54722213d99067935 # v3.9.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./api diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ae140f037a..6a77b1f6bda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,45 @@ ## Bug Fixes +* **ci:** linting issues after flake8-blackbear,mypy upgrades +* **deps:** update build system to poetry-core ([#1651](https://github.com/awslabs/aws-lambda-powertools-python/issues/1651)) +* **idempotency:** idempotent_function should support standalone falsy values ([#1669](https://github.com/awslabs/aws-lambda-powertools-python/issues/1669)) +* **logger:** fix unknown attributes being ignored by mypy ([#1670](https://github.com/awslabs/aws-lambda-powertools-python/issues/1670)) + +## Documentation + +* **community:** fix social handlers for Ran ([#1654](https://github.com/awslabs/aws-lambda-powertools-python/issues/1654)) +* **community:** fix twitch parent domain for embedded video +* **homepage:** remove 3.6 and add hero image +* **homepage:** add Pulumi code example ([#1652](https://github.com/awslabs/aws-lambda-powertools-python/issues/1652)) +* **index:** fold support us banner +* **index:** add quotes to pip for zsh customers +* **install:** address early v2 feedback on installation and project support +* **we-made-this:** new community content section ([#1650](https://github.com/awslabs/aws-lambda-powertools-python/issues/1650)) + +## Features + +* **layers:** add layer balancer script ([#1643](https://github.com/awslabs/aws-lambda-powertools-python/issues/1643)) +* **logger:** add use_rfc3339 and auto-complete formatter opts in Logger ([#1662](https://github.com/awslabs/aws-lambda-powertools-python/issues/1662)) +* **logger:** accept arbitrary keyword=value for ephemeral metadata ([#1658](https://github.com/awslabs/aws-lambda-powertools-python/issues/1658)) + +## Maintenance + +* **ci:** fix typo on version description +* **deps:** bump peaceiris/actions-gh-pages from 3.8.0 to 3.9.0 ([#1649](https://github.com/awslabs/aws-lambda-powertools-python/issues/1649)) +* **deps:** bump docker/setup-qemu-action from 2.0.0 to 2.1.0 ([#1627](https://github.com/awslabs/aws-lambda-powertools-python/issues/1627)) +* **deps-dev:** bump aws-cdk-lib from 2.47.0 to 2.48.0 ([#1664](https://github.com/awslabs/aws-lambda-powertools-python/issues/1664)) +* **deps-dev:** bump flake8-variables-names from 0.0.4 to 0.0.5 ([#1628](https://github.com/awslabs/aws-lambda-powertools-python/issues/1628)) +* **deps-dev:** bump pytest-asyncio from 0.16.0 to 0.20.1 ([#1635](https://github.com/awslabs/aws-lambda-powertools-python/issues/1635)) +* **deps-dev:** bump aws-cdk-lib from 2.48.0 to 2.49.0 ([#1671](https://github.com/awslabs/aws-lambda-powertools-python/issues/1671)) +* **docs:** remove v2 banner on top of the docs +* **governance:** remove 'area/' from PR labels + + + +## [v2.0.0] - 2022-10-24 +## Bug Fixes + * lock dependencies * mypy errors * lint files @@ -52,19 +91,20 @@ ## Maintenance -* bump pyproject version to 2.0 * update v2 layer ARN on documentation * update v2 layer ARN on documentation -* merge v2 branch * update v2 layer ARN on documentation +* update v2 layer ARN on documentation +* merge v2 branch +* bump pyproject version to 2.0 * **ci:** make release process manual * **ci:** migrate E2E tests to CDK CLI and off Docker ([#1501](https://github.com/awslabs/aws-lambda-powertools-python/issues/1501)) * **ci:** remove v1 workflows ([#1617](https://github.com/awslabs/aws-lambda-powertools-python/issues/1617)) * **core:** expose modules in the Top-level package ([#1517](https://github.com/awslabs/aws-lambda-powertools-python/issues/1517)) * **dep:** add cfn-lint as a dev dependency; pre-commit ([#1612](https://github.com/awslabs/aws-lambda-powertools-python/issues/1612)) +* **deps:** remove email-validator; use Str over EmailStr in SES model ([#1608](https://github.com/awslabs/aws-lambda-powertools-python/issues/1608)) * **deps:** bump release-drafter/release-drafter from 5.21.0 to 5.21.1 ([#1611](https://github.com/awslabs/aws-lambda-powertools-python/issues/1611)) * **deps:** lock importlib to 4.x -* **deps:** remove email-validator; use Str over EmailStr in SES model ([#1608](https://github.com/awslabs/aws-lambda-powertools-python/issues/1608)) * **deps-dev:** bump mypy-boto3-s3 from 1.24.76 to 1.24.94 ([#1622](https://github.com/awslabs/aws-lambda-powertools-python/issues/1622)) * **deps-dev:** bump aws-cdk-lib from 2.46.0 to 2.47.0 ([#1629](https://github.com/awslabs/aws-lambda-powertools-python/issues/1629)) * **layer:** bump to 1.31.1 (v39) @@ -2491,7 +2531,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/v1.31.1...HEAD +[Unreleased]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v2.0.0...HEAD +[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 [v1.31.0]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v1.30.0...v1.31.0 [v1.30.0]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v1.29.2...v1.30.0 diff --git a/README.md b/README.md index f34c0104c67..37eda06b5b4 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ [![Build](https://github.com/awslabs/aws-lambda-powertools-python/actions/workflows/python_build.yml/badge.svg)](https://github.com/awslabs/aws-lambda-powertools-python/actions/workflows/python_build.yml) [![codecov.io](https://codecov.io/github/awslabs/aws-lambda-powertools-python/branch/develop/graphs/badge.svg)](https://app.codecov.io/gh/awslabs/aws-lambda-powertools-python) -![PythonSupport](https://img.shields.io/static/v1?label=python&message=3.6%20|%203.7|%203.8|%203.9&color=blue?style=flat-square&logo=python) ![PyPI version](https://badge.fury.io/py/aws-lambda-powertools.svg) ![PyPi monthly downloads](https://img.shields.io/pypi/dm/aws-lambda-powertools) [![Join our Discord](https://dcbadge.vercel.app/api/server/B8zZKbbyET)](https://discord.gg/B8zZKbbyET) +![PythonSupport](https://img.shields.io/static/v1?label=python&message=%203.7|%203.8|%203.9&color=blue?style=flat-square&logo=python) ![PyPI version](https://badge.fury.io/py/aws-lambda-powertools.svg) ![PyPi monthly downloads](https://img.shields.io/pypi/dm/aws-lambda-powertools) [![Join our Discord](https://dcbadge.vercel.app/api/server/B8zZKbbyET)](https://discord.gg/B8zZKbbyET) -A suite of Python utilities for AWS Lambda functions to ease adopting best practices such as tracing, structured logging, custom metrics, and more. +A suite of Python utilities for AWS Lambda functions to ease adopting best practices such as tracing, structured logging, custom metrics, [and more](https://awslabs.github.io/aws-lambda-powertools-python/latest/#features). > Also available in [Java](https://github.com/awslabs/aws-lambda-powertools-java), [Typescript](https://github.com/awslabs/aws-lambda-powertools-typescript), and [.NET](https://awslabs.github.io/aws-lambda-powertools-dotnet/). @@ -13,6 +13,9 @@ A suite of Python utilities for AWS Lambda functions to ease adopting best pract > **An AWS Developer Acceleration (DevAx) initiative by Specialist Solution Architects | aws-devax-open-source@amazon.com** +![hero-image](https://user-images.githubusercontent.com/3340292/198254617-d0fdb672-86a6-4988-8a40-adf437135e0a.png) + + ## Features * **[Tracing](https://awslabs.github.io/aws-lambda-powertools-python/latest/core/tracer/)** - Decorators and utilities to trace Lambda function handlers, and both synchronous and asynchronous functions diff --git a/aws_lambda_powertools/logging/formatter.py b/aws_lambda_powertools/logging/formatter.py index 25f9c227c97..07971ba0a26 100644 --- a/aws_lambda_powertools/logging/formatter.py +++ b/aws_lambda_powertools/logging/formatter.py @@ -62,6 +62,7 @@ class LambdaPowertoolsFormatter(BasePowertoolsFormatter): default_time_format = "%Y-%m-%d %H:%M:%S,%F%z" # '2021-04-17 18:19:57,656+0200' custom_ms_time_directive = "%F" + RFC3339_ISO8601_FORMAT = "%Y-%m-%dT%H:%M:%S.%F%z" # '2022-10-27T16:27:43.738+02:00' def __init__( self, @@ -72,6 +73,7 @@ def __init__( use_datetime_directive: bool = False, log_record_order: Optional[List[str]] = None, utc: bool = False, + use_rfc3339: bool = False, **kwargs, ): """Return a LambdaPowertoolsFormatter instance. @@ -106,6 +108,9 @@ def __init__( also supports a custom %F directive for milliseconds. utc : bool, optional set logging timestamp to UTC, by default False to continue to use local time as per stdlib + use_rfc3339: bool, optional + Whether to use a popular dateformat that complies with both RFC3339 and ISO8601. + e.g., 2022-10-27T16:27:43.738+02:00. log_record_order : list, optional set order of log keys when logging, by default ["level", "location", "message", "timestamp"] kwargs @@ -129,6 +134,7 @@ def __init__( self.log_record_order = log_record_order or ["level", "location", "message", "timestamp"] self.log_format = dict.fromkeys(self.log_record_order) # Set the insertion order for the log messages self.update_formatter = self.append_keys # alias to old method + self.use_rfc3339_iso8601 = use_rfc3339 if self.utc: self.converter = time.gmtime # type: ignore @@ -153,36 +159,51 @@ def format(self, record: logging.LogRecord) -> str: # noqa: A003 return self.serialize(log=formatted_log) def formatTime(self, record: logging.LogRecord, datefmt: Optional[str] = None) -> str: + # As of Py3.7, we can infer milliseconds directly from any datetime + # saving processing time as we can shortcircuit early + # Maintenance: In V3, we (and Java) should move to this format by default + # since we've provided enough time for those migrating from std logging + if self.use_rfc3339_iso8601: + if self.utc: + ts_as_datetime = datetime.fromtimestamp(record.created, tz=timezone.utc) + else: + ts_as_datetime = datetime.fromtimestamp(record.created).astimezone() + + return ts_as_datetime.isoformat(timespec="milliseconds") # 2022-10-27T17:42:26.841+0200 + + # converts to local/UTC TZ as struct time record_ts = self.converter(record.created) # type: ignore if datefmt is None: # pragma: no cover, it'll always be None in std logging, but mypy datefmt = self.datefmt # NOTE: Python `time.strftime` doesn't provide msec directives - # so we create a custom one (%F) and replace logging record ts + # so we create a custom one (%F) and replace logging record_ts # Reason 2 is that std logging doesn't support msec after TZ msecs = "%03d" % record.msecs - # Datetime format codes might be optionally used - # however it only makes a difference if `datefmt` is passed - # since format codes are the same except %f + # Datetime format codes is a superset of time format codes + # therefore we only honour them if explicitly asked + # by default, those migrating from std logging will use time format codes + # https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes if self.use_datetime_directive and datefmt: - # record.msecs are microseconds, divide by 1000 and we get milliseconds + # record.msecs are microseconds, divide by 1000 to get milliseconds timestamp = record.created + record.msecs / 1000 if self.utc: dt = datetime.fromtimestamp(timestamp, tz=timezone.utc) else: - # make sure local timezone is included dt = datetime.fromtimestamp(timestamp).astimezone() custom_fmt = datefmt.replace(self.custom_ms_time_directive, msecs) return dt.strftime(custom_fmt) + # Only time format codes being used elif datefmt: custom_fmt = datefmt.replace(self.custom_ms_time_directive, msecs) return time.strftime(custom_fmt, record_ts) + # Use default fmt: 2021-05-03 10:20:19,650+0200 custom_fmt = self.default_time_format.replace(self.custom_ms_time_directive, msecs) return time.strftime(custom_fmt, record_ts) diff --git a/aws_lambda_powertools/logging/logger.py b/aws_lambda_powertools/logging/logger.py index a0e24f1dcf9..b82c510036a 100644 --- a/aws_lambda_powertools/logging/logger.py +++ b/aws_lambda_powertools/logging/logger.py @@ -1,10 +1,24 @@ import functools import inspect +import io import logging import os import random import sys -from typing import IO, Any, Callable, Dict, Iterable, Optional, TypeVar, Union +import traceback +from typing import ( + IO, + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterable, + List, + Mapping, + Optional, + TypeVar, + Union, +) import jmespath @@ -84,14 +98,16 @@ class Logger(logging.Logger): # lgtm [py/missing-call-to-init] Parameters propagated to LambdaPowertoolsFormatter -------------------------------------------------- datefmt: str, optional - String directives (strftime) to format log timestamp using `time`, by default it uses RFC - 3339. + String directives (strftime) to format log timestamp using `time`, by default it uses 2021-05-03 11:47:12,494+0200. # noqa: E501 use_datetime_directive: bool, optional Interpret `datefmt` as a format string for `datetime.datetime.strftime`, rather than `time.strftime`. See https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior . This also supports a custom %F directive for milliseconds. + use_rfc3339: bool, optional + Whether to use a popular date format that complies with both RFC3339 and ISO8601. + e.g., 2022-10-27T16:27:43.738+02:00. json_serializer : Callable, optional function to serialize `obj` to a JSON formatted `str`, by default json.dumps json_deserializer : Callable, optional @@ -185,6 +201,14 @@ def __init__( stream: Optional[IO[str]] = None, logger_formatter: Optional[PowertoolsFormatter] = None, logger_handler: Optional[logging.Handler] = None, + 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, + datefmt: Optional[str] = None, + use_datetime_directive: bool = False, + log_record_order: Optional[List[str]] = None, + utc: bool = False, + use_rfc3339: bool = False, **kwargs, ): self.service = resolve_env_var_choice( @@ -203,12 +227,29 @@ def __init__( self._default_log_keys = {"service": self.service, "sampling_rate": self.sampling_rate} self._logger = self._get_logger() - self._init_logger(**kwargs) - - def __getattr__(self, name): - # Proxy attributes not found to actual logger to support backward compatibility - # https://github.com/awslabs/aws-lambda-powertools-python/issues/97 - return getattr(self._logger, name) + # NOTE: This is primarily to improve UX, so IDEs can autocomplete LambdaPowertoolsFormatter options + # previously, we masked all of them as kwargs thus limiting feature discovery + formatter_options = { + "json_serializer": json_serializer, + "json_deserializer": json_deserializer, + "json_default": json_default, + "datefmt": datefmt, + "use_datetime_directive": use_datetime_directive, + "log_record_order": log_record_order, + "utc": utc, + "use_rfc3339": use_rfc3339, + } + + self._init_logger(formatter_options=formatter_options, **kwargs) + + # Prevent __getattr__ from shielding unknown attribute errors in type checkers + # https://github.com/awslabs/aws-lambda-powertools-python/issues/1660 + if not TYPE_CHECKING: + + def __getattr__(self, name): + # Proxy attributes not found to actual logger to support backward compatibility + # https://github.com/awslabs/aws-lambda-powertools-python/issues/97 + return getattr(self._logger, name) def _get_logger(self): """Returns a Logger named {self.service}, or {self.service.filename} for child loggers""" @@ -218,7 +259,7 @@ def _get_logger(self): return logging.getLogger(logger_name) - def _init_logger(self, **kwargs): + def _init_logger(self, formatter_options: Optional[Dict] = None, **kwargs): """Configures new logger""" # Skip configuration if it's a child logger or a pre-configured logger @@ -233,7 +274,10 @@ def _init_logger(self, **kwargs): self._configure_sampling() self._logger.setLevel(self.log_level) self._logger.addHandler(self.logger_handler) - self.structure_logs(**kwargs) + self.structure_logs(formatter_options=formatter_options, **kwargs) + + # Maintenance: We can drop this upon Py3.7 EOL. It's a backport for "location" key to work + self._logger.findCaller = self.findCaller # Pytest Live Log feature duplicates log records for colored output # but we explicitly add a filter for log deduplication. @@ -359,6 +403,126 @@ def decorate(event, context, *args, **kwargs): return decorate + def info( + self, + msg: object, + *args, + exc_info=None, + stack_info: bool = False, + stacklevel: int = 2, + extra: Optional[Mapping[str, object]] = None, + **kwargs, + ): + extra = extra or {} + extra = {**extra, **kwargs} + + # Maintenance: We can drop this upon Py3.7 EOL. It's a backport for "location" key to work + if sys.version_info < (3, 8): # pragma: no cover + return self._logger.info(msg, *args, exc_info=exc_info, stack_info=stack_info, extra=extra) + return self._logger.info( + msg, *args, exc_info=exc_info, stack_info=stack_info, stacklevel=stacklevel, extra=extra + ) + + def error( + self, + msg: object, + *args, + exc_info=None, + stack_info: bool = False, + stacklevel: int = 2, + extra: Optional[Mapping[str, object]] = None, + **kwargs, + ): + extra = extra or {} + extra = {**extra, **kwargs} + + # Maintenance: We can drop this upon Py3.7 EOL. It's a backport for "location" key to work + if sys.version_info < (3, 8): # pragma: no cover + return self._logger.error(msg, *args, exc_info=exc_info, stack_info=stack_info, extra=extra) + return self._logger.error( + msg, *args, exc_info=exc_info, stack_info=stack_info, stacklevel=stacklevel, extra=extra + ) + + def exception( + self, + msg: object, + *args, + exc_info=True, + stack_info: bool = False, + stacklevel: int = 2, + extra: Optional[Mapping[str, object]] = None, + **kwargs, + ): + extra = extra or {} + extra = {**extra, **kwargs} + + # Maintenance: We can drop this upon Py3.7 EOL. It's a backport for "location" key to work + if sys.version_info < (3, 8): # pragma: no cover + return self._logger.exception(msg, *args, exc_info=exc_info, stack_info=stack_info, extra=extra) + return self._logger.exception( + msg, *args, exc_info=exc_info, stack_info=stack_info, stacklevel=stacklevel, extra=extra + ) + + def critical( + self, + msg: object, + *args, + exc_info=None, + stack_info: bool = False, + stacklevel: int = 2, + extra: Optional[Mapping[str, object]] = None, + **kwargs, + ): + extra = extra or {} + extra = {**extra, **kwargs} + + # Maintenance: We can drop this upon Py3.7 EOL. It's a backport for "location" key to work + if sys.version_info < (3, 8): # pragma: no cover + return self._logger.critical(msg, *args, exc_info=exc_info, stack_info=stack_info, extra=extra) + return self._logger.critical( + msg, *args, exc_info=exc_info, stack_info=stack_info, stacklevel=stacklevel, extra=extra + ) + + def warning( + self, + msg: object, + *args, + exc_info=None, + stack_info: bool = False, + stacklevel: int = 2, + extra: Optional[Mapping[str, object]] = None, + **kwargs, + ): + extra = extra or {} + extra = {**extra, **kwargs} + + # Maintenance: We can drop this upon Py3.7 EOL. It's a backport for "location" key to work + if sys.version_info < (3, 8): # pragma: no cover + return self._logger.warning(msg, *args, exc_info=exc_info, stack_info=stack_info, extra=extra) + return self._logger.warning( + msg, *args, exc_info=exc_info, stack_info=stack_info, stacklevel=stacklevel, extra=extra + ) + + def debug( + self, + msg: object, + *args, + exc_info=None, + stack_info: bool = False, + stacklevel: int = 2, + extra: Optional[Mapping[str, object]] = None, + **kwargs, + ): + extra = extra or {} + extra = {**extra, **kwargs} + + # Maintenance: We can drop this upon Py3.7 EOL. It's a backport for "location" key to work + if sys.version_info < (3, 8): # pragma: no cover + return self._logger.debug(msg, *args, exc_info=exc_info, stack_info=stack_info, extra=extra) + return self._logger.debug( + msg, *args, exc_info=exc_info, stack_info=stack_info, stacklevel=stacklevel, extra=extra + ) + def append_keys(self, **additional_keys): self.registered_formatter.append_keys(**additional_keys) @@ -376,11 +540,11 @@ def registered_formatter(self) -> BasePowertoolsFormatter: """Convenience property to access logger formatter""" return self.registered_handler.formatter # type: ignore - def structure_logs(self, append: bool = False, **keys): + def structure_logs(self, append: bool = False, formatter_options: Optional[Dict] = None, **keys): """Sets logging formatting to JSON. Optionally, it can append keyword arguments - to an existing logger so it is available across future log statements. + to an existing logger, so it is available across future log statements. Last keyword argument and value wins if duplicated. @@ -388,7 +552,11 @@ def structure_logs(self, append: bool = False, **keys): ---------- append : bool, optional append keys provided to logger formatter, by default False + formatter_options : dict, optional + LambdaPowertoolsFormatter options to be propagated, by default {} """ + formatter_options = formatter_options or {} + # There are 3 operational modes for this method ## 1. Register a Powertools Formatter for the first time ## 2. Append new keys to the current logger formatter; deprecated in favour of append_keys @@ -398,7 +566,7 @@ def structure_logs(self, append: bool = False, **keys): log_keys = {**self._default_log_keys, **keys} is_logger_preconfigured = getattr(self._logger, "init", False) if not is_logger_preconfigured: - formatter = self.logger_formatter or LambdaPowertoolsFormatter(**log_keys) # type: ignore + formatter = self.logger_formatter or LambdaPowertoolsFormatter(**formatter_options, **log_keys) # type: ignore # noqa: E501 self.registered_handler.setFormatter(formatter) # when using a custom Lambda Powertools Formatter @@ -462,6 +630,41 @@ def _get_caller_filename(): caller_frame = frame.f_back.f_back.f_back return caller_frame.f_globals["__name__"] + # Maintenance: We can drop this upon Py3.7 EOL. It's a backport for "location" key to work + def findCaller(self, stack_info=False, stacklevel=2): # pragma: no cover + """ + Find the stack frame of the caller so that we can note the source + file name, line number and function name. + """ + f = logging.currentframe() # noqa: VNE001 + # On some versions of IronPython, currentframe() returns None if + # IronPython isn't run with -X:Frames. + if f is None: + return "(unknown file)", 0, "(unknown function)", None + while stacklevel > 0: + next_f = f.f_back + if next_f is None: + ## We've got options here. + ## If we want to use the last (deepest) frame: + break + ## If we want to mimic the warnings module: + # return ("sys", 1, "(unknown function)", None) # noqa: E800 + ## If we want to be pedantic: # noqa: E800 + # raise ValueError("call stack is not deep enough") # noqa: E800 + f = next_f # noqa: VNE001 + if not _is_internal_frame(f): + stacklevel -= 1 + co = f.f_code + sinfo = None + if stack_info: + with io.StringIO() as sio: + sio.write("Stack (most recent call last):\n") + traceback.print_stack(f, file=sio) + sinfo = sio.getvalue() + if sinfo[-1] == "\n": + sinfo = sinfo[:-1] + return co.co_filename, f.f_lineno, co.co_name, sinfo + def set_package_logger( level: Union[str, int] = logging.DEBUG, @@ -500,3 +703,13 @@ def set_package_logger( handler = logging.StreamHandler(stream) handler.setFormatter(formatter) logger.addHandler(handler) + + +# Maintenance: We can drop this upon Py3.7 EOL. It's a backport for "location" key to work +# The following is based on warnings._is_internal_frame. It makes sure that +# frames of the import mechanism are skipped when logging at module level and +# using a stacklevel value greater than one. +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) diff --git a/aws_lambda_powertools/utilities/idempotency/idempotency.py b/aws_lambda_powertools/utilities/idempotency/idempotency.py index abd45a86be1..14fd3bfe5af 100644 --- a/aws_lambda_powertools/utilities/idempotency/idempotency.py +++ b/aws_lambda_powertools/utilities/idempotency/idempotency.py @@ -134,14 +134,14 @@ def decorate(*args, **kwargs): if os.getenv(constants.IDEMPOTENCY_DISABLED_ENV): return function(*args, **kwargs) - payload = kwargs.get(data_keyword_argument) - - if not payload: + if data_keyword_argument not in kwargs: raise RuntimeError( f"Unable to extract '{data_keyword_argument}' from keyword arguments." f" Ensure this exists in your function's signature as well as the caller used it as a keyword argument" ) + payload = kwargs.get(data_keyword_argument) + idempotency_handler = IdempotencyHandler( function=function, function_payload=payload, diff --git a/aws_lambda_powertools/utilities/parser/parser.py b/aws_lambda_powertools/utilities/parser/parser.py index 9cb0c00f73a..eeaa5612fff 100644 --- a/aws_lambda_powertools/utilities/parser/parser.py +++ b/aws_lambda_powertools/utilities/parser/parser.py @@ -1,11 +1,7 @@ import logging from typing import Any, Callable, Dict, Optional, Type, overload -from aws_lambda_powertools.utilities.parser.types import ( - EnvelopeModel, - EventParserReturnType, - Model, -) +from aws_lambda_powertools.utilities.parser.types import EventParserReturnType, Model from ...middleware_factory import lambda_handler_decorator from ..typing import LambdaContext @@ -94,7 +90,7 @@ def parse(event: Dict[str, Any], model: Type[Model]) -> Model: @overload -def parse(event: Dict[str, Any], model: Type[Model], envelope: Type[Envelope]) -> EnvelopeModel: +def parse(event: Dict[str, Any], model: Type[Model], envelope: Type[Envelope]): ... # pragma: no cover diff --git a/docs/core/logger.md b/docs/core/logger.md index f98962a0f5f..471186cba5b 100644 --- a/docs/core/logger.md +++ b/docs/core/logger.md @@ -158,7 +158,7 @@ To ease routine tasks like extracting correlation ID from popular event sources, You can append additional keys using either mechanism: * Persist new keys across all future log messages via `append_keys` method -* Add additional keys on a per log message basis via `extra` parameter +* Add additional keys on a per log message basis as a keyword=value, or via `extra` parameter #### append_keys method @@ -184,14 +184,33 @@ You can append your own keys to your existing Logger via `append_keys(**addition This example will add `order_id` if its value is not empty, and in subsequent invocations where `order_id` might not be present it'll remove it from the Logger. +#### ephemeral metadata + +You can pass an arbitrary number of keyword arguments (kwargs) to all log level's methods, e.g. `logger.info, logger.warning`. + +Two common use cases for this feature is to enrich log statements with additional metadata, or only add certain keys conditionally. + +!!! info "Any keyword argument added will not be persisted in subsequent messages." + +=== "append_keys_kwargs.py" + + ```python hl_lines="8" + --8<-- "examples/logger/src/append_keys_kwargs.py" + ``` + +=== "append_keys_kwargs_output.json" + + ```json hl_lines="7" + --8<-- "examples/logger/src/append_keys_kwargs_output.json" + ``` + #### extra parameter Extra parameter is available for all log levels' methods, as implemented in the standard logging library - e.g. `logger.info, logger.warning`. It accepts any dictionary, and all keyword arguments will be added as part of the root structure of the logs for that log statement. -???+ info - Any keyword argument added using `extra` will not be persisted for subsequent messages. +!!! info "Any keyword argument added using `extra` will not be persisted in subsequent messages." === "append_keys_extra.py" @@ -272,6 +291,30 @@ Use `logger.exception` method to log contextual information about exceptions. Lo --8<-- "examples/logger/src/logging_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`. + +You can easily change the date format using one of the following parameters: + +* **`datefmt`**. You can pass any [strftime format codes](https://strftime.org/){target="_blank"}. Use `%F` if you need milliseconds. +* **`use_rfc3339`**. This flag will use a format compliant with both RFC3339 and ISO8601: `2022-10-27T16:27:43.738+02:00` + +???+ tip "Prefer using [datetime string formats](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes){target="_blank"}?" + Use `use_datetime_directive` flag along with `datefmt` to instruct Logger to use `datetime` instead of `time.strftime`. + +=== "date_formatting.py" + + ```python hl_lines="5 8" + --8<-- "examples/logger/src/date_formatting.py" + ``` + +=== "date_formatting_output.json" + + ```json hl_lines="6 13" + --8<-- "examples/logger/src/date_formatting_output.json" + ``` + ## Advanced ### Built-in Correlation ID expressions @@ -432,24 +475,19 @@ Do this instead: #### Overriding Log records -???+ tip - Use `datefmt` for custom date formats - We honour standard [logging library string formats](https://docs.python.org/3/howto/logging.html#displaying-the-date-time-in-messages){target="_blank"}. - - Prefer using [datetime string formats](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes){target="_blank"}? Set `use_datetime_directive` at Logger constructor or at [Lambda Powertools Formatter](#lambdapowertoolsformatter). - You might want to continue to use the same date formatting style, or override `location` to display the `package.function_name:line_number` as you previously had. -Logger allows you to either change the format or suppress the following keys altogether at the initialization: `location`, `timestamp`, `level`, `xray_trace_id`. +Logger allows you to either change the format or suppress the following keys at initialization: `location`, `timestamp`, `xray_trace_id`. === "overriding_log_records.py" - ```python hl_lines="7 10" + ```python hl_lines="6 10" --8<-- "examples/logger/src/overriding_log_records.py" ``` === "overriding_log_records_output.json" - ```json hl_lines="3 5" + ```json hl_lines="4" --8<-- "examples/logger/src/overriding_log_records_output.json" ``` diff --git a/docs/core/tracer.md b/docs/core/tracer.md index 018af91797b..ac0b73c9e27 100644 --- a/docs/core/tracer.md +++ b/docs/core/tracer.md @@ -21,11 +21,9 @@ Tracer is an opinionated thin wrapper for [AWS X-Ray Python SDK](https://github. ### Install -!!! info "This is not necessary if you're installing Powertools via [Lambda Layer](../index.md#lambda-layer){target="_blank"}" +!!! info "This is not necessary if you're installing Powertools via [Lambda Layer/SAR](../index.md#lambda-layer){target="_blank"}" -Add `aws-lambda-powertools[tracer]` as a dependency in your preferred tool: _e.g._, _requirements.txt_, _pyproject.toml_. - -This will ensure you have the required dependencies before using Tracer. +Add `aws-lambda-powertools[tracer]` as a dependency in your preferred tool: _e.g._, _requirements.txt_, _pyproject.toml_. This will ensure you have the required dependencies before using Tracer. ### Permissions diff --git a/docs/index.md b/docs/index.md index f1ef5a54a7f..2eb543f998c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,30 +3,44 @@ title: Homepage description: AWS Lambda Powertools for Python --- - + -A suite of utilities for AWS Lambda functions to ease adopting best practices such as tracing, structured logging, custom metrics, idempotency, batching, and more. +A suite of utilities for AWS Lambda functions to ease adopting best practices such as tracing, structured logging, custom metrics, idempotency, batching, [**and more**](#features). -???+ note +???+ tip Powertools is also available for [Java](https://awslabs.github.io/aws-lambda-powertools-java/){target="_blank"}, [TypeScript](https://awslabs.github.io/aws-lambda-powertools-typescript/latest/){target="_blank"}, and [.NET](https://awslabs.github.io/aws-lambda-powertools-dotnet/){target="_blank"} +??? hint "Support this project by becoming a reference customer, sharing your work, or using Layers/SAR :heart:" + + You can choose to support us in three ways: + + 1) [**Become a reference customers**](https://github.com/awslabs/aws-lambda-powertools-python/issues/new?assignees=&labels=customer-reference&template=support_powertools.yml&title=%5BSupport+Lambda+Powertools%5D%3A+%3Cyour+organization+name%3E). This gives us permission to list your company in our documentation. + + 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. + + 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. + ## Install Powertools is available in the following formats: -* **Lambda Layer (x86_64)**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:11**](#){: .copyMe}:clipboard: -* **Lambda Layer (arm64)**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11**](#){: .copyMe}:clipboard: -* **PyPi**: **`pip install aws-lambda-powertools`** +* **Lambda Layer (x86_64)**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:12**](#){: .copyMe}:clipboard: +* **Lambda Layer (arm64)**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12**](#){: .copyMe}:clipboard: +* **PyPi**: **`pip install "aws-lambda-powertools"`** ???+ info "Some utilities require additional dependencies" You can stop reading if you're using Lambda Layer. - [Tracer](./core/tracer.md){target="_blank"}, [Validation](./utilities/validation.md){target="_blank"} and [Parser](./utilities/parser.md){target="_blank"} require additional dependencies. If you prefer to install all of them, use `pip install aws-lambda-powertools[all]`. + [Tracer](./core/tracer.md){target="_blank"}, [Validation](./utilities/validation.md){target="_blank"} and [Parser](./utilities/parser.md){target="_blank"} require additional dependencies. If you prefer to install all of them, use `pip install "aws-lambda-powertools[all]"`. -???+ hint "Support this project by using Lambda Layers :heart:" - Lambda Layers allow us to understand who uses this library in a non-intrusive way. This helps us justify and gain future investments for other Lambda Powertools languages. + For example: - 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. + * [Tracer](./core/tracer.md#install){target="_blank"}: **`pip install "aws-lambda-powertools[tracer]"`** + * [Validation](./utilities/validation.md#install){target="_blank"}: **`pip install "aws-lambda-powertools[validation]"`** + * [Parser](./utilities/parser.md#install){target="_blank"}: **`pip install "aws-lambda-powertools[parser]"`** + * [Tracer](./core/tracer.md#install){target="_blank"} and [Parser](./utilities/parser.md#install){target="_blank"}: **`pip install "aws-lambda-powertools[tracer,parser]"`** ### Local development @@ -34,8 +48,8 @@ Powertools relies on the AWS SDK bundled in the Lambda runtime. This helps us ac This means you need to add AWS SDK as a development dependency (not as a production dependency). -* **Pip**: `pip install aws-lambda-powertools[aws-sdk]` -* **Poetry**: `poetry add aws-lambda-powertools[aws-sdk] --dev` +* **Pip**: `pip install "aws-lambda-powertools[aws-sdk]"` +* **Poetry**: `poetry add "aws-lambda-powertools[aws-sdk]" --dev` * **Pipenv**: `pipenv install --dev "aws-lambda-powertools[aws-sdk]"` ???+ note "Local emulation" @@ -47,465 +61,538 @@ This means you need to add AWS SDK as a development dependency (not as a product You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https://docs.aws.amazon.com/lambda/latest/dg/invocation-layers.html#invocation-layers-using){target="_blank"}, or your preferred deployment framework. -??? note "Note: Expand to copy any regional Lambda Layer ARN" +??? note "Note: Click to expand and copy any regional Lambda Layer ARN" === "x86_64" - | Region | Layer ARN | - | ---------------- | --------------------------------------------------------------------------------------------------------- | - | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | - | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:11](#){: .copyMe}:clipboard: | + | Region | Layer ARN | + | ---------------- | ---------------------------------------------------------------------------------------------------------- | + | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | + | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:12](#){: .copyMe}:clipboard: | === "arm64" - | Region | Layer ARN | - | ---------------- | --------------------------------------------------------------------------------------------------------------- | - | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11](#){: .copyMe}:clipboard: | - -=== "x86_64" + | Region | Layer ARN | + | ---------------- | ---------------------------------------------------------------------------------------------------------------- | + | `af-south-1` | [arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `ap-east-1` | [arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `eu-south-1` | [arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `me-south-1` | [arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12](#){: .copyMe}:clipboard: | + +??? note "Note: Click to expand and copy code snippets for popular frameworks" - === "SAM" + === "x86_64" - ```yaml hl_lines="5" - MyLambdaFunction: - Type: AWS::Serverless::Function - Properties: - Layers: - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:11 - ``` + === "SAM" + + ```yaml hl_lines="5" + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Layers: + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:12 + ``` + + === "Serverless framework" + + ```yaml hl_lines="5" + functions: + hello: + handler: lambda_function.lambda_handler + layers: + - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:12 + ``` + + === "CDK" + + ```python hl_lines="11 16" + from aws_cdk import core, aws_lambda + + class SampleApp(core.Construct): + + def __init__(self, scope: core.Construct, id_: str, env: core.Environment) -> None: + super().__init__(scope, id_) + + 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:12" + ) + aws_lambda.Function(self, + 'sample-app-lambda', + runtime=aws_lambda.Runtime.PYTHON_3_9, + layers=[powertools_layer] + # other props... + ) + ``` + + === "Terraform" + + ```terraform hl_lines="9 38" + terraform { + required_version = "~> 1.0.5" + required_providers { + aws = "~> 3.50.0" + } + } - === "Serverless framework" + provider "aws" { + region = "{region}" + } - ```yaml hl_lines="5" - functions: - hello: - handler: lambda_function.lambda_handler - layers: - - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:11 - ``` + resource "aws_iam_role" "iam_for_lambda" { + name = "iam_for_lambda" - === "CDK" + assume_role_policy = < None: - super().__init__(scope, id_) + ```zsh + # Create a new one with the layer + ❯ amplify add function + ? Select which capability you want to add: Lambda function (serverless function) + ? Provide an AWS Lambda function name: + ? Choose the runtime that you want to use: Python + ? Do you want to configure advanced settings? Yes + ... + ? Do you want to enable Lambda layers for this function? Yes + ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:12 + ❯ amplify push -y - 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:11" - ) - aws_lambda.Function(self, - 'sample-app-lambda', - runtime=aws_lambda.Runtime.PYTHON_3_9, - layers=[powertools_layer] - # other props... - ) - ``` - === "Terraform" + # Updating an existing function and add the layer + ❯ amplify update function + ? Select the Lambda function you want to update test2 + General information + - Name: + ? Which setting do you want to update? Lambda layers configuration + ? Do you want to enable Lambda layers for this function? Yes + ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:12 + ? Do you want to edit the local lambda function now? No + ``` - ```terraform hl_lines="9 38" - terraform { - required_version = "~> 1.0.5" - required_providers { - aws = "~> 3.50.0" - } - } + === "Get the Layer .zip contents" - provider "aws" { - region = "{region}" - } + 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:12 --region {region} + ``` + + The pre-signed URL to download this Lambda Layer will be within `Location` key. + + === "arm64" + + === "SAM" + + ```yaml hl_lines="6" + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Architectures: [arm64] + Layers: + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12 + ``` + + === "Serverless framework" + + ```yaml hl_lines="6" + functions: + hello: + handler: lambda_function.lambda_handler + architecture: arm64 + layers: + - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12 + ``` + + === "CDK" + + ```python hl_lines="11 17" + from aws_cdk import core, aws_lambda + + class SampleApp(core.Construct): + + def __init__(self, scope: core.Construct, id_: str, env: core.Environment) -> None: + super().__init__(scope, id_) + + 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:12" + ) + aws_lambda.Function(self, + 'sample-app-lambda', + runtime=aws_lambda.Runtime.PYTHON_3_9, + architecture=aws_lambda.Architecture.ARM_64, + layers=[powertools_layer] + # other props... + ) + ``` + + === "Terraform" + + ```terraform hl_lines="9 37" + terraform { + required_version = "~> 1.0.5" + required_providers { + aws = "~> 3.50.0" + } + } + + provider "aws" { + region = "{region}" + } - resource "aws_iam_role" "iam_for_lambda" { - name = "iam_for_lambda" + resource "aws_iam_role" "iam_for_lambda" { + name = "iam_for_lambda" - assume_role_policy = < - ? Choose the runtime that you want to use: Python - ? Do you want to configure advanced settings? Yes - ... - ? Do you want to enable Lambda layers for this function? Yes - ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:11 - ❯ amplify push -y - - - # Updating an existing function and add the layer - ❯ amplify update function - ? Select the Lambda function you want to update test2 - General information - - Name: - ? Which setting do you want to update? Lambda layers configuration - ? Do you want to enable Lambda layers for this function? Yes - ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:11 - ? Do you want to edit the local lambda function now? No - ``` - === "Get the Layer .zip contents" + ``` + + === "Pulumi" + + ```python + import json + import pulumi + import pulumi_aws as aws + + role = aws.iam.Role("role", + assume_role_policy=json.dumps({ + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Principal": { + "Service": "lambda.amazonaws.com" + }, + "Effect": "Allow" + } + ] + }), + managed_policy_arns=[aws.iam.ManagedPolicy.AWS_LAMBDA_BASIC_EXECUTION_ROLE] + ) + + lambda_function = aws.lambda_.Function("function", + layers=[pulumi.Output.concat("arn:aws:lambda:",aws.get_region_output().name,":017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11")], + tracing_config={ + "mode": "Active" + }, + runtime=aws.lambda_.Runtime.PYTHON3D9, + handler="index.handler", + role=role.arn, + architectures=["arm64"], + code=pulumi.FileArchive("lambda_function_payload.zip") + ) + ``` + + === "Amplify" + + ```zsh + # Create a new one with the layer + ❯ amplify add function + ? Select which capability you want to add: Lambda function (serverless function) + ? Provide an AWS Lambda function name: + ? Choose the runtime that you want to use: Python + ? Do you want to configure advanced settings? Yes + ... + ? Do you want to enable Lambda layers for this function? Yes + ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12 + ❯ amplify push -y + + + # Updating an existing function and add the layer + ❯ amplify update function + ? Select the Lambda function you want to update test2 + General information + - Name: + ? Which setting do you want to update? Lambda layers configuration + ? Do you want to enable Lambda layers for this function? Yes + ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:12 + ? Do you want to edit the local lambda function now? No + ``` + + === "Get the Layer .zip contents" + 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:12 --region {region} + ``` + + The pre-signed URL to download this Lambda Layer will be within `Location` key. - Change {region} to your AWS region, e.g. `eu-west-1` +???+ warning "Warning: Limitations" - ```bash title="AWS CLI" - aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:11 --region {region} - ``` + Container Image deployment (OCI) or inline Lambda functions do not support Lambda Layers. + + Lambda Powertools Lambda Layer do not include `pydantic` library - required dependency for the `parser` utility. See [SAR](#sar) option instead. + +#### SAR + +Serverless Application Repository (SAR) App deploys a CloudFormation stack with a copy of our Lambda Layer in your AWS account and region. + +Compared with the [public Layer ARN](#lambda-layer) option, SAR allows you to choose a semantic version and deploys a Layer in your target account. + +| App | ARN | Description | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- | +| [aws-lambda-powertools-python-layer](https://serverlessrepo.aws.amazon.com/applications/eu-west-1/057560766410/aws-lambda-powertools-python-layer) | [arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer](#){: .copyMe}:clipboard: | Contains all extra dependencies (e.g: pydantic). | +| [aws-lambda-powertools-python-layer-arm64](https://serverlessrepo.aws.amazon.com/applications/eu-west-1/057560766410/aws-lambda-powertools-python-layer-arm64) | [arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer-arm64](#){: .copyMe}:clipboard: | Contains all extra dependencies (e.g: pydantic). For arm64 functions. | - The pre-signed URL to download this Lambda Layer will be within `Location` key. +??? note "Click to expand and copy SAR code snippets for popular frameworks" -=== "arm64" + You can create a shared Lambda Layers stack and make this along with other account level layers stack. === "SAM" - ```yaml hl_lines="6" + ```yaml hl_lines="5-6 12-13" + AwsLambdaPowertoolsPythonLayer: + Type: AWS::Serverless::Application + Properties: + Location: + ApplicationId: arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer + SemanticVersion: 2.0.0 # change to latest semantic version available in SAR + MyLambdaFunction: Type: AWS::Serverless::Function Properties: - Architectures: [arm64] Layers: - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11 + # fetch Layer ARN from SAR App stack output + - !GetAtt AwsLambdaPowertoolsPythonLayer.Outputs.LayerVersionArn ``` === "Serverless framework" - ```yaml hl_lines="6" - functions: - hello: - handler: lambda_function.lambda_handler - architecture: arm64 - layers: - - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11 + ```yaml hl_lines="5 8 10-11" + functions: + main: + handler: lambda_function.lambda_handler + layers: + - !GetAtt AwsLambdaPowertoolsPythonLayer.Outputs.LayerVersionArn + + resources: + Transform: AWS::Serverless-2016-10-31 + Resources:**** + AwsLambdaPowertoolsPythonLayer: + Type: AWS::Serverless::Application + Properties: + Location: + ApplicationId: arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer + # Find latest from github.com/awslabs/aws-lambda-powertools-python/releases + SemanticVersion: 2.0.0 ``` === "CDK" - ```python hl_lines="11 17" - from aws_cdk import core, aws_lambda + ```python hl_lines="14 22-23 31" + from aws_cdk import core, aws_sam as sam, aws_lambda + + POWERTOOLS_BASE_NAME = 'AWSLambdaPowertools' + # Find latest from github.com/awslabs/aws-lambda-powertools-python/releases + POWERTOOLS_VER = '2.0.0' + POWERTOOLS_ARN = 'arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer' class SampleApp(core.Construct): - def __init__(self, scope: core.Construct, id_: str, env: core.Environment) -> None: + def __init__(self, scope: core.Construct, id_: str) -> None: super().__init__(scope, id_) - 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:11" + # Launches SAR App as CloudFormation nested stack and return Lambda Layer + powertools_app = sam.CfnApplication(self, + f'{POWERTOOLS_BASE_NAME}Application', + location={ + 'applicationId': POWERTOOLS_ARN, + 'semanticVersion': POWERTOOLS_VER + }, ) + + powertools_layer_arn = powertools_app.get_att("Outputs.LayerVersionArn").to_string() + powertools_layer_version = aws_lambda.LayerVersion.from_layer_version_arn(self, f'{POWERTOOLS_BASE_NAME}', powertools_layer_arn) + aws_lambda.Function(self, 'sample-app-lambda', - runtime=aws_lambda.Runtime.PYTHON_3_9, - architecture=aws_lambda.Architecture.ARM_64, - layers=[powertools_layer] - # other props... + runtime=aws_lambda.Runtime.PYTHON_3_8, + function_name='sample-lambda', + code=aws_lambda.Code.asset('./src'), + handler='app.handler', + layers: [powertools_layer_version] ) ``` === "Terraform" - ```terraform hl_lines="9 37" + > Credits to [Dani Comnea](https://github.com/DanyC97) for providing the Terraform equivalent. + + ```terraform hl_lines="12-13 15-20 23-25 40" terraform { - required_version = "~> 1.0.5" + required_version = "~> 0.13" required_providers { aws = "~> 3.50.0" } } provider "aws" { - region = "{region}" + region = "us-east-1" } - resource "aws_iam_role" "iam_for_lambda" { - name = "iam_for_lambda" + resource "aws_serverlessapplicationrepository_cloudformation_stack" "deploy_sar_stack" { + name = "aws-lambda-powertools-python-layer" - assume_role_policy = < - ? Choose the runtime that you want to use: Python - ? Do you want to configure advanced settings? Yes - ... - ? Do you want to enable Lambda layers for this function? Yes - ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11 - ❯ amplify push -y - - - # Updating an existing function and add the layer - ❯ amplify update function - ? Select the Lambda function you want to update test2 - General information - - Name: - ? Which setting do you want to update? Lambda layers configuration - ? Do you want to enable Lambda layers for this function? Yes - ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11 - ? Do you want to edit the local lambda function now? No - ``` + variable "aws_powertools_version" { + type = string + default = "2.0.0" + description = "The AWS Powertools release version" + } - === "Get the Layer .zip contents" - Change {region} to your AWS region, e.g. `eu-west-1` + output "deployed_powertools_sar_version" { + value = data.aws_serverlessapplicationrepository_application.sar_app.semantic_version + } - ```bash title="AWS CLI" - aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:11 --region {region} + # Fetch Lambda Powertools Layer ARN from deployed SAR App + output "aws_lambda_powertools_layer_arn" { + value = aws_serverlessapplicationrepository_cloudformation_stack.deploy_sar_stack.outputs.LayerVersionArn + } ``` - The pre-signed URL to download this Lambda Layer will be within `Location` key. - -???+ warning "Warning: Limitations" - - Container Image deployment (OCI) or inline Lambda functions do not support Lambda Layers. - - Lambda Powertools Lambda Layer do not include `pydantic` library - required dependency for the `parser` utility. See [SAR](#sar) option instead. - -#### SAR - -Serverless Application Repository (SAR) App deploys a CloudFormation stack with a copy of our Lambda Layer in your AWS account and region. - -Despite having more steps compared to the [public Layer ARN](#lambda-layer) option, the benefit is that you can specify a semantic version you want to use. - -| App | ARN | Description | -| -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- | -| [aws-lambda-powertools-python-layer](https://serverlessrepo.aws.amazon.com/applications/eu-west-1/057560766410/aws-lambda-powertools-python-layer) | [arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer](#){: .copyMe}:clipboard: | Contains all extra dependencies (e.g: pydantic). | -| [aws-lambda-powertools-python-layer-arm64](https://serverlessrepo.aws.amazon.com/applications/eu-west-1/057560766410/aws-lambda-powertools-python-layer-arm64) | [arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer-arm64](#){: .copyMe}:clipboard: | Contains all extra dependencies (e.g: pydantic). For arm64 functions. | - -???+ tip - You can create a shared Lambda Layers stack and make this along with other account level layers stack. - -If using SAM, you can include this SAR App as part of your shared Layers stack, and lock to a specific semantic version. Once deployed, it'll be available across the account this is deployed to. - -=== "SAM" - - ```yaml hl_lines="5-6 12-13" - AwsLambdaPowertoolsPythonLayer: - Type: AWS::Serverless::Application - Properties: - Location: - ApplicationId: arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer - SemanticVersion: 2.0.0 # change to latest semantic version available in SAR - - MyLambdaFunction: - Type: AWS::Serverless::Function - Properties: - Layers: - # fetch Layer ARN from SAR App stack output - - !GetAtt AwsLambdaPowertoolsPythonLayer.Outputs.LayerVersionArn - ``` - -=== "Serverless framework" - - ```yaml hl_lines="5 8 10-11" - functions: - main: - handler: lambda_function.lambda_handler - layers: - - !GetAtt AwsLambdaPowertoolsPythonLayer.Outputs.LayerVersionArn - - resources: - Transform: AWS::Serverless-2016-10-31 - Resources:**** - AwsLambdaPowertoolsPythonLayer: - Type: AWS::Serverless::Application - Properties: - Location: - ApplicationId: arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer - # Find latest from github.com/awslabs/aws-lambda-powertools-python/releases - SemanticVersion: 2.0.0 - ``` - -=== "CDK" - - ```python hl_lines="14 22-23 31" - from aws_cdk import core, aws_sam as sam, aws_lambda - - POWERTOOLS_BASE_NAME = 'AWSLambdaPowertools' - # Find latest from github.com/awslabs/aws-lambda-powertools-python/releases - POWERTOOLS_VER = '2.0.0' - POWERTOOLS_ARN = 'arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer' - - class SampleApp(core.Construct): - - def __init__(self, scope: core.Construct, id_: str) -> None: - super().__init__(scope, id_) - - # Launches SAR App as CloudFormation nested stack and return Lambda Layer - powertools_app = sam.CfnApplication(self, - f'{POWERTOOLS_BASE_NAME}Application', - location={ - 'applicationId': POWERTOOLS_ARN, - 'semanticVersion': POWERTOOLS_VER - }, - ) - - powertools_layer_arn = powertools_app.get_att("Outputs.LayerVersionArn").to_string() - powertools_layer_version = aws_lambda.LayerVersion.from_layer_version_arn(self, f'{POWERTOOLS_BASE_NAME}', powertools_layer_arn) - - aws_lambda.Function(self, - 'sample-app-lambda', - runtime=aws_lambda.Runtime.PYTHON_3_8, - function_name='sample-lambda', - code=aws_lambda.Code.asset('./src'), - handler='app.handler', - layers: [powertools_layer_version] - ) - ``` - -=== "Terraform" - - > Credits to [Dani Comnea](https://github.com/DanyC97) for providing the Terraform equivalent. - - ```terraform hl_lines="12-13 15-20 23-25 40" - terraform { - required_version = "~> 0.13" - required_providers { - aws = "~> 3.50.0" - } - } - - provider "aws" { - region = "us-east-1" - } - - resource "aws_serverlessapplicationrepository_cloudformation_stack" "deploy_sar_stack" { - name = "aws-lambda-powertools-python-layer" - - application_id = data.aws_serverlessapplicationrepository_application.sar_app.application_id - semantic_version = data.aws_serverlessapplicationrepository_application.sar_app.semantic_version - capabilities = [ - "CAPABILITY_IAM", - "CAPABILITY_NAMED_IAM" - ] - } - - data "aws_serverlessapplicationrepository_application" "sar_app" { - application_id = "arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer" - semantic_version = var.aws_powertools_version - } - - variable "aws_powertools_version" { - type = string - default = "2.0.0" - description = "The AWS Powertools release version" - } - - output "deployed_powertools_sar_version" { - value = data.aws_serverlessapplicationrepository_application.sar_app.semantic_version - } - - # Fetch Lambda Powertools Layer ARN from deployed SAR App - output "aws_lambda_powertools_layer_arn" { - value = aws_serverlessapplicationrepository_cloudformation_stack.deploy_sar_stack.outputs.LayerVersionArn - } - ``` - ??? example "Example: Least-privileged IAM permissions to deploy Layer" > Credits to [mwarkentin](https://github.com/mwarkentin) for providing the scoped down IAM permissions. @@ -571,12 +658,14 @@ If using SAM, you can include this SAR App as part of your shared Layers stack, - Ref: "PowertoolsLayerIamRole" ``` -You can fetch available versions via SAR ListApplicationVersions API: +??? note "Click to expand and copy an AWS CLI command to list all versions available in SAR" -```bash title="AWS CLI example" -aws serverlessrepo list-application-versions \ - --application-id arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer -``` + You can fetch available versions via SAR ListApplicationVersions API: + + ```bash title="AWS CLI example" + aws serverlessrepo list-application-versions \ + --application-id arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer + ``` ## Quick getting started @@ -588,22 +677,22 @@ sam init --location https://github.com/aws-samples/cookiecutter-aws-sam-python 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. -| Utility | Description | -| -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Tracing](./core/tracer.md) | Decorators and utilities to trace Lambda function handlers, and both synchronous and asynchronous functions | -| [Logger](./core/logger.md) | Structured logging made easier, and decorator to enrich structured logging with key Lambda context details | -| [Metrics](./core/metrics.md) | Custom Metrics created asynchronously via CloudWatch Embedded Metric Format (EMF) | -| [Event handler: AppSync](./core/event_handler/appsync.md) | AppSync event handler for Lambda Direct Resolver and Amplify GraphQL Transformer function | -| [Event handler: API Gateway, ALB and Lambda Function URL](https://awslabs.github.io/aws-lambda-powertools-python/latest/core/event_handler/api_gateway/) | Amazon API Gateway REST/HTTP API and ALB event handler for Lambda functions invoked using Proxy integration, and Lambda Function URL | -| [Middleware factory](./utilities/middleware_factory.md) | Decorator factory to create your own middleware to run logic before, and after each Lambda invocation | -| [Parameters](./utilities/parameters.md) | Retrieve parameter values from AWS Systems Manager Parameter Store, AWS Secrets Manager, or Amazon DynamoDB, and cache them for a specific amount of time | -| [Batch processing](./utilities/batch.md) | Handle partial failures for AWS SQS batch processing | -| [Typing](./utilities/typing.md) | Static typing classes to speedup development in your IDE | -| [Validation](./utilities/validation.md) | JSON Schema validator for inbound events and responses | -| [Event source data classes](./utilities/data_classes.md) | Data classes describing the schema of common Lambda event triggers | -| [Parser](./utilities/parser.md) | Data parsing and deep validation using Pydantic | -| [Idempotency](./utilities/idempotency.md) | Idempotent Lambda handler | -| [Feature Flags](./utilities/feature_flags.md) | A simple rule engine to evaluate when one or multiple features should be enabled depending on the input | +| Utility | Description | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [**Tracing**](./core/tracer.md) | Decorators and utilities to trace Lambda function handlers, and both synchronous and asynchronous functions | +| [**Logger**](./core/logger.md) | Structured logging made easier, and decorator to enrich structured logging with key Lambda context details | +| [**Metrics**](./core/metrics.md) | Custom Metrics created asynchronously via CloudWatch Embedded Metric Format (EMF) | +| [**Event handler: AppSync**](./core/event_handler/appsync.md) | AppSync event handler for Lambda Direct Resolver and Amplify GraphQL Transformer function | +| [**Event handler: API Gateway, ALB and Lambda Function URL**](https://awslabs.github.io/aws-lambda-powertools-python/latest/core/event_handler/api_gateway/) | Amazon API Gateway REST/HTTP API and ALB event handler for Lambda functions invoked using Proxy integration, and Lambda Function URL | +| [**Middleware factory**](./utilities/middleware_factory.md) | Decorator factory to create your own middleware to run logic before, and after each Lambda invocation | +| [**Parameters**](./utilities/parameters.md) | Retrieve parameter values from AWS Systems Manager Parameter Store, AWS Secrets Manager, or Amazon DynamoDB, and cache them for a specific amount of time | +| [**Batch processing**](./utilities/batch.md) | Handle partial failures for AWS SQS batch processing | +| [**Typing**](./utilities/typing.md) | Static typing classes to speedup development in your IDE | +| [**Validation**](./utilities/validation.md) | JSON Schema validator for inbound events and responses | +| [**Event source data classes**](./utilities/data_classes.md) | Data classes describing the schema of common Lambda event triggers | +| [**Parser**](./utilities/parser.md) | Data parsing and deep validation using Pydantic | +| [**Idempotency**](./utilities/idempotency.md) | Idempotent Lambda handler | +| [**Feature Flags**](./utilities/feature_flags.md) | A simple rule engine to evaluate when one or multiple features should be enabled depending on the input | ## Environment variables diff --git a/docs/overrides/main.html b/docs/overrides/main.html index e7908bbfa32..0af326afb24 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,10 +1,5 @@ {% extends "base.html" %} -{% block announce %} - 👋 Powertools for Python v2 is coming soon! - We encourage you to add your feedback and follow the progress on the RFC. -{% endblock %} - {% block outdated %} You're not viewing the latest version. diff --git a/docs/upgrade.md b/docs/upgrade.md index 391bc084d7f..e16acded572 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -22,10 +22,12 @@ We've made minimal breaking changes to make your transition to v2 as smooth as p ### First Steps +!!! note "All dependencies are optional now. [Tracer](core/tracer.md#install){target="_blank"}, [Validation](./utilities/validation.md#install){target="_blank"}, and [Parser](./utilities/parser.md){target="_blank"} now require additional dependencies." + Before you start, we suggest making a copy of your current working project or create a new branch with git. 1. **Upgrade** Python to at least v3.7 -2. **Ensure** you have the latest version via [Lambda Layer or PyPi](index.md#install){target="_blank"} +2. **Ensure** you have the latest version via [Lambda Layer or PyPi](index.md#install){target="_blank"}. 3. **Review** the following sections to confirm whether they affect your code ## Legacy SQS Batch Processor diff --git a/docs/utilities/parser.md b/docs/utilities/parser.md index fd157482c2c..4afa362f879 100644 --- a/docs/utilities/parser.md +++ b/docs/utilities/parser.md @@ -16,11 +16,9 @@ This utility provides data parsing and deep validation using [Pydantic](https:// ### Install -!!! info "This is not necessary if you're installing Powertools via [Lambda Layer](../index.md#lambda-layer){target="_blank"}" +!!! info "This is not necessary if you're installing Powertools via [Lambda Layer/SAR](../index.md#lambda-layer){target="_blank"}" -Add `aws-lambda-powertools[parser]` as a dependency in your preferred tool: _e.g._, _requirements.txt_, _pyproject.toml_. - -This will ensure you have the required dependencies before using Parser. +Add `aws-lambda-powertools[parser]` as a dependency in your preferred tool: _e.g._, _requirements.txt_, _pyproject.toml_. This will ensure you have the required dependencies before using Parser. ???+ warning This will increase the compressed package size by >10MB due to the Pydantic dependency. @@ -168,7 +166,7 @@ Parser comes with the following built-in models: | **S3Model** | Lambda Event Source payload for Amazon S3 | | **S3ObjectLambdaEvent** | Lambda Event Source payload for Amazon S3 Object Lambda | | **KinesisDataStreamModel** | Lambda Event Source payload for Amazon Kinesis Data Streams | -| **KinesisFirehoseModel** | Lambda Event Source payload for Amazon Kinesis Firehose | +| **KinesisFirehoseModel** | Lambda Event Source payload for Amazon Kinesis Firehose | | **SesModel** | Lambda Event Source payload for Amazon Simple Email Service | | **SnsModel** | Lambda Event Source payload for Amazon Simple Notification Service | | **APIGatewayProxyEventModel** | Lambda Event Source payload for Amazon API Gateway | @@ -325,7 +323,7 @@ Parser comes with the following built-in envelopes, where `Model` in the return | **SqsEnvelope** | 1. Parses data using `SqsModel`.
2. Parses records in `body` key using your model and return them in a list. | `List[Model]` | | **CloudWatchLogsEnvelope** | 1. Parses data using `CloudwatchLogsModel` which will base64 decode and decompress it.
2. Parses records in `message` key using your model and return them in a list. | `List[Model]` | | **KinesisDataStreamEnvelope** | 1. Parses data using `KinesisDataStreamModel` which will base64 decode it.
2. Parses records in in `Records` key using your model and returns them in a list. | `List[Model]` | -| **KinesisFirehoseEnvelope** | 1. Parses data using `KinesisFirehoseModel` which will base64 decode it.
2. Parses records in in `Records` key using your model and returns them in a list. | `List[Model]` | +| **KinesisFirehoseEnvelope** | 1. Parses data using `KinesisFirehoseModel` which will base64 decode it.
2. Parses records in in `Records` key using your model and returns them in a list. | `List[Model]` | | **SnsEnvelope** | 1. Parses data using `SnsModel`.
2. Parses records in `body` key using your model and return them in a list. | `List[Model]` | | **SnsSqsEnvelope** | 1. Parses data using `SqsModel`.
2. Parses SNS records in `body` key using `SnsNotificationModel`.
3. Parses data in `Message` key using your model and return them in a list. | `List[Model]` | | **ApiGatewayEnvelope** | 1. Parses data using `APIGatewayProxyEventModel`.
2. Parses `body` key using your model and returns it. | `Model` | diff --git a/docs/utilities/validation.md b/docs/utilities/validation.md index 43086c3d2d5..dcd35b9ab7a 100644 --- a/docs/utilities/validation.md +++ b/docs/utilities/validation.md @@ -18,14 +18,6 @@ This utility provides JSON Schema validation for events and responses, including ???+ tip All examples shared in this documentation are available within the [project repository](https://github.com/awslabs/aws-lambda-powertools-python/tree/develop/examples){target="_blank"}. -### Install - -!!! info "This is not necessary if you're installing Powertools via [Lambda Layer](../index.md#lambda-layer){target="_blank"}" - -Add `aws-lambda-powertools[validation]` as a dependency in your preferred tool: _e.g._, _requirements.txt_, _pyproject.toml_. - -This will ensure you have the required dependencies before using Validation. - You can validate inbound and outbound events using [`validator` decorator](#validator-decorator). You can also use the standalone `validate` function, if you want more control over the validation process such as handling a validation error. @@ -38,6 +30,12 @@ You can also use the standalone `validate` function, if you want more control ov ???+ warning Both `validator` decorator and `validate` standalone function expects your JSON Schema to be a **dictionary**, not a filename. +### Install + +!!! info "This is not necessary if you're installing Powertools via [Lambda Layer/SAR](../index.md#lambda-layer){target="_blank"}" + +Add `aws-lambda-powertools[validation]` as a dependency in your preferred tool: _e.g._, _requirements.txt_, _pyproject.toml_. This will ensure you have the required dependencies before using Validation. + ### Validator decorator **Validator** decorator is typically used to validate either inbound or functions' response. diff --git a/docs/we_made_this.md b/docs/we_made_this.md new file mode 100644 index 00000000000..ae6d516c2c8 --- /dev/null +++ b/docs/we_made_this.md @@ -0,0 +1,117 @@ +--- +title: We Made This (Community) +description: Blog posts, tutorials, and videos about Lambda Powertools created by the Powertools Community. +--- + + + +This space is dedicated to highlight our awesome community content featuring Lambda 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"}!" + +## Connect + +[![Join our Discord](https://dcbadge.vercel.app/api/server/B8zZKbbyET)](https://discord.gg/B8zZKbbyET){target="_blank"} + +Join us on [Discord](https://discord.gg/B8zZKbbyET){target="_blank"} to connect with the Powertools community 👋. Ask questions, learn from each other, contribute, hang out with key contributors, and more! + +## Blog posts + +### AWS Lambda Cookbook — Following best practices with Lambda Powertools + +> **Author: [Ran Isenberg](mailto:ran.isenberg@ranthebuilder.cloud) [:material-twitter:](https://twitter.com/IsenbergRan){target="_blank"} [:material-linkedin:](https://www.linkedin.com/in/ranisenberg/){target="_blank"}** + +A collection of articles explaining in detail how Lambda Powertools helps with a Serverless adoption strategy and its challenges. + +* [Part 1 - Logging](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-elevate-your-handler-s-code-part-1-logging){:target="_blank"} + +* [Part 2 - Observability: monitoring and tracing](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-elevate-your-handler-s-code-part-2-observability){:target="_blank"} + +* [Part 3 - Business Domain Observability](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-elevate-your-handler-s-code-part-3-business-domain-observability){:target="_blank"} + +* [Part 4 - Environment Variables](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-environment-variables){:target="_blank"} + +* [Part 5 - Input Validation](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-elevate-your-handler-s-code-part-5-input-validation){:target="_blank"} + +* [Part 6 - Configuration & Feature Flags](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-part-6-feature-flags-configuration-best-practices){:target="_blank"} + +### Making all your APIs idempotent + +> **Author: [Michael Walmsley](https://twitter.com/walmsles){target="_blank"}** :material-twitter: + +This article dives into what idempotency means for APIs, their use cases, and how to implement them. + +* [blog.walmsles.io/making-all-your-apis-idempotent](https://blog.walmsles.io/making-all-your-apis-idempotent){target="_blank"} + +### Deep dive on Lambda Powertools Idempotency feature + +> **Author: [Michael Walmsley](https://twitter.com/walmsles){target="_blank"}** :material-twitter: + +This article describes how to best calculate your idempotency token, implementation details, and how to handle idempotency in RESTful APIs. + +* [blog.walmsles.io/aws-lambda-powertools-idempotency-a-deeper-dive](https://blog.walmsles.io/aws-lambda-powertools-idempotency-a-deeper-dive){target="_blank"} + +### Developing AWS Lambda functions with AWS Lambda Powertools + +> **Author: [Stephan Huber](https://linkedin.com/in/sthuber90){target="_blank"}** :material-linkedin: + +This article walks through how to add Powertools to an existing project, covers Tracer, Logger, Metrics, and JSON Schema Validation. + +* [globaldatanet.com/tech-blog/develop-lambda-functions-with-aws-lambda-powertools](https://globaldatanet.com/tech-blog/develop-lambda-functions-with-aws-lambda-powertools){target="_blank"} + +### Speed-up event-driven projects + +> **Author: [Joris Conijn](https://www.linkedin.com/in/jorisconijn){target="_blank"}** :material-linkedin: + +This article walks through a sample AWS EventBridge cookiecutter template presented at the AWS Community Day Netherlands 2022. + +* [binx.io/2022/10/11/speedup-event-driven-projects/](https://binx.io/2022/10/11/speedup-event-driven-projects/){target="_blank"} +* [Slides](https://www.slideshare.net/JorisConijn/let-codecommit-work-for-you){target="_blank"} + +## Videos + +#### Building a resilient input handling with Parser + +> **Author: [Ran Isenberg](mailto:ran.isenberg@ranthebuilder.cloud) [:material-twitter:](https://twitter.com/IsenbergRan){target="_blank"} [:material-linkedin:](https://www.linkedin.com/in/ranisenberg/){target="_blank"}** + +When building applications with AWS Lambda it is critical to verify the data structure and validate the input due to the multiple different sources that can trigger them. In this session Ran Isenberg (CyberArk) will present one of the interesting features of AWS Lambda Powertools for python: the parser. + +In this session you will learn how to increase code quality, extensibility and testability, boost you productivity and ship rock solid apps to production. + + + +#### Talk DEV to me | Feature Flags with AWS Lambda Powertools + +> **Author: [Ran Isenberg](mailto:ran.isenberg@ranthebuilder.cloud) [:material-twitter:](https://twitter.com/IsenbergRan){target="_blank"} [:material-linkedin:](https://www.linkedin.com/in/ranisenberg/){target="_blank"}** + +A deep dive in the [Feature Flags](./utilities/feature_flags.md){target="_blank"} feature along with tips and tricks. + + + +## Workshops + +### Introduction to Lambda Powertools + +> **Author: [Michael Walmsley](https://twitter.com/walmsles){target="_blank"}** :material-twitter: + +This repo contains documentation for a live coding workshop for the AWS Programming and Tools Meetup in Melbourne. The workshop will start with the SAM Cli "Hello World" example API project. + +Throughout the labs we will introduce each of the AWS Lambda Powertools Core utilities to showcase how simple they are to use and adopt for all your projects, and how powerful they are at bringing you closer to the Well Architected Serverless Lens. + +* :material-github: [github.com/walmsles/lambda-powertools-coding-workshop](https://github.com/walmsles/lambda-powertools-coding-workshop){target="_blank"} + +**Walk-through video** + + + +## Sample projects + +### Complete Lambda Handler Cookbook + +> **Author: [Ran Isenberg](mailto:ran.isenberg@ranthebuilder.cloud) [:material-twitter:](https://twitter.com/IsenbergRan){target="_blank"} [:material-linkedin:](https://www.linkedin.com/in/ranisenberg/){target="_blank"}** + +This repository provides a working, deployable, open source based, AWS Lambda handler and [AWS CDK](https://aws.amazon.com/cdk/){target="_blank"} Python code. + +This handler embodies Serverless best practices and has all the bells and whistles for a proper production ready handler. It uses many of the AWS Lambda Powertools utilities for Python. + +:material-github: [github.com/ran-isenberg/aws-lambda-handler-cookbook](https://github.com/ran-isenberg/aws-lambda-handler-cookbook){:target="_blank"} diff --git a/examples/logger/src/append_keys_kwargs.py b/examples/logger/src/append_keys_kwargs.py new file mode 100644 index 00000000000..5885c7e2bd6 --- /dev/null +++ b/examples/logger/src/append_keys_kwargs.py @@ -0,0 +1,10 @@ +from aws_lambda_powertools import Logger +from aws_lambda_powertools.utilities.typing import LambdaContext + +logger = Logger() + + +def handler(event: dict, context: LambdaContext) -> str: + logger.info("Collecting payment", request_id="1123") + + return "hello world" diff --git a/examples/logger/src/append_keys_kwargs_output.json b/examples/logger/src/append_keys_kwargs_output.json new file mode 100644 index 00000000000..cd888e5e2af --- /dev/null +++ b/examples/logger/src/append_keys_kwargs_output.json @@ -0,0 +1,8 @@ +{ + "level": "INFO", + "location": "collect.handler:8", + "message": "Collecting payment", + "timestamp": "2022-11-26 11:47:12,494+0200", + "service": "payment", + "request_id": "1123" +} \ No newline at end of file diff --git a/examples/logger/src/date_formatting.py b/examples/logger/src/date_formatting.py new file mode 100644 index 00000000000..edca29201ec --- /dev/null +++ b/examples/logger/src/date_formatting.py @@ -0,0 +1,9 @@ +from aws_lambda_powertools import Logger + +date_format = "%m/%d/%Y %I:%M:%S %p" + +logger = Logger(service="payment", use_rfc3339=True) +logger.info("Collecting payment") + +logger_custom_format = Logger(service="loyalty", datefmt=date_format) +logger_custom_format.info("Calculating points") diff --git a/examples/logger/src/date_formatting_output.json b/examples/logger/src/date_formatting_output.json new file mode 100644 index 00000000000..2d5ba1dff88 --- /dev/null +++ b/examples/logger/src/date_formatting_output.json @@ -0,0 +1,16 @@ +[ + { + "level": "INFO", + "location": ":6", + "message": "Collecting payment", + "timestamp": "2022-10-28T14:35:03.210+02:00", + "service": "payment" + }, + { + "level": "INFO", + "location": ":9", + "message": "Calculating points", + "timestamp": "10/28/2022 02:35:03 PM", + "service": "loyalty" + } +] \ No newline at end of file diff --git a/examples/logger/src/overriding_log_records.py b/examples/logger/src/overriding_log_records.py index f32da431158..519d8ef2220 100644 --- a/examples/logger/src/overriding_log_records.py +++ b/examples/logger/src/overriding_log_records.py @@ -1,12 +1,11 @@ from aws_lambda_powertools import Logger -date_format = "%m/%d/%Y %I:%M:%S %p" location_format = "[%(funcName)s] %(module)s" # override location and timestamp format -logger = Logger(service="payment", location=location_format, datefmt=date_format) - -# suppress the location key with a None value -logger_two = Logger(service="payment", location=None) - +logger = Logger(service="payment", location=location_format) logger.info("Collecting payment") + +# suppress keys with a None value +logger_two = Logger(service="loyalty", location=None) +logger_two.info("Calculating points") diff --git a/examples/logger/src/overriding_log_records_output.json b/examples/logger/src/overriding_log_records_output.json index ba2f1dfe8d5..676f36ca298 100644 --- a/examples/logger/src/overriding_log_records_output.json +++ b/examples/logger/src/overriding_log_records_output.json @@ -1,7 +1,15 @@ -{ - "level": "INFO", - "location": "[] lambda_handler", - "message": "Collecting payment", - "timestamp": "02/09/2021 09:25:17 AM", - "service": "payment" -} +[ + { + "level": "INFO", + "location": "[] overriding_log_records", + "message": "Collecting payment", + "timestamp": "2022-10-28 14:40:43,801+0200", + "service": "payment" + }, + { + "level": "INFO", + "message": "Calculating points", + "timestamp": "2022-10-28 14:40:43,801+0200", + "service": "loyalty" + } +] \ No newline at end of file diff --git a/layer/scripts/layer-balancer/README.md b/layer/scripts/layer-balancer/README.md new file mode 100644 index 00000000000..001f2833d7e --- /dev/null +++ b/layer/scripts/layer-balancer/README.md @@ -0,0 +1,37 @@ + +# Layer balancer + +This folder contains a Go project that balances the layer version of Lambda Powertools across all regions, so +every region has the same layer version. + +Before: + +```text +arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:11 +... +arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:9 +``` + +After: + +```text +arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:11 +... +arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:11 +``` + +## What's happening under the hood? + +1. Query all regions to find the greatest version number +2. Download the latest layer from eu-central-1 +3. Use the layer contents to bump the version on each region until it matches 1 + +## Requirements + +* go >= 1.18 + +## How to use + +1. Set your AWS_PROFILE to the correct profile +2. `go run .` +3. Profit :-) diff --git a/layer/scripts/layer-balancer/go.mod b/layer/scripts/layer-balancer/go.mod new file mode 100644 index 00000000000..219d4d46736 --- /dev/null +++ b/layer/scripts/layer-balancer/go.mod @@ -0,0 +1,24 @@ +module layerbalancer + +go 1.18 + +require ( + github.com/aws/aws-sdk-go-v2 v1.16.16 + github.com/aws/aws-sdk-go-v2/config v1.17.8 + github.com/aws/aws-sdk-go-v2/service/lambda v1.24.6 + golang.org/x/sync v0.1.0 +) + +require ( + github.com/aws/aws-sdk-go-v2/credentials v1.12.21 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.24 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.11.23 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.16.19 // indirect + github.com/aws/smithy-go v1.13.3 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect +) diff --git a/layer/scripts/layer-balancer/go.sum b/layer/scripts/layer-balancer/go.sum new file mode 100644 index 00000000000..9bcb7428e79 --- /dev/null +++ b/layer/scripts/layer-balancer/go.sum @@ -0,0 +1,37 @@ +github.com/aws/aws-sdk-go-v2 v1.16.16 h1:M1fj4FE2lB4NzRb9Y0xdWsn2P0+2UHVxwKyOa4YJNjk= +github.com/aws/aws-sdk-go-v2 v1.16.16/go.mod h1:SwiyXi/1zTUZ6KIAmLK5V5ll8SiURNUYOqTerZPaF9k= +github.com/aws/aws-sdk-go-v2/config v1.17.8 h1:b9LGqNnOdg9vR4Q43tBTVWk4J6F+W774MSchvKJsqnE= +github.com/aws/aws-sdk-go-v2/config v1.17.8/go.mod h1:UkCI3kb0sCdvtjiXYiU4Zx5h07BOpgBTtkPu/49r+kA= +github.com/aws/aws-sdk-go-v2/credentials v1.12.21 h1:4tjlyCD0hRGNQivh5dN8hbP30qQhMLBE/FgQR1vHHWM= +github.com/aws/aws-sdk-go-v2/credentials v1.12.21/go.mod h1:O+4XyAt4e+oBAoIwNUYkRg3CVMscaIJdmZBOcPgJ8D8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.17 h1:r08j4sbZu/RVi+BNxkBJwPMUYY3P8mgSDuKkZ/ZN1lE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.17/go.mod h1:yIkQcCDYNsZfXpd5UX2Cy+sWA1jPgIhGTw9cOBzfVnQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 h1:s4g/wnzMf+qepSNgTvaQQHNxyMLKSawNhKCPNy++2xY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23/go.mod h1:2DFxAQ9pfIRy0imBCJv+vZ2X6RKxves6fbnEuSry6b4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 h1:/K482T5A3623WJgWT8w1yRAFK4RzGzEl7y39yhtn9eA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17/go.mod h1:pRwaTYCJemADaqCbUAxltMoHKata7hmB5PjEXeu0kfg= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.24 h1:wj5Rwc05hvUSvKuOF29IYb9QrCLjU+rHAy/x/o0DK2c= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.24/go.mod h1:jULHjqqjDlbyTa7pfM7WICATnOv+iOhjletM3N0Xbu8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17 h1:Jrd/oMh0PKQc6+BowB+pLEwLIgaQF29eYbe7E1Av9Ug= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17/go.mod h1:4nYOrY41Lrbk2170/BGkcJKBhws9Pfn8MG3aGqjjeFI= +github.com/aws/aws-sdk-go-v2/service/lambda v1.24.6 h1:N7RkXX2SJbN+TCp295J3LdMR0KRFd2Bhi5nIO+svLQY= +github.com/aws/aws-sdk-go-v2/service/lambda v1.24.6/go.mod h1:oTJIIluTaJCRT6xP1AZpuU3JwRHBC0Q5O4Hg+SUxFHw= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.23 h1:pwvCchFUEnlceKIgPUouBJwK81aCkQ8UDMORfeFtW10= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.23/go.mod h1:/w0eg9IhFGjGyyncHIQrXtU8wvNsTJOP0R6PPj0wf80= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.6 h1:OwhhKc1P9ElfWbMKPIbMMZBV6hzJlL2JKD76wNNVzgQ= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.6/go.mod h1:csZuQY65DAdFBt1oIjO5hhBR49kQqop4+lcuCjf2arA= +github.com/aws/aws-sdk-go-v2/service/sts v1.16.19 h1:9pPi0PsFNAGILFfPCk8Y0iyEBGc6lu6OQ97U7hmdesg= +github.com/aws/aws-sdk-go-v2/service/sts v1.16.19/go.mod h1:h4J3oPZQbxLhzGnk+j9dfYHi5qIOVJ5kczZd658/ydM= +github.com/aws/smithy-go v1.13.3 h1:l7LYxGuzK6/K+NzJ2mC+VvLUbae0sL3bXU//04MkmnA= +github.com/aws/smithy-go v1.13.3/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/layer/scripts/layer-balancer/main.go b/layer/scripts/layer-balancer/main.go new file mode 100644 index 00000000000..889675e5f71 --- /dev/null +++ b/layer/scripts/layer-balancer/main.go @@ -0,0 +1,292 @@ +package main + +import ( + "context" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "os/signal" + "sort" + "sync" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/lambda" + "github.com/aws/aws-sdk-go-v2/service/lambda/types" + "golang.org/x/sync/errgroup" +) + +type LayerInfo struct { + Name string + Description string + Architecture types.Architecture + + LayerContentOnce sync.Once + LayerContent []byte +} + +// canonicalLayers are the layers that we want to keep in sync across all regions +var canonicalLayers = []LayerInfo{ + { + Name: "AWSLambdaPowertoolsPythonV2", + Description: "Lambda Powertools for Python [x86_64] with extra dependencies version bump", + Architecture: types.ArchitectureX8664, + }, + { + Name: "AWSLambdaPowertoolsPythonV2-Arm64", + Description: "Lambda Powertools for Python [arm64] with extra dependencies version bump", + Architecture: types.ArchitectureArm64, + }, +} + +// regions are the regions that we want to keep in sync +var regions = []string{ + "af-south-1", + "eu-central-1", + "us-east-1", + "us-east-2", + "us-west-1", + "us-west-2", + "ap-east-1", + "ap-south-1", + "ap-northeast-1", + "ap-northeast-2", + "ap-southeast-1", + "ap-southeast-2", + "ca-central-1", + "eu-west-1", + "eu-west-2", + "eu-west-3", + "eu-south-1", + "eu-north-1", + "sa-east-1", + "ap-southeast-3", + "ap-northeast-3", + "me-south-1", +} + +// getLayerVersion returns the latest version of a layer in a region +func getLayerVersion(ctx context.Context, layerName string, region string) (int64, error) { + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + if err != nil { + return 0, err + } + + lambdaSvc := lambda.NewFromConfig(cfg) + + layerVersionsResult, err := lambdaSvc.ListLayerVersions(ctx, &lambda.ListLayerVersionsInput{ + LayerName: aws.String(layerName), + MaxItems: aws.Int32(1), + }) + if err != nil { + return 0, err + } + + if len(layerVersionsResult.LayerVersions) == 0 { + return 0, fmt.Errorf("no layer meets the search criteria %s - %s", layerName, region) + } + return layerVersionsResult.LayerVersions[0].Version, nil +} + +// getGreatestVersion returns the greatest version of a layer across all regions +func getGreatestVersion(ctx context.Context) (int64, error) { + var versions []int64 + + g, ctx := errgroup.WithContext(ctx) + + for idx := range canonicalLayers { + layer := &canonicalLayers[idx] + + for _, region := range regions { + layerName := layer.Name + ctx := ctx + region := region + + g.Go(func() error { + version, err := getLayerVersion(ctx, layerName, region) + if err != nil { + return err + } + + log.Printf("[%s] %s -> %d", layerName, region, version) + + versions = append(versions, version) + return nil + }) + } + } + + if err := g.Wait(); err != nil { + return 0, err + } + + // Find the maximum version by reverse sorting the versions array + sort.Slice(versions, func(i, j int) bool { return versions[i] > versions[j] }) + return versions[0], nil +} + +// balanceRegionToVersion creates a new layer version in a region with the same contents as the canonical layer, until it matches the maxVersion +func balanceRegionToVersion(ctx context.Context, region string, layer *LayerInfo, maxVersion int64) error { + currentLayerVersion, err := getLayerVersion(ctx, layer.Name, region) + if err != nil { + return fmt.Errorf("error getting layer version: %w", err) + } + + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + if err != nil { + return err + } + + lambdaSvc := lambda.NewFromConfig(cfg) + + for i := currentLayerVersion; i < maxVersion; i++ { + log.Printf("[%s] Bumping %s to version %d (max %d)", layer.Name, region, i, maxVersion) + + payload, err := downloadCanonicalLayerZip(ctx, layer) + if err != nil { + return fmt.Errorf("error downloading canonical zip: %w", err) + } + + layerVersionResponse, err := lambdaSvc.PublishLayerVersion(ctx, &lambda.PublishLayerVersionInput{ + Content: &types.LayerVersionContentInput{ + ZipFile: payload, + }, + LayerName: aws.String(layer.Name), + CompatibleArchitectures: []types.Architecture{layer.Architecture}, + CompatibleRuntimes: []types.Runtime{types.RuntimePython37, types.RuntimePython38, types.RuntimePython39}, + Description: aws.String(layer.Description), + LicenseInfo: aws.String("MIT-0"), + }) + if err != nil { + return fmt.Errorf("error publishing layer version: %w", err) + } + + _, err = lambdaSvc.AddLayerVersionPermission(ctx, &lambda.AddLayerVersionPermissionInput{ + Action: aws.String("lambda:GetLayerVersion"), + LayerName: aws.String(layer.Name), + Principal: aws.String("*"), + StatementId: aws.String("PublicLayerAccess"), + VersionNumber: layerVersionResponse.Version, + }) + if err != nil { + return fmt.Errorf("error making layer public: %w", err) + } + } + + return nil +} + +// balanceRegions creates new layer versions in all regions with the same contents as the canonical layer, until they match the maxVersion +func balanceRegions(ctx context.Context, maxVersion int64) error { + g, ctx := errgroup.WithContext(ctx) + + for idx := range canonicalLayers { + layer := &canonicalLayers[idx] + + for _, region := range regions { + ctx := ctx + region := region + layer := layer + version := maxVersion + + g.Go(func() error { + return balanceRegionToVersion(ctx, region, layer, version) + }) + } + } + + if err := g.Wait(); err != nil { + return err + } + + return nil +} + +// downloadCanonicalLayerZip downloads the canonical layer zip file that will be used to bump the versions later +func downloadCanonicalLayerZip(ctx context.Context, layer *LayerInfo) ([]byte, error) { + var innerErr error + + layer.LayerContentOnce.Do(func() { + // We use eu-central-1 as the canonical region to download the Layer from + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("eu-central-1")) + if err != nil { + innerErr = err + } + + lambdaSvc := lambda.NewFromConfig(cfg) + + // Gets the latest version of the layer + version, err := getLayerVersion(ctx, layer.Name, "eu-central-1") + if err != nil { + innerErr = fmt.Errorf("error getting eu-central-1 layer version: %w", err) + } + + // Gets the Layer content URL from S3 + getLayerVersionResult, err := lambdaSvc.GetLayerVersion(ctx, &lambda.GetLayerVersionInput{ + LayerName: aws.String(layer.Name), + VersionNumber: version, + }) + if err != nil { + innerErr = fmt.Errorf("error getting eu-central-1 layer download URL: %w", err) + } + + s3LayerUrl := getLayerVersionResult.Content.Location + log.Printf("[%s] Downloading Layer from %s", layer.Name, *s3LayerUrl) + + resp, err := http.Get(*s3LayerUrl) + if err != nil { + innerErr = err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + innerErr = err + } + + layer.LayerContent = body + }) + + return layer.LayerContent, innerErr +} + +func main() { + ctx := context.Background() + + // Cancel everything if interrupted + ctx, cancel := context.WithCancel(ctx) + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + defer func() { + signal.Stop(c) + cancel() + }() + go func() { + select { + case <-c: + cancel() + case <-ctx.Done(): + } + }() + + // Find the greatest layer version across all regions + greatestVersion, err := getGreatestVersion(ctx) + if err != nil { + cancel() + log.Printf("error getting layer version: %s", err) + os.Exit(1) + } + log.Printf("Greatest version is %d. Bumping all versions...", greatestVersion) + + // Elevate all regions to the greatest layer version found + err = balanceRegions(ctx, greatestVersion) + if err != nil { + cancel() + log.Printf("error balancing regions: %s", err) + os.Exit(1) + } + + log.Printf("DONE! All layers should be version %d", greatestVersion) +} diff --git a/mkdocs.yml b/mkdocs.yml index 59fcdfa6a08..65e053ae27c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,6 +11,7 @@ nav: - Roadmap: roadmap.md - API reference: api/" target="_blank - Upgrade guide: upgrade.md + - We Made This (Community): we_made_this.md - Core utilities: - core/tracer.md - core/logger.md @@ -77,7 +78,9 @@ markdown_extensions: permalink: true toc_depth: 4 - attr_list - - pymdownx.emoji + - pymdownx.emoji: + emoji_index: !!python/name:materialx.emoji.twemoji + emoji_generator: !!python/name:materialx.emoji.to_svg - pymdownx.inlinehilite - pymdownx.superfences: custom_fences: diff --git a/poetry.lock b/poetry.lock index 7898b732100..1ef3728253a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,14 +14,14 @@ tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy [[package]] name = "aws-cdk-aws-apigatewayv2-alpha" -version = "2.46.0a0" +version = "2.47.0a0" description = "The CDK Construct Library for AWS::APIGatewayv2" category = "dev" optional = false python-versions = "~=3.7" [package.dependencies] -aws-cdk-lib = ">=2.46.0,<3.0.0" +aws-cdk-lib = ">=2.47.0,<3.0.0" constructs = ">=10.0.0,<11.0.0" jsii = ">=1.69.0,<2.0.0" publication = ">=0.0.3" @@ -29,15 +29,15 @@ typeguard = ">=2.13.3,<2.14.0" [[package]] name = "aws-cdk-aws-apigatewayv2-integrations-alpha" -version = "2.46.0a0" +version = "2.47.0a0" description = "Integrations for AWS APIGateway V2" category = "dev" optional = false python-versions = "~=3.7" [package.dependencies] -"aws-cdk.aws-apigatewayv2-alpha" = "2.46.0.a0" -aws-cdk-lib = ">=2.46.0,<3.0.0" +"aws-cdk.aws-apigatewayv2-alpha" = "2.47.0.a0" +aws-cdk-lib = ">=2.47.0,<3.0.0" constructs = ">=10.0.0,<11.0.0" jsii = ">=1.69.0,<2.0.0" publication = ">=0.0.3" @@ -45,7 +45,7 @@ typeguard = ">=2.13.3,<2.14.0" [[package]] name = "aws-cdk-lib" -version = "2.47.0" +version = "2.49.0" description = "Version 2 of the AWS Cloud Development Kit library" category = "dev" optional = false @@ -53,7 +53,7 @@ python-versions = "~=3.7" [package.dependencies] constructs = ">=10.0.0,<11.0.0" -jsii = ">=1.69.0,<2.0.0" +jsii = ">=1.70.0,<2.0.0" publication = ">=0.0.3" typeguard = ">=2.13.3,<2.14.0" @@ -128,14 +128,14 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "boto3" -version = "1.24.94" +version = "1.25.0" description = "The AWS SDK for Python" category = "main" optional = false python-versions = ">= 3.7" [package.dependencies] -botocore = ">=1.27.94,<1.28.0" +botocore = ">=1.28.0,<1.29.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.6.0,<0.7.0" @@ -144,7 +144,7 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.27.94" +version = "1.28.0" description = "Low-level, data-driven core of boto 3." category = "main" optional = false @@ -230,15 +230,15 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "constructs" -version = "10.1.134" +version = "10.1.139" description = "A programming model for software-defined state" category = "dev" optional = false @@ -353,18 +353,18 @@ tomli = "*" [[package]] name = "flake8-bugbear" -version = "22.9.23" +version = "22.10.25" 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 -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] attrs = ">=19.2.0" flake8 = ">=3.0.0" [package.extras] -dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit"] +dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "tox"] [[package]] name = "flake8-builtins" @@ -426,31 +426,13 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "flake8-isort" -version = "4.2.0" -description = "flake8 plugin that integrates isort ." -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -flake8 = ">=3.2.1,<6" -isort = ">=4.3.5,<6" - -[package.extras] -test = ["pytest-cov"] - [[package]] name = "flake8-variables-names" -version = "0.0.4" +version = "0.0.5" description = "A flake8 extension that helps to make more readable variables names" category = "dev" optional = false -python-versions = "*" - -[package.dependencies] -setuptools = "*" +python-versions = ">=3.7" [[package]] name = "future" @@ -788,7 +770,7 @@ mkdocs = ">=0.17" [[package]] name = "mkdocs-material" -version = "8.5.6" +version = "8.5.7" description = "Documentation that simply works" category = "dev" optional = false @@ -805,19 +787,19 @@ requests = ">=2.26" [[package]] name = "mkdocs-material-extensions" -version = "1.0.3" -description = "Extension pack for Python Markdown." +version = "1.1" +description = "Extension pack for Python Markdown and MkDocs Material." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "mypy" -version = "0.971" +version = "0.982" description = "Optional static typing for Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] mypy-extensions = ">=0.4.3" @@ -832,8 +814,8 @@ reports = ["lxml"] [[package]] name = "mypy-boto3-appconfig" -version = "1.24.36.post1" -description = "Type annotations for boto3.AppConfig 1.24.36 service generated with mypy-boto3-builder 7.10.0" +version = "1.25.0" +description = "Type annotations for boto3.AppConfig 1.25.0 service generated with mypy-boto3-builder 7.11.10" category = "dev" optional = false python-versions = ">=3.7" @@ -843,8 +825,8 @@ typing-extensions = ">=4.1.0" [[package]] name = "mypy-boto3-appconfigdata" -version = "1.24.36.post1" -description = "Type annotations for boto3.AppConfigData 1.24.36 service generated with mypy-boto3-builder 7.10.0" +version = "1.25.0" +description = "Type annotations for boto3.AppConfigData 1.25.0 service generated with mypy-boto3-builder 7.11.10" category = "dev" optional = false python-versions = ">=3.7" @@ -854,8 +836,8 @@ typing-extensions = ">=4.1.0" [[package]] name = "mypy-boto3-cloudformation" -version = "1.24.36.post1" -description = "Type annotations for boto3.CloudFormation 1.24.36 service generated with mypy-boto3-builder 7.10.0" +version = "1.25.0" +description = "Type annotations for boto3.CloudFormation 1.25.0 service generated with mypy-boto3-builder 7.11.10" category = "dev" optional = false python-versions = ">=3.7" @@ -865,8 +847,8 @@ typing-extensions = ">=4.1.0" [[package]] name = "mypy-boto3-cloudwatch" -version = "1.24.55" -description = "Type annotations for boto3.CloudWatch 1.24.55 service generated with mypy-boto3-builder 7.11.6" +version = "1.25.0" +description = "Type annotations for boto3.CloudWatch 1.25.0 service generated with mypy-boto3-builder 7.11.10" category = "dev" optional = false python-versions = ">=3.7" @@ -876,8 +858,8 @@ typing-extensions = ">=4.1.0" [[package]] name = "mypy-boto3-dynamodb" -version = "1.24.74" -description = "Type annotations for boto3.DynamoDB 1.24.74 service generated with mypy-boto3-builder 7.11.8" +version = "1.25.0" +description = "Type annotations for boto3.DynamoDB 1.25.0 service generated with mypy-boto3-builder 7.11.10" category = "dev" optional = false python-versions = ">=3.7" @@ -887,8 +869,8 @@ typing-extensions = ">=4.1.0" [[package]] name = "mypy-boto3-lambda" -version = "1.24.54" -description = "Type annotations for boto3.Lambda 1.24.54 service generated with mypy-boto3-builder 7.11.6" +version = "1.25.0" +description = "Type annotations for boto3.Lambda 1.25.0 service generated with mypy-boto3-builder 7.11.10" category = "dev" optional = false python-versions = ">=3.7" @@ -898,8 +880,8 @@ typing-extensions = ">=4.1.0" [[package]] name = "mypy-boto3-logs" -version = "1.24.36.post1" -description = "Type annotations for boto3.CloudWatchLogs 1.24.36 service generated with mypy-boto3-builder 7.10.0" +version = "1.25.0" +description = "Type annotations for boto3.CloudWatchLogs 1.25.0 service generated with mypy-boto3-builder 7.11.10" category = "dev" optional = false python-versions = ">=3.7" @@ -909,8 +891,8 @@ typing-extensions = ">=4.1.0" [[package]] name = "mypy-boto3-s3" -version = "1.24.94" -description = "Type annotations for boto3.S3 1.24.94 service generated with mypy-boto3-builder 7.11.10" +version = "1.25.0" +description = "Type annotations for boto3.S3 1.25.0 service generated with mypy-boto3-builder 7.11.10" category = "dev" optional = false python-versions = ">=3.7" @@ -920,8 +902,8 @@ typing-extensions = ">=4.1.0" [[package]] name = "mypy-boto3-secretsmanager" -version = "1.24.83" -description = "Type annotations for boto3.SecretsManager 1.24.83 service generated with mypy-boto3-builder 7.11.10" +version = "1.25.0" +description = "Type annotations for boto3.SecretsManager 1.25.0 service generated with mypy-boto3-builder 7.11.10" category = "dev" optional = false python-versions = ">=3.7" @@ -931,8 +913,8 @@ typing-extensions = ">=4.1.0" [[package]] name = "mypy-boto3-ssm" -version = "1.24.90" -description = "Type annotations for boto3.SSM 1.24.90 service generated with mypy-boto3-builder 7.11.10" +version = "1.25.0" +description = "Type annotations for boto3.SSM 1.25.0 service generated with mypy-boto3-builder 7.11.10" category = "dev" optional = false python-versions = ">=3.7" @@ -942,8 +924,8 @@ typing-extensions = ">=4.1.0" [[package]] name = "mypy-boto3-xray" -version = "1.24.36.post1" -description = "Type annotations for boto3.XRay 1.24.36 service generated with mypy-boto3-builder 7.10.0" +version = "1.25.0" +description = "Type annotations for boto3.XRay 1.25.0 service generated with mypy-boto3-builder 7.11.10" category = "dev" optional = false python-versions = ">=3.7" @@ -1108,7 +1090,7 @@ plugins = ["importlib-metadata"] [[package]] name = "pymdown-extensions" -version = "9.6" +version = "9.7" description = "Extension pack for Python Markdown." category = "dev" optional = false @@ -1138,7 +1120,7 @@ python-versions = ">=3.7" [[package]] name = "pytest" -version = "7.1.3" +version = "7.2.0" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -1147,29 +1129,30 @@ python-versions = ">=3.7" [package.dependencies] attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.16.0" -description = "Pytest support for asyncio." +version = "0.20.1" +description = "Pytest support for asyncio" category = "dev" optional = false -python-versions = ">= 3.6" +python-versions = ">=3.7" [package.dependencies] -pytest = ">=5.4.0" +pytest = ">=6.1.0" +typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} [package.extras] -testing = ["coverage", "hypothesis (>=5.7.1)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] [[package]] name = "pytest-benchmark" @@ -1508,7 +1491,7 @@ requests = ">=2.0,<3.0" [[package]] name = "zipp" -version = "3.9.0" +version = "3.10.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false @@ -1528,7 +1511,7 @@ validation = ["fastjsonschema"] [metadata] lock-version = "1.1" python-versions = "^3.7.4" -content-hash = "6f3c081f72eee753bb06a79c43338312fb965c9afbe470675c6d87c27c2cda23" +content-hash = "48a6c11b4ef71716e88efa7ffa474aa73fd7fcb02553ffd49c0d03fe72c1f838" [metadata.files] attrs = [ @@ -1536,16 +1519,16 @@ attrs = [ {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, ] aws-cdk-aws-apigatewayv2-alpha = [ - {file = "aws-cdk.aws-apigatewayv2-alpha-2.46.0a0.tar.gz", hash = "sha256:10d9324da26db7aeee3a45853a2e249b6b85866fcc8f8f43fa1a0544ce582482"}, - {file = "aws_cdk.aws_apigatewayv2_alpha-2.46.0a0-py3-none-any.whl", hash = "sha256:2cdeac84fb1fe219e5686ee95d9528a1810e9d426b2bb7f305ea07cb43e328a8"}, + {file = "aws-cdk.aws-apigatewayv2-alpha-2.47.0a0.tar.gz", hash = "sha256:b1e32e046bd5ae224c8a962850215ac98e9639c453337fafeffb7a5618d66063"}, + {file = "aws_cdk.aws_apigatewayv2_alpha-2.47.0a0-py3-none-any.whl", hash = "sha256:f2d41b944d7781b9565135e832a416c54e2c1e52f31fefdc7b8b323142814033"}, ] aws-cdk-aws-apigatewayv2-integrations-alpha = [ - {file = "aws-cdk.aws-apigatewayv2-integrations-alpha-2.46.0a0.tar.gz", hash = "sha256:91a792c94500987b69fd97cb00afec5ace00f2039ffebebd99f91ee6b47c3c8b"}, - {file = "aws_cdk.aws_apigatewayv2_integrations_alpha-2.46.0a0-py3-none-any.whl", hash = "sha256:c7bbe1c08019cee41c14b6c1513f673d60b337422ef338c67f9a0cb3e17cc963"}, + {file = "aws-cdk.aws-apigatewayv2-integrations-alpha-2.47.0a0.tar.gz", hash = "sha256:c86f0291c8cac2b8db0391bf52f626e541a82a3c7cbb4a03cd8d5014882862cc"}, + {file = "aws_cdk.aws_apigatewayv2_integrations_alpha-2.47.0a0-py3-none-any.whl", hash = "sha256:4633c4e020400c7ee5790652f099f02d4f84bab24fff013250e2b41a4ffca1b0"}, ] aws-cdk-lib = [ - {file = "aws-cdk-lib-2.47.0.tar.gz", hash = "sha256:c9b1626c7afc42ceaee547558efe88439dd1447b18c7837db153e03fb8186167"}, - {file = "aws_cdk_lib-2.47.0-py3-none-any.whl", hash = "sha256:1f1a2e2309625d304a14b388ee0fedb0dffc15eef14dac65e33b10f6c73bf75c"}, + {file = "aws-cdk-lib-2.49.0.tar.gz", hash = "sha256:9146f03988aff823cb5b561090875c311d733bf1786859c706381e9b49f6adb7"}, + {file = "aws_cdk_lib-2.49.0-py3-none-any.whl", hash = "sha256:c9c197463e2685aa67248c2cea2a4d3f15e33a3c19250aa34e754e455295417b"}, ] aws-sam-translator = [ {file = "aws-sam-translator-1.53.0.tar.gz", hash = "sha256:392ed4f5fb08f72cb68a8800f0bc278d2a3b6609bd1ac66bfcdeaaa94cdc18e5"}, @@ -1584,12 +1567,12 @@ black = [ {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, ] boto3 = [ - {file = "boto3-1.24.94-py3-none-any.whl", hash = "sha256:f13db0beb3c9fe2cc1ed0f031189f144610d2909b5874a616e77b0bd1ae3b686"}, - {file = "boto3-1.24.94.tar.gz", hash = "sha256:f4842b395d1580454756622069f4ca0408993885ecede967001d2c101201cdfa"}, + {file = "boto3-1.25.0-py3-none-any.whl", hash = "sha256:81139cc9da154a1672c7dd92da1678cae0ea1601a3e1f0394c6cd010eab1acb6"}, + {file = "boto3-1.25.0.tar.gz", hash = "sha256:170eab4a87592741933b6f8a02c3a6a8664162ef33bb12a2c2b4d431490d9ac2"}, ] botocore = [ - {file = "botocore-1.27.94-py3-none-any.whl", hash = "sha256:8237c070d2ab29fac4fbcfe9dd2e84e0ee147402e0fed3ac1629f92459c7f1d2"}, - {file = "botocore-1.27.94.tar.gz", hash = "sha256:572224608a0b7662966fc303b768e2eba61bf53bdbf314481cd9e63a0d8e1a66"}, + {file = "botocore-1.28.0-py3-none-any.whl", hash = "sha256:5bc426647da9f7739b73b1ffb5fce37fb3691c3c66dd772bf541dc19f8da2f43"}, + {file = "botocore-1.28.0.tar.gz", hash = "sha256:75a4082543e2c1b005ccde90af87d0969003db06c3fcbe8a7854ddaa8d68fafb"}, ] cattrs = [ {file = "cattrs-22.2.0-py3-none-any.whl", hash = "sha256:bc12b1f0d000b9f9bee83335887d532a1d3e99a833d1bf0882151c97d3e68c21"}, @@ -1616,12 +1599,12 @@ click = [ {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] constructs = [ - {file = "constructs-10.1.134-py3-none-any.whl", hash = "sha256:b3f05ad138af83473cc9bd5f8949558bd31d38fb32c09fcc56d0de9057c2e61d"}, - {file = "constructs-10.1.134.tar.gz", hash = "sha256:4ab253a74e62a2c918456d20dff42ec0abb2e4393a6bab0218c81c09e19c1a41"}, + {file = "constructs-10.1.139-py3-none-any.whl", hash = "sha256:e809549fc4f1cad8ee57eb5a8a31d163f6321374c024a91e04bbb1f5520d53c7"}, + {file = "constructs-10.1.139.tar.gz", hash = "sha256:df319296e2efe699662323dc2b1dcb154439457aaaa036b8c7409975bfc5b43a"}, ] coverage = [ {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, @@ -1708,8 +1691,8 @@ flake8-black = [ {file = "flake8_black-0.3.3-py3-none-any.whl", hash = "sha256:7d667d0059fd1aa468de1669d77cc934b7f1feeac258d57bdae69a8e73c4cd90"}, ] flake8-bugbear = [ - {file = "flake8-bugbear-22.9.23.tar.gz", hash = "sha256:17b9623325e6e0dcdcc80ed9e4aa811287fcc81d7e03313b8736ea5733759937"}, - {file = "flake8_bugbear-22.9.23-py3-none-any.whl", hash = "sha256:cd2779b2b7ada212d7a322814a1e5651f1868ab0d3f24cc9da66169ab8fda474"}, + {file = "flake8-bugbear-22.10.25.tar.gz", hash = "sha256:89e51284eb929fbb7f23fbd428491e7427f7cdc8b45a77248daffe86a039d696"}, + {file = "flake8_bugbear-22.10.25-py3-none-any.whl", hash = "sha256:584631b608dc0d7d3f9201046d5840a45502da4732d5e8df6c7ac1694a91cb9e"}, ] flake8-builtins = [ {file = "flake8-builtins-2.0.0.tar.gz", hash = "sha256:98833fa16139a75cd4913003492a9bd9a61c6f8ac146c3db12a2ebaf420dade3"}, @@ -1731,12 +1714,9 @@ flake8-fixme = [ {file = "flake8-fixme-1.1.1.tar.gz", hash = "sha256:50cade07d27a4c30d4f12351478df87339e67640c83041b664724bda6d16f33a"}, {file = "flake8_fixme-1.1.1-py2.py3-none-any.whl", hash = "sha256:226a6f2ef916730899f29ac140bed5d4a17e5aba79f00a0e3ae1eff1997cb1ac"}, ] -flake8-isort = [ - {file = "flake8-isort-4.2.0.tar.gz", hash = "sha256:26571500cd54976bbc0cf1006ffbcd1a68dd102f816b7a1051b219616ba9fee0"}, - {file = "flake8_isort-4.2.0-py3-none-any.whl", hash = "sha256:5b87630fb3719bf4c1833fd11e0d9534f43efdeba524863e15d8f14a7ef6adbf"}, -] flake8-variables-names = [ - {file = "flake8_variables_names-0.0.4.tar.gz", hash = "sha256:d6fa0571a807c72940b5773827c5760421ea6f8206595ff0a8ecfa01e42bf2cf"}, + {file = "flake8_variables_names-0.0.5-py3-none-any.whl", hash = "sha256:e3277031696bbe10b5132b49938cde1d70fcae9561533b7bd7ab8e69cb27addb"}, + {file = "flake8_variables_names-0.0.5.tar.gz", hash = "sha256:30133e14ee2300e13a60393a00f74d98110c76070ac67d1ab91606f02824a7e1"}, ] future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, @@ -1878,81 +1858,82 @@ 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.6-py3-none-any.whl", hash = "sha256:b473162c800321b9760453f301a91f7cb40a120a85a9d0464e1e484e74b76bb2"}, - {file = "mkdocs_material-8.5.6.tar.gz", hash = "sha256:38a21d817265d0c203ab3dad64996e45859c983f72180f6937bd5540a4eb84e4"}, + {file = "mkdocs_material-8.5.7-py3-none-any.whl", hash = "sha256:07fc70dfa325a8019b99a124751c43e4c1c2a739ed1b0b82c00f823f31c9a1e2"}, + {file = "mkdocs_material-8.5.7.tar.gz", hash = "sha256:ff4c7851b2e5f9a6cfa0a8b247e973ebae753b9836a53bd68742827541ab73e5"}, ] mkdocs-material-extensions = [ - {file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"}, - {file = "mkdocs_material_extensions-1.0.3-py3-none-any.whl", hash = "sha256:a82b70e533ce060b2a5d9eb2bc2e1be201cf61f901f93704b4acf6e3d5983a44"}, + {file = "mkdocs_material_extensions-1.1-py3-none-any.whl", hash = "sha256:bcc2e5fc70c0ec50e59703ee6e639d87c7e664c0c441c014ea84461a90f1e902"}, + {file = "mkdocs_material_extensions-1.1.tar.gz", hash = "sha256:96ca979dae66d65c2099eefe189b49d5ac62f76afb59c38e069ffc7cf3c131ec"}, ] mypy = [ - {file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"}, - {file = "mypy-0.971-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5"}, - {file = "mypy-0.971-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3"}, - {file = "mypy-0.971-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655"}, - {file = "mypy-0.971-cp310-cp310-win_amd64.whl", hash = "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103"}, - {file = "mypy-0.971-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca"}, - {file = "mypy-0.971-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417"}, - {file = "mypy-0.971-cp36-cp36m-win_amd64.whl", hash = "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09"}, - {file = "mypy-0.971-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8"}, - {file = "mypy-0.971-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0"}, - {file = "mypy-0.971-cp37-cp37m-win_amd64.whl", hash = "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2"}, - {file = "mypy-0.971-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27"}, - {file = "mypy-0.971-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856"}, - {file = "mypy-0.971-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71"}, - {file = "mypy-0.971-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27"}, - {file = "mypy-0.971-cp38-cp38-win_amd64.whl", hash = "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58"}, - {file = "mypy-0.971-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6"}, - {file = "mypy-0.971-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe"}, - {file = "mypy-0.971-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9"}, - {file = "mypy-0.971-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf"}, - {file = "mypy-0.971-cp39-cp39-win_amd64.whl", hash = "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0"}, - {file = "mypy-0.971-py3-none-any.whl", hash = "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9"}, - {file = "mypy-0.971.tar.gz", hash = "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56"}, + {file = "mypy-0.982-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5085e6f442003fa915aeb0a46d4da58128da69325d8213b4b35cc7054090aed5"}, + {file = "mypy-0.982-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:41fd1cf9bc0e1c19b9af13a6580ccb66c381a5ee2cf63ee5ebab747a4badeba3"}, + {file = "mypy-0.982-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f793e3dd95e166b66d50e7b63e69e58e88643d80a3dcc3bcd81368e0478b089c"}, + {file = "mypy-0.982-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86ebe67adf4d021b28c3f547da6aa2cce660b57f0432617af2cca932d4d378a6"}, + {file = "mypy-0.982-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:175f292f649a3af7082fe36620369ffc4661a71005aa9f8297ea473df5772046"}, + {file = "mypy-0.982-cp310-cp310-win_amd64.whl", hash = "sha256:8ee8c2472e96beb1045e9081de8e92f295b89ac10c4109afdf3a23ad6e644f3e"}, + {file = "mypy-0.982-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58f27ebafe726a8e5ccb58d896451dd9a662a511a3188ff6a8a6a919142ecc20"}, + {file = "mypy-0.982-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6af646bd46f10d53834a8e8983e130e47d8ab2d4b7a97363e35b24e1d588947"}, + {file = "mypy-0.982-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7aeaa763c7ab86d5b66ff27f68493d672e44c8099af636d433a7f3fa5596d40"}, + {file = "mypy-0.982-cp37-cp37m-win_amd64.whl", hash = "sha256:724d36be56444f569c20a629d1d4ee0cb0ad666078d59bb84f8f887952511ca1"}, + {file = "mypy-0.982-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14d53cdd4cf93765aa747a7399f0961a365bcddf7855d9cef6306fa41de01c24"}, + {file = "mypy-0.982-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:26ae64555d480ad4b32a267d10cab7aec92ff44de35a7cd95b2b7cb8e64ebe3e"}, + {file = "mypy-0.982-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6389af3e204975d6658de4fb8ac16f58c14e1bacc6142fee86d1b5b26aa52bda"}, + {file = "mypy-0.982-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b35ce03a289480d6544aac85fa3674f493f323d80ea7226410ed065cd46f206"}, + {file = "mypy-0.982-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c6e564f035d25c99fd2b863e13049744d96bd1947e3d3d2f16f5828864506763"}, + {file = "mypy-0.982-cp38-cp38-win_amd64.whl", hash = "sha256:cebca7fd333f90b61b3ef7f217ff75ce2e287482206ef4a8b18f32b49927b1a2"}, + {file = "mypy-0.982-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a705a93670c8b74769496280d2fe6cd59961506c64f329bb179970ff1d24f9f8"}, + {file = "mypy-0.982-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75838c649290d83a2b83a88288c1eb60fe7a05b36d46cbea9d22efc790002146"}, + {file = "mypy-0.982-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:91781eff1f3f2607519c8b0e8518aad8498af1419e8442d5d0afb108059881fc"}, + {file = "mypy-0.982-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaa97b9ddd1dd9901a22a879491dbb951b5dec75c3b90032e2baa7336777363b"}, + {file = "mypy-0.982-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a692a8e7d07abe5f4b2dd32d731812a0175626a90a223d4b58f10f458747dd8a"}, + {file = "mypy-0.982-cp39-cp39-win_amd64.whl", hash = "sha256:eb7a068e503be3543c4bd329c994103874fa543c1727ba5288393c21d912d795"}, + {file = "mypy-0.982-py3-none-any.whl", hash = "sha256:1021c241e8b6e1ca5a47e4d52601274ac078a89845cfde66c6d5f769819ffa1d"}, + {file = "mypy-0.982.tar.gz", hash = "sha256:85f7a343542dc8b1ed0a888cdd34dca56462654ef23aa673907305b260b3d746"}, ] mypy-boto3-appconfig = [ - {file = "mypy-boto3-appconfig-1.24.36.post1.tar.gz", hash = "sha256:e1916b3754915cb411ef977083500e1f30f81f7b3aea6ff5eed1cec91944dea6"}, - {file = "mypy_boto3_appconfig-1.24.36.post1-py3-none-any.whl", hash = "sha256:a5dbe549dbebf4bc7a6cfcbfa9dff89ceb4983c042b785763ee656504bdb49f6"}, + {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"}, ] mypy-boto3-appconfigdata = [ - {file = "mypy-boto3-appconfigdata-1.24.36.post1.tar.gz", hash = "sha256:48c0b29a99f5e5a54a4585a4b3661bc00c7db40e481c5d014a4bfd86d1ae645e"}, - {file = "mypy_boto3_appconfigdata-1.24.36.post1-py3-none-any.whl", hash = "sha256:2bc495e6b6bd358d78d30f84b750d17ac326b2b4356a7786d0d1334812416edd"}, + {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"}, ] mypy-boto3-cloudformation = [ - {file = "mypy-boto3-cloudformation-1.24.36.post1.tar.gz", hash = "sha256:ed7df9ae3a8390a145229122a1489d0a58bbf9986cb54f0d7a65ed54f12c8e63"}, - {file = "mypy_boto3_cloudformation-1.24.36.post1-py3-none-any.whl", hash = "sha256:b39020c13a876bb18908aad22326478d0ac3faec0bdac0d2c11dc318c9dcf149"}, + {file = "mypy-boto3-cloudformation-1.25.0.tar.gz", hash = "sha256:1251b0f24b0a7c21a6e1f7f88fbf6aaa7fcc490b82d6ada46b5778cfd12d112f"}, + {file = "mypy_boto3_cloudformation-1.25.0-py3-none-any.whl", hash = "sha256:469e3cbc0a78003cc3134dece0203d0a0bfa908af34187aa53cd610b5f9bb19f"}, ] mypy-boto3-cloudwatch = [ - {file = "mypy-boto3-cloudwatch-1.24.55.tar.gz", hash = "sha256:f8950de7a93b3db890cd8524514a2245d9b5fd83ce2dd60a37047a2cd42d5dd6"}, - {file = "mypy_boto3_cloudwatch-1.24.55-py3-none-any.whl", hash = "sha256:23faf8fdfe928f9dcce453a60b03bda69177554eb88c2d7e5240ff91b5b14388"}, + {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"}, ] mypy-boto3-dynamodb = [ - {file = "mypy-boto3-dynamodb-1.24.74.tar.gz", hash = "sha256:7c5b4dc26e05375d3a2cadc28b253a38060c27aa4d6a9394b3d5deea45171f91"}, - {file = "mypy_boto3_dynamodb-1.24.74-py3-none-any.whl", hash = "sha256:1111c2eb8129bf8c09b5423d95ad483943f5fc08d745801c356f6a1b22b04b37"}, + {file = "mypy-boto3-dynamodb-1.25.0.tar.gz", hash = "sha256:a858453090955e29c0ca479ea19f627a2d0dcf916469b104a5c6ad648f1299ba"}, + {file = "mypy_boto3_dynamodb-1.25.0-py3-none-any.whl", hash = "sha256:03437167a084ac0eb718fdaf5931949247b344acc4f75b8b5528f078c8f6f073"}, ] mypy-boto3-lambda = [ - {file = "mypy-boto3-lambda-1.24.54.tar.gz", hash = "sha256:c76d28d84bdf94c8980acd85bc07f2747559ca11a990fd6785c9c2389e13aff1"}, - {file = "mypy_boto3_lambda-1.24.54-py3-none-any.whl", hash = "sha256:231b6aac22b107ebb7afa2ec6dc1311b769dbdd5bfae957cf60db3e8bc3133d7"}, + {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"}, ] mypy-boto3-logs = [ - {file = "mypy-boto3-logs-1.24.36.post1.tar.gz", hash = "sha256:8b00c2d5328e72023b1d1acd65e7cea7854f07827d23ce21c78391ca74271290"}, - {file = "mypy_boto3_logs-1.24.36.post1-py3-none-any.whl", hash = "sha256:f96257ec06099bfda1ce5f35b410e7fb93fb601bc312e8d7a09b13adaefd23f0"}, + {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"}, ] mypy-boto3-s3 = [ - {file = "mypy-boto3-s3-1.24.94.tar.gz", hash = "sha256:0907df7b0c03a8c158d7d08fcb3c255505efca88347224425a8fc687e4596fa1"}, - {file = "mypy_boto3_s3-1.24.94-py3-none-any.whl", hash = "sha256:58f1861c9dd9037301e0ccacebe8936bf61aba5a6a43223f74bf169c3df504d2"}, + {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"}, ] mypy-boto3-secretsmanager = [ - {file = "mypy-boto3-secretsmanager-1.24.83.tar.gz", hash = "sha256:e39b55ffa05310832544bc4212c5b6fad4f97bf9f60f05827385d266f481a7ff"}, - {file = "mypy_boto3_secretsmanager-1.24.83-py3-none-any.whl", hash = "sha256:9ed3ec38a6c05961cb39a2d9fb891441d4cf22c63e34a6998fbd3d28ba290d9a"}, + {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"}, ] mypy-boto3-ssm = [ - {file = "mypy-boto3-ssm-1.24.90.tar.gz", hash = "sha256:8fdc65a34958ae89d4ae8ea7748caec46226216b35d75adf87e8ed40a798bf95"}, - {file = "mypy_boto3_ssm-1.24.90-py3-none-any.whl", hash = "sha256:6fc26896e1fb4f84f5bbc04f79ba698e4dd296586ca462c517bc64e78d326fb5"}, + {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"}, ] mypy-boto3-xray = [ - {file = "mypy-boto3-xray-1.24.36.post1.tar.gz", hash = "sha256:104f1ecf7f1f6278c582201e71a7ab64843d3a3fdc8f23295cf68788cc77e9bb"}, - {file = "mypy_boto3_xray-1.24.36.post1-py3-none-any.whl", hash = "sha256:97b9f0686c717c8be99ac06cb52febaf71712b4e4cd0b61ed2eb5ed012a9b5fd"}, + {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"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, @@ -2048,8 +2029,8 @@ pygments = [ {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, ] pymdown-extensions = [ - {file = "pymdown_extensions-9.6-py3-none-any.whl", hash = "sha256:1e36490adc7bfcef1fdb21bb0306e93af99cff8ec2db199bd17e3bf009768c11"}, - {file = "pymdown_extensions-9.6.tar.gz", hash = "sha256:b956b806439bbff10f726103a941266beb03fbe99f897c7d5e774d7170339ad9"}, + {file = "pymdown_extensions-9.7-py3-none-any.whl", hash = "sha256:767d07d9dead0f52f5135545c01f4ed627f9a7918ee86c646d893e24c59db87d"}, + {file = "pymdown_extensions-9.7.tar.gz", hash = "sha256:651b0107bc9ee790aedea3673cb88832c0af27d2569cf45c2de06f1d65292e96"}, ] pyparsing = [ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, @@ -2079,12 +2060,12 @@ pyrsistent = [ {file = "pyrsistent-0.18.1.tar.gz", hash = "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96"}, ] pytest = [ - {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, - {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, + {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, + {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, ] pytest-asyncio = [ - {file = "pytest-asyncio-0.16.0.tar.gz", hash = "sha256:7496c5977ce88c34379df64a66459fe395cd05543f0a2f837016e7144391fcfb"}, - {file = "pytest_asyncio-0.16.0-py3-none-any.whl", hash = "sha256:5f2a21273c47b331ae6aa5b36087047b4899e40f03f18397c0e65fa5cca54e9b"}, + {file = "pytest-asyncio-0.20.1.tar.gz", hash = "sha256:626699de2a747611f3eeb64168b3575f70439b06c3d0206e6ceaeeb956e65519"}, + {file = "pytest_asyncio-0.20.1-py3-none-any.whl", hash = "sha256:2c85a835df33fda40fe3973b451e0c194ca11bc2c007eabff90bb3d156fc172b"}, ] pytest-benchmark = [ {file = "pytest-benchmark-3.4.1.tar.gz", hash = "sha256:40e263f912de5a81d891619032983557d62a3d85843f9a9f30b98baea0cd7b47"}, @@ -2394,6 +2375,6 @@ xenon = [ {file = "xenon-0.9.0.tar.gz", hash = "sha256:d2b9cb6c6260f771a432c1e588e51fddb17858f88f73ef641e7532f7a5f58fb8"}, ] zipp = [ - {file = "zipp-3.9.0-py3-none-any.whl", hash = "sha256:972cfa31bc2fedd3fa838a51e9bc7e64b7fb725a8c00e7431554311f180e9980"}, - {file = "zipp-3.9.0.tar.gz", hash = "sha256:3a7af91c3db40ec72dd9d154ae18e008c69efe8ca88dde4f9a731bb82fe2f9eb"}, + {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"}, + {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"}, ] diff --git a/pyproject.toml b/pyproject.toml index 3dc94b5c67e..cddceb2388d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,14 +34,13 @@ flake8-builtins = "^2.0.0" flake8-comprehensions = "^3.7.0" flake8-debugger = "^4.0.0" flake8-fixme = "^1.1.1" -flake8-isort = "^4.1.2" -flake8-variables-names = "^0.0.4" +flake8-variables-names = "^0.0.5" flake8-black = "^0.3.3" isort = "^5.10.1" pytest-cov = "^4.0.0" pytest-mock = "^3.5.1" pdoc3 = "^0.10.0" -pytest-asyncio = "^0.16.0" +pytest-asyncio = "^0.20.1" bandit = "^1.7.1" radon = "^5.1.0" xenon = "^0.9.0" @@ -49,10 +48,9 @@ flake8-eradicate = "^1.2.1" flake8-bugbear = "^22.9.23" mkdocs-git-revision-date-plugin = "^0.3.2" mike = "^1.1.2" -mypy = "^0.971" retry = "^0.9.2" pytest-xdist = "^2.5.0" -aws-cdk-lib = "^2.47.0" +aws-cdk-lib = "^2.49.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" @@ -85,6 +83,7 @@ aws-sdk = ["boto3"] [tool.poetry.group.dev.dependencies] cfn-lint = "0.67.0" +mypy = "^0.982" [tool.coverage.run] source = ["aws_lambda_powertools"] @@ -154,8 +153,8 @@ markers = [ ] [build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" +requires = ["poetry-core>=1.3.2"] +build-backend = "poetry.core.masonry.api" # NOTE # As of now, Feb 2020, flake8 don't support pyproject diff --git a/tests/e2e/utils/lambda_layer/base.py b/tests/e2e/utils/lambda_layer/base.py index 280fe19d4f8..e38e936eefc 100644 --- a/tests/e2e/utils/lambda_layer/base.py +++ b/tests/e2e/utils/lambda_layer/base.py @@ -27,6 +27,6 @@ def before_build(self): # Create missing parent directories if missing self.output_dir.mkdir(parents=True, exist_ok=True) - def after_build(self): + def after_build(self): # noqa: B027 """Any step after a build succeed""" ... diff --git a/tests/functional/idempotency/test_idempotency.py b/tests/functional/idempotency/test_idempotency.py index e5c5c777971..8f39b832554 100644 --- a/tests/functional/idempotency/test_idempotency.py +++ b/tests/functional/idempotency/test_idempotency.py @@ -72,7 +72,7 @@ def test_idempotent_lambda_already_completed( lambda_context, ): """ - Test idempotent decorator where event with matching event key has already been succesfully processed + Test idempotent decorator where event with matching event key has already been successfully processed """ stubber = stub.Stubber(persistence_store.table.meta.client) @@ -1247,6 +1247,46 @@ def lambda_handler(event, _): assert handler_result == expected_result +@pytest.mark.parametrize("data", [None, 0, False]) +def test_idempotent_function_falsy_values(data): + # Scenario to validate we can use idempotent_function with any function + # receiving a falsy value (`None`, `False`, `0`, etc.) + # shouldn't cause a RuntimeError + mock_event = data + idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_falsy_values..record_handler#{hash_idempotency_key(mock_event)}" # noqa: E501 + + persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key) + expected_result = {"message": "Foo"} + + @idempotent_function(persistence_store=persistence_layer, data_keyword_argument="record") + def record_handler(record): + return expected_result + + # WHEN calling the function + result = record_handler(record=mock_event) + # THEN we expect the function to execute successfully + assert result == expected_result + + +@pytest.mark.parametrize("data", [None, 0, False]) +def test_idempotent_function_falsy_values_with_raise_on_no_idempotency_key( + data, persistence_store: DynamoDBPersistenceLayer +): + # GIVEN raise_on_no_idempotency_key is True + idempotency_config = IdempotencyConfig(event_key_jmespath="idemKey", raise_on_no_idempotency_key=True) + + @idempotent_function(data_keyword_argument="record", persistence_store=persistence_store, config=idempotency_config) + def record_handler(record): + return ValueError("Should not be raised") + + # WHEN calling the function + with pytest.raises(IdempotencyKeyError) as e: + record_handler(record=data) + + # THEN we expect an idempotency key error message + assert "No data found to create a hashed idempotency_key" == e.value.args[0] + + def test_idempotent_data_sorting(): # Scenario to validate same data in different order hashes to the same idempotency key data_one = {"data": "test message 1", "more_data": "more data 1"} diff --git a/tests/functional/test_logger.py b/tests/functional/test_logger.py index 7eb3018ef64..f171ba7ee5b 100644 --- a/tests/functional/test_logger.py +++ b/tests/functional/test_logger.py @@ -415,6 +415,25 @@ def test_logger_extra_kwargs(stdout, service_name): assert "request_id" not in no_extra_fields_log +def test_logger_arbitrary_fields_as_kwargs(stdout, service_name): + # GIVEN Logger is initialized + logger = Logger(service=service_name, stream=stdout) + + # WHEN `request_id` is an arbitrary field in a log message to the existing structured log + fields = {"request_id": "blah"} + + logger.info("with arbitrary fields", **fields) + logger.info("without extra fields") + + extra_fields_log, no_extra_fields_log = capture_multiple_logging_statements_output(stdout) + + # THEN first log should have request_id field in the root structure + assert "request_id" in extra_fields_log + + # THEN second log should not have request_id in the root structure + assert "request_id" not in no_extra_fields_log + + def test_logger_log_twice_when_log_filter_isnt_present_and_root_logger_is_setup(monkeypatch, stdout, service_name): # GIVEN Lambda configures the root logger with a handler root_logger = logging.getLogger() @@ -795,6 +814,21 @@ def test_use_datetime(stdout, service_name, utc): ) +@pytest.mark.parametrize("utc", [False, True]) +def test_use_rfc3339_iso8601(stdout, service_name, utc): + # GIVEN + logger = Logger(service=service_name, stream=stdout, use_rfc3339=True, utc=utc) + RFC3339_REGEX = r"^((?:(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$" + + # WHEN a log statement happens + logger.info({}) + + # THEN the timestamp has the appropriate formatting + log = capture_logging_output(stdout) + + assert re.fullmatch(RFC3339_REGEX, log["timestamp"]) # "2022-10-27T17:42:26.841+0200" + + def test_inject_lambda_context_log_event_request_data_classes(lambda_context, stdout, lambda_event, service_name): # GIVEN Logger is initialized logger = Logger(service=service_name, stream=stdout)