xcpcio 0.64.3__py3-none-any.whl → 0.64.5__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/__version__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # This file is auto-generated by Hatchling. As such, do not:
2
2
  # - modify
3
3
  # - track in version control e.g. be sure to add to .gitignore
4
- __version__ = VERSION = '0.64.3'
4
+ __version__ = VERSION = '0.64.5'
@@ -0,0 +1,223 @@
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.clics.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
+ clics-archiver --base-url https://domjudge/api --contest-id contest123 -o ./output -u admin -p secret
100
+
101
+ # Output to ZIP archive
102
+ clics-archiver --base-url https://domjudge/api --contest-id contest123 -o contest.zip --token abc123
103
+
104
+ # Output to tar.gz archive
105
+ clics-archiver --base-url https://domjudge/api --contest-id contest123 -o contest.tar.gz -u admin -p secret
106
+
107
+ # Output to tar.zst archive
108
+ clics-archiver --base-url https://domjudge/api --contest-id contest123 -o contest.tar.zst -u admin -p secret
109
+
110
+ # Only archive specific endpoints
111
+ clics-archiver --base-url https://domjudge/api --contest-id contest123 -o ./output -u admin -p secret -e teams -e problems
112
+
113
+ # Skip file downloads
114
+ clics-archiver --base-url https://domjudge/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
+ try:
134
+ shutil.rmtree(temp_dir)
135
+ except Exception as e:
136
+ click.echo(f"Warning: Failed to cleanup temporary directory: {e}", err=True)
137
+
138
+ atexit.register(cleanup_temp_dir)
139
+
140
+ if is_archive:
141
+ if output_str.endswith(".zip"):
142
+ archive_format = "zip"
143
+ elif output_str.endswith(".tar.gz"):
144
+ archive_format = "tar.gz"
145
+ elif output_str.endswith(".tar.zst"):
146
+ archive_format = "tar.zst"
147
+
148
+ credentials = APICredentials(username=username, password=password, token=token)
149
+
150
+ if is_archive:
151
+ temp_dir = Path(tempfile.mkdtemp(prefix="clics_archive_"))
152
+ output_dir = temp_dir
153
+ else:
154
+ output_dir = output
155
+
156
+ config = ArchiveConfig(
157
+ base_url=base_url.rstrip("/"),
158
+ contest_id=contest_id,
159
+ credentials=credentials,
160
+ output_dir=output_dir,
161
+ include_files=not no_files,
162
+ endpoints=list(endpoints) if endpoints else None,
163
+ timeout=timeout,
164
+ max_concurrent=max_concurrent,
165
+ include_event_feed=not no_event_feed,
166
+ )
167
+
168
+ click.echo(f"Archiving contest '{contest_id}' from {base_url}")
169
+ if is_archive:
170
+ click.echo(f"Output archive: {output} (format: {archive_format})")
171
+ else:
172
+ click.echo(f"Output directory: {output}")
173
+ if config.endpoints:
174
+ click.echo(f"Endpoints: {', '.join(config.endpoints)}")
175
+
176
+ async def run_archive():
177
+ async with ContestArchiver(config) as archiver:
178
+ await archiver.dump_all()
179
+
180
+ try:
181
+ asyncio.run(run_archive())
182
+
183
+ if is_archive:
184
+ click.echo(f"Creating {archive_format} archive...")
185
+ output.parent.mkdir(parents=True, exist_ok=True)
186
+
187
+ if archive_format == "zip":
188
+ with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as zipf:
189
+ for file_path in output_dir.rglob("*"):
190
+ if file_path.is_file():
191
+ arcname = file_path.relative_to(output_dir)
192
+ zipf.write(file_path, arcname)
193
+
194
+ elif archive_format == "tar.gz":
195
+ with tarfile.open(output, "w:gz") as tarf:
196
+ tarf.add(output_dir, arcname=".")
197
+
198
+ elif archive_format == "tar.zst":
199
+ with open(output, "wb") as f:
200
+ cctx = zstd.ZstdCompressor()
201
+ with cctx.stream_writer(f) as compressor:
202
+ with tarfile.open(fileobj=compressor, mode="w") as tarf:
203
+ tarf.add(output_dir, arcname=".")
204
+
205
+ click.echo(click.style(f"Contest package created successfully: {output}", fg="green"))
206
+ click.echo("\nChecksums:")
207
+ checksums = calculate_checksums(output)
208
+ click.echo(f" MD5: {checksums['md5']}")
209
+ click.echo(f" SHA1: {checksums['sha1']}")
210
+ click.echo(f" SHA256: {checksums['sha256']}")
211
+ click.echo(f" SHA512: {checksums['sha512']}")
212
+ else:
213
+ click.echo(click.style(f"Contest package created successfully in: {config.output_dir}", fg="green"))
214
+ except KeyboardInterrupt:
215
+ click.echo(click.style("Archive interrupted by user", fg="yellow"), err=True)
216
+ raise click.Abort()
217
+ except Exception as e:
218
+ click.echo(click.style(f"Error during archive: {e}", fg="red"), err=True)
219
+ raise click.ClickException(str(e))
220
+
221
+
222
+ if __name__ == "__main__":
223
+ main()
@@ -0,0 +1,138 @@
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.clics.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(
59
+ "--log-level",
60
+ default="info",
61
+ type=click.Choice(["debug", "info", "warning", "error", "critical"], case_sensitive=False),
62
+ help="Log level",
63
+ )
64
+ @click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging (same as --log-level debug)")
65
+ def main(contest_package: Path, host: str, port: int, log_level: str, verbose: bool):
66
+ """
67
+ Start the Contest API Server.
68
+
69
+ Examples:
70
+
71
+ # Start server with contest directory
72
+ clics-server -p /path/to/contest
73
+
74
+ # Start server with archive file
75
+ clics-server -p /path/to/contest.zip
76
+ clics-server -p /path/to/contest.tar.gz
77
+ clics-server -p /path/to/contest.tar.zst
78
+
79
+ # Custom host and port
80
+ clics-server -p /path/to/contest --host 127.0.0.1 --port 9000
81
+ """
82
+ if verbose:
83
+ log_level = "debug"
84
+ setup_logging(log_level.upper())
85
+
86
+ temp_dir: Optional[Path] = None
87
+
88
+ def cleanup_temp_dir():
89
+ if temp_dir and temp_dir.exists():
90
+ click.echo(f"Cleaning up temporary directory: {temp_dir}")
91
+ try:
92
+ shutil.rmtree(temp_dir)
93
+ except Exception as e:
94
+ click.echo(f"Warning: Failed to cleanup temporary directory: {e}", err=True)
95
+
96
+ atexit.register(cleanup_temp_dir)
97
+
98
+ actual_contest_dir: Path = contest_package
99
+ is_archive = str(contest_package).endswith((".zip", ".tar.gz", ".tar.zst"))
100
+
101
+ if is_archive:
102
+ if not contest_package.is_file():
103
+ click.echo(f"Error: Archive file not found: {contest_package}", err=True)
104
+ raise click.Abort()
105
+
106
+ click.echo(f"Extracting archive: {contest_package}")
107
+ temp_dir = Path(tempfile.mkdtemp(prefix="clics_package_"))
108
+
109
+ try:
110
+ extract_archive(contest_package, temp_dir)
111
+ actual_contest_dir = temp_dir
112
+ click.echo(f"Archive extracted to: {temp_dir}")
113
+ except Exception as e:
114
+ click.echo(f"Error extracting archive: {e}", err=True)
115
+ raise click.Abort()
116
+ else:
117
+ if not contest_package.is_dir():
118
+ click.echo(f"Error: Contest directory not found: {contest_package}", err=True)
119
+ raise click.Abort()
120
+
121
+ click.echo("Starting Contest API Server...")
122
+ click.echo(f"Contest directory: {actual_contest_dir}")
123
+ click.echo(f"Host: {host}")
124
+ click.echo(f"Port: {port}")
125
+ click.echo(f"Log level: {log_level}")
126
+
127
+ try:
128
+ server = ContestAPIServer(actual_contest_dir)
129
+ server.run(host=host, port=port, log_level=log_level.lower())
130
+ except KeyboardInterrupt:
131
+ click.echo("\nServer stopped by user")
132
+ except Exception as e:
133
+ click.echo(f"Error starting server: {e}", err=True)
134
+ raise click.Abort()
135
+
136
+
137
+ if __name__ == "__main__":
138
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xcpcio
3
- Version: 0.64.3
3
+ Version: 0.64.5
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
@@ -1,10 +1,12 @@
1
1
  xcpcio/__init__.py,sha256=NB6wpVr5JUrOx2vLIQSVtYuCz0d7kNFE39TSQlvoENk,125
2
- xcpcio/__version__.py,sha256=LxfvIcnxuKT4V2X_7iFD7f1csO3sp0l-FcxO8fLkvo8,172
2
+ xcpcio/__version__.py,sha256=BYquUwEg9-V5NQhS7rpjqTa9mDXyR0Lo8MbEWDor-eI,172
3
3
  xcpcio/constants.py,sha256=MjpAgNXiBlUsx1S09m7JNT-nekNDR-aE6ggvGL3fg0I,2297
4
4
  xcpcio/types.py,sha256=AkYby2haJgxwtozlgaPMG2ryAZdvsSc3sH-p6qXcM4g,6575
5
5
  xcpcio/api/__init__.py,sha256=B9gLdAlR3FD7070cvAC5wAwMy3iV63I8hh4mUrnrKpk,274
6
6
  xcpcio/api/client.py,sha256=BuzH8DbJYudJ-Kne-2XziLW__B_7iEqElJ4n2SGZCoY,2374
7
7
  xcpcio/api/models.py,sha256=_dChApnIHVNN3hEL7mR5zonq8IUcxW_h7z1kUz6reSs,772
8
+ xcpcio/app/clics_archiver.py,sha256=wd7zkbq2oyWfkS09w0f3KBr32F2yCcTMx36H5HFMgRg,7995
9
+ xcpcio/app/clics_server.py,sha256=-zUixNf08yICT2sry23h72ZrEm6NPb30bH5AHCXZlBM,4623
8
10
  xcpcio/clics/__init__.py,sha256=coTZiqxzXesn2SYmI2ZCsDZW6XaFi_6p-PFozZ4dfl4,150
9
11
  xcpcio/clics/clics_api_client.py,sha256=jQiOYPNZlYs_cOmQIbp-QovWVMYcmT1yo-33SWoyAn0,7966
10
12
  xcpcio/clics/contest_archiver.py,sha256=66a4YTqHqSoHMe7dFTXo4OrdK40AUnavo0ICSd9UNGo,9911
@@ -38,7 +40,7 @@ xcpcio/clics/model/model_2023_06/model.py,sha256=bVMDWpJTwPSpz1fHPxWrWerxCBIboH3
38
40
  xcpcio/clics/reader/__init__.py,sha256=Nfi78X8J1tJPh7WeSRPLMRUprlS2JYelYJHW4DfyJ7U,162
39
41
  xcpcio/clics/reader/contest_package_reader.py,sha256=0wIzQp4zzdaB10zMY4WALLIeoXcMuhMJ6nRrfKxWhgw,14179
40
42
  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,,
43
+ xcpcio-0.64.5.dist-info/METADATA,sha256=cDO1ROuDxUmCp2p-g26F-cFsqXCJ-lmHRDZYAkPrDGA,2233
44
+ xcpcio-0.64.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
45
+ xcpcio-0.64.5.dist-info/entry_points.txt,sha256=JYkvmmxKFWv0EBU6Ys65XsjkOO02KGlzASau0GX9TQ8,110
46
+ xcpcio-0.64.5.dist-info/RECORD,,
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ clics-archiver = xcpcio.app.clics_archiver:main
3
+ clics-server = xcpcio.app.clics_server:main
@@ -1,3 +0,0 @@
1
- [console_scripts]
2
- clics-archiver = app.clics_archiver:main
3
- clics-server = app.clics_server:main