lib-layered-config 4.1.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.
Files changed (47) hide show
  1. lib_layered_config/__init__.py +58 -0
  2. lib_layered_config/__init__conf__.py +74 -0
  3. lib_layered_config/__main__.py +18 -0
  4. lib_layered_config/_layers.py +310 -0
  5. lib_layered_config/_platform.py +166 -0
  6. lib_layered_config/adapters/__init__.py +13 -0
  7. lib_layered_config/adapters/_nested_keys.py +126 -0
  8. lib_layered_config/adapters/dotenv/__init__.py +1 -0
  9. lib_layered_config/adapters/dotenv/default.py +143 -0
  10. lib_layered_config/adapters/env/__init__.py +5 -0
  11. lib_layered_config/adapters/env/default.py +288 -0
  12. lib_layered_config/adapters/file_loaders/__init__.py +1 -0
  13. lib_layered_config/adapters/file_loaders/structured.py +376 -0
  14. lib_layered_config/adapters/path_resolvers/__init__.py +28 -0
  15. lib_layered_config/adapters/path_resolvers/_base.py +166 -0
  16. lib_layered_config/adapters/path_resolvers/_dotenv.py +74 -0
  17. lib_layered_config/adapters/path_resolvers/_linux.py +89 -0
  18. lib_layered_config/adapters/path_resolvers/_macos.py +93 -0
  19. lib_layered_config/adapters/path_resolvers/_windows.py +126 -0
  20. lib_layered_config/adapters/path_resolvers/default.py +194 -0
  21. lib_layered_config/application/__init__.py +12 -0
  22. lib_layered_config/application/merge.py +379 -0
  23. lib_layered_config/application/ports.py +115 -0
  24. lib_layered_config/cli/__init__.py +92 -0
  25. lib_layered_config/cli/common.py +381 -0
  26. lib_layered_config/cli/constants.py +12 -0
  27. lib_layered_config/cli/deploy.py +71 -0
  28. lib_layered_config/cli/fail.py +19 -0
  29. lib_layered_config/cli/generate.py +57 -0
  30. lib_layered_config/cli/info.py +29 -0
  31. lib_layered_config/cli/read.py +120 -0
  32. lib_layered_config/core.py +301 -0
  33. lib_layered_config/domain/__init__.py +7 -0
  34. lib_layered_config/domain/config.py +372 -0
  35. lib_layered_config/domain/errors.py +59 -0
  36. lib_layered_config/domain/identifiers.py +366 -0
  37. lib_layered_config/examples/__init__.py +29 -0
  38. lib_layered_config/examples/deploy.py +333 -0
  39. lib_layered_config/examples/generate.py +406 -0
  40. lib_layered_config/observability.py +209 -0
  41. lib_layered_config/py.typed +0 -0
  42. lib_layered_config/testing.py +46 -0
  43. lib_layered_config-4.1.0.dist-info/METADATA +3263 -0
  44. lib_layered_config-4.1.0.dist-info/RECORD +47 -0
  45. lib_layered_config-4.1.0.dist-info/WHEEL +4 -0
  46. lib_layered_config-4.1.0.dist-info/entry_points.txt +3 -0
  47. lib_layered_config-4.1.0.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,379 @@
1
+ """Merge ordered configuration layers while keeping provenance crystal clear.
2
+
3
+ Implement the merge policy described in ``docs/systemdesign/concept.md`` by
4
+ folding a sequence of layer snapshots into a single mapping plus provenance.
5
+ Preserves the "last writer wins" rule without mutating caller-provided data.
6
+
7
+ Contents:
8
+ - ``LayerSnapshot``: immutable record describing a layer name, payload, and
9
+ origin path.
10
+ - ``merge_layers``: public API returning merged data and provenance mappings.
11
+ - Internal helpers (``_weave_layer``, ``_descend`` …) that manage recursive
12
+ merging, branch clearing, and dotted-key generation.
13
+
14
+ System Role:
15
+ The composition root assembles layer snapshots and delegates to
16
+ ``merge_layers`` before building the domain ``Config`` value object.
17
+ Adapters and CLI code depend on the provenance structure to explain precedence.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from collections.abc import Iterable, Mapping, MutableMapping, Sequence
23
+ from collections.abc import Mapping as MappingABC
24
+ from collections.abc import Mapping as TypingMapping
25
+ from dataclasses import dataclass
26
+ from typing import TypeGuard, cast
27
+
28
+ from ..observability import log_warn
29
+ from .ports import SourceInfoPayload
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class MergeResult:
34
+ """Result of merging configuration layers.
35
+
36
+ Provides a structured return type instead of raw tuples, improving
37
+ code readability and enabling better IDE support.
38
+
39
+ Attributes:
40
+ data: The merged configuration tree with all layers applied.
41
+ provenance: Provenance metadata keyed by dotted path, explaining which layer
42
+ contributed each final value.
43
+ """
44
+
45
+ data: dict[str, object]
46
+ provenance: dict[str, SourceInfoPayload]
47
+
48
+
49
+ @dataclass(frozen=True, eq=False, slots=True)
50
+ class LayerSnapshot:
51
+ """Immutable description of a configuration layer.
52
+
53
+ Keeps layer metadata compact and explicit so merge logic can reason about
54
+ precedence without coupling to adapter implementations.
55
+
56
+ Attributes:
57
+ name: Logical name of the layer (``"defaults"``, ``"app"``, ``"host"``,
58
+ ``"user"``, ``"dotenv"``, ``"env"``).
59
+ payload: Mapping produced by adapters; expected to contain only JSON-serialisable
60
+ types.
61
+ origin: Optional filesystem path (or ``None`` for in-memory sources).
62
+ """
63
+
64
+ name: str
65
+ payload: Mapping[str, object]
66
+ origin: str | None
67
+
68
+
69
+ def merge_layers(layers: Iterable[LayerSnapshot]) -> MergeResult:
70
+ """Merge ordered layers into data and provenance dictionaries.
71
+
72
+ Central policy point for layered configuration. Ensures later layers may
73
+ override earlier ones and that provenance stays aligned with the final data.
74
+
75
+ Args:
76
+ layers: Iterable of :class:`LayerSnapshot` instances in merge order (lowest to
77
+ highest precedence).
78
+
79
+ Returns:
80
+ Dataclass containing the merged configuration mapping and provenance
81
+ mapping keyed by dotted path.
82
+
83
+ Examples:
84
+ >>> base = LayerSnapshot("app", {"db": {"host": "localhost"}}, "/etc/app.toml")
85
+ >>> override = LayerSnapshot("env", {"db": {"host": "prod"}}, None)
86
+ >>> result = merge_layers([base, override])
87
+ >>> result.data["db"]["host"], result.provenance["db.host"]["layer"]
88
+ ('prod', 'env')
89
+ """
90
+ merged: dict[str, object] = {}
91
+ provenance: dict[str, SourceInfoPayload] = {}
92
+
93
+ for snapshot in layers:
94
+ _weave_layer(merged, provenance, snapshot)
95
+
96
+ return MergeResult(data=merged, provenance=provenance)
97
+
98
+
99
+ def _weave_layer(
100
+ target: MutableMapping[str, object],
101
+ provenance: MutableMapping[str, SourceInfoPayload],
102
+ snapshot: LayerSnapshot,
103
+ ) -> None:
104
+ """Clone snapshot payload and fold it into accumulators.
105
+
106
+ Provide a single entry point that ensures each snapshot is processed with
107
+ defensive cloning before descending into nested structures.
108
+
109
+ Args:
110
+ target: Mutable mapping accumulating merged configuration values.
111
+ provenance: Mutable mapping capturing dotted-path provenance entries.
112
+ snapshot: Layer snapshot being merged into the accumulators.
113
+
114
+ Side Effects:
115
+ Mutates *target* and *provenance* in place.
116
+
117
+ Examples:
118
+ >>> merged, prov = {}, {}
119
+ >>> snap = LayerSnapshot('env', {'flag': True}, None)
120
+ >>> _weave_layer(merged, prov, snap)
121
+ >>> merged['flag'], prov['flag']['layer']
122
+ (True, 'env')
123
+ """
124
+ _descend(target, provenance, snapshot.payload, snapshot, [])
125
+
126
+
127
+ def _descend(
128
+ target: MutableMapping[str, object],
129
+ provenance: MutableMapping[str, SourceInfoPayload],
130
+ incoming: Mapping[str, object],
131
+ snapshot: LayerSnapshot,
132
+ segments: list[str],
133
+ ) -> None:
134
+ """Walk each key/value pair, updating scalars or branches as needed.
135
+
136
+ Implements the recursive merge algorithm that honours nested structures and
137
+ ensures provenance stays aligned with the final data.
138
+
139
+ Args:
140
+ target: Mutable mapping receiving merged values.
141
+ provenance: Mutable mapping storing provenance per dotted path.
142
+ incoming: Mapping representing the current layer payload.
143
+ snapshot: Layer metadata used for provenance entries.
144
+ segments: Accumulated path segments used to compute dotted keys during recursion.
145
+
146
+ Side Effects:
147
+ Mutates *target* and *provenance* as it walks through *incoming*.
148
+ """
149
+ for key, value in incoming.items():
150
+ dotted = _join_segments(segments, key)
151
+ if _looks_like_mapping(value):
152
+ _store_branch(target, provenance, key, value, dotted, snapshot, segments)
153
+ else:
154
+ _store_scalar(target, provenance, key, value, dotted, snapshot)
155
+
156
+
157
+ def _store_branch(
158
+ target: MutableMapping[str, object],
159
+ provenance: MutableMapping[str, SourceInfoPayload],
160
+ key: str,
161
+ value: Mapping[str, object],
162
+ dotted: str,
163
+ snapshot: LayerSnapshot,
164
+ segments: list[str],
165
+ ) -> None:
166
+ """Ensure a nested mapping exists before descending into it.
167
+
168
+ Args:
169
+ target: Mutable mapping currently being merged into.
170
+ provenance: Provenance accumulator updated as recursion progresses.
171
+ key: Current key being processed.
172
+ value: Mapping representing the nested branch from the incoming layer.
173
+ dotted: Dotted representation of the branch path for provenance updates.
174
+ snapshot: Metadata describing the active layer.
175
+ segments: Mutable list containing the path segments of the current recursion.
176
+
177
+ Side Effects:
178
+ Mutates *target*, *provenance*, and *segments* while recursing.
179
+
180
+ Examples:
181
+ >>> target, prov = {}, {}
182
+ >>> branch_snapshot = LayerSnapshot('env', {'child': {'enabled': True}}, None)
183
+ >>> _store_branch(target, prov, 'child', {'enabled': True}, 'child', branch_snapshot, [])
184
+ >>> target['child']['enabled']
185
+ True
186
+ """
187
+ branch = _ensure_branch(target, key, dotted, snapshot)
188
+ segments.append(key)
189
+ _descend(branch, provenance, value, snapshot, segments)
190
+ segments.pop()
191
+ _clear_branch_if_empty(branch, dotted, provenance)
192
+
193
+
194
+ def _store_scalar(
195
+ target: MutableMapping[str, object],
196
+ provenance: MutableMapping[str, SourceInfoPayload],
197
+ key: str,
198
+ value: object,
199
+ dotted: str,
200
+ snapshot: LayerSnapshot,
201
+ ) -> None:
202
+ """Set the scalar value and update provenance in lockstep.
203
+
204
+ Warns when a mapping is being replaced by a scalar, as this may indicate
205
+ a configuration schema mismatch between layers.
206
+ """
207
+ current = target.get(key)
208
+ if _looks_like_mapping(current):
209
+ _warn_type_conflict(dotted, snapshot, "mapping", "scalar")
210
+
211
+ target[key] = _clone_leaf(value)
212
+ provenance[dotted] = {
213
+ "layer": snapshot.name,
214
+ "path": snapshot.origin,
215
+ "key": dotted,
216
+ }
217
+
218
+
219
+ def _clone_dict(value: dict[str, object]) -> dict[str, object]:
220
+ """Clone a dictionary recursively."""
221
+ return {k: _clone_leaf(i) for k, i in value.items()}
222
+
223
+
224
+ def _clone_list(value: list[object]) -> list[object]:
225
+ """Clone a list recursively."""
226
+ return [_clone_leaf(i) for i in value]
227
+
228
+
229
+ def _clone_set(value: set[object]) -> set[object]:
230
+ """Clone a set recursively."""
231
+ return {_clone_leaf(i) for i in value}
232
+
233
+
234
+ def _clone_tuple(value: tuple[object, ...]) -> tuple[object, ...]:
235
+ """Clone a tuple recursively."""
236
+ return tuple(_clone_leaf(i) for i in value)
237
+
238
+
239
+ def _clone_leaf(value: object) -> object:
240
+ """Return a defensive copy of mutable leaf values.
241
+
242
+ Prevents callers from mutating adapter-provided data after the merge,
243
+ preserving immutability guarantees described in the system design.
244
+
245
+ Args:
246
+ value: Leaf value drawn from the incoming layer.
247
+
248
+ Returns:
249
+ Clone of the input value; immutable types are returned unchanged.
250
+
251
+ Examples:
252
+ >>> original = {'items': [1, 2]}
253
+ >>> cloned = _clone_leaf(original)
254
+ >>> cloned is original
255
+ False
256
+ >>> cloned['items'][0] = 42
257
+ >>> original['items'][0]
258
+ 1
259
+ """
260
+ if isinstance(value, dict):
261
+ return _clone_dict(cast(dict[str, object], value))
262
+ if isinstance(value, list):
263
+ return _clone_list(cast(list[object], value))
264
+ if isinstance(value, set):
265
+ return _clone_set(cast(set[object], value))
266
+ if isinstance(value, tuple):
267
+ return _clone_tuple(cast(tuple[object, ...], value))
268
+ return value
269
+
270
+
271
+ def _ensure_branch(
272
+ target: MutableMapping[str, object],
273
+ key: str,
274
+ dotted: str,
275
+ snapshot: LayerSnapshot,
276
+ ) -> MutableMapping[str, object]:
277
+ """Return an existing branch or create a fresh empty one.
278
+
279
+ Warns when a scalar value is being replaced by a mapping, as this may
280
+ indicate a configuration schema mismatch between layers.
281
+ """
282
+ current = target.get(key)
283
+ if _looks_like_mapping(current):
284
+ return cast(MutableMapping[str, object], current)
285
+
286
+ if current is not None:
287
+ _warn_type_conflict(dotted, snapshot, "scalar", "mapping")
288
+
289
+ new_branch: MutableMapping[str, object] = {}
290
+ target[key] = new_branch
291
+ return new_branch
292
+
293
+
294
+ def _clear_branch_if_empty(
295
+ branch: MutableMapping[str, object], dotted: str, provenance: MutableMapping[str, SourceInfoPayload]
296
+ ) -> None:
297
+ """Remove empty branches from provenance when overwritten by scalars.
298
+
299
+ Args:
300
+ branch: Mutable mapping representing the nested branch just processed.
301
+ dotted: Dotted key corresponding to the branch.
302
+ provenance: Provenance mapping to prune when the branch becomes empty.
303
+
304
+ Side Effects:
305
+ Mutates *provenance* by removing entries when the branch no longer has data.
306
+
307
+ Examples:
308
+ >>> prov = {'a.b': {'layer': 'env', 'path': None, 'key': 'a.b'}}
309
+ >>> _clear_branch_if_empty({}, 'a.b', prov)
310
+ >>> 'a.b' in prov
311
+ False
312
+ """
313
+ if branch:
314
+ return
315
+ provenance.pop(dotted, None)
316
+
317
+
318
+ def _join_segments(segments: Sequence[str], key: str) -> str:
319
+ """Join the current path segments with the new key.
320
+
321
+ Args:
322
+ segments: Tuple of parent path segments accumulated so far.
323
+ key: Current key being appended to the dotted path.
324
+
325
+ Returns:
326
+ Dotted path string combining *segments* and *key*.
327
+
328
+ Examples:
329
+ >>> _join_segments(('db', 'config'), 'host')
330
+ 'db.config.host'
331
+ >>> _join_segments((), 'timeout')
332
+ 'timeout'
333
+ """
334
+ if not segments:
335
+ return key
336
+ return ".".join((*segments, key))
337
+
338
+
339
+ def _looks_like_mapping(value: object) -> TypeGuard[Mapping[str, object]]:
340
+ """Return ``True`` when *value* is a mapping with string keys.
341
+
342
+ Guards recursion so scalars are handled separately from nested mappings.
343
+
344
+ Args:
345
+ value: Candidate object inspected during recursion.
346
+
347
+ Returns:
348
+ ``True`` when *value* behaves like ``Mapping[str, object]``.
349
+
350
+ Examples:
351
+ >>> _looks_like_mapping({'a': 1})
352
+ True
353
+ >>> _looks_like_mapping(['not', 'mapping'])
354
+ False
355
+ """
356
+ if not isinstance(value, MappingABC):
357
+ return False
358
+ mapping = cast(TypingMapping[object, object], value)
359
+ keys = cast(Iterable[object], mapping.keys())
360
+ return all(isinstance(k, str) for k in keys)
361
+
362
+
363
+ def _warn_type_conflict(dotted: str, snapshot: LayerSnapshot, old_type: str, new_type: str) -> None:
364
+ """Emit a warning when a type conflict occurs during merge.
365
+
366
+ This indicates a potential configuration schema mismatch where one layer
367
+ defines a key as a scalar and another defines it as a mapping.
368
+ """
369
+ log_warn(
370
+ "type_conflict",
371
+ key=dotted,
372
+ layer=snapshot.name,
373
+ path=snapshot.origin,
374
+ old_type=old_type,
375
+ new_type=new_type,
376
+ )
377
+
378
+
379
+ __all__ = ["LayerSnapshot", "MergeResult", "merge_layers"]
@@ -0,0 +1,115 @@
1
+ """Runtime-checkable protocols defining adapter contracts.
2
+
3
+ Ensure the composition root depends on abstractions instead of concrete
4
+ implementations, mirroring the Clean Architecture layering in the system design.
5
+
6
+ Contents:
7
+ - ``SourceInfoPayload``: type alias for domain ``SourceInfo`` TypedDict.
8
+ - Type aliases (``ConfigData``, ``ProvenanceData``) for consistent signatures.
9
+ - Protocols for each adapter type (path resolver, file loader, dotenv loader,
10
+ environment loader) plus the merge interface consumed by tests and tooling.
11
+
12
+ System Role:
13
+ Adapters must implement these protocols; tests (`tests/adapters/test_port_contracts.py`)
14
+ use ``isinstance`` checks to enforce compliance at runtime.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from collections.abc import Iterable, Mapping
20
+ from typing import Protocol, runtime_checkable
21
+
22
+ from ..domain.config import SourceInfo
23
+
24
+ # Re-export domain SourceInfo as SourceInfoPayload for adapter contracts
25
+ SourceInfoPayload = SourceInfo
26
+ """Alias for :class:`~lib_layered_config.domain.config.SourceInfo`.
27
+
28
+ Provides a stable name for the provenance payload used across adapter and
29
+ application boundaries without duplicating the TypedDict definition.
30
+ """
31
+
32
+ # Type aliases for clarity in function signatures
33
+ ConfigData = Mapping[str, object]
34
+ """Type alias for merged configuration data."""
35
+
36
+ ProvenanceData = Mapping[str, SourceInfoPayload]
37
+ """Type alias for provenance metadata keyed by dotted path."""
38
+
39
+
40
+ @runtime_checkable
41
+ class PathResolver(Protocol):
42
+ """Provide ordered path iterables for each configuration layer.
43
+
44
+ Methods mirror the precedence hierarchy documented in
45
+ ``docs/systemdesign/concept.md``.
46
+ """
47
+
48
+ def app(self) -> Iterable[str]:
49
+ """Return paths for application-level configuration."""
50
+ ... # pragma: no cover - protocol
51
+
52
+ def host(self) -> Iterable[str]:
53
+ """Return paths for host-specific configuration."""
54
+ ... # pragma: no cover - protocol
55
+
56
+ def user(self) -> Iterable[str]:
57
+ """Return paths for user-level configuration."""
58
+ ... # pragma: no cover - protocol
59
+
60
+ def dotenv(self) -> Iterable[str]:
61
+ """Return candidate paths for `.env` file discovery."""
62
+ ... # pragma: no cover - protocol
63
+
64
+
65
+ @runtime_checkable
66
+ class FileLoader(Protocol):
67
+ """Parse a structured configuration file into a mapping."""
68
+
69
+ def load(self, path: str) -> ConfigData:
70
+ """Load and parse the file at *path* into a configuration mapping."""
71
+ ... # pragma: no cover - protocol
72
+
73
+
74
+ @runtime_checkable
75
+ class DotEnvLoader(Protocol):
76
+ """Convert `.env` files into nested mappings respecting prefix semantics."""
77
+
78
+ def load(self, start_dir: str | None = None) -> ConfigData:
79
+ """Discover and parse a `.env` file starting from *start_dir*."""
80
+ ... # pragma: no cover - protocol
81
+
82
+ @property
83
+ def last_loaded_path(self) -> str | None:
84
+ """Return the path of the most recently loaded `.env` file."""
85
+ ... # pragma: no cover - attribute contract
86
+
87
+
88
+ @runtime_checkable
89
+ class EnvLoader(Protocol):
90
+ """Translate prefixed environment variables into nested mappings."""
91
+
92
+ def load(self, prefix: str) -> ConfigData:
93
+ """Load environment variables matching *prefix* into a nested mapping."""
94
+ ... # pragma: no cover - protocol
95
+
96
+
97
+ @runtime_checkable
98
+ class Merger(Protocol):
99
+ """Combine ordered layers into merged data and provenance structures."""
100
+
101
+ def merge(self, layers: Iterable[tuple[str, ConfigData, str | None]]) -> tuple[ConfigData, ProvenanceData]:
102
+ """Merge *layers* into unified configuration data with provenance."""
103
+ ... # pragma: no cover - protocol
104
+
105
+
106
+ __all__ = [
107
+ "SourceInfoPayload",
108
+ "ConfigData",
109
+ "ProvenanceData",
110
+ "PathResolver",
111
+ "FileLoader",
112
+ "DotEnvLoader",
113
+ "EnvLoader",
114
+ "Merger",
115
+ ]
@@ -0,0 +1,92 @@
1
+ """Package exposing the lib_layered_config command-line interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Sequence
6
+ from typing import cast
7
+
8
+ import rich_click as click
9
+ from lib_cli_exit_tools import cli_session
10
+
11
+ from .common import version_string
12
+ from .constants import CLICK_CONTEXT_SETTINGS, TRACEBACK_SUMMARY, TRACEBACK_VERBOSE
13
+ from .read import read_command as cli_read_config
14
+ from .read import read_json_command as cli_read_config_json
15
+
16
+
17
+ @click.group(
18
+ help="Immutable layered configuration reader",
19
+ context_settings=CLICK_CONTEXT_SETTINGS,
20
+ invoke_without_command=False,
21
+ )
22
+ @click.version_option(
23
+ version=version_string(),
24
+ prog_name="lib_layered_config",
25
+ message="lib_layered_config version %(version)s",
26
+ )
27
+ @click.option(
28
+ "--traceback/--no-traceback",
29
+ is_flag=True,
30
+ default=False,
31
+ help="Show full Python traceback on errors",
32
+ )
33
+ @click.pass_context
34
+ def cli(ctx: click.Context, traceback: bool) -> None:
35
+ """Root command storing the requested traceback preference."""
36
+ ctx.ensure_object(dict)
37
+ ctx.obj["traceback"] = traceback
38
+
39
+
40
+ def main(argv: Sequence[str] | None = None, *, restore_traceback: bool = True) -> int:
41
+ """Entry point wiring the CLI through ``lib_cli_exit_tools.cli_session``."""
42
+ args_list = list(argv) if argv is not None else None
43
+ overrides = _session_overrides(args_list)
44
+
45
+ with cli_session(
46
+ summary_limit=TRACEBACK_SUMMARY,
47
+ verbose_limit=TRACEBACK_VERBOSE,
48
+ overrides=overrides or None,
49
+ restore=restore_traceback,
50
+ ) as run:
51
+ runner = cast("Callable[..., int]", run)
52
+ return runner(
53
+ cli,
54
+ argv=args_list,
55
+ prog_name="lib_layered_config",
56
+ )
57
+
58
+
59
+ def _session_overrides(argv: Sequence[str] | None) -> dict[str, object]:
60
+ """Derive configuration overrides for ``cli_session`` based on CLI args."""
61
+ if not argv:
62
+ return {}
63
+
64
+ try:
65
+ ctx = cli.make_context("lib_layered_config", list(argv), resilient_parsing=True)
66
+ except click.ClickException:
67
+ return {}
68
+
69
+ try:
70
+ enabled = bool(ctx.params.get("traceback", False))
71
+ finally:
72
+ ctx.close()
73
+
74
+ return {"traceback": enabled} if enabled else {}
75
+
76
+
77
+ def _register_commands() -> None:
78
+ from . import deploy, fail, generate, info, read
79
+
80
+ for module in (read, deploy, generate, info, fail):
81
+ module.register(cli)
82
+
83
+
84
+ _register_commands()
85
+
86
+
87
+ __all__ = [
88
+ "cli",
89
+ "main",
90
+ "cli_read_config",
91
+ "cli_read_config_json",
92
+ ]