From c71b3cc0ad4c064c780300f72c137915b1ec9843 Mon Sep 17 00:00:00 2001 From: Nils Philippsen Date: Tue, 19 Nov 2019 14:15:00 +0100 Subject: [PATCH] add configuration handling module This module contains the logic to load default, system-wide and, if specified, additional configuration files and merges them into two dictionaries: `config` and `email_overrides`. Signed-off-by: Nils Philippsen --- distgit_bugzilla_sync/config.py | 151 ++++++++++++++++++++++++++++++++ requirements.txt | 1 + 2 files changed, 152 insertions(+) create mode 100644 distgit_bugzilla_sync/config.py diff --git a/distgit_bugzilla_sync/config.py b/distgit_bugzilla_sync/config.py new file mode 100644 index 0000000..b87738d --- /dev/null +++ b/distgit_bugzilla_sync/config.py @@ -0,0 +1,151 @@ +# distgit_bugzilla_sync.config - handle configuration +# +# Copyright © 2018-2019 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, modify, +# copy, or redistribute it subject to the terms and conditions of the GNU +# General Public License v.2, or (at your option) any later version. This +# program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the GNU +# General Public License along with this program; if not, write to the Free +# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source +# code or documentation are not subject to the GNU General Public License and +# may only be used or replicated with the express permission of Red Hat, Inc. + +import os + +import toml + + +_here = os.path.dirname(__file__) +_default_conf_root = os.path.join(_here, 'default-config-files') +_system_conf_root = '/etc' + +config_files = { + 'default': { + 'configuration': os.path.join(_default_conf_root, 'configuration.toml'), + 'email_overrides': os.path.join(_default_conf_root, 'email_overrides.toml'), + }, + 'system': { + 'configuration': os.path.join(_system_conf_root, 'configuration.toml'), + 'email_overrides': os.path.join(_system_conf_root, 'email_overrides.toml'), + }, +} + + +class ConfigDict(dict): + """Dictionary class for configuration data. + + This contains a modified update() method which allows deep-merging other + dictionaries.""" + + def update(self, other: dict): + """Merge data from another dictionary into this one. + + Fails if any node isn't of the same type in both, to detect + configuration format issues early.""" + self_set = set(self) + other_set = set(other) + + type_error = False + + # keys that are in both self and other + for k in self_set & other_set: + self_val = self[k] + other_val = other[k] + if isinstance(other_val, dict): + if isinstance(self_val, ConfigDict): + self_val.update(other_val) + elif isinstance(self_val, dict): + self[k] = ConfigDict(self_val) + self[k].update(other_val) + else: + type_error = True + elif isinstance(other_val, type(self_val)): + self[k] = other_val + else: + type_error = True + + if type_error: + raise TypeError( + f"[{k!r}] values of incompatible types: {self_val!r}, {other_val!r}" + ) + + # add keys -> values which are missing + for k in other_set - self_set: + v = other[k] + if isinstance(v, dict): + self[k] = ConfigDict(v) + else: + self[k] = v + + +def load_configuration(addl_config_file=None, addl_email_overrides_file=None): + """Load stored configuration. + + This function loads default, system-wide, and if specified, additional + configuration files into the `config` and `email_overrides` + dictionaries.""" + # Load default files. + try: + default_config = toml.load(config_files['default']['configuration']) + except FileNotFoundError as e: + raise RuntimeError( + f"Default configuration file {config_files['default']['configuration']} not found." + ) from e + + try: + default_email_overrides = toml.load(config_files['default']['email_overrides']) + except FileNotFoundError as e: + raise RuntimeError( + f"Default email overrides file {config_files['default']['email_overrides']} not found." + ) from e + + # Attempt to load system-wide files. + try: + system_config = toml.load(config_files['system']['configuration']) + except FileNotFoundError: + system_config = {} + + try: + system_email_overrides = toml.load(config_files['system']['email_overrides']) + except FileNotFoundError: + system_email_overrides = {} + + # Load additional files (say, specified on the command line), if any. + if addl_config_file: + try: + addl_config = toml.load(addl_config_file) + except FileNotFoundError as e: + raise RuntimeError( + f"Additional configuration file {addl_config_file} not found." + ) from e + else: + addl_config = {} + + if addl_email_overrides_file: + try: + addl_email_overrides = toml.load(addl_email_overrides_file) + except FileNotFoundError as e: + raise RuntimeError( + f"Additional email overrides file {addl_email_overrides_file} not found." + ) from e + else: + addl_email_overrides = {} + + config.clear() + config.update(default_config) + config.update(system_config) + config.update(addl_config) + + email_overrides.clear() + email_overrides.update(default_email_overrides) + email_overrides.update(system_email_overrides) + email_overrides.update(addl_email_overrides) + + +config = ConfigDict() +email_overrides = ConfigDict() diff --git a/requirements.txt b/requirements.txt index 966ef6d..16460b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ dogpile.cache python-fedora PyYAML requests +toml urllib3