xcpcio 0.63.7__py3-none-any.whl → 0.64.0__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 +1 -1
- xcpcio/ccs/api_server/dependencies.py +8 -3
- xcpcio/ccs/api_server/routes/contests.py +5 -34
- xcpcio/ccs/api_server/routes/organizations.py +3 -24
- xcpcio/ccs/api_server/routes/problems.py +3 -22
- xcpcio/ccs/api_server/routes/submissions.py +3 -24
- xcpcio/ccs/api_server/routes/teams.py +3 -22
- xcpcio/ccs/api_server/services/contest_service.py +104 -235
- xcpcio/ccs/base/__init__.py +3 -0
- xcpcio/ccs/base/types.py +9 -0
- xcpcio/ccs/reader/__init__.py +0 -0
- xcpcio/ccs/reader/base_ccs_reader.py +165 -0
- xcpcio/ccs/reader/contest_package_reader.py +331 -0
- {xcpcio-0.63.7.dist-info → xcpcio-0.64.0.dist-info}/METADATA +2 -1
- {xcpcio-0.63.7.dist-info → xcpcio-0.64.0.dist-info}/RECORD +17 -12
- {xcpcio-0.63.7.dist-info → xcpcio-0.64.0.dist-info}/WHEEL +0 -0
- {xcpcio-0.63.7.dist-info → xcpcio-0.64.0.dist-info}/entry_points.txt +0 -0
xcpcio/__version__.py
CHANGED
|
@@ -5,13 +5,15 @@ Dependency injection system for Contest API Server.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Annotated
|
|
8
|
+
from typing import Annotated, Dict
|
|
9
9
|
|
|
10
10
|
from fastapi import Depends
|
|
11
11
|
|
|
12
|
+
from xcpcio.ccs.reader.base_ccs_reader import BaseCCSReader
|
|
13
|
+
from xcpcio.ccs.reader.contest_package_reader import ContestPackageReader
|
|
14
|
+
|
|
12
15
|
from .services.contest_service import ContestService
|
|
13
16
|
|
|
14
|
-
# Global contest service instance cache
|
|
15
17
|
_contest_service_instance = None
|
|
16
18
|
|
|
17
19
|
|
|
@@ -41,7 +43,10 @@ def configure_dependencies(contest_package_dir: Path) -> None:
|
|
|
41
43
|
contest_package_dir: Path to contest package directory
|
|
42
44
|
"""
|
|
43
45
|
global _contest_service_instance
|
|
44
|
-
|
|
46
|
+
reader_dict: Dict[str, BaseCCSReader] = {}
|
|
47
|
+
contest_package_reader = ContestPackageReader(contest_package_dir)
|
|
48
|
+
reader_dict[contest_package_reader.get_contest_id()] = contest_package_reader
|
|
49
|
+
_contest_service_instance = ContestService(reader_dict)
|
|
45
50
|
|
|
46
51
|
|
|
47
52
|
# Type alias for dependency injection
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
-
from pathlib import Path
|
|
4
3
|
from typing import Any, Dict, List, Optional
|
|
5
4
|
|
|
6
|
-
from fastapi import APIRouter,
|
|
5
|
+
from fastapi import APIRouter, Query
|
|
7
6
|
from fastapi import Path as FastAPIPath
|
|
8
7
|
from fastapi.responses import FileResponse, StreamingResponse
|
|
9
8
|
|
|
@@ -55,22 +54,8 @@ async def get_contest_banner(
|
|
|
55
54
|
contest_id: str = FastAPIPath(..., description="Contest identifier"),
|
|
56
55
|
service: ContestServiceDep = None,
|
|
57
56
|
) -> FileResponse:
|
|
58
|
-
service.
|
|
59
|
-
|
|
60
|
-
expected_href = f"contests/{contest_id}/banner"
|
|
61
|
-
|
|
62
|
-
try:
|
|
63
|
-
banners = service.contest.get("banner", [])
|
|
64
|
-
for banner in banners:
|
|
65
|
-
href = banner["href"]
|
|
66
|
-
if href == expected_href:
|
|
67
|
-
filename = banner["filename"]
|
|
68
|
-
banner_file: Path = service.contest_package_dir / "contest" / filename
|
|
69
|
-
if banner_file.exists():
|
|
70
|
-
mime_type = banner["mime"]
|
|
71
|
-
return FileResponse(path=banner_file, media_type=mime_type, filename=filename)
|
|
72
|
-
except Exception as e:
|
|
73
|
-
raise HTTPException(status_code=404, detail=f"Banner not found. [contest_id={contest_id}] [err={e}]")
|
|
57
|
+
file_attr = service.get_contest_banner(contest_id)
|
|
58
|
+
return FileResponse(path=file_attr.path, media_type=file_attr.media_type, filename=file_attr.name)
|
|
74
59
|
|
|
75
60
|
|
|
76
61
|
@router.get(
|
|
@@ -82,22 +67,8 @@ async def get_contest_problem_set(
|
|
|
82
67
|
contest_id: str = FastAPIPath(..., description="Contest identifier"),
|
|
83
68
|
service: ContestServiceDep = None,
|
|
84
69
|
) -> FileResponse:
|
|
85
|
-
service.
|
|
86
|
-
|
|
87
|
-
expected_href = f"contests/{contest_id}/problemset"
|
|
88
|
-
|
|
89
|
-
try:
|
|
90
|
-
problem_set_list = service.contest.get("problemset", [])
|
|
91
|
-
for problem_set in problem_set_list:
|
|
92
|
-
href = problem_set["href"]
|
|
93
|
-
if href == expected_href:
|
|
94
|
-
filename = problem_set["filename"]
|
|
95
|
-
problem_set_file: Path = service.contest_package_dir / "contest" / filename
|
|
96
|
-
if problem_set_file.exists():
|
|
97
|
-
mime_type = problem_set["mime"]
|
|
98
|
-
return FileResponse(path=problem_set_file, media_type=mime_type, filename=filename)
|
|
99
|
-
except Exception as e:
|
|
100
|
-
raise HTTPException(status_code=404, detail=f"Problem set not found. [contest_id={contest_id}] [err={e}]")
|
|
70
|
+
file_attr = service.get_contest_problemset(contest_id)
|
|
71
|
+
return FileResponse(path=file_attr.path, media_type=file_attr.media_type, filename=file_attr.name)
|
|
101
72
|
|
|
102
73
|
|
|
103
74
|
@router.get(
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from pathlib import Path
|
|
3
2
|
from typing import Any, Dict, List
|
|
4
3
|
|
|
5
|
-
from fastapi import APIRouter
|
|
4
|
+
from fastapi import APIRouter
|
|
6
5
|
from fastapi import Path as FastAPIPath
|
|
7
6
|
from fastapi.responses import FileResponse
|
|
8
7
|
|
|
@@ -47,25 +46,5 @@ async def get_organization_logo(
|
|
|
47
46
|
organization_id: str = FastAPIPath(..., description="Organization identifier"),
|
|
48
47
|
service: ContestServiceDep = None,
|
|
49
48
|
) -> FileResponse:
|
|
50
|
-
service.
|
|
51
|
-
|
|
52
|
-
org = service.organizations_by_id.get(organization_id)
|
|
53
|
-
if not org:
|
|
54
|
-
raise HTTPException(status_code=404, detail=f"Organization {organization_id} not found")
|
|
55
|
-
|
|
56
|
-
expected_href = f"contests/{contest_id}/organizations/{organization_id}/logo"
|
|
57
|
-
|
|
58
|
-
try:
|
|
59
|
-
logos = org["logo"]
|
|
60
|
-
for logo in logos:
|
|
61
|
-
href = logo["href"]
|
|
62
|
-
if href == expected_href:
|
|
63
|
-
filename = logo["filename"]
|
|
64
|
-
logo_file: Path = service.contest_package_dir / "organizations" / organization_id / filename
|
|
65
|
-
if logo_file.exists():
|
|
66
|
-
mime_type = logo["mime"]
|
|
67
|
-
return FileResponse(path=logo_file, media_type=mime_type, filename=filename)
|
|
68
|
-
except Exception as e:
|
|
69
|
-
raise HTTPException(
|
|
70
|
-
status_code=404, detail=f"Logo file not found. [organization_id={organization_id}] [err={e}]"
|
|
71
|
-
)
|
|
49
|
+
file_attr = service.get_organization_logo(contest_id, organization_id)
|
|
50
|
+
return FileResponse(path=file_attr.path, media_type=file_attr.media_type, filename=file_attr.name)
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from pathlib import Path
|
|
3
2
|
from typing import Any, Dict, List
|
|
4
3
|
|
|
5
|
-
from fastapi import APIRouter
|
|
4
|
+
from fastapi import APIRouter
|
|
6
5
|
from fastapi import Path as FastAPIPath
|
|
7
6
|
from fastapi.responses import FileResponse
|
|
8
7
|
|
|
@@ -47,23 +46,5 @@ async def get_problem_statement(
|
|
|
47
46
|
problem_id: str = FastAPIPath(..., description="Problem identifier"),
|
|
48
47
|
service: ContestServiceDep = None,
|
|
49
48
|
) -> FileResponse:
|
|
50
|
-
service.
|
|
51
|
-
|
|
52
|
-
problem: Dict = service.problems_by_id.get(problem_id)
|
|
53
|
-
if not problem:
|
|
54
|
-
raise HTTPException(status_code=404, detail=f"Problem {problem_id} not found")
|
|
55
|
-
|
|
56
|
-
expected_href = f"contests/{contest_id}/problems/{problem_id}/statement"
|
|
57
|
-
|
|
58
|
-
try:
|
|
59
|
-
statements = problem.get("statement", [])
|
|
60
|
-
for statement in statements:
|
|
61
|
-
href = statement["href"]
|
|
62
|
-
filename = statement["filename"]
|
|
63
|
-
if href == expected_href and filename:
|
|
64
|
-
statement_file: Path = service.contest_package_dir / "problems" / problem_id / filename
|
|
65
|
-
if statement_file.exists():
|
|
66
|
-
mime_type = statement["mime"]
|
|
67
|
-
return FileResponse(path=statement_file, media_type=mime_type, filename=filename)
|
|
68
|
-
except Exception as e:
|
|
69
|
-
raise HTTPException(status_code=404, detail=f"Statement not found. [problem_id={problem_id}] [err={e}]")
|
|
49
|
+
file_attr = service.get_problem_statement(contest_id, problem_id)
|
|
50
|
+
return FileResponse(path=file_attr.path, media_type=file_attr.media_type, filename=file_attr.name)
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from pathlib import Path
|
|
3
2
|
from typing import Any, Dict, List
|
|
4
3
|
|
|
5
|
-
from fastapi import APIRouter
|
|
4
|
+
from fastapi import APIRouter
|
|
6
5
|
from fastapi import Path as FastAPIPath
|
|
7
6
|
from fastapi.responses import FileResponse
|
|
8
7
|
|
|
@@ -47,25 +46,5 @@ async def get_submission_files(
|
|
|
47
46
|
submission_id: str = FastAPIPath(..., description="Submission identifier"),
|
|
48
47
|
service: ContestServiceDep = None,
|
|
49
48
|
) -> FileResponse:
|
|
50
|
-
service.
|
|
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
|
-
)
|
|
49
|
+
file_attr = service.get_submission_file(contest_id, submission_id)
|
|
50
|
+
return FileResponse(path=file_attr.path, media_type=file_attr.media_type, filename=file_attr.name)
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from pathlib import Path
|
|
3
2
|
from typing import Any, Dict, List
|
|
4
3
|
|
|
5
|
-
from fastapi import APIRouter
|
|
4
|
+
from fastapi import APIRouter
|
|
6
5
|
from fastapi import Path as FastAPIPath
|
|
7
6
|
from fastapi.responses import FileResponse
|
|
8
7
|
|
|
@@ -47,23 +46,5 @@ async def get_team_photo(
|
|
|
47
46
|
team_id: str = FastAPIPath(..., description="Team identifier"),
|
|
48
47
|
service: ContestServiceDep = None,
|
|
49
48
|
) -> FileResponse:
|
|
50
|
-
service.
|
|
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}]")
|
|
49
|
+
file_attr = service.get_team_photo(contest_id, team_id)
|
|
50
|
+
return FileResponse(path=file_attr.path, media_type=file_attr.media_type, filename=file_attr.name)
|
|
@@ -5,321 +5,190 @@ Business logic layer for Contest API operations.
|
|
|
5
5
|
Handles file reading, data validation, and business operations.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
from collections import defaultdict
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
from typing import Any, Dict, List, Optional, Union
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
13
10
|
|
|
14
11
|
from fastapi import HTTPException
|
|
15
12
|
|
|
13
|
+
from xcpcio.__version__ import __version__
|
|
14
|
+
from xcpcio.ccs.base.types import FileAttr
|
|
15
|
+
from xcpcio.ccs.reader.base_ccs_reader import BaseCCSReader
|
|
16
16
|
|
|
17
|
-
|
|
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
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
39
18
|
|
|
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
19
|
|
|
44
|
-
|
|
45
|
-
|
|
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.
|
|
20
|
+
class ContestService:
|
|
21
|
+
"""Service class for contest-related operations"""
|
|
139
22
|
|
|
140
|
-
|
|
141
|
-
|
|
23
|
+
def __init__(self, reader_dict: Dict[str, BaseCCSReader]):
|
|
24
|
+
self.reader_dict = reader_dict
|
|
142
25
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
"""
|
|
146
|
-
expected_id = self.get_contest_id()
|
|
147
|
-
if contest_id != expected_id:
|
|
26
|
+
def _get_reader(self, contest_id: str) -> BaseCCSReader:
|
|
27
|
+
if contest_id not in self.reader_dict:
|
|
148
28
|
raise HTTPException(status_code=404, detail=f"Contest {contest_id} not found")
|
|
29
|
+
return self.reader_dict[contest_id]
|
|
149
30
|
|
|
150
31
|
# API Information
|
|
151
32
|
def get_api_info(self) -> Dict[str, Any]:
|
|
152
|
-
return
|
|
33
|
+
return {
|
|
34
|
+
"version": "2023-06",
|
|
35
|
+
"version_url": "https://ccs-specs.icpc.io/2023-06/contest_api",
|
|
36
|
+
"name": "XCPCIO",
|
|
37
|
+
"provider": {
|
|
38
|
+
"name": "XCPCIO",
|
|
39
|
+
"version": __version__,
|
|
40
|
+
},
|
|
41
|
+
}
|
|
153
42
|
|
|
154
43
|
def get_access(self, contest_id: str) -> Dict[str, Any]:
|
|
155
|
-
self.
|
|
156
|
-
return
|
|
44
|
+
reader = self._get_reader(contest_id)
|
|
45
|
+
return reader.get_access()
|
|
157
46
|
|
|
158
47
|
# Account operations
|
|
159
48
|
def get_accounts(self, contest_id: str) -> List[Dict[str, Any]]:
|
|
160
|
-
self.
|
|
161
|
-
return
|
|
49
|
+
reader = self._get_reader(contest_id)
|
|
50
|
+
return reader.get_accounts()
|
|
162
51
|
|
|
163
52
|
def get_account(self, contest_id: str, account_id: str) -> Dict[str, Any]:
|
|
164
|
-
self.
|
|
165
|
-
|
|
166
|
-
raise HTTPException(status_code=404, detail=f"Account {account_id} not found")
|
|
167
|
-
return self.accounts_by_id[account_id]
|
|
53
|
+
reader = self._get_reader(contest_id)
|
|
54
|
+
return reader.get_account(account_id)
|
|
168
55
|
|
|
169
56
|
# Contest operations
|
|
170
57
|
def get_contests(self) -> List[Dict[str, Any]]:
|
|
171
|
-
return [self.
|
|
58
|
+
return [reader.get_contest() for reader in self.reader_dict.values()]
|
|
172
59
|
|
|
173
60
|
def get_contest(self, contest_id: str) -> Dict[str, Any]:
|
|
174
|
-
self.
|
|
175
|
-
return
|
|
61
|
+
reader = self._get_reader(contest_id)
|
|
62
|
+
return reader.get_contest()
|
|
176
63
|
|
|
177
64
|
def get_contest_state(self, contest_id: str) -> Dict[str, Any]:
|
|
178
|
-
self.
|
|
179
|
-
return
|
|
65
|
+
reader = self._get_reader(contest_id)
|
|
66
|
+
return reader.get_contest_state()
|
|
67
|
+
|
|
68
|
+
def get_contest_banner(self, contest_id: str) -> FileAttr:
|
|
69
|
+
reader = self._get_reader(contest_id)
|
|
70
|
+
return reader.get_contest_banner()
|
|
71
|
+
|
|
72
|
+
def get_contest_problemset(self, contest_id: str) -> FileAttr:
|
|
73
|
+
reader = self._get_reader(contest_id)
|
|
74
|
+
return reader.get_contest_problemset()
|
|
180
75
|
|
|
181
76
|
# Problem operations
|
|
182
77
|
def get_problems(self, contest_id: str) -> List[Dict[str, Any]]:
|
|
183
|
-
self.
|
|
184
|
-
return
|
|
78
|
+
reader = self._get_reader(contest_id)
|
|
79
|
+
return reader.get_problems()
|
|
185
80
|
|
|
186
81
|
def get_problem(self, contest_id: str, problem_id: str) -> Dict[str, Any]:
|
|
187
|
-
self.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
82
|
+
reader = self._get_reader(contest_id)
|
|
83
|
+
return reader.get_problem(problem_id)
|
|
84
|
+
|
|
85
|
+
def get_problem_statement(self, contest_id: str, problem_id: str) -> FileAttr:
|
|
86
|
+
reader = self._get_reader(contest_id)
|
|
87
|
+
return reader.get_problem_statement(problem_id)
|
|
191
88
|
|
|
192
89
|
# Team operations
|
|
193
90
|
def get_teams(self, contest_id: str) -> List[Dict[str, Any]]:
|
|
194
|
-
self.
|
|
195
|
-
return
|
|
91
|
+
reader = self._get_reader(contest_id)
|
|
92
|
+
return reader.get_teams()
|
|
196
93
|
|
|
197
94
|
def get_team(self, contest_id: str, team_id: str) -> Dict[str, Any]:
|
|
198
|
-
self.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
95
|
+
reader = self._get_reader(contest_id)
|
|
96
|
+
return reader.get_team(team_id)
|
|
97
|
+
|
|
98
|
+
def get_team_photo(self, contest_id: str, team_id: str) -> FileAttr:
|
|
99
|
+
reader = self._get_reader(contest_id)
|
|
100
|
+
return reader.get_team_photo(team_id)
|
|
202
101
|
|
|
203
102
|
# Organization operations
|
|
204
103
|
def get_organizations(self, contest_id: str) -> List[Dict[str, Any]]:
|
|
205
|
-
self.
|
|
206
|
-
return
|
|
104
|
+
reader = self._get_reader(contest_id)
|
|
105
|
+
return reader.get_organizations()
|
|
207
106
|
|
|
208
107
|
def get_organization(self, contest_id: str, organization_id: str) -> Dict[str, Any]:
|
|
209
|
-
self.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
108
|
+
reader = self._get_reader(contest_id)
|
|
109
|
+
return reader.get_organization(organization_id)
|
|
110
|
+
|
|
111
|
+
def get_organization_logo(self, contest_id: str, organization_id: str) -> FileAttr:
|
|
112
|
+
reader = self._get_reader(contest_id)
|
|
113
|
+
return reader.get_organization_logo(organization_id)
|
|
213
114
|
|
|
214
115
|
# Group operations
|
|
215
116
|
def get_groups(self, contest_id: str) -> List[Dict[str, Any]]:
|
|
216
|
-
self.
|
|
217
|
-
return
|
|
117
|
+
reader = self._get_reader(contest_id)
|
|
118
|
+
return reader.get_groups()
|
|
218
119
|
|
|
219
120
|
def get_group(self, contest_id: str, group_id: str) -> Dict[str, Any]:
|
|
220
|
-
self.
|
|
221
|
-
|
|
222
|
-
raise HTTPException(status_code=404, detail=f"Group {group_id} not found")
|
|
223
|
-
return self.groups_by_id[group_id]
|
|
121
|
+
reader = self._get_reader(contest_id)
|
|
122
|
+
return reader.get_group(group_id)
|
|
224
123
|
|
|
225
124
|
# Language operations
|
|
226
125
|
def get_languages(self, contest_id: str) -> List[Dict[str, Any]]:
|
|
227
|
-
self.
|
|
228
|
-
return
|
|
126
|
+
reader = self._get_reader(contest_id)
|
|
127
|
+
return reader.get_languages()
|
|
229
128
|
|
|
230
129
|
def get_language(self, contest_id: str, language_id: str) -> Dict[str, Any]:
|
|
231
|
-
self.
|
|
232
|
-
|
|
233
|
-
raise HTTPException(status_code=404, detail=f"Language {language_id} not found")
|
|
234
|
-
return self.languages_by_id[language_id]
|
|
130
|
+
reader = self._get_reader(contest_id)
|
|
131
|
+
return reader.get_language(language_id)
|
|
235
132
|
|
|
236
133
|
# Judgement type operations
|
|
237
134
|
def get_judgement_types(self, contest_id: str) -> List[Dict[str, Any]]:
|
|
238
|
-
self.
|
|
239
|
-
return
|
|
135
|
+
reader = self._get_reader(contest_id)
|
|
136
|
+
return reader.get_judgement_types()
|
|
240
137
|
|
|
241
138
|
def get_judgement_type(self, contest_id: str, judgement_type_id: str) -> Dict[str, Any]:
|
|
242
|
-
self.
|
|
243
|
-
|
|
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]
|
|
139
|
+
reader = self._get_reader(contest_id)
|
|
140
|
+
return reader.get_judgement_type(judgement_type_id)
|
|
246
141
|
|
|
247
142
|
# Submission operations
|
|
248
143
|
def get_submissions(self, contest_id: str) -> List[Dict[str, Any]]:
|
|
249
|
-
self.
|
|
250
|
-
return
|
|
144
|
+
reader = self._get_reader(contest_id)
|
|
145
|
+
return reader.get_submissions()
|
|
251
146
|
|
|
252
147
|
def get_submission(self, contest_id: str, submission_id: str) -> Dict[str, Any]:
|
|
253
|
-
self.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
148
|
+
reader = self._get_reader(contest_id)
|
|
149
|
+
return reader.get_submission(submission_id)
|
|
150
|
+
|
|
151
|
+
def get_submission_file(self, contest_id: str, submission_id: str) -> FileAttr:
|
|
152
|
+
reader = self._get_reader(contest_id)
|
|
153
|
+
return reader.get_submission_file(submission_id)
|
|
257
154
|
|
|
258
155
|
# Judgement operations
|
|
259
156
|
def get_judgements(self, contest_id: str, submission_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
260
|
-
self.
|
|
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
|
|
157
|
+
reader = self._get_reader(contest_id)
|
|
158
|
+
return reader.get_judgements(submission_id)
|
|
268
159
|
|
|
269
160
|
def get_judgement(self, contest_id: str, judgement_id: str) -> Dict[str, Any]:
|
|
270
|
-
self.
|
|
271
|
-
|
|
272
|
-
raise HTTPException(status_code=404, detail=f"Judgement {judgement_id} not found")
|
|
273
|
-
return self.judgements_by_id[judgement_id]
|
|
161
|
+
reader = self._get_reader(contest_id)
|
|
162
|
+
return reader.get_judgement(judgement_id)
|
|
274
163
|
|
|
275
164
|
# Run operations
|
|
276
165
|
def get_runs(self, contest_id: str, judgement_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
277
|
-
self.
|
|
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
|
|
166
|
+
reader = self._get_reader(contest_id)
|
|
167
|
+
return reader.get_runs(judgement_id)
|
|
285
168
|
|
|
286
169
|
def get_run(self, contest_id: str, run_id: str) -> Dict[str, Any]:
|
|
287
|
-
self.
|
|
288
|
-
|
|
289
|
-
raise HTTPException(status_code=404, detail=f"Run {run_id} not found")
|
|
290
|
-
return self.runs_by_id[run_id]
|
|
170
|
+
reader = self._get_reader(contest_id)
|
|
171
|
+
return reader.get_run(run_id)
|
|
291
172
|
|
|
292
173
|
# Clarification operations
|
|
293
|
-
def get_clarifications(
|
|
294
|
-
self
|
|
295
|
-
|
|
296
|
-
) -> List[Dict[str, Any]]:
|
|
297
|
-
self.validate_contest_id(contest_id)
|
|
298
|
-
return self.clarifications
|
|
174
|
+
def get_clarifications(self, contest_id: str) -> List[Dict[str, Any]]:
|
|
175
|
+
reader = self._get_reader(contest_id)
|
|
176
|
+
return reader.get_clarifications()
|
|
299
177
|
|
|
300
178
|
def get_clarification(self, contest_id: str, clarification_id: str) -> Dict[str, Any]:
|
|
301
|
-
self.
|
|
302
|
-
|
|
303
|
-
raise HTTPException(status_code=404, detail=f"Clarification {clarification_id} not found")
|
|
304
|
-
return self.clarifications_by_id[clarification_id]
|
|
179
|
+
reader = self._get_reader(contest_id)
|
|
180
|
+
return reader.get_clarification(clarification_id)
|
|
305
181
|
|
|
306
182
|
# Award operations
|
|
307
183
|
def get_awards(self, contest_id: str) -> List[Dict[str, Any]]:
|
|
308
|
-
self.
|
|
309
|
-
return
|
|
184
|
+
reader = self._get_reader(contest_id)
|
|
185
|
+
return reader.get_awards()
|
|
310
186
|
|
|
311
187
|
def get_award(self, contest_id: str, award_id: str) -> Dict[str, Any]:
|
|
312
|
-
self.
|
|
313
|
-
|
|
314
|
-
raise HTTPException(status_code=404, detail=f"Award {award_id} not found")
|
|
315
|
-
return self.awards_by_id[award_id]
|
|
188
|
+
reader = self._get_reader(contest_id)
|
|
189
|
+
return reader.get_award(award_id)
|
|
316
190
|
|
|
317
191
|
# Event Feed operations
|
|
318
192
|
def get_event_feed(self, contest_id: str, since_token: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
319
|
-
self.
|
|
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:]
|
|
193
|
+
reader = self._get_reader(contest_id)
|
|
194
|
+
return reader.get_event_feed(since_token)
|
xcpcio/ccs/base/types.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any, Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
from xcpcio.ccs.base.types import FileAttr
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseCCSReader(ABC):
|
|
8
|
+
# API Information
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def get_api_info(self) -> Dict[str, Any]:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def get_access(self) -> Dict[str, Any]:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
# Account operations
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def get_accounts(self) -> List[Dict[str, Any]]:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def get_account(self, account_id: str) -> Dict[str, Any]:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
# Contest operations
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def get_contest_id(self) -> str:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def get_contest(self) -> Dict[str, Any]:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def get_contest_state(self) -> Dict[str, Any]:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def get_contest_banner(self) -> FileAttr:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def get_contest_problemset(self) -> FileAttr:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
# Problem operations
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def get_problems(self) -> List[Dict[str, Any]]:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def get_problem(self, problem_id: str) -> Dict[str, Any]:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def get_problem_statement(self, problem_id: str) -> FileAttr:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
# Team operations
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def get_teams(self) -> List[Dict[str, Any]]:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def get_team(self, team_id: str) -> Dict[str, Any]:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def get_team_photo(self, team_id: str) -> FileAttr:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
# Organization operations
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def get_organizations(self) -> List[Dict[str, Any]]:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def get_organization(self, organization_id: str) -> Dict[str, Any]:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def get_organization_logo(self, organization_id: str) -> FileAttr:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
# Group operations
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def get_groups(self) -> List[Dict[str, Any]]:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def get_group(self, group_id: str) -> Dict[str, Any]:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
# Language operations
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def get_languages(self) -> List[Dict[str, Any]]:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
@abstractmethod
|
|
101
|
+
def get_language(self, language_id: str) -> Dict[str, Any]:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
# Judgement type operations
|
|
105
|
+
@abstractmethod
|
|
106
|
+
def get_judgement_types(self) -> List[Dict[str, Any]]:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def get_judgement_type(self, judgement_type_id: str) -> Dict[str, Any]:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
# Submission operations
|
|
114
|
+
@abstractmethod
|
|
115
|
+
def get_submissions(self) -> List[Dict[str, Any]]:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
@abstractmethod
|
|
119
|
+
def get_submission(self, submission_id: str) -> Dict[str, Any]:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
def get_submission_file(self, submission_id: str) -> FileAttr:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
# Judgement operations
|
|
127
|
+
@abstractmethod
|
|
128
|
+
def get_judgements(self, submission_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
@abstractmethod
|
|
132
|
+
def get_judgement(self, judgement_id: str) -> Dict[str, Any]:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
# Run operations
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def get_runs(self, judgement_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
@abstractmethod
|
|
141
|
+
def get_run(self, run_id: str) -> Dict[str, Any]:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
# Clarification operations
|
|
145
|
+
@abstractmethod
|
|
146
|
+
def get_clarifications(self) -> List[Dict[str, Any]]:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
@abstractmethod
|
|
150
|
+
def get_clarification(self, clarification_id: str) -> Dict[str, Any]:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
# Award operations
|
|
154
|
+
@abstractmethod
|
|
155
|
+
def get_awards(self) -> List[Dict[str, Any]]:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
@abstractmethod
|
|
159
|
+
def get_award(self, award_id: str) -> Dict[str, Any]:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
# Event Feed operations
|
|
163
|
+
@abstractmethod
|
|
164
|
+
def get_event_feed(self, since_token: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
165
|
+
pass
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import bisect
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Optional, Union
|
|
7
|
+
|
|
8
|
+
from fastapi import HTTPException
|
|
9
|
+
|
|
10
|
+
from xcpcio.ccs.base.types import FileAttr
|
|
11
|
+
from xcpcio.ccs.reader.base_ccs_reader import BaseCCSReader
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ContestPackageReader(BaseCCSReader):
|
|
17
|
+
def __init__(self, contest_package_dir: Path):
|
|
18
|
+
self.contest_package_dir = contest_package_dir
|
|
19
|
+
if not self.contest_package_dir.exists():
|
|
20
|
+
raise ValueError(f"Contest package directory does not exist: {contest_package_dir}")
|
|
21
|
+
|
|
22
|
+
self._load_indexes()
|
|
23
|
+
|
|
24
|
+
def _create_index_by_id(self, data: List[Dict[str, Any]], id_name: str) -> Dict[str, List[Dict]]:
|
|
25
|
+
res = defaultdict(list)
|
|
26
|
+
for item in data:
|
|
27
|
+
res[item[id_name]].append(item)
|
|
28
|
+
return res
|
|
29
|
+
|
|
30
|
+
def _load_json_file(self, filepath: str) -> Union[Dict[str, Any], List[Any]]:
|
|
31
|
+
full_path = self.contest_package_dir / filepath
|
|
32
|
+
try:
|
|
33
|
+
with open(full_path, "r", encoding="utf-8") as f:
|
|
34
|
+
return json.load(f)
|
|
35
|
+
except FileNotFoundError:
|
|
36
|
+
raise HTTPException(status_code=404, detail=f"File not found: {filepath}")
|
|
37
|
+
except json.JSONDecodeError as e:
|
|
38
|
+
raise HTTPException(status_code=500, detail=f"Invalid JSON in file {filepath}: {e}")
|
|
39
|
+
|
|
40
|
+
def _load_ndjson_file(self, filepath: str) -> List[Dict[str, Any]]:
|
|
41
|
+
full_path = self.contest_package_dir / filepath
|
|
42
|
+
try:
|
|
43
|
+
data = list()
|
|
44
|
+
with open(full_path, "r", encoding="utf-8") as f:
|
|
45
|
+
for line in f.readlines():
|
|
46
|
+
data.append(json.loads(line))
|
|
47
|
+
return data
|
|
48
|
+
except FileNotFoundError:
|
|
49
|
+
raise HTTPException(status_code=404, detail=f"File not found: {filepath}")
|
|
50
|
+
except json.JSONDecodeError as e:
|
|
51
|
+
raise HTTPException(status_code=500, detail=f"Invalid JSON in file {filepath}: {e}")
|
|
52
|
+
|
|
53
|
+
def _load_indexes(self) -> None:
|
|
54
|
+
self.access = self._load_json_file("access.json")
|
|
55
|
+
|
|
56
|
+
self.accounts = self._load_json_file("accounts.json")
|
|
57
|
+
self.accounts_by_id = {account["id"] for account in self.accounts}
|
|
58
|
+
|
|
59
|
+
self.api_info = self._load_json_file("api.json")
|
|
60
|
+
|
|
61
|
+
self.awards = self._load_json_file("awards.json")
|
|
62
|
+
self.awards_by_id = {award["id"] for award in self.awards}
|
|
63
|
+
|
|
64
|
+
self.clarifications = self._load_json_file("clarifications.json")
|
|
65
|
+
self.clarifications_by_id = {clarification["id"] for clarification in self.clarifications}
|
|
66
|
+
|
|
67
|
+
self.contest = self._load_json_file("contest.json")
|
|
68
|
+
self.contest_state = self._load_json_file("state.json")
|
|
69
|
+
|
|
70
|
+
self.groups = self._load_json_file("groups.json")
|
|
71
|
+
self.groups_by_id = {group["id"]: group for group in self.groups}
|
|
72
|
+
|
|
73
|
+
self.judgement_types = self._load_json_file("judgement-types.json")
|
|
74
|
+
self.judgement_types_by_id = {judgement_type["id"] for judgement_type in self.judgement_types}
|
|
75
|
+
|
|
76
|
+
self.judgements = self._load_json_file("judgements.json")
|
|
77
|
+
self.judgements_by_id = {judgement["id"] for judgement in self.judgements}
|
|
78
|
+
self.judgements_by_submission_id = self._create_index_by_id(self.judgements, "submission_id")
|
|
79
|
+
|
|
80
|
+
self.languages = self._load_json_file("languages.json")
|
|
81
|
+
self.languages_by_id = {language["id"] for language in self.languages}
|
|
82
|
+
|
|
83
|
+
self.organizations = self._load_json_file("organizations.json")
|
|
84
|
+
self.organizations_by_id = {org["id"]: org for org in self.organizations}
|
|
85
|
+
|
|
86
|
+
self.problems = self._load_json_file("problems.json")
|
|
87
|
+
self.problems_by_id = {problem["id"]: problem for problem in self.problems}
|
|
88
|
+
|
|
89
|
+
self.runs = self._load_json_file("runs.json")
|
|
90
|
+
self.runs_by_id = {run["id"] for run in self.runs}
|
|
91
|
+
self.runs_by_judgement_id = self._create_index_by_id(self.runs, "judgement_id")
|
|
92
|
+
|
|
93
|
+
self.submissions = self._load_json_file("submissions.json")
|
|
94
|
+
self.submissions_by_id = {submission["id"]: submission for submission in self.submissions}
|
|
95
|
+
|
|
96
|
+
self.teams = self._load_json_file("teams.json")
|
|
97
|
+
self.teams_by_id = {team["id"]: team for team in self.teams}
|
|
98
|
+
|
|
99
|
+
self.event_feed = self._load_ndjson_file("event-feed.ndjson")
|
|
100
|
+
self.event_feed_tokens = [event["token"] for event in self.event_feed]
|
|
101
|
+
|
|
102
|
+
self.contest_id = self.contest["id"]
|
|
103
|
+
|
|
104
|
+
def _get_file_attr(self, expected_href: str, base_path: Path, files: List[Dict]) -> FileAttr:
|
|
105
|
+
for file in files:
|
|
106
|
+
href = file["href"]
|
|
107
|
+
if href == expected_href:
|
|
108
|
+
filename = file["filename"]
|
|
109
|
+
mime_type = file["mime"]
|
|
110
|
+
filepath: Path = base_path / filename
|
|
111
|
+
if not filepath.exists():
|
|
112
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
113
|
+
return FileAttr(path=filepath, media_type=mime_type, name=filename)
|
|
114
|
+
raise KeyError(f"Href not found: {expected_href}")
|
|
115
|
+
|
|
116
|
+
# API Information
|
|
117
|
+
def get_api_info(self) -> Dict[str, Any]:
|
|
118
|
+
return self.api_info
|
|
119
|
+
|
|
120
|
+
def get_access(self) -> Dict[str, Any]:
|
|
121
|
+
return self.access
|
|
122
|
+
|
|
123
|
+
# Account operations
|
|
124
|
+
def get_accounts(self) -> List[Dict[str, Any]]:
|
|
125
|
+
return self.accounts
|
|
126
|
+
|
|
127
|
+
def get_account(self, account_id: str) -> Dict[str, Any]:
|
|
128
|
+
if account_id not in self.accounts_by_id:
|
|
129
|
+
raise HTTPException(status_code=404, detail=f"Account {account_id} not found")
|
|
130
|
+
return self.accounts_by_id[account_id]
|
|
131
|
+
|
|
132
|
+
# Contest operations
|
|
133
|
+
def get_contest_id(self) -> str:
|
|
134
|
+
return self.contest["id"]
|
|
135
|
+
|
|
136
|
+
def get_contest(self) -> Dict[str, Any]:
|
|
137
|
+
return self.contest
|
|
138
|
+
|
|
139
|
+
def get_contest_state(self) -> Dict[str, Any]:
|
|
140
|
+
return self.contest_state
|
|
141
|
+
|
|
142
|
+
def get_contest_banner(self) -> FileAttr:
|
|
143
|
+
expected_href = f"contests/{self.contest_id}/banner"
|
|
144
|
+
base_path = self.contest_package_dir / "contest"
|
|
145
|
+
files = self.contest.get("banner", [])
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
return self._get_file_attr(expected_href, base_path, files)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
raise HTTPException(status_code=404, detail=f"Banner not found. [contest_id={self.contest_id}] [err={e}]")
|
|
151
|
+
|
|
152
|
+
def get_contest_problemset(self) -> FileAttr:
|
|
153
|
+
expected_href = f"contests/{self.contest_id}/problemset"
|
|
154
|
+
base_path = self.contest_package_dir / "contest"
|
|
155
|
+
files = self.contest.get("problemset", [])
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
return self._get_file_attr(expected_href, base_path, files)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
raise HTTPException(
|
|
161
|
+
status_code=404, detail=f"Problemset not found. [contest_id={self.contest_id}] [err={e}]"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Problem operations
|
|
165
|
+
def get_problems(self) -> List[Dict[str, Any]]:
|
|
166
|
+
return self.problems
|
|
167
|
+
|
|
168
|
+
def get_problem(self, problem_id: str) -> Dict[str, Any]:
|
|
169
|
+
if problem_id not in self.problems_by_id:
|
|
170
|
+
raise HTTPException(status_code=404, detail=f"Problem {problem_id} not found")
|
|
171
|
+
return self.problems_by_id[problem_id]
|
|
172
|
+
|
|
173
|
+
def get_problem_statement(self, problem_id: str) -> FileAttr:
|
|
174
|
+
expected_href = f"contests/{self.contest_id}/problems/{problem_id}/statement"
|
|
175
|
+
base_path = self.contest_package_dir / "problems" / problem_id
|
|
176
|
+
files = self.get_problem(problem_id).get("statement", [])
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
return self._get_file_attr(expected_href, base_path, files)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
raise HTTPException(
|
|
182
|
+
status_code=404,
|
|
183
|
+
detail=f"Problem statement not found. [contest_id={self.contest_id}] [problem_id={problem_id}] [err={e}]",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Team operations
|
|
187
|
+
def get_teams(self) -> List[Dict[str, Any]]:
|
|
188
|
+
return self.teams
|
|
189
|
+
|
|
190
|
+
def get_team(self, team_id: str) -> Dict[str, Any]:
|
|
191
|
+
if team_id not in self.teams_by_id:
|
|
192
|
+
raise HTTPException(status_code=404, detail=f"Team {team_id} not found")
|
|
193
|
+
return self.teams_by_id[team_id]
|
|
194
|
+
|
|
195
|
+
def get_team_photo(self, team_id: str) -> FileAttr:
|
|
196
|
+
expected_href = f"contests/{self.contest_id}/teams/{team_id}/photo"
|
|
197
|
+
base_path = self.contest_package_dir / "teams" / team_id
|
|
198
|
+
files = self.get_team(team_id).get("photo", [])
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
return self._get_file_attr(expected_href, base_path, files)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
raise HTTPException(
|
|
204
|
+
status_code=404,
|
|
205
|
+
detail=f"Team photo not found. [contest_id={self.contest_id}] [team_id={team_id}] [err={e}]",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Organization operations
|
|
209
|
+
def get_organizations(self) -> List[Dict[str, Any]]:
|
|
210
|
+
return self.organizations
|
|
211
|
+
|
|
212
|
+
def get_organization(self, organization_id: str) -> Dict[str, Any]:
|
|
213
|
+
if organization_id not in self.organizations_by_id:
|
|
214
|
+
raise HTTPException(status_code=404, detail=f"Organization {organization_id} not found")
|
|
215
|
+
return self.organizations_by_id[organization_id]
|
|
216
|
+
|
|
217
|
+
def get_organization_logo(self, organization_id: str) -> FileAttr:
|
|
218
|
+
expected_href = f"contests/{self.contest_id}/organizations/{organization_id}/logo"
|
|
219
|
+
base_path = self.contest_package_dir / "organizations" / organization_id
|
|
220
|
+
files = self.get_organization(organization_id).get("logo", [])
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
return self._get_file_attr(expected_href, base_path, files)
|
|
224
|
+
except Exception as e:
|
|
225
|
+
raise HTTPException(
|
|
226
|
+
status_code=404,
|
|
227
|
+
detail=f"Organization logo not found. [contest_id={self.contest_id}] [organization_id={organization_id}] [err={e}]",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Group operations
|
|
231
|
+
def get_groups(self) -> List[Dict[str, Any]]:
|
|
232
|
+
return self.groups
|
|
233
|
+
|
|
234
|
+
def get_group(self, group_id: str) -> Dict[str, Any]:
|
|
235
|
+
if group_id not in self.groups_by_id:
|
|
236
|
+
raise HTTPException(status_code=404, detail=f"Group {group_id} not found")
|
|
237
|
+
return self.groups_by_id[group_id]
|
|
238
|
+
|
|
239
|
+
# Language operations
|
|
240
|
+
def get_languages(self) -> List[Dict[str, Any]]:
|
|
241
|
+
return self.languages
|
|
242
|
+
|
|
243
|
+
def get_language(self, language_id: str) -> Dict[str, Any]:
|
|
244
|
+
if language_id not in self.languages_by_id:
|
|
245
|
+
raise HTTPException(status_code=404, detail=f"Language {language_id} not found")
|
|
246
|
+
return self.languages_by_id[language_id]
|
|
247
|
+
|
|
248
|
+
# Judgement type operations
|
|
249
|
+
def get_judgement_types(self) -> List[Dict[str, Any]]:
|
|
250
|
+
return self.judgement_types
|
|
251
|
+
|
|
252
|
+
def get_judgement_type(self, judgement_type_id: str) -> Dict[str, Any]:
|
|
253
|
+
if judgement_type_id not in self.judgement_types_by_id:
|
|
254
|
+
raise HTTPException(status_code=404, detail=f"Judgement type {judgement_type_id} not found")
|
|
255
|
+
return self.judgement_types_by_id[judgement_type_id]
|
|
256
|
+
|
|
257
|
+
# Submission operations
|
|
258
|
+
def get_submissions(self) -> List[Dict[str, Any]]:
|
|
259
|
+
return self.submissions
|
|
260
|
+
|
|
261
|
+
def get_submission(self, submission_id: str) -> Dict[str, Any]:
|
|
262
|
+
if submission_id not in self.submissions_by_id:
|
|
263
|
+
raise HTTPException(status_code=404, detail=f"Submission {submission_id} not found")
|
|
264
|
+
return self.submissions_by_id[submission_id]
|
|
265
|
+
|
|
266
|
+
def get_submission_file(self, submission_id: str) -> FileAttr:
|
|
267
|
+
expected_href = f"contests/{self.contest_id}/submissions/{submission_id}/files"
|
|
268
|
+
base_path = self.contest_package_dir / "submissions" / submission_id
|
|
269
|
+
files = self.get_submission(submission_id).get("files", [])
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
return self._get_file_attr(expected_href, base_path, files)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
raise HTTPException(
|
|
275
|
+
status_code=404,
|
|
276
|
+
detail=f"Submission file not found. [contest_id={self.contest_id}] [submission_id={submission_id}] [err={e}]",
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Judgement operations
|
|
280
|
+
def get_judgements(self, submission_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
281
|
+
if submission_id is not None:
|
|
282
|
+
if submission_id not in self.judgements_by_submission_id:
|
|
283
|
+
raise HTTPException(status_code=404, detail=f"Submission id not found: {submission_id}")
|
|
284
|
+
return self.judgements_by_submission_id[submission_id]
|
|
285
|
+
|
|
286
|
+
return self.judgements
|
|
287
|
+
|
|
288
|
+
def get_judgement(self, judgement_id: str) -> Dict[str, Any]:
|
|
289
|
+
if judgement_id not in self.judgements_by_id:
|
|
290
|
+
raise HTTPException(status_code=404, detail=f"Judgement {judgement_id} not found")
|
|
291
|
+
return self.judgements_by_id[judgement_id]
|
|
292
|
+
|
|
293
|
+
# Run operations
|
|
294
|
+
def get_runs(self, judgement_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
295
|
+
if judgement_id is not None:
|
|
296
|
+
if judgement_id not in self.runs_by_judgement_id:
|
|
297
|
+
raise HTTPException(status_code=404, detail=f"Judgement id not found: {judgement_id}")
|
|
298
|
+
return self.runs_by_judgement_id[judgement_id]
|
|
299
|
+
|
|
300
|
+
return self.runs
|
|
301
|
+
|
|
302
|
+
def get_run(self, run_id: str) -> Dict[str, Any]:
|
|
303
|
+
if run_id not in self.runs_by_id:
|
|
304
|
+
raise HTTPException(status_code=404, detail=f"Run {run_id} not found")
|
|
305
|
+
return self.runs_by_id[run_id]
|
|
306
|
+
|
|
307
|
+
# Clarification operations
|
|
308
|
+
def get_clarifications(self) -> List[Dict[str, Any]]:
|
|
309
|
+
return self.clarifications
|
|
310
|
+
|
|
311
|
+
def get_clarification(self, clarification_id: str) -> Dict[str, Any]:
|
|
312
|
+
if clarification_id not in self.clarifications_by_id:
|
|
313
|
+
raise HTTPException(status_code=404, detail=f"Clarification {clarification_id} not found")
|
|
314
|
+
return self.clarifications_by_id[clarification_id]
|
|
315
|
+
|
|
316
|
+
# Award operations
|
|
317
|
+
def get_awards(self) -> List[Dict[str, Any]]:
|
|
318
|
+
return self.awards
|
|
319
|
+
|
|
320
|
+
def get_award(self, award_id: str) -> Dict[str, Any]:
|
|
321
|
+
if award_id not in self.awards_by_id:
|
|
322
|
+
raise HTTPException(status_code=404, detail=f"Award {award_id} not found")
|
|
323
|
+
return self.awards_by_id[award_id]
|
|
324
|
+
|
|
325
|
+
# Event Feed operations
|
|
326
|
+
def get_event_feed(self, since_token: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
327
|
+
if since_token is None:
|
|
328
|
+
return self.event_feed
|
|
329
|
+
|
|
330
|
+
idx = bisect.bisect_left(self.event_feed_tokens, since_token)
|
|
331
|
+
return self.event_feed[idx:]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xcpcio
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.64.0
|
|
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
|
|
@@ -25,6 +25,7 @@ Requires-Dist: pyyaml>=6.0.0
|
|
|
25
25
|
Requires-Dist: semver>=3.0.0
|
|
26
26
|
Requires-Dist: tenacity>=8.0.0
|
|
27
27
|
Requires-Dist: uvicorn>=0.36.0
|
|
28
|
+
Requires-Dist: zstandard>=0.25.0
|
|
28
29
|
Description-Content-Type: text/markdown
|
|
29
30
|
|
|
30
31
|
# xcpcio-python
|
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
xcpcio/__init__.py,sha256=kjd6itqBRSQ-OT83qUJXHt81KQQDRUtaIuykzfaWXLM,121
|
|
2
|
-
xcpcio/__version__.py,sha256=
|
|
2
|
+
xcpcio/__version__.py,sha256=klqfiPl7r99QoDZy8_HzKScRlEEWVa3Svby8c3yeAsY,172
|
|
3
3
|
xcpcio/constants.py,sha256=MjpAgNXiBlUsx1S09m7JNT-nekNDR-aE6ggvGL3fg0I,2297
|
|
4
4
|
xcpcio/types.py,sha256=AkYby2haJgxwtozlgaPMG2ryAZdvsSc3sH-p6qXcM4g,6575
|
|
5
5
|
xcpcio/ccs/__init__.py,sha256=LSoKFblEuSoIVBYcUxOFF8fn2bH2R6kSg9xNrBfzC0g,99
|
|
6
6
|
xcpcio/ccs/contest_archiver.py,sha256=ICogyPzKfFRoO7J5D2Eu-3JwIirC3etMev7hyTjn4z8,16198
|
|
7
7
|
xcpcio/ccs/api_server/__init__.py,sha256=ASvVJ_ibGkXFDSNmy05eb9gESXRS8hjYHCrBecSnaS0,174
|
|
8
|
-
xcpcio/ccs/api_server/dependencies.py,sha256=
|
|
8
|
+
xcpcio/ccs/api_server/dependencies.py,sha256=cbLHcP91SaRBj2W9OfC0yCQ1fasI2DofxGUhPPNs3F8,1518
|
|
9
9
|
xcpcio/ccs/api_server/server.py,sha256=3gft3MqDXvKWug5UCLhdV681bcQMRrewDkwQ7qSPtRU,2598
|
|
10
10
|
xcpcio/ccs/api_server/routes/__init__.py,sha256=uz65H4L5Wzef7QPi5PsLQM1xbMdG6FoZ0Np0y039_2k,1537
|
|
11
11
|
xcpcio/ccs/api_server/routes/access.py,sha256=O-RGLmgLNBQ-ccu8rlHOgonTjl02fYOdi3VTsUa-T0w,434
|
|
12
12
|
xcpcio/ccs/api_server/routes/accounts.py,sha256=nAwiIz-y4fGmqHBniMuQifk9jVt4i9YPZWBB7w40q5Q,994
|
|
13
13
|
xcpcio/ccs/api_server/routes/awards.py,sha256=gBPSFlDj6PM6Poys6JbO7VMsfCljKz6QrTjKEqQcS8w,957
|
|
14
14
|
xcpcio/ccs/api_server/routes/clarifications.py,sha256=vvMNMvQkZTOn1VJ8C5U86gpIkN3yNrxBll1VMTJjzQg,1046
|
|
15
|
-
xcpcio/ccs/api_server/routes/contests.py,sha256=
|
|
15
|
+
xcpcio/ccs/api_server/routes/contests.py,sha256=VbvegfP5ZnVNKdqoHmy7_Wd2L6xEBqON7UD2OQ6sQ8A,2979
|
|
16
16
|
xcpcio/ccs/api_server/routes/general.py,sha256=xLH-sqyeC4dSd6SUYpjg-w-1ZtmSqZAgIupoVOyYwD4,763
|
|
17
17
|
xcpcio/ccs/api_server/routes/groups.py,sha256=kWKFFty2iWckMN-j6G3NbgMVKCPQ478_mYKrYsHXgMA,949
|
|
18
18
|
xcpcio/ccs/api_server/routes/judgement_types.py,sha256=S3Dt0VYjntVBQBvLYhJGcDNSy7O8Zh7b7NSfrronZSU,1057
|
|
19
19
|
xcpcio/ccs/api_server/routes/judgements.py,sha256=3w0LHYbZazli_v587l98U23r5PuolRs_vtbNLMwgjTU,1127
|
|
20
20
|
xcpcio/ccs/api_server/routes/languages.py,sha256=2BYqTaSBWx0E5XlaTjofElLb7z4HTsVs3rrozdyqz0s,985
|
|
21
|
-
xcpcio/ccs/api_server/routes/organizations.py,sha256=
|
|
22
|
-
xcpcio/ccs/api_server/routes/problems.py,sha256=
|
|
21
|
+
xcpcio/ccs/api_server/routes/organizations.py,sha256=6xMl0Iqo7pjLaJbR7L1NWwo8JXwnXhH2O8hJKiLoRa0,1712
|
|
22
|
+
xcpcio/ccs/api_server/routes/problems.py,sha256=XAhXkShHwewZdugsffLj1UxvFNSorrxc-PyZymdBQTY,1632
|
|
23
23
|
xcpcio/ccs/api_server/routes/runs.py,sha256=1L6fo1KPcDeKwOEkTxCwEetmyfNKY_Z0uxATqBJftIs,1046
|
|
24
|
-
xcpcio/ccs/api_server/routes/submissions.py,sha256=
|
|
25
|
-
xcpcio/ccs/api_server/routes/teams.py,sha256=
|
|
24
|
+
xcpcio/ccs/api_server/routes/submissions.py,sha256=ByVX_wzab0Q9EHFmuE8JQtikvboMXtt_kXecEy6yiRc,1675
|
|
25
|
+
xcpcio/ccs/api_server/routes/teams.py,sha256=euDs-GDHLqmJS3zPTNxm9LfKXQYEuhwAlX-BF2tcrk4,1556
|
|
26
26
|
xcpcio/ccs/api_server/services/__init__.py,sha256=WQLNrLVomhtICl8HlFYaCoRewIHVZfUiiwrSBUOOWDg,171
|
|
27
|
-
xcpcio/ccs/api_server/services/contest_service.py,sha256=
|
|
27
|
+
xcpcio/ccs/api_server/services/contest_service.py,sha256=Jmt7h4rQoZLXPAkHx3FdQNsRtYOPpdSOlh7QA41nIm4,7467
|
|
28
|
+
xcpcio/ccs/base/__init__.py,sha256=JYKVtcQG-VGM2OfCg6VhxcXCeCp7r2zxpg_7s9AThS0,39
|
|
29
|
+
xcpcio/ccs/base/types.py,sha256=NfAG-XJjex7p2DfFTRecLWhmwpeO8Ul42nNMnZH39sM,137
|
|
28
30
|
xcpcio/ccs/model/__init__.py,sha256=cZE1q5JY-iHDEKZpsx0UZaMhH-23H4oAHaYOkW7dZ5s,43
|
|
29
31
|
xcpcio/ccs/model/model_2023_06/__init__.py,sha256=OmDQZqmigBpL64LXk5lIOGoQ3Uqis8-2z6qQpOO5aJc,167
|
|
30
32
|
xcpcio/ccs/model/model_2023_06/model.py,sha256=bVMDWpJTwPSpz1fHPxWrWerxCBIboH3LKVZpIZGQ2pY,15287
|
|
31
|
-
xcpcio
|
|
32
|
-
xcpcio
|
|
33
|
-
xcpcio
|
|
34
|
-
xcpcio-0.
|
|
33
|
+
xcpcio/ccs/reader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
|
+
xcpcio/ccs/reader/base_ccs_reader.py,sha256=fS7M0hD3-3PAEV7EYyorVZsBhD4HtABkQeV4fXNldhA,3912
|
|
35
|
+
xcpcio/ccs/reader/contest_package_reader.py,sha256=DPuKp3eptRNMi0-Ssx_K2roN5_0xXILMltucWy1NTPI,14173
|
|
36
|
+
xcpcio-0.64.0.dist-info/METADATA,sha256=1hiCdfGBJXTXTNnDe0AGnS-BoFWv4Q_W8LmbNjSMPvU,1112
|
|
37
|
+
xcpcio-0.64.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
38
|
+
xcpcio-0.64.0.dist-info/entry_points.txt,sha256=qvzh8oDJxIHqTN-rg2lRN6xR99AqxbWnlAQI7uzDibI,59
|
|
39
|
+
xcpcio-0.64.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|