Welcome to configstacker’s documentation¶
Introduction¶
What is configstacker?¶
Configstacker is a python library with the goal to simplify configuration handling as much as possible. You can read configurations from different sources (e.g. files, environment variables and others) and load single or merge multiple sources at will. The resulting configuration object can be used like a dictionary or with dot-notation. If you need additional flexibility configstacker also allows you to specify converters for values or merging strategies for keys that occur multiple times throughout different sources. If this is still not sufficient enough configstacker makes it very easy for you to add additional source handlers.
#
# Single Source Example
#
from configstacker import YAMLFile
config = YAMLFile('/path/to/my/config.yml')
# Dot-notation access
assert config.a_key is True
# Mixed dictionary and dot-notation access
assert config['subsection'].nested_key == 100
# Dictionary-like interface
assert config.keys() == ['a_key', 'subsection']
# Dictionary dumping on any level
assert config.dump() == {'a_key': True, 'subsection': {'nested_key': 100}}
assert config.subsection.dump() == {'nested_key': 100}
# New value assignment
config.new_value = 10.0
assert config.new_value == 10.0
#
# Multi Source Example
#
import configstacker as cs
# The resulting configuration object behaves
# the same as a single source one.
config = cs.StackedConfig(
# The order of sources defines their search priority whereas the
# last element has the highest one.
cs.Environment(prefix='MYAPP'),
cs.YAMLFile('/path/to/my/config.yml')
)
# Higher priority values shadow lower priority values.
assert config.a_key is False
# Lower prioritized values which are not shadowed stay accessible.
assert config['subsection'].nested_key == 100
# New values will be added to the source that has the highest
# priority and is writable.
config.other_new_value = True
assert config.other_new_value is True
# In this case the new item was added to the last element in the
# source list.
assert config.source_list[-1].other_new_value
Installation¶
Configstacker can be installed with pip and only requires six for the minimal installation.
pip install configstacker
However, some of the source handlers require additional packages when in use.
- YAMLFile requires pyyaml
You can use the following syntax to install all optional dependencies. Leave out those from the brackets you do not need.
pip install configstacker[yaml]
Note
New source handlers with additional dependencies might be added over time.
Next Steps¶
To get up and running you can check out the quickstart tutorial. After that you should be able to easily integrate configstacker into your project. If you want to dive deeper into configstacker you can also read about more advanced use cases like providing type information, key-merge-strategies or adding additional source handlers. Finally the api documentation will cover all available features, classes, parameters, etc.
Quickstart¶
For simple use cases the introductory examples contains essentially everything that is required for simple use cases. However, let’s create a more elaborate example by providing some real configuration sources that we can play with.
Note
The following code snippets are either simple code blocks which you
can copy and paste or interactive shell like code blocks with
a >>>
or $
symbol to demonstrate example usages.
Note
If you want to skip parts of the quickstart guide be aware that the examples depend on eachother.
Preparation¶
First create a JSON file named config.json
with the following content:
{
"simple_int": 10,
"a_bool": false,
"nested": {
"a_float": 2.0
}
}
Because JSON files have the same structure as Python’s dictionaries a
configuration object generated from the above file contains exactly the
same tree of keys and values as the file itself. Additionally the json
format stores type information which means that the parsed values will
already be converted to the respective type (e.g string, boolean,
number). Before jumping into the code let’s create another file. This
time it is an INI file named config.ini
with the following content:
[__root__]
simple_int = 5
a_bool = True
keys = 1,2,3
[section]
some_float = 0.1
[section.subsection]
some_string = A short string
By default Python’s builtin configparser does not provide any hierarchical information which means there are no concepts like root level keys and subsections. To solve that issue configstacker implements a common flavor of INI file formats which use a separator in their section names to denote a hierarchy. Also the builtin configparser requires that all keys belong to a section. Because of that we need a special name to differentiate root level keys from subsection keys. To summarize:
- The root level is indicated by a specially named section. By default
it is called
[__root__]
. - Subsections are denoted by separators inside section names. The default separator is a dot.
Finally it is important to note that INI files do not store type information. As such all values on a configuration object will be returned as strings. This can be solved in two ways. We can either make use of a stacked configuration with at least one other source that contains type information or we provide converters.
Note
The conventions used here are just the way how the INI file source handler shipped with configstacker works. Of course you can create your own INI file handler which uses another format. For example double brackets for subsections. However, in this case you also have to provide the respective parser as python’s builtin configparser does not support this format.
Now that everything is prepared we can import and setup configstacker itself. We will start by setting up a single source and playing around with it. After that we will put everything together into a stacked configuration.
Using Single Sources¶
Make sure you are in your (virtual) environment where you installed
configstacker and open up your interactive python shell. Start with
importing configstacker and instantiating a JSONFile
object with
the path pointing to the JSON file you created earlier.
import configstacker as cs
json = cs.JSONFile('config.json')
Without much surprise you should be able to access the information from the JSON file.
>>> json.simple_int
10
>>> json.a_bool
False
>>> json.nested.a_float
2.0
Let’s look at a more interesting case - the INI file.
ini = cs.INIFile('config.ini', subsection_token='.')
Note
The subsection_token
is already a dot by default and only set
for demonstration purpose.
You can access it the same way as with the JSON object.
>>> ini.simple_int
'5'
>>> ini.a_bool
'True'
>>> ini.section.some_float
'0.1'
>>> ini
INIFile({...})
There are two things to note here:
- As mentioned earlier due to the untyped nature of INI files the returned values are always strings.
- When you print the whole config object it will show you the type of source and its content.
You can always use dictionary style to access the values. This is especially important when a key has the same name as a builtin function.
>>> ini['simple_int']
'5'
>>> ini['section']['some_float']
'0.1'
>>> ini['keys']
'1,2,3'
>>> ini.keys()
['a_bool', 'keys', 'section', 'simple_int']
Although configuration objects behave like dictionaries in some cases you might still want to use a real python dictionary. Therefore you can easily dump the data from any sublevel.
>>> type(ini.dump())
<class 'dict'>
>>> ini.section.dump() == {'some_float': '0.1', 'subsection': {'some_string': 'A short string'}}
True
>>> ini.section.subsection.dump()
{'some_string': 'A short string'}
Using Stacked Sources¶
Now let’s have a look at stacked configurations. When stacking source handlers their order defines how keys and values are prioritized while searching and accessing them. By default the last element has the highest priority. This behavior can be changed though.
# We are reusing the configuration objects from the last examples
config = cs.StackedConfig(json, ini)
Note
The idea behind the default ordering is that you usually start with some global defaults which have the lowest priority and further advance to more specific configuration sources with a higher priority until you finally reach the last and most important one.
Let’s access the data again and check what it looks like:
>>> config.simple_int
5
>>> config.a_bool
True
>>> config.section.some_float
'0.1'
>>> config.nested.dump()
{'a_float': 2.0}
Some observations:
- The
config
object behaves exactly like the single source object. In fact both the stacked and the single source configuration objects provide the same dictionary interface and you can use them interchangeably. simple_int
is 5 and not 10 as the INI file has a higher priority over the JSON file. However, now that there is another source with type information available the value from the INI file was converted to an integer. This works because configstacker will search other sources for a typed version of the same key and converts it accordingly.some_float
is still untyped as there is no counterpart in the JSON file.- Because the INI file is lacking a
nested
section it is not shadowing the JSON file here and as such we have access to some data from the lower prioritized JSON file.
Changing Behavior of Stacked Sources¶
Now let us create some environment variables to play with. As with INI files environment variables also lack any hierarchical information. However we can fix that the same way as we did with INI files by decoding subsections into the variable names. Therefore we will use:
- the prefix MYAPP to indicate data related to our app.
- a pipe as a separator for nested sections.
- an underscore as a separator for phrases as we did in our last examples.
import os
os.environ['MYAPP|SIMPLE_INT'] = '-10'
os.environ['MYAPP|SECTION|SOME_FLOAT'] = '3.5'
Note
Usually it is recommended to only use alphanumeric characters and underscores for environment variables to prevent problems with less capable tools. However, using just one special character means we can’t differentiate between the separation of subsections and phrases. It is up to you to decide whether you can ignore such less capable tools or not.
Stacked configuration sources can be modified at any time so that we can
easily integrate additional source handlers. Therefore
StackedConfig
exposes a SourceList
object which behaves
like a python list.
First create the environment source itself. Then integrate it with the stacked configuration object.
env = cs.Environment('MYAPP', subsection_token='|')
# appending it to the end gives it the highest priority
config.source_list.append(env)
Now we can access the information as we did before:
>>> config.a_bool
True
>>> config.simple_int
-10
>>> config.section.some_float
'3.5'
How are the values generated?
a_bool
contains the value from INI file which then has been converted with type information from the JSON file.simple_int
contains the value from the newly added environment variables and was converted with type information from the JSON file.some_float
contains the value from the environment variables as it is higher prioritized than the INI file. However because there is no counterpart in the JSON file it is returned as a string.
Persisting Changes¶
In most cases changing values of a configuration is straight forward as it does not require any special setup. We can simply assign the new value to the respective key.
>>> config.a_bool = False
>>> config.a_bool
False
That also works with keys which do not yet exist.
>> config.new_value = 5
>> config.new_value
5
The following rules are applied to decide where to store changes.
- Newly added key-value pairs are written to the highest priority source that is writable.
- Changes to existing values are written to the highest priority source that is writable and where the respective keys were found.
- If no writable source was found a TypeError is raised when changing a value.
Note
If you want to protect a source from beeing changed at all you can
provide the parameter readonly=True
when
instantiating the source. However, if you just want to prevent
immediate writes to the underlying source (e.g. because of network
connectivity) you can make use of the cache functionality
.
Advanced Use Cases¶
Providing Type Converters¶
When working with untyped sources like INI files or environment variables there are a few approaches to convert values to the correct type.
- Guess a type based on the content
- Distill information from other sources
- Manually provide type information for each key
Each approach has is pros and cons, ranging from either more failures to otherwise more manual labor. Because failures are inacceptable only the the second approach will be applied whenever possible and everything else stays untouched. However, configstacker makes it fairly easy for you to provide additional information to convert values.
Converters for Specific Keys¶
Consider the following setup.
import os
import configstacker as cs
os.environ['MYAPP|SOME_NUMBER'] = '100'
os.environ['MYAPP|SOME_BOOL'] = 'False'
os.environ['MYAPP|NESTED|SOME_BOOL'] = 'True'
untyped_env = cs.Environment('MYAPP', subsection_token='|')
Accessing the values will return them as strings because configstacker has no other source of knowledge.
>>> type(untyped_env.some_number)
<class 'str'>
To solve that issue we can provide a list of converters. A converter consists of a value name
and two converter functions. One that turns the value into the expected
format (here int
) and another one that turns it back to a storable
version like str
.
converter_list = [
cs.Converter('some_number', int, str)
]
partly_typed_env = cs.Environment('MYAPP', converters=converter_list, subsection_token='|')
Now accessing some_number
will return it as an integer while
some_bool
is still a string.
>>> partly_typed_env.some_number
100
>>> type(partly_typed_env.some_number)
<class 'int'>
To also convert the boolean value we could just compare the value to strings and return the result.
def to_bool(value):
return value == 'True'
assert to_bool('True')
However, that is not very safe and we should be a bit smarter about it. So more elaborate versions might even use other libraries to do the heavy lifting for us.
import distutils
def to_bool(value):
return bool(distutils.util.strtobool(value))
assert to_bool('yes')
After we choose an approach let’s put everything together. For convenience we can provide the converters as simple tuples. Configstacker will convert them internally. Just make sure to stick to the correct order of elements. First the key to convert, then the customizing function and finally the resetting function.
converter_list = [
('some_bool', to_bool, str),
('some_number', int, str),
]
typed_env = cs.Environment('MYAPP', converters=converter_list, subsection_token='|')
Now all values including the nested ones are typed. Also the value assignment works as expected.
>>> typed_env.some_number
100
>>> typed_env.some_bool
False
>>> typed_env.some_bool = True
>>> typed_env.some_bool
True
>>> typed_env.some_bool = 'False'
>>> typed_env.some_bool
False
>>> typed_env.nested.some_bool
True
Converters With Wildcards¶
The previous method only works if you know the value names in advance. Consider we have a simple map of libraries with their name and version string.
DATA = {
'libraries': {
'six': '3.6.0',
'requests': '1.2.1',
}
}
When accessing a library it would be really nice to get its version as a named tuple rather then a simple string. The issue is that we don’t want to specify a converter for each library. Instead we can make use of a wildcard to apply a converter to all matching keys. Let’s first create the version class and its serializers.
import collections
Version = collections.namedtuple('Version', 'major minor patch')
def to_version(version_string):
parts = version_string.split('.')
return Version(*map(int, parts))
def to_str(version):
return '.'.join(map(str, version))
With that in place we can create our config object with a converter that uses a wildcard to match all nested elements of libraries.
import configstacker as cs
config = cs.DictSource(DATA, converters=[
('libraries.*', to_version, to_str)
])
Now we can access a library by its name and get a nice version object returned.
>>> config.libraries.six
Version(major=3, minor=6, patch=0)
>>> config.libraries.requests.major
1
It is important to understand that changing a value on the converted object will not be stored in the configuration automatically. This is because configstacker doesn’t know when the custom object changes. To save changes to the object you can simply reassign it to the same key that you used to access it in the first place. You can also assign the object to a new key as long as it is covered by a converter or the underlying source handler is capable of storing it.
Note
The previous example only show cased the idea. For real use cases you should probably use a library that knows how to properly handle versions strings.
Converting Lists¶
In the previous section we had a very simple data dictionary where all libraries consisted of a name-to-version mapping. In this example we will have a list of json-like objects instead. The information is the same as before. Each item consists of a name and version pair.
DATA = {
'libraries': [{
'name': 'six',
'version': '3.6.0',
}, {
'name': 'request',
'version': '1.2.0',
}]
}
Equally we will setup our classes and serializers first. Also we assume a very simple version bump logic where a major update will reset the minor and patch numbers to zero and a minor upate resets the patch number to zero.
import collections
import configstacker as cs
import six
Version = collections.namedtuple('Version', 'major minor patch')
class Library(object):
def __init__(self, name, version):
self.name = name
self.version = version
def bump(self, part):
major, minor, patch = self.version
if part == 'major':
new_parts = major + 1, 0, 0
elif part == 'minor':
new_parts = major, minor + 1, 0
elif part == 'patch':
new_parts = major, minor, patch + 1
else:
raise ValueError('part must be major, minor or patch')
self.version = Version(*new_parts)
Because configstacker doesn’t know anything about the type of the values it has no idea how to treat lists. Especially how to parse and iterate over the individual items. So instead of just creating converter functions for single items we additionally have to create wrappers to convert and reset the whole list.
def _load_library(library_spec):
version_parts = library_spec['version'].split('.')
version = Version(*map(int, version_parts))
return Library(library_spec['name'], version)
def load_list_of_libraries(library_specifications):
"""Convert list of json objects to Library objects."""
return [_load_library(specification) for specification in library_specifications]
def _dump_library(library):
version_str = '.'.join(map(str, library.version))
return {'name': library.name, 'version': version_str}
def dump_list_of_libraries(libraries):
"""Dump list of Library objects to json objects."""
return [_dump_library(library) for library in libraries]
The final config object will be created as follows:
config = cs.DictSource(DATA, converters=[
('libraries', load_list_of_libraries, dump_list_of_libraries)
])
Converting Subsections¶
Usually a subsection, or a nested dict in python terms, is handled in a special way as it needs to be converted into a configstacker instance. However, you can change that behavior by providing a converter for a subsection. This is useful if you want to use the nested information to assemble a larger object or only load an object if it is accessed.
As an example consider we have a todo application that stores todos in a database, a caldav resource or anything else. To keep the application flexible it needs to be ignorant to how the storage system works internally but it has to know about how it can be used. For that reason the storage should implement an interface that is known to the application. Also it shouldn’t be hardcoded into the application but injected into it at start or runtime. With this setup it is pretty simple for the user to specify which storage to use and how it should be configured. When starting the application we will then dispatch the configuration to the specific storage factory and assemble it there.
The next snippet contains the interface IStorage
that enforces the
existence of a save
and a get_by_name
method. Additionally you
will find two dummy implementations for a database and a caldav storage.
import abc
class IStorage(abc.ABC):
@abc.abstractmethod
def save(self, todo):
pass
@abc.abstractmethod
def get_by_name(self, name):
pass
class DB(IStorage):
def __init__(self, user, password, host='localhost', port=3306):
# some setup code
self.host = host
def get_by_name(self, name):
# return self._connection.select(...)
pass
def save(self, todo):
# self._connection.insert(...)
pass
def __repr__(self):
return '<DB(host="%s")>' % self.host
class CalDav(IStorage):
def __init__(self, url):
# some setup code
self.url = url
def get_by_name(self, name):
# return self._resource.parse(...)
pass
def save(self, todo):
# self._resource.update(...)
pass
def __repr__(self):
return '<CalDav(url="%s")>' % self.url
The following two json files are examples of how a storage configuration
could look like. They are following a simple convention. type
is the
class that should be loaded while the content of the optional setup
key will be passed to the class on instantiation.
{
"storage": {
"type": "DB",
"setup": {
"user": "myuser",
"password": "mypwd"
}
}
}
{
"storage": {
"type": "CalDav",
"setup": {
"url": "http://localhost/caldav.php"
}
}
}
First let’s continue without a converter to see the difference between
both versions. We read the config files as usual and assemble a storage
object with the respective class from the storage_module
. Finally
it gets injected into the todo application.
import configstacker as cs
import storage_module
config = cs.YAMLFile('/path/to/config.yml')
storage_class = getattr(storage_module, config.storage.type)
storage = storage_class(**config.storage.get('setup', {}))
app = TodoApp(storage)
To make the code cleaner we could refactor the storage related setup into its own function and assign it to a converter instead.
import configstacker as cs
import storage_module
def load_storage(spec):
storage_class = getattr(storage_module, spec.type)
return storage_class(**spec.get('setup', {}))
config = cs.YAMLFile('/path/to/config.yml',
converters=[('storage', load_storage, None)])
app = TodoApp(config.storage)
With the converter in place accessing the storage returns us a fully constructed storage object instead of a nested subsection.
>>> config.storage
<CalDav(url="http://localhost/caldav.php")>
Using Merge Strategies¶
Often configurations are meant to override each other depending on their priority. However, there are cases where consecutive values should not be overridden but handled differently, for example collected into a list.
Consider we are building a tool that allows to specify multiple paths. For that we want to define a set of default paths and enable our users to add additional paths if they want to.
import os
import configstacker as cs
# our predefined defaults
DEFAULTS = {
'path': '/path/to/default/file'
}
# a user set variable
os.environ['MYAPP|PATH'] = '/path/to/user/file'
config = cs.StackedConfig(
cs.DictSource(DEFAULTS),
cs.Environment('MYAPP', subsection_token='|'),
)
When we try to access path
we will only get the value from the
source with the highest priority which in this case is the environment
variable.
>>> config.path
'/path/to/user/file'
To solve this problem we can use a merge strategy that simply collects all values into a list. For that we create a strategy map which contains the value’s name and its merge function. All occurrences of the specified key will now be merged consecutively with the previous merge result.
config = cs.StackedConfig(
cs.DictSource(DEFAULTS),
cs.Environment('MYAPP', subsection_token='|'),
strategy_map={
'path': cs.strategies.collect
}
)
Here we use collect
which is one of the builtin strategies and perfectly fits our needs. Now when
we access path
it returns a list of values in the prioritized order.
>>> config.path
['/path/to/user/file', '/path/to/default/file']
Let’s say instead of merging the paths into a list we want to join all
paths with a colon (or semicolon if you are on Windows). Create
a function that accepts a previous
and a next_
parameter and
join both values together.
def join_paths(previous, next_):
if previous is cs.strategies.EMPTY:
return next_
return ':'.join([previous, next_])
assert join_paths(cs.strategies.EMPTY, '/a/path') == '/a/path'
assert join_paths('/a/path', '/other/path') == '/a/path:/other/path'
Some things to note:
next_
ends with an underscore to prevent name clashes with the builtinnext()
function.- When the merge function is called
previous
contains the result from the last call andnext_
contains the current value.- If the merge function is called for the first time configstacker will pass
EMPTY
toprevious
. This is a good time to return a default value which in our case isnext_
.
To have something to play with we will also access the system environment variables so that we can make use of our global path variable. To not accidentally change anything we will load them in a read-only mode.
config = cs.StackedConfig(
cs.Environment('', readonly=True),
cs.DictSource(DEFAULTS),
cs.Environment('MYAPP', subsection_token='|'),
strategy_map={
'path': join_paths
}
)
path
should now return a single string with the user defined path,
the default path and the system path joined together with a colon.
>>> config.path
'/path/to/user/file:/path/to/default/file:/...'
Warning
This is a demonstration only. Be extra cautious when accessing the system variables like that.
Note
When using an empty prefix to access the system variables understand
that MYAPP
variables will also show up unparsed as myapp|...
in the config object. This is because the source handler with the
empty prefix doesn’t know anything about the special meaning of
MYAPP
.
Extending Source Handlers¶
Configstacker already ships with a couple of source handlers. However, there are always reasons why you want to override an existing handler or create a completely new one. Maybe the builtin handlers are not working as required or there are simply no handlers for a specific type of source available.
Therefore configstacker makes it fairly easy for you to create new
handlers. You only have to extend the base Source
class and
create at least a _read()
method that returns
a dictionary. If you also want to make it writable just add
a _write(data)
method which in return accepts
a dictionary and stores the data in the underlying source.
Assume you want to read information from a command line parser. There
are a couple of ways to accomplish that. The easiest one would be to
handover already parsed cli parameters to configstacker as
a DictSource
which is readonly and has the highest priority in
a stacked configuration. This method is great because it allows us to
easily incorporate any cli parser you like. It just has to return
a dictionary. Another way would be to create a parser that hooks into
sys.argv
and strips out the information itself. For demonstration
purposes we will implement the latter case.
Note
Handling the cli is a complex task and varies greatly between
applications and use cases. As such there is no default cli
handler integrated into configstacker.
If you are building a cli for your application it is probably
easier to just go with the DictSource
approach and make use of
great tools like click.
The following handler will create an argparse.ArgumentParser
internally. Because cli parameters cannot be changed after the script or
application has been started we don’t need a _write
method to save
changes. Additionally because they are only entered once at the startup
of the application we also don’t need to lazy load them. Therefore the
arguments can already be parsed in the __init__
method which makes
_read
very simple.
import argparse
import sys
import configstacker as cs
class CliSource(cs.Source):
def __init__(self, argv=None):
self._parser = argparse.ArgumentParser()
self._parser.add_argument('job_name')
self._parser.add_argument('--job-cache', action='store_true')
self._parser.add_argument('-r', '--job-retries', type=int, default=0)
self._parser.add_argument('--host-url', default='localhost')
self._parser.add_argument('--host-port', type=int, default=5050)
self._parser.add_argument('-v', '--verbose', action='count', default=0)
self._parsed_data = {}
parsed = self._parser.parse_args(argv or sys.argv[1:])
for (argument, value) in parsed._get_kwargs():
tokens = argument.split('_')
subsections, key_name = tokens[:-1], tokens[-1]
last_subdict = cs.utils.make_subdicts(self._parsed_data, subsections)
last_subdict[key_name] = value
super(CliSource, self).__init__()
def _read(self):
return self._parsed_data
def main():
cfg = CliSource()
# just some demonstration code
if cfg.verbose > 0:
print('Job runner:\t{url}:{port}'.format(**cfg.host))
if cfg.verbose > 1:
cache_state = 'enabled' if cfg.job.cache else 'disabled'
print('Job cache:\t%s' % cache_state)
print('Max retries:\t%s' % cfg.job.retries)
print('Start job %s' % cfg.job.name)
if __name__ == '__main__':
main()
We can test it by invoking our handler and providing some arguments to it.
>>> cfg = CliSource(['-vv', 'some_job'])
>>> cfg.name
'some_job'
>>> cfg.verbose
2
>>> cfg.index.cache
False
Now let’s call it as a script to make use of the exemplary main function. It will show us the help.
$ python cli-source.py -h
usage: cli-source.py [-h] [--job-cache] [-r JOB_RETRIES] [--host-url HOST_URL]
[--host-port HOST_PORT] [-v]
job_name
positional arguments:
job_name
optional arguments:
-h, --help show this help message and exit
--job-cache
-r JOB_RETRIES, --job-retries JOB_RETRIES
--host-url HOST_URL
--host-port HOST_PORT
-v, --verbose
And finally run a pseudo job.
$ python cli-source.py -vv some_job
Job runner: localhost:5050
Job cache: disabled
Max retries: 0
Start job some_job
Package API¶
License¶
BSD 3-Clause License
Copyright (c) 2017, Philipp Busch <hakkeroid@philippbusch.de> All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
- Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Version¶
Configstacker adheres to Semantic Versioning.
The current version is 0.1.1 which means configstacker is still in a planning phase. As such it is not meant for production use. That said it is already very stable and should hit its first major version soon.
Change Log¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
[Unreleased]¶
nothing new yet
[0.1.1] - 2019-07-20¶
nothing new yet
[0.1.0] - 2019-07-20¶
Added¶
- Reimplementation of layeredconfig’s API
- Usable stand-alone sources
- Custom type conversion
- Custom folding strategy for consecutive sources