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.

Files changed (57) hide show
  1. {xcpcio-0.64.2 → xcpcio-0.64.4}/PKG-INFO +4 -2
  2. {xcpcio-0.64.2 → xcpcio-0.64.4}/README.md +3 -1
  3. xcpcio-0.64.2/cli/ccs_archiver_cli.py → xcpcio-0.64.4/app/clics_archiver.py +12 -10
  4. xcpcio-0.64.2/app/contest_api_server.py → xcpcio-0.64.4/app/clics_server.py +13 -15
  5. {xcpcio-0.64.2 → xcpcio-0.64.4}/pyproject.toml +2 -1
  6. {xcpcio-0.64.2 → xcpcio-0.64.4}/scripts/generate_ccs_models.sh +1 -1
  7. {xcpcio-0.64.2 → xcpcio-0.64.4}/tests/test_contest.py +37 -43
  8. xcpcio-0.64.4/xcpcio/__init__.py +4 -0
  9. {xcpcio-0.64.2 → xcpcio-0.64.4}/xcpcio/__version__.py +1 -1
  10. xcpcio-0.64.4/xcpcio/api/__init__.py +15 -0
  11. xcpcio-0.64.4/xcpcio/api/client.py +74 -0
  12. xcpcio-0.64.4/xcpcio/api/models.py +25 -0
  13. xcpcio-0.64.4/xcpcio/clics/__init__.py +9 -0
  14. xcpcio-0.64.4/xcpcio/clics/api_server/app.py +43 -0
  15. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/dependencies.py +3 -4
  16. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/__init__.py +1 -1
  17. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/server.py +15 -35
  18. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/services/__init__.py +3 -1
  19. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/services/contest_service.py +4 -4
  20. xcpcio-0.64.4/xcpcio/clics/clics_api_client.py +215 -0
  21. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/contest_archiver.py +27 -169
  22. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/model/model_2023_06/__init__.py +1 -1
  23. xcpcio-0.64.4/xcpcio/clics/reader/__init__.py +7 -0
  24. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/reader/contest_package_reader.py +3 -3
  25. xcpcio-0.64.2/xcpcio/ccs/reader/base_ccs_reader.py → xcpcio-0.64.4/xcpcio/clics/reader/interface.py +2 -2
  26. xcpcio-0.64.2/xcpcio/__init__.py +0 -4
  27. xcpcio-0.64.2/xcpcio/ccs/__init__.py +0 -3
  28. xcpcio-0.64.2/xcpcio/ccs/reader/__init__.py +0 -0
  29. {xcpcio-0.64.2 → xcpcio-0.64.4}/.gitignore +0 -0
  30. {xcpcio-0.64.2 → xcpcio-0.64.4}/.python-version +0 -0
  31. {xcpcio-0.64.2 → xcpcio-0.64.4}/tests/__init__.py +0 -0
  32. {xcpcio-0.64.2 → xcpcio-0.64.4}/tests/test_submission.py +0 -0
  33. {xcpcio-0.64.2 → xcpcio-0.64.4}/tests/test_team.py +0 -0
  34. {xcpcio-0.64.2 → xcpcio-0.64.4}/tests/test_types.py +0 -0
  35. {xcpcio-0.64.2 → xcpcio-0.64.4}/uv.lock +0 -0
  36. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/__init__.py +0 -0
  37. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/access.py +0 -0
  38. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/accounts.py +0 -0
  39. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/awards.py +0 -0
  40. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/clarifications.py +0 -0
  41. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/contests.py +0 -0
  42. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/general.py +0 -0
  43. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/groups.py +0 -0
  44. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/judgement_types.py +0 -0
  45. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/judgements.py +0 -0
  46. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/languages.py +0 -0
  47. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/organizations.py +0 -0
  48. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/problems.py +0 -0
  49. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/runs.py +0 -0
  50. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/submissions.py +0 -0
  51. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/api_server/routes/teams.py +0 -0
  52. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/base/__init__.py +0 -0
  53. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/base/types.py +0 -0
  54. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/model/__init__.py +0 -0
  55. {xcpcio-0.64.2/xcpcio/ccs → xcpcio-0.64.4/xcpcio/clics}/model/model_2023_06/model.py +0 -0
  56. {xcpcio-0.64.2 → xcpcio-0.64.4}/xcpcio/constants.py +0 -0
  57. {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.2
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 [Dup4](https://github.com/Dup4)
86
+ [MIT](../LICENSE) License © 2020 - PRESENT [XCPCIO][xcpcio]
87
+
88
+ [xcpcio]: https://xcpcio.com
@@ -53,4 +53,6 @@ For detailed documentation, visit:
53
53
 
54
54
  ## License
55
55
 
56
- MIT License © 2020-PRESENT [Dup4](https://github.com/Dup4)
56
+ [MIT](../LICENSE) License © 2020 - PRESENT [XCPCIO][xcpcio]
57
+
58
+ [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.ccs.contest_archiver import APICredentials, ArchiveConfig, ContestArchiver
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
- ccs-archiver --base-url https://api.example.com/api --contest-id contest123 -o ./output -u admin -p secret
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
- ccs-archiver --base-url https://api.example.com/api --contest-id contest123 -o contest.zip --token abc123
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
- ccs-archiver --base-url https://api.example.com/api --contest-id contest123 -o contest.tar.gz -u admin -p secret
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
- ccs-archiver --base-url https://api.example.com/api --contest-id contest123 -o contest.tar.zst -u admin -p secret
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
- ccs-archiver --base-url https://api.example.com/api --contest-id contest123 -o ./output -u admin -p secret -e teams -e problems
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
- ccs-archiver --base-url https://api.example.com/api --contest-id contest123 -o ./output --no-files
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
- shutil.rmtree(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)
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="ccs_archive_"))
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.ccs.api_server.server import ContestAPIServer
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, reload: bool, log_level: str, verbose: bool):
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
- contest-api-server -p /path/to/contest
72
+ clics-server -p /path/to/contest
74
73
 
75
74
  # Start server with archive file
76
- contest-api-server -p /path/to/contest.zip
77
- contest-api-server -p /path/to/contest.tar.gz
78
- contest-api-server -p /path/to/contest.tar.zst
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
- contest-api-server -p /path/to/contest --host 127.0.0.1 --port 9000
82
-
83
- # Enable reload for development
84
- contest-api-server -p /path/to/contest --reload
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
- shutil.rmtree(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)
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="ccs_package_"))
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, reload=reload, log_level=log_level.lower())
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
- ccs-archiver = "cli.ccs_archiver_cli:main"
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/ccs/model"
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
- group=custom_group,
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,4 @@
1
+ from . import clics, constants, types
2
+ from .__version__ import __version__
3
+
4
+ __all__ = [constants, types, clics, __version__]
@@ -1,4 +1,4 @@
1
1
  # This file is auto-generated by Hatchling. As such, do not:
2
2
  # - modify
3
3
  # - track in version control e.g. be sure to add to .gitignore
4
- __version__ = VERSION = '0.64.2'
4
+ __version__ = VERSION = '0.64.4'
@@ -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,9 @@
1
+ from . import api_server, base, contest_archiver, model, reader
2
+
3
+ __all__ = [
4
+ model,
5
+ contest_archiver,
6
+ api_server,
7
+ base,
8
+ reader,
9
+ ]
@@ -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.ccs.reader.base_ccs_reader import BaseCCSReader
13
- from xcpcio.ccs.reader.contest_package_reader import ContestPackageReader
12
+ from xcpcio.clics.reader import BaseContestReader, ContestPackageReader
14
13
 
15
- from .services.contest_service import ContestService
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, BaseCCSReader] = {}
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)
@@ -49,4 +49,4 @@ def create_router() -> APIRouter:
49
49
  return router
50
50
 
51
51
 
52
- __all__ = ["create_router"]
52
+ __all__ = [create_router]