phys-ai-dataprofiler 0.3.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.
- phys_ai_dataprofiler/__init__.py +13 -0
- phys_ai_dataprofiler/__main__.py +5 -0
- phys_ai_dataprofiler/cli.py +214 -0
- phys_ai_dataprofiler/compare.py +123 -0
- phys_ai_dataprofiler/connectors/__init__.py +1 -0
- phys_ai_dataprofiler/connectors/huggingface.py +1113 -0
- phys_ai_dataprofiler/evaluate.py +570 -0
- phys_ai_dataprofiler/inspectors/__init__.py +1 -0
- phys_ai_dataprofiler/inspectors/mat.py +485 -0
- phys_ai_dataprofiler/inspectors/mcap.py +266 -0
- phys_ai_dataprofiler/inspectors/rich.py +428 -0
- phys_ai_dataprofiler/presets/lidar-perception.yaml +15 -0
- phys_ai_dataprofiler/presets/teleop-imitation.yaml +22 -0
- phys_ai_dataprofiler/presets/timeseries-clean.yaml +14 -0
- phys_ai_dataprofiler/presets/visual-odometry.yaml +21 -0
- phys_ai_dataprofiler/remote/__init__.py +1 -0
- phys_ai_dataprofiler/remote/inspector.py +795 -0
- phys_ai_dataprofiler/remote/parquet.py +578 -0
- phys_ai_dataprofiler/remote/range_file.py +326 -0
- phys_ai_dataprofiler/remote/rosbag.py +998 -0
- phys_ai_dataprofiler/requirements.py +421 -0
- phys_ai_dataprofiler-0.3.1.dist-info/METADATA +659 -0
- phys_ai_dataprofiler-0.3.1.dist-info/RECORD +27 -0
- phys_ai_dataprofiler-0.3.1.dist-info/WHEEL +5 -0
- phys_ai_dataprofiler-0.3.1.dist-info/entry_points.txt +2 -0
- phys_ai_dataprofiler-0.3.1.dist-info/licenses/LICENSE +21 -0
- phys_ai_dataprofiler-0.3.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""phys_ai_dataprofiler: zero-copy profiling of physical-AI dataset files (MCAP, ROS bag, MATLAB, Parquet).
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anand Krishniyer. MIT License."""
|
|
4
|
+
|
|
5
|
+
__version__ = "0.3.1"
|
|
6
|
+
|
|
7
|
+
from phys_ai_dataprofiler.inspectors.mcap import McapFileInspector, McapInspectionResult # noqa: F401
|
|
8
|
+
from phys_ai_dataprofiler.inspectors.mat import MatFileInspector, MatInspectionResult # noqa: F401
|
|
9
|
+
from phys_ai_dataprofiler.inspectors.rich import RichInspector, RichProfile # noqa: F401
|
|
10
|
+
from phys_ai_dataprofiler.remote.range_file import HfRangeFile, LocalRangeFile, RangeFile # noqa: F401
|
|
11
|
+
from phys_ai_dataprofiler.remote.inspector import inspect_mcap_remote, inspect_mat_remote, sniff_format # noqa: F401
|
|
12
|
+
from phys_ai_dataprofiler.connectors.huggingface import HuggingFaceConnector # noqa: F401
|
|
13
|
+
from phys_ai_dataprofiler.remote.parquet import inspect_parquet, ParquetInspectionResult # noqa: F401
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Subcommands next to the default `phys-ai-dataprofiler org/dataset` profile:
|
|
3
|
+
|
|
4
|
+
phys-ai-dataprofiler check org/dataset -r req.yaml profile + verdict; exit 0 pass, 1 fail, 4 unknown
|
|
5
|
+
phys-ai-dataprofiler check --report run.json -r req.yaml verdict from an existing report, no network
|
|
6
|
+
phys-ai-dataprofiler compare a.json b.json -r req.yaml rank saved reports against the same requirements
|
|
7
|
+
phys-ai-dataprofiler compare org/a org/b -r req.yaml profile each repo, then rank
|
|
8
|
+
phys-ai-dataprofiler requirements list built-in presets
|
|
9
|
+
phys-ai-dataprofiler requirements show teleop-imitation print a preset (start your own from it)
|
|
10
|
+
phys-ai-dataprofiler requirements validate req.yaml check a file without profiling anything
|
|
11
|
+
|
|
12
|
+
Exit codes: 0 pass (or success), 1 fail, 2 usage or requirements error,
|
|
13
|
+
3 nothing to profile, 4 unknown (the profile could not settle every required check).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from phys_ai_dataprofiler.compare import compare_reports, compare_to_dict, print_compare, render_compare_markdown
|
|
25
|
+
from phys_ai_dataprofiler.evaluate import evaluate, print_verdict, render_verdict_markdown, verdict_to_dict
|
|
26
|
+
from phys_ai_dataprofiler.requirements import (
|
|
27
|
+
RequirementsError,
|
|
28
|
+
list_presets,
|
|
29
|
+
load_requirements,
|
|
30
|
+
preset_text,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
PROG = "phys-ai-dataprofiler"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _log(quiet: bool):
|
|
37
|
+
return (lambda m: None) if quiet else (lambda m: print(m, file=sys.stderr))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _load_req_or_exit(spec: str):
|
|
41
|
+
try:
|
|
42
|
+
return load_requirements(spec)
|
|
43
|
+
except RequirementsError as exc:
|
|
44
|
+
print(str(exc), file=sys.stderr)
|
|
45
|
+
raise SystemExit(2)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _read_report(path: str) -> dict:
|
|
49
|
+
try:
|
|
50
|
+
rep = json.loads(Path(path).read_text())
|
|
51
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
52
|
+
print(f"Error: cannot read report {path}: {exc}", file=sys.stderr)
|
|
53
|
+
raise SystemExit(2)
|
|
54
|
+
if not isinstance(rep, dict) or "files" not in rep:
|
|
55
|
+
print(f"Error: {path} is not a profiler run report (no 'files')", file=sys.stderr)
|
|
56
|
+
raise SystemExit(2)
|
|
57
|
+
return rep
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# --------------------------------------------------------------------------- #
|
|
61
|
+
# check
|
|
62
|
+
# --------------------------------------------------------------------------- #
|
|
63
|
+
|
|
64
|
+
def cmd_check(argv: list[str]) -> int:
|
|
65
|
+
from phys_ai_dataprofiler.connectors.huggingface import build_parser, profile_main
|
|
66
|
+
|
|
67
|
+
if "--report" in argv or any(a.startswith("--report=") for a in argv):
|
|
68
|
+
p = argparse.ArgumentParser(prog=f"{PROG} check",
|
|
69
|
+
description="Verdict for an existing run report (no network).")
|
|
70
|
+
p.add_argument("--report", required=True, help="JSON run report written by an earlier profile")
|
|
71
|
+
p.add_argument("--requirements", "-r", required=True, metavar="FILE|preset:NAME")
|
|
72
|
+
p.add_argument("--output", "-o", help="Write the verdict here (.md or .json; default: print only)")
|
|
73
|
+
p.add_argument("--quiet", action="store_true")
|
|
74
|
+
a = p.parse_args(argv)
|
|
75
|
+
req = _load_req_or_exit(a.requirements)
|
|
76
|
+
v = evaluate(_read_report(a.report), req)
|
|
77
|
+
print_verdict(v, _log(a.quiet))
|
|
78
|
+
if a.output:
|
|
79
|
+
out = Path(a.output)
|
|
80
|
+
out.write_text(json.dumps(verdict_to_dict(v), indent=2) if out.suffix == ".json"
|
|
81
|
+
else render_verdict_markdown(v, level=1))
|
|
82
|
+
_log(a.quiet)(f"Verdict: {out}")
|
|
83
|
+
return v.exit_code
|
|
84
|
+
|
|
85
|
+
p = build_parser(prog=f"{PROG} check")
|
|
86
|
+
p.description = ("Profile a repo and check it against requirements. Exit code 0 pass, 1 fail, 4 unknown. "
|
|
87
|
+
"With --report FILE, check an existing report instead (no network).")
|
|
88
|
+
args = p.parse_args(argv)
|
|
89
|
+
if not args.requirements:
|
|
90
|
+
p.error("check needs --requirements FILE or --requirements preset:NAME")
|
|
91
|
+
return profile_main(args)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# --------------------------------------------------------------------------- #
|
|
95
|
+
# compare
|
|
96
|
+
# --------------------------------------------------------------------------- #
|
|
97
|
+
|
|
98
|
+
def cmd_compare(argv: list[str]) -> int:
|
|
99
|
+
from phys_ai_dataprofiler.connectors.huggingface import build_parser, run, NothingToProfile, _to_jsonable
|
|
100
|
+
|
|
101
|
+
p = argparse.ArgumentParser(
|
|
102
|
+
prog=f"{PROG} compare",
|
|
103
|
+
description="Rank datasets against the same requirements. Each target is a saved JSON run report "
|
|
104
|
+
"or a Hub repo id (profiled now with the default settings plus any options after --).",
|
|
105
|
+
epilog="Example: compare lerobot/pusht org/other -r preset:teleop-imitation -- --max-files 20",
|
|
106
|
+
)
|
|
107
|
+
p.add_argument("targets", nargs="+", help="report.json files and/or Hub repo ids (at least two)")
|
|
108
|
+
p.add_argument("--requirements", "-r", required=True, metavar="FILE|preset:NAME")
|
|
109
|
+
p.add_argument("--output", "-o", help="Write the comparison here (.md or .json; default: "
|
|
110
|
+
"reports/compare_<requirements>_<time>.md and .json)")
|
|
111
|
+
p.add_argument("--quiet", action="store_true")
|
|
112
|
+
if "--" in argv:
|
|
113
|
+
i = argv.index("--")
|
|
114
|
+
argv, profile_opts = argv[:i], argv[i + 1:]
|
|
115
|
+
else:
|
|
116
|
+
profile_opts = []
|
|
117
|
+
a = p.parse_args(argv)
|
|
118
|
+
if len(a.targets) < 2:
|
|
119
|
+
p.error("compare needs at least two targets")
|
|
120
|
+
log = _log(a.quiet)
|
|
121
|
+
req = _load_req_or_exit(a.requirements)
|
|
122
|
+
|
|
123
|
+
reports: list[tuple[str, dict]] = []
|
|
124
|
+
for t in a.targets:
|
|
125
|
+
if t.endswith(".json") or Path(t).is_file():
|
|
126
|
+
reports.append((Path(t).stem, _read_report(t)))
|
|
127
|
+
continue
|
|
128
|
+
log(f"== profiling {t}")
|
|
129
|
+
args = build_parser().parse_args([t, *profile_opts, *(["--quiet"] if a.quiet else [])])
|
|
130
|
+
if req.files_include and not args.include and not args.file:
|
|
131
|
+
args.include = list(req.files_include)
|
|
132
|
+
if req.files_exclude:
|
|
133
|
+
args.exclude = list(args.exclude or []) + list(req.files_exclude)
|
|
134
|
+
try:
|
|
135
|
+
rep = run(args)
|
|
136
|
+
except NothingToProfile as exc:
|
|
137
|
+
log(f" {exc}")
|
|
138
|
+
rep = None
|
|
139
|
+
except RuntimeError as exc:
|
|
140
|
+
log(f" Error: {exc}")
|
|
141
|
+
rep = None
|
|
142
|
+
if rep is None:
|
|
143
|
+
reports.append((t, {"repo_id": t, "files": [], "totals": {}, "files_skipped": []}))
|
|
144
|
+
else:
|
|
145
|
+
reports.append((t, _to_jsonable(rep)))
|
|
146
|
+
|
|
147
|
+
cands = compare_reports(reports, req)
|
|
148
|
+
print_compare(cands, log)
|
|
149
|
+
md = render_compare_markdown(cands, req)
|
|
150
|
+
data = compare_to_dict(cands, req)
|
|
151
|
+
if a.output:
|
|
152
|
+
out = Path(a.output)
|
|
153
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
154
|
+
out.write_text(json.dumps(data, indent=2) if out.suffix == ".json" else md)
|
|
155
|
+
log(f"Comparison: {out}")
|
|
156
|
+
else:
|
|
157
|
+
d = Path("reports")
|
|
158
|
+
d.mkdir(exist_ok=True)
|
|
159
|
+
base = d / f"compare_{req.name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
|
160
|
+
base.with_suffix(".md").write_text(md)
|
|
161
|
+
base.with_suffix(".json").write_text(json.dumps(data, indent=2))
|
|
162
|
+
log(f"Comparison: {base}.md and .json")
|
|
163
|
+
best = cands[0].verdict.status
|
|
164
|
+
return {"pass": 0, "fail": 1, "unknown": 4}[best]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# --------------------------------------------------------------------------- #
|
|
168
|
+
# requirements
|
|
169
|
+
# --------------------------------------------------------------------------- #
|
|
170
|
+
|
|
171
|
+
def cmd_requirements(argv: list[str]) -> int:
|
|
172
|
+
p = argparse.ArgumentParser(prog=f"{PROG} requirements", description="Presets and requirements files.")
|
|
173
|
+
sp = p.add_subparsers(dest="action", required=True)
|
|
174
|
+
sp.add_parser("list", help="List built-in presets")
|
|
175
|
+
s = sp.add_parser("show", help="Print a preset's YAML (redirect to a file to start your own)")
|
|
176
|
+
s.add_argument("preset")
|
|
177
|
+
v = sp.add_parser("validate", help="Check a requirements file without profiling anything")
|
|
178
|
+
v.add_argument("spec", metavar="FILE|preset:NAME")
|
|
179
|
+
a = p.parse_args(argv)
|
|
180
|
+
|
|
181
|
+
if a.action == "list":
|
|
182
|
+
for name, desc in list_presets():
|
|
183
|
+
print(f"preset:{name}\n {desc}")
|
|
184
|
+
return 0
|
|
185
|
+
if a.action == "show":
|
|
186
|
+
try:
|
|
187
|
+
print(preset_text(a.preset.removeprefix("preset:")), end="")
|
|
188
|
+
except RequirementsError as exc:
|
|
189
|
+
print(str(exc), file=sys.stderr)
|
|
190
|
+
return 2
|
|
191
|
+
return 0
|
|
192
|
+
try:
|
|
193
|
+
req = load_requirements(a.spec)
|
|
194
|
+
except RequirementsError as exc:
|
|
195
|
+
print(str(exc), file=sys.stderr)
|
|
196
|
+
return 2
|
|
197
|
+
print(f"OK: {req.name}: {len(req.streams)} stream requirement(s)"
|
|
198
|
+
f" ({sum(s.required for s in req.streams)} required), "
|
|
199
|
+
f"{len(req.dataset.active())} dataset requirement(s)")
|
|
200
|
+
for s in req.streams:
|
|
201
|
+
th = ", ".join(f"{k}={val:g}" for k, val in s.thresholds())
|
|
202
|
+
print(f" stream '{s.name}'{'' if s.required else ' (optional)'}: [{s.match.describe()}]"
|
|
203
|
+
+ (f" {th}" if th else ""))
|
|
204
|
+
for k in req.dataset.active():
|
|
205
|
+
print(f" dataset {k} = {getattr(req.dataset, k)}")
|
|
206
|
+
return 0
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def main(argv: list[str]) -> int:
|
|
210
|
+
cmd, rest = argv[0], argv[1:]
|
|
211
|
+
try:
|
|
212
|
+
return {"check": cmd_check, "compare": cmd_compare, "requirements": cmd_requirements}[cmd](rest)
|
|
213
|
+
except SystemExit as exc:
|
|
214
|
+
return exc.code if isinstance(exc.code, int) else 2
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Put several datasets side by side against the same requirements.
|
|
3
|
+
|
|
4
|
+
Each candidate is a run report (dict, as written by the CLI). The result has
|
|
5
|
+
one row per check and one column per dataset, plus a ranking: fewer required
|
|
6
|
+
failures first, then fewer unknowns, then more passes, then more hours.
|
|
7
|
+
Ties keep the order the datasets were given in.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from phys_ai_dataprofiler.evaluate import BADGE, FAIL, PASS, UNKNOWN, Verdict, evaluate, verdict_to_dict
|
|
15
|
+
from phys_ai_dataprofiler.requirements import Requirements
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Candidate:
|
|
20
|
+
label: str
|
|
21
|
+
report: dict
|
|
22
|
+
verdict: Verdict
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def hours(self) -> float:
|
|
26
|
+
return (self.report.get("totals") or {}).get("duration_s", 0) / 3600
|
|
27
|
+
|
|
28
|
+
def sort_key(self) -> tuple:
|
|
29
|
+
req = [c for c in self.verdict.checks if c.required]
|
|
30
|
+
return (sum(c.status == FAIL for c in req), sum(c.status == UNKNOWN for c in req),
|
|
31
|
+
-sum(c.status == PASS for c in self.verdict.checks), -self.hours)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def compare_reports(reports: list[tuple[str, dict]], req: Requirements) -> list[Candidate]:
|
|
35
|
+
cands = [Candidate(label, rep, evaluate(rep, req)) for label, rep in reports]
|
|
36
|
+
order = sorted(range(len(cands)), key=lambda i: (cands[i].sort_key(), i))
|
|
37
|
+
return [cands[i] for i in order]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _size(n: int | None) -> str:
|
|
41
|
+
if not n:
|
|
42
|
+
return "0 B"
|
|
43
|
+
for unit in ("B", "KB", "MB", "GB", "TB"):
|
|
44
|
+
if abs(n) < 1024 or unit == "TB":
|
|
45
|
+
return f"{n:.1f} {unit}" if unit != "B" else f"{n} B"
|
|
46
|
+
n /= 1024
|
|
47
|
+
return f"{n:.1f} TB"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _cell(text: str) -> str:
|
|
51
|
+
return text.replace("|", "/").replace("\n", " ")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def render_compare_markdown(cands: list[Candidate], req: Requirements) -> str:
|
|
55
|
+
L = [f"# Dataset comparison against `{req.name}`", ""]
|
|
56
|
+
if req.description:
|
|
57
|
+
L += [req.description, ""]
|
|
58
|
+
L.append(f"Requirements: `{req.source}`. Ranked best first: fewer required failures, then fewer unknowns, "
|
|
59
|
+
"then more passes, then more hours.")
|
|
60
|
+
L.append("")
|
|
61
|
+
head = "| | " + " | ".join(f"{i + 1}. `{_cell(c.label)}`" for i, c in enumerate(cands)) + " |"
|
|
62
|
+
L += [head, "|---|" + "---|" * len(cands)]
|
|
63
|
+
L.append("| **Verdict** | " + " | ".join(f"**{BADGE[c.verdict.status]}** "
|
|
64
|
+
f"({c.verdict.passed}/{c.verdict.failed}/{c.verdict.unknown})"
|
|
65
|
+
for c in cands) + " |")
|
|
66
|
+
totals = [c.report.get("totals") or {} for c in cands]
|
|
67
|
+
L.append("| Files profiled | " + " | ".join(f"{t.get('files', 0)} ({t.get('invalid_or_errored', 0)} errored)"
|
|
68
|
+
for t in totals) + " |")
|
|
69
|
+
L.append("| Hours | " + " | ".join(f"{c.hours:.2f}" for c in cands) + " |")
|
|
70
|
+
L.append("| Size / read | " + " | ".join(f"{_size(t.get('bytes'))} / {_size(t.get('bytes_fetched'))}"
|
|
71
|
+
for t in totals) + " |")
|
|
72
|
+
ids = list(dict.fromkeys(ch.id for c in cands for ch in c.verdict.checks))
|
|
73
|
+
for cid in ids:
|
|
74
|
+
label = next(ch.label for c in cands for ch in c.verdict.checks if ch.id == cid)
|
|
75
|
+
opt = next((not ch.required) for c in cands for ch in c.verdict.checks if ch.id == cid)
|
|
76
|
+
row = []
|
|
77
|
+
for c in cands:
|
|
78
|
+
ch = next((x for x in c.verdict.checks if x.id == cid), None)
|
|
79
|
+
row.append("-" if ch is None else _cell(f"{BADGE[ch.status]}: {ch.headline or ch.observed}"))
|
|
80
|
+
L.append(f"| {_cell(label)}{' (optional)' if opt else ''} | " + " | ".join(row) + " |")
|
|
81
|
+
L.append("")
|
|
82
|
+
L.append("Cells show status and what was found. (n/n/n) is passed/failed/unknown checks.")
|
|
83
|
+
from phys_ai_dataprofiler import __version__
|
|
84
|
+
L.append("")
|
|
85
|
+
L.append(f"Generated by [phys-ai-dataprofiler](https://github.com/akrishni/phys-ai-dataprofiler) {__version__}.")
|
|
86
|
+
L.append("")
|
|
87
|
+
for i, c in enumerate(cands):
|
|
88
|
+
bad = [ch for ch in c.verdict.checks if ch.status != PASS]
|
|
89
|
+
if not bad and not c.verdict.notes:
|
|
90
|
+
continue
|
|
91
|
+
L.append(f"## {i + 1}. `{c.label}`: why not pass")
|
|
92
|
+
L.append("")
|
|
93
|
+
for n in c.verdict.notes:
|
|
94
|
+
L.append(f"- Note: {n}")
|
|
95
|
+
for ch in bad:
|
|
96
|
+
L.append(f"- **{ch.label}** {BADGE[ch.status]}: {ch.observed}")
|
|
97
|
+
for e in ch.evidence[:3]:
|
|
98
|
+
L.append(f" - {e}")
|
|
99
|
+
if ch.resolve:
|
|
100
|
+
L.append(f" - To settle: {ch.resolve}")
|
|
101
|
+
L.append("")
|
|
102
|
+
return "\n".join(L)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def print_compare(cands: list[Candidate], log) -> None:
|
|
106
|
+
log("")
|
|
107
|
+
log("COMPARISON (best first)")
|
|
108
|
+
for i, c in enumerate(cands, 1):
|
|
109
|
+
v = c.verdict
|
|
110
|
+
log(f" {i}. {BADGE[v.status]:<8} {c.label} ({v.passed} pass, {v.failed} fail, {v.unknown} unknown; "
|
|
111
|
+
f"{c.hours:.2f} h)")
|
|
112
|
+
for ch in v.checks:
|
|
113
|
+
if ch.status != PASS:
|
|
114
|
+
log(f" {BADGE[ch.status]:<8} {ch.label}: {ch.observed}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def compare_to_dict(cands: list[Candidate], req: Requirements) -> dict:
|
|
118
|
+
return {
|
|
119
|
+
"requirements": req.name, "requirements_source": req.source,
|
|
120
|
+
"ranking": [{"rank": i + 1, "label": c.label, "repo_id": c.report.get("repo_id"),
|
|
121
|
+
"status": c.verdict.status, "hours": round(c.hours, 4),
|
|
122
|
+
"verdict": verdict_to_dict(c.verdict)} for i, c in enumerate(cands)],
|
|
123
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Data-source connectors. Currently: Hugging Face Hub."""
|