Spaces:
Running
Running
Tengo Gzirishvili commited on
Commit ·
a326839
1
Parent(s): 5194826
Surface per-assay failures in admin endpoints instead of silently dropping them
Browse filesBoth /api/admin/run-benchmarks and /api/admin/seed-commons caught (or in
seed-commons' case, didn't even catch) per-assay errors with no way to see
why a dataset went missing from the result short of server log access. A
real run against the 3 bundled fixtures came back with only 1 dataset and
no indication why the other 2 were dropped. Now both endpoints return a
"failed": [{name, error}] list alongside any partial success, and
seed-commons' CSV-parsing loop is wrapped so one bad fixture degrades
instead of 500ing the whole request.
- dee/server.py +26 -9
- tests/test_admin_benchmarks_seed.py +62 -0
dee/server.py
CHANGED
|
@@ -2782,22 +2782,26 @@ def create_app() -> Flask:
|
|
| 2782 |
|
| 2783 |
scorer = _scoring.get_scorer(model)
|
| 2784 |
results = []
|
|
|
|
| 2785 |
for a in assays:
|
|
|
|
| 2786 |
try:
|
| 2787 |
csv_path = fixtures_dir / a["csv"]
|
| 2788 |
recs = _parse_dms(csv_path.read_text(encoding="utf-8"))
|
| 2789 |
if not recs:
|
|
|
|
| 2790 |
continue
|
| 2791 |
labels = [lab for (lab, _v) in recs]
|
| 2792 |
measured = [v for (_lab, v) in recs]
|
| 2793 |
scores_df = _scoring.score_guarded(scorer, a["sequence"])
|
| 2794 |
predicted = _bm.predict_additive(scores_df, labels)
|
| 2795 |
results.append(_bm.evaluate_dataset(
|
| 2796 |
-
|
| 2797 |
predicted, measured, source=a.get("source", ""),
|
| 2798 |
))
|
| 2799 |
-
except Exception: # noqa: BLE001 — one bad assay must not kill the run
|
| 2800 |
-
logger.exception("benchmark assay failed: %s",
|
|
|
|
| 2801 |
|
| 2802 |
out = {
|
| 2803 |
"generated_at": _dt3.datetime.now(_dt3.timezone.utc).isoformat(),
|
|
@@ -2807,6 +2811,8 @@ def create_app() -> Flask:
|
|
| 2807 |
"summary": _bm.summarize(results),
|
| 2808 |
"datasets": [r.as_dict() for r in results],
|
| 2809 |
}
|
|
|
|
|
|
|
| 2810 |
out_path = Path(__file__).resolve().parent / "data" / "benchmarks.json"
|
| 2811 |
try:
|
| 2812 |
out_path.write_text(json.dumps(out, indent=2), encoding="utf-8")
|
|
@@ -2840,11 +2846,19 @@ def create_app() -> Flask:
|
|
| 2840 |
return jsonify({"ok": False, "error": "manifest unreadable"}), 500
|
| 2841 |
|
| 2842 |
assays = []
|
|
|
|
| 2843 |
for a in manifest:
|
| 2844 |
-
|
| 2845 |
-
|
| 2846 |
-
|
| 2847 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2848 |
|
| 2849 |
try:
|
| 2850 |
rows = _seed_rows(assays) # enforces the effective-date gate
|
|
@@ -2854,8 +2868,11 @@ def create_app() -> Flask:
|
|
| 2854 |
|
| 2855 |
result = _auth.replace_mutation_priors(rows)
|
| 2856 |
_GLOBAL_PRIOR_CACHE["data"] = None # bust so the next design sees it immediately
|
| 2857 |
-
|
| 2858 |
-
|
|
|
|
|
|
|
|
|
|
| 2859 |
|
| 2860 |
@app.post("/api/admin/rebuild-priors")
|
| 2861 |
def admin_rebuild_priors() -> Response:
|
|
|
|
| 2782 |
|
| 2783 |
scorer = _scoring.get_scorer(model)
|
| 2784 |
results = []
|
| 2785 |
+
failed = []
|
| 2786 |
for a in assays:
|
| 2787 |
+
name = a.get("name", a.get("protein", "?"))
|
| 2788 |
try:
|
| 2789 |
csv_path = fixtures_dir / a["csv"]
|
| 2790 |
recs = _parse_dms(csv_path.read_text(encoding="utf-8"))
|
| 2791 |
if not recs:
|
| 2792 |
+
failed.append({"name": name, "error": "no parseable records in csv"})
|
| 2793 |
continue
|
| 2794 |
labels = [lab for (lab, _v) in recs]
|
| 2795 |
measured = [v for (_lab, v) in recs]
|
| 2796 |
scores_df = _scoring.score_guarded(scorer, a["sequence"])
|
| 2797 |
predicted = _bm.predict_additive(scores_df, labels)
|
| 2798 |
results.append(_bm.evaluate_dataset(
|
| 2799 |
+
name, a.get("protein", ""),
|
| 2800 |
predicted, measured, source=a.get("source", ""),
|
| 2801 |
))
|
| 2802 |
+
except Exception as exc: # noqa: BLE001 — one bad assay must not kill the run
|
| 2803 |
+
logger.exception("benchmark assay failed: %s", name)
|
| 2804 |
+
failed.append({"name": name, "error": f"{type(exc).__name__}: {exc}"})
|
| 2805 |
|
| 2806 |
out = {
|
| 2807 |
"generated_at": _dt3.datetime.now(_dt3.timezone.utc).isoformat(),
|
|
|
|
| 2811 |
"summary": _bm.summarize(results),
|
| 2812 |
"datasets": [r.as_dict() for r in results],
|
| 2813 |
}
|
| 2814 |
+
if failed:
|
| 2815 |
+
out["failed"] = failed
|
| 2816 |
out_path = Path(__file__).resolve().parent / "data" / "benchmarks.json"
|
| 2817 |
try:
|
| 2818 |
out_path.write_text(json.dumps(out, indent=2), encoding="utf-8")
|
|
|
|
| 2846 |
return jsonify({"ok": False, "error": "manifest unreadable"}), 500
|
| 2847 |
|
| 2848 |
assays = []
|
| 2849 |
+
failed = []
|
| 2850 |
for a in manifest:
|
| 2851 |
+
name = a.get("name", a.get("protein", "?"))
|
| 2852 |
+
try:
|
| 2853 |
+
csv_path = fixtures_dir / a["csv"]
|
| 2854 |
+
recs = _parse_dms(csv_path.read_text(encoding="utf-8"))
|
| 2855 |
+
if recs:
|
| 2856 |
+
assays.append((name, recs))
|
| 2857 |
+
else:
|
| 2858 |
+
failed.append({"name": name, "error": "no parseable records in csv"})
|
| 2859 |
+
except Exception as exc: # noqa: BLE001 — one bad assay must not kill the seed
|
| 2860 |
+
logger.exception("seed-commons assay failed: %s", name)
|
| 2861 |
+
failed.append({"name": name, "error": f"{type(exc).__name__}: {exc}"})
|
| 2862 |
|
| 2863 |
try:
|
| 2864 |
rows = _seed_rows(assays) # enforces the effective-date gate
|
|
|
|
| 2868 |
|
| 2869 |
result = _auth.replace_mutation_priors(rows)
|
| 2870 |
_GLOBAL_PRIOR_CACHE["data"] = None # bust so the next design sees it immediately
|
| 2871 |
+
out = {"ok": bool(result.get("ok")), "substitutions": len(rows),
|
| 2872 |
+
"contributing_assays": len(assays)}
|
| 2873 |
+
if failed:
|
| 2874 |
+
out["failed"] = failed
|
| 2875 |
+
return jsonify(out)
|
| 2876 |
|
| 2877 |
@app.post("/api/admin/rebuild-priors")
|
| 2878 |
def admin_rebuild_priors() -> Response:
|
tests/test_admin_benchmarks_seed.py
CHANGED
|
@@ -117,6 +117,44 @@ def test_run_benchmarks_missing_fixtures_404(client, monkeypatch, tmp_path):
|
|
| 117 |
assert r.status_code == 404
|
| 118 |
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
# --------------------------------------------------------------------------- #
|
| 121 |
# /api/admin/seed-commons — real bundled fixture, mocked Supabase write
|
| 122 |
# --------------------------------------------------------------------------- #
|
|
@@ -147,6 +185,30 @@ def test_seed_commons_against_real_fixture(client, monkeypatch):
|
|
| 147 |
assert ">" in row["substitution"]
|
| 148 |
|
| 149 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
def test_seed_commons_gated_before_effective_date(client, monkeypatch):
|
| 151 |
_with_admin(monkeypatch)
|
| 152 |
import datetime as _dt
|
|
|
|
| 117 |
assert r.status_code == 404
|
| 118 |
|
| 119 |
|
| 120 |
+
def test_run_benchmarks_surfaces_per_assay_failure(client, monkeypatch, tmp_path):
|
| 121 |
+
"""One assay's scoring blows up (e.g. a real ESM error on the Space) — the
|
| 122 |
+
other two must still complete, and the failure must be visible in the
|
| 123 |
+
response itself (name + error), not just swallowed into a shorter
|
| 124 |
+
'datasets' list with no explanation. This is what a real diagnosis of
|
| 125 |
+
'why did only 1 of 3 datasets come back' should read directly off the
|
| 126 |
+
curl output instead of requiring server-log access."""
|
| 127 |
+
_with_admin(monkeypatch)
|
| 128 |
+
real_fixtures = _fixtures_dir()
|
| 129 |
+
manifest = json.loads((real_fixtures / "manifest.json").read_text(encoding="utf-8"))
|
| 130 |
+
assert len(manifest) >= 3
|
| 131 |
+
|
| 132 |
+
def flaky_score_guarded(scorer, seq):
|
| 133 |
+
if len(seq) > 500: # the two longer real fixtures (PABP 577aa, DLG4 724aa)
|
| 134 |
+
raise RuntimeError("boom: simulated real scoring failure")
|
| 135 |
+
return pd.DataFrame([{"position": 0, "wt_aa": seq[0], "mut_aa": "Z", "delta_ll": 0.1}])
|
| 136 |
+
|
| 137 |
+
monkeypatch.setattr(server._scoring, "get_scorer", lambda *a, **kw: "the-scorer")
|
| 138 |
+
monkeypatch.setattr(server._scoring, "score_guarded", flaky_score_guarded)
|
| 139 |
+
monkeypatch.setattr(server, "__file__", str(tmp_path / "server.py"))
|
| 140 |
+
(tmp_path / "data").mkdir()
|
| 141 |
+
import shutil
|
| 142 |
+
shutil.copytree(real_fixtures, tmp_path / "data" / "dms_fixtures")
|
| 143 |
+
|
| 144 |
+
r = client.post("/api/admin/run-benchmarks",
|
| 145 |
+
headers={"X-Admin-Token": ADMIN_TOKEN}, json={"model": "small"})
|
| 146 |
+
assert r.status_code == 200
|
| 147 |
+
body = r.get_json()
|
| 148 |
+
long_assays = [a["name"] for a in manifest if len(a["sequence"]) > 500]
|
| 149 |
+
assert len(long_assays) == 2, "expected exactly PABP + DLG4 to be >500aa"
|
| 150 |
+
assert body["summary"]["n_datasets"] == len(manifest) - len(long_assays)
|
| 151 |
+
assert "failed" in body and len(body["failed"]) == len(long_assays)
|
| 152 |
+
failed_names = {f["name"] for f in body["failed"]}
|
| 153 |
+
assert failed_names == set(long_assays)
|
| 154 |
+
for f in body["failed"]:
|
| 155 |
+
assert "boom: simulated real scoring failure" in f["error"]
|
| 156 |
+
|
| 157 |
+
|
| 158 |
# --------------------------------------------------------------------------- #
|
| 159 |
# /api/admin/seed-commons — real bundled fixture, mocked Supabase write
|
| 160 |
# --------------------------------------------------------------------------- #
|
|
|
|
| 185 |
assert ">" in row["substitution"]
|
| 186 |
|
| 187 |
|
| 188 |
+
def test_seed_commons_survives_one_bad_csv(client, monkeypatch, tmp_path):
|
| 189 |
+
"""A single unreadable/corrupt fixture CSV must not 500 the whole request
|
| 190 |
+
(there was previously no try/except around this loop at all) — it should
|
| 191 |
+
degrade to the other assays and report the failure by name."""
|
| 192 |
+
_with_admin(monkeypatch)
|
| 193 |
+
real_fixtures = _fixtures_dir()
|
| 194 |
+
monkeypatch.setattr(server._auth, "replace_mutation_priors", lambda rows: {"ok": True})
|
| 195 |
+
monkeypatch.setattr(server, "__file__", str(tmp_path / "server.py"))
|
| 196 |
+
(tmp_path / "data").mkdir()
|
| 197 |
+
import shutil
|
| 198 |
+
dest = tmp_path / "data" / "dms_fixtures"
|
| 199 |
+
shutil.copytree(real_fixtures, dest)
|
| 200 |
+
manifest = json.loads((dest / "manifest.json").read_text(encoding="utf-8"))
|
| 201 |
+
broken_name = manifest[0]["name"]
|
| 202 |
+
(dest / manifest[0]["csv"]).unlink() # simulate a missing/corrupt fixture file
|
| 203 |
+
|
| 204 |
+
r = client.post("/api/admin/seed-commons", headers={"X-Admin-Token": ADMIN_TOKEN})
|
| 205 |
+
assert r.status_code == 200
|
| 206 |
+
body = r.get_json()
|
| 207 |
+
assert body["contributing_assays"] == len(manifest) - 1
|
| 208 |
+
assert "failed" in body and len(body["failed"]) == 1
|
| 209 |
+
assert body["failed"][0]["name"] == broken_name
|
| 210 |
+
|
| 211 |
+
|
| 212 |
def test_seed_commons_gated_before_effective_date(client, monkeypatch):
|
| 213 |
_with_admin(monkeypatch)
|
| 214 |
import datetime as _dt
|