Skip to content

Commit 27b4dc3

Browse files
authored
Suggestions (#3)
* Rename python-aws-ssm to aws-ssm Adding Python to a Python package is like a snake eating itself. * README suggestions People may not get involved if it's not obvious how to run tests. * Fix retrieval of SSM keys that do not exist Using the retrieved SSM parameters means excluding non-existant SSM keys. So we need to start from the provided SSM key names. * Add custom SSM client example * Improve docs and typing * Update package name in README * Improve type to reflect nested dictionaries * Update README with development instructions * Update License and authors * Rename the package :( and small fixes
1 parent b98a154 commit 27b4dc3

File tree

5 files changed

+97
-19
lines changed

5 files changed

+97
-19
lines changed

Makefile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
.PHONY: setup
32
setup: ## Create a virtual environment for the project and installs all dependencies
43
poetry install
@@ -17,7 +16,7 @@ lint: ## Check the .py files with Mypy, Flake8 and Black
1716

1817
.PHONY: tests
1918
tests: ## Run tests with pytest and create the coverage report
20-
poetry run pytest --cov=./ --cov-report=xml
19+
PYTHONPATH=${PYTHONPATH}:. poetry run pytest --cov=./ --cov-report=xml
2120

2221
.SILENT: help
2322
help: ## Shows all available commands

README.md

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,24 @@ Perfect use case for this package is when secure parameters for an application a
1212
[AWS Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html)
1313
using a path hierarchy. During application startup you can use this package to fetch them and use them in your application.
1414

15+
## Warning
16+
17+
The SSM service is rate-limited by default. We strongly suggest using
18+
retrieving SSM keys by path, e.g. via `ParameterStore.get_parameters_by_path()`.
19+
This requires grouping keys by a useful path but reduces the chance of
20+
your own services being rate-limited in turn.
21+
1522
## Install
1623
```bash
17-
pip install python-aws-ssm
24+
pip install aws-ssm
1825
```
1926

2027
## Examples
2128

2229
#### Basic Usage
2330

2431
```python
25-
from python_aws_ssm.parameters import ParameterStore
32+
from aws_ssm.parameters import ParameterStore
2633

2734
# Assuming you have the parameters in the following format:
2835
# my-service/dev/param-1 -> with value `a`
@@ -38,7 +45,7 @@ value = parameters.get("param-1")
3845
#### Recursive and nested options
3946

4047
```python
41-
from python_aws_ssm.parameters import ParameterStore
48+
from aws_ssm.parameters import ParameterStore
4249

4350
# Assuming you have the parameters in the following format:
4451
# my-service/dev/param-1 -> with value `a`
@@ -56,7 +63,7 @@ dev_parameters = parameters.get("dev")
5663
#### Get parameters by name
5764

5865
```python
59-
from python_aws_ssm.parameters import ParameterStore
66+
from aws_ssm.parameters import ParameterStore
6067

6168
# Assuming you have the parameters in the following format:
6269
# my-service/dev/param-1 -> with value `a`
@@ -70,3 +77,52 @@ parameters = parameter_store.get_parameters(
7077
dev_parameters = parameters.get("/common/dev/param-2")
7178
# value should be `b`
7279
```
80+
81+
#### With custom client
82+
83+
```python
84+
from aws_ssm.parameters import ParameterStore
85+
import boto3
86+
87+
# Initialise an SSM client to specify the source of the credentials.
88+
# e.g. locally a profile would be more likely; an AWS Lambda would most
89+
# likely not override the credentials source.
90+
ssm_client = boto3.Session(profile_name='dev').client('ssm')
91+
parameter_store = ParameterStore(ssm_client)
92+
93+
parameters = parameter_store.get_parameters(["/service/path/"])
94+
```
95+
96+
## Development
97+
98+
If you are missing any features or have found a bug, please open a PR or a new Github issue.
99+
100+
101+
#### Setup
102+
This project uses Poetry to manage the dependencies and the virtual environment.
103+
Follow the instructions from Poetry website (https://poetry.eustace.io/docs/#installation) to configure your local environment.
104+
105+
After completing the Poetry setup, the virtual environment can be created running:
106+
```shell
107+
make setup
108+
```
109+
110+
#### Tests
111+
Tests are run by Pytest
112+
```shell
113+
make test
114+
```
115+
116+
#### Code style
117+
- Mypy is used for type annotations (https://github.com/python/mypy)
118+
- Black formatter (https://github.com/psf/black) is used to keep the coding style consistent.
119+
- Isort (https://github.com/timothycrosley/isort) is used to sort the imports.
120+
To format the codebase just run:
121+
```shell
122+
make format
123+
```
124+
and to check it before pushing:
125+
```shell
126+
make lint
127+
```
128+

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
name = "python-aws-ssm"
33
version = "0.1.0"
44
description = "Python package that interfaces with AWS System Manager"
5-
authors = ["Luca Valentini <[email protected]>"]
5+
authors = ["Luca Valentini <[email protected]>", "Maarten Jacobs <[email protected]>"]
66
# New attributes
7-
license = "Apache License 2.0"
7+
license = "Apache-2.0"
88
readme = "README.md"
99
homepage = "https://github.com/PaddleHQ/python-aws-ssm"
1010
repository = "https://github.com/PaddleHQ/python-aws-ssm"

python_aws_ssm/parameters.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,41 @@ def get_parameters(self, ssm_key_names: List[str]) -> Dict[str, Optional[str]]:
1919
returned in the result dict.
2020
"""
2121

22-
parameters = self.client.get_parameters(
22+
retrieved_parameters = self.client.get_parameters(
2323
Names=ssm_key_names, WithDecryption=True
2424
).get("Parameters")
25-
return {
26-
parameter.get("Name"): parameter.get("Value")
27-
for parameter in parameters
28-
if parameter.get("Name") in ssm_key_names
25+
26+
# Initialise the result so that missing keys have a None value.
27+
filled_parameters: Dict[str, Optional[str]] = {
28+
parameter_name: None for parameter_name in ssm_key_names
2929
}
3030

31+
# Merge the retrieved parameters in.
32+
for retrieved in retrieved_parameters:
33+
if retrieved.get("Name") in ssm_key_names:
34+
filled_parameters[retrieved.get("Name")] = retrieved.get("Value")
35+
36+
return filled_parameters
37+
3138
def get_parameters_by_path(
3239
self,
3340
ssm_base_path: str,
3441
with_decryption: bool = True,
3542
recursive: bool = False,
3643
nested: bool = False,
37-
) -> Dict[str, Optional[str]]:
44+
) -> Dict[str, Union[Dict, Optional[str]]]:
3845
"""
3946
Retrieve all the keys under a certain path on SSM.
40-
* Wnen recursive is set to False, SSM doesn't parameters under a nested path.
47+
* When recursive is set to False, SSM doesn't return keys under a nested path.
4148
e.g.: /{ssm_base_path}/foo/bar will not return 'bar' nor '/foo/bar'.
4249
* When recursive and nested are set to True, a nested dictionary is returned.
4350
e.g.: /{ssm_base_path}/foo/bar will return {"foo": {"bar": "value"}}
4451
* When nested is set to False, the full subpath is returned as key.
4552
e.g.: /{ssm_base_path}/foo/bar will return {"foo/bar": "value"}}}
53+
54+
:return If nested=False, a dictionary of string to optional string value.
55+
If nested=True, a dictionary of string to potentially nested dictionaries with
56+
optional string values.
4657
"""
4758

4859
parameters = self.client.get_parameters_by_path(
@@ -55,21 +66,29 @@ def get_parameters_by_path(
5566
}
5667

5768
return (
58-
self._parse_parameters(parameters) if recursive and nested else parameters
69+
# Non-nested is the default behaviour (hence `else parameters`).
70+
self._parse_parameters(parameters)
71+
if recursive and nested
72+
else parameters
5973
)
6074

6175
@staticmethod
6276
def _parse_parameters(
6377
parameters: Dict[str, Optional[str]]
64-
) -> Dict[Union[Dict, str], Optional[Union[Dict, str]]]:
65-
parsed_dict: Dict[Union[Dict, str], Optional[Union[Dict, str]]] = {}
78+
) -> Dict[str, Union[Dict, Optional[str]]]:
79+
parsed_dict: Dict[str, Union[Dict, Optional[str]]] = {}
6680
for key, value in parameters.items():
6781
nested_dict = ParameterStore._tree_dict(key.split("/"), value)
6882
parsed_dict = ParameterStore._deep_merge(parsed_dict, nested_dict)
6983
return parsed_dict
7084

7185
@staticmethod
7286
def _tree_dict(key_list: List[Any], value: Optional[Any]) -> Dict[Any, Any]:
87+
"""
88+
Build a nested dictionary path from a list of keys and a value.
89+
For example:
90+
_tree_dict(["foo", "bar", "koo"], 42) ==> {"foo": {"bar": {"koo": 42}}}
91+
"""
7392
tree_dict: Dict[Any, Any] = {key_list[-1]: value}
7493
for key in reversed(key_list[:-1]):
7594
tree_dict = {key: tree_dict}

tests/test_python_aws_ssm.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ def test_get_parameters_keys_are_mapped(self):
3030
)
3131

3232
self.assertEqual(
33-
{"foo_ssm_key_1": "foo_ssm_value_1", "foo_ssm_key_3": "foo_ssm_value_3"},
33+
{
34+
"foo_ssm_key_1": "foo_ssm_value_1",
35+
"foo_ssm_key_2": None,
36+
"foo_ssm_key_3": "foo_ssm_value_3",
37+
},
3438
secrets,
3539
)
3640

0 commit comments

Comments
 (0)