lib-layered-config 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.
Potentially problematic release.
This version of lib-layered-config might be problematic. Click here for more details.
- lib_layered_config/__init__.py +60 -0
- lib_layered_config/__main__.py +19 -0
- lib_layered_config/_layers.py +457 -0
- lib_layered_config/_platform.py +200 -0
- lib_layered_config/adapters/__init__.py +13 -0
- lib_layered_config/adapters/dotenv/__init__.py +1 -0
- lib_layered_config/adapters/dotenv/default.py +438 -0
- lib_layered_config/adapters/env/__init__.py +5 -0
- lib_layered_config/adapters/env/default.py +509 -0
- lib_layered_config/adapters/file_loaders/__init__.py +1 -0
- lib_layered_config/adapters/file_loaders/structured.py +410 -0
- lib_layered_config/adapters/path_resolvers/__init__.py +1 -0
- lib_layered_config/adapters/path_resolvers/default.py +727 -0
- lib_layered_config/application/__init__.py +12 -0
- lib_layered_config/application/merge.py +442 -0
- lib_layered_config/application/ports.py +109 -0
- lib_layered_config/cli/__init__.py +162 -0
- lib_layered_config/cli/common.py +232 -0
- lib_layered_config/cli/constants.py +12 -0
- lib_layered_config/cli/deploy.py +70 -0
- lib_layered_config/cli/fail.py +21 -0
- lib_layered_config/cli/generate.py +60 -0
- lib_layered_config/cli/info.py +31 -0
- lib_layered_config/cli/read.py +117 -0
- lib_layered_config/core.py +384 -0
- lib_layered_config/domain/__init__.py +7 -0
- lib_layered_config/domain/config.py +490 -0
- lib_layered_config/domain/errors.py +65 -0
- lib_layered_config/examples/__init__.py +29 -0
- lib_layered_config/examples/deploy.py +305 -0
- lib_layered_config/examples/generate.py +537 -0
- lib_layered_config/observability.py +306 -0
- lib_layered_config/py.typed +0 -0
- lib_layered_config/testing.py +55 -0
- lib_layered_config-1.0.0.dist-info/METADATA +366 -0
- lib_layered_config-1.0.0.dist-info/RECORD +39 -0
- lib_layered_config-1.0.0.dist-info/WHEEL +4 -0
- lib_layered_config-1.0.0.dist-info/entry_points.txt +3 -0
- lib_layered_config-1.0.0.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
"""Immutable configuration value object with provenance tracking.
|
|
2
|
+
|
|
3
|
+
Purpose
|
|
4
|
+
-------
|
|
5
|
+
Provide the "configuration aggregate" described in
|
|
6
|
+
``docs/systemdesign/concept.md``: an immutable mapping that preserves both the
|
|
7
|
+
final merged values and the metadata explaining *where* every dotted key was
|
|
8
|
+
sourced. The application and adapter layers rely on this module to honour the
|
|
9
|
+
precedence rules documented for layered configuration.
|
|
10
|
+
|
|
11
|
+
Contents
|
|
12
|
+
--------
|
|
13
|
+
- ``SourceInfo``: typed dictionary describing layer, path, and dotted key.
|
|
14
|
+
- ``Config``: frozen mapping-like dataclass exposing lookup, provenance, and
|
|
15
|
+
serialisation helpers.
|
|
16
|
+
- Internal helpers (``_follow_path``, ``_clone_map`` …) that keep traversal
|
|
17
|
+
logic pure and testable.
|
|
18
|
+
- ``EMPTY_CONFIG``: canonical empty instance shared across the composition
|
|
19
|
+
root and CLI utilities.
|
|
20
|
+
|
|
21
|
+
System Role
|
|
22
|
+
-----------
|
|
23
|
+
The composition root builds ``Config`` instances after merging layer snapshots.
|
|
24
|
+
Presentation layers (CLI, examples) consume the public API to render human or
|
|
25
|
+
JSON output without re-implementing provenance rules.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
from collections.abc import Mapping, Mapping as MappingABC
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from types import MappingProxyType
|
|
34
|
+
from typing import Any, Iterable, Iterator, Mapping as MappingType, TypedDict, TypeGuard, TypeVar, cast
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SourceInfo(TypedDict):
|
|
38
|
+
"""Describe the provenance of a configuration value.
|
|
39
|
+
|
|
40
|
+
Why
|
|
41
|
+
----
|
|
42
|
+
Downstream tooling (CLI, deploy helpers) needs to display where a value
|
|
43
|
+
originated so operators can trace precedence decisions.
|
|
44
|
+
|
|
45
|
+
Fields
|
|
46
|
+
------
|
|
47
|
+
layer:
|
|
48
|
+
Name of the logical layer (``"defaults"``, ``"app"``, ``"host"``,
|
|
49
|
+
``"user"``, ``"dotenv"``, or ``"env"``).
|
|
50
|
+
path:
|
|
51
|
+
Absolute filesystem path when known; ``None`` for ephemeral sources
|
|
52
|
+
such as environment variables.
|
|
53
|
+
key:
|
|
54
|
+
Fully-qualified dotted key corresponding to the stored value.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
layer: str
|
|
58
|
+
path: str | None
|
|
59
|
+
key: str
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
T = TypeVar("T")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True, slots=True)
|
|
66
|
+
class Config(MappingABC[str, Any]):
|
|
67
|
+
"""Immutable mapping plus provenance metadata for a merged configuration.
|
|
68
|
+
|
|
69
|
+
Why
|
|
70
|
+
----
|
|
71
|
+
The system design mandates that merged configuration stays read-only after
|
|
72
|
+
assembly so every layer sees a consistent snapshot. ``Config`` enforces that
|
|
73
|
+
contract while providing ergonomic helpers for dotted lookups and
|
|
74
|
+
serialisation.
|
|
75
|
+
|
|
76
|
+
Attributes
|
|
77
|
+
----------
|
|
78
|
+
_data:
|
|
79
|
+
Mapping containing the merged configuration tree. Stored as a
|
|
80
|
+
``MappingProxyType`` to prevent mutation.
|
|
81
|
+
_meta:
|
|
82
|
+
Mapping of dotted keys to :class:`SourceInfo`, allowing provenance
|
|
83
|
+
queries via :meth:`origin`.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
_data: Mapping[str, Any]
|
|
87
|
+
_meta: Mapping[str, SourceInfo]
|
|
88
|
+
|
|
89
|
+
def __post_init__(self) -> None:
|
|
90
|
+
"""Freeze internal mappings immediately after construction."""
|
|
91
|
+
|
|
92
|
+
object.__setattr__(self, "_data", _lock_map(self._data))
|
|
93
|
+
object.__setattr__(self, "_meta", _lock_map(self._meta))
|
|
94
|
+
|
|
95
|
+
def __getitem__(self, key: str) -> Any:
|
|
96
|
+
"""Return the value stored under a top-level key.
|
|
97
|
+
|
|
98
|
+
Why
|
|
99
|
+
----
|
|
100
|
+
Consumers expect ``Config`` to behave like a standard mapping.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
key:
|
|
105
|
+
Top-level key to retrieve.
|
|
106
|
+
|
|
107
|
+
Returns
|
|
108
|
+
-------
|
|
109
|
+
Any
|
|
110
|
+
Stored value.
|
|
111
|
+
|
|
112
|
+
Raises
|
|
113
|
+
------
|
|
114
|
+
KeyError
|
|
115
|
+
When *key* does not exist.
|
|
116
|
+
|
|
117
|
+
Examples
|
|
118
|
+
--------
|
|
119
|
+
>>> cfg = Config({"debug": True}, {"debug": {"layer": "env", "path": None, "key": "debug"}})
|
|
120
|
+
>>> cfg["debug"]
|
|
121
|
+
True
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
return self._data[key]
|
|
125
|
+
|
|
126
|
+
def __iter__(self) -> Iterator[str]:
|
|
127
|
+
"""Iterate over top-level keys in insertion order."""
|
|
128
|
+
|
|
129
|
+
return iter(self._data)
|
|
130
|
+
|
|
131
|
+
def __len__(self) -> int:
|
|
132
|
+
"""Return the number of stored top-level keys."""
|
|
133
|
+
|
|
134
|
+
return len(self._data)
|
|
135
|
+
|
|
136
|
+
def as_dict(self) -> dict[str, Any]:
|
|
137
|
+
"""Return a deep, mutable copy of the configuration tree.
|
|
138
|
+
|
|
139
|
+
Why
|
|
140
|
+
----
|
|
141
|
+
Callers occasionally need to serialise or further mutate the data in a
|
|
142
|
+
context that does not require provenance.
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
dict[str, Any]
|
|
147
|
+
Independent copy of the configuration data.
|
|
148
|
+
|
|
149
|
+
Side Effects
|
|
150
|
+
------------
|
|
151
|
+
None. The original mapping remains locked.
|
|
152
|
+
|
|
153
|
+
Examples
|
|
154
|
+
--------
|
|
155
|
+
>>> cfg = Config({"debug": True}, {"debug": {"layer": "env", "path": None, "key": "debug"}})
|
|
156
|
+
>>> clone = cfg.as_dict()
|
|
157
|
+
>>> clone["debug"]
|
|
158
|
+
True
|
|
159
|
+
>>> clone["debug"] = False
|
|
160
|
+
>>> cfg["debug"]
|
|
161
|
+
True
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
return _clone_map(self._data)
|
|
165
|
+
|
|
166
|
+
def to_json(self, *, indent: int | None = None) -> str:
|
|
167
|
+
"""Serialise the configuration as JSON.
|
|
168
|
+
|
|
169
|
+
Why
|
|
170
|
+
----
|
|
171
|
+
CLI tooling and documentation examples render the merged configuration
|
|
172
|
+
in JSON to support piping into other scripts.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
indent:
|
|
177
|
+
Optional indentation level mirroring ``json.dumps`` semantics.
|
|
178
|
+
|
|
179
|
+
Returns
|
|
180
|
+
-------
|
|
181
|
+
str
|
|
182
|
+
JSON payload containing the cloned configuration data.
|
|
183
|
+
|
|
184
|
+
Examples
|
|
185
|
+
--------
|
|
186
|
+
>>> cfg = Config({"debug": True}, {"debug": {"layer": "env", "path": None, "key": "debug"}})
|
|
187
|
+
>>> cfg.to_json()
|
|
188
|
+
'{"debug":true}'
|
|
189
|
+
>>> "\n \"debug\"" in cfg.to_json(indent=2)
|
|
190
|
+
True
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
return json.dumps(self.as_dict(), indent=indent, separators=(",", ":"), ensure_ascii=False)
|
|
194
|
+
|
|
195
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
196
|
+
"""Return the value for *key* or a default when the path is missing.
|
|
197
|
+
|
|
198
|
+
Why
|
|
199
|
+
----
|
|
200
|
+
Layered configuration relies on dotted keys (e.g. ``"db.host"``).
|
|
201
|
+
This helper avoids repetitive traversal code at call sites.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
key:
|
|
206
|
+
Dotted path identifying nested entries.
|
|
207
|
+
default:
|
|
208
|
+
Value to return when the path does not resolve or encounters a
|
|
209
|
+
non-mapping.
|
|
210
|
+
|
|
211
|
+
Returns
|
|
212
|
+
-------
|
|
213
|
+
Any
|
|
214
|
+
The resolved value or *default* when missing.
|
|
215
|
+
|
|
216
|
+
Examples
|
|
217
|
+
--------
|
|
218
|
+
>>> cfg = Config({"db": {"host": "localhost"}}, {"db.host": {"layer": "app", "path": None, "key": "db.host"}})
|
|
219
|
+
>>> cfg.get("db.host")
|
|
220
|
+
'localhost'
|
|
221
|
+
>>> cfg.get("db.port", default=5432)
|
|
222
|
+
5432
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
return _follow_path(self._data, key, default)
|
|
226
|
+
|
|
227
|
+
def origin(self, key: str) -> SourceInfo | None:
|
|
228
|
+
"""Return provenance metadata for *key* when available.
|
|
229
|
+
|
|
230
|
+
Why
|
|
231
|
+
----
|
|
232
|
+
Operators need to understand which layer supplied a value to debug
|
|
233
|
+
precedence questions.
|
|
234
|
+
|
|
235
|
+
Parameters
|
|
236
|
+
----------
|
|
237
|
+
key:
|
|
238
|
+
Dotted key in the metadata map.
|
|
239
|
+
|
|
240
|
+
Returns
|
|
241
|
+
-------
|
|
242
|
+
SourceInfo | None
|
|
243
|
+
Metadata dictionary or ``None`` if the key was never observed.
|
|
244
|
+
|
|
245
|
+
Examples
|
|
246
|
+
--------
|
|
247
|
+
>>> meta = {"db.host": {"layer": "app", "path": "/etc/app.toml", "key": "db.host"}}
|
|
248
|
+
>>> cfg = Config({"db": {"host": "localhost"}}, meta)
|
|
249
|
+
>>> cfg.origin("db.host")["layer"]
|
|
250
|
+
'app'
|
|
251
|
+
>>> cfg.origin("missing") is None
|
|
252
|
+
True
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
return self._meta.get(key)
|
|
256
|
+
|
|
257
|
+
def with_overrides(self, overrides: Mapping[str, Any]) -> Config:
|
|
258
|
+
"""Return a new configuration with shallow top-level overrides applied.
|
|
259
|
+
|
|
260
|
+
Why
|
|
261
|
+
----
|
|
262
|
+
CLI helpers allow callers to inject ad-hoc overrides while keeping the
|
|
263
|
+
original snapshot intact. This method produces that variant.
|
|
264
|
+
|
|
265
|
+
Parameters
|
|
266
|
+
----------
|
|
267
|
+
overrides:
|
|
268
|
+
Top-level keys and values to override.
|
|
269
|
+
|
|
270
|
+
Returns
|
|
271
|
+
-------
|
|
272
|
+
Config
|
|
273
|
+
New configuration instance sharing provenance with the original.
|
|
274
|
+
|
|
275
|
+
Side Effects
|
|
276
|
+
------------
|
|
277
|
+
None. Both instances remain independent thanks to cloning.
|
|
278
|
+
|
|
279
|
+
Examples
|
|
280
|
+
--------
|
|
281
|
+
>>> cfg = Config({"feature": False}, {"feature": {"layer": "app", "path": None, "key": "feature"}})
|
|
282
|
+
>>> cfg.with_overrides({"feature": True})["feature"], cfg["feature"]
|
|
283
|
+
(True, False)
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
tinted = _blend_top_level(self._data, overrides)
|
|
287
|
+
return Config(tinted, self._meta)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _lock_map(mapping: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
291
|
+
"""Return a read-only view of *mapping*.
|
|
292
|
+
|
|
293
|
+
Why
|
|
294
|
+
----
|
|
295
|
+
Internal state must remain immutable to uphold the domain contract.
|
|
296
|
+
|
|
297
|
+
Parameters
|
|
298
|
+
----------
|
|
299
|
+
mapping:
|
|
300
|
+
Mapping to wrap. A shallow copy protects against caller mutation.
|
|
301
|
+
|
|
302
|
+
Returns
|
|
303
|
+
-------
|
|
304
|
+
Mapping[str, Any]
|
|
305
|
+
``MappingProxyType`` over a copy of the source mapping.
|
|
306
|
+
|
|
307
|
+
Examples
|
|
308
|
+
--------
|
|
309
|
+
>>> view = _lock_map({"flag": True})
|
|
310
|
+
>>> view["flag"], isinstance(view, MappingProxyType)
|
|
311
|
+
(True, True)
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
return MappingProxyType(dict(mapping))
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _blend_top_level(base: Mapping[str, Any], overrides: Mapping[str, Any]) -> dict[str, Any]:
|
|
318
|
+
"""Return a shallow copy of *base* with *overrides* applied.
|
|
319
|
+
|
|
320
|
+
Why
|
|
321
|
+
----
|
|
322
|
+
``Config.with_overrides`` depends on a pure helper so it can reuse
|
|
323
|
+
provenance metadata without mutation.
|
|
324
|
+
|
|
325
|
+
Parameters
|
|
326
|
+
----------
|
|
327
|
+
base:
|
|
328
|
+
Original mapping.
|
|
329
|
+
overrides:
|
|
330
|
+
Mapping whose keys replace entries in *base*.
|
|
331
|
+
|
|
332
|
+
Returns
|
|
333
|
+
-------
|
|
334
|
+
dict[str, Any]
|
|
335
|
+
New dictionary with updated top-level values.
|
|
336
|
+
|
|
337
|
+
Examples
|
|
338
|
+
--------
|
|
339
|
+
>>> _blend_top_level({"port": 8000}, {"port": 9000})["port"]
|
|
340
|
+
9000
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
tinted = dict(base)
|
|
344
|
+
tinted.update(overrides)
|
|
345
|
+
return tinted
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _follow_path(source: Mapping[str, Any], dotted: str, default: Any) -> Any:
|
|
349
|
+
"""Traverse *source* using dotted notation.
|
|
350
|
+
|
|
351
|
+
Why
|
|
352
|
+
----
|
|
353
|
+
Nested configuration should be accessible without exposing internal data
|
|
354
|
+
structures. This helper powers :meth:`Config.get`.
|
|
355
|
+
|
|
356
|
+
Parameters
|
|
357
|
+
----------
|
|
358
|
+
source:
|
|
359
|
+
Mapping to traverse.
|
|
360
|
+
dotted:
|
|
361
|
+
Dotted path, e.g. ``"db.host"``.
|
|
362
|
+
default:
|
|
363
|
+
Fallback when traversal fails.
|
|
364
|
+
|
|
365
|
+
Returns
|
|
366
|
+
-------
|
|
367
|
+
Any
|
|
368
|
+
Resolved value or *default*.
|
|
369
|
+
|
|
370
|
+
Examples
|
|
371
|
+
--------
|
|
372
|
+
>>> payload = {"db": {"host": "localhost"}}
|
|
373
|
+
>>> _follow_path(payload, "db.host", default=None)
|
|
374
|
+
'localhost'
|
|
375
|
+
>>> _follow_path(payload, "db.port", default=5432)
|
|
376
|
+
5432
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
current: object = source
|
|
380
|
+
for fragment in dotted.split("."):
|
|
381
|
+
if not _looks_like_mapping(current):
|
|
382
|
+
return default
|
|
383
|
+
if fragment not in current:
|
|
384
|
+
return default
|
|
385
|
+
current = current[fragment]
|
|
386
|
+
return cast(Any, current)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _clone_map(mapping: MappingType[str, Any]) -> dict[str, Any]:
|
|
390
|
+
"""Deep-clone *mapping* while preserving container types.
|
|
391
|
+
|
|
392
|
+
Why
|
|
393
|
+
----
|
|
394
|
+
``Config.as_dict`` and JSON serialisation must not leak references to the
|
|
395
|
+
internal immutable structures.
|
|
396
|
+
|
|
397
|
+
Parameters
|
|
398
|
+
----------
|
|
399
|
+
mapping:
|
|
400
|
+
Mapping to clone.
|
|
401
|
+
|
|
402
|
+
Returns
|
|
403
|
+
-------
|
|
404
|
+
dict[str, Any]
|
|
405
|
+
Deep copy containing cloned containers and scalar values.
|
|
406
|
+
|
|
407
|
+
Examples
|
|
408
|
+
--------
|
|
409
|
+
>>> original = {"levels": (1, 2), "queue": [1, 2]}
|
|
410
|
+
>>> cloned = _clone_map(original)
|
|
411
|
+
>>> cloned["levels"], cloned["queue"]
|
|
412
|
+
((1, 2), [1, 2])
|
|
413
|
+
>>> cloned["queue"].append(3)
|
|
414
|
+
>>> original["queue"]
|
|
415
|
+
[1, 2]
|
|
416
|
+
"""
|
|
417
|
+
|
|
418
|
+
sculpted: dict[str, Any] = {}
|
|
419
|
+
for key, value in mapping.items():
|
|
420
|
+
sculpted[key] = _clone_value(value)
|
|
421
|
+
return sculpted
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _clone_value(value: Any) -> Any:
|
|
425
|
+
"""Return a clone of *value*, respecting the container type.
|
|
426
|
+
|
|
427
|
+
Why
|
|
428
|
+
----
|
|
429
|
+
``_clone_map`` delegates element cloning here so complex structures (lists,
|
|
430
|
+
sets, tuples, nested mappings) remain detached from the immutable source.
|
|
431
|
+
|
|
432
|
+
Examples
|
|
433
|
+
--------
|
|
434
|
+
>>> cloned = _clone_value(({"flag": True},))
|
|
435
|
+
>>> cloned
|
|
436
|
+
({'flag': True},)
|
|
437
|
+
>>> cloned is _clone_value(({"flag": True},))
|
|
438
|
+
False
|
|
439
|
+
"""
|
|
440
|
+
|
|
441
|
+
if isinstance(value, MappingABC):
|
|
442
|
+
nested = cast(MappingType[str, Any], value)
|
|
443
|
+
return _clone_map(nested)
|
|
444
|
+
if isinstance(value, list):
|
|
445
|
+
items = cast(list[Any], value)
|
|
446
|
+
return [_clone_value(item) for item in items]
|
|
447
|
+
if isinstance(value, set):
|
|
448
|
+
items = cast(set[Any], value)
|
|
449
|
+
return {_clone_value(item) for item in items}
|
|
450
|
+
if isinstance(value, tuple):
|
|
451
|
+
items = cast(tuple[Any, ...], value)
|
|
452
|
+
return tuple(_clone_value(item) for item in items)
|
|
453
|
+
return value
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _looks_like_mapping(value: object) -> TypeGuard[MappingType[str, Any]]:
|
|
457
|
+
"""Return ``True`` when *value* is a mapping with string keys.
|
|
458
|
+
|
|
459
|
+
Why
|
|
460
|
+
----
|
|
461
|
+
Dotted traversal should stop when encountering scalars or non-string-keyed
|
|
462
|
+
mappings to avoid surprising behaviour.
|
|
463
|
+
|
|
464
|
+
Examples
|
|
465
|
+
--------
|
|
466
|
+
>>> _looks_like_mapping({"key": 1})
|
|
467
|
+
True
|
|
468
|
+
>>> _looks_like_mapping({1: "value"})
|
|
469
|
+
False
|
|
470
|
+
>>> _looks_like_mapping(["not", "mapping"])
|
|
471
|
+
False
|
|
472
|
+
"""
|
|
473
|
+
|
|
474
|
+
if not isinstance(value, MappingABC):
|
|
475
|
+
return False
|
|
476
|
+
for key in cast(Iterable[object], value.keys()):
|
|
477
|
+
if not isinstance(key, str):
|
|
478
|
+
return False
|
|
479
|
+
return True
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
EMPTY_CONFIG = Config(MappingProxyType({}), MappingProxyType({}))
|
|
483
|
+
"""Shared empty configuration used by the composition root and CLI helpers.
|
|
484
|
+
|
|
485
|
+
Why
|
|
486
|
+
---
|
|
487
|
+
Avoids repeated allocations when no layers contribute values. The empty
|
|
488
|
+
instance satisfies the domain contract (immutability, provenance available but
|
|
489
|
+
empty) and is safe to reuse across contexts.
|
|
490
|
+
"""
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Domain error taxonomy shared across layers.
|
|
2
|
+
|
|
3
|
+
Purpose
|
|
4
|
+
-------
|
|
5
|
+
codifies the error classes referenced throughout ``docs/systemdesign`` so the
|
|
6
|
+
application and adapter layers can communicate failures without depending on
|
|
7
|
+
concrete implementations.
|
|
8
|
+
|
|
9
|
+
Contents
|
|
10
|
+
--------
|
|
11
|
+
- ``ConfigError``: base class for every library-specific exception.
|
|
12
|
+
- ``InvalidFormat``: raised when structured configuration cannot be parsed.
|
|
13
|
+
- ``ValidationError``: reserved for semantic validation of configuration
|
|
14
|
+
payloads once implemented.
|
|
15
|
+
- ``NotFound``: indicates optional configuration sources were absent.
|
|
16
|
+
|
|
17
|
+
System Role
|
|
18
|
+
-----------
|
|
19
|
+
Adapters raise these exceptions; the composition root and CLI translate them
|
|
20
|
+
into operator-facing messages without leaking implementation details.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"ConfigError",
|
|
27
|
+
"InvalidFormat",
|
|
28
|
+
"ValidationError",
|
|
29
|
+
"NotFound",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ConfigError(Exception):
|
|
34
|
+
"""Base class for all configuration-related errors in the library.
|
|
35
|
+
|
|
36
|
+
Why
|
|
37
|
+
----
|
|
38
|
+
Centralises exception handling so callers can catch a single type when
|
|
39
|
+
operating at library boundaries.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class InvalidFormat(ConfigError):
|
|
44
|
+
"""Raised when a configuration source cannot be parsed.
|
|
45
|
+
|
|
46
|
+
Typical sources include malformed TOML, JSON, YAML, or dotenv files. The
|
|
47
|
+
message should reference the offending path for operator debugging.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ValidationError(ConfigError):
|
|
52
|
+
"""Placeholder for semantic configuration validation failures.
|
|
53
|
+
|
|
54
|
+
The current release does not perform semantic validation, but the class is
|
|
55
|
+
reserved so downstream integrations already depend on a stable type.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class NotFound(ConfigError):
|
|
60
|
+
"""Indicates an optional configuration source was not discovered.
|
|
61
|
+
|
|
62
|
+
Used when files, directory entries, or environment variable namespaces are
|
|
63
|
+
genuinely missing; callers generally treat this as informational rather than
|
|
64
|
+
fatal.
|
|
65
|
+
"""
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Expose example-generation and deployment helpers as a tidy façade.
|
|
2
|
+
|
|
3
|
+
Purpose
|
|
4
|
+
Provide a single import point for notebooks and docs that showcase layered
|
|
5
|
+
configuration scenarios. Keeps consumers away from internal module layout.
|
|
6
|
+
|
|
7
|
+
Contents
|
|
8
|
+
- :func:`deploy_config`: copy template files into etc/xdg directories.
|
|
9
|
+
- :class:`ExampleSpec`: describes example assets to generate.
|
|
10
|
+
- :data:`DEFAULT_HOST_PLACEHOLDER`: default hostname marker for templates.
|
|
11
|
+
- :func:`generate_examples`: materialise example configs on disk.
|
|
12
|
+
|
|
13
|
+
System Integration
|
|
14
|
+
Re-exports live in the ``examples`` namespace so tutorials can call
|
|
15
|
+
``lib_layered_config.examples.generate_examples`` without traversing the
|
|
16
|
+
package internals.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from .deploy import deploy_config
|
|
22
|
+
from .generate import DEFAULT_HOST_PLACEHOLDER, ExampleSpec, generate_examples
|
|
23
|
+
|
|
24
|
+
__all__ = (
|
|
25
|
+
"deploy_config",
|
|
26
|
+
"ExampleSpec",
|
|
27
|
+
"DEFAULT_HOST_PLACEHOLDER",
|
|
28
|
+
"generate_examples",
|
|
29
|
+
)
|