ocr-foundry 0.2.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.
@@ -0,0 +1,157 @@
1
+ """YAML configuration loader for CLI subcommands.
2
+
3
+ Loading order (highest priority wins):
4
+ 1. argparse built-in defaults (lowest)
5
+ 2. YAML config file values (middle)
6
+ 3. Explicit CLI flags (highest)
7
+
8
+ Keys in the YAML file may use either dashes or underscores;
9
+ both are normalized to underscores to match argparse `dest` names.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import logging
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ _log = logging.getLogger("khocr_gen.config_loader")
20
+
21
+
22
+ def load_yaml_config(path: str | Path) -> dict[str, Any]:
23
+ """Load a YAML file and return a flat `{dest: value}` dict.
24
+
25
+ Keys are normalized: hyphens -> underscores (matching argparse dest names).
26
+ Only top-level scalar / list values are supported; nested mappings are ignored with a warning.
27
+
28
+ Raises:
29
+ FileNotFoundError: If *path* does not exist.
30
+ ImportError: If PyYAML is not installed.
31
+ """
32
+ try:
33
+ import yaml # type: ignore[import-untyped]
34
+ except ImportError as exc:
35
+ raise ImportError(
36
+ "PyYAML is required to use --config (-c). Install it with: pip install pyyaml"
37
+ ) from exc
38
+
39
+ path = Path(path)
40
+ if not path.exists():
41
+ raise FileNotFoundError(f"Config file not found: {path}")
42
+
43
+ with path.open("r", encoding="utf-8") as fh:
44
+ raw = yaml.safe_load(fh)
45
+
46
+ if raw is None:
47
+ return {}
48
+
49
+ if not isinstance(raw, dict):
50
+ raise ValueError(f"Config file must be a YAML mapping (dict) at the top level: {path}")
51
+
52
+ result: dict[str, Any] = {}
53
+ for key, value in raw.items():
54
+ norm_key = str(key).replace("-", "_")
55
+ if isinstance(value, dict):
56
+ if norm_key in {
57
+ "script_intensity_scales", "script_intensity_scale",
58
+ "font_intensity_scales", "font_intensity_scale",
59
+ }:
60
+ result[norm_key] = value
61
+ continue
62
+ _log.warning(
63
+ "Config key %r is a nested mapping; only flat keys are supported, skipping.",
64
+ key,
65
+ )
66
+ continue
67
+ result[norm_key] = value
68
+
69
+ return result
70
+
71
+
72
+ def _build_dest_aliases(parser: argparse.ArgumentParser) -> dict[str, str]:
73
+ """Map option-string spellings to real `dest` names."""
74
+ aliases: dict[str, str] = {}
75
+ for action in parser._actions:
76
+ dest = action.dest
77
+ if not dest or dest == argparse.SUPPRESS:
78
+ continue
79
+ aliases[dest] = dest
80
+ for opt in action.option_strings:
81
+ if opt.startswith("--"):
82
+ aliases.setdefault(opt[2:].replace("-", "_"), dest)
83
+ aliases.setdefault("script_intensity_scales", "script_intensity_scale")
84
+ aliases.setdefault("font_intensity_scales", "font_intensity_scale")
85
+ return aliases
86
+
87
+
88
+ def _resolve_config_keys(parser: argparse.ArgumentParser, config: dict[str, Any]) -> dict[str, Any]:
89
+ """Remap *config*'s keys to real argparse `dest` names where they differ."""
90
+ aliases = _build_dest_aliases(parser)
91
+ result: dict[str, Any] = {}
92
+ for k, v in config.items():
93
+ norm_k = str(k).replace("-", "_")
94
+ result[aliases.get(norm_k, norm_k)] = v
95
+ return result
96
+
97
+
98
+ def validate_config_keys(
99
+ parser: argparse.ArgumentParser,
100
+ config: dict[str, Any],
101
+ command: str,
102
+ strict: bool = False,
103
+ ) -> None:
104
+ """Validate that all keys in *config* correspond to registered arguments.
105
+
106
+ If *strict*, exit with code 2 on unrecognized keys; otherwise log a warning.
107
+ """
108
+ valid_dests = {action.dest for action in parser._actions}
109
+ aliases = _build_dest_aliases(parser)
110
+ invalid_keys = [k for k in config if k not in valid_dests and k not in aliases]
111
+ if invalid_keys:
112
+ msg = (
113
+ f"[config] WARNING: Unrecognized keys in '{command}' configuration: "
114
+ f"{', '.join(sorted(invalid_keys))}"
115
+ )
116
+ if strict:
117
+ import sys
118
+
119
+ print(f"error: {msg}", file=sys.stderr)
120
+ sys.exit(2)
121
+ else:
122
+ _log.warning("%s", msg)
123
+
124
+
125
+ def apply_config_defaults(
126
+ parser: argparse.ArgumentParser,
127
+ config: dict[str, Any],
128
+ ) -> None:
129
+ """Push *config* values into *parser* as low-priority defaults.
130
+
131
+ argparse guarantees that values set via `set_defaults` are overridden
132
+ by any matching CLI flag actually present on the command line.
133
+ """
134
+ if config:
135
+ parser.set_defaults(**_resolve_config_keys(parser, config))
136
+
137
+
138
+ def resolve_config_path(
139
+ config_arg: str | None,
140
+ command: str,
141
+ cwd: str | Path | None = None,
142
+ ) -> Path | None:
143
+ """Resolve the config file to load.
144
+
145
+ Priority:
146
+ 1. Explicit `--config` / `-c` value.
147
+ 2. `configs/<command>.yml` relative to *cwd*.
148
+ """
149
+ if config_arg is not None:
150
+ return Path(config_arg)
151
+
152
+ base = Path(cwd) if cwd is not None else Path.cwd()
153
+ candidate = base / "configs" / f"{command}.yml"
154
+ if candidate.exists():
155
+ return candidate
156
+
157
+ return None