<!-- `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 creates a new dataclass by converting the config fields into
their `dataclass` equivalents providing the relevant dataclass field parameters.
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, `classmethod`s, `staticmethod`s, and properties
`envenom` will automatically fetch the environment variable values to populate the
dataclass fields (optionally running a parser so that the field is automatically
converted to a desired type). This works out of the box with all types trivially
convertible from `str`, like `StrEnum` 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.
`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`.
## 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 DbCfg:
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 = ""
if self.username:
auth += self.username
if self.password:
auth += f":{self.password}"
if auth:
auth += "@"
query: dict[str, str] = {}
if self.connection_timeout:
query["timeout"] = str(self.connection_timeout)
if self.sslmode_require:
query["sslmode"] = "require"
if query_string := "&".join((f"{key}={value}" for key, value in query.items())):
query_string = f"?{query_string}"
return (
f"postgresql+psycopg://{auth}{self.host}:{self.port}"
f"/{self.database}{query_string}"
)
@config(namespace="myapp")
class AppCfg:
secret_key: str = required()
db: DbCfg = subconfig(DbCfg)
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.db.host)} {type(cfg.db.host)}")
print(f"myapp/db/port: {repr(cfg.db.port)} {type(cfg.db.port)}")
print(f"myapp/db/database: {repr(cfg.db.database)} {type(cfg.db.database)}")
print(f"myapp/db/username: {repr(cfg.db.username)} {type(cfg.db.username)}")
print(f"myapp/db/password: {repr(cfg.db.password)} {type(cfg.db.password)}")
print(f"myapp/db/connection_timeout: {repr(cfg.db.connection_timeout)} {type(cfg.db.connection_timeout)}")
print(f"myapp/db/sslmode_require: {repr(cfg.db.sslmode_require)} {type(cfg.db.sslmode_require)}")
print(f"myapp/db/connection_string: {repr(cfg.db.connection_string)} {type(cfg.db.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.
Raw data
{
"_id": null,
"home_page": "https://gitlab.com/python-arcana/envenom",
"name": "envenom",
"maintainer": null,
"docs_url": null,
"requires_python": "<4.0,>=3.10",
"maintainer_email": null,
"keywords": "env, environment, config, configuration",
"author": "Artur Ciesielski",
"author_email": "artur.ciesielski@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/1e/30/6c8c8c9184864b8bd586218b5077432e2eebadcaf33a2cab76c00c95b6ed/envenom-1.0.9.tar.gz",
"platform": null,
"description": "<!-- `envenom` - an elegant application configurator for the more civilized age\nCopyright (C) 2024- Artur Ciesielski <artur.ciesielski@gmail.com>\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program. If not, see <https://www.gnu.org/licenses/>. -->\n\n# envenom\n\n[![pipeline status](https://gitlab.com/python-arcana/envenom/badges/main/pipeline.svg)](https://gitlab.com/python-arcana/envenom/-/commits/main)\n[![coverage report](https://gitlab.com/python-arcana/envenom/badges/main/coverage.svg)](https://gitlab.com/python-arcana/envenom/-/commits/main)\n[![latest release](https://gitlab.com/python-arcana/envenom/-/badges/release.svg)](https://gitlab.com/python-arcana/envenom/-/releases)\n\n## Introduction\n\n`envenom` is an elegant application configurator for the more civilized age.\n\n`envenom` is written with simplicity and type safety in mind. It allows\nyou to express your application configuration declaratively in a dataclass-like\nformat while providing your application with type information about each entry,\nits nullability and default values.\n\n`envenom` is designed for modern usecases, allowing for pulling configuration from\nenvironment variables or files for more sophisticated deployments on platforms\nlike Kubernetes.\n\n## How it works\n\nAn `envenom` config class looks like a regular Python dataclass - because it is one.\n\nThe `config` decorator creates a new dataclass by converting the config fields into\ntheir `dataclass` equivalents providing the relevant dataclass field parameters.\n\nThis also means it's 100% compatible with dataclasses. You can:\n- use a config class as a property of a regular dataclass\n- use a regular dataclass as a property of a config class\n- declare static or dynamic fields using standard dataclass syntax\n- use the `InitVar`/`__post_init__` method for delayed initialization of fields\n- use methods, `classmethod`s, `staticmethod`s, and properties\n\n`envenom` will automatically fetch the environment variable values to populate the\ndataclass fields (optionally running a parser so that the field is automatically\nconverted to a desired type). This works out of the box with all types trivially\nconvertible from `str`, like `StrEnum` and `UUID`, and with any object type that can be\ninstantiated easily from a single string (any function `(str,) -> T` will work as a\nparser).\n\nIf using a static type checker the type deduction system will correctly identify most\nmistakes if you declare fields, parsers or default values with mismatched types.\n\n`envenom` also offers reading variable contents from file by specifying an environment\nvariable with the suffix `__FILE` which contains the path to a file with the respective\nsecret. This aims to facilitate a common deploy pattern where secrets are mounted as\nfiles (especially prevalent with Kubernetes).\n\nAll interaction with the environment is case-sensitive - we'll convert everything to\nuppercase, and since `_` is a common separator within environment variable names we use\n`_` to replace any and all nonsensical characters, then use `__` to separate namespaces.\nTherefore a field `\"var\"` in namespaces `(\"ns-1\", \"ns2\")` will be mapped to\n`NS_1__NS2__VAR`.\n\n## Usage\n\n### Quickstart guide\n\nInstall `envenom` with `python -m pip install envenom`.\n\n```python\nfrom functools import cached_property\n\nfrom envenom import config, optional, required, subconfig, with_default\nfrom envenom.parsers import as_boolean\n\n\n@config(namespace=(\"myapp\", \"postgres\"))\nclass DbCfg:\n host: str = required()\n port: int = with_default(int, default=5432)\n database: str = required()\n username: str | None = optional()\n password: str | None = optional()\n connection_timeout: int | None = optional(int)\n sslmode_require: bool = with_default(as_boolean, default=False)\n\n @cached_property\n def connection_string(self) -> str:\n auth = \"\"\n if self.username:\n auth += self.username\n if self.password:\n auth += f\":{self.password}\"\n if auth:\n auth += \"@\"\n\n query: dict[str, str] = {}\n if self.connection_timeout:\n query[\"timeout\"] = str(self.connection_timeout)\n if self.sslmode_require:\n query[\"sslmode\"] = \"require\"\n\n if query_string := \"&\".join((f\"{key}={value}\" for key, value in query.items())):\n query_string = f\"?{query_string}\"\n\n return (\n f\"postgresql+psycopg://{auth}{self.host}:{self.port}\"\n f\"/{self.database}{query_string}\"\n )\n\n\n@config(namespace=\"myapp\")\nclass AppCfg:\n secret_key: str = required()\n\n db: DbCfg = subconfig(DbCfg)\n\n\nif __name__ == \"__main__\":\n cfg = AppCfg()\n\n print(f\"myapp/secret_key: {repr(cfg.secret_key)} {type(cfg.secret_key)}\")\n print(f\"myapp/db/host: {repr(cfg.db.host)} {type(cfg.db.host)}\")\n print(f\"myapp/db/port: {repr(cfg.db.port)} {type(cfg.db.port)}\")\n print(f\"myapp/db/database: {repr(cfg.db.database)} {type(cfg.db.database)}\")\n print(f\"myapp/db/username: {repr(cfg.db.username)} {type(cfg.db.username)}\")\n print(f\"myapp/db/password: {repr(cfg.db.password)} {type(cfg.db.password)}\")\n print(f\"myapp/db/connection_timeout: {repr(cfg.db.connection_timeout)} {type(cfg.db.connection_timeout)}\")\n print(f\"myapp/db/sslmode_require: {repr(cfg.db.sslmode_require)} {type(cfg.db.sslmode_require)}\")\n print(f\"myapp/db/connection_string: {repr(cfg.db.connection_string)} {type(cfg.db.connection_string)}\")\n```\n\nRun the example with `python -m envenom.examples.quickstart`:\n\n```\nTraceback (most recent call last):\n ...\n raise MissingConfiguration(self.env_name)\nenvenom.errors.MissingConfiguration: 'MYAPP__SECRET_KEY'\n```\n\nRun the example again with environment set:\n\n```bash\nMYAPP__SECRET_KEY='}uZ?uvJdKDM+$2[$dR)).n4q1SX!A$0u{(+D$PVB' \\\nMYAPP__POSTGRES__HOST='postgres' \\\nMYAPP__POSTGRES__DATABASE='database-name' \\\nMYAPP__POSTGRES__USERNAME='user' \\\nMYAPP__POSTGRES__SSLMODE_REQUIRE='t' \\\nMYAPP__POSTGRES__CONNECTION_TIMEOUT='15' \\\npython -m envenom.examples.quickstart\n```\n\n```bash\nmyapp/secret_key: '}uZ?uvJdKDM+$2[$dR)).n4q1SX!A$0u{(+D$PVB' <class 'str'>\nmyapp/db/host: 'postgres' <class 'str'>\nmyapp/db/port: 5432 <class 'int'>\nmyapp/db/database: 'database-name' <class 'str'>\nmyapp/db/username: 'user' <class 'str'>\nmyapp/db/password: None <class 'NoneType'>\nmyapp/db/connection_timeout: 15 <class 'int'>\nmyapp/db/sslmode_require: True <class 'bool'>\nmyapp/db/connection_string: 'postgresql+psycopg://user@postgres:5432/database-name?sslmode=require&timeout=15' <class 'str'>\n```\n\n### Next steps\n\nSee the [wiki](https://gitlab.com/python-arcana/envenom/-/wikis/Home) for more info\nand examples of advanced usage.\n\n",
"bugtrack_url": null,
"license": "GPL-3.0-or-later",
"summary": "An elegant application configurator for the more civilized age",
"version": "1.0.9",
"project_urls": {
"Documentation": "https://gitlab.com/python-arcana/envenom/-/wikis/Home",
"Homepage": "https://gitlab.com/python-arcana/envenom",
"Repository": "https://gitlab.com/python-arcana/envenom"
},
"split_keywords": [
"env",
" environment",
" config",
" configuration"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "3e65dfc3eba5babfdcaa0bf25bb8211516faff4f46f950f8f289fb737c5c6314",
"md5": "dd9c38ad80ac1e0a4dd0b3835dbc48bd",
"sha256": "7f86e087160f8dea3b82bb7f60a0b50c59227d86ae99252dc64cf552a08863b9"
},
"downloads": -1,
"filename": "envenom-1.0.9-py3-none-any.whl",
"has_sig": false,
"md5_digest": "dd9c38ad80ac1e0a4dd0b3835dbc48bd",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": "<4.0,>=3.10",
"size": 27253,
"upload_time": "2024-04-03T12:52:35",
"upload_time_iso_8601": "2024-04-03T12:52:35.930264Z",
"url": "https://files.pythonhosted.org/packages/3e/65/dfc3eba5babfdcaa0bf25bb8211516faff4f46f950f8f289fb737c5c6314/envenom-1.0.9-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "1e306c8c8c9184864b8bd586218b5077432e2eebadcaf33a2cab76c00c95b6ed",
"md5": "cb86f25eda9f7ac8b32da4f4587bd6da",
"sha256": "ea869f83e90bd50c1401eba7a53b0d8f288e7b654b01691776a442b8ec14a48a"
},
"downloads": -1,
"filename": "envenom-1.0.9.tar.gz",
"has_sig": false,
"md5_digest": "cb86f25eda9f7ac8b32da4f4587bd6da",
"packagetype": "sdist",
"python_version": "source",
"requires_python": "<4.0,>=3.10",
"size": 22400,
"upload_time": "2024-04-03T12:52:37",
"upload_time_iso_8601": "2024-04-03T12:52:37.658724Z",
"url": "https://files.pythonhosted.org/packages/1e/30/6c8c8c9184864b8bd586218b5077432e2eebadcaf33a2cab76c00c95b6ed/envenom-1.0.9.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-04-03 12:52:37",
"github": false,
"gitlab": true,
"bitbucket": false,
"codeberg": false,
"gitlab_user": "python-arcana",
"gitlab_project": "envenom",
"lcname": "envenom"
}