Skip to content

Commit d2673d8

Browse files
committed
feat(parameters): Allow settings boto3.client() arguments
closes aws-powertools#1079
1 parent 8ca082f commit d2673d8

File tree

5 files changed

+147
-12
lines changed

5 files changed

+147
-12
lines changed

aws_lambda_powertools/utilities/parameters/appconfig.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ class AppConfigProvider(BaseProvider):
3030
config: botocore.config.Config, optional
3131
Botocore configuration to pass during client initialization
3232
boto3_session : boto3.session.Session, optional
33-
Boto3 session to use for AWS API communication
33+
Boto3 session to use for AWS API communication, will not be used if boto3_client is not None
34+
boto3_client: AppConfigClient, optional
35+
Boto3 Client to use for AWS API communication, will be used instead of boto3_session if both provided
3436
3537
Example
3638
-------
@@ -68,14 +70,19 @@ def __init__(
6870
application: Optional[str] = None,
6971
config: Optional[Config] = None,
7072
boto3_session: Optional[boto3.session.Session] = None,
73+
boto3_client=None,
7174
):
7275
"""
7376
Initialize the App Config client
7477
"""
7578

7679
config = config or Config()
77-
session = boto3_session or boto3.session.Session()
78-
self.client = session.client("appconfig", config=config)
80+
if boto3_client is not None:
81+
self.client = boto3_client
82+
else:
83+
session = boto3_session or boto3.session.Session()
84+
self.client = session.client("appconfig", config=config)
85+
7986
self.application = resolve_env_var_choice(
8087
choice=application, env=os.getenv(constants.SERVICE_NAME_ENV, "service_undefined")
8188
)

aws_lambda_powertools/utilities/parameters/secrets.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ class SecretsProvider(BaseProvider):
2020
config: botocore.config.Config, optional
2121
Botocore configuration to pass during client initialization
2222
boto3_session : boto3.session.Session, optional
23-
Boto3 session to use for AWS API communication
23+
Boto3 session to use for AWS API communication, will not be used if boto3_client is not None
24+
boto3_client: SecretsManagerClient, optional
25+
Boto3 Client to use for AWS API communication, will be used instead of boto3_session if both provided
2426
2527
Example
2628
-------
@@ -60,14 +62,19 @@ class SecretsProvider(BaseProvider):
6062

6163
client: Any = None
6264

63-
def __init__(self, config: Optional[Config] = None, boto3_session: Optional[boto3.session.Session] = None):
65+
def __init__(
66+
self, config: Optional[Config] = None, boto3_session: Optional[boto3.session.Session] = None, boto3_client=None
67+
):
6468
"""
6569
Initialize the Secrets Manager client
6670
"""
6771

6872
config = config or Config()
69-
session = boto3_session or boto3.session.Session()
70-
self.client = session.client("secretsmanager", config=config)
73+
if boto3_client is not None:
74+
self.client = boto3_client
75+
else:
76+
session = boto3_session or boto3.session.Session()
77+
self.client = session.client("secretsmanager", config=config)
7178

7279
super().__init__()
7380

aws_lambda_powertools/utilities/parameters/ssm.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ class SSMProvider(BaseProvider):
2020
config: botocore.config.Config, optional
2121
Botocore configuration to pass during client initialization
2222
boto3_session : boto3.session.Session, optional
23-
Boto3 session to use for AWS API communication
23+
Boto3 session to use for AWS API communication, will not be used if boto3_client is not None
24+
boto3_client: SSMClient, optional
25+
Boto3 Client to use for AWS API communication, will be used instead of boto3_session if both provided
2426
2527
Example
2628
-------
@@ -76,14 +78,19 @@ class SSMProvider(BaseProvider):
7678

7779
client: Any = None
7880

79-
def __init__(self, config: Optional[Config] = None, boto3_session: Optional[boto3.session.Session] = None):
81+
def __init__(
82+
self, config: Optional[Config] = None, boto3_session: Optional[boto3.session.Session] = None, boto3_client=None
83+
):
8084
"""
8185
Initialize the SSM Parameter Store client
8286
"""
8387

8488
config = config or Config()
85-
session = boto3_session or boto3.session.Session()
86-
self.client = session.client("ssm", config=config)
89+
if boto3_client is not None:
90+
self.client = boto3_client
91+
else:
92+
session = boto3_session or boto3.session.Session()
93+
self.client = session.client("ssm", config=config)
8794

8895
super().__init__()
8996

docs/utilities/parameters.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,7 @@ Here is the mapping between this utility's functions and methods and the underly
482482

483483
### Customizing boto configuration
484484

485-
The **`config`** and **`boto3_session`** parameters enable you to pass in a custom [botocore config object](https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html) or a custom [boto3 session](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html) when constructing any of the built-in provider classes.
485+
The **`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) , [boto3 session](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html), or a [boto3 client](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/boto3.html) when constructing any of the built-in provider classes.
486486

487487
???+ tip
488488
You can use a custom session for retrieving parameters cross-account/region and for snapshot testing.
@@ -510,6 +510,20 @@ The **`config`** and **`boto3_session`** parameters enable you to pass in a cust
510510
boto_config = Config()
511511
ssm_provider = parameters.SSMProvider(config=boto_config)
512512

513+
def handler(event, context):
514+
# Retrieve a single parameter
515+
value = ssm_provider.get("/my/parameter")
516+
...
517+
```
518+
=== "Custom client"
519+
520+
```python hl_lines="2 4 5"
521+
from aws_lambda_powertools.utilities import parameters
522+
import boto3
523+
524+
boto3_client= session.client(service_name="ssm", endpoint_url='custom_endpoint')
525+
ssm_provider = parameters.SSMProvider(boto3_client=boto3_client)
526+
513527
def handler(event, context):
514528
# Retrieve a single parameter
515529
value = ssm_provider.get("/my/parameter")

tests/functional/test_utilities_parameters.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from io import BytesIO
77
from typing import Dict
88

9+
import boto3
910
import pytest
1011
from boto3.dynamodb.conditions import Key
1112
from botocore import stub
@@ -444,6 +445,43 @@ def test_ssm_provider_get(mock_name, mock_value, mock_version, config):
444445
stubber.deactivate()
445446

446447

448+
def test_ssm_provider_get_with_custom_client(mock_name, mock_value, mock_version, config):
449+
"""
450+
Test SSMProvider.get() with a non-cached value
451+
"""
452+
453+
client = boto3.client("ssm", config=config)
454+
455+
# Create a new provider
456+
provider = parameters.SSMProvider(boto3_client=client)
457+
458+
# Stub the boto3 client
459+
stubber = stub.Stubber(provider.client)
460+
response = {
461+
"Parameter": {
462+
"Name": mock_name,
463+
"Type": "String",
464+
"Value": mock_value,
465+
"Version": mock_version,
466+
"Selector": f"{mock_name}:{mock_version}",
467+
"SourceResult": "string",
468+
"LastModifiedDate": datetime(2015, 1, 1),
469+
"ARN": f"arn:aws:ssm:us-east-2:111122223333:parameter/{mock_name}",
470+
}
471+
}
472+
expected_params = {"Name": mock_name, "WithDecryption": False}
473+
stubber.add_response("get_parameter", response, expected_params)
474+
stubber.activate()
475+
476+
try:
477+
value = provider.get(mock_name)
478+
479+
assert value == mock_value
480+
stubber.assert_no_pending_responses()
481+
finally:
482+
stubber.deactivate()
483+
484+
447485
def test_ssm_provider_get_default_config(monkeypatch, mock_name, mock_value, mock_version):
448486
"""
449487
Test SSMProvider.get() without specifying the config
@@ -925,6 +963,37 @@ def test_secrets_provider_get(mock_name, mock_value, config):
925963
stubber.deactivate()
926964

927965

966+
def test_secrets_provider_get_with_custom_client(mock_name, mock_value, config):
967+
"""
968+
Test SecretsProvider.get() with a non-cached value
969+
"""
970+
client = boto3.client("secretsmanager", config=config)
971+
972+
# Create a new provider
973+
provider = parameters.SecretsProvider(boto3_client=client)
974+
975+
# Stub the boto3 client
976+
stubber = stub.Stubber(provider.client)
977+
response = {
978+
"ARN": f"arn:aws:secretsmanager:us-east-1:132456789012:secret/{mock_name}",
979+
"Name": mock_name,
980+
"VersionId": "7a9155b8-2dc9-466e-b4f6-5bc46516c84d",
981+
"SecretString": mock_value,
982+
"CreatedDate": datetime(2015, 1, 1),
983+
}
984+
expected_params = {"SecretId": mock_name}
985+
stubber.add_response("get_secret_value", response, expected_params)
986+
stubber.activate()
987+
988+
try:
989+
value = provider.get(mock_name)
990+
991+
assert value == mock_value
992+
stubber.assert_no_pending_responses()
993+
finally:
994+
stubber.deactivate()
995+
996+
928997
def test_secrets_provider_get_default_config(monkeypatch, mock_name, mock_value):
929998
"""
930999
Test SecretsProvider.get() without specifying a config
@@ -1555,6 +1624,37 @@ def test_appconf_provider_get_configuration_json_content_type(mock_name, config)
15551624
stubber.deactivate()
15561625

15571626

1627+
def test_appconf_provider_get_configuration_json_content_type_with_custom_client(mock_name, config):
1628+
"""
1629+
Test get_configuration.get with default values
1630+
"""
1631+
1632+
client = boto3.client("appconfig", config=config)
1633+
1634+
# Create a new provider
1635+
environment = "dev"
1636+
application = "myapp"
1637+
provider = parameters.AppConfigProvider(environment=environment, application=application, boto3_client=client)
1638+
1639+
mock_body_json = {"myenvvar1": "Black Panther", "myenvvar2": 3}
1640+
encoded_message = json.dumps(mock_body_json).encode("utf-8")
1641+
mock_value = StreamingBody(BytesIO(encoded_message), len(encoded_message))
1642+
1643+
# Stub the boto3 client
1644+
stubber = stub.Stubber(provider.client)
1645+
response = {"Content": mock_value, "ConfigurationVersion": "1", "ContentType": "application/json"}
1646+
stubber.add_response("get_configuration", response)
1647+
stubber.activate()
1648+
1649+
try:
1650+
value = provider.get(mock_name, transform="json", ClientConfigurationVersion="2")
1651+
1652+
assert value == mock_body_json
1653+
stubber.assert_no_pending_responses()
1654+
finally:
1655+
stubber.deactivate()
1656+
1657+
15581658
def test_appconf_provider_get_configuration_no_transform(mock_name, config):
15591659
"""
15601660
Test appconfigprovider.get with default values

0 commit comments

Comments
 (0)