"""
borgini.config
==============
All things ``config.ini``
"""
from __future__ import annotations
import configparser
import getpass
import os
import socket
import typing as t
NONE = "None"
DEFAULT = "DEFAULT"
[docs]class RawConfig:
"""Contains the ``configparser.ConfigParser`` object.
Write to and read from the ``config.ini`` file. The boolean values
get written to the file as strings, and they get read from the file
into the buffer as strings too. Not all values are as they should be
when read into Python so this will get subclassed into
``config.Proxy`` first.
:param configpath: The path to the config.ini file - this depends on
the profile used and whether this is run as ``"$USER"`` or as
root.
"""
def __init__(self, configpath: str | os.PathLike) -> None:
self.configpath = configpath
self.parser = configparser.ConfigParser(interpolation=None)
def _read_kwargs(self, **kwargs: t.Dict[str, t.Any]):
self.parser.read_dict(kwargs)
def _load_default_values(self) -> None:
hostname = socket.gethostname()
user = getpass.getuser()
self._read_kwargs(
DEFAULT={
"reponame": hostname,
"repopath": NONE,
"timestamp": "%Y-%m-%dT%H:%M:%S",
"ssh": True,
"prune": True,
},
SSH={"remoteuser": user, "remotehost": hostname, "port": "22"},
BACKUP={
"verbose": True,
"stats": True,
"list": True,
"show-rc": True,
"exclude-caches": True,
"filter": "AME",
"compression": "lz4",
},
PRUNE={
"verbose": False,
"stats": True,
"list": True,
"show-rc": True,
"keep-daily": "7",
"keep-weekly": "4",
"keep-monthly": "6",
},
ENVIRONMENT={"keyfile": NONE},
)
[docs] def write_values(self) -> None:
"""Write values from ``ConfigParser`` to the config file."""
with open(self.configpath, "w", encoding="utf-8") as configfile:
self.parser.write(configfile)
[docs] def write_new_config(self) -> None:
"""Load default values into the ``ConfigParser`` and write."""
self._load_default_values()
self.write_values()
[docs] def read(self) -> None:
"""Read the ``config.ini`` file and avoid non-critical errors.
Once the ``config.ini`` is read and loaded into the buffer write
it back to the file as this class will filter out non-parsable
configurations back to their default. If there is a key in the
config.ini file that cannot be parsed skip reading it into
buffer, as it will be removed once the config is subsequently
written. Any new keys and configurations that may be added in
the future will also be safely added to the config.
"""
self._load_default_values()
reader = configparser.ConfigParser(interpolation=None)
reader.read(self.configpath)
for section in reader:
for key in dict(reader[section]):
if section == DEFAULT or (
section != DEFAULT and key not in reader[DEFAULT]
):
try:
self.parser[section][key] = reader[section][key]
except KeyError:
pass
self.write_values()
[docs]class Proxy:
"""Subclass ``RawConfig`` to inherit the ``ConfigParser`` object.
Translate the string values into boolean and ``NoneType`` values or
some of the tests will not work i.e. ``None`` and ``False`` will be
``True`` as the strings ``"None"`` and ``"False"`` Inherit
``RawConfig`` after the ``parser.Catch`` string has identified
run-time errors.
:param raw_config: Instantiated ``RawConfig`` object containing the
``configparser.ConfigParser`` object as ``parser``.
"""
def __init__(self, raw_config: RawConfig) -> None:
self.parser = raw_config.parser
self.sections = self.section_list()
[docs] def section_list(self) -> t.List[str]:
"""Index the sections of the config.ini file.
:return: List of config sections.
"""
sections = self.parser.sections()
sections.append(DEFAULT)
return sections
def _filter_default(self, section: str) -> t.Dict[str, t.Any]:
if section == DEFAULT:
return dict(self.parser[section])
return {
k: v
for k, v in self.parser[section].items()
if k not in dict(self.parser[DEFAULT])
}
def _raw_dict(self) -> t.Dict[str, t.Any]:
return {s: self._filter_default(s) for s in self.sections}
@staticmethod
def _convert_null(obj: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
return {s: {k: v for k, v in obj[s].items() if v != NONE} for s in obj}
def _get_boolean(self, section: str, key: str) -> bool:
try:
if key.startswith("keep-"):
raise ValueError
return self.parser.getboolean(section, key)
except ValueError:
return self._filter_default(section)[key]
def _convert_booleans(self, obj: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
return {s: {k: self._get_boolean(s, k) for k in obj[s]} for s in obj}
@staticmethod
def _filter_null(raw_dict: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
return {k: v for k, v in raw_dict.items() if v}
[docs] def convert_proxy(self) -> t.Dict[str, t.Any]:
"""Convert ``ConfigParser`` into python friendly dictionary.
:return: Dictionary object.
"""
raw_dict = self._raw_dict()
truthy = self._convert_null(raw_dict)
obj = self._convert_booleans(truthy)
return self._filter_null(obj)
[docs]class Config(Proxy):
"""Final config object suitable for running with python methods.
Subclass the ``config.Proxy`` class to inherit the translated
config object from ``ConfigParser`` -> ``Dict``.
:param raw_config: The ``configparser.ConfigParser`` object.
"""
def __init__(self, raw_config: RawConfig) -> None:
super().__init__(raw_config)
self.dict = self.convert_proxy()
def _get_section(self, section: str) -> t.Dict[str, t.Any]:
return self.dict[section]
[docs] def get_key(self, section: str, key: str) -> str | None:
"""Get a key from the dictionary object in ``self``.
If the value is not found such as the ``BORG_PASSPHRASE``
environment variable (as the string value ``"None"`` would have
been omitted by ``config.Proxy``) return ``None`` in its
place to avoid an expected error and carry on omitting the key.
:param section: The primary key and the section from
``configparser.ConfigParser``.
:param key: The key containing the configured value.
:return: The value of the called key.
"""
try:
return self._get_section(section)[key]
except KeyError:
return None
[docs] def get_keytuple(
self, **kwargs: t.Tuple[str, ...]
) -> tuple[str | bool | None, ...]:
"""Return multiple values at once from ``self.dict[section]``.
Return as a tuple that can be unpacked by the keys passed to the
method.
:param kwargs: Sections to get.
:return: A tuple of multiple any one or more values.
"""
return tuple(
self.get_key(s, k)
for s in kwargs # pylint: disable=consider-using-dict-items
for k in kwargs[s]
)
@staticmethod
def _get_flags(obj: t.Dict[str, str]) -> t.List[str]:
return [f"--{k}" for k, v in obj.items() if v is True]
@staticmethod
def _get_keyvals(obj: t.Dict[str, str]) -> t.List[str]:
return [f"--{k} {v}" for k, v in obj.items() if isinstance(v, str)]
[docs] def return_all(self, section: str) -> t.Tuple[str, ...]:
"""Get all args belonging to a section.
The (kw)args that are boolean flags only need to exist, as their
existence in the script is the switch - so these are not
returned as kwargs but rather args. Treat values that aren't
boolean differently as the key and the value need to be
included.
:param section: The section from which the (kw)args should be
retrieved.
:return: A tuple of all switches prefixed with ``"--"`` and all
kwargs.
"""
obj = self.dict[section]
args = self._get_flags(obj)
args.extend(self._get_keyvals(obj))
return tuple(args)