Source code for pdfebc_core.config_utils

# -*- coding: utf-8 -*-
"""Module containing util functions relating to the configuration of pdfebc.

The SMTP server and port are configured in the config.cnf file.

Requires a config file called 'config.cnf' in the user conf directory specified by appdirs. In the
case of Arch Linux, this is '$HOME/.config/pdfebc/config.cnf', but this may vary with distributions.
The config file should have the following format:

| [EMAIL]
| user = <sender_email>
| pass = <password>
| receiver = <receiver_email>
| smtp_server = <smtp_server>
| smtp_port = <smtp_port>
| 
| [DEFAULTS]
| gs_binary = <ghostscript_binary>
| src = <source_dir>
| out = <out_dir>

.. module:: config_utils
    :platform: Unix
    :synopsis: Configuration utility functions.

.. moduleauthor:: Simon Larsén <slarse@kth.se>
"""
import os
import configparser
from collections import defaultdict
import appdirs

CONFIG_FILENAME = 'config.cnf'
CONFIG_PATH = os.path.join(appdirs.user_config_dir('pdfebc'), CONFIG_FILENAME)
EMAIL_SECTION_KEY = "EMAIL"
PASSWORD_KEY = "pass"
USER_KEY = "user"
RECEIVER_KEY = "receiver"
DEFAULT_SMTP_SERVER = "smtp.gmail.com"
DEFAULT_SMTP_PORT = 587
SMTP_SERVER_KEY = "smtp_server"
SMTP_PORT_KEY = "smtp_port"
EMAIL_SECTION_KEYS = {USER_KEY, PASSWORD_KEY, RECEIVER_KEY, SMTP_SERVER_KEY, SMTP_PORT_KEY}
DEFAULT_SECTION_KEY = "DEFAULTS"
GS_DEFAULT_BINARY_KEY = "gs_binary"
SRC_DEFAULT_DIR_KEY = "src"
OUT_DEFAULT_DIR_KEY = "out"
DEFAULT_SECTION_KEYS = {GS_DEFAULT_BINARY_KEY, SRC_DEFAULT_DIR_KEY, OUT_DEFAULT_DIR_KEY}
SECTION_KEYS = {EMAIL_SECTION_KEY: EMAIL_SECTION_KEYS,
                DEFAULT_SECTION_KEY: DEFAULT_SECTION_KEYS}

[docs]class ConfigurationError(configparser.ParsingError): """Error thrown whenever something is wrong with the configuration file.""" pass
[docs]def create_config(sections, section_contents): """Create a config file from the provided sections and key value pairs. Args: sections (List[str]): A list of section keys. key_value_pairs (Dict[str, str]): A list of of dictionaries. Must be as long as the list of sections. That is to say, if there are two sections, there should be two dicts. Returns: configparser.ConfigParser: A ConfigParser. Raises: ValueError """ sections_length, section_contents_length = len(sections), len(section_contents) if sections_length != section_contents_length: raise ValueError("Mismatch between argument lengths.\n" "len(sections) = {}\n" "len(section_contents) = {}" .format(sections_length, section_contents_length)) config = configparser.ConfigParser() for section, section_content in zip(sections, section_contents): config[section] = section_content return config
[docs]def write_config(config, config_path=CONFIG_PATH): """Write the config to the output path. Creates the necessary directories if they aren't there. Args: config (configparser.ConfigParser): A ConfigParser. """ if not os.path.exists(config_path): os.makedirs(os.path.dirname(config_path)) with open(config_path, 'w', encoding='utf-8') as f: config.write(f)
[docs]def read_config(config_path=CONFIG_PATH): """Read the config information from the config file. Args: config_path (str): Relative path to the email config file. Returns: defaultdict: A defaultdict with the config information. Raises: IOError """ if not os.path.isfile(config_path): raise IOError("No config file found at %s" % config_path) config_parser = configparser.ConfigParser() config_parser.read(config_path) config = _config_parser_to_defaultdict(config_parser) return config
[docs]def check_config(config): """Check that all sections of the config contain the keys that they should. Args: config (defaultdict): A defaultdict. Raises: ConfigurationError """ for section, expected_section_keys in SECTION_KEYS.items(): section_content = config.get(section) if not section_content: raise ConfigurationError("Config file badly formed! Section {} is missing." .format(section)) elif not _section_is_healthy(section_content, expected_section_keys): raise ConfigurationError("The {} section of the configuration file is badly formed!" .format(section))
[docs]def run_config_diagnostics(config_path=CONFIG_PATH): """Run diagnostics on the configuration file. Args: config_path (str): Path to the configuration file. Returns: str, Set[str], dict(str, Set[str]): The path to the configuration file, a set of missing sections and a dict that maps each section to the entries that have either missing or empty options. """ config = read_config(config_path) missing_sections = set() malformed_entries = defaultdict(set) for section, expected_section_keys in SECTION_KEYS.items(): section_content = config.get(section) if not section_content: missing_sections.add(section) else: for option in expected_section_keys: option_value = section_content.get(option) if not option_value: malformed_entries[section].add(option) return config_path, missing_sections, malformed_entries
[docs]def get_attribute_from_config(config, section, attribute): """Try to parse an attribute of the config file. Args: config (defaultdict): A defaultdict. section (str): The section of the config file to get information from. attribute (str): The attribute of the section to fetch. Returns: str: The string corresponding to the section and attribute. Raises: ConfigurationError """ section = config.get(section) if section: option = section.get(attribute) if option: return option raise ConfigurationError("Config file badly formed!\n" "Failed to get attribute '{}' from section '{}'!" .format(attribute, section))
[docs]def valid_config_exists(config_path=CONFIG_PATH): """Verify that a valid config file exists. Args: config_path (str): Path to the config file. Returns: boolean: True if there is a valid config file, false if not. """ if os.path.isfile(config_path): try: config = read_config(config_path) check_config(config) except (ConfigurationError, IOError): return False else: return False return True
[docs]def config_to_string(config): """Nice output string for the config, which is a nested defaultdict. Args: config (defaultdict(defaultdict)): The configuration information. Returns: str: A human-readable output string detailing the contents of the config. """ output = [] for section, section_content in config.items(): output.append("[{}]".format(section)) for option, option_value in section_content.items(): output.append("{} = {}".format(option, option_value)) return "\n".join(output)
def _config_parser_to_defaultdict(config_parser): """Convert a ConfigParser to a defaultdict. Args: config_parser (ConfigParser): A ConfigParser. """ config = defaultdict(defaultdict) for section, section_content in config_parser.items(): if section != 'DEFAULT': for option, option_value in section_content.items(): config[section][option] = option_value return config def _section_is_healthy(section, expected_keys): """Check that the section contains all keys it should. Args: section (defaultdict): A defaultdict. expected_keys (Iterable): A Set of keys that should be contained in the section. Returns: boolean: True if the section is healthy, false if not. """ return set(section.keys()) == set(expected_keys)