bepad 0.1.0__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.
- bepad/__init__.py +17 -0
- bepad/__main__.py +206 -0
- bepad/associator.py +339 -0
- bepad/build/__init__.py +11 -0
- bepad/build/config.py +116 -0
- bepad/build/geo.py +64 -0
- bepad/build/labels.py +341 -0
- bepad/build/pool.py +392 -0
- bepad/build/release.py +256 -0
- bepad/build/utdquake.py +76 -0
- bepad/build/windows.py +408 -0
- bepad/data.py +185 -0
- bepad/leaderboard.py +368 -0
- bepad/metrics.py +324 -0
- bepad/plots.py +950 -0
- bepad/stats.py +140 -0
- bepad/torch.py +159 -0
- bepad-0.1.0.dist-info/METADATA +85 -0
- bepad-0.1.0.dist-info/RECORD +23 -0
- bepad-0.1.0.dist-info/WHEEL +5 -0
- bepad-0.1.0.dist-info/entry_points.txt +2 -0
- bepad-0.1.0.dist-info/licenses/LICENSE +674 -0
- bepad-0.1.0.dist-info/top_level.txt +1 -0
bepad/leaderboard.py
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Leaderboard: submit results, verify them, build the tables.
|
|
2
|
+
|
|
3
|
+
Everything lives in the BEPAD Hugging Face dataset repo::
|
|
4
|
+
|
|
5
|
+
associators/<name>/ contributed code (associator.py, config.yaml, README.md, ...)
|
|
6
|
+
leaderboard/<release>/<name>/submission.json scores + metadata (written by Result.save)
|
|
7
|
+
leaderboard/<release>/<name>/predictions/ the predictions, so scores can be re-checked
|
|
8
|
+
leaderboard/README.md the tables (built by ``bepad leaderboard build``)
|
|
9
|
+
|
|
10
|
+
Authors run ``bepad submit results/<name> --code associators/<name>``: the
|
|
11
|
+
files are checked locally, then a pull request is opened on Hugging Face. It
|
|
12
|
+
only ever adds the author's own two folders, never touches anything else.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import pandas as pd
|
|
21
|
+
|
|
22
|
+
REPO_ID = "ecastillot/BEPAD"
|
|
23
|
+
SCORE_TOL = 1e-6
|
|
24
|
+
TABLE_COLS = ["event_f1", "pick_f1", "noise_rejection", "event_precision", "event_recall", "merge_rate",
|
|
25
|
+
"split_rate", "epi_median_km", "depth_median_km", "t0_median_s"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ----------------------------------------------------------------------
|
|
29
|
+
# Checking
|
|
30
|
+
# ----------------------------------------------------------------------
|
|
31
|
+
def verify(result_dir, root=None, code_dir=None):
|
|
32
|
+
"""Check a result folder: files, metadata, and scores recomputed from the predictions.
|
|
33
|
+
|
|
34
|
+
Returns a list of problems (empty = OK). ``root``: local release folder
|
|
35
|
+
(otherwise the needed labels are downloaded).
|
|
36
|
+
"""
|
|
37
|
+
from .metrics import evaluate
|
|
38
|
+
from .associator import METHOD_TYPES, TRAINING
|
|
39
|
+
|
|
40
|
+
d = Path(result_dir)
|
|
41
|
+
problems = []
|
|
42
|
+
sub_file = d / "submission.json"
|
|
43
|
+
if not sub_file.exists():
|
|
44
|
+
return [f"{sub_file} not found -- create it with result.save('{d}')"]
|
|
45
|
+
sub = json.loads(sub_file.read_text())
|
|
46
|
+
a, run = sub.get("associator", {}), sub.get("run", {})
|
|
47
|
+
for key in ("name", "version", "authors", "method_type", "training"):
|
|
48
|
+
if not a.get(key):
|
|
49
|
+
problems.append(f"associator.{key} is empty (set it in the class)")
|
|
50
|
+
if a.get("authors") == "Your Name":
|
|
51
|
+
problems.append("associator.authors is still 'Your Name'")
|
|
52
|
+
if a.get("method_type") not in METHOD_TYPES:
|
|
53
|
+
problems.append(f"method_type must be one of {METHOD_TYPES}")
|
|
54
|
+
if a.get("training") not in TRAINING:
|
|
55
|
+
problems.append(f"training must be one of {TRAINING}")
|
|
56
|
+
if run.get("split") != "test":
|
|
57
|
+
problems.append(f"leaderboard results must be on the test split (got {run.get('split')!r})")
|
|
58
|
+
from .data import CORE_NAME, CORE_NETWORKS
|
|
59
|
+
|
|
60
|
+
missing = sorted(set(CORE_NETWORKS) - set(run.get("networks") or []))
|
|
61
|
+
if run.get("release") != "dev" and missing:
|
|
62
|
+
problems.append(f"the {CORE_NAME} networks are required, missing: {', '.join(missing)} "
|
|
63
|
+
"(run with --networks core, plus any other networks)")
|
|
64
|
+
files = sorted((d / "predictions").glob("network=*.parquet"))
|
|
65
|
+
if not files:
|
|
66
|
+
problems.append("no predictions/network=*.parquet files")
|
|
67
|
+
return problems
|
|
68
|
+
if code_dir is not None:
|
|
69
|
+
c = Path(code_dir)
|
|
70
|
+
for f in ("associator.py", "config.yaml", "README.md"):
|
|
71
|
+
if not (c / f).exists():
|
|
72
|
+
problems.append(f"{c / f} missing (bepad new creates it)")
|
|
73
|
+
if c.name != a.get("name"):
|
|
74
|
+
problems.append(f"code folder is named {c.name!r} but the associator is {a.get('name')!r}")
|
|
75
|
+
|
|
76
|
+
pred = pd.concat([pd.read_parquet(f) for f in files], ignore_index=True)
|
|
77
|
+
lfiles = sorted((d / "locations").glob("network=*.parquet"))
|
|
78
|
+
locs = pd.concat([pd.read_parquet(f) for f in lfiles], ignore_index=True) if lfiles else None
|
|
79
|
+
try:
|
|
80
|
+
from .data import Release, download
|
|
81
|
+
|
|
82
|
+
rel = Release(root) if root else Release(download(run["release"], splits=("test",),
|
|
83
|
+
networks=run["networks"]))
|
|
84
|
+
report = evaluate(pred, rel, split="test", networks=run["networks"],
|
|
85
|
+
window_ids=set(pred.window_id), locations=locs)
|
|
86
|
+
except Exception as exc: # noqa: BLE001 -- report instead of crashing
|
|
87
|
+
return problems + [f"could not re-score the predictions: {exc!r}"]
|
|
88
|
+
got = report.overall.iloc[0]
|
|
89
|
+
for k, v in sub["scores"]["overall"].items():
|
|
90
|
+
if k in got and v is not None and abs(float(got[k]) - v) > SCORE_TOL:
|
|
91
|
+
problems.append(f"score {k}: submission says {v}, predictions give {float(got[k]):.6f}")
|
|
92
|
+
if report.meta["n_windows"] != run.get("n_windows"):
|
|
93
|
+
problems.append(f"n_windows: submission says {run.get('n_windows')}, predictions cover "
|
|
94
|
+
f"{report.meta['n_windows']}")
|
|
95
|
+
return problems
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ----------------------------------------------------------------------
|
|
99
|
+
# Submitting
|
|
100
|
+
# ----------------------------------------------------------------------
|
|
101
|
+
def _operations(result_dir, code_dir, release, name):
|
|
102
|
+
from huggingface_hub import CommitOperationAdd
|
|
103
|
+
|
|
104
|
+
ops = []
|
|
105
|
+
base = f"leaderboard/{release}/{name}"
|
|
106
|
+
for f in sorted(Path(result_dir).rglob("*")):
|
|
107
|
+
if f.is_file():
|
|
108
|
+
ops.append(CommitOperationAdd(f"{base}/{f.relative_to(result_dir).as_posix()}", str(f)))
|
|
109
|
+
if code_dir is not None:
|
|
110
|
+
for f in sorted(Path(code_dir).rglob("*")):
|
|
111
|
+
if f.is_file() and "__pycache__" not in f.parts:
|
|
112
|
+
ops.append(CommitOperationAdd(f"associators/{name}/{f.relative_to(code_dir).as_posix()}", str(f)))
|
|
113
|
+
return ops
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _pr_text(sub):
|
|
117
|
+
a, run, s = sub["associator"], sub["run"], sub["scores"]["overall"]
|
|
118
|
+
return (f"**{a['name']} {a['version']}** by {a['authors']} ({a['method_type']}, training: {a['training']})\n\n"
|
|
119
|
+
f"{a.get('description', '')}\n\n"
|
|
120
|
+
f"- release `{run['release']}`, split `{run['split']}`, networks: {', '.join(run['networks'])}\n"
|
|
121
|
+
f"- windows: {run['n_windows']} ({'complete' if run['complete'] else 'partial'})\n"
|
|
122
|
+
f"- event F1 {s['event_f1']}, pick F1 {s['pick_f1']}, noise rejection {s['noise_rejection']}\n"
|
|
123
|
+
f"- paper: {a.get('paper') or '-'} · code: {a.get('code') or '-'}\n\n"
|
|
124
|
+
"Opened with `bepad submit`.")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def preview(result_dir, repo_id=REPO_ID):
|
|
128
|
+
"""The leaderboard tables of this submission's networks, with it inserted among the current entries."""
|
|
129
|
+
import shutil
|
|
130
|
+
import tempfile
|
|
131
|
+
|
|
132
|
+
sub = json.loads((Path(result_dir) / "submission.json").read_text())
|
|
133
|
+
name, release = sub["associator"]["name"], sub["run"]["release"]
|
|
134
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
135
|
+
tmp = Path(tmp)
|
|
136
|
+
try:
|
|
137
|
+
from huggingface_hub import snapshot_download
|
|
138
|
+
|
|
139
|
+
cur = Path(snapshot_download(repo_id, repo_type="dataset",
|
|
140
|
+
allow_patterns=[f"leaderboard/{release}/*/submission.json"]))
|
|
141
|
+
for f in (cur / "leaderboard").glob("*/*/submission.json"):
|
|
142
|
+
dst = tmp / f.parent.relative_to(cur / "leaderboard")
|
|
143
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
144
|
+
shutil.copy(f, dst / "submission.json")
|
|
145
|
+
note = "current leaderboard + your submission"
|
|
146
|
+
except Exception: # noqa: BLE001 -- offline / repo not published yet
|
|
147
|
+
note = "the published leaderboard could not be read, so only your submission is shown"
|
|
148
|
+
(tmp / release / name).mkdir(parents=True, exist_ok=True)
|
|
149
|
+
shutil.copy(Path(result_dir) / "submission.json", tmp / release / name / "submission.json")
|
|
150
|
+
text = build(tmp)
|
|
151
|
+
from .data import CORE_NAME
|
|
152
|
+
|
|
153
|
+
keep = [f"## {release} · {n}" for n in [CORE_NAME, "All networks", *sub["run"]["networks"]]]
|
|
154
|
+
blocks = [b for b in text.split("\n## ") if any(("## " + b).startswith(k) for k in keep)]
|
|
155
|
+
return f"Preview ({note}):\n\n" + "\n".join("## " + b for b in blocks)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def submit(result_dir, code_dir=None, repo_id=REPO_ID, root=None, dry_run=False, token=None):
|
|
159
|
+
"""Check a result folder, then open a pull request on the BEPAD Hugging Face repo.
|
|
160
|
+
|
|
161
|
+
Needs a Hugging Face account and ``hf auth login`` once. With ``dry_run`` (or
|
|
162
|
+
without a login) nothing is sent and the manual steps are printed instead.
|
|
163
|
+
"""
|
|
164
|
+
sub = json.loads((Path(result_dir) / "submission.json").read_text())
|
|
165
|
+
name, release = sub["associator"]["name"], sub["run"]["release"]
|
|
166
|
+
problems = verify(result_dir, root=root, code_dir=code_dir)
|
|
167
|
+
if problems:
|
|
168
|
+
print("Please fix these first:\n - " + "\n - ".join(problems))
|
|
169
|
+
return None
|
|
170
|
+
ops = _operations(result_dir, code_dir, release, name)
|
|
171
|
+
print(preview(result_dir, repo_id))
|
|
172
|
+
print(f"[submit] checks passed. The pull request adds {len(ops)} files:")
|
|
173
|
+
print(f" leaderboard/{release}/{name}/...")
|
|
174
|
+
if code_dir is not None:
|
|
175
|
+
print(f" associators/{name}/...")
|
|
176
|
+
manual = (f"\nTo submit by hand instead: open https://huggingface.co/datasets/{repo_id}/tree/main "
|
|
177
|
+
f"('Files and versions'), choose 'Contribute' -> 'Upload files', and upload the folder "
|
|
178
|
+
f"{result_dir} into leaderboard/{release}/{name}/"
|
|
179
|
+
+ (f" and {code_dir} into associators/{name}/" if code_dir else "")
|
|
180
|
+
+ ". For people who do not own the repository, the upload is opened as a pull request.")
|
|
181
|
+
if dry_run:
|
|
182
|
+
print("[submit] dry run: nothing sent." + manual)
|
|
183
|
+
return None
|
|
184
|
+
try:
|
|
185
|
+
from huggingface_hub import HfApi
|
|
186
|
+
|
|
187
|
+
api = HfApi(token=token)
|
|
188
|
+
user = api.whoami()["name"]
|
|
189
|
+
except Exception: # noqa: BLE001
|
|
190
|
+
print("[submit] you are not logged in to Hugging Face. Run `hf auth login` (once) and try again."
|
|
191
|
+
+ manual)
|
|
192
|
+
return None
|
|
193
|
+
info = api.create_commit(repo_id, repo_type="dataset", operations=ops, create_pr=True,
|
|
194
|
+
commit_message=f"Leaderboard: {name} {sub['associator']['version']} on {release}",
|
|
195
|
+
commit_description=_pr_text(sub))
|
|
196
|
+
print(f"[submit] pull request opened by {user}: {info.pr_url}")
|
|
197
|
+
return info.pr_url
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ----------------------------------------------------------------------
|
|
201
|
+
# Tables
|
|
202
|
+
# ----------------------------------------------------------------------
|
|
203
|
+
def collect(leaderboard_dir):
|
|
204
|
+
"""All submission.json files of a local leaderboard folder -> one row per (method, network)."""
|
|
205
|
+
rows = []
|
|
206
|
+
for f in sorted(Path(leaderboard_dir).glob("*/*/submission.json")):
|
|
207
|
+
sub = json.loads(f.read_text())
|
|
208
|
+
a, run = sub["associator"], sub["run"]
|
|
209
|
+
common = {"release": run["release"], "method": a["name"], "version": a["version"], "type": a["method_type"],
|
|
210
|
+
"training": a["training"], "complete": run["complete"], "sec_per_window": run["seconds_per_window"],
|
|
211
|
+
"paper": a.get("paper", ""), "code": a.get("code", ""), "date": sub["created"][:10]}
|
|
212
|
+
for net, s in sub["scores"]["by_network"].items():
|
|
213
|
+
rows.append({**common, "network": net, "n_windows": s.get("n_windows"),
|
|
214
|
+
**{k: s.get(k) for k in TABLE_COLS}})
|
|
215
|
+
return pd.DataFrame(rows)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _mean_over(g, networks):
|
|
219
|
+
"""One method's scores averaged over ``networks`` (every network weighs the same)."""
|
|
220
|
+
g = g[g.network.isin(networks)]
|
|
221
|
+
w = g.n_windows.fillna(0).to_numpy(float)
|
|
222
|
+
return {**g.iloc[0][["release", "method", "version", "type", "training", "paper", "code"]].to_dict(),
|
|
223
|
+
"complete": bool(g.complete.all()) and g.network.nunique() == len(networks),
|
|
224
|
+
"n_networks": int(g.network.nunique()), "n_windows": int(w.sum()),
|
|
225
|
+
"sec_per_window": float((g.sec_per_window * w).sum() / w.sum()) if w.sum() else float("nan"),
|
|
226
|
+
**{k: float(pd.to_numeric(g[k], errors="coerce").mean()) for k in TABLE_COLS}}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def views(df, core=None):
|
|
230
|
+
"""{release: {view: rows}}, ranked by event F1. Views: ``core`` (mean over the core networks),
|
|
231
|
+
``all`` (mean over every network of the release), then one per network. A method that did not
|
|
232
|
+
run every window of the view's networks is *partial*: listed, not ranked."""
|
|
233
|
+
from .data import CORE_NETWORKS
|
|
234
|
+
|
|
235
|
+
core = list(core or CORE_NETWORKS)
|
|
236
|
+
out = {}
|
|
237
|
+
for release, d in df.groupby("release"):
|
|
238
|
+
nets = sorted(d.network.unique())
|
|
239
|
+
rel = {}
|
|
240
|
+
for view, members in [("core", core), ("all", nets)] + [(n, [n]) for n in nets]:
|
|
241
|
+
rows = [_mean_over(g, members) for _, g in d.groupby(["method", "version"]) if g.network.isin(members).any()]
|
|
242
|
+
rows = sorted(rows, key=lambda r: (not r["complete"], -(r["event_f1"] if r["event_f1"] == r["event_f1"] else -1)))
|
|
243
|
+
rank = 0
|
|
244
|
+
for r in rows:
|
|
245
|
+
rank += r["complete"]
|
|
246
|
+
r["rank"] = rank if r["complete"] else "partial"
|
|
247
|
+
r["n_networks_view"] = len(members)
|
|
248
|
+
rel[view] = rows
|
|
249
|
+
out[release] = rel
|
|
250
|
+
return out
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
HEADER = ["#", "Method", "Type", "Training", "Event F1", "Pick F1", "Noise rej.", "Merge", "Split",
|
|
254
|
+
"Epicentre err. (km)", "Depth err. (km)", "s/window", "Windows", "Networks"]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _cells(r):
|
|
258
|
+
fmt = lambda v: "–" if v is None or v != v else f"{v:.3f}" # noqa: E731
|
|
259
|
+
fmt1 = lambda v: "–" if v is None or v != v else f"{v:.1f}" # noqa: E731
|
|
260
|
+
return [str(r["rank"]), f"{r['method']} {r['version']}", r["type"], r["training"], fmt(r["event_f1"]),
|
|
261
|
+
fmt(r["pick_f1"]), fmt(r["noise_rejection"]), fmt(r["merge_rate"]), fmt(r["split_rate"]),
|
|
262
|
+
fmt1(r["epi_median_km"]), fmt1(r["depth_median_km"]), f"{r['sec_per_window']:.2f}",
|
|
263
|
+
str(r["n_windows"]), f"{r['n_networks']}/{r['n_networks_view']}"]
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _title(view, core):
|
|
267
|
+
from .data import CORE_NAME, CORE_NETWORKS
|
|
268
|
+
|
|
269
|
+
return {"core": f"{CORE_NAME} test ({', '.join(core or CORE_NETWORKS)})", "all": "All networks"}.get(view, view)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _html(tables, core):
|
|
273
|
+
"""A self-contained table with a network filter (default: the core networks), for the docs."""
|
|
274
|
+
data = {rel: {v: {"title": _title(v, core), "rows": [
|
|
275
|
+
{"cells": _cells(r), "link": r["paper"] or r["code"]} for r in rows]} for v, rows in vs.items()}
|
|
276
|
+
for rel, vs in tables.items()}
|
|
277
|
+
return f"""<div class="bepad-lb">
|
|
278
|
+
<label>Release <select class="bepad-lb-release"></select></label>
|
|
279
|
+
<label>Networks <select class="bepad-lb-view"></select></label>
|
|
280
|
+
<table class="docutils"><thead><tr>{"".join(f"<th>{h}</th>" for h in HEADER)}</tr></thead><tbody></tbody></table>
|
|
281
|
+
</div>
|
|
282
|
+
<script>
|
|
283
|
+
(function () {{
|
|
284
|
+
const DATA = {json.dumps(data)};
|
|
285
|
+
const box = document.currentScript.previousElementSibling;
|
|
286
|
+
const rel = box.querySelector(".bepad-lb-release"), view = box.querySelector(".bepad-lb-view");
|
|
287
|
+
const body = box.querySelector("tbody");
|
|
288
|
+
const opt = (v, t) => {{ const o = document.createElement("option"); o.value = v; o.textContent = t; return o; }};
|
|
289
|
+
function fillViews() {{
|
|
290
|
+
view.innerHTML = "";
|
|
291
|
+
for (const [v, d] of Object.entries(DATA[rel.value])) view.appendChild(opt(v, d.title));
|
|
292
|
+
}}
|
|
293
|
+
function draw() {{
|
|
294
|
+
body.innerHTML = "";
|
|
295
|
+
for (const r of DATA[rel.value][view.value].rows) {{
|
|
296
|
+
const tr = document.createElement("tr");
|
|
297
|
+
r.cells.forEach((c, i) => {{
|
|
298
|
+
const td = document.createElement("td");
|
|
299
|
+
if (i === 1 && r.link) {{ const a = document.createElement("a"); a.href = r.link; a.textContent = c; td.appendChild(a); }}
|
|
300
|
+
else td.textContent = c;
|
|
301
|
+
tr.appendChild(td);
|
|
302
|
+
}});
|
|
303
|
+
body.appendChild(tr);
|
|
304
|
+
}}
|
|
305
|
+
}}
|
|
306
|
+
Object.keys(DATA).sort().reverse().forEach(k => rel.appendChild(opt(k, k)));
|
|
307
|
+
rel.onchange = () => {{ fillViews(); draw(); }};
|
|
308
|
+
view.onchange = draw;
|
|
309
|
+
fillViews(); draw();
|
|
310
|
+
}})();
|
|
311
|
+
</script>
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def build(leaderboard_dir, out_md=None, out_csv=None, out_html=None, core=None):
|
|
316
|
+
"""The tables: the core networks first, then all networks, then one per network (Markdown, for the
|
|
317
|
+
Hugging Face card), a flat CSV, and a filterable HTML table for the docs."""
|
|
318
|
+
df = collect(leaderboard_dir)
|
|
319
|
+
if df.empty:
|
|
320
|
+
text = "# BEPAD leaderboard\n\nNo submissions yet.\n"
|
|
321
|
+
html = "<p><em>No submissions yet.</em></p>"
|
|
322
|
+
else:
|
|
323
|
+
tables = views(df, core)
|
|
324
|
+
lines = ["# BEPAD leaderboard", "",
|
|
325
|
+
"Ranked by event F1 on the test split; the core and all-networks tables average the networks "
|
|
326
|
+
"(each network weighs the same). *Partial* runs (not every window of the networks) are "
|
|
327
|
+
"listed but not ranked. Location errors are medians over the matched earthquakes, against the "
|
|
328
|
+
"catalog; – = the method does not locate. Submit with `bepad submit` (see the documentation).",
|
|
329
|
+
""]
|
|
330
|
+
for release in sorted(tables, reverse=True):
|
|
331
|
+
for view, rows in tables[release].items():
|
|
332
|
+
lines += [f"## {release} · {_title(view, core)}", "", "| " + " | ".join(HEADER) + " |",
|
|
333
|
+
"|" + "---|" * len(HEADER)]
|
|
334
|
+
for r in rows:
|
|
335
|
+
c = _cells(r)
|
|
336
|
+
link = r["paper"] or r["code"]
|
|
337
|
+
c[1] = f"[{c[1]}]({link})" if link else c[1]
|
|
338
|
+
lines.append("| " + " | ".join(c) + " |")
|
|
339
|
+
lines.append("")
|
|
340
|
+
text = "\n".join(lines)
|
|
341
|
+
html = _html(tables, core)
|
|
342
|
+
if out_md:
|
|
343
|
+
Path(out_md).write_text(text)
|
|
344
|
+
if out_csv and not df.empty:
|
|
345
|
+
df.to_csv(out_csv, index=False)
|
|
346
|
+
if out_html:
|
|
347
|
+
Path(out_html).write_text(html)
|
|
348
|
+
return text
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# ----------------------------------------------------------------------
|
|
352
|
+
# Maintainer
|
|
353
|
+
# ----------------------------------------------------------------------
|
|
354
|
+
def review(repo_id=REPO_ID, root=None, token=None):
|
|
355
|
+
"""List open leaderboard pull requests and re-check each one (maintainer)."""
|
|
356
|
+
from huggingface_hub import HfApi, snapshot_download
|
|
357
|
+
|
|
358
|
+
api = HfApi(token=token)
|
|
359
|
+
for d in api.get_repo_discussions(repo_id, repo_type="dataset", discussion_type="pull_request",
|
|
360
|
+
discussion_status="open"):
|
|
361
|
+
path = Path(snapshot_download(repo_id, repo_type="dataset", revision=d.git_reference,
|
|
362
|
+
allow_patterns=["leaderboard/*", "associators/*"], token=token))
|
|
363
|
+
new = [p for p in (path / "leaderboard").glob("*/*/submission.json")]
|
|
364
|
+
print(f"PR #{d.num} by {d.author}: {d.title}")
|
|
365
|
+
for sub in new:
|
|
366
|
+
probs = verify(sub.parent, root=root)
|
|
367
|
+
print(f" {sub.parent.relative_to(path)}: " + ("OK" if not probs else "; ".join(probs)))
|
|
368
|
+
print(f" merge: HfApi().merge_pull_request('{repo_id}', {d.num}, repo_type='dataset')")
|