Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/thilp/umwelt
Configure your program via environment variables, validated by pydantic.
https://github.com/thilp/umwelt
Last synced: 4 days ago
JSON representation
Configure your program via environment variables, validated by pydantic.
- Host: GitHub
- URL: https://github.com/thilp/umwelt
- Owner: thilp
- Created: 2019-08-18T14:35:21.000Z (about 5 years ago)
- Default Branch: master
- Last Pushed: 2019-08-23T21:15:53.000Z (about 5 years ago)
- Last Synced: 2024-10-30T21:37:50.167Z (12 days ago)
- Language: Python
- Homepage: https://pypi.org/project/umwelt/
- Size: 27.3 KB
- Stars: 3
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
Awesome Lists containing this project
README
# Umwelt
[dataclasses]: https://docs.python.org/3/library/dataclasses.html
[pydantic]: https://pydantic-docs.helpmanual.io/Describe a configuration schema with [dataclasses][] or [pydantic][] and
load values from the environment, in a static-typing-friendly way.## Examples
### Flat
```python
>>> os.environ["APP_HOSTS"] = '["b.org","sky.net"]'
>>> os.environ["APP_TOKEN"] = "very secret"
``````python
from typing import Sequence
from pydantic import SecretStr
import umweltclass MyConfig:
hosts: Sequence[str]
token: SecretStr
replicas: int = 2config = umwelt.new(MyConfig, prefix="app")
``````python
>>> dataclasses.is_dataclass(config)
True
>>> config.hosts
["b.org", "sky.net"]
>>> config.token
SecretStr('**********')
>>> config.replicas
2
```### Nested
```python
>>> os.environ["APP_DB_PORT"] = "32"
``````python
from __future__ import annotations # for forward-references
from pydantic import UrlStr
import umweltclass MyConfig:
db: DbConfig
host: UrlStr = "http://b.org"@umwelt.subconfig
class DbConfig:
port: int
debug: bool = Falseconfig = umwelt.new(MyConfig, prefix="app")
``````python
>>> config.host
"http://b.org"
>>> config.db.port
32
```## Install
```shell script
$ pip install umwelt
```## Features
### umwelt.new
`umwelt.new` expects one positional argument: the config class to fill.
Umwelt will convert it into a [dataclass][dataclasses] 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:
```python
class Point: # no @subconfig
def __init__(self, s: str): # string input
self.x, self.y = s.split(",", 1) # arbitrary implementationclass MyConf:
point: Pointconf = umwelt.new(MyConf, source={"POINT": "1,2"}) # one source entry
conf.point #
````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`:
```python
@umwelt.subconfig
class Point:
x: int
y: intclass MyConf:
point: Pointconf = 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.[ecological]: https://github.com/jmcs/ecological
[autoloading]: https://github.com/jmcs/ecological/issues/20