rbx.cp 0.11.2__py3-none-any.whl → 0.13.2__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/box/builder.py +3 -3
- rbx/box/cli.py +9 -0
- rbx/box/contest/build_contest_statements.py +5 -6
- rbx/box/contest/statements.py +0 -1
- rbx/box/generators.py +93 -23
- rbx/box/header.py +5 -0
- rbx/box/lang.py +25 -12
- rbx/box/package.py +56 -4
- rbx/box/packaging/contest_main.py +40 -7
- rbx/box/packaging/importer.py +37 -0
- rbx/box/packaging/main.py +18 -65
- rbx/box/packaging/packager.py +95 -2
- rbx/box/packaging/polygon/importer.py +232 -0
- rbx/box/packaging/polygon/packager.py +36 -5
- rbx/box/packaging/polygon/upload.py +34 -14
- rbx/box/packaging/polygon/xml_schema.py +15 -6
- rbx/box/schema.py +3 -3
- rbx/box/solutions.py +8 -12
- rbx/box/statements/build_statements.py +0 -1
- rbx/box/statements/latex.py +11 -0
- rbx/box/stresses.py +1 -1
- rbx/box/tooling/converter.py +76 -0
- rbx/box/tooling/main.py +54 -1
- rbx/grading/caching.py +1 -0
- rbx/grading/judge/sandbox.py +1 -0
- rbx/grading/steps.py +1 -0
- rbx/resources/presets/default/contest/.gitignore +15 -0
- rbx/resources/presets/default/contest/contest.rbx.yml +2 -2
- rbx/resources/presets/default/problem/.gitignore +15 -0
- rbx/resources/presets/default/problem/problem.rbx.yml +1 -4
- rbx/resources/presets/default/problem/testplan/random.py +1 -1
- rbx/resources/presets/default/problem/testplan/random.txt +2 -4
- rbx/resources/presets/default/problem/validator.cpp +2 -1
- rbx/resources/presets/default/shared/icpc.sty +1 -1
- rbx/utils.py +13 -0
- {rbx_cp-0.11.2.dist-info → rbx_cp-0.13.2.dist-info}/METADATA +2 -2
- {rbx_cp-0.11.2.dist-info → rbx_cp-0.13.2.dist-info}/RECORD +40 -37
- {rbx_cp-0.11.2.dist-info → rbx_cp-0.13.2.dist-info}/LICENSE +0 -0
- {rbx_cp-0.11.2.dist-info → rbx_cp-0.13.2.dist-info}/WHEEL +0 -0
- {rbx_cp-0.11.2.dist-info → rbx_cp-0.13.2.dist-info}/entry_points.txt +0 -0
rbx/box/builder.py
CHANGED
@@ -96,10 +96,10 @@ async def verify(verification: environment.VerificationParam) -> bool:
|
|
96
96
|
|
97
97
|
tracked_solutions = None
|
98
98
|
if verification < VerificationLevel.ALL_SOLUTIONS.value:
|
99
|
-
pkg = package.find_problem_package_or_die()
|
100
|
-
|
101
99
|
tracked_solutions = {
|
102
|
-
str(solution.path)
|
100
|
+
str(solution.path)
|
101
|
+
for solution in package.get_solutions()
|
102
|
+
if is_fast(solution)
|
103
103
|
}
|
104
104
|
|
105
105
|
with utils.StatusProgress('Running solutions...') as s:
|
rbx/box/cli.py
CHANGED
@@ -186,6 +186,15 @@ def on(ctx: typer.Context, problems: str) -> None:
|
|
186
186
|
contest.on(ctx, problems)
|
187
187
|
|
188
188
|
|
189
|
+
@app.command(
|
190
|
+
'each',
|
191
|
+
help='Run a command for each problem in the contest.',
|
192
|
+
context_settings={'allow_extra_args': True, 'ignore_unknown_options': True},
|
193
|
+
)
|
194
|
+
def each(ctx: typer.Context) -> None:
|
195
|
+
contest.each(ctx)
|
196
|
+
|
197
|
+
|
189
198
|
@app.command('diff', hidden=True)
|
190
199
|
def diff(path1: pathlib.Path, path2: pathlib.Path):
|
191
200
|
from rbx.box.ui import main as ui_pkg
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import dataclasses
|
2
2
|
import pathlib
|
3
|
-
import subprocess
|
4
3
|
import tempfile
|
5
4
|
import typing
|
6
5
|
from typing import Any, Dict, List, Optional, Tuple
|
@@ -13,7 +12,7 @@ from rbx.box.contest.contest_package import get_problems
|
|
13
12
|
from rbx.box.contest.schema import Contest, ContestProblem, ContestStatement
|
14
13
|
from rbx.box.formatting import href
|
15
14
|
from rbx.box.schema import Package, Testcase
|
16
|
-
from rbx.box.statements import build_statements
|
15
|
+
from rbx.box.statements import build_statements, latex
|
17
16
|
from rbx.box.statements.build_statements import (
|
18
17
|
get_builders,
|
19
18
|
get_environment_languages_for_statement,
|
@@ -253,10 +252,10 @@ def build_contest_only(
|
|
253
252
|
console.console.log(
|
254
253
|
f'Installing LaTeX packages for [item]{statement.name} {statement.language}[/item]...'
|
255
254
|
)
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
255
|
+
tmp_file = pathlib.Path(td) / '__tmp_install__.tex'
|
256
|
+
tmp_file.write_bytes(output)
|
257
|
+
latex.install_tex_packages(tmp_file, pathlib.Path(td))
|
258
|
+
|
260
259
|
last_content = output
|
261
260
|
last_output = bdr.output_type()
|
262
261
|
|
rbx/box/contest/statements.py
CHANGED
rbx/box/generators.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import functools
|
1
2
|
import pathlib
|
2
3
|
import shutil
|
3
4
|
from typing import Dict, List, Optional, Set
|
@@ -40,12 +41,44 @@ def _compile_generator(generator: CodeItem) -> str:
|
|
40
41
|
return compile_item(generator, sanitized=SanitizationLevel.PREFER)
|
41
42
|
|
42
43
|
|
44
|
+
@functools.cache
|
45
|
+
def _warn_once_about_crlf():
|
46
|
+
console.console.print(
|
47
|
+
'[warning]It seems a few files have CRLF (\\r\\n) line endings.[/warning]'
|
48
|
+
)
|
49
|
+
console.console.print(
|
50
|
+
'[warning]This usually happens when the file is created on Windows. Please convert the file to LF (\\n) line endings.[/warning]'
|
51
|
+
)
|
52
|
+
console.console.print(
|
53
|
+
'[warning]If you are in VSCode, you can make sure LF (\\n) line endings are used by changing the [item]"files.eol"[/item] setting.[/warning]'
|
54
|
+
)
|
55
|
+
|
56
|
+
|
57
|
+
@functools.cache
|
58
|
+
def _warn_about_crlf(path: pathlib.Path):
|
59
|
+
_warn_once_about_crlf()
|
60
|
+
console.console.print(
|
61
|
+
f'[warning]Testcase file [item]{path}[/item] has CRLF (\\r\\n) line endings, converting to LF (\\n).[/warning]'
|
62
|
+
)
|
63
|
+
|
64
|
+
|
65
|
+
def _check_crlf(path: pathlib.Path):
|
66
|
+
with open(path, 'rb') as f:
|
67
|
+
for line in f:
|
68
|
+
if line.endswith(b'\r\n') or line.endswith(b'\n\r'):
|
69
|
+
_warn_about_crlf(path)
|
70
|
+
break
|
71
|
+
|
72
|
+
path.write_text(path.read_text().replace('\r', ''))
|
73
|
+
|
74
|
+
|
43
75
|
def _copy_testcase_over(
|
44
76
|
testcase: Testcase,
|
45
77
|
dest: Testcase,
|
46
78
|
):
|
47
79
|
testcase = fill_output_for_defined_testcase(testcase)
|
48
80
|
dest.inputPath.parent.mkdir(parents=True, exist_ok=True)
|
81
|
+
_check_crlf(testcase.inputPath)
|
49
82
|
shutil.copy(
|
50
83
|
str(testcase.inputPath),
|
51
84
|
str(dest.inputPath),
|
@@ -55,6 +88,7 @@ def _copy_testcase_over(
|
|
55
88
|
and testcase.outputPath.is_file()
|
56
89
|
and dest.outputPath is not None
|
57
90
|
):
|
91
|
+
_check_crlf(testcase.outputPath)
|
58
92
|
dest.outputPath.parent.mkdir(parents=True, exist_ok=True)
|
59
93
|
shutil.copy(
|
60
94
|
str(testcase.outputPath),
|
@@ -63,7 +97,10 @@ def _copy_testcase_over(
|
|
63
97
|
|
64
98
|
|
65
99
|
def _copy_testcase_output_over(
|
66
|
-
src_output_path: pathlib.Path,
|
100
|
+
src_output_path: pathlib.Path,
|
101
|
+
dest_output_path: pathlib.Path,
|
102
|
+
suffix: str,
|
103
|
+
dry_run: bool = False,
|
67
104
|
) -> bool:
|
68
105
|
dest_output_path.parent.mkdir(parents=True, exist_ok=True)
|
69
106
|
|
@@ -71,38 +108,64 @@ def _copy_testcase_output_over(
|
|
71
108
|
if not src_path.is_file():
|
72
109
|
return False
|
73
110
|
|
111
|
+
if dry_run:
|
112
|
+
return True
|
113
|
+
|
114
|
+
_check_crlf(src_path)
|
74
115
|
shutil.copy(str(src_path), str(dest_output_path.with_suffix(suffix)))
|
75
116
|
return True
|
76
117
|
|
77
118
|
|
78
119
|
def _copy_testcase_outputs_over(
|
79
|
-
testcase: Testcase, dest: Testcase, pipes: bool = False
|
120
|
+
testcase: Testcase, dest: Testcase, pipes: bool = False, dry_run: bool = False
|
80
121
|
):
|
81
122
|
assert dest.outputPath is not None
|
82
|
-
|
123
|
+
if not dry_run:
|
124
|
+
dest.outputPath.parent.mkdir(parents=True, exist_ok=True)
|
83
125
|
|
84
126
|
has_copied = False
|
85
127
|
|
86
128
|
if testcase.outputPath is not None and testcase.outputPath.is_file():
|
87
|
-
|
129
|
+
if not dry_run:
|
130
|
+
_check_crlf(testcase.outputPath)
|
131
|
+
shutil.copy(str(testcase.outputPath), str(dest.outputPath))
|
88
132
|
has_copied = True
|
89
133
|
|
90
134
|
if not pipes:
|
91
135
|
return has_copied
|
92
136
|
|
93
137
|
reference_path = testcase.outputPath or testcase.inputPath
|
94
|
-
if _copy_testcase_output_over(
|
138
|
+
if _copy_testcase_output_over(
|
139
|
+
reference_path, dest.outputPath, '.pin', dry_run=dry_run
|
140
|
+
):
|
95
141
|
has_copied = True
|
96
142
|
|
97
|
-
if _copy_testcase_output_over(
|
143
|
+
if _copy_testcase_output_over(
|
144
|
+
reference_path, dest.outputPath, '.pout', dry_run=dry_run
|
145
|
+
):
|
98
146
|
has_copied = True
|
99
147
|
|
100
|
-
if _copy_testcase_output_over(
|
148
|
+
if _copy_testcase_output_over(
|
149
|
+
reference_path, dest.outputPath, '.pio', dry_run=dry_run
|
150
|
+
):
|
101
151
|
has_copied = True
|
102
152
|
|
103
153
|
return has_copied
|
104
154
|
|
105
155
|
|
156
|
+
def _needs_output(generation_entries: List[GenerationTestcaseEntry]) -> bool:
|
157
|
+
for entry in generation_entries:
|
158
|
+
tc = entry.metadata.copied_to
|
159
|
+
if not tc.inputPath.is_file():
|
160
|
+
continue
|
161
|
+
if entry.metadata.copied_from is not None and _copy_testcase_outputs_over(
|
162
|
+
entry.metadata.copied_from, tc, dry_run=True
|
163
|
+
):
|
164
|
+
continue
|
165
|
+
return True
|
166
|
+
return False
|
167
|
+
|
168
|
+
|
106
169
|
def get_all_built_testcases() -> Dict[str, List[Testcase]]:
|
107
170
|
pkg = package.find_problem_package_or_die()
|
108
171
|
res = {group.name: find_built_testcases(group) for group in pkg.testcases}
|
@@ -120,35 +183,42 @@ def get_call_from_string(call_str: str) -> GeneratorCall:
|
|
120
183
|
async def _get_necessary_generators_for_groups(
|
121
184
|
groups: Optional[Set[str]] = None,
|
122
185
|
) -> Set[str]:
|
123
|
-
pkg = package.find_problem_package_or_die()
|
124
|
-
existing_generators = set(generator.name for generator in pkg.generators)
|
125
186
|
necessary_generators = set()
|
126
187
|
|
127
188
|
class NecessaryGeneratorsVisitor(TestcaseGroupVisitor):
|
128
189
|
async def visit(self, entry: GenerationTestcaseEntry):
|
129
190
|
if entry.metadata.generator_call is not None:
|
191
|
+
if (
|
192
|
+
package.get_generator_or_nil(entry.metadata.generator_call.name)
|
193
|
+
is None
|
194
|
+
):
|
195
|
+
console.console.print(
|
196
|
+
f'[error]Generator [item]{entry.metadata.generator_call.name}[/item] is not present in the package.[/error]'
|
197
|
+
)
|
198
|
+
if entry.metadata.generator_script is not None:
|
199
|
+
console.console.print(
|
200
|
+
f'[error]This generator is referenced from [item]{entry.metadata.generator_script}[/item].[/error]'
|
201
|
+
)
|
202
|
+
raise typer.Exit(1)
|
130
203
|
necessary_generators.add(entry.metadata.generator_call.name)
|
131
204
|
|
132
205
|
await run_testcase_visitor(NecessaryGeneratorsVisitor(groups))
|
133
206
|
|
134
|
-
return
|
207
|
+
return necessary_generators
|
135
208
|
|
136
209
|
|
137
210
|
def compile_generators(
|
211
|
+
tracked_generators: Set[str],
|
138
212
|
progress: Optional[StatusProgress] = None,
|
139
|
-
tracked_generators: Optional[Set[str]] = None,
|
140
213
|
) -> Dict[str, str]:
|
141
214
|
def update_status(text: str):
|
142
215
|
if progress is not None:
|
143
216
|
progress.update(text)
|
144
217
|
|
145
|
-
pkg = package.find_problem_package_or_die()
|
146
|
-
|
147
218
|
generator_to_compiled_digest = {}
|
148
219
|
|
149
|
-
for
|
150
|
-
|
151
|
-
continue
|
220
|
+
for generator_name in tracked_generators:
|
221
|
+
generator = package.get_generator(generator_name)
|
152
222
|
update_status(f'Compiling generator [item]{generator.name}[/item]')
|
153
223
|
try:
|
154
224
|
generator_to_compiled_digest[generator.name] = _compile_generator(generator)
|
@@ -267,9 +337,7 @@ async def generate_testcases(
|
|
267
337
|
|
268
338
|
compiled_generators = compile_generators(
|
269
339
|
progress=progress,
|
270
|
-
tracked_generators=await _get_necessary_generators_for_groups(groups)
|
271
|
-
if groups is not None
|
272
|
-
else None,
|
340
|
+
tracked_generators=await _get_necessary_generators_for_groups(groups),
|
273
341
|
)
|
274
342
|
|
275
343
|
testcase_utils.clear_built_testcases()
|
@@ -353,17 +421,20 @@ async def generate_outputs_for_testcases(
|
|
353
421
|
if progress is not None:
|
354
422
|
progress.step()
|
355
423
|
|
424
|
+
generation_entries = await extract_generation_testcases(entries)
|
425
|
+
needs_output = _needs_output(generation_entries)
|
426
|
+
|
356
427
|
main_solution = package.get_main_solution()
|
357
428
|
solution_digest: Optional[str] = None
|
358
429
|
|
359
430
|
pkg = package.find_problem_package_or_die()
|
360
431
|
|
361
|
-
if pkg.type == TaskType.COMMUNICATION:
|
432
|
+
if pkg.type == TaskType.COMMUNICATION and needs_output:
|
362
433
|
interactor_digest = checkers.compile_interactor(progress)
|
363
434
|
else:
|
364
435
|
interactor_digest = None
|
365
436
|
|
366
|
-
if main_solution is not None:
|
437
|
+
if main_solution is not None and needs_output:
|
367
438
|
if progress:
|
368
439
|
progress.update('Compiling main solution...')
|
369
440
|
try:
|
@@ -376,8 +447,6 @@ async def generate_outputs_for_testcases(
|
|
376
447
|
shutil.rmtree(str(gen_runs_dir), ignore_errors=True)
|
377
448
|
gen_runs_dir.mkdir(parents=True, exist_ok=True)
|
378
449
|
|
379
|
-
generation_entries = await extract_generation_testcases(entries)
|
380
|
-
|
381
450
|
for entry in generation_entries:
|
382
451
|
tc = entry.metadata.copied_to
|
383
452
|
if not tc.inputPath.is_file():
|
@@ -392,6 +461,7 @@ async def generate_outputs_for_testcases(
|
|
392
461
|
step()
|
393
462
|
continue
|
394
463
|
|
464
|
+
assert needs_output
|
395
465
|
if (
|
396
466
|
main_solution is None or solution_digest is None
|
397
467
|
) and not tc.outputPath.is_file():
|
rbx/box/header.py
CHANGED
@@ -16,6 +16,11 @@ def get_header() -> pathlib.Path:
|
|
16
16
|
|
17
17
|
|
18
18
|
def generate_header():
|
19
|
+
override_header = pathlib.Path('rbx.override.h')
|
20
|
+
if override_header.is_file():
|
21
|
+
pathlib.Path('rbx.h').write_bytes(override_header.read_bytes())
|
22
|
+
return
|
23
|
+
|
19
24
|
with importlib.resources.as_file(
|
20
25
|
importlib.resources.files('rbx') / 'resources' / 'templates' / 'rbx.h'
|
21
26
|
) as file:
|
rbx/box/lang.py
CHANGED
@@ -1,27 +1,40 @@
|
|
1
1
|
import functools
|
2
|
-
from typing import List
|
2
|
+
from typing import Dict, List
|
3
3
|
|
4
4
|
import iso639
|
5
5
|
|
6
|
-
|
6
|
+
|
7
|
+
@functools.cache
|
8
|
+
def _get_lowercase_name_mapping() -> Dict[str, iso639.Lang]:
|
9
|
+
res = {}
|
10
|
+
for lang in iso639.iter_langs():
|
11
|
+
res[lang.name.lower()] = lang
|
12
|
+
return res
|
13
|
+
|
14
|
+
|
15
|
+
def _get_lang_name(lang: str) -> str:
|
16
|
+
mapping = _get_lowercase_name_mapping()
|
17
|
+
if lang.lower() in mapping:
|
18
|
+
return mapping[lang.lower()].name
|
19
|
+
return lang
|
20
|
+
|
21
|
+
|
22
|
+
def code_to_lang(lang: str) -> str:
|
23
|
+
return iso639.Lang(lang).name.lower()
|
7
24
|
|
8
25
|
|
9
26
|
def code_to_langs(langs: List[str]) -> List[str]:
|
10
|
-
return [
|
27
|
+
return [code_to_lang(lang) for lang in langs]
|
11
28
|
|
12
29
|
|
13
30
|
@functools.cache
|
14
31
|
def is_valid_lang_code(lang: str) -> bool:
|
15
|
-
|
16
|
-
|
17
|
-
except iso639.LanguageNotFoundError:
|
18
|
-
console.console.print(
|
19
|
-
f'[warning]Language [item]{lang}[/item] is being skipped because it is not a iso639 language.[/warning]'
|
20
|
-
)
|
21
|
-
return False
|
32
|
+
return iso639.is_language(lang)
|
33
|
+
|
22
34
|
|
23
|
-
|
35
|
+
def lang_to_code(lang: str) -> str:
|
36
|
+
return iso639.Lang(_get_lang_name(lang)).pt1
|
24
37
|
|
25
38
|
|
26
39
|
def langs_to_code(langs: List[str]) -> List[str]:
|
27
|
-
return [
|
40
|
+
return [lang_to_code(lang) for lang in langs]
|
rbx/box/package.py
CHANGED
@@ -229,13 +229,45 @@ def get_build_testgroup_path(
|
|
229
229
|
|
230
230
|
|
231
231
|
@functools.cache
|
232
|
-
def
|
232
|
+
def get_generator_or_nil(
|
233
|
+
name: str, root: pathlib.Path = pathlib.Path()
|
234
|
+
) -> Optional[Generator]:
|
233
235
|
package = find_problem_package_or_die(root)
|
234
236
|
for generator in package.generators:
|
235
237
|
if generator.name == name:
|
236
238
|
return generator
|
237
|
-
|
238
|
-
|
239
|
+
|
240
|
+
path = pathlib.Path(root / name)
|
241
|
+
if path.is_file():
|
242
|
+
return Generator(name=name, path=path)
|
243
|
+
|
244
|
+
path_pattern = path.with_suffix('.*')
|
245
|
+
matching_files = list(
|
246
|
+
file.relative_to(root) for file in root.glob(str(path_pattern))
|
247
|
+
)
|
248
|
+
|
249
|
+
if len(matching_files) > 1:
|
250
|
+
console.console.print(
|
251
|
+
f'[error]Multiple candidate generators found for [item]{name}[/item]: {matching_files}[/error]'
|
252
|
+
)
|
253
|
+
console.console.print(
|
254
|
+
'[info]Please specify the generator path explicitly, including the extension, or rename the conflicting files.[/info]'
|
255
|
+
)
|
256
|
+
raise typer.Exit(1)
|
257
|
+
|
258
|
+
if matching_files:
|
259
|
+
return Generator(name=name, path=matching_files[0])
|
260
|
+
|
261
|
+
return None
|
262
|
+
|
263
|
+
|
264
|
+
@functools.cache
|
265
|
+
def get_generator(name: str, root: pathlib.Path = pathlib.Path()) -> Generator:
|
266
|
+
generator = get_generator_or_nil(name, root)
|
267
|
+
if generator is None:
|
268
|
+
console.console.print(f'[error]Generator [item]{name}[/item] not found[/error]')
|
269
|
+
raise typer.Exit(1)
|
270
|
+
return generator
|
239
271
|
|
240
272
|
|
241
273
|
@functools.cache
|
@@ -292,7 +324,27 @@ def get_interactor(root: pathlib.Path = pathlib.Path()) -> CodeItem:
|
|
292
324
|
@functools.cache
|
293
325
|
def get_solutions(root: pathlib.Path = pathlib.Path()) -> List[Solution]:
|
294
326
|
package = find_problem_package_or_die(root)
|
295
|
-
|
327
|
+
seen_paths = set()
|
328
|
+
res = []
|
329
|
+
|
330
|
+
def add_solution(entry: Solution):
|
331
|
+
if entry.path in seen_paths:
|
332
|
+
return
|
333
|
+
seen_paths.add(entry.path)
|
334
|
+
res.append(entry)
|
335
|
+
|
336
|
+
for entry in package.solutions:
|
337
|
+
if '*' in str(entry.path):
|
338
|
+
for file in root.glob(str(entry.path)):
|
339
|
+
relative_file = file.relative_to(root)
|
340
|
+
add_solution(
|
341
|
+
Solution.model_copy(
|
342
|
+
entry, update={'path': relative_file}, deep=True
|
343
|
+
)
|
344
|
+
)
|
345
|
+
continue
|
346
|
+
add_solution(entry)
|
347
|
+
return res
|
296
348
|
|
297
349
|
|
298
350
|
@functools.cache
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import pathlib
|
2
2
|
import tempfile
|
3
|
-
from typing import Type
|
3
|
+
from typing import Optional, Type
|
4
4
|
|
5
5
|
import syncer
|
6
6
|
import typer
|
@@ -8,12 +8,13 @@ import typer
|
|
8
8
|
from rbx import annotations, console
|
9
9
|
from rbx.box import cd, environment, package
|
10
10
|
from rbx.box.contest import build_contest_statements, contest_package
|
11
|
-
from rbx.box.packaging.main import run_packager
|
12
11
|
from rbx.box.packaging.packager import (
|
13
12
|
BaseContestPackager,
|
14
13
|
BasePackager,
|
15
14
|
BuiltContestStatement,
|
16
15
|
BuiltProblemPackage,
|
16
|
+
ContestZipper,
|
17
|
+
run_packager,
|
17
18
|
)
|
18
19
|
|
19
20
|
app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
|
@@ -23,6 +24,7 @@ async def run_contest_packager(
|
|
23
24
|
contest_packager_cls: Type[BaseContestPackager],
|
24
25
|
packager_cls: Type[BasePackager],
|
25
26
|
verification: environment.VerificationParam,
|
27
|
+
**kwargs,
|
26
28
|
):
|
27
29
|
contest = contest_package.find_contest_package_or_die()
|
28
30
|
|
@@ -34,7 +36,9 @@ async def run_contest_packager(
|
|
34
36
|
)
|
35
37
|
with cd.new_package_cd(problem.get_path()):
|
36
38
|
package.clear_package_cache()
|
37
|
-
package_path = await run_packager(
|
39
|
+
package_path = await run_packager(
|
40
|
+
packager_cls, verification=verification, **kwargs
|
41
|
+
)
|
38
42
|
built_packages.append(
|
39
43
|
BuiltProblemPackage(
|
40
44
|
path=problem.get_path() / package_path,
|
@@ -44,7 +48,7 @@ async def run_contest_packager(
|
|
44
48
|
)
|
45
49
|
|
46
50
|
# Build statements.
|
47
|
-
packager = contest_packager_cls()
|
51
|
+
packager = contest_packager_cls(**kwargs)
|
48
52
|
statement_types = packager.statement_types()
|
49
53
|
built_statements = []
|
50
54
|
|
@@ -63,12 +67,12 @@ async def run_contest_packager(
|
|
63
67
|
|
64
68
|
# Build contest-level package.
|
65
69
|
with tempfile.TemporaryDirectory() as td:
|
66
|
-
packager.package(
|
70
|
+
result_path = packager.package(
|
67
71
|
built_packages, pathlib.Path('build'), pathlib.Path(td), built_statements
|
68
72
|
)
|
69
73
|
|
70
74
|
console.console.print(
|
71
|
-
f'[success]
|
75
|
+
f'[success]Created contest package for [item]{packager.name()}[/item] at [item]{result_path}[/item]![/success]'
|
72
76
|
)
|
73
77
|
|
74
78
|
|
@@ -77,6 +81,12 @@ async def run_contest_packager(
|
|
77
81
|
@syncer.sync
|
78
82
|
async def polygon(
|
79
83
|
verification: environment.VerificationParam,
|
84
|
+
language: Optional[str] = typer.Option(
|
85
|
+
None,
|
86
|
+
'--language',
|
87
|
+
'-l',
|
88
|
+
help='If set, will use the given language as the main language.',
|
89
|
+
),
|
80
90
|
):
|
81
91
|
from rbx.box.packaging.polygon.packager import (
|
82
92
|
PolygonContestPackager,
|
@@ -84,7 +94,30 @@ async def polygon(
|
|
84
94
|
)
|
85
95
|
|
86
96
|
await run_contest_packager(
|
87
|
-
PolygonContestPackager,
|
97
|
+
PolygonContestPackager,
|
98
|
+
PolygonPackager,
|
99
|
+
verification=verification,
|
100
|
+
main_language=language,
|
101
|
+
)
|
102
|
+
|
103
|
+
|
104
|
+
@app.command('boca', help='Build a contest package for BOCA.')
|
105
|
+
@contest_package.within_contest
|
106
|
+
@syncer.sync
|
107
|
+
async def boca(
|
108
|
+
verification: environment.VerificationParam,
|
109
|
+
):
|
110
|
+
from rbx.box.packaging.boca.packager import BocaPackager
|
111
|
+
|
112
|
+
class BocaContestPackager(ContestZipper):
|
113
|
+
def __init__(self, **kwargs):
|
114
|
+
super().__init__('boca-contest', zip_inner=True, **kwargs)
|
115
|
+
|
116
|
+
def name(self) -> str:
|
117
|
+
return 'boca'
|
118
|
+
|
119
|
+
await run_contest_packager(
|
120
|
+
BocaContestPackager, BocaPackager, verification=verification
|
88
121
|
)
|
89
122
|
|
90
123
|
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import pathlib
|
2
|
+
from abc import ABC, abstractmethod
|
3
|
+
from typing import Type
|
4
|
+
|
5
|
+
from rbx import console
|
6
|
+
|
7
|
+
|
8
|
+
class BaseImporter(ABC):
|
9
|
+
@classmethod
|
10
|
+
@abstractmethod
|
11
|
+
def name(cls) -> str:
|
12
|
+
pass
|
13
|
+
|
14
|
+
@abstractmethod
|
15
|
+
async def import_package(self, pkg_path: pathlib.Path, into_path: pathlib.Path):
|
16
|
+
pass
|
17
|
+
|
18
|
+
|
19
|
+
class BaseContestImporter(ABC):
|
20
|
+
@classmethod
|
21
|
+
@abstractmethod
|
22
|
+
def name(cls) -> str:
|
23
|
+
pass
|
24
|
+
|
25
|
+
@abstractmethod
|
26
|
+
async def import_package(self, pkg_path: pathlib.Path, into_path: pathlib.Path):
|
27
|
+
pass
|
28
|
+
|
29
|
+
|
30
|
+
async def run_importer(
|
31
|
+
importer_cls: Type[BaseImporter], pkg_path: pathlib.Path, into_path: pathlib.Path
|
32
|
+
):
|
33
|
+
importer = importer_cls()
|
34
|
+
console.console.print(
|
35
|
+
f'Importing package from [item]{pkg_path}[/item] to [item]{into_path}[/item] with [item]{importer.name()}[/item]...'
|
36
|
+
)
|
37
|
+
await importer.import_package(pkg_path, into_path)
|