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.
- rbx/__init__.py +0 -0
- rbx/annotations.py +127 -0
- rbx/autoenum.py +333 -0
- rbx/box/__init__.py +0 -0
- rbx/box/builder.py +77 -0
- rbx/box/cd.py +37 -0
- rbx/box/checkers.py +134 -0
- rbx/box/code.py +185 -0
- rbx/box/compile.py +56 -0
- rbx/box/conftest.py +42 -0
- rbx/box/contest/__init__.py +0 -0
- rbx/box/contest/build_contest_statements.py +347 -0
- rbx/box/contest/contest_package.py +76 -0
- rbx/box/contest/contest_utils.py +20 -0
- rbx/box/contest/main.py +179 -0
- rbx/box/contest/schema.py +155 -0
- rbx/box/contest/statements.py +82 -0
- rbx/box/creation.py +72 -0
- rbx/box/download.py +64 -0
- rbx/box/environment.py +345 -0
- rbx/box/extensions.py +26 -0
- rbx/box/generators.py +478 -0
- rbx/box/generators_test.py +63 -0
- rbx/box/main.py +449 -0
- rbx/box/package.py +316 -0
- rbx/box/packaging/boca/extension.py +27 -0
- rbx/box/packaging/boca/packager.py +245 -0
- rbx/box/packaging/contest_main.py +82 -0
- rbx/box/packaging/main.py +68 -0
- rbx/box/packaging/packager.py +117 -0
- rbx/box/packaging/polygon/packager.py +320 -0
- rbx/box/packaging/polygon/test.py +81 -0
- rbx/box/packaging/polygon/xml_schema.py +106 -0
- rbx/box/presets/__init__.py +503 -0
- rbx/box/presets/fetch.py +70 -0
- rbx/box/presets/lock_schema.py +20 -0
- rbx/box/presets/schema.py +59 -0
- rbx/box/schema.py +394 -0
- rbx/box/solutions.py +792 -0
- rbx/box/solutions_test.py +41 -0
- rbx/box/statements/__init__.py +0 -0
- rbx/box/statements/build_statements.py +359 -0
- rbx/box/statements/builders.py +375 -0
- rbx/box/statements/joiners.py +113 -0
- rbx/box/statements/latex.py +47 -0
- rbx/box/statements/latex_jinja.py +214 -0
- rbx/box/statements/schema.py +138 -0
- rbx/box/stresses.py +292 -0
- rbx/box/stressing/__init__.py +0 -0
- rbx/box/stressing/finder_parser.py +359 -0
- rbx/box/stressing/generator_parser.py +258 -0
- rbx/box/testcases.py +54 -0
- rbx/box/ui/__init__.py +0 -0
- rbx/box/ui/captured_log.py +372 -0
- rbx/box/ui/css/app.tcss +48 -0
- rbx/box/ui/main.py +38 -0
- rbx/box/ui/run.py +209 -0
- rbx/box/validators.py +245 -0
- rbx/box/validators_test.py +15 -0
- rbx/checker.py +128 -0
- rbx/clone.py +197 -0
- rbx/config.py +271 -0
- rbx/conftest.py +38 -0
- rbx/console.py +27 -0
- rbx/create.py +37 -0
- rbx/edit.py +24 -0
- rbx/grading/__init__.py +0 -0
- rbx/grading/caching.py +356 -0
- rbx/grading/conftest.py +33 -0
- rbx/grading/judge/__init__.py +0 -0
- rbx/grading/judge/cacher.py +503 -0
- rbx/grading/judge/digester.py +35 -0
- rbx/grading/judge/sandbox.py +748 -0
- rbx/grading/judge/sandboxes/__init__.py +0 -0
- rbx/grading/judge/sandboxes/isolate.py +683 -0
- rbx/grading/judge/sandboxes/stupid_sandbox.py +310 -0
- rbx/grading/judge/sandboxes/timeit.py +217 -0
- rbx/grading/judge/storage.py +284 -0
- rbx/grading/judge/test.py +38 -0
- rbx/grading/judge/testiso.py +54 -0
- rbx/grading/steps.py +522 -0
- rbx/grading/steps_with_caching.py +59 -0
- rbx/grading/steps_with_caching_run_test.py +429 -0
- rbx/grading_utils.py +148 -0
- rbx/hydration.py +101 -0
- rbx/main.py +122 -0
- rbx/metadata.py +105 -0
- rbx/providers/__init__.py +43 -0
- rbx/providers/codeforces.py +73 -0
- rbx/providers/provider.py +26 -0
- rbx/resources/checkers/boilerplate.cpp +20 -0
- rbx/resources/default_config.json +48 -0
- rbx/resources/envs/default.rbx.yml +37 -0
- rbx/resources/envs/isolate.rbx.yml +37 -0
- rbx/resources/packagers/boca/checker.sh +43 -0
- rbx/resources/packagers/boca/compare +53 -0
- rbx/resources/packagers/boca/compile/c +172 -0
- rbx/resources/packagers/boca/compile/cc +173 -0
- rbx/resources/packagers/boca/compile/cpp +172 -0
- rbx/resources/packagers/boca/compile/java +194 -0
- rbx/resources/packagers/boca/compile/kt +155 -0
- rbx/resources/packagers/boca/compile/pas +172 -0
- rbx/resources/packagers/boca/compile/py2 +173 -0
- rbx/resources/packagers/boca/compile/py3 +173 -0
- rbx/resources/packagers/boca/run/c +128 -0
- rbx/resources/packagers/boca/run/cc +128 -0
- rbx/resources/packagers/boca/run/cpp +128 -0
- rbx/resources/packagers/boca/run/java +194 -0
- rbx/resources/packagers/boca/run/kt +159 -0
- rbx/resources/packagers/boca/run/py2 +166 -0
- rbx/resources/packagers/boca/run/py3 +166 -0
- rbx/resources/presets/default/contest/contest.rbx.yml +14 -0
- rbx/resources/presets/default/contest/statement/contest.rbx.tex +97 -0
- rbx/resources/presets/default/contest/statement/olymp.sty +250 -0
- rbx/resources/presets/default/contest/statement/template.rbx.tex +42 -0
- rbx/resources/presets/default/preset.rbx.yml +12 -0
- rbx/resources/presets/default/problem/.gitignore +6 -0
- rbx/resources/presets/default/problem/gen.cpp +9 -0
- rbx/resources/presets/default/problem/problem.rbx.yml +44 -0
- rbx/resources/presets/default/problem/random.py +3 -0
- rbx/resources/presets/default/problem/random.txt +2 -0
- rbx/resources/presets/default/problem/sols/main.cpp +9 -0
- rbx/resources/presets/default/problem/sols/slow.cpp +15 -0
- rbx/resources/presets/default/problem/sols/wa.cpp +9 -0
- rbx/resources/presets/default/problem/statement/olymp.sty +250 -0
- rbx/resources/presets/default/problem/statement/projecao.png +0 -0
- rbx/resources/presets/default/problem/statement/statement.rbx.tex +18 -0
- rbx/resources/presets/default/problem/statement/template.rbx.tex +89 -0
- rbx/resources/presets/default/problem/tests/samples/000.in +1 -0
- rbx/resources/presets/default/problem/tests/samples/001.in +1 -0
- rbx/resources/presets/default/problem/validator.cpp +16 -0
- rbx/resources/presets/default/problem/wcmp.cpp +34 -0
- rbx/resources/templates/template.cpp +19 -0
- rbx/run.py +45 -0
- rbx/schema.py +64 -0
- rbx/submit.py +61 -0
- rbx/submitors/__init__.py +18 -0
- rbx/submitors/codeforces.py +120 -0
- rbx/submitors/submitor.py +25 -0
- rbx/test.py +347 -0
- rbx/testcase.py +70 -0
- rbx/testcase_rendering.py +79 -0
- rbx/testdata/box1/gen1.cpp +7 -0
- rbx/testdata/box1/gen2.cpp +9 -0
- rbx/testdata/box1/genScript.py +2 -0
- rbx/testdata/box1/hard-tle.sol.cpp +26 -0
- rbx/testdata/box1/ole.cpp +17 -0
- rbx/testdata/box1/problem.rbx.yml +39 -0
- rbx/testdata/box1/re.sol.cpp +23 -0
- rbx/testdata/box1/sol.cpp +22 -0
- rbx/testdata/box1/tests/1.in +1 -0
- rbx/testdata/box1/tle-and-incorrect.sol.cpp +33 -0
- rbx/testdata/box1/tle.sol.cpp +35 -0
- rbx/testdata/box1/validator.cpp +11 -0
- rbx/testdata/box1/wa.sol.cpp +22 -0
- rbx/testdata/caching/executable.py +1 -0
- rbx/testdata/compatible +0 -0
- rbx/testing_utils.py +65 -0
- rbx/utils.py +162 -0
- rbx_cp-0.5.0.dist-info/LICENSE +201 -0
- rbx_cp-0.5.0.dist-info/METADATA +89 -0
- rbx_cp-0.5.0.dist-info/RECORD +164 -0
- rbx_cp-0.5.0.dist-info/WHEEL +4 -0
- 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()
|
rbx/box/contest/main.py
ADDED
@@ -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
|
+
)
|