Skip to content

Commit d6ec7e1

Browse files
author
Mark Saroufim
committed
Add user submission management API endpoints
- Add get_user_submissions() DB method to query user's submissions - Add GET /user/submissions endpoint to list user's submissions - Add GET /user/submissions/{id} endpoint to view submission with code - Add DELETE /user/submissions/{id} endpoint to delete own submissions - All endpoints verify ownership before allowing access
1 parent 1eb2687 commit d6ec7e1

2 files changed

Lines changed: 196 additions & 0 deletions

File tree

src/kernelbot/api/main.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,3 +711,123 @@ async def get_submission_count(
711711
return {"count": count}
712712
except Exception as e:
713713
raise HTTPException(status_code=500, detail=f"Error fetching submission count: {e}") from e
714+
715+
716+
@app.get("/user/submissions")
717+
async def get_user_submissions(
718+
user_info: Annotated[dict, Depends(validate_user_header)],
719+
leaderboard: Optional[str] = None,
720+
limit: int = 50,
721+
offset: int = 0,
722+
db_context=Depends(get_db),
723+
) -> list[dict]:
724+
"""Get the authenticated user's submissions.
725+
726+
Args:
727+
leaderboard: Optional filter by leaderboard name
728+
limit: Maximum number of submissions to return (default 50)
729+
offset: Offset for pagination
730+
731+
Returns:
732+
List of user's submissions with summary info
733+
"""
734+
await simple_rate_limit()
735+
try:
736+
with db_context as db:
737+
return db.get_user_submissions(
738+
user_id=user_info["user_id"],
739+
leaderboard_name=leaderboard,
740+
limit=limit,
741+
offset=offset,
742+
)
743+
except Exception as e:
744+
raise HTTPException(status_code=500, detail=f"Error fetching user submissions: {e}") from e
745+
746+
747+
@app.get("/user/submissions/{submission_id}")
748+
async def get_user_submission(
749+
submission_id: int,
750+
user_info: Annotated[dict, Depends(validate_user_header)],
751+
db_context=Depends(get_db),
752+
) -> dict:
753+
"""Get a specific submission by ID. Only the owner can view their submission.
754+
755+
Args:
756+
submission_id: The submission ID to retrieve
757+
758+
Returns:
759+
Full submission details including code
760+
"""
761+
await simple_rate_limit()
762+
try:
763+
with db_context as db:
764+
submission = db.get_submission_by_id(submission_id)
765+
766+
if submission is None:
767+
raise HTTPException(status_code=404, detail="Submission not found")
768+
769+
# Verify ownership
770+
if str(submission.user_id) != str(user_info["user_id"]):
771+
raise HTTPException(status_code=403, detail="Not authorized to view this submission")
772+
773+
return {
774+
"id": submission.submission_id,
775+
"leaderboard_id": submission.leaderboard_id,
776+
"leaderboard_name": submission.leaderboard_name,
777+
"file_name": submission.file_name,
778+
"user_id": submission.user_id,
779+
"submission_time": submission.submission_time,
780+
"done": submission.done,
781+
"code": submission.code,
782+
"runs": [
783+
{
784+
"start_time": r.start_time,
785+
"end_time": r.end_time,
786+
"mode": r.mode,
787+
"secret": r.secret,
788+
"runner": r.runner,
789+
"score": r.score,
790+
"passed": r.passed,
791+
}
792+
for r in submission.runs
793+
],
794+
}
795+
except HTTPException:
796+
raise
797+
except Exception as e:
798+
raise HTTPException(status_code=500, detail=f"Error fetching submission: {e}") from e
799+
800+
801+
@app.delete("/user/submissions/{submission_id}")
802+
async def delete_user_submission(
803+
submission_id: int,
804+
user_info: Annotated[dict, Depends(validate_user_header)],
805+
db_context=Depends(get_db),
806+
) -> dict:
807+
"""Delete a submission by ID. Only the owner can delete their submission.
808+
809+
Args:
810+
submission_id: The submission ID to delete
811+
812+
Returns:
813+
Success message
814+
"""
815+
await simple_rate_limit()
816+
try:
817+
with db_context as db:
818+
submission = db.get_submission_by_id(submission_id)
819+
820+
if submission is None:
821+
raise HTTPException(status_code=404, detail="Submission not found")
822+
823+
# Verify ownership
824+
if str(submission.user_id) != str(user_info["user_id"]):
825+
raise HTTPException(status_code=403, detail="Not authorized to delete this submission")
826+
827+
db.delete_submission(submission_id)
828+
829+
return {"message": f"Submission {submission_id} deleted successfully"}
830+
except HTTPException:
831+
raise
832+
except Exception as e:
833+
raise HTTPException(status_code=500, detail=f"Error deleting submission: {e}") from e

src/libkernelbot/leaderboard_db.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,82 @@ def delete_submission(self, submission_id: int):
848848
logger.exception("Could not delete submission %s.", submission_id, exc_info=e)
849849
raise KernelBotError(f"Could not delete submission {submission_id}!") from e
850850

851+
def get_user_submissions(
852+
self,
853+
user_id: str,
854+
leaderboard_name: Optional[str] = None,
855+
limit: int = 50,
856+
offset: int = 0,
857+
) -> list[dict]:
858+
"""
859+
Get submissions for a specific user.
860+
861+
Args:
862+
user_id: The user's ID
863+
leaderboard_name: Optional filter by leaderboard name
864+
limit: Maximum number of submissions to return
865+
offset: Offset for pagination
866+
867+
Returns:
868+
List of submission dictionaries with summary info
869+
"""
870+
try:
871+
if leaderboard_name:
872+
query = """
873+
SELECT
874+
s.id,
875+
lb.name as leaderboard_name,
876+
s.file_name,
877+
s.submission_time,
878+
s.done,
879+
r.runner as gpu_type,
880+
r.score
881+
FROM leaderboard.submission s
882+
JOIN leaderboard.leaderboard lb ON s.leaderboard_id = lb.id
883+
LEFT JOIN leaderboard.runs r ON r.submission_id = s.id
884+
AND NOT r.secret AND r.passed
885+
WHERE s.user_id = %s AND lb.name = %s
886+
ORDER BY s.submission_time DESC
887+
LIMIT %s OFFSET %s
888+
"""
889+
self.cursor.execute(query, (user_id, leaderboard_name, limit, offset))
890+
else:
891+
query = """
892+
SELECT
893+
s.id,
894+
lb.name as leaderboard_name,
895+
s.file_name,
896+
s.submission_time,
897+
s.done,
898+
r.runner as gpu_type,
899+
r.score
900+
FROM leaderboard.submission s
901+
JOIN leaderboard.leaderboard lb ON s.leaderboard_id = lb.id
902+
LEFT JOIN leaderboard.runs r ON r.submission_id = s.id
903+
AND NOT r.secret AND r.passed
904+
WHERE s.user_id = %s
905+
ORDER BY s.submission_time DESC
906+
LIMIT %s OFFSET %s
907+
"""
908+
self.cursor.execute(query, (user_id, limit, offset))
909+
910+
results = []
911+
for row in self.cursor.fetchall():
912+
results.append({
913+
"id": row[0],
914+
"leaderboard_name": row[1],
915+
"file_name": row[2],
916+
"submission_time": row[3],
917+
"done": row[4],
918+
"gpu_type": row[5],
919+
"score": row[6],
920+
})
921+
return results
922+
except psycopg2.Error as e:
923+
self.connection.rollback()
924+
logger.exception("Error fetching user submissions for user %s", user_id, exc_info=e)
925+
raise KernelBotError("Error fetching user submissions") from e
926+
851927
def get_submission_by_id(self, submission_id: int) -> Optional["SubmissionItem"]:
852928
query = """
853929
SELECT s.leaderboard_id, lb.name, s.file_name, s.user_id,

0 commit comments

Comments
 (0)