Michael Rabinovich commited on
Commit
c87b253
·
1 Parent(s): 6facf47

submit+app: gate Submit on HF OAuth; populate hf_username from profile

Browse files

Bundle 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.

Files changed (4) hide show
  1. app.py +34 -2
  2. requirements.txt +3 -1
  3. submit.py +24 -8
  4. tests/test_submit.py +36 -0
app.py CHANGED
@@ -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
- submit_btn = gr.Button("Submit", variant="primary")
 
 
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`
requirements.txt CHANGED
@@ -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
- gradio==5.50.0
 
 
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
submit.py CHANGED
@@ -131,9 +131,18 @@ class _HubWriteError(Exception):
131
  """Raised when a Hub upload fails after validation succeeded."""
132
 
133
 
134
- def handle_submit(zip_file) -> None:
 
 
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, or Hub write),
151
- a single ``gr.Error`` toast carries the message; no second toast.
 
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`` is also
420
- ``None`` here; C10 wires it through from ``gr.OAuthProfile`` once
421
- the LoginButton lands.
 
 
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": None,
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
 
tests/test_submit.py CHANGED
@@ -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