Skip to content

Commit e8bcd64

Browse files
committed
Add canonical leaderboard rankings API
1 parent 972d019 commit e8bcd64

5 files changed

Lines changed: 184 additions & 5 deletions

File tree

‎src/kernelbot/api/main.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,55 @@ async def get_gpus(
887887
raise HTTPException(status_code=500, detail=f"Error fetching GPU data: {e}") from e
888888

889889

890+
@app.get("/leaderboard/{leaderboard_id}/rankings")
891+
async def get_leaderboard_rankings(
892+
leaderboard_id: int,
893+
user_info: Annotated[Optional[Any], Depends(optional_user_header)] = None,
894+
db_context=Depends(get_db),
895+
) -> dict:
896+
"""Return canonical leaderboard metadata and rankings for all runners."""
897+
await simple_rate_limit()
898+
try:
899+
with db_context as db:
900+
leaderboard = db.get_leaderboard_by_id(leaderboard_id)
901+
enforce_leaderboard_access(db, leaderboard["name"], user_info)
902+
903+
rankings = {}
904+
for gpu_type in leaderboard["gpu_types"]:
905+
ranked_entries = db.get_leaderboard_submissions(leaderboard["name"], gpu_type)
906+
rankings[gpu_type] = [
907+
{
908+
"user_name": entry["user_name"],
909+
"score": entry["submission_score"],
910+
"file_name": entry["submission_name"],
911+
"submission_id": entry["submission_id"],
912+
"submission_count": entry.get("submission_count", 0),
913+
"submission_time": entry["submission_time"],
914+
}
915+
for entry in ranked_entries
916+
]
917+
918+
task = leaderboard["task"]
919+
return {
920+
"rankings": rankings,
921+
"leaderboard": {
922+
"name": leaderboard["name"],
923+
"deadline": leaderboard["deadline"],
924+
"lang": task.lang.value,
925+
"description": leaderboard["description"],
926+
"reference": task.files.get("reference.py", ""),
927+
"benchmarks": task.benchmarks,
928+
"gpu_types": leaderboard["gpu_types"],
929+
},
930+
}
931+
except HTTPException:
932+
raise
933+
except KernelBotError:
934+
raise
935+
except Exception as e:
936+
raise HTTPException(status_code=500, detail=f"Error fetching leaderboard rankings: {e}") from e
937+
938+
890939
@app.get("/submissions/{leaderboard_name}/{gpu_name}")
891940
async def get_submissions(
892941
leaderboard_name: str,

‎src/libkernelbot/db_types.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class LeaderboardRankedEntry(TypedDict):
3030
submission_name: str
3131
submission_time: datetime.datetime
3232
submission_score: float
33+
submission_count: NotRequired[int]
3334
leaderboard_name: str
3435
user_id: int
3536
user_name: str

‎src/libkernelbot/leaderboard_db.py‎

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,35 @@ def get_leaderboard(self, leaderboard_name: str) -> "LeaderboardItem":
654654
else:
655655
raise LeaderboardDoesNotExist(leaderboard_name)
656656

657+
def get_leaderboard_by_id(self, leaderboard_id: int) -> "LeaderboardItem":
658+
self.cursor.execute(
659+
"""
660+
SELECT id, name, deadline, task, creator_id, forum_id, secret_seed, description, visibility
661+
FROM leaderboard.leaderboard
662+
WHERE id = %s
663+
""",
664+
(leaderboard_id,),
665+
)
666+
667+
res = self.cursor.fetchone()
668+
669+
if res:
670+
task = LeaderboardTask.from_dict(res[3])
671+
return LeaderboardItem(
672+
id=res[0],
673+
name=res[1],
674+
deadline=res[2],
675+
task=task,
676+
creator_id=res[4],
677+
forum_id=res[5],
678+
secret_seed=res[6],
679+
gpu_types=self.get_leaderboard_gpu_types(res[1]),
680+
description=res[7],
681+
visibility=res[8],
682+
)
683+
else:
684+
raise LeaderboardDoesNotExist(str(leaderboard_id))
685+
657686
def check_leaderboard_access(self, leaderboard_name: str, user_id: str) -> bool:
658687
"""Returns True if leaderboard is public or user has claimed an invite covering this leaderboard."""
659688
self.cursor.execute(
@@ -865,6 +894,14 @@ def get_leaderboard_submissions(
865894
if user_id:
866895
# Query all if user_id (means called from show-personal)
867896
query = """
897+
WITH submission_counts AS (
898+
SELECT s.user_id, r.runner, COUNT(DISTINCT s.id) AS submission_count
899+
FROM leaderboard.submission s
900+
JOIN leaderboard.runs r ON r.submission_id = s.id
901+
JOIN leaderboard.leaderboard l ON s.leaderboard_id = l.id
902+
WHERE l.name = %s
903+
GROUP BY s.user_id, r.runner
904+
)
868905
SELECT
869906
s.file_name,
870907
s.id,
@@ -873,11 +910,13 @@ def get_leaderboard_submissions(
873910
r.score,
874911
r.runner,
875912
ui.user_name,
876-
RANK() OVER (ORDER BY r.score ASC) as rank
913+
RANK() OVER (ORDER BY r.score ASC) as rank,
914+
COALESCE(sc.submission_count, 0) AS submission_count
877915
FROM leaderboard.runs r
878916
JOIN leaderboard.submission s ON r.submission_id = s.id
879917
JOIN leaderboard.leaderboard l ON s.leaderboard_id = l.id
880918
JOIN leaderboard.user_info ui ON s.user_id = ui.id
919+
LEFT JOIN submission_counts sc ON s.user_id = sc.user_id AND r.runner = sc.runner
881920
WHERE l.name = %s
882921
AND r.runner = %s
883922
AND NOT r.secret
@@ -895,11 +934,19 @@ def get_leaderboard_submissions(
895934
ORDER BY r.score ASC
896935
LIMIT %s OFFSET %s
897936
"""
898-
args = (leaderboard_name, gpu_name, user_id, limit, offset)
937+
args = (leaderboard_name, leaderboard_name, gpu_name, user_id, limit, offset)
899938
else:
900939
# Query best submission per user if no user_id (means called from show)
901940
query = """
902-
WITH best_submissions AS (
941+
WITH submission_counts AS (
942+
SELECT s.user_id, r.runner, COUNT(DISTINCT s.id) AS submission_count
943+
FROM leaderboard.submission s
944+
JOIN leaderboard.runs r ON r.submission_id = s.id
945+
JOIN leaderboard.leaderboard l ON s.leaderboard_id = l.id
946+
WHERE l.name = %s
947+
GROUP BY s.user_id, r.runner
948+
),
949+
best_submissions AS (
903950
SELECT DISTINCT ON (s.user_id)
904951
s.id as submission_id,
905952
s.file_name,
@@ -931,13 +978,15 @@ def get_leaderboard_submissions(
931978
bs.score,
932979
bs.runner,
933980
ui.user_name,
934-
RANK() OVER (ORDER BY bs.score ASC) as rank
981+
RANK() OVER (ORDER BY bs.score ASC) as rank,
982+
COALESCE(sc.submission_count, 0) AS submission_count
935983
FROM best_submissions bs
936984
JOIN leaderboard.user_info ui ON bs.user_id = ui.id
985+
LEFT JOIN submission_counts sc ON bs.user_id = sc.user_id AND bs.runner = sc.runner
937986
ORDER BY bs.score ASC
938987
LIMIT %s OFFSET %s
939988
"""
940-
args = (leaderboard_name, gpu_name, limit, offset)
989+
args = (leaderboard_name, leaderboard_name, gpu_name, limit, offset)
941990

942991
self.cursor.execute(query, args)
943992

@@ -950,6 +999,7 @@ def get_leaderboard_submissions(
950999
submission_score=submission[4],
9511000
user_name=submission[6],
9521001
rank=submission[7],
1002+
submission_count=submission[8],
9531003
leaderboard_name=leaderboard_name,
9541004
gpu_type=gpu_name,
9551005
)

‎tests/test_admin_api.py‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,83 @@ def test_public_leaderboard_submissions_no_auth(self, test_client, mock_backend)
700700

701701
response = test_client.get("/submissions/public-lb/A100")
702702
assert response.status_code == 200
703+
704+
def test_public_leaderboard_rankings_no_auth(self, test_client, mock_backend):
705+
"""GET /leaderboard/{id}/rankings returns metadata and all GPU rankings."""
706+
from libkernelbot.consts import Language
707+
from libkernelbot.task import LeaderboardTask, PythonTaskData
708+
709+
self._setup_db_mock(mock_backend)
710+
task = LeaderboardTask(
711+
lang=Language.Python,
712+
files={"reference.py": "def ref(): pass"},
713+
config=PythonTaskData(main="eval.py"),
714+
benchmarks=[{"n": 32}],
715+
)
716+
mock_backend.db.get_leaderboard_by_id = MagicMock(
717+
return_value={
718+
"id": 123,
719+
"name": "qr_v2",
720+
"deadline": "2026-06-30T00:00:00Z",
721+
"description": "QR",
722+
"task": task,
723+
"gpu_types": ["B200"],
724+
"visibility": "public",
725+
}
726+
)
727+
mock_backend.db.get_leaderboard = MagicMock(return_value={"visibility": "public"})
728+
mock_backend.db.get_leaderboard_submissions = MagicMock(
729+
return_value=[
730+
{
731+
"user_name": "mark",
732+
"submission_score": 1.5,
733+
"submission_name": "submission.py",
734+
"submission_id": 42,
735+
"submission_count": 3,
736+
"submission_time": "2026-06-17T00:00:00Z",
737+
}
738+
]
739+
)
740+
741+
response = test_client.get("/leaderboard/123/rankings")
742+
assert response.status_code == 200
743+
assert response.json() == {
744+
"rankings": {
745+
"B200": [
746+
{
747+
"user_name": "mark",
748+
"score": 1.5,
749+
"file_name": "submission.py",
750+
"submission_id": 42,
751+
"submission_count": 3,
752+
"submission_time": "2026-06-17T00:00:00Z",
753+
}
754+
]
755+
},
756+
"leaderboard": {
757+
"name": "qr_v2",
758+
"deadline": "2026-06-30T00:00:00Z",
759+
"lang": "py",
760+
"description": "QR",
761+
"reference": "def ref(): pass",
762+
"benchmarks": [{"n": 32}],
763+
"gpu_types": ["B200"],
764+
},
765+
}
766+
767+
def test_missing_leaderboard_rankings_returns_404(self, test_client, mock_backend):
768+
"""GET /leaderboard/{id}/rankings returns 404 for a missing leaderboard."""
769+
from libkernelbot.leaderboard_db import LeaderboardDoesNotExist
770+
771+
self._setup_db_mock(mock_backend)
772+
mock_backend.db.get_leaderboard_by_id = MagicMock(
773+
side_effect=LeaderboardDoesNotExist("999")
774+
)
775+
776+
response = test_client.get("/leaderboard/999/rankings")
777+
assert response.status_code == 404
778+
779+
703780
class TestAdminExportHF:
704781
"""Test admin HF export endpoint."""
705782

‎tests/test_leaderboard_db.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ def test_leaderboard_submission_ranked(database, submit_leaderboard):
350350
"rank": 1,
351351
"submission_id": 2,
352352
"submission_name": "submission.py",
353+
"submission_count": 3,
353354
"submission_score": Decimal("4.5"),
354355
"submission_time": submit_time,
355356
"user_id": "5",
@@ -361,6 +362,7 @@ def test_leaderboard_submission_ranked(database, submit_leaderboard):
361362
"rank": 2,
362363
"submission_id": 4,
363364
"submission_name": "submission.py",
365+
"submission_count": 1,
364366
"submission_score": Decimal("8.0"),
365367
"submission_time": submit_time,
366368
"user_id": "6",

0 commit comments

Comments
 (0)