Source code for pyra.config

"""
..
   config.py

Responsible for config related functions.
"""
# standard imports
import sys
from typing import Optional, List

# lib imports
from configobj import ConfigObj
from validate import Validator, ValidateError

# local imports
from pyra import definitions
from pyra import logger
from pyra import locales

# get log
log = logger.get_logger(name=__name__)

# get the config filename
FILENAME = definitions.Files.CONFIG

# access the config dictionary here
CONFIG = None

# localization
_ = locales.get_text()

# increase CONFIG_VERSION default and max when changing default values
# then do `if CONFIG_VERSION == x:` something to change the old default value to the new default value
# then update the CONFIG_VERSION number


[docs] def on_change_tray_toggle() -> bool: """ Toggle the tray icon. This is needed, since ``tray_icon`` cannot be imported at the module level without a circular import. Returns ------- bool ``True`` if successful, otherwise ``False``. See Also -------- pyra.tray_icon.tray_toggle : ``on_change_tray_toggle`` is an alias of this function. Examples -------- >>> on_change_tray_toggle() True """ from pyra import tray_icon return tray_icon.tray_toggle()
# types # - section # - boolean # - option # - string # - integer # - float # data parsley types (Parsley validation) # - alphanum (string) # - email (string) # - url (string) # - number (float, integer) # - integer (integer) # - digits (string) _CONFIG_SPEC_DICT = dict( Info=dict( type='section', name=_('Info'), description=_('For information purposes only.'), icon='info', CONFIG_VERSION=dict( type='integer', name=_('Config version'), description=_('The configuration version.'), default=0, # increment when updating config min=0, max=0, # increment when updating config data_parsley_type='integer', extra_class='col-md-3', locked=True, ), FIRST_RUN_COMPLETE=dict( type='boolean', name=_('First run complete'), description=_('Todo: Indicates if the user has completed the initial setup.'), default=False, locked=True, ), ), General=dict( type='section', name=_('General'), description=_('General settings.'), icon='gear', LOCALE=dict( type='option', name=_('Locale'), description=_('The localization setting to use.'), default='en', options=[ 'en', 'es', ], option_names=[ f'English ({_("English")})', f'EspaƱol ({_("Spanish")})', ], refresh=True, extra_class='col-lg-6', ), LAUNCH_BROWSER=dict( type='boolean', name=_('Launch Browser on Startup '), description=_(f'Open browser when {definitions.Names.name} starts.'), default=True, ), SYSTEM_TRAY=dict( type='boolean', name=_('Enable System Tray Icon'), description=_(f'Show {definitions.Names.name} shortcut in the system tray.'), default=True, # todo - fix circular import on_change=on_change_tray_toggle, ), ), Logging=dict( type='section', name=_('Logging'), description=_('Logging settings.'), icon='file-code', LOG_DIR=dict( type='string', name=_('Log directory'), advanced=True, description=_('The directory where to store the log files.'), data_parsley_pattern=r'^[a-zA-Z]:\\(?:\w+\\?)*$' if definitions.Platform.os_platform == 'win32' else r'^\/(?:[^/]+\/)*$ ', # https://regexpattern.com/windows-folder-path/ # https://regexpattern.com/linux-folder-path/ extra_class='col-lg-8', button_directory=True, ), DEBUG_LOGGING=dict( type='boolean', name=_('Debug logging'), advanced=True, description=_('Enable debug logging.'), default=True, ), ), Network=dict( type='section', name=_('Network'), description=_('Network settings.'), icon='network-wired', HTTP_HOST=dict( type='string', name=_('HTTP host address'), advanced=True, description=_('The HTTP address to bind to.'), default='0.0.0.0', data_parsley_pattern=r'\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)[.]){3}' r'(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b', # https://codverter.com/blog/articles/tech/20190105-extract-ipv4-ipv6-ip-addresses-using-regex.html extra_class='col-md-4', ), HTTP_PORT=dict( type='integer', name=_('HTTP port'), advanced=True, description=_('Port to bind web server to. Note that ports below 1024 may require root.'), default=9696, min=21, max=65535, data_parsley_type='integer', extra_class='col-md-3', ), HTTP_ROOT=dict( type='string', name=_('HTTP root'), beta=True, description=_('Todo: The base URL of the web server. Used for reverse proxies.'), extra_class='col-lg-6', ), ), User_Interface=dict( type='section', name=_('User Interface'), description=_('User interface settings.'), icon='display', BACKGROUND_VIDEO=dict( type='boolean', name=_('Background video'), description=_('Enable background video.'), default=True, ), ), Updater=dict( type='section', name=_('Updater'), description=_('Updater settings.'), icon='arrows-spin', AUTO_UPDATE=dict( type='boolean', name=_('Auto update'), beta=True, description=_(f'Todo: Automatically update {definitions.Names.name}.'), default=False, ), ), )
[docs] def convert_config(d: dict = _CONFIG_SPEC_DICT, _config_spec: Optional[List] = None) -> List: """ Convert a config spec dictionary to a config spec list. A config spec dictionary is a custom type of dictionary that will be converted into a standard config spec list which can later be used by ``configobj``. Parameters ---------- d : dict The dictionary to convert. _config_spec : Optional[List] This should not be set when using this function, but since this function calls itself it needs to pass in the list that is being built in order to return the correct list. Returns ------- list A list representing a configspec for ``configobj``. Examples -------- >>> convert_config(d=_CONFIG_SPEC_DICT) [...] """ if _config_spec is None: _config_spec = [] for k, v in d.items(): try: v['type'] except TypeError: pass else: checks = ['min', 'max', 'options', 'default'] check_value = '' for check in checks: try: v[check] except KeyError: pass else: check_value += f"{', ' if check_value != '' else ''}" if check == 'options': for option_value in v[check]: if check_value: check_value += f"{', ' if not check_value.endswith(', ') else ''}" if isinstance(option_value, str): check_value += f'"{option_value}"' else: check_value += f'{option_value}' elif isinstance(v[check], str): check_value += f"{check}=\"{v[check]}\"" else: check_value += f"{check}={v[check]}" check_value = f'({check_value})' if check_value else '' # add parenthesis if there's a value if v['type'] == 'section': # config section _config_spec.append(f'[{k}]') else: # int option _config_spec.append(f"{k} = {v['type']}{check_value}") if isinstance(v, dict): # continue parsing nested dictionary convert_config(d=v, _config_spec=_config_spec) return _config_spec
[docs] def create_config(config_file: str, config_spec: dict = _CONFIG_SPEC_DICT) -> ConfigObj: """ Create a config file and `ConfigObj` using a config spec dictionary. A config spec dictionary is a strictly formatted dictionary that will be converted into a standard config spec list to be later used by ``configobj``. The created config is validated against a Validator object. This function will remove keys from the user's config.ini if they no longer exist in the config spec. Parameters ---------- config_file : str Full filename of config file. config_spec : dict, default = _CONFIG_SPEC_DICT Config spec to use. Returns ------- ConfigObj Dictionary of config keys and values. Raises ------ SystemExit If config_spec is not valid. Examples -------- >>> create_config(config_file='config.ini') ConfigObj({...}) """ # convert config spec dictionary to list config_spec_list = convert_config(d=config_spec) config = ConfigObj( configspec=config_spec_list, encoding='UTF-8', list_values=True, stringify=True, write_empty_values=False ) config_valid = validate_config(config=config) if not config_valid: # logger may not be initialized log_msg = "Unable to initialize due to a corrupted config spec. Exiting..." log.error(msg=log_msg) raise SystemExit(log_msg) user_config = ConfigObj( infile=config_file, configspec=config_spec_list, encoding='UTF-8', list_values=True, stringify=True, write_empty_values=False ) user_config_valid = validate_config(config=user_config) if not user_config_valid: # write to stderr and logger log_msg = "Invalid 'config.ini' file, attempting to correct.\n" log.error(msg=log_msg) sys.stderr.write(log_msg) # dictionary comprehension if config_valid and user_config_valid: # remove values from user config that are no longer in the spec user_config = { key: { k: v for k, v in value.items() if k in config.get(key, {}) } for key, value in user_config.items() } # remove sections from user config that are no longer in the spec user_config = {key: value for key, value in user_config.items() if key in config} # merge user config into default config config.merge(indict=user_config) # validate merged config validate_config(config=config) config.filename = config_file config.write() # write the config file if config_spec == _CONFIG_SPEC_DICT: # set CONFIG dictionary global CONFIG CONFIG = config return config
[docs] def save_config(config: ConfigObj = CONFIG) -> bool: """ Save the config to file. Saves the `ConfigObj` to the specified file. Parameters ---------- config : ConfigObj, default = CONFIG Config to save. Returns ------- bool True if save successful, otherwise False. Examples -------- >>> config_object = create_config(config_file='config.ini') >>> save_config(config=config_object) True """ try: config.write() except Exception: return False else: return True
[docs] def validate_config(config: ConfigObj) -> bool: """ Validate ConfigObj dictionary. Ensures that the given `ConfigObj` is valid. Parameters ---------- config : ConfigObj Config to validate. Returns ------- bool True if validation passes, otherwise False. Examples -------- >>> config_object = create_config(config_file='config.ini') >>> validate_config(config=config_object) True """ validator = Validator() try: config.validate( validator=validator, copy=False # don't write out default values ) return True except ValidateError as e: log_msg = f"Config validation error: {e}.\n" log.error(msg=log_msg) sys.stderr.write(log_msg) return False