xcpcio 0.63.7__tar.gz → 0.64.1__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.64.1/PKG-INFO +86 -0
- xcpcio-0.64.1/README.md +56 -0
- xcpcio-0.64.1/app/contest_api_server.py +140 -0
- xcpcio-0.64.1/cli/ccs_archiver_cli.py +221 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/pyproject.toml +1 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/uv.lock +2 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/__version__.py +1 -1
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/dependencies.py +8 -3
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/contests.py +5 -34
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/organizations.py +3 -24
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/problems.py +3 -22
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/submissions.py +3 -24
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/teams.py +3 -22
- xcpcio-0.64.1/xcpcio/ccs/api_server/services/contest_service.py +194 -0
- xcpcio-0.64.1/xcpcio/ccs/base/__init__.py +3 -0
- xcpcio-0.64.1/xcpcio/ccs/base/types.py +9 -0
- xcpcio-0.64.1/xcpcio/ccs/reader/__init__.py +0 -0
- xcpcio-0.64.1/xcpcio/ccs/reader/base_ccs_reader.py +165 -0
- xcpcio-0.64.1/xcpcio/ccs/reader/contest_package_reader.py +331 -0
- xcpcio-0.63.7/PKG-INFO +0 -30
- xcpcio-0.63.7/README.md +0 -1
- xcpcio-0.63.7/app/contest_api_server.py +0 -77
- xcpcio-0.63.7/cli/ccs_archiver_cli.py +0 -124
- xcpcio-0.63.7/task.md +0 -22
- xcpcio-0.63.7/xcpcio/ccs/api_server/services/contest_service.py +0 -325
- {xcpcio-0.63.7 → xcpcio-0.64.1}/.gitignore +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/.python-version +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/scripts/generate_ccs_models.sh +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/tests/__init__.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/tests/test_contest.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/tests/test_submission.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/tests/test_team.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/tests/test_types.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/__init__.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/__init__.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/__init__.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/__init__.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/access.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/accounts.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/awards.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/clarifications.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/general.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/groups.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/judgement_types.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/judgements.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/languages.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/runs.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/server.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/services/__init__.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/contest_archiver.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/model/__init__.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/model/model_2023_06/__init__.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/model/model_2023_06/model.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/constants.py +0 -0
- {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/types.py +0 -0
xcpcio-0.64.1/PKG-INFO
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xcpcio
|
|
3
|
+
Version: 0.64.1
|
|
4
|
+
Summary: xcpcio python lib
|
|
5
|
+
Project-URL: homepage, https://github.com/xcpcio/xcpcio
|
|
6
|
+
Project-URL: documentation, https://github.com/xcpcio/xcpcio
|
|
7
|
+
Project-URL: repository, https://github.com/xcpcio/xcpcio
|
|
8
|
+
Author-email: Dup4 <hi@dup4.com>
|
|
9
|
+
Maintainer-email: Dup4 <hi@dup4.com>, cubercsl <hi@cubercsl.site>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: xcpcio
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: aiofiles>=23.0.0
|
|
20
|
+
Requires-Dist: aiohttp>=3.8.0
|
|
21
|
+
Requires-Dist: click>=8.0.0
|
|
22
|
+
Requires-Dist: fastapi>=0.117.1
|
|
23
|
+
Requires-Dist: pydantic>=2.11.7
|
|
24
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
25
|
+
Requires-Dist: semver>=3.0.0
|
|
26
|
+
Requires-Dist: tenacity>=8.0.0
|
|
27
|
+
Requires-Dist: uvicorn>=0.36.0
|
|
28
|
+
Requires-Dist: zstandard>=0.25.0
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# xcpcio-python
|
|
32
|
+
|
|
33
|
+
Python library and CLI tools for XCPCIO.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **Type Definitions**: Pydantic models for contest data structures (teams, submissions, problems, etc.)
|
|
38
|
+
- **Constants**: Shared constants for submission statuses, time units, and penalty calculations
|
|
39
|
+
- **CCS Archiver**: CLI tool to archive CCS API data to contest package format
|
|
40
|
+
- **Contest API Server**: CLI tool to serve contest packages via CCS API
|
|
41
|
+
- **Cross-Language Compatibility**: Mirrors TypeScript types for data consistency
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install xcpcio
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or install with [uv](https://github.com/astral-sh/uv):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv add xcpcio
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Development
|
|
56
|
+
|
|
57
|
+
### Setup
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Clone repository
|
|
61
|
+
git clone https://github.com/xcpcio/xcpcio.git
|
|
62
|
+
cd xcpcio/python
|
|
63
|
+
|
|
64
|
+
# Install dependencies with uv
|
|
65
|
+
uv sync
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Testing
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Run tests
|
|
72
|
+
uv run pytest
|
|
73
|
+
|
|
74
|
+
# Run specific test file
|
|
75
|
+
uv run pytest tests/test_types.py
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Documentation
|
|
79
|
+
|
|
80
|
+
For detailed documentation, visit:
|
|
81
|
+
|
|
82
|
+
- [CCS Utility Guide](https://xcpcio.com/guide/ccs-utility)
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT License © 2020-PRESENT [Dup4](https://github.com/Dup4)
|
xcpcio-0.64.1/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# xcpcio-python
|
|
2
|
+
|
|
3
|
+
Python library and CLI tools for XCPCIO.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Type Definitions**: Pydantic models for contest data structures (teams, submissions, problems, etc.)
|
|
8
|
+
- **Constants**: Shared constants for submission statuses, time units, and penalty calculations
|
|
9
|
+
- **CCS Archiver**: CLI tool to archive CCS API data to contest package format
|
|
10
|
+
- **Contest API Server**: CLI tool to serve contest packages via CCS API
|
|
11
|
+
- **Cross-Language Compatibility**: Mirrors TypeScript types for data consistency
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install xcpcio
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install with [uv](https://github.com/astral-sh/uv):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv add xcpcio
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Development
|
|
26
|
+
|
|
27
|
+
### Setup
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Clone repository
|
|
31
|
+
git clone https://github.com/xcpcio/xcpcio.git
|
|
32
|
+
cd xcpcio/python
|
|
33
|
+
|
|
34
|
+
# Install dependencies with uv
|
|
35
|
+
uv sync
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Testing
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Run tests
|
|
42
|
+
uv run pytest
|
|
43
|
+
|
|
44
|
+
# Run specific test file
|
|
45
|
+
uv run pytest tests/test_types.py
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Documentation
|
|
49
|
+
|
|
50
|
+
For detailed documentation, visit:
|
|
51
|
+
|
|
52
|
+
- [CCS Utility Guide](https://xcpcio.com/guide/ccs-utility)
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
MIT License © 2020-PRESENT [Dup4](https://github.com/Dup4)
|
|
@@ -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
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
-
from pathlib import Path
|
|
4
3
|
from typing import Any, Dict, List, Optional
|
|
5
4
|
|
|
6
|
-
from fastapi import APIRouter,
|
|
5
|
+
from fastapi import APIRouter, Query
|
|
7
6
|
from fastapi import Path as FastAPIPath
|
|
8
7
|
from fastapi.responses import FileResponse, StreamingResponse
|
|
9
8
|
|
|
@@ -55,22 +54,8 @@ async def get_contest_banner(
|
|
|
55
54
|
contest_id: str = FastAPIPath(..., description="Contest identifier"),
|
|
56
55
|
service: ContestServiceDep = None,
|
|
57
56
|
) -> FileResponse:
|
|
58
|
-
service.
|
|
59
|
-
|
|
60
|
-
expected_href = f"contests/{contest_id}/banner"
|
|
61
|
-
|
|
62
|
-
try:
|
|
63
|
-
banners = service.contest.get("banner", [])
|
|
64
|
-
for banner in banners:
|
|
65
|
-
href = banner["href"]
|
|
66
|
-
if href == expected_href:
|
|
67
|
-
filename = banner["filename"]
|
|
68
|
-
banner_file: Path = service.contest_package_dir / "contest" / filename
|
|
69
|
-
if banner_file.exists():
|
|
70
|
-
mime_type = banner["mime"]
|
|
71
|
-
return FileResponse(path=banner_file, media_type=mime_type, filename=filename)
|
|
72
|
-
except Exception as e:
|
|
73
|
-
raise HTTPException(status_code=404, detail=f"Banner not found. [contest_id={contest_id}] [err={e}]")
|
|
57
|
+
file_attr = service.get_contest_banner(contest_id)
|
|
58
|
+
return FileResponse(path=file_attr.path, media_type=file_attr.media_type, filename=file_attr.name)
|
|
74
59
|
|
|
75
60
|
|
|
76
61
|
@router.get(
|
|
@@ -82,22 +67,8 @@ async def get_contest_problem_set(
|
|
|
82
67
|
contest_id: str = FastAPIPath(..., description="Contest identifier"),
|
|
83
68
|
service: ContestServiceDep = None,
|
|
84
69
|
) -> FileResponse:
|
|
85
|
-
service.
|
|
86
|
-
|
|
87
|
-
expected_href = f"contests/{contest_id}/problemset"
|
|
88
|
-
|
|
89
|
-
try:
|
|
90
|
-
problem_set_list = service.contest.get("problemset", [])
|
|
91
|
-
for problem_set in problem_set_list:
|
|
92
|
-
href = problem_set["href"]
|
|
93
|
-
if href == expected_href:
|
|
94
|
-
filename = problem_set["filename"]
|
|
95
|
-
problem_set_file: Path = service.contest_package_dir / "contest" / filename
|
|
96
|
-
if problem_set_file.exists():
|
|
97
|
-
mime_type = problem_set["mime"]
|
|
98
|
-
return FileResponse(path=problem_set_file, media_type=mime_type, filename=filename)
|
|
99
|
-
except Exception as e:
|
|
100
|
-
raise HTTPException(status_code=404, detail=f"Problem set not found. [contest_id={contest_id}] [err={e}]")
|
|
70
|
+
file_attr = service.get_contest_problemset(contest_id)
|
|
71
|
+
return FileResponse(path=file_attr.path, media_type=file_attr.media_type, filename=file_attr.name)
|
|
101
72
|
|
|
102
73
|
|
|
103
74
|
@router.get(
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from pathlib import Path
|
|
3
2
|
from typing import Any, Dict, List
|
|
4
3
|
|
|
5
|
-
from fastapi import APIRouter
|
|
4
|
+
from fastapi import APIRouter
|
|
6
5
|
from fastapi import Path as FastAPIPath
|
|
7
6
|
from fastapi.responses import FileResponse
|
|
8
7
|
|
|
@@ -47,25 +46,5 @@ async def get_organization_logo(
|
|
|
47
46
|
organization_id: str = FastAPIPath(..., description="Organization identifier"),
|
|
48
47
|
service: ContestServiceDep = None,
|
|
49
48
|
) -> FileResponse:
|
|
50
|
-
service.
|
|
51
|
-
|
|
52
|
-
org = service.organizations_by_id.get(organization_id)
|
|
53
|
-
if not org:
|
|
54
|
-
raise HTTPException(status_code=404, detail=f"Organization {organization_id} not found")
|
|
55
|
-
|
|
56
|
-
expected_href = f"contests/{contest_id}/organizations/{organization_id}/logo"
|
|
57
|
-
|
|
58
|
-
try:
|
|
59
|
-
logos = org["logo"]
|
|
60
|
-
for logo in logos:
|
|
61
|
-
href = logo["href"]
|
|
62
|
-
if href == expected_href:
|
|
63
|
-
filename = logo["filename"]
|
|
64
|
-
logo_file: Path = service.contest_package_dir / "organizations" / organization_id / filename
|
|
65
|
-
if logo_file.exists():
|
|
66
|
-
mime_type = logo["mime"]
|
|
67
|
-
return FileResponse(path=logo_file, media_type=mime_type, filename=filename)
|
|
68
|
-
except Exception as e:
|
|
69
|
-
raise HTTPException(
|
|
70
|
-
status_code=404, detail=f"Logo file not found. [organization_id={organization_id}] [err={e}]"
|
|
71
|
-
)
|
|
49
|
+
file_attr = service.get_organization_logo(contest_id, organization_id)
|
|
50
|
+
return FileResponse(path=file_attr.path, media_type=file_attr.media_type, filename=file_attr.name)
|