Commit
·
23239a2
1
Parent(s):
c0da993
feat: update log feature
Browse files- .gitignore +4 -1
- Dockerfile +3 -31
- competitions/app.py +41 -8
- competitions/enums.py +9 -0
- competitions/runner.py +3 -3
- competitions/templates/index.html +11 -5
- competitions/utils.py +111 -10
- requirements.txt +3 -2
.gitignore
CHANGED
|
@@ -147,4 +147,7 @@ dmypy.json
|
|
| 147 |
**.tfvars
|
| 148 |
|
| 149 |
# Alembic / database artifcats
|
| 150 |
-
**.db
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
**.tfvars
|
| 148 |
|
| 149 |
# Alembic / database artifcats
|
| 150 |
+
**.db
|
| 151 |
+
|
| 152 |
+
# lock
|
| 153 |
+
submission_lock/*
|
Dockerfile
CHANGED
|
@@ -1,12 +1,9 @@
|
|
| 1 |
-
FROM
|
| 2 |
|
| 3 |
ENV DEBIAN_FRONTEND=noninteractive \
|
| 4 |
TZ=UTC \
|
| 5 |
HF_HUB_ENABLE_HF_TRANSFER=1
|
| 6 |
|
| 7 |
-
ENV PATH="${HOME}/miniconda3/bin:${PATH}"
|
| 8 |
-
ARG PATH="${HOME}/miniconda3/bin:${PATH}"
|
| 9 |
-
|
| 10 |
RUN mkdir -p /tmp/model && \
|
| 11 |
chown -R 1000:1000 /tmp/model && \
|
| 12 |
mkdir -p /tmp/data && \
|
|
@@ -30,7 +27,6 @@ RUN apt-get update && \
|
|
| 30 |
git-lfs \
|
| 31 |
libgl1 \
|
| 32 |
unzip \
|
| 33 |
-
openjdk-11-jre-headless \
|
| 34 |
libseccomp-dev \
|
| 35 |
&& rm -rf /var/lib/apt/lists/* && \
|
| 36 |
apt-get clean
|
|
@@ -48,34 +44,10 @@ ENV HOME=/app
|
|
| 48 |
|
| 49 |
ENV PYTHONPATH=$HOME/app \
|
| 50 |
PYTHONUNBUFFERED=1 \
|
| 51 |
-
GRADIO_ALLOW_FLAGGING=never \
|
| 52 |
-
GRADIO_NUM_PORTS=1 \
|
| 53 |
-
GRADIO_SERVER_NAME=0.0.0.0 \
|
| 54 |
-
GRADIO_THEME=huggingface \
|
| 55 |
SYSTEM=spaces
|
| 56 |
|
| 57 |
-
|
| 58 |
-
RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh \
|
| 59 |
-
&& sh Miniconda3-latest-Linux-x86_64.sh -b -p /app/miniconda \
|
| 60 |
-
&& rm -f Miniconda3-latest-Linux-x86_64.sh
|
| 61 |
-
ENV PATH /app/miniconda/bin:$PATH
|
| 62 |
-
|
| 63 |
-
RUN conda create -p /app/env -y python=3.10
|
| 64 |
-
|
| 65 |
-
SHELL ["conda", "run","--no-capture-output", "-p","/app/env", "/bin/bash", "-c"]
|
| 66 |
-
|
| 67 |
-
RUN conda install pytorch==2.4.0 torchvision==0.19.0 torchaudio==2.4.0 pytorch-cuda=12.1 -c pytorch -c nvidia && \
|
| 68 |
-
conda clean -ya && \
|
| 69 |
-
conda install -c "nvidia/label/cuda-12.1.0" cuda-nvcc && conda clean -ya
|
| 70 |
-
|
| 71 |
COPY --chown=1000:1000 . /app/
|
| 72 |
-
RUN make sandbox
|
| 73 |
-
|
| 74 |
-
# give permissions to run sandbox
|
| 75 |
-
RUN chmod +x /app/sandbox
|
| 76 |
|
| 77 |
-
|
| 78 |
|
| 79 |
-
|
| 80 |
-
RUN pip install -e .
|
| 81 |
-
RUN pip install -r requirements_docker.txt
|
|
|
|
| 1 |
+
FROM python:3.11.13-bullseye
|
| 2 |
|
| 3 |
ENV DEBIAN_FRONTEND=noninteractive \
|
| 4 |
TZ=UTC \
|
| 5 |
HF_HUB_ENABLE_HF_TRANSFER=1
|
| 6 |
|
|
|
|
|
|
|
|
|
|
| 7 |
RUN mkdir -p /tmp/model && \
|
| 8 |
chown -R 1000:1000 /tmp/model && \
|
| 9 |
mkdir -p /tmp/data && \
|
|
|
|
| 27 |
git-lfs \
|
| 28 |
libgl1 \
|
| 29 |
unzip \
|
|
|
|
| 30 |
libseccomp-dev \
|
| 31 |
&& rm -rf /var/lib/apt/lists/* && \
|
| 32 |
apt-get clean
|
|
|
|
| 44 |
|
| 45 |
ENV PYTHONPATH=$HOME/app \
|
| 46 |
PYTHONUNBUFFERED=1 \
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
SYSTEM=spaces
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
COPY --chown=1000:1000 . /app/
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
+
RUN pip install -r requirements.txt
|
| 52 |
|
| 53 |
+
CMD ["python", "-m", "uvicorn", "competitions.app:app", "--workers", "1", "--port", "7860", "--host", "0.0.0.0"]
|
|
|
|
|
|
competitions/app.py
CHANGED
|
@@ -4,9 +4,10 @@ import threading
|
|
| 4 |
import time
|
| 5 |
import io
|
| 6 |
from typing import Dict, Any
|
|
|
|
| 7 |
|
| 8 |
-
from fastapi import Depends, FastAPI,
|
| 9 |
-
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
|
| 10 |
from fastapi.staticfiles import StaticFiles
|
| 11 |
from fastapi.templating import Jinja2Templates
|
| 12 |
from huggingface_hub import hf_hub_download
|
|
@@ -25,8 +26,8 @@ from competitions.oauth import attach_oauth
|
|
| 25 |
from competitions.runner import JobRunner
|
| 26 |
from competitions.submissions import Submissions
|
| 27 |
from competitions.text import SUBMISSION_SELECTION_TEXT, SUBMISSION_TEXT
|
| 28 |
-
from competitions.utils import team_file_api, submission_api, leaderboard_api
|
| 29 |
-
from competitions.enums import SubmissionStatus
|
| 30 |
|
| 31 |
|
| 32 |
HF_TOKEN = os.environ.get("HF_TOKEN", None)
|
|
@@ -137,7 +138,9 @@ def use_oauth(request: Request, user_token: str = Depends(utils.user_authenticat
|
|
| 137 |
if user_token:
|
| 138 |
result["is_login"] = True
|
| 139 |
result["is_admin"] = utils.is_user_admin(user_token, comp_org)
|
| 140 |
-
|
|
|
|
|
|
|
| 141 |
return {"response": result}
|
| 142 |
|
| 143 |
|
|
@@ -185,7 +188,9 @@ def get_rules(request: Request):
|
|
| 185 |
|
| 186 |
|
| 187 |
@app.get("/submission_info", response_class=JSONResponse)
|
| 188 |
-
def get_submission_info(request: Request):
|
|
|
|
|
|
|
| 189 |
competition_info = CompetitionInfo(competition_id=COMPETITION_ID, autotrain_token=HF_TOKEN)
|
| 190 |
info = competition_info.submission_desc
|
| 191 |
resp = {"response": info}
|
|
@@ -213,6 +218,10 @@ def my_submissions(request: Request, user_token: str = Depends(utils.user_authen
|
|
| 213 |
"team_name": "",
|
| 214 |
}
|
| 215 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
sub = Submissions(
|
| 217 |
end_date=competition_info.end_date,
|
| 218 |
submission_limit=competition_info.submission_limit,
|
|
@@ -240,7 +249,13 @@ def my_submissions(request: Request, user_token: str = Depends(utils.user_authen
|
|
| 240 |
submission_text = SUBMISSION_TEXT.format(competition_info.submission_limit)
|
| 241 |
submission_selection_text = SUBMISSION_SELECTION_TEXT.format(competition_info.selection_limit)
|
| 242 |
|
| 243 |
-
team_name =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
|
| 245 |
resp = {
|
| 246 |
"response": {
|
|
@@ -273,6 +288,8 @@ def new_submission(
|
|
| 273 |
return {"response": "Competition has not started yet!"}
|
| 274 |
|
| 275 |
team_id = team_file_api.get_team_info(user_token)["id"]
|
|
|
|
|
|
|
| 276 |
|
| 277 |
lock = FileLock(f"./submission_lock/{team_id}.lock", blocking=False)
|
| 278 |
try:
|
|
@@ -348,7 +365,7 @@ def update_team_name(
|
|
| 348 |
return {"success": False, "error": "Team name cannot be empty."}
|
| 349 |
|
| 350 |
try:
|
| 351 |
-
utils.team_file_api.
|
| 352 |
return {"success": True, "error": ""}
|
| 353 |
except Exception as e:
|
| 354 |
return {"success": False, "error": str(e)}
|
|
@@ -464,3 +481,19 @@ def register(
|
|
| 464 |
)
|
| 465 |
|
| 466 |
return {"success": True, "response": "Team created successfully."}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import time
|
| 5 |
import io
|
| 6 |
from typing import Dict, Any
|
| 7 |
+
from urllib.parse import urlencode
|
| 8 |
|
| 9 |
+
from fastapi import Depends, FastAPI, Form, HTTPException, Request, UploadFile, Body, Query
|
| 10 |
+
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, StreamingResponse
|
| 11 |
from fastapi.staticfiles import StaticFiles
|
| 12 |
from fastapi.templating import Jinja2Templates
|
| 13 |
from huggingface_hub import hf_hub_download
|
|
|
|
| 26 |
from competitions.runner import JobRunner
|
| 27 |
from competitions.submissions import Submissions
|
| 28 |
from competitions.text import SUBMISSION_SELECTION_TEXT, SUBMISSION_TEXT
|
| 29 |
+
from competitions.utils import team_file_api, submission_api, leaderboard_api, error_log_api
|
| 30 |
+
from competitions.enums import SubmissionStatus, ErrorMessage
|
| 31 |
|
| 32 |
|
| 33 |
HF_TOKEN = os.environ.get("HF_TOKEN", None)
|
|
|
|
| 138 |
if user_token:
|
| 139 |
result["is_login"] = True
|
| 140 |
result["is_admin"] = utils.is_user_admin(user_token, comp_org)
|
| 141 |
+
team_info = utils.team_file_api.get_team_info(user_token)
|
| 142 |
+
result["is_registered"] = team_info is not None
|
| 143 |
+
result["is_white_team"] = team_info and team_info["id"] in team_file_api.get_team_white_list()
|
| 144 |
return {"response": result}
|
| 145 |
|
| 146 |
|
|
|
|
| 188 |
|
| 189 |
|
| 190 |
@app.get("/submission_info", response_class=JSONResponse)
|
| 191 |
+
def get_submission_info(request: Request, user_token: str = Depends(utils.user_authentication)):
|
| 192 |
+
if utils.team_file_api.get_team_info(user_token) is None:
|
| 193 |
+
return {"response": "Please register your team first."}
|
| 194 |
competition_info = CompetitionInfo(competition_id=COMPETITION_ID, autotrain_token=HF_TOKEN)
|
| 195 |
info = competition_info.submission_desc
|
| 196 |
resp = {"response": info}
|
|
|
|
| 218 |
"team_name": "",
|
| 219 |
}
|
| 220 |
}
|
| 221 |
+
team_info = utils.team_file_api.get_team_info(user_token)
|
| 222 |
+
if team_info["id"] not in team_file_api.get_team_white_list():
|
| 223 |
+
raise HTTPException(status_code=403, detail="You are not allowed to access this resource.")
|
| 224 |
+
|
| 225 |
sub = Submissions(
|
| 226 |
end_date=competition_info.end_date,
|
| 227 |
submission_limit=competition_info.submission_limit,
|
|
|
|
| 249 |
submission_text = SUBMISSION_TEXT.format(competition_info.submission_limit)
|
| 250 |
submission_selection_text = SUBMISSION_SELECTION_TEXT.format(competition_info.selection_limit)
|
| 251 |
|
| 252 |
+
team_name = team_info["name"]
|
| 253 |
+
|
| 254 |
+
for sub in subs:
|
| 255 |
+
if sub["error_message"] in {ErrorMessage.RUNTIME_ERROR, ErrorMessage.BUILD_SPACE_FAILED}:
|
| 256 |
+
sub["log_file_url"] = "/error_log?" + urlencode({"token": error_log_api.generate_log_token(sub["submission_id"])})
|
| 257 |
+
else:
|
| 258 |
+
sub["log_file_url"] = ""
|
| 259 |
|
| 260 |
resp = {
|
| 261 |
"response": {
|
|
|
|
| 288 |
return {"response": "Competition has not started yet!"}
|
| 289 |
|
| 290 |
team_id = team_file_api.get_team_info(user_token)["id"]
|
| 291 |
+
if team_id not in team_file_api.get_team_white_list():
|
| 292 |
+
return {"response": "You are not allowed to make submissions."}
|
| 293 |
|
| 294 |
lock = FileLock(f"./submission_lock/{team_id}.lock", blocking=False)
|
| 295 |
try:
|
|
|
|
| 365 |
return {"success": False, "error": "Team name cannot be empty."}
|
| 366 |
|
| 367 |
try:
|
| 368 |
+
utils.team_file_api.update_team_name(user_token, new_team_name)
|
| 369 |
return {"success": True, "error": ""}
|
| 370 |
except Exception as e:
|
| 371 |
return {"success": False, "error": str(e)}
|
|
|
|
| 481 |
)
|
| 482 |
|
| 483 |
return {"success": True, "response": "Team created successfully."}
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
@app.get("/error_log")
|
| 487 |
+
def get_error_log(token: str = Query(..., description="Token to access the error log file.")):
|
| 488 |
+
try:
|
| 489 |
+
log_content = error_log_api.get_log_by_token(token)
|
| 490 |
+
log_file = io.BytesIO(log_content.encode('utf-8'))
|
| 491 |
+
log_file.seek(0)
|
| 492 |
+
return StreamingResponse(
|
| 493 |
+
log_file,
|
| 494 |
+
media_type="text/plain",
|
| 495 |
+
headers={"Content-Disposition": "attachment; filename=error_log.txt"}
|
| 496 |
+
)
|
| 497 |
+
except Exception as e:
|
| 498 |
+
logger.error(f"Error while fetching log file: {e}")
|
| 499 |
+
raise HTTPException(status_code=500, detail="Internal server error.")
|
competitions/enums.py
CHANGED
|
@@ -12,3 +12,12 @@ class SubmissionStatus(enum.Enum):
|
|
| 12 |
class CompetitionType(enum.Enum):
|
| 13 |
GENERIC = 1
|
| 14 |
SCRIPT = 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
class CompetitionType(enum.Enum):
|
| 13 |
GENERIC = 1
|
| 14 |
SCRIPT = 2
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class ErrorMessage(str, enum.Enum):
|
| 19 |
+
FAILED_TO_CREATE_SPACE = "FAILED_TO_CREATE_SPACE"
|
| 20 |
+
START_SPACE_FAILED = "START_SPACE_FAILED"
|
| 21 |
+
BUILD_SPACE_FAILED = "BUILD_SPACE_FAILED"
|
| 22 |
+
RUNTIME_ERROR = "RUNTIME_ERROR"
|
| 23 |
+
SPACE_TIMEOUT = "SPACE_TIMEOUT"
|
competitions/runner.py
CHANGED
|
@@ -13,7 +13,7 @@ import pandas as pd
|
|
| 13 |
from huggingface_hub import HfApi, hf_hub_download, snapshot_download
|
| 14 |
from loguru import logger
|
| 15 |
|
| 16 |
-
from competitions.enums import SubmissionStatus
|
| 17 |
from competitions.info import CompetitionInfo
|
| 18 |
from competitions.utils import user_token_api, space_cleaner
|
| 19 |
|
|
@@ -221,7 +221,7 @@ class JobRunner:
|
|
| 221 |
all_submissions = self._get_all_submissions()
|
| 222 |
|
| 223 |
# Clean up spaces every 100 iterations
|
| 224 |
-
if cur % 100 ==
|
| 225 |
logger.info("Cleaning up spaces...")
|
| 226 |
for space in all_submissions:
|
| 227 |
if space["status"] in {SubmissionStatus.QUEUED.value, SubmissionStatus.PROCESSING.value}:
|
|
@@ -247,7 +247,7 @@ class JobRunner:
|
|
| 247 |
f"Failed to create space for {first_pending_sub['submission_id']}: {e}"
|
| 248 |
)
|
| 249 |
# mark submission as failed
|
| 250 |
-
self.mark_submission_failed(first_pending_sub['team_id'], first_pending_sub['submission_id'],
|
| 251 |
try:
|
| 252 |
space_cleaner.delete_space(first_pending_sub["space_id"])
|
| 253 |
except Exception as e:
|
|
|
|
| 13 |
from huggingface_hub import HfApi, hf_hub_download, snapshot_download
|
| 14 |
from loguru import logger
|
| 15 |
|
| 16 |
+
from competitions.enums import SubmissionStatus, ErrorMessage
|
| 17 |
from competitions.info import CompetitionInfo
|
| 18 |
from competitions.utils import user_token_api, space_cleaner
|
| 19 |
|
|
|
|
| 221 |
all_submissions = self._get_all_submissions()
|
| 222 |
|
| 223 |
# Clean up spaces every 100 iterations
|
| 224 |
+
if cur % 100 == 1:
|
| 225 |
logger.info("Cleaning up spaces...")
|
| 226 |
for space in all_submissions:
|
| 227 |
if space["status"] in {SubmissionStatus.QUEUED.value, SubmissionStatus.PROCESSING.value}:
|
|
|
|
| 247 |
f"Failed to create space for {first_pending_sub['submission_id']}: {e}"
|
| 248 |
)
|
| 249 |
# mark submission as failed
|
| 250 |
+
self.mark_submission_failed(first_pending_sub['team_id'], first_pending_sub['submission_id'], ErrorMessage.FAILED_TO_CREATE_SPACE.value)
|
| 251 |
try:
|
| 252 |
space_cleaner.delete_space(first_pending_sub["space_id"])
|
| 253 |
except Exception as e:
|
competitions/templates/index.html
CHANGED
|
@@ -150,15 +150,17 @@
|
|
| 150 |
if (data.response.submissions && data.response.submissions.length > 0 && data.response.error.length == 0) {
|
| 151 |
// Start building the table HTML
|
| 152 |
let tableHTML = teamNameDiv;
|
| 153 |
-
tableHTML += '<table border="1"><tr><th
|
| 154 |
|
| 155 |
// Iterate over each submission and add it to the table
|
| 156 |
data.response.submissions.forEach(submission => {
|
|
|
|
| 157 |
tableHTML += `<tr>
|
| 158 |
<td>${submission.datetime}</td>
|
| 159 |
<td>${submission.submission_id}</td>
|
| 160 |
<td>${submission.score}</td>
|
| 161 |
<td>${submission.status}</td>
|
|
|
|
| 162 |
</tr>`;
|
| 163 |
});
|
| 164 |
|
|
@@ -369,7 +371,7 @@
|
|
| 369 |
|
| 370 |
function checkOAuth() {
|
| 371 |
var url = "/login_status";
|
| 372 |
-
makeApiRequest(url, function ({is_login, is_admin, is_registered}) {
|
| 373 |
const registerRemoveStyle = ["pointer-events-none", "text-gray-400", "cursor-not-allowed"];
|
| 374 |
const registerNewStyle = ["ext-gray-900", "hover:bg-gray-100"]
|
| 375 |
if (is_login) {
|
|
@@ -383,12 +385,16 @@
|
|
| 383 |
document.getElementById("admin").classList.remove("hidden");
|
| 384 |
}
|
| 385 |
if (is_registered) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
document.getElementById("my_submissions").classList.remove(...registerRemoveStyle);
|
| 387 |
document.getElementById("my_submissions").classList.add(...registerNewStyle);
|
| 388 |
document.getElementById("new_submission").classList.remove(...registerRemoveStyle);
|
| 389 |
document.getElementById("new_submission").classList.add(...registerNewStyle);
|
| 390 |
-
} else {
|
| 391 |
-
document.getElementById("registerButton").style.display = "block";
|
| 392 |
}
|
| 393 |
});
|
| 394 |
}
|
|
@@ -478,7 +484,7 @@
|
|
| 478 |
<ul id="submissions-dropdown" class="py-2 space-y-2">
|
| 479 |
<li>
|
| 480 |
<a href="#" id="submission_info"
|
| 481 |
-
class="flex items-center w-full p-2
|
| 482 |
information</a>
|
| 483 |
</li>
|
| 484 |
<li>
|
|
|
|
| 150 |
if (data.response.submissions && data.response.submissions.length > 0 && data.response.error.length == 0) {
|
| 151 |
// Start building the table HTML
|
| 152 |
let tableHTML = teamNameDiv;
|
| 153 |
+
tableHTML += '<table border="1"><tr><th width: 17%;>Datetime</th><th style="width: 40%;">Submission ID</th><th style="width: 20%;">Score</th><th style="width: 8%;">Status</th><th style="width: 15%;">Error Code</th></tr>';
|
| 154 |
|
| 155 |
// Iterate over each submission and add it to the table
|
| 156 |
data.response.submissions.forEach(submission => {
|
| 157 |
+
logFileLink = submission.log_file_url === "" ? "" : `<a href="${submission.log_file_url}" target="_blank">log_file.txt</a>`;
|
| 158 |
tableHTML += `<tr>
|
| 159 |
<td>${submission.datetime}</td>
|
| 160 |
<td>${submission.submission_id}</td>
|
| 161 |
<td>${submission.score}</td>
|
| 162 |
<td>${submission.status}</td>
|
| 163 |
+
<td>${submission.error_message} <br> ${logFileLink}</td>
|
| 164 |
</tr>`;
|
| 165 |
});
|
| 166 |
|
|
|
|
| 371 |
|
| 372 |
function checkOAuth() {
|
| 373 |
var url = "/login_status";
|
| 374 |
+
makeApiRequest(url, function ({is_login, is_admin, is_registered, is_white_team}) {
|
| 375 |
const registerRemoveStyle = ["pointer-events-none", "text-gray-400", "cursor-not-allowed"];
|
| 376 |
const registerNewStyle = ["ext-gray-900", "hover:bg-gray-100"]
|
| 377 |
if (is_login) {
|
|
|
|
| 385 |
document.getElementById("admin").classList.remove("hidden");
|
| 386 |
}
|
| 387 |
if (is_registered) {
|
| 388 |
+
document.getElementById("submission_info").classList.remove(...registerRemoveStyle);
|
| 389 |
+
document.getElementById("submission_info").classList.add(...registerNewStyle);
|
| 390 |
+
} else {
|
| 391 |
+
document.getElementById("registerButton").style.display = "block";
|
| 392 |
+
}
|
| 393 |
+
if (is_white_team) {
|
| 394 |
document.getElementById("my_submissions").classList.remove(...registerRemoveStyle);
|
| 395 |
document.getElementById("my_submissions").classList.add(...registerNewStyle);
|
| 396 |
document.getElementById("new_submission").classList.remove(...registerRemoveStyle);
|
| 397 |
document.getElementById("new_submission").classList.add(...registerNewStyle);
|
|
|
|
|
|
|
| 398 |
}
|
| 399 |
});
|
| 400 |
}
|
|
|
|
| 484 |
<ul id="submissions-dropdown" class="py-2 space-y-2">
|
| 485 |
<li>
|
| 486 |
<a href="#" id="submission_info"
|
| 487 |
+
class="flex items-center w-full p-2 transition duration-75 rounded-lg pl-11 group pointer-events-none text-gray-400 cursor-not-allowed">Submission
|
| 488 |
information</a>
|
| 489 |
</li>
|
| 490 |
<li>
|
competitions/utils.py
CHANGED
|
@@ -8,11 +8,13 @@ import threading
|
|
| 8 |
import uuid
|
| 9 |
import base64
|
| 10 |
import glob
|
| 11 |
-
from typing import Optional, Dict, Any, List
|
| 12 |
from collections import defaultdict
|
|
|
|
| 13 |
|
| 14 |
import requests
|
| 15 |
import pandas as pd
|
|
|
|
| 16 |
from fastapi import Request
|
| 17 |
from huggingface_hub import HfApi, hf_hub_download
|
| 18 |
from huggingface_hub.utils._errors import RepositoryNotFoundError
|
|
@@ -21,7 +23,7 @@ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
| 21 |
from huggingface_hub import SpaceStage
|
| 22 |
from cachetools import cached, TTLCache
|
| 23 |
|
| 24 |
-
from competitions.enums import SubmissionStatus
|
| 25 |
from competitions.params import EvalParams
|
| 26 |
|
| 27 |
from . import HF_URL
|
|
@@ -400,6 +402,19 @@ class TeamFileApi:
|
|
| 400 |
repo_type="dataset",
|
| 401 |
)
|
| 402 |
return new_team_name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
|
| 404 |
|
| 405 |
team_file_api = TeamFileApi(
|
|
@@ -551,12 +566,83 @@ submission_api = SubmissionApi(
|
|
| 551 |
)
|
| 552 |
|
| 553 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 554 |
class SpaceCleaner:
|
| 555 |
-
def __init__(self, hf_token: str):
|
| 556 |
self.hf_token = hf_token
|
|
|
|
| 557 |
self.api = HfApi(token=hf_token)
|
| 558 |
self.space_build_error_count = defaultdict(int)
|
| 559 |
-
|
| 560 |
def delete_space(self, space_id: str):
|
| 561 |
"""Delete a space by its ID."""
|
| 562 |
self.api.delete_repo(repo_id=space_id, repo_type="space")
|
|
@@ -565,38 +651,53 @@ class SpaceCleaner:
|
|
| 565 |
try:
|
| 566 |
space_info = self.api.space_info(repo_id=space_id)
|
| 567 |
except RepositoryNotFoundError:
|
| 568 |
-
submission_api.
|
| 569 |
team_id=team_id,
|
| 570 |
submission_id=submission_id,
|
| 571 |
-
data={"status": SubmissionStatus.FAILED.value, "error_message":
|
|
|
|
|
|
|
| 572 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
)
|
| 574 |
return
|
|
|
|
| 575 |
if space_info.runtime.stage == SpaceStage.BUILD_ERROR:
|
| 576 |
self.space_build_error_count[space_id] += 1
|
| 577 |
if self.space_build_error_count[space_id] >= 3:
|
|
|
|
|
|
|
| 578 |
self.delete_space(space_id)
|
| 579 |
-
submission_api.
|
| 580 |
team_id=team_id,
|
| 581 |
submission_id=submission_id,
|
| 582 |
-
status
|
| 583 |
)
|
| 584 |
else:
|
| 585 |
self.api.restart_space(repo_id=space_id)
|
| 586 |
return
|
| 587 |
|
| 588 |
if space_info.runtime.stage == SpaceStage.RUNTIME_ERROR:
|
|
|
|
|
|
|
| 589 |
self.delete_space(space_id)
|
| 590 |
-
submission_api.
|
| 591 |
team_id=team_id,
|
| 592 |
submission_id=submission_id,
|
| 593 |
-
status
|
| 594 |
)
|
| 595 |
return
|
| 596 |
|
| 597 |
|
| 598 |
space_cleaner = SpaceCleaner(
|
| 599 |
os.environ.get("HF_TOKEN", None),
|
|
|
|
| 600 |
)
|
| 601 |
|
| 602 |
|
|
|
|
| 8 |
import uuid
|
| 9 |
import base64
|
| 10 |
import glob
|
| 11 |
+
from typing import Optional, Dict, Any, List, Literal
|
| 12 |
from collections import defaultdict
|
| 13 |
+
from datetime import datetime, timezone, timedelta
|
| 14 |
|
| 15 |
import requests
|
| 16 |
import pandas as pd
|
| 17 |
+
import jwt
|
| 18 |
from fastapi import Request
|
| 19 |
from huggingface_hub import HfApi, hf_hub_download
|
| 20 |
from huggingface_hub.utils._errors import RepositoryNotFoundError
|
|
|
|
| 23 |
from huggingface_hub import SpaceStage
|
| 24 |
from cachetools import cached, TTLCache
|
| 25 |
|
| 26 |
+
from competitions.enums import SubmissionStatus, ErrorMessage
|
| 27 |
from competitions.params import EvalParams
|
| 28 |
|
| 29 |
from . import HF_URL
|
|
|
|
| 402 |
repo_type="dataset",
|
| 403 |
)
|
| 404 |
return new_team_name
|
| 405 |
+
|
| 406 |
+
@cached(cache=TTLCache(maxsize=1, ttl=600))
|
| 407 |
+
def get_team_white_list(self) -> List[str]:
|
| 408 |
+
file = hf_hub_download(
|
| 409 |
+
repo_id=self.competition_id,
|
| 410 |
+
filename="team_id_whitelist.json",
|
| 411 |
+
token=self.hf_token,
|
| 412 |
+
repo_type="dataset",
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
with open(file, "r", encoding="utf-8") as f:
|
| 416 |
+
team_white_list = json.load(f)
|
| 417 |
+
return team_white_list
|
| 418 |
|
| 419 |
|
| 420 |
team_file_api = TeamFileApi(
|
|
|
|
| 566 |
)
|
| 567 |
|
| 568 |
|
| 569 |
+
class ErrorLogApi:
|
| 570 |
+
def __init__(self, hf_token: str, competition_id: str, encode_key: str):
|
| 571 |
+
self.hf_token = hf_token
|
| 572 |
+
self.competition_id = competition_id
|
| 573 |
+
self.api = HfApi(token=hf_token)
|
| 574 |
+
self.encode_key = encode_key
|
| 575 |
+
|
| 576 |
+
def save_error_log(self, submission_id: str, content: str):
|
| 577 |
+
"""Save the error log of a space to the submission."""
|
| 578 |
+
content_buffer = io.BytesIO(content.encode())
|
| 579 |
+
self.api.upload_file(
|
| 580 |
+
path_or_fileobj=content_buffer,
|
| 581 |
+
path_in_repo=f"error_logs/{submission_id}.txt",
|
| 582 |
+
repo_id=self.competition_id,
|
| 583 |
+
repo_type="dataset",
|
| 584 |
+
)
|
| 585 |
+
|
| 586 |
+
def get_log(self, space_id: str, kind: Literal["run", "build"], tail: int = -1) -> str:
|
| 587 |
+
"""Get the build log of a space."""
|
| 588 |
+
url = f"https://huggingface.co/api/spaces/{space_id}/logs/{kind}"
|
| 589 |
+
headers = {
|
| 590 |
+
"Authorization": f"Bearer {self.hf_token}"
|
| 591 |
+
}
|
| 592 |
+
response = requests.get(url, headers=headers)
|
| 593 |
+
if response.status_code != 200:
|
| 594 |
+
raise RuntimeError(f"Failed to get logs: {response.status_code}\n{response.text}")
|
| 595 |
+
content = response.text
|
| 596 |
+
line_str_list = []
|
| 597 |
+
start_index = 0 if tail == -1 else max(0, len(content.split('\n')) - tail)
|
| 598 |
+
for line in content.split('\n')[start_index:]:
|
| 599 |
+
if line.startswith("data:"):
|
| 600 |
+
line_json = json.loads(line[5:].strip())
|
| 601 |
+
line_str_list.append(f"{line_json['timestamp']}: {line_json['data']}")
|
| 602 |
+
return "\n".join(line_str_list)
|
| 603 |
+
|
| 604 |
+
def generate_log_token(self, submission_id: str) -> str:
|
| 605 |
+
payload = {
|
| 606 |
+
"submission_id": submission_id,
|
| 607 |
+
"exp": datetime.now(timezone.utc) + timedelta(hours=1)
|
| 608 |
+
}
|
| 609 |
+
token = jwt.encode(payload, self.encode_key, algorithm="HS256")
|
| 610 |
+
return token
|
| 611 |
+
|
| 612 |
+
def get_log_by_token(self, token: str) -> str:
|
| 613 |
+
try:
|
| 614 |
+
payload = jwt.decode(token, self.encode_key, algorithms=["HS256"])
|
| 615 |
+
submission_id = payload["submission_id"]
|
| 616 |
+
except jwt.ExpiredSignatureError:
|
| 617 |
+
raise RuntimeError("Token has expired.")
|
| 618 |
+
except jwt.InvalidTokenError as e:
|
| 619 |
+
raise RuntimeError(f"Invalid token: {e}")
|
| 620 |
+
|
| 621 |
+
log_file_path = self.api.hf_hub_download(
|
| 622 |
+
repo_id=self.competition_id,
|
| 623 |
+
filename=f"error_logs/{submission_id}.txt",
|
| 624 |
+
repo_type="dataset",
|
| 625 |
+
)
|
| 626 |
+
with open(log_file_path, 'r') as f:
|
| 627 |
+
file_content = f.read()
|
| 628 |
+
|
| 629 |
+
return file_content
|
| 630 |
+
|
| 631 |
+
|
| 632 |
+
error_log_api = ErrorLogApi(
|
| 633 |
+
hf_token=os.environ.get("HF_TOKEN", None),
|
| 634 |
+
competition_id=os.environ.get("COMPETITION_ID"),
|
| 635 |
+
encode_key=os.environ.get("ERROR_LOG_ENCODE_KEY", "key")
|
| 636 |
+
)
|
| 637 |
+
|
| 638 |
+
|
| 639 |
class SpaceCleaner:
|
| 640 |
+
def __init__(self, hf_token: str, competition_id: str):
|
| 641 |
self.hf_token = hf_token
|
| 642 |
+
self.competition_id = competition_id
|
| 643 |
self.api = HfApi(token=hf_token)
|
| 644 |
self.space_build_error_count = defaultdict(int)
|
| 645 |
+
|
| 646 |
def delete_space(self, space_id: str):
|
| 647 |
"""Delete a space by its ID."""
|
| 648 |
self.api.delete_repo(repo_id=space_id, repo_type="space")
|
|
|
|
| 651 |
try:
|
| 652 |
space_info = self.api.space_info(repo_id=space_id)
|
| 653 |
except RepositoryNotFoundError:
|
| 654 |
+
submission_api.update_submission_data(
|
| 655 |
team_id=team_id,
|
| 656 |
submission_id=submission_id,
|
| 657 |
+
data={"status": SubmissionStatus.FAILED.value, "error_message": ErrorMessage.START_SPACE_FAILED.value},
|
| 658 |
+
)
|
| 659 |
+
return
|
| 660 |
|
| 661 |
+
if (datetime.now(timezone.utc) - space_info.created_at).total_seconds() > 60 * 60 * 1.5:
|
| 662 |
+
# If the space is older than 1.5 hours, delete it
|
| 663 |
+
self.delete_space(space_id)
|
| 664 |
+
submission_api.update_submission_data(
|
| 665 |
+
team_id=team_id,
|
| 666 |
+
submission_id=submission_id,
|
| 667 |
+
data={"status": SubmissionStatus.FAILED.value, "error_message": ErrorMessage.SPACE_TIMEOUT.value},
|
| 668 |
)
|
| 669 |
return
|
| 670 |
+
|
| 671 |
if space_info.runtime.stage == SpaceStage.BUILD_ERROR:
|
| 672 |
self.space_build_error_count[space_id] += 1
|
| 673 |
if self.space_build_error_count[space_id] >= 3:
|
| 674 |
+
log_content = error_log_api.get_log(space_id, kind="build")
|
| 675 |
+
error_log_api.save_error_log(submission_id, log_content)
|
| 676 |
self.delete_space(space_id)
|
| 677 |
+
submission_api.update_submission_data(
|
| 678 |
team_id=team_id,
|
| 679 |
submission_id=submission_id,
|
| 680 |
+
data={"status": SubmissionStatus.FAILED.value, "error_message": ErrorMessage.BUILD_SPACE_FAILED.value},
|
| 681 |
)
|
| 682 |
else:
|
| 683 |
self.api.restart_space(repo_id=space_id)
|
| 684 |
return
|
| 685 |
|
| 686 |
if space_info.runtime.stage == SpaceStage.RUNTIME_ERROR:
|
| 687 |
+
log_content = error_log_api.get_log(space_id, kind="run")
|
| 688 |
+
error_log_api.save_error_log(submission_id, log_content)
|
| 689 |
self.delete_space(space_id)
|
| 690 |
+
submission_api.update_submission_data(
|
| 691 |
team_id=team_id,
|
| 692 |
submission_id=submission_id,
|
| 693 |
+
data={"status": SubmissionStatus.FAILED.value, "error_message": ErrorMessage.RUNTIME_ERROR.value},
|
| 694 |
)
|
| 695 |
return
|
| 696 |
|
| 697 |
|
| 698 |
space_cleaner = SpaceCleaner(
|
| 699 |
os.environ.get("HF_TOKEN", None),
|
| 700 |
+
os.environ.get("COMPETITION_ID")
|
| 701 |
)
|
| 702 |
|
| 703 |
|
requirements.txt
CHANGED
|
@@ -14,6 +14,7 @@ pydantic==2.8.2
|
|
| 14 |
gradio==4.37.2
|
| 15 |
authlib==1.3.1
|
| 16 |
itsdangerous==2.2.0
|
| 17 |
-
hf-transfer
|
| 18 |
-
filelock>=3.
|
| 19 |
cachetools==6.0.0
|
|
|
|
|
|
| 14 |
gradio==4.37.2
|
| 15 |
authlib==1.3.1
|
| 16 |
itsdangerous==2.2.0
|
| 17 |
+
hf-transfer>=0.1.9
|
| 18 |
+
filelock>=3.13.3
|
| 19 |
cachetools==6.0.0
|
| 20 |
+
PyJWT>=2.10.1
|