xtr-dotenv 1.2.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xterr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,248 @@
1
+ Metadata-Version: 2.4
2
+ Name: xtr-dotenv
3
+ Version: 1.2.0
4
+ Summary: Layered .env files loaded into the environment, and the same layers behind a typed settings model.
5
+ Keywords: dotenv,env,settings,configuration,layered
6
+ Author: Razvan Ceana
7
+ Author-email: Razvan Ceana <razvan@ceana.ro>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Typing :: Typed
17
+ Requires-Dist: pydantic-settings>=2.4,<3
18
+ Requires-Dist: python-dotenv>=1.0,<2
19
+ Requires-Dist: xtr-console>=1.0,<2 ; extra == 'console'
20
+ Requires-Dist: xtr-dependency-injection>=1.0,<2 ; extra == 'di'
21
+ Requires-Python: >=3.11
22
+ Provides-Extra: console
23
+ Provides-Extra: di
24
+ Description-Content-Type: text/markdown
25
+
26
+ <div align="center">
27
+
28
+ # xtr-dotenv
29
+
30
+ **Layered `.env` files loaded into the environment, and the same layers behind a typed settings model.**
31
+
32
+ <img alt="python 3.11+" src="https://img.shields.io/badge/python-%E2%89%A5%203.11-3776AB?logo=python&logoColor=white">
33
+ <img alt="typed" src="https://img.shields.io/badge/typed-ty%20%2B%20basedpyright-1f6feb">
34
+ <img alt="license MIT" src="https://img.shields.io/badge/license-MIT-blue">
35
+
36
+ </div>
37
+
38
+ ---
39
+
40
+ ## Why?
41
+
42
+ Real applications keep several `.env` files layered so a developer laptop, a CI runner and a
43
+ production host each end up with the settings that suit, without any one file knowing about
44
+ the other. This library is the loader every layer runs through, plus a pydantic-settings
45
+ source that feeds a typed model from the same cascade โ€” without ever touching
46
+ `os.environ` behind your back.
47
+
48
+ - ๐Ÿ“š **The layered cascade** โ€” `.env` โ†’ `.env.local` โ†’ `.env.{env}` โ†’ `.env.{env}.local`, with a
49
+ `.env.dist` read instead of a missing `.env`, and real environment variables always winning.
50
+ - ๐Ÿงช **Testable end-to-end** โ€” hand the loader a plain `dict` and no other test in the same
51
+ process can see anything it did.
52
+ - ๐Ÿงฉ **Pydantic-settings integration** โ€” `DotenvSettings` reads the same cascade from a
53
+ sandboxed copy of `os.environ`, so a model gets the values without side effects.
54
+ - ๐ŸŒ **Variable expansion** โ€” `$VAR`, `${VAR}`, `${VAR:-default}`, `${VAR:=default}`, `\$` as
55
+ a literal. Single-quoted values are literal too. `$(command)` is kept as text, never run.
56
+ - โšก **A dumped fast path** โ€” `dotenv:dump` writes the compiled cascade to `.env.local.json`;
57
+ `boot_env` reads that one file instead of the cascade.
58
+
59
+ ## Install
60
+
61
+ ```sh
62
+ uv add xtr-dotenv # the loader and the settings source
63
+ uv add "xtr-dotenv[di]" # + a DotenvBundle for xtr-dependency-injection
64
+ uv add "xtr-dotenv[console]" # + the dotenv:dump and debug:dotenv commands
65
+ ```
66
+
67
+ Requires Python 3.11+.
68
+
69
+ ## Quick start
70
+
71
+ Call `Dotenv().boot_env(...)` at the very top of your entry point โ€” **before** anything
72
+ reads a settings model, and before you build a kernel:
73
+
74
+ ```python
75
+ from pathlib import Path
76
+
77
+ from xtr_dotenv import Dotenv
78
+
79
+ Dotenv().boot_env(str(Path(__file__).parent / ".env"))
80
+
81
+ # ... only now import the kernel, settings, etc.
82
+ from app.kernel import kernel
83
+
84
+ raise SystemExit(kernel.run(...))
85
+ ```
86
+
87
+ The loader is stateless: hand it a `MutableMapping[str, str]` as `environ=` and no other
88
+ code sees a thing.
89
+
90
+ ```python
91
+ sandbox: dict[str, str] = {}
92
+ Dotenv(environ=sandbox).load_env("/etc/app/.env")
93
+ ```
94
+
95
+ ## The file cascade
96
+
97
+ `load_env(path)` reads, in order:
98
+
99
+ | Order | File | When it applies |
100
+ | --- | --- | --- |
101
+ | 1 | `path` | Always, or `path.dist` when `path` is missing |
102
+ | 2 | *(reads env key from environ; defaults to `default_env`)* | |
103
+ | 3 | `path.local` | Only outside `test_envs` โ€” a developer's per-machine overrides |
104
+ | 4 | *(re-reads env key)* | The `.local` file may have set a different environment |
105
+ | 5 | `path.{env}` | When `env` is not `"local"` |
106
+ | 6 | `path.{env}.local` | Same โ€” a per-machine override for that environment |
107
+
108
+ Later files override earlier ones. A **real** environment variable always wins over any file
109
+ unless `override_existing_vars=True` is passed. `XTR_DOTENV_VARS` tracks which names came
110
+ from a file so a later file may replace them; `XTR_DOTENV_PATH` records the base path so
111
+ tooling can find it.
112
+
113
+ ## Variable expansion
114
+
115
+ | Written | Means |
116
+ | --- | --- |
117
+ | `$VAR`, `${VAR}` | the value of `VAR`, or empty when unset |
118
+ | `${VAR:-default}` | `default` when `VAR` is unset or empty |
119
+ | `${VAR:=default}` | the same, and record `VAR=default` for later lookups |
120
+ | `\$` | a literal dollar sign |
121
+
122
+ **Single-quoted values are never expanded** โ€” that is the escape hatch for a literal `$`
123
+ inside a password. `$(command)` is kept as text and never run: a settings loader should do
124
+ no I/O beyond reading files. A default is plain text โ€” a quote, a brace or a `$` in one
125
+ (`${A:-${B}}`), or a `${` never closed, is a `FormatError` naming the line. A value spanning
126
+ lines must be quoted.
127
+
128
+ Expansion runs once every file of the cascade is read, so a value in `.env` may reference
129
+ one only `.env.local` defines; a real environment variable wins inside an expansion as it
130
+ does everywhere else. A self-referencing `A=${A:-x}` reads the value `A` has now (or the
131
+ default) โ€” it does not cycle. Anything still unresolved after five passes is a real cycle and
132
+ raises `VariableCircularReferenceError`.
133
+
134
+ ## Typed settings from the cascade
135
+
136
+ `DotenvSettings` is a `BaseSettings` base whose subclass reads the layered cascade from a
137
+ sandboxed copy of `os.environ`:
138
+
139
+ ```python
140
+ from typing import ClassVar
141
+
142
+ from xtr_dotenv import DotenvSettings
143
+
144
+
145
+ class AppSettings(DotenvSettings):
146
+ _dotenv_path: ClassVar[str] = "/etc/app/.env"
147
+ _dotenv_env_key: ClassVar[str] = "APP_ENV"
148
+ _dotenv_default_env: ClassVar[str] = "prod"
149
+
150
+ database_url: str
151
+ log_level: str = "INFO"
152
+ ```
153
+
154
+ Source priority is: init keyword arguments > real environment variables > the dotenv
155
+ cascade > secret files > field defaults. **No file the source reads writes into
156
+ `os.environ`.**
157
+
158
+ ## Errors
159
+
160
+ Everything the library raises derives from `DotenvError`, and carries the data as typed
161
+ attributes.
162
+
163
+ | Error | Raised when |
164
+ | --- | --- |
165
+ | `FormatError` | A line cannot be parsed (bad binding, key without `=`); carries `.path`, `.line`, `.reason` |
166
+ | `PathError` | A file cannot be read; carries `.path` |
167
+ | `VariableCircularReferenceError` | Variables reference each other and never resolve; carries `.names` |
168
+
169
+ `FormatError` and `VariableCircularReferenceError` are also `ValueError`s; `PathError` is
170
+ also `OSError`. Existing `except` blocks keep working.
171
+
172
+ ## Kernel / bundle
173
+
174
+ An application using [xtr-dependency-injection](../xtr-dependency-injection) lists
175
+ `DotenvBundle` in its `app/bundles.py`. **The bundle does not load `.env` files** โ€”
176
+ `Dotenv().boot_env(...)` runs before the kernel is built. What the bundle contributes is
177
+ the two commands that need the kernel to know its project directory:
178
+
179
+ ```sh
180
+ uv add "xtr-dotenv[di,console]"
181
+ ```
182
+
183
+ ```python
184
+ # app/bundles.py
185
+ from xtr_dotenv.bundle import DotenvBundle
186
+
187
+ BUNDLES = {DotenvBundle: {"all": True}}
188
+ ```
189
+
190
+ ```python
191
+ # app/config/dotenv.py
192
+ from xtr_dependency_injection import configure
193
+ from xtr_dotenv.bundle import DotenvConfig
194
+
195
+
196
+ @configure
197
+ def dotenv() -> DotenvConfig:
198
+ return DotenvConfig(path="%kernel.project_dir%/.env", env_key="APP_ENV")
199
+ ```
200
+
201
+ | `DotenvConfig` field | Meaning |
202
+ | --- | --- |
203
+ | `path` | The base `.env` path; default `%kernel.project_dir%/.env` โ€” parameter references are resolved by the kernel, and a relative path is taken from the project directory |
204
+ | `env_key` | The variable naming the active environment |
205
+ | `debug_key` | The variable naming debug mode |
206
+ | `test_envs` | Environments where the `.local` overlay is skipped |
207
+ | `prod_envs` | Environments considered production, used for debug defaulting |
208
+
209
+ ### Commands
210
+
211
+ | Command | What it does |
212
+ | --- | --- |
213
+ | `dotenv:dump [env]` | Compile the cascade for `env` (default: the kernel's env) into `<path>.local.json`. Runs on a fresh environ with only the env key, so real secrets never land in the dump |
214
+ | `debug:dotenv [name]` | List the files that apply in cascade order (loaded / missing) and each variable's value per file, filtered by `name` when given |
215
+
216
+ The commands are only registered when the console bundle is active; on a headless
217
+ application the bundle is still valid and boots at zero config.
218
+
219
+ ## Layout
220
+
221
+ ```
222
+ xtr_dotenv/
223
+ โ”œโ”€โ”€ dotenv.py the loader: parse, load, overload, populate, load_env, boot_env
224
+ โ”œโ”€โ”€ dotenv_settings_source.py a pydantic-settings source that runs the cascade in a sandbox
225
+ โ”œโ”€โ”€ dotenv_settings.py BaseSettings base declaring the cascade behind the real env
226
+ โ”œโ”€โ”€ exception/ DotenvError + FormatError, PathError, VariableCircularReferenceError
227
+ โ”œโ”€โ”€ command/ dotenv:dump and debug:dotenv (xtr-console commands)
228
+ โ””โ”€โ”€ bundle/ DotenvBundle for xtr-dependency-injection
229
+ ```
230
+
231
+ ## Development
232
+
233
+ Developed in the [python-xtr](https://github.com/xterr/python-xtr) monorepo, under
234
+ `packages/xtr-dotenv`; run the commands below from there. The `python-xtr-dotenv`
235
+ repository is a read-only copy, so send issues and pull requests to the monorepo.
236
+
237
+ ```sh
238
+ uv sync --all-extras
239
+ uv run ruff check
240
+ uv run ruff format --check
241
+ uv run basedpyright
242
+ uv run ty check
243
+ uv run pytest
244
+ ```
245
+
246
+ ## License
247
+
248
+ MIT โ€” see [LICENSE](LICENSE).
@@ -0,0 +1,223 @@
1
+ <div align="center">
2
+
3
+ # xtr-dotenv
4
+
5
+ **Layered `.env` files loaded into the environment, and the same layers behind a typed settings model.**
6
+
7
+ <img alt="python 3.11+" src="https://img.shields.io/badge/python-%E2%89%A5%203.11-3776AB?logo=python&logoColor=white">
8
+ <img alt="typed" src="https://img.shields.io/badge/typed-ty%20%2B%20basedpyright-1f6feb">
9
+ <img alt="license MIT" src="https://img.shields.io/badge/license-MIT-blue">
10
+
11
+ </div>
12
+
13
+ ---
14
+
15
+ ## Why?
16
+
17
+ Real applications keep several `.env` files layered so a developer laptop, a CI runner and a
18
+ production host each end up with the settings that suit, without any one file knowing about
19
+ the other. This library is the loader every layer runs through, plus a pydantic-settings
20
+ source that feeds a typed model from the same cascade โ€” without ever touching
21
+ `os.environ` behind your back.
22
+
23
+ - ๐Ÿ“š **The layered cascade** โ€” `.env` โ†’ `.env.local` โ†’ `.env.{env}` โ†’ `.env.{env}.local`, with a
24
+ `.env.dist` read instead of a missing `.env`, and real environment variables always winning.
25
+ - ๐Ÿงช **Testable end-to-end** โ€” hand the loader a plain `dict` and no other test in the same
26
+ process can see anything it did.
27
+ - ๐Ÿงฉ **Pydantic-settings integration** โ€” `DotenvSettings` reads the same cascade from a
28
+ sandboxed copy of `os.environ`, so a model gets the values without side effects.
29
+ - ๐ŸŒ **Variable expansion** โ€” `$VAR`, `${VAR}`, `${VAR:-default}`, `${VAR:=default}`, `\$` as
30
+ a literal. Single-quoted values are literal too. `$(command)` is kept as text, never run.
31
+ - โšก **A dumped fast path** โ€” `dotenv:dump` writes the compiled cascade to `.env.local.json`;
32
+ `boot_env` reads that one file instead of the cascade.
33
+
34
+ ## Install
35
+
36
+ ```sh
37
+ uv add xtr-dotenv # the loader and the settings source
38
+ uv add "xtr-dotenv[di]" # + a DotenvBundle for xtr-dependency-injection
39
+ uv add "xtr-dotenv[console]" # + the dotenv:dump and debug:dotenv commands
40
+ ```
41
+
42
+ Requires Python 3.11+.
43
+
44
+ ## Quick start
45
+
46
+ Call `Dotenv().boot_env(...)` at the very top of your entry point โ€” **before** anything
47
+ reads a settings model, and before you build a kernel:
48
+
49
+ ```python
50
+ from pathlib import Path
51
+
52
+ from xtr_dotenv import Dotenv
53
+
54
+ Dotenv().boot_env(str(Path(__file__).parent / ".env"))
55
+
56
+ # ... only now import the kernel, settings, etc.
57
+ from app.kernel import kernel
58
+
59
+ raise SystemExit(kernel.run(...))
60
+ ```
61
+
62
+ The loader is stateless: hand it a `MutableMapping[str, str]` as `environ=` and no other
63
+ code sees a thing.
64
+
65
+ ```python
66
+ sandbox: dict[str, str] = {}
67
+ Dotenv(environ=sandbox).load_env("/etc/app/.env")
68
+ ```
69
+
70
+ ## The file cascade
71
+
72
+ `load_env(path)` reads, in order:
73
+
74
+ | Order | File | When it applies |
75
+ | --- | --- | --- |
76
+ | 1 | `path` | Always, or `path.dist` when `path` is missing |
77
+ | 2 | *(reads env key from environ; defaults to `default_env`)* | |
78
+ | 3 | `path.local` | Only outside `test_envs` โ€” a developer's per-machine overrides |
79
+ | 4 | *(re-reads env key)* | The `.local` file may have set a different environment |
80
+ | 5 | `path.{env}` | When `env` is not `"local"` |
81
+ | 6 | `path.{env}.local` | Same โ€” a per-machine override for that environment |
82
+
83
+ Later files override earlier ones. A **real** environment variable always wins over any file
84
+ unless `override_existing_vars=True` is passed. `XTR_DOTENV_VARS` tracks which names came
85
+ from a file so a later file may replace them; `XTR_DOTENV_PATH` records the base path so
86
+ tooling can find it.
87
+
88
+ ## Variable expansion
89
+
90
+ | Written | Means |
91
+ | --- | --- |
92
+ | `$VAR`, `${VAR}` | the value of `VAR`, or empty when unset |
93
+ | `${VAR:-default}` | `default` when `VAR` is unset or empty |
94
+ | `${VAR:=default}` | the same, and record `VAR=default` for later lookups |
95
+ | `\$` | a literal dollar sign |
96
+
97
+ **Single-quoted values are never expanded** โ€” that is the escape hatch for a literal `$`
98
+ inside a password. `$(command)` is kept as text and never run: a settings loader should do
99
+ no I/O beyond reading files. A default is plain text โ€” a quote, a brace or a `$` in one
100
+ (`${A:-${B}}`), or a `${` never closed, is a `FormatError` naming the line. A value spanning
101
+ lines must be quoted.
102
+
103
+ Expansion runs once every file of the cascade is read, so a value in `.env` may reference
104
+ one only `.env.local` defines; a real environment variable wins inside an expansion as it
105
+ does everywhere else. A self-referencing `A=${A:-x}` reads the value `A` has now (or the
106
+ default) โ€” it does not cycle. Anything still unresolved after five passes is a real cycle and
107
+ raises `VariableCircularReferenceError`.
108
+
109
+ ## Typed settings from the cascade
110
+
111
+ `DotenvSettings` is a `BaseSettings` base whose subclass reads the layered cascade from a
112
+ sandboxed copy of `os.environ`:
113
+
114
+ ```python
115
+ from typing import ClassVar
116
+
117
+ from xtr_dotenv import DotenvSettings
118
+
119
+
120
+ class AppSettings(DotenvSettings):
121
+ _dotenv_path: ClassVar[str] = "/etc/app/.env"
122
+ _dotenv_env_key: ClassVar[str] = "APP_ENV"
123
+ _dotenv_default_env: ClassVar[str] = "prod"
124
+
125
+ database_url: str
126
+ log_level: str = "INFO"
127
+ ```
128
+
129
+ Source priority is: init keyword arguments > real environment variables > the dotenv
130
+ cascade > secret files > field defaults. **No file the source reads writes into
131
+ `os.environ`.**
132
+
133
+ ## Errors
134
+
135
+ Everything the library raises derives from `DotenvError`, and carries the data as typed
136
+ attributes.
137
+
138
+ | Error | Raised when |
139
+ | --- | --- |
140
+ | `FormatError` | A line cannot be parsed (bad binding, key without `=`); carries `.path`, `.line`, `.reason` |
141
+ | `PathError` | A file cannot be read; carries `.path` |
142
+ | `VariableCircularReferenceError` | Variables reference each other and never resolve; carries `.names` |
143
+
144
+ `FormatError` and `VariableCircularReferenceError` are also `ValueError`s; `PathError` is
145
+ also `OSError`. Existing `except` blocks keep working.
146
+
147
+ ## Kernel / bundle
148
+
149
+ An application using [xtr-dependency-injection](../xtr-dependency-injection) lists
150
+ `DotenvBundle` in its `app/bundles.py`. **The bundle does not load `.env` files** โ€”
151
+ `Dotenv().boot_env(...)` runs before the kernel is built. What the bundle contributes is
152
+ the two commands that need the kernel to know its project directory:
153
+
154
+ ```sh
155
+ uv add "xtr-dotenv[di,console]"
156
+ ```
157
+
158
+ ```python
159
+ # app/bundles.py
160
+ from xtr_dotenv.bundle import DotenvBundle
161
+
162
+ BUNDLES = {DotenvBundle: {"all": True}}
163
+ ```
164
+
165
+ ```python
166
+ # app/config/dotenv.py
167
+ from xtr_dependency_injection import configure
168
+ from xtr_dotenv.bundle import DotenvConfig
169
+
170
+
171
+ @configure
172
+ def dotenv() -> DotenvConfig:
173
+ return DotenvConfig(path="%kernel.project_dir%/.env", env_key="APP_ENV")
174
+ ```
175
+
176
+ | `DotenvConfig` field | Meaning |
177
+ | --- | --- |
178
+ | `path` | The base `.env` path; default `%kernel.project_dir%/.env` โ€” parameter references are resolved by the kernel, and a relative path is taken from the project directory |
179
+ | `env_key` | The variable naming the active environment |
180
+ | `debug_key` | The variable naming debug mode |
181
+ | `test_envs` | Environments where the `.local` overlay is skipped |
182
+ | `prod_envs` | Environments considered production, used for debug defaulting |
183
+
184
+ ### Commands
185
+
186
+ | Command | What it does |
187
+ | --- | --- |
188
+ | `dotenv:dump [env]` | Compile the cascade for `env` (default: the kernel's env) into `<path>.local.json`. Runs on a fresh environ with only the env key, so real secrets never land in the dump |
189
+ | `debug:dotenv [name]` | List the files that apply in cascade order (loaded / missing) and each variable's value per file, filtered by `name` when given |
190
+
191
+ The commands are only registered when the console bundle is active; on a headless
192
+ application the bundle is still valid and boots at zero config.
193
+
194
+ ## Layout
195
+
196
+ ```
197
+ xtr_dotenv/
198
+ โ”œโ”€โ”€ dotenv.py the loader: parse, load, overload, populate, load_env, boot_env
199
+ โ”œโ”€โ”€ dotenv_settings_source.py a pydantic-settings source that runs the cascade in a sandbox
200
+ โ”œโ”€โ”€ dotenv_settings.py BaseSettings base declaring the cascade behind the real env
201
+ โ”œโ”€โ”€ exception/ DotenvError + FormatError, PathError, VariableCircularReferenceError
202
+ โ”œโ”€โ”€ command/ dotenv:dump and debug:dotenv (xtr-console commands)
203
+ โ””โ”€โ”€ bundle/ DotenvBundle for xtr-dependency-injection
204
+ ```
205
+
206
+ ## Development
207
+
208
+ Developed in the [python-xtr](https://github.com/xterr/python-xtr) monorepo, under
209
+ `packages/xtr-dotenv`; run the commands below from there. The `python-xtr-dotenv`
210
+ repository is a read-only copy, so send issues and pull requests to the monorepo.
211
+
212
+ ```sh
213
+ uv sync --all-extras
214
+ uv run ruff check
215
+ uv run ruff format --check
216
+ uv run basedpyright
217
+ uv run ty check
218
+ uv run pytest
219
+ ```
220
+
221
+ ## License
222
+
223
+ MIT โ€” see [LICENSE](LICENSE).
@@ -0,0 +1,153 @@
1
+ [project]
2
+ name = "xtr-dotenv"
3
+ version = "1.2.0"
4
+ description = "Layered .env files loaded into the environment, and the same layers behind a typed settings model."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ keywords = [
10
+ "dotenv",
11
+ "env",
12
+ "settings",
13
+ "configuration",
14
+ "layered",
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ "Typing :: Typed",
24
+ ]
25
+ dependencies = [
26
+ "pydantic-settings>=2.4,<3",
27
+ "python-dotenv>=1.0,<2",
28
+ ]
29
+
30
+ [[project.authors]]
31
+ name = "Razvan Ceana"
32
+ email = "razvan@ceana.ro"
33
+
34
+ [project.optional-dependencies]
35
+ di = ["xtr-dependency-injection>=1.0,<2"]
36
+ console = ["xtr-console>=1.0,<2"]
37
+
38
+ [dependency-groups]
39
+ dev = [
40
+ "basedpyright>=1.21",
41
+ "ruff>=0.8",
42
+ "pytest>=8",
43
+ "pytest-cov>=5",
44
+ "anyio>=4.0",
45
+ "ty>=0.0.83",
46
+ "xtr-dependency-injection>=1.0,<2",
47
+ "xtr-console>=1.0,<2",
48
+ ]
49
+
50
+ [build-system]
51
+ requires = ["uv_build>=0.9.18,<0.10.0"]
52
+ build-backend = "uv_build"
53
+
54
+ [tool.basedpyright]
55
+ typeCheckingMode = "all"
56
+ pythonVersion = "3.11"
57
+ pythonPlatform = "All"
58
+ include = [
59
+ "src",
60
+ "tests",
61
+ ]
62
+ exclude = [
63
+ "**/__pycache__",
64
+ "**/.venv",
65
+ "**/build",
66
+ "**/dist",
67
+ ".tmp",
68
+ ]
69
+ reportUnusedCallResult = "warning"
70
+ reportUnnecessaryTypeIgnoreComment = "error"
71
+ reportUnusedVariable = "error"
72
+ reportMissingParameterType = "error"
73
+ reportPrivateUsage = "error"
74
+ reportImplicitOverride = "none"
75
+ reportImplicitStringConcatenation = "none"
76
+
77
+ [tool.ruff]
78
+ target-version = "py311"
79
+ line-length = 100
80
+ src = [
81
+ "src",
82
+ "tests",
83
+ ]
84
+
85
+ [tool.ruff.lint]
86
+ select = ["ALL"]
87
+ ignore = [
88
+ "COM812",
89
+ "ISC001",
90
+ "D203",
91
+ "D213",
92
+ "CPY001",
93
+ "FBT001",
94
+ "FBT002",
95
+ "TD002",
96
+ "TD003",
97
+ "FIX002",
98
+ "TRY003",
99
+ "EM101",
100
+ "EM102",
101
+ ]
102
+ fixable = ["ALL"]
103
+ unfixable = []
104
+
105
+ [tool.ruff.lint.per-file-ignores]
106
+ "tests/**/*.py" = [
107
+ "S101",
108
+ "S105",
109
+ "ARG",
110
+ "PLR2004",
111
+ "SLF001",
112
+ "D",
113
+ "ERA001",
114
+ "SIM112",
115
+ "TC002",
116
+ ]
117
+
118
+ [tool.ruff.lint.pydocstyle]
119
+ convention = "google"
120
+
121
+ [tool.ruff.format]
122
+ quote-style = "double"
123
+ indent-style = "space"
124
+ docstring-code-format = true
125
+ docstring-code-line-length = "dynamic"
126
+
127
+ [tool.ty.src]
128
+ include = [
129
+ "src",
130
+ "tests",
131
+ ]
132
+
133
+ [tool.pytest.ini_options]
134
+ minversion = "8.0"
135
+ testpaths = ["tests"]
136
+ addopts = [
137
+ "-ra",
138
+ "--strict-config",
139
+ "--strict-markers",
140
+ ]
141
+ filterwarnings = ["error"]
142
+
143
+ [tool.coverage.run]
144
+ source = ["src"]
145
+ branch = true
146
+
147
+ [tool.coverage.report]
148
+ exclude_lines = [
149
+ "pragma: no cover",
150
+ "if TYPE_CHECKING:",
151
+ "raise NotImplementedError",
152
+ '^\s*\.\.\.$',
153
+ ]