Deploy the results API
Browse files- README.md +7 -4
- app.py +97 -17
- tests/conftest.py +34 -1
- tests/test_app.py +79 -7
README.md
CHANGED
|
@@ -16,18 +16,21 @@ The results API behind the leaderboard pop-up of the Steerability Challenge site
|
|
| 16 |
|
| 17 |
The index of runs is `leaderboard/runs.json` in the archive, which `challenge-leaderboard` rebuilds on every render: `{"schema", "latest", "runs": [{"name", "label", "layer", "date", "deadline", "n_ranked", "n_baselines"}, ...]}`. A run's site board is `leaderboard/<run>/site.json`, `{"schema", "run", "rows"}` with the same `run` block and one row per ranked submission and baseline.
|
| 18 |
|
|
|
|
|
|
|
| 19 |
| Request | Response |
|
| 20 |
|---|---|
|
| 21 |
| `GET /healthz` | `200 {"run", "date"}` of the index's latest run, both null before one is published. The pop-up also pings it to wake the Space. |
|
| 22 |
-
| `GET /runs` | `200` with the index verbatim, or `503 {"error": "no_board"}` without
|
| 23 |
| `GET /board/<run>` | `200` with the run's site board verbatim, or `404 {"error": "not_found"}` for a run that is not a run name, is not in the index, or has no site board. No authentication. |
|
| 24 |
| `GET /me[?run=<run>]` with `Authorization: Bearer <token>` | `200` with `{"role": "participant", "run", "teams", "missing"}` or `{"role": "organizer", "run", "board", "teams", "pull", "progress", "failures"}` for the run, by default the index's latest; `run` is the site board's `run` block. |
|
|
|
|
| 25 |
| | `401 {"error": "invalid_token"}` when Hugging Face does not accept the token (checked before the run). |
|
| 26 |
-
| | `404 {"error": "not_found"}` for a `run` that is not a run name or is
|
| 27 |
| | `503 {"error": "no_board"}` before a board is published. |
|
| 28 |
| any of the above | `503 {"error": "archive_unavailable"}` when the archive cannot be read. |
|
| 29 |
|
| 30 |
-
HTTPS only, no cookies, `GET` and the `Authorization` header only. CORS allows `ALLOWED_ORIGINS` and, when set, `ALLOWED_ORIGIN_REGEX`. The hf.space proxy in front of the app answers CORS requests from any origin by design, and browsers do not enforce this list there. Access rests on the bearer token, which another site cannot obtain, and the app refuses no request by its `Origin`. Logs give the viewer's handle, role, run, and status code, never a token or a file's contents.
|
| 31 |
|
| 32 |
## Settings at deploy time
|
| 33 |
|
|
@@ -47,4 +50,4 @@ Optional: `USERINFO_URL` (default `https://huggingface.co/oauth/userinfo`) and `
|
|
| 47 |
|
| 48 |
## Tests
|
| 49 |
|
| 50 |
-
From the repository that contains this Space: `python -m pytest spaces/leaderboard-api/tests`. They run against a stub userinfo server on a loopback port and a fixture archive directory with two runs.
|
|
|
|
| 16 |
|
| 17 |
The index of runs is `leaderboard/runs.json` in the archive, which `challenge-leaderboard` rebuilds on every render: `{"schema", "latest", "runs": [{"name", "label", "layer", "date", "deadline", "n_ranked", "n_baselines"}, ...]}`. A run's site board is `leaderboard/<run>/site.json`, `{"schema", "run", "rows"}` with the same `run` block and one row per ranked submission and baseline.
|
| 18 |
|
| 19 |
+
The pending run is the run being evaluated between its intake and the publication of its board: the `run` block `{"name", "label", "layer", "pull_finished", "n_pulled", "models"}` of `leaderboard/pending.json` (written by `challenge-leaderboard --pending`, cleared to null by the render that publishes the board), unless the index lists that run. It has no site board. Each organization of its pull manifest has a pending file `leaderboard/<run>/pending/<org>.json`, `{"schema", "run", "org", "team", "entry"}` with the entry block of a team file, and the API adds `evaluation`, the status of the organization's submission per base model (`not_started`, `in_progress`, `completed`, or `error`) read from the run's `status.json` on every request. Nothing else of the status file reaches a viewer.
|
| 20 |
+
|
| 21 |
| Request | Response |
|
| 22 |
|---|---|
|
| 23 |
| `GET /healthz` | `200 {"run", "date"}` of the index's latest run, both null before one is published. The pop-up also pings it to wake the Space. |
|
| 24 |
+
| `GET /runs` | `200` with the index verbatim plus `"pending": <the pending run's block or null>`, or `503 {"error": "no_board"}` without an index. No authentication. |
|
| 25 |
| `GET /board/<run>` | `200` with the run's site board verbatim, or `404 {"error": "not_found"}` for a run that is not a run name, is not in the index, or has no site board. No authentication. |
|
| 26 |
| `GET /me[?run=<run>]` with `Authorization: Bearer <token>` | `200` with `{"role": "participant", "run", "teams", "missing"}` or `{"role": "organizer", "run", "board", "teams", "pull", "progress", "failures"}` for the run, by default the index's latest; `run` is the site board's `run` block. |
|
| 27 |
+
| `GET /me?run=<pending run>` | `200` with `{"role": "participant", "pending": true, "run", "teams", "missing"}`, `teams` being the viewer's organizations' pending files each with `evaluation`, or `{"role": "organizer", "pending": true, "run", "board": null, "teams", "pull", "progress", "failures"}` with every pending file. The published view has no `pending` key. |
|
| 28 |
| | `401 {"error": "invalid_token"}` when Hugging Face does not accept the token (checked before the run). |
|
| 29 |
+
| | `404 {"error": "not_found"}` for a `run` that is not a run name, or is neither in the index nor pending. |
|
| 30 |
| | `503 {"error": "no_board"}` before a board is published. |
|
| 31 |
| any of the above | `503 {"error": "archive_unavailable"}` when the archive cannot be read. |
|
| 32 |
|
| 33 |
+
HTTPS only, no cookies, `GET` and the `Authorization` header only. CORS allows `ALLOWED_ORIGINS` and, when set, `ALLOWED_ORIGIN_REGEX`. The hf.space proxy in front of the app answers CORS requests from any origin by design, and browsers do not enforce this list there. Access rests on the bearer token, which another site cannot obtain, and the app refuses no request by its `Origin`. Logs give the viewer's handle, role, run, whether it is pending, and status code, never a token or a file's contents.
|
| 34 |
|
| 35 |
## Settings at deploy time
|
| 36 |
|
|
|
|
| 50 |
|
| 51 |
## Tests
|
| 52 |
|
| 53 |
+
From the repository that contains this Space: `python -m pytest spaces/leaderboard-api/tests`. They run against a stub userinfo server on a loopback port and a fixture archive directory with two published runs and a pending one.
|
app.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
| 1 |
"""The results API behind the competition site's leaderboard pop-up, served from a public Hugging Face Space.
|
| 2 |
|
| 3 |
The index of runs is `leaderboard/runs.json` in the archive (`RUNS`), which `challenge-leaderboard` rebuilds on every
|
| 4 |
-
render
|
|
|
|
|
|
|
| 5 |
and `GET /board/<run>` gives a run's site board (`leaderboard/<run>/site.json`), each without authentication.
|
| 6 |
`GET /me` gives a signed-in viewer the files they may see for one run (`?run=<run>`, by default the index's
|
| 7 |
latest): a participant gets the team file of each of their organizations, and an organizer gets every file of the
|
| 8 |
-
run.
|
| 9 |
-
|
|
|
|
| 10 |
|
| 11 |
Settings (environment):
|
| 12 |
- `HF_TOKEN` (a Space secret): the token that reads the archive dataset. It is read from the environment only, a
|
|
@@ -19,7 +22,8 @@ Settings (environment):
|
|
| 19 |
- `USERINFO_URL`: the OAuth userinfo endpoint, `https://huggingface.co/oauth/userinfo` by default.
|
| 20 |
- `ARCHIVE_DIR`: a local directory read in place of the archive dataset (tests and local previews).
|
| 21 |
|
| 22 |
-
Logs give the viewer's handle, role, run, and status code only, never a token or a
|
|
|
|
| 23 |
"""
|
| 24 |
from __future__ import annotations
|
| 25 |
|
|
@@ -42,6 +46,9 @@ logger = logging.getLogger("leaderboard-api")
|
|
| 42 |
|
| 43 |
USERINFO_URL = "https://huggingface.co/oauth/userinfo"
|
| 44 |
RUNS = "leaderboard/runs.json" # the index of the runs with a site board, written by `challenge-leaderboard`
|
|
|
|
|
|
|
|
|
|
| 45 |
CACHE_SECONDS = 60 # how long a userinfo answer and a directory listing are reused
|
| 46 |
USERINFO_TIMEOUT_SECONDS = 15
|
| 47 |
HANDLE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,95}$") # a Hugging Face user or organization handle
|
|
@@ -155,6 +162,34 @@ def site_board(archive, run: str) -> dict | None:
|
|
| 155 |
return board if isinstance(board, dict) else None
|
| 156 |
|
| 157 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
def viewer_role(info: dict, settings: dict) -> tuple[str, list[str]]:
|
| 159 |
"""`organizer` for a handle in `ORGANIZER_USERS` or a member of `ORGANIZER_ORG`, else `participant`, with the
|
| 160 |
viewer's organization handles in the order the userinfo lists them."""
|
|
@@ -194,6 +229,41 @@ def organizer_payload(archive, entry: dict) -> dict:
|
|
| 194 |
"failures": _text(archive, f"{run}/failures.md")}
|
| 195 |
|
| 196 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
def create_app(settings: dict | None = None) -> FastAPI:
|
| 198 |
"""The API with CORS for the configured origins, `GET` and the `Authorization` header only, and no cookies."""
|
| 199 |
settings = settings_from_env() if settings is None else settings
|
|
@@ -251,8 +321,12 @@ def create_app(settings: dict | None = None) -> FastAPI:
|
|
| 251 |
if index is None:
|
| 252 |
logger.info("runs: 503")
|
| 253 |
return JSONResponse({"error": "no_board"}, status_code=503)
|
| 254 |
-
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
@app.get("/board/{run}")
|
| 258 |
def board(run: str):
|
|
@@ -278,21 +352,27 @@ def create_app(settings: dict | None = None) -> FastAPI:
|
|
| 278 |
role, orgs = viewer_role(info, settings)
|
| 279 |
try:
|
| 280 |
index = runs_index(archive)
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
except HfHubHTTPError as exc: # e.g., no read token yet
|
| 292 |
logger.error("me: %s %s %s 503; the archive could not be read (%s)", handle, role, run or "-",
|
| 293 |
type(exc).__name__)
|
| 294 |
return JSONResponse({"error": "archive_unavailable"}, status_code=503)
|
| 295 |
-
logger.info("me: %s %s %s 200", handle, role, name)
|
| 296 |
return payload
|
| 297 |
|
| 298 |
return app
|
|
|
|
| 1 |
"""The results API behind the competition site's leaderboard pop-up, served from a public Hugging Face Space.
|
| 2 |
|
| 3 |
The index of runs is `leaderboard/runs.json` in the archive (`RUNS`), which `challenge-leaderboard` rebuilds on every
|
| 4 |
+
render, and the pending run (the run being evaluated between its intake and the publication of its board) is the
|
| 5 |
+
`run` of `leaderboard/pending.json` (`PENDING`) when the index does not list it. `GET /healthz` gives the latest
|
| 6 |
+
run and its date (both null without an index), `GET /runs` gives the index with the pending run under `pending`,
|
| 7 |
and `GET /board/<run>` gives a run's site board (`leaderboard/<run>/site.json`), each without authentication.
|
| 8 |
`GET /me` gives a signed-in viewer the files they may see for one run (`?run=<run>`, by default the index's
|
| 9 |
latest): a participant gets the team file of each of their organizations, and an organizer gets every file of the
|
| 10 |
+
run. For the pending run it gives the pending files instead (`leaderboard/<run>/pending/<org>.json`), each with
|
| 11 |
+
the evaluation's progress per base model read from the run's `status.json` (`evaluation`). The server decides the
|
| 12 |
+
viewer's handle, organizations, and role from their OAuth token alone; a request names no organization.
|
| 13 |
|
| 14 |
Settings (environment):
|
| 15 |
- `HF_TOKEN` (a Space secret): the token that reads the archive dataset. It is read from the environment only, a
|
|
|
|
| 22 |
- `USERINFO_URL`: the OAuth userinfo endpoint, `https://huggingface.co/oauth/userinfo` by default.
|
| 23 |
- `ARCHIVE_DIR`: a local directory read in place of the archive dataset (tests and local previews).
|
| 24 |
|
| 25 |
+
Logs give the viewer's handle, role, run, whether it is pending, and the status code only, never a token or a
|
| 26 |
+
file's contents.
|
| 27 |
"""
|
| 28 |
from __future__ import annotations
|
| 29 |
|
|
|
|
| 46 |
|
| 47 |
USERINFO_URL = "https://huggingface.co/oauth/userinfo"
|
| 48 |
RUNS = "leaderboard/runs.json" # the index of the runs with a site board, written by `challenge-leaderboard`
|
| 49 |
+
PENDING = "leaderboard/pending.json" # the run being evaluated, written by `challenge-leaderboard --pending`
|
| 50 |
+
STATUS_FILE = "status.json" # `<run>/status.json`, the evaluation's record of the run's work, one entry per key
|
| 51 |
+
EVALUATION_STATUSES = ("not_started", "in_progress", "completed", "error") # the status codes of its entries
|
| 52 |
CACHE_SECONDS = 60 # how long a userinfo answer and a directory listing are reused
|
| 53 |
USERINFO_TIMEOUT_SECONDS = 15
|
| 54 |
HANDLE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,95}$") # a Hugging Face user or organization handle
|
|
|
|
| 162 |
return board if isinstance(board, dict) else None
|
| 163 |
|
| 164 |
|
| 165 |
+
def pending_run(archive, index: dict | None) -> dict | None:
|
| 166 |
+
"""The pending run's block, i.e., the `run` of `PENDING`, or None without one, for a malformed one (`run` not a
|
| 167 |
+
mapping, `name` not matching `RUN_NAME`, `models` not a list of strings), or when the index lists the run, since
|
| 168 |
+
a run in the index has been published whatever the pending index says."""
|
| 169 |
+
pending = _json(archive, PENDING)
|
| 170 |
+
run = pending.get("run") if isinstance(pending, dict) else None
|
| 171 |
+
if not isinstance(run, dict) or not RUN_NAME.fullmatch(str(run.get("name"))):
|
| 172 |
+
return None
|
| 173 |
+
if not isinstance(run.get("models"), list) or not all(isinstance(model, str) for model in run["models"]):
|
| 174 |
+
return None
|
| 175 |
+
if index is not None and index_entry(index, run["name"]) is not None:
|
| 176 |
+
return None
|
| 177 |
+
return run
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def evaluation(status: dict | None, org: str, models: list[str]) -> dict[str, str]:
|
| 181 |
+
"""The evaluation's progress of one organization's submission per base model, in the order of `models`: the
|
| 182 |
+
status code of the `status.json` entry `submission-<org>/<model slug>` (the ref with `/` replaced by `__`), and
|
| 183 |
+
`not_started` without the file, the entry, or a code of `EVALUATION_STATUSES`. The code is the only field read
|
| 184 |
+
from an entry, and no other entry is read."""
|
| 185 |
+
codes = {}
|
| 186 |
+
for model in models:
|
| 187 |
+
entry = status.get(f"submission-{org}/{model.replace('/', '__')}") if isinstance(status, dict) else None
|
| 188 |
+
code = entry.get("status") if isinstance(entry, dict) else None
|
| 189 |
+
codes[model] = code if code in EVALUATION_STATUSES else "not_started"
|
| 190 |
+
return codes
|
| 191 |
+
|
| 192 |
+
|
| 193 |
def viewer_role(info: dict, settings: dict) -> tuple[str, list[str]]:
|
| 194 |
"""`organizer` for a handle in `ORGANIZER_USERS` or a member of `ORGANIZER_ORG`, else `participant`, with the
|
| 195 |
viewer's organization handles in the order the userinfo lists them."""
|
|
|
|
| 229 |
"failures": _text(archive, f"{run}/failures.md")}
|
| 230 |
|
| 231 |
|
| 232 |
+
def _with_evaluation(archive, run: dict, org: str, team: dict | None, status: dict | None) -> dict | None:
|
| 233 |
+
"""A pending file with the organization's `evaluation` block added, or None without the file."""
|
| 234 |
+
if not isinstance(team, dict):
|
| 235 |
+
return None
|
| 236 |
+
return {**team, "evaluation": evaluation(status, org, run["models"])}
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
def pending_participant_payload(archive, run: dict, orgs: list[str]) -> dict:
|
| 240 |
+
"""The pending run's `run` block, the pending file of each of the viewer's organizations that has one, in the
|
| 241 |
+
userinfo's order, each with its `evaluation` block from the run's `status.json` (read on every request), and
|
| 242 |
+
the organizations without one under `missing`."""
|
| 243 |
+
status = _json(archive, f"{run['name']}/{STATUS_FILE}")
|
| 244 |
+
teams, missing = [], []
|
| 245 |
+
for org in orgs:
|
| 246 |
+
team = _with_evaluation(archive, run, org, _json(archive, f"leaderboard/{run['name']}/pending/{org}.json"),
|
| 247 |
+
status)
|
| 248 |
+
(missing.append(org) if team is None else teams.append(team))
|
| 249 |
+
return {"role": "participant", "pending": True, "run": run, "teams": teams, "missing": missing}
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def pending_organizer_payload(archive, run: dict) -> dict:
|
| 253 |
+
"""The pending run's `run` block, a null board, every pending file of the run with its `evaluation` block, the
|
| 254 |
+
pull manifest, and the run's progress and failures files."""
|
| 255 |
+
name = run["name"]
|
| 256 |
+
status = _json(archive, f"{name}/{STATUS_FILE}")
|
| 257 |
+
teams = {}
|
| 258 |
+
for path in archive.listing(f"leaderboard/{name}/pending"):
|
| 259 |
+
if path.endswith(".json"):
|
| 260 |
+
org = Path(path).stem
|
| 261 |
+
teams[org] = _with_evaluation(archive, run, org, _json(archive, path), status)
|
| 262 |
+
return {"role": "organizer", "pending": True, "run": run, "board": None, "teams": teams,
|
| 263 |
+
"pull": _json(archive, f"{name}/manifest.json"), "progress": _text(archive, f"{name}/progress.txt"),
|
| 264 |
+
"failures": _text(archive, f"{name}/failures.md")}
|
| 265 |
+
|
| 266 |
+
|
| 267 |
def create_app(settings: dict | None = None) -> FastAPI:
|
| 268 |
"""The API with CORS for the configured origins, `GET` and the `Authorization` header only, and no cookies."""
|
| 269 |
settings = settings_from_env() if settings is None else settings
|
|
|
|
| 321 |
if index is None:
|
| 322 |
logger.info("runs: 503")
|
| 323 |
return JSONResponse({"error": "no_board"}, status_code=503)
|
| 324 |
+
try:
|
| 325 |
+
pending = pending_run(archive, index)
|
| 326 |
+
except HfHubHTTPError as exc:
|
| 327 |
+
return unavailable("runs", exc)
|
| 328 |
+
logger.info("runs: 200 (pending %s)", pending["name"] if pending else "-")
|
| 329 |
+
return {**index, "pending": pending}
|
| 330 |
|
| 331 |
@app.get("/board/{run}")
|
| 332 |
def board(run: str):
|
|
|
|
| 352 |
role, orgs = viewer_role(info, settings)
|
| 353 |
try:
|
| 354 |
index = runs_index(archive)
|
| 355 |
+
pending = pending_run(archive, index) if run is not None else None
|
| 356 |
+
if pending is not None and run == pending["name"]:
|
| 357 |
+
name, kind = run, "pending"
|
| 358 |
+
payload = (pending_organizer_payload(archive, pending) if role == "organizer"
|
| 359 |
+
else pending_participant_payload(archive, pending, orgs))
|
| 360 |
+
else:
|
| 361 |
+
if index is None:
|
| 362 |
+
logger.info("me: %s %s %s 503", handle, role, run or "-")
|
| 363 |
+
return JSONResponse({"error": "no_board"}, status_code=503)
|
| 364 |
+
name, kind = (index["latest"] if run is None else run), "published"
|
| 365 |
+
entry = index_entry(index, name) if RUN_NAME.fullmatch(name) else None
|
| 366 |
+
if entry is None:
|
| 367 |
+
logger.info("me: %s %s %s 404", handle, role, name)
|
| 368 |
+
return JSONResponse({"error": "not_found"}, status_code=404)
|
| 369 |
+
payload = (organizer_payload(archive, entry) if role == "organizer"
|
| 370 |
+
else participant_payload(archive, entry, orgs))
|
| 371 |
except HfHubHTTPError as exc: # e.g., no read token yet
|
| 372 |
logger.error("me: %s %s %s 503; the archive could not be read (%s)", handle, role, run or "-",
|
| 373 |
type(exc).__name__)
|
| 374 |
return JSONResponse({"error": "archive_unavailable"}, status_code=503)
|
| 375 |
+
logger.info("me: %s %s %s %s 200", handle, role, name, kind)
|
| 376 |
return payload
|
| 377 |
|
| 378 |
return app
|
tests/conftest.py
CHANGED
|
@@ -23,7 +23,10 @@ TOKENS = {
|
|
| 23 |
"orgs": [{"preferred_username": "team-a"}, {"preferred_username": "team-z"}]},
|
| 24 |
"organizer-token": {"preferred_username": ORGANIZER, "orgs": []},
|
| 25 |
"member-token": {"preferred_username": "bob", "orgs": [{"preferred_username": ORGANIZER_ORG}]},
|
|
|
|
| 26 |
}
|
|
|
|
|
|
|
| 27 |
RUN_BLOCKS = {
|
| 28 |
"week_01": {"name": "week_01", "label": "Week 1", "layer": "public", "date": "2026-10-02",
|
| 29 |
"deadline": "2026-09-29T12:00:00+00:00", "n_ranked": 2, "n_baselines": 1},
|
|
@@ -74,6 +77,22 @@ def site_board(run: str) -> dict:
|
|
| 74 |
"delta": 0.2, "r_side": 0.01, "r_guard": 0.02}]}
|
| 75 |
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
def team_file(run: str, org: str) -> dict:
|
| 78 |
return {"schema": 2, "run": {**RUN_BLOCKS[run], "pull_finished": None, "models": ["m/one"]}, "org": org,
|
| 79 |
"team": org.title(), "entry": {"status": "pulled", "record": [["repo", f"{org}/submission"]]},
|
|
@@ -83,7 +102,8 @@ def team_file(run: str, org: str) -> dict:
|
|
| 83 |
@pytest.fixture()
|
| 84 |
def archive(tmp_path: Path) -> Path:
|
| 85 |
"""An archive with the runs `week_01` and `week_02` indexed, `week_00` indexed without a site board, `week_03`
|
| 86 |
-
|
|
|
|
| 87 |
root = tmp_path / "archive"
|
| 88 |
write(root, "leaderboard/runs.json", {"schema": 1, "latest": "week_02",
|
| 89 |
"runs": [{"name": "week_00", "label": "Week 0", "layer": "public",
|
|
@@ -101,6 +121,19 @@ def archive(tmp_path: Path) -> Path:
|
|
| 101 |
write(root, "week_02/manifest.json", {"run": "week_02", "orgs": [{"org": "team-a", "status": "pulled"}]})
|
| 102 |
write(root, "week_02/progress.txt", "## submissions\n- [✓] team-a\n")
|
| 103 |
write(root, "week_02/failures.md", "# Failures\n\nnone\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
return root
|
| 105 |
|
| 106 |
|
|
|
|
| 23 |
"orgs": [{"preferred_username": "team-a"}, {"preferred_username": "team-z"}]},
|
| 24 |
"organizer-token": {"preferred_username": ORGANIZER, "orgs": []},
|
| 25 |
"member-token": {"preferred_username": "bob", "orgs": [{"preferred_username": ORGANIZER_ORG}]},
|
| 26 |
+
"other-token": {"preferred_username": "carol", "orgs": [{"preferred_username": "team-q"}]},
|
| 27 |
}
|
| 28 |
+
PENDING_RUN = {"name": "week_03", "label": "Week 3", "layer": "public", "pull_finished": "2026-10-13T12:41:10+00:00",
|
| 29 |
+
"n_pulled": 2, "models": ["m/one", "m/two", "m/three"]}
|
| 30 |
RUN_BLOCKS = {
|
| 31 |
"week_01": {"name": "week_01", "label": "Week 1", "layer": "public", "date": "2026-10-02",
|
| 32 |
"deadline": "2026-09-29T12:00:00+00:00", "n_ranked": 2, "n_baselines": 1},
|
|
|
|
| 77 |
"delta": 0.2, "r_side": 0.01, "r_guard": 0.02}]}
|
| 78 |
|
| 79 |
|
| 80 |
+
def pending_file(org: str, team: str) -> dict:
|
| 81 |
+
return {"schema": 1, "run": PENDING_RUN, "org": org, "team": team,
|
| 82 |
+
"entry": {"status": "pulled", "repo_private": True, "evaluator_role": "read", "commit": "c" * 40,
|
| 83 |
+
"commit_time": "2026-10-12T10:00:00+00:00", "pulled_at": "2026-10-13T12:03:51+00:00",
|
| 84 |
+
"files": [{"path": "method_card.md", "bytes": 3120}], "card": None, "label": "ccccccc",
|
| 85 |
+
"intake": None, "record": [["repo", f"{org}/submission"], ["commit", f"{'c' * 12} 2026-10-12 10:00 UTC"],
|
| 86 |
+
["pulled", "2026-10-13 12:03 UTC"], ["files", "method_card.md 3 KB"],
|
| 87 |
+
["intake", "no intake report for this run"]],
|
| 88 |
+
"method_card": {"file": "method_card.md", "text": f"# {team}\n", "truncated": False}}}
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def status_entry(status: str, **fields) -> dict:
|
| 92 |
+
return {"after": ["elicit/m__one"], "error": None, "heartbeat": "2026-10-13T14:00:00+00:00", "kind": "submission",
|
| 93 |
+
"status": status, "updated_at": "2026-10-13T14:00:00+00:00", "worker": "worker-7:12345", **fields}
|
| 94 |
+
|
| 95 |
+
|
| 96 |
def team_file(run: str, org: str) -> dict:
|
| 97 |
return {"schema": 2, "run": {**RUN_BLOCKS[run], "pull_finished": None, "models": ["m/one"]}, "org": org,
|
| 98 |
"team": org.title(), "entry": {"status": "pulled", "record": [["repo", f"{org}/submission"]]},
|
|
|
|
| 102 |
@pytest.fixture()
|
| 103 |
def archive(tmp_path: Path) -> Path:
|
| 104 |
"""An archive with the runs `week_01` and `week_02` indexed, `week_00` indexed without a site board, `week_03`
|
| 105 |
+
pending (an internal board and no site board, not indexed, with a pending index, two pending files, and a status
|
| 106 |
+
file), and the pull files of `week_02` and `week_03`."""
|
| 107 |
root = tmp_path / "archive"
|
| 108 |
write(root, "leaderboard/runs.json", {"schema": 1, "latest": "week_02",
|
| 109 |
"runs": [{"name": "week_00", "label": "Week 0", "layer": "public",
|
|
|
|
| 121 |
write(root, "week_02/manifest.json", {"run": "week_02", "orgs": [{"org": "team-a", "status": "pulled"}]})
|
| 122 |
write(root, "week_02/progress.txt", "## submissions\n- [✓] team-a\n")
|
| 123 |
write(root, "week_02/failures.md", "# Failures\n\nnone\n")
|
| 124 |
+
# the pending run: an index, two pending files, and a status file with entries for one of them on two models
|
| 125 |
+
write(root, "leaderboard/pending.json", {"schema": 1, "run": PENDING_RUN})
|
| 126 |
+
write(root, "leaderboard/week_03/pending/team-a.json", pending_file("team-a", "Team A"))
|
| 127 |
+
write(root, "leaderboard/week_03/pending/team-b.json", pending_file("team-b", "Team B Secret"))
|
| 128 |
+
write(root, "week_03/status.json", {
|
| 129 |
+
"elicit/m__one": status_entry("completed", kind="elicit", after=[]),
|
| 130 |
+
"submission-team-a/m__one": status_entry("completed"),
|
| 131 |
+
"submission-team-a/m__two": status_entry("in_progress", error="a secret traceback"),
|
| 132 |
+
"submission-team-b/m__one": status_entry("error", error="another secret traceback")})
|
| 133 |
+
write(root, "week_03/manifest.json", {"run": "week_03", "orgs": [{"org": "team-a", "status": "pulled"},
|
| 134 |
+
{"org": "team-b", "status": "pulled"}]})
|
| 135 |
+
write(root, "week_03/progress.txt", "## submissions\n- [ ] team-a\n- [✗] team-b\n")
|
| 136 |
+
write(root, "week_03/failures.md", "# Failures\n\nteam-b\n")
|
| 137 |
return root
|
| 138 |
|
| 139 |
|
tests/test_app.py
CHANGED
|
@@ -10,7 +10,7 @@ from fastapi.testclient import TestClient
|
|
| 10 |
from huggingface_hub.errors import HfHubHTTPError
|
| 11 |
|
| 12 |
import app as leaderboard_api
|
| 13 |
-
from conftest import RUN_BLOCKS, settings
|
| 14 |
|
| 15 |
|
| 16 |
def read(archive: Path, path: str):
|
|
@@ -29,10 +29,12 @@ def test_the_fixture_archive_holds_two_runs_with_a_site_board(archive: Path) ->
|
|
| 29 |
|
| 30 |
|
| 31 |
def test_runs_gives_the_index_verbatim(client: TestClient, archive: Path) -> None:
|
|
|
|
| 32 |
response = client.get("/runs")
|
| 33 |
assert response.status_code == 200
|
| 34 |
-
assert response.json() == read(
|
| 35 |
-
|
|
|
|
| 36 |
|
| 37 |
|
| 38 |
def test_runs_without_an_index_is_no_board(empty_client: TestClient) -> None:
|
|
@@ -62,7 +64,7 @@ def test_board_gives_a_runs_site_board_verbatim(client: TestClient, archive: Pat
|
|
| 62 |
def test_board_is_not_found_for_an_unknown_malformed_or_unindexed_run(client: TestClient) -> None:
|
| 63 |
"""An unknown run, a name that is not a run name, a run with an internal board but no site board that the index
|
| 64 |
does not list, and an indexed run without a site board each answer 404."""
|
| 65 |
-
for run in ("nope", "Week-01", "week_03", "week_00"):
|
| 66 |
response = client.get(f"/board/{run}")
|
| 67 |
assert response.status_code == 404 and response.json() == {"error": "not_found"}, run
|
| 68 |
|
|
@@ -106,9 +108,9 @@ def test_me_gives_an_organizer_every_file_of_the_run(client: TestClient, archive
|
|
| 106 |
|
| 107 |
|
| 108 |
def test_me_answers_404_for_an_unknown_run_and_401_before_it(client: TestClient) -> None:
|
| 109 |
-
"""An unknown run, a name that is not a run name, and a run
|
| 110 |
-
or rejected token answers 401 before the run is looked at."""
|
| 111 |
-
for run in ("nope", "Week-01", "
|
| 112 |
response = client.get("/me", params={"run": run}, headers=bearer("participant-token"))
|
| 113 |
assert response.status_code == 404 and response.json() == {"error": "not_found"}, run
|
| 114 |
assert client.get("/me", params={"run": "nope"}).status_code == 401
|
|
@@ -151,3 +153,73 @@ def test_an_unreadable_archive_is_503_everywhere(archive: Path, userinfo_url: st
|
|
| 151 |
client.get("/me", headers=bearer("participant-token"))):
|
| 152 |
assert request.status_code == 503 and request.json() == {"error": "archive_unavailable"}
|
| 153 |
assert client.get("/me").status_code == 401
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
from huggingface_hub.errors import HfHubHTTPError
|
| 11 |
|
| 12 |
import app as leaderboard_api
|
| 13 |
+
from conftest import PENDING_RUN, RUN_BLOCKS, settings
|
| 14 |
|
| 15 |
|
| 16 |
def read(archive: Path, path: str):
|
|
|
|
| 29 |
|
| 30 |
|
| 31 |
def test_runs_gives_the_index_verbatim(client: TestClient, archive: Path) -> None:
|
| 32 |
+
"""The index's fields are served as written, with the pending run added under `pending`."""
|
| 33 |
response = client.get("/runs")
|
| 34 |
assert response.status_code == 200
|
| 35 |
+
assert {key: value for key, value in response.json().items() if key != "pending"} == read(
|
| 36 |
+
archive, "leaderboard/runs.json")
|
| 37 |
+
assert response.json()["latest"] == "week_02" and "pending" in response.json()
|
| 38 |
|
| 39 |
|
| 40 |
def test_runs_without_an_index_is_no_board(empty_client: TestClient) -> None:
|
|
|
|
| 64 |
def test_board_is_not_found_for_an_unknown_malformed_or_unindexed_run(client: TestClient) -> None:
|
| 65 |
"""An unknown run, a name that is not a run name, a run with an internal board but no site board that the index
|
| 66 |
does not list, and an indexed run without a site board each answer 404."""
|
| 67 |
+
for run in ("nope", "Week-01", "week_03", "week_04", "week_00"):
|
| 68 |
response = client.get(f"/board/{run}")
|
| 69 |
assert response.status_code == 404 and response.json() == {"error": "not_found"}, run
|
| 70 |
|
|
|
|
| 108 |
|
| 109 |
|
| 110 |
def test_me_answers_404_for_an_unknown_run_and_401_before_it(client: TestClient) -> None:
|
| 111 |
+
"""An unknown run, a name that is not a run name, and a run that is neither in the index nor pending each
|
| 112 |
+
answer 404; a missing or rejected token answers 401 before the run is looked at."""
|
| 113 |
+
for run in ("nope", "Week-01", "week_04"):
|
| 114 |
response = client.get("/me", params={"run": run}, headers=bearer("participant-token"))
|
| 115 |
assert response.status_code == 404 and response.json() == {"error": "not_found"}, run
|
| 116 |
assert client.get("/me", params={"run": "nope"}).status_code == 401
|
|
|
|
| 153 |
client.get("/me", headers=bearer("participant-token"))):
|
| 154 |
assert request.status_code == 503 and request.json() == {"error": "archive_unavailable"}
|
| 155 |
assert client.get("/me").status_code == 401
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
# ---- the pending run ----------------------------------------------------------------------------------------
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def test_runs_names_the_pending_run(client: TestClient, archive: Path, userinfo_url: str) -> None:
|
| 162 |
+
"""`/runs` carries the pending run's block; it is null with the index cleared, with a pending index naming a
|
| 163 |
+
run the index lists, and with a malformed pending index."""
|
| 164 |
+
response = client.get("/runs")
|
| 165 |
+
assert response.status_code == 200
|
| 166 |
+
assert response.json()["pending"] == PENDING_RUN
|
| 167 |
+
assert {key: value for key, value in response.json().items() if key != "pending"} == read(archive, "leaderboard/runs.json")
|
| 168 |
+
pending = archive / "leaderboard" / "pending.json"
|
| 169 |
+
for cleared in ({"schema": 1, "run": None}, {"schema": 1, "run": {**PENDING_RUN, "name": "week_02"}},
|
| 170 |
+
{"schema": 1, "run": {**PENDING_RUN, "name": "Week 3"}}, {"schema": 1, "run": {**PENDING_RUN, "models": "m/one"}},
|
| 171 |
+
{"schema": 1}):
|
| 172 |
+
pending.write_text(json.dumps(cleared), encoding="utf-8")
|
| 173 |
+
assert client.get("/runs").json()["pending"] is None, cleared
|
| 174 |
+
assert client.get("/me", params={"run": "week_03"}, headers=bearer("participant-token")).status_code == 404
|
| 175 |
+
pending.unlink()
|
| 176 |
+
assert client.get("/runs").json()["pending"] is None
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def test_me_gives_a_participant_their_pending_files_with_the_evaluation_per_model(client: TestClient,
|
| 180 |
+
archive: Path) -> None:
|
| 181 |
+
response = client.get("/me", params={"run": "week_03"}, headers=bearer("participant-token"))
|
| 182 |
+
assert response.status_code == 200
|
| 183 |
+
payload = response.json()
|
| 184 |
+
assert list(payload) == ["role", "pending", "run", "teams", "missing"]
|
| 185 |
+
assert (payload["role"], payload["pending"], payload["run"]) == ("participant", True, PENDING_RUN)
|
| 186 |
+
assert payload["missing"] == ["team-z"] and len(payload["teams"]) == 1
|
| 187 |
+
team = payload["teams"][0]
|
| 188 |
+
assert team["evaluation"] == {"m/one": "completed", "m/two": "in_progress", "m/three": "not_started"}
|
| 189 |
+
assert {key: value for key, value in team.items() if key != "evaluation"} == read(
|
| 190 |
+
archive, "leaderboard/week_03/pending/team-a.json")
|
| 191 |
+
text = response.text
|
| 192 |
+
for forbidden in ("team-b", "Team B Secret", "error", "worker", "heartbeat", "traceback", "after", "updated_at"):
|
| 193 |
+
assert forbidden not in text, forbidden
|
| 194 |
+
other = client.get("/me", params={"run": "week_03"}, headers=bearer("other-token"))
|
| 195 |
+
assert other.status_code == 200 and other.json()["teams"] == [] and other.json()["missing"] == ["team-q"]
|
| 196 |
+
(archive / "week_03" / "status.json").unlink()
|
| 197 |
+
without = client.get("/me", params={"run": "week_03"}, headers=bearer("participant-token")).json()
|
| 198 |
+
assert without["teams"][0]["evaluation"] == {model: "not_started" for model in PENDING_RUN["models"]}
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def test_me_gives_an_organizer_every_pending_file(client: TestClient, archive: Path) -> None:
|
| 202 |
+
response = client.get("/me", params={"run": "week_03"}, headers=bearer("organizer-token"))
|
| 203 |
+
assert response.status_code == 200
|
| 204 |
+
payload = response.json()
|
| 205 |
+
assert list(payload) == ["role", "pending", "run", "board", "teams", "pull", "progress", "failures"]
|
| 206 |
+
assert (payload["role"], payload["pending"], payload["run"], payload["board"]) == ("organizer", True,
|
| 207 |
+
PENDING_RUN, None)
|
| 208 |
+
assert set(payload["teams"]) == {"team-a", "team-b"}
|
| 209 |
+
assert payload["teams"]["team-a"]["evaluation"] == {"m/one": "completed", "m/two": "in_progress",
|
| 210 |
+
"m/three": "not_started"}
|
| 211 |
+
assert payload["teams"]["team-b"]["evaluation"] == {"m/one": "error", "m/two": "not_started",
|
| 212 |
+
"m/three": "not_started"}
|
| 213 |
+
assert payload["teams"]["team-b"]["team"] == "Team B Secret"
|
| 214 |
+
assert payload["pull"] == read(archive, "week_03/manifest.json")
|
| 215 |
+
assert payload["progress"].startswith("## submissions") and payload["failures"].startswith("# Failures")
|
| 216 |
+
assert "traceback" not in response.text
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def test_the_pending_run_has_no_board_and_is_not_the_default(client: TestClient) -> None:
|
| 220 |
+
assert client.get("/board/week_03").status_code == 404
|
| 221 |
+
latest = client.get("/me", headers=bearer("participant-token")).json()
|
| 222 |
+
assert latest["run"]["name"] == "week_02" and "pending" not in latest
|
| 223 |
+
assert client.get("/healthz").json() == {"run": "week_02", "date": "2026-10-09"}
|
| 224 |
+
assert client.get("/me", params={"run": "week_03"}).status_code == 401
|
| 225 |
+
assert client.get("/me", params={"run": "week_03"}, headers=bearer("bad-token")).status_code == 401
|