grift
grift copied to clipboard
A clean approach to app configuration
|Build Status| |Coverage Status| |Pyup Updates| |Py3 Ready|
grift
Classes to define, load, validate, and store values for an application's configuration.
Installation
::
pip install grift
Define Configuration Schema
Create a class that inherits from BaseConfig and add a
ConfigProperty for each setting
.. code:: python
from grift import BaseConfig, ConfigProperty, DictLoader
from schematics.types import BooleanType, StringType, IntType
class AppConfig(BaseConfig):
# defaults can be specified, and properties can be optional
DEBUG = ConfigProperty(property_type=BooleanType(), required=False, default=False)
# attribute name does not need to match the property key
ELASTICSEARCH_HOST = ConfigProperty(property_key='ES_HOST', property_type=StringType())
ELASTICSEARCH_SHARDS = ConfigProperty(property_key='ES_SHARDS', property_type=IntType(), default=5)
# properties can be excluded from varz
AWS_SECRET_KEY = ConfigProperty(property_type=StringType(), exclude_from_varz=True)
ConfigProperty
''''''''''''''''''
property_key is the key used to pull a property value from a loader.
If unspecified, defaults to the name of the attribute on the Config
class (e.g. 'DEBUG' and 'AWS_SECRET_KEY' in the example above).
To skip validation, leave property_type as the default (None).
To define new property type classes, subclass
schematic.types.BaseType (see .property_types.DictType as an
example).
When required is True, the value cannot be None (i.e. the value
must exist in one of the loaders or a default value should be
specified.) However, if a default is specified (and not None),
the requirement is always satisfied -- required is effectively
False.
exclude_from_varz indicates whether the property should be left out
of BaseConfig's varz dict (defaults to False).
Load and Validate Settings
Initialize the custom configuration class with one or more loaders. Each
loader specifies a source for loading configuration settings.
.. code:: python
config_dict = {
'DEBUG': 1,
'ES_HOST': 'http://localhost:9200',
'ES_SHARDS': '1',
'AWS_SECRET_KEY': 'whatever'
}
loaders = [DictLoader(config_dict)]
app_config = AppConfig(loaders)
The config can be initialized with multiple loaders. Each
``ConfigProperty``'s value is assigned from the first loader that
contains the ``ConfigProperty.property_key``. For example, with
``ES_SHARDS = '5'`` in the environment:
.. code:: python
loaders = [EnvLoader(), DictLoader(config_dict)]
app_config2 = AppConfig(loaders)
app_config2.ELASTICSEARCH_SHARDS # 5 (from env)
app_config2.ELASTICSEARCH_HOST # u'http://localhost:9200' (from dict)
When the configuration class is initialized, the sequence for loading a
value is as follows, for each ``ConfigProperty``:
- If ``property_key`` is not defined, use the name of the attribute on
the class (e.g. ``DEBUG`` in ``AppConfig``).
- Check whether the ``property_key`` exists in each of the loaders.
Iterate through the loaders in the order provided, stopping at the
first loader where the key exists.
- If the key exists in one of the loaders:
- Pull the value of the ``property_key`` from that loader.
- If a ``property_type`` class is defined for the ConfigProperty,
use ``to_native()`` to convert the loaded value to the appropriate
type and ``validate()`` to check any other assumptions (e.g. max
string length, connectivity to a network type, etc). An exception
may be raised at this stage, if the value cannot be converted or
validated.
- If no ``property_type`` was defined, use the value as is.
- If the key does not exist in any of the loaders:
- If a default value was specified, use it.
- If no default value was specified AND the property is required,
raise an exception.
- Otherwise, the value of the property is set to ``None``
| Default loaders are available in ``loaders.DEFAULT_LOADERS``. The
default is to prefer the ``EnvLoader``, which reads in environment
variables. If ``$SETTINGS_PATH`` is defined in the env, a second
loader is
| added to pull in settings from a json file at the specified path.
Property Types
~~~~~~~~~~~~~~
Use ``schematics.types`` classes to convert and validate values at load
time.
A custom property type can be created by extending
``schematics.type.BaseType``. Implement ``.to_native()`` to convert a
value type (returning the converted value or raising an exception for
incompatible types). Define one or more methods with names that start
with ``validate_`` (e.g. ``.validate_length()``) to add validation
steps. Validation methods should raise
``schematics.exceptions.ValidationError`` for failed checks.
Access Property Values
~~~~~~~~~~~~~~~~~~~~~~
.. code:: python
>>> app_config.DEBUG
True
>>> app_config.ELASTICSEARCH_HOST
u'http://localhost:9200'
>>> app_config.ELASTICSEARCH_SHARDS
1
| Note that when the class is initialized, attributes that are
``ConfigProperty`` instances are set to
| the loaded values:
.. code:: python
>>> type(AppConfig.ELASTICSEARCH_SHARDS)
grift.config.ConfigProperty
>>> type(app_config.ELASTICSEARCH_SHARDS)
int
Get public configuration settings
The varz property of BaseConfig classes is a dict with the
values for each ConfigProperty attribute. Any ConfigProperty can
be excluded from varz by specifying exclude_from_varz=True.
::
>>> app_config.varz
{
'DEBUG': True,
'ELASTICSEARCH_HOST': 'http://localhost:9200',
'ES_SHARDS': 1
}
All ConfigProperty values can be accessed in a dict, using
.as_dict():
::
>>> app_config.as_dict()
{'AWS_SECRET_KEY': 'whatever'
'DEBUG': 1,
'ES_HOST': 'http://localhost:9200',
'ES_SHARDS': '1'}
Maximizing Startup Guarantees
You may want to set up your config class to maximize startup guarantees
of having the right configuration set. There are a few property types
that attempt to make a basic connection with whatever network resouce is
specified. The supported protocols are http, postgres, redis, amqp, and
etcd. By default, the validator will back off 5 times before giving up,
but that can be overridden with the 'max\_tries' kwarg.
For example:
.. code:: python
class AppConfig(BaseConfig):
DATABASE_URL = ConfigProperty(property_type=PostgresType(), default='postgres://...')
REDIS_URL = ConfigProperty(property_type=RedisType(max_tries=1))
SHARED_CONFIG = ConfigProperty(property_type=StringType(), default='A')
class DeploymentConfig(AppConfig):
DATABASE_URL = ConfigProperty(property_type=PostgresType())
ConfigCls = AppConfig if deploy.env not in [STAGE, PROD] else DeployedConfig
config = ConfigCls(loaders)
An important distinction in this example is that the config schema
changes based on the deploy env. For the staging and production
environments, ``DeploymentConfig`` will fail to initialize if
``DATABASE_URL`` isn't set.
License
=======
Licensed under the Apache 2.0 License. Unless required by applicable law
or agreed to in writing, software distributed under the License is
distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
Copyright 2017 Kensho Technologies, LLC.
.. |Build Status| image:: https://travis-ci.org/kensho-technologies/grift.svg?branch=master
:target: https://travis-ci.org/kensho-technologies/grift
.. |Coverage Status| image:: https://coveralls.io/repos/github/kensho-technologies/grift/badge.svg?branch=master
:target: https://coveralls.io/github/kensho-technologies/grift?branch=master
.. |Pyup Updates| image:: https://pyup.io/repos/github/kensho-technologies/grift/shield.svg
:target: https://pyup.io/repos/github/kensho-technologies/grift/
:alt: Updates
.. |Py3 Ready| image:: https://pyup.io/repos/github/kensho-technologies/grift/python-3-shield.svg
:target: https://pyup.io/repos/github/kensho-technologies/grift/
:alt: Python 3