xcpcio 0.63.6__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.

@@ -5,102 +5,31 @@ Business logic layer for Contest API operations.
5
5
  Handles file reading, data validation, and business operations.
6
6
  """
7
7
 
8
- import json
9
- from pathlib import Path
10
- from typing import Any, Dict, List, Optional, Union
8
+ import logging
9
+ from typing import Any, Dict, List, Optional
11
10
 
12
11
  from fastapi import HTTPException
13
12
 
14
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
+
17
+ logger = logging.getLogger(__name__)
15
18
 
16
19
 
17
20
  class ContestService:
18
21
  """Service class for contest-related operations"""
19
22
 
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 _load_indexes(self) -> None:
35
- """Load and index commonly accessed data for faster lookups"""
36
- # Load contest data
37
- self.contest_data = self.load_json_file("contest.json")
38
-
39
- # Load organizations and create index
40
- organizations_data = self.load_json_file("organizations.json")
41
- self.organizations_by_id = {org["id"]: org for org in organizations_data}
42
-
43
- # Load teams and create index
44
- teams_data = self.load_json_file("teams.json")
45
- self.teams_by_id = {team["id"]: team for team in teams_data}
46
-
47
- # Load problems and create index
48
- problems_data = self.load_json_file("problems.json")
49
- self.problems_by_id = {problem["id"]: problem for problem in problems_data}
50
-
51
- # Load submissions and create index
52
- submissions_data = self.load_json_file("submissions.json")
53
- self.submissions_by_id = {submission["id"]: submission for submission in submissions_data}
54
-
55
- def load_json_file(self, filepath: str) -> Union[Dict[str, Any], List[Any]]:
56
- """
57
- Load JSON data from contest package directory.
58
-
59
- Args:
60
- filepath: Relative path to JSON file within contest package
61
-
62
- Returns:
63
- Parsed JSON data
64
-
65
- Raises:
66
- HTTPException: If file not found or invalid JSON
67
- """
68
- full_path = self.contest_package_dir / filepath
69
- try:
70
- with open(full_path, "r", encoding="utf-8") as f:
71
- return json.load(f)
72
- except FileNotFoundError:
73
- raise HTTPException(status_code=404, detail=f"File not found: {filepath}")
74
- except json.JSONDecodeError as e:
75
- raise HTTPException(status_code=500, detail=f"Invalid JSON in file {filepath}: {e}")
76
-
77
- def get_contest_id(self) -> str:
78
- """
79
- Get contest ID from contest.json.
80
-
81
- Returns:
82
- Contest ID string
83
- """
84
- contest_data = self.load_json_file("contest.json")
85
- return contest_data.get("id", "unknown")
86
-
87
- def validate_contest_id(self, contest_id: str) -> None:
88
- """
89
- Validate that the provided contest ID matches the expected one.
90
-
91
- Args:
92
- contest_id: Contest ID to validate
93
-
94
- Raises:
95
- HTTPException: If contest ID doesn't match
96
- """
97
- expected_id = self.get_contest_id()
98
- if contest_id != expected_id:
23
+ def __init__(self, reader_dict: Dict[str, BaseCCSReader]):
24
+ self.reader_dict = reader_dict
25
+
26
+ def _get_reader(self, contest_id: str) -> BaseCCSReader:
27
+ if contest_id not in self.reader_dict:
99
28
  raise HTTPException(status_code=404, detail=f"Contest {contest_id} not found")
29
+ return self.reader_dict[contest_id]
100
30
 
101
31
  # API Information
102
32
  def get_api_info(self) -> Dict[str, Any]:
103
- """Get API information"""
104
33
  return {
105
34
  "version": "2023-06",
106
35
  "version_url": "https://ccs-specs.icpc.io/2023-06/contest_api",
@@ -111,298 +40,155 @@ class ContestService:
111
40
  },
112
41
  }
113
42
 
114
- def get_access_info(self, contest_id: str) -> Dict[str, Any]:
115
- """Get access information for current client"""
116
- self.validate_contest_id(contest_id)
117
- return {
118
- "capabilities": [],
119
- "endpoints": [
120
- {
121
- "type": "contest",
122
- "properties": [
123
- "id",
124
- "name",
125
- "formal_name",
126
- "start_time",
127
- "duration",
128
- "scoreboard_type",
129
- "penalty_time",
130
- ],
131
- },
132
- {
133
- "type": "problems",
134
- "properties": ["id", "label", "name", "ordinal", "color", "rgb", "time_limit", "test_data_count"],
135
- },
136
- {"type": "teams", "properties": ["id", "name", "label", "organization_id", "group_ids", "hidden"]},
137
- {"type": "organizations", "properties": ["id", "name", "formal_name"]},
138
- {"type": "groups", "properties": ["id", "name"]},
139
- {"type": "judgement-types", "properties": ["id", "name", "penalty", "solved"]},
140
- {"type": "languages", "properties": ["id", "name", "extensions"]},
141
- {
142
- "type": "state",
143
- "properties": ["started", "ended", "frozen", "thawed", "finalized", "end_of_updates"],
144
- },
145
- {
146
- "type": "submissions",
147
- "properties": ["id", "team_id", "problem_id", "language_id", "time", "contest_time"],
148
- },
149
- {
150
- "type": "judgements",
151
- "properties": ["id", "submission_id", "judgement_type_id", "start_time", "start_contest_time"],
152
- },
153
- {
154
- "type": "runs",
155
- "properties": [
156
- "id",
157
- "judgement_id",
158
- "ordinal",
159
- "judgement_type_id",
160
- "time",
161
- "contest_time",
162
- "run_time",
163
- ],
164
- },
165
- {
166
- "type": "clarifications",
167
- "properties": ["id", "from_team_id", "to_team_id", "problem_id", "text", "time", "contest_time"],
168
- },
169
- {"type": "awards", "properties": ["id", "citation", "team_ids"]},
170
- ],
171
- }
43
+ def get_access(self, contest_id: str) -> Dict[str, Any]:
44
+ reader = self._get_reader(contest_id)
45
+ return reader.get_access()
46
+
47
+ # Account operations
48
+ def get_accounts(self, contest_id: str) -> List[Dict[str, Any]]:
49
+ reader = self._get_reader(contest_id)
50
+ return reader.get_accounts()
51
+
52
+ def get_account(self, contest_id: str, account_id: str) -> Dict[str, Any]:
53
+ reader = self._get_reader(contest_id)
54
+ return reader.get_account(account_id)
172
55
 
173
56
  # Contest operations
174
57
  def get_contests(self) -> List[Dict[str, Any]]:
175
- """Get all contests"""
176
- contest_data = self.load_json_file("contest.json")
177
- return [contest_data]
58
+ return [reader.get_contest() for reader in self.reader_dict.values()]
178
59
 
179
60
  def get_contest(self, contest_id: str) -> Dict[str, Any]:
180
- """Get specific contest"""
181
- self.validate_contest_id(contest_id)
182
- return self.load_json_file("contest.json")
61
+ reader = self._get_reader(contest_id)
62
+ return reader.get_contest()
183
63
 
184
64
  def get_contest_state(self, contest_id: str) -> Dict[str, Any]:
185
- """Get contest state"""
186
- self.validate_contest_id(contest_id)
187
- return self.load_json_file("state.json")
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()
188
75
 
189
76
  # Problem operations
190
77
  def get_problems(self, contest_id: str) -> List[Dict[str, Any]]:
191
- """Get all problems"""
192
- self.validate_contest_id(contest_id)
193
- return self.load_json_file("problems.json")
78
+ reader = self._get_reader(contest_id)
79
+ return reader.get_problems()
194
80
 
195
81
  def get_problem(self, contest_id: str, problem_id: str) -> Dict[str, Any]:
196
- """Get specific problem"""
197
- self.validate_contest_id(contest_id)
198
- problems = self.load_json_file("problems.json")
199
- for problem in problems:
200
- if problem["id"] == problem_id:
201
- return problem
202
- raise HTTPException(status_code=404, detail=f"Problem {problem_id} not found")
82
+ reader = self._get_reader(contest_id)
83
+ return reader.get_problem(problem_id)
203
84
 
204
- # Team operations
205
- def get_teams(self, contest_id: str, group_id: Optional[str] = None) -> List[Dict[str, Any]]:
206
- """Get all teams, optionally filtered by group"""
207
- self.validate_contest_id(contest_id)
208
- teams = self.load_json_file("teams.json")
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)
209
88
 
210
- if group_id:
211
- filtered_teams = []
212
- for team in teams:
213
- if group_id in team.get("group_ids", []):
214
- filtered_teams.append(team)
215
- return filtered_teams
216
-
217
- return teams
89
+ # Team operations
90
+ def get_teams(self, contest_id: str) -> List[Dict[str, Any]]:
91
+ reader = self._get_reader(contest_id)
92
+ return reader.get_teams()
218
93
 
219
94
  def get_team(self, contest_id: str, team_id: str) -> Dict[str, Any]:
220
- """Get specific team"""
221
- self.validate_contest_id(contest_id)
222
- teams = self.load_json_file("teams.json")
223
- for team in teams:
224
- if team["id"] == team_id:
225
- return team
226
- raise HTTPException(status_code=404, detail=f"Team {team_id} not found")
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)
227
101
 
228
102
  # Organization operations
229
103
  def get_organizations(self, contest_id: str) -> List[Dict[str, Any]]:
230
- """Get all organizations"""
231
- self.validate_contest_id(contest_id)
232
- return self.load_json_file("organizations.json")
104
+ reader = self._get_reader(contest_id)
105
+ return reader.get_organizations()
233
106
 
234
107
  def get_organization(self, contest_id: str, organization_id: str) -> Dict[str, Any]:
235
- """Get specific organization"""
236
- self.validate_contest_id(contest_id)
237
- organizations = self.load_json_file("organizations.json")
238
- for org in organizations:
239
- if org["id"] == organization_id:
240
- return org
241
- raise HTTPException(status_code=404, detail=f"Organization {organization_id} not found")
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)
242
114
 
243
115
  # Group operations
244
116
  def get_groups(self, contest_id: str) -> List[Dict[str, Any]]:
245
- """Get all groups"""
246
- self.validate_contest_id(contest_id)
247
- return self.load_json_file("groups.json")
117
+ reader = self._get_reader(contest_id)
118
+ return reader.get_groups()
248
119
 
249
120
  def get_group(self, contest_id: str, group_id: str) -> Dict[str, Any]:
250
- """Get specific group"""
251
- self.validate_contest_id(contest_id)
252
- groups = self.load_json_file("groups.json")
253
- for group in groups:
254
- if group["id"] == group_id:
255
- return group
256
- raise HTTPException(status_code=404, detail=f"Group {group_id} not found")
121
+ reader = self._get_reader(contest_id)
122
+ return reader.get_group(group_id)
257
123
 
258
124
  # Language operations
259
125
  def get_languages(self, contest_id: str) -> List[Dict[str, Any]]:
260
- """Get all languages"""
261
- self.validate_contest_id(contest_id)
262
- return self.load_json_file("languages.json")
126
+ reader = self._get_reader(contest_id)
127
+ return reader.get_languages()
263
128
 
264
129
  def get_language(self, contest_id: str, language_id: str) -> Dict[str, Any]:
265
- """Get specific language"""
266
- self.validate_contest_id(contest_id)
267
- languages = self.load_json_file("languages.json")
268
- for lang in languages:
269
- if lang["id"] == language_id:
270
- return lang
271
- raise HTTPException(status_code=404, detail=f"Language {language_id} not found")
130
+ reader = self._get_reader(contest_id)
131
+ return reader.get_language(language_id)
272
132
 
273
133
  # Judgement type operations
274
134
  def get_judgement_types(self, contest_id: str) -> List[Dict[str, Any]]:
275
- """Get all judgement types"""
276
- self.validate_contest_id(contest_id)
277
- return self.load_json_file("judgement-types.json")
135
+ reader = self._get_reader(contest_id)
136
+ return reader.get_judgement_types()
278
137
 
279
138
  def get_judgement_type(self, contest_id: str, judgement_type_id: str) -> Dict[str, Any]:
280
- """Get specific judgement type"""
281
- self.validate_contest_id(contest_id)
282
- judgement_types = self.load_json_file("judgement-types.json")
283
- for jt in judgement_types:
284
- if jt["id"] == judgement_type_id:
285
- return jt
286
- raise HTTPException(status_code=404, detail=f"Judgement type {judgement_type_id} not found")
139
+ reader = self._get_reader(contest_id)
140
+ return reader.get_judgement_type(judgement_type_id)
287
141
 
288
142
  # Submission operations
289
- def get_submissions(
290
- self, contest_id: str, team_id: Optional[str] = None, problem_id: Optional[str] = None
291
- ) -> List[Dict[str, Any]]:
292
- """Get all submissions, optionally filtered"""
293
- self.validate_contest_id(contest_id)
294
- submissions = self.load_json_file("submissions.json")
295
-
296
- # Apply filters
297
- if team_id:
298
- submissions = [s for s in submissions if s.get("team_id") == team_id]
299
- if problem_id:
300
- submissions = [s for s in submissions if s.get("problem_id") == problem_id]
301
-
302
- return submissions
143
+ def get_submissions(self, contest_id: str) -> List[Dict[str, Any]]:
144
+ reader = self._get_reader(contest_id)
145
+ return reader.get_submissions()
303
146
 
304
147
  def get_submission(self, contest_id: str, submission_id: str) -> Dict[str, Any]:
305
- """Get specific submission"""
306
- self.validate_contest_id(contest_id)
307
- submissions = self.load_json_file("submissions.json")
308
- for submission in submissions:
309
- if submission["id"] == submission_id:
310
- return submission
311
- raise HTTPException(status_code=404, detail=f"Submission {submission_id} not found")
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)
312
154
 
313
155
  # Judgement operations
314
156
  def get_judgements(self, contest_id: str, submission_id: Optional[str] = None) -> List[Dict[str, Any]]:
315
- """Get all judgements, optionally filtered by submission"""
316
- self.validate_contest_id(contest_id)
317
- judgements = self.load_json_file("judgements.json")
318
-
319
- if submission_id:
320
- judgements = [j for j in judgements if j.get("submission_id") == submission_id]
321
-
322
- return judgements
157
+ reader = self._get_reader(contest_id)
158
+ return reader.get_judgements(submission_id)
323
159
 
324
160
  def get_judgement(self, contest_id: str, judgement_id: str) -> Dict[str, Any]:
325
- """Get specific judgement"""
326
- self.validate_contest_id(contest_id)
327
- judgements = self.load_json_file("judgements.json")
328
- for judgement in judgements:
329
- if judgement["id"] == judgement_id:
330
- return judgement
331
- raise HTTPException(status_code=404, detail=f"Judgement {judgement_id} not found")
161
+ reader = self._get_reader(contest_id)
162
+ return reader.get_judgement(judgement_id)
332
163
 
333
164
  # Run operations
334
165
  def get_runs(self, contest_id: str, judgement_id: Optional[str] = None) -> List[Dict[str, Any]]:
335
- """Get all runs, optionally filtered by judgement"""
336
- self.validate_contest_id(contest_id)
337
- runs = self.load_json_file("runs.json")
338
-
339
- if judgement_id:
340
- runs = [r for r in runs if r.get("judgement_id") == judgement_id]
341
-
342
- return runs
166
+ reader = self._get_reader(contest_id)
167
+ return reader.get_runs(judgement_id)
343
168
 
344
169
  def get_run(self, contest_id: str, run_id: str) -> Dict[str, Any]:
345
- """Get specific run"""
346
- self.validate_contest_id(contest_id)
347
- runs = self.load_json_file("runs.json")
348
- for run in runs:
349
- if run["id"] == run_id:
350
- return run
351
- raise HTTPException(status_code=404, detail=f"Run {run_id} not found")
170
+ reader = self._get_reader(contest_id)
171
+ return reader.get_run(run_id)
352
172
 
353
173
  # Clarification operations
354
- def get_clarifications(
355
- self,
356
- contest_id: str,
357
- from_team_id: Optional[str] = None,
358
- to_team_id: Optional[str] = None,
359
- problem_id: Optional[str] = None,
360
- ) -> List[Dict[str, Any]]:
361
- """Get all clarifications, optionally filtered"""
362
- self.validate_contest_id(contest_id)
363
- clarifications = self.load_json_file("clarifications.json")
364
-
365
- # Apply filters (empty string means null)
366
- if from_team_id is not None:
367
- if from_team_id == "":
368
- clarifications = [c for c in clarifications if c.get("from_team_id") is None]
369
- else:
370
- clarifications = [c for c in clarifications if c.get("from_team_id") == from_team_id]
371
-
372
- if to_team_id is not None:
373
- if to_team_id == "":
374
- clarifications = [c for c in clarifications if c.get("to_team_id") is None]
375
- else:
376
- clarifications = [c for c in clarifications if c.get("to_team_id") == to_team_id]
377
-
378
- if problem_id is not None:
379
- if problem_id == "":
380
- clarifications = [c for c in clarifications if c.get("problem_id") is None]
381
- else:
382
- clarifications = [c for c in clarifications if c.get("problem_id") == problem_id]
383
-
384
- return 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()
385
177
 
386
178
  def get_clarification(self, contest_id: str, clarification_id: str) -> Dict[str, Any]:
387
- """Get specific clarification"""
388
- self.validate_contest_id(contest_id)
389
- clarifications = self.load_json_file("clarifications.json")
390
- for clarification in clarifications:
391
- if clarification["id"] == clarification_id:
392
- return clarification
393
- raise HTTPException(status_code=404, detail=f"Clarification {clarification_id} not found")
179
+ reader = self._get_reader(contest_id)
180
+ return reader.get_clarification(clarification_id)
394
181
 
395
182
  # Award operations
396
183
  def get_awards(self, contest_id: str) -> List[Dict[str, Any]]:
397
- """Get all awards"""
398
- self.validate_contest_id(contest_id)
399
- return self.load_json_file("awards.json")
184
+ reader = self._get_reader(contest_id)
185
+ return reader.get_awards()
400
186
 
401
187
  def get_award(self, contest_id: str, award_id: str) -> Dict[str, Any]:
402
- """Get specific award"""
403
- self.validate_contest_id(contest_id)
404
- awards = self.load_json_file("awards.json")
405
- for award in awards:
406
- if award["id"] == award_id:
407
- return award
408
- raise HTTPException(status_code=404, detail=f"Award {award_id} not found")
188
+ reader = self._get_reader(contest_id)
189
+ return reader.get_award(award_id)
190
+
191
+ # Event Feed operations
192
+ def get_event_feed(self, contest_id: str, since_token: Optional[str] = None) -> List[Dict[str, Any]]:
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
@@ -59,7 +59,6 @@ class ContestArchiver:
59
59
 
60
60
  # Known endpoints that can be fetched
61
61
  KNOWN_ENDPOINTS = [
62
- "access",
63
62
  "contests",
64
63
  "judgement-types",
65
64
  "languages",
@@ -80,7 +79,6 @@ class ContestArchiver:
80
79
  ]
81
80
 
82
81
  DOMJUDGE_KNOWN_ENDPOINTS = [
83
- "access",
84
82
  "contests",
85
83
  "judgement-types",
86
84
  "languages",
@@ -289,7 +287,7 @@ class ContestArchiver:
289
287
 
290
288
  data = await self.fetch_json("/")
291
289
  if not data:
292
- raise RuntimeError("Failed to fetch API information from root endpoint")
290
+ raise RuntimeError("Failed to fetch API information")
293
291
 
294
292
  self._api_info = data # Store API info for later use
295
293
 
@@ -401,6 +399,7 @@ class ContestArchiver:
401
399
  # Always dump API and contest info
402
400
  await self.dump_api_info()
403
401
  await self.dump_contest_info()
402
+ await self.dump_endpoint_single("access")
404
403
 
405
404
  # Get list of endpoints to dump
406
405
  if self._config.endpoints:
File without changes