mlperf-sysinfo 1.0.0a1__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.
- mlperf_sysinfo/__init__.py +73 -0
- mlperf_sysinfo/cli.py +411 -0
- mlperf_sysinfo/collector.py +399 -0
- mlperf_sysinfo/config.py +488 -0
- mlperf_sysinfo/errors.py +37 -0
- mlperf_sysinfo/output.py +395 -0
- mlperf_sysinfo/preflight.py +304 -0
- mlperf_sysinfo/profiles/__init__.py +125 -0
- mlperf_sysinfo/profiles/endpoints.yaml +32 -0
- mlperf_sysinfo/profiles/inference.yaml +29 -0
- mlperf_sysinfo/report.py +209 -0
- mlperf_sysinfo/templates/endpoints.yaml +65 -0
- mlperf_sysinfo/templates/inference.yaml +34 -0
- mlperf_sysinfo/ui.py +101 -0
- mlperf_sysinfo-1.0.0a1.dist-info/METADATA +316 -0
- mlperf_sysinfo-1.0.0a1.dist-info/RECORD +19 -0
- mlperf_sysinfo-1.0.0a1.dist-info/WHEEL +4 -0
- mlperf_sysinfo-1.0.0a1.dist-info/entry_points.txt +2 -0
- mlperf_sysinfo-1.0.0a1.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Copyright 2026 MLCommons contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""mlperf-sysinfo -- capture MLPerf system descriptions.
|
|
4
|
+
|
|
5
|
+
One config format, one command line, and a profile per working group that
|
|
6
|
+
decides what is required and what the output looks like.
|
|
7
|
+
|
|
8
|
+
Embedding it in another tool:
|
|
9
|
+
|
|
10
|
+
from mlperf_sysinfo import load_config, capture
|
|
11
|
+
|
|
12
|
+
config = load_config("sysinfo.yaml")
|
|
13
|
+
result = capture(config)
|
|
14
|
+
|
|
15
|
+
result.output_path # where the file landed
|
|
16
|
+
result.nodes # per-node status, so callers can report properly
|
|
17
|
+
result.complete # False if any node did not answer
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
# Read from the installed distribution so this can never drift from
|
|
23
|
+
# pyproject.toml -- the release workflow checks the tag against pyproject,
|
|
24
|
+
# and a hand-maintained copy here would not be caught by that check.
|
|
25
|
+
from importlib.metadata import PackageNotFoundError # noqa: E402
|
|
26
|
+
from importlib.metadata import version as _version # noqa: E402
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
__version__ = _version("mlperf-sysinfo")
|
|
30
|
+
except PackageNotFoundError: # running from a source tree, not installed
|
|
31
|
+
__version__ = "0.0.0+unknown"
|
|
32
|
+
|
|
33
|
+
from .config import SysinfoConfig, load_config # noqa: E402
|
|
34
|
+
from .errors import ( # noqa: E402
|
|
35
|
+
CaptureError,
|
|
36
|
+
CheckFailed,
|
|
37
|
+
ConfigError,
|
|
38
|
+
DependencyMissing,
|
|
39
|
+
ProfileError,
|
|
40
|
+
SysinfoError,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"__version__",
|
|
45
|
+
"SysinfoConfig",
|
|
46
|
+
"load_config",
|
|
47
|
+
"capture",
|
|
48
|
+
"check",
|
|
49
|
+
"SysinfoError",
|
|
50
|
+
"ConfigError",
|
|
51
|
+
"ProfileError",
|
|
52
|
+
"CheckFailed",
|
|
53
|
+
"CaptureError",
|
|
54
|
+
"DependencyMissing",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def capture(config, profile=None, **kwargs):
|
|
59
|
+
"""Check, collect, shape, write. See :mod:`mlperf_sysinfo.collector`."""
|
|
60
|
+
from .collector import capture as _capture
|
|
61
|
+
|
|
62
|
+
return _capture(config, profile, **kwargs)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def check(config, profile=None, **kwargs):
|
|
66
|
+
"""Run pre-flight only. See :mod:`mlperf_sysinfo.preflight`."""
|
|
67
|
+
from .preflight import run_check
|
|
68
|
+
from .profiles import load as load_profile
|
|
69
|
+
|
|
70
|
+
if profile is None:
|
|
71
|
+
relative_to = config.source_path.parent if config.source_path else None
|
|
72
|
+
profile = load_profile(config.profile, relative_to=relative_to)
|
|
73
|
+
return run_check(config, profile, **kwargs)
|
mlperf_sysinfo/cli.py
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# Copyright 2026 MLCommons contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""The command line.
|
|
4
|
+
|
|
5
|
+
Five commands: init, check, capture, show, validate. Check is not optional --
|
|
6
|
+
capture always runs it first and there is no flag to skip it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import shutil
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Annotated
|
|
16
|
+
|
|
17
|
+
import cyclopts
|
|
18
|
+
from cyclopts.exceptions import CycloptsError
|
|
19
|
+
|
|
20
|
+
from . import __version__, ui
|
|
21
|
+
from .collector import capture as run_capture
|
|
22
|
+
from .config import load_config
|
|
23
|
+
from .errors import CheckFailed, SysinfoError
|
|
24
|
+
from .preflight import CheckReport, run_check
|
|
25
|
+
from .profiles import available as available_profiles
|
|
26
|
+
from .profiles import load as load_profile
|
|
27
|
+
from .report import summarise, validate
|
|
28
|
+
|
|
29
|
+
_TEMPLATE_DIR = Path(__file__).parent / "templates"
|
|
30
|
+
|
|
31
|
+
EXIT_OK = 0
|
|
32
|
+
EXIT_PROBLEMS = 1
|
|
33
|
+
EXIT_ERROR = 2
|
|
34
|
+
|
|
35
|
+
app = cyclopts.App(
|
|
36
|
+
name="mlperf-sysinfo",
|
|
37
|
+
version=__version__,
|
|
38
|
+
help="Capture MLPerf system descriptions. One config, one CLI, a profile per working group.",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
ConfigOpt = Annotated[
|
|
42
|
+
Path, cyclopts.Parameter(name=["--config", "-c"], help="Path to the config file.")
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _setup_logging(verbose: bool) -> None:
|
|
47
|
+
logging.basicConfig(
|
|
48
|
+
level=logging.DEBUG if verbose else logging.WARNING,
|
|
49
|
+
format="%(levelname)s %(message)s",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _load(config_path: Path):
|
|
54
|
+
"""Load config and its profile together -- they are always needed as a pair."""
|
|
55
|
+
config = load_config(config_path)
|
|
56
|
+
profile = load_profile(config.profile, relative_to=Path(config_path).resolve().parent)
|
|
57
|
+
return config, profile
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# rendering
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _render_check(report: CheckReport, *, out_file: Path) -> None:
|
|
66
|
+
config, profile = report.config, report.profile
|
|
67
|
+
|
|
68
|
+
ui.blank()
|
|
69
|
+
ui.kv("profile", f"{ui.bold(profile.name)} {ui.dim(f'(v{profile.round} rules)')}")
|
|
70
|
+
ui.kv("output", str(out_file))
|
|
71
|
+
|
|
72
|
+
all_targets = [str(t) for t in config.all_targets]
|
|
73
|
+
labels = [n.label for n in report.nodes] + all_targets + ["this machine"]
|
|
74
|
+
if report.endpoint:
|
|
75
|
+
labels.append(report.endpoint.label)
|
|
76
|
+
if report.serving_log:
|
|
77
|
+
labels.append(report.serving_log.label)
|
|
78
|
+
width = max((len(x) for x in labels), default=12) + 2
|
|
79
|
+
|
|
80
|
+
ui.heading("nodes")
|
|
81
|
+
if not report.network_checked:
|
|
82
|
+
for raw in all_targets:
|
|
83
|
+
ui.row(ui.SKIP, raw, ui.dim("not checked"), "--offline was passed", width)
|
|
84
|
+
for node in report.nodes:
|
|
85
|
+
if node.reachable:
|
|
86
|
+
ui.row(ui.OK, node.label, ui.green("reachable"), node.detail, width)
|
|
87
|
+
else:
|
|
88
|
+
ui.row(ui.BAD, node.label, ui.red("unreachable"), node.detail, width)
|
|
89
|
+
if config.nodes.include_local:
|
|
90
|
+
ui.row(ui.OK, "this machine", ui.green("included"), "", width)
|
|
91
|
+
else:
|
|
92
|
+
ui.row(ui.SKIP, "this machine", ui.dim("excluded"), "nodes.include_local is false", width)
|
|
93
|
+
|
|
94
|
+
if report.endpoint or report.serving_log:
|
|
95
|
+
ui.heading("serving")
|
|
96
|
+
if report.endpoint:
|
|
97
|
+
e = report.endpoint
|
|
98
|
+
ui.row(
|
|
99
|
+
ui.OK if e.ok else ui.WARN,
|
|
100
|
+
e.label,
|
|
101
|
+
ui.green("reachable") if e.ok else ui.yellow("no answer"),
|
|
102
|
+
e.detail,
|
|
103
|
+
width,
|
|
104
|
+
)
|
|
105
|
+
if report.serving_log:
|
|
106
|
+
s = report.serving_log
|
|
107
|
+
ui.row(
|
|
108
|
+
ui.OK if s.ok else ui.WARN,
|
|
109
|
+
s.label,
|
|
110
|
+
ui.green("present") if s.ok else ui.yellow("missing"),
|
|
111
|
+
s.detail,
|
|
112
|
+
width,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# The field block has its own column width -- dotted paths are much longer
|
|
116
|
+
# than host names and would otherwise ragged-edge against them.
|
|
117
|
+
field_labels = [
|
|
118
|
+
p
|
|
119
|
+
for p, _ in (
|
|
120
|
+
report.missing_required
|
|
121
|
+
+ report.placeholder_required
|
|
122
|
+
+ report.missing_recommended
|
|
123
|
+
+ report.placeholder_recommended
|
|
124
|
+
+ report.placeholder_other
|
|
125
|
+
)
|
|
126
|
+
] + report.unresolved_env
|
|
127
|
+
fwidth = max((len(x) for x in field_labels), default=20) + 2
|
|
128
|
+
|
|
129
|
+
ui.heading(f"required by profile '{profile.name}'")
|
|
130
|
+
if report.satisfied_count:
|
|
131
|
+
ui.row(ui.OK, f"{report.satisfied_count} fields set", "", "", fwidth)
|
|
132
|
+
for path, why in report.missing_required:
|
|
133
|
+
ui.row(ui.BAD, path, ui.red("missing"), why, fwidth)
|
|
134
|
+
for path, why in report.placeholder_required:
|
|
135
|
+
ui.row(ui.BAD, path, ui.red("placeholder"), f"still the starter value -- {why}", fwidth)
|
|
136
|
+
for ref in report.unresolved_env:
|
|
137
|
+
ui.row(ui.BAD, ref, ui.red("unset"), "not found in the environment", fwidth)
|
|
138
|
+
|
|
139
|
+
if report.placeholder_other:
|
|
140
|
+
ui.heading("still starter text")
|
|
141
|
+
for path, value in report.placeholder_other:
|
|
142
|
+
ui.row(ui.BAD, path, ui.red("placeholder"), repr(value), fwidth)
|
|
143
|
+
|
|
144
|
+
if report.missing_recommended or report.placeholder_recommended:
|
|
145
|
+
ui.heading("worth filling in")
|
|
146
|
+
for path, why in report.missing_recommended:
|
|
147
|
+
ui.row(ui.WARN, path, ui.yellow("empty"), why, fwidth)
|
|
148
|
+
for path, why in report.placeholder_recommended:
|
|
149
|
+
ui.row(
|
|
150
|
+
ui.WARN, path, ui.yellow("placeholder"), f"still the starter value -- {why}", fwidth
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
ui.blank()
|
|
154
|
+
if report.ok:
|
|
155
|
+
if report.network_checked:
|
|
156
|
+
print(f" {ui.green('Ready to capture.')}")
|
|
157
|
+
else:
|
|
158
|
+
print(f" {ui.green('Config is valid.')}")
|
|
159
|
+
ui.hint("Nothing was reached -- run without --offline before capturing.")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
count = report.problem_count
|
|
163
|
+
noun = "problem" if count == 1 else "problems"
|
|
164
|
+
print(f" {ui.red(f'{count} {noun}.')}")
|
|
165
|
+
if report.has_config_problems:
|
|
166
|
+
ui.hint("Fill in the missing fields and run check again.")
|
|
167
|
+
if report.has_reach_problems:
|
|
168
|
+
ui.hint(
|
|
169
|
+
"Fix the unreachable nodes, or run capture --allow-partial "
|
|
170
|
+
"to proceed without them."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
_SYMBOLS = {"ok": ui.OK, "bad": ui.BAD, "warn": ui.WARN, "skip": ui.SKIP}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
# commands
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@app.command
|
|
183
|
+
def init(
|
|
184
|
+
profile: str = "endpoints",
|
|
185
|
+
*,
|
|
186
|
+
path: Annotated[
|
|
187
|
+
Path,
|
|
188
|
+
cyclopts.Parameter(
|
|
189
|
+
name=["--path"], help="Where to write the config. Defaults to sysinfo.yaml in the current directory."
|
|
190
|
+
),
|
|
191
|
+
] = Path("sysinfo.yaml"),
|
|
192
|
+
force: Annotated[
|
|
193
|
+
bool, cyclopts.Parameter(help="Overwrite the file if it already exists.")
|
|
194
|
+
] = False,
|
|
195
|
+
) -> int:
|
|
196
|
+
"""Write a starter config containing only the fields your profile needs."""
|
|
197
|
+
template = _TEMPLATE_DIR / f"{profile}.yaml"
|
|
198
|
+
if not template.exists():
|
|
199
|
+
ui.error(
|
|
200
|
+
f"no starter config for profile {profile!r}. "
|
|
201
|
+
f"Built-in profiles: {', '.join(available_profiles())}"
|
|
202
|
+
)
|
|
203
|
+
return EXIT_ERROR
|
|
204
|
+
if path.exists() and not force:
|
|
205
|
+
ui.error(f"{path} already exists. Pass --force to overwrite it.")
|
|
206
|
+
return EXIT_ERROR
|
|
207
|
+
|
|
208
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
209
|
+
shutil.copyfile(template, path)
|
|
210
|
+
|
|
211
|
+
loaded = load_profile(profile)
|
|
212
|
+
ui.blank()
|
|
213
|
+
print(f" {ui.green('Written')} template {path.name} to path: {path.resolve()}")
|
|
214
|
+
ui.blank()
|
|
215
|
+
ui.hint(f"profile: {loaded.name}")
|
|
216
|
+
ui.blank()
|
|
217
|
+
ui.hint(f"Edit the template {path.name} before running the actual capture command.")
|
|
218
|
+
return EXIT_OK
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@app.command
|
|
222
|
+
def check(
|
|
223
|
+
*,
|
|
224
|
+
config: ConfigOpt,
|
|
225
|
+
offline: Annotated[
|
|
226
|
+
bool,
|
|
227
|
+
cyclopts.Parameter(help="Validate the config only; do not touch the network."),
|
|
228
|
+
] = False,
|
|
229
|
+
verbose: Annotated[bool, cyclopts.Parameter(name=["--verbose", "-v"])] = False,
|
|
230
|
+
) -> int:
|
|
231
|
+
"""Validate the config and reach every node it names. Nothing is collected."""
|
|
232
|
+
_setup_logging(verbose)
|
|
233
|
+
cfg, profile = _load(config)
|
|
234
|
+
report = run_check(cfg, profile, skip_network=offline)
|
|
235
|
+
out_file = cfg.output_dir / (cfg.output.file or profile.output_file)
|
|
236
|
+
_render_check(report, out_file=out_file)
|
|
237
|
+
return EXIT_OK if report.ok else EXIT_PROBLEMS
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@app.command
|
|
241
|
+
def capture(
|
|
242
|
+
*,
|
|
243
|
+
config: ConfigOpt,
|
|
244
|
+
allow_partial: Annotated[
|
|
245
|
+
bool,
|
|
246
|
+
cyclopts.Parameter(
|
|
247
|
+
help="Proceed even if a node is unreachable. The output is marked partial."
|
|
248
|
+
),
|
|
249
|
+
] = False,
|
|
250
|
+
run_metadata: Annotated[
|
|
251
|
+
Path | None,
|
|
252
|
+
cyclopts.Parameter(help="A run_metadata file to patch with serving config values."),
|
|
253
|
+
] = None,
|
|
254
|
+
verbose: Annotated[bool, cyclopts.Parameter(name=["--verbose", "-v"])] = False,
|
|
255
|
+
) -> int:
|
|
256
|
+
"""Run the check, then collect and write the system description."""
|
|
257
|
+
_setup_logging(verbose)
|
|
258
|
+
cfg, profile = _load(config)
|
|
259
|
+
|
|
260
|
+
ui.blank()
|
|
261
|
+
|
|
262
|
+
def progress(kind: str, label: str, detail: str) -> None:
|
|
263
|
+
ui.row(_SYMBOLS.get(kind, " "), label, detail, "", 20)
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
result = run_capture(
|
|
267
|
+
cfg,
|
|
268
|
+
profile,
|
|
269
|
+
allow_partial=allow_partial,
|
|
270
|
+
run_metadata_path=run_metadata,
|
|
271
|
+
progress=progress,
|
|
272
|
+
verbose=verbose,
|
|
273
|
+
)
|
|
274
|
+
except CheckFailed as e:
|
|
275
|
+
if e.report is not None:
|
|
276
|
+
out_file = cfg.output_dir / (cfg.output.file or profile.output_file)
|
|
277
|
+
_render_check(e.report, out_file=out_file)
|
|
278
|
+
else: # pragma: no cover - defensive
|
|
279
|
+
ui.error(str(e))
|
|
280
|
+
ui.blank()
|
|
281
|
+
ui.error("nothing was collected")
|
|
282
|
+
return EXIT_PROBLEMS
|
|
283
|
+
|
|
284
|
+
ui.blank()
|
|
285
|
+
bits = [f"{result.nodes_collected} node(s)"]
|
|
286
|
+
accel = result.accelerator_total
|
|
287
|
+
if accel:
|
|
288
|
+
bits.append(f"{accel} accelerators")
|
|
289
|
+
bits.append(f"profile {profile.name}")
|
|
290
|
+
print(" " + ui.dim(" - ".join(bits)))
|
|
291
|
+
ui.blank()
|
|
292
|
+
|
|
293
|
+
if result.complete:
|
|
294
|
+
print(f" {ui.green('Written')} {result.output_path}")
|
|
295
|
+
else:
|
|
296
|
+
print(f" {ui.yellow('Written (partial)')} {result.output_path}")
|
|
297
|
+
ui.hint("One or more nodes did not answer. The file records this.")
|
|
298
|
+
for extra in result.extra_files:
|
|
299
|
+
print(f" {ui.dim('also')} {extra}")
|
|
300
|
+
ui.blank()
|
|
301
|
+
ui.hint(f"Next: mlperf-sysinfo show {result.output_path}")
|
|
302
|
+
return EXIT_OK if result.complete else EXIT_PROBLEMS
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@app.command
|
|
306
|
+
def show(path: Path) -> int:
|
|
307
|
+
"""Print a readable summary of a captured file."""
|
|
308
|
+
s = summarise(path)
|
|
309
|
+
|
|
310
|
+
ui.blank()
|
|
311
|
+
ui.kv("system", ui.bold(s.system_name), 12)
|
|
312
|
+
ui.kv("profile", f"{s.profile} {ui.dim(f'(v{s.profile_round} rules)')}", 12)
|
|
313
|
+
ui.kv("captured", s.captured_at, 12)
|
|
314
|
+
if not s.complete:
|
|
315
|
+
ui.kv("state", ui.yellow("PARTIAL -- not the whole system"), 12)
|
|
316
|
+
if s.system_size:
|
|
317
|
+
ui.kv("size", s.system_size, 12)
|
|
318
|
+
if s.framework:
|
|
319
|
+
ui.kv("serving", s.framework, 12)
|
|
320
|
+
|
|
321
|
+
if s.nodes:
|
|
322
|
+
ui.heading("node types")
|
|
323
|
+
for accel, count, cpu in s.nodes:
|
|
324
|
+
detail = f"on {cpu}" if cpu else ""
|
|
325
|
+
ui.row(ui.dim("·"), f"{count} x {accel}", "", detail, 44)
|
|
326
|
+
|
|
327
|
+
if s.detected:
|
|
328
|
+
ui.heading("detected")
|
|
329
|
+
for label, value in s.detected:
|
|
330
|
+
ui.kv(label, value, 14)
|
|
331
|
+
|
|
332
|
+
if s.supplied:
|
|
333
|
+
ui.heading("from your config")
|
|
334
|
+
for label, value in s.supplied:
|
|
335
|
+
ui.kv(label, value, 14)
|
|
336
|
+
|
|
337
|
+
ui.blank()
|
|
338
|
+
return EXIT_OK
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@app.command(name="validate")
|
|
342
|
+
def validate_cmd(
|
|
343
|
+
path: Path,
|
|
344
|
+
*,
|
|
345
|
+
profile: Annotated[
|
|
346
|
+
str | None,
|
|
347
|
+
cyclopts.Parameter(help="Validate against this profile instead of the stamped one."),
|
|
348
|
+
] = None,
|
|
349
|
+
) -> int:
|
|
350
|
+
"""Check a captured file against a profile before submitting it."""
|
|
351
|
+
report = validate(path, profile_name=profile)
|
|
352
|
+
|
|
353
|
+
ui.blank()
|
|
354
|
+
ui.kv("file", str(report.path), 10)
|
|
355
|
+
ui.kv("profile", f"{report.profile_name} {ui.dim(f'(v{report.profile_round} rules)')}", 10)
|
|
356
|
+
|
|
357
|
+
if report.problems:
|
|
358
|
+
ui.heading("problems")
|
|
359
|
+
for problem in report.problems:
|
|
360
|
+
ui.row(ui.BAD, problem, "", "", 0)
|
|
361
|
+
if report.warnings:
|
|
362
|
+
ui.heading("warnings")
|
|
363
|
+
for warning in report.warnings:
|
|
364
|
+
ui.row(ui.WARN, warning, "", "", 0)
|
|
365
|
+
|
|
366
|
+
ui.blank()
|
|
367
|
+
if report.ok:
|
|
368
|
+
extra = f" {len(report.warnings)} warning(s)." if report.warnings else ""
|
|
369
|
+
print(f" {ui.green('Valid.')} {report.checked} required field(s) present.{extra}")
|
|
370
|
+
return EXIT_OK
|
|
371
|
+
count = len(report.problems)
|
|
372
|
+
print(f" {ui.red(f'{count} problem(s).')} This file is not ready to submit.")
|
|
373
|
+
return EXIT_PROBLEMS
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@app.command(name="profiles")
|
|
377
|
+
def profiles_cmd() -> int:
|
|
378
|
+
"""List the built-in profiles."""
|
|
379
|
+
ui.blank()
|
|
380
|
+
for name in available_profiles():
|
|
381
|
+
p = load_profile(name)
|
|
382
|
+
print(f" {ui.bold(p.name.ljust(12))} {p.title} {ui.dim(f'round {p.round}')}")
|
|
383
|
+
ui.hint(f" {p.description.strip()}")
|
|
384
|
+
ui.hint(f" {len(p.requires)} required field(s), writes {p.shape} {p.output_file}")
|
|
385
|
+
ui.blank()
|
|
386
|
+
return EXIT_OK
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def main() -> None:
|
|
390
|
+
"""Entry point.
|
|
391
|
+
|
|
392
|
+
The exit codes are a contract, because ``check`` is meant to be scriptable:
|
|
393
|
+
0 all good, 1 the run found problems, 2 the command or config was wrong.
|
|
394
|
+
A mistyped flag must not look like a failed check.
|
|
395
|
+
"""
|
|
396
|
+
try:
|
|
397
|
+
code = app(exit_on_error=False)
|
|
398
|
+
except SysinfoError as e:
|
|
399
|
+
ui.error(str(e))
|
|
400
|
+
sys.exit(EXIT_ERROR)
|
|
401
|
+
except CycloptsError as e:
|
|
402
|
+
ui.error(str(e))
|
|
403
|
+
sys.exit(EXIT_ERROR)
|
|
404
|
+
except KeyboardInterrupt: # pragma: no cover
|
|
405
|
+
ui.error("interrupted")
|
|
406
|
+
sys.exit(130)
|
|
407
|
+
sys.exit(code if isinstance(code, int) else EXIT_OK)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
if __name__ == "__main__": # pragma: no cover
|
|
411
|
+
main()
|