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:

# recipes_appconfig.py

import logging

from everett.manager import ConfigManager, Option


TEXT_TO_LOGGING_LEVEL = {
    "CRITICAL": 50,
    "ERROR": 40,
    "WARNING": 30,
    "INFO": 20,
    "DEBUG": 10,
}


def parse_loglevel(value):
    try:
        return TEXT_TO_LOGGING_LEVEL[value.upper()]
    except KeyError:
        raise ValueError(
            f'"{value}" is not a valid logging level. Try CRITICAL, ERROR, '
            "WARNING, INFO, DEBUG"
        )


class AppConfig:
    class Config:
        debug = Option(
            parser=bool,
            default="false",
            doc="Turns on debug mode for the application",
        )
        loglevel = Option(
            parser=parse_loglevel,
            default="INFO",
            doc=(
                "Log level for the application; CRITICAL, ERROR, WARNING, INFO, "
                "DEBUG"
            ),
        )


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

    logging.basicConfig(level=config("loglevel"))

    if 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 autocomponentconfig to automatically document your configuration based on the code. Second, you can use everett.manager.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:

# recipes_shared.py

import os

from everett.manager import ConfigManager, Option, parse_class


class App:
    class Config:
        basedir = Option()
        reader = Option(parser=parse_class)
        writer = Option(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 FilesystemReader:
    class Config:
        file_type = Option(default="json")

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


class FilesystemWriter:
    class Config:
        file_type = Option(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__.FilesystemReader",
        "WRITER": "__main__.FilesystemWriter",
        "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 components 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:

# recipes_alternate_keys.py

from everett.manager import ConfigManager, Option


class DatabaseReader:
    class Config:
        username = Option(alternate_keys=["root:db_username"])
        password = Option(alternate_keys=["root:db_password"])

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


class DatabaseWriter:
    class Config:
        username = Option(alternate_keys=["root:db_username"])
        password = Option(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 = DatabaseReader(config.with_namespace("reader"))
assert reader.config("username") == "foo"
assert reader.config("password") == "bar"

writer = DatabaseWriter(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 = DatabaseReader(config.with_namespace("reader"))
assert reader.config("username") == "joe"
assert reader.config("password") == "foo"

writer = DatabaseWriter(config.with_namespace("writer"))
assert writer.config("username") == "pete"
assert writer.config("password") == "bar"