xcpcio 0.64.4__py3-none-any.whl → 0.65.0__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 +1 -1
- xcpcio/app/clics_archiver.py +223 -0
- xcpcio/app/clics_server.py +138 -0
- xcpcio/types.py +60 -26
- {xcpcio-0.64.4.dist-info → xcpcio-0.65.0.dist-info}/METADATA +1 -1
- {xcpcio-0.64.4.dist-info → xcpcio-0.65.0.dist-info}/RECORD +8 -6
- xcpcio-0.65.0.dist-info/entry_points.txt +3 -0
- xcpcio-0.64.4.dist-info/entry_points.txt +0 -3
- {xcpcio-0.64.4.dist-info → xcpcio-0.65.0.dist-info}/WHEEL +0 -0
xcpcio/__version__.py
CHANGED
|
@@ -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()
|
xcpcio/types.py
CHANGED
|
@@ -61,9 +61,28 @@ SubmissionStatus = Literal[
|
|
|
61
61
|
ImagePreset = Literal["ICPC", "CCPC", "HUNAN_CPC"]
|
|
62
62
|
MedalPreset = Literal["ccpc", "icpc"]
|
|
63
63
|
BannerMode = Literal["ONLY_BANNER", "ALL"]
|
|
64
|
+
Lang = Literal["en", "zh-CN"]
|
|
64
65
|
DateTimeISO8601String = str
|
|
65
66
|
|
|
66
67
|
|
|
68
|
+
class I18NStringSet(BaseModel):
|
|
69
|
+
fallback: Optional[str] = None
|
|
70
|
+
fallback_lang: Optional[Lang] = None
|
|
71
|
+
texts: Optional[Dict[Lang, str]] = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
Text = Union[str, I18NStringSet]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Person(BaseModel):
|
|
78
|
+
name: Text
|
|
79
|
+
cf_id: Optional[str] = None
|
|
80
|
+
icpc_id: Optional[str] = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
Persons = List[Person]
|
|
84
|
+
|
|
85
|
+
|
|
67
86
|
class Image(BaseModel):
|
|
68
87
|
url: Optional[str] = None
|
|
69
88
|
base64: Optional[str] = None
|
|
@@ -71,20 +90,33 @@ class Image(BaseModel):
|
|
|
71
90
|
preset: Optional[ImagePreset] = None
|
|
72
91
|
|
|
73
92
|
|
|
74
|
-
class
|
|
93
|
+
class BalloonColor(BaseModel):
|
|
75
94
|
color: str
|
|
76
95
|
background_color: str
|
|
77
96
|
|
|
78
97
|
|
|
79
|
-
class
|
|
98
|
+
class Problem(BaseModel):
|
|
99
|
+
id: str
|
|
100
|
+
label: str
|
|
101
|
+
name: Optional[Text] = None
|
|
102
|
+
time_limit: Optional[str] = None
|
|
103
|
+
memory_limit: Optional[str] = None
|
|
104
|
+
balloon_color: Optional[BalloonColor] = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
Problems = List[Problem]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class SubmissionReaction(BaseModel):
|
|
80
111
|
url: Optional[str] = None
|
|
81
112
|
|
|
82
113
|
|
|
83
114
|
class Submission(BaseModel):
|
|
84
|
-
id: str =
|
|
115
|
+
id: Optional[str] = None
|
|
116
|
+
submission_id: Optional[str] = None
|
|
85
117
|
|
|
86
118
|
team_id: str = ""
|
|
87
|
-
problem_id: int = 0
|
|
119
|
+
problem_id: Union[int, str] = 0
|
|
88
120
|
timestamp: int = 0 # unit: seconds
|
|
89
121
|
status: SubmissionStatus = constants.SUBMISSION_STATUS_UNKNOWN
|
|
90
122
|
|
|
@@ -93,7 +125,7 @@ class Submission(BaseModel):
|
|
|
93
125
|
|
|
94
126
|
is_ignore: Optional[bool] = None
|
|
95
127
|
|
|
96
|
-
reaction: Optional[
|
|
128
|
+
reaction: Optional[SubmissionReaction] = None
|
|
97
129
|
|
|
98
130
|
|
|
99
131
|
class Submissions(RootModel[List[Submission]]):
|
|
@@ -102,14 +134,14 @@ class Submissions(RootModel[List[Submission]]):
|
|
|
102
134
|
|
|
103
135
|
class Team(BaseModel):
|
|
104
136
|
id: str = ""
|
|
105
|
-
name:
|
|
137
|
+
name: Text = ""
|
|
106
138
|
|
|
107
139
|
organization: str = ""
|
|
108
140
|
group: List[str] = Field(default_factory=list)
|
|
109
141
|
tag: List[str] = Field(default_factory=list)
|
|
110
142
|
|
|
111
|
-
|
|
112
|
-
members: Optional[List[
|
|
143
|
+
coaches: Optional[Union[Text, List[Text], Persons]] = None
|
|
144
|
+
members: Optional[Union[Text, List[Text], Persons]] = None
|
|
113
145
|
|
|
114
146
|
badge: Optional[Image] = None
|
|
115
147
|
|
|
@@ -139,7 +171,7 @@ class ContestOptions(BaseModel):
|
|
|
139
171
|
|
|
140
172
|
|
|
141
173
|
class Contest(BaseModel):
|
|
142
|
-
contest_name:
|
|
174
|
+
contest_name: Text = ""
|
|
143
175
|
|
|
144
176
|
start_time: Union[int, DateTimeISO8601String] = 0
|
|
145
177
|
end_time: Union[int, DateTimeISO8601String] = 0
|
|
@@ -149,15 +181,17 @@ class Contest(BaseModel):
|
|
|
149
181
|
frozen_time: int = 60 * 60 # unit: seconds
|
|
150
182
|
unfrozen_time: int = 0x3F3F3F3F3F3F3F3F
|
|
151
183
|
|
|
184
|
+
problems: Optional[Problems] = None
|
|
152
185
|
problem_quantity: int = 0
|
|
153
186
|
problem_id: List[str] = Field(default_factory=list)
|
|
187
|
+
balloon_color: Optional[List[BalloonColor]] = None
|
|
154
188
|
|
|
155
|
-
organization: str = "School"
|
|
156
189
|
status_time_display: Optional[Dict[str, bool]] = constants.FULL_STATUS_TIME_DISPLAY
|
|
157
190
|
|
|
158
191
|
badge: Optional[str] = None
|
|
192
|
+
organization: str = "School"
|
|
193
|
+
|
|
159
194
|
medal: Optional[Union[Dict[str, Dict[str, int]], MedalPreset]] = None
|
|
160
|
-
balloon_color: Optional[List[Color]] = None
|
|
161
195
|
|
|
162
196
|
group: Optional[Dict[str, str]] = None
|
|
163
197
|
tag: Optional[Dict[str, str]] = None
|
|
@@ -171,7 +205,7 @@ class Contest(BaseModel):
|
|
|
171
205
|
|
|
172
206
|
options: ContestOptions = Field(default_factory=ContestOptions)
|
|
173
207
|
|
|
174
|
-
def append_balloon_color(self, color:
|
|
208
|
+
def append_balloon_color(self, color: BalloonColor):
|
|
175
209
|
if self.balloon_color is None:
|
|
176
210
|
self.balloon_color = []
|
|
177
211
|
self.balloon_color.append(color)
|
|
@@ -183,20 +217,20 @@ class Contest(BaseModel):
|
|
|
183
217
|
|
|
184
218
|
def fill_balloon_color(self):
|
|
185
219
|
default_balloon_color_list = [
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
220
|
+
BalloonColor(background_color="rgba(189, 14, 14, 0.7)", color="#fff"),
|
|
221
|
+
BalloonColor(background_color="rgba(149, 31, 217, 0.7)", color="#fff"),
|
|
222
|
+
BalloonColor(background_color="rgba(16, 32, 96, 0.7)", color="#fff"),
|
|
223
|
+
BalloonColor(background_color="rgba(38, 185, 60, 0.7)", color="#000"),
|
|
224
|
+
BalloonColor(background_color="rgba(239, 217, 9, 0.7)", color="#000"),
|
|
225
|
+
BalloonColor(background_color="rgba(243, 88, 20, 0.7)", color="#fff"),
|
|
226
|
+
BalloonColor(background_color="rgba(12, 76, 138, 0.7)", color="#fff"),
|
|
227
|
+
BalloonColor(background_color="rgba(156, 155, 155, 0.7)", color="#000"),
|
|
228
|
+
BalloonColor(background_color="rgba(4, 154, 115, 0.7)", color="#000"),
|
|
229
|
+
BalloonColor(background_color="rgba(159, 19, 236, 0.7)", color="#fff"),
|
|
230
|
+
BalloonColor(background_color="rgba(42, 197, 202, 0.7)", color="#000"),
|
|
231
|
+
BalloonColor(background_color="rgba(142, 56, 54, 0.7)", color="#fff"),
|
|
232
|
+
BalloonColor(background_color="rgba(144, 238, 144, 0.7)", color="#000"),
|
|
233
|
+
BalloonColor(background_color="rgba(77, 57, 0, 0.7)", color="#fff"),
|
|
200
234
|
]
|
|
201
235
|
|
|
202
236
|
self.balloon_color = default_balloon_color_list[: self.problem_quantity]
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
xcpcio/__init__.py,sha256=NB6wpVr5JUrOx2vLIQSVtYuCz0d7kNFE39TSQlvoENk,125
|
|
2
|
-
xcpcio/__version__.py,sha256=
|
|
2
|
+
xcpcio/__version__.py,sha256=uSD0tE8vcfG2kBfQmbthAcsvXUvzg1w2cXDA2Bunbco,172
|
|
3
3
|
xcpcio/constants.py,sha256=MjpAgNXiBlUsx1S09m7JNT-nekNDR-aE6ggvGL3fg0I,2297
|
|
4
|
-
xcpcio/types.py,sha256=
|
|
4
|
+
xcpcio/types.py,sha256=CJYZc3RIWKYFbXIpyqt8fZANqJUMR66vLPno6nFyegA,7466
|
|
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.
|
|
42
|
-
xcpcio-0.
|
|
43
|
-
xcpcio-0.
|
|
44
|
-
xcpcio-0.
|
|
43
|
+
xcpcio-0.65.0.dist-info/METADATA,sha256=8VaTk5-ea4wUe-gBv4j2GfrErvY-Yk8l5ey6ouzMHkI,2233
|
|
44
|
+
xcpcio-0.65.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
45
|
+
xcpcio-0.65.0.dist-info/entry_points.txt,sha256=JYkvmmxKFWv0EBU6Ys65XsjkOO02KGlzASau0GX9TQ8,110
|
|
46
|
+
xcpcio-0.65.0.dist-info/RECORD,,
|
|
File without changes
|