Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions src/kernelbot/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,55 @@ async def get_gpus(
raise HTTPException(status_code=500, detail=f"Error fetching GPU data: {e}") from e


@app.get("/leaderboard/{leaderboard_id}/rankings")
async def get_leaderboard_rankings(
leaderboard_id: int,
user_info: Annotated[Optional[Any], Depends(optional_user_header)] = None,
db_context=Depends(get_db),
) -> dict:
"""Return canonical leaderboard metadata and rankings for all runners."""
await simple_rate_limit()
try:
with db_context as db:
leaderboard = db.get_leaderboard_by_id(leaderboard_id)
enforce_leaderboard_access(db, leaderboard["name"], user_info)

rankings = {}
for gpu_type in leaderboard["gpu_types"]:
ranked_entries = db.get_leaderboard_submissions(leaderboard["name"], gpu_type)
rankings[gpu_type] = [
{
"user_name": entry["user_name"],
"score": entry["submission_score"],
"file_name": entry["submission_name"],
"submission_id": entry["submission_id"],
"submission_count": entry.get("submission_count", 0),
"submission_time": entry["submission_time"],
}
for entry in ranked_entries
]

task = leaderboard["task"]
return {
"rankings": rankings,
"leaderboard": {
"name": leaderboard["name"],
"deadline": leaderboard["deadline"],
"lang": task.lang.value,
"description": leaderboard["description"],
"reference": task.files.get("reference.py", ""),
"benchmarks": task.benchmarks,
"gpu_types": leaderboard["gpu_types"],
},
}
except HTTPException:
raise
except KernelBotError:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error fetching leaderboard rankings: {e}") from e


@app.get("/submissions/{leaderboard_name}/{gpu_name}")
async def get_submissions(
leaderboard_name: str,
Expand Down
1 change: 1 addition & 0 deletions src/libkernelbot/db_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class LeaderboardRankedEntry(TypedDict):
submission_name: str
submission_time: datetime.datetime
submission_score: float
submission_count: NotRequired[int]
leaderboard_name: str
user_id: int
user_name: str
Expand Down
60 changes: 55 additions & 5 deletions src/libkernelbot/leaderboard_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,35 @@ def get_leaderboard(self, leaderboard_name: str) -> "LeaderboardItem":
else:
raise LeaderboardDoesNotExist(leaderboard_name)

def get_leaderboard_by_id(self, leaderboard_id: int) -> "LeaderboardItem":
self.cursor.execute(
"""
SELECT id, name, deadline, task, creator_id, forum_id, secret_seed, description, visibility
FROM leaderboard.leaderboard
WHERE id = %s
""",
(leaderboard_id,),
)

res = self.cursor.fetchone()

if res:
task = LeaderboardTask.from_dict(res[3])
return LeaderboardItem(
id=res[0],
name=res[1],
deadline=res[2],
task=task,
creator_id=res[4],
forum_id=res[5],
secret_seed=res[6],
gpu_types=self.get_leaderboard_gpu_types(res[1]),
description=res[7],
visibility=res[8],
)
else:
raise LeaderboardDoesNotExist(str(leaderboard_id))

def check_leaderboard_access(self, leaderboard_name: str, user_id: str) -> bool:
"""Returns True if leaderboard is public or user has claimed an invite covering this leaderboard."""
self.cursor.execute(
Expand Down Expand Up @@ -865,6 +894,14 @@ def get_leaderboard_submissions(
if user_id:
# Query all if user_id (means called from show-personal)
query = """
WITH submission_counts AS (
SELECT s.user_id, r.runner, COUNT(DISTINCT s.id) AS submission_count
FROM leaderboard.submission s
JOIN leaderboard.runs r ON r.submission_id = s.id
JOIN leaderboard.leaderboard l ON s.leaderboard_id = l.id
WHERE l.name = %s
GROUP BY s.user_id, r.runner
)
SELECT
s.file_name,
s.id,
Expand All @@ -873,11 +910,13 @@ def get_leaderboard_submissions(
r.score,
r.runner,
ui.user_name,
RANK() OVER (ORDER BY r.score ASC) as rank
RANK() OVER (ORDER BY r.score ASC) as rank,
COALESCE(sc.submission_count, 0) AS submission_count
FROM leaderboard.runs r
JOIN leaderboard.submission s ON r.submission_id = s.id
JOIN leaderboard.leaderboard l ON s.leaderboard_id = l.id
JOIN leaderboard.user_info ui ON s.user_id = ui.id
LEFT JOIN submission_counts sc ON s.user_id = sc.user_id AND r.runner = sc.runner
WHERE l.name = %s
AND r.runner = %s
AND NOT r.secret
Expand All @@ -895,11 +934,19 @@ def get_leaderboard_submissions(
ORDER BY r.score ASC
LIMIT %s OFFSET %s
"""
args = (leaderboard_name, gpu_name, user_id, limit, offset)
args = (leaderboard_name, leaderboard_name, gpu_name, user_id, limit, offset)
else:
# Query best submission per user if no user_id (means called from show)
query = """
WITH best_submissions AS (
WITH submission_counts AS (
SELECT s.user_id, r.runner, COUNT(DISTINCT s.id) AS submission_count
FROM leaderboard.submission s
JOIN leaderboard.runs r ON r.submission_id = s.id
JOIN leaderboard.leaderboard l ON s.leaderboard_id = l.id
WHERE l.name = %s
GROUP BY s.user_id, r.runner
),
best_submissions AS (
SELECT DISTINCT ON (s.user_id)
s.id as submission_id,
s.file_name,
Expand Down Expand Up @@ -931,13 +978,15 @@ def get_leaderboard_submissions(
bs.score,
bs.runner,
ui.user_name,
RANK() OVER (ORDER BY bs.score ASC) as rank
RANK() OVER (ORDER BY bs.score ASC) as rank,
COALESCE(sc.submission_count, 0) AS submission_count
FROM best_submissions bs
JOIN leaderboard.user_info ui ON bs.user_id = ui.id
LEFT JOIN submission_counts sc ON bs.user_id = sc.user_id AND bs.runner = sc.runner
ORDER BY bs.score ASC
LIMIT %s OFFSET %s
"""
args = (leaderboard_name, gpu_name, limit, offset)
args = (leaderboard_name, leaderboard_name, gpu_name, limit, offset)

self.cursor.execute(query, args)

Expand All @@ -950,6 +999,7 @@ def get_leaderboard_submissions(
submission_score=submission[4],
user_name=submission[6],
rank=submission[7],
submission_count=submission[8],
leaderboard_name=leaderboard_name,
gpu_type=gpu_name,
)
Expand Down
77 changes: 77 additions & 0 deletions tests/test_admin_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,83 @@ def test_public_leaderboard_submissions_no_auth(self, test_client, mock_backend)

response = test_client.get("/submissions/public-lb/A100")
assert response.status_code == 200

def test_public_leaderboard_rankings_no_auth(self, test_client, mock_backend):
"""GET /leaderboard/{id}/rankings returns metadata and all GPU rankings."""
from libkernelbot.consts import Language
from libkernelbot.task import LeaderboardTask, PythonTaskData

self._setup_db_mock(mock_backend)
task = LeaderboardTask(
lang=Language.Python,
files={"reference.py": "def ref(): pass"},
config=PythonTaskData(main="eval.py"),
benchmarks=[{"n": 32}],
)
mock_backend.db.get_leaderboard_by_id = MagicMock(
return_value={
"id": 123,
"name": "qr_v2",
"deadline": "2026-06-30T00:00:00Z",
"description": "QR",
"task": task,
"gpu_types": ["B200"],
"visibility": "public",
}
)
mock_backend.db.get_leaderboard = MagicMock(return_value={"visibility": "public"})
mock_backend.db.get_leaderboard_submissions = MagicMock(
return_value=[
{
"user_name": "mark",
"submission_score": 1.5,
"submission_name": "submission.py",
"submission_id": 42,
"submission_count": 3,
"submission_time": "2026-06-17T00:00:00Z",
}
]
)

response = test_client.get("/leaderboard/123/rankings")
assert response.status_code == 200
assert response.json() == {
"rankings": {
"B200": [
{
"user_name": "mark",
"score": 1.5,
"file_name": "submission.py",
"submission_id": 42,
"submission_count": 3,
"submission_time": "2026-06-17T00:00:00Z",
}
]
},
"leaderboard": {
"name": "qr_v2",
"deadline": "2026-06-30T00:00:00Z",
"lang": "py",
"description": "QR",
"reference": "def ref(): pass",
"benchmarks": [{"n": 32}],
"gpu_types": ["B200"],
},
}

def test_missing_leaderboard_rankings_returns_404(self, test_client, mock_backend):
"""GET /leaderboard/{id}/rankings returns 404 for a missing leaderboard."""
from libkernelbot.leaderboard_db import LeaderboardDoesNotExist

self._setup_db_mock(mock_backend)
mock_backend.db.get_leaderboard_by_id = MagicMock(
side_effect=LeaderboardDoesNotExist("999")
)

response = test_client.get("/leaderboard/999/rankings")
assert response.status_code == 404


class TestAdminExportHF:
"""Test admin HF export endpoint."""

Expand Down
2 changes: 2 additions & 0 deletions tests/test_leaderboard_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ def test_leaderboard_submission_ranked(database, submit_leaderboard):
"rank": 1,
"submission_id": 2,
"submission_name": "submission.py",
"submission_count": 3,
"submission_score": Decimal("4.5"),
"submission_time": submit_time,
"user_id": "5",
Expand All @@ -361,6 +362,7 @@ def test_leaderboard_submission_ranked(database, submit_leaderboard):
"rank": 2,
"submission_id": 4,
"submission_name": "submission.py",
"submission_count": 1,
"submission_score": Decimal("8.0"),
"submission_time": submit_time,
"user_id": "6",
Expand Down
Loading