bison/bison/bison.py

220 lines
7.4 KiB
Python

# -*- coding: utf-8 -*-
"""
bison.bison
~~~~~~~~~~~
This module implements the `bison` API.
"""
import logging
import os
import yaml
from bison.errors import BisonError
from bison.utils import DotDict
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
# enumerate the supported configuration formats
YAML, = range(1)
class Bison(object):
"""The configuration management object.
The `Bison` object is used to search for, read, parse, and validate
application configurations based on a set of defaults, configuration
files, environment variables, and overrides. `Bison` is not intended
to incorporate command line arguments.
By default, `Bison` is configured to look for a file named 'config'
with the YAML format (file extension .yml or .yaml).
Args:
scheme (bison.Scheme): The scheme to use for validation when parsing
a configuration file. If no scheme is provided, no validation will
occur. (default: None)
enable_logging (bool): Enable `bison` logging. If this is set to True,
`bison` will log out at the logging.INFO level; otherwise, logging
for `bison` is disabled. (default: False)
"""
# map the configuration format to its supported file extensions
_fmt_to_ext = {
YAML: ('.yml', '.yaml')
}
# map the configuration format to the function is uses to load
# the configuration data from file.
_fmt_to_parser = {
YAML: yaml.safe_load
}
def __init__(self, scheme=None, enable_logging=False):
logger.disabled = not enable_logging
self.scheme = scheme
self.config_name = 'config' # the name of the config file
self.config_format = YAML # format of the config file
self.config_paths = [] # the path(s) to search for the config file
self.config_file = None # the config file to read
self.env_prefix = None # the environment variable prefix
self.auto_env = False # automatically bind options to env variables
# the component configurations
self._default = DotDict()
self._config = DotDict()
self._environment = DotDict()
self._override = DotDict()
# the unified configuration.
self._full_config = None
@property
def config(self):
"""Get the complete configuration where the default, config,
environment, and override values are merged together.
Returns:
(DotDict): A dictionary of configuration values that
allows lookups using dot notation.
"""
if self._full_config is None:
self._full_config = DotDict()
self._full_config.update(self._default)
self._full_config.update(self._config)
self._full_config.update(self._environment)
self._full_config.update(self._override)
return self._full_config
def get(self, key, default=None):
"""Get the value for the configuration `key`.
Args:
key (str): The key into the configuration dictionary to get.
default: The value to return if the given `key` is not found
in the `Bison` config. This defaults to `None`.
Returns:
The value for the given key, if it exists; `None` otherwise.
"""
return self.config.get(key, default)
def set(self, key, value):
"""Set a value in the `Bison` configuration.
Args:
key (str): The configuration key to set a new value for.
value: The value to set.
"""
# the configuration changes, so we invalidate the cached config
self._full_config = None
self._override[key] = value
def add_config_paths(self, *paths):
"""Add paths to search for the configuration file.
Args:
*paths (str): The paths to search in for the configuration file.
"""
self.config_paths.extend(paths)
def validate(self):
"""Validate the `Bison` configuration against the `Scheme`, if one
is set.
Raises:
errors.SchemeValidationError: The `Bison` configuration fails
schema validation.
"""
if self.scheme:
self.scheme.validate(self.config)
def parse(self):
"""Parse the configuration sources into `Bison`."""
self._parse_default()
self._parse_config()
self._parse_env()
def _find_config(self):
"""Searches through the configured `config_paths` for the `config_name`
file.
If there are no `config_paths` defined, this will raise an error, so the
caller should take care to check the value of `config_paths` first.
Returns:
str: The fully qualified path to the configuration that was found.
Raises:
Exception: No paths are defined in `config_paths` or no file with
the `config_name` was found in any of the specified `config_paths`.
"""
for search_path in self.config_paths:
for ext in self._fmt_to_ext.get(self.config_format):
path = os.path.abspath(os.path.join(search_path, self.config_name + ext))
if os.path.isfile(path):
self.config_file = path
return
raise BisonError('No file named {} found in search paths {}'.format(
self.config_name, self.config_paths))
def _parse_config(self):
"""Parse the configuration file, if one is configured, and add it to
the `Bison` state.
"""
if len(self.config_paths) > 0:
self._find_config()
try:
with open(self.config_file, 'r') as f:
parsed = self._fmt_to_parser[self.config_format](f)
except Exception as e:
raise BisonError(
'Failed to parse config file: {}'.format(self.config_file)
) from e
# the configuration changes, so we invalidate the cached config
self._full_config = None
self._config = parsed
def _parse_env(self):
"""Parse the environment variables for any configuration if an `env_prefix`
is set.
"""
env_cfg = DotDict()
# if the env prefix doesn't end with '_', we'll append it here
if self.env_prefix and not self.env_prefix.endswith('_'):
self.env_prefix = self.env_prefix + '_'
# if there is no scheme, we won't know what to look for so only parse
# config if there is a scheme.
if self.scheme:
for k, v in self.scheme.flatten().items():
value = v.parse_env(k, self.env_prefix, self.auto_env)
if value is not None:
env_cfg[k] = value
if len(env_cfg) > 0:
# the configuration changes, so we invalidate the cached config
self._full_config = None
self._environment.update(env_cfg)
def _parse_default(self):
"""Parse the `Schema` for the `Bison` instance to create the set of
default values.
If no defaults are specified in the `Schema`, the default dictionary
will not contain anything.
"""
# the configuration changes, so we invalidate the cached config
self._full_config = None
if self.scheme:
self._default.update(self.scheme.build_defaults())