From ad771dc6f2d0733fe4453444c4c9d7e541f2547e Mon Sep 17 00:00:00 2001 From: Mark Saroufim Date: Tue, 10 Feb 2026 23:35:50 -0800 Subject: [PATCH 1/3] Add optional leaderboard_name filter to admin show-stats Support filtering stats by a specific leaderboard in both the Discord command and the API endpoint. Defaults to all leaderboards when omitted. --- src/kernelbot/api/main.py | 3 +- src/kernelbot/cogs/admin_cog.py | 12 +++- src/libkernelbot/leaderboard_db.py | 96 +++++++++++++++++++++++------- tests/test_admin_api.py | 18 +++++- tests/test_leaderboard_db.py | 23 +++++++ 5 files changed, 126 insertions(+), 26 deletions(-) diff --git a/src/kernelbot/api/main.py b/src/kernelbot/api/main.py index 2ae2bf97..5eda2a15 100644 --- a/src/kernelbot/api/main.py +++ b/src/kernelbot/api/main.py @@ -586,9 +586,10 @@ async def admin_stats( _: Annotated[None, Depends(require_admin)], db_context=Depends(get_db), last_day_only: bool = False, + leaderboard_name: Optional[str] = None, ) -> dict: with db_context as db: - stats = db.generate_stats(last_day_only) + stats = db.generate_stats(last_day_only, leaderboard_name) return {"status": "ok", "stats": stats} diff --git a/src/kernelbot/cogs/admin_cog.py b/src/kernelbot/cogs/admin_cog.py index a2d0f85b..c9e2e9f0 100644 --- a/src/kernelbot/cogs/admin_cog.py +++ b/src/kernelbot/cogs/admin_cog.py @@ -764,8 +764,14 @@ async def update_competition( logger.exception("Error updating problem set", exc_info=e) @with_error_handling - @discord.app_commands.describe(last_day_only="Only show stats for the last day") - async def show_bot_stats(self, interaction: discord.Interaction, last_day_only: bool): + @discord.app_commands.describe( + last_day_only="Only show stats for the last day", + leaderboard_name="Filter stats to a specific leaderboard (default: all)", + ) + @discord.app_commands.autocomplete(leaderboard_name=leaderboard_name_autocomplete) + async def show_bot_stats( + self, interaction: discord.Interaction, last_day_only: bool, leaderboard_name: Optional[str] = None + ): is_admin = await self.admin_check(interaction) if not is_admin: await send_discord_message( @@ -776,7 +782,7 @@ async def show_bot_stats(self, interaction: discord.Interaction, last_day_only: return with self.bot.leaderboard_db as db: - stats = db.generate_stats(last_day_only) + stats = db.generate_stats(last_day_only, leaderboard_name) msg = """```""" for k, v in stats.items(): msg += f"\n{k} = {v}" diff --git a/src/libkernelbot/leaderboard_db.py b/src/libkernelbot/leaderboard_db.py index 334ad633..c176e915 100644 --- a/src/libkernelbot/leaderboard_db.py +++ b/src/libkernelbot/leaderboard_db.py @@ -713,15 +713,28 @@ def get_leaderboard_submissions( return result - def generate_stats(self, last_day: bool): + def generate_stats(self, last_day: bool, leaderboard_name: Optional[str] = None): try: - return self._generate_stats(last_day) + return self._generate_stats(last_day, leaderboard_name) except Exception as e: logger.exception("error generating stats", exc_info=e) raise - def _generate_runner_stats(self, last_day: bool = False): - select_expr = "WHERE NOW() - s.submission_time <= interval '24 hours'" if last_day else "" + def _generate_runner_stats(self, last_day: bool = False, leaderboard_name: Optional[str] = None): + joins = "" + conditions = [] + params = [] + + if leaderboard_name: + joins = "JOIN leaderboard.leaderboard lb ON s.leaderboard_id = lb.id" + conditions.append("lb.name = %s") + params.append(leaderboard_name) + + if last_day: + conditions.append("NOW() - s.submission_time <= interval '24 hours'") + + where_clause = f"WHERE {' AND '.join(conditions)}" if conditions else "" + # per-runner stats self.cursor.execute( f""" @@ -735,9 +748,11 @@ def _generate_runner_stats(self, last_day: bool = False): AVG(runs.start_time - s.submission_time), SUM(runs.end_time - runs.start_time) FROM leaderboard.runs JOIN leaderboard.submission s ON submission_id = s.id - {select_expr} + {joins} + {where_clause} GROUP BY runner; - """ + """, + params, ) result = {} @@ -752,8 +767,21 @@ def _generate_runner_stats(self, last_day: bool = False): return result - def _generate_submission_stats(self, last_day: bool = False): - select_expr = "WHERE NOW() - submission_time <= interval '24 hours'" if last_day else "" + def _generate_submission_stats(self, last_day: bool = False, leaderboard_name: Optional[str] = None): + joins = "" + conditions = [] + params = [] + + if leaderboard_name: + joins = "JOIN leaderboard.leaderboard lb ON leaderboard.submission.leaderboard_id = lb.id" + conditions.append("lb.name = %s") + params.append(leaderboard_name) + + if last_day: + conditions.append("NOW() - submission_time <= interval '24 hours'") + + where_clause = f"WHERE {' AND '.join(conditions)}" if conditions else "" + self.cursor.execute( f""" SELECT @@ -761,9 +789,11 @@ def _generate_submission_stats(self, last_day: bool = False): COUNT(*) FILTER (WHERE NOT done), COUNT(DISTINCT user_id) FROM leaderboard.submission - {select_expr} + {joins} + {where_clause} ; - """ + """, + params, ) num_sub, num_sub_wait, num_users = self.cursor.fetchone() return { @@ -772,30 +802,53 @@ def _generate_submission_stats(self, last_day: bool = False): "num_users": num_users, } - def _generate_stats(self, last_day: bool = False): - result = self._generate_submission_stats(last_day) - result.update(self._generate_runner_stats(last_day)) + def _generate_stats(self, last_day: bool = False, leaderboard_name: Optional[str] = None): + result = self._generate_submission_stats(last_day, leaderboard_name) + result.update(self._generate_runner_stats(last_day, leaderboard_name)) # code-level stats if not last_day: - self.cursor.execute( - """ - SELECT COUNT(*) FROM leaderboard.code_files; - """ - ) + if leaderboard_name: + self.cursor.execute( + """ + SELECT COUNT(DISTINCT s.code_id) + FROM leaderboard.submission s + JOIN leaderboard.leaderboard lb ON s.leaderboard_id = lb.id + WHERE lb.name = %s; + """, + (leaderboard_name,), + ) + else: + self.cursor.execute( + """ + SELECT COUNT(*) FROM leaderboard.code_files; + """ + ) result["num_unique_codes"] = self.cursor.fetchone()[0] else: # calculate heavy hitters + joins = "" + conditions = ["NOW() - s.submission_time <= interval '24 hours'"] + params = [] + + if leaderboard_name: + joins = "JOIN leaderboard.leaderboard lb ON s.leaderboard_id = lb.id" + conditions.append("lb.name = %s") + params.append(leaderboard_name) + + where_clause = f"WHERE {' AND '.join(conditions)}" + self.cursor.execute( - """ + f""" WITH run_durations AS ( SELECT s.user_id AS user_id, r.end_time - r.start_time AS duration FROM leaderboard.runs r JOIN leaderboard.submission s ON r.submission_id = s.id - WHERE NOW() - s.submission_time <= interval '24 hours' + {joins} + {where_clause} ) SELECT user_id, @@ -804,7 +857,8 @@ def _generate_stats(self, last_day: bool = False): GROUP BY user_id ORDER BY total DESC LIMIT 10; - """ + """, + params, ) for row in self.cursor.fetchall(): diff --git a/tests/test_admin_api.py b/tests/test_admin_api.py index ecd1b33c..cfc021f0 100644 --- a/tests/test_admin_api.py +++ b/tests/test_admin_api.py @@ -115,7 +115,23 @@ def test_admin_stats_last_day_only(self, test_client, mock_backend): headers={"Authorization": "Bearer test_token"} ) assert response.status_code == 200 - mock_backend.db.generate_stats.assert_called_once_with(True) + mock_backend.db.generate_stats.assert_called_once_with(True, None) + + def test_admin_stats_with_leaderboard_name(self, test_client, mock_backend): + """GET /admin/stats with leaderboard_name parameter.""" + mock_backend.db.__enter__ = MagicMock(return_value=mock_backend.db) + mock_backend.db.__exit__ = MagicMock(return_value=None) + mock_backend.db.generate_stats = MagicMock(return_value={ + "num_submissions": 5, + "num_users": 3, + }) + + response = test_client.get( + "/admin/stats?leaderboard_name=my-leaderboard", + headers={"Authorization": "Bearer test_token"} + ) + assert response.status_code == 200 + mock_backend.db.generate_stats.assert_called_once_with(False, "my-leaderboard") class TestAdminSubmissions: diff --git a/tests/test_leaderboard_db.py b/tests/test_leaderboard_db.py index 1b349816..a9680ad8 100644 --- a/tests/test_leaderboard_db.py +++ b/tests/test_leaderboard_db.py @@ -604,6 +604,29 @@ def test_generate_stats(database, submit_leaderboard): "total_runtime.A100": datetime.timedelta(seconds=35), } + # Same results when filtering by the correct leaderboard + assert db.generate_stats(False, leaderboard_name="submit-leaderboard") == { + "avg_delay.A100": datetime.timedelta(seconds=10), + "max_delay.A100": datetime.timedelta(seconds=20), + "num_run.A100": 3, + "num_submissions": 1, + "num_unique_codes": 1, + "num_users": 1, + "runs_passed.A100": 3, + "runs_scored.A100": 3, + "runs_secret.A100": 1, + "sub_waiting": 0, + "total_runtime.A100": datetime.timedelta(seconds=35), + } + + # Empty results when filtering by a non-existent leaderboard + assert db.generate_stats(False, leaderboard_name="nonexistent") == { + "num_submissions": 0, + "sub_waiting": 0, + "num_users": 0, + "num_unique_codes": 0, + } + def test_get_user_submissions_empty(database, submit_leaderboard): """Test get_user_submissions returns empty list for user with no submissions""" From 14085507efdd92dc6479541cf734763d06a03499 Mon Sep 17 00:00:00 2001 From: Mark Saroufim Date: Tue, 10 Feb 2026 23:40:36 -0800 Subject: [PATCH 2/3] Extract _stats_filter helper and qualify column references Deduplicate JOIN/WHERE/params construction across stats queries into a shared _stats_filter staticmethod. Alias leaderboard.submission as s in _generate_submission_stats and qualify all column references. --- src/libkernelbot/leaderboard_db.py | 42 +++++++++--------------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/src/libkernelbot/leaderboard_db.py b/src/libkernelbot/leaderboard_db.py index c176e915..f76c00c9 100644 --- a/src/libkernelbot/leaderboard_db.py +++ b/src/libkernelbot/leaderboard_db.py @@ -720,20 +720,25 @@ def generate_stats(self, last_day: bool, leaderboard_name: Optional[str] = None) logger.exception("error generating stats", exc_info=e) raise - def _generate_runner_stats(self, last_day: bool = False, leaderboard_name: Optional[str] = None): + @staticmethod + def _stats_filter(last_day: bool, leaderboard_name: Optional[str], submission_alias: str = "s"): joins = "" conditions = [] params = [] if leaderboard_name: - joins = "JOIN leaderboard.leaderboard lb ON s.leaderboard_id = lb.id" + joins = f"JOIN leaderboard.leaderboard lb ON {submission_alias}.leaderboard_id = lb.id" conditions.append("lb.name = %s") params.append(leaderboard_name) if last_day: - conditions.append("NOW() - s.submission_time <= interval '24 hours'") + conditions.append(f"NOW() - {submission_alias}.submission_time <= interval '24 hours'") where_clause = f"WHERE {' AND '.join(conditions)}" if conditions else "" + return joins, where_clause, params + + def _generate_runner_stats(self, last_day: bool = False, leaderboard_name: Optional[str] = None): + joins, where_clause, params = self._stats_filter(last_day, leaderboard_name) # per-runner stats self.cursor.execute( @@ -768,27 +773,15 @@ def _generate_runner_stats(self, last_day: bool = False, leaderboard_name: Optio return result def _generate_submission_stats(self, last_day: bool = False, leaderboard_name: Optional[str] = None): - joins = "" - conditions = [] - params = [] - - if leaderboard_name: - joins = "JOIN leaderboard.leaderboard lb ON leaderboard.submission.leaderboard_id = lb.id" - conditions.append("lb.name = %s") - params.append(leaderboard_name) - - if last_day: - conditions.append("NOW() - submission_time <= interval '24 hours'") - - where_clause = f"WHERE {' AND '.join(conditions)}" if conditions else "" + joins, where_clause, params = self._stats_filter(last_day, leaderboard_name) self.cursor.execute( f""" SELECT COUNT(*), - COUNT(*) FILTER (WHERE NOT done), - COUNT(DISTINCT user_id) - FROM leaderboard.submission + COUNT(*) FILTER (WHERE NOT s.done), + COUNT(DISTINCT s.user_id) + FROM leaderboard.submission s {joins} {where_clause} ; @@ -828,16 +821,7 @@ def _generate_stats(self, last_day: bool = False, leaderboard_name: Optional[str else: # calculate heavy hitters - joins = "" - conditions = ["NOW() - s.submission_time <= interval '24 hours'"] - params = [] - - if leaderboard_name: - joins = "JOIN leaderboard.leaderboard lb ON s.leaderboard_id = lb.id" - conditions.append("lb.name = %s") - params.append(leaderboard_name) - - where_clause = f"WHERE {' AND '.join(conditions)}" + joins, where_clause, params = self._stats_filter(last_day, leaderboard_name) self.cursor.execute( f""" From b6d7934c1793c93b8fb9da6ee030f6f989054506 Mon Sep 17 00:00:00 2001 From: Mark Saroufim Date: Tue, 10 Feb 2026 23:44:19 -0800 Subject: [PATCH 3/3] Add Query description for leaderboard_name and use flexible assertions Add FastAPI Query metadata so leaderboard_name shows a description in OpenAPI docs. Use call_args-based assertions in tests instead of positional assert_called_once_with to decouple from argument passing style. --- src/kernelbot/api/main.py | 4 ++-- tests/test_admin_api.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/kernelbot/api/main.py b/src/kernelbot/api/main.py index 5eda2a15..78c22f0b 100644 --- a/src/kernelbot/api/main.py +++ b/src/kernelbot/api/main.py @@ -7,7 +7,7 @@ from dataclasses import asdict from typing import Annotated, Any, Optional -from fastapi import Depends, FastAPI, Header, HTTPException, Request, UploadFile +from fastapi import Depends, FastAPI, Header, HTTPException, Query, Request, UploadFile from fastapi.responses import JSONResponse, StreamingResponse from kernelbot.env import env @@ -586,7 +586,7 @@ async def admin_stats( _: Annotated[None, Depends(require_admin)], db_context=Depends(get_db), last_day_only: bool = False, - leaderboard_name: Optional[str] = None, + leaderboard_name: Optional[str] = Query(None, description="Filter stats to a specific leaderboard name"), ) -> dict: with db_context as db: stats = db.generate_stats(last_day_only, leaderboard_name) diff --git a/tests/test_admin_api.py b/tests/test_admin_api.py index cfc021f0..fa4b6751 100644 --- a/tests/test_admin_api.py +++ b/tests/test_admin_api.py @@ -115,7 +115,9 @@ def test_admin_stats_last_day_only(self, test_client, mock_backend): headers={"Authorization": "Bearer test_token"} ) assert response.status_code == 200 - mock_backend.db.generate_stats.assert_called_once_with(True, None) + mock_backend.db.generate_stats.assert_called_once() + args, kwargs = mock_backend.db.generate_stats.call_args + assert args[0] is True # last_day_only def test_admin_stats_with_leaderboard_name(self, test_client, mock_backend): """GET /admin/stats with leaderboard_name parameter.""" @@ -131,7 +133,9 @@ def test_admin_stats_with_leaderboard_name(self, test_client, mock_backend): headers={"Authorization": "Bearer test_token"} ) assert response.status_code == 200 - mock_backend.db.generate_stats.assert_called_once_with(False, "my-leaderboard") + mock_backend.db.generate_stats.assert_called_once() + args, kwargs = mock_backend.db.generate_stats.call_args + assert args[1] == "my-leaderboard" # leaderboard_name class TestAdminSubmissions: