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.
- xcpcio/__version__.py +4 -1
- xcpcio/ccs/__init__.py +2 -2
- xcpcio/ccs/api_server/__init__.py +9 -0
- xcpcio/ccs/api_server/dependencies.py +48 -0
- xcpcio/ccs/api_server/routes/__init__.py +52 -0
- xcpcio/ccs/api_server/routes/access.py +18 -0
- xcpcio/ccs/api_server/routes/accounts.py +35 -0
- xcpcio/ccs/api_server/routes/awards.py +33 -0
- xcpcio/ccs/api_server/routes/clarifications.py +34 -0
- xcpcio/ccs/api_server/routes/contests.py +119 -0
- xcpcio/ccs/api_server/routes/general.py +33 -0
- xcpcio/ccs/api_server/routes/groups.py +34 -0
- xcpcio/ccs/api_server/routes/judgement_types.py +34 -0
- xcpcio/ccs/api_server/routes/judgements.py +35 -0
- xcpcio/ccs/api_server/routes/languages.py +34 -0
- xcpcio/ccs/api_server/routes/organizations.py +71 -0
- xcpcio/ccs/api_server/routes/problems.py +69 -0
- xcpcio/ccs/api_server/routes/runs.py +35 -0
- xcpcio/ccs/api_server/routes/submissions.py +71 -0
- xcpcio/ccs/api_server/routes/teams.py +69 -0
- xcpcio/ccs/api_server/server.py +83 -0
- xcpcio/ccs/api_server/services/__init__.py +9 -0
- xcpcio/ccs/api_server/services/contest_service.py +325 -0
- xcpcio/ccs/contest_archiver.py +2 -3
- {xcpcio-0.63.5.dist-info → xcpcio-0.63.7.dist-info}/METADATA +4 -1
- xcpcio-0.63.7.dist-info/RECORD +34 -0
- xcpcio-0.63.5.dist-info/RECORD +0 -13
- {xcpcio-0.63.5.dist-info → xcpcio-0.63.7.dist-info}/WHEEL +0 -0
- {xcpcio-0.63.5.dist-info → xcpcio-0.63.7.dist-info}/entry_points.txt +0 -0
|
@@ -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,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:]
|
xcpcio/ccs/contest_archiver.py
CHANGED
|
@@ -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
|
|
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.
|
|
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
|