Configuration sources

At the core of your config files, there are Sources. A Source is a single source of configuration - it could be an environment variable, or a particular file, or a directory full of these files.

class satella.configuration.sources.StaticSource(config: dict)

A static piece of configuration. Returns exactly what is passed

provide() dict

Return your configuration, as a dict

Raises:

ConfigurationError – on invalid configuration

class satella.configuration.sources.StaticSource(config: dict)

A static piece of configuration. Returns exactly what is passed

provide() dict

Return your configuration, as a dict

Raises:

ConfigurationError – on invalid configuration

class satella.configuration.sources.EnvironmentSource(env_name: str, config_name: str | None = None, cast_to=None)

This just returns a dictionary of { env_name => that env’s value }

Parameters:
  • env_name – name of the environment variable to check for

  • config_name – name of the env_name in the dictionary to return

  • cast_to – callable that converts a string to whatever form is desired. Note that this is deprecated. Use schema instead to cast to desired typing.

provide() dict

Return your configuration, as a dict

Raises:

ConfigurationError – on invalid configuration

class satella.configuration.sources.EnvVarsSource(env_name: str)

Return a dictionary that is the JSON encoded within a particular environment variable

provide() dict

Return your configuration, as a dict

Raises:

ConfigurationError – on invalid configuration

class satella.configuration.sources.FileSource(path: str, encoding: str = 'utf-8', interpret_as: List[Type[FormatSource] | str] = ['JSONSource'])

Try to read a file and parse it with a known format.

provide() dict

Return your configuration, as a dict

Raises:

ConfigurationError – on invalid configuration

class satella.configuration.sources.DirectorySource(path, encoding: str = 'utf-8', interpret_as=['JSONSource'], fname_filter: ~typing.Callable[[str], bool] = <function DirectorySource.<lambda>>, scan_subdirectories: bool = True, on_fail: int = 0)

Load all files from given directory and merge them

Parameters:
  • filter – callable that tells whether to use this file (or subdirectory if scan_subdirectories is enabled)

  • on_fail – what to do in case a resource fails

provide() dict

Return your configuration, as a dict

Raises:

ConfigurationError – on invalid configuration

Then there are abstract sources of configuration.

class satella.configuration.sources.AlternativeSource(*sources: BaseSource)

If first source of configuration fails with ConfigurationError, use the next one instead, ad nauseam.

Ivar:

sources (list[BaseSource]) sources to examine left to right

provide() dict
Raises:

ConfigurationError – when backup fails too

class satella.configuration.sources.OptionalSource(source: BaseSource)

This will substitute for empty dict if underlying config would fail.

Apply this to your sources if you expect that they will fail.

Use as

>>> OptionalSource(SomeOtherSource1)
class satella.configuration.sources.MergingSource(*sources: BaseSource, on_fail: int = 0, fail_if_no_sources_are_correct: bool = True)

Source that merges configuration from a bunch of sources. The configuration has to be a dictionary!!

Parameters:
  • sources – Sources to examine. Source later in queue will override earlier’s entries, so take care.

  • on_fail – how to behave when a source fails

  • fail_if_no_sources_are_correct – even if on_fail == MergingSource.SILENT, if all sources fail, this will fail as well. Of course this makes sense only if on_fail == MergingSource.SILENT

provide() dict

Return your configuration, as a dict

Raises:

ConfigurationError – on invalid configuration

class satella.configuration.sources.BuildObjectFrom(key: str, child: BaseSource)

A source that outputs a single key with given name, and as it’s contents the contents of it’s child.

provide() dict

Return your configuration, as a dict

Raises:

ConfigurationError – on invalid configuration

In order to actually load the configuration, use the method provide().

Note that FileSource will try parsing the file with any modules, available, so if you want parsing for yaml and toml, you better install pyyaml and toml respectively.

Note that JSON will be parsed using ujson if the module is available.

JSON schema

The JSON schema consists of defining particular sources, embedded in one another.

{
    "type": "ClassNameOfTheSource",
    "args": [
    ],
    "kwarg_1": ...,
    "kwarg_2": ...,
}

If an argument consists of a dict with type key, it will be also loaded and passed internally as a source. One of three reserved types is lambda, which expects to have a key of operation. This will be appended to lambda x: `` and ``eval()-uated.

Always you can provide a key called optional with a value of True, this will wrap given Source in OptionalSource.

The second reserved type if binary. This will encode the value key with encoding encoding (default is ascii).

The third reserved type is import. It imports an expression and calls it with discovered value, returning the output.

It accepts the following variables:

  • module - module to import the expression from

  • attribute - name of the attribute inside the module

  • cast_before - a type to convert the value to before applying it to.

Eg:

class MyEnum(enum.IntEnum):
    A = 0

os.environ['TEST_ENV'] = '2'

dct = {
    "type": "EnvironmentSource",
    "args": ["TEST_ENV", "TEST_ENV"],
    "cast_to": {
        "module": "my_module",
        "attribute": "MyEnum",
        "cast_before": {
            "type": "lambda",
            "operation": "int"
        }
    }
}

config = load_source_from_dict(dct)
assert config.provide()['TEST_ENV'] == MyEnum(0)

To instantiate the schema, use the following functions:

satella.configuration.sources.load_source_from_dict(dct: dict) BaseSource

obj has a form of

{

“type”: “BaseSource”, “args”: [] # optional … kwargs

}

Raises:

ConfigurationError – upon failure to instantiate

satella.configuration.sources.load_source_from_list(obj: list) MergingSource

Builds a MergingSource from dict-ed objects

Please note that if your attacker has control over these files, he might provoke the application into executing arbitrary Python, so remember to sanitize your inputs!

You use BuildFromObject in such a way:

{
    "type": "BuildObjectFrom",
    "key": "child",
    "child": {
        "type": "StaticSource",
        "args": [
            {"a": 5}
        ]
    }
}

The result of this execution will be a dictionary:

{
    "test": {
        "a": 5
    }
}

If you have only a single argument, you can also do:

{
    "type": "DirectorySource",
    "arg": "/app/config"
}

You can put any objects you like as the arguments, note however that if you pass a dictionary, that has a key of “type” and it’s value is one of recognized sources, an attempt will be made to parse it as a child.

Note that in case you pass a dict with a type that is not recognized, a warning will be emitted.