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.
- lib_layered_config/__init__.py +58 -0
- lib_layered_config/__init__conf__.py +74 -0
- lib_layered_config/__main__.py +18 -0
- lib_layered_config/_layers.py +310 -0
- lib_layered_config/_platform.py +166 -0
- lib_layered_config/adapters/__init__.py +13 -0
- lib_layered_config/adapters/_nested_keys.py +126 -0
- lib_layered_config/adapters/dotenv/__init__.py +1 -0
- lib_layered_config/adapters/dotenv/default.py +143 -0
- lib_layered_config/adapters/env/__init__.py +5 -0
- lib_layered_config/adapters/env/default.py +288 -0
- lib_layered_config/adapters/file_loaders/__init__.py +1 -0
- lib_layered_config/adapters/file_loaders/structured.py +376 -0
- lib_layered_config/adapters/path_resolvers/__init__.py +28 -0
- lib_layered_config/adapters/path_resolvers/_base.py +166 -0
- lib_layered_config/adapters/path_resolvers/_dotenv.py +74 -0
- lib_layered_config/adapters/path_resolvers/_linux.py +89 -0
- lib_layered_config/adapters/path_resolvers/_macos.py +93 -0
- lib_layered_config/adapters/path_resolvers/_windows.py +126 -0
- lib_layered_config/adapters/path_resolvers/default.py +194 -0
- lib_layered_config/application/__init__.py +12 -0
- lib_layered_config/application/merge.py +379 -0
- lib_layered_config/application/ports.py +115 -0
- lib_layered_config/cli/__init__.py +92 -0
- lib_layered_config/cli/common.py +381 -0
- lib_layered_config/cli/constants.py +12 -0
- lib_layered_config/cli/deploy.py +71 -0
- lib_layered_config/cli/fail.py +19 -0
- lib_layered_config/cli/generate.py +57 -0
- lib_layered_config/cli/info.py +29 -0
- lib_layered_config/cli/read.py +120 -0
- lib_layered_config/core.py +301 -0
- lib_layered_config/domain/__init__.py +7 -0
- lib_layered_config/domain/config.py +372 -0
- lib_layered_config/domain/errors.py +59 -0
- lib_layered_config/domain/identifiers.py +366 -0
- lib_layered_config/examples/__init__.py +29 -0
- lib_layered_config/examples/deploy.py +333 -0
- lib_layered_config/examples/generate.py +406 -0
- lib_layered_config/observability.py +209 -0
- lib_layered_config/py.typed +0 -0
- lib_layered_config/testing.py +46 -0
- lib_layered_config-4.1.0.dist-info/METADATA +3263 -0
- lib_layered_config-4.1.0.dist-info/RECORD +47 -0
- lib_layered_config-4.1.0.dist-info/WHEEL +4 -0
- lib_layered_config-4.1.0.dist-info/entry_points.txt +3 -0
- 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
|
+
[](https://github.com/bitranox/lib_layered_config/actions/workflows/ci.yml)
|
|
51
|
+
[](https://github.com/bitranox/lib_layered_config/actions/workflows/codeql.yml)
|
|
52
|
+
[](LICENSE)
|
|
53
|
+
[](https://codespaces.new/bitranox/lib_layered_config?quickstart=1)
|
|
54
|
+
[](https://pypi.org/project/lib-layered-config/)
|
|
55
|
+
[](https://pypi.org/project/lib-layered-config/)
|
|
56
|
+
[](https://docs.astral.sh/ruff/)
|
|
57
|
+
[](https://codecov.io/gh/bitranox/lib_layered_config)
|
|
58
|
+
[](https://qlty.sh/gh/bitranox/projects/lib_layered_config)
|
|
59
|
+
[](https://snyk.io/test/github/bitranox/lib_layered_config)
|
|
60
|
+
[](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
|