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,200 @@
1
+ """Shared helpers for normalising user-provided platform aliases.
2
+
3
+ Purpose
4
+ -------
5
+ Bridge CLI/example inputs with resolver internals by translating human-friendly
6
+ platform strings into the canonical identifiers expected across adapters and
7
+ documentation.
8
+
9
+ Contents
10
+ --------
11
+ - ``normalise_resolver_platform``: map CLI adapter aliases to ``sys.platform``
12
+ style identifiers.
13
+ - ``normalise_examples_platform``: map example-generation aliases to the two
14
+ supported documentation families.
15
+ - ``_sanitize`` plus canonical mapping constants that keep user inputs tidy and
16
+ predictable.
17
+
18
+ System Role
19
+ -----------
20
+ Reusable utilities consumed by CLI commands and example tooling to ensure
21
+ terminology matches ``docs/systemdesign/concept.md`` regardless of user input
22
+ quirks.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import Final
28
+
29
+ #: Canonical resolver identifiers used when wiring the path resolver adapter.
30
+ #: Values mirror ``sys.platform`` strings so downstream code can branch safely.
31
+ _CANONICAL_RESOLVER: Final[dict[str, str]] = {
32
+ "linux": "linux",
33
+ "posix": "linux",
34
+ "darwin": "darwin",
35
+ "mac": "darwin",
36
+ "macos": "darwin",
37
+ "windows": "win32",
38
+ "win": "win32",
39
+ "win32": "win32",
40
+ "wine": "win32",
41
+ }
42
+
43
+ #: Canonical families used by documentation/example helpers. They collapse the
44
+ #: wide variety of aliases into the two supported directory layouts.
45
+ _CANONICAL_EXAMPLES: Final[dict[str, str]] = {
46
+ "posix": "posix",
47
+ "linux": "posix",
48
+ "darwin": "posix",
49
+ "mac": "posix",
50
+ "macos": "posix",
51
+ "windows": "windows",
52
+ "win": "windows",
53
+ "win32": "windows",
54
+ "wine": "windows",
55
+ }
56
+
57
+
58
+ def _sanitize(alias: str | None) -> str | None:
59
+ """Return a lower-cased alias stripped of whitespace when *alias* is truthy.
60
+
61
+ Why
62
+ ----
63
+ User input may include spacing or mixed casing; sanitising up front keeps the
64
+ canonical lookup tables compact and dependable.
65
+
66
+ Parameters
67
+ ----------
68
+ alias:
69
+ Optional raw alias provided by a user or CLI flag. ``None`` indicates no
70
+ override.
71
+
72
+ Returns
73
+ -------
74
+ str | None
75
+ Lower-case alias when *alias* contains characters, otherwise ``None`` when
76
+ no override is requested.
77
+
78
+ Raises
79
+ ------
80
+ ValueError
81
+ If *alias* contains only whitespace, because such inputs indicate a user
82
+ error that should surface immediately.
83
+
84
+ Examples
85
+ --------
86
+ >>> _sanitize(' MacOS ')
87
+ 'macos'
88
+ >>> _sanitize(None) is None
89
+ True
90
+ >>> _sanitize(' ')
91
+ Traceback (most recent call last):
92
+ ...
93
+ ValueError: Platform alias cannot be empty.
94
+ """
95
+
96
+ if alias is None:
97
+ return None
98
+ stripped = alias.strip().lower()
99
+ if not stripped:
100
+ raise ValueError("Platform alias cannot be empty.")
101
+ return stripped
102
+
103
+
104
+ def normalise_resolver_platform(alias: str | None) -> str | None:
105
+ """Return canonical resolver platform identifiers for *alias*.
106
+
107
+ Why
108
+ ----
109
+ The path resolver adapter expects ``sys.platform`` style identifiers. This
110
+ helper converts human-friendly values (``"mac"``, ``"win"``) into the canonical
111
+ tokens documented in the system design.
112
+
113
+ Parameters
114
+ ----------
115
+ alias:
116
+ User-provided alias or ``None``. ``None`` preserves auto-detection.
117
+
118
+ Returns
119
+ -------
120
+ str | None
121
+ Canonical resolver identifier or ``None`` when auto-detection should be
122
+ used.
123
+
124
+ Raises
125
+ ------
126
+ ValueError
127
+ If *alias* is not recognised. The error message enumerates valid options
128
+ so CLI tooling can surface helpful guidance.
129
+
130
+ Examples
131
+ --------
132
+ >>> normalise_resolver_platform('mac')
133
+ 'darwin'
134
+ >>> normalise_resolver_platform('win32')
135
+ 'win32'
136
+ >>> normalise_resolver_platform(None) is None
137
+ True
138
+ >>> normalise_resolver_platform('beos')
139
+ Traceback (most recent call last):
140
+ ...
141
+ ValueError: Platform must be one of: darwin, linux, mac, macos, posix, win, win32, windows, wine.
142
+ """
143
+
144
+ sanitized = _sanitize(alias)
145
+ if sanitized is None:
146
+ return None
147
+ try:
148
+ return _CANONICAL_RESOLVER[sanitized]
149
+ except KeyError as exc: # pragma: no cover - exercised via caller tests
150
+ allowed = ", ".join(sorted(_CANONICAL_RESOLVER))
151
+ raise ValueError(f"Platform must be one of: {allowed}.") from exc
152
+
153
+
154
+ def normalise_examples_platform(alias: str | None) -> str | None:
155
+ """Return the example-generation platform family for *alias*.
156
+
157
+ Why
158
+ ----
159
+ Documentation and example helpers target two directory layouts (POSIX and
160
+ Windows). This function collapses a wide variety of synonyms into those
161
+ families for predictable template generation.
162
+
163
+ Parameters
164
+ ----------
165
+ alias:
166
+ User-provided alias or ``None`` to let the caller choose a default.
167
+
168
+ Returns
169
+ -------
170
+ str | None
171
+ Canonical example platform (``"posix"`` or ``"windows"``) or ``None`` when
172
+ the caller should rely on runtime defaults.
173
+
174
+ Raises
175
+ ------
176
+ ValueError
177
+ If *alias* is provided but not known.
178
+
179
+ Examples
180
+ --------
181
+ >>> normalise_examples_platform('darwin')
182
+ 'posix'
183
+ >>> normalise_examples_platform('windows')
184
+ 'windows'
185
+ >>> normalise_examples_platform(None) is None
186
+ True
187
+ >>> normalise_examples_platform('amiga')
188
+ Traceback (most recent call last):
189
+ ...
190
+ ValueError: Platform must be one of: darwin, linux, mac, macos, posix, win, win32, windows, wine.
191
+ """
192
+
193
+ sanitized = _sanitize(alias)
194
+ if sanitized is None:
195
+ return None
196
+ try:
197
+ return _CANONICAL_EXAMPLES[sanitized]
198
+ except KeyError as exc: # pragma: no cover - exercised via caller tests
199
+ allowed = ", ".join(sorted(_CANONICAL_EXAMPLES))
200
+ raise ValueError(f"Platform must be one of: {allowed}.") from exc
@@ -0,0 +1,13 @@
1
+ """Adapter implementations for ``lib_layered_config``.
2
+
3
+ Purpose
4
+ -------
5
+ Group concrete boundary code (filesystem, dotenv, environment, file parsers)
6
+ that fulfils the application layer's ports.
7
+
8
+ System Role
9
+ -----------
10
+ Modules inside this package implement contracts defined in
11
+ :mod:`lib_layered_config.application.ports` and are wired together by the
12
+ composition root.
13
+ """
@@ -0,0 +1 @@
1
+ """Dotenv adapter implementations."""
@@ -0,0 +1,438 @@
1
+ """`.env` adapter.
2
+
3
+ Purpose
4
+ -------
5
+ Implement the :class:`lib_layered_config.application.ports.DotEnvLoader`
6
+ protocol by scanning for `.env` files using the search discipline captured in
7
+ ``docs/systemdesign/module_reference.md``.
8
+
9
+ Contents
10
+ - ``DefaultDotEnvLoader``: public loader that composes the helpers.
11
+ - ``_iter_candidates`` / ``_build_search_list``: gather candidate paths.
12
+ - ``_parse_dotenv``: strict parser converting dotenv files into nested dicts.
13
+ - ``_assign_nested`` and friends: ensure ``__`` nesting mirrors environment
14
+ variable semantics.
15
+ - ``_log_dotenv_*``: appetite of logging helpers that narrate discovery and
16
+ parsing outcomes.
17
+
18
+ System Role
19
+ -----------
20
+ Feeds `.env` key/value pairs into the merge pipeline using the same nesting
21
+ semantics as the environment adapter.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from collections.abc import Mapping
27
+ from pathlib import Path
28
+ from typing import Iterable, cast
29
+
30
+ from ...domain.errors import InvalidFormat
31
+ from ...observability import log_debug, log_error
32
+
33
+ DOTENV_LAYER = "dotenv"
34
+ """Layer name used for structured logging calls.
35
+
36
+ Why
37
+ ----
38
+ Tag observability events with a stable layer identifier.
39
+
40
+ What
41
+ ----
42
+ Constant shared across logging helpers within this module.
43
+ """
44
+
45
+
46
+ def _log_dotenv_loaded(path: Path, keys: Mapping[str, object]) -> None:
47
+ """Record a successful dotenv load with sorted key names.
48
+
49
+ Why
50
+ ----
51
+ Provide visibility into which dotenv file was applied and which keys were
52
+ present without dumping values.
53
+
54
+ Parameters
55
+ ----------
56
+ path:
57
+ Path to the loaded dotenv file.
58
+ keys:
59
+ Mapping of parsed keys (values are ignored; only key names are logged).
60
+ """
61
+
62
+ log_debug("dotenv_loaded", layer=DOTENV_LAYER, path=str(path), keys=sorted(keys.keys()))
63
+
64
+
65
+ def _log_dotenv_missing() -> None:
66
+ """Record that no dotenv file was discovered.
67
+
68
+ Why
69
+ ----
70
+ Signal to operators that the dotenv layer was absent (useful for debugging
71
+ precedence expectations).
72
+ """
73
+
74
+ log_debug("dotenv_not_found", layer=DOTENV_LAYER, path=None)
75
+
76
+
77
+ def _log_dotenv_error(path: Path, line_number: int) -> None:
78
+ """Capture malformed line diagnostics.
79
+
80
+ Why
81
+ ----
82
+ Provide actionable telemetry when dotenv parsing fails on a particular line.
83
+
84
+ Parameters
85
+ ----------
86
+ path:
87
+ Path to the dotenv file being parsed.
88
+ line_number:
89
+ Line number containing the malformed entry.
90
+ """
91
+
92
+ log_error("dotenv_invalid_line", layer=DOTENV_LAYER, path=str(path), line=line_number)
93
+
94
+
95
+ class DefaultDotEnvLoader:
96
+ """Load a dotenv file into a nested configuration dictionary.
97
+
98
+ Why
99
+ ----
100
+ `.env` files supply secrets and developer overrides. They need deterministic
101
+ discovery and identical nesting semantics to environment variables.
102
+
103
+ What
104
+ ----
105
+ Searches for candidate files, parses the first hit, records provenance, and
106
+ exposes the loaded path for diagnostics.
107
+ """
108
+
109
+ def __init__(self, *, extras: Iterable[str] | None = None) -> None:
110
+ """Initialise the loader with optional *extras* supplied by the path resolver.
111
+
112
+ Why
113
+ ----
114
+ Allow callers to append OS-specific directories to the search order.
115
+
116
+ Parameters
117
+ ----------
118
+ extras:
119
+ Additional absolute paths (typically OS-specific config directories)
120
+ appended to the search order.
121
+ """
122
+
123
+ self._extras = [Path(p) for p in extras or []]
124
+ self.last_loaded_path: str | None = None
125
+
126
+ def load(self, start_dir: str | None = None) -> Mapping[str, object]:
127
+ """Return the first parsed dotenv file discovered in the search order.
128
+
129
+ Why
130
+ ----
131
+ Provide the precedence layer ``dotenv`` used by the composition root.
132
+
133
+ What
134
+ ----
135
+ Builds the search list, parses the first existing file into a nested
136
+ mapping, stores the loaded path, and logs success or absence.
137
+
138
+ Parameters
139
+ ----------
140
+ start_dir:
141
+ Directory that seeds the upward search (often the project root).
142
+
143
+ Returns
144
+ -------
145
+ Mapping[str, object]
146
+ Nested mapping representing parsed key/value pairs.
147
+
148
+ Side Effects
149
+ ------------
150
+ Sets :attr:`last_loaded_path` and emits structured logging events.
151
+
152
+ Examples
153
+ --------
154
+ >>> from tempfile import TemporaryDirectory
155
+ >>> tmp = TemporaryDirectory()
156
+ >>> path = Path(tmp.name) / '.env'
157
+ >>> _ = path.write_text(
158
+ ... 'SERVICE__TOKEN=secret',
159
+ ... encoding='utf-8',
160
+ ... )
161
+ >>> loader = DefaultDotEnvLoader()
162
+ >>> loader.load(tmp.name)["service"]["token"]
163
+ 'secret'
164
+ >>> loader.last_loaded_path == str(path)
165
+ True
166
+ >>> tmp.cleanup()
167
+ """
168
+
169
+ candidates = _build_search_list(start_dir, self._extras)
170
+ self.last_loaded_path = None
171
+ for candidate in candidates:
172
+ if not candidate.is_file():
173
+ continue
174
+ self.last_loaded_path = str(candidate)
175
+ data = _parse_dotenv(candidate)
176
+ _log_dotenv_loaded(candidate, data)
177
+ return data
178
+ _log_dotenv_missing()
179
+ return {}
180
+
181
+
182
+ def _build_search_list(start_dir: str | None, extras: Iterable[Path]) -> list[Path]:
183
+ """Return ordered candidate paths including *extras* supplied by adapters.
184
+
185
+ Why
186
+ ----
187
+ Combine project-relative candidates with platform-specific extras while
188
+ preserving precedence order.
189
+
190
+ Parameters
191
+ ----------
192
+ start_dir:
193
+ Directory that seeds the upward search.
194
+ extras:
195
+ Additional absolute paths appended after the upward search.
196
+
197
+ Returns
198
+ -------
199
+ list[Path]
200
+ Ordered candidate paths for dotenv discovery.
201
+ """
202
+
203
+ return [*list(_iter_candidates(start_dir)), *extras]
204
+
205
+
206
+ def _iter_candidates(start_dir: str | None) -> Iterable[Path]:
207
+ """Yield candidate dotenv paths walking from ``start_dir`` to filesystem root.
208
+
209
+ Why
210
+ ----
211
+ Support layered overrides by checking the working directory and all parent
212
+ directories.
213
+
214
+ Parameters
215
+ ----------
216
+ start_dir:
217
+ Starting directory for the upward search; ``None`` uses the current
218
+ working directory.
219
+
220
+ Returns
221
+ -------
222
+ Iterable[Path]
223
+ Sequence of candidate `.env` paths ordered from closest to farthest.
224
+
225
+ Examples
226
+ --------
227
+ >>> from pathlib import Path
228
+ >>> base = Path('.')
229
+ >>> next(_iter_candidates(str(base))).name
230
+ '.env'
231
+ """
232
+
233
+ base = Path(start_dir) if start_dir else Path.cwd()
234
+ for directory in [base, *base.parents]:
235
+ yield directory / ".env"
236
+
237
+
238
+ def _parse_dotenv(path: Path) -> Mapping[str, object]:
239
+ """Parse ``path`` into a nested dictionary, raising ``InvalidFormat`` on malformed lines.
240
+
241
+ Why
242
+ ----
243
+ Ensure dotenv parsing is strict and produces dictionaries compatible with
244
+ the merge algorithm.
245
+
246
+ Parameters
247
+ ----------
248
+ path:
249
+ Absolute path to the dotenv file to parse.
250
+
251
+ Returns
252
+ -------
253
+ Mapping[str, object]
254
+ Nested dictionary representing the parsed file.
255
+
256
+ Raises
257
+ ------
258
+ InvalidFormat
259
+ When a line lacks an ``=`` delimiter or contains invalid syntax.
260
+
261
+ Examples
262
+ --------
263
+ >>> import os
264
+ >>> tmp = Path('example.env')
265
+ >>> body = os.linesep.join(['FEATURE=true', 'SERVICE__TIMEOUT=10']) + os.linesep
266
+ >>> _ = tmp.write_text(body, encoding='utf-8')
267
+ >>> parsed = _parse_dotenv(tmp)
268
+ >>> parsed["service"]["timeout"]
269
+ '10'
270
+ >>> tmp.unlink()
271
+ """
272
+
273
+ result: dict[str, object] = {}
274
+ with path.open("r", encoding="utf-8") as handle:
275
+ for line_number, raw_line in enumerate(handle, start=1):
276
+ line = raw_line.strip()
277
+ if not line or line.startswith("#"):
278
+ continue
279
+ if "=" not in line:
280
+ _log_dotenv_error(path, line_number)
281
+ raise InvalidFormat(f"Malformed line {line_number} in {path}")
282
+ key, value = line.split("=", 1)
283
+ key = key.strip()
284
+ value = _strip_quotes(value.strip())
285
+ _assign_nested(result, key, value)
286
+ return result
287
+
288
+
289
+ def _strip_quotes(value: str) -> str:
290
+ """Trim surrounding quotes and inline comments from ``value``.
291
+
292
+ Why
293
+ ----
294
+ `.env` syntax allows quoted strings and trailing inline comments; stripping
295
+ them keeps behaviour aligned with community conventions.
296
+
297
+ Parameters
298
+ ----------
299
+ value:
300
+ Raw value token read from the dotenv file.
301
+
302
+ Returns
303
+ -------
304
+ str
305
+ Cleaned value with quotes and trailing comments removed.
306
+
307
+ Examples
308
+ --------
309
+ >>> _strip_quotes('"token"')
310
+ 'token'
311
+ >>> _strip_quotes("value # comment")
312
+ 'value'
313
+ """
314
+
315
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
316
+ return value[1:-1]
317
+ if value.startswith("#"):
318
+ return ""
319
+ if " #" in value:
320
+ return value.split(" #", 1)[0].strip()
321
+ return value
322
+
323
+
324
+ def _assign_nested(target: dict[str, object], key: str, value: object) -> None:
325
+ """Assign ``value`` in ``target`` using case-insensitive dotted syntax.
326
+
327
+ Why
328
+ ----
329
+ Ensure dotenv keys with ``__`` delimiters mirror environment variable
330
+ nesting rules.
331
+
332
+ What
333
+ ----
334
+ Splits the key on ``__``, ensures each intermediate mapping exists, resolves
335
+ case-insensitive keys, and assigns the final value.
336
+
337
+ Parameters
338
+ ----------
339
+ target:
340
+ Mapping being mutated.
341
+ key:
342
+ Dotenv key using ``__`` separators.
343
+ value:
344
+ Parsed string value to assign.
345
+
346
+ Returns
347
+ -------
348
+ None
349
+
350
+ Side Effects
351
+ ------------
352
+ Mutates ``target``.
353
+
354
+ Examples
355
+ --------
356
+ >>> data: dict[str, object] = {}
357
+ >>> _assign_nested(data, 'SERVICE__TOKEN', 'secret')
358
+ >>> data
359
+ {'service': {'token': 'secret'}}
360
+ """
361
+
362
+ parts = key.split("__")
363
+ cursor = target
364
+ for part in parts[:-1]:
365
+ cursor = _ensure_child_mapping(cursor, part, error_cls=InvalidFormat)
366
+ final_key = _resolve_key(cursor, parts[-1])
367
+ cursor[final_key] = value
368
+
369
+
370
+ def _resolve_key(mapping: dict[str, object], key: str) -> str:
371
+ """Return an existing key with matching case-insensitive name or create a new lowercase entry.
372
+
373
+ Why
374
+ ----
375
+ Preserve original casing when keys repeat while avoiding duplicates that
376
+ differ only by case.
377
+
378
+ Parameters
379
+ ----------
380
+ mapping:
381
+ Mutable mapping being inspected.
382
+ key:
383
+ Raw key from the dotenv file.
384
+
385
+ Returns
386
+ -------
387
+ str
388
+ Existing key or lowercase variant suitable for insertion.
389
+ """
390
+
391
+ lower = key.lower()
392
+ for existing in mapping.keys():
393
+ if existing.lower() == lower:
394
+ return existing
395
+ return lower
396
+
397
+
398
+ def _ensure_child_mapping(mapping: dict[str, object], key: str, *, error_cls: type[Exception]) -> dict[str, object]:
399
+ """Ensure ``mapping[key]`` is a ``dict`` or raise ``error_cls`` when a scalar blocks nesting.
400
+
401
+ Why
402
+ ----
403
+ Nested keys should never overwrite scalar values without an explicit error.
404
+ This keeps configuration shapes predictable.
405
+
406
+ What
407
+ ----
408
+ Resolves the key, creates an empty mapping when missing, or raises the
409
+ provided error when a scalar is encountered.
410
+
411
+ Parameters
412
+ ----------
413
+ mapping:
414
+ Mapping being mutated.
415
+ key:
416
+ Key segment to ensure.
417
+ error_cls:
418
+ Exception type raised on scalar collisions.
419
+
420
+ Returns
421
+ -------
422
+ dict[str, object]
423
+ Child mapping stored at the resolved key.
424
+
425
+ Side Effects
426
+ ------------
427
+ Mutates ``mapping`` by inserting a new child mapping when missing.
428
+ """
429
+
430
+ resolved = _resolve_key(mapping, key)
431
+ if resolved not in mapping:
432
+ mapping[resolved] = dict[str, object]()
433
+ child = mapping[resolved]
434
+ if not isinstance(child, dict):
435
+ raise error_cls(f"Cannot overwrite scalar with mapping for key {key}")
436
+ typed_child = cast(dict[str, object], child)
437
+ mapping[resolved] = typed_child
438
+ return typed_child
@@ -0,0 +1,5 @@
1
+ """Environment variable adapters for ``lib_layered_config``."""
2
+
3
+ from . import default
4
+
5
+ __all__ = ["default"]