An elegant application configurator for the more civilized age
Project description
envenom
Introduction
envenom
is an elegant application configurator for the more civilized age.
envenom
is written with simplicity and type safety in mind. It allows
you to express your application configuration declaratively in a dataclass-like
format while providing your application with type information about each entry,
its nullability and default values.
envenom
is designed for modern usecases, allowing for pulling configuration from
environment variables or files for more sophisticated deployments on platforms
like Kubernetes.
How it works
An envenom
config class looks like a regular Python dataclass - because it is one!
The config
decorator really just creates a new dataclass by converting the config
fields into their dataclass
equivalents providing the relevant default_factory
.
This also means it's 100% compatible with dataclasses - you can use the config class as part of a regular dataclass, or a regular dataclass as part of the config class!
envenom
will automatically build the environment variable names for you, trying to
populate the dataclass fields from the environment, optionally running a parser so that
the field is automatically converted to a desired type (works out of the box with all
simple types like Enum
and UUID
or with any custom class that can be instantiated
from a single string). If using a static type checker the type deduction system will
correctly identify mistakes if you declare fields, parsers or default values with
mismatched types.
Because the environment's case-sensitivity is... questionable, to put it lightly,
all interaction is case-insensitive - environment variable names are in full
uppercase, and since _
is a common separator within environment variable names
it uses __
to separate namespaces instead. This means
envenom
also offers reading variable contents from file by specifying an environment
variable <name>__FILE
which contains the name of a file with the respective secret.
This aims to facilitate a common deploy pattern where secrets are mounted as files
(especially with Kubernetes and containers everywhere).
Usage
Quickstart guide
Install envenom
with python -m pip install envenom
.
from functools import cached_property
from envenom import config, optional, required, subconfig, with_default
from envenom.parsers import as_boolean
@config(namespace=("myapp", "postgres"))
class DatabaseCfg:
host: str = required()
port: int = with_default(int, default=5432)
database: str = required()
username: str | None = optional()
password: str | None = optional()
connection_timeout: int | None = optional(int)
sslmode_require: bool = with_default(as_boolean, default=False)
@cached_property
def connection_string(self) -> str:
auth = f"{self.username}:{self.password}" if self.password else self.username
query: dict[str, str] = {}
if self.connection_timeout:
query["timeout"] = str(self.connection_timeout)
if self.sslmode_require:
query["sslmode"] = "require"
query_string = "&".join({f"{key}={value}" for key, value in query.items()})
return (
f"postgresql+psycopg://{auth}@{self.host}:{self.port}"
f"/{self.database}?{query_string}"
)
@config(namespace="myapp")
class AppCfg:
secret_key: str = required()
database: DatabaseCfg = subconfig(DatabaseCfg)
if __name__ == "__main__":
cfg = AppCfg()
print(f"myapp/secret_key: {repr(cfg.secret_key)} {type(cfg.secret_key)}")
print(f"myapp/db/host: {repr(cfg.database.host)} {type(cfg.database.host)}")
print(f"myapp/db/port: {repr(cfg.database.port)} {type(cfg.database.port)}")
print(f"myapp/db/database: {repr(cfg.database.database)} {type(cfg.database.database)}")
print(f"myapp/db/username: {repr(cfg.database.username)} {type(cfg.database.username)}")
print(f"myapp/db/password: {repr(cfg.database.password)} {type(cfg.database.password)}")
print(f"myapp/db/connection_timeout: {repr(cfg.database.connection_timeout)} {type(cfg.database.connection_timeout)}")
print(f"myapp/db/sslmode_require: {repr(cfg.database.sslmode_require)} {type(cfg.database.sslmode_require)}")
print(f"myapp/db/connection_string: {repr(cfg.database.connection_string)} {type(cfg.database.connection_string)}")
Run the example with python -m envenom.examples.quickstart
:
Traceback (most recent call last):
...
raise MissingConfiguration(self.env_name)
envenom.errors.MissingConfiguration: 'MYAPP__SECRET_KEY'
Run the example again with environment set:
MYAPP__SECRET_KEY='}uZ?uvJdKDM+$2[$dR)).n4q1SX!A$0u{(+D$PVB' \
MYAPP__POSTGRES__HOST='postgres' \
MYAPP__POSTGRES__DATABASE='database-name' \
MYAPP__POSTGRES__USERNAME='user' \
MYAPP__POSTGRES__SSLMODE_REQUIRE='t' \
MYAPP__POSTGRES__CONNECTION_TIMEOUT='15' \
python -m envenom.examples.quickstart
myapp/secret_key: '}uZ?uvJdKDM+$2[$dR)).n4q1SX!A$0u{(+D$PVB' <class 'str'>
myapp/db/host: 'postgres' <class 'str'>
myapp/db/port: 5432 <class 'int'>
myapp/db/database: 'database-name' <class 'str'>
myapp/db/username: 'user' <class 'str'>
myapp/db/password: None <class 'NoneType'>
myapp/db/connection_timeout: 15 <class 'int'>
myapp/db/sslmode_require: True <class 'bool'>
myapp/db/connection_string: 'postgresql+psycopg://user@postgres:5432/database-name?sslmode=require&timeout=15' <class 'str'>
Next steps
See the wiki for more info and examples of advanced usage.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.