|
|
|
|
@ -1,6 +1,6 @@
|
|
|
|
|
"""
|
|
|
|
|
Configuration management module. Enables configuration to be validated against
|
|
|
|
|
requirement definitions before being loaded and used.
|
|
|
|
|
requirement specifications before being loaded and used.
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
@ -41,7 +41,7 @@ import toml
|
|
|
|
|
from abc import ABC, abstractmethod
|
|
|
|
|
from copy import deepcopy
|
|
|
|
|
|
|
|
|
|
from .freezedry import freezedryable, rehydrate
|
|
|
|
|
from preserve import preservable, restore
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidConfigError(Exception):
|
|
|
|
|
@ -51,7 +51,7 @@ class InvalidConfigError(Exception):
|
|
|
|
|
# map directly to Dictionaries (Tables), and Lists (Arrays)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _ConfigDefinition(ABC):
|
|
|
|
|
class _ValueSpecification(ABC):
|
|
|
|
|
def __init__(self, default=None, optional=False, helptext=""):
|
|
|
|
|
self.default = default
|
|
|
|
|
self.optional = optional
|
|
|
|
|
@ -60,14 +60,14 @@ class _ConfigDefinition(ABC):
|
|
|
|
|
@abstractmethod
|
|
|
|
|
def validate(self, value):
|
|
|
|
|
"""
|
|
|
|
|
Checks the supplied value to confirm that it complies with this ConfigDefinition.
|
|
|
|
|
Checks the supplied value to confirm that it complies with this specification.
|
|
|
|
|
Raises InvalidConfigError on failure.
|
|
|
|
|
"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@freezedryable
|
|
|
|
|
class BoolDef(_ConfigDefinition):
|
|
|
|
|
@preservable
|
|
|
|
|
class BoolSpec(_ValueSpecification):
|
|
|
|
|
def __init__(self, default=None, optional=False, helptext=""):
|
|
|
|
|
super().__init__(default, optional, helptext)
|
|
|
|
|
|
|
|
|
|
@ -76,8 +76,8 @@ class BoolDef(_ConfigDefinition):
|
|
|
|
|
raise InvalidConfigError("Config value must be a boolean")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@freezedryable
|
|
|
|
|
class IntDef(_ConfigDefinition):
|
|
|
|
|
@preservable
|
|
|
|
|
class IntSpec(_ValueSpecification):
|
|
|
|
|
def __init__(self, default=None, minval=None, maxval=None,
|
|
|
|
|
optional=False, helptext=""):
|
|
|
|
|
super().__init__(default, optional, helptext)
|
|
|
|
|
@ -95,8 +95,8 @@ class IntDef(_ConfigDefinition):
|
|
|
|
|
str(self.maxval))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@freezedryable
|
|
|
|
|
class StringDef(_ConfigDefinition):
|
|
|
|
|
@preservable
|
|
|
|
|
class StringSpec(_ValueSpecification):
|
|
|
|
|
def __init__(self, default="", minlength=None, maxlength=None,
|
|
|
|
|
optional=False, helptext=""):
|
|
|
|
|
super().__init__(default, optional, helptext)
|
|
|
|
|
@ -114,53 +114,53 @@ class StringDef(_ConfigDefinition):
|
|
|
|
|
str(self.maxlength))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@freezedryable
|
|
|
|
|
class DictDef(_ConfigDefinition):
|
|
|
|
|
@preservable
|
|
|
|
|
class DictSpec(_ValueSpecification):
|
|
|
|
|
def __init__(self, default=None, optional=False, helptext=""):
|
|
|
|
|
super().__init__(default, optional, helptext)
|
|
|
|
|
self.def_dict = {}
|
|
|
|
|
self.spec_dict = {}
|
|
|
|
|
|
|
|
|
|
def add_def(self, name, newdef):
|
|
|
|
|
if not isinstance(newdef, _ConfigDefinition):
|
|
|
|
|
raise TypeError("Config definiton must be an instance of a "
|
|
|
|
|
"ConfigDefinition subclass")
|
|
|
|
|
def add_spec(self, name, newspec):
|
|
|
|
|
if not isinstance(newspec, _ValueSpecification):
|
|
|
|
|
raise TypeError("Config specification must be an instance of a "
|
|
|
|
|
"_ValueSpecification subclass")
|
|
|
|
|
if not isinstance(name, str):
|
|
|
|
|
raise TypeError("Config definition name must be a string")
|
|
|
|
|
self.def_dict[name] = newdef
|
|
|
|
|
return newdef
|
|
|
|
|
raise TypeError("Config specification name must be a string")
|
|
|
|
|
self.spec_dict[name] = newspec
|
|
|
|
|
return newspec
|
|
|
|
|
|
|
|
|
|
def validate(self, value_dict):
|
|
|
|
|
"""
|
|
|
|
|
Checks the supplied value to confirm that it complies with this ConfigDefinition.
|
|
|
|
|
Checks the supplied value to confirm that it complies with this specification.
|
|
|
|
|
Raises InvalidConfigError on failure.
|
|
|
|
|
|
|
|
|
|
This *can* modify the supplied value dict, inserting defaults for any child
|
|
|
|
|
ConfigDefinitions that are marked as optional.
|
|
|
|
|
config specifications that are marked as optional.
|
|
|
|
|
"""
|
|
|
|
|
def_set = set(self.def_dict.keys())
|
|
|
|
|
spec_set = set(self.spec_dict.keys())
|
|
|
|
|
value_set = set(value_dict.keys())
|
|
|
|
|
|
|
|
|
|
for missing_key in def_set - value_set:
|
|
|
|
|
if not self.def_dict[missing_key].optional:
|
|
|
|
|
for missing_key in spec_set - value_set:
|
|
|
|
|
if not self.spec_dict[missing_key].optional:
|
|
|
|
|
raise InvalidConfigError("Dict must contain key: " +
|
|
|
|
|
missing_key)
|
|
|
|
|
else:
|
|
|
|
|
value_dict[missing_key] = self.def_dict[missing_key].default
|
|
|
|
|
value_dict[missing_key] = self.spec_dict[missing_key].default
|
|
|
|
|
|
|
|
|
|
for extra_key in value_set - def_set:
|
|
|
|
|
for extra_key in value_set - spec_set:
|
|
|
|
|
raise InvalidConfigError("Dict contains unknown key: " +
|
|
|
|
|
extra_key)
|
|
|
|
|
|
|
|
|
|
for key, value in value_dict.items():
|
|
|
|
|
try:
|
|
|
|
|
self.def_dict[key].validate(value)
|
|
|
|
|
self.spec_dict[key].validate(value)
|
|
|
|
|
except InvalidConfigError as e:
|
|
|
|
|
e.args = ("Key: " + key,) + e.args
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
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 specification.
|
|
|
|
|
Default values will be included, though not all required fields will necessarily have
|
|
|
|
|
defaults that successfully validate.
|
|
|
|
|
|
|
|
|
|
@ -169,21 +169,21 @@ class DictDef(_ConfigDefinition):
|
|
|
|
|
required ones
|
|
|
|
|
Returns:
|
|
|
|
|
Dict containing the structure that should be passed back in (with values) to comply
|
|
|
|
|
with this ConfigDefinition.
|
|
|
|
|
with this config specification.
|
|
|
|
|
"""
|
|
|
|
|
template = {}
|
|
|
|
|
for key, confdef in self.def_dict.items():
|
|
|
|
|
if confdef.optional and (not include_optional):
|
|
|
|
|
for key, confspec in self.spec_dict.items():
|
|
|
|
|
if confspec.optional and (not include_optional):
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if hasattr(confdef, "get_template"):
|
|
|
|
|
template[key] = confdef.get_template(include_optional)
|
|
|
|
|
if hasattr(confspec, "get_template"):
|
|
|
|
|
template[key] = confspec.get_template(include_optional)
|
|
|
|
|
else:
|
|
|
|
|
template[key] = confdef.default
|
|
|
|
|
template[key] = confspec.default
|
|
|
|
|
return template
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _ListDefMixin():
|
|
|
|
|
class _ListSpecMixin():
|
|
|
|
|
def validate(self, value_list):
|
|
|
|
|
if not isinstance(value_list, list):
|
|
|
|
|
raise InvalidConfigError("Config item must be a list")
|
|
|
|
|
@ -201,35 +201,35 @@ class _ListDefMixin():
|
|
|
|
|
return [self.default]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@freezedryable
|
|
|
|
|
class BoolListDef(_ListDefMixin, BoolDef):
|
|
|
|
|
@preservable
|
|
|
|
|
class BoolListSpec(_ListSpecMixin, BoolSpec):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@freezedryable
|
|
|
|
|
class IntListDef(_ListDefMixin, IntDef):
|
|
|
|
|
@preservable
|
|
|
|
|
class IntListSpec(_ListSpecMixin, IntSpec):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@freezedryable
|
|
|
|
|
class StringListDef(_ListDefMixin, StringDef):
|
|
|
|
|
@preservable
|
|
|
|
|
class StringListSpec(_ListSpecMixin, StringSpec):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@freezedryable
|
|
|
|
|
class DictListDef(_ListDefMixin, DictDef):
|
|
|
|
|
@preservable
|
|
|
|
|
class DictListSpec(_ListSpecMixin, DictSpec):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@freezedryable
|
|
|
|
|
class ConfDefinition(DictDef):
|
|
|
|
|
@preservable
|
|
|
|
|
class ConfigSpecification(DictSpec):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ConfigManager():
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.root_config = {}
|
|
|
|
|
self.confdefs = {}
|
|
|
|
|
self.confspecs = {}
|
|
|
|
|
self.frozen_config = {}
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
@ -297,32 +297,32 @@ class ConfigManager():
|
|
|
|
|
|
|
|
|
|
frozen_value[names[-1]] = target_field[names[-1]]
|
|
|
|
|
|
|
|
|
|
def add_confdef(self, bundle_name, confdef):
|
|
|
|
|
def add_confspec(self, bundle_name, confspec):
|
|
|
|
|
"""
|
|
|
|
|
Stores a ConfigDefinition for future use when validating the corresponding config bundle
|
|
|
|
|
Stores a ConfigSpecification for future use when validating the corresponding config bundle
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
bundle_name (str) : The name to store the config definition under.
|
|
|
|
|
confdef (ConfigDefinition): The populated ConfigDefinition to store.
|
|
|
|
|
bundle_name (str) : The name to store the config specification under.
|
|
|
|
|
confspec (ConfigSpecification): The populated ConfigSpecification to store.
|
|
|
|
|
"""
|
|
|
|
|
self.confdefs[bundle_name] = confdef
|
|
|
|
|
self.confspecs[bundle_name] = confspec
|
|
|
|
|
|
|
|
|
|
def add_confdefs(self, confdefs):
|
|
|
|
|
def add_confspecs(self, confspecs):
|
|
|
|
|
"""
|
|
|
|
|
Stores multiple ConfigDefinitions at once for future use when validating the corresponding
|
|
|
|
|
Stores multiple ConfigSpecifications at once for future use when validating the corresponding
|
|
|
|
|
config bundles
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
confdefs : A dict of populated ConfigDefinitions to store, using their keys as names.
|
|
|
|
|
confspecs : A dict of populated ConfigSpecifications to store, using their keys as names.
|
|
|
|
|
"""
|
|
|
|
|
self.confdefs.update(confdefs)
|
|
|
|
|
self.confspecs.update(confspecs)
|
|
|
|
|
|
|
|
|
|
def list_missing_confdefs(self):
|
|
|
|
|
def list_missing_confspecs(self):
|
|
|
|
|
"""
|
|
|
|
|
Returns a list of config bundle names that do not have a corresponding ConfigDefinition
|
|
|
|
|
Returns a list of config bundle names that do not have a corresponding ConfigSpecification
|
|
|
|
|
stored in the ConfigManager.
|
|
|
|
|
"""
|
|
|
|
|
return list(self.root_config.keys() - self.confdefs.keys())
|
|
|
|
|
return list(self.root_config.keys() - self.confspecs.keys())
|
|
|
|
|
|
|
|
|
|
def _overlay(self, src, dest):
|
|
|
|
|
for key in src:
|
|
|
|
|
@ -333,31 +333,31 @@ class ConfigManager():
|
|
|
|
|
# Otherwise it's either an existing value to be replaced or needs to be added.
|
|
|
|
|
dest[key] = src[key]
|
|
|
|
|
|
|
|
|
|
def get_config_bundle(self, bundle_name, conf_def=None):
|
|
|
|
|
def get_config_bundle(self, bundle_name, conf_spec=None):
|
|
|
|
|
"""
|
|
|
|
|
Get a config bundle called ``bundle_name`` and validate
|
|
|
|
|
it against the corresponding config definition stored in the ConfigManager.
|
|
|
|
|
If ``conf_def`` is supplied, it gets used instead. Returns a validated
|
|
|
|
|
it against the corresponding config specification stored in the ConfigManager.
|
|
|
|
|
If ``conf_spec`` is supplied, it gets used instead. Returns a copy of the validated
|
|
|
|
|
config bundle dict.
|
|
|
|
|
|
|
|
|
|
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 ``DictSpec``). This function will copy
|
|
|
|
|
the config bundle *after* validation, and so config loaded in the ConfManager will
|
|
|
|
|
be modified, but future ConfigManager manipulations won't change the returned config
|
|
|
|
|
bundle.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
config_name: (str) Name of the config dict to find.
|
|
|
|
|
conf_def: (ConfDefinition) Optional config definition to validate against.
|
|
|
|
|
conf_spec: (ConfigSpecification) Optional config specification to validate against.
|
|
|
|
|
"""
|
|
|
|
|
if not isinstance(conf_def, ConfDefinition):
|
|
|
|
|
conf_def = self.confdefs[bundle_name]
|
|
|
|
|
if not isinstance(conf_spec, ConfigSpecification):
|
|
|
|
|
conf_spec = self.confspecs[bundle_name]
|
|
|
|
|
|
|
|
|
|
if bundle_name not in self.root_config:
|
|
|
|
|
raise InvalidConfigError(
|
|
|
|
|
"Config must contain dict: " + bundle_name)
|
|
|
|
|
try:
|
|
|
|
|
conf_def.validate(self.root_config[bundle_name])
|
|
|
|
|
conf_spec.validate(self.root_config[bundle_name])
|
|
|
|
|
except InvalidConfigError as e:
|
|
|
|
|
e.args = ("Bundle: " + bundle_name,) + e.args
|
|
|
|
|
raise
|
|
|
|
|
@ -366,11 +366,11 @@ class ConfigManager():
|
|
|
|
|
def get_config_bundles(self, bundle_names):
|
|
|
|
|
"""
|
|
|
|
|
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 ConfigSpecification stored in the ConfigManager. See ``get_config_bundle``
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
bundle_names: A list of config bundle names to get. If dictionary is supplied, uses
|
|
|
|
|
the values as ConfigDefinitions rather than looking up a stored one in the
|
|
|
|
|
the values as ConfigSpecifications rather than looking up a stored one in the
|
|
|
|
|
ConfigManager.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
@ -378,8 +378,8 @@ class ConfigManager():
|
|
|
|
|
"""
|
|
|
|
|
config_values = {}
|
|
|
|
|
if isinstance(bundle_names, dict):
|
|
|
|
|
for name, conf_def in bundle_names.items():
|
|
|
|
|
config_values[name] = self.get_config_bundle(name, conf_def)
|
|
|
|
|
for name, conf_spec in bundle_names.items():
|
|
|
|
|
config_values[name] = self.get_config_bundle(name, conf_spec)
|
|
|
|
|
else:
|
|
|
|
|
for name in bundle_names:
|
|
|
|
|
config_values[name] = self.get_config_bundle(name)
|
|
|
|
|
@ -419,4 +419,4 @@ def update_toml_message(filepath, message):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def gen_comment(string):
|
|
|
|
|
return '\n# shepherd_message: ' + '\n# '.join(string.replace('#', '').splitlines()) + '\n'
|
|
|
|
|
return '\n# config-spec_message: ' + '\n# '.join(string.replace('#', '').splitlines()) + '\n'
|
|
|
|
|
|