Skip to content

Commit 10d551c

Browse files
feat(feature_flags): allow customers to bring their own boto3 client and session (aws-powertools#4717)
* feat: add `sdk_client` argument to AppConfigStore `__init__` Signed-off-by: Adrián Tomás <[email protected]> * style: fix typing Signed-off-by: Adrián Tomás <[email protected]> * test: modify tests including sdk_client Signed-off-by: Adrián Tomás <[email protected]> * docs: add sdk_client to feature flags docs Signed-off-by: Adrián Tomás <[email protected]> * Adding some small changes * Addressing Adrian's feedback --------- Signed-off-by: Adrián Tomás <[email protected]> Co-authored-by: Leandro Damascena <[email protected]>
1 parent 14b44ce commit 10d551c

File tree

7 files changed

+182
-19
lines changed

7 files changed

+182
-19
lines changed

aws_lambda_powertools/utilities/feature_flags/appconfig.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import logging
22
import traceback
3-
from typing import Any, Dict, Optional, Union, cast
3+
from typing import TYPE_CHECKING, Any, Dict, Optional, Union, cast
44

5+
import boto3
56
from botocore.config import Config
67

78
from aws_lambda_powertools.utilities import jmespath_utils
@@ -15,6 +16,9 @@
1516
from .base import StoreProvider
1617
from .exceptions import ConfigurationStoreError, StoreClientError
1718

19+
if TYPE_CHECKING:
20+
from mypy_boto3_appconfigdata import AppConfigDataClient
21+
1822

1923
class AppConfigStore(StoreProvider):
2024
def __init__(
@@ -27,6 +31,9 @@ def __init__(
2731
envelope: Optional[str] = "",
2832
jmespath_options: Optional[Dict] = None,
2933
logger: Optional[Union[logging.Logger, Logger]] = None,
34+
boto_config: Optional[Config] = None,
35+
boto3_session: Optional[boto3.session.Session] = None,
36+
boto3_client: Optional["AppConfigDataClient"] = None,
3037
):
3138
"""This class fetches JSON schemas from AWS AppConfig
3239
@@ -48,17 +55,29 @@ def __init__(
4855
Alternative JMESPath options to be included when filtering expr
4956
logger: A logging object
5057
Used to log messages. If None is supplied, one will be created.
58+
boto_config: botocore.config.Config, optional
59+
Botocore configuration to pass during client initialization
60+
boto3_session : boto3.Session, optional
61+
Boto3 session to use for AWS API communication
62+
boto3_client : AppConfigDataClient, optional
63+
Boto3 AppConfigDataClient Client to use, boto3_session and boto_config will be ignored if both are provided
5164
"""
5265
super().__init__()
5366
self.logger = logger or logging.getLogger(__name__)
5467
self.environment = environment
5568
self.application = application
5669
self.name = name
5770
self.cache_seconds = max_age
58-
self.config = sdk_config
71+
self.config = sdk_config or boto_config
5972
self.envelope = envelope
6073
self.jmespath_options = jmespath_options
61-
self._conf_store = AppConfigProvider(environment=environment, application=application, config=sdk_config)
74+
self._conf_store = AppConfigProvider(
75+
environment=environment,
76+
application=application,
77+
config=sdk_config or boto_config,
78+
boto3_client=boto3_client,
79+
boto3_session=boto3_session,
80+
)
6281

6382
@property
6483
def get_raw_configuration(self) -> Dict[str, Any]:

docs/utilities/feature_flags.md

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -496,16 +496,18 @@ AppConfig store provider fetches any JSON document from AWS AppConfig.
496496

497497
These are the available options for further customization.
498498

499-
| Parameter | Default | Description |
500-
| -------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
501-
| **environment** | `""` | AWS AppConfig Environment, e.g. `dev` |
502-
| **application** | `""` | AWS AppConfig Application, e.g. `product-catalogue` |
503-
| **name** | `""` | AWS AppConfig Configuration name, e.g `features` |
504-
| **envelope** | `None` | JMESPath expression to use to extract feature flags configuration from AWS AppConfig configuration |
505-
| **max_age** | `5` | Number of seconds to cache feature flags configuration fetched from AWS AppConfig |
506-
| **sdk_config** | `None` | [Botocore Config object](https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html){target="_blank"} |
499+
| Parameter | Default | Description |
500+
| -------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
501+
| **environment** | `""` | AWS AppConfig Environment, e.g. `dev` |
502+
| **application** | `""` | AWS AppConfig Application, e.g. `product-catalogue` |
503+
| **name** | `""` | AWS AppConfig Configuration name, e.g `features` |
504+
| **envelope** | `None` | JMESPath expression to use to extract feature flags configuration from AWS AppConfig configuration |
505+
| **max_age** | `5` | Number of seconds to cache feature flags configuration fetched from AWS AppConfig |
507506
| **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"} |
508-
| **logger** | `logging.Logger` | Logger to use for debug. You can optionally supply an instance of Powertools for AWS Lambda (Python) Logger. |
507+
| **logger** | `logging.Logger` | Logger to use for debug. You can optionally supply an instance of Powertools for AWS Lambda (Python) Logger. |
508+
| **boto3_client** | `None` | [AppConfigData boto3 client](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/appconfigdata.html#AppConfigData.Client){target="_blank"} |
509+
| **boto3_session** | `None` | [Boto3 session](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html){target="_blank"} |
510+
| **boto_config** | `None` | [Botocore config](https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html){target="_blank"} |
509511

510512
=== "appconfig_provider_options.py"
511513

@@ -525,6 +527,27 @@ These are the available options for further customization.
525527
--8<-- "examples/feature_flags/src/appconfig_provider_options_features.json"
526528
```
527529

530+
#### Customizing boto configuration
531+
532+
<!-- markdownlint-disable MD013 -->
533+
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.
534+
<!-- markdownlint-enable MD013 -->
535+
536+
=== "custom_boto_session_feature_flags.py"
537+
```python hl_lines="8 14"
538+
--8<-- "examples/feature_flags/src/custom_boto_session_feature_flags.py"
539+
```
540+
541+
=== "custom_boto_config_feature_flags.py"
542+
```python hl_lines="8 14"
543+
--8<-- "examples/feature_flags/src/custom_boto_config_feature_flags.py"
544+
```
545+
546+
=== "custom_boto_client_feature_flags.py"
547+
```python hl_lines="8 14"
548+
--8<-- "examples/feature_flags/src/custom_boto_client_feature_flags.py"
549+
```
550+
528551
### Create your own store provider
529552

530553
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.

examples/feature_flags/src/appconfig_provider_options.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def _func_special_decoder(self, features):
2626
name="features",
2727
max_age=120,
2828
envelope="special_decoder(features)", # using a custom function defined in CustomFunctions Class
29-
sdk_config=boto_config,
29+
boto_config=boto_config,
3030
jmespath_options=custom_jmespath_options,
3131
)
3232

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from typing import Any
2+
3+
import boto3
4+
5+
from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags
6+
from aws_lambda_powertools.utilities.typing import LambdaContext
7+
8+
boto3_client = boto3.client("appconfigdata")
9+
10+
app_config = AppConfigStore(
11+
environment="dev",
12+
application="product-catalogue",
13+
name="features",
14+
boto3_client=boto3_client,
15+
)
16+
17+
feature_flags = FeatureFlags(store=app_config)
18+
19+
20+
def lambda_handler(event: dict, context: LambdaContext):
21+
apply_discount: Any = feature_flags.evaluate(name="ten_percent_off_campaign", default=False)
22+
23+
price: Any = event.get("price")
24+
25+
if apply_discount:
26+
# apply 10% discount to product
27+
price = price * 0.9
28+
29+
return {"price": price}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from typing import Any
2+
3+
from botocore.config import Config
4+
5+
from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags
6+
from aws_lambda_powertools.utilities.typing import LambdaContext
7+
8+
boto_config = Config(read_timeout=10, retries={"total_max_attempts": 2})
9+
10+
app_config = AppConfigStore(
11+
environment="dev",
12+
application="product-catalogue",
13+
name="features",
14+
boto_config=boto_config,
15+
)
16+
17+
feature_flags = FeatureFlags(store=app_config)
18+
19+
20+
def lambda_handler(event: dict, context: LambdaContext):
21+
apply_discount: Any = feature_flags.evaluate(name="ten_percent_off_campaign", default=False)
22+
23+
price: Any = event.get("price")
24+
25+
if apply_discount:
26+
# apply 10% discount to product
27+
price = price * 0.9
28+
29+
return {"price": price}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from typing import Any
2+
3+
import boto3
4+
5+
from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags
6+
from aws_lambda_powertools.utilities.typing import LambdaContext
7+
8+
boto3_session = boto3.session.Session()
9+
10+
app_config = AppConfigStore(
11+
environment="dev",
12+
application="product-catalogue",
13+
name="features",
14+
boto3_session=boto3_session,
15+
)
16+
17+
feature_flags = FeatureFlags(store=app_config)
18+
19+
20+
def lambda_handler(event: dict, context: LambdaContext):
21+
apply_discount: Any = feature_flags.evaluate(name="ten_percent_off_campaign", default=False)
22+
23+
price: Any = event.get("price")
24+
25+
if apply_discount:
26+
# apply 10% discount to product
27+
price = price * 0.9
28+
29+
return {"price": price}

tests/functional/feature_flags/_boto3/test_feature_flags.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
from io import BytesIO
2+
from json import dumps
13
from typing import Dict, List, Optional
24

5+
import boto3
36
import pytest
47
from botocore.config import Config
8+
from botocore.response import StreamingBody
9+
from botocore.stub import Stubber
510

611
from aws_lambda_powertools.utilities.feature_flags import (
712
ConfigurationStoreError,
@@ -37,17 +42,46 @@ def init_feature_flags(
3742
envelope: str = "",
3843
jmespath_options: Optional[Dict] = None,
3944
) -> FeatureFlags:
40-
mocked_get_conf = mocker.patch("aws_lambda_powertools.utilities.parameters.AppConfigProvider.get")
41-
mocked_get_conf.return_value = mock_schema
45+
environment = "test_env"
46+
application = "test_app"
47+
name = "test_conf_name"
48+
configuration_token = "foo"
49+
mock_schema_to_bytes = dumps(mock_schema).encode()
50+
51+
client = boto3.client("appconfigdata", config=config)
52+
stubber = Stubber(client)
53+
54+
stubber.add_response(
55+
method="start_configuration_session",
56+
expected_params={
57+
"ConfigurationProfileIdentifier": name,
58+
"ApplicationIdentifier": application,
59+
"EnvironmentIdentifier": environment,
60+
},
61+
service_response={"InitialConfigurationToken": configuration_token},
62+
)
63+
stubber.add_response(
64+
method="get_latest_configuration",
65+
expected_params={"ConfigurationToken": configuration_token},
66+
service_response={
67+
"Configuration": StreamingBody(
68+
raw_stream=BytesIO(mock_schema_to_bytes),
69+
content_length=len(mock_schema_to_bytes),
70+
),
71+
"NextPollConfigurationToken": configuration_token,
72+
},
73+
)
74+
stubber.activate()
4275

4376
app_conf_fetcher = AppConfigStore(
44-
environment="test_env",
45-
application="test_app",
46-
name="test_conf_name",
77+
environment=environment,
78+
application=application,
79+
name=name,
4780
max_age=600,
48-
sdk_config=config,
4981
envelope=envelope,
5082
jmespath_options=jmespath_options,
83+
boto_config=config,
84+
boto3_client=client,
5185
)
5286
feature_flags: FeatureFlags = FeatureFlags(store=app_conf_fetcher)
5387
return feature_flags

0 commit comments

Comments
 (0)