envbase 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- envbase/__init__.py +8 -0
- envbase/env_base.py +86 -0
- envbase-1.0.0.dist-info/METADATA +112 -0
- envbase-1.0.0.dist-info/RECORD +5 -0
- envbase-1.0.0.dist-info/WHEEL +4 -0
envbase/__init__.py
ADDED
envbase/env_base.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EnvBase is a pydantic settings model with class name prefixes and
|
|
3
|
+
per environment dotenv files.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from os import environ
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from re import sub
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def snake(name: str) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Converts a class name to snake case, keeping acronyms together.
|
|
17
|
+
"""
|
|
18
|
+
return sub(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", "_", name).lower()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EnvBase(BaseSettings):
|
|
22
|
+
"""
|
|
23
|
+
Settings read from the environment and a dotenv file.
|
|
24
|
+
|
|
25
|
+
Each field reads the variable PREFIX_FIELD, where the prefix is the
|
|
26
|
+
snake case class name (RedisConfig.uri reads REDIS_CONFIG_URI) or
|
|
27
|
+
_prefix when set (_prefix = "redis" reads REDIS_URI). An empty
|
|
28
|
+
_prefix reads bare field names.
|
|
29
|
+
|
|
30
|
+
The dotenv file is .env in the _resources directory, or
|
|
31
|
+
.env.{name} when the variable named by _variable holds a name.
|
|
32
|
+
Keyword arguments win over the environment, the environment wins
|
|
33
|
+
over the file, and the file wins over defaults.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
model_config = SettingsConfigDict(
|
|
37
|
+
extra="ignore",
|
|
38
|
+
env_nested_delimiter="__",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
_prefix: str | None = None
|
|
42
|
+
_resources: str = "./resources"
|
|
43
|
+
_variable: str = "EXECUTION_ENVIRONMENT"
|
|
44
|
+
|
|
45
|
+
def __init__(self, **values: Any) -> None:
|
|
46
|
+
values.setdefault("_env_prefix", type(self).prefix())
|
|
47
|
+
values.setdefault("_env_file", type(self).file())
|
|
48
|
+
super().__init__(**values)
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def option(cls, name: str) -> Any:
|
|
52
|
+
"""
|
|
53
|
+
Returns the class level value of an underscore option.
|
|
54
|
+
"""
|
|
55
|
+
return cls.__private_attributes__[name].default
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def prefix(cls) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Returns the variable prefix, with its trailing underscore.
|
|
61
|
+
"""
|
|
62
|
+
prefix = cls.option("_prefix")
|
|
63
|
+
if prefix is None:
|
|
64
|
+
prefix = snake(cls.__name__)
|
|
65
|
+
return f"{prefix.upper()}_" if prefix else ""
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def file(cls) -> Path:
|
|
69
|
+
"""
|
|
70
|
+
Returns the dotenv file for the current execution environment.
|
|
71
|
+
"""
|
|
72
|
+
name = environ.get(cls.option("_variable"))
|
|
73
|
+
resources = Path(cls.option("_resources"))
|
|
74
|
+
return resources / (f".env.{name}" if name else ".env")
|
|
75
|
+
|
|
76
|
+
def dump(self, **options: Any) -> dict[str, Any]:
|
|
77
|
+
"""
|
|
78
|
+
Returns the fields as a dictionary of python values.
|
|
79
|
+
"""
|
|
80
|
+
return self.model_dump(**options)
|
|
81
|
+
|
|
82
|
+
def json(self, **options: Any) -> dict[str, Any]:
|
|
83
|
+
"""
|
|
84
|
+
Returns the fields as a dictionary of json safe values.
|
|
85
|
+
"""
|
|
86
|
+
return self.model_dump(mode="json", **options)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: envbase
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Pydantic settings with class name prefixes and per environment dotenv files
|
|
5
|
+
Project-URL: Homepage, https://github.com/parkermmr/envbase
|
|
6
|
+
Project-URL: Issues, https://github.com/parkermmr/envbase/issues
|
|
7
|
+
Author: Parker
|
|
8
|
+
Classifier: Framework :: Pydantic :: 2
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Requires-Dist: pydantic-settings>=2.4
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
[![CI Status][ci-shield]][ci-url]
|
|
20
|
+
[![Coverage][coverage-shield]][coverage-url]
|
|
21
|
+
[![Contributors][contributors-shield]][contributors-url]
|
|
22
|
+
[![Forks][forks-shield]][forks-url]
|
|
23
|
+
[![Stargazers][stars-shield]][stars-url]
|
|
24
|
+
[![Issues][issues-shield]][issues-url]
|
|
25
|
+
|
|
26
|
+
# Envbase
|
|
27
|
+
|
|
28
|
+
Pydantic settings with class name prefixes and per environment dotenv
|
|
29
|
+
files.
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
pip install envbase
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from envbase import EnvBase
|
|
39
|
+
|
|
40
|
+
class RedisConfig(EnvBase):
|
|
41
|
+
uri: str = "localhost"
|
|
42
|
+
port: int = 9010
|
|
43
|
+
ssl: bool = False
|
|
44
|
+
|
|
45
|
+
config = RedisConfig()
|
|
46
|
+
config.uri # REDIS_CONFIG_URI, else resources/.env, else "localhost"
|
|
47
|
+
config.dump() # {"uri": "localhost", "port": 9010, "ssl": False}
|
|
48
|
+
config.json() # same, json safe values (secrets masked, paths as strings)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Prefixes
|
|
52
|
+
|
|
53
|
+
Each field reads `PREFIX_FIELD`. The prefix is the snake case class name
|
|
54
|
+
unless `_prefix` is set. Matching is case insensitive.
|
|
55
|
+
|
|
56
|
+
| Class | `uri` reads |
|
|
57
|
+
|---------------------------------|--------------------|
|
|
58
|
+
| `class RedisConfig(EnvBase)` | `REDIS_CONFIG_URI` |
|
|
59
|
+
| `class HTTPServer(EnvBase)` | `HTTP_SERVER_URI` |
|
|
60
|
+
| `_prefix = "redis"` | `REDIS_URI` |
|
|
61
|
+
| `_prefix = ""` | `URI` |
|
|
62
|
+
|
|
63
|
+
Nested models read `PREFIX_FIELD__SUBFIELD`. Subclasses inherit `_prefix`.
|
|
64
|
+
|
|
65
|
+
## Files
|
|
66
|
+
|
|
67
|
+
Values are loaded from a dotenv file in the resources directory:
|
|
68
|
+
|
|
69
|
+
| `EXECUTION_ENVIRONMENT` | File |
|
|
70
|
+
|-------------------------|--------------------------|
|
|
71
|
+
| unset | `./resources/.env` |
|
|
72
|
+
| `prod` | `./resources/.env.prod` |
|
|
73
|
+
|
|
74
|
+
A missing file is skipped. Keys in the file that belong to other classes
|
|
75
|
+
are ignored, so one file can hold every class's settings.
|
|
76
|
+
|
|
77
|
+
Precedence, highest first: keyword arguments, environment variables, the
|
|
78
|
+
dotenv file, field defaults.
|
|
79
|
+
|
|
80
|
+
## Options
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
class RedisConfig(EnvBase):
|
|
84
|
+
uri: str = "localhost"
|
|
85
|
+
|
|
86
|
+
_prefix = "redis" # default: snake case class name
|
|
87
|
+
_resources = "./config" # default: ./resources (relative to cwd)
|
|
88
|
+
_variable = "STAGE" # default: EXECUTION_ENVIRONMENT
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`RedisConfig.prefix()` and `RedisConfig.file()` show what a class resolves
|
|
92
|
+
to. Everything else is plain pydantic settings: validators, `SecretStr`,
|
|
93
|
+
`model_config`, and the `_env_file` keyword all work as usual.
|
|
94
|
+
|
|
95
|
+
## Tests
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
pytest --cov
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
[ci-shield]: https://img.shields.io/github/actions/workflow/status/parkermmr/envbase/ci.yml?branch=main&style=for-the-badge&label=CI
|
|
102
|
+
[ci-url]: https://github.com/parkermmr/envbase/actions/workflows/ci.yml
|
|
103
|
+
[coverage-shield]: https://img.shields.io/codecov/c/github/parkermmr/envbase?style=for-the-badge
|
|
104
|
+
[coverage-url]: https://codecov.io/gh/parkermmr/envbase
|
|
105
|
+
[contributors-shield]: https://img.shields.io/github/contributors/parkermmr/envbase.svg?style=for-the-badge
|
|
106
|
+
[contributors-url]: https://github.com/parkermmr/envbase/graphs/contributors
|
|
107
|
+
[forks-shield]: https://img.shields.io/github/forks/parkermmr/envbase.svg?style=for-the-badge
|
|
108
|
+
[forks-url]: https://github.com/parkermmr/envbase/network/members
|
|
109
|
+
[stars-shield]: https://img.shields.io/github/stars/parkermmr/envbase.svg?style=for-the-badge
|
|
110
|
+
[stars-url]: https://github.com/parkermmr/envbase/stargazers
|
|
111
|
+
[issues-shield]: https://img.shields.io/github/issues/parkermmr/envbase.svg?style=for-the-badge
|
|
112
|
+
[issues-url]: https://github.com/parkermmr/envbase/issues
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
envbase/__init__.py,sha256=JsOf5uZ1INiA563jmmek5CHNjWycHvz1DGqzD3ZwVN4,155
|
|
2
|
+
envbase/env_base.py,sha256=B6vPwVJEzNPyCSXVguT9qiEFMUVTjf8772wA_F24X38,2666
|
|
3
|
+
envbase-1.0.0.dist-info/METADATA,sha256=8gGXRUoAGL0pAqftoWXfkrYnlpWLWsqGSjWthT_r3B8,4091
|
|
4
|
+
envbase-1.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
5
|
+
envbase-1.0.0.dist-info/RECORD,,
|