onefig 0.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.
onefig/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from onefig._diff import MISSING
4
+ from onefig._format import flatten, format_tree, unflatten
5
+ from onefig.model import ConfigModel
6
+
7
+ __all__ = ["ConfigModel", "MISSING", "flatten", "format_tree", "unflatten"]
8
+ __version__ = "0.1.0"
onefig/_cli.py ADDED
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+
7
+ def parse_overrides(tokens: list[str]) -> dict[str, Any]:
8
+ """Parse a list of ``key=value`` tokens into a flat override dict.
9
+
10
+ Values are coerced with best-effort JSON parsing (so ``5`` → int, ``5.0``
11
+ → float, ``true`` / ``false`` → bool, ``null`` → ``None``, ``[1,2]`` →
12
+ list) and fall back to the raw string. Pydantic re-validates at
13
+ assignment, so the coercion is a hint, not a contract.
14
+
15
+ Args:
16
+ tokens: List of CLI tokens, each of the form ``key=value``.
17
+
18
+ Returns:
19
+ Flat mapping of keys to coerced values.
20
+
21
+ Raises:
22
+ ValueError: If a token has no ``=``, or has an empty key.
23
+ """
24
+ out: dict[str, Any] = {}
25
+ for tok in tokens:
26
+ if "=" not in tok:
27
+ raise ValueError(f"Bad override token {tok!r}: expected key=value.")
28
+ key, _, raw = tok.partition("=")
29
+ if not key:
30
+ raise ValueError(f"Bad override token {tok!r}: empty key.")
31
+ out[key] = _coerce(raw)
32
+ return out
33
+
34
+
35
+ def _coerce(raw: str) -> Any:
36
+ if raw == "None":
37
+ return None
38
+ try:
39
+ return json.loads(raw)
40
+ except (ValueError, json.JSONDecodeError):
41
+ return raw
onefig/_cli_entry.py ADDED
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from onefig._completion import python_completion_script, shell_script
7
+
8
+
9
+ def build_parser() -> argparse.ArgumentParser:
10
+ parser = argparse.ArgumentParser(
11
+ prog="onefig",
12
+ description=(
13
+ "onefig command-line utilities. The library's primary API is the "
14
+ "Python ConfigModel; this CLI exposes a small number of install "
15
+ "helpers for shell tab completion."
16
+ ),
17
+ )
18
+ sub = parser.add_subparsers(dest="command", required=True, metavar="COMMAND")
19
+
20
+ p_install = sub.add_parser(
21
+ "install-completion",
22
+ help=(
23
+ "Print a shell-completion install snippet bound to a specific "
24
+ "script command name."
25
+ ),
26
+ description=(
27
+ "Print a shell-completion install snippet for a script. Source "
28
+ "the output (or append it to your shell rc file) to enable TAB "
29
+ "completion for the named command."
30
+ ),
31
+ )
32
+ p_install.add_argument(
33
+ "shell",
34
+ choices=("bash", "zsh", "fish"),
35
+ help="Target shell.",
36
+ )
37
+ p_install.add_argument(
38
+ "--prog",
39
+ required=True,
40
+ metavar="NAME",
41
+ help="Command name the user types to invoke the script.",
42
+ )
43
+
44
+ p_py = sub.add_parser(
45
+ "install-python-completion",
46
+ help=(
47
+ "Print a generic shell snippet that enables tab completion for "
48
+ "every onefig script invoked via `python <script>.py`."
49
+ ),
50
+ description=(
51
+ "Print a generic shell snippet that hooks tab completion onto "
52
+ "`python` and `python3`. After installing it once, every "
53
+ "onefig-based script invoked via `python script.py` gets "
54
+ "completion automatically."
55
+ ),
56
+ )
57
+ p_py.add_argument(
58
+ "shell",
59
+ choices=("bash", "zsh", "fish"),
60
+ help="Target shell.",
61
+ )
62
+
63
+ return parser
64
+
65
+
66
+ def main(argv: list[str] | None = None) -> int:
67
+ """Entry point for the ``onefig`` console script.
68
+
69
+ Args:
70
+ argv: CLI arguments. Defaults to ``sys.argv[1:]``.
71
+
72
+ Returns:
73
+ Process exit code (``0`` on success).
74
+ """
75
+ args = build_parser().parse_args(argv)
76
+ if args.command == "install-completion":
77
+ print(shell_script(args.shell, prog=args.prog))
78
+ elif args.command == "install-python-completion":
79
+ print(python_completion_script(args.shell))
80
+ return 0
81
+
82
+
83
+ if __name__ == "__main__":
84
+ sys.exit(main())
onefig/_completion.py ADDED
@@ -0,0 +1,295 @@
1
+ from __future__ import annotations
2
+
3
+ import shlex
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from onefig._overrides import _resolve_keys
8
+
9
+ _SPECIAL_FLAGS = ("--show", "--help", "-h")
10
+
11
+
12
+ def completion_candidates(model: BaseModel) -> list[str]:
13
+ """Return every override-key candidate the shell should offer.
14
+
15
+ Each scalar field contributes its full dotted path suffixed with ``=``
16
+ (e.g. ``"optimizer.lr="``). When the leaf name resolves uniquely it is
17
+ also offered as a shorthand (``"lr="``); ambiguous leaves are omitted
18
+ so users aren't shown a shortcut the override engine would refuse.
19
+
20
+ Special flags (``--show``, ``--help``, ``-h``) round out the list.
21
+
22
+ Args:
23
+ model: A Pydantic model instance to introspect.
24
+
25
+ Returns:
26
+ A flat list of completion strings, in stable insertion order.
27
+ """
28
+ candidates = _resolve_keys(model)
29
+ seen: set[str] = set()
30
+ out: list[str] = []
31
+ for key, paths in candidates.items():
32
+ if len(paths) != 1:
33
+ # Ambiguous leaf — skip; the full-path form will appear separately.
34
+ continue
35
+ token = f"{key}="
36
+ if token not in seen:
37
+ seen.add(token)
38
+ out.append(token)
39
+ out.extend(_SPECIAL_FLAGS)
40
+ return out
41
+
42
+
43
+ def shell_script(shell: str, *, prog: str) -> str:
44
+ """Render a shell-completion install snippet for ``prog``.
45
+
46
+ The generated script binds tab-completion of ``prog`` to a callback
47
+ that invokes ``prog --onefig-completions <prefix>`` and uses the
48
+ output as the candidate list. Source the snippet in your shell rc to
49
+ install (or eval it inline for one-shot use).
50
+
51
+ Args:
52
+ shell: One of ``"bash"``, ``"zsh"``, ``"fish"``.
53
+ prog: The command name the user types to invoke the script (used
54
+ both as the completion target and as the callback command).
55
+
56
+ Returns:
57
+ A shell snippet ready to write to a file or eval.
58
+
59
+ Raises:
60
+ ValueError: If ``shell`` isn't one of the supported shells.
61
+ """
62
+ if shell == "bash":
63
+ return _bash_script(prog)
64
+ if shell == "zsh":
65
+ return _zsh_script(prog)
66
+ if shell == "fish":
67
+ return _fish_script(prog)
68
+ raise ValueError(f"Unsupported shell {shell!r}; expected one of: bash, zsh, fish.")
69
+
70
+
71
+ def _safe_func_suffix(prog: str) -> str:
72
+ """Turn ``prog`` into an identifier-safe suffix for the completion function."""
73
+ out = []
74
+ for ch in prog:
75
+ if ch.isalnum() or ch == "_":
76
+ out.append(ch)
77
+ else:
78
+ out.append("_")
79
+ return "".join(out) or "onefig"
80
+
81
+
82
+ _BANNER_OPEN = "# ========== onefig: tab completion =========="
83
+ _BANNER_CLOSE = "# ============================================"
84
+ _PY_BANNER_OPEN = "# ====== onefig: python tab completion ======="
85
+ _PY_BANNER_CLOSE = "# ============================================"
86
+
87
+
88
+ def _bash_script(prog: str) -> str:
89
+ func = f"_onefig_complete_{_safe_func_suffix(prog)}"
90
+ qprog = shlex.quote(prog)
91
+ return f"""\
92
+ {_BANNER_OPEN}
93
+ {func}() {{
94
+ local cur
95
+ cur="${{COMP_WORDS[COMP_CWORD]}}"
96
+ local IFS=$'\\n'
97
+ local candidates
98
+ candidates=$({qprog} --onefig-completions "$cur" 2>/dev/null)
99
+ COMPREPLY=( $(compgen -W "$candidates" -- "$cur") )
100
+ }}
101
+ complete -o nospace -F {func} {qprog}
102
+ {_BANNER_CLOSE}
103
+ """
104
+
105
+
106
+ def _zsh_script(prog: str) -> str:
107
+ func = f"_onefig_complete_{_safe_func_suffix(prog)}"
108
+ qprog = shlex.quote(prog)
109
+ return f"""\
110
+ {_BANNER_OPEN}
111
+ {func}() {{
112
+ local -a candidates
113
+ candidates=("${{(@f)$({qprog} --onefig-completions \"$PREFIX\" 2>/dev/null)}}")
114
+ [[ ${{#candidates[@]}} -gt 0 ]] && compadd -S '' -- $candidates
115
+ }}
116
+ if (( ! ${{+functions[compdef]}} )); then
117
+ autoload -Uz compinit 2>/dev/null && compinit -u 2>/dev/null
118
+ fi
119
+ (( ${{+functions[compdef]}} )) && compdef {func} {prog}
120
+ {_BANNER_CLOSE}
121
+ """
122
+
123
+
124
+ def _fish_script(prog: str) -> str:
125
+ func = f"__onefig_complete_{_safe_func_suffix(prog)}"
126
+ qprog = shlex.quote(prog)
127
+ return f"""\
128
+ {_BANNER_OPEN}
129
+ function {func}
130
+ set -l cur (commandline -t)
131
+ {qprog} --onefig-completions "$cur" 2>/dev/null
132
+ end
133
+ complete -c {prog} -f -a '({func})'
134
+ {_BANNER_CLOSE}
135
+ """
136
+
137
+
138
+ def python_completion_script(shell: str) -> str:
139
+ """Render an install snippet for ``python <script>.py`` tab completion.
140
+
141
+ Bound to ``python`` / ``python3`` (and friends), the generated function
142
+ walks the current command line to find the script arg. Before
143
+ invoking the script it greps for the literal word ``onefig`` in the
144
+ file; if not found, the wrapper returns immediately without running
145
+ the script. This keeps TAB safe for arbitrary Python scripts that
146
+ have side effects at import time. Onefig scripts (anything that
147
+ ``import``s or references ``onefig``) match the grep and are invoked
148
+ with ``--onefig-completions <prefix>``; the printed candidates are
149
+ used as the completion list.
150
+
151
+ Falls back to file completion while the user is still typing the
152
+ script path.
153
+
154
+ One-time install. Sourcing the snippet enables completion for every
155
+ onefig-based script invoked via ``python``, regardless of whether the
156
+ script is on ``$PATH`` or directly executable.
157
+
158
+ Args:
159
+ shell: One of ``"bash"``, ``"zsh"``, ``"fish"``.
160
+
161
+ Returns:
162
+ A shell snippet ready to write to a file or eval.
163
+
164
+ Raises:
165
+ ValueError: If ``shell`` isn't one of the supported shells.
166
+ """
167
+ if shell == "bash":
168
+ return _python_bash_script()
169
+ if shell == "zsh":
170
+ return _python_zsh_script()
171
+ if shell == "fish":
172
+ return _python_fish_script()
173
+ raise ValueError(f"Unsupported shell {shell!r}; expected one of: bash, zsh, fish.")
174
+
175
+
176
+ _PYTHON_NAMES = (
177
+ "python",
178
+ "python3",
179
+ "python3.9",
180
+ "python3.10",
181
+ "python3.11",
182
+ "python3.12",
183
+ "python3.13",
184
+ )
185
+
186
+
187
+ def _python_bash_script() -> str:
188
+ targets = " ".join(_PYTHON_NAMES)
189
+ return f"""\
190
+ {_PY_BANNER_OPEN}
191
+ _onefig_python_complete() {{
192
+ local cur script i
193
+ cur="${{COMP_WORDS[COMP_CWORD]}}"
194
+ script=""
195
+ for ((i=1; i<COMP_CWORD; i++)); do
196
+ case "${{COMP_WORDS[i]}}" in
197
+ -*) ;;
198
+ *.py)
199
+ if [[ -f "${{COMP_WORDS[i]}}" ]]; then
200
+ script="${{COMP_WORDS[i]}}"
201
+ break
202
+ fi
203
+ ;;
204
+ esac
205
+ done
206
+ if [[ -z "$script" ]]; then
207
+ COMPREPLY=( $(compgen -f -- "$cur") )
208
+ return
209
+ fi
210
+ # Skip non-onefig scripts to avoid running their side effects on TAB.
211
+ grep -qw onefig "$script" 2>/dev/null || return
212
+ local IFS=$'\\n'
213
+ local candidates
214
+ candidates=$("${{COMP_WORDS[0]}}" "$script" --onefig-completions "$cur" 2>/dev/null)
215
+ if [[ -z "$candidates" ]]; then
216
+ return
217
+ fi
218
+ COMPREPLY=( $(compgen -W "$candidates" -- "$cur") )
219
+ }}
220
+ complete -o nospace -F _onefig_python_complete {targets}
221
+ {_PY_BANNER_CLOSE}
222
+ """
223
+
224
+
225
+ def _python_zsh_script() -> str:
226
+ targets = " ".join(_PYTHON_NAMES)
227
+ cand_expr = (
228
+ '("${(@f)$($python_cmd "$script" '
229
+ '--onefig-completions "$PREFIX" 2>/dev/null)}")'
230
+ )
231
+ return f"""\
232
+ {_PY_BANNER_OPEN}
233
+ _onefig_python_complete() {{
234
+ local script python_cmd i
235
+ python_cmd="${{words[1]}}"
236
+ script=""
237
+ for ((i=2; i<CURRENT; i++)); do
238
+ case "${{words[i]}}" in
239
+ -*) ;;
240
+ *.py)
241
+ if [[ -f "${{words[i]}}" ]]; then
242
+ script="${{words[i]}}"
243
+ break
244
+ fi
245
+ ;;
246
+ esac
247
+ done
248
+ if [[ -z "$script" ]]; then
249
+ _files
250
+ return
251
+ fi
252
+ # Skip non-onefig scripts to avoid running their side effects on TAB.
253
+ grep -qw onefig "$script" 2>/dev/null || return
254
+ local -a candidates
255
+ candidates={cand_expr}
256
+ [[ ${{#candidates[@]}} -gt 0 ]] && compadd -S '' -- $candidates
257
+ }}
258
+ if (( ! ${{+functions[compdef]}} )); then
259
+ autoload -Uz compinit 2>/dev/null && compinit -u 2>/dev/null
260
+ fi
261
+ (( ${{+functions[compdef]}} )) && compdef _onefig_python_complete {targets}
262
+ {_PY_BANNER_CLOSE}
263
+ """
264
+
265
+
266
+ def _python_fish_script() -> str:
267
+ cmds = " ".join(f"-c {n}" for n in _PYTHON_NAMES)
268
+ return f"""\
269
+ {_PY_BANNER_OPEN}
270
+ function __onefig_python_complete
271
+ set -l tokens (commandline -opc)
272
+ set -l cur (commandline -t)
273
+ set -l script ""
274
+ for tok in $tokens[2..]
275
+ if string match -q -- '-*' $tok
276
+ continue
277
+ end
278
+ if string match -q -- '*.py' $tok; and test -f "$tok"
279
+ set script $tok
280
+ break
281
+ end
282
+ end
283
+ if test -z "$script"
284
+ __fish_complete_path "$cur"
285
+ return
286
+ end
287
+ # Skip non-onefig scripts to avoid running their side effects on TAB.
288
+ if not grep -qw onefig "$script" 2>/dev/null
289
+ return
290
+ end
291
+ $tokens[1] "$script" --onefig-completions "$cur" 2>/dev/null
292
+ end
293
+ complete {cmds} -f -a '(__onefig_python_complete)'
294
+ {_PY_BANNER_CLOSE}
295
+ """
onefig/_diff.py ADDED
@@ -0,0 +1,185 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from typing import Any
5
+
6
+ _RED = "\033[31m"
7
+ _GREEN = "\033[32m"
8
+ _DIM = "\033[2m"
9
+ _RESET = "\033[0m"
10
+
11
+
12
+ class _MissingType:
13
+ """Singleton sentinel for keys that are absent from one side of a diff."""
14
+
15
+ _instance: _MissingType | None = None
16
+
17
+ def __new__(cls) -> _MissingType:
18
+ if cls._instance is None:
19
+ cls._instance = super().__new__(cls)
20
+ return cls._instance
21
+
22
+ def __repr__(self) -> str:
23
+ return "<MISSING>"
24
+
25
+ def __bool__(self) -> bool:
26
+ return False
27
+
28
+
29
+ MISSING: Any = _MissingType()
30
+
31
+
32
+ def compute_diff(
33
+ a: dict[str, Any], b: dict[str, Any]
34
+ ) -> dict[str, tuple[Any, Any]]:
35
+ """Diff two flat dotted-key dicts.
36
+
37
+ Returns ``{path: (old, new)}`` for every key whose value differs.
38
+ Keys present in only one side use the :data:`MISSING` sentinel for
39
+ the absent side.
40
+
41
+ The returned dict is ordered: keys present in ``a`` come first (in
42
+ ``a``'s order), then keys only present in ``b`` (in ``b``'s order).
43
+ Makes diff output stable and easy to scan.
44
+ """
45
+ out: dict[str, tuple[Any, Any]] = {}
46
+ for k, va in a.items():
47
+ vb = b.get(k, MISSING)
48
+ if va != vb:
49
+ out[k] = (va, vb)
50
+ for k, vb in b.items():
51
+ if k in a:
52
+ continue
53
+ out[k] = (MISSING, vb)
54
+ return out
55
+
56
+
57
+ def format_diff(
58
+ diff: dict[str, tuple[Any, Any]],
59
+ *,
60
+ color: bool | None = None,
61
+ empty_message: str = "(no changes)",
62
+ ) -> str:
63
+ """Render a diff dict as an aligned ``old → new`` table.
64
+
65
+ Every row uses the side-by-side ``key old → new`` layout, with
66
+ ANSI red on the old side, green on the new side, and dimmed
67
+ ``<MISSING>`` for any side without a value (cross-schema diffs
68
+ against partial dicts).
69
+
70
+ Args:
71
+ diff: Mapping from dotted path to ``(old, new)`` tuple (the
72
+ output of :func:`compute_diff` / :meth:`ConfigModel.diff`).
73
+ color: ``True`` / ``False`` to force ANSI on or off. ``None``
74
+ (default) auto-detects via ``sys.stdout.isatty()``.
75
+ empty_message: Text to return when ``diff`` is empty.
76
+
77
+ Returns:
78
+ A multi-line string ready to print.
79
+ """
80
+ if not diff:
81
+ return empty_message
82
+
83
+ use_color = sys.stdout.isatty() if color is None else color
84
+
85
+ raw_olds = [_format_value(old) for old, _ in diff.values()]
86
+ raw_news = [_format_value(new) for _, new in diff.values()]
87
+
88
+ key_width = max(len(k) for k in diff)
89
+ old_width = max(len(s) for s in raw_olds)
90
+
91
+ lines: list[str] = []
92
+ for (key, (old, new)), old_str, new_str in zip(
93
+ diff.items(), raw_olds, raw_news
94
+ ):
95
+ old_padded = old_str.ljust(old_width)
96
+ old_styled = _style_value(old_padded, old, _RED, enabled=use_color)
97
+ new_styled = _style_value(new_str, new, _GREEN, enabled=use_color)
98
+ lines.append(f" {key.ljust(key_width)} {old_styled} → {new_styled}")
99
+ return "\n".join(lines)
100
+
101
+
102
+ def format_against_defaults(
103
+ current: dict[str, Any],
104
+ defaults: dict[str, Any],
105
+ *,
106
+ color: bool | None = None,
107
+ empty_message: str = "(empty config)",
108
+ ) -> str:
109
+ """Render a config's full state with override highlights.
110
+
111
+ Walks every key in ``current``. For fields that match ``defaults``,
112
+ the value renders alone in green (it's the active value, untouched).
113
+ For fields that diverge, the row renders as ``default → current``
114
+ with the default in red and the current in green.
115
+
116
+ Useful as a "what does this run actually look like, and where did I
117
+ deviate from defaults?" snapshot.
118
+
119
+ Args:
120
+ current: Flat dotted-key dict of the config's current values.
121
+ defaults: Flat dotted-key dict of the schema's default values.
122
+ color: ``True`` / ``False`` to force ANSI on or off. ``None``
123
+ (default) auto-detects via ``sys.stdout.isatty()``.
124
+ empty_message: Text to return when ``current`` is empty.
125
+
126
+ Returns:
127
+ A multi-line string ready to print.
128
+ """
129
+ if not current:
130
+ return empty_message
131
+
132
+ use_color = sys.stdout.isatty() if color is None else color
133
+
134
+ keys = list(current.keys())
135
+ key_width = max(len(k) for k in keys)
136
+
137
+ overridden_olds = [
138
+ repr(defaults[k])
139
+ for k in keys
140
+ if k in defaults and defaults[k] != current[k]
141
+ ]
142
+ old_width = max((len(s) for s in overridden_olds), default=0)
143
+
144
+ lines: list[str] = []
145
+ for key in keys:
146
+ key_col = key.ljust(key_width)
147
+ cur_val = current[key]
148
+ cur_repr = repr(cur_val)
149
+ if key in defaults and defaults[key] == cur_val:
150
+ # Unchanged — single green value, no arrow.
151
+ value_styled = _style(cur_repr, _GREEN, enabled=use_color)
152
+ lines.append(f" {key_col} {value_styled}")
153
+ else:
154
+ # Overridden — default in red, current in green.
155
+ default_repr = (
156
+ repr(defaults[key]) if key in defaults else "<MISSING>"
157
+ )
158
+ old_padded = default_repr.ljust(old_width)
159
+ old_styled = _style(old_padded, _RED, enabled=use_color)
160
+ new_styled = _style(cur_repr, _GREEN, enabled=use_color)
161
+ lines.append(f" {key_col} {old_styled} → {new_styled}")
162
+ return "\n".join(lines)
163
+
164
+
165
+ def _format_value(value: Any) -> str:
166
+ if value is MISSING:
167
+ return "<MISSING>"
168
+ return repr(value)
169
+
170
+
171
+ def _style_value(
172
+ text: str, value: Any, color_code: str, *, enabled: bool
173
+ ) -> str:
174
+ """Style a value's rendered text. MISSING dims; everything else uses
175
+ its side's color."""
176
+ if not enabled:
177
+ return text
178
+ code = _DIM if value is MISSING else color_code
179
+ return f"{code}{text}{_RESET}"
180
+
181
+
182
+ def _style(text: str, color_code: str, *, enabled: bool) -> str:
183
+ if not enabled:
184
+ return text
185
+ return f"{color_code}{text}{_RESET}"
onefig/_env.py ADDED
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+ from onefig._cli import _coerce
7
+
8
+
9
+ def parse_env(
10
+ environ: Mapping[str, str],
11
+ *,
12
+ prefix: str,
13
+ delimiter: str = "__",
14
+ case_sensitive: bool = False,
15
+ ) -> dict[str, Any]:
16
+ """Parse environment variables into a flat override dict.
17
+
18
+ Keys that don't start with ``prefix`` are ignored. For matching keys,
19
+ the prefix is stripped, ``delimiter`` is replaced with ``.`` (so a
20
+ POSIX-legal name can address nested fields), and the result is
21
+ lowercased unless ``case_sensitive`` is set. Values are coerced with
22
+ best-effort JSON parsing (same rules as the CLI parser).
23
+
24
+ Args:
25
+ environ: Mapping of env var names to string values (typically
26
+ ``os.environ``).
27
+ prefix: Required prefix to scope which env vars are consumed.
28
+ Pass ``""`` to read every variable (rarely what you want).
29
+ delimiter: Substring that separates nested-field segments inside
30
+ an env var name. Defaults to ``"__"``, matching the
31
+ pydantic-settings convention.
32
+ case_sensitive: If ``False`` (default), keys are lowercased after
33
+ stripping the prefix. Set ``True`` for schemas with mixed-case
34
+ field names.
35
+
36
+ Returns:
37
+ Flat mapping of override keys (e.g. ``"model.lr"``) to coerced
38
+ values, ready to hand to ``apply_overrides``.
39
+
40
+ Raises:
41
+ ValueError: If a matching env var, after stripping the prefix and
42
+ splitting on the delimiter, contains an empty segment.
43
+ """
44
+ out: dict[str, Any] = {}
45
+ for raw_name, raw_value in environ.items():
46
+ if not raw_name.startswith(prefix):
47
+ continue
48
+ tail = raw_name[len(prefix) :]
49
+ if not tail:
50
+ # Var name equals the prefix exactly; nothing to address.
51
+ continue
52
+ segments = tail.split(delimiter)
53
+ if any(seg == "" for seg in segments):
54
+ raise ValueError(
55
+ f"Env var {raw_name!r} produces an empty key segment "
56
+ f"after stripping prefix {prefix!r} and splitting on "
57
+ f"{delimiter!r}."
58
+ )
59
+ key = ".".join(segments)
60
+ if not case_sensitive:
61
+ key = key.lower()
62
+ out[key] = _coerce(raw_value)
63
+ return out