|
|
|
@ -2,10 +2,10 @@
|
|
|
|
Configuration management module. Enables configuration to be validated against
|
|
|
|
Configuration management module. Enables configuration to be validated against
|
|
|
|
requirement definitions before being loaded and used.
|
|
|
|
requirement definitions before being loaded and used.
|
|
|
|
|
|
|
|
|
|
|
|
Compatible with both raw config data structures and TOML files, config data must
|
|
|
|
Compatible with both raw config data structures and TOML files, config data must
|
|
|
|
start with a root dict containing named "config bundles". These are intended to
|
|
|
|
start with a root dict containing named "config bundles". These are intended to
|
|
|
|
refer to different modular parts of the application needing configuration, and the
|
|
|
|
refer to different modular parts of the application needing configuration, and the
|
|
|
|
config data structure must contain at least one.
|
|
|
|
config data structure must contain at least one.
|
|
|
|
|
|
|
|
|
|
|
|
Each config bundle itself needs to have a dict at the root, and so in practice a minimal
|
|
|
|
Each config bundle itself needs to have a dict at the root, and so in practice a minimal
|
|
|
|
TOML config file would look like::
|
|
|
|
TOML config file would look like::
|
|
|
|
@ -18,7 +18,8 @@ This would resolve to a config bundle named "myapp" that results in the dict::
|
|
|
|
|
|
|
|
|
|
|
|
{"config_thingy_a": "foooooo!", "important_number": 8237}
|
|
|
|
{"config_thingy_a": "foooooo!", "important_number": 8237}
|
|
|
|
|
|
|
|
|
|
|
|
Root items that are not dicts are not supported, for instance both the following TOML files would fail::
|
|
|
|
Root items that are not dicts are not supported, for instance both the following
|
|
|
|
|
|
|
|
TOML files would fail::
|
|
|
|
|
|
|
|
|
|
|
|
[[myapp]]
|
|
|
|
[[myapp]]
|
|
|
|
important_number = 8237
|
|
|
|
important_number = 8237
|
|
|
|
@ -43,14 +44,13 @@ from copy import deepcopy
|
|
|
|
from .freezedry import freezedryable, rehydrate
|
|
|
|
from .freezedry import freezedryable, rehydrate
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidConfigError(Exception):
|
|
|
|
class InvalidConfigError(Exception):
|
|
|
|
pass
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# The Table and Array terms from the TOML convention essentially
|
|
|
|
# The Table and Array terms from the TOML convention essentially
|
|
|
|
# map directly to Dictionaries (Tables), and Lists (Arrays)
|
|
|
|
# map directly to Dictionaries (Tables), and Lists (Arrays)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _ConfigDefinition(ABC):
|
|
|
|
class _ConfigDefinition(ABC):
|
|
|
|
def __init__(self, default=None, optional=False, helptext=""):
|
|
|
|
def __init__(self, default=None, optional=False, helptext=""):
|
|
|
|
self.default = default
|
|
|
|
self.default = default
|
|
|
|
@ -64,7 +64,7 @@ class _ConfigDefinition(ABC):
|
|
|
|
Raises InvalidConfigError on failure.
|
|
|
|
Raises InvalidConfigError on failure.
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@freezedryable
|
|
|
|
@freezedryable
|
|
|
|
class BoolDef(_ConfigDefinition):
|
|
|
|
class BoolDef(_ConfigDefinition):
|
|
|
|
@ -75,6 +75,7 @@ class BoolDef(_ConfigDefinition):
|
|
|
|
if not isinstance(value, bool):
|
|
|
|
if not isinstance(value, bool):
|
|
|
|
raise InvalidConfigError("Config value must be a boolean")
|
|
|
|
raise InvalidConfigError("Config value must be a boolean")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@freezedryable
|
|
|
|
@freezedryable
|
|
|
|
class IntDef(_ConfigDefinition):
|
|
|
|
class IntDef(_ConfigDefinition):
|
|
|
|
def __init__(self, default=None, minval=None, maxval=None,
|
|
|
|
def __init__(self, default=None, minval=None, maxval=None,
|
|
|
|
@ -93,6 +94,7 @@ class IntDef(_ConfigDefinition):
|
|
|
|
raise InvalidConfigError("Config value must be <= " +
|
|
|
|
raise InvalidConfigError("Config value must be <= " +
|
|
|
|
str(self.maxval))
|
|
|
|
str(self.maxval))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@freezedryable
|
|
|
|
@freezedryable
|
|
|
|
class StringDef(_ConfigDefinition):
|
|
|
|
class StringDef(_ConfigDefinition):
|
|
|
|
def __init__(self, default="", minlength=None, maxlength=None,
|
|
|
|
def __init__(self, default="", minlength=None, maxlength=None,
|
|
|
|
@ -111,6 +113,7 @@ class StringDef(_ConfigDefinition):
|
|
|
|
raise InvalidConfigError("Config string length must be <= " +
|
|
|
|
raise InvalidConfigError("Config string length must be <= " +
|
|
|
|
str(self.maxlength))
|
|
|
|
str(self.maxlength))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@freezedryable
|
|
|
|
@freezedryable
|
|
|
|
class DictDef(_ConfigDefinition):
|
|
|
|
class DictDef(_ConfigDefinition):
|
|
|
|
def __init__(self, default=None, optional=False, helptext=""):
|
|
|
|
def __init__(self, default=None, optional=False, helptext=""):
|
|
|
|
@ -154,7 +157,7 @@ class DictDef(_ConfigDefinition):
|
|
|
|
except InvalidConfigError as e:
|
|
|
|
except InvalidConfigError as e:
|
|
|
|
e.args = ("Key: " + key,) + e.args
|
|
|
|
e.args = ("Key: " + key,) + e.args
|
|
|
|
raise
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
def get_template(self, include_optional=False):
|
|
|
|
def get_template(self, include_optional=False):
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Return a config dict with the minimum structure required for this ConfigDefinition.
|
|
|
|
Return a config dict with the minimum structure required for this ConfigDefinition.
|
|
|
|
@ -162,7 +165,7 @@ class DictDef(_ConfigDefinition):
|
|
|
|
defaults that successfully validate.
|
|
|
|
defaults that successfully validate.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
Args:
|
|
|
|
include_optional: If set true, will include *all* config fields, not just the
|
|
|
|
include_optional: If set true, will include *all* config fields, not just the
|
|
|
|
required ones
|
|
|
|
required ones
|
|
|
|
Returns:
|
|
|
|
Returns:
|
|
|
|
Dict containing the structure that should be passed back in (with values) to comply
|
|
|
|
Dict containing the structure that should be passed back in (with values) to comply
|
|
|
|
@ -172,11 +175,11 @@ class DictDef(_ConfigDefinition):
|
|
|
|
for key, confdef in self.def_dict.items():
|
|
|
|
for key, confdef in self.def_dict.items():
|
|
|
|
if confdef.optional and (not include_optional):
|
|
|
|
if confdef.optional and (not include_optional):
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if hasattr(confdef,"get_template"):
|
|
|
|
if hasattr(confdef, "get_template"):
|
|
|
|
template[key]=confdef.get_template(include_optional)
|
|
|
|
template[key] = confdef.get_template(include_optional)
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
template[key]=confdef.default
|
|
|
|
template[key] = confdef.default
|
|
|
|
return template
|
|
|
|
return template
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -192,27 +195,32 @@ class _ListDefMixin():
|
|
|
|
raise
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
def get_template(self, include_optional=False):
|
|
|
|
def get_template(self, include_optional=False):
|
|
|
|
if hasattr(super(),"get_template"):
|
|
|
|
if hasattr(super(), "get_template"):
|
|
|
|
return [super().get_template(include_optional)]
|
|
|
|
return [super().get_template(include_optional)]
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
return [self.default]
|
|
|
|
return [self.default]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@freezedryable
|
|
|
|
@freezedryable
|
|
|
|
class BoolListDef(_ListDefMixin, BoolDef):
|
|
|
|
class BoolListDef(_ListDefMixin, BoolDef):
|
|
|
|
pass
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@freezedryable
|
|
|
|
@freezedryable
|
|
|
|
class IntListDef(_ListDefMixin, IntDef):
|
|
|
|
class IntListDef(_ListDefMixin, IntDef):
|
|
|
|
pass
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@freezedryable
|
|
|
|
@freezedryable
|
|
|
|
class StringListDef(_ListDefMixin, StringDef):
|
|
|
|
class StringListDef(_ListDefMixin, StringDef):
|
|
|
|
pass
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@freezedryable
|
|
|
|
@freezedryable
|
|
|
|
class DictListDef(_ListDefMixin, DictDef):
|
|
|
|
class DictListDef(_ListDefMixin, DictDef):
|
|
|
|
pass
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@freezedryable
|
|
|
|
@freezedryable
|
|
|
|
class ConfDefinition(DictDef):
|
|
|
|
class ConfDefinition(DictDef):
|
|
|
|
pass
|
|
|
|
pass
|
|
|
|
@ -231,14 +239,13 @@ class ConfigManager():
|
|
|
|
and return a plain parsed dict.
|
|
|
|
and return a plain parsed dict.
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
if isinstance(source, dict): # load from dict
|
|
|
|
if isinstance(source, dict): # load from dict
|
|
|
|
return source
|
|
|
|
return source
|
|
|
|
elif isinstance(source, str): # load from pathname
|
|
|
|
elif isinstance(source, str): # load from pathname
|
|
|
|
with open(source, 'r') as conf_file:
|
|
|
|
with open(source, 'r') as conf_file:
|
|
|
|
return toml.load(conf_file)
|
|
|
|
return toml.load(conf_file)
|
|
|
|
else: # load from file
|
|
|
|
else: # load from file
|
|
|
|
return toml.load(source)
|
|
|
|
return toml.load(source)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load(self, source):
|
|
|
|
def load(self, source):
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Load a config source into the ConfigManager, replacing any existing config.
|
|
|
|
Load a config source into the ConfigManager, replacing any existing config.
|
|
|
|
@ -256,27 +263,26 @@ class ConfigManager():
|
|
|
|
config. Dicts will be recursively processed with keys being merged and existing values
|
|
|
|
config. Dicts will be recursively processed with keys being merged and existing values
|
|
|
|
being replaced by the new source. This includes lists, which will be treated as any other
|
|
|
|
being replaced by the new source. This includes lists, which will be treated as any other
|
|
|
|
value and completely replaced.
|
|
|
|
value and completely replaced.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
Args:
|
|
|
|
source: Either the root dict of a data structure to load directly, a filepath to a TOML file,
|
|
|
|
source: Either the root dict of a data structure to load directly, a filepath to a TOML
|
|
|
|
or an open TOML file.
|
|
|
|
file, or an open TOML file.
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
self._overlay(self._load_source(source), self.root_config)
|
|
|
|
self._overlay(self._load_source(source), self.root_config)
|
|
|
|
self._overlay(self.frozen_config, self.root_config)
|
|
|
|
self._overlay(self.frozen_config, self.root_config)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def freeze_value(self, bundle_name, *field_names):
|
|
|
|
def freeze_value(self, bundle_name, *field_names):
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Freeze the given config field so that subsequent calls to ``load`` and ``load_overlay``
|
|
|
|
Freeze the given config field so that subsequent calls to ``load`` and ``load_overlay``
|
|
|
|
cannot change it. Can only be used for dict values or dict values nested in parent dicts.
|
|
|
|
cannot change it. Can only be used for dict values or dict values nested in parent dicts.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
Args:
|
|
|
|
bundle_name: The name of the bundle to look for the field in.
|
|
|
|
bundle_name: The name of the bundle to look for the field in.
|
|
|
|
*field_names: a series of strings that locate the config field, either a single
|
|
|
|
*field_names: a series of strings that locate the config field, either a single
|
|
|
|
key or series of nested keys.
|
|
|
|
key or series of nested keys.
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
#Bundle names are really no different from any other nested dict
|
|
|
|
# Bundle names are really no different from any other nested dict
|
|
|
|
names = (bundle_name,) + field_names
|
|
|
|
names = (bundle_name,) + field_names
|
|
|
|
|
|
|
|
|
|
|
|
target_field = self.root_config
|
|
|
|
target_field = self.root_config
|
|
|
|
@ -289,29 +295,26 @@ class ConfigManager():
|
|
|
|
frozen_value[name] = {}
|
|
|
|
frozen_value[name] = {}
|
|
|
|
frozen_value = frozen_value[name]
|
|
|
|
frozen_value = frozen_value[name]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frozen_value[names[-1]] = target_field[names[-1]]
|
|
|
|
frozen_value[names[-1]] = target_field[names[-1]]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_confdef(self, bundle_name, confdef):
|
|
|
|
def add_confdef(self, bundle_name, confdef):
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Stores a ConfigDefinition for future use when validating the corresponding config bundle
|
|
|
|
Stores a ConfigDefinition for future use when validating the corresponding config bundle
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
Args:
|
|
|
|
bundle_name (str) : The name to store the config definition under.
|
|
|
|
bundle_name (str) : The name to store the config definition under.
|
|
|
|
confdef (ConfigDefinition): The populated ConfigDefinition to store.
|
|
|
|
confdef (ConfigDefinition): The populated ConfigDefinition to store.
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
self.confdefs[bundle_name]=confdef
|
|
|
|
self.confdefs[bundle_name] = confdef
|
|
|
|
|
|
|
|
|
|
|
|
def add_confdefs(self, confdefs):
|
|
|
|
def add_confdefs(self, confdefs):
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Stores multiple ConfigDefinitions at once for future use when validating the corresponding config bundles
|
|
|
|
Stores multiple ConfigDefinitions at once for future use when validating the corresponding
|
|
|
|
|
|
|
|
config bundles
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
Args:
|
|
|
|
confdefs : A dict of populated ConfigDefinitions to store, using their keys as names.
|
|
|
|
confdefs : A dict of populated ConfigDefinitions to store, using their keys as names.
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
self.confdefs.update(confdefs)
|
|
|
|
self.confdefs.update(confdefs)
|
|
|
|
|
|
|
|
|
|
|
|
def list_missing_confdefs(self):
|
|
|
|
def list_missing_confdefs(self):
|
|
|
|
@ -321,7 +324,6 @@ class ConfigManager():
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
return list(self.root_config.keys() - self.confdefs.keys())
|
|
|
|
return list(self.root_config.keys() - self.confdefs.keys())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _overlay(self, src, dest):
|
|
|
|
def _overlay(self, src, dest):
|
|
|
|
for key in src:
|
|
|
|
for key in src:
|
|
|
|
# If the key is also in the dest and both are dicts, merge them.
|
|
|
|
# If the key is also in the dest and both are dicts, merge them.
|
|
|
|
@ -329,14 +331,14 @@ class ConfigManager():
|
|
|
|
self._overlay(src[key], dest[key])
|
|
|
|
self._overlay(src[key], dest[key])
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
# Otherwise it's either an existing value to be replaced or needs to be added.
|
|
|
|
# Otherwise it's either an existing value to be replaced or needs to be added.
|
|
|
|
dest[key] = src[key]
|
|
|
|
dest[key] = src[key]
|
|
|
|
|
|
|
|
|
|
|
|
def get_config_bundle(self, bundle_name, conf_def=None):
|
|
|
|
def get_config_bundle(self, bundle_name, conf_def=None):
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Get a config bundle called ``bundle_name`` and validate
|
|
|
|
Get a config bundle called ``bundle_name`` and validate
|
|
|
|
it against the corresponding config definition stored in the ConfigManager.
|
|
|
|
it against the corresponding config definition stored in the ConfigManager.
|
|
|
|
If ``conf_def`` is supplied, it gets used instead. Returns a validated
|
|
|
|
If ``conf_def`` is supplied, it gets used instead. Returns a validated
|
|
|
|
config bundle dict.
|
|
|
|
config bundle dict.
|
|
|
|
|
|
|
|
|
|
|
|
Note that as part of validation, optional keys that are missing will be
|
|
|
|
Note that as part of validation, optional keys that are missing will be
|
|
|
|
filled in with their default values (see ``DictDef``). This function will copy
|
|
|
|
filled in with their default values (see ``DictDef``). This function will copy
|
|
|
|
@ -363,13 +365,14 @@ class ConfigManager():
|
|
|
|
|
|
|
|
|
|
|
|
def get_config_bundles(self, bundle_names):
|
|
|
|
def get_config_bundles(self, bundle_names):
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Get multiple config bundles from the root dict at once, validating each one with the
|
|
|
|
Get multiple config bundles from the root dict at once, validating each one with the
|
|
|
|
corresponding confdef stored in the ConfigManager. See ``get_config_bundle``
|
|
|
|
corresponding confdef stored in the ConfigManager. See ``get_config_bundle``
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
Args:
|
|
|
|
bundle_names: A list of config bundle names to get. If dictionary is supplied, uses the values
|
|
|
|
bundle_names: A list of config bundle names to get. If dictionary is supplied, uses
|
|
|
|
as ConfigDefinitions rather than looking up a stored one in the ConfigManager.
|
|
|
|
the values as ConfigDefinitions rather than looking up a stored one in the
|
|
|
|
|
|
|
|
ConfigManager.
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Returns:
|
|
|
|
A dict of config dicts, with keys matching those passed in ``bundle_names``.
|
|
|
|
A dict of config dicts, with keys matching those passed in ``bundle_names``.
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
@ -384,7 +387,7 @@ class ConfigManager():
|
|
|
|
|
|
|
|
|
|
|
|
def get_bundle_names(self):
|
|
|
|
def get_bundle_names(self):
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Returns a list of names of top level config bundles
|
|
|
|
Returns a list of names of top level config bundles
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
return list(self.root_config.keys())
|
|
|
|
return list(self.root_config.keys())
|
|
|
|
|
|
|
|
|
|
|
|
@ -417,4 +420,3 @@ def update_toml_message(filepath, message):
|
|
|
|
|
|
|
|
|
|
|
|
def gen_comment(string):
|
|
|
|
def gen_comment(string):
|
|
|
|
return '\n# shepherd_message: ' + '\n# '.join(string.replace('#', '').splitlines()) + '\n'
|
|
|
|
return '\n# shepherd_message: ' + '\n# '.join(string.replace('#', '').splitlines()) + '\n'
|
|
|
|
|
|
|
|
|
|
|
|
|