Skip to content

Commit 9c78458

Browse files
feat: add parameter utility (#96)
* feat: add get_parameter utility * fix: add AWS_DEFAULT_REGION for boto3 tests * revert "fix: add AWS_DEFAULT_REGION for boto3 tests" This reverts commit 29d27b8. * fix: fix AWS_DEFAULT_REGION for get_parameter tests * fix: fix AWS_DEFAULT_REGION for get_parameter tests * chore: rename _get_from_external_store to _get * feat: add get_multiple for parameter providers * tests: increase test coverage * tests: increase test coverage (2) * tests: increase coverage to 100% * fix: add get_parameters in __all__ * chore: split parameter utilities into smaller files * feat: use botocore.config.Config for parameter providers * feat: make arguments explicits in parameter utilities * docs: add examples for parameter utilities * feat: add override SDK options for parameter utilities * docs: add examples for shorthands in the parameter utility * fix: fix typo in DynamoDB parameter example * feat: throw exception on failed transform for parameter utility * docs: add examples on how to retrieve parameters in the parameter utility * feat: use paginator for SSM parameter utility * feat: make SSM parameter provider recursive by default * feat: move sort_attr to init for DynamoDB parameter provider * feat: add 'raise_on_transform_error' for get_multiple parameter utility * docs: add sdk_options to parameters for get and get_multiple * docs: add documentation for parameters utility * docs: add passing arguments to SDK * docs: restructure based on feedback * docs: tweaks based on feedback * improv: iam permissions table Merge both high level and class provider functions and methods, since they require the same IAM permission. Co-authored-by: Heitor Lessa <[email protected]>
1 parent 8621d4e commit 9c78458

File tree

14 files changed

+2654
-4
lines changed

14 files changed

+2654
-4
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray,
1313
* **[Logging](https://awslabs.github.io/aws-lambda-powertools-python/core/logger/)** - Structured logging made easier, and decorator to enrich structured logging with key Lambda context details
1414
* **[Metrics](https://awslabs.github.io/aws-lambda-powertools-python/core/metrics/)** - Custom Metrics created asynchronously via CloudWatch Embedded Metric Format (EMF)
1515
* **[Bring your own middleware](https://awslabs.github.io/aws-lambda-powertools-python/utilities/middleware_factory/)** - Decorator factory to create your own middleware to run logic before, and after each Lambda invocation
16+
* **[Parameters utility](https://awslabs.github.io/aws-lambda-powertools-python/utilities/parameters/)** - Retrieve and cache parameter values from Parameter Store, Secrets Manager, or DynamoDB
1617

1718
### Installation
1819

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""General utilities for Powertools"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
Parameter retrieval and caching utility
5+
"""
6+
7+
from .base import BaseProvider
8+
from .dynamodb import DynamoDBProvider
9+
from .exceptions import GetParameterError, TransformParameterError
10+
from .secrets import SecretsProvider, get_secret
11+
from .ssm import SSMProvider, get_parameter, get_parameters
12+
13+
__all__ = [
14+
"BaseProvider",
15+
"GetParameterError",
16+
"DynamoDBProvider",
17+
"SecretsProvider",
18+
"SSMProvider",
19+
"TransformParameterError",
20+
"get_parameter",
21+
"get_parameters",
22+
"get_secret",
23+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
"""
2+
Base for Parameter providers
3+
"""
4+
5+
import base64
6+
import json
7+
from abc import ABC, abstractmethod
8+
from collections import namedtuple
9+
from datetime import datetime, timedelta
10+
from typing import Dict, Optional, Union
11+
12+
from .exceptions import GetParameterError, TransformParameterError
13+
14+
DEFAULT_MAX_AGE_SECS = 5
15+
ExpirableValue = namedtuple("ExpirableValue", ["value", "ttl"])
16+
# These providers will be dynamically initialized on first use of the helper functions
17+
DEFAULT_PROVIDERS = {}
18+
19+
20+
class BaseProvider(ABC):
21+
"""
22+
Abstract Base Class for Parameter providers
23+
"""
24+
25+
store = None
26+
27+
def __init__(self):
28+
"""
29+
Initialize the base provider
30+
"""
31+
32+
self.store = {}
33+
34+
def get(
35+
self, name: str, max_age: int = DEFAULT_MAX_AGE_SECS, transform: Optional[str] = None, **sdk_options
36+
) -> Union[str, list, dict, bytes]:
37+
"""
38+
Retrieve a parameter value or return the cached value
39+
40+
Parameters
41+
----------
42+
name: str
43+
Parameter name
44+
max_age: int
45+
Maximum age of the cached value
46+
transform: str
47+
Optional transformation of the parameter value. Supported values
48+
are "json" for JSON strings and "binary" for base 64 encoded
49+
values.
50+
sdk_options: dict, optional
51+
Arguments that will be passed directly to the underlying API call
52+
53+
Raises
54+
------
55+
GetParameterError
56+
When the parameter provider fails to retrieve a parameter value for
57+
a given name.
58+
TransformParameterError
59+
When the parameter provider fails to transform a parameter value.
60+
"""
61+
62+
# If there are multiple calls to the same parameter but in a different
63+
# transform, they will be stored multiple times. This allows us to
64+
# optimize by transforming the data only once per retrieval, thus there
65+
# is no need to transform cached values multiple times. However, this
66+
# means that we need to make multiple calls to the underlying parameter
67+
# store if we need to return it in different transforms. Since the number
68+
# of supported transform is small and the probability that a given
69+
# parameter will always be used in a specific transform, this should be
70+
# an acceptable tradeoff.
71+
key = (name, transform)
72+
73+
if key not in self.store or self.store[key].ttl < datetime.now():
74+
try:
75+
value = self._get(name, **sdk_options)
76+
# Encapsulate all errors into a generic GetParameterError
77+
except Exception as exc:
78+
raise GetParameterError(str(exc))
79+
80+
if transform is not None:
81+
value = transform_value(value, transform)
82+
83+
self.store[key] = ExpirableValue(value, datetime.now() + timedelta(seconds=max_age),)
84+
85+
return self.store[key].value
86+
87+
@abstractmethod
88+
def _get(self, name: str, **sdk_options) -> str:
89+
"""
90+
Retrieve paramater value from the underlying parameter store
91+
"""
92+
raise NotImplementedError()
93+
94+
def get_multiple(
95+
self,
96+
path: str,
97+
max_age: int = DEFAULT_MAX_AGE_SECS,
98+
transform: Optional[str] = None,
99+
raise_on_transform_error: bool = False,
100+
**sdk_options,
101+
) -> Union[Dict[str, str], Dict[str, dict], Dict[str, bytes]]:
102+
"""
103+
Retrieve multiple parameters based on a path prefix
104+
105+
Parameters
106+
----------
107+
path: str
108+
Parameter path used to retrieve multiple parameters
109+
max_age: int, optional
110+
Maximum age of the cached value
111+
transform: str, optional
112+
Optional transformation of the parameter value. Supported values
113+
are "json" for JSON strings and "binary" for base 64 encoded
114+
values.
115+
raise_on_transform_error: bool, optional
116+
Raises an exception if any transform fails, otherwise this will
117+
return a None value for each transform that failed
118+
sdk_options: dict, optional
119+
Arguments that will be passed directly to the underlying API call
120+
121+
Raises
122+
------
123+
GetParameterError
124+
When the parameter provider fails to retrieve parameter values for
125+
a given path.
126+
TransformParameterError
127+
When the parameter provider fails to transform a parameter value.
128+
"""
129+
130+
key = (path, transform)
131+
132+
if key not in self.store or self.store[key].ttl < datetime.now():
133+
try:
134+
values = self._get_multiple(path, **sdk_options)
135+
# Encapsulate all errors into a generic GetParameterError
136+
except Exception as exc:
137+
raise GetParameterError(str(exc))
138+
139+
if transform is not None:
140+
new_values = {}
141+
for key, value in values.items():
142+
try:
143+
new_values[key] = transform_value(value, transform)
144+
except Exception as exc:
145+
if raise_on_transform_error:
146+
raise exc
147+
else:
148+
new_values[key] = None
149+
150+
values = new_values
151+
152+
self.store[key] = ExpirableValue(values, datetime.now() + timedelta(seconds=max_age),)
153+
154+
return self.store[key].value
155+
156+
@abstractmethod
157+
def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]:
158+
"""
159+
Retrieve multiple parameter values from the underlying parameter store
160+
"""
161+
raise NotImplementedError()
162+
163+
164+
def transform_value(value: str, transform: str) -> Union[dict, bytes]:
165+
"""
166+
Apply a transform to a value
167+
168+
Parameters
169+
---------
170+
value: str
171+
Parameter alue to transform
172+
transform: str
173+
Type of transform, supported values are "json" and "binary"
174+
175+
Raises
176+
------
177+
TransformParameterError:
178+
When the parameter value could not be transformed
179+
"""
180+
181+
try:
182+
if transform == "json":
183+
return json.loads(value)
184+
elif transform == "binary":
185+
return base64.b64decode(value)
186+
else:
187+
raise ValueError(f"Invalid transform type '{transform}'")
188+
189+
except Exception as exc:
190+
raise TransformParameterError(str(exc))

0 commit comments

Comments
 (0)