xcpcio 0.64.2__tar.gz → 0.64.4__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.2 → xcpcio-0.64.4}/PKG-INFO +4 -2
- {xcpcio-0.64.2 → xcpcio-0.64.4}/README.md +3 -1
- xcpcio-0.64.2/cli/ccs_archiver_cli.py → xcpcio-0.64.4/app/clics_archiver.py +12 -10
- xcpcio-0.64.2/app/contest_api_server.py → xcpcio-0.64.4/app/clics_server.py +13 -15
- {xcpcio-0.64.2 → xcpcio-0.64.4}/pyproject.toml +2 -1
- {xcpcio-0.64.2 → xcpcio-0.64.4}/scripts/generate_ccs_models.sh +1 -1
- {xcpcio-0.64.2 → xcpcio-0.64.4}/tests/test_contest.py +37 -43
- xcpcio-0.64.4/xcpcio/__init__.py +4 -0
- {xcpcio-0.64.2 → xcpcio-0.64.4}/xcpcio/__version__.py +1 -1
- xcpcio-0.64.4/xcpcio/api/__init__.py +15 -0
- xcpcio-0.64.4/xcpcio/api/client.py +74 -0
- xcpcio-0.64.4/xcpcio/api/models.py +25 -0
- xcpcio-0.64.4/xcpcio/clics/__init__.py +9 -0
- xcpcio-0.64.4/xcpcio/clics/api_server/app.py +43 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/dependencies.py +3 -4
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/__init__.py +1 -1
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/server.py +15 -35
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/services/__init__.py +3 -1
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/services/contest_service.py +4 -4
- xcpcio-0.64.4/xcpcio/clics/clics_api_client.py +215 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/contest_archiver.py +27 -169
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/model/model_2023_06/__init__.py +1 -1
- xcpcio-0.64.4/xcpcio/clics/reader/__init__.py +7 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/reader/contest_package_reader.py +3 -3
- xcpcio-0.64.2/xcpcio/ccs/reader/base_ccs_reader.py → xcpcio-0.64.4/xcpcio/clics/reader/interface.py +2 -2
- xcpcio-0.64.2/xcpcio/__init__.py +0 -4
- xcpcio-0.64.2/xcpcio/ccs/__init__.py +0 -3
- xcpcio-0.64.2/xcpcio/ccs/reader/__init__.py +0 -0
- {xcpcio-0.64.2 → xcpcio-0.64.4}/.gitignore +0 -0
- {xcpcio-0.64.2 → xcpcio-0.64.4}/.python-version +0 -0
- {xcpcio-0.64.2 → xcpcio-0.64.4}/tests/__init__.py +0 -0
- {xcpcio-0.64.2 → xcpcio-0.64.4}/tests/test_submission.py +0 -0
- {xcpcio-0.64.2 → xcpcio-0.64.4}/tests/test_team.py +0 -0
- {xcpcio-0.64.2 → xcpcio-0.64.4}/tests/test_types.py +0 -0
- {xcpcio-0.64.2 → xcpcio-0.64.4}/uv.lock +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/__init__.py +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/access.py +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/accounts.py +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/awards.py +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/clarifications.py +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/contests.py +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/general.py +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/groups.py +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/judgement_types.py +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/judgements.py +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/languages.py +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/organizations.py +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/problems.py +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/runs.py +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/submissions.py +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/teams.py +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/base/__init__.py +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/base/types.py +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/model/__init__.py +0 -0
- {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/model/model_2023_06/model.py +0 -0
- {xcpcio-0.64.2 → xcpcio-0.64.4}/xcpcio/constants.py +0 -0
- {xcpcio-0.64.2 → xcpcio-0.64.4}/xcpcio/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xcpcio
|
|
3
|
-
Version: 0.64.
|
|
3
|
+
Version: 0.64.4
|
|
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
|
|
@@ -83,4 +83,6 @@ For detailed documentation, visit:
|
|
|
83
83
|
|
|
84
84
|
## License
|
|
85
85
|
|
|
86
|
-
MIT License © 2020-PRESENT [
|
|
86
|
+
[MIT](../LICENSE) License © 2020 - PRESENT [XCPCIO][xcpcio]
|
|
87
|
+
|
|
88
|
+
[xcpcio]: https://xcpcio.com
|
|
@@ -13,7 +13,7 @@ import click
|
|
|
13
13
|
import zstandard as zstd
|
|
14
14
|
|
|
15
15
|
from xcpcio import __version__
|
|
16
|
-
from xcpcio.
|
|
16
|
+
from xcpcio.clics.contest_archiver import APICredentials, ArchiveConfig, ContestArchiver
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def setup_logging(level: str = "INFO"):
|
|
@@ -96,22 +96,22 @@ def main(
|
|
|
96
96
|
Examples:
|
|
97
97
|
|
|
98
98
|
# Output to directory
|
|
99
|
-
|
|
99
|
+
clics-archiver --base-url https://domjudge/api --contest-id contest123 -o ./output -u admin -p secret
|
|
100
100
|
|
|
101
101
|
# Output to ZIP archive
|
|
102
|
-
|
|
102
|
+
clics-archiver --base-url https://domjudge/api --contest-id contest123 -o contest.zip --token abc123
|
|
103
103
|
|
|
104
104
|
# Output to tar.gz archive
|
|
105
|
-
|
|
105
|
+
clics-archiver --base-url https://domjudge/api --contest-id contest123 -o contest.tar.gz -u admin -p secret
|
|
106
106
|
|
|
107
107
|
# Output to tar.zst archive
|
|
108
|
-
|
|
108
|
+
clics-archiver --base-url https://domjudge/api --contest-id contest123 -o contest.tar.zst -u admin -p secret
|
|
109
109
|
|
|
110
110
|
# Only archive specific endpoints
|
|
111
|
-
|
|
111
|
+
clics-archiver --base-url https://domjudge/api --contest-id contest123 -o ./output -u admin -p secret -e teams -e problems
|
|
112
112
|
|
|
113
113
|
# Skip file downloads
|
|
114
|
-
|
|
114
|
+
clics-archiver --base-url https://domjudge/api --contest-id contest123 -o ./output --no-files
|
|
115
115
|
"""
|
|
116
116
|
|
|
117
117
|
if verbose:
|
|
@@ -130,7 +130,10 @@ def main(
|
|
|
130
130
|
def cleanup_temp_dir():
|
|
131
131
|
if temp_dir and temp_dir.exists():
|
|
132
132
|
click.echo(f"Cleaning up temporary directory: {temp_dir}")
|
|
133
|
-
|
|
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)
|
|
134
137
|
|
|
135
138
|
atexit.register(cleanup_temp_dir)
|
|
136
139
|
|
|
@@ -145,7 +148,7 @@ def main(
|
|
|
145
148
|
credentials = APICredentials(username=username, password=password, token=token)
|
|
146
149
|
|
|
147
150
|
if is_archive:
|
|
148
|
-
temp_dir = Path(tempfile.mkdtemp(prefix="
|
|
151
|
+
temp_dir = Path(tempfile.mkdtemp(prefix="clics_archive_"))
|
|
149
152
|
output_dir = temp_dir
|
|
150
153
|
else:
|
|
151
154
|
output_dir = output
|
|
@@ -208,7 +211,6 @@ def main(
|
|
|
208
211
|
click.echo(f" SHA512: {checksums['sha512']}")
|
|
209
212
|
else:
|
|
210
213
|
click.echo(click.style(f"Contest package created successfully in: {config.output_dir}", fg="green"))
|
|
211
|
-
|
|
212
214
|
except KeyboardInterrupt:
|
|
213
215
|
click.echo(click.style("Archive interrupted by user", fg="yellow"), err=True)
|
|
214
216
|
raise click.Abort()
|
|
@@ -11,7 +11,7 @@ import click
|
|
|
11
11
|
import zstandard as zstd
|
|
12
12
|
|
|
13
13
|
from xcpcio import __version__
|
|
14
|
-
from xcpcio.
|
|
14
|
+
from xcpcio.clics.api_server.server import ContestAPIServer
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def setup_logging(level: str = "INFO"):
|
|
@@ -55,7 +55,6 @@ def extract_archive(archive_path: Path, dest_dir: Path) -> None:
|
|
|
55
55
|
)
|
|
56
56
|
@click.option("--host", default="0.0.0.0", help="Host to bind to")
|
|
57
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
58
|
@click.option(
|
|
60
59
|
"--log-level",
|
|
61
60
|
default="info",
|
|
@@ -63,25 +62,22 @@ def extract_archive(archive_path: Path, dest_dir: Path) -> None:
|
|
|
63
62
|
help="Log level",
|
|
64
63
|
)
|
|
65
64
|
@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,
|
|
65
|
+
def main(contest_package: Path, host: str, port: int, log_level: str, verbose: bool):
|
|
67
66
|
"""
|
|
68
67
|
Start the Contest API Server.
|
|
69
68
|
|
|
70
69
|
Examples:
|
|
71
70
|
|
|
72
71
|
# Start server with contest directory
|
|
73
|
-
|
|
72
|
+
clics-server -p /path/to/contest
|
|
74
73
|
|
|
75
74
|
# Start server with archive file
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
79
78
|
|
|
80
79
|
# Custom host and port
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
# Enable reload for development
|
|
84
|
-
contest-api-server -p /path/to/contest --reload
|
|
80
|
+
clics-server -p /path/to/contest --host 127.0.0.1 --port 9000
|
|
85
81
|
"""
|
|
86
82
|
if verbose:
|
|
87
83
|
log_level = "debug"
|
|
@@ -92,7 +88,10 @@ def main(contest_package: Path, host: str, port: int, reload: bool, log_level: s
|
|
|
92
88
|
def cleanup_temp_dir():
|
|
93
89
|
if temp_dir and temp_dir.exists():
|
|
94
90
|
click.echo(f"Cleaning up temporary directory: {temp_dir}")
|
|
95
|
-
|
|
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)
|
|
96
95
|
|
|
97
96
|
atexit.register(cleanup_temp_dir)
|
|
98
97
|
|
|
@@ -105,7 +104,7 @@ def main(contest_package: Path, host: str, port: int, reload: bool, log_level: s
|
|
|
105
104
|
raise click.Abort()
|
|
106
105
|
|
|
107
106
|
click.echo(f"Extracting archive: {contest_package}")
|
|
108
|
-
temp_dir = Path(tempfile.mkdtemp(prefix="
|
|
107
|
+
temp_dir = Path(tempfile.mkdtemp(prefix="clics_package_"))
|
|
109
108
|
|
|
110
109
|
try:
|
|
111
110
|
extract_archive(contest_package, temp_dir)
|
|
@@ -123,12 +122,11 @@ def main(contest_package: Path, host: str, port: int, reload: bool, log_level: s
|
|
|
123
122
|
click.echo(f"Contest directory: {actual_contest_dir}")
|
|
124
123
|
click.echo(f"Host: {host}")
|
|
125
124
|
click.echo(f"Port: {port}")
|
|
126
|
-
click.echo(f"Reload: {reload}")
|
|
127
125
|
click.echo(f"Log level: {log_level}")
|
|
128
126
|
|
|
129
127
|
try:
|
|
130
128
|
server = ContestAPIServer(actual_contest_dir)
|
|
131
|
-
server.run(host=host, port=port,
|
|
129
|
+
server.run(host=host, port=port, log_level=log_level.lower())
|
|
132
130
|
except KeyboardInterrupt:
|
|
133
131
|
click.echo("\nServer stopped by user")
|
|
134
132
|
except Exception as e:
|
|
@@ -38,7 +38,8 @@ documentation = "https://github.com/xcpcio/xcpcio"
|
|
|
38
38
|
repository = "https://github.com/xcpcio/xcpcio"
|
|
39
39
|
|
|
40
40
|
[project.scripts]
|
|
41
|
-
|
|
41
|
+
clics-archiver = "app.clics_archiver:main"
|
|
42
|
+
clics-server = "app.clics_server:main"
|
|
42
43
|
|
|
43
44
|
[tool.ruff]
|
|
44
45
|
line-length = 120
|
|
@@ -5,7 +5,7 @@ set -euo pipefail
|
|
|
5
5
|
CUR_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
|
|
6
6
|
PYTHON_DIR="$(dirname "${CUR_DIR}")"
|
|
7
7
|
CCS_SPECS_DIR="${PYTHON_DIR}/ccs-specs"
|
|
8
|
-
CCS_MODELS_DIR="${PYTHON_DIR}/xcpcio/
|
|
8
|
+
CCS_MODELS_DIR="${PYTHON_DIR}/xcpcio/clics/model"
|
|
9
9
|
|
|
10
10
|
BRANCH="${1:-2023-06}"
|
|
11
11
|
CCS_REPO_URL="https://github.com/icpc/ccs-specs.git"
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
1
|
from xcpcio import constants
|
|
4
2
|
from xcpcio.types import Color, Contest, ContestOptions, Image
|
|
5
3
|
|
|
@@ -10,7 +8,7 @@ class TestContest:
|
|
|
10
8
|
def test_contest_creation_defaults(self):
|
|
11
9
|
"""Test Contest creation with default values"""
|
|
12
10
|
contest = Contest()
|
|
13
|
-
|
|
11
|
+
|
|
14
12
|
assert contest.contest_name == ""
|
|
15
13
|
assert contest.start_time == 0
|
|
16
14
|
assert contest.end_time == 0
|
|
@@ -30,7 +28,7 @@ class TestContest:
|
|
|
30
28
|
assert contest.tag is None
|
|
31
29
|
assert contest.board_link is None
|
|
32
30
|
assert contest.version is None
|
|
33
|
-
|
|
31
|
+
|
|
34
32
|
# Check default values
|
|
35
33
|
assert contest.status_time_display == constants.FULL_STATUS_TIME_DISPLAY
|
|
36
34
|
assert isinstance(contest.options, ContestOptions)
|
|
@@ -38,10 +36,9 @@ class TestContest:
|
|
|
38
36
|
def test_contest_creation_with_values(self):
|
|
39
37
|
"""Test Contest creation with provided values"""
|
|
40
38
|
contest_options = ContestOptions(
|
|
41
|
-
calculation_of_penalty=constants.CALCULATION_OF_PENALTY_IN_SECONDS,
|
|
42
|
-
has_reaction_videos=True
|
|
39
|
+
calculation_of_penalty=constants.CALCULATION_OF_PENALTY_IN_SECONDS, has_reaction_videos=True
|
|
43
40
|
)
|
|
44
|
-
|
|
41
|
+
|
|
45
42
|
contest = Contest(
|
|
46
43
|
contest_name="ICPC World Finals 2024",
|
|
47
44
|
start_time=1234567890,
|
|
@@ -52,9 +49,9 @@ class TestContest:
|
|
|
52
49
|
problem_id=["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"],
|
|
53
50
|
organization="ICPC",
|
|
54
51
|
medal="icpc", # Use Literal value
|
|
55
|
-
options=contest_options
|
|
52
|
+
options=contest_options,
|
|
56
53
|
)
|
|
57
|
-
|
|
54
|
+
|
|
58
55
|
assert contest.contest_name == "ICPC World Finals 2024"
|
|
59
56
|
assert contest.start_time == 1234567890
|
|
60
57
|
assert contest.end_time == 1234567890 + 5 * 60 * 60
|
|
@@ -74,16 +71,16 @@ class TestContest:
|
|
|
74
71
|
end_time=1000000000 + 60 * 60 * 5,
|
|
75
72
|
problem_quantity=5,
|
|
76
73
|
problem_id=["A", "B", "C", "D", "E"],
|
|
77
|
-
organization="Test Org"
|
|
74
|
+
organization="Test Org",
|
|
78
75
|
)
|
|
79
|
-
|
|
76
|
+
|
|
80
77
|
# Test model_dump
|
|
81
78
|
contest_dict = contest.model_dump()
|
|
82
79
|
assert contest_dict["contest_name"] == "Test Contest"
|
|
83
80
|
assert contest_dict["start_time"] == 1000000000
|
|
84
81
|
assert contest_dict["problem_quantity"] == 5
|
|
85
82
|
assert contest_dict["organization"] == "Test Org"
|
|
86
|
-
|
|
83
|
+
|
|
87
84
|
# Test JSON round-trip
|
|
88
85
|
contest_json = contest.model_dump_json()
|
|
89
86
|
reconstructed_contest = Contest.model_validate_json(contest_json)
|
|
@@ -93,20 +90,20 @@ class TestContest:
|
|
|
93
90
|
"""Test Contest with balloon colors, logo, and banner"""
|
|
94
91
|
colors = [
|
|
95
92
|
Color(color="#fff", background_color="rgba(255, 0, 0, 0.7)"),
|
|
96
|
-
Color(color="#000", background_color="rgba(0, 255, 0, 0.7)")
|
|
93
|
+
Color(color="#000", background_color="rgba(0, 255, 0, 0.7)"),
|
|
97
94
|
]
|
|
98
95
|
logo = Image(url="https://example.com/logo.png", type="png")
|
|
99
96
|
banner = Image(url="https://example.com/banner.jpg", type="jpg")
|
|
100
|
-
|
|
97
|
+
|
|
101
98
|
contest = Contest(
|
|
102
99
|
contest_name="Contest with Media",
|
|
103
100
|
problem_quantity=2,
|
|
104
101
|
balloon_color=colors,
|
|
105
102
|
logo=logo,
|
|
106
103
|
banner=banner,
|
|
107
|
-
banner_mode="ALL" # Use correct Literal value
|
|
104
|
+
banner_mode="ALL", # Use correct Literal value
|
|
108
105
|
)
|
|
109
|
-
|
|
106
|
+
|
|
110
107
|
assert len(contest.balloon_color) == 2
|
|
111
108
|
assert contest.balloon_color[0].color == "#fff"
|
|
112
109
|
assert contest.balloon_color[1].background_color == "rgba(0, 255, 0, 0.7)"
|
|
@@ -117,59 +114,59 @@ class TestContest:
|
|
|
117
114
|
def test_append_balloon_color(self):
|
|
118
115
|
"""Test append_balloon_color method"""
|
|
119
116
|
contest = Contest()
|
|
120
|
-
|
|
117
|
+
|
|
121
118
|
# Initially no colors
|
|
122
119
|
assert contest.balloon_color is None
|
|
123
|
-
|
|
120
|
+
|
|
124
121
|
# Add first color
|
|
125
122
|
red_color = Color(color="#fff", background_color="red")
|
|
126
123
|
contest.append_balloon_color(red_color)
|
|
127
|
-
|
|
124
|
+
|
|
128
125
|
assert contest.balloon_color is not None
|
|
129
126
|
assert len(contest.balloon_color) == 1
|
|
130
127
|
assert contest.balloon_color[0] == red_color
|
|
131
|
-
|
|
128
|
+
|
|
132
129
|
# Add second color
|
|
133
130
|
blue_color = Color(color="#fff", background_color="blue")
|
|
134
131
|
contest.append_balloon_color(blue_color)
|
|
135
|
-
|
|
132
|
+
|
|
136
133
|
assert len(contest.balloon_color) == 2
|
|
137
134
|
assert contest.balloon_color[1] == blue_color
|
|
138
135
|
|
|
139
136
|
def test_fill_problem_id(self):
|
|
140
137
|
"""Test fill_problem_id method"""
|
|
141
138
|
contest = Contest(problem_quantity=5)
|
|
142
|
-
|
|
139
|
+
|
|
143
140
|
# Initially empty
|
|
144
141
|
assert contest.problem_id == []
|
|
145
|
-
|
|
142
|
+
|
|
146
143
|
# Fill with A-E
|
|
147
144
|
contest.fill_problem_id()
|
|
148
|
-
|
|
145
|
+
|
|
149
146
|
assert len(contest.problem_id) == 5
|
|
150
147
|
assert contest.problem_id == ["A", "B", "C", "D", "E"]
|
|
151
|
-
|
|
148
|
+
|
|
152
149
|
# Test with larger quantity
|
|
153
150
|
contest.problem_quantity = 10
|
|
154
151
|
contest.fill_problem_id()
|
|
155
|
-
|
|
152
|
+
|
|
156
153
|
assert len(contest.problem_id) == 10
|
|
157
154
|
assert contest.problem_id == ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]
|
|
158
155
|
|
|
159
156
|
def test_fill_balloon_color(self):
|
|
160
157
|
"""Test fill_balloon_color method"""
|
|
161
158
|
contest = Contest(problem_quantity=3)
|
|
162
|
-
|
|
159
|
+
|
|
163
160
|
# Initially no colors
|
|
164
161
|
assert contest.balloon_color is None
|
|
165
|
-
|
|
162
|
+
|
|
166
163
|
# Fill with default colors
|
|
167
164
|
contest.fill_balloon_color()
|
|
168
|
-
|
|
165
|
+
|
|
169
166
|
assert contest.balloon_color is not None
|
|
170
167
|
assert len(contest.balloon_color) == 3
|
|
171
168
|
assert all(isinstance(color, Color) for color in contest.balloon_color)
|
|
172
|
-
|
|
169
|
+
|
|
173
170
|
# Check first few default colors
|
|
174
171
|
assert contest.balloon_color[0].background_color == "rgba(189, 14, 14, 0.7)"
|
|
175
172
|
assert contest.balloon_color[0].color == "#fff"
|
|
@@ -185,20 +182,20 @@ class TestContest:
|
|
|
185
182
|
end_time=1234567890 + 5 * 60 * 60,
|
|
186
183
|
problem_quantity=3,
|
|
187
184
|
organization="Complex Org",
|
|
188
|
-
medal="ccpc" # Use preset instead of dict
|
|
185
|
+
medal="ccpc", # Use preset instead of dict
|
|
189
186
|
)
|
|
190
|
-
|
|
187
|
+
|
|
191
188
|
# Add problem IDs and colors
|
|
192
189
|
contest.fill_problem_id()
|
|
193
190
|
contest.fill_balloon_color()
|
|
194
|
-
|
|
191
|
+
|
|
195
192
|
# Add logo
|
|
196
193
|
contest.logo = Image(url="https://example.com/logo.png")
|
|
197
|
-
|
|
194
|
+
|
|
198
195
|
# Test serialization
|
|
199
196
|
contest_json = contest.model_dump_json()
|
|
200
197
|
reconstructed_contest = Contest.model_validate_json(contest_json)
|
|
201
|
-
|
|
198
|
+
|
|
202
199
|
# Verify all data is preserved
|
|
203
200
|
assert reconstructed_contest.contest_name == contest.contest_name
|
|
204
201
|
assert reconstructed_contest.start_time == contest.start_time
|
|
@@ -212,18 +209,15 @@ class TestContest:
|
|
|
212
209
|
"""Test contest with custom group and status display"""
|
|
213
210
|
custom_group = {"team_a": "Team A", "team_b": "Team B"}
|
|
214
211
|
custom_status = {"show_penalty": True, "show_time": False}
|
|
215
|
-
|
|
216
|
-
contest = Contest(
|
|
217
|
-
|
|
218
|
-
status_time_display=custom_status
|
|
219
|
-
)
|
|
220
|
-
|
|
212
|
+
|
|
213
|
+
contest = Contest(group=custom_group, status_time_display=custom_status)
|
|
214
|
+
|
|
221
215
|
# Custom values should override defaults
|
|
222
216
|
assert contest.group == custom_group
|
|
223
217
|
assert contest.status_time_display == custom_status
|
|
224
|
-
|
|
218
|
+
|
|
225
219
|
# Test serialization preserves custom values
|
|
226
220
|
contest_json = contest.model_dump_json()
|
|
227
221
|
reconstructed = Contest.model_validate_json(contest_json)
|
|
228
222
|
assert reconstructed.group == custom_group
|
|
229
|
-
assert reconstructed.status_time_display == custom_status
|
|
223
|
+
assert reconstructed.status_time_display == custom_status
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .client import ApiClient
|
|
2
|
+
from .models import (
|
|
3
|
+
HTTPValidationError,
|
|
4
|
+
UploadBoardDataReq,
|
|
5
|
+
UploadBoardDataResp,
|
|
6
|
+
ValidationError,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
ApiClient,
|
|
11
|
+
HTTPValidationError,
|
|
12
|
+
UploadBoardDataReq,
|
|
13
|
+
UploadBoardDataResp,
|
|
14
|
+
ValidationError,
|
|
15
|
+
]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
import aiohttp
|
|
4
|
+
|
|
5
|
+
from .models import UploadBoardDataReq, UploadBoardDataResp
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ApiClient:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
base_url: str = "https://board-admin.xcpcio.com",
|
|
12
|
+
token: Optional[str] = None,
|
|
13
|
+
timeout: int = 10,
|
|
14
|
+
):
|
|
15
|
+
self._base_url = base_url.rstrip("/")
|
|
16
|
+
self._token = token
|
|
17
|
+
self._timeout = aiohttp.ClientTimeout(total=timeout)
|
|
18
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
19
|
+
|
|
20
|
+
async def __aenter__(self):
|
|
21
|
+
self._session = aiohttp.ClientSession(timeout=self._timeout)
|
|
22
|
+
return self
|
|
23
|
+
|
|
24
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
25
|
+
if self._session:
|
|
26
|
+
await self._session.close()
|
|
27
|
+
|
|
28
|
+
async def _ensure_session(self):
|
|
29
|
+
if self._session is None:
|
|
30
|
+
self._session = aiohttp.ClientSession(timeout=self._timeout)
|
|
31
|
+
|
|
32
|
+
async def _ensure_token(self):
|
|
33
|
+
if self._token is None:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
"Token is required for this operation. Please provide a token when initializing the client."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
async def close(self):
|
|
39
|
+
if self._session:
|
|
40
|
+
await self._session.close()
|
|
41
|
+
self._session = None
|
|
42
|
+
|
|
43
|
+
async def ping(self) -> Dict[str, Any]:
|
|
44
|
+
await self._ensure_session()
|
|
45
|
+
async with self._session.get(f"{self._base_url}/api/ping") as response:
|
|
46
|
+
response.raise_for_status()
|
|
47
|
+
return await response.json()
|
|
48
|
+
|
|
49
|
+
async def upload_board_data(
|
|
50
|
+
self,
|
|
51
|
+
config: Optional[str] = None,
|
|
52
|
+
teams: Optional[str] = None,
|
|
53
|
+
submissions: Optional[str] = None,
|
|
54
|
+
extra_files: Optional[Dict[str, str]] = None,
|
|
55
|
+
) -> UploadBoardDataResp:
|
|
56
|
+
await self._ensure_session()
|
|
57
|
+
await self._ensure_token()
|
|
58
|
+
|
|
59
|
+
request_data = UploadBoardDataReq(
|
|
60
|
+
token=self._token,
|
|
61
|
+
config=config,
|
|
62
|
+
teams=teams,
|
|
63
|
+
submissions=submissions,
|
|
64
|
+
extra_files=extra_files,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
async with self._session.post(
|
|
68
|
+
f"{self._base_url}/api/upload-board-data",
|
|
69
|
+
json=request_data.model_dump(exclude_none=True),
|
|
70
|
+
headers={"Content-Type": "application/json"},
|
|
71
|
+
) as response:
|
|
72
|
+
response.raise_for_status()
|
|
73
|
+
data = await response.json()
|
|
74
|
+
return UploadBoardDataResp(**data)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Dict, List, Optional, Union
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ValidationError(BaseModel):
|
|
7
|
+
loc: List[Union[str, int]] = Field(..., title="Location")
|
|
8
|
+
msg: str = Field(..., title="Message")
|
|
9
|
+
type: str = Field(..., title="Error Type")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HTTPValidationError(BaseModel):
|
|
13
|
+
detail: Optional[List[ValidationError]] = Field(None, title="Detail")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UploadBoardDataReq(BaseModel):
|
|
17
|
+
token: str = Field(..., title="Token")
|
|
18
|
+
config: Optional[str] = Field(None, title="Config")
|
|
19
|
+
teams: Optional[str] = Field(None, title="Teams")
|
|
20
|
+
submissions: Optional[str] = Field(None, title="Submissions")
|
|
21
|
+
extra_files: Optional[Dict[str, str]] = Field(None, title="Extra Files")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UploadBoardDataResp(BaseModel):
|
|
25
|
+
pass
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Contest API Server Application
|
|
3
|
+
|
|
4
|
+
FastAPI application instance for Contest API Server.
|
|
5
|
+
This module can be used directly with uvicorn for reload support.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
10
|
+
|
|
11
|
+
from xcpcio.__version__ import __version__
|
|
12
|
+
|
|
13
|
+
from .routes import create_router
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_app() -> FastAPI:
|
|
17
|
+
"""
|
|
18
|
+
Create and configure FastAPI application.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Configured FastAPI application instance
|
|
22
|
+
"""
|
|
23
|
+
app = FastAPI(
|
|
24
|
+
title="Contest API Server",
|
|
25
|
+
description="REST API for Contest Control System specifications",
|
|
26
|
+
version=__version__,
|
|
27
|
+
docs_url="/docs",
|
|
28
|
+
redoc_url="/redoc",
|
|
29
|
+
openapi_url="/openapi.json",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
app.add_middleware(
|
|
33
|
+
CORSMiddleware,
|
|
34
|
+
allow_origins=["*"],
|
|
35
|
+
allow_credentials=True,
|
|
36
|
+
allow_methods=["*"],
|
|
37
|
+
allow_headers=["*"],
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
router = create_router()
|
|
41
|
+
app.include_router(router)
|
|
42
|
+
|
|
43
|
+
return app
|
|
@@ -9,10 +9,9 @@ from typing import Annotated, Dict
|
|
|
9
9
|
|
|
10
10
|
from fastapi import Depends
|
|
11
11
|
|
|
12
|
-
from xcpcio.
|
|
13
|
-
from xcpcio.ccs.reader.contest_package_reader import ContestPackageReader
|
|
12
|
+
from xcpcio.clics.reader import BaseContestReader, ContestPackageReader
|
|
14
13
|
|
|
15
|
-
from .services
|
|
14
|
+
from .services import ContestService
|
|
16
15
|
|
|
17
16
|
_contest_service_instance = None
|
|
18
17
|
|
|
@@ -43,7 +42,7 @@ def configure_dependencies(contest_package_dir: Path) -> None:
|
|
|
43
42
|
contest_package_dir: Path to contest package directory
|
|
44
43
|
"""
|
|
45
44
|
global _contest_service_instance
|
|
46
|
-
reader_dict: Dict[str,
|
|
45
|
+
reader_dict: Dict[str, BaseContestReader] = {}
|
|
47
46
|
contest_package_reader = ContestPackageReader(contest_package_dir)
|
|
48
47
|
reader_dict[contest_package_reader.get_contest_id()] = contest_package_reader
|
|
49
48
|
_contest_service_instance = ContestService(reader_dict)
|