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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. lib_layered_config/__init__.py +58 -0
  2. lib_layered_config/__init__conf__.py +74 -0
  3. lib_layered_config/__main__.py +18 -0
  4. lib_layered_config/_layers.py +310 -0
  5. lib_layered_config/_platform.py +166 -0
  6. lib_layered_config/adapters/__init__.py +13 -0
  7. lib_layered_config/adapters/_nested_keys.py +126 -0
  8. lib_layered_config/adapters/dotenv/__init__.py +1 -0
  9. lib_layered_config/adapters/dotenv/default.py +143 -0
  10. lib_layered_config/adapters/env/__init__.py +5 -0
  11. lib_layered_config/adapters/env/default.py +288 -0
  12. lib_layered_config/adapters/file_loaders/__init__.py +1 -0
  13. lib_layered_config/adapters/file_loaders/structured.py +376 -0
  14. lib_layered_config/adapters/path_resolvers/__init__.py +28 -0
  15. lib_layered_config/adapters/path_resolvers/_base.py +166 -0
  16. lib_layered_config/adapters/path_resolvers/_dotenv.py +74 -0
  17. lib_layered_config/adapters/path_resolvers/_linux.py +89 -0
  18. lib_layered_config/adapters/path_resolvers/_macos.py +93 -0
  19. lib_layered_config/adapters/path_resolvers/_windows.py +126 -0
  20. lib_layered_config/adapters/path_resolvers/default.py +194 -0
  21. lib_layered_config/application/__init__.py +12 -0
  22. lib_layered_config/application/merge.py +379 -0
  23. lib_layered_config/application/ports.py +115 -0
  24. lib_layered_config/cli/__init__.py +92 -0
  25. lib_layered_config/cli/common.py +381 -0
  26. lib_layered_config/cli/constants.py +12 -0
  27. lib_layered_config/cli/deploy.py +71 -0
  28. lib_layered_config/cli/fail.py +19 -0
  29. lib_layered_config/cli/generate.py +57 -0
  30. lib_layered_config/cli/info.py +29 -0
  31. lib_layered_config/cli/read.py +120 -0
  32. lib_layered_config/core.py +301 -0
  33. lib_layered_config/domain/__init__.py +7 -0
  34. lib_layered_config/domain/config.py +372 -0
  35. lib_layered_config/domain/errors.py +59 -0
  36. lib_layered_config/domain/identifiers.py +366 -0
  37. lib_layered_config/examples/__init__.py +29 -0
  38. lib_layered_config/examples/deploy.py +333 -0
  39. lib_layered_config/examples/generate.py +406 -0
  40. lib_layered_config/observability.py +209 -0
  41. lib_layered_config/py.typed +0 -0
  42. lib_layered_config/testing.py +46 -0
  43. lib_layered_config-4.1.0.dist-info/METADATA +3263 -0
  44. lib_layered_config-4.1.0.dist-info/RECORD +47 -0
  45. lib_layered_config-4.1.0.dist-info/WHEEL +4 -0
  46. lib_layered_config-4.1.0.dist-info/entry_points.txt +3 -0
  47. lib_layered_config-4.1.0.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,406 @@
1
+ """Example configuration asset generation helpers.
2
+
3
+ Produce reproducible configuration scaffolding referenced in documentation and
4
+ onboarding materials. This module belongs to the outer ring of the architecture
5
+ and has no runtime coupling to the composition root.
6
+
7
+ Contents:
8
+ - ``DEFAULT_HOST_PLACEHOLDER``: filename stub for host examples.
9
+ - ``ExampleSpec``: dataclass capturing a relative path and text content.
10
+ - ``generate_examples``: public orchestration expressed through helper
11
+ verbs.
12
+ - ``_build_specs``: yields platform-aware specifications.
13
+ - ``_write_spec`` / ``_should_write`` / ``_ensure_parent``: tiny filesystem
14
+ helpers that narrate how files are written.
15
+
16
+ System Role:
17
+ Called by docs/scripts to create filesystem layouts demonstrating how layered
18
+ configuration works.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import os
24
+ from collections.abc import Iterator
25
+ from dataclasses import dataclass
26
+ from pathlib import Path
27
+
28
+ from .._platform import normalise_examples_platform
29
+
30
+ DEFAULT_HOST_PLACEHOLDER = "your-hostname"
31
+ """Filename stub used for host-specific example files (documented in README)."""
32
+
33
+
34
+ @dataclass(slots=True)
35
+ class ExampleSpec:
36
+ """Describe a single example file to be written to disk.
37
+
38
+ Encapsulate metadata for templated files so generation logic stays simple
39
+ and testable.
40
+
41
+ Attributes:
42
+ relative_path: Path relative to the destination directory where the example will be
43
+ created.
44
+ content: File contents (UTF-8 text) including explanatory comments.
45
+ """
46
+
47
+ relative_path: Path
48
+ content: str
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class ExamplePlan:
53
+ """Plan describing how example files should be generated.
54
+
55
+ Attributes:
56
+ destination: Target directory where files will be written.
57
+ slug: Configuration slug used in templates.
58
+ vendor: Vendor name interpolated into paths/content.
59
+ app: Application name interpolated into paths/content.
60
+ force: Whether generation should overwrite existing files.
61
+ platform: Normalised platform key (``"posix"`` or ``"windows"``).
62
+ """
63
+
64
+ destination: Path
65
+ slug: str
66
+ vendor: str
67
+ app: str
68
+ force: bool
69
+ platform: str
70
+
71
+
72
+ def generate_examples(
73
+ destination: str | Path,
74
+ *,
75
+ slug: str,
76
+ vendor: str,
77
+ app: str,
78
+ force: bool = False,
79
+ platform: str | None = None,
80
+ ) -> list[Path]:
81
+ """Write canonical example files for each configuration layer.
82
+
83
+ Creates directories and files under *destination* mirroring the recommended
84
+ filesystem layout. Returns paths of files written. Use *force=True* to
85
+ overwrite existing files; *platform* overrides OS detection ("posix"/"windows").
86
+ """
87
+ plan = _build_example_plan(
88
+ destination=destination,
89
+ slug=slug,
90
+ vendor=vendor,
91
+ app=app,
92
+ force=force,
93
+ platform=platform,
94
+ )
95
+ specs = _build_specs(plan.destination, slug=plan.slug, vendor=plan.vendor, app=plan.app, platform=plan.platform)
96
+ return _write_examples(plan.destination, specs, plan.force)
97
+
98
+
99
+ def _build_example_plan(
100
+ *,
101
+ destination: str | Path,
102
+ slug: str,
103
+ vendor: str,
104
+ app: str,
105
+ force: bool,
106
+ platform: str | None,
107
+ ) -> ExamplePlan:
108
+ """Compose an example generation plan.
109
+
110
+ Args:
111
+ destination: Root destination directory (string or :class:`Path`).
112
+ slug: Identifiers embedded into generated content.
113
+ vendor: Identifiers embedded into generated content.
114
+ app: Identifiers embedded into generated content.
115
+ force: Whether existing files may be overwritten.
116
+ platform: Optional platform override supplied by the caller.
117
+
118
+ Returns:
119
+ ExamplePlan: Immutable plan consumed by downstream helpers.
120
+ """
121
+ dest = Path(destination)
122
+ return ExamplePlan(
123
+ destination=dest,
124
+ slug=slug,
125
+ vendor=vendor,
126
+ app=app,
127
+ force=force,
128
+ platform=_normalise_platform(platform),
129
+ )
130
+
131
+
132
+ def _write_examples(destination: Path, specs: Iterator[ExampleSpec], force: bool) -> list[Path]:
133
+ """Write all ``specs`` under *destination* honouring the *force* flag.
134
+
135
+ Centralise the loop that applies ``force`` semantics and records written paths.
136
+
137
+ Args:
138
+ destination: Root directory that will receive the examples.
139
+ specs: Iterator of example specifications to materialise.
140
+ force: When ``True`` existing files are overwritten.
141
+
142
+ Returns:
143
+ list[Path]: Paths written during this invocation.
144
+
145
+ Side Effects:
146
+ Creates directories and writes files to disk.
147
+ """
148
+ written: list[Path] = []
149
+ for spec in specs:
150
+ path = destination / spec.relative_path
151
+ if not _should_write(path, force):
152
+ continue
153
+ _ensure_parent(path)
154
+ _write_spec(path, spec)
155
+ written.append(path)
156
+ return written
157
+
158
+
159
+ def _write_spec(path: Path, spec: ExampleSpec) -> None:
160
+ """Persist ``spec`` content at *path* using UTF-8 encoding.
161
+
162
+ Keep the actual write primitive isolated for easy stubbing in tests.
163
+
164
+ Args:
165
+ path: Destination path for the example file.
166
+ spec: Example specification containing content to write.
167
+
168
+ Side Effects:
169
+ Writes UTF-8 text to *path*.
170
+ """
171
+ path.write_text(spec.content, encoding="utf-8")
172
+
173
+
174
+ def _should_write(path: Path, force: bool) -> bool:
175
+ """Return ``True`` when *path* should be written respecting *force*.
176
+
177
+ Avoid clobbering existing content unless the caller explicitly requests it.
178
+
179
+ Args:
180
+ path: Destination path under consideration.
181
+ force: Whether overwriting is allowed.
182
+
183
+ Returns:
184
+ bool: ``True`` when writing should proceed.
185
+
186
+ Examples:
187
+ >>> from tempfile import NamedTemporaryFile
188
+ >>> tmp = NamedTemporaryFile(delete=True)
189
+ >>> _should_write(Path(tmp.name), force=False)
190
+ False
191
+ >>> _should_write(Path(tmp.name), force=True)
192
+ True
193
+ >>> tmp.close()
194
+ """
195
+ return force or not path.exists()
196
+
197
+
198
+ def _ensure_parent(path: Path) -> None:
199
+ """Create parent directories for *path* when missing.
200
+
201
+ Ensure example generation works even on fresh directories.
202
+
203
+ Args:
204
+ path: Target file path whose parent directories should exist.
205
+
206
+ Side Effects:
207
+ Creates directories on disk.
208
+ """
209
+ path.parent.mkdir(parents=True, exist_ok=True)
210
+
211
+
212
+ def _build_specs(destination: Path, *, slug: str, vendor: str, app: str, platform: str) -> Iterator[ExampleSpec]:
213
+ """Yield :class:`ExampleSpec` instances for each canonical layer.
214
+
215
+ Keep file templates in one place so they stay aligned with documentation.
216
+
217
+ Args:
218
+ destination: Destination root (currently unused; reserved for future dynamic templates).
219
+ slug: Metadata interpolated into template content.
220
+ vendor: Metadata interpolated into template content.
221
+ app: Metadata interpolated into template content.
222
+ platform: Normalised platform key (``"posix"`` or ``"windows"``).
223
+
224
+ Yields:
225
+ ExampleSpec: Specification describing one file to render.
226
+
227
+ Examples:
228
+ >>> specs = list(_build_specs(Path('.'), slug='demo', vendor='Acme', app='ConfigKit', platform='posix'))
229
+ >>> specs[0].relative_path.as_posix()
230
+ 'xdg/demo/config.toml'
231
+ """
232
+ yield from _platform_specs(slug=slug, vendor=vendor, app=app, platform=platform)
233
+ yield _env_example(slug)
234
+
235
+
236
+ def _platform_specs(*, slug: str, vendor: str, app: str, platform: str) -> Iterator[ExampleSpec]:
237
+ """Dispatch to platform-specific example specifications.
238
+
239
+ Args:
240
+ slug: Metadata interpolated into generated content.
241
+ vendor: Metadata interpolated into generated content.
242
+ app: Metadata interpolated into generated content.
243
+ platform: Normalised platform key (``"posix"`` or ``"windows"``).
244
+
245
+ Yields:
246
+ ExampleSpec: Specifications describing files to create.
247
+ """
248
+ if platform == "windows":
249
+ yield from _windows_specs(slug=slug, vendor=vendor, app=app)
250
+ return
251
+ yield from _posix_specs(slug=slug, vendor=vendor, app=app)
252
+
253
+
254
+ def _windows_specs(*, slug: str, vendor: str, app: str) -> Iterator[ExampleSpec]:
255
+ """Yield Windows layout examples.
256
+
257
+ Args:
258
+ slug: Metadata interpolated into template content.
259
+ vendor: Metadata interpolated into template content.
260
+ app: Metadata interpolated into template content.
261
+
262
+ Yields:
263
+ ExampleSpec: Specification for each Windows example file.
264
+ """
265
+ root = Path("ProgramData") / vendor / app
266
+ yield ExampleSpec(root / "config.toml", _app_defaults_body(slug))
267
+ yield ExampleSpec(root / "hosts" / f"{DEFAULT_HOST_PLACEHOLDER}.toml", _host_override_body())
268
+ user_root = Path("AppData") / "Roaming" / vendor / app
269
+ yield ExampleSpec(user_root / "config.toml", _user_preferences_body(vendor, app))
270
+ yield ExampleSpec(user_root / "config.d" / "10-override.toml", _split_override_body())
271
+
272
+
273
+ def _posix_specs(*, slug: str, vendor: str, app: str) -> Iterator[ExampleSpec]:
274
+ """Yield POSIX layout examples following XDG Base Directory specification.
275
+
276
+ Args:
277
+ slug: Metadata interpolated into template content.
278
+ vendor: Metadata interpolated into template content.
279
+ app: Metadata interpolated into template content.
280
+
281
+ Yields:
282
+ ExampleSpec: Specification for each POSIX example file.
283
+ """
284
+ # System-wide app configuration (XDG-compliant)
285
+ app_root = Path("xdg") / slug
286
+ yield ExampleSpec(app_root / "config.toml", _app_defaults_body(slug))
287
+ yield ExampleSpec(app_root / "hosts" / f"{DEFAULT_HOST_PLACEHOLDER}.toml", _host_override_body())
288
+ # User-level configuration
289
+ user_root = Path("home") / slug
290
+ yield ExampleSpec(user_root / "config.toml", _user_preferences_body(vendor, app))
291
+ yield ExampleSpec(user_root / "config.d" / "10-override.toml", _split_override_body())
292
+
293
+
294
+ def _env_example(slug: str) -> ExampleSpec:
295
+ """Return the shared .env example specification.
296
+
297
+ Args:
298
+ slug: Configuration slug used when building environment variable names.
299
+
300
+ Returns:
301
+ ExampleSpec: Specification describing the `.env.example` file.
302
+ """
303
+ return ExampleSpec(Path(".env.example"), _env_secrets_body(slug))
304
+
305
+
306
+ def _app_defaults_body(slug: str) -> str:
307
+ """Describe baseline application defaults.
308
+
309
+ Args:
310
+ slug: Configuration slug inserted into the template heading.
311
+
312
+ Returns:
313
+ str: TOML content explaining application-wide defaults.
314
+ """
315
+ return f"""# Application-wide defaults for {slug}
316
+ [service]
317
+ endpoint = "https://api.example.com"
318
+ timeout = 10
319
+ """
320
+
321
+
322
+ def _host_override_body() -> str:
323
+ """Describe host-level overrides.
324
+
325
+ Returns:
326
+ str: TOML content illustrating host-specific timeout overrides.
327
+ """
328
+ return """# Host overrides (replace filename with the machine hostname)
329
+ [service]
330
+ timeout = 15
331
+ """
332
+
333
+
334
+ def _user_preferences_body(vendor: str, app: str) -> str:
335
+ """Describe user-level preferences.
336
+
337
+ Args:
338
+ vendor: Metadata interpolated into the template to keep prose friendly.
339
+ app: Metadata interpolated into the template to keep prose friendly.
340
+
341
+ Returns:
342
+ str: TOML content illustrating user-level overrides.
343
+ """
344
+ return f"""# User-specific preferences for {vendor} {app}
345
+ [service]
346
+ retry = 2
347
+ """
348
+
349
+
350
+ def _split_override_body() -> str:
351
+ """Describe config.d overrides used for granular layering.
352
+
353
+ Returns:
354
+ str: TOML content emphasising lexicographic ordering of split overrides.
355
+ """
356
+ return """# Split overrides live in config.d/ and apply in lexical order
357
+ [service]
358
+ retry = 3
359
+ """
360
+
361
+
362
+ def _env_secrets_body(slug: str) -> str:
363
+ """Describe .env secrets guidance.
364
+
365
+ Args:
366
+ slug: Configuration slug converted into an uppercase environment prefix.
367
+
368
+ Returns:
369
+ str: `.env` template content reminding users to provide secrets.
370
+ """
371
+ key = slug.replace("-", "_").upper()
372
+ return f"""# Copy to .env to provide secrets and local overrides
373
+ {key}___SERVICE__PASSWORD=changeme
374
+ """
375
+
376
+
377
+ def _default_platform() -> str:
378
+ """Return the default platform based on OS."""
379
+ return "windows" if os.name == "nt" else "posix"
380
+
381
+
382
+ def _normalise_platform(value: str | None) -> str:
383
+ """Return a canonical platform key for example generation.
384
+
385
+ Args:
386
+ value: Optional platform alias supplied by the caller.
387
+
388
+ Returns:
389
+ str: Normalised platform key (``"posix"`` or ``"windows"``).
390
+
391
+ Raises:
392
+ ValueError: When *value* is invalid.
393
+
394
+ Examples:
395
+ >>> _normalise_platform('posix')
396
+ 'posix'
397
+ >>> _normalise_platform(None) in {'posix', 'windows'}
398
+ True
399
+ """
400
+ if value is None:
401
+ return _default_platform()
402
+ try:
403
+ resolved = normalise_examples_platform(value)
404
+ except ValueError as exc: # pragma: no cover - validated via CLI helpers
405
+ raise ValueError(str(exc)) from exc
406
+ return resolved or _default_platform()
@@ -0,0 +1,209 @@
1
+ """Structured logging helpers distilled into tiny orchestration phrases.
2
+
3
+ Keep every emission of logging data predictable, contextual, and ready for
4
+ downstream aggregation pipelines without forcing applications to adopt a
5
+ specific logging backend.
6
+
7
+ Contents:
8
+ - ``TRACE_ID``: context variable storing the active trace identifier.
9
+ - ``get_logger``: returns the shared package logger (quiet by default).
10
+ - ``bind_trace_id``: binds or clears the active trace identifier.
11
+ - ``log_debug`` / ``log_info`` / ``log_warn`` / ``log_error``: emit structured
12
+ entries via a single private emitter.
13
+ - ``make_event``: convenience builder for structured event payloads.
14
+
15
+ System Integration:
16
+ Used by adapters and the composition root to ensure all diagnostics carry
17
+ the same trace metadata. Keeps the domain layer free from logging concerns
18
+ while still offering consumers consistent observability hooks.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import logging
24
+ from collections.abc import Mapping
25
+ from contextvars import ContextVar
26
+ from typing import Any, Final
27
+
28
+ TRACE_ID: ContextVar[str | None] = ContextVar("lib_layered_config_trace_id", default=None)
29
+ """Current trace identifier propagated through logging helpers.
30
+
31
+ Cross-cutting observability features (CLI, adapters) need a shared context without threading identifiers manually.
32
+
33
+ Context variable storing a string identifier or ``None``.
34
+ """
35
+
36
+ _LOGGER: Final[logging.Logger] = logging.getLogger("lib_layered_config")
37
+ _LOGGER.addHandler(logging.NullHandler())
38
+
39
+
40
+ def get_logger() -> logging.Logger:
41
+ """Expose the package logger so applications may attach handlers.
42
+
43
+ Leave the library silent by default while giving host applications full control over handler configuration.
44
+
45
+ Returns:
46
+ logging.Logger: Shared logger instance for ``lib_layered_config``.
47
+ """
48
+ return _LOGGER
49
+
50
+
51
+ def bind_trace_id(trace_id: str | None) -> None:
52
+ """Bind or clear the active trace identifier.
53
+
54
+ Correlate configuration events with external trace spans.
55
+
56
+ Args:
57
+ trace_id: Identifier string or ``None`` to drop the binding.
58
+
59
+ Side Effects:
60
+ Mutates :data:`TRACE_ID`, affecting subsequent logging helpers.
61
+
62
+ Examples:
63
+ >>> bind_trace_id('abc123')
64
+ >>> TRACE_ID.get()
65
+ 'abc123'
66
+ >>> bind_trace_id(None)
67
+ >>> TRACE_ID.get() is None
68
+ True
69
+ """
70
+ TRACE_ID.set(trace_id)
71
+
72
+
73
+ def log_debug(message: str, **fields: Any) -> None:
74
+ """Emit a structured debug log entry that includes the trace context.
75
+
76
+ Provide consistent debug telemetry across adapters while threading trace metadata.
77
+
78
+ Args:
79
+ message: Event name rendered by the logger.
80
+ **fields: Structured context merged into the payload.
81
+
82
+ Side Effects:
83
+ Calls :func:`_emit`, which writes to the shared logger.
84
+ """
85
+ _emit(logging.DEBUG, message, fields)
86
+
87
+
88
+ def log_info(message: str, **fields: Any) -> None:
89
+ """Emit a structured info log entry that includes the trace context.
90
+
91
+ Capture high-level lifecycle events (layer loaded, merge complete) with trace IDs attached.
92
+
93
+ Args:
94
+ message: Event name rendered by the logger.
95
+ **fields: Structured context merged into the payload.
96
+
97
+ Side Effects:
98
+ Calls :func:`_emit` with ``logging.INFO``.
99
+ """
100
+ _emit(logging.INFO, message, fields)
101
+
102
+
103
+ def log_warn(message: str, **fields: Any) -> None:
104
+ """Emit a structured warning log entry that includes the trace context.
105
+
106
+ Surface potential configuration issues (e.g., type conflicts) that don't prevent
107
+ loading but may indicate user error.
108
+ """
109
+ _emit(logging.WARNING, message, fields)
110
+
111
+
112
+ def log_error(message: str, **fields: Any) -> None:
113
+ """Emit a structured error log entry that includes the trace context.
114
+
115
+ Surface recoverable adapter failures (e.g., invalid files) with enough context for diagnosis.
116
+ """
117
+ _emit(logging.ERROR, message, fields)
118
+
119
+
120
+ def make_event(
121
+ layer: str,
122
+ path: str | None,
123
+ payload: Mapping[str, Any] | None = None,
124
+ ) -> dict[str, Any]:
125
+ """Build a structured logging payload for configuration lifecycle events.
126
+
127
+ Keep event construction consistent so downstream log processors can rely on stable keys.
128
+
129
+ Args:
130
+ layer: Name of the configuration layer being observed.
131
+ path: Filesystem path associated with the event, if available.
132
+ payload: Optional mapping with extra diagnostic detail.
133
+
134
+ Returns:
135
+ dict[str, Any]: Data safe to unpack into :func:`log_debug`, :func:`log_info`, :func:`log_warn`, or :func:`log_error`.
136
+
137
+ Examples:
138
+ >>> make_event('env', None, {'keys': 3})
139
+ {'layer': 'env', 'path': None, 'keys': 3}
140
+ """
141
+ event = _base_event(layer, path)
142
+ return _merge_payload(event, payload)
143
+
144
+
145
+ def _emit(level: int, message: str, fields: Mapping[str, Any]) -> None:
146
+ """Send a log entry through the shared logger with contextual metadata.
147
+
148
+ Centralise the call to ``logging.Logger.log`` so trace injection and field handling stay consistent.
149
+
150
+ Args:
151
+ level: Standard library logging level.
152
+ message: Event name rendered by the logger.
153
+ fields: Structured payload to merge with the trace identifier.
154
+
155
+ Side Effects:
156
+ Writes to the shared package logger.
157
+ """
158
+ _LOGGER.log(level, message, extra={"context": _with_trace(fields)})
159
+
160
+
161
+ def _with_trace(fields: Mapping[str, Any]) -> dict[str, Any]:
162
+ """Attach the current trace identifier to the provided structured fields.
163
+
164
+ Guarantee that every log entry includes the active trace (when present).
165
+
166
+ Args:
167
+ fields: Mapping of additional structured context.
168
+
169
+ Returns:
170
+ dict[str, Any]: Copy of ``fields`` with ``trace_id`` added.
171
+ """
172
+ context = {"trace_id": TRACE_ID.get()}
173
+ context.update(fields)
174
+ return context
175
+
176
+
177
+ def _base_event(layer: str, path: str | None) -> dict[str, Any]:
178
+ """Create the minimal event payload containing layer and path information.
179
+
180
+ Provide a consistent foundation for layer-related logging events.
181
+
182
+ Args:
183
+ layer: Layer name to annotate the event.
184
+ path: Optional filesystem path associated with the event.
185
+
186
+ Returns:
187
+ dict[str, Any]: Base payload ready for augmentation.
188
+ """
189
+ return {"layer": layer, "path": path}
190
+
191
+
192
+ def _merge_payload(event: dict[str, Any], payload: Mapping[str, Any] | None) -> dict[str, Any]:
193
+ """Merge optional diagnostic data into the event payload when provided.
194
+
195
+ Allow callers to enrich events without mutating the original dictionary outside this helper.
196
+
197
+ Args:
198
+ event: Base event payload.
199
+ payload: Optional mapping of diagnostic data.
200
+
201
+ Returns:
202
+ dict[str, Any]: Updated payload containing merged data.
203
+
204
+ Side Effects:
205
+ Mutates ``event`` when ``payload`` is provided.
206
+ """
207
+ if payload:
208
+ event |= dict(payload)
209
+ return event
File without changes
@@ -0,0 +1,46 @@
1
+ """Testing diagnostics that keep failure scenarios observable and predictable.
2
+
3
+ Provide intentionally failing helpers that exercise error-handling paths in
4
+ the CLI and integration suites without relying on brittle fixtures.
5
+
6
+ Contents:
7
+ - ``FAILURE_MESSAGE``: stable message used when forcing a failure.
8
+ - ``i_should_fail``: raises ``RuntimeError`` so callers can assert on the
9
+ propagated error details.
10
+
11
+ System Integration:
12
+ Resides in the testing support layer referenced by CLI end-to-end tests and
13
+ notebooks that demonstrate error propagation semantics.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Final
19
+
20
+ FAILURE_MESSAGE: Final[str] = "i should fail"
21
+ """Stable message emitted when :func:`i_should_fail` triggers a failure.
22
+
23
+ Integration tests and tutorial notebooks assert on the wording to guarantee
24
+ deterministic output during regression checks.
25
+
26
+ What:
27
+ A short, lower-case sentence that stays compatible with published examples.
28
+ """
29
+
30
+
31
+ def i_should_fail() -> None:
32
+ """Raise a deterministic :class:`RuntimeError` for failure-path testing.
33
+
34
+ Validates that higher-level orchestrators preserve stack traces and
35
+ messages when surfacing errors to end users.
36
+
37
+ Raises:
38
+ RuntimeError: Always raises with :data:`FAILURE_MESSAGE`.
39
+
40
+ Examples:
41
+ >>> i_should_fail()
42
+ Traceback (most recent call last):
43
+ ...
44
+ RuntimeError: i should fail
45
+ """
46
+ raise RuntimeError(FAILURE_MESSAGE)