algorithm-nexus 0.2.1__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.
- algorithm_nexus/__init__.py +4 -0
- algorithm_nexus/cli.py +101 -0
- algorithm_nexus/commands/__init__.py +6 -0
- algorithm_nexus/commands/ado_validator.py +176 -0
- algorithm_nexus/commands/benchmark_manager.py +1264 -0
- algorithm_nexus/commands/get.py +120 -0
- algorithm_nexus/commands/list.py +349 -0
- algorithm_nexus/commands/run.py +162 -0
- algorithm_nexus/commands/utils.py +584 -0
- algorithm_nexus/commands/validate.py +683 -0
- algorithm_nexus/commands/venv_manager.py +158 -0
- algorithm_nexus/models.py +408 -0
- algorithm_nexus-0.2.1.dist-info/METADATA +319 -0
- algorithm_nexus-0.2.1.dist-info/RECORD +18 -0
- algorithm_nexus-0.2.1.dist-info/WHEEL +5 -0
- algorithm_nexus-0.2.1.dist-info/entry_points.txt +2 -0
- algorithm_nexus-0.2.1.dist-info/licenses/LICENSE +201 -0
- algorithm_nexus-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
# Copyright IBM Corp. 2026
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Utility functions for Algorithm Nexus CLI commands."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import csv
|
|
9
|
+
import io
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import yaml
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import typer
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
except ImportError:
|
|
23
|
+
print(
|
|
24
|
+
"Error: CLI dependencies are not installed.\n"
|
|
25
|
+
"Please install them with: pip install algorithm-nexus[cli]",
|
|
26
|
+
file=sys.stderr,
|
|
27
|
+
)
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
from algorithm_nexus.models import AlgorithmNexusPackageConfig
|
|
31
|
+
|
|
32
|
+
console = Console()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def strip_ansi_codes(text: str) -> str:
|
|
36
|
+
"""Remove ANSI escape sequences from text.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
text: Text potentially containing ANSI codes
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Text with ANSI codes removed
|
|
43
|
+
"""
|
|
44
|
+
ansi_escape = re.compile(r"\x1b\[[0-9;]*m")
|
|
45
|
+
return ansi_escape.sub("", text)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ValidationErrorCollector:
|
|
49
|
+
"""Collects validation errors and info messages during package validation."""
|
|
50
|
+
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
"""Initialize the error collector."""
|
|
53
|
+
self.errors: list[str] = []
|
|
54
|
+
self.info: list[str] = []
|
|
55
|
+
|
|
56
|
+
def add(self, message: str) -> None:
|
|
57
|
+
"""Add a single error message."""
|
|
58
|
+
self.errors.append(message)
|
|
59
|
+
|
|
60
|
+
def add_info(self, message: str) -> None:
|
|
61
|
+
"""Add a single info message."""
|
|
62
|
+
self.info.append(message)
|
|
63
|
+
|
|
64
|
+
def extend(self, messages: list[str]) -> None:
|
|
65
|
+
"""Add multiple error messages."""
|
|
66
|
+
self.errors.extend(messages)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def has_errors(self) -> bool:
|
|
70
|
+
"""Check if any errors have been collected."""
|
|
71
|
+
return bool(self.errors)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def has_info(self) -> bool:
|
|
75
|
+
"""Check if any info messages have been collected."""
|
|
76
|
+
return bool(self.info)
|
|
77
|
+
|
|
78
|
+
def format_errors(self) -> str:
|
|
79
|
+
"""Format errors as a bulleted list."""
|
|
80
|
+
return "\n".join(f"[red]✗[/red] {error}" for error in self.errors)
|
|
81
|
+
|
|
82
|
+
def format_info(self) -> str:
|
|
83
|
+
"""Format info messages as a bulleted list."""
|
|
84
|
+
return "\n".join(f"[yellow]i[/yellow] {msg}" for msg in self.info)
|
|
85
|
+
|
|
86
|
+
def __str__(self) -> str:
|
|
87
|
+
"""Format errors as a bulleted list."""
|
|
88
|
+
return self.format_errors()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load_yaml_file(
|
|
92
|
+
path: Path, collector: ValidationErrorCollector
|
|
93
|
+
) -> dict[str, Any] | None:
|
|
94
|
+
"""Load and parse a YAML file, collecting any errors.
|
|
95
|
+
|
|
96
|
+
Returns a dict if successful, None otherwise.
|
|
97
|
+
Validates that the YAML contains a mapping (dict) at the top level.
|
|
98
|
+
"""
|
|
99
|
+
if not path.is_file():
|
|
100
|
+
collector.add(f"Missing YAML file: {path}")
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
105
|
+
except yaml.YAMLError as exc:
|
|
106
|
+
collector.add(f"Invalid YAML syntax in {path}: {exc}")
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
if data is None:
|
|
110
|
+
collector.add(f"YAML file is empty: {path}")
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
if not isinstance(data, dict):
|
|
114
|
+
collector.add(
|
|
115
|
+
f"{path} must contain a YAML mapping at the top level, got {type(data).__name__}"
|
|
116
|
+
)
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
return data
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def validate_output_format(
|
|
123
|
+
output_format: str | None,
|
|
124
|
+
allow_txt: bool = False,
|
|
125
|
+
allow_yaml: bool = False,
|
|
126
|
+
allow_csv: bool = True,
|
|
127
|
+
allow_table: bool = False,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Validate the output format parameter.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
output_format: The output format to validate
|
|
133
|
+
allow_txt: Whether to allow 'txt' format (for requirements files)
|
|
134
|
+
allow_yaml: Whether to allow 'yaml' format
|
|
135
|
+
allow_csv: Whether to allow 'csv' format (default: True)
|
|
136
|
+
allow_table: Whether to allow 'table' format (for human-readable tables)
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
typer.Exit: If the format is invalid
|
|
140
|
+
"""
|
|
141
|
+
valid_formats = ["json"]
|
|
142
|
+
if allow_csv:
|
|
143
|
+
valid_formats.append("csv")
|
|
144
|
+
if allow_txt:
|
|
145
|
+
valid_formats.append("txt")
|
|
146
|
+
if allow_yaml:
|
|
147
|
+
valid_formats.append("yaml")
|
|
148
|
+
if allow_table:
|
|
149
|
+
valid_formats.append("table")
|
|
150
|
+
|
|
151
|
+
if output_format is not None and output_format not in valid_formats:
|
|
152
|
+
formats_str = "', '".join(valid_formats)
|
|
153
|
+
console.print(
|
|
154
|
+
f"[red]Error:[/red] Invalid output format '{output_format}'. "
|
|
155
|
+
f"Must be '{formats_str}'."
|
|
156
|
+
)
|
|
157
|
+
raise typer.Exit(code=1)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def validate_txt_only_format(output_format: str | None) -> None:
|
|
161
|
+
"""Validate that output format is 'txt' if specified.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
output_format: The output format to validate
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
typer.Exit: If the format is not 'txt'
|
|
168
|
+
"""
|
|
169
|
+
if output_format is not None and output_format != "txt":
|
|
170
|
+
console.print(
|
|
171
|
+
f"[red]Error:[/red] Invalid output format '{output_format}'. Must be 'txt'."
|
|
172
|
+
)
|
|
173
|
+
raise typer.Exit(code=1)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def output_data(
|
|
177
|
+
data: list[dict[str, Any]],
|
|
178
|
+
headers: list[str],
|
|
179
|
+
output_format: str | None,
|
|
180
|
+
output_file: Path | None,
|
|
181
|
+
table_title: str,
|
|
182
|
+
) -> None:
|
|
183
|
+
"""Output data in the specified format.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
data: List of dictionaries containing the data rows
|
|
187
|
+
headers: List of column headers
|
|
188
|
+
output_format: Output format ('csv', 'json', 'yaml', or None for table)
|
|
189
|
+
output_file: Optional file path to write output to
|
|
190
|
+
table_title: Title for the table output
|
|
191
|
+
"""
|
|
192
|
+
if output_format == "json":
|
|
193
|
+
json_output = json.dumps(data, indent=2)
|
|
194
|
+
if output_file:
|
|
195
|
+
output_file.write_text(json_output)
|
|
196
|
+
console.print(f"[green]Output written to {output_file}[/green]")
|
|
197
|
+
else:
|
|
198
|
+
console.print(json_output)
|
|
199
|
+
elif output_format == "yaml":
|
|
200
|
+
yaml_output = yaml.dump(data, default_flow_style=False, sort_keys=False)
|
|
201
|
+
if output_file:
|
|
202
|
+
output_file.write_text(yaml_output)
|
|
203
|
+
console.print(f"[green]Output written to {output_file}[/green]")
|
|
204
|
+
else:
|
|
205
|
+
console.print(yaml_output)
|
|
206
|
+
elif output_format == "csv":
|
|
207
|
+
# Write CSV to string first
|
|
208
|
+
csv_buffer = io.StringIO()
|
|
209
|
+
writer = csv.DictWriter(csv_buffer, fieldnames=headers)
|
|
210
|
+
writer.writeheader()
|
|
211
|
+
writer.writerows(data)
|
|
212
|
+
csv_output = csv_buffer.getvalue()
|
|
213
|
+
|
|
214
|
+
if output_file:
|
|
215
|
+
output_file.write_text(csv_output)
|
|
216
|
+
console.print(f"[green]Output written to {output_file}[/green]")
|
|
217
|
+
else:
|
|
218
|
+
console.print(csv_output)
|
|
219
|
+
else:
|
|
220
|
+
# Default table output
|
|
221
|
+
if output_file:
|
|
222
|
+
console.print(
|
|
223
|
+
"[yellow]Warning: --output-file is ignored for table output[/yellow]"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
console.print(f"\n[bold]{table_title}[/bold]\n")
|
|
227
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
228
|
+
|
|
229
|
+
# Add columns with appropriate styling
|
|
230
|
+
for header in headers:
|
|
231
|
+
if "package" in header.lower() and "nexus" in header.lower():
|
|
232
|
+
table.add_column(header, style="cyan")
|
|
233
|
+
elif "package" in header.lower():
|
|
234
|
+
table.add_column(header, style="yellow")
|
|
235
|
+
elif "experiment" in header.lower():
|
|
236
|
+
table.add_column(header, style="white")
|
|
237
|
+
else:
|
|
238
|
+
table.add_column(header)
|
|
239
|
+
|
|
240
|
+
# Add rows
|
|
241
|
+
for row in data:
|
|
242
|
+
table.add_row(*[str(row[h]) for h in headers])
|
|
243
|
+
|
|
244
|
+
console.print(table)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def collect_benchmark_data(
|
|
248
|
+
packages_root: Path,
|
|
249
|
+
warn_on_error: bool = False,
|
|
250
|
+
) -> dict[str, dict[str, list[str]]]:
|
|
251
|
+
"""Collect benchmark package data from all nexus packages.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
packages_root: Root directory containing package directories
|
|
255
|
+
warn_on_error: If True, print warnings to stderr for failed loads
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Dictionary mapping benchmark_package -> experiment_id -> [nexus_package_names]
|
|
259
|
+
"""
|
|
260
|
+
# Structure: benchmark_package -> experiment_id -> list of nexus packages
|
|
261
|
+
benchmark_data: dict[str, dict[str, list[str]]] = {}
|
|
262
|
+
|
|
263
|
+
for package_dir in packages_root.iterdir():
|
|
264
|
+
if not package_dir.is_dir() or package_dir.name.startswith("."):
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
nexus_yaml_path = package_dir / "nexus.yaml"
|
|
268
|
+
if not nexus_yaml_path.exists():
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
collector = ValidationErrorCollector()
|
|
272
|
+
nexus_data = load_yaml_file(nexus_yaml_path, collector)
|
|
273
|
+
if nexus_data is None or collector.has_errors:
|
|
274
|
+
if warn_on_error:
|
|
275
|
+
error_console = Console(stderr=True)
|
|
276
|
+
error_console.print(
|
|
277
|
+
f"[yellow]Warning:[/yellow] Skipping {package_dir.name}: "
|
|
278
|
+
f"Failed to load nexus.yaml"
|
|
279
|
+
)
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
package_config = AlgorithmNexusPackageConfig.model_validate(nexus_data)
|
|
284
|
+
package_name = package_config.package.name
|
|
285
|
+
|
|
286
|
+
if package_config.package.benchmark_packages:
|
|
287
|
+
for bench_pkg in package_config.package.benchmark_packages:
|
|
288
|
+
req_spec = bench_pkg.requirement_specifier
|
|
289
|
+
if req_spec not in benchmark_data:
|
|
290
|
+
benchmark_data[req_spec] = {}
|
|
291
|
+
|
|
292
|
+
for exp_id in bench_pkg.experiments:
|
|
293
|
+
if exp_id not in benchmark_data[req_spec]:
|
|
294
|
+
benchmark_data[req_spec][exp_id] = []
|
|
295
|
+
benchmark_data[req_spec][exp_id].append(package_name)
|
|
296
|
+
except Exception as e: # noqa: S112
|
|
297
|
+
if warn_on_error:
|
|
298
|
+
error_console = Console(stderr=True)
|
|
299
|
+
error_console.print(
|
|
300
|
+
f"[yellow]Warning:[/yellow] Skipping {package_dir.name}: "
|
|
301
|
+
f"Invalid package configuration ({type(e).__name__})"
|
|
302
|
+
)
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
return benchmark_data
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def try_load_package_config(
|
|
309
|
+
package_dir: Path,
|
|
310
|
+
warn_on_error: bool = False,
|
|
311
|
+
) -> AlgorithmNexusPackageConfig | None:
|
|
312
|
+
"""Attempt to load and validate a package configuration.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
package_dir: Directory containing the nexus.yaml file
|
|
316
|
+
warn_on_error: If True, print warnings to stderr for failed loads
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Validated AlgorithmNexusPackageConfig if successful, None otherwise
|
|
320
|
+
"""
|
|
321
|
+
nexus_yaml_path = package_dir / "nexus.yaml"
|
|
322
|
+
if not nexus_yaml_path.exists():
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
collector = ValidationErrorCollector()
|
|
326
|
+
nexus_data = load_yaml_file(nexus_yaml_path, collector)
|
|
327
|
+
if nexus_data is None or collector.has_errors:
|
|
328
|
+
if warn_on_error:
|
|
329
|
+
error_console = Console(stderr=True)
|
|
330
|
+
error_console.print(
|
|
331
|
+
f"[yellow]Warning:[/yellow] Skipping {package_dir.name}: "
|
|
332
|
+
f"Failed to load nexus.yaml"
|
|
333
|
+
)
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
return AlgorithmNexusPackageConfig.model_validate(nexus_data)
|
|
338
|
+
except Exception as e: # noqa: S112
|
|
339
|
+
if warn_on_error:
|
|
340
|
+
error_console = Console(stderr=True)
|
|
341
|
+
error_console.print(
|
|
342
|
+
f"[yellow]Warning:[/yellow] Skipping {package_dir.name}: "
|
|
343
|
+
f"Invalid package configuration ({type(e).__name__})"
|
|
344
|
+
)
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def find_package_config(
|
|
349
|
+
nexus_package: str,
|
|
350
|
+
packages_root: Path,
|
|
351
|
+
) -> tuple[Path, AlgorithmNexusPackageConfig] | None:
|
|
352
|
+
"""Find and load a package configuration by name.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
nexus_package: Name of the Nexus package to find
|
|
356
|
+
packages_root: Root directory containing package directories
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Tuple of (package_dir, package_config) if found, None otherwise
|
|
360
|
+
"""
|
|
361
|
+
for pkg_dir in packages_root.iterdir():
|
|
362
|
+
if not pkg_dir.is_dir() or pkg_dir.name.startswith("."):
|
|
363
|
+
continue
|
|
364
|
+
|
|
365
|
+
package_config = try_load_package_config(pkg_dir)
|
|
366
|
+
if package_config and package_config.package.name == nexus_package:
|
|
367
|
+
return (pkg_dir, package_config)
|
|
368
|
+
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def output_requirements_txt(
|
|
373
|
+
requirements: list[str],
|
|
374
|
+
output_file: Path | None,
|
|
375
|
+
) -> None:
|
|
376
|
+
"""Output requirements in txt format.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
requirements: List of requirement specifiers
|
|
380
|
+
output_file: Optional file path to write output to
|
|
381
|
+
"""
|
|
382
|
+
txt_output = "\n".join(requirements) + "\n"
|
|
383
|
+
|
|
384
|
+
if output_file:
|
|
385
|
+
output_file.write_text(txt_output)
|
|
386
|
+
console.print(f"[green]Requirements written to {output_file}[/green]")
|
|
387
|
+
else:
|
|
388
|
+
console.print(txt_output, end="")
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def output_benchmark_requirements_table(
|
|
392
|
+
benchmark_packages: list[Any],
|
|
393
|
+
nexus_package: str,
|
|
394
|
+
output_format: str | None,
|
|
395
|
+
output_file: Path | None,
|
|
396
|
+
) -> None:
|
|
397
|
+
"""Output benchmark requirements in table format.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
benchmark_packages: List of BenchmarkPackage objects
|
|
401
|
+
nexus_package: Name of the Nexus package
|
|
402
|
+
output_format: Output format (csv, json, or None for table)
|
|
403
|
+
output_file: Optional file path to write output to
|
|
404
|
+
"""
|
|
405
|
+
headers = ["Requirement Specifier"]
|
|
406
|
+
|
|
407
|
+
data = [
|
|
408
|
+
{"Requirement Specifier": bench_pkg.requirement_specifier}
|
|
409
|
+
for bench_pkg in benchmark_packages
|
|
410
|
+
]
|
|
411
|
+
|
|
412
|
+
output_data(
|
|
413
|
+
data=data,
|
|
414
|
+
headers=headers,
|
|
415
|
+
output_format=output_format,
|
|
416
|
+
output_file=output_file,
|
|
417
|
+
table_title=f"Benchmark Requirements for Nexus Package: {nexus_package}",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def format_results(results: dict[str, Any], fmt: str) -> str:
|
|
422
|
+
"""Format benchmark results as JSON or YAML string.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
results: Dictionary containing results to format
|
|
426
|
+
fmt: Output format ('json' or 'yaml')
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
Formatted string representation of results
|
|
430
|
+
"""
|
|
431
|
+
if fmt == "json":
|
|
432
|
+
return json.dumps(results, indent=2)
|
|
433
|
+
else: # yaml
|
|
434
|
+
return yaml.safe_dump(results, default_flow_style=False, sort_keys=False)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def get_status_color(status: str) -> str:
|
|
438
|
+
"""Get the color name for a given status.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
status: Status string (e.g., 'success', 'failed', 'started')
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
Color name for Rich console formatting
|
|
445
|
+
"""
|
|
446
|
+
status_colors = {
|
|
447
|
+
"success": "green",
|
|
448
|
+
"started": "cyan",
|
|
449
|
+
"failed": "red",
|
|
450
|
+
}
|
|
451
|
+
return status_colors.get(status, "yellow")
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def get_status_display(status: str) -> str:
|
|
455
|
+
"""Get color-coded status display string.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
status: Status string (e.g., 'success', 'failed', 'started')
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
Rich-formatted status string with color
|
|
462
|
+
"""
|
|
463
|
+
color = get_status_color(status)
|
|
464
|
+
return f"[{color}]{status}[/{color}]"
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def determine_output_format(
|
|
468
|
+
output_format: str | None,
|
|
469
|
+
output_file: Path | None,
|
|
470
|
+
default: str = "table",
|
|
471
|
+
) -> str:
|
|
472
|
+
"""Determine the effective output format from CLI options.
|
|
473
|
+
|
|
474
|
+
Precedence: explicit --output-format > file extension > default.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
output_format: Explicitly requested format, or None.
|
|
478
|
+
output_file: Output file path (used to infer format from extension), or None.
|
|
479
|
+
default: Fallback format when neither of the above apply.
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
Resolved format string (e.g. 'json', 'yaml', 'table').
|
|
483
|
+
"""
|
|
484
|
+
if output_format:
|
|
485
|
+
return output_format
|
|
486
|
+
if output_file and output_file.suffix in (".yaml", ".yml"):
|
|
487
|
+
return "yaml"
|
|
488
|
+
if output_file:
|
|
489
|
+
return "json"
|
|
490
|
+
return default
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def write_results_to_file(results: dict[str, Any], output_file: Path, fmt: str) -> None:
|
|
494
|
+
"""Write benchmark results to a file in the specified format.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
results: Dictionary containing results to write
|
|
498
|
+
output_file: Path to the output file
|
|
499
|
+
fmt: Output format ('json' or 'yaml')
|
|
500
|
+
"""
|
|
501
|
+
output_file.write_text(format_results(results, fmt))
|
|
502
|
+
console.print(f"\nResults written to: {output_file}")
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def print_structured_results(results: dict[str, Any], fmt: str) -> None:
|
|
506
|
+
"""Print benchmark results in structured format (JSON or YAML).
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
results: Dictionary containing results to print
|
|
510
|
+
fmt: Output format ('json' or 'yaml')
|
|
511
|
+
"""
|
|
512
|
+
console.print() # Blank line before output
|
|
513
|
+
console.print(format_results(results, fmt))
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def print_results_table(
|
|
517
|
+
rows: list[dict[str, Any]],
|
|
518
|
+
title: str,
|
|
519
|
+
details_column: str,
|
|
520
|
+
) -> None:
|
|
521
|
+
"""Print a results table with Instance, Status, and a details column.
|
|
522
|
+
|
|
523
|
+
Each row dict may contain:
|
|
524
|
+
- ``instance_path`` (str): shown in the Instance column.
|
|
525
|
+
- ``status`` (str): coloured in the Status column.
|
|
526
|
+
- ``details`` (str): pre-formatted text for the details column.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
rows: List of row dicts as described above.
|
|
530
|
+
title: Table title.
|
|
531
|
+
details_column: Header label for the third column.
|
|
532
|
+
"""
|
|
533
|
+
table = Table(title=f"\n{title}", show_header=True)
|
|
534
|
+
table.add_column("Instance", style="cyan")
|
|
535
|
+
table.add_column("Status", style="bold")
|
|
536
|
+
table.add_column(details_column)
|
|
537
|
+
|
|
538
|
+
for row in rows:
|
|
539
|
+
status = row.get("status", "unknown")
|
|
540
|
+
color = get_status_color(status)
|
|
541
|
+
status_display = f"[{color}]{status}[/{color}]"
|
|
542
|
+
table.add_row(
|
|
543
|
+
row.get("instance_path", "Unknown"),
|
|
544
|
+
status_display,
|
|
545
|
+
row.get("details", "-"),
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
console.print(table)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def print_human_readable_results(results: dict[str, Any]) -> None:
|
|
552
|
+
"""Print benchmark execution results as a table.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
results: Dictionary containing results with 'instances' key
|
|
556
|
+
"""
|
|
557
|
+
instances = results.get("instances")
|
|
558
|
+
if not instances:
|
|
559
|
+
console.print("[yellow]No benchmark instances found[/yellow]")
|
|
560
|
+
return
|
|
561
|
+
|
|
562
|
+
rows = []
|
|
563
|
+
for instance in instances:
|
|
564
|
+
details = []
|
|
565
|
+
if instance.get("message"):
|
|
566
|
+
details.append(instance["message"])
|
|
567
|
+
if instance.get("space_id"):
|
|
568
|
+
details.append(f"Space ID: {instance['space_id']}")
|
|
569
|
+
if instance.get("operation_id"):
|
|
570
|
+
details.append(f"Operation ID: {instance['operation_id']}")
|
|
571
|
+
if instance.get("ray_job_id"):
|
|
572
|
+
details.append(f"Ray Job ID: {instance['ray_job_id']}")
|
|
573
|
+
rows.append(
|
|
574
|
+
{
|
|
575
|
+
"instance_path": instance.get("instance_path", "Unknown"),
|
|
576
|
+
"status": instance.get("status", "unknown"),
|
|
577
|
+
"details": "\n".join(details) if details else "-",
|
|
578
|
+
}
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
print_results_table(rows, title="Execution Results", details_column="Details")
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
# Made with Bob
|