submit+app: gate Submit on HF OAuth; populate hf_username from profile
Browse filesBundle 1+2 C10. Login-required submit shaped per the bundle spec.
submit.py:
- `handle_submit` grows a `profile: gr.OAuthProfile | None` arg.
Gradio dependency-injects the visitor's profile based on the
signature. None -> immediate `gr.Error("Please log in via the
HF button before submitting.")` so a UI mishap can't write an
anonymous row.
- `_build_pending_row` grows an `hf_username=None` kwarg.
`handle_submit` passes `profile.username` through so the row
carries the canonical HF identity (not a free-text claim in
meta.json's submitter_name).
app.py:
- New `gr.LoginButton()` in the Submit tab above the zip dropzone.
- Submit button starts `interactive=False`; new
`_enable_submit_when_logged_in(profile)` reads the visitor's
OAuth profile via `blocks.load` and flips the button on / off
per page load. Server-side gate in `handle_submit` mirrors this
so a logged-out user who somehow bypasses the disabled-button
UI still gets rejected.
requirements.txt:
- `gradio==5.50.0` -> `gradio[oauth]==5.50.0`. The `[oauth]` extra
pulls in `authlib` + `itsdangerous` + `cryptography`, which
gr.LoginButton + gr.OAuthProfile depend on. Without them
importing app.py fails with `ImportError: Cannot initialize OAuth`
(caught both locally and at Space build time).
README.md: untouched. Bundle 3 already landed `hf_oauth: true` on
line 10 of the front-matter, so the defensive add C10 was
prepared to do is a no-op.
tests/test_submit.py:
- New `test_pending_row_populates_hf_username_when_provided`
covers the kwarg path the OAuth flow takes.
- New `test_pending_row_hf_username_defaults_to_none` confirms
pre-OAuth callers (tests + pre-C10 row writers) stay clean.
- Existing tests untouched (still call _build_pending_row without
the hf_username kwarg, exercising the default).
Verification (autonomous):
- 24/24 unit tests green (2 new + 22 existing).
- Local boot: Gradio config carries the LoginButton (`"Sign in"` /
`"Logout"` label strings present), Submit button starts
non-interactive (`"interactive":false`), accordions / Download
CSV / report viewer all intact. handle_submit signature exposes
`profile`.
Manual OAuth check (per bundle spec C10) requires a real browser
login and is up to the human reviewer; can't simulate the HF
OAuth handshake from a sandboxed shell.
- app.py +34 -2
- requirements.txt +3 -1
- submit.py +24 -8
- tests/test_submit.py +36 -0
|
@@ -158,6 +158,21 @@ def _refresh_leaderboard_with_toast():
|
|
| 158 |
return load_leaderboard_split()
|
| 159 |
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
def _format_detail_and_report(
|
| 162 |
df: pd.DataFrame | None, evt: gr.SelectData,
|
| 163 |
) -> tuple[str, str]:
|
|
@@ -359,10 +374,21 @@ not in the main leaderboard table.
|
|
| 359 |
to publish the resulting row on the public leaderboard.
|
| 360 |
"""
|
| 361 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
zip_in = gr.File(label="Submission ZIP", file_types=[".zip"])
|
| 363 |
-
|
|
|
|
|
|
|
| 364 |
# No static markdown output: handle_submit surfaces every
|
| 365 |
-
# status update via gr.Info / gr.Error toasts.
|
|
|
|
|
|
|
| 366 |
submit_btn.click(fn=handle_submit, inputs=[zip_in])
|
| 367 |
|
| 368 |
with gr.Tab("About"):
|
|
@@ -378,6 +404,12 @@ to publish the resulting row on the public leaderboard.
|
|
| 378 |
outputs=[validated_view, unvalidated_view],
|
| 379 |
)
|
| 380 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
|
| 382 |
# Mount Gradio under a FastAPI parent so the custom proxy route
|
| 383 |
# above lives at the same origin as the UI. Direct routes on `app`
|
|
|
|
| 158 |
return load_leaderboard_split()
|
| 159 |
|
| 160 |
|
| 161 |
+
def _enable_submit_when_logged_in(
|
| 162 |
+
profile: gr.OAuthProfile | None,
|
| 163 |
+
) -> gr.Button:
|
| 164 |
+
"""Flip the Submit button's interactivity based on login state.
|
| 165 |
+
|
| 166 |
+
Runs once per page load via ``blocks.load``. Gradio injects
|
| 167 |
+
``gr.OAuthProfile`` automatically (``None`` if the visitor isn't
|
| 168 |
+
logged in via the LoginButton). The visible-disable mirrors the
|
| 169 |
+
server-side gate in :func:`submit.handle_submit`; the handler
|
| 170 |
+
still raises ``gr.Error`` defensively if it ever gets called
|
| 171 |
+
without a profile.
|
| 172 |
+
"""
|
| 173 |
+
return gr.Button(interactive=profile is not None)
|
| 174 |
+
|
| 175 |
+
|
| 176 |
def _format_detail_and_report(
|
| 177 |
df: pd.DataFrame | None, evt: gr.SelectData,
|
| 178 |
) -> tuple[str, str]:
|
|
|
|
| 374 |
to publish the resulting row on the public leaderboard.
|
| 375 |
"""
|
| 376 |
)
|
| 377 |
+
# OAuth gate. The user must log in via the HF button before
|
| 378 |
+
# the Submit button becomes interactive; the row gets the
|
| 379 |
+
# canonical `hf_username` from `gr.OAuthProfile.username`
|
| 380 |
+
# (not a free-text claim in meta.json). README front-matter
|
| 381 |
+
# already carries `hf_oauth: true` so HF's OAuth integration
|
| 382 |
+
# is wired up at the Space level.
|
| 383 |
+
login_btn = gr.LoginButton()
|
| 384 |
zip_in = gr.File(label="Submission ZIP", file_types=[".zip"])
|
| 385 |
+
# Starts disabled; the `blocks.load` handler below flips it
|
| 386 |
+
# to interactive when an OAuthProfile is present.
|
| 387 |
+
submit_btn = gr.Button("Submit", variant="primary", interactive=False)
|
| 388 |
# No static markdown output: handle_submit surfaces every
|
| 389 |
+
# status update via gr.Info / gr.Error toasts. The handler
|
| 390 |
+
# also reads `gr.OAuthProfile` implicitly via its parameter
|
| 391 |
+
# type annotation (Gradio's dependency-injection convention).
|
| 392 |
submit_btn.click(fn=handle_submit, inputs=[zip_in])
|
| 393 |
|
| 394 |
with gr.Tab("About"):
|
|
|
|
| 404 |
outputs=[validated_view, unvalidated_view],
|
| 405 |
)
|
| 406 |
|
| 407 |
+
# On page load, read the visitor's OAuth profile (None if not
|
| 408 |
+
# logged in) and flip the Submit button's interactivity. Runs once
|
| 409 |
+
# per page load; LoginButton clicks also re-trigger this through
|
| 410 |
+
# Gradio's auth-event plumbing.
|
| 411 |
+
blocks.load(fn=_enable_submit_when_logged_in, outputs=submit_btn)
|
| 412 |
+
|
| 413 |
|
| 414 |
# Mount Gradio under a FastAPI parent so the custom proxy route
|
| 415 |
# above lives at the same origin as the UI. Direct routes on `app`
|
|
@@ -7,7 +7,9 @@
|
|
| 7 |
# DABstep, bigcodebench) all run on this stack. Revisit once a
|
| 8 |
# Gradio 6-compatible gradio_leaderboard ships AND the underlying
|
| 9 |
# Dataframe-update bug is fixed upstream.
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
gradio-leaderboard==0.0.14
|
| 12 |
pandas>=2.0
|
| 13 |
huggingface_hub>=0.27.0
|
|
|
|
| 7 |
# DABstep, bigcodebench) all run on this stack. Revisit once a
|
| 8 |
# Gradio 6-compatible gradio_leaderboard ships AND the underlying
|
| 9 |
# Dataframe-update bug is fixed upstream.
|
| 10 |
+
# OAuth extras (authlib + itsdangerous) are required by gr.LoginButton
|
| 11 |
+
# + gr.OAuthProfile, which gate the Submit tab on HF login.
|
| 12 |
+
gradio[oauth]==5.50.0
|
| 13 |
gradio-leaderboard==0.0.14
|
| 14 |
pandas>=2.0
|
| 15 |
huggingface_hub>=0.27.0
|
|
@@ -131,9 +131,18 @@ class _HubWriteError(Exception):
|
|
| 131 |
"""Raised when a Hub upload fails after validation succeeded."""
|
| 132 |
|
| 133 |
|
| 134 |
-
def handle_submit(
|
|
|
|
|
|
|
| 135 |
"""Validate a submission upload; surface progress + outcome via toasts.
|
| 136 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
Side-effect-only (returns ``None``): every status update lands in
|
| 138 |
a ``gr.Info`` / ``gr.Error`` toast instead of a static markdown
|
| 139 |
output below the button. Rejection paths raise ``gr.Error``,
|
|
@@ -147,9 +156,12 @@ def handle_submit(zip_file) -> None:
|
|
| 147 |
the pending row + zip are on the Hub and the worker has
|
| 148 |
been spawned.
|
| 149 |
|
| 150 |
-
On rejection (form-level, validation gate, dedup,
|
| 151 |
-
a single ``gr.Error`` toast carries the message;
|
|
|
|
| 152 |
"""
|
|
|
|
|
|
|
| 153 |
form_err = _validate_form(zip_file)
|
| 154 |
if form_err is not None:
|
| 155 |
raise gr.Error(form_err)
|
|
@@ -191,7 +203,8 @@ def handle_submit(zip_file) -> None:
|
|
| 191 |
try:
|
| 192 |
blob_url = _upload_submission_zip(submission_id, zip_path)
|
| 193 |
row = _build_pending_row(
|
| 194 |
-
submission_id, meta, blob_url, zip_sha256
|
|
|
|
| 195 |
)
|
| 196 |
_append_pending_row(row)
|
| 197 |
except _HubWriteError as e:
|
|
@@ -406,6 +419,7 @@ def _build_pending_row(
|
|
| 406 |
meta: dict[str, Any],
|
| 407 |
blob_url: str,
|
| 408 |
submission_sha256: str,
|
|
|
|
| 409 |
) -> dict[str, Any]:
|
| 410 |
"""Construct the JSON row written for a freshly-queued submission.
|
| 411 |
|
|
@@ -416,9 +430,11 @@ def _build_pending_row(
|
|
| 416 |
|
| 417 |
Validation-tier fields default per the validation-policy decision
|
| 418 |
doc: ``validation_status: "unvalidated"`` (maintainers promote
|
| 419 |
-
post-eval), ``validation_method: None``. ``hf_username``
|
| 420 |
-
``None``
|
| 421 |
-
the
|
|
|
|
|
|
|
| 422 |
"""
|
| 423 |
return {
|
| 424 |
"submission_id": submission_id,
|
|
@@ -441,7 +457,7 @@ def _build_pending_row(
|
|
| 441 |
"submission_sha256": submission_sha256,
|
| 442 |
"validation_status": "unvalidated",
|
| 443 |
"validation_method": None,
|
| 444 |
-
"hf_username":
|
| 445 |
}
|
| 446 |
|
| 447 |
|
|
|
|
| 131 |
"""Raised when a Hub upload fails after validation succeeded."""
|
| 132 |
|
| 133 |
|
| 134 |
+
def handle_submit(
|
| 135 |
+
zip_file, profile: gr.OAuthProfile | None,
|
| 136 |
+
) -> None:
|
| 137 |
"""Validate a submission upload; surface progress + outcome via toasts.
|
| 138 |
|
| 139 |
+
Requires the user to be logged in via ``gr.LoginButton`` so the
|
| 140 |
+
row's ``hf_username`` is the canonical HF identity (not a
|
| 141 |
+
free-text claim). The submit button in :mod:`app` is disabled
|
| 142 |
+
until login completes; this function also raises ``gr.Error``
|
| 143 |
+
defensively if it's called without a profile so a UI mishap
|
| 144 |
+
can't write an anonymous row.
|
| 145 |
+
|
| 146 |
Side-effect-only (returns ``None``): every status update lands in
|
| 147 |
a ``gr.Info`` / ``gr.Error`` toast instead of a static markdown
|
| 148 |
output below the button. Rejection paths raise ``gr.Error``,
|
|
|
|
| 156 |
the pending row + zip are on the Hub and the worker has
|
| 157 |
been spawned.
|
| 158 |
|
| 159 |
+
On rejection (login-missing, form-level, validation gate, dedup,
|
| 160 |
+
or Hub write), a single ``gr.Error`` toast carries the message;
|
| 161 |
+
no second toast.
|
| 162 |
"""
|
| 163 |
+
if profile is None:
|
| 164 |
+
raise gr.Error("Please log in via the HF button before submitting.")
|
| 165 |
form_err = _validate_form(zip_file)
|
| 166 |
if form_err is not None:
|
| 167 |
raise gr.Error(form_err)
|
|
|
|
| 203 |
try:
|
| 204 |
blob_url = _upload_submission_zip(submission_id, zip_path)
|
| 205 |
row = _build_pending_row(
|
| 206 |
+
submission_id, meta, blob_url, zip_sha256,
|
| 207 |
+
hf_username=profile.username,
|
| 208 |
)
|
| 209 |
_append_pending_row(row)
|
| 210 |
except _HubWriteError as e:
|
|
|
|
| 419 |
meta: dict[str, Any],
|
| 420 |
blob_url: str,
|
| 421 |
submission_sha256: str,
|
| 422 |
+
hf_username: str | None = None,
|
| 423 |
) -> dict[str, Any]:
|
| 424 |
"""Construct the JSON row written for a freshly-queued submission.
|
| 425 |
|
|
|
|
| 430 |
|
| 431 |
Validation-tier fields default per the validation-policy decision
|
| 432 |
doc: ``validation_status: "unvalidated"`` (maintainers promote
|
| 433 |
+
post-eval), ``validation_method: None``. ``hf_username`` defaults
|
| 434 |
+
to ``None``; callers post-OAuth pass ``profile.username`` so the
|
| 435 |
+
row carries the canonical HF identity. Pre-OAuth-era rows
|
| 436 |
+
(anything written before C10 landed) and any test paths that
|
| 437 |
+
don't supply the kwarg keep ``null``.
|
| 438 |
"""
|
| 439 |
return {
|
| 440 |
"submission_id": submission_id,
|
|
|
|
| 457 |
"submission_sha256": submission_sha256,
|
| 458 |
"validation_status": "unvalidated",
|
| 459 |
"validation_method": None,
|
| 460 |
+
"hf_username": hf_username,
|
| 461 |
}
|
| 462 |
|
| 463 |
|
|
@@ -52,6 +52,42 @@ def test_pending_row_preserves_sha256(monkeypatch):
|
|
| 52 |
assert row["submission_sha256"] == expected_hash
|
| 53 |
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
def test_pending_row_preserves_existing_metadata(monkeypatch):
|
| 56 |
"""Pre-Bundle-1+2 fields keep their values from meta + args.
|
| 57 |
|
|
|
|
| 52 |
assert row["submission_sha256"] == expected_hash
|
| 53 |
|
| 54 |
|
| 55 |
+
def test_pending_row_populates_hf_username_when_provided(monkeypatch):
|
| 56 |
+
"""C10 OAuth path: profile.username flows into the row's hf_username.
|
| 57 |
+
|
| 58 |
+
The submit handler reads ``gr.OAuthProfile`` (injected by Gradio)
|
| 59 |
+
and passes ``profile.username`` through as a kwarg. This test
|
| 60 |
+
exercises just the row builder's side of that handoff so a
|
| 61 |
+
refactor that drops the kwarg gets caught.
|
| 62 |
+
"""
|
| 63 |
+
monkeypatch.setattr(submit, "_resolve_data_revision", lambda: "test-rev")
|
| 64 |
+
row = submit._build_pending_row(
|
| 65 |
+
submission_id="sub-test-x",
|
| 66 |
+
meta=_stub_meta(),
|
| 67 |
+
blob_url="https://huggingface.co/datasets/example/sub-test-x.zip",
|
| 68 |
+
submission_sha256="a" * 64,
|
| 69 |
+
hf_username="alice",
|
| 70 |
+
)
|
| 71 |
+
assert row["hf_username"] == "alice"
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def test_pending_row_hf_username_defaults_to_none(monkeypatch):
|
| 75 |
+
"""Omitting the kwarg keeps `hf_username` null.
|
| 76 |
+
|
| 77 |
+
Covers the pre-OAuth callers (test fixtures, scripts) that don't
|
| 78 |
+
have a profile in scope. Pre-C10 row writers and any future
|
| 79 |
+
non-OAuth caller default cleanly.
|
| 80 |
+
"""
|
| 81 |
+
monkeypatch.setattr(submit, "_resolve_data_revision", lambda: "test-rev")
|
| 82 |
+
row = submit._build_pending_row(
|
| 83 |
+
submission_id="sub-test-x",
|
| 84 |
+
meta=_stub_meta(),
|
| 85 |
+
blob_url="https://huggingface.co/datasets/example/sub-test-x.zip",
|
| 86 |
+
submission_sha256="a" * 64,
|
| 87 |
+
)
|
| 88 |
+
assert row["hf_username"] is None
|
| 89 |
+
|
| 90 |
+
|
| 91 |
def test_pending_row_preserves_existing_metadata(monkeypatch):
|
| 92 |
"""Pre-Bundle-1+2 fields keep their values from meta + args.
|
| 93 |
|