Skip to content

Commit 138f1f6

Browse files
committed
Update
[ghstack-poisoned]
1 parent 0e00df3 commit 138f1f6

7 files changed

Lines changed: 458 additions & 198 deletions

File tree

src/kernelbot/api/api_utils.py

Lines changed: 269 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
import asyncio
2+
from dataclasses import asdict
3+
from datetime import datetime
4+
from re import sub
5+
import time
6+
from typing import Any, Optional, Tuple
17
import requests
2-
from fastapi import HTTPException
3-
8+
from fastapi import HTTPException, UploadFile
9+
from fastapi import BackgroundTasks
10+
import json
411
from kernelbot.env import env
512
from libkernelbot.backend import KernelBackend
613
from libkernelbot.consts import SubmissionMode
@@ -12,21 +19,31 @@
1219
Text,
1320
)
1421
from libkernelbot.submission import SubmissionRequest, prepare_submission
22+
from src.kernelbot.api.main import simple_rate_limit
23+
from src.libkernelbot.leaderboard_db import LeaderboardDB
1524

1625

17-
async def _handle_discord_oauth(code: str, redirect_uri: str) -> tuple[str, str]:
26+
async def _handle_discord_oauth(
27+
code: str, redirect_uri: str
28+
) -> tuple[str, str]:
1829
"""Handles the Discord OAuth code exchange and user info retrieval."""
1930
client_id = env.CLI_DISCORD_CLIENT_ID
2031
client_secret = env.CLI_DISCORD_CLIENT_SECRET
2132
token_url = env.CLI_TOKEN_URL
2233
user_api_url = "https://discord.com/api/users/@me"
2334

2435
if not client_id:
25-
raise HTTPException(status_code=500, detail="Discord client ID not configured.")
36+
raise HTTPException(
37+
status_code=500, detail="Discord client ID not configured."
38+
)
2639
if not client_secret:
27-
raise HTTPException(status_code=500, detail="Discord client secret not configured.")
40+
raise HTTPException(
41+
status_code=500, detail="Discord client secret not configured."
42+
)
2843
if not token_url:
29-
raise HTTPException(status_code=500, detail="Discord token URL not configured.")
44+
raise HTTPException(
45+
status_code=500, detail="Discord token URL not configured."
46+
)
3047

3148
token_data = {
3249
"client_id": client_id,
@@ -69,13 +86,16 @@ async def _handle_discord_oauth(code: str, redirect_uri: str) -> tuple[str, str]
6986

7087
if not user_id or not user_name:
7188
raise HTTPException(
72-
status_code=500, detail="Failed to retrieve user ID or username from Discord."
89+
status_code=500,
90+
detail="Failed to retrieve user ID or username from Discord.",
7391
)
7492

7593
return user_id, user_name
7694

7795

78-
async def _handle_github_oauth(code: str, redirect_uri: str) -> tuple[str, str]:
96+
async def _handle_github_oauth(
97+
code: str, redirect_uri: str
98+
) -> tuple[str, str]:
7999
"""Handles the GitHub OAuth code exchange and user info retrieval."""
80100
client_id = env.CLI_GITHUB_CLIENT_ID
81101
client_secret = env.CLI_GITHUB_CLIENT_SECRET
@@ -84,9 +104,13 @@ async def _handle_github_oauth(code: str, redirect_uri: str) -> tuple[str, str]:
84104
user_api_url = "https://api.github.com/user"
85105

86106
if not client_id:
87-
raise HTTPException(status_code=500, detail="GitHub client ID not configured.")
107+
raise HTTPException(
108+
status_code=500, detail="GitHub client ID not configured."
109+
)
88110
if not client_secret:
89-
raise HTTPException(status_code=500, detail="GitHub client secret not configured.")
111+
raise HTTPException(
112+
status_code=500, detail="GitHub client secret not configured."
113+
)
90114

91115
token_data = {
92116
"client_id": client_id,
@@ -98,7 +122,9 @@ async def _handle_github_oauth(code: str, redirect_uri: str) -> tuple[str, str]:
98122
headers = {"Accept": "application/json"} # Request JSON response for token
99123

100124
try:
101-
token_response = requests.post(token_url, data=token_data, headers=headers)
125+
token_response = requests.post(
126+
token_url, data=token_data, headers=headers
127+
)
102128
token_response.raise_for_status()
103129
except requests.exceptions.RequestException as e:
104130
raise HTTPException(
@@ -125,33 +151,20 @@ async def _handle_github_oauth(code: str, redirect_uri: str) -> tuple[str, str]:
125151
) from e
126152

127153
user_json = user_response.json()
128-
user_id = str(user_json.get("id")) # GitHub ID is integer, convert to string for consistency
154+
user_id = str(
155+
user_json.get("id")
156+
) # GitHub ID is integer, convert to string for consistency
129157
user_name = user_json.get("login") # GitHub uses 'login' for username
130158

131159
if not user_id or not user_name:
132160
raise HTTPException(
133-
status_code=500, detail="Failed to retrieve user ID or username from GitHub."
161+
status_code=500,
162+
detail="Failed to retrieve user ID or username from GitHub.",
134163
)
135164

136165
return user_id, user_name
137166

138167

139-
async def _run_submission(
140-
submission: SubmissionRequest, mode: SubmissionMode, backend: KernelBackend
141-
):
142-
try:
143-
req = prepare_submission(submission, backend)
144-
except Exception as e:
145-
raise HTTPException(status_code=400, detail=str(e)) from e
146-
147-
if len(req.gpus) != 1:
148-
raise HTTPException(status_code=400, detail="Invalid GPU type")
149-
150-
reporter = MultiProgressReporterAPI()
151-
sub_id, results = await backend.submit_full(req, mode, reporter)
152-
return results, [rep.get_message() + "\n" + rep.long_report for rep in reporter.runs]
153-
154-
155168
class MultiProgressReporterAPI(MultiProgressReporter):
156169
def __init__(self):
157170
self.runs = []
@@ -183,3 +196,230 @@ async def display_report(self, title: str, report: RunResultReport):
183196
elif isinstance(part, Log):
184197
self.long_report += f"\n\n## {part.header}:\n"
185198
self.long_report += f"```\n{part.content}```"
199+
200+
201+
async def to_submission_info(
202+
user_info: Any,
203+
submission_mode: str,
204+
file: UploadFile,
205+
leaderboard_name: str,
206+
gpu_type: str,
207+
db_context: LeaderboardDB,
208+
) -> tuple[SubmissionRequest, SubmissionMode]:
209+
user_name = user_info["user_name"]
210+
user_id = user_info["user_id"]
211+
212+
try:
213+
submission_mode_enum: SubmissionMode = SubmissionMode(
214+
submission_mode.lower()
215+
)
216+
except ValueError:
217+
raise HTTPException(
218+
status_code=400,
219+
detail=f"Invalid submission mode value: '{submission_mode}'",
220+
) from None
221+
222+
if submission_mode_enum in [SubmissionMode.PROFILE]:
223+
raise HTTPException(
224+
status_code=400,
225+
detail="Profile submissions are not currently supported via API",
226+
)
227+
228+
allowed_modes = [
229+
SubmissionMode.TEST,
230+
SubmissionMode.BENCHMARK,
231+
SubmissionMode.LEADERBOARD,
232+
]
233+
if submission_mode_enum not in allowed_modes:
234+
raise HTTPException(
235+
status_code=400,
236+
detail=f"Submission mode '{submission_mode}' is not supported for this endpoint",
237+
)
238+
239+
try:
240+
with db_context as db:
241+
leaderboard_item = db.get_leaderboard(leaderboard_name)
242+
gpus = leaderboard_item.get("gpu_types", [])
243+
if gpu_type not in gpus:
244+
supported_gpus = ", ".join(gpus) if gpus else "None"
245+
raise HTTPException(
246+
status_code=400,
247+
detail=f"GPU type '{gpu_type}' is not supported for "
248+
f"leaderboard '{leaderboard_name}'. Supported GPUs: {supported_gpus}",
249+
)
250+
except HTTPException:
251+
raise
252+
except Exception as e:
253+
raise HTTPException(
254+
status_code=500,
255+
detail=f"Internal server error while validating leaderboard/GPU: {e}",
256+
) from e
257+
258+
try:
259+
submission_content = await file.read()
260+
if not submission_content:
261+
raise HTTPException(
262+
status_code=400,
263+
detail="Empty file submitted. Please provide a file with code.",
264+
)
265+
if len(submission_content) > 1_000_000:
266+
raise HTTPException(
267+
status_code=413,
268+
detail="Submission file is too large (limit: 1MB).",
269+
)
270+
271+
except HTTPException:
272+
raise
273+
except Exception as e:
274+
raise HTTPException(
275+
status_code=400, detail=f"Error reading submission file: {e}"
276+
) from e
277+
278+
try:
279+
submission_code = submission_content.decode("utf-8")
280+
submission_request = SubmissionRequest(
281+
code=submission_code,
282+
file_name=file.filename or "submission.py",
283+
user_id=user_id,
284+
user_name=user_name,
285+
gpus=[gpu_type],
286+
leaderboard=leaderboard_name,
287+
)
288+
except UnicodeDecodeError:
289+
raise HTTPException(
290+
status_code=400,
291+
detail="Failed to decode submission file content as UTF-8.",
292+
) from None
293+
except Exception as e:
294+
raise HTTPException(
295+
status_code=500,
296+
detail=f"Internal server error creating submission request: {e}",
297+
) from e
298+
299+
return submission_request, submission_mode_enum
300+
301+
302+
def json_serializer(obj):
303+
"""JSON serializer for objects not serializable by default json code"""
304+
if isinstance(obj, (datetime.datetime, datetime.date, datetime.time)):
305+
return obj.isoformat()
306+
raise TypeError(f"Type {type(obj)} not serializable")
307+
308+
309+
async def _run_submission(
310+
submission: SubmissionRequest,
311+
mode: SubmissionMode,
312+
backend: KernelBackend,
313+
submission_id: Optional[int] = None,
314+
):
315+
try:
316+
req = prepare_submission(submission, backend)
317+
except Exception as e:
318+
raise HTTPException(status_code=400, detail=str(e)) from e
319+
320+
if len(req.gpus) != 1:
321+
raise HTTPException(status_code=400, detail="Invalid GPU type")
322+
323+
reporter = MultiProgressReporterAPI()
324+
sub_id, results = await backend.submit_full(
325+
req, mode, reporter, submission_id
326+
)
327+
return (
328+
results,
329+
[rep.get_message() + "\n" + rep.long_report for rep in reporter.runs],
330+
sub_id,
331+
)
332+
333+
334+
def start_detached_run(
335+
submission_request: SubmissionRequest,
336+
submission_mode_enum: SubmissionMode,
337+
backend: KernelBackend,
338+
db: "LeaderboardDB",
339+
background_tasks: BackgroundTasks,
340+
) -> int:
341+
"""Starts a submission in the background and returns the submission id immediately."""
342+
343+
# create submission id, so that it can be return to client before task is started
344+
with db:
345+
req = submission_request
346+
sub_id = db.create_submission(
347+
leaderboard=req.leaderboard,
348+
file_name=req.file_name,
349+
code=req.code,
350+
user_id=req.user_id,
351+
time=datetime.now(),
352+
user_name=req.user_name,
353+
)
354+
355+
# makes the task run in the background
356+
background_tasks.add_task(
357+
_run_submission,
358+
submission_request,
359+
submission_mode_enum,
360+
backend,
361+
db,
362+
sub_id,
363+
)
364+
return sub_id
365+
366+
367+
async def sse_stream_submission(
368+
submission_request: SubmissionRequest,
369+
submission_mode_enum: SubmissionMode,
370+
backend: KernelBackend,
371+
):
372+
start_time = time.time()
373+
task: asyncio.Task | None = None
374+
try:
375+
task = asyncio.create_task(
376+
_run_submission(submission_request, submission_mode_enum, backend)
377+
)
378+
379+
while not task.done():
380+
elapsed_time = time.time() - start_time
381+
yield (
382+
"event: status\n"
383+
f"data: {json.dumps({'status': 'processing','elapsed_time': round(elapsed_time, 2)}, default=json_serializer)}\n\n"
384+
)
385+
try:
386+
await asyncio.wait_for(asyncio.shield(task), timeout=15.0)
387+
except asyncio.TimeoutError:
388+
continue
389+
except asyncio.CancelledError:
390+
yield (
391+
"event: error\n"
392+
f"data: {json.dumps({'status': 'error','detail': 'Submission cancelled'}, default=json_serializer)}\n\n"
393+
)
394+
return
395+
396+
result, reports = await task
397+
result_data = {
398+
"status": "success",
399+
"results": [asdict(r) for r in result],
400+
"reports": reports,
401+
}
402+
yield "event: result\n" f"data: {json.dumps(result_data, default=json_serializer)}\n\n"
403+
404+
except HTTPException as http_exc:
405+
error_data = {
406+
"status": "error",
407+
"detail": http_exc.detail,
408+
"status_code": http_exc.status_code,
409+
}
410+
yield "event: error\n" f"data: {json.dumps(error_data, default=json_serializer)}\n\n"
411+
except Exception as e:
412+
error_type = type(e).__name__
413+
error_data = {
414+
"status": "error",
415+
"detail": f"An unexpected error occurred: {error_type}",
416+
"raw_error": str(e),
417+
}
418+
yield "event: error\n" f"data: {json.dumps(error_data, default=json_serializer)}\n\n"
419+
finally:
420+
if task and not task.done():
421+
task.cancel()
422+
try:
423+
await task
424+
except asyncio.CancelledError:
425+
pass

0 commit comments

Comments
 (0)