Skip to content

Commit b6787f9

Browse files
committed
feat: add get_parameter utility
1 parent 0c75ef1 commit b6787f9

File tree

5 files changed

+204
-4
lines changed

5 files changed

+204
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""General utilities for Powertools"""
4+
5+
6+
from .parameters import get_parameter
7+
8+
__all__ = ["get_parameter"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""
2+
Parameter retrieval and caching utility
3+
"""
4+
5+
6+
from collections import namedtuple
7+
from datetime import datetime, timedelta
8+
9+
import boto3
10+
11+
DEFAULT_MAX_AGE = 5
12+
ExpirableValue = namedtuple("ExpirableValue", ["value", "ttl"])
13+
PARAMETER_VALUES = {}
14+
ssm = boto3.client("ssm")
15+
16+
17+
def get_parameter(name: str, max_age: int = DEFAULT_MAX_AGE) -> str:
18+
"""
19+
Retrieve a parameter from the AWS Systems Manager (SSM) Parameter Store
20+
21+
This will keep a local version in cache for `max_age` seconds to prevent
22+
overfetching from SSM Parameter Store.
23+
24+
See the [AWS Systems Manager Parameter Store documentation]
25+
(https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html)
26+
for more information.
27+
28+
Parameters
29+
----------
30+
name: str
31+
Name of the SSM Parameter
32+
max_age: int
33+
Duration for which the parameter value can be cached
34+
35+
Example
36+
-------
37+
38+
from aws_lambda_powertools.utilities import get_parameter
39+
40+
def lambda_handler(event, context):
41+
# This will only make a call to the SSM service every 30 seconds.
42+
value = get_parameter("my-parameter", max_age=30)
43+
44+
Raises
45+
------
46+
ssm.exceptions.InternalServerError
47+
When there is an internal server error from AWS Systems Manager
48+
ssm.exceptions.InvalidKeyId
49+
When the key ID is invalid
50+
ssm.exceptions.ParameterNotFound
51+
When the parameter name is not found in AWS Systems Manager
52+
ssm.exceptions.ParameterVersionNotFound
53+
When a version of the parameter is not found in AWS Systems Manager
54+
"""
55+
56+
if name not in PARAMETER_VALUES or PARAMETER_VALUES[name].ttl < datetime.now():
57+
# Retrieve the parameter from AWS Systems Manager
58+
parameter = ssm.get_parameter(Name=name)
59+
PARAMETER_VALUES[name] = ExpirableValue(
60+
parameter["Parameter"]["Value"], datetime.now() + timedelta(seconds=max_age)
61+
)
62+
63+
return PARAMETER_VALUES[name].value

poetry.lock

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ license = "MIT-0"
2121
python = "^3.6"
2222
aws-xray-sdk = "^2.5.0"
2323
fastjsonschema = "~=2.14.4"
24+
boto3 = "^1.12"
2425

2526
[tool.poetry.dev-dependencies]
2627
coverage = {extras = ["toml"], version = "^5.0.3"}

tests/functional/test_utilities.py

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import random
2+
import string
3+
from datetime import datetime, timedelta
4+
5+
import pytest
6+
from botocore import stub
7+
8+
from aws_lambda_powertools import utilities
9+
from aws_lambda_powertools.utilities import parameters
10+
11+
12+
@pytest.fixture(scope="function")
13+
def mock_name():
14+
# Parameter name must match [a-zA-Z0-9_.-/]+
15+
return "".join(random.choices(string.ascii_letters + string.digits + "_.-/", k=random.randrange(3, 200)))
16+
17+
18+
@pytest.fixture(scope="function")
19+
def mock_value():
20+
# Standard parameters can be up to 4 KB
21+
return "".join(random.choices(string.printable, k=random.randrange(100, 4000)))
22+
23+
24+
@pytest.fixture(scope="function")
25+
def mock_version():
26+
return random.randrange(1, 1000)
27+
28+
29+
def test_get_parameter_new(monkeypatch, mock_name, mock_value, mock_version):
30+
"""
31+
Test get_parameter() with a new parameter name
32+
"""
33+
34+
# Patch the parameter value store
35+
monkeypatch.setattr(parameters, "PARAMETER_VALUES", {})
36+
37+
# Stub boto3
38+
stubber = stub.Stubber(parameters.ssm)
39+
response = {
40+
"Parameter": {
41+
"Name": mock_name,
42+
"Type": "String",
43+
"Value": mock_value,
44+
"Version": mock_version,
45+
"Selector": f"{mock_name}:{mock_version}",
46+
"SourceResult": "string",
47+
"LastModifiedDate": datetime(2015, 1, 1),
48+
"ARN": f"arn:aws:ssm:us-east-2:111122223333:parameter/{mock_name}",
49+
}
50+
}
51+
expected_params = {"Name": mock_name}
52+
stubber.add_response("get_parameter", response, expected_params)
53+
stubber.activate()
54+
55+
# Get the parameter value
56+
try:
57+
value = utilities.get_parameter(mock_name)
58+
59+
assert value == mock_value
60+
stubber.assert_no_pending_responses()
61+
finally:
62+
stubber.deactivate()
63+
64+
65+
def test_get_parameter_cached(monkeypatch, mock_name, mock_value, mock_version):
66+
"""
67+
Test get_parameter() with a cached value for parameter name
68+
"""
69+
70+
# Patch the parameter value store
71+
monkeypatch.setattr(
72+
parameters,
73+
"PARAMETER_VALUES",
74+
{mock_name: parameters.ExpirableValue(mock_value, datetime.now() + timedelta(seconds=60))},
75+
)
76+
77+
# Stub boto3
78+
stubber = stub.Stubber(parameters.ssm)
79+
stubber.activate()
80+
81+
# Get the parameter value
82+
try:
83+
value = utilities.get_parameter(mock_name)
84+
85+
assert value == mock_value
86+
stubber.assert_no_pending_responses()
87+
finally:
88+
stubber.deactivate()
89+
90+
91+
def test_get_parameter_expired(monkeypatch, mock_name, mock_value, mock_version):
92+
"""
93+
Test get_parameter() with a cached, but expired value for parameter name
94+
"""
95+
96+
# Patch the parameter value store
97+
monkeypatch.setattr(
98+
parameters,
99+
"PARAMETER_VALUES",
100+
{mock_name: parameters.ExpirableValue(mock_value, datetime.now() - timedelta(seconds=60))},
101+
)
102+
103+
# Stub boto3
104+
stubber = stub.Stubber(parameters.ssm)
105+
response = {
106+
"Parameter": {
107+
"Name": mock_name,
108+
"Type": "String",
109+
"Value": mock_value,
110+
"Version": mock_version,
111+
"Selector": f"{mock_name}:{mock_version}",
112+
"SourceResult": "string",
113+
"LastModifiedDate": datetime(2015, 1, 1),
114+
"ARN": f"arn:aws:ssm:us-east-2:111122223333:parameter/{mock_name}",
115+
}
116+
}
117+
expected_params = {"Name": mock_name}
118+
stubber.add_response("get_parameter", response, expected_params)
119+
stubber.activate()
120+
121+
# Get the parameter value
122+
try:
123+
value = utilities.get_parameter(mock_name)
124+
125+
assert value == mock_value
126+
stubber.assert_no_pending_responses()
127+
finally:
128+
stubber.deactivate()

0 commit comments

Comments
 (0)