mirror of
https://projects.torsion.org/witten/borgmatic.git
synced 2025-04-28 13:52:24 +00:00
176 lines
5.6 KiB
Python
176 lines
5.6 KiB
Python
import io
|
|
import re
|
|
|
|
import ruamel.yaml
|
|
|
|
import borgmatic.config.schema
|
|
|
|
LIST_INDEX_KEY_PATTERN = re.compile(r'^(?P<list_name>[a-zA-z-]+)\[(?P<index>\d+)\]$')
|
|
|
|
|
|
def set_values(config, keys, value):
|
|
'''
|
|
Given a configuration dict, a sequence of parsed key strings, and a string value, descend into
|
|
the configuration hierarchy based on the given keys and set the value into the right place.
|
|
For example, consider these keys:
|
|
|
|
('foo', 'bar', 'baz')
|
|
|
|
This looks up "foo" in the given configuration dict. And within that, it looks up "bar". And
|
|
then within that, it looks up "baz" and sets it to the given value. Another example:
|
|
|
|
('mylist[0]', 'foo')
|
|
|
|
This looks for the zeroth element of "mylist" in the given configuration. And within that, it
|
|
looks up "foo" and sets it to the given value.
|
|
'''
|
|
if not keys:
|
|
return
|
|
|
|
first_key = keys[0]
|
|
|
|
# Support "mylist[0]" list index syntax.
|
|
match = LIST_INDEX_KEY_PATTERN.match(first_key)
|
|
|
|
if match:
|
|
list_key = match.group('list_name')
|
|
list_index = int(match.group('index'))
|
|
|
|
try:
|
|
if len(keys) == 1:
|
|
config[list_key][list_index] = value
|
|
|
|
return
|
|
|
|
if list_key not in config:
|
|
config[list_key] = []
|
|
|
|
set_values(config[list_key][list_index], keys[1:], value)
|
|
except (IndexError, KeyError):
|
|
raise ValueError(f'Argument list index {first_key} is out of range')
|
|
|
|
return
|
|
|
|
if len(keys) == 1:
|
|
config[first_key] = value
|
|
|
|
return
|
|
|
|
if first_key not in config:
|
|
config[first_key] = {}
|
|
|
|
set_values(config[first_key], keys[1:], value)
|
|
|
|
|
|
def type_for_option(schema, option_keys):
|
|
'''
|
|
Given a configuration schema dict and a sequence of keys identifying a potentially nested
|
|
option, e.g. ('extra_borg_options', 'create'), return the schema type of that option as a
|
|
string.
|
|
|
|
Return None if the option or its type cannot be found in the schema.
|
|
'''
|
|
option_schema = schema
|
|
|
|
for key in option_keys:
|
|
# Support "name[0]"-style list index syntax.
|
|
match = LIST_INDEX_KEY_PATTERN.match(key)
|
|
properties = borgmatic.config.schema.get_properties(option_schema)
|
|
|
|
try:
|
|
if match:
|
|
option_schema = properties[match.group('list_name')]['items']
|
|
else:
|
|
option_schema = properties[key]
|
|
except KeyError:
|
|
return None
|
|
|
|
try:
|
|
return option_schema['type']
|
|
except KeyError:
|
|
return None
|
|
|
|
|
|
def convert_value_type(value, option_type):
|
|
'''
|
|
Given a string value and its schema type as a string, determine its logical type (string,
|
|
boolean, integer, etc.), and return it converted to that type.
|
|
|
|
If the destination option type is a string, then leave the value as-is so that special
|
|
characters in it don't get interpreted as YAML during conversion.
|
|
|
|
And if the source value isn't a string, return it as-is.
|
|
|
|
Raise ruamel.yaml.error.YAMLError if there's a parse issue with the YAML.
|
|
Raise ValueError if the parsed value doesn't match the option type.
|
|
'''
|
|
if not isinstance(value, str):
|
|
return value
|
|
|
|
if option_type == 'string':
|
|
return value
|
|
|
|
try:
|
|
parsed_value = ruamel.yaml.YAML(typ='safe').load(io.StringIO(value))
|
|
except ruamel.yaml.error.YAMLError as error:
|
|
raise ValueError(f'Argument value "{value}" is invalid: {error.problem}')
|
|
|
|
if not isinstance(parsed_value, borgmatic.config.schema.parse_type(option_type)):
|
|
raise ValueError(f'Argument value "{value}" is not of the expected type: {option_type}')
|
|
|
|
return parsed_value
|
|
|
|
|
|
def prepare_arguments_for_config(global_arguments, schema):
|
|
'''
|
|
Given global arguments as an argparse.Namespace and a configuration schema dict, parse each
|
|
argument that corresponds to an option in the schema and return a sequence of tuples (keys,
|
|
values) for that option, where keys is a sequence of strings. For instance, given the following
|
|
arguments:
|
|
|
|
argparse.Namespace(**{'my_option.sub_option': 'value1', 'other_option': 'value2'})
|
|
|
|
... return this:
|
|
|
|
(
|
|
(('my_option', 'sub_option'), 'value1'),
|
|
(('other_option',), 'value2'),
|
|
)
|
|
'''
|
|
prepared_values = []
|
|
|
|
for argument_name, value in global_arguments.__dict__.items():
|
|
if value is None:
|
|
continue
|
|
|
|
keys = tuple(argument_name.split('.'))
|
|
option_type = type_for_option(schema, keys)
|
|
|
|
# The argument doesn't correspond to any option in the schema, so ignore it. It's
|
|
# probably a flag that borgmatic has on the command-line but not in configuration.
|
|
if option_type is None:
|
|
continue
|
|
|
|
prepared_values.append(
|
|
(
|
|
keys,
|
|
convert_value_type(value, option_type),
|
|
)
|
|
)
|
|
|
|
return tuple(prepared_values)
|
|
|
|
|
|
def apply_arguments_to_config(config, schema, arguments):
|
|
'''
|
|
Given a configuration dict, a corresponding configuration schema dict, and arguments as a dict
|
|
from action name to argparse.Namespace, set those given argument values into their corresponding
|
|
configuration options in the configuration dict.
|
|
|
|
This supports argument flags of the from "--foo.bar.baz" where each dotted component is a nested
|
|
configuration object. Additionally, flags like "--foo.bar[0].baz" are supported to update a list
|
|
element in the configuration.
|
|
'''
|
|
for action_arguments in arguments.values():
|
|
for keys, value in prepare_arguments_for_config(action_arguments, schema):
|
|
set_values(config, keys, value)
|