Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

envenom

Package Overview
Dependencies
Maintainers
1
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

envenom

An elegant application configurator for the more civilized age

  • 2.0.4.post1
  • PyPI
  • Socket score

Maintainers
1

envenom

pipeline status coverage report latest release

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 - all in the spirit of 12factor.

How it works

An envenom config class looks like a regular Python dataclass - because it is one.

The @envenom.config decorator creates a new dataclass by converting the config fields into their dataclass equivalents providing the relevant default field parameters. All config classes created that way are marked as frozen=True - because the config should not change mid-flight - and eq=True.

This also means it's 100% compatible with dataclasses. You can:

  • use a config class as a property of a regular dataclass
  • use a regular dataclass as a property of a config class
  • declare static or dynamic fields using standard dataclass syntax
  • use the InitVar/__post_init__ method for delayed initialization of fields
  • use methods, classmethods, staticmethods, and properties

envenom will automatically fetch the environment variable values to populate dataclass fields (optionally running parsers so that fields are automatically converted to desired types). This works out of the box with all types trivially convertible from str, like Enum and UUID, and with any object type that can be instantiated easily from a single string (any function (str,) -> T will work as a parser).

If using a static type checker the type deduction system will correctly identify most mistakes if you declare fields, parsers or default values with mismatched types. There are certain exceptions, for example T will always satisfy type bounds T | None.

envenom also offers reading variable contents from file by specifying an environment variable with the suffix __FILE which contains the path to a file with the respective secret. This aims to facilitate a common deploy pattern where secrets are mounted as files (especially prevalent with Kubernetes).

All interaction with the environment is case-sensitive - we'll convert everything to uppercase, and since _ is a common separator within environment variable names we use _ to replace any and all nonsensical characters, then use __ to separate namespaces. Therefore a field "var" in namespaces ("ns-1", "ns2") will be mapped to NS_1__NS2__VAR.

What envenom isn't

envenom has a clearly defined scope limited to configuration management from the application's point of view.

This means envenom is only interested in converting the environment into application configuration and does not care about how the environment gets populated in the first place.

Things that are out of scope for envenom include, but are not limited to:

  • injecting the environment into the runtime or orchestrator
  • retrieving configuration or secrets from the cloud or another storage (AWS Parameter/Secret Store, Azure Key Vault, HashiCorp Vault, etc.)
  • retrieving and parsing configuration from structured config files (YAML/JSON/INI etc.)

Using envenom

Installing envenom

python -m pip install envenom

Creating a config class

Config classes are created with the envenom.config class decorator. It behaves exactly like dataclasses.dataclass but allows to replace standard dataclasses.field definitions with one of envenom-specific configuration field types.

from envenom import config


@config()
class MainCfg:
    ...

envenom field types

envenom offers four supported field types:

  • required for configuration variables that have to be provided. If the value cannot be found, envenom.errors.MissingConfiguration will be raised.
  • optional for configuration variables that don't have to be provided. If the value cannot be found, it will be set to None.
  • with_default for configuration variables where a default value can be provided. If the value cannot be found, it will be set to the default.
  • with_default_factory for configuration variables where a default value can be provided. If the value cannot be found, it will call the default factory and set the value to the result.

Basic usage example

This example shows how to build a basic config structure using a database config as an example. It is available in the envenom.examples.quickstart runnable module.

from functools import cached_property
from uuid import UUID, uuid4

from envenom import (
    config,
    optional,
    required,
    subconfig,
    with_default,
    with_default_factory,
)
from envenom.parsers import bool_parser


@config(namespace=("myapp", "db"))
class DbCfg:
    scheme: str = with_default(default="postgresql+psycopg://")
    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(bool_parser(), default=False)

    @cached_property
    def auth(self) -> str:
        if not self.username and not self.password:
            return ""

        auth = ""
        if self.username:
            auth += self.username
        if self.password:
            auth += f":{self.password}"
        if auth:
            auth += "@"

        return auth

    @cached_property
    def query_string(self) -> str:
        query: dict[str, str] = {}
        if self.connection_timeout:
            query["timeout"] = str(self.connection_timeout)
        if self.sslmode_require:
            query["sslmode"] = "require"

        if not query:
            return ""

        query_string = "&".join((f"{key}={value}" for key, value in query.items()))
        return f"?{query_string}"

    @cached_property
    def connection_string(self) -> str:
        return (
            f"{self.scheme}{self.auth}{self.host}:{self.port}"
            f"/{self.database}{self.query_string}"
        )


@config(namespace="myapp")
class AppCfg:
    worker_id: UUID = with_default_factory(UUID, default_factory=uuid4)
    secret_key: str = required()
    db: DbCfg = subconfig(DbCfg)


if __name__ == "__main__":
    cfg = AppCfg()

    print(f"cfg.worker_id ({type(cfg.worker_id)}): {repr(cfg.worker_id)}")
    print(f"cfg.secret_key ({type(cfg.secret_key)}): {repr(cfg.secret_key)}")
    print(f"cfg.db.host ({type(cfg.db.host)}): {repr(cfg.db.host)}")
    print(f"cfg.db.port ({type(cfg.db.port)}): {repr(cfg.db.port)}")
    print(f"cfg.db.database ({type(cfg.db.database)}): {repr(cfg.db.database)}")
    print(f"cfg.db.username ({type(cfg.db.username)}): {repr(cfg.db.username)}")
    print(f"cfg.db.password ({type(cfg.db.password)}): {repr(cfg.db.password)}")
    print(f"cfg.db.connection_timeout ({type(cfg.db.connection_timeout)}): {repr(cfg.db.connection_timeout)}")
    print(f"cfg.db.sslmode_require ({type(cfg.db.sslmode_require)}): {repr(cfg.db.sslmode_require)}")
    print(f"cfg.db.connection_string ({type(cfg.db.connection_string)}): {repr(cfg.db.connection_string)}")

Run the example:

python -m envenom.examples.quickstart
Traceback (most recent call last):
    ...
    raise MissingConfiguration(self.env_name)
envenom.errors.MissingConfiguration: 'MYAPP__SECRET_KEY'

Immediately throws an error, as soon as it encounters a required field.

Run the example again with the environment set:

MYAPP__SECRET_KEY='}uZ?uvJdKDM+$2[$dR)).n4q1SX!A$0u{(+D$PVB' \
MYAPP__DB__HOST='postgres' \
MYAPP__DB__DATABASE='database-name' \
MYAPP__DB__USERNAME='user' \
MYAPP__DB__SSLMODE_REQUIRE='t' \
MYAPP__DB__CONNECTION_TIMEOUT='15' \
python -m envenom.examples.quickstart
cfg.worker_id (<class 'uuid.UUID'>): UUID('edf6c50a-37a4-42d4-a2d4-c1ee1f3975bc')
cfg.secret_key (<class 'str'>): '}uZ?uvJdKDM+$2[$dR)).n4q1SX!A$0u{(+D$PVB'
cfg.db.host (<class 'str'>): 'postgres'
cfg.db.port (<class 'int'>): 5432
cfg.db.database (<class 'str'>): 'database-name'
cfg.db.username (<class 'str'>): 'user'
cfg.db.password (<class 'NoneType'>): None
cfg.db.connection_timeout (<class 'int'>): 15
cfg.db.sslmode_require (<class 'bool'>): True
cfg.db.connection_string (<class 'str'>): 'postgresql+psycopg://user@postgres:5432/database-name?sslmode=require&timeout=15'

Next steps

See the documentation for more examples of advanced usage and instructions for setting up a development environment.

Keywords

FAQs


Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc