xcpcio 0.64.2__py3-none-any.whl → 0.64.3__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/__init__.py +2 -2
- xcpcio/__version__.py +1 -1
- xcpcio/api/__init__.py +15 -0
- xcpcio/api/client.py +74 -0
- xcpcio/api/models.py +25 -0
- xcpcio/clics/__init__.py +9 -0
- xcpcio/clics/api_server/app.py +43 -0
- xcpcio/{ccs → clics}/api_server/dependencies.py +3 -4
- xcpcio/{ccs → clics}/api_server/routes/__init__.py +1 -1
- xcpcio/{ccs → clics}/api_server/server.py +15 -35
- xcpcio/{ccs → clics}/api_server/services/__init__.py +3 -1
- xcpcio/{ccs → clics}/api_server/services/contest_service.py +4 -4
- xcpcio/clics/clics_api_client.py +215 -0
- xcpcio/{ccs → clics}/contest_archiver.py +27 -169
- xcpcio/{ccs → clics}/model/model_2023_06/__init__.py +1 -1
- xcpcio/clics/reader/__init__.py +7 -0
- xcpcio/{ccs → clics}/reader/contest_package_reader.py +3 -3
- xcpcio/{ccs/reader/base_ccs_reader.py → clics/reader/interface.py} +2 -2
- {xcpcio-0.64.2.dist-info → xcpcio-0.64.3.dist-info}/METADATA +4 -2
- xcpcio-0.64.3.dist-info/RECORD +44 -0
- xcpcio-0.64.3.dist-info/entry_points.txt +3 -0
- xcpcio/ccs/__init__.py +0 -3
- xcpcio/ccs/reader/__init__.py +0 -0
- xcpcio-0.64.2.dist-info/RECORD +0 -39
- xcpcio-0.64.2.dist-info/entry_points.txt +0 -2
- /xcpcio/{ccs → clics}/api_server/__init__.py +0 -0
- /xcpcio/{ccs → clics}/api_server/routes/access.py +0 -0
- /xcpcio/{ccs → clics}/api_server/routes/accounts.py +0 -0
- /xcpcio/{ccs → clics}/api_server/routes/awards.py +0 -0
- /xcpcio/{ccs → clics}/api_server/routes/clarifications.py +0 -0
- /xcpcio/{ccs → clics}/api_server/routes/contests.py +0 -0
- /xcpcio/{ccs → clics}/api_server/routes/general.py +0 -0
- /xcpcio/{ccs → clics}/api_server/routes/groups.py +0 -0
- /xcpcio/{ccs → clics}/api_server/routes/judgement_types.py +0 -0
- /xcpcio/{ccs → clics}/api_server/routes/judgements.py +0 -0
- /xcpcio/{ccs → clics}/api_server/routes/languages.py +0 -0
- /xcpcio/{ccs → clics}/api_server/routes/organizations.py +0 -0
- /xcpcio/{ccs → clics}/api_server/routes/problems.py +0 -0
- /xcpcio/{ccs → clics}/api_server/routes/runs.py +0 -0
- /xcpcio/{ccs → clics}/api_server/routes/submissions.py +0 -0
- /xcpcio/{ccs → clics}/api_server/routes/teams.py +0 -0
- /xcpcio/{ccs → clics}/base/__init__.py +0 -0
- /xcpcio/{ccs → clics}/base/types.py +0 -0
- /xcpcio/{ccs → clics}/model/__init__.py +0 -0
- /xcpcio/{ccs → clics}/model/model_2023_06/model.py +0 -0
- {xcpcio-0.64.2.dist-info → xcpcio-0.64.3.dist-info}/WHEEL +0 -0
xcpcio/__init__.py
CHANGED
xcpcio/__version__.py
CHANGED
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
|
xcpcio/clics/__init__.py
ADDED
|
@@ -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.
|
|
13
|
-
from xcpcio.ccs.reader.contest_package_reader import ContestPackageReader
|
|
12
|
+
from xcpcio.clics.reader import BaseContestReader, ContestPackageReader
|
|
14
13
|
|
|
15
|
-
from .services
|
|
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,
|
|
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)
|
|
@@ -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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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(
|
|
58
|
+
uvicorn.run(
|
|
59
|
+
self.app,
|
|
60
|
+
host=host,
|
|
61
|
+
port=port,
|
|
62
|
+
log_level=log_level,
|
|
63
|
+
)
|
|
@@ -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.
|
|
15
|
-
from xcpcio.
|
|
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,
|
|
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) ->
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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")
|
|
@@ -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.
|
|
11
|
-
from xcpcio.
|
|
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(
|
|
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.
|
|
4
|
+
from xcpcio.clics.base.types import FileAttr
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
class
|
|
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.
|
|
3
|
+
Version: 0.64.3
|
|
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 © 2020-PRESENT [
|
|
86
|
+
[MIT](../LICENSE) License © 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=LxfvIcnxuKT4V2X_7iFD7f1csO3sp0l-FcxO8fLkvo8,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.3.dist-info/METADATA,sha256=INRnI5HBYrv2D3sSUEH4zyHUt0w7iBQXEKcDEB-o_KU,2233
|
|
42
|
+
xcpcio-0.64.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
43
|
+
xcpcio-0.64.3.dist-info/entry_points.txt,sha256=vJ7vcfzL_j7M1jvq--10ENgAJNB15O6WVGjwiRWAgas,96
|
|
44
|
+
xcpcio-0.64.3.dist-info/RECORD,,
|
xcpcio/ccs/__init__.py
DELETED
xcpcio/ccs/reader/__init__.py
DELETED
|
File without changes
|
xcpcio-0.64.2.dist-info/RECORD
DELETED
|
@@ -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,,
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|