xcpcio 0.64.2__py3-none-any.whl → 0.64.4__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.

Files changed (46) hide show
  1. xcpcio/__init__.py +2 -2
  2. xcpcio/__version__.py +1 -1
  3. xcpcio/api/__init__.py +15 -0
  4. xcpcio/api/client.py +74 -0
  5. xcpcio/api/models.py +25 -0
  6. xcpcio/clics/__init__.py +9 -0
  7. xcpcio/clics/api_server/app.py +43 -0
  8. xcpcio/{ccs → clics}/api_server/dependencies.py +3 -4
  9. xcpcio/{ccs → clics}/api_server/routes/__init__.py +1 -1
  10. xcpcio/{ccs → clics}/api_server/server.py +15 -35
  11. xcpcio/{ccs → clics}/api_server/services/__init__.py +3 -1
  12. xcpcio/{ccs → clics}/api_server/services/contest_service.py +4 -4
  13. xcpcio/clics/clics_api_client.py +215 -0
  14. xcpcio/{ccs → clics}/contest_archiver.py +27 -169
  15. xcpcio/{ccs → clics}/model/model_2023_06/__init__.py +1 -1
  16. xcpcio/clics/reader/__init__.py +7 -0
  17. xcpcio/{ccs → clics}/reader/contest_package_reader.py +3 -3
  18. xcpcio/{ccs/reader/base_ccs_reader.py → clics/reader/interface.py} +2 -2
  19. {xcpcio-0.64.2.dist-info → xcpcio-0.64.4.dist-info}/METADATA +4 -2
  20. xcpcio-0.64.4.dist-info/RECORD +44 -0
  21. xcpcio-0.64.4.dist-info/entry_points.txt +3 -0
  22. xcpcio/ccs/__init__.py +0 -3
  23. xcpcio/ccs/reader/__init__.py +0 -0
  24. xcpcio-0.64.2.dist-info/RECORD +0 -39
  25. xcpcio-0.64.2.dist-info/entry_points.txt +0 -2
  26. /xcpcio/{ccs → clics}/api_server/__init__.py +0 -0
  27. /xcpcio/{ccs → clics}/api_server/routes/access.py +0 -0
  28. /xcpcio/{ccs → clics}/api_server/routes/accounts.py +0 -0
  29. /xcpcio/{ccs → clics}/api_server/routes/awards.py +0 -0
  30. /xcpcio/{ccs → clics}/api_server/routes/clarifications.py +0 -0
  31. /xcpcio/{ccs → clics}/api_server/routes/contests.py +0 -0
  32. /xcpcio/{ccs → clics}/api_server/routes/general.py +0 -0
  33. /xcpcio/{ccs → clics}/api_server/routes/groups.py +0 -0
  34. /xcpcio/{ccs → clics}/api_server/routes/judgement_types.py +0 -0
  35. /xcpcio/{ccs → clics}/api_server/routes/judgements.py +0 -0
  36. /xcpcio/{ccs → clics}/api_server/routes/languages.py +0 -0
  37. /xcpcio/{ccs → clics}/api_server/routes/organizations.py +0 -0
  38. /xcpcio/{ccs → clics}/api_server/routes/problems.py +0 -0
  39. /xcpcio/{ccs → clics}/api_server/routes/runs.py +0 -0
  40. /xcpcio/{ccs → clics}/api_server/routes/submissions.py +0 -0
  41. /xcpcio/{ccs → clics}/api_server/routes/teams.py +0 -0
  42. /xcpcio/{ccs → clics}/base/__init__.py +0 -0
  43. /xcpcio/{ccs → clics}/base/types.py +0 -0
  44. /xcpcio/{ccs → clics}/model/__init__.py +0 -0
  45. /xcpcio/{ccs → clics}/model/model_2023_06/model.py +0 -0
  46. {xcpcio-0.64.2.dist-info → xcpcio-0.64.4.dist-info}/WHEEL +0 -0
xcpcio/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- from . import ccs, constants, types
1
+ from . import clics, constants, types
2
2
  from .__version__ import __version__
3
3
 
4
- __all__ = [constants, types, ccs, __version__]
4
+ __all__ = [constants, types, clics, __version__]
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.64.2'
4
+ __version__ = VERSION = '0.64.4'
xcpcio/api/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ from .client import ApiClient
2
+ from .models import (
3
+ HTTPValidationError,
4
+ UploadBoardDataReq,
5
+ UploadBoardDataResp,
6
+ ValidationError,
7
+ )
8
+
9
+ __all__ = [
10
+ ApiClient,
11
+ HTTPValidationError,
12
+ UploadBoardDataReq,
13
+ UploadBoardDataResp,
14
+ ValidationError,
15
+ ]
xcpcio/api/client.py ADDED
@@ -0,0 +1,74 @@
1
+ from typing import Any, Dict, Optional
2
+
3
+ import aiohttp
4
+
5
+ from .models import UploadBoardDataReq, UploadBoardDataResp
6
+
7
+
8
+ class ApiClient:
9
+ def __init__(
10
+ self,
11
+ base_url: str = "https://board-admin.xcpcio.com",
12
+ token: Optional[str] = None,
13
+ timeout: int = 10,
14
+ ):
15
+ self._base_url = base_url.rstrip("/")
16
+ self._token = token
17
+ self._timeout = aiohttp.ClientTimeout(total=timeout)
18
+ self._session: Optional[aiohttp.ClientSession] = None
19
+
20
+ async def __aenter__(self):
21
+ self._session = aiohttp.ClientSession(timeout=self._timeout)
22
+ return self
23
+
24
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
25
+ if self._session:
26
+ await self._session.close()
27
+
28
+ async def _ensure_session(self):
29
+ if self._session is None:
30
+ self._session = aiohttp.ClientSession(timeout=self._timeout)
31
+
32
+ async def _ensure_token(self):
33
+ if self._token is None:
34
+ raise ValueError(
35
+ "Token is required for this operation. Please provide a token when initializing the client."
36
+ )
37
+
38
+ async def close(self):
39
+ if self._session:
40
+ await self._session.close()
41
+ self._session = None
42
+
43
+ async def ping(self) -> Dict[str, Any]:
44
+ await self._ensure_session()
45
+ async with self._session.get(f"{self._base_url}/api/ping") as response:
46
+ response.raise_for_status()
47
+ return await response.json()
48
+
49
+ async def upload_board_data(
50
+ self,
51
+ config: Optional[str] = None,
52
+ teams: Optional[str] = None,
53
+ submissions: Optional[str] = None,
54
+ extra_files: Optional[Dict[str, str]] = None,
55
+ ) -> UploadBoardDataResp:
56
+ await self._ensure_session()
57
+ await self._ensure_token()
58
+
59
+ request_data = UploadBoardDataReq(
60
+ token=self._token,
61
+ config=config,
62
+ teams=teams,
63
+ submissions=submissions,
64
+ extra_files=extra_files,
65
+ )
66
+
67
+ async with self._session.post(
68
+ f"{self._base_url}/api/upload-board-data",
69
+ json=request_data.model_dump(exclude_none=True),
70
+ headers={"Content-Type": "application/json"},
71
+ ) as response:
72
+ response.raise_for_status()
73
+ data = await response.json()
74
+ return UploadBoardDataResp(**data)
xcpcio/api/models.py ADDED
@@ -0,0 +1,25 @@
1
+ from typing import Dict, List, Optional, Union
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class ValidationError(BaseModel):
7
+ loc: List[Union[str, int]] = Field(..., title="Location")
8
+ msg: str = Field(..., title="Message")
9
+ type: str = Field(..., title="Error Type")
10
+
11
+
12
+ class HTTPValidationError(BaseModel):
13
+ detail: Optional[List[ValidationError]] = Field(None, title="Detail")
14
+
15
+
16
+ class UploadBoardDataReq(BaseModel):
17
+ token: str = Field(..., title="Token")
18
+ config: Optional[str] = Field(None, title="Config")
19
+ teams: Optional[str] = Field(None, title="Teams")
20
+ submissions: Optional[str] = Field(None, title="Submissions")
21
+ extra_files: Optional[Dict[str, str]] = Field(None, title="Extra Files")
22
+
23
+
24
+ class UploadBoardDataResp(BaseModel):
25
+ pass
@@ -0,0 +1,9 @@
1
+ from . import api_server, base, contest_archiver, model, reader
2
+
3
+ __all__ = [
4
+ model,
5
+ contest_archiver,
6
+ api_server,
7
+ base,
8
+ reader,
9
+ ]
@@ -0,0 +1,43 @@
1
+ """
2
+ Contest API Server Application
3
+
4
+ FastAPI application instance for Contest API Server.
5
+ This module can be used directly with uvicorn for reload support.
6
+ """
7
+
8
+ from fastapi import FastAPI
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+
11
+ from xcpcio.__version__ import __version__
12
+
13
+ from .routes import create_router
14
+
15
+
16
+ def create_app() -> FastAPI:
17
+ """
18
+ Create and configure FastAPI application.
19
+
20
+ Returns:
21
+ Configured FastAPI application instance
22
+ """
23
+ app = FastAPI(
24
+ title="Contest API Server",
25
+ description="REST API for Contest Control System specifications",
26
+ version=__version__,
27
+ docs_url="/docs",
28
+ redoc_url="/redoc",
29
+ openapi_url="/openapi.json",
30
+ )
31
+
32
+ app.add_middleware(
33
+ CORSMiddleware,
34
+ allow_origins=["*"],
35
+ allow_credentials=True,
36
+ allow_methods=["*"],
37
+ allow_headers=["*"],
38
+ )
39
+
40
+ router = create_router()
41
+ app.include_router(router)
42
+
43
+ return app
@@ -9,10 +9,9 @@ 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
12
+ from xcpcio.clics.reader import BaseContestReader, ContestPackageReader
14
13
 
15
- from .services.contest_service import ContestService
14
+ from .services import ContestService
16
15
 
17
16
  _contest_service_instance = None
18
17
 
@@ -43,7 +42,7 @@ def configure_dependencies(contest_package_dir: Path) -> None:
43
42
  contest_package_dir: Path to contest package directory
44
43
  """
45
44
  global _contest_service_instance
46
- reader_dict: Dict[str, BaseCCSReader] = {}
45
+ reader_dict: Dict[str, BaseContestReader] = {}
47
46
  contest_package_reader = ContestPackageReader(contest_package_dir)
48
47
  reader_dict[contest_package_reader.get_contest_id()] = contest_package_reader
49
48
  _contest_service_instance = ContestService(reader_dict)
@@ -49,4 +49,4 @@ def create_router() -> APIRouter:
49
49
  return router
50
50
 
51
51
 
52
- __all__ = ["create_router"]
52
+ __all__ = [create_router]
@@ -8,13 +8,9 @@ import logging
8
8
  from pathlib import Path
9
9
 
10
10
  import uvicorn
11
- from fastapi import FastAPI
12
- from fastapi.middleware.cors import CORSMiddleware
13
-
14
- from xcpcio.__version__ import __version__
15
11
 
12
+ from .app import create_app
16
13
  from .dependencies import configure_dependencies
17
- from .routes import create_router
18
14
 
19
15
  logger = logging.getLogger(__name__)
20
16
 
@@ -35,42 +31,21 @@ class ContestAPIServer:
35
31
  contest_packages: Dictionary mapping contest_id to contest package directory
36
32
  """
37
33
  self.contest_package_dir = contest_package_dir
38
-
39
- # Configure dependency injection for multi-contest mode
40
- # This might need adjustment based on how dependencies work
41
34
  configure_dependencies(contest_package_dir)
42
-
43
- # Create FastAPI application
44
- self.app = FastAPI(
45
- title="Contest API Server",
46
- description="REST API for Contest Control System specifications",
47
- version=__version__,
48
- docs_url="/docs",
49
- redoc_url="/redoc",
50
- openapi_url="/openapi.json",
51
- )
52
-
53
- # Add CORS middleware
54
- self.app.add_middleware(
55
- CORSMiddleware,
56
- allow_origins=["*"],
57
- allow_credentials=True,
58
- allow_methods=["*"],
59
- allow_headers=["*"],
60
- )
61
-
62
- # Include all routes
63
- router = create_router()
64
- self.app.include_router(router)
65
-
66
- def run(self, host: str = "0.0.0.0", port: int = 8000, reload: bool = True, log_level: str = "info"):
35
+ self.app = create_app()
36
+
37
+ def run(
38
+ self,
39
+ host: str = "0.0.0.0",
40
+ port: int = 8000,
41
+ log_level: str = "info",
42
+ ):
67
43
  """
68
44
  Run the contest API server.
69
45
 
70
46
  Args:
71
47
  host: Host to bind to
72
48
  port: Port to bind to
73
- reload: Enable auto-reload for development
74
49
  log_level: Log level (debug, info, warning, error, critical)
75
50
  """
76
51
 
@@ -80,4 +55,9 @@ class ContestAPIServer:
80
55
  logger.info(f"Interactive docs at: http://{host}:{port}/docs")
81
56
  logger.info(f"ReDoc at: http://{host}:{port}/redoc")
82
57
 
83
- uvicorn.run(self.app, host=host, port=port, reload=reload, log_level=log_level)
58
+ uvicorn.run(
59
+ self.app,
60
+ host=host,
61
+ port=port,
62
+ log_level=log_level,
63
+ )
@@ -6,4 +6,6 @@ Contains business logic and data access layers.
6
6
 
7
7
  from .contest_service import ContestService
8
8
 
9
- __all__ = ["ContestService"]
9
+ __all__ = [
10
+ ContestService,
11
+ ]
@@ -11,8 +11,8 @@ from typing import Any, Dict, List, Optional
11
11
  from fastapi import HTTPException
12
12
 
13
13
  from xcpcio.__version__ import __version__
14
- from xcpcio.ccs.base.types import FileAttr
15
- from xcpcio.ccs.reader.base_ccs_reader import BaseCCSReader
14
+ from xcpcio.clics.base.types import FileAttr
15
+ from xcpcio.clics.reader.interface import BaseContestReader
16
16
 
17
17
  logger = logging.getLogger(__name__)
18
18
 
@@ -20,10 +20,10 @@ logger = logging.getLogger(__name__)
20
20
  class ContestService:
21
21
  """Service class for contest-related operations"""
22
22
 
23
- def __init__(self, reader_dict: Dict[str, BaseCCSReader]):
23
+ def __init__(self, reader_dict: Dict[str, BaseContestReader]):
24
24
  self.reader_dict = reader_dict
25
25
 
26
- def _get_reader(self, contest_id: str) -> BaseCCSReader:
26
+ def _get_reader(self, contest_id: str) -> BaseContestReader:
27
27
  if contest_id not in self.reader_dict:
28
28
  raise HTTPException(status_code=404, detail=f"Contest {contest_id} not found")
29
29
  return self.reader_dict[contest_id]
@@ -0,0 +1,215 @@
1
+ """
2
+ CLICS API Client
3
+ A reusable client for interacting with CLICS (Contest Logging for ICPC Systems) APIs.
4
+
5
+ Based on the CCS Contest API specification:
6
+ https://ccs-specs.icpc.io/2023-06/contest_api
7
+ """
8
+
9
+ import asyncio
10
+ import logging
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import Any, Dict, Optional
14
+ from urllib.parse import urljoin
15
+
16
+ import aiofiles
17
+ import aiohttp
18
+ import semver
19
+ from tenacity import before_sleep_log, retry, retry_if_exception_type, stop_after_attempt, wait_exponential
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ @dataclass
25
+ class APICredentials:
26
+ """API authentication credentials"""
27
+
28
+ username: Optional[str] = None
29
+ password: Optional[str] = None
30
+ token: Optional[str] = None
31
+
32
+
33
+ @dataclass
34
+ class ClicsApiConfig:
35
+ """Configuration for CLICS API client"""
36
+
37
+ base_url: str
38
+ credentials: APICredentials
39
+ timeout: int = 30
40
+ max_concurrent: int = 10
41
+
42
+
43
+ class ClicsApiClient:
44
+ """
45
+ Client for interacting with CLICS (Contest Logging for ICPC Systems) APIs.
46
+
47
+ Provides async methods for fetching JSON data and downloading files from
48
+ CLICS-compliant contest management systems.
49
+ """
50
+
51
+ def __init__(self, config: ClicsApiConfig):
52
+ self._config = config
53
+ self._session: Optional[aiohttp.ClientSession] = None
54
+ self._semaphore = asyncio.Semaphore(config.max_concurrent)
55
+ self._api_info: Optional[Dict[str, Any]] = None
56
+ self._provider_name: Optional[str] = None
57
+ self._provider_version: Optional[semver.VersionInfo] = None
58
+
59
+ def _build_url(self, endpoint: str) -> str:
60
+ """Build API URL ensuring proper path joining"""
61
+ base = self._config.base_url.rstrip("/") + "/"
62
+ endpoint = endpoint.lstrip("/")
63
+ return urljoin(base, endpoint)
64
+
65
+ async def __aenter__(self):
66
+ """Async context manager entry"""
67
+ await self.start_session()
68
+ return self
69
+
70
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
71
+ """Async context manager exit"""
72
+ await self.close_session()
73
+
74
+ async def start_session(self):
75
+ """Initialize the HTTP session with authentication"""
76
+ auth = None
77
+ headers = {}
78
+
79
+ if self._config.credentials.username and self._config.credentials.password:
80
+ auth = aiohttp.BasicAuth(self._config.credentials.username, self._config.credentials.password)
81
+ elif self._config.credentials.token:
82
+ headers["Authorization"] = f"Bearer {self._config.credentials.token}"
83
+
84
+ self._session = aiohttp.ClientSession(auth=auth, headers=headers)
85
+
86
+ async def close_session(self):
87
+ """Close the HTTP session"""
88
+ if self._session:
89
+ await self._session.close()
90
+
91
+ @retry(
92
+ stop=stop_after_attempt(3),
93
+ wait=wait_exponential(multiplier=1, min=1, max=10),
94
+ retry=retry_if_exception_type((asyncio.TimeoutError, aiohttp.ClientError)),
95
+ before_sleep=before_sleep_log(logger, logging.WARNING),
96
+ reraise=True,
97
+ )
98
+ async def _fetch_json_internal(self, url: str, override_timeout: Optional[int] = None) -> Optional[Dict[str, Any]]:
99
+ """Internal fetch method with retry logic"""
100
+ logger.info(f"Fetching {url}")
101
+ timeout = aiohttp.ClientTimeout(total=override_timeout or self._config.timeout)
102
+ async with self._session.get(url, timeout=timeout) as response:
103
+ if response.status == 404:
104
+ logger.warning(f"Endpoint not found: {url}")
105
+ return None
106
+ elif response.status != 200:
107
+ raise aiohttp.ClientResponseError(
108
+ request_info=response.request_info, history=response.history, status=response.status
109
+ )
110
+
111
+ data = await response.json()
112
+ logger.debug(f"Fetched {len(str(data))} bytes from {url}")
113
+ return data
114
+
115
+ async def fetch_json(self, endpoint: str, override_timeout: Optional[int] = None) -> Optional[Dict[str, Any]]:
116
+ """Fetch JSON data from an API endpoint"""
117
+ url = self._build_url(endpoint)
118
+
119
+ async with self._semaphore:
120
+ try:
121
+ return await self._fetch_json_internal(url, override_timeout)
122
+ except Exception as e:
123
+ logger.error(f"Failed to fetch. [url={url}] [err={e}]")
124
+ return None
125
+
126
+ @retry(
127
+ stop=stop_after_attempt(3),
128
+ wait=wait_exponential(multiplier=1, min=1, max=10),
129
+ retry=retry_if_exception_type((asyncio.TimeoutError, aiohttp.ClientError)),
130
+ before_sleep=before_sleep_log(logger, logging.WARNING),
131
+ reraise=True,
132
+ )
133
+ async def _fetch_file_internal(
134
+ self, file_url: str, output_path: Path, override_timeout: Optional[int] = None
135
+ ) -> bool:
136
+ """Internal file download method with retry logic"""
137
+ logger.info(f"Downloading {file_url} -> {output_path}")
138
+ output_path.parent.mkdir(parents=True, exist_ok=True)
139
+
140
+ timeout = aiohttp.ClientTimeout(total=override_timeout or self._config.timeout)
141
+ async with self._session.get(file_url, timeout=timeout) as response:
142
+ if response.status != 200:
143
+ raise aiohttp.ClientResponseError(
144
+ request_info=response.request_info, history=response.history, status=response.status
145
+ )
146
+
147
+ async with aiofiles.open(output_path, "wb") as f:
148
+ async for chunk in response.content.iter_chunked(8192):
149
+ await f.write(chunk)
150
+
151
+ logger.debug(f"Downloaded {output_path}")
152
+ return True
153
+
154
+ async def fetch_file(
155
+ self, file_url: Optional[str], output_path: Path, override_timeout: Optional[int] = None
156
+ ) -> bool:
157
+ """Download a file from URL to local path"""
158
+ if not file_url:
159
+ return False
160
+
161
+ if not file_url.startswith(("http://", "https://")):
162
+ file_url = self._build_url(file_url)
163
+
164
+ async with self._semaphore:
165
+ try:
166
+ return await self._fetch_file_internal(file_url, output_path, override_timeout)
167
+ except Exception as e:
168
+ logger.error(f"Failed to download {file_url} after retries: {e}")
169
+ return False
170
+
171
+ async def fetch_api_info(self) -> Optional[Dict[str, Any]]:
172
+ """Fetch API root endpoint information and parse provider details"""
173
+ data = await self.fetch_json("/")
174
+ if not data:
175
+ return None
176
+
177
+ self._api_info = data
178
+
179
+ if "provider" in data:
180
+ provider: Dict = data.get("provider", {})
181
+ self._provider_name = provider.get("name", "")
182
+
183
+ version_str: str = provider.get("version", "")
184
+ if version_str:
185
+ try:
186
+ version_clean = version_str.split("/")[0]
187
+ if version_clean.endswith("DEV"):
188
+ version_clean = version_clean[:-3] + "-dev"
189
+
190
+ self._provider_version = semver.VersionInfo.parse(version_clean)
191
+ logger.info(
192
+ f"Detected API provider: {self._provider_name} version {version_str} (parsed: {self._provider_version})"
193
+ )
194
+ except (ValueError, TypeError) as e:
195
+ logger.warning(f"Could not parse version string: {version_str}, error: {e}")
196
+ self._provider_version = None
197
+ else:
198
+ logger.info(f"Detected API provider: {self._provider_name} (no version)")
199
+
200
+ return data
201
+
202
+ @property
203
+ def api_info(self) -> Optional[Dict[str, Any]]:
204
+ """Get cached API information"""
205
+ return self._api_info
206
+
207
+ @property
208
+ def provider_name(self) -> Optional[str]:
209
+ """Get the detected API provider name"""
210
+ return self._provider_name
211
+
212
+ @property
213
+ def provider_version(self) -> Optional[semver.VersionInfo]:
214
+ """Get the detected API provider version"""
215
+ return self._provider_version
@@ -14,24 +14,13 @@ import json
14
14
  import logging
15
15
  from dataclasses import dataclass
16
16
  from pathlib import Path
17
- from typing import Any, Dict, List, Optional
18
- from urllib.parse import urljoin
17
+ from typing import Any, List, Optional
19
18
 
20
19
  import aiofiles
21
- import aiohttp
22
- import semver
23
- from tenacity import before_sleep_log, retry, retry_if_exception_type, stop_after_attempt, wait_exponential
24
20
 
25
- logger = logging.getLogger(__name__)
26
-
27
-
28
- @dataclass
29
- class APICredentials:
30
- """API authentication credentials"""
21
+ from xcpcio.clics.clics_api_client import APICredentials, ClicsApiClient, ClicsApiConfig
31
22
 
32
- username: Optional[str] = None
33
- password: Optional[str] = None
34
- token: Optional[str] = None
23
+ logger = logging.getLogger(__name__)
35
24
 
36
25
 
37
26
  @dataclass
@@ -48,6 +37,15 @@ class ArchiveConfig:
48
37
  max_concurrent: int = 10
49
38
  include_event_feed: bool = False
50
39
 
40
+ def to_api_config(self) -> ClicsApiConfig:
41
+ """Convert to ClicsApiConfig"""
42
+ return ClicsApiConfig(
43
+ base_url=self.base_url,
44
+ credentials=self.credentials,
45
+ timeout=self.timeout,
46
+ max_concurrent=self.max_concurrent,
47
+ )
48
+
51
49
 
52
50
  class ContestArchiver:
53
51
  """
@@ -93,134 +91,22 @@ class ContestArchiver:
93
91
  "runs",
94
92
  "clarifications",
95
93
  "awards",
96
- "scoreboard",
97
94
  ]
98
95
 
99
96
  def __init__(self, config: ArchiveConfig):
100
97
  self._config = config
101
- self._session: Optional[aiohttp.ClientSession] = None
102
- self._semaphore = asyncio.Semaphore(config.max_concurrent)
103
- self._api_info: Optional[Dict[str, Any]] = None
104
- self._provider_name: Optional[str] = None
105
- self._provider_version: Optional[semver.VersionInfo] = None
98
+ self._client = ClicsApiClient(config.to_api_config())
106
99
 
107
- # Create output directory
108
100
  self._config.output_dir.mkdir(parents=True, exist_ok=True)
109
101
 
110
- def _build_url(self, endpoint: str) -> str:
111
- """Build API URL ensuring proper path joining"""
112
- # Ensure base_url ends with / and endpoint doesn't start with /
113
- base = self._config.base_url.rstrip("/") + "/"
114
- endpoint = endpoint.lstrip("/")
115
- return urljoin(base, endpoint)
116
-
117
102
  async def __aenter__(self):
118
103
  """Async context manager entry"""
119
- await self.start_session()
104
+ await self._client.__aenter__()
120
105
  return self
121
106
 
122
107
  async def __aexit__(self, exc_type, exc_val, exc_tb):
123
108
  """Async context manager exit"""
124
- await self.close_session()
125
-
126
- async def start_session(self):
127
- """Initialize the HTTP session with authentication"""
128
- # Setup authentication
129
- auth = None
130
- headers = {}
131
-
132
- if self._config.credentials.username and self._config.credentials.password:
133
- auth = aiohttp.BasicAuth(self._config.credentials.username, self._config.credentials.password)
134
- elif self._config.credentials.token:
135
- headers["Authorization"] = f"Bearer {self._config.credentials.token}"
136
-
137
- self._session = aiohttp.ClientSession(auth=auth, headers=headers)
138
-
139
- async def close_session(self):
140
- """Close the HTTP session"""
141
- if self._session:
142
- await self._session.close()
143
-
144
- @retry(
145
- stop=stop_after_attempt(3),
146
- wait=wait_exponential(multiplier=1, min=1, max=10),
147
- retry=retry_if_exception_type((asyncio.TimeoutError, aiohttp.ClientError)),
148
- before_sleep=before_sleep_log(logger, logging.WARNING),
149
- reraise=True,
150
- )
151
- async def _fetch_json_internal(self, url: str, override_timeout: Optional[int] = None) -> Optional[Dict[str, Any]]:
152
- """Internal fetch method with retry logic"""
153
- logger.info(f"Fetching {url}")
154
- timeout = aiohttp.ClientTimeout(total=override_timeout or self._config.timeout)
155
- async with self._session.get(url, timeout=timeout) as response:
156
- if response.status == 404:
157
- logger.warning(f"Endpoint not found: {url}")
158
- return None
159
- elif response.status != 200:
160
- raise aiohttp.ClientResponseError(
161
- request_info=response.request_info, history=response.history, status=response.status
162
- )
163
-
164
- data = await response.json()
165
- logger.debug(f"Fetched {len(str(data))} bytes from {url}")
166
- return data
167
-
168
- async def fetch_json(self, endpoint: str, override_timeout: Optional[int] = None) -> Optional[Dict[str, Any]]:
169
- """Fetch JSON data from an API endpoint"""
170
- url = self._build_url(endpoint)
171
-
172
- async with self._semaphore:
173
- try:
174
- return await self._fetch_json_internal(url, override_timeout)
175
- except Exception as e:
176
- logger.error(f"Failed to fetch. [url={url}] [err={e}]")
177
- return None
178
-
179
- @retry(
180
- stop=stop_after_attempt(3),
181
- wait=wait_exponential(multiplier=1, min=1, max=10),
182
- retry=retry_if_exception_type((asyncio.TimeoutError, aiohttp.ClientError)),
183
- before_sleep=before_sleep_log(logger, logging.WARNING),
184
- reraise=True,
185
- )
186
- async def _fetch_file_internal(
187
- self, file_url: str, output_path: Path, override_timeout: Optional[int] = None
188
- ) -> bool:
189
- """Internal file download method with retry logic"""
190
- logger.info(f"Downloading {file_url} -> {output_path}")
191
- output_path.parent.mkdir(parents=True, exist_ok=True)
192
-
193
- timeout = aiohttp.ClientTimeout(total=override_timeout or self._config.timeout)
194
- async with self._session.get(file_url, timeout=timeout) as response:
195
- if response.status != 200:
196
- raise aiohttp.ClientResponseError(
197
- request_info=response.request_info, history=response.history, status=response.status
198
- )
199
-
200
- async with aiofiles.open(output_path, "wb") as f:
201
- async for chunk in response.content.iter_chunked(8192):
202
- await f.write(chunk)
203
-
204
- logger.debug(f"Downloaded {output_path}")
205
- return True
206
-
207
- async def fetch_file(
208
- self, file_url: Optional[str], output_path: Path, override_timeout: Optional[int] = None
209
- ) -> bool:
210
- """Download a file from URL to local path"""
211
- if not file_url:
212
- return False
213
-
214
- # Handle relative URLs
215
- if not file_url.startswith(("http://", "https://")):
216
- file_url = self._build_url(file_url)
217
-
218
- async with self._semaphore:
219
- try:
220
- return await self._fetch_file_internal(file_url, output_path, override_timeout)
221
- except Exception as e:
222
- logger.error(f"Failed to download {file_url} after retries: {e}")
223
- return False
109
+ await self._client.__aexit__(exc_type, exc_val, exc_tb)
224
110
 
225
111
  def _get_file_output_path(
226
112
  self, filename: str, base_path: Optional[str] = None, object_id: Optional[str] = None
@@ -265,8 +151,7 @@ class ContestArchiver:
265
151
  if not file_refs:
266
152
  return
267
153
 
268
- # Download all files in parallel (controlled by self.semaphore)
269
- download_tasks = [self.fetch_file(href, output_path) for href, output_path in file_refs]
154
+ download_tasks = [self._client.fetch_file(href, output_path) for href, output_path in file_refs]
270
155
 
271
156
  if download_tasks:
272
157
  await asyncio.gather(*download_tasks, return_exceptions=True)
@@ -285,37 +170,10 @@ class ContestArchiver:
285
170
  """Dump API root endpoint information"""
286
171
  logger.info("Dumping API information...")
287
172
 
288
- data = await self.fetch_json("/")
173
+ data = await self._client.fetch_api_info()
289
174
  if not data:
290
175
  raise RuntimeError("Failed to fetch API information")
291
176
 
292
- self._api_info = data # Store API info for later use
293
-
294
- # Parse provider information
295
- if "provider" in data:
296
- provider: Dict = data.get("provider", {})
297
- self._provider_name = provider.get("name", "")
298
-
299
- # Parse version string to semver.VersionInfo
300
- version_str: str = provider.get("version", "")
301
- if version_str:
302
- try:
303
- # Clean version string: "8.3.1/3324986cd" -> "8.3.1", "9.0.0DEV/26e89f701" -> "9.0.0-dev"
304
- version_clean = version_str.split("/")[0]
305
- # Convert DEV suffix to semver prerelease format
306
- if version_clean.endswith("DEV"):
307
- version_clean = version_clean[:-3] + "-dev"
308
-
309
- self._provider_version = semver.VersionInfo.parse(version_clean)
310
- logger.info(
311
- f"Detected API provider: {self._provider_name} version {version_str} (parsed: {self._provider_version})"
312
- )
313
- except (ValueError, TypeError) as e:
314
- logger.warning(f"Could not parse version string: {version_str}, error: {e}")
315
- self._provider_version = None
316
- else:
317
- logger.info(f"Detected API provider: {self._provider_name} (no version)")
318
-
319
177
  await self.save_data("api.json", data)
320
178
  await self._download_file_references(data, "api")
321
179
 
@@ -324,7 +182,7 @@ class ContestArchiver:
324
182
  logger.info("Dumping contest information...")
325
183
 
326
184
  endpoint = f"contests/{self._config.contest_id}"
327
- data = await self.fetch_json(endpoint)
185
+ data = await self._client.fetch_json(endpoint)
328
186
  if data:
329
187
  await self.save_data("contest.json", data)
330
188
  await self._download_file_references(data, "contest")
@@ -334,7 +192,7 @@ class ContestArchiver:
334
192
  logger.info(f"Dumping {endpoint}...")
335
193
 
336
194
  api_endpoint = f"contests/{self._config.contest_id}/{endpoint}"
337
- data = await self.fetch_json(api_endpoint)
195
+ data = await self._client.fetch_json(api_endpoint)
338
196
 
339
197
  if data is None:
340
198
  return
@@ -352,7 +210,7 @@ class ContestArchiver:
352
210
  logger.info(f"Dumping {endpoint}...")
353
211
 
354
212
  api_endpoint = f"contests/{self._config.contest_id}/{endpoint}"
355
- data = await self.fetch_json(api_endpoint)
213
+ data = await self._client.fetch_json(api_endpoint)
356
214
 
357
215
  if data is None:
358
216
  return
@@ -365,8 +223,7 @@ class ContestArchiver:
365
223
  logger.info("Dumping event-feed...")
366
224
 
367
225
  api_endpoint = f"contests/{self._config.contest_id}/event-feed?stream=false"
368
- # Use extended timeout for event-feed as it may contain large amounts of data
369
- await self.fetch_file(
226
+ await self._client.fetch_file(
370
227
  api_endpoint,
371
228
  output_path=self._get_file_output_path("event-feed.ndjson"),
372
229
  override_timeout=self._config.timeout * 10,
@@ -374,13 +231,14 @@ class ContestArchiver:
374
231
 
375
232
  async def get_available_endpoints(self) -> List[str]:
376
233
  """Get list of available endpoints based on API provider and version"""
377
- # Check if it's DOMjudge with version < 9.0.0
378
- if self._provider_name == "DOMjudge" and self._provider_version and self._provider_version.major < 9:
379
- logger.info(f"Using DOMjudge known endpoints for version < 9.0.0 (detected: {self._provider_version})")
234
+ provider_name = self._client.provider_name
235
+ provider_version = self._client.provider_version
236
+
237
+ if provider_name == "DOMjudge" and provider_version and provider_version.major < 9:
238
+ logger.info(f"Using DOMjudge known endpoints for version < 9.0.0 (detected: {provider_version})")
380
239
  return self.DOMJUDGE_KNOWN_ENDPOINTS
381
240
 
382
- # For other providers or DOMjudge >= 9.0.0, try to get from access endpoint
383
- access_data = await self.fetch_json(f"contests/{self._config.contest_id}/access")
241
+ access_data = await self._client.fetch_json(f"contests/{self._config.contest_id}/access")
384
242
 
385
243
  if not access_data or "endpoints" not in access_data:
386
244
  logger.warning("Could not fetch access info, using default endpoints")
@@ -4,4 +4,4 @@ CCS (Contest Control System) models for branch 2023-06.
4
4
  Auto-generated from https://github.com/icpc/ccs-specs/tree/2023-06
5
5
  """
6
6
 
7
- from .model import * # noqa: F403
7
+ from .model import * # noqa: F403
@@ -0,0 +1,7 @@
1
+ from .contest_package_reader import ContestPackageReader
2
+ from .interface import BaseContestReader
3
+
4
+ __all__ = [
5
+ BaseContestReader,
6
+ ContestPackageReader,
7
+ ]
@@ -7,13 +7,13 @@ from typing import Any, Dict, List, Optional, Union
7
7
 
8
8
  from fastapi import HTTPException
9
9
 
10
- from xcpcio.ccs.base.types import FileAttr
11
- from xcpcio.ccs.reader.base_ccs_reader import BaseCCSReader
10
+ from xcpcio.clics.base.types import FileAttr
11
+ from xcpcio.clics.reader.interface import BaseContestReader
12
12
 
13
13
  logger = logging.getLogger(__name__)
14
14
 
15
15
 
16
- class ContestPackageReader(BaseCCSReader):
16
+ class ContestPackageReader(BaseContestReader):
17
17
  def __init__(self, contest_package_dir: Path):
18
18
  self.contest_package_dir = contest_package_dir
19
19
  if not self.contest_package_dir.exists():
@@ -1,10 +1,10 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from typing import Any, Dict, List, Optional
3
3
 
4
- from xcpcio.ccs.base.types import FileAttr
4
+ from xcpcio.clics.base.types import FileAttr
5
5
 
6
6
 
7
- class BaseCCSReader(ABC):
7
+ class BaseContestReader(ABC):
8
8
  # API Information
9
9
  @abstractmethod
10
10
  def get_api_info(self) -> Dict[str, Any]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xcpcio
3
- Version: 0.64.2
3
+ Version: 0.64.4
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
@@ -83,4 +83,6 @@ For detailed documentation, visit:
83
83
 
84
84
  ## License
85
85
 
86
- MIT License &copy; 2020-PRESENT [Dup4](https://github.com/Dup4)
86
+ [MIT](../LICENSE) License &copy; 2020 - PRESENT [XCPCIO][xcpcio]
87
+
88
+ [xcpcio]: https://xcpcio.com
@@ -0,0 +1,44 @@
1
+ xcpcio/__init__.py,sha256=NB6wpVr5JUrOx2vLIQSVtYuCz0d7kNFE39TSQlvoENk,125
2
+ xcpcio/__version__.py,sha256=YJ36rfIhlC5_Ph8FNpZ2YYGKo1_DImruLCg7WIClnWg,172
3
+ xcpcio/constants.py,sha256=MjpAgNXiBlUsx1S09m7JNT-nekNDR-aE6ggvGL3fg0I,2297
4
+ xcpcio/types.py,sha256=AkYby2haJgxwtozlgaPMG2ryAZdvsSc3sH-p6qXcM4g,6575
5
+ xcpcio/api/__init__.py,sha256=B9gLdAlR3FD7070cvAC5wAwMy3iV63I8hh4mUrnrKpk,274
6
+ xcpcio/api/client.py,sha256=BuzH8DbJYudJ-Kne-2XziLW__B_7iEqElJ4n2SGZCoY,2374
7
+ xcpcio/api/models.py,sha256=_dChApnIHVNN3hEL7mR5zonq8IUcxW_h7z1kUz6reSs,772
8
+ xcpcio/clics/__init__.py,sha256=coTZiqxzXesn2SYmI2ZCsDZW6XaFi_6p-PFozZ4dfl4,150
9
+ xcpcio/clics/clics_api_client.py,sha256=jQiOYPNZlYs_cOmQIbp-QovWVMYcmT1yo-33SWoyAn0,7966
10
+ xcpcio/clics/contest_archiver.py,sha256=66a4YTqHqSoHMe7dFTXo4OrdK40AUnavo0ICSd9UNGo,9911
11
+ xcpcio/clics/api_server/__init__.py,sha256=ASvVJ_ibGkXFDSNmy05eb9gESXRS8hjYHCrBecSnaS0,174
12
+ xcpcio/clics/api_server/app.py,sha256=dYvkxaR8PjkDIHbSX8YyU3TvPud93Dg29k6midylakE,979
13
+ xcpcio/clics/api_server/dependencies.py,sha256=2Zhom6vUnAOikr9bh-_kXYCc3g6JGXReVuQX7Ok90r4,1444
14
+ xcpcio/clics/api_server/server.py,sha256=lM87sRrFc5dWCRRrXyzs8Bl4wY77QLadiJnAkmjJt7U,1705
15
+ xcpcio/clics/api_server/routes/__init__.py,sha256=y4rxsXe3g-HCwuky1bvr3I2YWa8NjVxz2uAvLqlyeiE,1535
16
+ xcpcio/clics/api_server/routes/access.py,sha256=O-RGLmgLNBQ-ccu8rlHOgonTjl02fYOdi3VTsUa-T0w,434
17
+ xcpcio/clics/api_server/routes/accounts.py,sha256=nAwiIz-y4fGmqHBniMuQifk9jVt4i9YPZWBB7w40q5Q,994
18
+ xcpcio/clics/api_server/routes/awards.py,sha256=gBPSFlDj6PM6Poys6JbO7VMsfCljKz6QrTjKEqQcS8w,957
19
+ xcpcio/clics/api_server/routes/clarifications.py,sha256=vvMNMvQkZTOn1VJ8C5U86gpIkN3yNrxBll1VMTJjzQg,1046
20
+ xcpcio/clics/api_server/routes/contests.py,sha256=VbvegfP5ZnVNKdqoHmy7_Wd2L6xEBqON7UD2OQ6sQ8A,2979
21
+ xcpcio/clics/api_server/routes/general.py,sha256=xLH-sqyeC4dSd6SUYpjg-w-1ZtmSqZAgIupoVOyYwD4,763
22
+ xcpcio/clics/api_server/routes/groups.py,sha256=kWKFFty2iWckMN-j6G3NbgMVKCPQ478_mYKrYsHXgMA,949
23
+ xcpcio/clics/api_server/routes/judgement_types.py,sha256=S3Dt0VYjntVBQBvLYhJGcDNSy7O8Zh7b7NSfrronZSU,1057
24
+ xcpcio/clics/api_server/routes/judgements.py,sha256=3w0LHYbZazli_v587l98U23r5PuolRs_vtbNLMwgjTU,1127
25
+ xcpcio/clics/api_server/routes/languages.py,sha256=2BYqTaSBWx0E5XlaTjofElLb7z4HTsVs3rrozdyqz0s,985
26
+ xcpcio/clics/api_server/routes/organizations.py,sha256=6xMl0Iqo7pjLaJbR7L1NWwo8JXwnXhH2O8hJKiLoRa0,1712
27
+ xcpcio/clics/api_server/routes/problems.py,sha256=XAhXkShHwewZdugsffLj1UxvFNSorrxc-PyZymdBQTY,1632
28
+ xcpcio/clics/api_server/routes/runs.py,sha256=1L6fo1KPcDeKwOEkTxCwEetmyfNKY_Z0uxATqBJftIs,1046
29
+ xcpcio/clics/api_server/routes/submissions.py,sha256=ByVX_wzab0Q9EHFmuE8JQtikvboMXtt_kXecEy6yiRc,1675
30
+ xcpcio/clics/api_server/routes/teams.py,sha256=euDs-GDHLqmJS3zPTNxm9LfKXQYEuhwAlX-BF2tcrk4,1556
31
+ xcpcio/clics/api_server/services/__init__.py,sha256=yQqrH72olLFG9KEivWNYmaIES_vi8mnzZGX2frdB5cQ,177
32
+ xcpcio/clics/api_server/services/contest_service.py,sha256=1vnt9NB0ZOKJjjParbudHrpnC8_kQRFVuOVxNVR9Drs,7477
33
+ xcpcio/clics/base/__init__.py,sha256=JYKVtcQG-VGM2OfCg6VhxcXCeCp7r2zxpg_7s9AThS0,39
34
+ xcpcio/clics/base/types.py,sha256=NfAG-XJjex7p2DfFTRecLWhmwpeO8Ul42nNMnZH39sM,137
35
+ xcpcio/clics/model/__init__.py,sha256=cZE1q5JY-iHDEKZpsx0UZaMhH-23H4oAHaYOkW7dZ5s,43
36
+ xcpcio/clics/model/model_2023_06/__init__.py,sha256=VzBaFcAwYw9G18p0Lh7rNPrvchyaYx_jgw6YE4W1yNg,168
37
+ xcpcio/clics/model/model_2023_06/model.py,sha256=bVMDWpJTwPSpz1fHPxWrWerxCBIboH3LKVZpIZGQ2pY,15287
38
+ xcpcio/clics/reader/__init__.py,sha256=Nfi78X8J1tJPh7WeSRPLMRUprlS2JYelYJHW4DfyJ7U,162
39
+ xcpcio/clics/reader/contest_package_reader.py,sha256=0wIzQp4zzdaB10zMY4WALLIeoXcMuhMJ6nRrfKxWhgw,14179
40
+ xcpcio/clics/reader/interface.py,sha256=lK2JXU1n8GJ4PecXnfFBijMaCVLYk404e4QwV_Ti2Hk,3918
41
+ xcpcio-0.64.4.dist-info/METADATA,sha256=AFDUMdnTKGrH97RAEfoBBaXg2FYjWe97bFTYU-dSpIc,2233
42
+ xcpcio-0.64.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
43
+ xcpcio-0.64.4.dist-info/entry_points.txt,sha256=vJ7vcfzL_j7M1jvq--10ENgAJNB15O6WVGjwiRWAgas,96
44
+ xcpcio-0.64.4.dist-info/RECORD,,
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ clics-archiver = app.clics_archiver:main
3
+ clics-server = app.clics_server:main
xcpcio/ccs/__init__.py DELETED
@@ -1,3 +0,0 @@
1
- from . import api_server, contest_archiver, model
2
-
3
- __all__ = [model, contest_archiver, api_server]
File without changes
@@ -1,39 +0,0 @@
1
- xcpcio/__init__.py,sha256=kjd6itqBRSQ-OT83qUJXHt81KQQDRUtaIuykzfaWXLM,121
2
- xcpcio/__version__.py,sha256=6ddC-elV7Uw3Fsy5ndjK8ZE0aXQ1P_auAsXjJeT0Jrg,172
3
- xcpcio/constants.py,sha256=MjpAgNXiBlUsx1S09m7JNT-nekNDR-aE6ggvGL3fg0I,2297
4
- xcpcio/types.py,sha256=AkYby2haJgxwtozlgaPMG2ryAZdvsSc3sH-p6qXcM4g,6575
5
- xcpcio/ccs/__init__.py,sha256=LSoKFblEuSoIVBYcUxOFF8fn2bH2R6kSg9xNrBfzC0g,99
6
- xcpcio/ccs/contest_archiver.py,sha256=ICogyPzKfFRoO7J5D2Eu-3JwIirC3etMev7hyTjn4z8,16198
7
- xcpcio/ccs/api_server/__init__.py,sha256=ASvVJ_ibGkXFDSNmy05eb9gESXRS8hjYHCrBecSnaS0,174
8
- xcpcio/ccs/api_server/dependencies.py,sha256=cbLHcP91SaRBj2W9OfC0yCQ1fasI2DofxGUhPPNs3F8,1518
9
- xcpcio/ccs/api_server/server.py,sha256=3gft3MqDXvKWug5UCLhdV681bcQMRrewDkwQ7qSPtRU,2598
10
- xcpcio/ccs/api_server/routes/__init__.py,sha256=uz65H4L5Wzef7QPi5PsLQM1xbMdG6FoZ0Np0y039_2k,1537
11
- xcpcio/ccs/api_server/routes/access.py,sha256=O-RGLmgLNBQ-ccu8rlHOgonTjl02fYOdi3VTsUa-T0w,434
12
- xcpcio/ccs/api_server/routes/accounts.py,sha256=nAwiIz-y4fGmqHBniMuQifk9jVt4i9YPZWBB7w40q5Q,994
13
- xcpcio/ccs/api_server/routes/awards.py,sha256=gBPSFlDj6PM6Poys6JbO7VMsfCljKz6QrTjKEqQcS8w,957
14
- xcpcio/ccs/api_server/routes/clarifications.py,sha256=vvMNMvQkZTOn1VJ8C5U86gpIkN3yNrxBll1VMTJjzQg,1046
15
- xcpcio/ccs/api_server/routes/contests.py,sha256=VbvegfP5ZnVNKdqoHmy7_Wd2L6xEBqON7UD2OQ6sQ8A,2979
16
- xcpcio/ccs/api_server/routes/general.py,sha256=xLH-sqyeC4dSd6SUYpjg-w-1ZtmSqZAgIupoVOyYwD4,763
17
- xcpcio/ccs/api_server/routes/groups.py,sha256=kWKFFty2iWckMN-j6G3NbgMVKCPQ478_mYKrYsHXgMA,949
18
- xcpcio/ccs/api_server/routes/judgement_types.py,sha256=S3Dt0VYjntVBQBvLYhJGcDNSy7O8Zh7b7NSfrronZSU,1057
19
- xcpcio/ccs/api_server/routes/judgements.py,sha256=3w0LHYbZazli_v587l98U23r5PuolRs_vtbNLMwgjTU,1127
20
- xcpcio/ccs/api_server/routes/languages.py,sha256=2BYqTaSBWx0E5XlaTjofElLb7z4HTsVs3rrozdyqz0s,985
21
- xcpcio/ccs/api_server/routes/organizations.py,sha256=6xMl0Iqo7pjLaJbR7L1NWwo8JXwnXhH2O8hJKiLoRa0,1712
22
- xcpcio/ccs/api_server/routes/problems.py,sha256=XAhXkShHwewZdugsffLj1UxvFNSorrxc-PyZymdBQTY,1632
23
- xcpcio/ccs/api_server/routes/runs.py,sha256=1L6fo1KPcDeKwOEkTxCwEetmyfNKY_Z0uxATqBJftIs,1046
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
- xcpcio/ccs/api_server/services/__init__.py,sha256=WQLNrLVomhtICl8HlFYaCoRewIHVZfUiiwrSBUOOWDg,171
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
30
- xcpcio/ccs/model/__init__.py,sha256=cZE1q5JY-iHDEKZpsx0UZaMhH-23H4oAHaYOkW7dZ5s,43
31
- xcpcio/ccs/model/model_2023_06/__init__.py,sha256=OmDQZqmigBpL64LXk5lIOGoQ3Uqis8-2z6qQpOO5aJc,167
32
- xcpcio/ccs/model/model_2023_06/model.py,sha256=bVMDWpJTwPSpz1fHPxWrWerxCBIboH3LKVZpIZGQ2pY,15287
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.2.dist-info/METADATA,sha256=Ay3dU_2FC8jDzF0WqZqn7ml18-lOJRJITv1l4nRL7e4,2202
37
- xcpcio-0.64.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
38
- xcpcio-0.64.2.dist-info/entry_points.txt,sha256=qvzh8oDJxIHqTN-rg2lRN6xR99AqxbWnlAQI7uzDibI,59
39
- xcpcio-0.64.2.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- ccs-archiver = cli.ccs_archiver_cli:main
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes