Skip to main content

Configure your program via environment variables, validated by pydantic.

Project description

Umwelt

Describe a configuration schema with dataclasses or pydantic and load values from the environment, in a static-typing-friendly way.

Examples

Flat

>>> os.environ["APP_HOSTS"] = '["b.org","sky.net"]'
>>> os.environ["APP_TOKEN"] = "very secret"
from typing import Sequence
from pydantic import SecretStr
import umwelt

class MyConfig:
    hosts: Sequence[str]
    token: SecretStr
    replicas: int = 2

config = umwelt.new(MyConfig, prefix="app")
>>> dataclasses.is_dataclass(config)
True
>>> config.hosts
["b.org", "sky.net"]
>>> config.token
SecretStr('**********')
>>> config.replicas
2

Nested

>>> os.environ["APP_DB_PORT"] = "32"
from __future__ import annotations  # for forward-references
from pydantic import UrlStr
import umwelt

class MyConfig:
    db: DbConfig
    host: UrlStr = "http://b.org"

@umwelt.subconfig
class DbConfig:
    port: int
    debug: bool = False

config = umwelt.new(MyConfig, prefix="app")
>>> config.host
"http://b.org"
>>> config.db.port
32

Install

$ pip install umwelt

Features

umwelt.new

umwelt.new expects one positional argument: the config class to fill. Umwelt will convert it into a dataclass if it's not one already.

umwelt.new also accepts named arguments:

  • source (by default os.environ) is a Mapping[str, str] from which values are extracted.
  • prefix can be a string or a callable. As a string, it is prepended to the config field's name. As a callable, it receives the config field's name and its result is the source key name.
  • decoder is a callable expecting a type and a string, and returns a conversion of that string in that type, or in a type that pydantic can convert in that type. For example, when umwelt's default decoder is called with (List[Set[int]], "[[1]]"), it simply decodes the string from JSON and hence returns a list of lists, which pydantic properly converts into a list of sets.

@umwelt.subconfig

@umwelt.subconfig tags classes so that, when they appear as field annotations in another config class, umwelt.new doesn't instantiate them from a single source value, but rather from one source value per class field.

Example:

class Point:                              # no @subconfig
    def __init__(self, s: str):           # string input
        self.x, self.y = s.split(",", 1)  # arbitrary implementation

class MyConf:
    point: Point

conf = umwelt.new(MyConf, source={"POINT": "1,2"})  # one source entry
conf.point  # <Point at 0x7f07b1d04750>

conf.point is an instance of Point, built by passing the input value "1,2" directly to Point.__new__. There is only one source key: POINT.

Now compare with @umwelt.subconfig:

@umwelt.subconfig
class Point:
    x: int
    y: int

class MyConf:
    point: Point

conf = umwelt.new(MyConf, source={"POINT_X": "1", "POINT_Y": "2"})
conf.point  # Point(x=1, y=2)

conf.point is still an instance of Point (Point has been made a dataclass by Umwelt, hence the automatic __str__ implementation). There are two source keys: POINT_X and POINT_Y, each corresponding to a field of the Point class.

Comparison with Ecological

I've used Ecological for a long time. Today, a large part of Ecological's codebase implements features already found in dataclasses and pydantic, which are more mature. I believe Ecological's design can be dramatically simplified and improved by enforcing a strict separation of concerns:

  • class scaffolding is the responsibility of dataclasses (which, compared to metaclasses, is simpler, more introspectable, and comes with helpers like asdict);
  • type coercion and validation is the responsibility of pydantic (which has more features, e.g. nested data types, JSON Schema, serialization, etc.);
  • mapping a pydantic schema (the configuration class) to a string-to-string dict (like os.environ) is the responsibility of Umwelt.

Some compatibility-breaking decisions prevent from doing this in Ecological:

  • Don't autoload configuration values, especially not at class definition time. Instead, offer just one function (umwelt.new) that loads the configuration when it is called.
  • Don't tie variable prefixes to configuration classes, as that doesn't play well with nested configurations.

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

umwelt-2019.8.2.tar.gz (5.9 kB view details)

Uploaded Source

Built Distribution

umwelt-2019.8.2-py3-none-any.whl (6.2 kB view details)

Uploaded Python 3

File details

Details for the file umwelt-2019.8.2.tar.gz.

File metadata

  • Download URL: umwelt-2019.8.2.tar.gz
  • Upload date:
  • Size: 5.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/0.12.16 CPython/3.7.4 Linux/5.2.7-100.fc29.x86_64

File hashes

Hashes for umwelt-2019.8.2.tar.gz
Algorithm Hash digest
SHA256 72c2c095e3b4aa059fb0082d0e70f117641782b4a35459f6a6fdeef85c873dbf
MD5 eae743b4d26b12b2447f38ca92d9c548
BLAKE2b-256 e71ae85037f106b8f6035262b8bd50046c6a0a0a5e56dd6e23b6c32bb378578a

See more details on using hashes here.

File details

Details for the file umwelt-2019.8.2-py3-none-any.whl.

File metadata

  • Download URL: umwelt-2019.8.2-py3-none-any.whl
  • Upload date:
  • Size: 6.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/0.12.16 CPython/3.7.4 Linux/5.2.7-100.fc29.x86_64

File hashes

Hashes for umwelt-2019.8.2-py3-none-any.whl
Algorithm Hash digest
SHA256 4e6b8c9aa33324fab9807975051548953f7cc524af5301059028e2f1fc91cf0e
MD5 92dc3d5760a614b5fcb46ab7fb75b32a
BLAKE2b-256 d3dc2fcaa9d72457c2bbcbe0aa6897d3443848044b7d7bcc0ef089b328ed30b2

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