Ecological Tutorial

Getting Started

Before we start to set some environment variables, note than in a real application this would be set outside of your application.


In [1]:
import os
os.environ["INTEGER_LIST"] = "[1, 2, 3, 4, 5]"
os.environ["DICTIONARY"] = "{'key': 'value'}"
os.environ["INTEGER"] = "42"
os.environ["BOOLEAN"] = "False"
os.environ["OVERRIDE_DEFAULT"] = "This is NOT the default value"

Now let's create a configuration class:


In [2]:
import ecological

class Configuration(ecological.Config):
    integer_list: list
    integer: int
    dictionary: dict
    boolean: bool
    with_default: str = "This is the default value"
    override_default: str = "This is the default value"

Easy right? Now that we created the configuration class. Let's look at what's inside:


In [3]:
print(repr(Configuration.integer_list))
print(type(Configuration.integer_list))


[1, 2, 3, 4, 5]
<class 'list'>

In [4]:
print(repr(Configuration.integer))
print(type(Configuration.integer))


42
<class 'int'>

In [5]:
print(repr(Configuration.dictionary))
print(type(Configuration.dictionary))


{'key': 'value'}
<class 'dict'>

In [6]:
print(repr(Configuration.boolean))
print(type(Configuration.boolean))


False
<class 'bool'>

In [7]:
print(repr(Configuration.with_default))
print(type(Configuration.with_default))


'This is the default value'
<class 'str'>

In [8]:
print(repr(Configuration.override_default))
print(type(Configuration.override_default))


'This is NOT the default value'
<class 'str'>

As you can see all the values where cast from str to the expected types, and if a default value is set it will be used if the corresponding environment variable doesn't exist.

Typing Support

Ecological also supports some of the types defined in PEP 484, for example:


In [9]:
from typing import List, Dict

class ConfigurationTyping(ecological.Config):
    integer_list: List
    dictionary: Dict

As expected the variables were converted to the real types:


In [10]:
print(repr(ConfigurationTyping.integer_list))
print(type(ConfigurationTyping.integer_list))


[1, 2, 3, 4, 5]
<class 'list'>

In [11]:
print(repr(ConfigurationTyping.dictionary))
print(type(ConfigurationTyping.dictionary))


{'key': 'value'}
<class 'dict'>

Prefixed Configuration

You can also decide to prefix your application configuration, for example, to avoid collisions:


In [12]:
os.environ["HOME"] = "/home/myuser/"
os.environ["VALUE"] = "Not Prefixed"
os.environ["CONFIG_HOME"] = "/app/home"
os.environ["CONFIG_VALUE"] = "Prefixed"

class ConfigurationPrefix(ecological.Config, prefix="config"):
    home: str
    value: str

In this case the home and value properties will be fetched from the CONFIG_HOME and CONFIG_VALUE environment properties:


In [13]:
print(repr(ConfigurationPrefix.home))


'/app/home'

In [14]:
print(repr(ConfigurationPrefix.value))


'Prefixed'

Fine-grained control

You can control how the configuration properties are set by providing a ecological.Variable instance as the default value.

ecological.Variable receives the following parameters:

variable_name (optional) - exact name of the environment variable that will be used. default (optional) - default value for the property if it isn't set. transform (optional) - function that converts the string in the environment to the value and type you expect in your application. The default transform function will try to cast the string to the annotation type of the property.

Transformation function

The transformation function receive two parameters, a string representation with the raw value, and a wanted_type with the value of the annotation (usually, but not necessarily a type).


In [22]:
os.environ["Integer"] = "42"

def times_2(value, wanted_type):
    assert wanted_type is int
    return int(value) * 2

class ConfigurationVariable(ecological.Config, prefix="this_is_going_to_be_ignored"):
    integer = ecological.Variable("Integer", transform=lambda v, wt: int(v))
    integer_x2: int = ecological.Variable("Integer", transform=times_2)
    integer_as_str: str = ecological.Variable("Integer", transform=lambda v, wt: v)
    boolean: bool = ecological.Variable("404", default=False)

integer, integer_x2 and integer_as_str will use the same enviroment variable but return different values:


In [17]:
print(repr(ConfigurationVariable.integer))


42

In [20]:
print(repr(ConfigurationVariable.integer_x2))


84

In [23]:
print(repr(ConfigurationVariable.integer_as_str))


'42'

Because the environment variable 404 is not set, boolean will have the default value:


In [24]:
print(repr(ConfigurationVariable.boolean))


False

Nested Configuration

ecological.Config also supports nested configurations, for example:


In [26]:
os.environ["INTEGER"] = "42"
os.environ["NESTED_BOOLEAN"] = "True"

class ConfigurationNested(ecological.Config):
    integer: int

    class Nested(ecological.Config, prefix='nested'):
        boolean: bool

This way you can group related configuration properties hierarchically:


In [28]:
print(repr(ConfigurationNested.integer))


42

In [27]:
print(repr(ConfigurationNested.Nested.boolean))


True