import dataclasses import datetime import json from typing import Dict, List, Optional import psycopg2 from libkernelbot.db_types import ( IdentityType, LeaderboardItem, LeaderboardRankedEntry, RunItem, SubmissionItem, ) from libkernelbot.run_eval import CompileResult, RunResult, SystemInfo from libkernelbot.task import LeaderboardDefinition, LeaderboardTask from libkernelbot.utils import ( KernelBotError, LRUCache, setup_logging, ) logger = setup_logging(__name__) class LeaderboardDB: def __init__(self, url: str, ssl_mode: str): """Initialize database connection parameters""" self.url = url self.ssl_mode = ssl_mode self.connection: Optional[psycopg2.extensions.connection] = None self.refcount: int = 0 self.cursor: Optional[psycopg2.extensions.cursor] = None self.name_cache = LRUCache(max_size=512) def connect(self) -> bool: """Establish connection to the database""" try: self.connection = psycopg2.connect(self.url, sslmode=self.ssl_mode) self.cursor = self.connection.cursor() return True except psycopg2.Error as e: logger.exception("Error connecting to PostgreSQL", exc_info=e) return False def disconnect(self): """Close database connection and cursor""" if self.cursor: self.cursor.close() if self.connection: self.connection.close() self.cursor = None self.connection = None def __enter__(self) -> "LeaderboardDB": """Context manager entry""" if self.connection is not None: self.refcount += 1 return self if self.connect(): self.refcount = 1 return self raise KernelBotError("Could not connect to database", code=500) def __exit__(self, exc_type, exc_val, exc_tb): """Context manager exit""" self.refcount -= 1 if self.refcount == 0: self.disconnect() def create_leaderboard( self, *, name: str, deadline: datetime.datetime, definition: LeaderboardDefinition, creator_id: int, forum_id: int, gpu_types: list | str, ) -> int: # to prevent surprises, ensure we have specified a timezone try: task = definition.task self.cursor.execute( """ INSERT INTO leaderboard.leaderboard (name, deadline, task, creator_id, forum_id, description) VALUES (%s, %s, %s, %s, %s, %s) RETURNING id """, (name, deadline, task.to_str(), creator_id, forum_id, definition.description), ) leaderboard_id = self.cursor.fetchone()[0] if isinstance(gpu_types, str): gpu_types = [gpu_types] for gpu_type in gpu_types: self.cursor.execute( """ INSERT INTO leaderboard.gpu_type (leaderboard_id, gpu_type) VALUES (%s, %s) """, (leaderboard_id, gpu_type), ) # insert templates for lang, code in definition.templates.items(): self.cursor.execute( """ INSERT INTO leaderboard.templates (leaderboard_id, lang, code) VALUES (%s, %s, %s) """, (leaderboard_id, lang, code), ) self.connection.commit() self.name_cache.invalidate() # Invalidate autocomplete cache return leaderboard_id except psycopg2.Error as e: logger.exception("Error in leaderboard creation.", exc_info=e) if isinstance(e, psycopg2.errors.UniqueViolation): raise KernelBotError( f"Error: Tried to create a leaderboard '{name}' that already exists." ) from e self.connection.rollback() # Ensure rollback if error occurs raise KernelBotError("Error in leaderboard creation.") from e def update_leaderboard( self, name, deadline: datetime.datetime, definition: LeaderboardDefinition ): task = definition.task try: lb_id = self.get_leaderboard_id(name) self.cursor.execute( """ UPDATE leaderboard.leaderboard SET deadline = %s, task = %s, description = %s WHERE id = %s; """, ( deadline.astimezone(datetime.timezone.utc), task.to_str(), definition.description, lb_id, ), ) # replace templates self.cursor.execute( """ DELETE FROM leaderboard.templates WHERE leaderboard_id = %s """, (lb_id,), ) for lang, code in definition.templates.items(): self.cursor.execute( """ INSERT INTO leaderboard.templates (leaderboard_id, lang, code) VALUES (%s, %s, %s) """, (lb_id, lang, code), ) self.connection.commit() except psycopg2.Error as e: self.connection.rollback() logger.exception("Error during leaderboard update", exc_info=e) raise KernelBotError("Error during leaderboard update") from e def delete_leaderboard(self, leaderboard_name: str, force: bool = False): try: if force: self.cursor.execute( """ DELETE FROM leaderboard.runs WHERE submission_id IN ( SELECT leaderboard.submission.id FROM leaderboard.submission WHERE leaderboard.submission.leaderboard_id IN ( SELECT leaderboard.leaderboard.id FROM leaderboard.leaderboard WHERE leaderboard.leaderboard.name = %s ) ); """, (leaderboard_name,), ) self.cursor.execute( """ DELETE FROM leaderboard.submission USING leaderboard.leaderboard WHERE leaderboard.submission.leaderboard_id = leaderboard.leaderboard.id AND leaderboard.leaderboard.name = %s; """, (leaderboard_name,), ) self.cursor.execute( """ DELETE FROM leaderboard.templates USING leaderboard.leaderboard WHERE leaderboard.templates.leaderboard_id = leaderboard.leaderboard.id AND leaderboard.leaderboard.name = %s; """, (leaderboard_name,), ) self.cursor.execute( """ DELETE FROM leaderboard.leaderboard WHERE name = %s """, (leaderboard_name,), ) self.connection.commit() self.name_cache.invalidate() # Invalidate autocomplete cache except psycopg2.Error as e: self.connection.rollback() if isinstance(e, psycopg2.errors.ForeignKeyViolation): raise KernelBotError( f"Could not delete leaderboard `{leaderboard_name}` with existing submissions." ) from e logger.exception("Could not delete leaderboard %s.", leaderboard_name, exc_info=e) raise KernelBotError(f"Could not delete leaderboard `{leaderboard_name}`.") from e def validate_identity( self, identifier: str, id_type: IdentityType, ) -> Optional[dict[str, str]]: """ Validate an identity (CLI or Web) and return {user_id, user_name} if found. Args: identifier: The identifier value (CLI ID or Web Auth ID). id_type: IdentityType enum (IdentityType.CLI or IdentityType.WEB). Returns: Optional[dict[str, str]]: {"user_id": ..., "user_name": ...} if valid; else None. """ where_by_type = { IdentityType.CLI: ("cli_id = %s AND cli_valid = TRUE", "CLI ID"), IdentityType.WEB: ("web_auth_id = %s", "WEB AUTH ID"), } where_clause, human_label = where_by_type[id_type] try: self.cursor.execute( f""" SELECT id, user_name FROM leaderboard.user_info WHERE {where_clause} """, (identifier,), ) row = self.cursor.fetchone() return ( {"user_id": row[0], "user_name": row[1], "id_type": id_type.value} if row else None ) except psycopg2.Error as e: self.connection.rollback() logger.exception("Error validating %s %s", human_label, identifier, exc_info=e) raise KernelBotError(f"Error validating {human_label}") from e def create_submission( self, leaderboard: str, file_name: str, user_id: int, code: str, time: datetime.datetime, user_name: str = None, ) -> Optional[int]: try: # check if we already have the code self.cursor.execute( """ SELECT id, code FROM leaderboard.code_files WHERE hash = encode(sha256(%s), 'hex') """, (code.encode("utf-8"),), ) code_id = None for candidate in self.cursor.fetchall(): if bytes(candidate[1]).decode("utf-8") == code: code_id = candidate[0] break if code_id is None: # a genuinely new submission self.cursor.execute( """ INSERT INTO leaderboard.code_files (CODE) VALUES (%s) RETURNING id """, (code.encode("utf-8"),), ) code_id = self.cursor.fetchone() # Check if user exists in user_info, if not add them self.cursor.execute( """ SELECT 1 FROM leaderboard.user_info WHERE id = %s """, (str(user_id),), ) if not self.cursor.fetchone(): self.cursor.execute( """ INSERT INTO leaderboard.user_info (id, user_name) VALUES (%s, %s) """, (str(user_id), user_name), ) self.cursor.execute( """ INSERT INTO leaderboard.submission (leaderboard_id, file_name, user_id, code_id, submission_time) VALUES ( (SELECT id FROM leaderboard.leaderboard WHERE name = %s), %s, %s, %s, %s) RETURNING id """, ( leaderboard, file_name, user_id, code_id, time, ), ) submission_id = self.cursor.fetchone()[0] assert submission_id is not None self.connection.commit() return submission_id except psycopg2.Error as e: logger.error( "Error during creation of submission for leaderboard '%s' by user '%s'", leaderboard, user_id, exc_info=e, ) self.connection.rollback() # Ensure rollback if error occurs raise KernelBotError("Error during creation of submission") from e def mark_submission_done( self, submission: int, ) -> Optional[int]: try: self.cursor.execute( """ UPDATE leaderboard.submission SET done = TRUE WHERE id = %s """, (submission,), ) self.connection.commit() except psycopg2.Error as e: logger.error("Could not mark submission '%s' as done.", submission, exc_info=e) self.connection.rollback() # Ensure rollback if error occurs raise KernelBotError("Error while finalizing submission") from e def update_heartbeat_if_active(self, sub_id: int, ts: datetime.datetime) -> None: try: self.cursor.execute( """ UPDATE leaderboard.submission_job_status SET last_heartbeat = %s WHERE submission_id = %s AND status IN ('pending','running') """, (ts, sub_id), ) self.connection.commit() except psycopg2.Error as e: self.connection.rollback() logger.error("Failed to upsert submission job status. sub_id: '%s'", sub_id, exc_info=e) raise KernelBotError("Error updating job status") from e def upsert_submission_job_status( self, sub_id: int, status: str | None = None, error: str | None = None, last_heartbeat: datetime.datetime | None = None, ) -> int: try: self.cursor.execute( """ INSERT INTO leaderboard.submission_job_status AS s (submission_id, status, error, last_heartbeat) VALUES (%s, %s, %s, %s) ON CONFLICT (submission_id) DO UPDATE SET status = COALESCE(EXCLUDED.status, s.status), error = COALESCE(EXCLUDED.error, s.error), last_heartbeat = COALESCE(EXCLUDED.last_heartbeat, s.last_heartbeat) RETURNING id; """, (sub_id, status, error, last_heartbeat), ) job_id = self.cursor.fetchone()[0] self.connection.commit() return int(job_id) except psycopg2.Error as e: self.connection.rollback() logger.error("Failed to upsert submission job status. sub_id: '%s'", sub_id, exc_info=e) raise KernelBotError("Error updating job status") from e def create_submission_run( self, submission: int, start: datetime.datetime, end: datetime.datetime, mode: str, secret: bool, runner: str, score: Optional[float], compilation: Optional[CompileResult], result: RunResult, system: SystemInfo, ): try: if compilation is not None: compilation = json.dumps(dataclasses.asdict(compilation)) # check validity self.cursor.execute( """ SELECT done FROM leaderboard.submission WHERE id = %s """, (submission,), ) if self.cursor.fetchone()[0]: logger.error( "Submission '%s' is already marked as done when trying to add %s run.", submission, mode, ) raise KernelBotError( "Internal error: Attempted to add run, " "but submission was already marked as done." ) meta = { k: result.__dict__[k] for k in ["stdout", "stderr", "success", "exit_code", "command", "duration"] } self.cursor.execute( """ INSERT INTO leaderboard.runs (submission_id, start_time, end_time, mode, secret, runner, score, passed, compilation, meta, result, system_info ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """, ( submission, start, end, mode, secret, runner, score, result.passed, compilation, json.dumps(meta), json.dumps(result.result), json.dumps(dataclasses.asdict(system)), ), ) self.connection.commit() except psycopg2.Error as e: logger.exception( "Error during adding %s run on %s for submission '%s'", mode, runner, submission, exc_info=e, ) self.connection.rollback() # Ensure rollback if error occurs raise KernelBotError("Could not create leaderboard submission entry in database") from e def get_leaderboard_names(self, active_only: bool = False) -> list[str]: if active_only: self.cursor.execute( "SELECT name FROM leaderboard.leaderboard WHERE leaderboard.deadline > %s", (datetime.datetime.now().astimezone(datetime.timezone.utc),), ) else: self.cursor.execute("SELECT name FROM leaderboard.leaderboard") return [x[0] for x in self.cursor.fetchall()] def get_leaderboards(self) -> list["LeaderboardItem"]: self.cursor.execute( """ SELECT id, name, deadline, task, creator_id, forum_id, description, secret_seed FROM leaderboard.leaderboard """ ) lbs = self.cursor.fetchall() leaderboards = [] for lb in lbs: self.cursor.execute( "SELECT * from leaderboard.gpu_type WHERE leaderboard_id = %s", [lb[0]] ) gpu_types = [x[1] for x in self.cursor.fetchall()] leaderboards.append( LeaderboardItem( id=lb[0], name=lb[1], deadline=lb[2], task=LeaderboardTask.from_dict(lb[3]), gpu_types=gpu_types, creator_id=lb[4], forum_id=lb[5], description=lb[6], secret_seed=lb[7], ) ) return leaderboards def get_leaderboard_gpu_types(self, leaderboard_name: str) -> List[str]: self.cursor.execute( """ SELECT id FROM leaderboard.leaderboard WHERE name = %s """, (leaderboard_name,), ) lb_id = self.cursor.fetchone() if lb_id is None: raise LeaderboardDoesNotExist(leaderboard_name) self.cursor.execute( """ SELECT gpu_type FROM leaderboard.gpu_type WHERE leaderboard_id = %s """, (lb_id[0],), ) return [x[0] for x in self.cursor.fetchall()] def get_leaderboard_id(self, leaderboard_name: str) -> int: self.cursor.execute( """ SELECT id FROM leaderboard.leaderboard WHERE name = %s """, (leaderboard_name,), ) lb_id = self.cursor.fetchone() if lb_id is None: raise LeaderboardDoesNotExist(leaderboard_name) return lb_id[0] def get_leaderboard_templates(self, leaderboard_name: str) -> Dict[str, str]: lb_id = self.get_leaderboard_id(leaderboard_name) self.cursor.execute( """ SELECT lang, code FROM leaderboard.templates WHERE leaderboard_id = %s """, (lb_id,), ) return {x[0]: x[1] for x in self.cursor.fetchall()} def get_leaderboard(self, leaderboard_name: str) -> "LeaderboardItem": self.cursor.execute( """ SELECT id, name, deadline, task, creator_id, forum_id, secret_seed, description FROM leaderboard.leaderboard WHERE name = %s """, (leaderboard_name,), ) 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], ) else: raise LeaderboardDoesNotExist(leaderboard_name) def get_leaderboard_submissions( self, leaderboard_name: str, gpu_name: str, user_id: Optional[str] = None, limit: int = None, offset: int = 0, ) -> list["LeaderboardRankedEntry"]: # separate cases, for personal we want all submissions, for general we want best per user if user_id: # Query all if user_id (means called from show-personal) query = """ SELECT s.file_name, s.id, s.user_id, s.submission_time, r.score, r.runner, ui.user_name, RANK() OVER (ORDER BY r.score ASC) as rank 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 WHERE l.name = %s AND r.runner = %s AND NOT r.secret AND r.score IS NOT NULL AND r.passed AND s.user_id = %s ORDER BY r.score ASC LIMIT %s OFFSET %s """ args = (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 ( SELECT DISTINCT ON (s.user_id) s.id as submission_id, s.file_name, s.user_id, s.submission_time, r.score, r.runner 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 WHERE l.name = %s AND r.runner = %s AND NOT r.secret AND r.score IS NOT NULL AND r.passed ORDER BY s.user_id, r.score ASC ) SELECT bs.file_name, bs.submission_id, bs.user_id, bs.submission_time, bs.score, bs.runner, ui.user_name, RANK() OVER (ORDER BY bs.score ASC) as rank FROM best_submissions bs JOIN leaderboard.user_info ui ON bs.user_id = ui.id ORDER BY bs.score ASC LIMIT %s OFFSET %s """ args = (leaderboard_name, gpu_name, limit, offset) self.cursor.execute(query, args) result = [ LeaderboardRankedEntry( submission_name=submission[0], submission_id=submission[1], user_id=submission[2], submission_time=submission[3], submission_score=submission[4], user_name=submission[6], rank=submission[7], leaderboard_name=leaderboard_name, gpu_type=gpu_name, ) for submission in self.cursor.fetchall() ] if len(result) == 0: # try to diagnose why we didn't get anything # this will raise if the LB does not exist at all. self.get_leaderboard_id(leaderboard_name) # did we specify a valid GPU? gpus = self.get_leaderboard_gpu_types(leaderboard_name) if gpu_name not in gpus: raise KernelBotError( f"Invalid GPU type '{gpu_name}' for leaderboard '{leaderboard_name}'" ) return result def generate_stats(self, last_day: bool): try: return self._generate_stats(last_day) 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 "" # per-runner stats self.cursor.execute( f""" SELECT runner, COUNT(*), COUNT(*) FILTER (WHERE passed), COUNT(score), COUNT(*) FILTER (WHERE secret), MAX(runs.start_time - s.submission_time), 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} GROUP BY runner; """ ) result = {} for row in self.cursor.fetchall(): result[f"num_run.{row[0]}"] = row[1] result[f"runs_passed.{row[0]}"] = row[2] result[f"runs_scored.{row[0]}"] = row[3] result[f"runs_secret.{row[0]}"] = row[4] result[f"max_delay.{row[0]}"] = row[5] result[f"avg_delay.{row[0]}"] = row[6] result[f"total_runtime.{row[0]}"] = row[7] return result def _generate_submission_stats(self, last_day: bool = False): select_expr = "WHERE NOW() - submission_time <= interval '24 hours'" if last_day else "" self.cursor.execute( f""" SELECT COUNT(*), COUNT(*) FILTER (WHERE NOT done), COUNT(DISTINCT user_id) FROM leaderboard.submission {select_expr} ; """ ) num_sub, num_sub_wait, num_users = self.cursor.fetchone() return { "num_submissions": num_sub, "sub_waiting": num_sub_wait, "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)) # code-level stats if not last_day: self.cursor.execute( """ SELECT COUNT(*) FROM leaderboard.code_files; """ ) result["num_unique_codes"] = self.cursor.fetchone()[0] else: # calculate heavy hitters self.cursor.execute( """ 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' ) SELECT user_id, SUM(duration) AS total FROM run_durations GROUP BY user_id ORDER BY total DESC LIMIT 10; """ ) for row in self.cursor.fetchall(): result[f"total.{row[0]}"] = row[1] return result def get_user_from_id(self, id: str) -> Optional[str]: try: self.cursor.execute( """ SELECT user_name FROM leaderboard.user_info WHERE id = %s """, (id,), ) return self.cursor.fetchone()[0] except Exception: return None def delete_submission(self, submission_id: int): try: # first, the runs query = """ DELETE FROM leaderboard.runs WHERE submission_id = %s """ self.cursor.execute(query, (submission_id,)) # next, the submission itself query = """ DELETE FROM leaderboard.submission WHERE id = %s """ self.cursor.execute(query, (submission_id,)) # TODO delete code file? Could be one-to-many mapping, so we'd need # to figure out if it is used elsewhere first. self.connection.commit() except psycopg2.Error as e: self.connection.rollback() logger.exception("Could not delete submission %s.", submission_id, exc_info=e) raise KernelBotError(f"Could not delete submission {submission_id}!") from e def get_submission_by_id(self, submission_id: int) -> Optional["SubmissionItem"]: query = """ SELECT s.leaderboard_id, lb.name, s.file_name, s.user_id, s.submission_time, s.done, c.code FROM leaderboard.submission s JOIN leaderboard.code_files c ON s.code_id = c.id JOIN leaderboard.leaderboard lb ON s.leaderboard_id = lb.id WHERE s.id = %s """ self.cursor.execute(query, (submission_id,)) submission = self.cursor.fetchone() if submission is None: return None # OK, now get the runs query = """ SELECT start_time, end_time, mode, secret, runner, score, passed, compilation, meta, result, system_info FROM leaderboard.runs WHERE submission_id = %s """ self.cursor.execute(query, (submission_id,)) runs = self.cursor.fetchall() runs = [ RunItem( start_time=r[0], end_time=r[1], mode=r[2], secret=r[3], runner=r[4], score=r[5], passed=r[6], compilation=r[7], meta=r[8], result=r[9], system=r[10], ) for r in runs ] return SubmissionItem( submission_id=submission_id, leaderboard_id=submission[0], leaderboard_name=submission[1], file_name=submission[2], user_id=submission[3], submission_time=submission[4], done=submission[5], code=bytes(submission[6]).decode("utf-8"), runs=runs, ) def get_leaderboard_submission_count( self, leaderboard_name: str, gpu_name: str, user_id: Optional[str] = None, ) -> int: """Get the total count of submissions for a leaderboard""" if user_id: query = """ SELECT 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 WHERE l.name = %s AND r.runner = %s AND NOT r.secret AND r.score IS NOT NULL AND r.passed AND s.user_id = %s """ args = (leaderboard_name, gpu_name, user_id) else: query = """ SELECT COUNT(DISTINCT s.user_id) FROM leaderboard.runs r JOIN leaderboard.submission s ON r.submission_id = s.id JOIN leaderboard.leaderboard l ON s.leaderboard_id = l.id WHERE l.name = %s AND r.runner = %s AND NOT r.secret AND r.score IS NOT NULL AND r.passed """ args = (leaderboard_name, gpu_name) self.cursor.execute(query, args) count = self.cursor.fetchone()[0] if count == 0: # try to diagnose why we didn't get anything # this will raise if the LB does not exist at all. self.get_leaderboard_id(leaderboard_name) # did we specify a valid GPU? gpus = self.get_leaderboard_gpu_types(leaderboard_name) if gpu_name not in gpus: raise KernelBotError( f"Invalid GPU type '{gpu_name}' for leaderboard '{leaderboard_name}'" ) return count def init_user_from_cli(self, cli_id: str, auth_provider: str): """ Initialize a user from CLI authentication flow. Checks if cli_id already exists, and if so returns an error. Creates a temporary user entry with the auth provider and cli_id. Args: cli_id (str): The unique identifier from the CLI auth_provider (str): The authentication provider ('discord' or 'github') Raises: KernelBotError: If auth provider is invalid or cli_id already exists """ if auth_provider not in ["discord", "github"]: raise Exception("Invalid auth provider") try: # Check if cli_id already exists self.cursor.execute( """ SELECT 1 FROM leaderboard.user_info WHERE cli_id = %s """, (cli_id,), ) if self.cursor.fetchone(): raise Exception("CLI ID already exists") self.cursor.execute( """ INSERT INTO leaderboard.user_info (id, user_name, cli_id, cli_auth_provider, cli_valid) VALUES (%s, %s, %s, %s, %s) """, (f"temp_{cli_id}", f"temp_user_{cli_id}", cli_id, auth_provider, False), ) self.connection.commit() except psycopg2.Error as e: self.connection.rollback() logger.exception("Error initializing user from CLI with ID %s", cli_id, exc_info=e) raise KernelBotError("Error initializing user from CLI") from e def create_user_from_cli(self, user_id: str, user_name: str, cli_id: str, auth_provider: str): """ Method to create a user from the CLI. Shouldn't be used for Discord. Validates that the user doesn't already have a valid row and that the user_id/user_name are temporary values that need to be updated. """ try: self.cursor.execute( """ SELECT 1 FROM leaderboard.user_info WHERE id = %s """, (user_id,), ) if self.cursor.fetchone(): raise Exception( "User already has a valid account with this User ID." "Please use the re-register command to re-authenticate." ) self.cursor.execute( """ SELECT cli_valid FROM leaderboard.user_info WHERE cli_id = %s AND cli_valid = TRUE AND cli_auth_provider = %s """, (cli_id, auth_provider), ) if self.cursor.fetchone(): raise Exception( "User already has a valid account with this CLI ID." "Please use the re-register command to re-authenticate." ) self.cursor.execute( """ UPDATE leaderboard.user_info SET id = %s, user_name = %s, cli_valid = TRUE, cli_auth_provider = %s WHERE cli_id = %s AND cli_valid = FALSE """, (user_id, user_name, auth_provider, cli_id), ) if self.cursor.rowcount == 0: raise Exception("No temporary user found with this CLI ID. No effect.") self.connection.commit() except psycopg2.Error as e: self.connection.rollback() logger.exception("Could not create/update user %s from CLI.", user_id, exc_info=e) raise KernelBotError("Database error while creating/updating user from CLI") from e def reset_user_from_cli(self, user_id: str, cli_id: str, auth_provider: str): try: self.cursor.execute( """ SELECT 1 FROM leaderboard.user_info WHERE id = %s """, (user_id,), ) if not self.cursor.fetchone(): raise Exception( "User not found. Please use the register command to create an account." ) self.cursor.execute( """ UPDATE leaderboard.user_info SET cli_id = %s, cli_auth_provider = %s, cli_valid = TRUE WHERE id = %s """, (cli_id, auth_provider, user_id), ) self.connection.commit() except psycopg2.Error as e: self.connection.rollback() logger.exception("Could not reset user %s from CLI.", user_id, exc_info=e) raise KernelBotError("Database error while resetting user from CLI") from e def cleanup_temp_users(self): try: self.cursor.execute( """ DELETE FROM leaderboard.user_info WHERE cli_valid = FALSE and created_at < NOW() - INTERVAL '10 minutes' AND id LIKE 'temp_%' AND user_name LIKE 'temp_%' """ ) self.connection.commit() except psycopg2.Error as e: self.connection.rollback() logger.exception("Could not cleanup temp users", exc_info=e) raise KernelBotError("Database error while cleaning up temp users") from e def validate_cli_id(self, cli_id: str) -> Optional[dict[str, str]]: """ Validates a CLI ID and returns the associated user ID if valid. Args: cli_id (str): The CLI ID to validate. Returns: Optional[str]: The user ID if the CLI ID is valid, otherwise None. """ try: self.cursor.execute( """ SELECT id, user_name FROM leaderboard.user_info WHERE cli_id = %s AND cli_valid = TRUE """, (cli_id,), ) result = self.cursor.fetchone() return {"user_id": result[0], "user_name": result[1]} if result else None except psycopg2.Error as e: self.connection.rollback() logger.exception("Error validating CLI ID %s", cli_id, exc_info=e) raise KernelBotError("Error validating CLI ID") from e class LeaderboardDoesNotExist(KernelBotError): def __init__(self, name: str): super().__init__(message=f"Leaderboard `{name}` does not exist.", code=404)