Recipes

This contains some ways of solving problems I’ve had with applications I use Everett in. These use cases help me to shape the Everett architecture such that it’s convenient and flexible, but not big and overbearing.

Hopefully they help you, too.

If there are things you’re trying to solve and you’re using Everett that aren’t covered here, add an item to the issue tracker.

Centralizing configuration specification

It’s easy to set up a everett.manager.ConfigManager and then call it for configuration. However, with any non-trivial application, it’s likely you’re going to refer to configuration options multiple times in different parts of the code.

One way to do this is to pull out the configuration value and store it in a global constant or an attribute somewhere and pass that around.

Another way to do this is to create a configuration component, define all the configuration options there and then pass that component around.

For example, this creates an AppConfig component which has configuration for the application:

import logging

from everett.component import ConfigOptions, RequiredConfigMixin
from everett.manager import ConfigManager


def parse_loglevel(value):
    text_to_level = {
        'CRITICAL': 50,
        'ERROR': 40,
        'WARNING': 30,
        'INFO': 20,
        'DEBUG': 10
    }
    try:
        return text_to_level[value.upper()]
    except KeyError:
        raise ValueError(
            '"%s" is not a valid logging level. Try CRITICAL, ERROR, '
            'WARNING, INFO, DEBUG' % value
        )


class AppConfig(RequiredConfigMixin):
    required_config = ConfigOptions()
    required_config.add_option(
        'debug',
        parser=bool,
        default='false',
        doc='Turns on debug mode for the applciation'
    )
    required_config.add_option(
        'loglevel',
        parser=parse_loglevel,
        default='INFO',
        doc='Log level for the application'
    )

    def __init__(self, config):
        self.raw_config = config
        self.config = config.with_options(self)

    def __call__(self, *args, **kwargs):
        return self.config(*args, **kwargs)


def init_app():
    config = ConfigManager.from_dict({})
    app_config = AppConfig(config)

    logging.basicConfig(level=app_config('loglevel'))

    if app_config('debug'):
        logging.info('debug mode!')


if __name__ == '__main__':
    init_app()

Couple of nice things here. First, is that if you do Sphinx documentation, you can use autocomponent to automatically document your configuration based on the code. Second, you can use everett.component.RequiredConfigMixin.get_runtime_config() to print out the runtime configuration at startup.

Using components that share configuration by passing arguments

Say we have multiple components that share some configuration value that’s probably managed by another component.

For example, a “basedir” configuration value that defines the root directory for all the things this application does things with.

Let’s create an app component which creates two file system components passing them a basedir:

import os

from everett.component import RequiredConfigMixin, ConfigOptions
from everett.manager import ConfigManager, parse_class


class App(RequiredConfigMixin):
    required_config = ConfigOptions()
    required_config.add_option(
        'basedir'
    )
    required_config.add_option(
        'reader',
        parser=parse_class
    )
    required_config.add_option(
        'writer',
        parser=parse_class
    )

    def __init__(self, config):
        self.config = config.with_options(self)

        self.basedir = self.config('basedir')
        self.reader = self.config('reader')(config, self.basedir)
        self.writer = self.config('writer')(config, self.basedir)


class FSReader(RequiredConfigMixin):
    required_config = ConfigOptions()
    required_config.add_option(
        'file_type',
        default='json'
    )

    def __init__(self, config, basedir):
        self.config = config.with_options(self)
        self.read_dir = os.path.join(basedir, 'read')


class FSWriter(RequiredConfigMixin):
    required_config = ConfigOptions()
    required_config.add_option(
        'file_type',
        default='json'
    )

    def __init__(self, config, basedir):
        self.config = config.with_options(self)
        self.write_dir = os.path.join(basedir, 'write')


config = ConfigManager.from_dict({
    'BASEDIR': '/tmp',
    'READER': '__main__.FSReader',
    'WRITER': '__main__.FSWriter',

    'READER_FILE_TYPE': 'json',

    'WRITER_FILE_TYPE': 'yaml'
})


app = App(config)
assert app.reader.read_dir == '/tmp/read'
assert app.writer.write_dir == '/tmp/write'

Why do it this way?

In this scenario, the basedir is defined at the app-scope and is passed to the reader and writer classes when they’re created. In this way, basedir is app configuration, but not reader/writer configuration.

Using components that share configuration using alternate keys

Say we have two components that share a set of credentials. We don’t want to have to specify the same set of credentials twice, so instead, we use alternate keys which let you specify other keys to look at for a configuration value. This lets us have both components look at the same keys for their credentials and then we only have to define them once.

Let’s create a db reader and a db writer component:

from everett.component import RequiredConfigMixin, ConfigOptions
from everett.manager import ConfigManager


class DBReader(RequiredConfigMixin):
    required_config = ConfigOptions()
    required_config.add_option(
        'username',
        alternate_keys=['root:db_username']
    )
    required_config.add_option(
        'password',
        alternate_keys=['root:db_password']
    )

    def __init__(self, config):
        self.config = config.with_options(self)


class DBWriter(RequiredConfigMixin):
    required_config = ConfigOptions()
    required_config.add_option(
        'username',
        alternate_keys=['root:db_username']
    )
    required_config.add_option(
        'password',
        alternate_keys=['root:db_password']
    )

    def __init__(self, config):
        self.config = config.with_options(self)


# Define a shared configuration
config = ConfigManager.from_dict({
    'DB_USERNAME': 'foo',
    'DB_PASSWORD': 'bar'
})

reader = DBReader(config.with_namespace('reader'))
assert reader.config('username') == 'foo'
assert reader.config('password') == 'bar'

writer = DBWriter(config.with_namespace('writer'))
assert writer.config('username') == 'foo'
assert writer.config('password') == 'bar'


# Or define different credentials
config = ConfigManager.from_dict({
    'READER_USERNAME': 'joe',
    'READER_PASSWORD': 'foo',

    'WRITER_USERNAME': 'pete',
    'WRITER_PASSWORD': 'bar',
})

reader = DBReader(config.with_namespace('reader'))
assert reader.config('username') == 'joe'
assert reader.config('password') == 'foo'

writer = DBWriter(config.with_namespace('writer'))
assert writer.config('username') == 'pete'
assert writer.config('password') == 'bar'