Skip to content

Commit

Permalink
Merge pull request #32 from markusressel/feature/#29_output_current_c…
Browse files Browse the repository at this point in the history
…onfig

Feature/#29 output current config
  • Loading branch information
markusressel authored Oct 22, 2019
2 parents 8b26ba0 + 5c21337 commit 06096f4
Show file tree
Hide file tree
Showing 16 changed files with 346 additions and 23 deletions.
80 changes: 78 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ constructor parameter to `True`.
If you want to allow setting a `None` value even if the default value
is **not** `None`, you have to explicitly set `required=False`.

## Secret values

If your config contains secret values like passwords you can mark them
as such using the `secret=True` constructor parameter. That way their
value will be redacted when [printing the current configuration](#print-current-config).

## Data sources

**container-app-conf** supports the simultaneous use of multiple data
Expand Down Expand Up @@ -107,11 +113,18 @@ env_key = "_".join(key_path).upper()

yields `MY_APP_EXAMPLE`.

### YamlSource

### Filesystem Source

Multiple data sources using the filesystem are available:

* YamlSource
* TomlSource
* JsonSource

#### File paths

By default the `YamlSource` looks for a YAML config file in multiple
By default config files are searched for in multiple
directories that are commonly used for configuration files which include:

- `./`
Expand Down Expand Up @@ -140,6 +153,69 @@ config1 = AppConfig(singleton=False)
config2 = AppConfig(singleton=False)
```

## Print current config

Oftentimes it can be useful to print the current configuration of an
application. To do this you can use

```python
config = AppConfig()
config.print()
```

which will result in an output similar to this:

```text
test->bool: _REDACTED_
test->this->date->is->nested->deep: 2019-10-22T04:21:02.316907
test->this->is->a->range: [0..100]
test->this->is->a->list: None
test->this->timediff->is->in->this->branch: 0:00:10
test->directory: None
test->file: None
test->float: 1.23
test->int: 100
test->regex: ^[a-zA-Z0-9]$
test->string: default value
secret->list: _REDACTED_
secret->regex: _REDACTED_
```

If you don't like the style you can specify a custom `ConfigFormatter`
like this:

```python
from container_app_conf.formatter.toml import TomlFormatter
config = AppConfig()
config.print(TomlFormatter())
```

Which would output the same config like this:

```text
[test]
bool = "_REDACTED_"
directory = "None"
file = "None"
float = 1.23
int = 100
regex = "^[a-zA-Z0-9]$"
string = "default value"
[secret]
list = "_REDACTED_"
regex = "_REDACTED_"
[test.this.is.a]
range = "[0..100]"
[test.this.date.is.nested]
deep = "2019-10-22T04:26:10.654541"
[test.this.timediff.is.in.this]
branch = "0:00:10"
```

## Generate reference config

**container-app-conf** will (by default) generate a reference config
Expand Down
15 changes: 14 additions & 1 deletion container_app_conf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@

from container_app_conf.const import DEFAULT_CONFIG_FILE_PATHS
from container_app_conf.entry import ConfigEntry
from container_app_conf.formatter import ConfigFormatter, SimpleFormatter
from container_app_conf.source import DataSource
from container_app_conf.util import find_duplicates, generate_reference_config
from container_app_conf.util import find_duplicates, generate_reference_config, config_entries_to_dict

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -114,6 +115,18 @@ def validate(self):
for entry in self._config_entries.values():
entry.value = entry.value

def print(self, formatter: ConfigFormatter = None) -> str:
"""
Prints
:return: printable description of the current configuration
"""
if formatter is None:
formatter = SimpleFormatter()

data = config_entries_to_dict(list(self._config_entries.values()), hide_secrets=True)
output = formatter.format(data)
return output

def _find_config_entries(self) -> Dict[str, ConfigEntry]:
"""
Detects config entry constants in this class
Expand Down
9 changes: 8 additions & 1 deletion container_app_conf/entry/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ def __init__(self, item_type: Type[ConfigEntry], key_path: [str], example: any =
"""
if item_args is None:
item_args = {}

# pass "secret" value to item if necessary
if secret:
item_args["secret"] = secret

self._item_entry = item_type(key_path=["dummy"], **item_args)
self.delimiter = delimiter if delimiter is not None else ","

Expand All @@ -55,7 +60,7 @@ def __init__(self, item_type: Type[ConfigEntry], key_path: [str], example: any =
example=example,
default=default,
required=required,
secret=secret,
secret=secret
)

@property
Expand All @@ -81,6 +86,8 @@ def _value_to_type(self, value: any) -> [any] or None:
return list(map(lambda x: self._item_entry._value_to_type(x), filter(lambda x: x, value)))

def _type_to_value(self, type: list or str) -> any:
if type is None:
return None
if isinstance(type, str):
return type
str_items = list(map(lambda x: self._item_entry._type_to_value(x), type))
Expand Down
15 changes: 8 additions & 7 deletions container_app_conf/entry/regex.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,14 @@ def _value_to_type(self, value: any) -> Pattern or None:
if value is None and self._required:
return None

if isinstance(value, Pattern) and self.flags is not None:
if value.flags == int(self.flags):
return value
else:
raise ValueError("Value does not match expected flags: {}".format(self.flags))

value = str(value)
if isinstance(value, Pattern):
if self.flags is not None:
if value.flags == int(self.flags):
return value
else:
raise ValueError("Value does not match expected flags: {}".format(self.flags))
else:
value = str(value)
return re.compile(value, flags=self.flags if self.flags is not None else 0)

def _type_to_value(self, type: any) -> str:
Expand Down
57 changes: 57 additions & 0 deletions container_app_conf/formatter/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Copyright (c) 2019 Markus Ressel
# .
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# .
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# .
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


class ConfigFormatter:
"""
Allows config entries to be formatted into a string
"""

def format(self, data: dict) -> str:
"""
Formats the given entry data
:param data: entries to format
:return: formatted string
"""
raise NotImplementedError()


class SimpleFormatter(ConfigFormatter):
"""
Prints all config entries in a human readable manner
"""

def format(self, data: dict) -> str:
return "\n".join(self._format(data)).strip()

def _format(self, data: dict, prefix: str = "") -> [str]:
"""
Recursively formats the dictionary
:param data:
:return:
"""
lines = []
for key, value in data.items():
output = prefix + key
if isinstance(value, dict):
lines.extend(self._format(value, "{}->".format(output)))
else:
lines.append(output + ": {}".format(value))
return lines
35 changes: 35 additions & 0 deletions container_app_conf/formatter/json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright (c) 2019 Markus Ressel
# .
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# .
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# .
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import io
import json

from container_app_conf import ConfigFormatter


class JsonFormatter(ConfigFormatter):
"""
Formats config entries like a JSON config file
"""

def format(self, data: dict) -> str:
output = io.StringIO()
json.dump(data, output, indent=2)
output.seek(0)
return output.read()
36 changes: 36 additions & 0 deletions container_app_conf/formatter/toml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright (c) 2019 Markus Ressel
# .
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# .
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# .
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import io

import toml

from container_app_conf import ConfigFormatter


class TomlFormatter(ConfigFormatter):
"""
Formats config entries like a TOML config file
"""

def format(self, data: dict) -> str:
output = io.StringIO()
toml.dump(data, output)
output.seek(0)
return output.read()
39 changes: 39 additions & 0 deletions container_app_conf/formatter/yaml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright (c) 2019 Markus Ressel
# .
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# .
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# .
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import io

from ruamel.yaml import YAML

from container_app_conf import ConfigFormatter


class YamlFormatter(ConfigFormatter):
"""
Formats config entries like a YAML config file
"""
yaml = YAML()
yaml.default_style = False
yaml.default_flow_style = False

def format(self, data: dict) -> str:
output = io.StringIO()
self.yaml.dump(data, output)
output.seek(0)
return output.read()
5 changes: 3 additions & 2 deletions container_app_conf/source/env_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ def get(self, entry: ConfigEntry) -> str or None:
key = self.env_key(entry)
return os.environ.get(key, None)

def env_key(self, entry: ConfigEntry) -> str:
return self.KEY_SPLIT_CHAR.join(entry.key_path).upper()
@staticmethod
def env_key(entry: ConfigEntry) -> str:
return EnvSource.KEY_SPLIT_CHAR.join(entry.key_path).upper()

def _load(self) -> dict:
# loading env is pointless since it is already in memory
Expand Down
8 changes: 7 additions & 1 deletion container_app_conf/source/json_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import json
import logging

from container_app_conf.formatter.json import JsonFormatter
from container_app_conf.source import FilesystemSource

LOGGER = logging.getLogger(__name__)
Expand All @@ -31,10 +32,15 @@ class JsonSource(FilesystemSource):
"""
DEFAULT_FILE_EXTENSIONS = ['json']

formatter = JsonFormatter()

def _load_file(self, file_path: str) -> dict:
with open(file_path, 'r') as file:
return json.load(file)

def _write_reference(self, reference: dict, file_path: str):
text = self.formatter.format(reference)
with open(file_path, "w") as file:
json.dump(reference, file, indent=2)
file.seek(0)
file.write(text)
file.truncate()
Loading

0 comments on commit 06096f4

Please sign in to comment.