xcpcio 0.63.6__tar.gz → 0.64.0__tar.gz
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-0.63.6 → xcpcio-0.64.0}/PKG-INFO +2 -1
- xcpcio-0.64.0/app/contest_api_server.py +140 -0
- xcpcio-0.64.0/cli/ccs_archiver_cli.py +221 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/pyproject.toml +1 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/uv.lock +2 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/__version__.py +1 -1
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/ccs/api_server/dependencies.py +8 -3
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/ccs/api_server/routes/__init__.py +2 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/ccs/api_server/routes/access.py +2 -4
- xcpcio-0.64.0/xcpcio/ccs/api_server/routes/accounts.py +35 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/ccs/api_server/routes/awards.py +4 -9
- xcpcio-0.64.0/xcpcio/ccs/api_server/routes/clarifications.py +34 -0
- xcpcio-0.64.0/xcpcio/ccs/api_server/routes/contests.py +90 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/ccs/api_server/routes/general.py +5 -4
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/ccs/api_server/routes/groups.py +6 -13
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/ccs/api_server/routes/judgement_types.py +6 -13
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/ccs/api_server/routes/judgements.py +4 -9
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/ccs/api_server/routes/languages.py +6 -13
- xcpcio-0.64.0/xcpcio/ccs/api_server/routes/organizations.py +50 -0
- xcpcio-0.64.0/xcpcio/ccs/api_server/routes/problems.py +50 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/ccs/api_server/routes/runs.py +4 -9
- xcpcio-0.64.0/xcpcio/ccs/api_server/routes/submissions.py +50 -0
- xcpcio-0.64.0/xcpcio/ccs/api_server/routes/teams.py +50 -0
- xcpcio-0.64.0/xcpcio/ccs/api_server/services/contest_service.py +194 -0
- xcpcio-0.64.0/xcpcio/ccs/base/__init__.py +3 -0
- xcpcio-0.64.0/xcpcio/ccs/base/types.py +9 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/ccs/contest_archiver.py +2 -3
- xcpcio-0.64.0/xcpcio/ccs/reader/__init__.py +0 -0
- xcpcio-0.64.0/xcpcio/ccs/reader/base_ccs_reader.py +165 -0
- xcpcio-0.64.0/xcpcio/ccs/reader/contest_package_reader.py +331 -0
- xcpcio-0.63.6/app/contest_api_server.py +0 -77
- xcpcio-0.63.6/cli/ccs_archiver_cli.py +0 -124
- xcpcio-0.63.6/task.md +0 -22
- xcpcio-0.63.6/xcpcio/ccs/api_server/routes/clarifications.py +0 -42
- xcpcio-0.63.6/xcpcio/ccs/api_server/routes/contests.py +0 -74
- xcpcio-0.63.6/xcpcio/ccs/api_server/routes/organizations.py +0 -82
- xcpcio-0.63.6/xcpcio/ccs/api_server/routes/problems.py +0 -77
- xcpcio-0.63.6/xcpcio/ccs/api_server/routes/submissions.py +0 -82
- xcpcio-0.63.6/xcpcio/ccs/api_server/routes/teams.py +0 -82
- xcpcio-0.63.6/xcpcio/ccs/api_server/services/contest_service.py +0 -408
- {xcpcio-0.63.6 → xcpcio-0.64.0}/.gitignore +0 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/.python-version +0 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/README.md +0 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/scripts/generate_ccs_models.sh +0 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/tests/__init__.py +0 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/tests/test_contest.py +0 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/tests/test_submission.py +0 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/tests/test_team.py +0 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/tests/test_types.py +0 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/__init__.py +0 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/ccs/__init__.py +0 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/ccs/api_server/__init__.py +0 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/ccs/api_server/server.py +0 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/ccs/api_server/services/__init__.py +0 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/ccs/model/__init__.py +0 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/ccs/model/model_2023_06/__init__.py +0 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/ccs/model/model_2023_06/model.py +0 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/constants.py +0 -0
- {xcpcio-0.63.6 → xcpcio-0.64.0}/xcpcio/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xcpcio
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.64.0
|
|
4
4
|
Summary: xcpcio python lib
|
|
5
5
|
Project-URL: homepage, https://github.com/xcpcio/xcpcio
|
|
6
6
|
Project-URL: documentation, https://github.com/xcpcio/xcpcio
|
|
@@ -25,6 +25,7 @@ Requires-Dist: pyyaml>=6.0.0
|
|
|
25
25
|
Requires-Dist: semver>=3.0.0
|
|
26
26
|
Requires-Dist: tenacity>=8.0.0
|
|
27
27
|
Requires-Dist: uvicorn>=0.36.0
|
|
28
|
+
Requires-Dist: zstandard>=0.25.0
|
|
28
29
|
Description-Content-Type: text/markdown
|
|
29
30
|
|
|
30
31
|
# xcpcio-python
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import logging
|
|
3
|
+
import shutil
|
|
4
|
+
import tarfile
|
|
5
|
+
import tempfile
|
|
6
|
+
import zipfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
import zstandard as zstd
|
|
12
|
+
|
|
13
|
+
from xcpcio import __version__
|
|
14
|
+
from xcpcio.ccs.api_server.server import ContestAPIServer
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def setup_logging(level: str = "INFO"):
|
|
18
|
+
"""Setup logging configuration"""
|
|
19
|
+
logging.basicConfig(
|
|
20
|
+
level=getattr(logging, level.upper()),
|
|
21
|
+
format="%(asctime)s [%(name)s] %(filename)s:%(lineno)d %(levelname)s: %(message)s",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def extract_archive(archive_path: Path, dest_dir: Path) -> None:
|
|
26
|
+
"""Extract archive to destination directory"""
|
|
27
|
+
archive_str = str(archive_path)
|
|
28
|
+
|
|
29
|
+
if archive_str.endswith(".zip"):
|
|
30
|
+
with zipfile.ZipFile(archive_path, "r") as zipf:
|
|
31
|
+
zipf.extractall(dest_dir)
|
|
32
|
+
elif archive_str.endswith(".tar.gz"):
|
|
33
|
+
with tarfile.open(archive_path, "r:gz") as tarf:
|
|
34
|
+
tarf.extractall(dest_dir, filter="data")
|
|
35
|
+
elif archive_str.endswith(".tar.zst"):
|
|
36
|
+
with tempfile.NamedTemporaryFile(suffix=".tar") as tmp_tar:
|
|
37
|
+
with open(archive_path, "rb") as f:
|
|
38
|
+
dctx = zstd.ZstdDecompressor()
|
|
39
|
+
dctx.copy_stream(f, tmp_tar)
|
|
40
|
+
tmp_tar.seek(0)
|
|
41
|
+
with tarfile.open(fileobj=tmp_tar, mode="r") as tarf:
|
|
42
|
+
tarf.extractall(dest_dir, filter="data")
|
|
43
|
+
else:
|
|
44
|
+
raise ValueError(f"Unsupported archive format: {archive_path}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@click.command()
|
|
48
|
+
@click.version_option(__version__)
|
|
49
|
+
@click.option(
|
|
50
|
+
"--contest-package",
|
|
51
|
+
"-p",
|
|
52
|
+
required=True,
|
|
53
|
+
type=click.Path(exists=True, path_type=Path),
|
|
54
|
+
help="Contest package directory or archive file (.zip, .tar.gz, .tar.zst)",
|
|
55
|
+
)
|
|
56
|
+
@click.option("--host", default="0.0.0.0", help="Host to bind to")
|
|
57
|
+
@click.option("--port", default=8000, type=int, help="Port to bind to")
|
|
58
|
+
@click.option("--reload", is_flag=True, help="Enable auto-reload for development")
|
|
59
|
+
@click.option(
|
|
60
|
+
"--log-level",
|
|
61
|
+
default="info",
|
|
62
|
+
type=click.Choice(["debug", "info", "warning", "error", "critical"], case_sensitive=False),
|
|
63
|
+
help="Log level",
|
|
64
|
+
)
|
|
65
|
+
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging (same as --log-level debug)")
|
|
66
|
+
def main(contest_package: Path, host: str, port: int, reload: bool, log_level: str, verbose: bool):
|
|
67
|
+
"""
|
|
68
|
+
Start the Contest API Server.
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
|
|
72
|
+
# Start server with contest directory
|
|
73
|
+
contest-api-server -p /path/to/contest
|
|
74
|
+
|
|
75
|
+
# Start server with archive file
|
|
76
|
+
contest-api-server -p /path/to/contest.zip
|
|
77
|
+
contest-api-server -p /path/to/contest.tar.gz
|
|
78
|
+
contest-api-server -p /path/to/contest.tar.zst
|
|
79
|
+
|
|
80
|
+
# Custom host and port
|
|
81
|
+
contest-api-server -p /path/to/contest --host 127.0.0.1 --port 9000
|
|
82
|
+
|
|
83
|
+
# Enable reload for development
|
|
84
|
+
contest-api-server -p /path/to/contest --reload
|
|
85
|
+
"""
|
|
86
|
+
if verbose:
|
|
87
|
+
log_level = "debug"
|
|
88
|
+
setup_logging(log_level.upper())
|
|
89
|
+
|
|
90
|
+
temp_dir: Optional[Path] = None
|
|
91
|
+
|
|
92
|
+
def cleanup_temp_dir():
|
|
93
|
+
if temp_dir and temp_dir.exists():
|
|
94
|
+
click.echo(f"Cleaning up temporary directory: {temp_dir}")
|
|
95
|
+
shutil.rmtree(temp_dir)
|
|
96
|
+
|
|
97
|
+
atexit.register(cleanup_temp_dir)
|
|
98
|
+
|
|
99
|
+
actual_contest_dir: Path = contest_package
|
|
100
|
+
is_archive = str(contest_package).endswith((".zip", ".tar.gz", ".tar.zst"))
|
|
101
|
+
|
|
102
|
+
if is_archive:
|
|
103
|
+
if not contest_package.is_file():
|
|
104
|
+
click.echo(f"Error: Archive file not found: {contest_package}", err=True)
|
|
105
|
+
raise click.Abort()
|
|
106
|
+
|
|
107
|
+
click.echo(f"Extracting archive: {contest_package}")
|
|
108
|
+
temp_dir = Path(tempfile.mkdtemp(prefix="ccs_package_"))
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
extract_archive(contest_package, temp_dir)
|
|
112
|
+
actual_contest_dir = temp_dir
|
|
113
|
+
click.echo(f"Archive extracted to: {temp_dir}")
|
|
114
|
+
except Exception as e:
|
|
115
|
+
click.echo(f"Error extracting archive: {e}", err=True)
|
|
116
|
+
raise click.Abort()
|
|
117
|
+
else:
|
|
118
|
+
if not contest_package.is_dir():
|
|
119
|
+
click.echo(f"Error: Contest directory not found: {contest_package}", err=True)
|
|
120
|
+
raise click.Abort()
|
|
121
|
+
|
|
122
|
+
click.echo("Starting Contest API Server...")
|
|
123
|
+
click.echo(f"Contest directory: {actual_contest_dir}")
|
|
124
|
+
click.echo(f"Host: {host}")
|
|
125
|
+
click.echo(f"Port: {port}")
|
|
126
|
+
click.echo(f"Reload: {reload}")
|
|
127
|
+
click.echo(f"Log level: {log_level}")
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
server = ContestAPIServer(actual_contest_dir)
|
|
131
|
+
server.run(host=host, port=port, reload=reload, log_level=log_level.lower())
|
|
132
|
+
except KeyboardInterrupt:
|
|
133
|
+
click.echo("\nServer stopped by user")
|
|
134
|
+
except Exception as e:
|
|
135
|
+
click.echo(f"Error starting server: {e}", err=True)
|
|
136
|
+
raise click.Abort()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
main()
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import atexit
|
|
3
|
+
import hashlib
|
|
4
|
+
import logging
|
|
5
|
+
import shutil
|
|
6
|
+
import tarfile
|
|
7
|
+
import tempfile
|
|
8
|
+
import zipfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
import zstandard as zstd
|
|
14
|
+
|
|
15
|
+
from xcpcio import __version__
|
|
16
|
+
from xcpcio.ccs.contest_archiver import APICredentials, ArchiveConfig, ContestArchiver
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def setup_logging(level: str = "INFO"):
|
|
20
|
+
"""Setup logging configuration"""
|
|
21
|
+
logging.basicConfig(
|
|
22
|
+
level=getattr(logging, level.upper()),
|
|
23
|
+
format="%(asctime)s [%(name)s] %(filename)s:%(lineno)d %(levelname)s: %(message)s",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def calculate_checksums(file_path: Path) -> dict:
|
|
28
|
+
"""Calculate MD5, SHA1, SHA256, SHA512 checksums for a file"""
|
|
29
|
+
md5 = hashlib.md5()
|
|
30
|
+
sha1 = hashlib.sha1()
|
|
31
|
+
sha256 = hashlib.sha256()
|
|
32
|
+
sha512 = hashlib.sha512()
|
|
33
|
+
|
|
34
|
+
with open(file_path, "rb") as f:
|
|
35
|
+
while chunk := f.read(8192):
|
|
36
|
+
md5.update(chunk)
|
|
37
|
+
sha1.update(chunk)
|
|
38
|
+
sha256.update(chunk)
|
|
39
|
+
sha512.update(chunk)
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
"md5": md5.hexdigest(),
|
|
43
|
+
"sha1": sha1.hexdigest(),
|
|
44
|
+
"sha256": sha256.hexdigest(),
|
|
45
|
+
"sha512": sha512.hexdigest(),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@click.command()
|
|
50
|
+
@click.version_option(__version__)
|
|
51
|
+
@click.option("--base-url", required=True, help="Base URL of the CCS API (e.g., https://example.com/api)")
|
|
52
|
+
@click.option("--contest-id", required=True, help="Contest ID to dump")
|
|
53
|
+
@click.option(
|
|
54
|
+
"--output",
|
|
55
|
+
"-o",
|
|
56
|
+
required=True,
|
|
57
|
+
type=click.Path(path_type=Path),
|
|
58
|
+
help="Output path: directory, or archive file (.zip, .tar.gz, .tar.zst)",
|
|
59
|
+
)
|
|
60
|
+
@click.option("--username", "-u", help="HTTP Basic Auth username")
|
|
61
|
+
@click.option("--password", "-p", help="HTTP Basic Auth password")
|
|
62
|
+
@click.option("--token", "-t", help="Bearer token for authentication")
|
|
63
|
+
@click.option("--no-files", is_flag=True, help="Skip downloading files")
|
|
64
|
+
@click.option("--no-event-feed", is_flag=True, help="Skip dump event-feed(large aggregated data)")
|
|
65
|
+
@click.option("--endpoints", "-e", multiple=True, help="Specific endpoints to dump (can be used multiple times)")
|
|
66
|
+
@click.option("--timeout", default=30, type=int, help="Request timeout in seconds")
|
|
67
|
+
@click.option("--max-concurrent", default=10, type=int, help="Max concurrent requests")
|
|
68
|
+
@click.option(
|
|
69
|
+
"--log-level",
|
|
70
|
+
default="INFO",
|
|
71
|
+
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"], case_sensitive=False),
|
|
72
|
+
help="Log level",
|
|
73
|
+
)
|
|
74
|
+
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging (same as --log-level DEBUG)")
|
|
75
|
+
def main(
|
|
76
|
+
base_url: str,
|
|
77
|
+
contest_id: str,
|
|
78
|
+
output: Path,
|
|
79
|
+
username: Optional[str],
|
|
80
|
+
password: Optional[str],
|
|
81
|
+
token: Optional[str],
|
|
82
|
+
no_files: bool,
|
|
83
|
+
no_event_feed: bool,
|
|
84
|
+
endpoints: tuple,
|
|
85
|
+
timeout: int,
|
|
86
|
+
max_concurrent: int,
|
|
87
|
+
log_level: str,
|
|
88
|
+
verbose: bool,
|
|
89
|
+
):
|
|
90
|
+
"""
|
|
91
|
+
Archive CCS Contest API data to contest package format.
|
|
92
|
+
|
|
93
|
+
This tool fetches contest data from a CCS API and organizes it into the standard
|
|
94
|
+
contest package format as specified by the CCS Contest Package specification.
|
|
95
|
+
|
|
96
|
+
Examples:
|
|
97
|
+
|
|
98
|
+
# Output to directory
|
|
99
|
+
ccs-archiver --base-url https://api.example.com/api --contest-id contest123 -o ./output -u admin -p secret
|
|
100
|
+
|
|
101
|
+
# Output to ZIP archive
|
|
102
|
+
ccs-archiver --base-url https://api.example.com/api --contest-id contest123 -o contest.zip --token abc123
|
|
103
|
+
|
|
104
|
+
# Output to tar.gz archive
|
|
105
|
+
ccs-archiver --base-url https://api.example.com/api --contest-id contest123 -o contest.tar.gz -u admin -p secret
|
|
106
|
+
|
|
107
|
+
# Output to tar.zst archive
|
|
108
|
+
ccs-archiver --base-url https://api.example.com/api --contest-id contest123 -o contest.tar.zst -u admin -p secret
|
|
109
|
+
|
|
110
|
+
# Only archive specific endpoints
|
|
111
|
+
ccs-archiver --base-url https://api.example.com/api --contest-id contest123 -o ./output -u admin -p secret -e teams -e problems
|
|
112
|
+
|
|
113
|
+
# Skip file downloads
|
|
114
|
+
ccs-archiver --base-url https://api.example.com/api --contest-id contest123 -o ./output --no-files
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
if verbose:
|
|
118
|
+
log_level = "DEBUG"
|
|
119
|
+
|
|
120
|
+
setup_logging(log_level)
|
|
121
|
+
|
|
122
|
+
if not (username and password) and not token:
|
|
123
|
+
click.echo("Warning: No authentication provided. Some endpoints may not be accessible.", err=True)
|
|
124
|
+
|
|
125
|
+
output_str = str(output)
|
|
126
|
+
is_archive = output_str.endswith((".zip", ".tar.gz", ".tar.zst"))
|
|
127
|
+
archive_format = None
|
|
128
|
+
temp_dir: Optional[Path] = None
|
|
129
|
+
|
|
130
|
+
def cleanup_temp_dir():
|
|
131
|
+
if temp_dir and temp_dir.exists():
|
|
132
|
+
click.echo(f"Cleaning up temporary directory: {temp_dir}")
|
|
133
|
+
shutil.rmtree(temp_dir)
|
|
134
|
+
|
|
135
|
+
atexit.register(cleanup_temp_dir)
|
|
136
|
+
|
|
137
|
+
if is_archive:
|
|
138
|
+
if output_str.endswith(".zip"):
|
|
139
|
+
archive_format = "zip"
|
|
140
|
+
elif output_str.endswith(".tar.gz"):
|
|
141
|
+
archive_format = "tar.gz"
|
|
142
|
+
elif output_str.endswith(".tar.zst"):
|
|
143
|
+
archive_format = "tar.zst"
|
|
144
|
+
|
|
145
|
+
credentials = APICredentials(username=username, password=password, token=token)
|
|
146
|
+
|
|
147
|
+
if is_archive:
|
|
148
|
+
temp_dir = Path(tempfile.mkdtemp(prefix="ccs_archive_"))
|
|
149
|
+
output_dir = temp_dir
|
|
150
|
+
else:
|
|
151
|
+
output_dir = output
|
|
152
|
+
|
|
153
|
+
config = ArchiveConfig(
|
|
154
|
+
base_url=base_url.rstrip("/"),
|
|
155
|
+
contest_id=contest_id,
|
|
156
|
+
credentials=credentials,
|
|
157
|
+
output_dir=output_dir,
|
|
158
|
+
include_files=not no_files,
|
|
159
|
+
endpoints=list(endpoints) if endpoints else None,
|
|
160
|
+
timeout=timeout,
|
|
161
|
+
max_concurrent=max_concurrent,
|
|
162
|
+
include_event_feed=not no_event_feed,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
click.echo(f"Archiving contest '{contest_id}' from {base_url}")
|
|
166
|
+
if is_archive:
|
|
167
|
+
click.echo(f"Output archive: {output} (format: {archive_format})")
|
|
168
|
+
else:
|
|
169
|
+
click.echo(f"Output directory: {output}")
|
|
170
|
+
if config.endpoints:
|
|
171
|
+
click.echo(f"Endpoints: {', '.join(config.endpoints)}")
|
|
172
|
+
|
|
173
|
+
async def run_archive():
|
|
174
|
+
async with ContestArchiver(config) as archiver:
|
|
175
|
+
await archiver.dump_all()
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
asyncio.run(run_archive())
|
|
179
|
+
|
|
180
|
+
if is_archive:
|
|
181
|
+
click.echo(f"Creating {archive_format} archive...")
|
|
182
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
|
|
184
|
+
if archive_format == "zip":
|
|
185
|
+
with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as zipf:
|
|
186
|
+
for file_path in output_dir.rglob("*"):
|
|
187
|
+
if file_path.is_file():
|
|
188
|
+
arcname = file_path.relative_to(output_dir)
|
|
189
|
+
zipf.write(file_path, arcname)
|
|
190
|
+
|
|
191
|
+
elif archive_format == "tar.gz":
|
|
192
|
+
with tarfile.open(output, "w:gz") as tarf:
|
|
193
|
+
tarf.add(output_dir, arcname=".")
|
|
194
|
+
|
|
195
|
+
elif archive_format == "tar.zst":
|
|
196
|
+
with open(output, "wb") as f:
|
|
197
|
+
cctx = zstd.ZstdCompressor()
|
|
198
|
+
with cctx.stream_writer(f) as compressor:
|
|
199
|
+
with tarfile.open(fileobj=compressor, mode="w") as tarf:
|
|
200
|
+
tarf.add(output_dir, arcname=".")
|
|
201
|
+
|
|
202
|
+
click.echo(click.style(f"Contest package created successfully: {output}", fg="green"))
|
|
203
|
+
click.echo("\nChecksums:")
|
|
204
|
+
checksums = calculate_checksums(output)
|
|
205
|
+
click.echo(f" MD5: {checksums['md5']}")
|
|
206
|
+
click.echo(f" SHA1: {checksums['sha1']}")
|
|
207
|
+
click.echo(f" SHA256: {checksums['sha256']}")
|
|
208
|
+
click.echo(f" SHA512: {checksums['sha512']}")
|
|
209
|
+
else:
|
|
210
|
+
click.echo(click.style(f"Contest package created successfully in: {config.output_dir}", fg="green"))
|
|
211
|
+
|
|
212
|
+
except KeyboardInterrupt:
|
|
213
|
+
click.echo(click.style("Archive interrupted by user", fg="yellow"), err=True)
|
|
214
|
+
raise click.Abort()
|
|
215
|
+
except Exception as e:
|
|
216
|
+
click.echo(click.style(f"Error during archive: {e}", fg="red"), err=True)
|
|
217
|
+
raise click.ClickException(str(e))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
main()
|
|
@@ -1408,6 +1408,7 @@ dependencies = [
|
|
|
1408
1408
|
{ name = "semver" },
|
|
1409
1409
|
{ name = "tenacity" },
|
|
1410
1410
|
{ name = "uvicorn" },
|
|
1411
|
+
{ name = "zstandard" },
|
|
1411
1412
|
]
|
|
1412
1413
|
|
|
1413
1414
|
[package.dev-dependencies]
|
|
@@ -1429,6 +1430,7 @@ requires-dist = [
|
|
|
1429
1430
|
{ name = "semver", specifier = ">=3.0.0" },
|
|
1430
1431
|
{ name = "tenacity", specifier = ">=8.0.0" },
|
|
1431
1432
|
{ name = "uvicorn", specifier = ">=0.36.0" },
|
|
1433
|
+
{ name = "zstandard", specifier = ">=0.25.0" },
|
|
1432
1434
|
]
|
|
1433
1435
|
|
|
1434
1436
|
[package.metadata.requires-dev]
|
|
@@ -5,13 +5,15 @@ Dependency injection system for Contest API Server.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Annotated
|
|
8
|
+
from typing import Annotated, Dict
|
|
9
9
|
|
|
10
10
|
from fastapi import Depends
|
|
11
11
|
|
|
12
|
+
from xcpcio.ccs.reader.base_ccs_reader import BaseCCSReader
|
|
13
|
+
from xcpcio.ccs.reader.contest_package_reader import ContestPackageReader
|
|
14
|
+
|
|
12
15
|
from .services.contest_service import ContestService
|
|
13
16
|
|
|
14
|
-
# Global contest service instance cache
|
|
15
17
|
_contest_service_instance = None
|
|
16
18
|
|
|
17
19
|
|
|
@@ -41,7 +43,10 @@ def configure_dependencies(contest_package_dir: Path) -> None:
|
|
|
41
43
|
contest_package_dir: Path to contest package directory
|
|
42
44
|
"""
|
|
43
45
|
global _contest_service_instance
|
|
44
|
-
|
|
46
|
+
reader_dict: Dict[str, BaseCCSReader] = {}
|
|
47
|
+
contest_package_reader = ContestPackageReader(contest_package_dir)
|
|
48
|
+
reader_dict[contest_package_reader.get_contest_id()] = contest_package_reader
|
|
49
|
+
_contest_service_instance = ContestService(reader_dict)
|
|
45
50
|
|
|
46
51
|
|
|
47
52
|
# Type alias for dependency injection
|
|
@@ -8,6 +8,7 @@ from fastapi import APIRouter
|
|
|
8
8
|
|
|
9
9
|
from . import (
|
|
10
10
|
access,
|
|
11
|
+
accounts,
|
|
11
12
|
awards,
|
|
12
13
|
clarifications,
|
|
13
14
|
contests,
|
|
@@ -30,6 +31,7 @@ def create_router() -> APIRouter:
|
|
|
30
31
|
|
|
31
32
|
# Include all route modules
|
|
32
33
|
router.include_router(access.router, tags=["Access"])
|
|
34
|
+
router.include_router(accounts.router, tags=["Accounts"])
|
|
33
35
|
router.include_router(awards.router, tags=["Awards"])
|
|
34
36
|
router.include_router(clarifications.router, tags=["Clarifications"])
|
|
35
37
|
router.include_router(contests.router, tags=["Contests"])
|
|
@@ -11,10 +11,8 @@ logger = logging.getLogger(__name__)
|
|
|
11
11
|
|
|
12
12
|
@router.get(
|
|
13
13
|
"/contests/{contest_id}/access",
|
|
14
|
-
summary="Get
|
|
15
|
-
description="Get access capabilities and visible endpoints for current client",
|
|
14
|
+
summary="Get access information",
|
|
16
15
|
response_model=Dict[str, Any],
|
|
17
16
|
)
|
|
18
17
|
async def get_access(contest_id: str, service: ContestServiceDep) -> Dict[str, Any]:
|
|
19
|
-
|
|
20
|
-
return service.get_access_info(contest_id)
|
|
18
|
+
return service.get_access(contest_id)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, Dict, List
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter
|
|
5
|
+
from fastapi import Path as FastAPIPath
|
|
6
|
+
|
|
7
|
+
from ..dependencies import ContestServiceDep
|
|
8
|
+
|
|
9
|
+
router = APIRouter()
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get(
|
|
14
|
+
"/contests/{contest_id}/accounts",
|
|
15
|
+
summary="Get all the accounts",
|
|
16
|
+
response_model=List[Dict[str, Any]],
|
|
17
|
+
)
|
|
18
|
+
async def get_accounts(
|
|
19
|
+
contest_id: str = FastAPIPath(..., description="Contest identifier"),
|
|
20
|
+
service: ContestServiceDep = None,
|
|
21
|
+
) -> List[Dict[str, Any]]:
|
|
22
|
+
return service.get_accounts(contest_id)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.get(
|
|
26
|
+
"/contests/{contest_id}/accounts/{account_id}",
|
|
27
|
+
summary="Get the given account",
|
|
28
|
+
response_model=Dict[str, Any],
|
|
29
|
+
)
|
|
30
|
+
async def get_account(
|
|
31
|
+
contest_id: str = FastAPIPath(..., description="Contest identifier"),
|
|
32
|
+
account_id: str = FastAPIPath(..., description="Account identifier"),
|
|
33
|
+
service: ContestServiceDep = None,
|
|
34
|
+
) -> Dict[str, Any]:
|
|
35
|
+
return service.get_account(contest_id, account_id)
|
|
@@ -3,7 +3,6 @@ from typing import Any, Dict, List
|
|
|
3
3
|
|
|
4
4
|
from fastapi import APIRouter, Path
|
|
5
5
|
|
|
6
|
-
from ...model import Award, Awards
|
|
7
6
|
from ..dependencies import ContestServiceDep
|
|
8
7
|
|
|
9
8
|
router = APIRouter()
|
|
@@ -12,27 +11,23 @@ logger = logging.getLogger(__name__)
|
|
|
12
11
|
|
|
13
12
|
@router.get(
|
|
14
13
|
"/contests/{contest_id}/awards",
|
|
15
|
-
summary="Get
|
|
16
|
-
|
|
17
|
-
response_model=Awards,
|
|
14
|
+
summary="Get all the awards standings for this contest",
|
|
15
|
+
response_model=List[Dict[str, Any]],
|
|
18
16
|
)
|
|
19
17
|
async def get_awards(
|
|
20
18
|
contest_id: str = Path(..., description="Contest identifier"), service: ContestServiceDep = None
|
|
21
19
|
) -> List[Dict[str, Any]]:
|
|
22
|
-
"""Get all awards"""
|
|
23
20
|
return service.get_awards(contest_id)
|
|
24
21
|
|
|
25
22
|
|
|
26
23
|
@router.get(
|
|
27
24
|
"/contests/{contest_id}/awards/{award_id}",
|
|
28
|
-
summary="Get
|
|
29
|
-
|
|
30
|
-
response_model=Award,
|
|
25
|
+
summary="Get the specific award for this contest",
|
|
26
|
+
response_model=Dict[str, Any],
|
|
31
27
|
)
|
|
32
28
|
async def get_award(
|
|
33
29
|
contest_id: str = Path(..., description="Contest identifier"),
|
|
34
30
|
award_id: str = Path(..., description="Award identifier"),
|
|
35
31
|
service: ContestServiceDep = None,
|
|
36
32
|
) -> Dict[str, Any]:
|
|
37
|
-
"""Get specific award information"""
|
|
38
33
|
return service.get_award(contest_id, award_id)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, Dict, List
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, Path
|
|
5
|
+
|
|
6
|
+
from ..dependencies import ContestServiceDep
|
|
7
|
+
|
|
8
|
+
router = APIRouter()
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@router.get(
|
|
13
|
+
"/contests/{contest_id}/clarifications",
|
|
14
|
+
summary="Get all the clarifications for this contest",
|
|
15
|
+
response_model=List[Dict[str, Any]],
|
|
16
|
+
)
|
|
17
|
+
async def get_clarifications(
|
|
18
|
+
contest_id: str = Path(..., description="Contest identifier"),
|
|
19
|
+
service: ContestServiceDep = None,
|
|
20
|
+
) -> List[Dict[str, Any]]:
|
|
21
|
+
return service.get_clarifications(contest_id)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@router.get(
|
|
25
|
+
"/contests/{contest_id}/clarifications/{clarification_id}",
|
|
26
|
+
summary="Get the given clarifications for this contest",
|
|
27
|
+
response_model=Dict[str, Any],
|
|
28
|
+
)
|
|
29
|
+
async def get_clarification(
|
|
30
|
+
contest_id: str = Path(..., description="Contest identifier"),
|
|
31
|
+
clarification_id: str = Path(..., description="Clarification identifier"),
|
|
32
|
+
service: ContestServiceDep = None,
|
|
33
|
+
) -> Dict[str, Any]:
|
|
34
|
+
return service.get_clarification(contest_id, clarification_id)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Query
|
|
6
|
+
from fastapi import Path as FastAPIPath
|
|
7
|
+
from fastapi.responses import FileResponse, StreamingResponse
|
|
8
|
+
|
|
9
|
+
from ..dependencies import ContestServiceDep
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@router.get(
|
|
16
|
+
"/contests",
|
|
17
|
+
summary="Get all the contests",
|
|
18
|
+
response_model=List[Dict[str, Any]],
|
|
19
|
+
)
|
|
20
|
+
async def get_contests(service: ContestServiceDep) -> List[Dict[str, Any]]:
|
|
21
|
+
return service.get_contests()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@router.get(
|
|
25
|
+
"/contests/{contest_id}",
|
|
26
|
+
summary="Get the given contest",
|
|
27
|
+
response_model=Dict[str, Any],
|
|
28
|
+
)
|
|
29
|
+
async def get_contest(
|
|
30
|
+
contest_id: str = FastAPIPath(..., description="Contest identifier"),
|
|
31
|
+
service: ContestServiceDep = None,
|
|
32
|
+
) -> Dict[str, Any]:
|
|
33
|
+
return service.get_contest(contest_id)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@router.get(
|
|
37
|
+
"/contests/{contest_id}/state",
|
|
38
|
+
summary="Get the current contest state",
|
|
39
|
+
response_model=Dict[str, Any],
|
|
40
|
+
)
|
|
41
|
+
async def get_state(
|
|
42
|
+
contest_id: str = FastAPIPath(..., description="Contest identifier"),
|
|
43
|
+
service: ContestServiceDep = None,
|
|
44
|
+
) -> Dict[str, Any]:
|
|
45
|
+
return service.get_contest_state(contest_id)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@router.get(
|
|
49
|
+
"/contests/{contest_id}/banner",
|
|
50
|
+
summary="Get the banner for the given contest",
|
|
51
|
+
response_class=FileResponse,
|
|
52
|
+
)
|
|
53
|
+
async def get_contest_banner(
|
|
54
|
+
contest_id: str = FastAPIPath(..., description="Contest identifier"),
|
|
55
|
+
service: ContestServiceDep = None,
|
|
56
|
+
) -> FileResponse:
|
|
57
|
+
file_attr = service.get_contest_banner(contest_id)
|
|
58
|
+
return FileResponse(path=file_attr.path, media_type=file_attr.media_type, filename=file_attr.name)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@router.get(
|
|
62
|
+
"/contests/{contest_id}/problemset",
|
|
63
|
+
summary="Get the problemset document for the given contest",
|
|
64
|
+
response_class=FileResponse,
|
|
65
|
+
)
|
|
66
|
+
async def get_contest_problem_set(
|
|
67
|
+
contest_id: str = FastAPIPath(..., description="Contest identifier"),
|
|
68
|
+
service: ContestServiceDep = None,
|
|
69
|
+
) -> FileResponse:
|
|
70
|
+
file_attr = service.get_contest_problemset(contest_id)
|
|
71
|
+
return FileResponse(path=file_attr.path, media_type=file_attr.media_type, filename=file_attr.name)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@router.get(
|
|
75
|
+
"/contests/{contest_id}/event-feed",
|
|
76
|
+
summary="Get event feed for contest",
|
|
77
|
+
description="Get events for the contest in NDJSON format. Each line contains a single event object.",
|
|
78
|
+
)
|
|
79
|
+
async def get_event_feed(
|
|
80
|
+
contest_id: str = FastAPIPath(..., description="Contest identifier"),
|
|
81
|
+
stream: bool = Query(False, description="Whether to stream the output or stop immediately"),
|
|
82
|
+
since_token: Optional[str] = Query(None, description="Return events after this token"),
|
|
83
|
+
service: ContestServiceDep = None,
|
|
84
|
+
) -> StreamingResponse:
|
|
85
|
+
async def generate():
|
|
86
|
+
events = service.get_event_feed(contest_id, since_token)
|
|
87
|
+
for event in events:
|
|
88
|
+
yield json.dumps(event, ensure_ascii=False, separators=(",", ":")) + "\n"
|
|
89
|
+
|
|
90
|
+
return StreamingResponse(generate(), media_type="application/x-ndjson")
|