clay/config.py (152 lines of code) (raw):

from __future__ import absolute_import import logging.config import logging import random import signal import json import time import os.path import os import sys SERIALIZERS = {'json': json} try: import yaml SERIALIZERS['yaml'] = yaml except ImportError: pass class Configuration(object): ''' Manages global configuration from JSON files ''' def __init__(self): self.paths = [] self.config = {} self.last_updated = None self.init_logging() def load(self, signum=None, frame=None): ''' Called when the configuration should be loaded. May be called multiple times during the execution of a program to change or update the configuration. This method should be overridden by a subclass. ''' return def debug(self): ''' Returns True if this server should use debug configuration and logging. This method is deprecated and ''' log = self.get_logger('clay.config') log.warning('Configuration.debug() is deprecated and may be removed in a future release of clay-flask. Please use config.get("debug.enabled", False) instead') return self.get('debug.enabled', False) def get(self, key, default=None): ''' Get the configuration for a specific variable, using dots as delimiters for nested objects. Example: config.get('api.host') returns the value of self.config['api']['host'] or None if any of those keys does not exist. The default return value can be overridden. ''' value = self.config for k in key.split('.'): try: value = value[k] except KeyError: return default #sys.stderr.write('config: %s=%r\n' % (key, value)) return value def init_logging(self): ''' Configure the default root logger to output WARNING to stderr ''' logging.basicConfig( format='%(asctime)s %(name)s %(levelname)s %(message)s', level=logging.WARNING) def reset_logging(self): ''' Reset the root logger configuration to no handlers ''' root = logging.getLogger() if root.handlers: for handler in list(root.handlers): root.removeHandler(handler) def configure_logging(self, log_config): ''' Remove all existing logging configuration and use the given configuration instead. The format of the log_config dict is specified at http://docs.python.org/2/library/logging.config.html#logging-config-dictschema ''' logging.config.dictConfig(log_config) def get_logger(self, name): ''' Returns a Logger instance that may be used to emit messages with the given log name, respecting debug behavior. ''' log = logging.getLogger(name) if self.get('debug.logging', False): log.setLevel(logging.DEBUG) else: log.setLevel(logging.INFO) return log def feature_flag(self, name): ''' Returns a boolean value for the given feature, which may be probabalistic. ''' feature = self.get('features.%s' % name) if not feature: return False if 'percent' in feature: percent = float(feature['percent']) / 100.0 return (random.random() < percent) return feature.get('enabled', False) class FileConfiguration(Configuration): def load(self, signum=None, frame=None): ''' Iterate through expected config file paths, loading the ones that exist and can be parsed. ''' cwd = os.getcwd() if not cwd in sys.path: sys.path.insert(0, cwd) self.config = {} paths = list(self.paths) if 'CLAY_CONFIG' in os.environ: paths += os.environ['CLAY_CONFIG'].split(':') for path in paths: path = os.path.expandvars(path) path = os.path.abspath(path) config = self.load_from_file(path) self.config.update(config) self.last_updated = time.time() self.init_logging() log_config = self.get('logging') if log_config: self.configure_logging(log_config) def load_from_file(self, filename): ''' Attempt to load configuration from the given filename. Returns an empty dict upon failure. ''' log = self.get_logger('clay.config') try: filetype = os.path.splitext(filename)[-1].lstrip('.').lower() if not filetype in SERIALIZERS: log.warning('Unknown config format %s, parsing as JSON' % filetype) filetype = 'json' # Try getting a safe_load function. If absent, use 'load'. load = getattr(SERIALIZERS[filetype], "safe_load", getattr(SERIALIZERS[filetype], "load")) config = load(file(filename, 'r')) if not config: raise ValueError('Empty config') log.info('Loaded configuration from %s' % filename) return config except ValueError as e: log.critical('Error loading config from %s: %s' % (filename, str(e))) sys.exit(1) return {} CONFIG = FileConfiguration() CONFIG.load() # Upon receiving a SIGHUP, configuration will be reloaded signal.signal(signal.SIGHUP, CONFIG.load) # Expose some functions at the top level for convenience get = CONFIG.get get_logger = CONFIG.get_logger feature_flag = CONFIG.feature_flag debug = CONFIG.debug