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.
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.
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.
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'