Metadata-Version: 2.1
Name: envenom
Version: 1.0.5
Summary: An elegant application configurator for the more civilized age
Home-page: https://gitlab.com/python-arcana/envenom
License: GPL-3.0-or-later
Keywords: env,environment,configuration
Author: Artur Ciesielski
Author-email: artur.ciesielski@gmail.com
Requires-Python: >=3.10,<4.0
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Project-URL: Documentation, https://gitlab.com/python-arcana/envenom/-/wikis/Home
Project-URL: Repository, https://gitlab.com/python-arcana/envenom
Description-Content-Type: text/markdown

<!-- `envenom` - an elegant application configurator for the more civilized age
Copyright (C) 2024-  Artur Ciesielski <artur.ciesielski@gmail.com>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>. -->

# envenom

[![pipeline status](https://gitlab.com/python-arcana/envenom/badges/main/pipeline.svg)](https://gitlab.com/python-arcana/envenom/-/commits/main)
[![coverage report](https://gitlab.com/python-arcana/envenom/badges/main/coverage.svg)](https://gitlab.com/python-arcana/envenom/-/commits/main)
[![latest release](https://gitlab.com/python-arcana/envenom/-/badges/release.svg)](https://gitlab.com/python-arcana/envenom/-/releases)

## 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`.

```python
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:

```bash
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
```

```bash
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](https://gitlab.com/python-arcana/envenom/-/wikis/Home) for more info
and examples of advanced usage.

