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,3263 @@
1
+ Metadata-Version: 2.4
2
+ Name: lib_layered_config
3
+ Version: 4.1.0
4
+ Summary: Cross-platform layered configuration loader for Python
5
+ Project-URL: Homepage, https://github.com/bitranox/lib_layered_config
6
+ Project-URL: Repository, https://github.com/bitranox/lib_layered_config.git
7
+ Project-URL: Issues, https://github.com/bitranox/lib_layered_config/issues
8
+ Author-email: bitranox <bitranox@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: configuration,dotenv,env,layered,toml
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: lib-cli-exit-tools>=2.2.1
24
+ Requires-Dist: rich-click>=1.9.4
25
+ Requires-Dist: tomli>=2.3.0; python_version < '3.11'
26
+ Provides-Extra: dev
27
+ Requires-Dist: bandit>=1.9.2; extra == 'dev'
28
+ Requires-Dist: build>=1.3.0; extra == 'dev'
29
+ Requires-Dist: codecov-cli>=11.2.6; extra == 'dev'
30
+ Requires-Dist: coverage[toml]>=7.13.0; extra == 'dev'
31
+ Requires-Dist: hypothesis>=6.148.7; extra == 'dev'
32
+ Requires-Dist: import-linter>=2.9; extra == 'dev'
33
+ Requires-Dist: pip-audit>=2.10.0; extra == 'dev'
34
+ Requires-Dist: pyright>=1.1.407; extra == 'dev'
35
+ Requires-Dist: pytest-asyncio>=1.3.0; extra == 'dev'
36
+ Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
37
+ Requires-Dist: pytest>=9.0.2; extra == 'dev'
38
+ Requires-Dist: pyyaml>=6.0.3; extra == 'dev'
39
+ Requires-Dist: ruff>=0.14.8; extra == 'dev'
40
+ Requires-Dist: textual>=6.8.0; extra == 'dev'
41
+ Requires-Dist: tomli>=2.3.0; (python_version < '3.11') and extra == 'dev'
42
+ Requires-Dist: twine>=6.2.0; extra == 'dev'
43
+ Provides-Extra: yaml
44
+ Requires-Dist: pyyaml>=6.0.3; extra == 'yaml'
45
+ Description-Content-Type: text/markdown
46
+
47
+ # lib_layered_config
48
+
49
+ <!-- Badges -->
50
+ [![CI](https://github.com/bitranox/lib_layered_config/actions/workflows/ci.yml/badge.svg)](https://github.com/bitranox/lib_layered_config/actions/workflows/ci.yml)
51
+ [![CodeQL](https://github.com/bitranox/lib_layered_config/actions/workflows/codeql.yml/badge.svg)](https://github.com/bitranox/lib_layered_config/actions/workflows/codeql.yml)
52
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
53
+ [![Open in Codespaces](https://img.shields.io/badge/Codespaces-Open-blue?logo=github&logoColor=white&style=flat-square)](https://codespaces.new/bitranox/lib_layered_config?quickstart=1)
54
+ [![PyPI](https://img.shields.io/pypi/v/lib-layered-config.svg)](https://pypi.org/project/lib-layered-config/)
55
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/lib-layered-config.svg)](https://pypi.org/project/lib-layered-config/)
56
+ [![Code Style: Ruff](https://img.shields.io/badge/Code%20Style-Ruff-46A3FF?logo=ruff&labelColor=000)](https://docs.astral.sh/ruff/)
57
+ [![codecov](https://codecov.io/gh/bitranox/lib_layered_config/graph/badge.svg)](https://codecov.io/gh/bitranox/lib_layered_config)
58
+ [![Maintainability](https://qlty.sh/gh/bitranox/projects/lib_layered_config/maintainability.svg)](https://qlty.sh/gh/bitranox/projects/lib_layered_config)
59
+ [![Known Vulnerabilities](https://snyk.io/test/github/bitranox/lib_layered_config/badge.svg)](https://snyk.io/test/github/bitranox/lib_layered_config)
60
+ [![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://github.com/PyCQA/bandit)
61
+
62
+ A cross-platform configuration loader that deep-merges application defaults, host overrides, user profiles, `.env` files, and environment variables into a single immutable object. The core follows Clean Architecture boundaries so adapters (filesystem, dotenv, environment) stay isolated from the domain model while the CLI mirrors the same orchestration.
63
+
64
+ ## Table of Contents
65
+
66
+ 1. [Key Features](#key-features)
67
+ 2. [Architecture Overview](#architecture-overview)
68
+ 3. [Installation](#installation)
69
+ 4. [Quick Start](#quick-start)
70
+ 5. [Understanding Key Identifiers: Vendor, App, and Slug](#understanding-key-identifiers-vendor-app-and-slug)
71
+ - [Configuration Profiles](#configuration-profiles)
72
+ 6. [Configuration File Structure](#configuration-file-structure)
73
+ 7. [Configuration Sources & Precedence](#configuration-sources--precedence)
74
+ 8. [CLI Usage](#cli-usage)
75
+ 9. [Python API](#python-api)
76
+ 10. [Example Generation & Deployment](#example-generation--deployment)
77
+ 11. [Provenance & Observability](#provenance--observability)
78
+ 12. [Development](#development)
79
+ 13. [License](#license)
80
+
81
+ ## Key Features
82
+
83
+ - **Deterministic layering** — precedence is always `defaults → app → host → user → dotenv → env`.
84
+ - **Immutable value object** — returned `Config` prevents accidental mutation and exposes dotted-path helpers.
85
+ - **Provenance tracking** — every key reports the layer and path that produced it.
86
+ - **Cross-platform path discovery** — Linux (XDG), macOS, and Windows layouts with environment overrides for tests.
87
+ - **Configuration profiles** — organize environment-specific configs (test, staging, production) into isolated subdirectories.
88
+ - **Extensible formats** — TOML and JSON are built-in; YAML is available via the optional `yaml` extra.
89
+ - **Automation-friendly CLI** — inspect, deploy, or scaffold configurations without writing Python.
90
+ - **Structured logging** — adapters emit trace-aware events without polluting the domain layer.
91
+
92
+ ## Architecture Overview
93
+
94
+ The project follows a Clean Architecture layout so responsibilities remain easy to reason about and test:
95
+
96
+ - **Domain** — immutable `Config` value object plus error taxonomy.
97
+ - **Application** — merge policy (`LayerSnapshot`, `merge_layers`) and adapter protocols.
98
+ - **Adapters** — filesystem discovery, structured file loaders, dotenv, and environment ingress.
99
+ - **Composition** — `core` and `_layers` wire adapters together and expose the public API.
100
+ - **Presentation & Tooling** — CLI commands, deployment/example helpers, observability utilities, and testing hooks.
101
+
102
+ Consult [`docs/systemdesign/module_reference.md`](docs/systemdesign/module_reference.md) for a per-module catalogue and traceability back to the system design notes.
103
+
104
+ ## Installation
105
+
106
+ ```bash
107
+ pip install lib_layered_config
108
+ # or with optional YAML support
109
+ pip install "lib_layered_config[yaml]"
110
+ ```
111
+
112
+ > **Requires Python 3.10+** — uses `tomllib` on Python 3.11+, or the `tomli` backport on Python 3.10.
113
+ >
114
+ > Install the optional `yaml` extra only when you actually ship `.yml` files to keep the dependency footprint small.
115
+
116
+ For local development add tooling extras:
117
+
118
+ ```bash
119
+ pip install "lib_layered_config[dev]"
120
+ ```
121
+
122
+ ## Quick Start
123
+
124
+ ```python
125
+ from lib_layered_config import read_config
126
+
127
+ config = read_config(vendor="Acme", app="ConfigKit", slug="config-kit")
128
+ print(config.get("service.timeout", default=30))
129
+ print(config.origin("service.timeout"))
130
+ ```
131
+
132
+ CLI equivalent (human readable by default):
133
+
134
+ ```bash
135
+ lib_layered_config read --vendor Acme --app ConfigKit --slug config-kit
136
+ ```
137
+
138
+ JSON output including provenance:
139
+
140
+ ```bash
141
+ lib_layered_config read --vendor Acme --app ConfigKit --slug config-kit --format json
142
+ # or
143
+ lib_layered_config read-json --vendor Acme --app ConfigKit --slug config-kit
144
+ ```
145
+
146
+ ## Understanding Key Identifiers: Vendor, App, Slug, and Profile
147
+
148
+ Before diving into configuration sources, it's important to understand the four key identifiers used throughout this library:
149
+
150
+ ### Vendor
151
+
152
+ **What it is:** Your organization or company name (e.g., `"Acme"`, `"Mozilla"`, `"MyCompany"`).
153
+
154
+ **Where it's used:**
155
+ - **macOS:** `/Library/Application Support/Acme/MyApp/` and `~/Library/Application Support/Acme/MyApp/`
156
+ - **Windows:** `C:\ProgramData\Acme\MyApp\` and `%APPDATA%\Acme\MyApp\`
157
+ - **Linux:** Not used (Linux uses the slug directly)
158
+
159
+ **Example:**
160
+ ```python
161
+ # Your company is "Acme Corp"
162
+ config = read_config(vendor="Acme", app="DatabaseTool", slug="db-tool")
163
+ # macOS paths: /Library/Application Support/Acme/DatabaseTool/config.toml
164
+ ```
165
+
166
+ ---
167
+
168
+ ### App
169
+
170
+ **What it is:** Your application's full/display name (e.g., `"DatabaseTool"`, `"ConfigKit"`, `"MyService"`).
171
+
172
+ **Where it's used:**
173
+ - **macOS:** Combined with vendor in paths: `/Library/Application Support/Acme/MyApp/`
174
+ - **Windows:** Combined with vendor: `C:\ProgramData\Acme\MyApp\`
175
+ - **Linux:** Not used (Linux uses the slug directly)
176
+ - **Default slug:** If you don't specify a slug, the app name is used as the slug
177
+
178
+ **Example:**
179
+ ```python
180
+ config = read_config(vendor="Acme", app="ConfigKit", slug="config-kit")
181
+ # macOS: /Library/Application Support/Acme/ConfigKit/config.toml
182
+ # Windows: C:\ProgramData\Acme\ConfigKit\config.toml
183
+ ```
184
+
185
+ ---
186
+
187
+ ### Slug (Configuration Slug)
188
+
189
+ **What it is:** A lowercase, filesystem-friendly identifier for your configuration (e.g., `"myapp"`, `"config-kit"`, `"db-tool"`).
190
+
191
+ **Why it exists:** The slug serves as a **universal, platform-independent identifier** for your configuration that works consistently across:
192
+ 1. Linux/UNIX filesystem paths (case-sensitive, prefers hyphens)
193
+ 2. Environment variable prefixes (converted to uppercase)
194
+ 3. Cross-platform scripts and automation
195
+
196
+ **Where it's used:**
197
+
198
+ #### 1. **Linux/UNIX Paths**
199
+ ```bash
200
+ /etc/xdg/myapp/config.toml # System-wide (XDG-compliant)
201
+ /etc/xdg/myapp/hosts/server-01.toml # Host-specific (XDG-compliant)
202
+ ~/.config/myapp/config.toml # User-specific
203
+ ~/.config/myapp/.env # Environment variables
204
+ ```
205
+
206
+ Note: For backwards compatibility, the library also checks `/etc/myapp/` if `/etc/xdg/myapp/` is not found.
207
+
208
+ #### 2. **Environment Variable Prefix**
209
+ The slug is converted to uppercase with underscores, followed by a triple underscore (`___`) separator to clearly distinguish the prefix from section/key separators (which use double underscores `__`):
210
+ ```bash
211
+ # Slug: "myapp" → Environment prefix: "MYAPP___"
212
+ MYAPP___DATABASE__HOST=localhost
213
+ MYAPP___DATABASE__PORT=5432
214
+ MYAPP___SERVICE__TIMEOUT=30
215
+
216
+ # Slug: "config-kit" → Environment prefix: "CONFIG_KIT___"
217
+ CONFIG_KIT___API__KEY=secret
218
+ CONFIG_KIT___DEBUG__ENABLED=true
219
+ ```
220
+
221
+ #### 3. **Cross-Platform Consistency**
222
+ The slug provides a consistent identifier regardless of platform:
223
+ ```python
224
+ # Same slug works on all platforms
225
+ config = read_config(vendor="Acme", app="My App", slug="myapp")
226
+
227
+ # Linux: /etc/xdg/myapp/config.toml
228
+ # macOS: /Library/Application Support/Acme/My App/config.toml
229
+ # Windows: C:\ProgramData\Acme\My App\config.toml
230
+ # Env vars: MYAPP___DATABASE__HOST (all platforms)
231
+ ```
232
+
233
+ ---
234
+
235
+ ### Slug Naming Best Practices
236
+
237
+ ✅ **DO:**
238
+ - Use lowercase letters: `"myapp"`, `"database-tool"`
239
+ - Use hyphens for word separation: `"config-kit"`, `"db-manager"`
240
+ - Keep it short and memorable: `"myapp"` not `"my-super-awesome-application"`
241
+ - Use ASCII characters only: `"myapp"` not `"my-àpp"`
242
+ - Use the same slug everywhere in your application
243
+
244
+ ❌ **DON'T:**
245
+ - Use spaces: `"my app"` → use `"myapp"` or `"my-app"`
246
+ - Use uppercase: `"MyApp"` → use `"myapp"` (uppercase works but isn't recommended)
247
+ - Use underscores in the slug: `"my_app"` → use `"my-app"` (underscores are added automatically for env vars)
248
+ - Use non-ASCII characters: `"café"` → will raise `ValueError`
249
+ - Use Windows reserved names: `"CON"`, `"PRN"`, `"NUL"` → will raise `ValueError`
250
+ - Mix naming conventions across your codebase
251
+ - Use path separators (`/` or `\`): `"../etc"` will raise `ValueError`
252
+ - Start with a dot: `".hidden"` will raise `ValueError`
253
+
254
+ ---
255
+
256
+ ### Profile (Optional)
257
+
258
+ **What it is:** An optional identifier for environment-specific configurations (e.g., `"test"`, `"staging"`, `"production"`).
259
+
260
+ **Why it exists:** Profiles allow you to organize separate configuration sets for different environments (development, testing, staging, production) without mixing files or relying solely on environment variables.
261
+
262
+ **Where it's used:**
263
+ When a profile is specified, a `profile/<name>/` subdirectory is inserted into all configuration paths:
264
+
265
+ #### 1. **Linux/UNIX Paths (with profile)**
266
+ ```bash
267
+ # Without profile:
268
+ /etc/xdg/myapp/config.toml
269
+ ~/.config/myapp/config.toml
270
+
271
+ # With profile="production":
272
+ /etc/xdg/myapp/profile/production/config.toml
273
+ ~/.config/myapp/profile/production/config.toml
274
+ ```
275
+
276
+ #### 2. **macOS Paths (with profile)**
277
+ ```bash
278
+ # Without profile:
279
+ /Library/Application Support/Acme/MyApp/config.toml
280
+
281
+ # With profile="production":
282
+ /Library/Application Support/Acme/MyApp/profile/production/config.toml
283
+ ```
284
+
285
+ #### 3. **Windows Paths (with profile)**
286
+ ```bash
287
+ # Without profile:
288
+ C:\ProgramData\Acme\MyApp\config.toml
289
+
290
+ # With profile="production":
291
+ C:\ProgramData\Acme\MyApp\profile\production\config.toml
292
+ ```
293
+
294
+ #### 4. **Usage Example**
295
+ ```python
296
+ from lib_layered_config import read_config
297
+
298
+ # Load production configuration
299
+ prod_config = read_config(
300
+ vendor="Acme",
301
+ app="MyApp",
302
+ slug="myapp",
303
+ profile="production"
304
+ )
305
+
306
+ # Load test configuration (different paths, completely isolated)
307
+ test_config = read_config(
308
+ vendor="Acme",
309
+ app="MyApp",
310
+ slug="myapp",
311
+ profile="test"
312
+ )
313
+
314
+ # Load default configuration (no profile, original paths)
315
+ default_config = read_config(
316
+ vendor="Acme",
317
+ app="MyApp",
318
+ slug="myapp"
319
+ # profile=None (default)
320
+ )
321
+ ```
322
+
323
+ #### 5. **CLI Usage**
324
+ ```bash
325
+ # Read production profile
326
+ lib_layered_config read --vendor Acme --app MyApp --slug myapp --profile production
327
+
328
+ # Deploy to test profile
329
+ lib_layered_config deploy --source config.toml --vendor Acme --app MyApp --slug myapp --profile test --target app
330
+ ```
331
+
332
+ ---
333
+
334
+ ### Profile Naming Best Practices
335
+
336
+ ✅ **DO:**
337
+ - Use lowercase letters: `"test"`, `"production"`
338
+ - Use hyphens for word separation: `"staging-v2"`, `"dev-local"`
339
+ - Keep it short and descriptive: `"prod"` or `"production"`
340
+ - Use consistent profile names across your infrastructure
341
+
342
+ ❌ **DON'T:**
343
+ - Use spaces: `"my profile"` → use `"my-profile"`
344
+ - Use non-ASCII characters: `"tëst"` → will raise `ValueError`
345
+ - Use Windows reserved names: `"CON"`, `"NUL"` → will raise `ValueError`
346
+ - Use path separators: `"../etc"` → will raise `ValueError`
347
+
348
+ ---
349
+
350
+ ### Complete Example: How They Work Together
351
+
352
+ ```python
353
+ from lib_layered_config import read_config
354
+
355
+ # Define your application identity (without profile)
356
+ config = read_config(
357
+ vendor="Acme", # Your company name
358
+ app="DatabaseManager", # Your application's display name
359
+ slug="db-manager" # Filesystem/environment-friendly identifier
360
+ )
361
+
362
+ # Or with a profile for environment-specific configuration
363
+ prod_config = read_config(
364
+ vendor="Acme",
365
+ app="DatabaseManager",
366
+ slug="db-manager",
367
+ profile="production" # Optional: isolates config in profile subdirectory
368
+ )
369
+ ```
370
+
371
+ **This creates the following structure (without profile):**
372
+
373
+ **On Linux:**
374
+ ```
375
+ /etc/xdg/db-manager/config.toml # System-wide (uses slug, XDG-compliant)
376
+ ~/.config/db-manager/config.toml # User-specific (uses slug)
377
+ Environment: DB_MANAGER___* # Env prefix (slug → uppercase + ___)
378
+ ```
379
+
380
+ **On macOS:**
381
+ ```
382
+ /Library/Application Support/Acme/DatabaseManager/config.toml # System-wide (vendor + app)
383
+ ~/Library/Application Support/Acme/DatabaseManager/config.toml # User-specific (vendor + app)
384
+ Environment: DB_MANAGER___* # Env prefix (slug → uppercase + ___)
385
+ ```
386
+
387
+ **On Windows:**
388
+ ```
389
+ C:\ProgramData\Acme\DatabaseManager\config.toml # System-wide (vendor + app)
390
+ %APPDATA%\Acme\DatabaseManager\config.toml # User-specific (vendor + app)
391
+ Environment: DB_MANAGER___* # Env prefix (slug → uppercase + ___)
392
+ ```
393
+
394
+ **With `profile="production"`:**
395
+
396
+ | Platform | Path |
397
+ |----------|------|
398
+ | Linux | `/etc/xdg/db-manager/profile/production/config.toml` |
399
+ | macOS | `/Library/Application Support/Acme/DatabaseManager/profile/production/config.toml` |
400
+ | Windows | `C:\ProgramData\Acme\DatabaseManager\profile\production\config.toml` |
401
+
402
+ ---
403
+
404
+ ### Why Four Identifiers?
405
+
406
+ **Different platforms have different conventions:**
407
+
408
+ - **Windows/macOS:** Prefer human-readable names with spaces and mixed case (`"Acme Corp"`, `"My Application"`)
409
+ - **Linux/UNIX:** Prefer lowercase with hyphens (`myapp`, `config-kit`)
410
+ - **Environment variables:** Must use uppercase with underscores (`MYAPP_`, `CONFIG_KIT_`)
411
+ - **Profiles:** Allow environment-specific configuration isolation (`test`, `staging`, `production`)
412
+
413
+ This library uses four identifiers so your application can follow **native conventions on each platform** while maintaining a **consistent configuration identity** and supporting **environment-specific configurations**.
414
+
415
+ ---
416
+
417
+ ### Quick Reference Table
418
+
419
+ | Identifier | Format | Example | Used In |
420
+ |------------|--------|---------|---------|
421
+ | **vendor** | ASCII, spaces allowed | `"Acme"`, `"Acme Corp"` | macOS, Windows paths |
422
+ | **app** | ASCII, spaces allowed | `"My App"`, `"Btx Fix Mcp"` | macOS, Windows paths |
423
+ | **slug** | lowercase-with-hyphens (recommended) | `"db-manager"` | Linux paths, env var prefix (becomes `DB_MANAGER___`) |
424
+ | **profile** | lowercase-with-hyphens (recommended) | `"production"` | Optional subdirectory for environment-specific configs |
425
+
426
+ **All identifiers are validated** to ensure cross-platform filesystem safety. See [Identifier Validation Rules](#identifier-validation-rules) below.
427
+
428
+ ---
429
+
430
+ ### Configuration Profiles
431
+
432
+ Profiles allow you to organize environment-specific configurations (e.g., `test`, `staging`, `production`) into isolated subdirectories. When a profile is specified, all configuration paths include a `profile/<name>/` segment.
433
+
434
+ #### How Profiles Work
435
+
436
+ **Without profile:**
437
+ ```
438
+ /etc/xdg/myapp/config.toml
439
+ /etc/xdg/myapp/hosts/server-01.toml
440
+ ~/.config/myapp/config.toml
441
+ ```
442
+
443
+ **With `profile="production"`:**
444
+ ```
445
+ /etc/xdg/myapp/profile/production/config.toml
446
+ /etc/xdg/myapp/profile/production/hosts/server-01.toml
447
+ ~/.config/myapp/profile/production/config.toml
448
+ ```
449
+
450
+ #### Using Profiles in Python
451
+
452
+ ```python
453
+ from lib_layered_config import read_config
454
+
455
+ # Load production configuration
456
+ config = read_config(
457
+ vendor="Acme",
458
+ app="ConfigKit",
459
+ slug="config-kit",
460
+ profile="production"
461
+ )
462
+
463
+ # Load test configuration
464
+ test_config = read_config(
465
+ vendor="Acme",
466
+ app="ConfigKit",
467
+ slug="config-kit",
468
+ profile="test"
469
+ )
470
+ ```
471
+
472
+ #### Using Profiles in CLI
473
+
474
+ ```bash
475
+ # Read configuration for production profile
476
+ lib_layered_config read --vendor Acme --app ConfigKit --slug config-kit --profile production
477
+
478
+ # Deploy configuration to production profile paths
479
+ lib_layered_config deploy --source config.toml --vendor Acme --app ConfigKit --slug config-kit --profile production --target app
480
+ ```
481
+
482
+ #### Profile Path Examples
483
+
484
+ | Platform | Without Profile | With `profile="test"` |
485
+ |----------|-----------------|----------------------|
486
+ | **Linux (app)** | `/etc/xdg/<slug>/config.toml` | `/etc/xdg/<slug>/profile/test/config.toml` |
487
+ | **Linux (host)** | `/etc/xdg/<slug>/hosts/<hostname>.toml` | `/etc/xdg/<slug>/profile/test/hosts/<hostname>.toml` |
488
+ | **Linux (user)** | `~/.config/<slug>/config.toml` | `~/.config/<slug>/profile/test/config.toml` |
489
+ | **macOS (app)** | `/Library/Application Support/<vendor>/<app>/config.toml` | `/Library/Application Support/<vendor>/<app>/profile/test/config.toml` |
490
+ | **Windows (app)** | `C:\ProgramData\<vendor>\<app>\config.toml` | `C:\ProgramData\<vendor>\<app>\profile\test\config.toml` |
491
+
492
+ #### Profile Naming Rules
493
+
494
+ Profile names follow the same validation as other identifiers (see below).
495
+
496
+ **Valid:** `test`, `production`, `staging-v2`, `dev_local`
497
+ **Invalid:** `../etc`, `.hidden`, `my profile`, `CON`
498
+
499
+ ---
500
+
501
+ ### Identifier Validation Rules
502
+
503
+ All identifiers are validated to ensure they are safe for use as filesystem directory names on both Windows and Linux.
504
+
505
+ #### Validation by Identifier Type
506
+
507
+ | Identifier | Spaces Allowed | Used For |
508
+ |------------|----------------|----------|
509
+ | **vendor** | ✅ Yes | macOS/Windows paths (`/Library/Application Support/Acme Corp/`) |
510
+ | **app** | ✅ Yes | macOS/Windows paths (`/Library/Application Support/.../My App/`) |
511
+ | **slug** | ❌ No | Linux paths, environment variable prefix |
512
+ | **profile** | ❌ No | Profile subdirectory name |
513
+ | **hostname** | ❌ No | Host-specific config files |
514
+
515
+ #### Common Validation Rules (All Identifiers)
516
+
517
+ | Rule | Description | Example Invalid Value |
518
+ |------|-------------|----------------------|
519
+ | **ASCII-only** | No Unicode/UTF-8 special characters | `café`, `日本語`, `app🚀` |
520
+ | **Must start with alphanumeric** | Cannot start with dot, hyphen, underscore, or space | `.hidden`, `-app`, `_private` |
521
+ | **No path separators** | Prevents path traversal attacks | `../etc`, `foo/bar`, `C:\Windows` |
522
+ | **No Windows-invalid chars** | `<`, `>`, `:`, `"`, `\|`, `?`, `*` are forbidden | `app<test>`, `file:name` |
523
+ | **No Windows reserved names** | CON, PRN, AUX, NUL, COM1-9, LPT1-9 | `CON`, `prn`, `NUL.txt` |
524
+ | **Cannot end with dot/space** | Windows restriction | `app.`, `name ` |
525
+
526
+ #### Examples
527
+
528
+ ```python
529
+ from lib_layered_config import read_config
530
+
531
+ # ✅ Valid identifiers
532
+ config = read_config(
533
+ vendor="Acme Corp", # OK: spaces allowed in vendor
534
+ app="Btx Fix Mcp", # OK: spaces allowed in app
535
+ slug="db-manager", # OK: lowercase with hyphens (no spaces)
536
+ profile="production" # OK: lowercase (no spaces)
537
+ )
538
+
539
+ # ❌ These will raise ValueError
540
+ read_config(vendor="../etc", ...) # Path traversal
541
+ read_config(app="café", ...) # Non-ASCII character
542
+ read_config(slug="CON", ...) # Windows reserved name
543
+ read_config(slug="my slug", ...) # Slug cannot have spaces
544
+ read_config(profile="my profile", ...) # Profile cannot have spaces
545
+ read_config(vendor=".hidden", ...) # Starts with dot
546
+ read_config(app="app<test>", ...) # Windows-invalid character
547
+ ```
548
+
549
+ ---
550
+
551
+ ## Configuration File Structure
552
+
553
+ Configuration files use TOML, JSON, or YAML format. Here's a comprehensive example showing how to structure your configuration:
554
+
555
+ ### Example Configuration File (TOML)
556
+
557
+ ```toml
558
+ # config.toml - Complete example showing all structural features
559
+
560
+ # ============================================================================
561
+ # TOP-LEVEL KEYS (Simple Values)
562
+ # ============================================================================
563
+ # Access in Python: config.get("debug")
564
+ # Access in CLI: config["debug"]
565
+ # Environment variable: MYAPP___DEBUG=true
566
+
567
+ debug = false
568
+ environment = "production"
569
+ version = "1.0.0"
570
+
571
+
572
+ # ============================================================================
573
+ # SECTIONS (Tables in TOML)
574
+ # ============================================================================
575
+ # Sections group related configuration values
576
+ # Access in Python: config.get("database.host")
577
+ # Environment variable: MYAPP___DATABASE__HOST=localhost
578
+
579
+ [database]
580
+ host = "localhost"
581
+ port = 5432
582
+ name = "myapp_db"
583
+ username = "admin"
584
+ # Passwords should come from environment variables or .env files!
585
+ # Set via: MYAPP___DATABASE__PASSWORD=secret
586
+
587
+
588
+ # ============================================================================
589
+ # NESTED SECTIONS (Subtables)
590
+ # ============================================================================
591
+ # Use dot notation for nested sections
592
+ # Access in Python: config.get("database.pool.size")
593
+ # Environment variable: MYAPP___DATABASE__POOL__SIZE=20
594
+
595
+ [database.pool]
596
+ size = 10
597
+ max_overflow = 20
598
+ timeout = 30
599
+ recycle = 3600 # seconds
600
+
601
+
602
+ [database.ssl]
603
+ enabled = true
604
+ verify = true
605
+ cert_path = "/etc/ssl/certs/db.pem"
606
+
607
+
608
+ # ============================================================================
609
+ # ARRAYS (Lists)
610
+ # ============================================================================
611
+ # Access in Python: config.get("database.replicas")
612
+ # Returns: ["replica1.db.local", "replica2.db.local"]
613
+
614
+ [database]
615
+ replicas = [
616
+ "replica1.db.local",
617
+ "replica2.db.local",
618
+ "replica3.db.local"
619
+ ]
620
+
621
+
622
+ # ============================================================================
623
+ # SERVICE CONFIGURATION
624
+ # ============================================================================
625
+
626
+ [service]
627
+ name = "MyApp Service"
628
+ host = "0.0.0.0"
629
+ port = 8080
630
+ timeout = 30
631
+ base_url = "https://api.example.com"
632
+
633
+
634
+ # Nested retry configuration
635
+ [service.retry]
636
+ max_attempts = 3
637
+ backoff_multiplier = 2
638
+ initial_delay = 1.0 # seconds
639
+
640
+
641
+ # Multiple endpoints
642
+ [service.endpoints]
643
+ api = "/api/v1"
644
+ health = "/health"
645
+ metrics = "/metrics"
646
+
647
+
648
+ # ============================================================================
649
+ # LOGGING CONFIGURATION
650
+ # ============================================================================
651
+
652
+ [logging]
653
+ level = "INFO" # DEBUG, INFO, WARNING, ERROR, CRITICAL
654
+ format = "json"
655
+ output = "stdout"
656
+
657
+
658
+ [logging.handlers]
659
+ console = true
660
+ file = true
661
+ syslog = false
662
+
663
+
664
+ [logging.files]
665
+ path = "/var/log/myapp/app.log"
666
+ max_bytes = 10485760 # 10 MB
667
+ backup_count = 5
668
+
669
+
670
+ # ============================================================================
671
+ # FEATURE FLAGS
672
+ # ============================================================================
673
+ # Use sections for grouping related feature flags
674
+
675
+ [features]
676
+ new_ui = false
677
+ experimental_api = false
678
+ beta_features = false
679
+
680
+
681
+ [features.analytics]
682
+ enabled = true
683
+ sampling_rate = 0.1 # 10% of requests
684
+
685
+
686
+ # ============================================================================
687
+ # API CONFIGURATION
688
+ # ============================================================================
689
+
690
+ [api]
691
+ rate_limit = 1000 # requests per minute
692
+ timeout = 60
693
+
694
+
695
+ [api.authentication]
696
+ type = "jwt" # jwt, oauth, apikey
697
+ token_expiry = 3600 # seconds
698
+
699
+
700
+ [api.cors]
701
+ enabled = true
702
+ allowed_origins = ["https://example.com", "https://app.example.com"]
703
+ allowed_methods = ["GET", "POST", "PUT", "DELETE"]
704
+
705
+
706
+ # ============================================================================
707
+ # CACHE CONFIGURATION
708
+ # ============================================================================
709
+
710
+ [cache]
711
+ backend = "redis" # redis, memcached, memory
712
+ default_ttl = 300 # seconds
713
+
714
+
715
+ [cache.redis]
716
+ host = "localhost"
717
+ port = 6379
718
+ db = 0
719
+ password = "" # Set via MYAPP___CACHE__REDIS__PASSWORD
720
+
721
+
722
+ # ============================================================================
723
+ # EMAIL CONFIGURATION
724
+ # ============================================================================
725
+
726
+ [email]
727
+ enabled = true
728
+ from_address = "noreply@example.com"
729
+ from_name = "MyApp"
730
+
731
+
732
+ [email.smtp]
733
+ host = "smtp.gmail.com"
734
+ port = 587
735
+ use_tls = true
736
+ username = "notifications@example.com"
737
+ # Password should come from environment: MYAPP___EMAIL__SMTP__PASSWORD
738
+
739
+
740
+ # ============================================================================
741
+ # MONITORING & OBSERVABILITY
742
+ # ============================================================================
743
+
744
+ [monitoring]
745
+ enabled = true
746
+
747
+
748
+ [monitoring.metrics]
749
+ backend = "prometheus"
750
+ port = 9090
751
+ path = "/metrics"
752
+
753
+
754
+ [monitoring.tracing]
755
+ enabled = true
756
+ backend = "jaeger"
757
+ sample_rate = 0.01 # 1% of requests
758
+
759
+
760
+ [monitoring.healthcheck]
761
+ enabled = true
762
+ interval = 30 # seconds
763
+ ```
764
+
765
+ ---
766
+
767
+ ### Understanding the Structure
768
+
769
+ #### 1. **Top-Level Keys**
770
+ Simple key-value pairs at the root level:
771
+ ```toml
772
+ debug = false
773
+ environment = "production"
774
+ ```
775
+ **Access:**
776
+ - Python: `config.get("debug")` or `config["debug"]`
777
+ - CLI: Value appears as `debug: false`
778
+ - Environment: `MYAPP___DEBUG=true`
779
+
780
+ ---
781
+
782
+ #### 2. **Sections (Tables)**
783
+ Sections group related configuration:
784
+ ```toml
785
+ [database]
786
+ host = "localhost"
787
+ port = 5432
788
+ ```
789
+ **Access:**
790
+ - Python: `config.get("database.host")` → `"localhost"`
791
+ - Python: `config["database"]` → `{"host": "localhost", "port": 5432}`
792
+ - Environment: `MYAPP___DATABASE__HOST=postgres.local`
793
+
794
+ ---
795
+
796
+ #### 3. **Nested Sections (Subtables)**
797
+ Use dot notation for deeper nesting:
798
+ ```toml
799
+ [database.pool]
800
+ size = 10
801
+ timeout = 30
802
+
803
+ [database.ssl]
804
+ enabled = true
805
+ verify = true
806
+ ```
807
+ **Access:**
808
+ - Python: `config.get("database.pool.size")` → `10`
809
+ - Python: `config.get("database.ssl.enabled")` → `true`
810
+ - Environment: `MYAPP___DATABASE__POOL__SIZE=20`
811
+ - Environment: `MYAPP___DATABASE__SSL__ENABLED=false`
812
+
813
+ **Structure visualization:**
814
+ ```python
815
+ {
816
+ "database": {
817
+ "host": "localhost",
818
+ "port": 5432,
819
+ "pool": {
820
+ "size": 10,
821
+ "timeout": 30
822
+ },
823
+ "ssl": {
824
+ "enabled": true,
825
+ "verify": true
826
+ }
827
+ }
828
+ }
829
+ ```
830
+
831
+ ---
832
+
833
+ #### 4. **Arrays (Lists)**
834
+ Multiple values in a list:
835
+ ```toml
836
+ [database]
837
+ replicas = [
838
+ "replica1.db.local",
839
+ "replica2.db.local"
840
+ ]
841
+
842
+ [api.cors]
843
+ allowed_origins = ["https://example.com", "https://app.example.com"]
844
+ ```
845
+ **Access:**
846
+ - Python: `config.get("database.replicas")` → `["replica1.db.local", "replica2.db.local"]`
847
+ - Python: `config.get("database.replicas")[0]` → `"replica1.db.local"`
848
+
849
+ ---
850
+
851
+ #### 5. **Data Types**
852
+
853
+ TOML supports multiple data types:
854
+
855
+ ```toml
856
+ # Strings
857
+ name = "MyApp"
858
+ host = "localhost"
859
+
860
+ # Integers
861
+ port = 8080
862
+ timeout = 30
863
+
864
+ # Floats
865
+ sample_rate = 0.1
866
+ backoff_multiplier = 2.5
867
+
868
+ # Booleans
869
+ debug = false
870
+ enabled = true
871
+
872
+ # Arrays
873
+ replicas = ["host1", "host2"]
874
+ allowed_methods = ["GET", "POST"]
875
+
876
+ # Dates/Times (TOML feature)
877
+ created_at = 2024-01-15T10:30:00Z
878
+ ```
879
+
880
+ ---
881
+
882
+ ### Complete Python Access Example
883
+
884
+ Given the example configuration above, here's how to access values:
885
+
886
+ ```python
887
+ from lib_layered_config import read_config
888
+
889
+ config = read_config(vendor="Acme", app="MyApp", slug="myapp")
890
+
891
+ # Top-level keys
892
+ debug = config.get("debug") # false
893
+ env = config.get("environment") # "production"
894
+
895
+ # Section values
896
+ db_host = config.get("database.host") # "localhost"
897
+ db_port = config.get("database.port") # 5432
898
+
899
+ # Nested section values
900
+ pool_size = config.get("database.pool.size") # 10
901
+ ssl_enabled = config.get("database.ssl.enabled") # true
902
+
903
+ # Deep nesting
904
+ retry_attempts = config.get("service.retry.max_attempts") # 3
905
+ smtp_port = config.get("email.smtp.port") # 587
906
+
907
+ # Arrays
908
+ replicas = config.get("database.replicas") # ["replica1.db.local", ...]
909
+ first_replica = config.get("database.replicas")[0] # "replica1.db.local"
910
+
911
+ # Feature flags
912
+ new_ui_enabled = config.get("features.new_ui") # false
913
+ analytics_rate = config.get("features.analytics.sampling_rate") # 0.1
914
+
915
+ # With defaults
916
+ api_timeout = config.get("api.timeout", default=60) # 60
917
+ cache_ttl = config.get("cache.default_ttl", default=300) # 300
918
+ ```
919
+
920
+ ---
921
+
922
+ ### Environment Variable Mapping
923
+
924
+ The slug is converted to an environment prefix (uppercase with triple underscore `___` separator), and nested keys use double underscores (`__`):
925
+
926
+ ```bash
927
+ # Slug: "myapp" → Prefix: "MYAPP___"
928
+
929
+ # Top-level keys
930
+ MYAPP___DEBUG=true
931
+ MYAPP___ENVIRONMENT=staging
932
+
933
+ # Section keys (triple underscore after prefix, double for nesting)
934
+ MYAPP___DATABASE__HOST=postgres.production.local
935
+ MYAPP___DATABASE__PORT=5433
936
+
937
+ # Nested sections (each level separated by __)
938
+ MYAPP___DATABASE__POOL__SIZE=50
939
+ MYAPP___DATABASE__SSL__ENABLED=true
940
+
941
+ # Deep nesting
942
+ MYAPP___SERVICE__RETRY__MAX_ATTEMPTS=5
943
+ MYAPP___EMAIL__SMTP__PASSWORD=secret123
944
+ MYAPP___FEATURES__ANALYTICS__SAMPLING_RATE=0.5
945
+
946
+ # Arrays (JSON format for complex types)
947
+ MYAPP___DATABASE__REPLICAS='["replica1", "replica2"]'
948
+ ```
949
+
950
+ **Key Pattern:**
951
+ - Prefix: `SLUG` in uppercase followed by `___`
952
+ - Separator: Triple underscore (`___`) after prefix to distinguish from nesting
953
+ - Nesting: Double underscores (`__`) for each level
954
+ - Format: `PREFIX___SECTION__SUBSECTION__KEY=value`
955
+
956
+ ---
957
+
958
+ ### JSON and YAML Equivalents
959
+
960
+ The same structure in JSON:
961
+
962
+ ```json
963
+ {
964
+ "debug": false,
965
+ "environment": "production",
966
+ "database": {
967
+ "host": "localhost",
968
+ "port": 5432,
969
+ "pool": {
970
+ "size": 10,
971
+ "timeout": 30
972
+ },
973
+ "ssl": {
974
+ "enabled": true,
975
+ "verify": true
976
+ },
977
+ "replicas": ["replica1.db.local", "replica2.db.local"]
978
+ },
979
+ "service": {
980
+ "host": "0.0.0.0",
981
+ "port": 8080,
982
+ "retry": {
983
+ "max_attempts": 3,
984
+ "backoff_multiplier": 2
985
+ }
986
+ }
987
+ }
988
+ ```
989
+
990
+ And in YAML:
991
+
992
+ ```yaml
993
+ debug: false
994
+ environment: production
995
+
996
+ database:
997
+ host: localhost
998
+ port: 5432
999
+ pool:
1000
+ size: 10
1001
+ timeout: 30
1002
+ ssl:
1003
+ enabled: true
1004
+ verify: true
1005
+ replicas:
1006
+ - replica1.db.local
1007
+ - replica2.db.local
1008
+
1009
+ service:
1010
+ host: 0.0.0.0
1011
+ port: 8080
1012
+ retry:
1013
+ max_attempts: 3
1014
+ backoff_multiplier: 2
1015
+ ```
1016
+
1017
+ All three formats produce the same configuration structure and can be accessed identically through the library.
1018
+
1019
+ ---
1020
+
1021
+ ## Configuration Sources & Precedence
1022
+
1023
+ Later layers override earlier ones **per key** while leaving unrelated keys untouched.
1024
+
1025
+ | Precedence | Layer | Description |
1026
+ | ---------- | ----------- | ----------- |
1027
+ | 0 | `defaults` | Optional baseline file provided via the API/CLI `--default-file` flag |
1028
+ | 1 | `app` | System-wide defaults (e.g. `/etc/<slug>/…`) |
1029
+ | 2 | `host` | Machine-specific overrides (`hosts/<hostname>.toml`) |
1030
+ | 3 | `user` | Per-user settings (XDG, Application Support, AppData) |
1031
+ | 4 | `dotenv` | First `.env` found via upward search plus platform extras |
1032
+ | 5 | `env` | Process environment with namespacing and `__` nesting |
1033
+
1034
+ Use the optional defaults layer when you want one explicitly-provided file to seed configuration before host/user overrides apply.
1035
+
1036
+ Important directories (overridable via environment variables):
1037
+
1038
+ ### Linux
1039
+ - `/etc/xdg/<slug>/config.toml` (XDG system-wide, checked first)
1040
+ - `/etc/xdg/<slug>/config.d/*.{toml,json,yaml,yml}`
1041
+ - `/etc/<slug>/config.toml` (legacy fallback)
1042
+ - `/etc/<slug>/config.d/*.{toml,json,yaml,yml}`
1043
+ - `/etc/xdg/<slug>/hosts/<hostname>.toml` or `/etc/<slug>/hosts/<hostname>.toml`
1044
+ - `$XDG_CONFIG_HOME/<slug>/config.toml` (user; falls back to `~/.config/<slug>/config.toml`)
1045
+ - `$XDG_CONFIG_HOME/<slug>/config.d/*.{toml,json,yaml,yml}`
1046
+ - `.env` search: current directory upwards + `$XDG_CONFIG_HOME/<slug>/.env`
1047
+
1048
+ ### macOS
1049
+ - `/Library/Application Support/<Vendor>/<App>/config.toml` (system-wide app layer)
1050
+ - `/Library/Application Support/<Vendor>/<App>/config.d/*.{toml,json,yaml,yml}`
1051
+ - `/Library/Application Support/<Vendor>/<App>/hosts/<hostname>.toml`
1052
+ - `~/Library/Application Support/<Vendor>/<App>/config.toml` (user layer)
1053
+ - `~/Library/Application Support/<Vendor>/<App>/config.d/*.{toml,json,yaml,yml}`
1054
+ - `.env` search: current directory upwards + `~/Library/Application Support/<Vendor>/<App>/.env`
1055
+
1056
+ ### Windows
1057
+ - `%ProgramData%\<Vendor>\<App>\config.toml` (system-wide app layer)
1058
+ - `%ProgramData%\<Vendor>\<App>\config.d\*.{toml,json,yaml,yml}`
1059
+ - `%ProgramData%\<Vendor>\<App>\hosts\%COMPUTERNAME%.toml`
1060
+ - `%APPDATA%\<Vendor>\<App>\config.toml` (user layer; resolver order: `LIB_LAYERED_CONFIG_APPDATA` → `%APPDATA%`; falls back to `%LOCALAPPDATA%`)
1061
+ - `%APPDATA%\<Vendor>\<App>\config.d\*.{toml,json,yaml,yml}`
1062
+ - `.env` search: current directory upwards + `%APPDATA%\<Vendor>\<App>\.env`
1063
+
1064
+ Environment overrides: `LIB_LAYERED_CONFIG_ETC`, `LIB_LAYERED_CONFIG_PROGRAMDATA`, `LIB_LAYERED_CONFIG_APPDATA`, `LIB_LAYERED_CONFIG_LOCALAPPDATA`, `LIB_LAYERED_CONFIG_MAC_APP_ROOT`, `LIB_LAYERED_CONFIG_MAC_HOME_ROOT`. Both the runtime readers and the `deploy` helper honour these variables so generated files land in the same directories that `read_config` inspects.
1065
+
1066
+ **Fallback note:** Whenever a path is marked as a fallback, the resolver first consults the documented environment overrides (`LIB_LAYERED_CONFIG_*`, `$XDG_CONFIG_HOME`, `%APPDATA%`, etc.). If those variables are unset or the computed directory does not exist, it switches to the stated fallback location (`~/.config`, `%LOCALAPPDATA%`, ...). This keeps local installs working without additional environment configuration while still allowing operators to steer resolution explicitly.
1067
+
1068
+ ### The `config.d` Directory
1069
+
1070
+ Each layer can include a `config.d/` directory for split configuration files. This follows the common Linux pattern (similar to `/etc/apt/sources.list.d/` or `/etc/sudoers.d/`).
1071
+
1072
+ **How it works:**
1073
+ 1. The resolver first loads `config.toml` (if present)
1074
+ 2. Then loads all files from `config.d/` in **lexicographic order**
1075
+ 3. Only files with supported extensions are loaded: `.toml`, `.json`, `.yaml`, `.yml`
1076
+ 4. Files are merged in order, so later files override earlier ones
1077
+
1078
+ **Naming convention:** Use numeric prefixes to control ordering:
1079
+ ```
1080
+ config.d/
1081
+ ├── 10-base.toml # Loaded first
1082
+ ├── 20-database.toml # Loaded second
1083
+ ├── 30-logging.toml # Loaded third
1084
+ └── 99-overrides.toml # Loaded last (highest precedence within config.d)
1085
+ ```
1086
+
1087
+ **Use cases:**
1088
+ - **Package managers** can drop configuration snippets without modifying the main file
1089
+ - **Automation tools** can add/remove specific settings independently
1090
+ - **Team workflows** can split configuration by concern (database, logging, features)
1091
+
1092
+ **Example:**
1093
+ ```bash
1094
+ # Main config defines defaults
1095
+ /etc/myapp/config.toml:
1096
+ [database]
1097
+ host = "localhost"
1098
+ port = 5432
1099
+
1100
+ # Ops team adds production overrides
1101
+ /etc/myapp/config.d/50-production.toml:
1102
+ [database]
1103
+ host = "db.prod.example.com"
1104
+ pool_size = 20
1105
+
1106
+ # Result: database.host = "db.prod.example.com", database.port = 5432, database.pool_size = 20
1107
+ ```
1108
+
1109
+ ## CLI Usage
1110
+
1111
+ ### Command Summary
1112
+
1113
+ | Command | Description |
1114
+ |----------------------------------------|-------------------------------------------------------|
1115
+ | `lib_layered_config read` | Load configuration (human readable by default) |
1116
+ | `lib_layered_config read-json` | Emit config + provenance JSON envelope |
1117
+ | `lib_layered_config deploy` | Copy a source file into one or more layer directories |
1118
+ | `lib_layered_config generate-examples` | Scaffold example trees (POSIX/Windows layouts) |
1119
+ | `lib_layered_config env-prefix` | Compute the canonical environment prefix |
1120
+ | `lib_layered_config info` | Print package metadata |
1121
+ | `lib_layered_config fail` | Intentionally raise a `RuntimeError` (for testing) |
1122
+
1123
+ ---
1124
+
1125
+ ### `read`
1126
+
1127
+ Load configuration and print either human-readable prose or JSON.
1128
+
1129
+ **Usage:**
1130
+ ```bash
1131
+ lib_layered_config read --vendor Acme --app ConfigKit --slug config-kit \
1132
+ [--prefer toml] [--prefer json] \
1133
+ [--start-dir /path/to/project] \
1134
+ [--default-file ./config.defaults.toml] \
1135
+ [--format human|json] \
1136
+ [--indent | --no-indent] \
1137
+ [--provenance | --no-provenance]
1138
+ ```
1139
+
1140
+ **Parameters:**
1141
+
1142
+ | Parameter | Type | Required | Default | Description |
1143
+ |-----------|------|----------|---------|-------------|
1144
+ | `--vendor` | string | Yes | - | Vendor namespace used to compute filesystem paths |
1145
+ | `--app` | string | Yes | - | Application name used to compute filesystem paths |
1146
+ | `--slug` | string | Yes | - | Configuration slug for file paths and environment prefix |
1147
+ | `--prefer` | string | No | None | Preferred file suffix (repeatable flag: `--prefer toml --prefer json`). Earlier values take precedence. Valid values: `toml`, `json`, `yaml`, `yml` |
1148
+ | `--start-dir` | path | No | current dir | Starting directory for upward `.env` file search. Must be an existing directory |
1149
+ | `--default-file` | path | No | None | Path to lowest-precedence defaults file. Must be an existing file |
1150
+ | `--format` | choice | No | `human` | Output format. Valid values: `human` (annotated prose), `json` (structured JSON) |
1151
+ | `--indent` / `--no-indent` | flag | No | `--indent` | Pretty-print JSON output with indentation. Only applies when `--format json` |
1152
+ | `--provenance` / `--no-provenance` | flag | No | `--provenance` | Include provenance metadata in JSON output. Only applies when `--format json` |
1153
+
1154
+ **Examples:**
1155
+
1156
+ **Example 1: Basic configuration inspection (human-readable)**
1157
+ ```bash
1158
+ # Load and display configuration in human-readable format
1159
+ lib_layered_config read --vendor Acme --app MyApp --slug myapp
1160
+ ```
1161
+
1162
+ **Output:**
1163
+ ```
1164
+ service.timeout: 30
1165
+ provenance: layer=app, path=/etc/xdg/myapp/config.toml
1166
+ service.endpoint: https://api.example.com
1167
+ provenance: layer=user, path=/home/alice/.config/myapp/config.toml
1168
+ database.host: localhost
1169
+ provenance: layer=env, path=None
1170
+ database.port: 5432
1171
+ provenance: layer=app, path=/etc/xdg/myapp/config.toml
1172
+ ```
1173
+
1174
+ **Explanation:** The default format shows each configuration value with its source layer and file path (or "None" for environment variables). Perfect for quick debugging.
1175
+
1176
+ **Example 2: JSON output for automation scripts**
1177
+ ```bash
1178
+ # Get configuration as JSON for use in shell scripts
1179
+ config_json=$(lib_layered_config read \
1180
+ --vendor Acme --app MyApp --slug myapp \
1181
+ --format json --no-provenance --no-indent)
1182
+
1183
+ # Parse with jq
1184
+ echo "$config_json" | jq -r '.database.host'
1185
+ # Output: localhost
1186
+ ```
1187
+
1188
+ **Explanation:** Use `--format json --no-provenance --no-indent` to get just the configuration values as compact JSON, perfect for piping to `jq` or other JSON processors.
1189
+
1190
+ **Example 3: Full audit with provenance (JSON)**
1191
+ ```bash
1192
+ # Get both configuration and provenance metadata
1193
+ lib_layered_config read \
1194
+ --vendor Acme --app MyApp --slug myapp \
1195
+ --format json --provenance --indent > config-audit.json
1196
+
1197
+ # View the structure
1198
+ cat config-audit.json
1199
+ ```
1200
+
1201
+ **Output:**
1202
+ ```json
1203
+ {
1204
+ "config": {
1205
+ "service": {
1206
+ "timeout": 30,
1207
+ "endpoint": "https://api.example.com"
1208
+ },
1209
+ "database": {
1210
+ "host": "localhost",
1211
+ "port": 5432
1212
+ }
1213
+ },
1214
+ "provenance": {
1215
+ "service.timeout": {
1216
+ "layer": "app",
1217
+ "path": "/etc/xdg/myapp/config.toml",
1218
+ "key": "service.timeout"
1219
+ },
1220
+ "service.endpoint": {
1221
+ "layer": "user",
1222
+ "path": "/home/alice/.config/myapp/config.toml",
1223
+ "key": "service.endpoint"
1224
+ },
1225
+ "database.host": {
1226
+ "layer": "env",
1227
+ "path": null,
1228
+ "key": "database.host"
1229
+ }
1230
+ }
1231
+ }
1232
+ ```
1233
+
1234
+ **Explanation:** This gives you complete audit information - both the final configuration values and where each one came from.
1235
+
1236
+ **Example 4: Using file format preferences**
1237
+ ```bash
1238
+ # Prefer TOML files, then JSON, then YAML
1239
+ lib_layered_config read \
1240
+ --vendor Acme --app MyApp --slug myapp \
1241
+ --prefer toml --prefer json --prefer yaml
1242
+ ```
1243
+
1244
+ **Explanation:** When multiple configuration file formats exist in the same directory (e.g., `config.toml` and `config.json`), the `--prefer` flag controls which one takes precedence. Earlier values win.
1245
+
1246
+ **Example 5: Load with defaults and specific .env location**
1247
+ ```bash
1248
+ # Load configuration with shipped defaults and project-specific .env
1249
+ lib_layered_config read \
1250
+ --vendor Acme --app MyApp --slug myapp \
1251
+ --default-file ./config/defaults.toml \
1252
+ --start-dir /opt/myapp \
1253
+ --format human
1254
+ ```
1255
+
1256
+ **Explanation:** Use `--default-file` to provide application defaults that ship with your app, and `--start-dir` to specify where to start searching for `.env` files (useful when running from a different directory).
1257
+
1258
+ **Example 6: Debugging configuration issues**
1259
+ ```bash
1260
+ # Check if environment variables are overriding your config
1261
+ MYAPP___SERVICE__TIMEOUT=5 lib_layered_config read \
1262
+ --vendor Acme --app MyApp --slug myapp \
1263
+ --format human | grep -A1 "service.timeout"
1264
+ ```
1265
+
1266
+ **Output:**
1267
+ ```
1268
+ service.timeout: 5
1269
+ provenance: layer=env, path=None
1270
+ ```
1271
+
1272
+ **Explanation:** Set environment variables before the command to test how they override file-based configuration. The provenance shows which layer won.
1273
+
1274
+ ---
1275
+
1276
+ ### `read-json`
1277
+
1278
+ Always emit combined JSON output (config + provenance). This is a convenience alias for `read --format json --provenance`.
1279
+
1280
+ **Usage:**
1281
+ ```bash
1282
+ lib_layered_config read-json --vendor Acme --app ConfigKit --slug config-kit \
1283
+ [--prefer toml] [--prefer json] \
1284
+ [--start-dir /path/to/project] \
1285
+ [--default-file ./config.defaults.toml] \
1286
+ [--indent | --no-indent]
1287
+ ```
1288
+
1289
+ **Parameters:**
1290
+
1291
+ | Parameter | Type | Required | Default | Description |
1292
+ |-----------|------|----------|---------|-------------|
1293
+ | `--vendor` | string | Yes | - | Vendor namespace |
1294
+ | `--app` | string | Yes | - | Application name |
1295
+ | `--slug` | string | Yes | - | Configuration slug |
1296
+ | `--prefer` | string | No | None | Preferred file suffix (repeatable) |
1297
+ | `--start-dir` | path | No | current dir | Starting directory for `.env` search |
1298
+ | `--default-file` | path | No | None | Path to defaults file |
1299
+ | `--indent` / `--no-indent` | flag | No | `--indent` | Pretty-print JSON output |
1300
+
1301
+ **Example:**
1302
+ ```bash
1303
+ lib_layered_config read-json --vendor Acme --app ConfigKit --slug config-kit --indent
1304
+ ```
1305
+
1306
+ ---
1307
+
1308
+ ### `deploy`
1309
+
1310
+ Copy a source configuration file into one or more layer directories.
1311
+
1312
+ **Usage:**
1313
+ ```bash
1314
+ lib_layered_config deploy --source ./config/app.toml \
1315
+ --vendor Acme --app ConfigKit --slug config-kit \
1316
+ --target app [--target host] [--target user] \
1317
+ [--profile production] \
1318
+ [--platform linux|darwin|windows] \
1319
+ [--force | --no-force]
1320
+ ```
1321
+
1322
+ **Parameters:**
1323
+
1324
+ | Parameter | Type | Required | Default | Description |
1325
+ |-----------|------|----------|---------|-------------|
1326
+ | `--source` | path | Yes | - | Path to the configuration file to copy. Must be an existing file |
1327
+ | `--vendor` | string | Yes | - | Vendor namespace |
1328
+ | `--app` | string | Yes | - | Application name |
1329
+ | `--slug` | string | Yes | - | Configuration slug |
1330
+ | `--profile` | string | No | - | Configuration profile name (e.g., `test`, `production`). Adds `profile/<name>/` segment to deployment paths |
1331
+ | `--target` | choice | Yes | - | Layer targets to deploy to (repeatable flag). Valid values: `app`, `host`, `user`. Can specify multiple: `--target app --target user` |
1332
+ | `--platform` | string | No | auto-detect | Override platform. Valid values: `linux`, `darwin`, `windows`, or any string starting with `win` |
1333
+ | `--force` / `--no-force` | flag | No | `--no-force` | Overwrite existing files at destinations |
1334
+
1335
+ **Returns:** JSON array of file paths created or overwritten.
1336
+
1337
+ **Profile Examples:**
1338
+ ```bash
1339
+ # Deploy to production profile
1340
+ lib_layered_config deploy --source ./configs/prod.toml \
1341
+ --vendor Acme --app MyApp --slug myapp \
1342
+ --profile production --target app
1343
+ # Linux: /etc/xdg/myapp/profile/production/config.toml
1344
+
1345
+ # Deploy to test profile
1346
+ lib_layered_config deploy --source ./configs/test.toml \
1347
+ --vendor Acme --app MyApp --slug myapp \
1348
+ --profile test --target app --target user
1349
+ # Linux: /etc/xdg/myapp/profile/test/config.toml
1350
+ # ~/.config/myapp/profile/test/config.toml
1351
+ ```
1352
+
1353
+ ---
1354
+
1355
+ ### 🔒 File Overwrite Behavior
1356
+
1357
+ The `deploy` command has **safe-by-default** behavior to prevent accidental data loss:
1358
+
1359
+ #### **Default Behavior (without `--force`):**
1360
+ - ✅ **Creates new files** if they don't exist
1361
+ - ❌ **Skips existing files** - will NOT overwrite
1362
+ - 📋 Returns empty array `[]` or partial array if some files were skipped
1363
+ - 🛡️ **Protects user customizations** from being accidentally overwritten
1364
+
1365
+ ```bash
1366
+ # First deployment - creates file
1367
+ lib_layered_config deploy --source ./config.toml \
1368
+ --vendor Acme --app MyApp --slug myapp --target user
1369
+ # Output: ["/home/alice/.config/myapp/config.toml"]
1370
+
1371
+ # Second deployment (same command) - skips existing file
1372
+ lib_layered_config deploy --source ./config.toml \
1373
+ --vendor Acme --app MyApp --slug myapp --target user
1374
+ # Output: [] ← File already exists, not overwritten
1375
+ ```
1376
+
1377
+ #### **With `--force` Flag:**
1378
+ - ✅ **Creates new files** if they don't exist
1379
+ - ✅ **Overwrites existing files** without warning
1380
+ - 📋 Returns array of all files created/overwritten
1381
+ - ⚠️ **Use with caution** - existing content will be lost
1382
+
1383
+ ```bash
1384
+ # Force overwrite existing files
1385
+ lib_layered_config deploy --source ./config.toml \
1386
+ --vendor Acme --app MyApp --slug myapp --target user --force
1387
+ # Output: ["/home/alice/.config/myapp/config.toml"] ← Overwritten
1388
+ ```
1389
+
1390
+ ---
1391
+
1392
+ ### Decision Flow Diagram
1393
+
1394
+ ```
1395
+ ┌─────────────────────────────────┐
1396
+ │ lib_layered_config deploy │
1397
+ │ --source config.toml │
1398
+ │ --target user │
1399
+ └────────────┬────────────────────┘
1400
+
1401
+
1402
+ ┌────────────────────┐
1403
+ │ Does destination │
1404
+ │ file exist? │
1405
+ └────┬──────────┬────┘
1406
+ │ │
1407
+ YES│ │NO
1408
+ │ │
1409
+ ▼ ▼
1410
+ ┌──────────┐ ┌──────────────┐
1411
+ │ --force │ │ Create file │
1412
+ │ flag? │ │ ✅ │
1413
+ └─┬──────┬─┘ └──────────────┘
1414
+ │ │
1415
+ YES│ │NO
1416
+ │ │
1417
+ ▼ ▼
1418
+ ┌─────┐ ┌──────────────┐
1419
+ │Over-│ │ Skip file │
1420
+ │write│ │ Return [] │
1421
+ │✅ │ │ ❌ │
1422
+ └─────┘ └──────────────┘
1423
+ ```
1424
+
1425
+ ---
1426
+
1427
+ ### Practical Scenarios
1428
+
1429
+ #### **Scenario 1: Initial Installation (Safe)**
1430
+ ```bash
1431
+ # First time deploying - no files exist yet
1432
+ sudo lib_layered_config deploy \
1433
+ --source ./dist/config.toml \
1434
+ --vendor Acme --app MyApp --slug myapp \
1435
+ --target app
1436
+
1437
+ # ✅ Result: File created
1438
+ # Output: ["/etc/xdg/myapp/config.toml"]
1439
+ ```
1440
+
1441
+ #### **Scenario 2: User Has Customizations (Protected)**
1442
+ ```bash
1443
+ # User has already customized their config
1444
+ # Try to deploy again without --force
1445
+ lib_layered_config deploy \
1446
+ --source ./new-defaults.toml \
1447
+ --vendor Acme --app MyApp --slug myapp \
1448
+ --target user
1449
+
1450
+ # ❌ Result: File skipped (user's customizations preserved)
1451
+ # Output: []
1452
+ ```
1453
+
1454
+ #### **Scenario 3: Update During Upgrade (Intentional Overwrite)**
1455
+ ```bash
1456
+ # Major version upgrade - want to reset to new defaults
1457
+ lib_layered_config deploy \
1458
+ --source ./v2-config.toml \
1459
+ --vendor Acme --app MyApp --slug myapp \
1460
+ --target user \
1461
+ --force
1462
+
1463
+ # ⚠️ Result: File overwritten with new version
1464
+ # Output: ["/home/alice/.config/myapp/config.toml"]
1465
+ # User's customizations are LOST - they should back up first!
1466
+ ```
1467
+
1468
+ #### **Scenario 4: Multiple Targets (Mixed Result)**
1469
+ ```bash
1470
+ # Deploy to both app and user
1471
+ # App directory is empty, user directory has existing config
1472
+ lib_layered_config deploy \
1473
+ --source ./config.toml \
1474
+ --vendor Acme --app MyApp --slug myapp \
1475
+ --target app --target user
1476
+
1477
+ # 📋 Result: App created, user skipped
1478
+ # Output: ["/etc/xdg/myapp/config.toml"]
1479
+ # Note: User config not in output because it was skipped
1480
+ ```
1481
+
1482
+ ---
1483
+
1484
+ ### Best Practices
1485
+
1486
+ #### ✅ **DO:**
1487
+
1488
+ 1. **Test first without `--force`:**
1489
+ ```bash
1490
+ # See what would be deployed
1491
+ lib_layered_config deploy --source ./config.toml \
1492
+ --vendor Acme --app MyApp --slug myapp --target user
1493
+
1494
+ # Empty output? Files exist. Check them before using --force
1495
+ ```
1496
+
1497
+ 2. **Use `--force` only when necessary:**
1498
+ - During clean installations
1499
+ - After backing up existing configs
1500
+ - When intentionally resetting to defaults
1501
+
1502
+ 3. **Backup before force-deploying:**
1503
+ ```bash
1504
+ # Backup user config before overwriting
1505
+ cp ~/.config/myapp/config.toml ~/.config/myapp/config.toml.backup
1506
+
1507
+ # Now safe to force deploy
1508
+ lib_layered_config deploy --source ./new-config.toml \
1509
+ --vendor Acme --app MyApp --slug myapp --target user --force
1510
+ ```
1511
+
1512
+ 4. **Document in installation scripts:**
1513
+ ```bash
1514
+ #!/bin/bash
1515
+ # Installation script
1516
+
1517
+ echo "Deploying system-wide defaults..."
1518
+ sudo lib_layered_config deploy \
1519
+ --source ./defaults.toml \
1520
+ --vendor Acme --app MyApp --slug myapp \
1521
+ --target app
1522
+
1523
+ echo "Note: User configurations preserved."
1524
+ echo "To reset user config: add --force flag"
1525
+ ```
1526
+
1527
+ #### ❌ **DON'T:**
1528
+
1529
+ 1. **Don't use `--force` in automated scripts without user confirmation:**
1530
+ ```bash
1531
+ # BAD: Might destroy user customizations
1532
+ lib_layered_config deploy --source ./config.toml \
1533
+ --target user --force # ⚠️ Dangerous!
1534
+
1535
+ # GOOD: Prompt user first
1536
+ read -p "Overwrite existing config? (y/N): " confirm
1537
+ if [ "$confirm" = "y" ]; then
1538
+ lib_layered_config deploy --source ./config.toml \
1539
+ --target user --force
1540
+ fi
1541
+ ```
1542
+
1543
+ 2. **Don't assume empty output means failure:**
1544
+ ```bash
1545
+ # Check if command succeeded even with empty output
1546
+ result=$(lib_layered_config deploy --source config.toml --target user)
1547
+
1548
+ # Empty array means files were skipped, not an error!
1549
+ if [ "$result" = "[]" ]; then
1550
+ echo "Files already exist (not overwritten)"
1551
+ fi
1552
+ ```
1553
+
1554
+ ---
1555
+
1556
+ ### Python API Equivalent
1557
+
1558
+ The Python `deploy_config()` function has the same behavior:
1559
+
1560
+ ```python
1561
+ from lib_layered_config import deploy_config
1562
+
1563
+ # Safe by default - won't overwrite
1564
+ paths = deploy_config(
1565
+ source="./config.toml",
1566
+ vendor="Acme",
1567
+ app="MyApp",
1568
+ targets=["user"],
1569
+ slug="myapp",
1570
+ force=False # Default
1571
+ )
1572
+
1573
+ if not paths:
1574
+ print("File already exists and was not overwritten")
1575
+ print("Use force=True to overwrite")
1576
+ else:
1577
+ print(f"Deployed to: {paths}")
1578
+
1579
+ # Force overwrite
1580
+ paths = deploy_config(
1581
+ source="./config.toml",
1582
+ vendor="Acme",
1583
+ app="MyApp",
1584
+ targets=["user"],
1585
+ slug="myapp",
1586
+ force=True # Overwrites existing files
1587
+ )
1588
+ ```
1589
+
1590
+ **Examples:**
1591
+
1592
+ **Example 1: Deploy system-wide defaults during installation**
1593
+ ```bash
1594
+ # Deploy app defaults to the system directory (requires sudo on Linux/macOS)
1595
+ sudo lib_layered_config deploy \
1596
+ --source ./dist/config.toml \
1597
+ --vendor Acme --app MyApp --slug myapp \
1598
+ --target app
1599
+ ```
1600
+
1601
+ **Output:**
1602
+ ```json
1603
+ ["/etc/xdg/myapp/config.toml"]
1604
+ ```
1605
+
1606
+ **Explanation:** This copies your configuration file to the system-wide location (`/etc/xdg/myapp/config.toml` on Linux, `/Library/Application Support/Acme/MyApp/config.toml` on macOS, etc.). This is typically done during package installation.
1607
+
1608
+ **Example 2: Deploy user-specific configuration**
1609
+ ```bash
1610
+ # Deploy user config (no sudo needed)
1611
+ lib_layered_config deploy \
1612
+ --source ./my-preferences.toml \
1613
+ --vendor Acme --app MyApp --slug myapp \
1614
+ --target user
1615
+ ```
1616
+
1617
+ **Output:**
1618
+ ```json
1619
+ ["/home/alice/.config/myapp/config.toml"]
1620
+ ```
1621
+
1622
+ **Explanation:** Deploys configuration to the current user's config directory. Great for user onboarding or preference templates.
1623
+
1624
+ **Example 3: Deploy to multiple layers**
1625
+ ```bash
1626
+ # Deploy base configuration to both system and user levels
1627
+ lib_layered_config deploy \
1628
+ --source ./config/base.toml \
1629
+ --vendor Acme --app MyApp --slug myapp \
1630
+ --target app --target user \
1631
+ --force
1632
+ ```
1633
+
1634
+ **Output:**
1635
+ ```json
1636
+ [
1637
+ "/etc/xdg/myapp/config.toml",
1638
+ "/home/alice/.config/myapp/config.toml"
1639
+ ]
1640
+ ```
1641
+
1642
+ **Explanation:** Using multiple `--target` flags deploys the same file to multiple locations. The `--force` flag overwrites existing files.
1643
+
1644
+ **Example 4: Cross-platform deployment**
1645
+ ```bash
1646
+ # Deploy for Windows even when running on Linux (for CI/testing)
1647
+ lib_layered_config deploy \
1648
+ --source ./config.toml \
1649
+ --vendor Acme --app MyApp --slug myapp \
1650
+ --target user \
1651
+ --platform windows
1652
+ ```
1653
+
1654
+ **Output:**
1655
+ ```json
1656
+ ["C:\\Users\\alice\\AppData\\Roaming\\Acme\\MyApp\\config.toml"]
1657
+ ```
1658
+
1659
+ **Explanation:** Use `--platform` to override platform detection. Useful for testing deployment paths on different platforms without actually being on that platform.
1660
+
1661
+ **Example 5: Deploy host-specific configuration**
1662
+ ```bash
1663
+ # Deploy configuration specific to this server
1664
+ hostname=$(hostname)
1665
+ lib_layered_config deploy \
1666
+ --source ./hosts/${hostname}.toml \
1667
+ --vendor Acme --app MyApp --slug myapp \
1668
+ --target host
1669
+ ```
1670
+
1671
+ **Output:**
1672
+ ```json
1673
+ ["/etc/xdg/myapp/hosts/server-01.toml"]
1674
+ ```
1675
+
1676
+ **Explanation:** Host-specific configurations are stored in the `hosts/` subdirectory with the hostname as the filename. They override app defaults but only on machines with matching hostnames.
1677
+
1678
+ **Example 6: Safe deployment (check before overwriting)**
1679
+ ```bash
1680
+ # Try to deploy without --force to prevent accidental overwrites
1681
+ lib_layered_config deploy \
1682
+ --source ./new-config.toml \
1683
+ --vendor Acme --app MyApp --slug myapp \
1684
+ --target user
1685
+
1686
+ # If file exists, you'll get an empty array (nothing deployed)
1687
+ # Output: []
1688
+
1689
+ # Then deploy with --force if you really want to overwrite
1690
+ lib_layered_config deploy \
1691
+ --source ./new-config.toml \
1692
+ --vendor Acme --app MyApp --slug myapp \
1693
+ --target user \
1694
+ --force
1695
+ ```
1696
+
1697
+ **Explanation:** Without `--force`, the command skips existing files. This prevents accidental overwrites of user customizations.
1698
+
1699
+ ---
1700
+
1701
+ ### `generate-examples`
1702
+
1703
+ Generate example configuration trees for documentation or onboarding.
1704
+
1705
+ **Usage:**
1706
+ ```bash
1707
+ lib_layered_config generate-examples --destination ./examples \
1708
+ --vendor Acme --app ConfigKit --slug config-kit \
1709
+ [--platform posix|windows] \
1710
+ [--force | --no-force]
1711
+ ```
1712
+
1713
+ **Parameters:**
1714
+
1715
+ | Parameter | Type | Required | Default | Description |
1716
+ |-----------|------|----------|---------|-------------|
1717
+ | `--destination` | path | Yes | - | Directory that will receive the example tree. Will be created if it doesn't exist |
1718
+ | `--slug` | string | Yes | - | Configuration slug used in generated files |
1719
+ | `--vendor` | string | Yes | - | Vendor namespace interpolated into examples |
1720
+ | `--app` | string | Yes | - | Application name interpolated into examples |
1721
+ | `--platform` | choice | No | auto-detect | Override platform layout. Valid values: `posix` (Linux/macOS layout), `windows` (Windows layout) |
1722
+ | `--force` / `--no-force` | flag | No | `--no-force` | Overwrite existing example files |
1723
+
1724
+ **Returns:** JSON array of file paths created.
1725
+
1726
+ **Examples:**
1727
+
1728
+ **Example 1: Generate examples for your project documentation**
1729
+ ```bash
1730
+ # Create example configuration files in your docs directory
1731
+ lib_layered_config generate-examples \
1732
+ --destination ./docs/configuration-examples \
1733
+ --vendor Acme --app MyApp --slug myapp
1734
+ ```
1735
+
1736
+ **Output:**
1737
+ ```json
1738
+ [
1739
+ "/path/to/docs/configuration-examples/xdg/myapp/config.toml",
1740
+ "/path/to/docs/configuration-examples/xdg/myapp/hosts/your-hostname.toml",
1741
+ "/path/to/docs/configuration-examples/xdg/myapp/config.d/10-override.toml",
1742
+ "/path/to/docs/configuration-examples/home/myapp/config.toml",
1743
+ "/path/to/docs/configuration-examples/.env.example"
1744
+ ]
1745
+ ```
1746
+
1747
+ **File contents preview:**
1748
+ ```toml
1749
+ # docs/configuration-examples/xdg/myapp/config.toml
1750
+ # Application-wide defaults for myapp
1751
+ [service]
1752
+ endpoint = "https://api.example.com"
1753
+ timeout = 10
1754
+ ```
1755
+
1756
+ **Explanation:** Creates a complete set of example configuration files showing users how to configure your application. Include these in your documentation or repository.
1757
+
1758
+ **Example 2: Generate both POSIX and Windows examples**
1759
+ ```bash
1760
+ # Generate Linux/macOS examples
1761
+ lib_layered_config generate-examples \
1762
+ --destination ./docs/examples/unix \
1763
+ --vendor Acme --app MyApp --slug myapp \
1764
+ --platform posix
1765
+
1766
+ # Generate Windows examples
1767
+ lib_layered_config generate-examples \
1768
+ --destination ./docs/examples/windows \
1769
+ --vendor Acme --app MyApp --slug myapp \
1770
+ --platform windows
1771
+ ```
1772
+
1773
+ **Explanation:** Generate platform-specific examples for comprehensive documentation. Windows examples use paths like `ProgramData\Acme\MyApp\config.toml`, while POSIX examples use `/etc/xdg/myapp/config.toml`.
1774
+
1775
+ **Example 3: Update examples after configuration changes**
1776
+ ```bash
1777
+ # Regenerate examples with --force to update them
1778
+ lib_layered_config generate-examples \
1779
+ --destination ./examples \
1780
+ --vendor Acme --app MyApp --slug myapp \
1781
+ --force
1782
+ ```
1783
+
1784
+ **Explanation:** When you update your configuration schema, use `--force` to regenerate all example files. This ensures your documentation stays in sync with your application.
1785
+
1786
+ **Example 4: Generated file structure (POSIX)**
1787
+ ```bash
1788
+ lib_layered_config generate-examples \
1789
+ --destination ./examples \
1790
+ --vendor Acme --app MyApp --slug myapp \
1791
+ --platform posix
1792
+
1793
+ # View the generated structure
1794
+ tree ./examples
1795
+ ```
1796
+
1797
+ **Output:**
1798
+ ```
1799
+ ./examples/
1800
+ ├── etc/
1801
+ │ └── myapp/
1802
+ │ ├── config.toml # System-wide defaults
1803
+ │ └── hosts/
1804
+ │ └── your-hostname.toml # Host-specific overrides
1805
+ ├── xdg/
1806
+ │ └── myapp/
1807
+ │ ├── config.toml # User preferences
1808
+ │ └── config.d/
1809
+ │ └── 10-override.toml # Split configuration
1810
+ └── .env.example # Environment variable template
1811
+ ```
1812
+
1813
+ **Explanation:** The generated structure mirrors the actual configuration layout your application will use, making it easy for users to understand where to place their config files.
1814
+
1815
+ **Example 5: Use examples as onboarding templates**
1816
+ ```bash
1817
+ # Generate examples in a temp directory
1818
+ lib_layered_config generate-examples \
1819
+ --destination /tmp/myapp-examples \
1820
+ --vendor Acme --app MyApp --slug myapp
1821
+
1822
+ # User can copy these to actual locations
1823
+ echo "To get started, copy these examples:"
1824
+ echo " sudo cp /tmp/myapp-examples/etc/myapp/config.toml /etc/myapp/"
1825
+ echo " cp /tmp/myapp-examples/xdg/myapp/config.toml ~/.config/myapp/"
1826
+ echo " cp /tmp/myapp-examples/.env.example .env"
1827
+ ```
1828
+
1829
+ **Explanation:** Generate examples in a temporary location, then provide instructions for users to copy them to the actual configuration directories.
1830
+
1831
+ **Example 6: CI/CD - Validate configuration structure**
1832
+ ```bash
1833
+ #!/bin/bash
1834
+ # In your CI pipeline, generate examples and validate them
1835
+
1836
+ # Generate examples
1837
+ lib_layered_config generate-examples \
1838
+ --destination ./ci-examples \
1839
+ --vendor Acme --app MyApp --slug myapp
1840
+
1841
+ # Check that all expected files were created
1842
+ expected_files=(
1843
+ "etc/myapp/config.toml"
1844
+ "xdg/myapp/config.toml"
1845
+ ".env.example"
1846
+ )
1847
+
1848
+ for file in "${expected_files[@]}"; do
1849
+ if [ ! -f "./ci-examples/$file" ]; then
1850
+ echo "ERROR: Missing example file: $file"
1851
+ exit 1
1852
+ fi
1853
+ done
1854
+
1855
+ echo "✓ All configuration examples are valid"
1856
+ ```
1857
+
1858
+ **Explanation:** Use in CI/CD to ensure your configuration structure is correct and all example files can be generated successfully.
1859
+
1860
+ ---
1861
+
1862
+ ### `env-prefix`
1863
+
1864
+ Compute the canonical environment variable prefix for a configuration slug.
1865
+
1866
+ **Usage:**
1867
+ ```bash
1868
+ lib_layered_config env-prefix <slug>
1869
+ ```
1870
+
1871
+ **Parameters:**
1872
+
1873
+ | Parameter | Type | Required | Default | Description |
1874
+ |-----------|------|----------|---------|-------------|
1875
+ | `slug` | string | Yes (positional) | - | Configuration slug to convert to environment prefix |
1876
+
1877
+ **Returns:** Uppercase environment prefix with dashes converted to underscores.
1878
+
1879
+ **Examples:**
1880
+
1881
+ **Example 1: Check what environment prefix your app uses**
1882
+ ```bash
1883
+ lib_layered_config env-prefix myapp
1884
+ ```
1885
+
1886
+ **Output:**
1887
+ ```
1888
+ MYAPP___
1889
+ ```
1890
+
1891
+ **Explanation:** This shows the environment variable prefix for your application (including the `___` separator). Use this prefix with double underscores for nested keys: `MYAPP___DATABASE__HOST`, `MYAPP___SERVICE__TIMEOUT`.
1892
+
1893
+ **Example 2: Generate documentation for users**
1894
+ ```bash
1895
+ #!/bin/bash
1896
+ # Script to document environment variables
1897
+
1898
+ app_slug="myapp"
1899
+ prefix=$(lib_layered_config env-prefix "$app_slug")
1900
+
1901
+ cat << EOF
1902
+ Environment Variables for $app_slug
1903
+ ====================================
1904
+
1905
+ All environment variables must be prefixed with: ${prefix}
1906
+
1907
+ Examples:
1908
+ ${prefix}DATABASE__HOST=localhost
1909
+ ${prefix}DATABASE__PORT=5432
1910
+ ${prefix}SERVICE__TIMEOUT=30
1911
+ ${prefix}SERVICE__RETRY__MAX_ATTEMPTS=3
1912
+
1913
+ Note: Use double underscores (__) to denote nesting in configuration keys.
1914
+ EOF
1915
+ ```
1916
+
1917
+ **Output:**
1918
+ ```
1919
+ Environment Variables for myapp
1920
+ ====================================
1921
+
1922
+ All environment variables must be prefixed with: MYAPP___
1923
+
1924
+ Examples:
1925
+ MYAPP___DATABASE__HOST=localhost
1926
+ MYAPP___DATABASE__PORT=5432
1927
+ MYAPP___SERVICE__TIMEOUT=30
1928
+ MYAPP___SERVICE__RETRY__MAX_ATTEMPTS=3
1929
+
1930
+ Note: Use double underscores (__) to denote nesting in configuration keys.
1931
+ ```
1932
+
1933
+ **Explanation:** Use this in documentation generation scripts to automatically show users the correct environment variable format.
1934
+
1935
+ **Example 3: Validate environment variables in a script**
1936
+ ```bash
1937
+ #!/bin/bash
1938
+ # Validate that users have set required environment variables
1939
+
1940
+ app_slug="config-kit"
1941
+ prefix=$(lib_layered_config env-prefix "$app_slug")
1942
+
1943
+ required_vars=(
1944
+ "${prefix}DATABASE__HOST"
1945
+ "${prefix}DATABASE__PASSWORD"
1946
+ "${prefix}API__SECRET_KEY"
1947
+ )
1948
+
1949
+ missing=()
1950
+ for var in "${required_vars[@]}"; do
1951
+ if [ -z "${!var}" ]; then
1952
+ missing+=("$var")
1953
+ fi
1954
+ done
1955
+
1956
+ if [ ${#missing[@]} -gt 0 ]; then
1957
+ echo "Error: Missing required environment variables:"
1958
+ printf ' %s\n' "${missing[@]}"
1959
+ exit 1
1960
+ fi
1961
+
1962
+ echo "✓ All required environment variables are set"
1963
+ ```
1964
+
1965
+ **Explanation:** Programmatically check that required environment variables are set with the correct prefix before starting your application.
1966
+
1967
+ **Example 4: Set test environment variables**
1968
+ ```bash
1969
+ # In a test script, set environment variables with the correct prefix
1970
+ prefix=$(lib_layered_config env-prefix myapp)
1971
+
1972
+ export ${prefix}DATABASE__HOST="test-db.local"
1973
+ export ${prefix}DATABASE__PORT="5432"
1974
+ export ${prefix}SERVICE__TIMEOUT="5"
1975
+
1976
+ # Run tests
1977
+ python -m pytest tests/
1978
+ ```
1979
+
1980
+ **Explanation:** Dynamically generate environment variable names for testing, ensuring they match your application's expected prefix.
1981
+
1982
+ ---
1983
+
1984
+ ### `info`
1985
+
1986
+ Print package metadata including version, author, and license.
1987
+
1988
+ **Usage:**
1989
+ ```bash
1990
+ lib_layered_config info
1991
+ ```
1992
+
1993
+ **Parameters:** None
1994
+
1995
+ **Example:**
1996
+ ```bash
1997
+ lib_layered_config info
1998
+ ```
1999
+
2000
+ ---
2001
+
2002
+ ### `fail`
2003
+
2004
+ Intentionally raise a `RuntimeError` for testing error handling and CLI behavior.
2005
+
2006
+ **Usage:**
2007
+ ```bash
2008
+ lib_layered_config fail
2009
+ ```
2010
+
2011
+ **Parameters:** None
2012
+
2013
+ **Raises:** `RuntimeError` with message `"i should fail"`.
2014
+
2015
+ **Example:**
2016
+ ```bash
2017
+ lib_layered_config fail
2018
+ # Output: RuntimeError: i should fail
2019
+ # Exit code: 1
2020
+ ```
2021
+
2022
+ ## Python API
2023
+
2024
+ ```python
2025
+ from lib_layered_config import (
2026
+ Config,
2027
+ Layer,
2028
+ read_config,
2029
+ read_config_json,
2030
+ read_config_raw,
2031
+ default_env_prefix,
2032
+ deploy_config,
2033
+ generate_examples,
2034
+ i_should_fail,
2035
+ )
2036
+ ```
2037
+
2038
+ ### `Layer` Enum
2039
+
2040
+ The `Layer` enum provides type-safe constants for configuration layer names:
2041
+
2042
+ ```python
2043
+ from lib_layered_config import Layer
2044
+
2045
+ # Available layers (in precedence order, lowest to highest):
2046
+ Layer.DEFAULTS # "defaults" - bundled application defaults
2047
+ Layer.APP # "app" - system-wide application config
2048
+ Layer.HOST # "host" - machine-specific overrides
2049
+ Layer.USER # "user" - per-user preferences
2050
+ Layer.DOTENV # "dotenv" - project-local .env file
2051
+ Layer.ENV # "env" - environment variables (highest precedence)
2052
+
2053
+ # Layer values are strings, so they work seamlessly with provenance:
2054
+ origin = config.origin("service.timeout")
2055
+ if origin and origin["layer"] == Layer.ENV:
2056
+ print("Value comes from environment variable")
2057
+ ```
2058
+
2059
+ ### `Config` Class
2060
+
2061
+ Immutable configuration value object with provenance tracking and dotted-path lookups.
2062
+
2063
+ #### Methods
2064
+
2065
+ ##### `Config.get(key, default=None)`
2066
+
2067
+ Return the value for a dotted key path or a default when the path is missing.
2068
+
2069
+ **Parameters:**
2070
+ - `key` (str, required): Dotted path identifying nested entries (e.g., `"service.timeout"` or `"db.host"`).
2071
+ - `default` (Any, optional): Value to return when the path does not resolve or encounters a non-mapping. Default: `None`.
2072
+
2073
+ **Returns:** The resolved value or `default` when missing.
2074
+
2075
+ **Examples:**
2076
+
2077
+ **Example 1: Basic dotted-path lookup**
2078
+ ```python
2079
+ from lib_layered_config import read_config
2080
+
2081
+ # Load configuration
2082
+ config = read_config(vendor="Acme", app="Demo", slug="demo")
2083
+
2084
+ # Access nested configuration values using dotted paths
2085
+ timeout = config.get("service.timeout", default=30)
2086
+ endpoint = config.get("service.endpoint")
2087
+ db_host = config.get("database.host", default="localhost")
2088
+
2089
+ print(f"Service timeout: {timeout}s")
2090
+ print(f"Service endpoint: {endpoint}")
2091
+ print(f"Database host: {db_host}")
2092
+ ```
2093
+ **Explanation:** The `get` method traverses nested dictionaries using dot notation. If `service.timeout` exists in your configuration, it returns that value; otherwise, it returns the default (30).
2094
+
2095
+ **Example 2: Handling missing keys gracefully**
2096
+ ```python
2097
+ # This returns None if the key doesn't exist
2098
+ api_key = config.get("api.secret_key")
2099
+ if api_key is None:
2100
+ print("Warning: API key not configured")
2101
+
2102
+ # This returns a default value
2103
+ max_retries = config.get("api.max_retries", default=3)
2104
+ print(f"Max retries: {max_retries}")
2105
+ ```
2106
+ **Explanation:** When you don't provide a default, `get` returns `None` for missing keys. This is useful for optional configuration values where you need to check if they were configured.
2107
+
2108
+ **Example 3: Deep nested paths**
2109
+ ```python
2110
+ # Access deeply nested configuration
2111
+ smtp_host = config.get("email.smtp.host", default="smtp.gmail.com")
2112
+ smtp_port = config.get("email.smtp.port", default=587)
2113
+ use_tls = config.get("email.smtp.tls.enabled", default=True)
2114
+
2115
+ print(f"SMTP: {smtp_host}:{smtp_port} (TLS: {use_tls})")
2116
+ ```
2117
+ **Explanation:** The dotted path can be arbitrarily deep. If any intermediate key is missing or not a dictionary, the default value is returned.
2118
+
2119
+ ##### `Config.origin(key)`
2120
+
2121
+ Return provenance metadata for a dotted key when available.
2122
+
2123
+ **Parameters:**
2124
+ - `key` (str, required): Dotted key in the metadata map (e.g., `"service.timeout"`).
2125
+
2126
+ **Returns:** Dictionary with keys `layer` (str), `path` (str | None), and `key` (str), or `None` if the key was never observed.
2127
+
2128
+ **Examples:**
2129
+
2130
+ **Example 1: Check where a value came from**
2131
+ ```python
2132
+ from lib_layered_config import read_config
2133
+
2134
+ config = read_config(vendor="Acme", app="Demo", slug="demo")
2135
+
2136
+ # Get provenance information
2137
+ timeout_origin = config.origin("service.timeout")
2138
+ if timeout_origin:
2139
+ print(f"service.timeout = {config.get('service.timeout')}")
2140
+ print(f" Layer: {timeout_origin['layer']}")
2141
+ print(f" Source: {timeout_origin['path'] or 'environment variable'}")
2142
+ print(f" Key: {timeout_origin['key']}")
2143
+
2144
+ # Output example:
2145
+ # service.timeout = 30
2146
+ # Layer: env
2147
+ # Source: environment variable
2148
+ # Key: service.timeout
2149
+ ```
2150
+ **Explanation:** The `origin` method tells you which configuration layer provided a value. This is crucial for debugging when you need to understand why a particular value is being used.
2151
+
2152
+ **Example 2: Debugging configuration precedence**
2153
+ ```python
2154
+ # Check multiple values to understand the configuration hierarchy
2155
+ keys_to_check = ["database.host", "database.port", "service.timeout"]
2156
+
2157
+ for key in keys_to_check:
2158
+ value = config.get(key)
2159
+ origin = config.origin(key)
2160
+
2161
+ if origin:
2162
+ layer = origin['layer']
2163
+ source = origin['path'] or '(ephemeral)'
2164
+ print(f"{key}: {value} [from {layer}] {source}")
2165
+ else:
2166
+ print(f"{key}: Not configured")
2167
+
2168
+ # Output example:
2169
+ # database.host: localhost [from user] /home/alice/.config/demo/config.toml
2170
+ # database.port: 5432 [from app] /etc/demo/config.toml
2171
+ # service.timeout: 30 [from env] (ephemeral)
2172
+ ```
2173
+ **Explanation:** This shows how to audit your entire configuration to see which layer each value came from. Useful when troubleshooting unexpected configuration values.
2174
+
2175
+ **Example 3: Validate configuration source for security**
2176
+ ```python
2177
+ # Ensure sensitive values come from environment or dotenv
2178
+ sensitive_keys = ["api.secret_key", "database.password"]
2179
+
2180
+ for key in sensitive_keys:
2181
+ origin = config.origin(key)
2182
+ if origin:
2183
+ if origin['layer'] not in ['env', 'dotenv']:
2184
+ print(f"WARNING: {key} should come from env/dotenv, not {origin['layer']}")
2185
+ print(f" Currently in: {origin['path']}")
2186
+ else:
2187
+ print(f"ERROR: {key} is not configured!")
2188
+
2189
+ # This helps ensure secrets aren't committed to config files
2190
+ ```
2191
+ **Explanation:** You can use provenance to enforce security policies, ensuring sensitive values only come from appropriate sources (environment variables or .env files, not checked-in config files).
2192
+
2193
+ ##### `Config.as_dict()`
2194
+
2195
+ Return a deep, mutable copy of the configuration tree.
2196
+
2197
+ **Parameters:** None
2198
+
2199
+ **Returns:** Dictionary containing a deep copy of all configuration data.
2200
+
2201
+ **Examples:**
2202
+
2203
+ **Example 1: Export configuration for serialization**
2204
+ ```python
2205
+ from lib_layered_config import read_config
2206
+ import json
2207
+
2208
+ config = read_config(vendor="Acme", app="Demo", slug="demo")
2209
+
2210
+ # Get a mutable copy of the entire configuration
2211
+ data = config.as_dict()
2212
+
2213
+ # Now you can serialize it however you want
2214
+ with open("config-snapshot.json", "w") as f:
2215
+ json.dump(data, f, indent=2)
2216
+
2217
+ print("Configuration exported to config-snapshot.json")
2218
+ ```
2219
+ **Explanation:** Use `as_dict()` when you need to export or serialize the configuration data. The returned dictionary is completely independent from the original Config object.
2220
+
2221
+ **Example 2: Modify configuration copy for testing**
2222
+ ```python
2223
+ # Create a modified copy for testing without affecting the original
2224
+ test_config = config.as_dict()
2225
+ test_config["database"]["host"] = "test-db.example.com"
2226
+ test_config["service"]["timeout"] = 1 # Short timeout for tests
2227
+
2228
+ # Original config is unchanged
2229
+ print(f"Original DB: {config.get('database.host')}") # localhost
2230
+ print(f"Test DB: {test_config['database']['host']}") # test-db.example.com
2231
+ ```
2232
+ **Explanation:** This is useful in tests where you want to create variations of your configuration without modifying the immutable Config object.
2233
+
2234
+ ##### `Config.to_json(indent=None)`
2235
+
2236
+ Serialize the configuration as JSON.
2237
+
2238
+ **Parameters:**
2239
+ - `indent` (int | None, optional): Indentation level for pretty-printing. `None` produces compact output. Default: `None`.
2240
+
2241
+ **Returns:** JSON string containing the configuration data.
2242
+
2243
+ **Examples:**
2244
+
2245
+ **Example 1: Pretty-printed JSON for logs**
2246
+ ```python
2247
+ from lib_layered_config import read_config
2248
+
2249
+ config = read_config(vendor="Acme", app="Demo", slug="demo")
2250
+
2251
+ # Pretty-printed JSON with 2-space indentation
2252
+ pretty_json = config.to_json(indent=2)
2253
+ print("Current configuration:")
2254
+ print(pretty_json)
2255
+
2256
+ # Output:
2257
+ # {
2258
+ # "service": {
2259
+ # "timeout": 30,
2260
+ # "endpoint": "https://api.example.com"
2261
+ # },
2262
+ # "database": {
2263
+ # "host": "localhost"
2264
+ # }
2265
+ # }
2266
+ ```
2267
+ **Explanation:** Use `indent=2` or `indent=4` for human-readable JSON output, perfect for logging or debugging.
2268
+
2269
+ **Example 2: Compact JSON for APIs or storage**
2270
+ ```python
2271
+ # Compact JSON (no whitespace)
2272
+ compact_json = config.to_json()
2273
+ print(compact_json)
2274
+ # Output: {"service":{"timeout":30,"endpoint":"https://api.example.com"},...}
2275
+
2276
+ # This is useful when sending config over the network or storing in databases
2277
+ ```
2278
+ **Explanation:** Compact JSON (no indent) minimizes the payload size, useful for network transmission or storage.
2279
+
2280
+ ##### `Config.with_overrides(overrides)`
2281
+
2282
+ Return a new configuration with shallow top-level overrides applied.
2283
+
2284
+ **Parameters:**
2285
+ - `overrides` (Mapping[str, Any], required): Dictionary of top-level keys and values to override.
2286
+
2287
+ **Returns:** New `Config` instance with overrides applied, sharing provenance with the original.
2288
+
2289
+ **Examples:**
2290
+
2291
+ **Example 1: Override configuration for specific environment**
2292
+ ```python
2293
+ from lib_layered_config import read_config
2294
+
2295
+ # Load base configuration
2296
+ config = read_config(vendor="Acme", app="Demo", slug="demo")
2297
+
2298
+ # Create a version with production overrides
2299
+ prod_config = config.with_overrides({
2300
+ "service": {
2301
+ "endpoint": "https://prod-api.example.com",
2302
+ "timeout": 60
2303
+ },
2304
+ "database": {
2305
+ "host": "prod-db.example.com",
2306
+ "pool_size": 100
2307
+ }
2308
+ })
2309
+
2310
+ print(f"Dev endpoint: {config.get('service.endpoint')}")
2311
+ print(f"Prod endpoint: {prod_config.get('service.endpoint')}")
2312
+
2313
+ # Original config is unchanged
2314
+ ```
2315
+ **Explanation:** This allows you to create environment-specific configurations from a base configuration without mutating the original.
2316
+
2317
+ **Example 2: Testing with feature flags**
2318
+ ```python
2319
+ # Enable feature flags for testing
2320
+ test_config = config.with_overrides({
2321
+ "features": {
2322
+ "new_ui": True,
2323
+ "experimental_api": True,
2324
+ "debug_mode": True
2325
+ }
2326
+ })
2327
+
2328
+ # Use test_config in your tests
2329
+ if test_config.get("features.new_ui"):
2330
+ print("Running tests with new UI enabled")
2331
+ ```
2332
+ **Explanation:** Great for testing different configurations or feature flag combinations without modifying files or environment variables.
2333
+
2334
+ ##### `Config[key]` (item access)
2335
+
2336
+ Access top-level keys directly using bracket notation.
2337
+
2338
+ **Parameters:**
2339
+ - `key` (str): Top-level key to retrieve.
2340
+
2341
+ **Returns:** Stored value.
2342
+
2343
+ **Raises:** `KeyError` when key does not exist.
2344
+
2345
+ **Examples:**
2346
+
2347
+ **Example 1: Direct access to top-level keys**
2348
+ ```python
2349
+ from lib_layered_config import read_config
2350
+
2351
+ config = read_config(vendor="Acme", app="Demo", slug="demo")
2352
+
2353
+ # Access top-level sections directly
2354
+ service_config = config["service"]
2355
+ database_config = config["database"]
2356
+
2357
+ print(f"Service section: {service_config}")
2358
+ # Output: {'timeout': 30, 'endpoint': 'https://api.example.com'}
2359
+
2360
+ print(f"DB host: {database_config['host']}")
2361
+ # Output: localhost
2362
+ ```
2363
+ **Explanation:** Use bracket notation `config[key]` to access top-level configuration sections. This returns the full nested dictionary for that section.
2364
+
2365
+ **Example 2: Iterate over configuration sections**
2366
+ ```python
2367
+ # Iterate over all top-level configuration keys
2368
+ for section in config:
2369
+ print(f"Section: {section}")
2370
+ print(f" Keys: {list(config[section].keys())}")
2371
+
2372
+ # Output:
2373
+ # Section: service
2374
+ # Keys: ['timeout', 'endpoint']
2375
+ # Section: database
2376
+ # Keys: ['host', 'port']
2377
+ ```
2378
+ **Explanation:** Since Config implements the Mapping protocol, you can iterate over it like a dictionary to discover all configured sections.
2379
+
2380
+ ---
2381
+
2382
+ ### `read_config`
2383
+
2384
+ Load and merge all configuration layers into an immutable `Config` object with provenance metadata.
2385
+
2386
+ **Parameters:**
2387
+ - `vendor` (str, required): Vendor namespace used to compute filesystem paths (e.g., `"Acme"`).
2388
+ - `app` (str, required): Application name used to compute filesystem paths (e.g., `"ConfigKit"`).
2389
+ - `slug` (str, required): Configuration slug used for file paths and environment variable prefix (e.g., `"config-kit"`).
2390
+ - `profile` (str | None, optional): Configuration profile name (e.g., `"test"`, `"production"`). When specified, adds a `profile/<name>/` segment to all configuration paths. Default: `None` (no profile).
2391
+ - `prefer` (Sequence[str] | None, optional): Ordered sequence of preferred file suffixes (e.g., `["toml", "json", "yaml"]`). Files matching earlier suffixes take precedence. Default: `None` (accepts all supported formats with default ordering).
2392
+ - `start_dir` (str | Path | None, optional): Starting directory for upward `.env` file search. Default: `None` (uses current working directory).
2393
+ - `default_file` (str | Path | None, optional): Path to a file injected as the lowest-precedence layer (loaded before app/host/user layers). Default: `None` (no defaults layer).
2394
+
2395
+ **Returns:** Immutable `Config` object with merged configuration and provenance tracking.
2396
+
2397
+ **Examples:**
2398
+
2399
+ **Example 1: Basic usage - Load configuration with defaults**
2400
+ ```python
2401
+ from lib_layered_config import read_config
2402
+
2403
+ # Simplest usage - just specify your app identity
2404
+ config = read_config(
2405
+ vendor="Acme",
2406
+ app="MyApp",
2407
+ slug="myapp"
2408
+ )
2409
+
2410
+ # Access configuration values
2411
+ timeout = config.get("service.timeout", default=30)
2412
+ endpoint = config.get("service.endpoint", default="https://api.example.com")
2413
+
2414
+ print(f"Service will connect to {endpoint} with {timeout}s timeout")
2415
+ ```
2416
+ **Explanation:** This is the minimal setup. The library will automatically look for configuration files in standard locations (`/etc/myapp/`, `~/.config/myapp/`, etc.) and merge them with environment variables.
2417
+
2418
+ **Example 2: Using file format preferences**
2419
+ ```python
2420
+ from lib_layered_config import read_config
2421
+
2422
+ # Prefer TOML files over JSON when both exist
2423
+ config = read_config(
2424
+ vendor="Acme",
2425
+ app="MyApp",
2426
+ slug="myapp",
2427
+ prefer=["toml", "json", "yaml"]
2428
+ )
2429
+
2430
+ # If both config.toml and config.json exist in the same directory,
2431
+ # config.toml will be loaded because it appears first in the prefer list
2432
+ ```
2433
+ **Explanation:** The `prefer` parameter controls which file format takes precedence when multiple formats exist in the same directory. This is useful when migrating from one format to another.
2434
+
2435
+ **Example 3: Using a defaults file**
2436
+ ```python
2437
+ from pathlib import Path
2438
+ from lib_layered_config import read_config
2439
+
2440
+ # Start with application defaults before applying environment-specific overrides
2441
+ config = read_config(
2442
+ vendor="Acme",
2443
+ app="MyApp",
2444
+ slug="myapp",
2445
+ default_file=Path("./config/defaults.toml")
2446
+ )
2447
+
2448
+ # Precedence order now becomes:
2449
+ # 1. defaults.toml (lowest)
2450
+ # 2. /etc/myapp/config.toml (app layer)
2451
+ # 3. /etc/myapp/hosts/hostname.toml (host layer)
2452
+ # 4. ~/.config/myapp/config.toml (user layer)
2453
+ # 5. .env files (dotenv layer)
2454
+ # 6. Environment variables (highest)
2455
+ ```
2456
+ **Explanation:** Use `default_file` to ship reasonable defaults with your application that can be overridden by system admins (app layer), per-machine configs (host layer), or users.
2457
+
2458
+ **Example 4: Project-specific .env search**
2459
+ ```python
2460
+ from pathlib import Path
2461
+ from lib_layered_config import read_config
2462
+
2463
+ # Specify where to start searching for .env files
2464
+ project_root = Path(__file__).parent.parent
2465
+ config = read_config(
2466
+ vendor="Acme",
2467
+ app="MyApp",
2468
+ slug="myapp",
2469
+ start_dir=str(project_root)
2470
+ )
2471
+
2472
+ # The library will search for .env files starting from project_root
2473
+ # and moving upward through parent directories
2474
+ ```
2475
+ **Explanation:** Use `start_dir` to control where `.env` file discovery begins. This ensures your project's `.env` file is found even if your script runs from a subdirectory.
2476
+
2477
+ **Example 5: Complete setup with all parameters**
2478
+ ```python
2479
+ from pathlib import Path
2480
+ from lib_layered_config import read_config
2481
+
2482
+ # Production-ready configuration loading
2483
+ config = read_config(
2484
+ vendor="Acme",
2485
+ app="MyApp",
2486
+ slug="myapp",
2487
+ prefer=["toml", "json"], # TOML preferred
2488
+ start_dir=Path.cwd(), # Search .env from current directory
2489
+ default_file=Path(__file__).parent / "defaults.toml" # Ship defaults
2490
+ )
2491
+
2492
+ # Use the configuration
2493
+ db_host = config.get("database.host", default="localhost")
2494
+ db_port = config.get("database.port", default=5432)
2495
+ db_name = config.get("database.name", default="myapp")
2496
+
2497
+ print(f"Connecting to PostgreSQL at {db_host}:{db_port}/{db_name}")
2498
+
2499
+ # Check where each value came from
2500
+ for key in ["database.host", "database.port", "database.name"]:
2501
+ origin = config.origin(key)
2502
+ if origin:
2503
+ print(f" {key}: from {origin['layer']} layer")
2504
+ ```
2505
+ **Explanation:** This complete example shows production-ready configuration loading with defaults, format preferences, and provenance tracking for debugging.
2506
+
2507
+ ---
2508
+
2509
+ ### `read_config_json`
2510
+
2511
+ Load configuration and return it as JSON with provenance metadata.
2512
+
2513
+ **Parameters:**
2514
+ - `vendor` (str, required): Vendor namespace.
2515
+ - `app` (str, required): Application name.
2516
+ - `slug` (str, required): Configuration slug.
2517
+ - `profile` (str | None, optional): Configuration profile name. Adds `profile/<name>/` to paths. Default: `None`.
2518
+ - `prefer` (Sequence[str] | None, optional): Ordered sequence of preferred file suffixes. Default: `None`.
2519
+ - `start_dir` (str | Path | None, optional): Starting directory for `.env` search. Default: `None`.
2520
+ - `default_file` (str | Path | None, optional): Path to lowest-precedence defaults file. Default: `None`.
2521
+ - `indent` (int | None, optional): JSON indentation level. `None` for compact output. Default: `None`.
2522
+
2523
+ **Returns:** JSON string containing `{"config": {...}, "provenance": {...}}`.
2524
+
2525
+ **Examples:**
2526
+
2527
+ **Example 1: API endpoint - Return configuration as JSON**
2528
+ ```python
2529
+ from lib_layered_config import read_config_json
2530
+ from flask import Flask, jsonify
2531
+
2532
+ app = Flask(__name__)
2533
+
2534
+ @app.route("/api/config")
2535
+ def get_config():
2536
+ # Load and return configuration as JSON with provenance
2537
+ json_payload = read_config_json(
2538
+ vendor="Acme",
2539
+ app="MyApp",
2540
+ slug="myapp",
2541
+ indent=2 # Pretty-printed for readability
2542
+ )
2543
+ return json_payload, 200, {'Content-Type': 'application/json'}
2544
+
2545
+ # The response includes both config values and their sources
2546
+ ```
2547
+ **Explanation:** Perfect for exposing configuration through APIs. The JSON includes provenance data so clients can see where each value came from.
2548
+
2549
+ **Example 2: Configuration audit tool**
2550
+ ```python
2551
+ from lib_layered_config import read_config_json
2552
+ import json
2553
+
2554
+ # Load configuration with provenance
2555
+ payload = read_config_json(
2556
+ vendor="Acme",
2557
+ app="MyApp",
2558
+ slug="myapp",
2559
+ indent=2
2560
+ )
2561
+
2562
+ data = json.loads(payload)
2563
+
2564
+ # Audit where sensitive values come from
2565
+ print("Configuration Audit Report")
2566
+ print("=" * 50)
2567
+
2568
+ for key, info in data["provenance"].items():
2569
+ value = data["config"]
2570
+ # Navigate to the value using the key
2571
+ for part in key.split("."):
2572
+ value = value.get(part, {})
2573
+
2574
+ print(f"\n{key}: {value}")
2575
+ print(f" Source Layer: {info['layer']}")
2576
+ print(f" File Path: {info['path'] or '(environment variable)'}")
2577
+ ```
2578
+ **Explanation:** Use this for creating audit reports that show exactly where each configuration value originated from.
2579
+
2580
+ **Example 3: Compact JSON for logging**
2581
+ ```python
2582
+ from lib_layered_config import read_config_json
2583
+ import logging
2584
+
2585
+ # Get compact JSON (no indentation) for structured logging
2586
+ compact_json = read_config_json(
2587
+ vendor="Acme",
2588
+ app="MyApp",
2589
+ slug="myapp",
2590
+ indent=None # Compact output
2591
+ )
2592
+
2593
+ # Log the configuration snapshot
2594
+ logging.info(f"Application started with config: {compact_json}")
2595
+ ```
2596
+ **Explanation:** Compact JSON is ideal for log aggregation systems where you want to log the entire configuration as a single line.
2597
+
2598
+ ---
2599
+
2600
+ ### `read_config_raw`
2601
+
2602
+ Return raw data and provenance mappings for advanced tooling.
2603
+
2604
+ **Parameters:**
2605
+ - `vendor` (str, required): Vendor namespace.
2606
+ - `app` (str, required): Application name.
2607
+ - `slug` (str, required): Configuration slug.
2608
+ - `profile` (str | None, optional): Configuration profile name. Adds `profile/<name>/` to paths. Default: `None`.
2609
+ - `prefer` (Sequence[str] | None, optional): Ordered sequence of preferred file suffixes. Default: `None`.
2610
+ - `start_dir` (str | None, optional): Starting directory for `.env` search. Default: `None`.
2611
+ - `default_file` (str | Path | None, optional): Path to lowest-precedence defaults file. Default: `None`.
2612
+
2613
+ **Returns:** Tuple of `(data_dict, provenance_dict)` where both are mutable dictionaries.
2614
+
2615
+ **Examples:**
2616
+
2617
+ **Example 1: Template rendering with configuration**
2618
+ ```python
2619
+ from lib_layered_config import read_config_raw
2620
+ from jinja2 import Template
2621
+
2622
+ # Load configuration as raw dictionaries
2623
+ data, provenance = read_config_raw(
2624
+ vendor="Acme",
2625
+ app="MyApp",
2626
+ slug="myapp"
2627
+ )
2628
+
2629
+ # Use in template rendering
2630
+ template = Template("""
2631
+ Database Configuration:
2632
+ Host: {{ database.host }}
2633
+ Port: {{ database.port }}
2634
+ Database: {{ database.name }}
2635
+
2636
+ Service Configuration:
2637
+ Timeout: {{ service.timeout }}s
2638
+ Endpoint: {{ service.endpoint }}
2639
+ """)
2640
+
2641
+ output = template.render(**data)
2642
+ print(output)
2643
+ ```
2644
+ **Explanation:** Raw dictionaries are perfect for template rendering where you need mutable data structures.
2645
+
2646
+ **Example 2: Configuration validation**
2647
+ ```python
2648
+ from lib_layered_config import read_config_raw
2649
+
2650
+ # Load configuration
2651
+ data, provenance = read_config_raw(
2652
+ vendor="Acme",
2653
+ app="MyApp",
2654
+ slug="myapp"
2655
+ )
2656
+
2657
+ # Validate required fields
2658
+ required_keys = [
2659
+ ("database.host", str),
2660
+ ("database.port", int),
2661
+ ("service.timeout", int),
2662
+ ]
2663
+
2664
+ errors = []
2665
+ for key, expected_type in required_keys:
2666
+ # Navigate the nested dictionary
2667
+ value = data
2668
+ for part in key.split("."):
2669
+ value = value.get(part) if isinstance(value, dict) else None
2670
+ if value is None:
2671
+ break
2672
+
2673
+ if value is None:
2674
+ errors.append(f"Missing required key: {key}")
2675
+ elif not isinstance(value, expected_type):
2676
+ errors.append(f"{key} must be {expected_type.__name__}, got {type(value).__name__}")
2677
+
2678
+ if errors:
2679
+ print("Configuration validation errors:")
2680
+ for error in errors:
2681
+ print(f" - {error}")
2682
+ else:
2683
+ print("Configuration is valid!")
2684
+ ```
2685
+ **Explanation:** Use `read_config_raw` for advanced validation or transformation where you need full control over the data structures.
2686
+
2687
+ **Example 3: Merge with runtime overrides**
2688
+ ```python
2689
+ from lib_layered_config import read_config_raw
2690
+
2691
+ # Load base configuration
2692
+ data, provenance = read_config_raw(
2693
+ vendor="Acme",
2694
+ app="MyApp",
2695
+ slug="myapp"
2696
+ )
2697
+
2698
+ # Apply runtime overrides (e.g., from command-line arguments)
2699
+ if args.db_host:
2700
+ data["database"]["host"] = args.db_host
2701
+ if args.debug:
2702
+ data["logging"]["level"] = "DEBUG"
2703
+
2704
+ # Now use the modified configuration
2705
+ print(f"Final configuration: {data}")
2706
+ ```
2707
+ **Explanation:** Raw dictionaries can be mutated, making them useful when you need to apply runtime overrides from command-line arguments or other sources.
2708
+
2709
+ ---
2710
+
2711
+ ### `default_env_prefix`
2712
+
2713
+ Compute the canonical environment variable prefix for a slug.
2714
+
2715
+ **Parameters:**
2716
+ - `slug` (str, required): Configuration slug (e.g., `"config-kit"`).
2717
+
2718
+ **Returns:** Uppercase environment prefix with dashes converted to underscores (e.g., `"CONFIG_KIT"`).
2719
+
2720
+ **Examples:**
2721
+
2722
+ **Example 1: Generate documentation for environment variables**
2723
+ ```python
2724
+ from lib_layered_config import default_env_prefix
2725
+
2726
+ # Calculate the prefix for your application
2727
+ slug = "myapp"
2728
+ prefix = default_env_prefix(slug)
2729
+
2730
+ print(f"Environment Variables for {slug}:")
2731
+ print(f"=" * 50)
2732
+ print(f"\n{prefix}<SECTION>__<KEY>=<value>\n")
2733
+ print("Examples:")
2734
+ print(f" {prefix}DATABASE__HOST=localhost")
2735
+ print(f" {prefix}DATABASE__PORT=5432")
2736
+ print(f" {prefix}SERVICE__TIMEOUT=30")
2737
+ print(f"\nNote: Use double underscores (__) for nested keys")
2738
+ ```
2739
+ **Explanation:** Use this to generate documentation showing users how to set environment variables for your application.
2740
+
2741
+ **Example 2: Programmatically set environment variables**
2742
+ ```python
2743
+ import os
2744
+ from lib_layered_config import default_env_prefix
2745
+
2746
+ # Calculate prefix
2747
+ prefix = default_env_prefix("myapp")
2748
+
2749
+ # Set environment variables programmatically (useful in tests)
2750
+ os.environ[f"{prefix}DATABASE__HOST"] = "test-db.example.com"
2751
+ os.environ[f"{prefix}DATABASE__PORT"] = "5432"
2752
+ os.environ[f"{prefix}SERVICE__TIMEOUT"] = "5"
2753
+
2754
+ # Now when you load configuration, these will be picked up
2755
+ from lib_layered_config import read_config
2756
+ config = read_config(vendor="Acme", app="MyApp", slug="myapp")
2757
+
2758
+ print(f"DB Host: {config.get('database.host')}") # test-db.example.com
2759
+ ```
2760
+ **Explanation:** Programmatically generate environment variable names for testing or dynamic configuration.
2761
+
2762
+ **Example 3: Validate environment variable names**
2763
+ ```python
2764
+ import os
2765
+ from lib_layered_config import default_env_prefix
2766
+
2767
+ slug = "myapp"
2768
+ expected_prefix = default_env_prefix(slug)
2769
+
2770
+ # Check if environment variables are correctly namespaced
2771
+ print(f"Checking environment variables for prefix: {expected_prefix}_")
2772
+
2773
+ mismatched = []
2774
+ for key in os.environ:
2775
+ if "DATABASE" in key or "SERVICE" in key:
2776
+ if not key.startswith(expected_prefix + "_"):
2777
+ mismatched.append(key)
2778
+
2779
+ if mismatched:
2780
+ print("\nWarning: Found environment variables that won't be loaded:")
2781
+ for key in mismatched:
2782
+ correct_name = f"{expected_prefix}_{key}"
2783
+ print(f" {key} should be {correct_name}")
2784
+ else:
2785
+ print("All environment variables are correctly prefixed!")
2786
+ ```
2787
+ **Explanation:** Validate that your environment variables are correctly prefixed so they'll be picked up by the configuration loader.
2788
+
2789
+ ---
2790
+
2791
+ ### `deploy_config`
2792
+
2793
+ Copy a source configuration file into one or more layer directories.
2794
+
2795
+ **Parameters:**
2796
+ - `source` (str | Path, required): Path to the configuration file to copy.
2797
+ - `vendor` (str, required): Vendor namespace.
2798
+ - `app` (str, required): Application name.
2799
+ - `targets` (Sequence[str], required): Layer targets to deploy to. Valid values: `"app"`, `"host"`, `"user"`.
2800
+ - `slug` (str | None, optional): Configuration slug. Default: `None` (uses `app` as slug).
2801
+ - `profile` (str | None, optional): Configuration profile name. Adds `profile/<name>/` to deployment paths. Default: `None`.
2802
+ - `platform` (str | None, optional): Override auto-detected platform. Valid values: `"linux"`, `"darwin"`, `"windows"`, or any value starting with `"win"`. Default: `None` (auto-detects from current platform).
2803
+ - `force` (bool, optional): Overwrite existing files at destinations. Default: `False`.
2804
+
2805
+ **Returns:** List of `Path` objects for files created or overwritten.
2806
+
2807
+ **Raises:** `FileNotFoundError` if source file does not exist.
2808
+
2809
+ **Examples:**
2810
+
2811
+ **Example 1: Deploy system-wide defaults**
2812
+ ```python
2813
+ from lib_layered_config import deploy_config
2814
+ from pathlib import Path
2815
+
2816
+ # Deploy app-wide defaults to the system directory
2817
+ created_paths = deploy_config(
2818
+ source=Path("./config/defaults.toml"),
2819
+ vendor="Acme",
2820
+ app="MyApp",
2821
+ targets=["app"], # Deploy to system-wide location
2822
+ slug="myapp"
2823
+ )
2824
+
2825
+ # On Linux, this copies to: /etc/myapp/config.toml
2826
+ # On macOS: /Library/Application Support/Acme/MyApp/config.toml
2827
+ # On Windows: C:\ProgramData\Acme\MyApp\config.toml
2828
+
2829
+ for path in created_paths:
2830
+ print(f"Deployed to: {path}")
2831
+ ```
2832
+ **Explanation:** Use the `"app"` target to deploy system-wide defaults that all users share. This is typically done during installation.
2833
+
2834
+ **Example 2: Deploy user-specific configuration**
2835
+ ```python
2836
+ from lib_layered_config import deploy_config
2837
+
2838
+ # Deploy user-specific configuration
2839
+ created_paths = deploy_config(
2840
+ source="./my-config.toml",
2841
+ vendor="Acme",
2842
+ app="MyApp",
2843
+ targets=["user"], # Deploy to user's config directory
2844
+ slug="myapp"
2845
+ )
2846
+
2847
+ # On Linux, this copies to: ~/.config/myapp/config.toml
2848
+ # On macOS: ~/Library/Application Support/Acme/MyApp/config.toml
2849
+ # On Windows: %APPDATA%\Acme\MyApp\config.toml
2850
+
2851
+ print(f"User configuration deployed to: {created_paths[0]}")
2852
+ ```
2853
+ **Explanation:** Use the `"user"` target to set up per-user configuration. Great for onboarding scripts or user preference templates.
2854
+
2855
+ **Example 3: Deploy host-specific configuration**
2856
+ ```python
2857
+ from lib_layered_config import deploy_config
2858
+ import socket
2859
+
2860
+ # Deploy configuration specific to this host
2861
+ hostname = socket.gethostname()
2862
+ created_paths = deploy_config(
2863
+ source=f"./configs/{hostname}.toml",
2864
+ vendor="Acme",
2865
+ app="MyApp",
2866
+ targets=["host"], # Deploy to host-specific location
2867
+ slug="myapp"
2868
+ )
2869
+
2870
+ # On Linux, this copies to: /etc/myapp/hosts/{hostname}.toml
2871
+ # The file will only be loaded on machines with this hostname
2872
+
2873
+ print(f"Host-specific config for '{hostname}' deployed to: {created_paths[0]}")
2874
+ ```
2875
+ **Explanation:** Host-specific configurations override app defaults but are still system-wide. Useful for server-specific settings in multi-server deployments.
2876
+
2877
+ **Example 4: Deploy to multiple layers at once**
2878
+ ```python
2879
+ from lib_layered_config import deploy_config
2880
+
2881
+ # Deploy the same config to multiple layers
2882
+ created_paths = deploy_config(
2883
+ source="./base-config.toml",
2884
+ vendor="Acme",
2885
+ app="MyApp",
2886
+ targets=["app", "user"], # Deploy to both system and user directories
2887
+ slug="myapp",
2888
+ force=True # Overwrite if already exists
2889
+ )
2890
+
2891
+ print(f"Deployed to {len(created_paths)} locations:")
2892
+ for path in created_paths:
2893
+ print(f" - {path}")
2894
+ ```
2895
+ **Explanation:** Deploy to multiple layers simultaneously. Useful for setting up consistent defaults across system and user levels. The `force=True` parameter allows overwriting existing files.
2896
+
2897
+ **Example 5: Cross-platform deployment script**
2898
+ ```python
2899
+ from lib_layered_config import deploy_config
2900
+ import sys
2901
+
2902
+ # Deployment script that works across platforms
2903
+ source_config = "./dist/config.toml"
2904
+
2905
+ print(f"Deploying configuration on {sys.platform}...")
2906
+
2907
+ try:
2908
+ created_paths = deploy_config(
2909
+ source=source_config,
2910
+ vendor="Acme",
2911
+ app="MyApp",
2912
+ targets=["app"],
2913
+ slug="myapp"
2914
+ # platform auto-detected
2915
+ )
2916
+
2917
+ print(f"✓ Successfully deployed to {len(created_paths)} location(s)")
2918
+ for path in created_paths:
2919
+ print(f" {path}")
2920
+
2921
+ except FileNotFoundError:
2922
+ print(f"✗ Error: Source file '{source_config}' not found")
2923
+ sys.exit(1)
2924
+ ```
2925
+ **Explanation:** The function automatically detects the platform and deploys to the appropriate directories. Perfect for cross-platform installation scripts.
2926
+
2927
+ **Example 6: Deploy to a specific profile (environment-specific)**
2928
+ ```python
2929
+ from lib_layered_config import deploy_config
2930
+
2931
+ # Deploy production configuration to the production profile
2932
+ created_paths = deploy_config(
2933
+ source="./configs/production.toml",
2934
+ vendor="Acme",
2935
+ app="MyApp",
2936
+ targets=["app"],
2937
+ slug="myapp",
2938
+ profile="production" # Deploy to profile-specific subdirectory
2939
+ )
2940
+
2941
+ # On Linux: /etc/xdg/myapp/profile/production/config.toml
2942
+ # On macOS: /Library/Application Support/Acme/MyApp/profile/production/config.toml
2943
+ # On Windows: C:\ProgramData\Acme\MyApp\profile\production\config.toml
2944
+
2945
+ print(f"Production config deployed to: {created_paths[0]}")
2946
+
2947
+ # Deploy test configuration to a separate profile
2948
+ test_paths = deploy_config(
2949
+ source="./configs/test.toml",
2950
+ vendor="Acme",
2951
+ app="MyApp",
2952
+ targets=["app", "user"],
2953
+ slug="myapp",
2954
+ profile="test" # Completely isolated from production
2955
+ )
2956
+
2957
+ # On Linux: /etc/xdg/myapp/profile/test/config.toml
2958
+ # ~/.config/myapp/profile/test/config.toml
2959
+ ```
2960
+ **Explanation:** Use the `profile` parameter to deploy environment-specific configurations to isolated subdirectories. This keeps production, staging, and test configurations completely separate, preventing accidental cross-environment configuration leaks.
2961
+
2962
+ **Example 7: Deploy multiple profiles in a CI/CD pipeline**
2963
+ ```python
2964
+ from lib_layered_config import deploy_config
2965
+ from pathlib import Path
2966
+
2967
+ # Deploy configurations for all environments
2968
+ environments = ["development", "staging", "production"]
2969
+
2970
+ for env in environments:
2971
+ config_file = Path(f"./environments/{env}.toml")
2972
+ if not config_file.exists():
2973
+ print(f"⚠ Skipping {env}: config file not found")
2974
+ continue
2975
+
2976
+ paths = deploy_config(
2977
+ source=config_file,
2978
+ vendor="Acme",
2979
+ app="MyApp",
2980
+ targets=["app"],
2981
+ slug="myapp",
2982
+ profile=env,
2983
+ force=True # Update existing configs
2984
+ )
2985
+ print(f"✓ Deployed {env} config to: {paths[0]}")
2986
+ ```
2987
+ **Explanation:** Profiles are ideal for CI/CD pipelines where you need to deploy different configurations for each environment. Each profile is isolated, so you can safely deploy all environments to the same system.
2988
+
2989
+ ---
2990
+
2991
+ ### `generate_examples`
2992
+
2993
+ Generate example configuration trees for documentation or onboarding.
2994
+
2995
+ **Parameters:**
2996
+ - `destination` (str | Path, required): Directory that will receive the example tree.
2997
+ - `slug` (str, required): Configuration slug used in generated files.
2998
+ - `vendor` (str, required): Vendor namespace.
2999
+ - `app` (str, required): Application name.
3000
+ - `force` (bool, optional): Overwrite existing example files. Default: `False`.
3001
+ - `platform` (str | None, optional): Override platform layout. Valid values: `"posix"`, `"windows"`. Default: `None` (uses current platform).
3002
+
3003
+ **Returns:** List of `Path` objects for files created.
3004
+
3005
+ **Examples:**
3006
+
3007
+ **Example 1: Generate documentation examples**
3008
+ ```python
3009
+ from lib_layered_config import generate_examples
3010
+ from pathlib import Path
3011
+
3012
+ # Generate example configuration files for documentation
3013
+ docs_dir = Path("./docs/examples")
3014
+ created_files = generate_examples(
3015
+ destination=docs_dir,
3016
+ slug="myapp",
3017
+ vendor="Acme",
3018
+ app="MyApp",
3019
+ platform="posix" # Generate Linux/macOS examples
3020
+ )
3021
+
3022
+ print(f"Generated {len(created_files)} example files:")
3023
+ for file_path in created_files:
3024
+ relative = file_path.relative_to(docs_dir)
3025
+ print(f" - {relative}")
3026
+
3027
+ # Output shows:
3028
+ # - etc/myapp/config.toml (app defaults)
3029
+ # - etc/myapp/hosts/your-hostname.toml (host overrides)
3030
+ # - xdg/myapp/config.toml (user preferences)
3031
+ # - xdg/myapp/config.d/10-override.toml (split overrides)
3032
+ # - .env.example (environment variables)
3033
+ ```
3034
+ **Explanation:** Perfect for generating example configurations to include in your documentation or repository. Users can copy these examples to get started quickly.
3035
+
3036
+ **Example 2: Generate Windows examples for cross-platform project**
3037
+ ```python
3038
+ from lib_layered_config import generate_examples
3039
+ from pathlib import Path
3040
+
3041
+ # Generate Windows-specific examples even on Linux/macOS
3042
+ windows_examples = Path("./docs/examples-windows")
3043
+ created_files = generate_examples(
3044
+ destination=windows_examples,
3045
+ slug="myapp",
3046
+ vendor="Acme",
3047
+ app="MyApp",
3048
+ platform="windows" # Force Windows layout
3049
+ )
3050
+
3051
+ print("Windows configuration examples:")
3052
+ for file_path in created_files:
3053
+ print(f" {file_path.relative_to(windows_examples)}")
3054
+
3055
+ # Output shows Windows paths:
3056
+ # - ProgramData/Acme/MyApp/config.toml
3057
+ # - ProgramData/Acme/MyApp/hosts/your-hostname.toml
3058
+ # - AppData/Roaming/Acme/MyApp/config.toml
3059
+ # - .env.example
3060
+ ```
3061
+ **Explanation:** Generate platform-specific examples regardless of your current OS. Great for maintaining documentation for all supported platforms.
3062
+
3063
+ **Example 3: Onboarding script - Generate and customize examples**
3064
+ ```python
3065
+ from lib_layered_config import generate_examples
3066
+ from pathlib import Path
3067
+
3068
+ def onboard_user(username: str):
3069
+ """Generate personalized configuration examples for a new user."""
3070
+
3071
+ # Create user-specific examples directory
3072
+ user_examples = Path(f"/tmp/{username}-config-examples")
3073
+ user_examples.mkdir(exist_ok=True)
3074
+
3075
+ # Generate example files
3076
+ created = generate_examples(
3077
+ destination=user_examples,
3078
+ slug="myapp",
3079
+ vendor="Acme",
3080
+ app="MyApp"
3081
+ )
3082
+
3083
+ print(f"Generated {len(created)} example files for {username}:")
3084
+
3085
+ # Customize the examples with user-specific values
3086
+ user_config = user_examples / "xdg/myapp/config.toml"
3087
+ if user_config.exists():
3088
+ content = user_config.read_text()
3089
+ # Add user-specific comment
3090
+ content = f"# Configuration for {username}\n" + content
3091
+ user_config.write_text(content)
3092
+
3093
+ print(f"\nExamples generated in: {user_examples}")
3094
+ print("Copy these files to get started:")
3095
+ for f in created:
3096
+ print(f" {f.relative_to(user_examples)}")
3097
+
3098
+ # Run onboarding
3099
+ onboard_user("alice")
3100
+ ```
3101
+ **Explanation:** Generate examples as part of an onboarding workflow. You can then customize the generated files programmatically before presenting them to users.
3102
+
3103
+ **Example 4: Update examples (force overwrite)**
3104
+ ```python
3105
+ from lib_layered_config import generate_examples
3106
+ from pathlib import Path
3107
+
3108
+ # Regenerate examples, overwriting existing ones
3109
+ examples_dir = Path("./examples")
3110
+ created = generate_examples(
3111
+ destination=examples_dir,
3112
+ slug="myapp",
3113
+ vendor="Acme",
3114
+ app="MyApp",
3115
+ force=True # Overwrite existing examples
3116
+ )
3117
+
3118
+ print(f"Regenerated {len(created)} example files")
3119
+
3120
+ # This is useful when you update your configuration schema
3121
+ # and need to refresh the documentation examples
3122
+ ```
3123
+ **Explanation:** Use `force=True` when updating examples after schema changes. This ensures all example files reflect your latest configuration structure.
3124
+
3125
+ **Example 5: Generate both POSIX and Windows examples**
3126
+ ```python
3127
+ from lib_layered_config import generate_examples
3128
+ from pathlib import Path
3129
+
3130
+ def generate_all_examples():
3131
+ """Generate examples for all platforms."""
3132
+
3133
+ base_dir = Path("./docs/config-examples")
3134
+
3135
+ # Generate POSIX examples
3136
+ posix_files = generate_examples(
3137
+ destination=base_dir / "linux-macos",
3138
+ slug="myapp",
3139
+ vendor="Acme",
3140
+ app="MyApp",
3141
+ platform="posix"
3142
+ )
3143
+ print(f"Generated {len(posix_files)} POSIX examples")
3144
+
3145
+ # Generate Windows examples
3146
+ windows_files = generate_examples(
3147
+ destination=base_dir / "windows",
3148
+ slug="myapp",
3149
+ vendor="Acme",
3150
+ app="MyApp",
3151
+ platform="windows"
3152
+ )
3153
+ print(f"Generated {len(windows_files)} Windows examples")
3154
+
3155
+ print(f"\nTotal: {len(posix_files) + len(windows_files)} example files")
3156
+ print(f"Location: {base_dir}")
3157
+
3158
+ generate_all_examples()
3159
+ ```
3160
+ **Explanation:** Generate complete documentation showing users how to configure your app on any platform. This is essential for cross-platform applications.
3161
+
3162
+ ---
3163
+
3164
+ ### `i_should_fail`
3165
+
3166
+ Intentionally raise a `RuntimeError` for testing error handling.
3167
+
3168
+ **Parameters:** None
3169
+
3170
+ **Raises:** `RuntimeError` with message `"i should fail"`.
3171
+
3172
+ **Example:**
3173
+ ```python
3174
+ from lib_layered_config import i_should_fail
3175
+
3176
+ try:
3177
+ i_should_fail()
3178
+ except RuntimeError as e:
3179
+ print(f"Caught expected error: {e}")
3180
+ ```
3181
+
3182
+ ## Example Generation & Deployment
3183
+
3184
+ Use the Python helpers or CLI equivalents:
3185
+
3186
+ ```python
3187
+ from pathlib import Path
3188
+ from lib_layered_config.examples import deploy_config, generate_examples
3189
+
3190
+ # copy one file into the system/user layers
3191
+ paths = deploy_config("./myapp/config.toml", vendor="Acme", app="ConfigKit", targets=("app", "user"))
3192
+
3193
+ # scaffold an example tree for documentation
3194
+ examples = generate_examples(Path("./examples"), slug="config-kit", vendor="Acme", app="ConfigKit")
3195
+ ```
3196
+
3197
+ ## Provenance & Observability
3198
+
3199
+ - Every merged key stores metadata (`layer`, `path`, `key`).
3200
+ - Structured logging lives in `lib_layered_config.observability` (trace-aware `log_debug`, `log_info`, `log_warn`, `log_error`).
3201
+ - Use `bind_trace_id("abc123")` to correlate CLI/log events with your own tracing.
3202
+
3203
+ ### Type Conflict Warnings
3204
+
3205
+ When a later layer overwrites a scalar value with a mapping (or vice versa), a warning is emitted:
3206
+
3207
+ ```python
3208
+ import logging
3209
+ logging.basicConfig(level=logging.WARNING)
3210
+
3211
+ # If user.toml has: service = "disabled"
3212
+ # And app.toml has: [service]
3213
+ # timeout = 30
3214
+ # A WARNING log is emitted: "type_conflict" with details about the key, layers, and types involved
3215
+ ```
3216
+
3217
+ This helps identify configuration mismatches where a key changes from a simple value to a nested structure (or the reverse) across layers.
3218
+
3219
+ ## Further documentation
3220
+
3221
+ - [CHANGELOG](CHANGELOG.md) — user-facing release notes.
3222
+ - [CONTRIBUTING](CONTRIBUTING.md) — guidelines for issues, pull requests, and coding style.
3223
+ - [DEVELOPMENT](DEVELOPMENT.md) — local tooling, recommended workflow, and release checklist.
3224
+ - [Module Reference](docs/systemdesign/module_reference.md) — architecture-aligned responsibilities per module.
3225
+ - [LICENSE](LICENSE) — MIT license text.
3226
+
3227
+
3228
+ ## Development
3229
+
3230
+ ```bash
3231
+ pip install "lib_layered_config[dev]"
3232
+ make test # lint + type-check + pytest + coverage (fail-under=90%)
3233
+ make build # build wheel / sdist artifacts
3234
+ make run -- --help # run the CLI via the repo entrypoint
3235
+ ```
3236
+
3237
+ The development extra now targets the latest stable releases of the toolchain
3238
+ (pytest 8.4.2, ruff 0.14.0, codecov-cli 11.2.3, etc.), so upgrading your local
3239
+ environment before running `make` is recommended.
3240
+
3241
+ *Formatting gate:* Ruff formatting runs in check mode during `make test`. Run `ruff format .` (or `pre-commit run --all-files`) before pushing and consider `pre-commit install` to keep local edits aligned.
3242
+
3243
+ *Coverage gate:* the maintained test suite must stay ≥90% (see `pyproject.toml`). Add targeted unit tests if you extend functionality.
3244
+
3245
+ **Platform notes**
3246
+
3247
+ - Windows runners install `pipx` and `uv` automatically in CI; locally ensure `pipx` is on your `PATH` before running `make test` so the wheel verification step succeeds.
3248
+ - The journald prerequisite step runs only on Linux; macOS/Windows skips it, so there is no extra setup required on those platforms.
3249
+
3250
+ ### Continuous integration
3251
+
3252
+ The GitHub Actions workflow executes three jobs:
3253
+
3254
+ - **Test matrix** (Linux/macOS/Windows, Python 3.10-3.13 + latest 3.x) running the same pipeline as `make test`.
3255
+ - **pipx / uv verification** to prove the built wheel installs cleanly with the common Python app launchers.
3256
+ - **Notebook smoke test** that executes `notebooks/Quickstart.ipynb` to keep the tutorial in sync using the native nbformat workflow (no compatibility shims required).
3257
+ - CLI jobs run through `lib_cli_exit_tools.cli_session`, ensuring the `--traceback` flag behaves the same locally and in automation.
3258
+
3259
+ Packaging-specific jobs (conda, Nix, Homebrew sync) were retired; the Python packaging metadata in `pyproject.toml` remains the single source of truth.
3260
+
3261
+ ## License
3262
+
3263
+ MIT © Robert Nowotny