Skip to content

Commit 9c09099

Browse files
feat(parameters): transform = "auto" (aws-powertools#133)
* feat(parameters): transform = "auto" Add a new tranform type called "auto", which looks at the key name to autodetect the encoding of the values in dynamodb or the parameter store. Keys ending with ".json" are assumed to be "json" string Keys ending with ".binary" are assumed to be "binary" base64 decode strings * tests: include some additional tests and docs * feat(parameters): add list of supported transforms Changes: * base.py - Add `SUPPORTED_TRANSFORM_METHODS` as a list of supported transform methods and add constants for the currently supported methods * base.py - Update `get_transform_method` to use `SUPPORTED_TRANSFORM_METHODS` to check for supported extensions * feat: Make `transform` optional Changes: * Make `transform` optional in `get_transform_method` * Update tests to include this use case
1 parent 1951574 commit 9c09099

File tree

2 files changed

+124
-5
lines changed

2 files changed

+124
-5
lines changed

aws_lambda_powertools/utilities/parameters/base.py

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
ExpirableValue = namedtuple("ExpirableValue", ["value", "ttl"])
1616
# These providers will be dynamically initialized on first use of the helper functions
1717
DEFAULT_PROVIDERS = {}
18+
TRANSFORM_METHOD_JSON = "json"
19+
TRANSFORM_METHOD_BINARY = "binary"
20+
SUPPORTED_TRANSFORM_METHODS = [TRANSFORM_METHOD_JSON, TRANSFORM_METHOD_BINARY]
1821

1922

2023
class BaseProvider(ABC):
@@ -115,8 +118,8 @@ def get_multiple(
115118
Maximum age of the cached value
116119
transform: str, optional
117120
Optional transformation of the parameter value. Supported values
118-
are "json" for JSON strings and "binary" for base 64 encoded
119-
values.
121+
are "json" for JSON strings, "binary" for base 64 encoded
122+
values or "auto" which looks at the attribute key to determine the type.
120123
raise_on_transform_error: bool, optional
121124
Raises an exception if any transform fails, otherwise this will
122125
return a None value for each transform that failed
@@ -145,7 +148,11 @@ def get_multiple(
145148

146149
if transform is not None:
147150
for (key, value) in values.items():
148-
values[key] = transform_value(value, transform, raise_on_transform_error)
151+
_transform = get_transform_method(key, transform)
152+
if _transform is None:
153+
continue
154+
155+
values[key] = transform_value(value, _transform, raise_on_transform_error)
149156

150157
self.store[key] = ExpirableValue(values, datetime.now() + timedelta(seconds=max_age),)
151158

@@ -159,6 +166,45 @@ def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]:
159166
raise NotImplementedError()
160167

161168

169+
def get_transform_method(key: str, transform: Optional[str] = None) -> Optional[str]:
170+
"""
171+
Determine the transform method
172+
173+
Examples
174+
-------
175+
>>> get_transform_method("key", "any_other_value")
176+
'any_other_value'
177+
>>> get_transform_method("key.json", "auto")
178+
'json'
179+
>>> get_transform_method("key.binary", "auto")
180+
'binary'
181+
>>> get_transform_method("key", "auto")
182+
None
183+
>>> get_transform_method("key", None)
184+
None
185+
186+
Parameters
187+
---------
188+
key: str
189+
Only used when the tranform is "auto".
190+
transform: str, optional
191+
Original transform method, only "auto" will try to detect the transform method by the key
192+
193+
Returns
194+
------
195+
Optional[str]:
196+
The transform method either when transform is "auto" then None, "json" or "binary" is returned
197+
or the original transform method
198+
"""
199+
if transform != "auto":
200+
return transform
201+
202+
for transform_method in SUPPORTED_TRANSFORM_METHODS:
203+
if key.endswith("." + transform_method):
204+
return transform_method
205+
return None
206+
207+
162208
def transform_value(value: str, transform: str, raise_on_transform_error: bool = True) -> Union[dict, bytes, None]:
163209
"""
164210
Apply a transform to a value
@@ -180,9 +226,9 @@ def transform_value(value: str, transform: str, raise_on_transform_error: bool =
180226
"""
181227

182228
try:
183-
if transform == "json":
229+
if transform == TRANSFORM_METHOD_JSON:
184230
return json.loads(value)
185-
elif transform == "binary":
231+
elif transform == TRANSFORM_METHOD_BINARY:
186232
return base64.b64decode(value)
187233
else:
188234
raise ValueError(f"Invalid transform type '{transform}'")

tests/functional/test_utilities_parameters.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,48 @@ def test_dynamodb_provider_get_multiple(mock_name, mock_value, config):
233233
stubber.deactivate()
234234

235235

236+
def test_dynamodb_provider_get_multiple_auto(mock_name, mock_value, config):
237+
"""
238+
Test DynamoDBProvider.get_multiple() with transform = "auto"
239+
"""
240+
mock_binary = mock_value.encode()
241+
mock_binary_data = base64.b64encode(mock_binary).decode()
242+
mock_json_data = json.dumps({mock_name: mock_value})
243+
mock_params = {"D.json": mock_json_data, "E.binary": mock_binary_data, "F": mock_value}
244+
table_name = "TEST_TABLE_AUTO"
245+
246+
# Create a new provider
247+
provider = parameters.DynamoDBProvider(table_name, config=config)
248+
249+
# Stub the boto3 client
250+
stubber = stub.Stubber(provider.table.meta.client)
251+
response = {
252+
"Items": [
253+
{"id": {"S": mock_name}, "sk": {"S": name}, "value": {"S": value}} for (name, value) in mock_params.items()
254+
]
255+
}
256+
expected_params = {"TableName": table_name, "KeyConditionExpression": Key("id").eq(mock_name)}
257+
stubber.add_response("query", response, expected_params)
258+
stubber.activate()
259+
260+
try:
261+
values = provider.get_multiple(mock_name, transform="auto")
262+
263+
stubber.assert_no_pending_responses()
264+
265+
assert len(values) == len(mock_params)
266+
for key in mock_params.keys():
267+
assert key in values
268+
if key.endswith(".json"):
269+
assert values[key][mock_name] == mock_value
270+
elif key.endswith(".binary"):
271+
assert values[key] == mock_binary
272+
else:
273+
assert values[key] == mock_value
274+
finally:
275+
stubber.deactivate()
276+
277+
236278
def test_dynamodb_provider_get_multiple_next_token(mock_name, mock_value, config):
237279
"""
238280
Test DynamoDBProvider.get_multiple() with a non-cached path
@@ -1481,3 +1523,34 @@ def test_transform_value_ignore_error(mock_value):
14811523
value = parameters.base.transform_value(mock_value, "INCORRECT", raise_on_transform_error=False)
14821524

14831525
assert value is None
1526+
1527+
1528+
@pytest.mark.parametrize("original_transform", ["json", "binary", "other", "Auto", None])
1529+
def test_get_transform_method_preserve_original(original_transform):
1530+
"""
1531+
Check if original transform method is returned for anything other than "auto"
1532+
"""
1533+
transform = parameters.base.get_transform_method("key", original_transform)
1534+
1535+
assert transform == original_transform
1536+
1537+
1538+
@pytest.mark.parametrize("extension", ["json", "binary"])
1539+
def test_get_transform_method_preserve_auto(extension, mock_name):
1540+
"""
1541+
Check if we can auto detect the transform method by the support extensions json / binary
1542+
"""
1543+
transform = parameters.base.get_transform_method(f"{mock_name}.{extension}", "auto")
1544+
1545+
assert transform == extension
1546+
1547+
1548+
@pytest.mark.parametrize("key", ["json", "binary", "example", "example.jsonp"])
1549+
def test_get_transform_method_preserve_auto_unhandled(key):
1550+
"""
1551+
Check if any key that does not end with a supported extension returns None when
1552+
using the transform="auto"
1553+
"""
1554+
transform = parameters.base.get_transform_method(key, "auto")
1555+
1556+
assert transform is None

0 commit comments

Comments
 (0)