Skip to main content

Lightweight layered configuration library.

Project description

llconfig

Lightweight layered configuration library.

All you need is Python >= 3.5 (and a keyboard).


Basic concept

There is a Config object holding several layers of configuration keys and their values (= configuration directives). From top to bottom:

  1. Override layer = holds runtime directive overrides, if any.
  2. Env layer = directives loaded from environment variables are kept in this layer, if any.
  3. File layer = directives loaded from configuration file(s) are kept in this layer, if any.
  4. Default layer = holds a default value for every initialized directive.

Learn by example

The behavior is best shown on following example:

from llconfig import Config

c = Config('local/override.cnf.py', '/etc/my_app/conf.d', env_prefix='MY_', config_files_env_var='CONFIG')
c.init('PORT', int, 80)
c.load()  # recommended, but not required (see docstrings)
c['PORT']

The returned value is 80, given that there is no MY_PORT env variable, no MY_CONFIG env variable and no PORT = 1234 line in any of local/override.cnf.py or /etc/my_app/conf.d/*.cnf.py configuration files.

Search process

First, the override layer is searched, but there is no runtime override (no c['PORT'] = 1234) in this example.

The environment layer is searched next. If there is an env variable called MY_PORT, its value is taken and converted using int function (this is necessary otherwise it wouldn't be possible to load anything else than str from env variables).

Then, if the env variable is not present, the file layer is searched. There can be multiple files in this layer (forming sub-layers) and all of them must be Python-executable. File sub-layers are processed in following order:

  1. Files loaded from MY_CONFIG env variable. Its value is splitted using : (colon) as a delimiter and each part is handled the same way as if it would be passed to constructor (see bellow). The handling preserves order (so the leftmost part is always handled first).
  2. Files passed to constructor (local/override.cnf.py and /etc/my_app/conf.d in this example). If there is a path pointing to directory instead of simple file, the directory is expanded (non-recursively). The expansion lists all files in given directory using expansion_glob_pattern attribute sorted by file name in reverse order (you can change this behavior by extending this class and overriding _expand_dir method). The expanded files are used as separate sub-layers in place of original directory.

When all of the file sub-layers are created, each configuration file is executed and each file's global namespace is searched for the PORT directive (still preserving the order). If found, the directive is returned as is (without conversion).

The default layer is searched as a last resort. As it contains values from directives' init, there is always a default value (unless a search for non-initialized directive is performed). The default value is returned as is.

Directive initialization

Directive is initialized using init method. It takes directive name, converter function (see bellow) and a default value (which is None by default). It is recommended to name directives using upper-case only. Any directive you want to use must be initialized, otherwise it is ignored (unknown env variables, unknown directives in configuration files, etc.).

This means that once you initialize a directive you can safely use it without KeyErrors or without calling c.get('PORT', 'default'). There will always be at least the default value.

Converters

Converters are arbitrary callables taking single str argument and returning anything. The converter is called only for conversion from env variable. There are some predefined converters available in llconfig.converters, but it is easy to create own. For example:

from llconfig import Config
from llconfig.converters import json, bool_like
from pathlib import Path

c = Config()
c.init('PORT', int)  # "443" => 443
c.init('HOSTNAME', str)  # "localhost" => "localhost"
c.init('DEBUG', bool_like)  # "off" => False
c.init('DEBUG_2', bool)  # BEWARE: "0" => True 
c.init('FLEXIBLE', json)  # '{"hello": 1}' => {"hello": 1}
c.init('PICTURES', lambda raw: [Path(p) for p in raw.split(':')])  # "a.jpg:b.jpg" => [Path("a.jpg"), Path("b.jpg")]

Any exception raised during conversion is re-raised as a ValueError.

Getting the values out

The Config object implements a mapping protocol, so you can use it as if it was a dict. In addition, there is a get_namespace method taken from Flask framework with exact same behavior (see their docs for more examples).

from llconfig import Config

c = Config()
c.init('DB_HOST', str, 'localhost')
c.init('DB_PORT', int, 3306)
c.init('DB_USER', str)

c['DB_HOST']  # => 'localhost'
c['DB_USER']  # => None

c.get_namespace('DB_')  # => {'host': 'localhost', 'port': 3306, 'user': None}
c['DB_':]  # syntactic sugar - does the same as `c.get_namespace('DB_')`

dict(c)  # => {'DB_HOST': 'localhost', 'DB_PORT': 3306, 'DB_USER': None}

Security

In short: do not use this library in untrusted environment, unless you completely understand how it works and what possible attack vectors are. The main concern is that each file forming the file layer is executed. There is also a possibility to load files using config_files_env_var environment variable (APP_CONFIG by default), unless disabled. On top of that, you can compromise your application using badly written converter.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

llconfig-2.0.1.tar.gz (10.1 kB view details)

Uploaded Source

Built Distribution

llconfig-2.0.1-py3-none-any.whl (11.6 kB view details)

Uploaded Python 3

File details

Details for the file llconfig-2.0.1.tar.gz.

File metadata

  • Download URL: llconfig-2.0.1.tar.gz
  • Upload date:
  • Size: 10.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.7.0 importlib_metadata/4.8.2 pkginfo/1.8.2 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.9.4

File hashes

Hashes for llconfig-2.0.1.tar.gz
Algorithm Hash digest
SHA256 64ab4b9eda60cf476dcb066d83bc4e32e18ef09e76ae583b221b731f2455621d
MD5 9d339940228f1ab4a2ffddf6bce57f6d
BLAKE2b-256 4d2401620f43c2a5966647ca1c20bb8a29cda7741d34d7090a5aee0ccc331db8

See more details on using hashes here.

File details

Details for the file llconfig-2.0.1-py3-none-any.whl.

File metadata

  • Download URL: llconfig-2.0.1-py3-none-any.whl
  • Upload date:
  • Size: 11.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.7.0 importlib_metadata/4.8.2 pkginfo/1.8.2 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.9.4

File hashes

Hashes for llconfig-2.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 a2f9894f5155023d014525f2758f19c611eb785078116c46caed13eae5b4a74c
MD5 d66b4330760372904fa603a08c1c3887
BLAKE2b-256 46011f30b6da751f5f695c4a6b32e0b8dbd9ec61e8d71c8e318f81bd34c9fa8a

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page