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 ADDED
@@ -0,0 +1,8 @@
1
+ """
2
+ envbase: pydantic settings with class name prefixes and per
3
+ environment dotenv files.
4
+ """
5
+
6
+ from envbase.env_base import EnvBase
7
+
8
+ __all__ = ["EnvBase"]
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any