Welcome to Configly’s documentation!¶
Quickstart¶
TL;DR¶
# config.yml
foo:
bar: <% ENV[REQUIRED] %>
baz: <% ENV[OPTIONAL, true] %>
list_of_stuff:
- fun<% ENV[NICE, dament] %>al
- fun<% ENV[AGH, er] %>al
- more/<% ENV[THAN, er] %>/one/<% ENV[interpolation, er] %>!
# app.py
from configly import Config
config = Config.from_yaml('config.yml')
print(config.foo.bar)
print(config.foo['baz'])
for item in config.list_of_stuff:
print(item)
pip install configly[yaml]
Introduction¶
Loading configuration is done in every (application) project, and yet it is often overlooked and condidered too easy or straightforward to bother using a library to manage doing it.
Therefore, we often see code like this:
# config.py
import os
# Maybe it's following 12factor and loading all the config from the environment.
config = {
'log_level': os.getenv('LOG_LEVEL'),
'database': {
# At least here, I can nest values if I want to organize things.
'password': os.environ['DATABASE_PASSWORD'],
'port': int(os.environ['DATABASE_PORT']),
}
}
or this
# config.py
import os
class Config:
log_level = os.getenv('LOG_LEVEL')
# Here it's not so easy to namespace
database_password = os.environ['DATABASE_PASSWORD']
database_port = int(os.environ['DATABASE_PORT'])
# Oh goodness!
class DevConfig(Config):
environment = 'dev'
or this
import configparser
# ...🤢... Okay I dont even want to get into this one.
And this is all assuming that everyone is loading configuration at the outermost entrypoint! The two worst possible outcomes in configuration are:
You are loading configuration lazily and/or deeply within your application, such that it hits a critical failure after having seemingly successfully started up.
There is not a singular location at which you can go to see all configuration your app might possibly be reading from.
The pitch¶
Configly
asserts configuration should:
Be centralized
One should be able to look at one file to see all (env vars, files, etc) which must exist for the application to function.
Be comprehensive
One should not find configuration being loaded secretly elsewhere
Be declarative/static
code-execution (e.g. the class above) in the definition of the config inevitably makes it hard to interpret, as the config becomes more complex.
Be namespacable
One should not have to prepend
foo_
namespaces to allfoo
related config names
Be loaded, once, at app startup
(At least the definition of the configuration you’re loading)
(Ideally) have structured output
If something is an
int
, ideally it would be read as an int.
To that end, the configly.Config
class exposes a series of classmethods from which your config
can be loaded. It’s largely unimportant what the input format is, but we started with formats
that deserialize into at least str
, float
, int
, bool
and None
types.
from configly import Config
# Currently supported input formats.
config = Config.from_yaml('config.yml')
config = Config.from_json('config.json')
config = Config.from_toml('config.toml')
Given an input config.yml
file:
# config.yml
foo:
bar: <% ENV[REQUIRED] %>
baz: <% ENV[OPTIONAL, true] %>
list_of_stuff:
- fun<% ENV[NICE, dament] %>al
- fun<% ENV[AGH, er] %>al
- more/<% ENV[THAN, er] %>/one/<% ENV[interpolation, er] %>!
A number of things are exemplified in the example above:
Each
<% ... %>
section indicates an interpolated value, the interpolation can be a fragment of the overall value, and multiple values can be interpolated within a single value.ENV
is an “interpolator” which knows how to obtain environment variables[VAR]
Will raise an error if that piece of config is not found, whereas[VAR, true]
will defaultVAR
to the value after the commaWhatever the final value is, it’s interpreted as a literal value in the format of the file which loads it. I.E.
true
-> pythonTrue
,1
-> python1
, andnull
-> pythonNone
.
Now that you’ve loaded the above configuration:
# app.py
from configly import Config
config = Config.from_yaml('config.yml')
# You can access namespaced config using dot access
print(config.foo.bar)
# You have use index syntax for dynamic, or non-attribute-safe key values.
print(config.foo['baz'])
# You can iterate over lists
for item in config.list_of_stuff:
print(item)
# You can *generally* treat key-value maps as dicts
for key, value in config.foo.items():
print(key, value)
# You can *actually* turn key-value maps into dicts
dict(config.foo) == config.foo.to_dict()
Installing¶
# Basic installation
pip install configly
# To use the yaml config loader
pip install configly[yaml]
# To use the toml config loader
pip install configly[toml]
# To use the vault config loader
pip install configly[vault]
Interpolators¶
An “interpolator” is a class which knows how to get values from a particular source by interpreting the internal portion of a dynamic config value and replacing it with a value.
Default included interpolators include:
ENV (environment variables)
FILE (file data)
You can use all registered interpolators when loading the configuration
namespace:
env: <% ENV[ENV, production] %>
log_level: DEBUG
ssl_cert: FILE[ssl_cert.crt]
theoretical_http_loaded_value: <% HTTP[localhost:5000/variable, 3] %>
The point is that all pieces of individual configuration can be defined centrally and declaratively, while interpolators actually go obtain that config value associate with the given input.
Configly allows the dynamic addition of new interpolator through the use of
the register_interpolator()
function.
API¶
Config¶
-
class
configly.
Config
(value=None, _src_input=None, _loader=None, _registry=<configly.registry.Registry object>)¶ Container for configuration.
>>> config = Config({"a": 1, "b": {"c": 2}}) >>> config.a 1 >>> config.b.c 2
-
classmethod
from_json
(file=None, content=None, registry=<configly.registry.Registry object>)¶ Open a toml file and load it into the resulting config object.
-
classmethod
from_toml
(file=None, content=None, registry=<configly.registry.Registry object>)¶ Open a toml file and load it into the resulting config object.
-
classmethod
from_yaml
(file=None, *, content=None, registry=<configly.registry.Registry object>)¶ Open a yaml file and load it into the resulting config object.
-
refresh
()¶ Reevaluate the interpolation of variable values in the given sub-config.
This will be particularly useful for values which are coming from sources where the value might change.
-
to_dict
()¶ Return a dict equivalent of the config object.
Roughly equivalent to >>> dict(Config({1:1})) == Config({1:1}).to_dict() True
-
classmethod
-
class
configly.
Interpolator
¶ ABC to define the interface required by an interpolator.
It is not required to subclass Interpolator, but it does provide the interface and ensures the class implements it.
-
abstract
__getitem__
(name)¶ Override this method to implement a method to get the value for a piece of config.
This method should return a KeyError when the value cannot be found.
-
get
(name, default=None)¶ Implement get operation with a default.
Override this method to get more tailored behavior.
-
abstract
-
class
configly.
Registry
¶ A registry to allow for non-bundled interpolators and config loaders to be added.
By default Config uses a global registry, to which you can register_interpolator.
If you need more flexibility, you can pass registry to any of the from_* classmethods to use your own registry.
>>> from configly import Config >>> local_registry = Registry() >>> config = Config.from_yaml('readthedocs.yml', registry=local_registry)
-
register_interpolator
(name, interpolator_cls, overwrite=False)¶ Register a new interpolator for loading configuration from different sources.
By default Config classes read from a global registry of interpolators. This function registers new interpolators to that global registry.
For example, internally configly registers environment interpolation through a call like:
>>> from configly import EnvVarInterpolator >>> register_interpolator("ENV", EnvVarInterpolator, overwrite=True)
-
-
configly.
register_interpolator
(name, interpolator_cls, overwrite=False)¶ Register a new interpolator for loading configuration from different sources.
By default Config classes read from a global registry of interpolators. This function registers new interpolators to that global registry.
For example, internally configly registers environment interpolation through a call like:
>>> from configly import EnvVarInterpolator >>> register_interpolator("ENV", EnvVarInterpolator, overwrite=True)
Contributing¶
Prerequisites¶
If you are not already familiar with Poetry, this is a poetry project, so you’ll need this!
Getting Setup¶
See the Makefile
for common commands, but for some basic setup:
# Installs the package with all the extras
make install
And you’ll want to make sure you can run the tests and linters successfully:
# Runs CI-level tests, with coverage reports
make test lint
Need help¶
Submit an issue!