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.

Files changed (55) hide show
  1. xcpcio-0.64.1/PKG-INFO +86 -0
  2. xcpcio-0.64.1/README.md +56 -0
  3. xcpcio-0.64.1/app/contest_api_server.py +140 -0
  4. xcpcio-0.64.1/cli/ccs_archiver_cli.py +221 -0
  5. {xcpcio-0.63.7 → xcpcio-0.64.1}/pyproject.toml +1 -0
  6. {xcpcio-0.63.7 → xcpcio-0.64.1}/uv.lock +2 -0
  7. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/__version__.py +1 -1
  8. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/dependencies.py +8 -3
  9. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/contests.py +5 -34
  10. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/organizations.py +3 -24
  11. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/problems.py +3 -22
  12. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/submissions.py +3 -24
  13. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/teams.py +3 -22
  14. xcpcio-0.64.1/xcpcio/ccs/api_server/services/contest_service.py +194 -0
  15. xcpcio-0.64.1/xcpcio/ccs/base/__init__.py +3 -0
  16. xcpcio-0.64.1/xcpcio/ccs/base/types.py +9 -0
  17. xcpcio-0.64.1/xcpcio/ccs/reader/__init__.py +0 -0
  18. xcpcio-0.64.1/xcpcio/ccs/reader/base_ccs_reader.py +165 -0
  19. xcpcio-0.64.1/xcpcio/ccs/reader/contest_package_reader.py +331 -0
  20. xcpcio-0.63.7/PKG-INFO +0 -30
  21. xcpcio-0.63.7/README.md +0 -1
  22. xcpcio-0.63.7/app/contest_api_server.py +0 -77
  23. xcpcio-0.63.7/cli/ccs_archiver_cli.py +0 -124
  24. xcpcio-0.63.7/task.md +0 -22
  25. xcpcio-0.63.7/xcpcio/ccs/api_server/services/contest_service.py +0 -325
  26. {xcpcio-0.63.7 → xcpcio-0.64.1}/.gitignore +0 -0
  27. {xcpcio-0.63.7 → xcpcio-0.64.1}/.python-version +0 -0
  28. {xcpcio-0.63.7 → xcpcio-0.64.1}/scripts/generate_ccs_models.sh +0 -0
  29. {xcpcio-0.63.7 → xcpcio-0.64.1}/tests/__init__.py +0 -0
  30. {xcpcio-0.63.7 → xcpcio-0.64.1}/tests/test_contest.py +0 -0
  31. {xcpcio-0.63.7 → xcpcio-0.64.1}/tests/test_submission.py +0 -0
  32. {xcpcio-0.63.7 → xcpcio-0.64.1}/tests/test_team.py +0 -0
  33. {xcpcio-0.63.7 → xcpcio-0.64.1}/tests/test_types.py +0 -0
  34. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/__init__.py +0 -0
  35. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/__init__.py +0 -0
  36. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/__init__.py +0 -0
  37. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/__init__.py +0 -0
  38. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/access.py +0 -0
  39. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/accounts.py +0 -0
  40. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/awards.py +0 -0
  41. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/clarifications.py +0 -0
  42. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/general.py +0 -0
  43. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/groups.py +0 -0
  44. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/judgement_types.py +0 -0
  45. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/judgements.py +0 -0
  46. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/languages.py +0 -0
  47. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/routes/runs.py +0 -0
  48. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/server.py +0 -0
  49. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/api_server/services/__init__.py +0 -0
  50. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/contest_archiver.py +0 -0
  51. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/model/__init__.py +0 -0
  52. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/model/model_2023_06/__init__.py +0 -0
  53. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/ccs/model/model_2023_06/model.py +0 -0
  54. {xcpcio-0.63.7 → xcpcio-0.64.1}/xcpcio/constants.py +0 -0
  55. {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 &copy; 2020-PRESENT [Dup4](https://github.com/Dup4)
@@ -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 &copy; 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()
@@ -28,6 +28,7 @@ dependencies = [
28
28
  "tenacity>=8.0.0",
29
29
  "fastapi>=0.117.1",
30
30
  "uvicorn>=0.36.0",
31
+ "zstandard>=0.25.0",
31
32
  ]
32
33
  dynamic = ["version"]
33
34
 
@@ -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]
@@ -1,4 +1,4 @@
1
1
  # This file is auto-generated by Hatchling. As such, do not:
2
2
  # - modify
3
3
  # - track in version control e.g. be sure to add to .gitignore
4
- __version__ = VERSION = '0.63.7'
4
+ __version__ = VERSION = '0.64.1'
@@ -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
- _contest_service_instance = ContestService(contest_package_dir)
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, HTTPException, Query
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.validate_contest_id(contest_id)
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.validate_contest_id(contest_id)
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, HTTPException
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.validate_contest_id(contest_id)
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)