lib-layered-config 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of lib-layered-config might be problematic. Click here for more details.

Files changed (39) hide show
  1. lib_layered_config/__init__.py +60 -0
  2. lib_layered_config/__main__.py +19 -0
  3. lib_layered_config/_layers.py +457 -0
  4. lib_layered_config/_platform.py +200 -0
  5. lib_layered_config/adapters/__init__.py +13 -0
  6. lib_layered_config/adapters/dotenv/__init__.py +1 -0
  7. lib_layered_config/adapters/dotenv/default.py +438 -0
  8. lib_layered_config/adapters/env/__init__.py +5 -0
  9. lib_layered_config/adapters/env/default.py +509 -0
  10. lib_layered_config/adapters/file_loaders/__init__.py +1 -0
  11. lib_layered_config/adapters/file_loaders/structured.py +410 -0
  12. lib_layered_config/adapters/path_resolvers/__init__.py +1 -0
  13. lib_layered_config/adapters/path_resolvers/default.py +727 -0
  14. lib_layered_config/application/__init__.py +12 -0
  15. lib_layered_config/application/merge.py +442 -0
  16. lib_layered_config/application/ports.py +109 -0
  17. lib_layered_config/cli/__init__.py +162 -0
  18. lib_layered_config/cli/common.py +232 -0
  19. lib_layered_config/cli/constants.py +12 -0
  20. lib_layered_config/cli/deploy.py +70 -0
  21. lib_layered_config/cli/fail.py +21 -0
  22. lib_layered_config/cli/generate.py +60 -0
  23. lib_layered_config/cli/info.py +31 -0
  24. lib_layered_config/cli/read.py +117 -0
  25. lib_layered_config/core.py +384 -0
  26. lib_layered_config/domain/__init__.py +7 -0
  27. lib_layered_config/domain/config.py +490 -0
  28. lib_layered_config/domain/errors.py +65 -0
  29. lib_layered_config/examples/__init__.py +29 -0
  30. lib_layered_config/examples/deploy.py +305 -0
  31. lib_layered_config/examples/generate.py +537 -0
  32. lib_layered_config/observability.py +306 -0
  33. lib_layered_config/py.typed +0 -0
  34. lib_layered_config/testing.py +55 -0
  35. lib_layered_config-1.0.0.dist-info/METADATA +366 -0
  36. lib_layered_config-1.0.0.dist-info/RECORD +39 -0
  37. lib_layered_config-1.0.0.dist-info/WHEEL +4 -0
  38. lib_layered_config-1.0.0.dist-info/entry_points.txt +3 -0
  39. lib_layered_config-1.0.0.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,537 @@
1
+ """Example configuration asset generation helpers.
2
+
3
+ Purpose
4
+ -------
5
+ Produce reproducible configuration scaffolding referenced in documentation and
6
+ onboarding materials. This module belongs to the outer ring of the architecture
7
+ and has no runtime coupling to the composition root.
8
+
9
+ Contents
10
+ - ``DEFAULT_HOST_PLACEHOLDER``: filename stub for host examples.
11
+ - ``ExampleSpec``: dataclass capturing a relative path and text content.
12
+ - ``generate_examples``: public orchestration expressed through helper
13
+ verbs.
14
+ - ``_build_specs``: yields platform-aware specifications.
15
+ - ``_write_spec`` / ``_should_write`` / ``_ensure_parent``: tiny filesystem
16
+ helpers that narrate how files are written.
17
+
18
+ System Role
19
+ -----------
20
+ Called by docs/scripts to create filesystem layouts demonstrating how layered
21
+ configuration works.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from dataclasses import dataclass
27
+ from pathlib import Path
28
+ from typing import Iterator
29
+
30
+ import os
31
+
32
+ from .._platform import normalise_examples_platform
33
+
34
+ DEFAULT_HOST_PLACEHOLDER = "your-hostname"
35
+ """Filename stub used for host-specific example files (documented in README)."""
36
+
37
+
38
+ @dataclass(slots=True)
39
+ class ExampleSpec:
40
+ """Describe a single example file to be written to disk.
41
+
42
+ Why
43
+ ----
44
+ Encapsulate metadata for templated files so generation logic stays simple
45
+ and testable.
46
+
47
+ Attributes
48
+ ----------
49
+ relative_path:
50
+ Path relative to the destination directory where the example will be
51
+ created.
52
+ content:
53
+ File contents (UTF-8 text) including explanatory comments.
54
+ """
55
+
56
+ relative_path: Path
57
+ content: str
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class ExamplePlan:
62
+ """Plan describing how example files should be generated.
63
+
64
+ Attributes
65
+ ----------
66
+ destination:
67
+ Target directory where files will be written.
68
+ slug:
69
+ Configuration slug used in templates.
70
+ vendor:
71
+ Vendor name interpolated into paths/content.
72
+ app:
73
+ Application name interpolated into paths/content.
74
+ force:
75
+ Whether generation should overwrite existing files.
76
+ platform:
77
+ Normalised platform key (``"posix"`` or ``"windows"``).
78
+ """
79
+
80
+ destination: Path
81
+ slug: str
82
+ vendor: str
83
+ app: str
84
+ force: bool
85
+ platform: str
86
+
87
+
88
+ def generate_examples(
89
+ destination: str | Path,
90
+ *,
91
+ slug: str,
92
+ vendor: str,
93
+ app: str,
94
+ force: bool = False,
95
+ platform: str | None = None,
96
+ ) -> list[Path]:
97
+ """Write the canonical example files for each configuration layer.
98
+
99
+ Why
100
+ ----
101
+ Quickly bootstrap demos, tests, or documentation assets that mirror the
102
+ recommended filesystem layout.
103
+
104
+ Parameters
105
+ ----------
106
+ destination:
107
+ Directory that will receive the generated structure.
108
+ slug / vendor / app:
109
+ Metadata used to fill placeholders so examples read naturally.
110
+ force:
111
+ When ``True`` existing files are overwritten; otherwise the function
112
+ skips files that already exist.
113
+ platform:
114
+ Optional override for the OS layout (``"posix"`` or ``"windows"``).
115
+ When ``None`` it follows the running interpreter platform.
116
+
117
+ Returns
118
+ -------
119
+ list[Path]
120
+ Absolute file paths written during this invocation.
121
+
122
+ Side Effects
123
+ ------------
124
+ Creates directories and writes files under ``destination``.
125
+
126
+ Examples
127
+ --------
128
+ >>> from tempfile import TemporaryDirectory
129
+ >>> tmp = TemporaryDirectory()
130
+ >>> generated = generate_examples(tmp.name, slug='demo', vendor='Acme', app='ConfigKit')
131
+ >>> any(path.name == 'config.toml' for path in generated)
132
+ True
133
+ >>> tmp.cleanup()
134
+ """
135
+
136
+ plan = _build_example_plan(
137
+ destination=destination,
138
+ slug=slug,
139
+ vendor=vendor,
140
+ app=app,
141
+ force=force,
142
+ platform=platform,
143
+ )
144
+ specs = _build_specs(plan.destination, slug=plan.slug, vendor=plan.vendor, app=plan.app, platform=plan.platform)
145
+ return _write_examples(plan.destination, specs, plan.force)
146
+
147
+
148
+ def _build_example_plan(
149
+ *,
150
+ destination: str | Path,
151
+ slug: str,
152
+ vendor: str,
153
+ app: str,
154
+ force: bool,
155
+ platform: str | None,
156
+ ) -> ExamplePlan:
157
+ """Compose an example generation plan.
158
+
159
+ Parameters
160
+ ----------
161
+ destination:
162
+ Root destination directory (string or :class:`Path`).
163
+ slug / vendor / app:
164
+ Identifiers embedded into generated content.
165
+ force:
166
+ Whether existing files may be overwritten.
167
+ platform:
168
+ Optional platform override supplied by the caller.
169
+
170
+ Returns
171
+ -------
172
+ ExamplePlan
173
+ Immutable plan consumed by downstream helpers.
174
+ """
175
+
176
+ dest = Path(destination)
177
+ return ExamplePlan(
178
+ destination=dest,
179
+ slug=slug,
180
+ vendor=vendor,
181
+ app=app,
182
+ force=force,
183
+ platform=_normalise_platform(platform),
184
+ )
185
+
186
+
187
+ def _write_examples(destination: Path, specs: Iterator[ExampleSpec], force: bool) -> list[Path]:
188
+ """Write all ``specs`` under *destination* honouring the *force* flag.
189
+
190
+ Why
191
+ ----
192
+ Centralise the loop that applies ``force`` semantics and records written paths.
193
+
194
+ Parameters
195
+ ----------
196
+ destination:
197
+ Root directory that will receive the examples.
198
+ specs:
199
+ Iterator of example specifications to materialise.
200
+ force:
201
+ When ``True`` existing files are overwritten.
202
+
203
+ Returns
204
+ -------
205
+ list[Path]
206
+ Paths written during this invocation.
207
+
208
+ Side Effects
209
+ ------------
210
+ Creates directories and writes files to disk.
211
+ """
212
+
213
+ written: list[Path] = []
214
+ for spec in specs:
215
+ path = destination / spec.relative_path
216
+ if not _should_write(path, force):
217
+ continue
218
+ _ensure_parent(path)
219
+ _write_spec(path, spec)
220
+ written.append(path)
221
+ return written
222
+
223
+
224
+ def _write_spec(path: Path, spec: ExampleSpec) -> None:
225
+ """Persist ``spec`` content at *path* using UTF-8 encoding.
226
+
227
+ Why
228
+ ----
229
+ Keep the actual write primitive isolated for easy stubbing in tests.
230
+
231
+ Parameters
232
+ ----------
233
+ path:
234
+ Destination path for the example file.
235
+ spec:
236
+ Example specification containing content to write.
237
+
238
+ Side Effects
239
+ ------------
240
+ Writes UTF-8 text to *path*.
241
+ """
242
+
243
+ path.write_text(spec.content, encoding="utf-8")
244
+
245
+
246
+ def _should_write(path: Path, force: bool) -> bool:
247
+ """Return ``True`` when *path* should be written respecting *force*.
248
+
249
+ Why
250
+ ----
251
+ Avoid clobbering existing content unless the caller explicitly requests it.
252
+
253
+ Parameters
254
+ ----------
255
+ path:
256
+ Destination path under consideration.
257
+ force:
258
+ Whether overwriting is allowed.
259
+
260
+ Returns
261
+ -------
262
+ bool
263
+ ``True`` when writing should proceed.
264
+
265
+ Examples
266
+ --------
267
+ >>> from tempfile import NamedTemporaryFile
268
+ >>> tmp = NamedTemporaryFile(delete=True)
269
+ >>> _should_write(Path(tmp.name), force=False)
270
+ False
271
+ >>> _should_write(Path(tmp.name), force=True)
272
+ True
273
+ >>> tmp.close()
274
+ """
275
+
276
+ return force or not path.exists()
277
+
278
+
279
+ def _ensure_parent(path: Path) -> None:
280
+ """Create parent directories for *path* when missing.
281
+
282
+ Why
283
+ ----
284
+ Ensure example generation works even on fresh directories.
285
+
286
+ Parameters
287
+ ----------
288
+ path:
289
+ Target file path whose parent directories should exist.
290
+
291
+ Side Effects
292
+ ------------
293
+ Creates directories on disk.
294
+ """
295
+
296
+ path.parent.mkdir(parents=True, exist_ok=True)
297
+
298
+
299
+ def _build_specs(destination: Path, *, slug: str, vendor: str, app: str, platform: str) -> Iterator[ExampleSpec]:
300
+ """Yield :class:`ExampleSpec` instances for each canonical layer.
301
+
302
+ Why
303
+ ----
304
+ Keep file templates in one place so they stay aligned with documentation.
305
+
306
+ Parameters
307
+ ----------
308
+ destination:
309
+ Destination root (currently unused; reserved for future dynamic templates).
310
+ slug / vendor / app:
311
+ Metadata interpolated into template content.
312
+ platform:
313
+ Normalised platform key (``"posix"`` or ``"windows"``).
314
+
315
+ Yields
316
+ ------
317
+ ExampleSpec
318
+ Specification describing one file to render.
319
+
320
+ Examples
321
+ --------
322
+ >>> specs = list(_build_specs(Path('.'), slug='demo', vendor='Acme', app='ConfigKit', platform='posix'))
323
+ >>> specs[0].relative_path.as_posix()
324
+ 'etc/demo/config.toml'
325
+ """
326
+
327
+ yield from _platform_specs(slug=slug, vendor=vendor, app=app, platform=platform)
328
+ yield _env_example(slug)
329
+
330
+
331
+ def _platform_specs(*, slug: str, vendor: str, app: str, platform: str) -> Iterator[ExampleSpec]:
332
+ """Dispatch to platform-specific example specifications.
333
+
334
+ Parameters
335
+ ----------
336
+ slug / vendor / app:
337
+ Metadata interpolated into generated content.
338
+ platform:
339
+ Normalised platform key (``"posix"`` or ``"windows"``).
340
+
341
+ Yields
342
+ ------
343
+ ExampleSpec
344
+ Specifications describing files to create.
345
+ """
346
+
347
+ if platform == "windows":
348
+ yield from _windows_specs(slug=slug, vendor=vendor, app=app)
349
+ return
350
+ yield from _posix_specs(slug=slug, vendor=vendor, app=app)
351
+
352
+
353
+ def _windows_specs(*, slug: str, vendor: str, app: str) -> Iterator[ExampleSpec]:
354
+ """Yield Windows layout examples.
355
+
356
+ Parameters
357
+ ----------
358
+ slug / vendor / app:
359
+ Metadata interpolated into template content.
360
+
361
+ Yields
362
+ ------
363
+ ExampleSpec
364
+ Specification for each Windows example file.
365
+ """
366
+
367
+ root = Path("ProgramData") / vendor / app
368
+ yield ExampleSpec(root / "config.toml", _app_defaults_body(slug))
369
+ yield ExampleSpec(root / "hosts" / f"{DEFAULT_HOST_PLACEHOLDER}.toml", _host_override_body())
370
+ user_root = Path("AppData") / "Roaming" / vendor / app
371
+ yield ExampleSpec(user_root / "config.toml", _user_preferences_body(vendor, app))
372
+ yield ExampleSpec(user_root / "config.d" / "10-override.toml", _split_override_body())
373
+
374
+
375
+ def _posix_specs(*, slug: str, vendor: str, app: str) -> Iterator[ExampleSpec]:
376
+ """Yield POSIX layout examples.
377
+
378
+ Parameters
379
+ ----------
380
+ slug / vendor / app:
381
+ Metadata interpolated into template content.
382
+
383
+ Yields
384
+ ------
385
+ ExampleSpec
386
+ Specification for each POSIX example file.
387
+ """
388
+
389
+ slug_root = Path("etc") / slug
390
+ yield ExampleSpec(slug_root / "config.toml", _app_defaults_body(slug))
391
+ yield ExampleSpec(slug_root / "hosts" / f"{DEFAULT_HOST_PLACEHOLDER}.toml", _host_override_body())
392
+ user_root = Path("xdg") / slug
393
+ yield ExampleSpec(user_root / "config.toml", _user_preferences_body(vendor, app))
394
+ yield ExampleSpec(user_root / "config.d" / "10-override.toml", _split_override_body())
395
+
396
+
397
+ def _env_example(slug: str) -> ExampleSpec:
398
+ """Return the shared .env example specification.
399
+
400
+ Parameters
401
+ ----------
402
+ slug:
403
+ Configuration slug used when building environment variable names.
404
+
405
+ Returns
406
+ -------
407
+ ExampleSpec
408
+ Specification describing the `.env.example` file.
409
+ """
410
+
411
+ return ExampleSpec(Path(".env.example"), _env_secrets_body(slug))
412
+
413
+
414
+ def _app_defaults_body(slug: str) -> str:
415
+ """Describe baseline application defaults.
416
+
417
+ Parameters
418
+ ----------
419
+ slug:
420
+ Configuration slug inserted into the template heading.
421
+
422
+ Returns
423
+ -------
424
+ str
425
+ TOML content explaining application-wide defaults.
426
+ """
427
+
428
+ return f"""# Application-wide defaults for {slug}
429
+ [service]
430
+ endpoint = "https://api.example.com"
431
+ timeout = 10
432
+ """
433
+
434
+
435
+ def _host_override_body() -> str:
436
+ """Describe host-level overrides.
437
+
438
+ Returns
439
+ -------
440
+ str
441
+ TOML content illustrating host-specific timeout overrides.
442
+ """
443
+
444
+ return """# Host overrides (replace filename with the machine hostname)
445
+ [service]
446
+ timeout = 15
447
+ """
448
+
449
+
450
+ def _user_preferences_body(vendor: str, app: str) -> str:
451
+ """Describe user-level preferences.
452
+
453
+ Parameters
454
+ ----------
455
+ vendor / app:
456
+ Metadata interpolated into the template to keep prose friendly.
457
+
458
+ Returns
459
+ -------
460
+ str
461
+ TOML content illustrating user-level overrides.
462
+ """
463
+
464
+ return f"""# User-specific preferences for {vendor} {app}
465
+ [service]
466
+ retry = 2
467
+ """
468
+
469
+
470
+ def _split_override_body() -> str:
471
+ """Describe config.d overrides used for granular layering.
472
+
473
+ Returns
474
+ -------
475
+ str
476
+ TOML content emphasising lexicographic ordering of split overrides.
477
+ """
478
+
479
+ return """# Split overrides live in config.d/ and apply in lexical order
480
+ [service]
481
+ retry = 3
482
+ """
483
+
484
+
485
+ def _env_secrets_body(slug: str) -> str:
486
+ """Describe .env secrets guidance.
487
+
488
+ Parameters
489
+ ----------
490
+ slug:
491
+ Configuration slug converted into an uppercase environment prefix.
492
+
493
+ Returns
494
+ -------
495
+ str
496
+ `.env` template content reminding users to provide secrets.
497
+ """
498
+
499
+ key = slug.replace("-", "_").upper()
500
+ return f"""# Copy to .env to provide secrets and local overrides
501
+ {key}_SERVICE__PASSWORD=changeme
502
+ """
503
+
504
+
505
+ def _normalise_platform(value: str | None) -> str:
506
+ """Return a canonical platform key for example generation.
507
+
508
+ Parameters
509
+ ----------
510
+ value:
511
+ Optional platform alias supplied by the caller.
512
+
513
+ Returns
514
+ -------
515
+ str
516
+ Normalised platform key (``"posix"`` or ``"windows"``).
517
+
518
+ Raises
519
+ ------
520
+ ValueError
521
+ When *value* is invalid.
522
+
523
+ Examples
524
+ --------
525
+ >>> _normalise_platform('posix')
526
+ 'posix'
527
+ >>> _normalise_platform(None) in {'posix', 'windows'}
528
+ True
529
+ """
530
+
531
+ if value is None:
532
+ return "windows" if os.name == "nt" else "posix"
533
+ try:
534
+ resolved = normalise_examples_platform(value)
535
+ except ValueError as exc: # pragma: no cover - validated via CLI helpers
536
+ raise ValueError(str(exc)) from exc
537
+ return resolved or ("windows" if os.name == "nt" else "posix")