lib-layered-config 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of lib-layered-config might be problematic. Click here for more details.
- lib_layered_config/__init__.py +60 -0
- lib_layered_config/__main__.py +19 -0
- lib_layered_config/_layers.py +457 -0
- lib_layered_config/_platform.py +200 -0
- lib_layered_config/adapters/__init__.py +13 -0
- lib_layered_config/adapters/dotenv/__init__.py +1 -0
- lib_layered_config/adapters/dotenv/default.py +438 -0
- lib_layered_config/adapters/env/__init__.py +5 -0
- lib_layered_config/adapters/env/default.py +509 -0
- lib_layered_config/adapters/file_loaders/__init__.py +1 -0
- lib_layered_config/adapters/file_loaders/structured.py +410 -0
- lib_layered_config/adapters/path_resolvers/__init__.py +1 -0
- lib_layered_config/adapters/path_resolvers/default.py +727 -0
- lib_layered_config/application/__init__.py +12 -0
- lib_layered_config/application/merge.py +442 -0
- lib_layered_config/application/ports.py +109 -0
- lib_layered_config/cli/__init__.py +162 -0
- lib_layered_config/cli/common.py +232 -0
- lib_layered_config/cli/constants.py +12 -0
- lib_layered_config/cli/deploy.py +70 -0
- lib_layered_config/cli/fail.py +21 -0
- lib_layered_config/cli/generate.py +60 -0
- lib_layered_config/cli/info.py +31 -0
- lib_layered_config/cli/read.py +117 -0
- lib_layered_config/core.py +384 -0
- lib_layered_config/domain/__init__.py +7 -0
- lib_layered_config/domain/config.py +490 -0
- lib_layered_config/domain/errors.py +65 -0
- lib_layered_config/examples/__init__.py +29 -0
- lib_layered_config/examples/deploy.py +305 -0
- lib_layered_config/examples/generate.py +537 -0
- lib_layered_config/observability.py +306 -0
- lib_layered_config/py.typed +0 -0
- lib_layered_config/testing.py +55 -0
- lib_layered_config-1.0.0.dist-info/METADATA +366 -0
- lib_layered_config-1.0.0.dist-info/RECORD +39 -0
- lib_layered_config-1.0.0.dist-info/WHEEL +4 -0
- lib_layered_config-1.0.0.dist-info/entry_points.txt +3 -0
- lib_layered_config-1.0.0.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,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")
|