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,117 @@
|
|
|
1
|
+
"""CLI commands related to reading configuration layers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, Sequence
|
|
7
|
+
|
|
8
|
+
import rich_click as click
|
|
9
|
+
|
|
10
|
+
from .common import (
|
|
11
|
+
build_read_query,
|
|
12
|
+
human_payload,
|
|
13
|
+
json_payload,
|
|
14
|
+
resolve_indent,
|
|
15
|
+
wants_json,
|
|
16
|
+
)
|
|
17
|
+
from .constants import CLICK_CONTEXT_SETTINGS
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@click.command("read", context_settings=CLICK_CONTEXT_SETTINGS)
|
|
21
|
+
@click.option("--vendor", required=True, help="Vendor namespace")
|
|
22
|
+
@click.option("--app", required=True, help="Application name")
|
|
23
|
+
@click.option("--slug", required=True, help="Slug identifying the configuration set")
|
|
24
|
+
@click.option("--prefer", multiple=True, help="Preferred file suffix ordering (repeatable)")
|
|
25
|
+
@click.option(
|
|
26
|
+
"--start-dir",
|
|
27
|
+
type=click.Path(path_type=Path, exists=True, file_okay=False, dir_okay=True, readable=True),
|
|
28
|
+
default=None,
|
|
29
|
+
help="Starting directory for .env upward search",
|
|
30
|
+
)
|
|
31
|
+
@click.option(
|
|
32
|
+
"--default-file",
|
|
33
|
+
type=click.Path(path_type=Path, exists=True, file_okay=True, dir_okay=False, readable=True),
|
|
34
|
+
default=None,
|
|
35
|
+
help="Optional lowest-precedence defaults file",
|
|
36
|
+
)
|
|
37
|
+
@click.option(
|
|
38
|
+
"--format",
|
|
39
|
+
"output_format",
|
|
40
|
+
type=click.Choice(["human", "json"], case_sensitive=False),
|
|
41
|
+
default="human",
|
|
42
|
+
show_default=True,
|
|
43
|
+
help="Choose between human prose or JSON",
|
|
44
|
+
)
|
|
45
|
+
@click.option(
|
|
46
|
+
"--indent/--no-indent",
|
|
47
|
+
default=True,
|
|
48
|
+
show_default=True,
|
|
49
|
+
help="Pretty-print JSON output",
|
|
50
|
+
)
|
|
51
|
+
@click.option(
|
|
52
|
+
"--provenance/--no-provenance",
|
|
53
|
+
default=True,
|
|
54
|
+
show_default=True,
|
|
55
|
+
help="Include provenance metadata in JSON output",
|
|
56
|
+
)
|
|
57
|
+
def read_command(
|
|
58
|
+
vendor: str,
|
|
59
|
+
app: str,
|
|
60
|
+
slug: str,
|
|
61
|
+
prefer: Sequence[str],
|
|
62
|
+
start_dir: Optional[Path],
|
|
63
|
+
default_file: Optional[Path],
|
|
64
|
+
output_format: str,
|
|
65
|
+
indent: bool,
|
|
66
|
+
provenance: bool,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Read configuration and print either human prose or JSON."""
|
|
69
|
+
|
|
70
|
+
query = build_read_query(vendor, app, slug, prefer, start_dir, default_file)
|
|
71
|
+
if wants_json(output_format):
|
|
72
|
+
click.echo(json_payload(query, resolve_indent(indent), provenance))
|
|
73
|
+
return
|
|
74
|
+
click.echo(human_payload(query))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@click.command("read-json", context_settings=CLICK_CONTEXT_SETTINGS)
|
|
78
|
+
@click.option("--vendor", required=True)
|
|
79
|
+
@click.option("--app", required=True)
|
|
80
|
+
@click.option("--slug", required=True)
|
|
81
|
+
@click.option("--prefer", multiple=True)
|
|
82
|
+
@click.option(
|
|
83
|
+
"--start-dir",
|
|
84
|
+
type=click.Path(path_type=Path, exists=True, file_okay=False, dir_okay=True, readable=True),
|
|
85
|
+
default=None,
|
|
86
|
+
)
|
|
87
|
+
@click.option(
|
|
88
|
+
"--default-file",
|
|
89
|
+
type=click.Path(path_type=Path, exists=True, file_okay=True, dir_okay=False, readable=True),
|
|
90
|
+
default=None,
|
|
91
|
+
)
|
|
92
|
+
@click.option(
|
|
93
|
+
"--indent/--no-indent",
|
|
94
|
+
default=True,
|
|
95
|
+
show_default=True,
|
|
96
|
+
help="Pretty-print JSON output",
|
|
97
|
+
)
|
|
98
|
+
def read_json_command(
|
|
99
|
+
vendor: str,
|
|
100
|
+
app: str,
|
|
101
|
+
slug: str,
|
|
102
|
+
prefer: Sequence[str],
|
|
103
|
+
start_dir: Optional[Path],
|
|
104
|
+
default_file: Optional[Path],
|
|
105
|
+
indent: bool,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Always emit combined JSON (config + provenance)."""
|
|
108
|
+
|
|
109
|
+
query = build_read_query(vendor, app, slug, prefer, start_dir, default_file)
|
|
110
|
+
click.echo(json_payload(query, resolve_indent(indent), include_provenance=True))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def register(cli_group: click.Group) -> None:
|
|
114
|
+
"""Register CLI commands defined in this module."""
|
|
115
|
+
|
|
116
|
+
cli_group.add_command(read_command)
|
|
117
|
+
cli_group.add_command(read_json_command)
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""Composition root tying adapters, merge policy, and domain objects together.
|
|
2
|
+
|
|
3
|
+
Purpose
|
|
4
|
+
-------
|
|
5
|
+
Implement the orchestration described in ``docs/systemdesign/concept.md`` by
|
|
6
|
+
discovering configuration layers, merging them with provenance, and returning a
|
|
7
|
+
domain-level :class:`Config` value object. Also provides convenience helpers for
|
|
8
|
+
JSON output and CLI wiring.
|
|
9
|
+
|
|
10
|
+
Contents
|
|
11
|
+
--------
|
|
12
|
+
- ``read_config`` / ``read_config_json`` / ``read_config_raw``: public APIs used
|
|
13
|
+
by library consumers and the CLI.
|
|
14
|
+
- ``LayerLoadError``: wraps adapter failures with a consistent exception type.
|
|
15
|
+
- Private helpers for resolver/builder construction, JSON dumping, and
|
|
16
|
+
configuration composition.
|
|
17
|
+
|
|
18
|
+
System Role
|
|
19
|
+
-----------
|
|
20
|
+
This module sits at the composition layer of the architecture. It instantiates
|
|
21
|
+
adapters from ``lib_layered_config.adapters.*``, invokes
|
|
22
|
+
``lib_layered_config._layers.collect_layers``, and converts merge results into
|
|
23
|
+
domain objects returned to callers.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Sequence, cast
|
|
31
|
+
|
|
32
|
+
from ._layers import collect_layers, merge_or_empty
|
|
33
|
+
from .adapters.dotenv.default import DefaultDotEnvLoader
|
|
34
|
+
from .adapters.env.default import DefaultEnvLoader, default_env_prefix
|
|
35
|
+
from .adapters.path_resolvers.default import DefaultPathResolver
|
|
36
|
+
from .application.merge import SourceInfoPayload
|
|
37
|
+
from .domain.config import Config, EMPTY_CONFIG, SourceInfo
|
|
38
|
+
from .domain.errors import ConfigError, InvalidFormat, NotFound, ValidationError
|
|
39
|
+
from .observability import bind_trace_id
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LayerLoadError(ConfigError):
|
|
43
|
+
"""Adapter failure raised during layer collection.
|
|
44
|
+
|
|
45
|
+
Why
|
|
46
|
+
----
|
|
47
|
+
Provides a single exception type for callers who need to distinguish merge
|
|
48
|
+
orchestration errors from other configuration issues.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def read_config(
|
|
53
|
+
*,
|
|
54
|
+
vendor: str,
|
|
55
|
+
app: str,
|
|
56
|
+
slug: str,
|
|
57
|
+
prefer: Sequence[str] | None = None,
|
|
58
|
+
start_dir: str | None = None,
|
|
59
|
+
default_file: str | Path | None = None,
|
|
60
|
+
) -> Config:
|
|
61
|
+
"""Return an immutable :class:`Config` built from all reachable layers.
|
|
62
|
+
|
|
63
|
+
Why
|
|
64
|
+
----
|
|
65
|
+
Most consumers want the merged configuration value object rather than raw
|
|
66
|
+
dictionaries. This function wraps the lower-level helper and constructs the
|
|
67
|
+
domain aggregate in one step.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
vendor / app / slug:
|
|
72
|
+
Identifiers used by adapters to compute filesystem paths and prefixes.
|
|
73
|
+
prefer:
|
|
74
|
+
Optional sequence of preferred file suffixes (``["toml", "json"]``).
|
|
75
|
+
start_dir:
|
|
76
|
+
Optional directory that seeds `.env` discovery.
|
|
77
|
+
default_file:
|
|
78
|
+
Optional lowest-precedence file injected before filesystem layers.
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
Config
|
|
83
|
+
Immutable configuration with provenance metadata.
|
|
84
|
+
|
|
85
|
+
Examples
|
|
86
|
+
--------
|
|
87
|
+
>>> from pathlib import Path
|
|
88
|
+
>>> tmp = Path('.') # doctest: +SKIP (illustrative)
|
|
89
|
+
>>> config = read_config(vendor="Acme", app="Demo", slug="demo", start_dir=str(tmp)) # doctest: +SKIP
|
|
90
|
+
>>> isinstance(config, Config)
|
|
91
|
+
True
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
data, raw_meta = read_config_raw(
|
|
95
|
+
vendor=vendor,
|
|
96
|
+
app=app,
|
|
97
|
+
slug=slug,
|
|
98
|
+
prefer=prefer,
|
|
99
|
+
start_dir=start_dir,
|
|
100
|
+
default_file=_stringify_path(default_file),
|
|
101
|
+
)
|
|
102
|
+
return _compose_config(data, raw_meta)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def read_config_json(
|
|
106
|
+
*,
|
|
107
|
+
vendor: str,
|
|
108
|
+
app: str,
|
|
109
|
+
slug: str,
|
|
110
|
+
prefer: Sequence[str] | None = None,
|
|
111
|
+
start_dir: str | Path | None = None,
|
|
112
|
+
indent: int | None = None,
|
|
113
|
+
default_file: str | Path | None = None,
|
|
114
|
+
) -> str:
|
|
115
|
+
"""Return configuration and provenance as JSON suitable for tooling.
|
|
116
|
+
|
|
117
|
+
Why
|
|
118
|
+
----
|
|
119
|
+
CLI commands and automation scripts often prefer JSON to Python objects.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
vendor / app / slug / prefer / start_dir / default_file:
|
|
124
|
+
Same meaning as :func:`read_config`.
|
|
125
|
+
indent:
|
|
126
|
+
Optional indentation level passed to ``json.dumps``.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
str
|
|
131
|
+
JSON document containing ``{"config": ..., "provenance": ...}``.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
data, meta = read_config_raw(
|
|
135
|
+
vendor=vendor,
|
|
136
|
+
app=app,
|
|
137
|
+
slug=slug,
|
|
138
|
+
prefer=prefer,
|
|
139
|
+
start_dir=_stringify_path(start_dir),
|
|
140
|
+
default_file=_stringify_path(default_file),
|
|
141
|
+
)
|
|
142
|
+
return _dump_json({"config": data, "provenance": meta}, indent)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def read_config_raw(
|
|
146
|
+
*,
|
|
147
|
+
vendor: str,
|
|
148
|
+
app: str,
|
|
149
|
+
slug: str,
|
|
150
|
+
prefer: Sequence[str] | None = None,
|
|
151
|
+
start_dir: str | None = None,
|
|
152
|
+
default_file: str | Path | None = None,
|
|
153
|
+
) -> tuple[dict[str, object], dict[str, SourceInfoPayload]]:
|
|
154
|
+
"""Return raw data and provenance mappings for advanced tooling.
|
|
155
|
+
|
|
156
|
+
Why
|
|
157
|
+
----
|
|
158
|
+
Some consumers need dictionaries they can mutate or serialise differently
|
|
159
|
+
without enforcing the :class:`Config` abstraction, while reusing the same
|
|
160
|
+
precedence pipeline and provenance metadata as the public API.
|
|
161
|
+
|
|
162
|
+
Parameters
|
|
163
|
+
----------
|
|
164
|
+
vendor / app / slug:
|
|
165
|
+
Identifiers passed to the path resolver to compute search roots and
|
|
166
|
+
prefixes.
|
|
167
|
+
prefer:
|
|
168
|
+
Optional ordered sequence of preferred file suffixes (lower precedence
|
|
169
|
+
when omitted).
|
|
170
|
+
start_dir:
|
|
171
|
+
Optional directory seeding the upward `.env` search. ``None`` keeps the
|
|
172
|
+
resolver default.
|
|
173
|
+
default_file:
|
|
174
|
+
Optional path injected as the lowest-precedence layer. Accepts either
|
|
175
|
+
:class:`pathlib.Path` or string values.
|
|
176
|
+
|
|
177
|
+
Returns
|
|
178
|
+
-------
|
|
179
|
+
tuple[dict[str, object], dict[str, SourceInfoPayload]]
|
|
180
|
+
Pair of mutable dictionaries mirroring the merge results prior to
|
|
181
|
+
construction of the domain value object.
|
|
182
|
+
|
|
183
|
+
Side Effects
|
|
184
|
+
------------
|
|
185
|
+
Resets the active trace identifier and emits structured logging events via
|
|
186
|
+
the layer collection helpers.
|
|
187
|
+
|
|
188
|
+
Raises
|
|
189
|
+
------
|
|
190
|
+
LayerLoadError
|
|
191
|
+
When a structured file loader raises :class:`InvalidFormat`.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
resolver = _build_resolver(vendor=vendor, app=app, slug=slug, start_dir=start_dir)
|
|
195
|
+
dotenv_loader, env_loader = _build_loaders(resolver)
|
|
196
|
+
|
|
197
|
+
bind_trace_id(None)
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
layers = collect_layers(
|
|
201
|
+
resolver=resolver,
|
|
202
|
+
prefer=prefer,
|
|
203
|
+
default_file=_stringify_path(default_file),
|
|
204
|
+
dotenv_loader=dotenv_loader,
|
|
205
|
+
env_loader=env_loader,
|
|
206
|
+
slug=slug,
|
|
207
|
+
start_dir=start_dir,
|
|
208
|
+
)
|
|
209
|
+
except InvalidFormat as exc: # pragma: no cover - adapter tests exercise
|
|
210
|
+
raise LayerLoadError(str(exc)) from exc
|
|
211
|
+
|
|
212
|
+
return merge_or_empty(layers)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _compose_config(
|
|
216
|
+
data: dict[str, object],
|
|
217
|
+
raw_meta: dict[str, SourceInfoPayload],
|
|
218
|
+
) -> Config:
|
|
219
|
+
"""Wrap merged data and provenance into an immutable :class:`Config`.
|
|
220
|
+
|
|
221
|
+
Why
|
|
222
|
+
----
|
|
223
|
+
Keep the boundary between application-layer dictionaries and the domain
|
|
224
|
+
value object explicit so provenance typing stays consistent.
|
|
225
|
+
|
|
226
|
+
Parameters
|
|
227
|
+
----------
|
|
228
|
+
data:
|
|
229
|
+
Mutable mapping returned by :func:`merge_layers`.
|
|
230
|
+
raw_meta:
|
|
231
|
+
Provenance mapping keyed by dotted path as produced by the merge policy.
|
|
232
|
+
|
|
233
|
+
Returns
|
|
234
|
+
-------
|
|
235
|
+
Config
|
|
236
|
+
Immutable configuration aggregate. Returns :data:`EMPTY_CONFIG` when
|
|
237
|
+
*data* is empty.
|
|
238
|
+
|
|
239
|
+
Side Effects
|
|
240
|
+
------------
|
|
241
|
+
None beyond constructing the dataclass instance.
|
|
242
|
+
|
|
243
|
+
Examples
|
|
244
|
+
--------
|
|
245
|
+
>>> cfg = _compose_config({'debug': True}, {'debug': {'layer': 'env', 'path': None, 'key': 'debug'}})
|
|
246
|
+
>>> cfg['debug'], cfg.origin('debug')['layer']
|
|
247
|
+
(True, 'env')
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
if not data:
|
|
251
|
+
return EMPTY_CONFIG
|
|
252
|
+
meta = {key: cast(SourceInfo, details) for key, details in raw_meta.items()}
|
|
253
|
+
return Config(data, meta)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _build_resolver(
|
|
257
|
+
*,
|
|
258
|
+
vendor: str,
|
|
259
|
+
app: str,
|
|
260
|
+
slug: str,
|
|
261
|
+
start_dir: str | None,
|
|
262
|
+
) -> DefaultPathResolver:
|
|
263
|
+
"""Create a path resolver configured with optional ``start_dir`` context.
|
|
264
|
+
|
|
265
|
+
Why
|
|
266
|
+
----
|
|
267
|
+
Reuse the same resolver wiring for CLI and library entry points while
|
|
268
|
+
keeping construction logic centralised for testing.
|
|
269
|
+
|
|
270
|
+
Parameters
|
|
271
|
+
----------
|
|
272
|
+
vendor / app / slug:
|
|
273
|
+
Identifiers forwarded to :class:`DefaultPathResolver`.
|
|
274
|
+
start_dir:
|
|
275
|
+
Optional directory that seeds project-relative resolution (used for
|
|
276
|
+
`.env` discovery); ``None`` preserves resolver defaults.
|
|
277
|
+
|
|
278
|
+
Returns
|
|
279
|
+
-------
|
|
280
|
+
DefaultPathResolver
|
|
281
|
+
Resolver instance ready for layer discovery.
|
|
282
|
+
|
|
283
|
+
Examples
|
|
284
|
+
--------
|
|
285
|
+
>>> resolver = _build_resolver(vendor='Acme', app='Demo', slug='demo', start_dir=None)
|
|
286
|
+
>>> resolver.slug
|
|
287
|
+
'demo'
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
return DefaultPathResolver(vendor=vendor, app=app, slug=slug, cwd=Path(start_dir) if start_dir else None)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _build_loaders(resolver: DefaultPathResolver) -> tuple[DefaultDotEnvLoader, DefaultEnvLoader]:
|
|
294
|
+
"""Instantiate dotenv and environment loaders sharing resolver context.
|
|
295
|
+
|
|
296
|
+
Why
|
|
297
|
+
----
|
|
298
|
+
Keeps loader construction aligned with the resolver extras (e.g., additional
|
|
299
|
+
dotenv directories) and centralises wiring for tests.
|
|
300
|
+
|
|
301
|
+
Parameters
|
|
302
|
+
----------
|
|
303
|
+
resolver:
|
|
304
|
+
Resolver supplying platform-specific extras for dotenv discovery.
|
|
305
|
+
|
|
306
|
+
Returns
|
|
307
|
+
-------
|
|
308
|
+
tuple[DefaultDotEnvLoader, DefaultEnvLoader]
|
|
309
|
+
Pair of loader instances ready for layer collection.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
return DefaultDotEnvLoader(extras=resolver.dotenv()), DefaultEnvLoader()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _stringify_path(value: str | Path | None) -> str | None:
|
|
316
|
+
"""Convert ``Path`` or string inputs into plain string values for adapters.
|
|
317
|
+
|
|
318
|
+
Why
|
|
319
|
+
----
|
|
320
|
+
Adapters expect plain strings while public APIs accept :class:`Path` objects
|
|
321
|
+
for user convenience. Centralising the conversion avoids duplicate logic.
|
|
322
|
+
|
|
323
|
+
Parameters
|
|
324
|
+
----------
|
|
325
|
+
value:
|
|
326
|
+
Optional path expressed as either a string or :class:`pathlib.Path`.
|
|
327
|
+
|
|
328
|
+
Returns
|
|
329
|
+
-------
|
|
330
|
+
str | None
|
|
331
|
+
Stringified path or ``None`` when *value* is ``None``.
|
|
332
|
+
|
|
333
|
+
Examples
|
|
334
|
+
--------
|
|
335
|
+
>>> _stringify_path(Path('/tmp/config.toml'))
|
|
336
|
+
'/tmp/config.toml'
|
|
337
|
+
>>> _stringify_path(None) is None
|
|
338
|
+
True
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
if isinstance(value, Path):
|
|
342
|
+
return str(value)
|
|
343
|
+
return value
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _dump_json(payload: object, indent: int | None) -> str:
|
|
347
|
+
"""Serialise *payload* to JSON while preserving non-ASCII characters.
|
|
348
|
+
|
|
349
|
+
Parameters
|
|
350
|
+
----------
|
|
351
|
+
payload:
|
|
352
|
+
JSON-serialisable object to dump.
|
|
353
|
+
indent:
|
|
354
|
+
Optional indentation level mirroring :func:`json.dumps`. ``None`` produces
|
|
355
|
+
the most compact output.
|
|
356
|
+
|
|
357
|
+
Returns
|
|
358
|
+
-------
|
|
359
|
+
str
|
|
360
|
+
JSON document encoded as UTF-8 friendly text.
|
|
361
|
+
|
|
362
|
+
Examples
|
|
363
|
+
--------
|
|
364
|
+
>>> _dump_json({"a": 1}, indent=None)
|
|
365
|
+
'{"a":1}'
|
|
366
|
+
>>> "\n" in _dump_json({"a": 1}, indent=2)
|
|
367
|
+
True
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
return json.dumps(payload, indent=indent, separators=(",", ":"), ensure_ascii=False)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
__all__ = [
|
|
374
|
+
"Config",
|
|
375
|
+
"ConfigError",
|
|
376
|
+
"InvalidFormat",
|
|
377
|
+
"ValidationError",
|
|
378
|
+
"NotFound",
|
|
379
|
+
"LayerLoadError",
|
|
380
|
+
"read_config",
|
|
381
|
+
"read_config_json",
|
|
382
|
+
"read_config_raw",
|
|
383
|
+
"default_env_prefix",
|
|
384
|
+
]
|