""" Application configuration values can be configured in the OS environment via KEY_NAME=... or in a toml file using [section] foo = ... bar_baz = ... In the latter case, the section and name are uppercased and joined with `_`, then inserted into the OS environment (in this case, resulting in SECTION_FOO=... and SECTION_BAR_BAZ=... ). The OS environment takes precedence - if a key is found in both env and config, the config setting will be ignored. Author(s): anatol.ulrich@basebox.io Copyright (c) 2023 basebox GmbH. All rights reserved. """ # stdlib imports from os import environ from functools import wraps # dependencies imports try: import tomllib except ImportError: import tomli as tomllib import logging # app imports import consts logger = logging.getLogger(consts.LOG_ROOT) def load(file): """ Load configuration from toml and add to environment. Skip anything already present there. Configuration key names are converted according to the module documentation. """ if file is None: logger.debug('no config file provided.') else: logger.debug('loading config') with open(file, 'rb') as handle: config = tomllib.load(handle) for section, vals in config.items(): for key, val in vals.items(): effective_key = f'{section.upper()}_{key.upper()}' if effective_key in environ: logger.warning( '%s already exists in os environment, skipping', effective_key) else: environ[effective_key] = str(val) logger.debug('config/raw: %s = %s', effective_key, environ[effective_key]) def env(key, default=None): """ retrieve a key from the OS environment. throws `KeyError` when the key is not found and no default fallback is provided. """ if default is None: return environ[key] return environ.get(key, default=default) def logged(f): """ logging wrapper for resolved configuration values """ @wraps(f) def decorated(*args, **kwargs): res = f(*args, **kwargs) logger.debug('config/resolved: %s = %s', args[0], res) return res return decorated @logged def int_val(key, default=None): """ retrieve configuration value as int """ return int(env(key, default=default)) def strtobool(val): """Convert a string representation of truth to `True` or `False`. True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if 'val' is anything else. """ val = val.lower() if val in ("y", "yes", "t", "true", "on", "1"): return True elif val in ("n", "no", "f", "false", "off", "0"): return False else: raise ValueError(f'Invalid bool string: {val}') @logged def bool_val(key, default=None): """ retrieve configuration value as bool """ return strtobool(env(key, default=default)) @logged def str_val(key, default=None): """ retrieve configuration value as bool """ return env(key, default=default)