Coverage for ml_workbench/cli_config.py: 17%
69 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-19 11:25 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-19 11:25 +0200
1"""
2CLI tool for processing YAML configuration files.
4This tool loads a YAML configuration file, processes any include directives,
5performs variable interpolation, and outputs the resulting merged YAML.
6"""
8from __future__ import annotations
10import argparse
11import json
12import sys
13from pathlib import Path
14from typing import Optional
16import yaml
18from ml_workbench.config import YamlConfig
21def build_arg_parser() -> argparse.ArgumentParser:
22 """
23 Build the argument parser for the CLI.
25 Returns
26 -------
27 argparse.ArgumentParser
28 Configured argument parser
29 """
30 parser = argparse.ArgumentParser(
31 description="Process YAML configuration files with includes and variable interpolation",
32 formatter_class=argparse.RawDescriptionHelpFormatter,
33 epilog="""
34Examples:
35 # Process a YAML file and output the result
36 %(prog)s config.yaml
38 # Process with variable substitution
39 %(prog)s config.yaml --var catalog=prod --var env=production
41 # Output as JSON instead of YAML
42 %(prog)s config.yaml --json
44 # Output to a file
45 %(prog)s config.yaml -o output.yaml
47 # Show only specific sections
48 %(prog)s config.yaml --section datasets
49 %(prog)s config.yaml --section features --section datasets
50 """,
51 )
53 parser.add_argument(
54 "yaml",
55 type=str,
56 help="Path to the YAML configuration file",
57 )
59 parser.add_argument(
60 "-o",
61 "--output",
62 type=str,
63 help="Output file path (default: stdout)",
64 )
66 parser.add_argument(
67 "--json",
68 action="store_true",
69 help="Output as JSON instead of YAML",
70 )
72 parser.add_argument(
73 "--var",
74 action="append",
75 dest="vars",
76 metavar="KEY=VALUE",
77 help="Variable for interpolation (can be specified multiple times)",
78 )
80 parser.add_argument(
81 "--section",
82 action="append",
83 dest="sections",
84 metavar="SECTION",
85 help="Output only specific section(s) (can be specified multiple times)",
86 )
88 parser.add_argument(
89 "--no-impute",
90 action="store_true",
91 help="Skip dataset type/format imputation",
92 )
94 parser.add_argument(
95 "--show-variables",
96 action="store_true",
97 help="Show resolved variables",
98 )
100 parser.add_argument(
101 "--strict",
102 action="store_true",
103 default=True,
104 help="Strict mode: fail on missing variables (default: True)",
105 )
107 parser.add_argument(
108 "--no-strict",
109 action="store_false",
110 dest="strict",
111 help="Non-strict mode: leave unknown placeholders unchanged",
112 )
114 return parser
117def parse_kv_pairs(pairs: list[str]) -> dict[str, str]:
118 """
119 Parse key=value pairs from command line arguments.
121 Parameters
122 ----------
123 pairs : list[str]
124 List of "key=value" strings
126 Returns
127 -------
128 dict[str, str]
129 Dictionary of parsed key-value pairs
131 Raises
132 ------
133 ValueError
134 If a pair is not in "key=value" format
135 """
136 result = {}
137 for pair in pairs:
138 if "=" not in pair:
139 raise ValueError(f"Invalid key=value pair: {pair}")
140 key, value = pair.split("=", 1)
141 result[key.strip()] = value.strip()
142 return result
145def filter_sections(data: dict, sections: list[str]) -> dict:
146 """
147 Filter configuration to include only specified sections.
149 Parameters
150 ----------
151 data : dict
152 Full configuration dictionary
153 sections : list[str]
154 List of section names to include
156 Returns
157 -------
158 dict
159 Filtered configuration with only specified sections
160 """
161 result = {}
162 for section in sections:
163 if section in data:
164 result[section] = data[section]
165 else:
166 print(f"Warning: Section '{section}' not found in configuration", file=sys.stderr)
167 return result
170def main(argv: Optional[list[str]] = None) -> int:
171 """
172 Main entry point for the CLI.
174 Parameters
175 ----------
176 argv : Optional[list[str]]
177 Command line arguments (default: sys.argv)
179 Returns
180 -------
181 int
182 Exit code (0 for success, non-zero for error)
183 """
184 parser = build_arg_parser()
185 args = parser.parse_args(argv)
187 try:
188 # Parse variable substitutions
189 variables = parse_kv_pairs(list(args.vars)) if args.vars else {}
191 # Load configuration
192 cfg = YamlConfig(args.yaml, strict=args.strict, **variables)
194 # Get the processed data
195 data = cfg.to_dict()
197 # Filter sections if specified
198 if args.sections:
199 data = filter_sections(data, args.sections)
201 # Show variables if requested
202 if args.show_variables:
203 print("# Resolved Variables:", file=sys.stderr)
204 for key, value in sorted(cfg.variables.items()):
205 print(f"# {key}: {value}", file=sys.stderr)
206 print(file=sys.stderr)
208 # Format output
209 if args.json:
210 output_str = json.dumps(data, indent=2)
211 else:
212 output_str = yaml.dump(
213 data,
214 default_flow_style=False,
215 sort_keys=False,
216 allow_unicode=True,
217 )
219 # Write output
220 if args.output:
221 output_path = Path(args.output)
222 output_path.write_text(output_str, encoding="utf-8")
223 print(f"Output written to: {output_path}", file=sys.stderr)
224 else:
225 print(output_str)
227 return 0
229 except FileNotFoundError as e:
230 print(f"Error: {e}", file=sys.stderr)
231 return 1
232 except ValueError as e:
233 print(f"Error: {e}", file=sys.stderr)
234 return 1
235 except Exception as e:
236 print(f"Unexpected error: {e}", file=sys.stderr)
237 if "--debug" in (argv or sys.argv):
238 raise
239 return 1
242if __name__ == "__main__":
243 sys.exit(main())