xcpcio 0.58.2__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.58.2/PKG-INFO +21 -0
- xcpcio-0.58.2/README.md +1 -0
- xcpcio-0.58.2/pyproject.toml +29 -0
- xcpcio-0.58.2/setup.cfg +4 -0
- xcpcio-0.58.2/tests/test_contest.py +229 -0
- xcpcio-0.58.2/tests/test_submission.py +227 -0
- xcpcio-0.58.2/tests/test_team.py +238 -0
- xcpcio-0.58.2/tests/test_types.py +135 -0
- xcpcio-0.58.2/xcpcio/__init__.py +5 -0
- xcpcio-0.58.2/xcpcio/constants.py +66 -0
- xcpcio-0.58.2/xcpcio/types.py +204 -0
- xcpcio-0.58.2/xcpcio.egg-info/PKG-INFO +21 -0
- xcpcio-0.58.2/xcpcio.egg-info/SOURCES.txt +14 -0
- xcpcio-0.58.2/xcpcio.egg-info/dependency_links.txt +1 -0
- xcpcio-0.58.2/xcpcio.egg-info/requires.txt +1 -0
- xcpcio-0.58.2/xcpcio.egg-info/top_level.txt +1 -0
xcpcio-0.58.2/PKG-INFO
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xcpcio
|
|
3
|
+
Version: 0.58.2
|
|
4
|
+
Summary: xcpcio python lib
|
|
5
|
+
Author-email: Dup4 <hi@dup4.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: homepage, https://github.com/xcpcio/xcpcio
|
|
8
|
+
Project-URL: documentation, https://github.com/xcpcio/xcpcio
|
|
9
|
+
Project-URL: repository, https://github.com/xcpcio/xcpcio
|
|
10
|
+
Keywords: xcpcio
|
|
11
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: pydantic>=2.11.7
|
|
20
|
+
|
|
21
|
+
# xcpcio-python
|
xcpcio-0.58.2/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# xcpcio-python
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "xcpcio"
|
|
3
|
+
version = "0.58.2"
|
|
4
|
+
description = "xcpcio python lib"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [ { name = "Dup4", email = "hi@dup4.com" } ]
|
|
7
|
+
license = "MIT"
|
|
8
|
+
keywords = [ "xcpcio" ]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Topic :: Software Development :: Build Tools",
|
|
11
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
]
|
|
17
|
+
requires-python = ">=3.11"
|
|
18
|
+
dependencies = [ "pydantic>=2.11.7", ]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
homepage = "https://github.com/xcpcio/xcpcio"
|
|
22
|
+
documentation = "https://github.com/xcpcio/xcpcio"
|
|
23
|
+
repository = "https://github.com/xcpcio/xcpcio"
|
|
24
|
+
|
|
25
|
+
[tool.ruff]
|
|
26
|
+
line-length = 120
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = [ "pytest>=8.4.2", "ruff>=0.4.0", ]
|
xcpcio-0.58.2/setup.cfg
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from xcpcio import constants
|
|
4
|
+
from xcpcio.types import Color, Contest, ContestOptions, Image
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestContest:
|
|
8
|
+
"""Test cases for Contest Pydantic model"""
|
|
9
|
+
|
|
10
|
+
def test_contest_creation_defaults(self):
|
|
11
|
+
"""Test Contest creation with default values"""
|
|
12
|
+
contest = Contest()
|
|
13
|
+
|
|
14
|
+
assert contest.contest_name == ""
|
|
15
|
+
assert contest.start_time == 0
|
|
16
|
+
assert contest.end_time == 0
|
|
17
|
+
assert contest.frozen_time == 60 * 60 # 1 hour
|
|
18
|
+
assert contest.unfrozen_time == 0x3F3F3F3F3F3F3F3F
|
|
19
|
+
assert contest.penalty == 20 * 60 # 20 minutes
|
|
20
|
+
assert contest.problem_quantity == 0
|
|
21
|
+
assert contest.problem_id == []
|
|
22
|
+
assert contest.organization == "School"
|
|
23
|
+
assert contest.medal is None
|
|
24
|
+
assert contest.balloon_color is None
|
|
25
|
+
assert contest.logo is None
|
|
26
|
+
assert contest.banner is None
|
|
27
|
+
assert contest.banner_mode is None
|
|
28
|
+
assert contest.badge is None
|
|
29
|
+
assert contest.group is None
|
|
30
|
+
assert contest.tag is None
|
|
31
|
+
assert contest.board_link is None
|
|
32
|
+
assert contest.version is None
|
|
33
|
+
|
|
34
|
+
# Check default values
|
|
35
|
+
assert contest.status_time_display == constants.FULL_STATUS_TIME_DISPLAY
|
|
36
|
+
assert isinstance(contest.options, ContestOptions)
|
|
37
|
+
|
|
38
|
+
def test_contest_creation_with_values(self):
|
|
39
|
+
"""Test Contest creation with provided values"""
|
|
40
|
+
contest_options = ContestOptions(
|
|
41
|
+
calculation_of_penalty=constants.CALCULATION_OF_PENALTY_IN_SECONDS,
|
|
42
|
+
has_reaction_videos=True
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
contest = Contest(
|
|
46
|
+
contest_name="ICPC World Finals 2024",
|
|
47
|
+
start_time=1234567890,
|
|
48
|
+
end_time=1234567890 + 5 * 60 * 60, # 5 hours later
|
|
49
|
+
frozen_time=60 * 60, # 1 hour before end
|
|
50
|
+
penalty=20 * 60, # 20 minutes
|
|
51
|
+
problem_quantity=12,
|
|
52
|
+
problem_id=["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"],
|
|
53
|
+
organization="ICPC",
|
|
54
|
+
medal="icpc", # Use Literal value
|
|
55
|
+
options=contest_options
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
assert contest.contest_name == "ICPC World Finals 2024"
|
|
59
|
+
assert contest.start_time == 1234567890
|
|
60
|
+
assert contest.end_time == 1234567890 + 5 * 60 * 60
|
|
61
|
+
assert contest.problem_quantity == 12
|
|
62
|
+
assert len(contest.problem_id) == 12
|
|
63
|
+
assert contest.problem_id[0] == "A"
|
|
64
|
+
assert contest.problem_id[-1] == "L"
|
|
65
|
+
assert contest.organization == "ICPC"
|
|
66
|
+
assert contest.medal == "icpc"
|
|
67
|
+
assert contest.options == contest_options
|
|
68
|
+
|
|
69
|
+
def test_contest_serialization(self):
|
|
70
|
+
"""Test Contest serialization and deserialization"""
|
|
71
|
+
contest = Contest(
|
|
72
|
+
contest_name="Test Contest",
|
|
73
|
+
start_time=1000000000,
|
|
74
|
+
end_time=1000000000 + 60 * 60 * 5,
|
|
75
|
+
problem_quantity=5,
|
|
76
|
+
problem_id=["A", "B", "C", "D", "E"],
|
|
77
|
+
organization="Test Org"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Test model_dump
|
|
81
|
+
contest_dict = contest.model_dump()
|
|
82
|
+
assert contest_dict["contest_name"] == "Test Contest"
|
|
83
|
+
assert contest_dict["start_time"] == 1000000000
|
|
84
|
+
assert contest_dict["problem_quantity"] == 5
|
|
85
|
+
assert contest_dict["organization"] == "Test Org"
|
|
86
|
+
|
|
87
|
+
# Test JSON round-trip
|
|
88
|
+
contest_json = contest.model_dump_json()
|
|
89
|
+
reconstructed_contest = Contest.model_validate_json(contest_json)
|
|
90
|
+
assert reconstructed_contest == contest
|
|
91
|
+
|
|
92
|
+
def test_contest_with_colors_and_images(self):
|
|
93
|
+
"""Test Contest with balloon colors, logo, and banner"""
|
|
94
|
+
colors = [
|
|
95
|
+
Color(color="#fff", background_color="rgba(255, 0, 0, 0.7)"),
|
|
96
|
+
Color(color="#000", background_color="rgba(0, 255, 0, 0.7)")
|
|
97
|
+
]
|
|
98
|
+
logo = Image(url="https://example.com/logo.png", type="png")
|
|
99
|
+
banner = Image(url="https://example.com/banner.jpg", type="jpg")
|
|
100
|
+
|
|
101
|
+
contest = Contest(
|
|
102
|
+
contest_name="Contest with Media",
|
|
103
|
+
problem_quantity=2,
|
|
104
|
+
balloon_color=colors,
|
|
105
|
+
logo=logo,
|
|
106
|
+
banner=banner,
|
|
107
|
+
banner_mode="ALL" # Use correct Literal value
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
assert len(contest.balloon_color) == 2
|
|
111
|
+
assert contest.balloon_color[0].color == "#fff"
|
|
112
|
+
assert contest.balloon_color[1].background_color == "rgba(0, 255, 0, 0.7)"
|
|
113
|
+
assert contest.logo.url == "https://example.com/logo.png"
|
|
114
|
+
assert contest.banner.url == "https://example.com/banner.jpg"
|
|
115
|
+
assert contest.banner_mode == "ALL"
|
|
116
|
+
|
|
117
|
+
def test_append_balloon_color(self):
|
|
118
|
+
"""Test append_balloon_color method"""
|
|
119
|
+
contest = Contest()
|
|
120
|
+
|
|
121
|
+
# Initially no colors
|
|
122
|
+
assert contest.balloon_color is None
|
|
123
|
+
|
|
124
|
+
# Add first color
|
|
125
|
+
red_color = Color(color="#fff", background_color="red")
|
|
126
|
+
contest.append_balloon_color(red_color)
|
|
127
|
+
|
|
128
|
+
assert contest.balloon_color is not None
|
|
129
|
+
assert len(contest.balloon_color) == 1
|
|
130
|
+
assert contest.balloon_color[0] == red_color
|
|
131
|
+
|
|
132
|
+
# Add second color
|
|
133
|
+
blue_color = Color(color="#fff", background_color="blue")
|
|
134
|
+
contest.append_balloon_color(blue_color)
|
|
135
|
+
|
|
136
|
+
assert len(contest.balloon_color) == 2
|
|
137
|
+
assert contest.balloon_color[1] == blue_color
|
|
138
|
+
|
|
139
|
+
def test_fill_problem_id(self):
|
|
140
|
+
"""Test fill_problem_id method"""
|
|
141
|
+
contest = Contest(problem_quantity=5)
|
|
142
|
+
|
|
143
|
+
# Initially empty
|
|
144
|
+
assert contest.problem_id == []
|
|
145
|
+
|
|
146
|
+
# Fill with A-E
|
|
147
|
+
contest.fill_problem_id()
|
|
148
|
+
|
|
149
|
+
assert len(contest.problem_id) == 5
|
|
150
|
+
assert contest.problem_id == ["A", "B", "C", "D", "E"]
|
|
151
|
+
|
|
152
|
+
# Test with larger quantity
|
|
153
|
+
contest.problem_quantity = 10
|
|
154
|
+
contest.fill_problem_id()
|
|
155
|
+
|
|
156
|
+
assert len(contest.problem_id) == 10
|
|
157
|
+
assert contest.problem_id == ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]
|
|
158
|
+
|
|
159
|
+
def test_fill_balloon_color(self):
|
|
160
|
+
"""Test fill_balloon_color method"""
|
|
161
|
+
contest = Contest(problem_quantity=3)
|
|
162
|
+
|
|
163
|
+
# Initially no colors
|
|
164
|
+
assert contest.balloon_color is None
|
|
165
|
+
|
|
166
|
+
# Fill with default colors
|
|
167
|
+
contest.fill_balloon_color()
|
|
168
|
+
|
|
169
|
+
assert contest.balloon_color is not None
|
|
170
|
+
assert len(contest.balloon_color) == 3
|
|
171
|
+
assert all(isinstance(color, Color) for color in contest.balloon_color)
|
|
172
|
+
|
|
173
|
+
# Check first few default colors
|
|
174
|
+
assert contest.balloon_color[0].background_color == "rgba(189, 14, 14, 0.7)"
|
|
175
|
+
assert contest.balloon_color[0].color == "#fff"
|
|
176
|
+
assert contest.balloon_color[1].background_color == "rgba(149, 31, 217, 0.7)"
|
|
177
|
+
assert contest.balloon_color[1].color == "#fff"
|
|
178
|
+
|
|
179
|
+
def test_contest_round_trip_serialization(self):
|
|
180
|
+
"""Test complete round-trip serialization with complex data"""
|
|
181
|
+
# Create a contest with all features
|
|
182
|
+
contest = Contest(
|
|
183
|
+
contest_name="Complex Contest",
|
|
184
|
+
start_time=1234567890,
|
|
185
|
+
end_time=1234567890 + 5 * 60 * 60,
|
|
186
|
+
problem_quantity=3,
|
|
187
|
+
organization="Complex Org",
|
|
188
|
+
medal="ccpc" # Use preset instead of dict
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Add problem IDs and colors
|
|
192
|
+
contest.fill_problem_id()
|
|
193
|
+
contest.fill_balloon_color()
|
|
194
|
+
|
|
195
|
+
# Add logo
|
|
196
|
+
contest.logo = Image(url="https://example.com/logo.png")
|
|
197
|
+
|
|
198
|
+
# Test serialization
|
|
199
|
+
contest_json = contest.model_dump_json()
|
|
200
|
+
reconstructed_contest = Contest.model_validate_json(contest_json)
|
|
201
|
+
|
|
202
|
+
# Verify all data is preserved
|
|
203
|
+
assert reconstructed_contest.contest_name == contest.contest_name
|
|
204
|
+
assert reconstructed_contest.start_time == contest.start_time
|
|
205
|
+
assert reconstructed_contest.end_time == contest.end_time
|
|
206
|
+
assert reconstructed_contest.problem_id == contest.problem_id
|
|
207
|
+
assert reconstructed_contest.medal == contest.medal
|
|
208
|
+
assert len(reconstructed_contest.balloon_color) == len(contest.balloon_color)
|
|
209
|
+
assert reconstructed_contest.logo.url == contest.logo.url
|
|
210
|
+
|
|
211
|
+
def test_custom_group_and_status_display(self):
|
|
212
|
+
"""Test contest with custom group and status display"""
|
|
213
|
+
custom_group = {"team_a": "Team A", "team_b": "Team B"}
|
|
214
|
+
custom_status = {"show_penalty": True, "show_time": False}
|
|
215
|
+
|
|
216
|
+
contest = Contest(
|
|
217
|
+
group=custom_group,
|
|
218
|
+
status_time_display=custom_status
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Custom values should override defaults
|
|
222
|
+
assert contest.group == custom_group
|
|
223
|
+
assert contest.status_time_display == custom_status
|
|
224
|
+
|
|
225
|
+
# Test serialization preserves custom values
|
|
226
|
+
contest_json = contest.model_dump_json()
|
|
227
|
+
reconstructed = Contest.model_validate_json(contest_json)
|
|
228
|
+
assert reconstructed.group == custom_group
|
|
229
|
+
assert reconstructed.status_time_display == custom_status
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from xcpcio import constants
|
|
6
|
+
from xcpcio.types import Reaction, Submission, Submissions
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestSubmission:
|
|
10
|
+
"""Test cases for Submission Pydantic model"""
|
|
11
|
+
|
|
12
|
+
def test_submission_creation_defaults(self):
|
|
13
|
+
"""Test Submission creation with default values"""
|
|
14
|
+
submission = Submission()
|
|
15
|
+
assert submission.id == ""
|
|
16
|
+
assert submission.team_id == ""
|
|
17
|
+
assert submission.problem_id == 0
|
|
18
|
+
assert submission.timestamp == 0
|
|
19
|
+
assert submission.status == constants.SUBMISSION_STATUS_UNKNOWN
|
|
20
|
+
assert submission.time is None
|
|
21
|
+
assert submission.language is None
|
|
22
|
+
assert submission.is_ignore is None
|
|
23
|
+
assert submission.reaction is None
|
|
24
|
+
|
|
25
|
+
def test_submission_creation_with_values(self):
|
|
26
|
+
"""Test Submission creation with provided values"""
|
|
27
|
+
reaction = Reaction(url="https://reaction.com/video.mp4")
|
|
28
|
+
submission = Submission(
|
|
29
|
+
id="sub_001",
|
|
30
|
+
status=constants.SUBMISSION_STATUS_ACCEPTED,
|
|
31
|
+
team_id="team001",
|
|
32
|
+
problem_id=1,
|
|
33
|
+
timestamp=1234567890,
|
|
34
|
+
time=120,
|
|
35
|
+
language="Python",
|
|
36
|
+
is_ignore=False,
|
|
37
|
+
reaction=reaction,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
assert submission.id == "sub_001"
|
|
41
|
+
assert submission.status == constants.SUBMISSION_STATUS_ACCEPTED
|
|
42
|
+
assert submission.team_id == "team001"
|
|
43
|
+
assert submission.problem_id == 1
|
|
44
|
+
assert submission.timestamp == 1234567890
|
|
45
|
+
assert submission.time == 120
|
|
46
|
+
assert submission.language == "Python"
|
|
47
|
+
assert not submission.is_ignore
|
|
48
|
+
assert submission.reaction == reaction
|
|
49
|
+
assert submission.reaction.url == "https://reaction.com/video.mp4"
|
|
50
|
+
|
|
51
|
+
def test_submission_serialization(self):
|
|
52
|
+
"""Test Submission serialization and deserialization"""
|
|
53
|
+
submission = Submission(
|
|
54
|
+
id="sub_002",
|
|
55
|
+
status=constants.SUBMISSION_STATUS_WRONG_ANSWER,
|
|
56
|
+
team_id="team002",
|
|
57
|
+
problem_id=3,
|
|
58
|
+
timestamp=1234567891,
|
|
59
|
+
time=300,
|
|
60
|
+
language="C++",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Test model_dump
|
|
64
|
+
submission_dict = submission.model_dump()
|
|
65
|
+
assert submission_dict["id"] == "sub_002"
|
|
66
|
+
assert submission_dict["status"] == constants.SUBMISSION_STATUS_WRONG_ANSWER
|
|
67
|
+
assert submission_dict["team_id"] == "team002"
|
|
68
|
+
assert submission_dict["problem_id"] == 3
|
|
69
|
+
assert submission_dict["timestamp"] == 1234567891
|
|
70
|
+
assert submission_dict["time"] == 300
|
|
71
|
+
assert submission_dict["language"] == "C++"
|
|
72
|
+
assert submission_dict["is_ignore"] is None
|
|
73
|
+
assert submission_dict["reaction"] is None
|
|
74
|
+
|
|
75
|
+
# Test JSON round-trip
|
|
76
|
+
submission_json = submission.model_dump_json()
|
|
77
|
+
reconstructed_submission = Submission.model_validate_json(submission_json)
|
|
78
|
+
assert reconstructed_submission == submission
|
|
79
|
+
|
|
80
|
+
def test_submission_with_reaction_serialization(self):
|
|
81
|
+
"""Test Submission with Reaction serialization"""
|
|
82
|
+
reaction = Reaction(url="https://example.com/reaction.mp4")
|
|
83
|
+
submission = Submission(
|
|
84
|
+
id="sub_003",
|
|
85
|
+
status=constants.SUBMISSION_STATUS_ACCEPTED,
|
|
86
|
+
team_id="team003",
|
|
87
|
+
problem_id=2,
|
|
88
|
+
timestamp=1234567892,
|
|
89
|
+
reaction=reaction,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Test model_dump
|
|
93
|
+
submission_dict = submission.model_dump()
|
|
94
|
+
assert submission_dict["reaction"]["url"] == "https://example.com/reaction.mp4"
|
|
95
|
+
|
|
96
|
+
# Test JSON round-trip
|
|
97
|
+
submission_json = submission.model_dump_json()
|
|
98
|
+
reconstructed_submission = Submission.model_validate_json(submission_json)
|
|
99
|
+
assert reconstructed_submission == submission
|
|
100
|
+
assert reconstructed_submission.reaction.url == "https://example.com/reaction.mp4"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestSubmissions:
|
|
104
|
+
"""Test cases for Submissions RootModel"""
|
|
105
|
+
|
|
106
|
+
@pytest.fixture
|
|
107
|
+
def sample_submissions(self) -> Submissions:
|
|
108
|
+
"""Create sample submissions for testing"""
|
|
109
|
+
return Submissions(
|
|
110
|
+
[
|
|
111
|
+
Submission(
|
|
112
|
+
id="sub_001",
|
|
113
|
+
status=constants.SUBMISSION_STATUS_ACCEPTED,
|
|
114
|
+
team_id="team001",
|
|
115
|
+
problem_id=1,
|
|
116
|
+
timestamp=1234567890,
|
|
117
|
+
time=120,
|
|
118
|
+
language="Python",
|
|
119
|
+
),
|
|
120
|
+
Submission(
|
|
121
|
+
id="sub_002",
|
|
122
|
+
status=constants.SUBMISSION_STATUS_WRONG_ANSWER,
|
|
123
|
+
team_id="team002",
|
|
124
|
+
problem_id=2,
|
|
125
|
+
timestamp=1234567891,
|
|
126
|
+
time=300,
|
|
127
|
+
language="C++",
|
|
128
|
+
reaction=Reaction(url="https://reaction.com/video.mp4"),
|
|
129
|
+
),
|
|
130
|
+
Submission(
|
|
131
|
+
id="sub_003",
|
|
132
|
+
status=constants.SUBMISSION_STATUS_TIME_LIMIT_EXCEEDED,
|
|
133
|
+
team_id="team003",
|
|
134
|
+
problem_id=1,
|
|
135
|
+
timestamp=1234567892,
|
|
136
|
+
time=600,
|
|
137
|
+
language="Java",
|
|
138
|
+
),
|
|
139
|
+
]
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def test_submissions_basic_operations(self, sample_submissions: Submissions):
|
|
143
|
+
"""Test basic Submissions operations"""
|
|
144
|
+
assert len(sample_submissions.root) == 3
|
|
145
|
+
|
|
146
|
+
# Test iteration
|
|
147
|
+
statuses = [sub.status for sub in sample_submissions.root]
|
|
148
|
+
assert constants.SUBMISSION_STATUS_ACCEPTED in statuses
|
|
149
|
+
assert constants.SUBMISSION_STATUS_WRONG_ANSWER in statuses
|
|
150
|
+
assert constants.SUBMISSION_STATUS_TIME_LIMIT_EXCEEDED in statuses
|
|
151
|
+
|
|
152
|
+
def test_submissions_serialization(self, sample_submissions: Submissions):
|
|
153
|
+
"""Test Submissions serialization and deserialization"""
|
|
154
|
+
# Test JSON serialization
|
|
155
|
+
submissions_json = sample_submissions.model_dump_json()
|
|
156
|
+
assert isinstance(submissions_json, str)
|
|
157
|
+
|
|
158
|
+
# Verify it's a valid JSON array
|
|
159
|
+
parsed = json.loads(submissions_json)
|
|
160
|
+
assert isinstance(parsed, list)
|
|
161
|
+
assert len(parsed) == 3
|
|
162
|
+
|
|
163
|
+
# Check first submission data
|
|
164
|
+
assert parsed[0]["status"] == constants.SUBMISSION_STATUS_ACCEPTED
|
|
165
|
+
assert parsed[0]["team_id"] == "team001"
|
|
166
|
+
assert parsed[0]["problem_id"] == 1
|
|
167
|
+
|
|
168
|
+
# Test JSON deserialization
|
|
169
|
+
reconstructed_submissions = Submissions.model_validate_json(submissions_json)
|
|
170
|
+
assert len(reconstructed_submissions.root) == 3
|
|
171
|
+
|
|
172
|
+
# Verify the data is correct
|
|
173
|
+
assert reconstructed_submissions.root[0].status == constants.SUBMISSION_STATUS_ACCEPTED
|
|
174
|
+
assert reconstructed_submissions.root[1].status == constants.SUBMISSION_STATUS_WRONG_ANSWER
|
|
175
|
+
assert reconstructed_submissions.root[2].status == constants.SUBMISSION_STATUS_TIME_LIMIT_EXCEEDED
|
|
176
|
+
|
|
177
|
+
# Check reaction is preserved
|
|
178
|
+
assert reconstructed_submissions.root[1].reaction.url == "https://reaction.com/video.mp4"
|
|
179
|
+
|
|
180
|
+
def test_submissions_round_trip(self, sample_submissions: Submissions):
|
|
181
|
+
"""Test complete round-trip serialization"""
|
|
182
|
+
# Dict round-trip
|
|
183
|
+
submissions_dict = sample_submissions.model_dump()
|
|
184
|
+
reconstructed_from_dict = Submissions.model_validate(submissions_dict)
|
|
185
|
+
assert len(reconstructed_from_dict.root) == len(sample_submissions.root)
|
|
186
|
+
|
|
187
|
+
# JSON round-trip
|
|
188
|
+
submissions_json = sample_submissions.model_dump_json()
|
|
189
|
+
reconstructed_from_json = Submissions.model_validate_json(submissions_json)
|
|
190
|
+
assert len(reconstructed_from_json.root) == len(sample_submissions.root)
|
|
191
|
+
|
|
192
|
+
def test_empty_submissions(self):
|
|
193
|
+
"""Test operations with empty Submissions"""
|
|
194
|
+
empty_submissions = Submissions([])
|
|
195
|
+
|
|
196
|
+
assert len(empty_submissions.root) == 0
|
|
197
|
+
|
|
198
|
+
# Test serialization
|
|
199
|
+
submissions_json = empty_submissions.model_dump_json()
|
|
200
|
+
assert submissions_json == "[]"
|
|
201
|
+
|
|
202
|
+
# Test deserialization
|
|
203
|
+
reconstructed = Submissions.model_validate_json("[]")
|
|
204
|
+
assert len(reconstructed.root) == 0
|
|
205
|
+
|
|
206
|
+
def test_submissions_with_minimal_data(self):
|
|
207
|
+
"""Test Submissions with minimal submission data"""
|
|
208
|
+
minimal_submissions = Submissions(
|
|
209
|
+
[
|
|
210
|
+
Submission(
|
|
211
|
+
id="sub_min",
|
|
212
|
+
status=constants.SUBMISSION_STATUS_ACCEPTED,
|
|
213
|
+
team_id="team001",
|
|
214
|
+
problem_id=1,
|
|
215
|
+
timestamp=123,
|
|
216
|
+
),
|
|
217
|
+
Submission(), # All defaults
|
|
218
|
+
]
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Test serialization
|
|
222
|
+
submissions_json = minimal_submissions.model_dump_json()
|
|
223
|
+
reconstructed = Submissions.model_validate_json(submissions_json)
|
|
224
|
+
|
|
225
|
+
assert len(reconstructed.root) == 2
|
|
226
|
+
assert reconstructed.root[0].status == constants.SUBMISSION_STATUS_ACCEPTED
|
|
227
|
+
assert reconstructed.root[1].status == constants.SUBMISSION_STATUS_UNKNOWN # Default value
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from xcpcio.types import Team, Teams
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestTeam:
|
|
9
|
+
"""Test cases for the Team dataclass"""
|
|
10
|
+
|
|
11
|
+
def test_team_creation(self):
|
|
12
|
+
"""Test basic team creation with default values"""
|
|
13
|
+
team = Team()
|
|
14
|
+
assert team.id == ""
|
|
15
|
+
assert team.name == ""
|
|
16
|
+
assert team.organization == ""
|
|
17
|
+
assert team.members is None
|
|
18
|
+
assert team.coach is None
|
|
19
|
+
assert team.location is None
|
|
20
|
+
assert team.group == []
|
|
21
|
+
assert team.extra == {}
|
|
22
|
+
|
|
23
|
+
def test_team_creation_with_values(self):
|
|
24
|
+
"""Test team creation with provided values"""
|
|
25
|
+
team = Team(
|
|
26
|
+
id="team001",
|
|
27
|
+
name="Test Team",
|
|
28
|
+
organization="Test University",
|
|
29
|
+
members=["Alice", "Bob", "Charlie"],
|
|
30
|
+
coach="Dr. Smith",
|
|
31
|
+
location="Building A",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
assert team.id == "team001"
|
|
35
|
+
assert team.name == "Test Team"
|
|
36
|
+
assert team.organization == "Test University"
|
|
37
|
+
assert team.members == ["Alice", "Bob", "Charlie"]
|
|
38
|
+
assert team.coach == "Dr. Smith"
|
|
39
|
+
assert team.location == "Building A"
|
|
40
|
+
|
|
41
|
+
def test_group_management(self):
|
|
42
|
+
"""Test add_group and remove_group methods"""
|
|
43
|
+
team = Team(id="test", name="Test Team", organization="Test Org")
|
|
44
|
+
|
|
45
|
+
# Test adding groups
|
|
46
|
+
team.add_group("undergraduate")
|
|
47
|
+
assert "undergraduate" in team.group
|
|
48
|
+
|
|
49
|
+
team.add_group("local")
|
|
50
|
+
assert "local" in team.group
|
|
51
|
+
assert len(team.group) == 2
|
|
52
|
+
|
|
53
|
+
# Test that duplicate groups are not added
|
|
54
|
+
team.add_group("undergraduate")
|
|
55
|
+
assert len(team.group) == 2
|
|
56
|
+
|
|
57
|
+
# Test removing groups
|
|
58
|
+
team.remove_group("local")
|
|
59
|
+
assert "local" not in team.group
|
|
60
|
+
assert len(team.group) == 1
|
|
61
|
+
|
|
62
|
+
# Test removing non-existent group (should not raise error)
|
|
63
|
+
team.remove_group("nonexistent")
|
|
64
|
+
assert len(team.group) == 1
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestTeamSerialization:
|
|
68
|
+
"""Test cases for Team serialization and deserialization"""
|
|
69
|
+
|
|
70
|
+
@pytest.fixture
|
|
71
|
+
def sample_team(self) -> Team:
|
|
72
|
+
"""Create a sample team for testing"""
|
|
73
|
+
return Team(
|
|
74
|
+
id="team001",
|
|
75
|
+
name="Alpha Team",
|
|
76
|
+
organization="University A",
|
|
77
|
+
members=["Alice", "Bob", "Charlie"],
|
|
78
|
+
coach="Dr. Smith",
|
|
79
|
+
location="Building A",
|
|
80
|
+
group=["undergraduate", "local"],
|
|
81
|
+
extra={"room": "101", "contact": "alice@test.edu"},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def test_model_dump(self, sample_team: Team):
|
|
85
|
+
"""Test Team model_dump method"""
|
|
86
|
+
team_dict = sample_team.model_dump()
|
|
87
|
+
|
|
88
|
+
assert isinstance(team_dict, dict)
|
|
89
|
+
assert team_dict["id"] == "team001"
|
|
90
|
+
assert team_dict["name"] == "Alpha Team"
|
|
91
|
+
assert team_dict["organization"] == "University A"
|
|
92
|
+
assert team_dict["members"] == ["Alice", "Bob", "Charlie"]
|
|
93
|
+
assert team_dict["coach"] == "Dr. Smith"
|
|
94
|
+
assert team_dict["location"] == "Building A"
|
|
95
|
+
assert team_dict["group"] == ["undergraduate", "local"]
|
|
96
|
+
assert team_dict["extra"] == {"room": "101", "contact": "alice@test.edu"}
|
|
97
|
+
|
|
98
|
+
def test_model_validate(self, sample_team: Team):
|
|
99
|
+
"""Test Team model_validate method"""
|
|
100
|
+
team_dict = sample_team.model_dump()
|
|
101
|
+
reconstructed_team = Team.model_validate(team_dict)
|
|
102
|
+
|
|
103
|
+
assert reconstructed_team == sample_team
|
|
104
|
+
|
|
105
|
+
def test_model_dump_json(self, sample_team: Team):
|
|
106
|
+
"""Test Team model_dump_json method"""
|
|
107
|
+
team_json = sample_team.model_dump_json()
|
|
108
|
+
|
|
109
|
+
assert isinstance(team_json, str)
|
|
110
|
+
# Verify it's valid JSON
|
|
111
|
+
parsed = json.loads(team_json)
|
|
112
|
+
assert parsed["id"] == "team001"
|
|
113
|
+
assert parsed["name"] == "Alpha Team"
|
|
114
|
+
|
|
115
|
+
def test_model_validate_json(self, sample_team: Team):
|
|
116
|
+
"""Test Team model_validate_json method"""
|
|
117
|
+
team_json = sample_team.model_dump_json()
|
|
118
|
+
reconstructed_team = Team.model_validate_json(team_json)
|
|
119
|
+
|
|
120
|
+
assert reconstructed_team == sample_team
|
|
121
|
+
|
|
122
|
+
def test_round_trip_dict(self, sample_team: Team):
|
|
123
|
+
"""Test complete round-trip through dict serialization"""
|
|
124
|
+
team_dict = sample_team.model_dump()
|
|
125
|
+
reconstructed_team = Team.model_validate(team_dict)
|
|
126
|
+
|
|
127
|
+
assert reconstructed_team == sample_team
|
|
128
|
+
|
|
129
|
+
def test_round_trip_json(self, sample_team: Team):
|
|
130
|
+
"""Test complete round-trip through JSON serialization"""
|
|
131
|
+
team_json = sample_team.model_dump_json()
|
|
132
|
+
reconstructed_team = Team.model_validate_json(team_json)
|
|
133
|
+
|
|
134
|
+
assert reconstructed_team == sample_team
|
|
135
|
+
|
|
136
|
+
def test_minimal_team_serialization(self):
|
|
137
|
+
"""Test serialization of team with default/minimal values"""
|
|
138
|
+
minimal_team = Team()
|
|
139
|
+
|
|
140
|
+
# Test dict round-trip
|
|
141
|
+
team_dict = minimal_team.model_dump()
|
|
142
|
+
reconstructed_from_dict = Team.model_validate(team_dict)
|
|
143
|
+
assert reconstructed_from_dict == minimal_team
|
|
144
|
+
|
|
145
|
+
# Test JSON round-trip
|
|
146
|
+
team_json = minimal_team.model_dump_json()
|
|
147
|
+
reconstructed_from_json = Team.model_validate_json(team_json)
|
|
148
|
+
assert reconstructed_from_json == minimal_team
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TestTeamList:
|
|
152
|
+
"""Test cases for Teams (RootModel) serialization and deserialization"""
|
|
153
|
+
|
|
154
|
+
@pytest.fixture
|
|
155
|
+
def sample_teams(self) -> Teams:
|
|
156
|
+
"""Create a Teams instance for testing"""
|
|
157
|
+
return Teams(
|
|
158
|
+
[
|
|
159
|
+
Team(
|
|
160
|
+
id="team001",
|
|
161
|
+
name="Alpha Team",
|
|
162
|
+
organization="University A",
|
|
163
|
+
members=["Alice", "Bob"],
|
|
164
|
+
coach="Coach A",
|
|
165
|
+
),
|
|
166
|
+
Team(
|
|
167
|
+
id="team002",
|
|
168
|
+
name="Beta Team",
|
|
169
|
+
organization="University B",
|
|
170
|
+
members=["Charlie", "David", "Eve"],
|
|
171
|
+
location="Remote",
|
|
172
|
+
),
|
|
173
|
+
Team(
|
|
174
|
+
id="team003",
|
|
175
|
+
name="Gamma Team",
|
|
176
|
+
organization="University C",
|
|
177
|
+
group=["graduate", "international"],
|
|
178
|
+
extra={"sponsor": "Tech Corp"},
|
|
179
|
+
),
|
|
180
|
+
]
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def test_teams_basic_operations(self, sample_teams: Teams):
|
|
184
|
+
"""Test basic Teams operations"""
|
|
185
|
+
assert len(sample_teams.root) == 3
|
|
186
|
+
|
|
187
|
+
# Test iteration
|
|
188
|
+
team_ids = [team.id for team in sample_teams.root]
|
|
189
|
+
assert "team001" in team_ids
|
|
190
|
+
assert "team002" in team_ids
|
|
191
|
+
assert "team003" in team_ids
|
|
192
|
+
|
|
193
|
+
def test_teams_serialization(self, sample_teams: Teams):
|
|
194
|
+
"""Test Teams serialization and deserialization"""
|
|
195
|
+
# Test JSON serialization
|
|
196
|
+
teams_json = sample_teams.model_dump_json()
|
|
197
|
+
assert isinstance(teams_json, str)
|
|
198
|
+
|
|
199
|
+
# Verify it's a valid JSON array
|
|
200
|
+
parsed = json.loads(teams_json)
|
|
201
|
+
assert isinstance(parsed, list)
|
|
202
|
+
assert len(parsed) == 3
|
|
203
|
+
|
|
204
|
+
# Test JSON deserialization
|
|
205
|
+
reconstructed_teams = Teams.model_validate_json(teams_json)
|
|
206
|
+
assert len(reconstructed_teams.root) == 3
|
|
207
|
+
|
|
208
|
+
# Verify the data is correct
|
|
209
|
+
reconstructed_teams.root.sort(key=lambda x: x.id)
|
|
210
|
+
assert reconstructed_teams.root[0].id == "team001"
|
|
211
|
+
assert reconstructed_teams.root[1].id == "team002"
|
|
212
|
+
assert reconstructed_teams.root[2].id == "team003"
|
|
213
|
+
|
|
214
|
+
def test_teams_round_trip(self, sample_teams: Teams):
|
|
215
|
+
"""Test complete round-trip serialization"""
|
|
216
|
+
# Dict round-trip
|
|
217
|
+
teams_dict = sample_teams.model_dump()
|
|
218
|
+
reconstructed_from_dict = Teams.model_validate(teams_dict)
|
|
219
|
+
assert len(reconstructed_from_dict.root) == len(sample_teams.root)
|
|
220
|
+
|
|
221
|
+
# JSON round-trip
|
|
222
|
+
teams_json = sample_teams.model_dump_json()
|
|
223
|
+
reconstructed_from_json = Teams.model_validate_json(teams_json)
|
|
224
|
+
assert len(reconstructed_from_json.root) == len(sample_teams.root)
|
|
225
|
+
|
|
226
|
+
def test_empty_teams(self):
|
|
227
|
+
"""Test operations with empty Teams"""
|
|
228
|
+
empty_teams = Teams([])
|
|
229
|
+
|
|
230
|
+
assert len(empty_teams.root) == 0
|
|
231
|
+
|
|
232
|
+
# Test serialization
|
|
233
|
+
teams_json = empty_teams.model_dump_json()
|
|
234
|
+
assert teams_json == "[]"
|
|
235
|
+
|
|
236
|
+
# Test deserialization
|
|
237
|
+
reconstructed = Teams.model_validate_json("[]")
|
|
238
|
+
assert len(reconstructed.root) == 0
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from xcpcio import constants
|
|
2
|
+
from xcpcio.types import Color, ContestOptions, Image, Reaction
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestImage:
|
|
6
|
+
"""Test cases for Image Pydantic model"""
|
|
7
|
+
|
|
8
|
+
def test_image_creation_empty(self):
|
|
9
|
+
"""Test Image creation with default values"""
|
|
10
|
+
image = Image()
|
|
11
|
+
assert image.url is None
|
|
12
|
+
assert image.base64 is None
|
|
13
|
+
assert image.preset is None
|
|
14
|
+
|
|
15
|
+
def test_image_creation_with_values(self):
|
|
16
|
+
"""Test Image creation with provided values"""
|
|
17
|
+
image = Image(url="https://example.com/image.png", base64="base64data", preset="ICPC")
|
|
18
|
+
assert image.url == "https://example.com/image.png"
|
|
19
|
+
assert image.base64 == "base64data"
|
|
20
|
+
assert image.preset == "ICPC"
|
|
21
|
+
|
|
22
|
+
def test_image_serialization(self):
|
|
23
|
+
"""Test Image serialization and deserialization"""
|
|
24
|
+
image = Image(url="https://test.com/img.jpg", preset="CCPC")
|
|
25
|
+
|
|
26
|
+
# Test model_dump
|
|
27
|
+
image_dict = image.model_dump()
|
|
28
|
+
assert image_dict["url"] == "https://test.com/img.jpg"
|
|
29
|
+
assert image_dict["preset"] == "CCPC"
|
|
30
|
+
assert image_dict["base64"] is None
|
|
31
|
+
|
|
32
|
+
# Test JSON serialization
|
|
33
|
+
image_json = image.model_dump_json()
|
|
34
|
+
reconstructed_image = Image.model_validate_json(image_json)
|
|
35
|
+
assert reconstructed_image == image
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestColor:
|
|
39
|
+
"""Test cases for Color Pydantic model"""
|
|
40
|
+
|
|
41
|
+
def test_color_creation(self):
|
|
42
|
+
"""Test Color creation"""
|
|
43
|
+
color = Color(color="#ffffff", background_color="#000000")
|
|
44
|
+
assert color.color == "#ffffff"
|
|
45
|
+
assert color.background_color == "#000000"
|
|
46
|
+
|
|
47
|
+
def test_color_serialization(self):
|
|
48
|
+
"""Test Color serialization and deserialization"""
|
|
49
|
+
color = Color(color="red", background_color="blue")
|
|
50
|
+
|
|
51
|
+
# Test model_dump
|
|
52
|
+
color_dict = color.model_dump()
|
|
53
|
+
assert color_dict["color"] == "red"
|
|
54
|
+
assert color_dict["background_color"] == "blue"
|
|
55
|
+
|
|
56
|
+
# Test JSON round-trip
|
|
57
|
+
color_json = color.model_dump_json()
|
|
58
|
+
reconstructed_color = Color.model_validate_json(color_json)
|
|
59
|
+
assert reconstructed_color == color
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestContestOptions:
|
|
63
|
+
"""Test cases for ContestOptions Pydantic model"""
|
|
64
|
+
|
|
65
|
+
def test_contest_options_empty(self):
|
|
66
|
+
"""Test ContestOptions with default values"""
|
|
67
|
+
options = ContestOptions()
|
|
68
|
+
assert options.calculation_of_penalty is None
|
|
69
|
+
assert options.submission_timestamp_unit is None
|
|
70
|
+
assert options.has_reaction_videos is None
|
|
71
|
+
assert options.reaction_video_url_template is None
|
|
72
|
+
|
|
73
|
+
def test_contest_options_with_values(self):
|
|
74
|
+
"""Test ContestOptions with provided values"""
|
|
75
|
+
options = ContestOptions(
|
|
76
|
+
calculation_of_penalty=constants.CALCULATION_OF_PENALTY_IN_SECONDS,
|
|
77
|
+
submission_timestamp_unit=constants.TIME_UNIT_MILLISECOND,
|
|
78
|
+
has_reaction_videos=True,
|
|
79
|
+
reaction_video_url_template="https://videos.com/{team_id}/{problem_id}",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
assert options.calculation_of_penalty == constants.CALCULATION_OF_PENALTY_IN_SECONDS
|
|
83
|
+
assert options.submission_timestamp_unit == constants.TIME_UNIT_MILLISECOND
|
|
84
|
+
assert options.has_reaction_videos is True
|
|
85
|
+
assert options.reaction_video_url_template == "https://videos.com/{team_id}/{problem_id}"
|
|
86
|
+
|
|
87
|
+
def test_contest_options_serialization(self):
|
|
88
|
+
"""Test ContestOptions serialization"""
|
|
89
|
+
options = ContestOptions(
|
|
90
|
+
calculation_of_penalty=constants.CALCULATION_OF_PENALTY_IN_MINUTES, has_reaction_videos=False
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Test JSON round-trip
|
|
94
|
+
options_json = options.model_dump_json()
|
|
95
|
+
reconstructed_options = ContestOptions.model_validate_json(options_json)
|
|
96
|
+
assert reconstructed_options == options
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TestReaction:
|
|
100
|
+
"""Test cases for Reaction Pydantic model"""
|
|
101
|
+
|
|
102
|
+
def test_reaction_empty(self):
|
|
103
|
+
"""Test Reaction with default values"""
|
|
104
|
+
reaction = Reaction()
|
|
105
|
+
assert reaction.url is None
|
|
106
|
+
|
|
107
|
+
def test_reaction_with_url(self):
|
|
108
|
+
"""Test Reaction with URL"""
|
|
109
|
+
reaction = Reaction(url="https://reaction.com/video.mp4")
|
|
110
|
+
assert reaction.url == "https://reaction.com/video.mp4"
|
|
111
|
+
|
|
112
|
+
def test_reaction_serialization(self):
|
|
113
|
+
"""Test Reaction serialization"""
|
|
114
|
+
reaction = Reaction(url="https://test.com/reaction.mp4")
|
|
115
|
+
|
|
116
|
+
# Test model_dump
|
|
117
|
+
reaction_dict = reaction.model_dump()
|
|
118
|
+
assert reaction_dict["url"] == "https://test.com/reaction.mp4"
|
|
119
|
+
|
|
120
|
+
# Test JSON round-trip
|
|
121
|
+
reaction_json = reaction.model_dump_json()
|
|
122
|
+
reconstructed_reaction = Reaction.model_validate_json(reaction_json)
|
|
123
|
+
assert reconstructed_reaction == reaction
|
|
124
|
+
|
|
125
|
+
def test_reaction_no_url_serialization(self):
|
|
126
|
+
"""Test Reaction serialization with no URL"""
|
|
127
|
+
reaction = Reaction()
|
|
128
|
+
|
|
129
|
+
reaction_dict = reaction.model_dump()
|
|
130
|
+
assert reaction_dict["url"] is None
|
|
131
|
+
|
|
132
|
+
# Test JSON round-trip
|
|
133
|
+
reaction_json = reaction.model_dump_json()
|
|
134
|
+
reconstructed_reaction = Reaction.model_validate_json(reaction_json)
|
|
135
|
+
assert reconstructed_reaction == reaction
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
SUBMISSION_STATUS_PENDING = "PENDING"
|
|
2
|
+
SUBMISSION_STATUS_WAITING = "WAITING"
|
|
3
|
+
SUBMISSION_STATUS_PREPARING = "PREPARING"
|
|
4
|
+
SUBMISSION_STATUS_COMPILING = "COMPILING"
|
|
5
|
+
SUBMISSION_STATUS_RUNNING = "RUNNING"
|
|
6
|
+
SUBMISSION_STATUS_JUDGING = "JUDGING"
|
|
7
|
+
SUBMISSION_STATUS_FROZEN = "FROZEN"
|
|
8
|
+
|
|
9
|
+
SUBMISSION_STATUS_ACCEPTED = "ACCEPTED"
|
|
10
|
+
SUBMISSION_STATUS_CORRECT = "CORRECT"
|
|
11
|
+
SUBMISSION_STATUS_PARTIALLY_CORRECT = "PARTIALLY_CORRECT"
|
|
12
|
+
|
|
13
|
+
SUBMISSION_STATUS_REJECTED = "REJECTED"
|
|
14
|
+
SUBMISSION_STATUS_WRONG_ANSWER = "WRONG_ANSWER"
|
|
15
|
+
SUBMISSION_STATUS_INCORRECT = "INCORRECT"
|
|
16
|
+
|
|
17
|
+
SUBMISSION_STATUS_NO_OUTPUT = "NO_OUTPUT"
|
|
18
|
+
|
|
19
|
+
SUBMISSION_STATUS_COMPILATION_ERROR = "COMPILATION_ERROR"
|
|
20
|
+
SUBMISSION_STATUS_PRESENTATION_ERROR = "PRESENTATION_ERROR"
|
|
21
|
+
|
|
22
|
+
SUBMISSION_STATUS_RUNTIME_ERROR = "RUNTIME_ERROR"
|
|
23
|
+
SUBMISSION_STATUS_TIME_LIMIT_EXCEEDED = "TIME_LIMIT_EXCEEDED"
|
|
24
|
+
SUBMISSION_STATUS_MEMORY_LIMIT_EXCEEDED = "MEMORY_LIMIT_EXCEEDED"
|
|
25
|
+
SUBMISSION_STATUS_OUTPUT_LIMIT_EXCEEDED = "OUTPUT_LIMIT_EXCEEDED"
|
|
26
|
+
SUBMISSION_STATUS_IDLENESS_LIMIT_EXCEEDED = "IDLENESS_LIMIT_EXCEEDED"
|
|
27
|
+
|
|
28
|
+
SUBMISSION_STATUS_HACKED = "HACKED"
|
|
29
|
+
|
|
30
|
+
SUBMISSION_STATUS_JUDGEMENT_FAILED = "JUDGEMENT_FAILED"
|
|
31
|
+
SUBMISSION_STATUS_CONFIGURATION_ERROR = "CONFIGURATION_ERROR"
|
|
32
|
+
SUBMISSION_STATUS_FILE_ERROR = "FILE_ERROR"
|
|
33
|
+
SUBMISSION_STATUS_SYSTEM_ERROR = "SYSTEM_ERROR"
|
|
34
|
+
SUBMISSION_STATUS_CANCELED = "CANCELED"
|
|
35
|
+
SUBMISSION_STATUS_SKIPPED = "SKIPPED"
|
|
36
|
+
|
|
37
|
+
SUBMISSION_STATUS_SECURITY_VIOLATED = "SECURITY_VIOLATED"
|
|
38
|
+
SUBMISSION_STATUS_DENIAL_OF_JUDGEMENT = "DENIAL_OF_JUDGEMENT"
|
|
39
|
+
|
|
40
|
+
SUBMISSION_STATUS_UNKNOWN = "UNKNOWN"
|
|
41
|
+
SUBMISSION_STATUS_UNDEFINED = "UNDEFINED"
|
|
42
|
+
|
|
43
|
+
CALCULATION_OF_PENALTY_IN_MINUTES = "in_minutes"
|
|
44
|
+
CALCULATION_OF_PENALTY_IN_SECONDS = "in_seconds"
|
|
45
|
+
CALCULATION_OF_PENALTY_ACCUMULATE_IN_SECONDS_AND_FINALLY_TO_THE_MINUTE = (
|
|
46
|
+
"accumulate_in_seconds_and_finally_to_the_minute"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
TIME_UNIT_SECOND = "second"
|
|
50
|
+
TIME_UNIT_MILLISECOND = "millisecond"
|
|
51
|
+
TIME_UNIT_MICROSECOND = "microsecond"
|
|
52
|
+
TIME_UNIT_NANOSECOND = "nanosecond"
|
|
53
|
+
|
|
54
|
+
FULL_STATUS_TIME_DISPLAY = {
|
|
55
|
+
SUBMISSION_STATUS_CORRECT.lower(): True,
|
|
56
|
+
SUBMISSION_STATUS_INCORRECT.lower(): True,
|
|
57
|
+
SUBMISSION_STATUS_PENDING.lower(): True,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
TEAM_TYPE_OFFICIAL = "official"
|
|
61
|
+
TEAM_TYPE_UNOFFICIAL = "unofficial"
|
|
62
|
+
TEAM_TYPE_GIRL = "girl"
|
|
63
|
+
|
|
64
|
+
TEAM_TYPE_ZH_CN_OFFICIAL = "正式队伍"
|
|
65
|
+
TEAM_TYPE_ZH_CN_UNOFFICIAL = "打星队伍"
|
|
66
|
+
TEAM_TYPE_ZH_CN_GIRL = "女队"
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from typing import Dict, List, Literal, Optional, Union
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, RootModel
|
|
4
|
+
|
|
5
|
+
from . import constants
|
|
6
|
+
|
|
7
|
+
CalculationOfPenalty = Literal[
|
|
8
|
+
constants.CALCULATION_OF_PENALTY_IN_SECONDS,
|
|
9
|
+
constants.CALCULATION_OF_PENALTY_IN_MINUTES,
|
|
10
|
+
constants.CALCULATION_OF_PENALTY_ACCUMULATE_IN_SECONDS_AND_FINALLY_TO_THE_MINUTE,
|
|
11
|
+
]
|
|
12
|
+
TimeUnit = Literal[
|
|
13
|
+
constants.TIME_UNIT_SECOND,
|
|
14
|
+
constants.TIME_UNIT_MILLISECOND,
|
|
15
|
+
constants.TIME_UNIT_MICROSECOND,
|
|
16
|
+
constants.TIME_UNIT_NANOSECOND,
|
|
17
|
+
]
|
|
18
|
+
SubmissionStatus = Literal[
|
|
19
|
+
constants.SUBMISSION_STATUS_PENDING,
|
|
20
|
+
constants.SUBMISSION_STATUS_WAITING,
|
|
21
|
+
constants.SUBMISSION_STATUS_PREPARING,
|
|
22
|
+
constants.SUBMISSION_STATUS_COMPILING,
|
|
23
|
+
constants.SUBMISSION_STATUS_RUNNING,
|
|
24
|
+
constants.SUBMISSION_STATUS_JUDGING,
|
|
25
|
+
constants.SUBMISSION_STATUS_FROZEN,
|
|
26
|
+
#
|
|
27
|
+
constants.SUBMISSION_STATUS_ACCEPTED,
|
|
28
|
+
constants.SUBMISSION_STATUS_CORRECT,
|
|
29
|
+
constants.SUBMISSION_STATUS_PARTIALLY_CORRECT,
|
|
30
|
+
#
|
|
31
|
+
constants.SUBMISSION_STATUS_REJECTED,
|
|
32
|
+
constants.SUBMISSION_STATUS_WRONG_ANSWER,
|
|
33
|
+
constants.SUBMISSION_STATUS_INCORRECT,
|
|
34
|
+
#
|
|
35
|
+
constants.SUBMISSION_STATUS_NO_OUTPUT,
|
|
36
|
+
#
|
|
37
|
+
constants.SUBMISSION_STATUS_COMPILATION_ERROR,
|
|
38
|
+
constants.SUBMISSION_STATUS_PRESENTATION_ERROR,
|
|
39
|
+
#
|
|
40
|
+
constants.SUBMISSION_STATUS_RUNTIME_ERROR,
|
|
41
|
+
constants.SUBMISSION_STATUS_TIME_LIMIT_EXCEEDED,
|
|
42
|
+
constants.SUBMISSION_STATUS_MEMORY_LIMIT_EXCEEDED,
|
|
43
|
+
constants.SUBMISSION_STATUS_OUTPUT_LIMIT_EXCEEDED,
|
|
44
|
+
constants.SUBMISSION_STATUS_IDLENESS_LIMIT_EXCEEDED,
|
|
45
|
+
#
|
|
46
|
+
constants.SUBMISSION_STATUS_HACKED,
|
|
47
|
+
#
|
|
48
|
+
constants.SUBMISSION_STATUS_JUDGEMENT_FAILED,
|
|
49
|
+
constants.SUBMISSION_STATUS_CONFIGURATION_ERROR,
|
|
50
|
+
constants.SUBMISSION_STATUS_FILE_ERROR,
|
|
51
|
+
constants.SUBMISSION_STATUS_SYSTEM_ERROR,
|
|
52
|
+
constants.SUBMISSION_STATUS_CANCELED,
|
|
53
|
+
constants.SUBMISSION_STATUS_SKIPPED,
|
|
54
|
+
#
|
|
55
|
+
constants.SUBMISSION_STATUS_SECURITY_VIOLATED,
|
|
56
|
+
constants.SUBMISSION_STATUS_DENIAL_OF_JUDGEMENT,
|
|
57
|
+
#
|
|
58
|
+
constants.SUBMISSION_STATUS_UNKNOWN,
|
|
59
|
+
constants.SUBMISSION_STATUS_UNDEFINED,
|
|
60
|
+
]
|
|
61
|
+
ImagePreset = Literal["ICPC", "CCPC", "HUNAN_CPC"]
|
|
62
|
+
MedalPreset = Literal["ccpc", "icpc"]
|
|
63
|
+
BannerMode = Literal["ONLY_BANNER", "ALL"]
|
|
64
|
+
DateTimeISO8601String = str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Image(BaseModel):
|
|
68
|
+
url: Optional[str] = None
|
|
69
|
+
base64: Optional[str] = None
|
|
70
|
+
type: Optional[Literal["png", "svg", "jpg", "jpeg"]] = None
|
|
71
|
+
preset: Optional[ImagePreset] = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Color(BaseModel):
|
|
75
|
+
color: str
|
|
76
|
+
background_color: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Reaction(BaseModel):
|
|
80
|
+
url: Optional[str] = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Submission(BaseModel):
|
|
84
|
+
id: str = ""
|
|
85
|
+
|
|
86
|
+
team_id: str = ""
|
|
87
|
+
problem_id: int = 0
|
|
88
|
+
timestamp: int = 0 # unit: seconds
|
|
89
|
+
status: SubmissionStatus = constants.SUBMISSION_STATUS_UNKNOWN
|
|
90
|
+
|
|
91
|
+
time: Optional[int] = None
|
|
92
|
+
language: Optional[str] = None
|
|
93
|
+
|
|
94
|
+
is_ignore: Optional[bool] = None
|
|
95
|
+
|
|
96
|
+
reaction: Optional[Reaction] = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Submissions(RootModel[List[Submission]]):
|
|
100
|
+
root: List[Submission]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class Team(BaseModel):
|
|
104
|
+
id: str = ""
|
|
105
|
+
name: str = ""
|
|
106
|
+
|
|
107
|
+
organization: str = ""
|
|
108
|
+
group: List[str] = Field(default_factory=list)
|
|
109
|
+
tag: List[str] = Field(default_factory=list)
|
|
110
|
+
|
|
111
|
+
coach: Optional[str] = None
|
|
112
|
+
members: Optional[List[str]] = None
|
|
113
|
+
|
|
114
|
+
badge: Optional[Image] = None
|
|
115
|
+
|
|
116
|
+
location: Optional[str] = None
|
|
117
|
+
icpc_id: Optional[str] = None
|
|
118
|
+
|
|
119
|
+
extra: Dict[str, str] = Field(default_factory=dict)
|
|
120
|
+
|
|
121
|
+
def add_group(self, group: str):
|
|
122
|
+
if group not in self.group:
|
|
123
|
+
self.group.append(group)
|
|
124
|
+
|
|
125
|
+
def remove_group(self, group: str):
|
|
126
|
+
if group in self.group:
|
|
127
|
+
self.group.remove(group)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class Teams(RootModel[List[Team]]):
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ContestOptions(BaseModel):
|
|
135
|
+
calculation_of_penalty: Optional[CalculationOfPenalty] = None
|
|
136
|
+
submission_timestamp_unit: Optional[TimeUnit] = None
|
|
137
|
+
has_reaction_videos: Optional[bool] = None
|
|
138
|
+
reaction_video_url_template: Optional[str] = None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class Contest(BaseModel):
|
|
142
|
+
contest_name: str = ""
|
|
143
|
+
|
|
144
|
+
start_time: Union[int, DateTimeISO8601String] = 0
|
|
145
|
+
end_time: Union[int, DateTimeISO8601String] = 0
|
|
146
|
+
penalty: int = 20 * 60 # unit: seconds
|
|
147
|
+
|
|
148
|
+
freeze_time: Optional[Union[int, DateTimeISO8601String]] = None
|
|
149
|
+
frozen_time: int = 60 * 60 # unit: seconds
|
|
150
|
+
unfrozen_time: int = 0x3F3F3F3F3F3F3F3F
|
|
151
|
+
|
|
152
|
+
problem_quantity: int = 0
|
|
153
|
+
problem_id: List[str] = Field(default_factory=list)
|
|
154
|
+
|
|
155
|
+
organization: str = "School"
|
|
156
|
+
status_time_display: Optional[Dict[str, bool]] = constants.FULL_STATUS_TIME_DISPLAY
|
|
157
|
+
|
|
158
|
+
badge: Optional[str] = None
|
|
159
|
+
medal: Optional[Union[Dict[str, Dict[str, int]], MedalPreset]] = None
|
|
160
|
+
balloon_color: Optional[List[Color]] = None
|
|
161
|
+
|
|
162
|
+
group: Optional[Dict[str, str]] = None
|
|
163
|
+
tag: Optional[Dict[str, str]] = None
|
|
164
|
+
|
|
165
|
+
logo: Optional[Image] = None
|
|
166
|
+
banner: Optional[Image] = None
|
|
167
|
+
banner_mode: Optional[BannerMode] = None
|
|
168
|
+
board_link: Optional[str] = None
|
|
169
|
+
|
|
170
|
+
version: Optional[str] = None
|
|
171
|
+
|
|
172
|
+
options: ContestOptions = Field(default_factory=ContestOptions)
|
|
173
|
+
|
|
174
|
+
def append_balloon_color(self, color: Color):
|
|
175
|
+
if self.balloon_color is None:
|
|
176
|
+
self.balloon_color = []
|
|
177
|
+
self.balloon_color.append(color)
|
|
178
|
+
return self
|
|
179
|
+
|
|
180
|
+
def fill_problem_id(self):
|
|
181
|
+
self.problem_id = [chr(ord("A") + i) for i in range(self.problem_quantity)]
|
|
182
|
+
return self
|
|
183
|
+
|
|
184
|
+
def fill_balloon_color(self):
|
|
185
|
+
default_balloon_color_list = [
|
|
186
|
+
Color(background_color="rgba(189, 14, 14, 0.7)", color="#fff"),
|
|
187
|
+
Color(background_color="rgba(149, 31, 217, 0.7)", color="#fff"),
|
|
188
|
+
Color(background_color="rgba(16, 32, 96, 0.7)", color="#fff"),
|
|
189
|
+
Color(background_color="rgba(38, 185, 60, 0.7)", color="#000"),
|
|
190
|
+
Color(background_color="rgba(239, 217, 9, 0.7)", color="#000"),
|
|
191
|
+
Color(background_color="rgba(243, 88, 20, 0.7)", color="#fff"),
|
|
192
|
+
Color(background_color="rgba(12, 76, 138, 0.7)", color="#fff"),
|
|
193
|
+
Color(background_color="rgba(156, 155, 155, 0.7)", color="#000"),
|
|
194
|
+
Color(background_color="rgba(4, 154, 115, 0.7)", color="#000"),
|
|
195
|
+
Color(background_color="rgba(159, 19, 236, 0.7)", color="#fff"),
|
|
196
|
+
Color(background_color="rgba(42, 197, 202, 0.7)", color="#000"),
|
|
197
|
+
Color(background_color="rgba(142, 56, 54, 0.7)", color="#fff"),
|
|
198
|
+
Color(background_color="rgba(144, 238, 144, 0.7)", color="#000"),
|
|
199
|
+
Color(background_color="rgba(77, 57, 0, 0.7)", color="#fff"),
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
self.balloon_color = default_balloon_color_list[: self.problem_quantity]
|
|
203
|
+
|
|
204
|
+
return self
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xcpcio
|
|
3
|
+
Version: 0.58.2
|
|
4
|
+
Summary: xcpcio python lib
|
|
5
|
+
Author-email: Dup4 <hi@dup4.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: homepage, https://github.com/xcpcio/xcpcio
|
|
8
|
+
Project-URL: documentation, https://github.com/xcpcio/xcpcio
|
|
9
|
+
Project-URL: repository, https://github.com/xcpcio/xcpcio
|
|
10
|
+
Keywords: xcpcio
|
|
11
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: pydantic>=2.11.7
|
|
20
|
+
|
|
21
|
+
# xcpcio-python
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
tests/test_contest.py
|
|
4
|
+
tests/test_submission.py
|
|
5
|
+
tests/test_team.py
|
|
6
|
+
tests/test_types.py
|
|
7
|
+
xcpcio/__init__.py
|
|
8
|
+
xcpcio/constants.py
|
|
9
|
+
xcpcio/types.py
|
|
10
|
+
xcpcio.egg-info/PKG-INFO
|
|
11
|
+
xcpcio.egg-info/SOURCES.txt
|
|
12
|
+
xcpcio.egg-info/dependency_links.txt
|
|
13
|
+
xcpcio.egg-info/requires.txt
|
|
14
|
+
xcpcio.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pydantic>=2.11.7
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
xcpcio
|