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 CHANGED
@@ -1,4 +1,4 @@
1
1
  # This file is auto-generated by Hatchling. As such, do not:
2
2
  # - modify
3
3
  # - track in version control e.g. be sure to add to .gitignore
4
- __version__ = VERSION = '0.63.7'
4
+ __version__ = VERSION = '0.64.0'
@@ -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
- _contest_service_instance = ContestService(contest_package_dir)
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, HTTPException, Query
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.validate_contest_id(contest_id)
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.validate_contest_id(contest_id)
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, HTTPException
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.validate_contest_id(contest_id)
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, HTTPException
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.validate_contest_id(contest_id)
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, HTTPException
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.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
- )
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, HTTPException
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.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}]")
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 bisect
9
- import json
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
- 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
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
- 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.
20
+ class ContestService:
21
+ """Service class for contest-related operations"""
139
22
 
140
- Args:
141
- contest_id: Contest ID to validate
23
+ def __init__(self, reader_dict: Dict[str, BaseCCSReader]):
24
+ self.reader_dict = reader_dict
142
25
 
143
- Raises:
144
- HTTPException: If contest ID doesn't match
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 self.api_info
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.validate_contest_id(contest_id)
156
- return self.access
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.validate_contest_id(contest_id)
161
- return self.accounts
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.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]
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.contest]
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.validate_contest_id(contest_id)
175
- return self.contest
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.validate_contest_id(contest_id)
179
- return self.contest_state
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.validate_contest_id(contest_id)
184
- return self.problems
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.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]
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.validate_contest_id(contest_id)
195
- return self.teams
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.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]
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.validate_contest_id(contest_id)
206
- return self.organizations
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.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]
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.validate_contest_id(contest_id)
217
- return self.groups
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.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]
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.validate_contest_id(contest_id)
228
- return self.languages
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.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]
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.validate_contest_id(contest_id)
239
- return self.judgement_types
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.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]
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.validate_contest_id(contest_id)
250
- return self.submissions
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.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]
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.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
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.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]
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.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
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.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]
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
- contest_id: str,
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.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]
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.validate_contest_id(contest_id)
309
- return self.awards
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.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]
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.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:]
193
+ reader = self._get_reader(contest_id)
194
+ return reader.get_event_feed(since_token)
@@ -0,0 +1,3 @@
1
+ from . import types
2
+
3
+ __all__ = [types]
@@ -0,0 +1,9 @@
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+
4
+
5
+ @dataclass
6
+ class FileAttr:
7
+ path: Path
8
+ media_type: str
9
+ name: str
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.63.7
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=95oCDxj_oHcOcxQzEfotvdWgY-p_3xsHZ2AaFTYVdR4,172
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=5nosGVwY-3Mq7eK9C5ZOMEJFozzzw_kLlpCraFbJ9wY,1225
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=o-A5wMDy5UC9wM71K3ruMmP4p4lSoU9B3XmDGPV0M7g,4203
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=608tPsK2bCUmqSalEFysC1zQf3FDDIeweT6PLLrOyLg,2515
22
- xcpcio/ccs/api_server/routes/problems.py,sha256=2YFw_ODw-6L015UgpL1p-lLItgCgnqDamni6_9Qmbdk,2458
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=UKgDt9yv45Hfpr8P_8-2Po0jF4HvzG87zgkcb1YIKVc,2542
25
- xcpcio/ccs/api_server/routes/teams.py,sha256=9nWz0tvMnccCNEzSN9DY-ZJtxlgaka9YSPe_5Gykbl8,2282
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=rMbAm7HVw5dHMTwp1tLAP0QhlCKlEMWQ65LZLZzjbfg,13210
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-0.63.7.dist-info/METADATA,sha256=oWlZ8fA8EO1Ly_wvo8JUlxbeDmlV7PVKPKum4ydxzDk,1079
32
- xcpcio-0.63.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
33
- xcpcio-0.63.7.dist-info/entry_points.txt,sha256=qvzh8oDJxIHqTN-rg2lRN6xR99AqxbWnlAQI7uzDibI,59
34
- xcpcio-0.63.7.dist-info/RECORD,,
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,,