xcpcio 0.63.5__py3-none-any.whl → 0.63.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of xcpcio might be problematic. Click here for more details.

@@ -0,0 +1,35 @@
1
+ import logging
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ from fastapi import APIRouter, Path, Query
5
+
6
+ from ..dependencies import ContestServiceDep
7
+
8
+ router = APIRouter()
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ @router.get(
13
+ "/contests/{contest_id}/runs",
14
+ summary="Get all the runs for this contest",
15
+ response_model=List[Dict[str, Any]],
16
+ )
17
+ async def get_runs(
18
+ contest_id: str = Path(..., description="Contest identifier"),
19
+ judgement_id: Optional[str] = Query(None, description="Filter runs by judgement ID"),
20
+ service: ContestServiceDep = None,
21
+ ) -> List[Dict[str, Any]]:
22
+ return service.get_runs(contest_id, judgement_id)
23
+
24
+
25
+ @router.get(
26
+ "/contests/{contest_id}/runs/{run_id}",
27
+ summary="Get the given run for this contest",
28
+ response_model=Dict[str, Any],
29
+ )
30
+ async def get_run(
31
+ contest_id: str = Path(..., description="Contest identifier"),
32
+ run_id: str = Path(..., description="Run identifier"),
33
+ service: ContestServiceDep = None,
34
+ ) -> Dict[str, Any]:
35
+ return service.get_run(contest_id, run_id)
@@ -0,0 +1,71 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Any, Dict, List
4
+
5
+ from fastapi import APIRouter, HTTPException
6
+ from fastapi import Path as FastAPIPath
7
+ from fastapi.responses import FileResponse
8
+
9
+ from ..dependencies import ContestServiceDep
10
+
11
+ router = APIRouter()
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @router.get(
16
+ "/contests/{contest_id}/submissions",
17
+ summary="Get all the submissions for this contest",
18
+ response_model=List[Dict[str, Any]],
19
+ )
20
+ async def get_submissions(
21
+ contest_id: str = FastAPIPath(..., description="Contest identifier"),
22
+ service: ContestServiceDep = None,
23
+ ) -> List[Dict[str, Any]]:
24
+ return service.get_submissions(contest_id)
25
+
26
+
27
+ @router.get(
28
+ "/contests/{contest_id}/submissions/{submission_id}",
29
+ summary="Get the given submission for this contest",
30
+ response_model=Dict[str, Any],
31
+ )
32
+ async def get_submission(
33
+ contest_id: str = FastAPIPath(..., description="Contest identifier"),
34
+ submission_id: str = FastAPIPath(..., description="Submission identifier"),
35
+ service: ContestServiceDep = None,
36
+ ) -> Dict[str, Any]:
37
+ return service.get_submission(contest_id, submission_id)
38
+
39
+
40
+ @router.get(
41
+ "/contests/{contest_id}/submissions/{submission_id}/files",
42
+ summary="Get Submission Files",
43
+ response_class=FileResponse,
44
+ )
45
+ async def get_submission_files(
46
+ contest_id: str = FastAPIPath(..., description="Contest identifier"),
47
+ submission_id: str = FastAPIPath(..., description="Submission identifier"),
48
+ service: ContestServiceDep = None,
49
+ ) -> FileResponse:
50
+ service.validate_contest_id(contest_id)
51
+
52
+ submission = service.submissions_by_id.get(submission_id)
53
+ if not submission:
54
+ raise HTTPException(status_code=404, detail=f"Submission {submission_id} not found")
55
+
56
+ expected_href = f"contests/{contest_id}/submissions/{submission_id}/files"
57
+
58
+ try:
59
+ files: List[Dict] = submission["files"]
60
+ for file_info in files:
61
+ href = file_info["href"]
62
+ if href == expected_href:
63
+ filename = file_info["filename"]
64
+ submission_file: Path = service.contest_package_dir / "submissions" / submission_id / filename
65
+ if submission_file.exists():
66
+ mime_type = file_info["mime"]
67
+ return FileResponse(path=submission_file, media_type=mime_type, filename=filename)
68
+ except Exception as e:
69
+ raise HTTPException(
70
+ status_code=404, detail=f"Submission files not found. [submission_id={submission_id}] [err={e}]"
71
+ )
@@ -0,0 +1,69 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Any, Dict, List
4
+
5
+ from fastapi import APIRouter, HTTPException
6
+ from fastapi import Path as FastAPIPath
7
+ from fastapi.responses import FileResponse
8
+
9
+ from ..dependencies import ContestServiceDep
10
+
11
+ router = APIRouter()
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @router.get(
16
+ "/contests/{contest_id}/teams",
17
+ summary="Get all the teams for this contest",
18
+ response_model=List[Dict[str, Any]],
19
+ )
20
+ async def get_teams(
21
+ contest_id: str = FastAPIPath(..., description="Contest identifier"),
22
+ service: ContestServiceDep = None,
23
+ ) -> List[Dict[str, Any]]:
24
+ return service.get_teams(contest_id)
25
+
26
+
27
+ @router.get(
28
+ "/contests/{contest_id}/teams/{team_id}",
29
+ summary="Get the given team for this contest",
30
+ response_model=Dict[str, Any],
31
+ )
32
+ async def get_team(
33
+ contest_id: str = FastAPIPath(..., description="Contest identifier"),
34
+ team_id: str = FastAPIPath(..., description="Team identifier"),
35
+ service: ContestServiceDep = None,
36
+ ) -> Dict[str, Any]:
37
+ return service.get_team(contest_id, team_id)
38
+
39
+
40
+ @router.get(
41
+ "/contests/{contest_id}/teams/{team_id}/photo",
42
+ summary="Get Team Photo",
43
+ response_class=FileResponse,
44
+ )
45
+ async def get_team_photo(
46
+ contest_id: str = FastAPIPath(..., description="Contest identifier"),
47
+ team_id: str = FastAPIPath(..., description="Team identifier"),
48
+ service: ContestServiceDep = None,
49
+ ) -> FileResponse:
50
+ service.validate_contest_id(contest_id)
51
+
52
+ team = service.teams_by_id.get(team_id)
53
+ if not team:
54
+ raise HTTPException(status_code=404, detail=f"Team {team_id} not found")
55
+
56
+ expected_href = f"contests/{contest_id}/teams/{team_id}/photo"
57
+
58
+ try:
59
+ photos = team["photo"]
60
+ for photo in photos:
61
+ href = photo["href"]
62
+ if href == expected_href:
63
+ filename = photo["filename"]
64
+ photo_file: Path = service.contest_package_dir / "teams" / team_id / filename
65
+ if photo_file.exists():
66
+ mime_type = photo["mime"]
67
+ return FileResponse(path=photo_file, media_type=mime_type, filename=filename)
68
+ except Exception as e:
69
+ raise HTTPException(status_code=404, detail=f"Photo not found. [team_id={team_id}] [err={e}]")
@@ -0,0 +1,83 @@
1
+ """
2
+ Contest API Server
3
+
4
+ Main server class implementing the Contest API specification using modern FastAPI architecture.
5
+ """
6
+
7
+ import logging
8
+ from pathlib import Path
9
+
10
+ import uvicorn
11
+ from fastapi import FastAPI
12
+ from fastapi.middleware.cors import CORSMiddleware
13
+
14
+ from xcpcio.__version__ import __version__
15
+
16
+ from .dependencies import configure_dependencies
17
+ from .routes import create_router
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class ContestAPIServer:
23
+ """
24
+ Contest API Server implementing the Contest API specification.
25
+
26
+ This server provides REST API endpoints for contest data based on
27
+ the Contest Package format and Contest API specification.
28
+ """
29
+
30
+ def __init__(self, contest_package_dir: Path):
31
+ """
32
+ Initialize the Contest API Server.
33
+
34
+ Args:
35
+ contest_packages: Dictionary mapping contest_id to contest package directory
36
+ """
37
+ self.contest_package_dir = contest_package_dir
38
+
39
+ # Configure dependency injection for multi-contest mode
40
+ # This might need adjustment based on how dependencies work
41
+ configure_dependencies(contest_package_dir)
42
+
43
+ # Create FastAPI application
44
+ self.app = FastAPI(
45
+ title="Contest API Server",
46
+ description="REST API for Contest Control System specifications",
47
+ version=__version__,
48
+ docs_url="/docs",
49
+ redoc_url="/redoc",
50
+ openapi_url="/openapi.json",
51
+ )
52
+
53
+ # Add CORS middleware
54
+ self.app.add_middleware(
55
+ CORSMiddleware,
56
+ allow_origins=["*"],
57
+ allow_credentials=True,
58
+ allow_methods=["*"],
59
+ allow_headers=["*"],
60
+ )
61
+
62
+ # Include all routes
63
+ router = create_router()
64
+ self.app.include_router(router)
65
+
66
+ def run(self, host: str = "0.0.0.0", port: int = 8000, reload: bool = True, log_level: str = "info"):
67
+ """
68
+ Run the contest API server.
69
+
70
+ Args:
71
+ host: Host to bind to
72
+ port: Port to bind to
73
+ reload: Enable auto-reload for development
74
+ log_level: Log level (debug, info, warning, error, critical)
75
+ """
76
+
77
+ logger.info("Starting Contest API Server...")
78
+ logger.info(f"Contest package dir: {self.contest_package_dir}")
79
+ logger.info(f"API will be available at: http://{host}:{port}")
80
+ logger.info(f"Interactive docs at: http://{host}:{port}/docs")
81
+ logger.info(f"ReDoc at: http://{host}:{port}/redoc")
82
+
83
+ uvicorn.run(self.app, host=host, port=port, reload=reload, log_level=log_level)
@@ -0,0 +1,9 @@
1
+ """
2
+ Services package for Contest API Server
3
+
4
+ Contains business logic and data access layers.
5
+ """
6
+
7
+ from .contest_service import ContestService
8
+
9
+ __all__ = ["ContestService"]
@@ -0,0 +1,325 @@
1
+ """
2
+ Contest Service
3
+
4
+ Business logic layer for Contest API operations.
5
+ Handles file reading, data validation, and business operations.
6
+ """
7
+
8
+ import bisect
9
+ import json
10
+ from collections import defaultdict
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional, Union
13
+
14
+ from fastapi import HTTPException
15
+
16
+
17
+ class ContestService:
18
+ """Service class for contest-related operations"""
19
+
20
+ def __init__(self, contest_package_dir: Path):
21
+ """
22
+ Initialize the contest service.
23
+
24
+ Args:
25
+ contest_package_dir: Path to the contest package directory
26
+ """
27
+ self.contest_package_dir = contest_package_dir
28
+ if not self.contest_package_dir.exists():
29
+ raise ValueError(f"Contest package directory does not exist: {contest_package_dir}")
30
+
31
+ # Initialize data indexes for faster lookups
32
+ self._load_indexes()
33
+
34
+ def _create_index_by_id(self, data: List[Dict[str, Any]], id_name: str) -> Dict[str, List[Dict]]:
35
+ res = defaultdict(list)
36
+ for item in data:
37
+ res[item[id_name]].append(item)
38
+ return res
39
+
40
+ def _load_indexes(self) -> None:
41
+ """Load and index commonly accessed data for faster lookups"""
42
+ self.access = self.load_json_file("access.json")
43
+
44
+ self.accounts = self.load_json_file("accounts.json")
45
+ self.accounts_by_id = {account["id"] for account in self.accounts}
46
+
47
+ self.api_info = self.load_json_file("api.json")
48
+
49
+ self.awards = self.load_json_file("awards.json")
50
+ self.awards_by_id = {award["id"] for award in self.awards}
51
+
52
+ self.clarifications = self.load_json_file("clarifications.json")
53
+ self.clarifications_by_id = {clarification["id"] for clarification in self.clarifications}
54
+
55
+ self.contest = self.load_json_file("contest.json")
56
+ self.contest_state = self.load_json_file("state.json")
57
+
58
+ self.groups = self.load_json_file("groups.json")
59
+ self.groups_by_id = {group["id"]: group for group in self.groups}
60
+
61
+ self.judgement_types = self.load_json_file("judgement-types.json")
62
+ self.judgement_types_by_id = {judgement_type["id"] for judgement_type in self.judgement_types}
63
+
64
+ self.judgements = self.load_json_file("judgements.json")
65
+ self.judgements_by_id = {judgement["id"] for judgement in self.judgements}
66
+ self.judgements_by_submission_id = self._create_index_by_id(self.judgements, "submission_id")
67
+
68
+ self.languages = self.load_json_file("languages.json")
69
+ self.languages_by_id = {language["id"] for language in self.languages}
70
+
71
+ self.organizations = self.load_json_file("organizations.json")
72
+ self.organizations_by_id = {org["id"]: org for org in self.organizations}
73
+
74
+ self.problems = self.load_json_file("problems.json")
75
+ self.problems_by_id = {problem["id"]: problem for problem in self.problems}
76
+
77
+ self.runs = self.load_json_file("runs.json")
78
+ self.runs_by_id = {run["id"] for run in self.runs}
79
+ self.runs_by_judgement_id = self._create_index_by_id(self.runs, "judgement_id")
80
+
81
+ self.submissions = self.load_json_file("submissions.json")
82
+ self.submissions_by_id = {submission["id"]: submission for submission in self.submissions}
83
+
84
+ self.teams = self.load_json_file("teams.json")
85
+ self.teams_by_id = {team["id"]: team for team in self.teams}
86
+
87
+ self.event_feed = self.load_ndjson_file("event-feed.ndjson")
88
+ self.event_feed_tokens = [event["token"] for event in self.event_feed]
89
+
90
+ def load_json_file(self, filepath: str) -> Union[Dict[str, Any], List[Any]]:
91
+ """
92
+ Load JSON data from contest package directory.
93
+
94
+ Args:
95
+ filepath: Relative path to JSON file within contest package
96
+
97
+ Returns:
98
+ Parsed JSON data
99
+
100
+ Raises:
101
+ HTTPException: If file not found or invalid JSON
102
+ """
103
+
104
+ full_path = self.contest_package_dir / filepath
105
+ try:
106
+ with open(full_path, "r", encoding="utf-8") as f:
107
+ return json.load(f)
108
+ except FileNotFoundError:
109
+ raise HTTPException(status_code=404, detail=f"File not found: {filepath}")
110
+ except json.JSONDecodeError as e:
111
+ raise HTTPException(status_code=500, detail=f"Invalid JSON in file {filepath}: {e}")
112
+
113
+ def load_ndjson_file(self, filepath: str) -> List[Dict[str, Any]]:
114
+ full_path = self.contest_package_dir / filepath
115
+ try:
116
+ data = list()
117
+ with open(full_path, "r", encoding="utf-8") as f:
118
+ for line in f.readlines():
119
+ data.append(json.loads(line))
120
+ return data
121
+ except FileNotFoundError:
122
+ raise HTTPException(status_code=404, detail=f"File not found: {filepath}")
123
+ except json.JSONDecodeError as e:
124
+ raise HTTPException(status_code=500, detail=f"Invalid JSON in file {filepath}: {e}")
125
+
126
+ def get_contest_id(self) -> str:
127
+ """
128
+ Get contest ID from contest.json.
129
+
130
+ Returns:
131
+ Contest ID string
132
+ """
133
+ contest_data = self.load_json_file("contest.json")
134
+ return contest_data.get("id", "unknown")
135
+
136
+ def validate_contest_id(self, contest_id: str) -> None:
137
+ """
138
+ Validate that the provided contest ID matches the expected one.
139
+
140
+ Args:
141
+ contest_id: Contest ID to validate
142
+
143
+ Raises:
144
+ HTTPException: If contest ID doesn't match
145
+ """
146
+ expected_id = self.get_contest_id()
147
+ if contest_id != expected_id:
148
+ raise HTTPException(status_code=404, detail=f"Contest {contest_id} not found")
149
+
150
+ # API Information
151
+ def get_api_info(self) -> Dict[str, Any]:
152
+ return self.api_info
153
+
154
+ def get_access(self, contest_id: str) -> Dict[str, Any]:
155
+ self.validate_contest_id(contest_id)
156
+ return self.access
157
+
158
+ # Account operations
159
+ def get_accounts(self, contest_id: str) -> List[Dict[str, Any]]:
160
+ self.validate_contest_id(contest_id)
161
+ return self.accounts
162
+
163
+ def get_account(self, contest_id: str, account_id: str) -> Dict[str, Any]:
164
+ self.validate_contest_id(contest_id)
165
+ if account_id not in self.accounts_by_id:
166
+ raise HTTPException(status_code=404, detail=f"Account {account_id} not found")
167
+ return self.accounts_by_id[account_id]
168
+
169
+ # Contest operations
170
+ def get_contests(self) -> List[Dict[str, Any]]:
171
+ return [self.contest]
172
+
173
+ def get_contest(self, contest_id: str) -> Dict[str, Any]:
174
+ self.validate_contest_id(contest_id)
175
+ return self.contest
176
+
177
+ def get_contest_state(self, contest_id: str) -> Dict[str, Any]:
178
+ self.validate_contest_id(contest_id)
179
+ return self.contest_state
180
+
181
+ # Problem operations
182
+ def get_problems(self, contest_id: str) -> List[Dict[str, Any]]:
183
+ self.validate_contest_id(contest_id)
184
+ return self.problems
185
+
186
+ def get_problem(self, contest_id: str, problem_id: str) -> Dict[str, Any]:
187
+ self.validate_contest_id(contest_id)
188
+ if problem_id not in self.problems_by_id:
189
+ raise HTTPException(status_code=404, detail=f"Problem {problem_id} not found")
190
+ return self.problems_by_id[problem_id]
191
+
192
+ # Team operations
193
+ def get_teams(self, contest_id: str) -> List[Dict[str, Any]]:
194
+ self.validate_contest_id(contest_id)
195
+ return self.teams
196
+
197
+ def get_team(self, contest_id: str, team_id: str) -> Dict[str, Any]:
198
+ self.validate_contest_id(contest_id)
199
+ if team_id not in self.teams_by_id:
200
+ raise HTTPException(status_code=404, detail=f"Team {team_id} not found")
201
+ return self.teams_by_id[team_id]
202
+
203
+ # Organization operations
204
+ def get_organizations(self, contest_id: str) -> List[Dict[str, Any]]:
205
+ self.validate_contest_id(contest_id)
206
+ return self.organizations
207
+
208
+ def get_organization(self, contest_id: str, organization_id: str) -> Dict[str, Any]:
209
+ self.validate_contest_id(contest_id)
210
+ if organization_id not in self.organizations_by_id:
211
+ raise HTTPException(status_code=404, detail=f"Organization {organization_id} not found")
212
+ return self.organizations_by_id[organization_id]
213
+
214
+ # Group operations
215
+ def get_groups(self, contest_id: str) -> List[Dict[str, Any]]:
216
+ self.validate_contest_id(contest_id)
217
+ return self.groups
218
+
219
+ def get_group(self, contest_id: str, group_id: str) -> Dict[str, Any]:
220
+ self.validate_contest_id(contest_id)
221
+ if group_id not in self.groups_by_id:
222
+ raise HTTPException(status_code=404, detail=f"Group {group_id} not found")
223
+ return self.groups_by_id[group_id]
224
+
225
+ # Language operations
226
+ def get_languages(self, contest_id: str) -> List[Dict[str, Any]]:
227
+ self.validate_contest_id(contest_id)
228
+ return self.languages
229
+
230
+ def get_language(self, contest_id: str, language_id: str) -> Dict[str, Any]:
231
+ self.validate_contest_id(contest_id)
232
+ if language_id not in self.languages_by_id:
233
+ raise HTTPException(status_code=404, detail=f"Language {language_id} not found")
234
+ return self.languages_by_id[language_id]
235
+
236
+ # Judgement type operations
237
+ def get_judgement_types(self, contest_id: str) -> List[Dict[str, Any]]:
238
+ self.validate_contest_id(contest_id)
239
+ return self.judgement_types
240
+
241
+ def get_judgement_type(self, contest_id: str, judgement_type_id: str) -> Dict[str, Any]:
242
+ self.validate_contest_id(contest_id)
243
+ if judgement_type_id not in self.judgement_types_by_id:
244
+ raise HTTPException(status_code=404, detail=f"Judgement type {judgement_type_id} not found")
245
+ return self.judgement_types_by_id[judgement_type_id]
246
+
247
+ # Submission operations
248
+ def get_submissions(self, contest_id: str) -> List[Dict[str, Any]]:
249
+ self.validate_contest_id(contest_id)
250
+ return self.submissions
251
+
252
+ def get_submission(self, contest_id: str, submission_id: str) -> Dict[str, Any]:
253
+ self.validate_contest_id(contest_id)
254
+ if submission_id not in self.submissions_by_id:
255
+ raise HTTPException(status_code=404, detail=f"Submission {submission_id} not found")
256
+ return self.submissions_by_id[submission_id]
257
+
258
+ # Judgement operations
259
+ def get_judgements(self, contest_id: str, submission_id: Optional[str] = None) -> List[Dict[str, Any]]:
260
+ self.validate_contest_id(contest_id)
261
+
262
+ if submission_id is not None:
263
+ if submission_id not in self.judgements_by_submission_id:
264
+ raise HTTPException(status_code=404, detail=f"Submission id not found: {submission_id}")
265
+ return self.judgements_by_submission_id[submission_id]
266
+
267
+ return self.judgements
268
+
269
+ def get_judgement(self, contest_id: str, judgement_id: str) -> Dict[str, Any]:
270
+ self.validate_contest_id(contest_id)
271
+ if judgement_id not in self.judgements_by_id:
272
+ raise HTTPException(status_code=404, detail=f"Judgement {judgement_id} not found")
273
+ return self.judgements_by_id[judgement_id]
274
+
275
+ # Run operations
276
+ def get_runs(self, contest_id: str, judgement_id: Optional[str] = None) -> List[Dict[str, Any]]:
277
+ self.validate_contest_id(contest_id)
278
+
279
+ if judgement_id is not None:
280
+ if judgement_id not in self.runs_by_judgement_id:
281
+ raise HTTPException(status_code=404, detail=f"Judgement id not found: {judgement_id}")
282
+ return self.runs_by_judgement_id[judgement_id]
283
+
284
+ return self.runs
285
+
286
+ def get_run(self, contest_id: str, run_id: str) -> Dict[str, Any]:
287
+ self.validate_contest_id(contest_id)
288
+ if run_id not in self.runs_by_id:
289
+ raise HTTPException(status_code=404, detail=f"Run {run_id} not found")
290
+ return self.runs_by_id[run_id]
291
+
292
+ # Clarification operations
293
+ def get_clarifications(
294
+ self,
295
+ contest_id: str,
296
+ ) -> List[Dict[str, Any]]:
297
+ self.validate_contest_id(contest_id)
298
+ return self.clarifications
299
+
300
+ def get_clarification(self, contest_id: str, clarification_id: str) -> Dict[str, Any]:
301
+ self.validate_contest_id(contest_id)
302
+ if clarification_id not in self.clarifications_by_id:
303
+ raise HTTPException(status_code=404, detail=f"Clarification {clarification_id} not found")
304
+ return self.clarifications_by_id[clarification_id]
305
+
306
+ # Award operations
307
+ def get_awards(self, contest_id: str) -> List[Dict[str, Any]]:
308
+ self.validate_contest_id(contest_id)
309
+ return self.awards
310
+
311
+ def get_award(self, contest_id: str, award_id: str) -> Dict[str, Any]:
312
+ self.validate_contest_id(contest_id)
313
+ if award_id not in self.awards_by_id:
314
+ raise HTTPException(status_code=404, detail=f"Award {award_id} not found")
315
+ return self.awards_by_id[award_id]
316
+
317
+ # Event Feed operations
318
+ def get_event_feed(self, contest_id: str, since_token: Optional[str] = None) -> List[Dict[str, Any]]:
319
+ self.validate_contest_id(contest_id)
320
+
321
+ if since_token is None:
322
+ return self.event_feed
323
+
324
+ idx = bisect.bisect_left(self.event_feed_tokens, since_token)
325
+ return self.event_feed[idx:]
@@ -59,7 +59,6 @@ class ContestArchiver:
59
59
 
60
60
  # Known endpoints that can be fetched
61
61
  KNOWN_ENDPOINTS = [
62
- "access",
63
62
  "contests",
64
63
  "judgement-types",
65
64
  "languages",
@@ -80,7 +79,6 @@ class ContestArchiver:
80
79
  ]
81
80
 
82
81
  DOMJUDGE_KNOWN_ENDPOINTS = [
83
- "access",
84
82
  "contests",
85
83
  "judgement-types",
86
84
  "languages",
@@ -289,7 +287,7 @@ class ContestArchiver:
289
287
 
290
288
  data = await self.fetch_json("/")
291
289
  if not data:
292
- raise RuntimeError("Failed to fetch API information from root endpoint")
290
+ raise RuntimeError("Failed to fetch API information")
293
291
 
294
292
  self._api_info = data # Store API info for later use
295
293
 
@@ -401,6 +399,7 @@ class ContestArchiver:
401
399
  # Always dump API and contest info
402
400
  await self.dump_api_info()
403
401
  await self.dump_contest_info()
402
+ await self.dump_endpoint_single("access")
404
403
 
405
404
  # Get list of endpoints to dump
406
405
  if self._config.endpoints:
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xcpcio
3
- Version: 0.63.5
3
+ Version: 0.63.7
4
4
  Summary: xcpcio python lib
5
5
  Project-URL: homepage, https://github.com/xcpcio/xcpcio
6
6
  Project-URL: documentation, https://github.com/xcpcio/xcpcio
7
7
  Project-URL: repository, https://github.com/xcpcio/xcpcio
8
8
  Author-email: Dup4 <hi@dup4.com>
9
+ Maintainer-email: Dup4 <hi@dup4.com>, cubercsl <hi@cubercsl.site>
9
10
  License-Expression: MIT
10
11
  Keywords: xcpcio
11
12
  Classifier: Programming Language :: Python :: 3
@@ -18,10 +19,12 @@ Requires-Python: >=3.11
18
19
  Requires-Dist: aiofiles>=23.0.0
19
20
  Requires-Dist: aiohttp>=3.8.0
20
21
  Requires-Dist: click>=8.0.0
22
+ Requires-Dist: fastapi>=0.117.1
21
23
  Requires-Dist: pydantic>=2.11.7
22
24
  Requires-Dist: pyyaml>=6.0.0
23
25
  Requires-Dist: semver>=3.0.0
24
26
  Requires-Dist: tenacity>=8.0.0
27
+ Requires-Dist: uvicorn>=0.36.0
25
28
  Description-Content-Type: text/markdown
26
29
 
27
30
  # xcpcio-python