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.
@@ -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,5 @@
1
+ """`python -m phys_ai_dataprofiler` runs the Hugging Face connector CLI."""
2
+ import sys
3
+ from phys_ai_dataprofiler.connectors.huggingface import main
4
+
5
+ sys.exit(main())
@@ -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."""