msgspec-config 0.1.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.
Files changed (38) hide show
  1. msgspec_config-0.1.0/LICENSE +21 -0
  2. msgspec_config-0.1.0/MANIFEST.in +1 -0
  3. msgspec_config-0.1.0/PKG-INFO +404 -0
  4. msgspec_config-0.1.0/README.md +379 -0
  5. msgspec_config-0.1.0/docs/assets/msgspec-config-logo.svg +54 -0
  6. msgspec_config-0.1.0/docs/index.html +7 -0
  7. msgspec_config-0.1.0/docs/msgspec_config.html +4187 -0
  8. msgspec_config-0.1.0/msgspec_config/__init__.py +26 -0
  9. msgspec_config-0.1.0/msgspec_config/base.py +612 -0
  10. msgspec_config-0.1.0/msgspec_config/fields.py +357 -0
  11. msgspec_config-0.1.0/msgspec_config/mapping.py +466 -0
  12. msgspec_config-0.1.0/msgspec_config/merge.py +76 -0
  13. msgspec_config-0.1.0/msgspec_config/sources/__init__.py +17 -0
  14. msgspec_config-0.1.0/msgspec_config/sources/api.py +77 -0
  15. msgspec_config-0.1.0/msgspec_config/sources/cli.py +492 -0
  16. msgspec_config-0.1.0/msgspec_config/sources/dotenv.py +205 -0
  17. msgspec_config-0.1.0/msgspec_config/sources/env.py +70 -0
  18. msgspec_config-0.1.0/msgspec_config/sources/json.py +68 -0
  19. msgspec_config-0.1.0/msgspec_config/sources/toml.py +57 -0
  20. msgspec_config-0.1.0/msgspec_config/sources/yaml.py +56 -0
  21. msgspec_config-0.1.0/msgspec_config/typing.py +224 -0
  22. msgspec_config-0.1.0/msgspec_config.egg-info/PKG-INFO +404 -0
  23. msgspec_config-0.1.0/msgspec_config.egg-info/SOURCES.txt +36 -0
  24. msgspec_config-0.1.0/msgspec_config.egg-info/dependency_links.txt +1 -0
  25. msgspec_config-0.1.0/msgspec_config.egg-info/requires.txt +4 -0
  26. msgspec_config-0.1.0/msgspec_config.egg-info/top_level.txt +1 -0
  27. msgspec_config-0.1.0/pyproject.toml +64 -0
  28. msgspec_config-0.1.0/setup.cfg +4 -0
  29. msgspec_config-0.1.0/tests/test_base.py +198 -0
  30. msgspec_config-0.1.0/tests/test_fields.py +299 -0
  31. msgspec_config-0.1.0/tests/test_functions.py +194 -0
  32. msgspec_config-0.1.0/tests/test_sources_api.py +179 -0
  33. msgspec_config-0.1.0/tests/test_sources_cli.py +208 -0
  34. msgspec_config-0.1.0/tests/test_sources_dotenv.py +192 -0
  35. msgspec_config-0.1.0/tests/test_sources_env.py +164 -0
  36. msgspec_config-0.1.0/tests/test_sources_json.py +99 -0
  37. msgspec_config-0.1.0/tests/test_sources_toml.py +77 -0
  38. msgspec_config-0.1.0/tests/test_sources_yaml.py +77 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Max Pareschi
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 @@
1
+ recursive-include docs *.svg *.html
@@ -0,0 +1,404 @@
1
+ Metadata-Version: 2.4
2
+ Name: msgspec-config
3
+ Version: 0.1.0
4
+ Summary: A Settings library using msgspec as a backend for validation and serialization.
5
+ Author-email: Max Pareschi <max.pareschi@gmail.com>
6
+ Project-URL: Homepage, https://github.com/maxpareschi/msgspec-config
7
+ Project-URL: Issues, https://github.com/maxpareschi/msgspec-config/issues
8
+ Project-URL: Docs, https://maxpareschi.github.io/msgspec-config
9
+ Keywords: Settings,CLI,Configuration,Validation,Serialization,msgspec
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Requires-Python: >=3.13
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: msgspec>=0.20.0
21
+ Requires-Dist: pyyaml>=6.0.3
22
+ Requires-Dist: rich>=14.3.1
23
+ Requires-Dist: rich-click>=1.9.6
24
+ Dynamic: license-file
25
+
26
+
27
+ <p align="center">
28
+ <img src="docs/assets/msgspec-config-logo.svg" width="35%" alt="msgspec-config">
29
+ </p>
30
+
31
+ # msgspec-config
32
+
33
+ Typed, multi-source configuration loading on top of `msgspec`.
34
+
35
+ `msgspec-config` is for applications that need:
36
+ - one typed model for configuration shape
37
+ - multiple config inputs (files, `.env`, environment, CLI, custom providers)
38
+ - deterministic precedence across all inputs
39
+ - strict validation/coercion without writing parsing glue
40
+
41
+ The core idea is simple: define one `DataModel`, attach ordered `DataSource`s, and instantiate the model.
42
+
43
+ ## API Docs
44
+ Please visit the API docs at this project's github pages site:
45
+ <a href="https://maxpareschi.github.io/msgspec-config">https://maxpareschi.github.io/msgspec-config/</a>
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ pip install msgspec-config
51
+ ```
52
+
53
+ ```bash
54
+ uv add msgspec-config
55
+ ```
56
+
57
+ Tested on `Python>=3.13`, probably works also on `Python>=3.11`.
58
+
59
+ ## Quick Start (Layered Config)
60
+
61
+ `config.toml`:
62
+
63
+ ```toml
64
+ host = "toml-host"
65
+ port = 7000
66
+ [log]
67
+ level = "INFO"
68
+ ```
69
+
70
+ `.env`:
71
+
72
+ ```dotenv
73
+ APP_PORT=7500
74
+ APP_LOG_LEVEL=DEBUG
75
+ ```
76
+
77
+ ```python
78
+ from msgspec_config import (
79
+ APISource,
80
+ CliSource,
81
+ DataModel,
82
+ DotEnvSource,
83
+ EnvironSource,
84
+ JSONSource,
85
+ TomlSource,
86
+ datasources,
87
+ entry,
88
+ group,
89
+ )
90
+
91
+
92
+ class LogConfig(DataModel):
93
+ level: str = "WARN"
94
+ file_path: str = "/var/log/app.log"
95
+
96
+
97
+ @datasources(
98
+ TomlSource(toml_path="config.toml"),
99
+ DotEnvSource(dotenv_path=".env", env_prefix="APP"),
100
+ EnvironSource(env_prefix="APP"),
101
+ CliSource(),
102
+ )
103
+ class AppConfig(DataModel):
104
+ host: str = entry("127.0.0.1", min_length=1)
105
+ port: int = entry(8080, ge=1, le=65535)
106
+ debug: bool = False
107
+ log: LogConfig = group(collapsed=True)
108
+
109
+
110
+ cfg = AppConfig(port=9000)
111
+ print(cfg.model_dump_json(indent=2))
112
+ ```
113
+
114
+ Precedence is deterministic and intentional:
115
+
116
+ ```
117
+ defaults < source_1 < source_2 < ... < source_n < kwargs
118
+ ```
119
+
120
+ With the example above:
121
+ - model defaults are the baseline
122
+ - `TomlSource` overrides defaults
123
+ - `DotEnvSource` overrides TOML
124
+ - `EnvironSource` overrides `.env`
125
+ - `CliSource` overrides environment values
126
+ - constructor kwargs (`AppConfig(port=9000)`) win last
127
+
128
+ Rationale: this gives safe defaults in code, then progressive override points for deploy/runtime, while still keeping a final explicit override path in Python.
129
+
130
+ Important:
131
+ - `env_prefix` is mandatory for both `EnvironSource` and `DotEnvSource`.
132
+ - Empty/blank prefixes raise `ValueError`.
133
+
134
+ ## Field Helpers (`entry` and `group`)
135
+
136
+ ### `entry(...)`
137
+
138
+ Use `entry(...)` when you need validation metadata and/or safe mutable defaults.
139
+
140
+ Why it exists:
141
+ - attaches `msgspec.Meta(...)` constraints directly from field declaration
142
+ - converts mutable defaults (`list`, `dict`, `set`) into factories automatically
143
+ - supports extra UI/schema keys: `hidden_if`, `disabled_if`, `parent_group`, `ui_component`
144
+
145
+ ```python
146
+ from msgspec_config import DataModel, entry
147
+
148
+
149
+ class ApiConfig(DataModel):
150
+ timeout_seconds: int = entry(30, ge=1, le=120, description="Request timeout")
151
+ tags: list[str] = entry([], description="Dynamic tags")
152
+ ```
153
+
154
+ ### `group(...)`
155
+
156
+ Use `group(...)` for nested object/list/dict fields inferred from annotations.
157
+
158
+ Why it exists:
159
+ - creates safe defaults for nested structures without shared state
160
+ - adds optional UI/schema hints (`collapsed`, `mutable`)
161
+
162
+ ```python
163
+ from msgspec_config import DataModel, group
164
+
165
+
166
+ class Child(DataModel):
167
+ value: int = 1
168
+
169
+
170
+ class Parent(DataModel):
171
+ child: Child = group(collapsed=True)
172
+ children: list[Child] = group(mutable=True)
173
+ by_name: dict[str, Child] = group(mutable=True)
174
+ ```
175
+
176
+ Notes:
177
+ - object annotations used with `group()` must be zero-arg constructible
178
+ - `group()` is for object/list/dict-like fields, not primitive scalars
179
+
180
+ ## Built-in Sources (Behavior)
181
+
182
+ All built-ins are importable from both `msgspec_config` and `msgspec_config.sources`.
183
+
184
+ When a source is used with `resolve(model=...)` (or through `@datasources(...)` on a
185
+ `DataModel`), field resolution accepts both canonical and encoded/alias names, and mapped
186
+ output keys are emitted using encoded names.
187
+
188
+ ### `TomlSource` and `YamlSource`
189
+
190
+ - load mappings from files using `msgspec.toml.decode` / `msgspec.yaml.decode`
191
+ - if path is unset or missing, they return `{}` (treated as "source absent")
192
+ - parse/read failures raise `RuntimeError` with file context
193
+
194
+ ### `JSONSource`
195
+
196
+ - decodes inline JSON (`json_data`) or loads JSON from `json_path`
197
+ - if both are set, `json_data` takes precedence
198
+ - if path is unset/missing, returns `{}`
199
+ - parse/read failures raise `RuntimeError` with context
200
+
201
+ ### `DotEnvSource`
202
+
203
+ - parses dotenv syntax (`export`, quotes, inline comments)
204
+ - requires non-empty `env_prefix` (prefix scoping is mandatory)
205
+ - nested keys are mapped with `nested_separator` (default `_`)
206
+ - with a `model`, values are coerced to field types
207
+ - recognized keys that fail coercion are captured in source `__unmapped_kwargs__`
208
+
209
+ Example precedence inside one source:
210
+
211
+ ```dotenv
212
+ APP_LOG={"level":"DEBUG"}
213
+ APP_LOG_LEVEL=WARN
214
+ ```
215
+
216
+ `APP_LOG_LEVEL` overrides `APP_LOG.level`, regardless of line order.
217
+
218
+ ### `EnvironSource`
219
+
220
+ Same mapping/coercion behavior as `DotEnvSource`, but reads from `os.environ`.
221
+ `env_prefix` is mandatory, and failed coercions/unmatched keys are captured in
222
+ source `__unmapped_kwargs__`.
223
+
224
+ ```python
225
+ EnvironSource(env_prefix="APP", nested_separator="__")
226
+ # APP_LOG__LEVEL=ERROR -> {"log": {"level": "ERROR"}}
227
+ ```
228
+
229
+ ### `CliSource`
230
+
231
+ Generates options from model fields (including nested fields).
232
+
233
+ Key behavior:
234
+ - nested fields become flags like `--log-level`
235
+ - bools support both positive and negative forms: `--debug` / `--no-debug`
236
+ - nested struct fields also accept JSON on the top-level flag:
237
+ - `--log '{"level":"DEBUG"}'`
238
+ - explicit nested flags override keys from that JSON
239
+ - unknown CLI args are stored on source runtime state in `__unmapped_kwargs__`
240
+ - set `kebab_case=False` to use dotted long flags (e.g. `--log.level`)
241
+ - CLI accepts canonical and encoded/alias field names, and maps parsed values to encoded field names
242
+
243
+ ```python
244
+ src = CliSource(cli_args=["--host", "api", "--unknown-flag"])
245
+ data = src.resolve(model=AppConfig)
246
+ print(data) # {"host": "api"}
247
+ print(src.__unmapped_kwargs__) # {"unknown-flag": True}
248
+ ```
249
+
250
+ ### `APISource`
251
+
252
+ - performs an HTTP `GET` request against `api_url`
253
+ - optional auth header via `header_name` + `header_value`
254
+ - optional `root_node` to unwrap wrapped payloads (for example `{"data": {...}}`)
255
+ - request or parse failures raise `RuntimeError` with endpoint context
256
+
257
+ ```python
258
+ src = APISource(
259
+ api_url="https://example.com/config",
260
+ header_name="Authorization",
261
+ header_value="Bearer <token>",
262
+ root_node="data",
263
+ )
264
+ data = src.resolve()
265
+ ```
266
+
267
+ ## Custom Source Example
268
+
269
+ When built-ins are not enough, implement `DataSource.load(...)`.
270
+
271
+ ```python
272
+ from typing import Any
273
+
274
+ from msgspec_config import DataModel, DataSource, datasources
275
+
276
+
277
+ class SecretsSource(DataSource):
278
+ def load(self, model: type[DataModel] | None = None) -> dict[str, Any]:
279
+ # Replace this with Vault/AWS/GCP/etc.
280
+ return {"host": "secrets-host", "port": 8443}
281
+
282
+
283
+ @datasources(SecretsSource())
284
+ class ServiceConfig(DataModel):
285
+ host: str = "localhost"
286
+ port: int = 8080
287
+ ```
288
+
289
+ Rationale: sources are deep-cloned per model instantiation, so source-local mutable state does not leak across `DataModel()` calls. `DataSource.resolve(...)` is the public finalized loader (reset + finalize); custom sources should override `load(...)`.
290
+
291
+ ## Limitations
292
+
293
+ - Do not shadow `DataModel`/`DataSource` method names with fields; this is user responsibility and can break runtime behavior.
294
+
295
+ ## DataModel Helpers
296
+
297
+ `DataModel` is a `msgspec.Struct` configured as keyword-only and with dict-like output support.
298
+
299
+ Useful methods:
300
+ - `from_data(data)` to create an instance from a Python mapping
301
+ - `from_json(json_str)` to create an instance from JSON bytes/string
302
+ - `model_dump()` to get the model converted in Python builtins
303
+ - `model_dump_json(indent=...)` for JSON output
304
+ - `model_json_schema(indent=...)` for JSON Schema export
305
+ - `get_datasources_payload(*sources, **kwargs)` to retrieve merged source payloads manually
306
+ - `get_unmapped_payload()` to lazily merge source runtime `__unmapped_kwargs__` in source order plus unknown constructor kwargs (merged last)
307
+
308
+ Notes:
309
+ - `from_data(...)` and `from_json(...)` ignore unknown keys.
310
+ - Unknown keyword arguments passed to `DataModel(...)` are available through
311
+ `get_unmapped_payload()`.
312
+
313
+ Example:
314
+
315
+ ```python
316
+ cfg = AppConfig.from_json('{"host":"example.com","port":8081}')
317
+ print(cfg.model_dump())
318
+ print(AppConfig.model_json_schema(indent=2))
319
+ ```
320
+
321
+ ## API Summary
322
+
323
+ - `DataModel`: typed model base class with validation/serialization helpers
324
+ - `DataSource`: source base class (`load(model=...) -> raw mapping`, `resolve(model=...) -> finalized mapping`)
325
+ - `datasources(*sources)`: decorator that attaches ordered source templates
326
+ - `entry(...)`: field helper with validation metadata and safe mutable defaults
327
+ - `group(...)`: helper for grouped object/list/dict fields
328
+ - built-ins: `TomlSource`, `YamlSource`, `JSONSource`, `DotEnvSource`, `EnvironSource`, `CliSource`, `APISource`
329
+
330
+ ## Development (Makefile + Commands)
331
+
332
+ The repository includes a `Makefile` to standardize common local tasks. Run targets from the project root.
333
+
334
+ Prerequisites:
335
+ - `uv`
336
+ - GNU Make (`make`)
337
+ - on Windows, use a GNU Make provider (for example Git Bash `make` or `mingw32-make`)
338
+
339
+ Typical workflow:
340
+
341
+ ```bash
342
+ make venv # install/update dependencies from lockfile
343
+ make ruff # format + lint autofix
344
+ make test # run tests
345
+ make docs # regenerate docs in ./docs
346
+ ```
347
+
348
+ Run the full local pipeline:
349
+
350
+ ```bash
351
+ make all
352
+ ```
353
+
354
+ `all` expands to:
355
+
356
+ ```text
357
+ venv -> ruff -> test -> docs
358
+ ```
359
+
360
+ Makefile targets:
361
+ - `make venv`: `uv sync`
362
+ - `make docs`: `uv run pdoc -o ./docs --docformat google --favicon assets/msgspec-config-logo.svg --logo assets/msgspec-config-logo.svg --search -t ./docs --show-source msgspec_config`
363
+ - `make ruff`: `uv run ruff format .` and `uv run ruff check --fix .`
364
+ - `make test`: `uv run pytest`
365
+ - `make build`: `uv build --clear --no-sources`
366
+ - `make publish-testpypi`: runs `make build`, then `uv publish --index testpypi`
367
+ - `make publish-pypi`: runs `make build`, then `uv publish`
368
+
369
+ Equivalent direct commands (without `make`):
370
+
371
+ ```bash
372
+ uv sync
373
+ uv run ruff format .
374
+ uv run ruff check --fix .
375
+ uv run pytest
376
+ uv run pdoc -o ./docs --docformat google --favicon assets/msgspec-config-logo.svg --logo assets/msgspec-config-logo.svg --search -t ./docs --show-source msgspec_config
377
+ uv build --clear --no-sources
378
+ ```
379
+
380
+ ## Release (uv)
381
+
382
+ Build clean artifacts:
383
+
384
+ ```bash
385
+ make build
386
+ ```
387
+
388
+ Publish to TestPyPI first:
389
+
390
+ ```bash
391
+ $env:UV_PUBLISH_TOKEN="pypi-<testpypi-token>"
392
+ make publish-testpypi
393
+ ```
394
+
395
+ Publish to PyPI:
396
+
397
+ ```bash
398
+ $env:UV_PUBLISH_TOKEN="pypi-<pypi-token>"
399
+ make publish-pypi
400
+ ```
401
+
402
+ Packaging policy:
403
+ - wheel: runtime package only (`msgspec_config`)
404
+ - sdist: includes source, tests, and docs metadata for downstream builds/tests