diff --git a/aws_lambda_powertools/utilities/parameters/appconfig.py b/aws_lambda_powertools/utilities/parameters/appconfig.py index 380e355d673..a3a340a62be 100644 --- a/aws_lambda_powertools/utilities/parameters/appconfig.py +++ b/aws_lambda_powertools/utilities/parameters/appconfig.py @@ -5,20 +5,17 @@ import os from typing import TYPE_CHECKING, Any, Dict, Optional, Union -from uuid import uuid4 import boto3 from botocore.config import Config if TYPE_CHECKING: - from mypy_boto3_appconfig import AppConfigClient + from mypy_boto3_appconfigdata import AppConfigDataClient from ...shared import constants from ...shared.functions import resolve_env_var_choice from .base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider -CLIENT_ID = str(uuid4()) - class AppConfigProvider(BaseProvider): """ @@ -34,8 +31,8 @@ class AppConfigProvider(BaseProvider): Botocore configuration to pass during client initialization boto3_session : boto3.session.Session, optional Boto3 session to create a boto3_client from - boto3_client: AppConfigClient, optional - Boto3 AppConfig Client to use, boto3_session will be ignored if both are provided + boto3_client: AppConfigDataClient, optional + Boto3 AppConfigData Client to use, boto3_session will be ignored if both are provided Example ------- @@ -73,7 +70,7 @@ def __init__( application: Optional[str] = None, config: Optional[Config] = None, boto3_session: Optional[boto3.session.Session] = None, - boto3_client: Optional["AppConfigClient"] = None, + boto3_client: Optional["AppConfigDataClient"] = None, ): """ Initialize the App Config client @@ -81,8 +78,8 @@ def __init__( super().__init__() - self.client: "AppConfigClient" = self._build_boto3_client( - service_name="appconfig", client=boto3_client, session=boto3_session, config=config + self.client: "AppConfigDataClient" = self._build_boto3_client( + service_name="appconfigdata", client=boto3_client, session=boto3_session, config=config ) self.application = resolve_env_var_choice( @@ -91,6 +88,9 @@ def __init__( self.environment = environment self.current_version = "" + self._next_token = "" # nosec - token for get_latest_configuration executions + self.last_returned_value = "" + def _get(self, name: str, **sdk_options) -> str: """ Retrieve a parameter value from AWS App config. @@ -100,16 +100,26 @@ def _get(self, name: str, **sdk_options) -> str: name: str Name of the configuration sdk_options: dict, optional - Dictionary of options that will be passed to the client's get_configuration API call + SDK options to propagate to `start_configuration_session` API call """ + if not self._next_token: + sdk_options["ConfigurationProfileIdentifier"] = name + sdk_options["ApplicationIdentifier"] = self.application + sdk_options["EnvironmentIdentifier"] = self.environment + response_configuration = self.client.start_configuration_session(**sdk_options) + self._next_token = response_configuration["InitialConfigurationToken"] - sdk_options["Configuration"] = name - sdk_options["Application"] = self.application - sdk_options["Environment"] = self.environment - sdk_options["ClientId"] = CLIENT_ID + # The new AppConfig APIs require two API calls to return the configuration + # First we start the session and after that we retrieve the configuration + # We need to store the token to use in the next execution + response = self.client.get_latest_configuration(ConfigurationToken=self._next_token) + return_value = response["Configuration"].read() + self._next_token = response["NextPollConfigurationToken"] - response = self.client.get_configuration(**sdk_options) - return response["Content"].read() # read() of botocore.response.StreamingBody + if return_value: + self.last_returned_value = return_value + + return self.last_returned_value def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]: """ @@ -145,7 +155,7 @@ def get_app_config( max_age: int Maximum age of the cached value sdk_options: dict, optional - Dictionary of options that will be passed to the boto client get_configuration API call + SDK options to propagate to `start_configuration_session` API call Raises ------ @@ -180,8 +190,6 @@ def get_app_config( if "appconfig" not in DEFAULT_PROVIDERS: DEFAULT_PROVIDERS["appconfig"] = AppConfigProvider(environment=environment, application=application) - sdk_options["ClientId"] = CLIENT_ID - return DEFAULT_PROVIDERS["appconfig"].get( name, max_age=max_age, transform=transform, force_fetch=force_fetch, **sdk_options ) diff --git a/aws_lambda_powertools/utilities/parameters/base.py b/aws_lambda_powertools/utilities/parameters/base.py index ce03b757618..b76b16e1dd8 100644 --- a/aws_lambda_powertools/utilities/parameters/base.py +++ b/aws_lambda_powertools/utilities/parameters/base.py @@ -15,7 +15,7 @@ from .exceptions import GetParameterError, TransformParameterError if TYPE_CHECKING: - from mypy_boto3_appconfig import AppConfigClient + from mypy_boto3_appconfigdata import AppConfigDataClient from mypy_boto3_dynamodb import DynamoDBServiceResource from mypy_boto3_secretsmanager import SecretsManagerClient from mypy_boto3_ssm import SSMClient @@ -28,7 +28,7 @@ TRANSFORM_METHOD_JSON = "json" TRANSFORM_METHOD_BINARY = "binary" SUPPORTED_TRANSFORM_METHODS = [TRANSFORM_METHOD_JSON, TRANSFORM_METHOD_BINARY] -ParameterClients = Union["AppConfigClient", "SecretsManagerClient", "SSMClient"] +ParameterClients = Union["AppConfigDataClient", "SecretsManagerClient", "SSMClient"] class BaseProvider(ABC): diff --git a/docs/upgrade.md b/docs/upgrade.md index 37e9a318522..f6b3c7e9d00 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -13,6 +13,7 @@ Changes at a glance: * The API for **event handler's `Response`** has minor changes to support multi value headers and cookies. * The **legacy SQS batch processor** was removed. * The **Idempotency key** format changed slightly, invalidating all the existing cached results. +* The **Feature Flags and AppConfig Parameter utility** API calls have changed and you must update your IAM permissions. ???+ important Powertools for Python v2 drops suport for Python 3.6, following the Python 3.6 End-Of-Life (EOL) reached on December 23, 2021. @@ -154,3 +155,9 @@ Prior to this change, the Idempotency key was generated using only the caller fu After this change, the key is generated using the `module name` + `qualified function name` + `idempotency key` (e.g: `app.classExample.function#app.handler#282e83393862a613b612c00283fef4c8`). Using qualified names prevents distinct functions with the same name to contend for the same Idempotency key. + +## Feature Flags and AppConfig Parameter utility + +AWS AppConfig deprecated the current API (GetConfiguration) - [more details here](https://github.com/awslabs/aws-lambda-powertools-python/issues/1506#issuecomment-1266645884). + +You must update your IAM permissions to allow `appconfig:GetLatestConfiguration` and `appconfig:StartConfigurationSession`. There are no code changes required. diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index 1d586d9377d..f35dd88106b 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -3,11 +3,11 @@ title: Feature flags description: Utility --- -???+ note - This is currently in Beta, as we might change Store parameters in the next release. - The feature flags utility provides a simple rule engine to define when one or multiple features should be enabled depending on the input. +???+ info + We currently only support AppConfig using [freeform configuration profile](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-configuration-and-profile.html#appconfig-creating-configuration-and-profile-free-form-configurations). + ## Terminology Feature flags are used to modify behaviour without changing the application's code. These flags can be **static** or **dynamic**. @@ -28,6 +28,9 @@ If you want to learn more about feature flags, their variations and trade-offs, * [AWS Lambda Feature Toggles Made Simple - Ran Isenberg](https://isenberg-ran.medium.com/aws-lambda-feature-toggles-made-simple-580b0c444233) * [Feature Flags Getting Started - CloudBees](https://www.cloudbees.com/blog/ultimate-feature-flag-guide) +???+ note + AWS AppConfig requires two API calls to fetch configuration for the first time. You can improve latency by consolidating your feature settings in a single [Configuration](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-configuration-and-profile.html). + ## Key features * Define simple feature flags to dynamically decide when to enable a feature @@ -38,7 +41,7 @@ If you want to learn more about feature flags, their variations and trade-offs, ### IAM Permissions -Your Lambda function must have `appconfig:GetConfiguration` IAM permission in order to fetch configuration from AWS AppConfig. +Your Lambda function IAM Role must have `appconfig:GetLatestConfiguration` and `appconfig:StartConfigurationSession` IAM permissions before using this feature. ### Required resources diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md index 2559044b632..6b7d64b66b9 100644 --- a/docs/utilities/parameters.md +++ b/docs/utilities/parameters.md @@ -24,14 +24,15 @@ This utility requires additional permissions to work as expected. ???+ note Different parameter providers require different permissions. -| Provider | Function/Method | IAM Permission | -| ------------------- | ---------------------------------------------------- | ------------------------------- | -| SSM Parameter Store | `get_parameter`, `SSMProvider.get` | `ssm:GetParameter` | -| SSM Parameter Store | `get_parameters`, `SSMProvider.get_multiple` | `ssm:GetParametersByPath` | -| Secrets Manager | `get_secret`, `SecretsManager.get` | `secretsmanager:GetSecretValue` | -| DynamoDB | `DynamoDBProvider.get` | `dynamodb:GetItem` | -| DynamoDB | `DynamoDBProvider.get_multiple` | `dynamodb:Query` | -| App Config | `AppConfigProvider.get_app_config`, `get_app_config` | `appconfig:GetConfiguration` | +| Provider | Function/Method | IAM Permission | +| ------------------- | -----------------------------------------------------------------| -----------------------------------------------------------------------------| +| SSM Parameter Store | `get_parameter`, `SSMProvider.get` | `ssm:GetParameter` | +| SSM Parameter Store | `get_parameters`, `SSMProvider.get_multiple` | `ssm:GetParametersByPath` | +| SSM Parameter Store | If using `decrypt=True` | You must add an additional permission `kms:Decrypt` | +| Secrets Manager | `get_secret`, `SecretsManager.get` | `secretsmanager:GetSecretValue` | +| DynamoDB | `DynamoDBProvider.get` | `dynamodb:GetItem` | +| DynamoDB | `DynamoDBProvider.get_multiple` | `dynamodb:Query` | +| App Config | `get_app_config`, `AppConfigProvider.get_app_config` | `appconfig:GetLatestConfiguration` and `appconfig:StartConfigurationSession` | ### Fetching parameters diff --git a/poetry.lock b/poetry.lock index 696df3610b7..420c01745c9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -753,6 +753,17 @@ python-versions = ">=3.7" [package.dependencies] 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" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = ">=4.1.0" + [[package]] name = "mypy-boto3-cloudformation" version = "1.24.36.post1" @@ -1400,7 +1411,10 @@ aws-cdk-lib = [ {file = "aws-cdk.aws-apigatewayv2-integrations-alpha-2.45.0a0.tar.gz", hash = "sha256:a3a3413c34e444d752d44d7bbe8891d5c1af6d64e10185eaa21885710c12b33a"}, {file = "aws_cdk.aws_apigatewayv2_integrations_alpha-2.45.0a0-py3-none-any.whl", hash = "sha256:d8ed1c1f0b1ea29255c561f528f494e3e8a85ebc7373234a58872c558408acbc"}, ] -aws-xray-sdk = [] +aws-xray-sdk = [ + {file = "aws-xray-sdk-2.10.0.tar.gz", hash = "sha256:9b14924fd0628cf92936055864655354003f0b1acc3e1c3ffde6403d0799dd7a"}, + {file = "aws_xray_sdk-2.10.0-py2.py3-none-any.whl", hash = "sha256:7551e81a796e1a5471ebe84844c40e8edf7c218db33506d046fec61f7495eda4"}, +] bandit = [ {file = "bandit-1.7.4-py3-none-any.whl", hash = "sha256:412d3f259dab4077d0e7f0c11f50f650cc7d10db905d98f6520a95a18049658a"}, {file = "bandit-1.7.4.tar.gz", hash = "sha256:2d63a8c573417bae338962d4b9b06fbc6080f74ecd955a092849e1e65c717bd2"}, @@ -1427,7 +1441,10 @@ botocore = [ {file = "botocore-1.27.88-py3-none-any.whl", hash = "sha256:de4e087b24cd3bc369eb2e27f8fe94a6499f7dea08c919fba13cefb2496bd2bb"}, {file = "botocore-1.27.88.tar.gz", hash = "sha256:ded0a4035baf91eb358ef501c92a8512543f5ab7658f459df3077a70a555b5cd"}, ] -cattrs = [] +cattrs = [ + {file = "cattrs-22.1.0-py3-none-any.whl", hash = "sha256:d55c477b4672f93606e992049f15d526dc7867e6c756cd6256d4af92e2b1e364"}, + {file = "cattrs-22.1.0.tar.gz", hash = "sha256:94b67b64cf92c994f8784c40c082177dc916e0489a73a9a36b24eb18a9db40c6"}, +] certifi = [ {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, @@ -1444,7 +1461,10 @@ click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] -colorama = [] +colorama = [ + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +] constructs = [ {file = "constructs-10.1.124-py3-none-any.whl", hash = "sha256:76ef2e6776cfdd1c4131a18b0e83c1b154d462b3ebb37ddbeaf3043b3b0de301"}, {file = "constructs-10.1.124.tar.gz", hash = "sha256:5e4bfcca275867e5cfb4636ef65ed334c861a816f287ce1136f082ad0e0c1c2c"}, @@ -1501,7 +1521,10 @@ coverage = [ {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, ] -decorator = [] +decorator = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] dnspython = [ {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"}, {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"}, @@ -1518,7 +1541,10 @@ exceptiongroup = [ {file = "exceptiongroup-1.0.0rc9-py3-none-any.whl", hash = "sha256:2e3c3fc1538a094aab74fad52d6c33fc94de3dfee3ee01f187c0e0c72aec5337"}, {file = "exceptiongroup-1.0.0rc9.tar.gz", hash = "sha256:9086a4a21ef9b31c72181c77c040a074ba0889ee56a7b289ff0afb0d97655f96"}, ] -execnet = [] +execnet = [ + {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, + {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, +] fastjsonschema = [ {file = "fastjsonschema-2.16.2-py3-none-any.whl", hash = "sha256:21f918e8d9a1a4ba9c22e09574ba72267a6762d47822db9add95f6454e51cc1c"}, {file = "fastjsonschema-2.16.2.tar.gz", hash = "sha256:01e366f25d9047816fe3d288cbfc3e10541daf0af2044763f3d0ade42476da18"}, @@ -1539,7 +1565,10 @@ 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"}, ] -flake8-builtins = [] +flake8-builtins = [ + {file = "flake8-builtins-1.5.3.tar.gz", hash = "sha256:09998853b2405e98e61d2ff3027c47033adbdc17f9fe44ca58443d876eb00f3b"}, + {file = "flake8_builtins-1.5.3-py2.py3-none-any.whl", hash = "sha256:7706babee43879320376861897e5d1468e396a40b8918ed7bccf70e5f90b8687"}, +] flake8-comprehensions = [ {file = "flake8-comprehensions-3.10.0.tar.gz", hash = "sha256:181158f7e7aa26a63a0a38e6017cef28c6adee71278ce56ce11f6ec9c4905058"}, {file = "flake8_comprehensions-3.10.0-py3-none-any.whl", hash = "sha256:dad454fd3d525039121e98fa1dd90c46bc138708196a4ebbc949ad3c859adedb"}, @@ -1552,18 +1581,28 @@ flake8-eradicate = [ {file = "flake8-eradicate-1.4.0.tar.gz", hash = "sha256:3088cfd6717d1c9c6c3ac45ef2e5f5b6c7267f7504d5a74b781500e95cb9c7e1"}, {file = "flake8_eradicate-1.4.0-py3-none-any.whl", hash = "sha256:e3bbd0871be358e908053c1ab728903c114f062ba596b4d40c852fd18f473d56"}, ] -flake8-fixme = [] +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 = [] -future = [] +flake8-variables-names = [ + {file = "flake8_variables_names-0.0.4.tar.gz", hash = "sha256:d6fa0571a807c72940b5773827c5760421ea6f8206595ff0a8ecfa01e42bf2cf"}, +] +future = [ + {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, +] ghp-import = [ {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, ] -gitdb = [] +gitdb = [ + {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"}, + {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"}, +] gitpython = [ {file = "GitPython-3.1.28-py3-none-any.whl", hash = "sha256:77bfbd299d8709f6af7e0c70840ef26e7aff7cf0c1ed53b42dd7fc3a310fcb02"}, {file = "GitPython-3.1.28.tar.gz", hash = "sha256:6bd3451b8271132f099ceeaf581392eaf6c274af74bb06144307870479d0697c"}, @@ -1576,8 +1615,14 @@ importlib-metadata = [ {file = "importlib_metadata-4.13.0-py3-none-any.whl", hash = "sha256:8a8a81bcf996e74fee46f0d16bd3eaa382a7eb20fd82445c3ad11f4090334116"}, {file = "importlib_metadata-4.13.0.tar.gz", hash = "sha256:dd0173e8f150d6815e098fd354f6414b0f079af4644ddfe90c71e2fc6174346d"}, ] -iniconfig = [] -isort = [] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +isort = [ + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, +] jinja2 = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, @@ -1594,7 +1639,10 @@ mako = [ {file = "Mako-1.2.3-py3-none-any.whl", hash = "sha256:c413a086e38cd885088d5e165305ee8eed04e8b3f8f62df343480da0a385735f"}, {file = "Mako-1.2.3.tar.gz", hash = "sha256:7fde96466fcfeedb0eed94f187f20b23d85e4cb41444be0e542e2c8c65c396cd"}, ] -mando = [] +mando = [ + {file = "mando-0.6.4-py2.py3-none-any.whl", hash = "sha256:4ce09faec7e5192ffc3c57830e26acba0fd6cd11e1ee81af0d4df0657463bd1c"}, + {file = "mando-0.6.4.tar.gz", hash = "sha256:79feb19dc0f097daa64a1243db578e7674909b75f88ac2220f1c065c10a0d960"}, +] markdown = [ {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"}, {file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"}, @@ -1641,8 +1689,14 @@ markupsafe = [ {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, ] -mccabe = [] -mergedeep = [] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mergedeep = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] mike = [ {file = "mike-1.1.2-py3-none-any.whl", hash = "sha256:4c307c28769834d78df10f834f57f810f04ca27d248f80a75f49c6fa2d1527ca"}, {file = "mike-1.1.2.tar.gz", hash = "sha256:56c3f1794c2d0b5fdccfa9b9487beb013ca813de2e3ad0744724e9d34d40b77b"}, @@ -1651,17 +1705,50 @@ mkdocs = [ {file = "mkdocs-1.4.0-py3-none-any.whl", hash = "sha256:ce057e9992f017b8e1496b591b6c242cbd34c2d406e2f9af6a19b97dd6248faa"}, {file = "mkdocs-1.4.0.tar.gz", hash = "sha256:e5549a22d59e7cb230d6a791edd2c3d06690908454c0af82edc31b35d57e3069"}, ] -mkdocs-git-revision-date-plugin = [] +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"}, ] -mkdocs-material-extensions = [] -mypy = [] +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"}, +] +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"}, +] 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"}, ] +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"}, +] 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"}, @@ -1698,8 +1785,14 @@ 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"}, ] -mypy-extensions = [] -packaging = [] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] pathspec = [ {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, @@ -1708,15 +1801,29 @@ pbr = [ {file = "pbr-5.10.0-py2.py3-none-any.whl", hash = "sha256:da3e18aac0a3c003e9eea1a81bd23e5a3a75d745670dcf736317b7d966887fdf"}, {file = "pbr-5.10.0.tar.gz", hash = "sha256:cfcc4ff8e698256fc17ea3ff796478b050852585aa5bae79ecd05b2ab7b39b9a"}, ] -pdoc3 = [] +pdoc3 = [ + {file = "pdoc3-0.10.0-py3-none-any.whl", hash = "sha256:ba45d1ada1bd987427d2bf5cdec30b2631a3ff5fb01f6d0e77648a572ce6028b"}, + {file = "pdoc3-0.10.0.tar.gz", hash = "sha256:5f22e7bcb969006738e1aa4219c75a32f34c2d62d46dc9d2fb2d3e0b0287e4b7"}, +] platformdirs = [ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, ] -pluggy = [] -publication = [] -py = [] -py-cpuinfo = [] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +publication = [ + {file = "publication-0.0.3-py2.py3-none-any.whl", hash = "sha256:0248885351febc11d8a1098d5c8e3ab2dabcf3e8c0c96db1e17ecd12b53afbe6"}, + {file = "publication-0.0.3.tar.gz", hash = "sha256:68416a0de76dddcdd2930d1c8ef853a743cc96c82416c4e4d3b5d901c6276dc4"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +py-cpuinfo = [ + {file = "py-cpuinfo-8.0.0.tar.gz", hash = "sha256:5f269be0e08e33fd959de96b34cd4aeeeacac014dd8305f70eb28d06de2345c5"}, +] pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, @@ -1776,16 +1883,34 @@ pytest = [ {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, ] -pytest-asyncio = [] -pytest-benchmark = [] -pytest-cov = [] -pytest-forked = [] +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"}, +] +pytest-benchmark = [ + {file = "pytest-benchmark-3.4.1.tar.gz", hash = "sha256:40e263f912de5a81d891619032983557d62a3d85843f9a9f30b98baea0cd7b47"}, + {file = "pytest_benchmark-3.4.1-py2.py3-none-any.whl", hash = "sha256:36d2b08c4882f6f997fd3126a3d6dfd70f3249cde178ed8bbc0b73db7c20f809"}, +] +pytest-cov = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] +pytest-forked = [ + {file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"}, + {file = "pytest_forked-1.4.0-py3-none-any.whl", hash = "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"}, +] pytest-mock = [ {file = "pytest-mock-3.10.0.tar.gz", hash = "sha256:fbbdb085ef7c252a326fd8cdcac0aa3b1333d8811f131bdcc701002e1be7ed4f"}, {file = "pytest_mock-3.10.0-py3-none-any.whl", hash = "sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b"}, ] -pytest-xdist = [] -python-dateutil = [] +pytest-xdist = [ + {file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"}, + {file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] python-snappy = [ {file = "python-snappy-0.6.1.tar.gz", hash = "sha256:b6a107ab06206acc5359d4c5632bd9b22d448702a79b3169b0c62e0fb808bb2a"}, {file = "python_snappy-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b7f920eaf46ebf41bd26f9df51c160d40f9e00b7b48471c3438cb8d027f7fb9b"}, @@ -1871,10 +1996,22 @@ pyyaml = [ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] -pyyaml-env-tag = [] -radon = [] -requests = [] -retry = [] +pyyaml-env-tag = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] +radon = [ + {file = "radon-5.1.0-py2.py3-none-any.whl", hash = "sha256:fa74e018197f1fcb54578af0f675d8b8e2342bd8e0b72bef8197bc4c9e645f36"}, + {file = "radon-5.1.0.tar.gz", hash = "sha256:cb1d8752e5f862fb9e20d82b5f758cbc4fb1237c92c9a66450ea0ea7bf29aeee"}, +] +requests = [ + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, +] +retry = [ + {file = "retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606"}, + {file = "retry-0.9.2.tar.gz", hash = "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4"}, +] s3transfer = [ {file = "s3transfer-0.6.0-py3-none-any.whl", hash = "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd"}, {file = "s3transfer-0.6.0.tar.gz", hash = "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"}, @@ -1883,8 +2020,14 @@ six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -smmap = [] -stevedore = [] +smmap = [ + {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, + {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, +] +stevedore = [ + {file = "stevedore-3.5.0-py3-none-any.whl", hash = "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c"}, + {file = "stevedore-3.5.0.tar.gz", hash = "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335"}, +] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, @@ -2032,7 +2175,10 @@ wrapt = [ {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, ] -xenon = [] +xenon = [ + {file = "xenon-0.9.0-py2.py3-none-any.whl", hash = "sha256:994c80c7f1c6d40596b600b93734d85a5739208f31895ef99f1e4d362caf9e35"}, + {file = "xenon-0.9.0.tar.gz", hash = "sha256:d2b9cb6c6260f771a432c1e588e51fddb17858f88f73ef641e7532f7a5f58fb8"}, +] zipp = [ {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, diff --git a/pyproject.toml b/pyproject.toml index 9a0f5a3ff38..8f1a1f2a9db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,7 @@ python-snappy = "^0.6.1" mkdocs-material = "^8.5.0" filelock = "^3.8.0" checksumdir = "^1.2.0" +mypy-boto3-appconfigdata = "^1.24.36" importlib-metadata = "^4.13" [tool.poetry.extras] diff --git a/tests/e2e/parameters/__init__.py b/tests/e2e/parameters/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/e2e/parameters/conftest.py b/tests/e2e/parameters/conftest.py new file mode 100644 index 00000000000..f4c9d7396dd --- /dev/null +++ b/tests/e2e/parameters/conftest.py @@ -0,0 +1,19 @@ +import pytest + +from tests.e2e.parameters.infrastructure import ParametersStack + + +@pytest.fixture(autouse=True, scope="module") +def infrastructure(tmp_path_factory, worker_id): + """Setup and teardown logic for E2E test infrastructure + + Yields + ------ + Dict[str, str] + CloudFormation Outputs from deployed infrastructure + """ + stack = ParametersStack() + try: + yield stack.deploy() + finally: + stack.delete() diff --git a/tests/e2e/parameters/handlers/parameter_appconfig_freeform_handler.py b/tests/e2e/parameters/handlers/parameter_appconfig_freeform_handler.py new file mode 100644 index 00000000000..51b56eba95a --- /dev/null +++ b/tests/e2e/parameters/handlers/parameter_appconfig_freeform_handler.py @@ -0,0 +1,11 @@ +from aws_lambda_powertools.utilities import parameters +from aws_lambda_powertools.utilities.typing import LambdaContext + + +def lambda_handler(event: dict, context: LambdaContext): + # Retrieve a single configuration, latest version + value: bytes = parameters.get_app_config( + name=event.get("name"), environment=event.get("environment"), application=event.get("application") + ) + + return value diff --git a/tests/e2e/parameters/infrastructure.py b/tests/e2e/parameters/infrastructure.py new file mode 100644 index 00000000000..e1dfb13bcdd --- /dev/null +++ b/tests/e2e/parameters/infrastructure.py @@ -0,0 +1,105 @@ +from pyclbr import Function + +from aws_cdk import CfnOutput +from aws_cdk import aws_appconfig as appconfig +from aws_cdk import aws_iam as iam + +from tests.e2e.utils.data_builder import build_service_name +from tests.e2e.utils.infrastructure import BaseInfrastructure + + +class ParametersStack(BaseInfrastructure): + def create_resources(self): + functions = self.create_lambda_functions() + self._create_app_config(function=functions["ParameterAppconfigFreeformHandler"]) + + def _create_app_config(self, function: Function): + + service_name = build_service_name() + + cfn_application = appconfig.CfnApplication( + self.stack, id="appconfig-app", name=f"powertools-e2e-{service_name}", description="Lambda Powertools End-to-End testing for AppConfig" + ) + CfnOutput(self.stack, "AppConfigApplication", value=cfn_application.name) + + cfn_environment = appconfig.CfnEnvironment( + self.stack, + "appconfig-env", + application_id=cfn_application.ref, + name=f"powertools-e2e{service_name}", + description="Lambda Powertools End-to-End testing environment", + ) + CfnOutput(self.stack, "AppConfigEnvironment", value=cfn_environment.name) + + cfn_deployment_strategy = appconfig.CfnDeploymentStrategy( + self.stack, + "appconfig-deployment-strategy", + deployment_duration_in_minutes=0, + final_bake_time_in_minutes=0, + growth_factor=100, + name=f"deploymente2e{service_name}", + description="deploymente2e", + replicate_to="NONE", + growth_type="LINEAR", + ) + + self._create_app_config_freeform( + app=cfn_application, + environment=cfn_environment, + strategy=cfn_deployment_strategy, + function=function, + service_name=service_name, + ) + + def _create_app_config_freeform( + self, + app: appconfig.CfnApplication, + environment: appconfig.CfnEnvironment, + strategy: appconfig.CfnDeploymentStrategy, + function: Function, + service_name: str, + ): + + cfn_configuration_profile = appconfig.CfnConfigurationProfile( + self.stack, + "appconfig-profile", + application_id=app.ref, + location_uri="hosted", + type="AWS.Freeform", + name=f"profilee2e{service_name}", + description="profilee2e", + ) + CfnOutput(self.stack, "AppConfigProfile", value=cfn_configuration_profile.name) + + cfn_hosted_configuration_version = appconfig.CfnHostedConfigurationVersion( + self.stack, + "appconfig-hosted-deploy", + application_id=app.ref, + configuration_profile_id=cfn_configuration_profile.ref, + content='{"save_history": {"default": true}}', + content_type="application/json", + description="hostedconfiguratione2e", + ) + CfnOutput(self.stack, "AppConfigConfigurationValue", value=cfn_hosted_configuration_version.content) + + appconfig.CfnDeployment( + self.stack, + "appconfig-deployment", + application_id=app.ref, + configuration_profile_id=cfn_configuration_profile.ref, + configuration_version=cfn_hosted_configuration_version.ref, + deployment_strategy_id=strategy.ref, + environment_id=environment.ref, + description="deployment", + ) + + function.add_to_role_policy( + iam.PolicyStatement( + effect=iam.Effect.ALLOW, + actions=[ + "appconfig:GetLatestConfiguration", + "appconfig:StartConfigurationSession", + ], + resources=["*"], + ) + ) diff --git a/tests/e2e/parameters/test_appconfig.py b/tests/e2e/parameters/test_appconfig.py new file mode 100644 index 00000000000..0129adb1515 --- /dev/null +++ b/tests/e2e/parameters/test_appconfig.py @@ -0,0 +1,61 @@ +import json + +import pytest + +from tests.e2e.utils import data_fetcher + + +@pytest.fixture +def parameter_appconfig_freeform_handler_fn_arn(infrastructure: dict) -> str: + return infrastructure.get("ParameterAppconfigFreeformHandlerArn", "") + + +@pytest.fixture +def parameter_appconfig_freeform_handler_fn(infrastructure: dict) -> str: + return infrastructure.get("ParameterAppconfigFreeformHandler", "") + + +@pytest.fixture +def parameter_appconfig_freeform_value(infrastructure: dict) -> str: + return infrastructure.get("AppConfigConfigurationValue", "") + + +@pytest.fixture +def parameter_appconfig_freeform_application(infrastructure: dict) -> str: + return infrastructure.get("AppConfigApplication", "") + + +@pytest.fixture +def parameter_appconfig_freeform_environment(infrastructure: dict) -> str: + return infrastructure.get("AppConfigEnvironment", "") + + +@pytest.fixture +def parameter_appconfig_freeform_profile(infrastructure: dict) -> str: + return infrastructure.get("AppConfigProfile", "") + + +def test_get_parameter_appconfig_freeform( + parameter_appconfig_freeform_handler_fn_arn: str, + parameter_appconfig_freeform_value: str, + parameter_appconfig_freeform_application: str, + parameter_appconfig_freeform_environment: str, + parameter_appconfig_freeform_profile: str, +): + # GIVEN + payload = json.dumps( + { + "name": parameter_appconfig_freeform_profile, + "environment": parameter_appconfig_freeform_environment, + "application": parameter_appconfig_freeform_application, + } + ) + expected_return = parameter_appconfig_freeform_value + + # WHEN + parameter_execution, _ = data_fetcher.get_lambda_response( + lambda_arn=parameter_appconfig_freeform_handler_fn_arn, payload=payload + ) + parameter_value = parameter_execution["Payload"].read().decode("utf-8") + + assert parameter_value == expected_return diff --git a/tests/functional/test_utilities_parameters.py b/tests/functional/test_utilities_parameters.py index 2b8291db47b..123c2fdbcc2 100644 --- a/tests/functional/test_utilities_parameters.py +++ b/tests/functional/test_utilities_parameters.py @@ -1639,14 +1639,22 @@ def test_appconf_provider_get_configuration_json_content_type(mock_name, config) encoded_message = json.dumps(mock_body_json).encode("utf-8") mock_value = StreamingBody(BytesIO(encoded_message), len(encoded_message)) - # Stub the boto3 client stubber = stub.Stubber(provider.client) - response = {"Content": mock_value, "ConfigurationVersion": "1", "ContentType": "application/json"} - stubber.add_response("get_configuration", response) + response_start_config_session = {"InitialConfigurationToken": "initial_token"} + stubber.add_response("start_configuration_session", response_start_config_session) + + response_get_latest_config = { + "Configuration": mock_value, + "NextPollConfigurationToken": "initial_token", + "ContentType": "application/json", + } + stubber.add_response("get_latest_configuration", response_get_latest_config) stubber.activate() try: - value = provider.get(mock_name, transform="json", ClientConfigurationVersion="2") + value = provider.get( + mock_name, transform="json", ApplicationIdentifier=application, EnvironmentIdentifier=environment + ) assert value == mock_body_json stubber.assert_no_pending_responses() @@ -1659,7 +1667,7 @@ def test_appconf_provider_get_configuration_json_content_type_with_custom_client Test get_configuration.get with default values """ - client = boto3.client("appconfig", config=config) + client = boto3.client("appconfigdata", config=config) # Create a new provider environment = "dev" @@ -1670,14 +1678,22 @@ def test_appconf_provider_get_configuration_json_content_type_with_custom_client encoded_message = json.dumps(mock_body_json).encode("utf-8") mock_value = StreamingBody(BytesIO(encoded_message), len(encoded_message)) - # Stub the boto3 client stubber = stub.Stubber(provider.client) - response = {"Content": mock_value, "ConfigurationVersion": "1", "ContentType": "application/json"} - stubber.add_response("get_configuration", response) + response_start_config_session = {"InitialConfigurationToken": "initial_token"} + stubber.add_response("start_configuration_session", response_start_config_session) + + response_get_latest_config = { + "Configuration": mock_value, + "NextPollConfigurationToken": "initial_token", + "ContentType": "application/json", + } + stubber.add_response("get_latest_configuration", response_get_latest_config) stubber.activate() try: - value = provider.get(mock_name, transform="json", ClientConfigurationVersion="2") + value = provider.get( + mock_name, transform="json", ApplicationIdentifier=application, EnvironmentIdentifier=environment + ) assert value == mock_body_json stubber.assert_no_pending_responses() @@ -1699,10 +1715,16 @@ def test_appconf_provider_get_configuration_no_transform(mock_name, config): encoded_message = json.dumps(mock_body_json).encode("utf-8") mock_value = StreamingBody(BytesIO(encoded_message), len(encoded_message)) - # Stub the boto3 client stubber = stub.Stubber(provider.client) - response = {"Content": mock_value, "ConfigurationVersion": "1", "ContentType": "application/json"} - stubber.add_response("get_configuration", response) + response_start_config_session = {"InitialConfigurationToken": "initial_token"} + stubber.add_response("start_configuration_session", response_start_config_session) + + response_get_latest_config = { + "Configuration": mock_value, + "NextPollConfigurationToken": "initial_token", + "ContentType": "application/json", + } + stubber.add_response("get_latest_configuration", response_get_latest_config) stubber.activate() try: