caw2rng commited on
Commit
9bcd73f
·
1 Parent(s): 0756d61
.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
+ # Reference repository
153
+ ICCV2025-RealADSim-ClosedLoop_ref/
competitions/app.py CHANGED
@@ -2,16 +2,17 @@ import datetime
2
  import os
3
  import threading
4
  import time
 
5
 
6
  from fastapi import Depends, FastAPI, File, Form, HTTPException, Request, UploadFile
7
- from fastapi.responses import HTMLResponse, JSONResponse
8
  from fastapi.staticfiles import StaticFiles
9
  from fastapi.templating import Jinja2Templates
10
  from huggingface_hub import hf_hub_download
11
  from huggingface_hub.utils import disable_progress_bars
12
  from huggingface_hub.utils._errors import EntryNotFoundError
13
  from loguru import logger
14
- from pydantic import BaseModel
15
  from requests.exceptions import RequestException
16
 
17
  from competitions import __version__, utils
@@ -48,6 +49,8 @@ if REQUIREMENTS_FNAME:
48
  utils.uninstall_requirements(REQUIREMENTS_FNAME)
49
  utils.install_requirements(REQUIREMENTS_FNAME)
50
 
 
 
51
 
52
  class LeaderboardRequest(BaseModel):
53
  lb: str
@@ -61,6 +64,25 @@ class UpdateTeamNameRequest(BaseModel):
61
  new_team_name: str
62
 
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  def run_job_runner():
65
  job_runner = JobRunner(
66
  competition_id=COMPETITION_ID,
@@ -122,9 +144,34 @@ async def read_form(request: Request):
122
 
123
  @app.get("/login_status", response_class=JSONResponse)
124
  async def use_oauth(request: Request, user_token: str = Depends(utils.user_authentication)):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  if user_token:
126
- return {"response": 2}
127
- return {"response": 1}
 
 
 
 
 
128
 
129
 
130
  @app.get("/logout", response_class=HTMLResponse)
@@ -171,7 +218,9 @@ async def get_rules(request: Request):
171
 
172
 
173
  @app.get("/submission_info", response_class=JSONResponse)
174
- async def get_submission_info(request: Request):
 
 
175
  competition_info = CompetitionInfo(competition_id=COMPETITION_ID, autotrain_token=HF_TOKEN)
176
  info = competition_info.submission_desc
177
  resp = {"response": info}
@@ -226,6 +275,29 @@ async def my_submissions(request: Request, user_token: str = Depends(utils.user_
226
  "team_name": "",
227
  }
228
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  sub = Submissions(
230
  end_date=competition_info.end_date,
231
  submission_limit=competition_info.submission_limit,
@@ -253,7 +325,7 @@ async def my_submissions(request: Request, user_token: str = Depends(utils.user_
253
  submission_text = SUBMISSION_TEXT.format(competition_info.submission_limit)
254
  submission_selection_text = SUBMISSION_SELECTION_TEXT.format(competition_info.selection_limit)
255
 
256
- team_name = utils.get_team_name(user_token, COMPETITION_ID, HF_TOKEN)
257
 
258
  resp = {
259
  "response": {
@@ -445,3 +517,47 @@ async def update_comp_info(request: Request, user_token: str = Depends(utils.use
445
  return {"success": False}, 500
446
 
447
  return {"success": True}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import os
3
  import threading
4
  import time
5
+ from typing import List, Optional
6
 
7
  from fastapi import Depends, FastAPI, File, Form, HTTPException, Request, UploadFile
8
+ from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
9
  from fastapi.staticfiles import StaticFiles
10
  from fastapi.templating import Jinja2Templates
11
  from huggingface_hub import hf_hub_download
12
  from huggingface_hub.utils import disable_progress_bars
13
  from huggingface_hub.utils._errors import EntryNotFoundError
14
  from loguru import logger
15
+ from pydantic import BaseModel, Field
16
  from requests.exceptions import RequestException
17
 
18
  from competitions import __version__, utils
 
49
  utils.uninstall_requirements(REQUIREMENTS_FNAME)
50
  utils.install_requirements(REQUIREMENTS_FNAME)
51
 
52
+ # REQUIREMENTS_FNAME = None
53
+
54
 
55
  class LeaderboardRequest(BaseModel):
56
  lb: str
 
64
  new_team_name: str
65
 
66
 
67
+ class TeamMember(BaseModel):
68
+ """Team member data model"""
69
+ name: str = Field(..., max_length=50, description="Name of the team member")
70
+ email: str = Field(..., max_length=100, description="Email of the team member")
71
+ institution: str = Field(..., max_length=100, description="Institution of the team member")
72
+ is_student: bool = Field(..., description="Is the team member a student?")
73
+ majar: Optional[str] = Field(None, max_length=50, description="Major of the team member")
74
+ degree: Optional[str] = Field(None, description="Degree of the team member")
75
+ advising_professor: Optional[str] = Field(None, max_length=50, description="Advising professor of the team member")
76
+ grade: Optional[str] = Field(None, max_length=50, description="Grade of the team member")
77
+ expected_graduation_year: Optional[str] = Field(None, max_length=4, description="Expected graduation year of the team member")
78
+
79
+
80
+ class RegisterRequest(BaseModel):
81
+ """Team registration request data model"""
82
+ team_name: str = Field(..., max_length=20, description="Name of the team")
83
+ team_members: List[TeamMember] = Field(min_length=1, max_length=4, description="List of team members")
84
+
85
+
86
  def run_job_runner():
87
  job_runner = JobRunner(
88
  competition_id=COMPETITION_ID,
 
144
 
145
  @app.get("/login_status", response_class=JSONResponse)
146
  async def use_oauth(request: Request, user_token: str = Depends(utils.user_authentication)):
147
+ """
148
+ Get user login and registration status
149
+ Return format:
150
+ {
151
+ "response": {
152
+ "is_login": bool, # Whether user is logged in
153
+ "is_admin": bool, # Whether user is admin
154
+ "is_registered": bool, # Whether team is registered
155
+ "is_white_team": bool # Whether team is in whitelist
156
+ }
157
+ }
158
+ """
159
+ result = {
160
+ "is_login": False,
161
+ "is_admin": False,
162
+ "is_registered": False,
163
+ "is_white_team": False,
164
+ }
165
+
166
+ comp_org = COMPETITION_ID.split("/")[0]
167
  if user_token:
168
+ result["is_login"] = True
169
+ result["is_admin"] = utils.is_user_admin(user_token, comp_org)
170
+ team_info = utils.team_file_api.get_team_info(user_token)
171
+ result["is_registered"] = team_info is not None
172
+ if team_info:
173
+ result["is_white_team"] = team_info["id"] in utils.team_file_api.get_team_white_list()
174
+ return {"response": result}
175
 
176
 
177
  @app.get("/logout", response_class=HTMLResponse)
 
218
 
219
 
220
  @app.get("/submission_info", response_class=JSONResponse)
221
+ async def get_submission_info(request: Request, user_token: str = Depends(utils.user_authentication)):
222
+ if utils.team_file_api.get_team_info(user_token) is None:
223
+ return {"response": "Please register your team first."}
224
  competition_info = CompetitionInfo(competition_id=COMPETITION_ID, autotrain_token=HF_TOKEN)
225
  info = competition_info.submission_desc
226
  resp = {"response": info}
 
275
  "team_name": "",
276
  }
277
  }
278
+
279
+ # Check team registration and whitelist status
280
+ team_info = utils.team_file_api.get_team_info(user_token)
281
+ if team_info is None:
282
+ return {
283
+ "response": {
284
+ "submissions": "",
285
+ "submission_text": SUBMISSION_TEXT.format(competition_info.submission_limit),
286
+ "error": "**Please register your team first.**",
287
+ "team_name": "",
288
+ }
289
+ }
290
+
291
+ if team_info["id"] not in utils.team_file_api.get_team_white_list():
292
+ return {
293
+ "response": {
294
+ "submissions": "",
295
+ "submission_text": SUBMISSION_TEXT.format(competition_info.submission_limit),
296
+ "error": "**You are not allowed to access this resource.**",
297
+ "team_name": team_info["name"],
298
+ }
299
+ }
300
+
301
  sub = Submissions(
302
  end_date=competition_info.end_date,
303
  submission_limit=competition_info.submission_limit,
 
325
  submission_text = SUBMISSION_TEXT.format(competition_info.submission_limit)
326
  submission_selection_text = SUBMISSION_SELECTION_TEXT.format(competition_info.selection_limit)
327
 
328
+ team_name = team_info["name"]
329
 
330
  resp = {
331
  "response": {
 
517
  return {"success": False}, 500
518
 
519
  return {"success": True}
520
+
521
+
522
+ @app.post("/register")
523
+ def register(
524
+ request: RegisterRequest,
525
+ user_token: str = Depends(utils.user_authentication)
526
+ ):
527
+ """
528
+ Team registration endpoint
529
+
530
+ Handle user team registration requests:
531
+ 1. Verify user is logged in
532
+ 2. Check if user is already registered
533
+ 3. Create new team
534
+ 4. Return registration result
535
+ """
536
+ if user_token is None:
537
+ return {"success": False, "response": "Please login."}
538
+
539
+ team_info = utils.team_file_api.get_team_info(user_token)
540
+ if team_info is not None:
541
+ return {"success": False, "response": "You have already registered your team."}
542
+
543
+ utils.team_file_api.create_team(
544
+ user_token,
545
+ request.team_name,
546
+ request.model_dump()
547
+ )
548
+
549
+ return {"success": True, "response": "Team created successfully."}
550
+
551
+
552
+ @app.get("/register_page", response_class=HTMLResponse)
553
+ def register_page(request: Request):
554
+ """
555
+ Display team registration page
556
+ """
557
+ if HF_TOKEN is None:
558
+ return HTTPException(status_code=500, detail="HF_TOKEN is not set.")
559
+ context = {
560
+ "request": request,
561
+ "version": __version__,
562
+ }
563
+ return templates.TemplateResponse("register_page.html", context)
competitions/templates/register_page.html ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Team Registration - JSON Editor</title>
6
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/spectre.css@latest/dist/spectre.min.css">
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/spectre.css@latest/dist/spectre-exp.min.css">
8
+ <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css">
9
+
10
+ <style>
11
+ body {
12
+ font-family: Arial, sans-serif;
13
+ padding: 20px;
14
+ background-color: #f8f9fa;
15
+ }
16
+ .container {
17
+ max-width: 800px;
18
+ margin: 0 auto;
19
+ background: white;
20
+ padding: 2rem;
21
+ border-radius: 8px;
22
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
23
+ }
24
+ #editor_holder {
25
+ margin-bottom: 20px;
26
+ }
27
+ .btn-group-custom {
28
+ display: flex;
29
+ justify-content: center;
30
+ gap: 1rem;
31
+ margin-top: 3rem;
32
+ }
33
+ .header {
34
+ text-align: center;
35
+ margin-bottom: 2rem;
36
+ color: #333;
37
+ }
38
+ .info-box {
39
+ background: #e3f2fd;
40
+ border-left: 4px solid #2196f3;
41
+ padding: 1rem;
42
+ margin-bottom: 2rem;
43
+ border-radius: 4px;
44
+ }
45
+ </style>
46
+ </head>
47
+ <body>
48
+ <div class="container">
49
+ <div class="header">
50
+ <h1>🚗 Autonomous Driving Competition Team Registration</h1>
51
+ <p>Please fill in the following information accurately. Inaccurate information will result in registration rejection.</p>
52
+ </div>
53
+
54
+ <div class="info-box">
55
+ <h4>📝 Registration Instructions:</h4>
56
+ <ul>
57
+ <li>Team name cannot exceed 20 characters</li>
58
+ <li>Each team can have a maximum of 4 members and a minimum of 1 member</li>
59
+ <li>If you are a student participant, please check the "Is Student" option and fill in the relevant information</li>
60
+ <li>Please ensure the email address is correct, as we will contact you via email</li>
61
+ </ul>
62
+ </div>
63
+
64
+ <div id="editor_holder"></div>
65
+
66
+ <div class="btn-group btn-group-custom">
67
+ <button id="submit" class="btn btn-primary btn-lg">
68
+ <i class="fas fa-paper-plane"></i> Submit Registration
69
+ </button>
70
+ <button id="cancel" class="btn btn-error btn-lg">
71
+ <i class="fas fa-times"></i> Cancel
72
+ </button>
73
+ </div>
74
+ </div>
75
+
76
+ <script src="https://cdn.jsdelivr.net/npm/@json-editor/json-editor@latest/dist/jsoneditor.min.js"></script>
77
+
78
+ <script>
79
+ const schema = {
80
+ "title": "Team Registration Information",
81
+ "type": "object",
82
+ "definitions": {
83
+ "student": {
84
+ "type": "object",
85
+ "id": "student",
86
+ "properties": {
87
+ "majar": {
88
+ "type": "string",
89
+ "title": "Major",
90
+ "minLength": 1,
91
+ },
92
+ "degree": {
93
+ "type": "string",
94
+ "title": "Degree",
95
+ "enum": ["Bachelor", "Master", "PhD"]
96
+ },
97
+ "advising_professor": {
98
+ "type": "string",
99
+ "title": "Advising Professor",
100
+ "minLength": 1,
101
+ },
102
+ "grade": {
103
+ "type": "string",
104
+ "title": "Grade",
105
+ "minLength": 1,
106
+ },
107
+ "expected_graduation_year": {
108
+ "type": "string",
109
+ "title": "Expected Graduation Year",
110
+ "minLength": 4,
111
+ "maxLength": 4,
112
+ "pattern": "^[0-9]{4}$"
113
+ }
114
+ }
115
+ }
116
+ },
117
+ "properties": {
118
+ "team_name": {
119
+ "type": "string",
120
+ "title": "Team Name",
121
+ "minLength": 1,
122
+ "maxLength": 20,
123
+ "description": "Please enter team name (no more than 20 characters)"
124
+ },
125
+ "team_members": {
126
+ "type": "array",
127
+ "title": "Team Members",
128
+ "minItems": 1,
129
+ "maxItems": 4,
130
+ "format": "tabs",
131
+ "items": {
132
+ "type": "object",
133
+ "title": "Member",
134
+ "properties": {
135
+ "name": {
136
+ "type": "string",
137
+ "title": "Name",
138
+ "minLength": 1,
139
+ "maxLength": 50
140
+ },
141
+ "email": {
142
+ "type": "string",
143
+ "title": "Email",
144
+ "format": "email",
145
+ "minLength": 1,
146
+ "maxLength": 100
147
+ },
148
+ "institution": {
149
+ "type": "string",
150
+ "title": "Institution",
151
+ "minLength": 1,
152
+ "maxLength": 100
153
+ },
154
+ "student_info": {
155
+ "title": "Is Student",
156
+ "oneOf": [
157
+ {
158
+ "title": "No",
159
+ "type": "null"
160
+ },
161
+ {
162
+ "title": "Yes",
163
+ "$ref": "#/definitions/student"
164
+ }
165
+ ]
166
+ }
167
+ }
168
+ }
169
+ }
170
+ }
171
+ };
172
+
173
+ const editor = new JSONEditor(document.getElementById('editor_holder'), {
174
+ schema: schema,
175
+ theme: 'spectre',
176
+ iconlib: 'fontawesome5',
177
+ disable_array_delete_all_rows: true,
178
+ disable_array_delete_last_row: true,
179
+ disable_collapse: true,
180
+ disable_edit_json: true,
181
+ disable_properties: true,
182
+ disable_array_reorder: true,
183
+ prompt_before_delete: false,
184
+ required_by_default: true,
185
+ array_controls_top: true,
186
+ remove_button_labels: true,
187
+ show_errors: "change",
188
+ });
189
+
190
+ const submitButton = document.getElementById('submit');
191
+ const cancelButton = document.getElementById('cancel');
192
+
193
+ submitButton.addEventListener('click', () => {
194
+ const errors = editor.validate();
195
+ editor.showValidationErrors(errors);
196
+
197
+ if (errors.length > 0) {
198
+ alert('Please fix the errors in the form before submitting!');
199
+ return;
200
+ }
201
+
202
+ const registerData = editor.getValue();
203
+
204
+ // Convert data format to match backend API
205
+ registerData.team_members = registerData.team_members.map(member => {
206
+ return {
207
+ name: member.name,
208
+ email: member.email,
209
+ institution: member.institution,
210
+ is_student: member.student_info !== null,
211
+ ...member.student_info
212
+ };
213
+ });
214
+
215
+ submitButton.textContent = "Submitting...";
216
+ submitButton.classList.add('disabled');
217
+ cancelButton.classList.add('disabled');
218
+
219
+ fetch('/register', {
220
+ method: 'POST',
221
+ headers: {'Content-Type': 'application/json'},
222
+ body: JSON.stringify(registerData)
223
+ })
224
+ .then(response => {
225
+ if (response.status !== 200) {
226
+ throw new Error('Network response was not ok');
227
+ }
228
+ return response.json();
229
+ })
230
+ .then(data => {
231
+ if (data.success) {
232
+ alert("Registration successful! Redirecting to homepage...");
233
+ window.location.href = "/";
234
+ } else {
235
+ alert("Registration failed: " + data.response);
236
+ }
237
+ })
238
+ .catch(error => {
239
+ console.error('Error:', error);
240
+ alert("An error occurred during registration. Please try again later.");
241
+ })
242
+ .finally(() => {
243
+ setTimeout(() => {
244
+ submitButton.textContent = "Submit Registration";
245
+ submitButton.classList.remove('disabled');
246
+ cancelButton.classList.remove('disabled');
247
+ }, 2000);
248
+ });
249
+ });
250
+
251
+ cancelButton.addEventListener('click', () => {
252
+ if (confirm('Are you sure you want to cancel registration?')) {
253
+ window.location.href = "/";
254
+ }
255
+ });
256
+ </script>
257
+ </body>
258
+ </html>
competitions/utils.py CHANGED
@@ -4,11 +4,15 @@ import os
4
  import shlex
5
  import subprocess
6
  import traceback
 
 
 
7
 
8
  import requests
9
  from fastapi import Request
10
  from huggingface_hub import HfApi, hf_hub_download
11
  from loguru import logger
 
12
 
13
  from competitions.enums import SubmissionStatus
14
  from competitions.params import EvalParams
@@ -384,3 +388,189 @@ def update_team_name(user_token, new_team_name, competition_id, hf_token):
384
  repo_type="dataset",
385
  )
386
  return new_team_name
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import shlex
5
  import subprocess
6
  import traceback
7
+ import threading
8
+ import uuid
9
+ from typing import Dict, Any, List, Optional
10
 
11
  import requests
12
  from fastapi import Request
13
  from huggingface_hub import HfApi, hf_hub_download
14
  from loguru import logger
15
+ from cachetools import TTLCache, cached
16
 
17
  from competitions.enums import SubmissionStatus
18
  from competitions.params import EvalParams
 
388
  repo_type="dataset",
389
  )
390
  return new_team_name
391
+
392
+
393
+ class TeamAlreadyExistsError(Exception):
394
+ """Custom exception for when a team already exists."""
395
+ pass
396
+
397
+
398
+ class TeamFileApi:
399
+ """
400
+ Team File Management API Class
401
+
402
+ This class manages all team-related operations:
403
+ - Create new teams
404
+ - Query team information
405
+ - Update team names
406
+ - Manage team whitelist
407
+ """
408
+ def __init__(self, hf_token: str, competition_id: str):
409
+ self.hf_token = hf_token
410
+ self.competition_id = competition_id
411
+ self._lock = threading.Lock() # Thread lock to ensure thread safety for concurrent operations
412
+
413
+ def _get_team_info(self, user_id: str) -> Optional[Dict[str, Any]]:
414
+ """
415
+ Get team information by user ID
416
+ """
417
+ user_team = hf_hub_download(
418
+ repo_id=self.competition_id,
419
+ filename="user_team.json",
420
+ token=self.hf_token,
421
+ repo_type="dataset",
422
+ )
423
+
424
+ with open(user_team, "r", encoding="utf-8") as f:
425
+ user_team = json.load(f)
426
+
427
+ if user_id not in user_team:
428
+ return None
429
+
430
+ team_id = user_team[user_id]
431
+
432
+ team_metadata = hf_hub_download(
433
+ repo_id=self.competition_id,
434
+ filename="teams.json",
435
+ token=self.hf_token,
436
+ repo_type="dataset",
437
+ )
438
+
439
+ with open(team_metadata, "r", encoding="utf-8") as f:
440
+ team_metadata = json.load(f)
441
+
442
+ return team_metadata[team_id]
443
+
444
+ def _create_team(self, user_id: str, team_name: str, other_data: Dict[str, Any]) -> str:
445
+ """
446
+ Create new team (internal method)
447
+ """
448
+ with self._lock: # Use lock to ensure thread safety
449
+ user_team = hf_hub_download(
450
+ repo_id=self.competition_id,
451
+ filename="user_team.json",
452
+ token=self.hf_token,
453
+ repo_type="dataset",
454
+ )
455
+ with open(user_team, "r", encoding="utf-8") as f:
456
+ user_team = json.load(f)
457
+
458
+ team_metadata = hf_hub_download(
459
+ repo_id=self.competition_id,
460
+ filename="teams.json",
461
+ token=self.hf_token,
462
+ repo_type="dataset",
463
+ )
464
+
465
+ with open(team_metadata, "r", encoding="utf-8") as f:
466
+ team_metadata = json.load(f)
467
+
468
+ # Create new team ID
469
+ team_id = str(uuid.uuid4())
470
+ user_team[user_id] = team_id
471
+
472
+ # Create team metadata
473
+ team_metadata[team_id] = {
474
+ "id": team_id,
475
+ "name": team_name,
476
+ "members": [user_id],
477
+ "leader": user_id,
478
+ "other_data": other_data,
479
+ }
480
+
481
+ # Upload user-team mapping file
482
+ user_team_json = json.dumps(user_team, indent=4)
483
+ user_team_json_bytes = user_team_json.encode("utf-8")
484
+ user_team_json_buffer = io.BytesIO(user_team_json_bytes)
485
+
486
+ # Upload team metadata file
487
+ team_metadata_json = json.dumps(team_metadata, indent=4)
488
+ team_metadata_json_bytes = team_metadata_json.encode("utf-8")
489
+ team_metadata_json_buffer = io.BytesIO(team_metadata_json_bytes)
490
+
491
+ api = HfApi(token=self.hf_token)
492
+ api.upload_file(
493
+ path_or_fileobj=user_team_json_buffer,
494
+ path_in_repo="user_team.json",
495
+ repo_id=self.competition_id,
496
+ repo_type="dataset",
497
+ )
498
+ api.upload_file(
499
+ path_or_fileobj=team_metadata_json_buffer,
500
+ path_in_repo="teams.json",
501
+ repo_id=self.competition_id,
502
+ repo_type="dataset",
503
+ )
504
+ return team_id
505
+
506
+ def create_team(self, user_token: str, team_name: str, other_data: Dict[str, Any]) -> str:
507
+ """
508
+ Create team using user token (public interface)
509
+ """
510
+ user_info = token_information(token=user_token)
511
+ return self._create_team(user_info["id"], team_name, other_data)
512
+
513
+ def get_team_info(self, user_token: str) -> Optional[Dict[str, Any]]:
514
+ """
515
+ Get user's team information
516
+ """
517
+ user_info = token_information(token=user_token)
518
+ return self._get_team_info(user_info["id"])
519
+
520
+ def update_team_name(self, user_token, new_team_name):
521
+ """
522
+ Update team name
523
+ """
524
+ user_info = token_information(token=user_token)
525
+ user_id = user_info["id"]
526
+ team_info = self._get_team_info(user_id)
527
+
528
+ with self._lock:
529
+ team_metadata = hf_hub_download(
530
+ repo_id=self.competition_id,
531
+ filename="teams.json",
532
+ token=self.hf_token,
533
+ repo_type="dataset",
534
+ )
535
+ with open(team_metadata, "r", encoding="utf-8") as f:
536
+ team_metadata = json.load(f)
537
+
538
+ team_metadata[team_info["id"]]["name"] = new_team_name
539
+ team_metadata_json = json.dumps(team_metadata, indent=4)
540
+ team_metadata_json_bytes = team_metadata_json.encode("utf-8")
541
+ team_metadata_json_buffer = io.BytesIO(team_metadata_json_bytes)
542
+ api = HfApi(token=self.hf_token)
543
+ api.upload_file(
544
+ path_or_fileobj=team_metadata_json_buffer,
545
+ path_in_repo="teams.json",
546
+ repo_id=self.competition_id,
547
+ repo_type="dataset",
548
+ )
549
+ return new_team_name
550
+
551
+ @cached(cache=TTLCache(maxsize=1, ttl=600)) # Cache for 10 minutes
552
+ def get_team_white_list(self) -> List[str]:
553
+ """
554
+ Get team whitelist (cached for 10 minutes)
555
+ """
556
+ try:
557
+ file = hf_hub_download(
558
+ repo_id=self.competition_id,
559
+ filename="team_id_whitelist.json",
560
+ token=self.hf_token,
561
+ repo_type="dataset",
562
+ )
563
+
564
+ with open(file, "r", encoding="utf-8") as f:
565
+ team_white_list = json.load(f)
566
+ return team_white_list
567
+ except:
568
+ # Return empty list if whitelist file doesn't exist
569
+ return []
570
+
571
+
572
+ # Create global team file API instance
573
+ team_file_api = TeamFileApi(
574
+ os.environ.get("HF_TOKEN", None),
575
+ os.environ.get("COMPETITION_ID"),
576
+ )