From d53231dd3ac5e08293a7690c9bffd5da57323ad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Tom=C3=A1s?= <22175056+adriantomas@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:36:58 +0200 Subject: [PATCH 1/6] feat: add `sdk_client` argument to AppConfigStore `__init__` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adrián Tomás <22175056+adriantomas@users.noreply.github.com> --- .../utilities/feature_flags/appconfig.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/appconfig.py b/aws_lambda_powertools/utilities/feature_flags/appconfig.py index 1fb7e8d62af..8b80ec5d081 100644 --- a/aws_lambda_powertools/utilities/feature_flags/appconfig.py +++ b/aws_lambda_powertools/utilities/feature_flags/appconfig.py @@ -1,7 +1,8 @@ import logging import traceback -from typing import Any, Dict, Optional, Union, cast +from typing import TYPE_CHECKING, Any, Dict, Optional, Union, cast +from botocore.client import BaseClient from botocore.config import Config from aws_lambda_powertools.utilities import jmespath_utils @@ -15,6 +16,11 @@ from .base import StoreProvider from .exceptions import ConfigurationStoreError, StoreClientError +if TYPE_CHECKING: + from mypy_boto3_appconfigdata import AppConfigDataClient +else: + AppConfigDataClient = BaseClient + class AppConfigStore(StoreProvider): def __init__( @@ -27,6 +33,7 @@ def __init__( envelope: Optional[str] = "", jmespath_options: Optional[Dict] = None, logger: Optional[Union[logging.Logger, Logger]] = None, + sdk_client: Optional[AppConfigDataClient] = None, ): """This class fetches JSON schemas from AWS AppConfig @@ -48,6 +55,8 @@ def __init__( Alternative JMESPath options to be included when filtering expr logger: A logging object Used to log messages. If None is supplied, one will be created. + sdk_client: Optional[AppConfigDataClient] + AppConfigData boto3 client, `sdk_config` will be ignored if this value is provided. """ super().__init__() self.logger = logger or logging.getLogger(__name__) @@ -58,7 +67,12 @@ def __init__( self.config = sdk_config self.envelope = envelope self.jmespath_options = jmespath_options - self._conf_store = AppConfigProvider(environment=environment, application=application, config=sdk_config) + self._conf_store = AppConfigProvider( + environment=environment, + application=application, + config=sdk_config, + boto3_client=sdk_client, + ) @property def get_raw_configuration(self) -> Dict[str, Any]: From 32dcb12dfbe6ce6fee062e96e6d893b6d88a3820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Tom=C3=A1s?= <22175056+adriantomas@users.noreply.github.com> Date: Wed, 10 Jul 2024 12:04:28 +0200 Subject: [PATCH 2/6] style: fix typing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adrián Tomás <22175056+adriantomas@users.noreply.github.com> --- aws_lambda_powertools/utilities/feature_flags/appconfig.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/appconfig.py b/aws_lambda_powertools/utilities/feature_flags/appconfig.py index 8b80ec5d081..48fbf375933 100644 --- a/aws_lambda_powertools/utilities/feature_flags/appconfig.py +++ b/aws_lambda_powertools/utilities/feature_flags/appconfig.py @@ -2,7 +2,6 @@ import traceback from typing import TYPE_CHECKING, Any, Dict, Optional, Union, cast -from botocore.client import BaseClient from botocore.config import Config from aws_lambda_powertools.utilities import jmespath_utils @@ -18,8 +17,6 @@ if TYPE_CHECKING: from mypy_boto3_appconfigdata import AppConfigDataClient -else: - AppConfigDataClient = BaseClient class AppConfigStore(StoreProvider): @@ -33,7 +30,7 @@ def __init__( envelope: Optional[str] = "", jmespath_options: Optional[Dict] = None, logger: Optional[Union[logging.Logger, Logger]] = None, - sdk_client: Optional[AppConfigDataClient] = None, + sdk_client: Optional["AppConfigDataClient"] = None, ): """This class fetches JSON schemas from AWS AppConfig From 0c5be23b75ca2bcc3a742fe422df2a39d52f89c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Tom=C3=A1s?= <22175056+adriantomas@users.noreply.github.com> Date: Wed, 10 Jul 2024 12:05:07 +0200 Subject: [PATCH 3/6] test: modify tests including sdk_client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adrián Tomás <22175056+adriantomas@users.noreply.github.com> --- .../_boto3/test_feature_flags.py | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/tests/functional/feature_flags/_boto3/test_feature_flags.py b/tests/functional/feature_flags/_boto3/test_feature_flags.py index cc6aa60aaac..faa022d3a95 100644 --- a/tests/functional/feature_flags/_boto3/test_feature_flags.py +++ b/tests/functional/feature_flags/_boto3/test_feature_flags.py @@ -1,7 +1,12 @@ +from io import BytesIO +from json import dumps from typing import Dict, List, Optional +import boto3 import pytest from botocore.config import Config +from botocore.response import StreamingBody +from botocore.stub import Stubber from aws_lambda_powertools.utilities.feature_flags import ( ConfigurationStoreError, @@ -37,17 +42,46 @@ def init_feature_flags( envelope: str = "", jmespath_options: Optional[Dict] = None, ) -> FeatureFlags: - mocked_get_conf = mocker.patch("aws_lambda_powertools.utilities.parameters.AppConfigProvider.get") - mocked_get_conf.return_value = mock_schema + environment = "test_env" + application = "test_app" + name = "test_conf_name" + configuration_token = "foo" + mock_schema_to_bytes = dumps(mock_schema).encode() + + client = boto3.client("appconfigdata", config=config) + stubber = Stubber(client) + + stubber.add_response( + method="start_configuration_session", + expected_params={ + "ConfigurationProfileIdentifier": name, + "ApplicationIdentifier": application, + "EnvironmentIdentifier": environment, + }, + service_response={"InitialConfigurationToken": configuration_token}, + ) + stubber.add_response( + method="get_latest_configuration", + expected_params={"ConfigurationToken": configuration_token}, + service_response={ + "Configuration": StreamingBody( + raw_stream=BytesIO(mock_schema_to_bytes), + content_length=len(mock_schema_to_bytes), + ), + "NextPollConfigurationToken": configuration_token, + }, + ) + stubber.activate() app_conf_fetcher = AppConfigStore( - environment="test_env", - application="test_app", - name="test_conf_name", + environment=environment, + application=application, + name=name, max_age=600, sdk_config=config, envelope=envelope, jmespath_options=jmespath_options, + sdk_client=client, ) feature_flags: FeatureFlags = FeatureFlags(store=app_conf_fetcher) return feature_flags From 0d91b0f8e3f983e0e5a4eda27df3895937f26f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Tom=C3=A1s?= <22175056+adriantomas@users.noreply.github.com> Date: Wed, 10 Jul 2024 12:05:36 +0200 Subject: [PATCH 4/6] docs: add sdk_client to feature flags docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adrián Tomás <22175056+adriantomas@users.noreply.github.com> --- docs/utilities/feature_flags.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index 57069681a72..18b7b8cc9d7 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -496,16 +496,17 @@ AppConfig store provider fetches any JSON document from AWS AppConfig. These are the available options for further customization. -| Parameter | Default | Description | -| -------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **environment** | `""` | AWS AppConfig Environment, e.g. `dev` | -| **application** | `""` | AWS AppConfig Application, e.g. `product-catalogue` | -| **name** | `""` | AWS AppConfig Configuration name, e.g `features` | -| **envelope** | `None` | JMESPath expression to use to extract feature flags configuration from AWS AppConfig configuration | -| **max_age** | `5` | Number of seconds to cache feature flags configuration fetched from AWS AppConfig | -| **sdk_config** | `None` | [Botocore Config object](https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html){target="_blank"} | +| Parameter | Default | Description | +| -------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **environment** | `""` | AWS AppConfig Environment, e.g. `dev` | +| **application** | `""` | AWS AppConfig Application, e.g. `product-catalogue` | +| **name** | `""` | AWS AppConfig Configuration name, e.g `features` | +| **envelope** | `None` | JMESPath expression to use to extract feature flags configuration from AWS AppConfig configuration | +| **max_age** | `5` | Number of seconds to cache feature flags configuration fetched from AWS AppConfig | +| **sdk_config** | `None` | [Botocore Config object](https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html){target="_blank"} | | **jmespath_options** | `None` | For advanced use cases when you want to bring your own [JMESPath functions](https://github.com/jmespath/jmespath.py#custom-functions){target="_blank" rel="nofollow"} | -| **logger** | `logging.Logger` | Logger to use for debug. You can optionally supply an instance of Powertools for AWS Lambda (Python) Logger. | +| **logger** | `logging.Logger` | Logger to use for debug. You can optionally supply an instance of Powertools for AWS Lambda (Python) Logger. | +| **sdk_client** | `None` | [AppConfigData boto3 client](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/appconfigdata.html#AppConfigData.Client){target="_blank"} | === "appconfig_provider_options.py" From 07c9f8f4ad605ecea09e60860cb1062a56563ddf Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Wed, 10 Jul 2024 12:07:04 +0100 Subject: [PATCH 5/6] Adding some small changes --- .../utilities/feature_flags/appconfig.py | 20 +++++++++---- docs/utilities/feature_flags.md | 26 +++++++++++++++-- .../src/appconfig_provider_options.py | 2 +- .../src/custom_boto_client_feature_flags.py | 29 +++++++++++++++++++ .../src/custom_boto_config_feature_flags.py | 29 +++++++++++++++++++ .../src/custom_boto_session_feature_flags.py | 29 +++++++++++++++++++ .../_boto3/test_feature_flags.py | 4 +-- 7 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 examples/feature_flags/src/custom_boto_client_feature_flags.py create mode 100644 examples/feature_flags/src/custom_boto_config_feature_flags.py create mode 100644 examples/feature_flags/src/custom_boto_session_feature_flags.py diff --git a/aws_lambda_powertools/utilities/feature_flags/appconfig.py b/aws_lambda_powertools/utilities/feature_flags/appconfig.py index 48fbf375933..f828577dd69 100644 --- a/aws_lambda_powertools/utilities/feature_flags/appconfig.py +++ b/aws_lambda_powertools/utilities/feature_flags/appconfig.py @@ -2,6 +2,7 @@ import traceback from typing import TYPE_CHECKING, Any, Dict, Optional, Union, cast +import boto3 from botocore.config import Config from aws_lambda_powertools.utilities import jmespath_utils @@ -30,7 +31,9 @@ def __init__( envelope: Optional[str] = "", jmespath_options: Optional[Dict] = None, logger: Optional[Union[logging.Logger, Logger]] = None, - sdk_client: Optional["AppConfigDataClient"] = None, + boto_config: Optional[Config] = None, + boto3_session: Optional[boto3.session.Session] = None, + boto3_client: Optional["AppConfigDataClient"] = None, ): """This class fetches JSON schemas from AWS AppConfig @@ -52,8 +55,12 @@ def __init__( Alternative JMESPath options to be included when filtering expr logger: A logging object Used to log messages. If None is supplied, one will be created. - sdk_client: Optional[AppConfigDataClient] - AppConfigData boto3 client, `sdk_config` will be ignored if this value is provided. + boto_config: botocore.config.Config, optional + Botocore configuration to pass during client initialization + boto3_session : boto3.Session, optional + Boto3 session to use for AWS API communication + boto3_client : AppConfigDataClient, optional + Boto3 AppConfigDataClient Client to use, boto3_session and boto_config will be ignored if both are provided """ super().__init__() self.logger = logger or logging.getLogger(__name__) @@ -61,14 +68,15 @@ def __init__( self.application = application self.name = name self.cache_seconds = max_age - self.config = sdk_config + self.config = sdk_config or boto_config self.envelope = envelope self.jmespath_options = jmespath_options self._conf_store = AppConfigProvider( environment=environment, application=application, - config=sdk_config, - boto3_client=sdk_client, + config=sdk_config or boto_config, + boto3_client=boto3_client, + boto3_session=boto3_session, ) @property diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index 18b7b8cc9d7..2d95e025b06 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -503,10 +503,11 @@ These are the available options for further customization. | **name** | `""` | AWS AppConfig Configuration name, e.g `features` | | **envelope** | `None` | JMESPath expression to use to extract feature flags configuration from AWS AppConfig configuration | | **max_age** | `5` | Number of seconds to cache feature flags configuration fetched from AWS AppConfig | -| **sdk_config** | `None` | [Botocore Config object](https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html){target="_blank"} | | **jmespath_options** | `None` | For advanced use cases when you want to bring your own [JMESPath functions](https://github.com/jmespath/jmespath.py#custom-functions){target="_blank" rel="nofollow"} | | **logger** | `logging.Logger` | Logger to use for debug. You can optionally supply an instance of Powertools for AWS Lambda (Python) Logger. | -| **sdk_client** | `None` | [AppConfigData boto3 client](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/appconfigdata.html#AppConfigData.Client){target="_blank"} | +| **boto3_client** | `None` | [AppConfigData boto3 client](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/appconfigdata.html#AppConfigData.Client){target="_blank"} | +| **boto3_session** | `None` | [Boto3 session](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html){target="_blank"} | +| **boto_config** | `None` | [Botocore config](https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html){target="_blank"} | === "appconfig_provider_options.py" @@ -526,6 +527,27 @@ These are the available options for further customization. --8<-- "examples/feature_flags/src/appconfig_provider_options_features.json" ``` +#### Customizing boto configuration + + +The **`boto_config`** , **`boto3_session`**, and **`boto3_client`** parameters enable you to pass in a custom [botocore config object](https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html){target="_blank"}, [boto3 session](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html){target="_blank"}, or a [boto3 client](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/boto3.html){target="_blank"} when constructing the AppConfig store provider. + + +=== "custom_boto_session_feature_flags.py" + ```python hl_lines="8 14" + --8<-- "examples/feature_flags/src/custom_boto_session_feature_flags.py" + ``` + +=== "custom_boto_config_feature_flags.py" + ```python hl_lines="8 14" + --8<-- "examples/feature_flags/src/custom_boto_config_feature_flags.py" + ``` + +=== "custom_boto_client_feature_flags.py" + ```python hl_lines="8 14" + --8<-- "examples/feature_flags/src/custom_boto_client_feature_flags.py" + ``` + ### Create your own store provider You can create your own custom FeatureFlags store provider by inheriting the `StoreProvider` class, and implementing both `get_raw_configuration()` and `get_configuration()` methods to retrieve the configuration from your custom store. diff --git a/examples/feature_flags/src/appconfig_provider_options.py b/examples/feature_flags/src/appconfig_provider_options.py index 8a41f651fc9..43df7e85da7 100644 --- a/examples/feature_flags/src/appconfig_provider_options.py +++ b/examples/feature_flags/src/appconfig_provider_options.py @@ -26,7 +26,7 @@ def _func_special_decoder(self, features): name="features", max_age=120, envelope="special_decoder(features)", # using a custom function defined in CustomFunctions Class - sdk_config=boto_config, + boto_config=boto_config, jmespath_options=custom_jmespath_options, ) diff --git a/examples/feature_flags/src/custom_boto_client_feature_flags.py b/examples/feature_flags/src/custom_boto_client_feature_flags.py new file mode 100644 index 00000000000..88f0c6f2bc1 --- /dev/null +++ b/examples/feature_flags/src/custom_boto_client_feature_flags.py @@ -0,0 +1,29 @@ +from typing import Any + +import boto3 + +from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags +from aws_lambda_powertools.utilities.typing import LambdaContext + +boto3_client = boto3.client("ssm") + +app_config = AppConfigStore( + environment="dev", + application="product-catalogue", + name="features", + boto3_client=boto3_client, +) + +feature_flags = FeatureFlags(store=app_config) + + +def lambda_handler(event: dict, context: LambdaContext): + apply_discount: Any = feature_flags.evaluate(name="ten_percent_off_campaign", default=False) + + price: Any = event.get("price") + + if apply_discount: + # apply 10% discount to product + price = price * 0.9 + + return {"price": price} diff --git a/examples/feature_flags/src/custom_boto_config_feature_flags.py b/examples/feature_flags/src/custom_boto_config_feature_flags.py new file mode 100644 index 00000000000..d736a297d13 --- /dev/null +++ b/examples/feature_flags/src/custom_boto_config_feature_flags.py @@ -0,0 +1,29 @@ +from typing import Any + +from botocore.config import Config + +from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags +from aws_lambda_powertools.utilities.typing import LambdaContext + +boto_config = Config(read_timeout=10, retries={"total_max_attempts": 2}) + +app_config = AppConfigStore( + environment="dev", + application="product-catalogue", + name="features", + boto_config=boto_config, +) + +feature_flags = FeatureFlags(store=app_config) + + +def lambda_handler(event: dict, context: LambdaContext): + apply_discount: Any = feature_flags.evaluate(name="ten_percent_off_campaign", default=False) + + price: Any = event.get("price") + + if apply_discount: + # apply 10% discount to product + price = price * 0.9 + + return {"price": price} diff --git a/examples/feature_flags/src/custom_boto_session_feature_flags.py b/examples/feature_flags/src/custom_boto_session_feature_flags.py new file mode 100644 index 00000000000..a83f81d5c6c --- /dev/null +++ b/examples/feature_flags/src/custom_boto_session_feature_flags.py @@ -0,0 +1,29 @@ +from typing import Any + +import boto3 + +from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags +from aws_lambda_powertools.utilities.typing import LambdaContext + +boto3_session = boto3.session.Session() + +app_config = AppConfigStore( + environment="dev", + application="product-catalogue", + name="features", + boto3_session=boto3_session, +) + +feature_flags = FeatureFlags(store=app_config) + + +def lambda_handler(event: dict, context: LambdaContext): + apply_discount: Any = feature_flags.evaluate(name="ten_percent_off_campaign", default=False) + + price: Any = event.get("price") + + if apply_discount: + # apply 10% discount to product + price = price * 0.9 + + return {"price": price} diff --git a/tests/functional/feature_flags/_boto3/test_feature_flags.py b/tests/functional/feature_flags/_boto3/test_feature_flags.py index faa022d3a95..08035f2989f 100644 --- a/tests/functional/feature_flags/_boto3/test_feature_flags.py +++ b/tests/functional/feature_flags/_boto3/test_feature_flags.py @@ -78,10 +78,10 @@ def init_feature_flags( application=application, name=name, max_age=600, - sdk_config=config, envelope=envelope, jmespath_options=jmespath_options, - sdk_client=client, + boto_config=config, + boto3_client=client, ) feature_flags: FeatureFlags = FeatureFlags(store=app_conf_fetcher) return feature_flags From d747a57d3505dce8642368b5c89146491745f19c Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Wed, 10 Jul 2024 14:16:55 +0100 Subject: [PATCH 6/6] Addressing Adrian's feedback --- examples/feature_flags/src/custom_boto_client_feature_flags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/feature_flags/src/custom_boto_client_feature_flags.py b/examples/feature_flags/src/custom_boto_client_feature_flags.py index 88f0c6f2bc1..d8a90061bce 100644 --- a/examples/feature_flags/src/custom_boto_client_feature_flags.py +++ b/examples/feature_flags/src/custom_boto_client_feature_flags.py @@ -5,7 +5,7 @@ from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags from aws_lambda_powertools.utilities.typing import LambdaContext -boto3_client = boto3.client("ssm") +boto3_client = boto3.client("appconfigdata") app_config = AppConfigStore( environment="dev",