rbx.cp 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. rbx/__init__.py +0 -0
  2. rbx/annotations.py +127 -0
  3. rbx/autoenum.py +333 -0
  4. rbx/box/__init__.py +0 -0
  5. rbx/box/builder.py +77 -0
  6. rbx/box/cd.py +37 -0
  7. rbx/box/checkers.py +134 -0
  8. rbx/box/code.py +185 -0
  9. rbx/box/compile.py +56 -0
  10. rbx/box/conftest.py +42 -0
  11. rbx/box/contest/__init__.py +0 -0
  12. rbx/box/contest/build_contest_statements.py +347 -0
  13. rbx/box/contest/contest_package.py +76 -0
  14. rbx/box/contest/contest_utils.py +20 -0
  15. rbx/box/contest/main.py +179 -0
  16. rbx/box/contest/schema.py +155 -0
  17. rbx/box/contest/statements.py +82 -0
  18. rbx/box/creation.py +72 -0
  19. rbx/box/download.py +64 -0
  20. rbx/box/environment.py +345 -0
  21. rbx/box/extensions.py +26 -0
  22. rbx/box/generators.py +478 -0
  23. rbx/box/generators_test.py +63 -0
  24. rbx/box/main.py +449 -0
  25. rbx/box/package.py +316 -0
  26. rbx/box/packaging/boca/extension.py +27 -0
  27. rbx/box/packaging/boca/packager.py +245 -0
  28. rbx/box/packaging/contest_main.py +82 -0
  29. rbx/box/packaging/main.py +68 -0
  30. rbx/box/packaging/packager.py +117 -0
  31. rbx/box/packaging/polygon/packager.py +320 -0
  32. rbx/box/packaging/polygon/test.py +81 -0
  33. rbx/box/packaging/polygon/xml_schema.py +106 -0
  34. rbx/box/presets/__init__.py +503 -0
  35. rbx/box/presets/fetch.py +70 -0
  36. rbx/box/presets/lock_schema.py +20 -0
  37. rbx/box/presets/schema.py +59 -0
  38. rbx/box/schema.py +394 -0
  39. rbx/box/solutions.py +792 -0
  40. rbx/box/solutions_test.py +41 -0
  41. rbx/box/statements/__init__.py +0 -0
  42. rbx/box/statements/build_statements.py +359 -0
  43. rbx/box/statements/builders.py +375 -0
  44. rbx/box/statements/joiners.py +113 -0
  45. rbx/box/statements/latex.py +47 -0
  46. rbx/box/statements/latex_jinja.py +214 -0
  47. rbx/box/statements/schema.py +138 -0
  48. rbx/box/stresses.py +292 -0
  49. rbx/box/stressing/__init__.py +0 -0
  50. rbx/box/stressing/finder_parser.py +359 -0
  51. rbx/box/stressing/generator_parser.py +258 -0
  52. rbx/box/testcases.py +54 -0
  53. rbx/box/ui/__init__.py +0 -0
  54. rbx/box/ui/captured_log.py +372 -0
  55. rbx/box/ui/css/app.tcss +48 -0
  56. rbx/box/ui/main.py +38 -0
  57. rbx/box/ui/run.py +209 -0
  58. rbx/box/validators.py +245 -0
  59. rbx/box/validators_test.py +15 -0
  60. rbx/checker.py +128 -0
  61. rbx/clone.py +197 -0
  62. rbx/config.py +271 -0
  63. rbx/conftest.py +38 -0
  64. rbx/console.py +27 -0
  65. rbx/create.py +37 -0
  66. rbx/edit.py +24 -0
  67. rbx/grading/__init__.py +0 -0
  68. rbx/grading/caching.py +356 -0
  69. rbx/grading/conftest.py +33 -0
  70. rbx/grading/judge/__init__.py +0 -0
  71. rbx/grading/judge/cacher.py +503 -0
  72. rbx/grading/judge/digester.py +35 -0
  73. rbx/grading/judge/sandbox.py +748 -0
  74. rbx/grading/judge/sandboxes/__init__.py +0 -0
  75. rbx/grading/judge/sandboxes/isolate.py +683 -0
  76. rbx/grading/judge/sandboxes/stupid_sandbox.py +310 -0
  77. rbx/grading/judge/sandboxes/timeit.py +217 -0
  78. rbx/grading/judge/storage.py +284 -0
  79. rbx/grading/judge/test.py +38 -0
  80. rbx/grading/judge/testiso.py +54 -0
  81. rbx/grading/steps.py +522 -0
  82. rbx/grading/steps_with_caching.py +59 -0
  83. rbx/grading/steps_with_caching_run_test.py +429 -0
  84. rbx/grading_utils.py +148 -0
  85. rbx/hydration.py +101 -0
  86. rbx/main.py +122 -0
  87. rbx/metadata.py +105 -0
  88. rbx/providers/__init__.py +43 -0
  89. rbx/providers/codeforces.py +73 -0
  90. rbx/providers/provider.py +26 -0
  91. rbx/resources/checkers/boilerplate.cpp +20 -0
  92. rbx/resources/default_config.json +48 -0
  93. rbx/resources/envs/default.rbx.yml +37 -0
  94. rbx/resources/envs/isolate.rbx.yml +37 -0
  95. rbx/resources/packagers/boca/checker.sh +43 -0
  96. rbx/resources/packagers/boca/compare +53 -0
  97. rbx/resources/packagers/boca/compile/c +172 -0
  98. rbx/resources/packagers/boca/compile/cc +173 -0
  99. rbx/resources/packagers/boca/compile/cpp +172 -0
  100. rbx/resources/packagers/boca/compile/java +194 -0
  101. rbx/resources/packagers/boca/compile/kt +155 -0
  102. rbx/resources/packagers/boca/compile/pas +172 -0
  103. rbx/resources/packagers/boca/compile/py2 +173 -0
  104. rbx/resources/packagers/boca/compile/py3 +173 -0
  105. rbx/resources/packagers/boca/run/c +128 -0
  106. rbx/resources/packagers/boca/run/cc +128 -0
  107. rbx/resources/packagers/boca/run/cpp +128 -0
  108. rbx/resources/packagers/boca/run/java +194 -0
  109. rbx/resources/packagers/boca/run/kt +159 -0
  110. rbx/resources/packagers/boca/run/py2 +166 -0
  111. rbx/resources/packagers/boca/run/py3 +166 -0
  112. rbx/resources/presets/default/contest/contest.rbx.yml +14 -0
  113. rbx/resources/presets/default/contest/statement/contest.rbx.tex +97 -0
  114. rbx/resources/presets/default/contest/statement/olymp.sty +250 -0
  115. rbx/resources/presets/default/contest/statement/template.rbx.tex +42 -0
  116. rbx/resources/presets/default/preset.rbx.yml +12 -0
  117. rbx/resources/presets/default/problem/.gitignore +6 -0
  118. rbx/resources/presets/default/problem/gen.cpp +9 -0
  119. rbx/resources/presets/default/problem/problem.rbx.yml +44 -0
  120. rbx/resources/presets/default/problem/random.py +3 -0
  121. rbx/resources/presets/default/problem/random.txt +2 -0
  122. rbx/resources/presets/default/problem/sols/main.cpp +9 -0
  123. rbx/resources/presets/default/problem/sols/slow.cpp +15 -0
  124. rbx/resources/presets/default/problem/sols/wa.cpp +9 -0
  125. rbx/resources/presets/default/problem/statement/olymp.sty +250 -0
  126. rbx/resources/presets/default/problem/statement/projecao.png +0 -0
  127. rbx/resources/presets/default/problem/statement/statement.rbx.tex +18 -0
  128. rbx/resources/presets/default/problem/statement/template.rbx.tex +89 -0
  129. rbx/resources/presets/default/problem/tests/samples/000.in +1 -0
  130. rbx/resources/presets/default/problem/tests/samples/001.in +1 -0
  131. rbx/resources/presets/default/problem/validator.cpp +16 -0
  132. rbx/resources/presets/default/problem/wcmp.cpp +34 -0
  133. rbx/resources/templates/template.cpp +19 -0
  134. rbx/run.py +45 -0
  135. rbx/schema.py +64 -0
  136. rbx/submit.py +61 -0
  137. rbx/submitors/__init__.py +18 -0
  138. rbx/submitors/codeforces.py +120 -0
  139. rbx/submitors/submitor.py +25 -0
  140. rbx/test.py +347 -0
  141. rbx/testcase.py +70 -0
  142. rbx/testcase_rendering.py +79 -0
  143. rbx/testdata/box1/gen1.cpp +7 -0
  144. rbx/testdata/box1/gen2.cpp +9 -0
  145. rbx/testdata/box1/genScript.py +2 -0
  146. rbx/testdata/box1/hard-tle.sol.cpp +26 -0
  147. rbx/testdata/box1/ole.cpp +17 -0
  148. rbx/testdata/box1/problem.rbx.yml +39 -0
  149. rbx/testdata/box1/re.sol.cpp +23 -0
  150. rbx/testdata/box1/sol.cpp +22 -0
  151. rbx/testdata/box1/tests/1.in +1 -0
  152. rbx/testdata/box1/tle-and-incorrect.sol.cpp +33 -0
  153. rbx/testdata/box1/tle.sol.cpp +35 -0
  154. rbx/testdata/box1/validator.cpp +11 -0
  155. rbx/testdata/box1/wa.sol.cpp +22 -0
  156. rbx/testdata/caching/executable.py +1 -0
  157. rbx/testdata/compatible +0 -0
  158. rbx/testing_utils.py +65 -0
  159. rbx/utils.py +162 -0
  160. rbx_cp-0.5.0.dist-info/LICENSE +201 -0
  161. rbx_cp-0.5.0.dist-info/METADATA +89 -0
  162. rbx_cp-0.5.0.dist-info/RECORD +164 -0
  163. rbx_cp-0.5.0.dist-info/WHEEL +4 -0
  164. rbx_cp-0.5.0.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,347 @@
1
+ import dataclasses
2
+ import pathlib
3
+ import tempfile
4
+ import typing
5
+ from typing import List, Optional, Tuple
6
+
7
+ import typer
8
+
9
+ from rbx import console, testing_utils, utils
10
+ from rbx.box.contest import contest_utils
11
+ from rbx.box.contest.contest_package import get_problems
12
+ from rbx.box.contest.schema import Contest, ContestProblem, ContestStatement
13
+ from rbx.box.schema import Package, Testcase
14
+ from rbx.box.statements import build_statements
15
+ from rbx.box.statements.build_statements import (
16
+ get_builders,
17
+ get_environment_languages_for_statement,
18
+ get_relative_assets,
19
+ )
20
+ from rbx.box.statements.builders import (
21
+ CONTEST_BUILDER_LIST,
22
+ StatementBuilderContest,
23
+ StatementBuilderContext,
24
+ StatementBuilderProblem,
25
+ prepare_assets,
26
+ )
27
+ from rbx.box.statements.joiners import (
28
+ JOINER_LIST,
29
+ StatementJoiner,
30
+ StatementJoinerContext,
31
+ )
32
+ from rbx.box.statements.schema import Statement, StatementType
33
+ from rbx.box.testcases import get_samples
34
+
35
+
36
+ @dataclasses.dataclass
37
+ class ExtractedProblem:
38
+ package: Package
39
+ statement: Statement
40
+ problem: ContestProblem
41
+ samples: List[Testcase]
42
+ built_statement: Optional[pathlib.Path] = None
43
+
44
+ def get_statement_path(self) -> pathlib.Path:
45
+ return self.problem.get_path() / self.statement.path
46
+
47
+ def get_statement_assets(self) -> List[str]:
48
+ return [str(self.problem.get_path() / asset) for asset in self.statement.assets]
49
+
50
+ def get_statement_builder_problem(self) -> StatementBuilderProblem:
51
+ return StatementBuilderProblem(
52
+ package=self.package,
53
+ statement=self.statement,
54
+ samples=self.samples,
55
+ io_path=self.built_statement,
56
+ short_name=self.problem.short_name,
57
+ )
58
+
59
+
60
+ def _get_samples(problem: ContestProblem) -> List[Testcase]:
61
+ with utils.new_cd(problem.get_path()):
62
+ contest_utils.clear_package_cache()
63
+ return get_samples()
64
+
65
+
66
+ def get_statement_builder_problems(
67
+ extracted_problems: List[ExtractedProblem],
68
+ ) -> List[StatementBuilderProblem]:
69
+ return [ex.get_statement_builder_problem() for ex in extracted_problems]
70
+
71
+
72
+ def get_statement_builder_contest(
73
+ statement: ContestStatement,
74
+ extracted_problems: List[ExtractedProblem],
75
+ ) -> StatementBuilderContest:
76
+ return StatementBuilderContest(
77
+ title=statement.title,
78
+ location=statement.location,
79
+ date=statement.date,
80
+ problems=get_statement_builder_problems(extracted_problems),
81
+ )
82
+
83
+
84
+ def get_problems_for_statement(
85
+ contest: Contest,
86
+ language: str,
87
+ requires_matching_statement: bool = True,
88
+ ) -> List[ExtractedProblem]:
89
+ pkgs = get_problems(contest)
90
+ if not pkgs:
91
+ console.console.print(
92
+ '[error]No problems found in the contest, cannot infer statement type.[/error]'
93
+ )
94
+ raise typer.Exit(1)
95
+
96
+ res = []
97
+ for pkg, problem in zip(pkgs, contest.problems):
98
+ found = False
99
+ for statement in pkg.statements:
100
+ if statement.language == language or not requires_matching_statement:
101
+ found = True
102
+ res.append(
103
+ ExtractedProblem(
104
+ package=pkg,
105
+ statement=statement,
106
+ problem=problem,
107
+ samples=_get_samples(problem),
108
+ )
109
+ )
110
+ break
111
+ if not found:
112
+ console.console.print(
113
+ f'[error]No statement found for language {language} in problem {problem.short_name}[/error]'
114
+ )
115
+ raise typer.Exit(1)
116
+
117
+ return res
118
+
119
+
120
+ def get_builder_problems(
121
+ extracted_problems: List[ExtractedProblem],
122
+ ) -> List[StatementBuilderProblem]:
123
+ return [
124
+ StatementBuilderProblem(
125
+ package=ex.package,
126
+ statement=ex.statement,
127
+ samples=ex.samples,
128
+ )
129
+ for ex in extracted_problems
130
+ ]
131
+
132
+
133
+ def get_joiner(name: str) -> StatementJoiner:
134
+ for joiner in JOINER_LIST:
135
+ if joiner.name() == name:
136
+ return joiner
137
+ console.console.print(f'[error]Joiner [item]{name}[/item] not found.[/error]')
138
+ raise typer.Exit(1)
139
+
140
+
141
+ def _build_problem_statements(
142
+ statement: ContestStatement,
143
+ contest: Contest,
144
+ root: pathlib.Path,
145
+ output_type: StatementType,
146
+ use_samples: bool = True,
147
+ is_editorial: bool = False,
148
+ ) -> List[ExtractedProblem]:
149
+ console.console.print('Building problem-level statements...')
150
+ extracted_problems = get_problems_for_statement(contest, statement.language)
151
+ res = []
152
+ contest_cwd_absolute = pathlib.Path().resolve()
153
+ contest_assets = get_relative_assets(statement.path, statement.assets)
154
+
155
+ for extracted_problem in extracted_problems:
156
+ console.console.print(
157
+ f'Building statement for problem {extracted_problem.problem.short_name}...'
158
+ )
159
+ with utils.new_cd(extracted_problem.problem.get_path()):
160
+ contest_utils.clear_package_cache()
161
+ # TODO: respect steps override
162
+ content, _ = build_statements.build_statement_bytes(
163
+ extracted_problem.statement,
164
+ extracted_problem.package,
165
+ output_type=output_type,
166
+ short_name=extracted_problem.problem.short_name,
167
+ overridden_params={
168
+ cfg.type: cfg for cfg in statement.override.configure
169
+ }
170
+ if statement.override is not None
171
+ else {}, # overridden configure params
172
+ overridden_assets=contest_assets, # overridden assets
173
+ overridden_params_root=contest_cwd_absolute,
174
+ use_samples=use_samples,
175
+ is_editorial=is_editorial,
176
+ )
177
+ dest_dir = root / '.problems' / extracted_problem.problem.short_name
178
+ dest_path = dest_dir / f'statement{output_type.get_file_suffix()}'
179
+ dest_dir.mkdir(parents=True, exist_ok=True)
180
+ dest_path.write_bytes(content)
181
+
182
+ problem_assets = (
183
+ get_relative_assets(
184
+ extracted_problem.get_statement_path(),
185
+ extracted_problem.get_statement_assets(),
186
+ )
187
+ + contest_assets
188
+ )
189
+ prepare_assets(problem_assets, dest_dir)
190
+
191
+ res.append(dataclasses.replace(extracted_problem, built_statement=dest_path))
192
+ return res
193
+
194
+
195
+ def build_contest_only(
196
+ statement: ContestStatement,
197
+ contest: Contest,
198
+ extracted_problems: List[ExtractedProblem],
199
+ input: bytes,
200
+ input_type: StatementType,
201
+ output_type: Optional[StatementType] = None,
202
+ is_editorial: bool = False,
203
+ ) -> Tuple[bytes, StatementType]:
204
+ console.console.print('Building contest-level statement.')
205
+ bdrs = get_builders(
206
+ contest.name,
207
+ statement.steps,
208
+ statement.configure,
209
+ input_type,
210
+ output_type=output_type,
211
+ builder_list=CONTEST_BUILDER_LIST,
212
+ )
213
+
214
+ last_content = input
215
+ last_output = input_type
216
+ for bdr, params in bdrs:
217
+ with tempfile.TemporaryDirectory() as td:
218
+ assets = get_relative_assets(
219
+ statement.path, statement.assets
220
+ ) + bdr.inject_assets(pathlib.Path(), params)
221
+ prepare_assets(assets, pathlib.Path(td))
222
+ output = bdr.build(
223
+ input=last_content,
224
+ context=StatementBuilderContext(
225
+ languages=get_environment_languages_for_statement(),
226
+ params=params,
227
+ root=pathlib.Path(td),
228
+ editorial=is_editorial,
229
+ vars={**contest.expanded_vars, **statement.expanded_vars},
230
+ ),
231
+ item=get_statement_builder_contest(statement, extracted_problems),
232
+ verbose=False,
233
+ )
234
+ last_content = output
235
+ last_output = bdr.output_type()
236
+
237
+ return last_content, last_output
238
+
239
+
240
+ def build_statement_rooted(
241
+ statement: ContestStatement,
242
+ contest: Contest,
243
+ root: pathlib.Path,
244
+ output_type: Optional[StatementType] = None,
245
+ use_samples: bool = True,
246
+ is_editorial: bool = False,
247
+ ) -> Tuple[bytes, StatementType]:
248
+ # Validate.
249
+ if not statement.path.is_file():
250
+ console.console.print(
251
+ f'[error]Statement file [item]{statement.path}[/item] does not exist for contest.[/error]'
252
+ )
253
+ raise typer.Exit(1)
254
+
255
+ if statement.joiner is None:
256
+ joiner = None
257
+ extracted_problems = get_problems_for_statement(
258
+ contest, statement.language, requires_matching_statement=False
259
+ )
260
+ else:
261
+ # Build problem-level statements.
262
+ joiner = get_joiner(statement.joiner.type)
263
+ extracted_problems = _build_problem_statements(
264
+ statement,
265
+ contest,
266
+ root,
267
+ output_type=joiner.joined_type(),
268
+ use_samples=use_samples,
269
+ is_editorial=is_editorial,
270
+ )
271
+
272
+ # Build contest-level statement into joiner input type.
273
+ last_content, last_output = build_contest_only(
274
+ statement,
275
+ contest,
276
+ extracted_problems,
277
+ statement.path.read_bytes(),
278
+ statement.type,
279
+ output_type=joiner.joined_type() if joiner is not None else output_type,
280
+ is_editorial=is_editorial,
281
+ )
282
+
283
+ if joiner is None:
284
+ return last_content, last_output
285
+
286
+ # Join statements.
287
+ console.console.print('Joining statements...')
288
+ joiner_assets = get_relative_assets(statement.path, statement.assets)
289
+ prepare_assets(joiner_assets, root)
290
+
291
+ testing_utils.print_directory_tree(root, show_hidden=True)
292
+
293
+ joiner_context = StatementJoinerContext(
294
+ languages=get_environment_languages_for_statement(),
295
+ params=statement.joiner,
296
+ root=root,
297
+ )
298
+ last_content = joiner.build(
299
+ last_content,
300
+ context=joiner_context,
301
+ contest=get_statement_builder_contest(statement, extracted_problems),
302
+ )
303
+ last_output = joiner.output_type()
304
+
305
+ # Finish statement.
306
+ last_content, last_output = build_contest_only(
307
+ statement,
308
+ contest,
309
+ extracted_problems,
310
+ last_content,
311
+ last_output,
312
+ output_type=output_type,
313
+ is_editorial=is_editorial,
314
+ )
315
+
316
+ return last_content, last_output
317
+
318
+
319
+ def build_statement(
320
+ statement: ContestStatement,
321
+ contest: Contest,
322
+ output_type: Optional[StatementType] = None,
323
+ use_samples: bool = True,
324
+ is_editorial: bool = False,
325
+ ) -> pathlib.Path:
326
+ with tempfile.TemporaryDirectory() as td:
327
+ root = pathlib.Path(td)
328
+ last_content, last_output = build_statement_rooted(
329
+ statement,
330
+ contest,
331
+ root,
332
+ output_type=output_type,
333
+ use_samples=use_samples,
334
+ is_editorial=is_editorial,
335
+ )
336
+
337
+ statement_path = pathlib.Path(
338
+ f'build/{statement.path.stem}{last_output.get_file_suffix()}'
339
+ )
340
+ statement_path.parent.mkdir(parents=True, exist_ok=True)
341
+ statement_path.write_bytes(typing.cast(bytes, last_content))
342
+ console.console.print(
343
+ f'Statement built successfully for language '
344
+ f'[item]{statement.language}[/item] at '
345
+ f'[item]{statement_path}[/item].'
346
+ )
347
+ return statement_path
@@ -0,0 +1,76 @@
1
+ import functools
2
+ import pathlib
3
+ from typing import List, Optional
4
+
5
+ import typer
6
+
7
+ from rbx import console, utils
8
+ from rbx.box.contest.schema import Contest
9
+ from rbx.box.package import find_problem_package_or_die, warn_preset_deactivated
10
+ from rbx.box.schema import Package
11
+
12
+ YAML_NAME = 'contest.rbx.yml'
13
+
14
+
15
+ @functools.cache
16
+ def find_contest_yaml(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.Path]:
17
+ root = root.resolve()
18
+ contest_yaml_path = root / YAML_NAME
19
+ while root != pathlib.PosixPath('/') and not contest_yaml_path.is_file():
20
+ root = root.parent
21
+ contest_yaml_path = root / YAML_NAME
22
+ if not contest_yaml_path.is_file():
23
+ return None
24
+ warn_preset_deactivated(root)
25
+ return contest_yaml_path
26
+
27
+
28
+ @functools.cache
29
+ def find_contest_package(root: pathlib.Path = pathlib.Path()) -> Optional[Contest]:
30
+ contest_yaml_path = find_contest_yaml(root)
31
+ if not contest_yaml_path:
32
+ return None
33
+ return utils.model_from_yaml(Contest, contest_yaml_path.read_text())
34
+
35
+
36
+ def find_contest_package_or_die(root: pathlib.Path = pathlib.Path()) -> Contest:
37
+ package = find_contest_package(root)
38
+ if package is None:
39
+ console.console.print(f'Contest not found in {root.absolute()}', style='error')
40
+ raise typer.Exit(1)
41
+ return package
42
+
43
+
44
+ def find_contest(root: pathlib.Path = pathlib.Path()) -> pathlib.Path:
45
+ found = find_contest_yaml(root)
46
+ if found is None:
47
+ console.console.print(f'Contest not found in {root.absolute()}', style='error')
48
+ raise typer.Exit(1)
49
+ return found.parent
50
+
51
+
52
+ def within_contest(func):
53
+ @functools.wraps(func)
54
+ def wrapper(*args, **kwargs):
55
+ with utils.new_cd(find_contest()):
56
+ return func(*args, **kwargs)
57
+
58
+ return wrapper
59
+
60
+
61
+ def save_contest(
62
+ package: Optional[Contest] = None, root: pathlib.Path = pathlib.Path()
63
+ ) -> None:
64
+ package = package or find_contest_package_or_die(root)
65
+ contest_yaml_path = find_contest_yaml(root)
66
+ if not contest_yaml_path:
67
+ console.console.print(f'Contest not found in {root.absolute()}', style='error')
68
+ raise typer.Exit(1)
69
+ contest_yaml_path.write_text(utils.model_to_yaml(package))
70
+
71
+
72
+ def get_problems(contest: Contest) -> List[Package]:
73
+ problems = []
74
+ for problem in contest.problems:
75
+ problems.append(find_problem_package_or_die(problem.get_path()))
76
+ return problems
@@ -0,0 +1,20 @@
1
+ from rbx.box import environment, package
2
+ from rbx.box.contest import contest_package
3
+
4
+
5
+ def clear_package_cache():
6
+ pkgs = [package]
7
+
8
+ for pkg in pkgs:
9
+ for fn in pkg.__dict__.values():
10
+ if hasattr(fn, 'cache_clear'):
11
+ fn.cache_clear()
12
+
13
+
14
+ def clear_all_caches():
15
+ pkgs = [package, environment, contest_package]
16
+
17
+ for pkg in pkgs:
18
+ for fn in pkg.__dict__.values():
19
+ if hasattr(fn, 'cache_clear'):
20
+ fn.cache_clear()
@@ -0,0 +1,179 @@
1
+ import pathlib
2
+ import shutil
3
+ import subprocess
4
+ from typing import Annotated, Optional
5
+
6
+ import typer
7
+
8
+ from rbx import annotations, console, utils
9
+ from rbx.box import creation, presets
10
+ from rbx.box.contest import contest_utils, statements
11
+ from rbx.box.contest.contest_package import (
12
+ find_contest,
13
+ find_contest_package_or_die,
14
+ find_contest_yaml,
15
+ save_contest,
16
+ within_contest,
17
+ )
18
+ from rbx.box.contest.schema import ContestProblem
19
+ from rbx.box.packaging import contest_main as packaging
20
+ from rbx.box.presets.fetch import get_preset_fetch_info
21
+ from rbx.box.schema import Package
22
+ from rbx.config import open_editor
23
+
24
+ app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
25
+ app.add_typer(
26
+ statements.app,
27
+ name='statements, st',
28
+ cls=annotations.AliasGroup,
29
+ help='Manage contest-level statements.',
30
+ )
31
+ app.add_typer(
32
+ packaging.app,
33
+ name='package, pkg',
34
+ cls=annotations.AliasGroup,
35
+ help='Build contest-level packages.',
36
+ )
37
+
38
+
39
+ @app.command('create, c', help='Create a new contest package.')
40
+ @within_contest
41
+ def create(
42
+ name: str,
43
+ preset: Annotated[
44
+ str,
45
+ typer.Option(
46
+ '--preset',
47
+ '-p',
48
+ help='Which preset to use to create this package. Can be a named of an already installed preset, or an URI, in which case the preset will be downloaded.',
49
+ ),
50
+ ] = 'default',
51
+ local: bool = typer.Option(
52
+ False,
53
+ '--local',
54
+ '-l',
55
+ help='Whether to inline the installed preset within the contest folder.',
56
+ ),
57
+ ):
58
+ console.console.print(f'Creating new contest [item]{name}[/item]...')
59
+
60
+ fetch_info = get_preset_fetch_info(preset)
61
+ if fetch_info is None:
62
+ console.console.print(
63
+ f'[error]Invalid preset name/URI [item]{preset}[/item].[/error]'
64
+ )
65
+ raise typer.Exit(1)
66
+
67
+ if fetch_info.fetch_uri is not None:
68
+ preset = presets.install_from_remote(fetch_info)
69
+
70
+ preset_cfg = presets.get_installed_preset(preset)
71
+ preset_path = (
72
+ presets.get_preset_installation_path(preset)
73
+ if preset_cfg.contest is not None
74
+ else presets.get_preset_installation_path('default')
75
+ )
76
+
77
+ contest_path = (
78
+ presets.get_preset_installation_path(preset) / preset_cfg.contest
79
+ if preset_cfg.contest is not None
80
+ else presets.get_preset_installation_path('default') / 'contest'
81
+ )
82
+
83
+ if not contest_path.is_dir():
84
+ console.console.print(
85
+ f'[error]Contest template [item]{contest_path}[/item] does not exist.[/error]'
86
+ )
87
+ raise typer.Exit(1)
88
+
89
+ dest_path = pathlib.Path(name)
90
+
91
+ if dest_path.exists():
92
+ console.console.print(
93
+ f'[error]Directory [item]{dest_path}[/item] already exists.[/error]'
94
+ )
95
+ raise typer.Exit(1)
96
+
97
+ shutil.copytree(str(contest_path), str(dest_path))
98
+ shutil.rmtree(str(dest_path / 'build'), ignore_errors=True)
99
+ shutil.rmtree(str(dest_path / '.box'), ignore_errors=True)
100
+ # TODO: consider clearing build and .box recursively for nested problem directories
101
+ for lock in dest_path.rglob('.preset-lock.yml'):
102
+ lock.unlink(missing_ok=True)
103
+
104
+ if local:
105
+ shutil.copytree(str(preset_path), str(dest_path / '.local.rbx'))
106
+
107
+ with utils.new_cd(dest_path):
108
+ contest_utils.clear_all_caches()
109
+ presets.generate_lock(preset if not local else presets.LOCAL)
110
+
111
+
112
+ @app.command('edit, e', help='Open contest.rbx.yml in your default editor.')
113
+ @within_contest
114
+ def edit():
115
+ console.console.print('Opening contest definition in editor...')
116
+ # Call this function just to raise exception in case we're no in
117
+ # a problem package.
118
+ find_contest()
119
+ open_editor(find_contest_yaml() or pathlib.Path())
120
+
121
+
122
+ @app.command('add, a', help='Add new problem to contest.')
123
+ @within_contest
124
+ def add(name: str, short_name: str, preset: Optional[str] = None):
125
+ utils.validate_field(ContestProblem, 'short_name', short_name)
126
+ utils.validate_field(Package, 'name', name)
127
+
128
+ if short_name in [p.short_name for p in find_contest_package_or_die().problems]:
129
+ console.console.print(
130
+ f'[error]Problem [item]{short_name}[/item] already exists in contest.[/error]',
131
+ )
132
+ raise typer.Exit(1)
133
+
134
+ preset_lock = presets.get_preset_lock()
135
+ if preset is None and preset_lock is not None:
136
+ preset = preset_lock.preset_name
137
+ creation.create(name, preset=preset, path=pathlib.Path(short_name))
138
+
139
+ contest = find_contest_package_or_die()
140
+ # Reassign mutable object before saving.
141
+ contest.problems = sorted(
142
+ [
143
+ *contest.problems,
144
+ ContestProblem(short_name=short_name, path=pathlib.Path(short_name)),
145
+ ],
146
+ key=lambda p: p.short_name,
147
+ )
148
+
149
+ save_contest(contest)
150
+ console.console.print(f'Problem [item]{short_name}[/item] added to contest.')
151
+
152
+
153
+ @app.command(
154
+ 'each',
155
+ help='Run a command for each problem in the contest.',
156
+ context_settings={'allow_extra_args': True, 'ignore_unknown_options': True},
157
+ )
158
+ @within_contest
159
+ def each(ctx: typer.Context) -> None:
160
+ command = ' '.join(['rbx'] + ctx.args)
161
+ contest = find_contest_package_or_die()
162
+ ok = True
163
+ for problem in contest.problems:
164
+ console.console.print(
165
+ f'[status]Running [item]{command}[/item] for [item]{problem.short_name}[/item]...[/status]'
166
+ )
167
+
168
+ retcode = subprocess.call(
169
+ command,
170
+ cwd=problem.get_path(),
171
+ shell=True,
172
+ )
173
+ ok = ok and retcode == 0
174
+ console.console.print()
175
+
176
+ if not ok:
177
+ console.console.print(
178
+ '[error]One of the commands above failed. Check the output![/error]'
179
+ )