rbx.cp 0.5.72__py3-none-any.whl → 0.6.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/annotations.py +21 -1
- rbx/box/cli.py +24 -8
- rbx/box/code.py +140 -3
- rbx/box/contest/build_contest_statements.py +44 -34
- rbx/box/contest/contest_utils.py +25 -0
- rbx/box/contest/main.py +24 -0
- rbx/box/contest/schema.py +52 -8
- rbx/box/contest/statements.py +53 -25
- rbx/box/download.py +19 -1
- rbx/box/fields.py +35 -0
- rbx/box/lang.py +27 -0
- rbx/box/package.py +1 -1
- rbx/box/packaging/boca/packager.py +48 -5
- rbx/box/packaging/contest_main.py +13 -0
- rbx/box/packaging/main.py +13 -2
- rbx/box/packaging/packager.py +4 -4
- rbx/box/packaging/pkg/packager.py +142 -0
- rbx/box/packaging/polygon/packager.py +2 -24
- rbx/box/packaging/polygon/upload.py +35 -17
- rbx/box/remote.py +2 -2
- rbx/box/schema.py +68 -18
- rbx/box/solutions.py +6 -1
- rbx/box/statements/build_statements.py +44 -27
- rbx/box/statements/builders.py +18 -10
- rbx/box/statements/expander.py +49 -0
- rbx/box/statements/latex_jinja.py +61 -4
- rbx/box/statements/schema.py +33 -9
- rbx/box/testcase_utils.py +19 -47
- rbx/box/tooling/__init__.py +0 -0
- rbx/box/tooling/boca/__init__.py +0 -0
- rbx/box/tooling/boca/main.py +13 -0
- rbx/box/tooling/boca/scrape.py +34 -0
- rbx/box/{packaging/boca/upload.py → tooling/boca/scraper.py} +77 -8
- rbx/box/tooling/main.py +8 -0
- rbx/box/ui/screens/run_explorer.py +1 -1
- rbx/box/ui/widgets/interaction_box.py +19 -1
- rbx/grading/caching.py +18 -2
- rbx/grading/judge/sandbox.py +48 -5
- rbx/grading/judge/sandboxes/isolate.py +1 -0
- rbx/grading/judge/sandboxes/stupid_sandbox.py +11 -5
- rbx/grading/judge/sandboxes/timeit.py +36 -15
- rbx/grading/processing_context.py +62 -78
- rbx/grading/steps.py +91 -40
- rbx/resources/packagers/boca/checker.sh +4 -1
- rbx/resources/packagers/boca/compile/c +2 -6
- rbx/resources/packagers/boca/compile/cc +2 -6
- rbx/resources/packagers/boca/compile/cpp +2 -6
- rbx/resources/packagers/boca/compile/java +1 -6
- rbx/resources/packagers/boca/compile/kt +24 -28
- rbx/resources/packagers/boca/compile/py2 +2 -6
- rbx/resources/packagers/boca/compile/py3 +2 -6
- rbx/resources/packagers/boca/interactive/c +15 -62
- rbx/resources/packagers/boca/interactive/cc +15 -62
- rbx/resources/packagers/boca/interactive/cpp +15 -61
- rbx/resources/packagers/boca/interactive/java +15 -67
- rbx/resources/packagers/boca/interactive/kt +15 -67
- rbx/resources/packagers/boca/interactive/py2 +15 -67
- rbx/resources/packagers/boca/interactive/py3 +15 -65
- rbx/resources/packagers/boca/interactor_compile.sh +5 -2
- rbx/resources/packagers/boca/interactor_run.sh +174 -0
- rbx/resources/packagers/boca/safeexec.c +530 -0
- rbx/resources/packagers/boca/safeexec_compile.sh +49 -0
- rbx/resources/presets/default/contest/contest.rbx.yml +9 -8
- rbx/resources/presets/default/problem/problem.rbx.yml +27 -26
- rbx/resources/templates/rbx.h +2 -3
- {rbx_cp-0.5.72.dist-info → rbx_cp-0.6.0.dist-info}/METADATA +2 -1
- {rbx_cp-0.5.72.dist-info → rbx_cp-0.6.0.dist-info}/RECORD +70 -59
- rbx/resources/packagers/boca/compile/pas +0 -172
- {rbx_cp-0.5.72.dist-info → rbx_cp-0.6.0.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.72.dist-info → rbx_cp-0.6.0.dist-info}/WHEEL +0 -0
- {rbx_cp-0.5.72.dist-info → rbx_cp-0.6.0.dist-info}/entry_points.txt +0 -0
rbx/box/contest/statements.py
CHANGED
@@ -10,6 +10,9 @@ from rbx.box.contest.contest_package import (
|
|
10
10
|
find_contest_package_or_die,
|
11
11
|
within_contest,
|
12
12
|
)
|
13
|
+
from rbx.box.contest.schema import ContestStatement
|
14
|
+
from rbx.box.formatting import href
|
15
|
+
from rbx.box.schema import expand_any_vars
|
13
16
|
from rbx.box.statements.schema import StatementType
|
14
17
|
|
15
18
|
app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
|
@@ -20,13 +23,18 @@ app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
|
|
20
23
|
@syncer.sync
|
21
24
|
async def build(
|
22
25
|
verification: environment.VerificationParam,
|
26
|
+
names: Annotated[
|
27
|
+
Optional[List[str]],
|
28
|
+
typer.Argument(
|
29
|
+
help='Names of statements to build.',
|
30
|
+
),
|
31
|
+
] = None,
|
23
32
|
languages: Annotated[
|
24
33
|
Optional[List[str]],
|
25
34
|
typer.Option(
|
26
|
-
default_factory=list,
|
27
35
|
help='Languages to build statements for. If not specified, build statements for all available languages.',
|
28
36
|
),
|
29
|
-
],
|
37
|
+
] = None,
|
30
38
|
output: Annotated[
|
31
39
|
Optional[StatementType],
|
32
40
|
typer.Option(
|
@@ -38,10 +46,14 @@ async def build(
|
|
38
46
|
bool,
|
39
47
|
typer.Option(help='Whether to build the statement with samples or not.'),
|
40
48
|
] = True,
|
41
|
-
|
42
|
-
|
43
|
-
typer.Option(
|
44
|
-
|
49
|
+
vars: Annotated[
|
50
|
+
Optional[List[str]],
|
51
|
+
typer.Option(
|
52
|
+
'-v',
|
53
|
+
'--vars',
|
54
|
+
help='Variables to be used in the statements.',
|
55
|
+
),
|
56
|
+
] = None,
|
45
57
|
):
|
46
58
|
contest = find_contest_package_or_die()
|
47
59
|
# At most run the validators, only in samples.
|
@@ -64,26 +76,42 @@ async def build(
|
|
64
76
|
raise typer.Exit(1)
|
65
77
|
|
66
78
|
contest = find_contest_package_or_die()
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
79
|
+
|
80
|
+
candidate_languages = set(languages or [])
|
81
|
+
candidate_names = set(names or [])
|
82
|
+
|
83
|
+
def should_process(st: ContestStatement) -> bool:
|
84
|
+
if candidate_languages and st.language not in candidate_languages:
|
85
|
+
return False
|
86
|
+
if candidate_names and st.name not in candidate_names:
|
87
|
+
return False
|
88
|
+
return True
|
89
|
+
|
90
|
+
valid_statements = [st for st in contest.expanded_statements if should_process(st)]
|
91
|
+
|
92
|
+
if not valid_statements:
|
93
|
+
console.console.print(
|
94
|
+
'[error]No statement found according to the specified criteria.[/error]',
|
95
|
+
)
|
96
|
+
raise typer.Exit(1)
|
97
|
+
|
98
|
+
built_statements = []
|
99
|
+
|
100
|
+
for statement in valid_statements:
|
101
|
+
built_statements.append(
|
102
|
+
build_statement(
|
103
|
+
statement,
|
104
|
+
contest,
|
105
|
+
output_type=output,
|
106
|
+
use_samples=samples,
|
107
|
+
custom_vars=expand_any_vars(annotations.parse_dictionary_items(vars)),
|
78
108
|
)
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
use_samples=samples,
|
86
|
-
is_editorial=editorial,
|
109
|
+
)
|
110
|
+
|
111
|
+
console.console.rule(title='Built statements')
|
112
|
+
for statement, built_path in zip(valid_statements, built_statements):
|
113
|
+
console.console.print(
|
114
|
+
f'[item]{statement.name} {statement.language}[/item] -> {href(built_path)}'
|
87
115
|
)
|
88
116
|
|
89
117
|
|
rbx/box/download.py
CHANGED
@@ -5,7 +5,7 @@ from typing import Optional
|
|
5
5
|
import typer
|
6
6
|
|
7
7
|
from rbx import annotations, console
|
8
|
-
from rbx.box import header, package
|
8
|
+
from rbx.box import header, package, remote
|
9
9
|
from rbx.box.schema import CodeItem
|
10
10
|
from rbx.config import get_builtin_checker, get_jngen, get_testlib
|
11
11
|
from rbx.grading import steps
|
@@ -72,3 +72,21 @@ def checker(name: str):
|
|
72
72
|
console.console.print(
|
73
73
|
f'[success]Downloaded [item]{name}[/item] into current package.[/success]'
|
74
74
|
)
|
75
|
+
|
76
|
+
|
77
|
+
@app.command('remote, r', help='Download a remote code.')
|
78
|
+
@package.within_problem
|
79
|
+
def remote_cmd(
|
80
|
+
name: str,
|
81
|
+
output: Optional[str] = typer.Option(
|
82
|
+
None,
|
83
|
+
'-o',
|
84
|
+
'--output',
|
85
|
+
help='Whether to not build outputs for tests and run checker.',
|
86
|
+
),
|
87
|
+
):
|
88
|
+
path = remote.expand_file(name)
|
89
|
+
|
90
|
+
if output is not None:
|
91
|
+
pathlib.Path(output).parent.mkdir(parents=True, exist_ok=True)
|
92
|
+
shutil.copy(str(path), output)
|
rbx/box/fields.py
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
from typing import TypeVar
|
2
|
+
|
3
|
+
from deepmerge import always_merger
|
4
|
+
from pydantic import BaseModel, Field
|
5
|
+
|
6
|
+
|
7
|
+
def NameField(**kwargs):
|
8
|
+
return Field(
|
9
|
+
pattern=r'^[a-zA-Z0-9][a-zA-Z0-9\-_]*$', min_length=3, max_length=32, **kwargs
|
10
|
+
)
|
11
|
+
|
12
|
+
|
13
|
+
def FNameField(**kwargs):
|
14
|
+
return Field(
|
15
|
+
pattern=r'^[a-zA-Z0-9][a-zA-Z0-9\-_]*$', min_length=3, max_length=128, **kwargs
|
16
|
+
)
|
17
|
+
|
18
|
+
|
19
|
+
T = TypeVar('T', bound=BaseModel)
|
20
|
+
|
21
|
+
|
22
|
+
def merge_pydantic_models(base: T, nxt: T) -> T:
|
23
|
+
"""Merge two Pydantic model instances.
|
24
|
+
|
25
|
+
The attributes of 'base' and 'nxt' that weren't explicitly set are dumped into dicts
|
26
|
+
using '.model_dump(exclude_unset=True)', which are then merged using 'deepmerge',
|
27
|
+
and the merged result is turned into a model instance using '.model_validate'.
|
28
|
+
|
29
|
+
For attributes set on both 'base' and 'nxt', the value from 'nxt' will be used in
|
30
|
+
the output result.
|
31
|
+
"""
|
32
|
+
base_dict = base.model_dump(exclude_unset=True)
|
33
|
+
nxt_dict = nxt.model_dump(exclude_unset=True)
|
34
|
+
merged_dict = always_merger.merge(base_dict, nxt_dict)
|
35
|
+
return base.model_validate(merged_dict)
|
rbx/box/lang.py
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
import functools
|
2
|
+
from typing import List
|
3
|
+
|
4
|
+
import iso639
|
5
|
+
|
6
|
+
from rbx import console
|
7
|
+
|
8
|
+
|
9
|
+
def code_to_langs(langs: List[str]) -> List[str]:
|
10
|
+
return [iso639.Language.from_part1(lang).name.lower() for lang in langs]
|
11
|
+
|
12
|
+
|
13
|
+
@functools.cache
|
14
|
+
def is_valid_lang_code(lang: str) -> bool:
|
15
|
+
try:
|
16
|
+
code_to_langs([lang])
|
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
|
22
|
+
|
23
|
+
return True
|
24
|
+
|
25
|
+
|
26
|
+
def langs_to_code(langs: List[str]) -> List[str]:
|
27
|
+
return [iso639.Language.from_name(lang).part1 for lang in langs]
|
rbx/box/package.py
CHANGED
@@ -459,7 +459,7 @@ def get_merged_capture_path(root: pathlib.Path = pathlib.Path()) -> pathlib.Path
|
|
459
459
|
def is_cache_valid(root: pathlib.Path = pathlib.Path()):
|
460
460
|
cache_dir = find_problem(root) / '.box'
|
461
461
|
if not cache_dir.is_dir():
|
462
|
-
return
|
462
|
+
return True
|
463
463
|
|
464
464
|
fingerprint_file = cache_dir / 'fingerprint'
|
465
465
|
if not fingerprint_file.is_file():
|
@@ -32,11 +32,11 @@ class BocaPackager(BasePackager):
|
|
32
32
|
def _get_main_statement(self) -> Statement:
|
33
33
|
pkg = package.find_problem_package_or_die()
|
34
34
|
|
35
|
-
if not pkg.
|
35
|
+
if not pkg.expanded_statements:
|
36
36
|
console.console.print('[error]No statements found.[/error]')
|
37
37
|
raise typer.Exit(1)
|
38
38
|
|
39
|
-
return pkg.
|
39
|
+
return pkg.expanded_statements[0]
|
40
40
|
|
41
41
|
def _get_main_built_statement(
|
42
42
|
self, built_statements: List[BuiltStatement]
|
@@ -119,10 +119,18 @@ class BocaPackager(BasePackager):
|
|
119
119
|
|
120
120
|
def _get_limits(self, language: BocaLanguage) -> str:
|
121
121
|
pkg = package.find_problem_package_or_die()
|
122
|
-
|
122
|
+
if pkg.type == TaskType.COMMUNICATION:
|
123
|
+
# Interactive tasks only support a single run.
|
124
|
+
no_of_runs = 1
|
125
|
+
time_limit = f'{self._get_pkg_timelimit(language) / 1000:.2f}'
|
126
|
+
else:
|
127
|
+
no_of_runs = self._get_number_of_runs(language)
|
128
|
+
time_limit = test_time(
|
129
|
+
self._get_pkg_timelimit(language) / 1000 * no_of_runs
|
130
|
+
)
|
123
131
|
return (
|
124
132
|
'#!/bin/bash\n'
|
125
|
-
f'echo {
|
133
|
+
f'echo {time_limit}\n'
|
126
134
|
f'echo {no_of_runs}\n'
|
127
135
|
f'echo {self._get_pkg_memorylimit(language)}\n'
|
128
136
|
f'echo {pkg.outputLimit}\n'
|
@@ -176,6 +184,25 @@ class BocaPackager(BasePackager):
|
|
176
184
|
'{{rbxFlags}}', extension.flags_with_defaults()['cc']
|
177
185
|
).replace('{{interactor_content}}', interactor)
|
178
186
|
|
187
|
+
def _get_safeexec(self) -> str:
|
188
|
+
safeexec_script_path = (
|
189
|
+
get_default_app_path() / 'packagers' / 'boca' / 'safeexec_compile.sh'
|
190
|
+
)
|
191
|
+
safeexec_path = get_default_app_path() / 'packagers' / 'boca' / 'safeexec.c'
|
192
|
+
if not safeexec_script_path.exists():
|
193
|
+
console.console.print(
|
194
|
+
'[error]BOCA template safeexec compile script not found.[/error]'
|
195
|
+
)
|
196
|
+
raise typer.Exit(1)
|
197
|
+
if not safeexec_path.exists():
|
198
|
+
console.console.print(
|
199
|
+
'[error]BOCA template safeexec source code not found.[/error]'
|
200
|
+
)
|
201
|
+
raise typer.Exit(1)
|
202
|
+
return safeexec_script_path.read_text().replace(
|
203
|
+
'{{safeexec_content}}', safeexec_path.read_text()
|
204
|
+
)
|
205
|
+
|
179
206
|
def _get_compile(self, language: BocaLanguage) -> str:
|
180
207
|
pkg = package.find_problem_package_or_die()
|
181
208
|
extension = get_extension_or_default('boca', BocaExtension)
|
@@ -196,6 +223,9 @@ class BocaPackager(BasePackager):
|
|
196
223
|
compile_text = compile_text.replace(
|
197
224
|
'umask 0022', 'umask 0022\n\n' + self._get_interactor()
|
198
225
|
)
|
226
|
+
compile_text = compile_text.replace(
|
227
|
+
'umask 0022', 'umask 0022\n\n' + self._get_safeexec()
|
228
|
+
)
|
199
229
|
compile_text = compile_text.replace(
|
200
230
|
'umask 0022', 'umask 0022\n\n' + self._get_checker()
|
201
231
|
)
|
@@ -206,7 +236,6 @@ class BocaPackager(BasePackager):
|
|
206
236
|
return compile_text
|
207
237
|
|
208
238
|
def _copy_solutions(self, into_path: pathlib.Path):
|
209
|
-
into_path = into_path / 'solutions'
|
210
239
|
for solution in package.get_solutions():
|
211
240
|
dest_path = (
|
212
241
|
into_path
|
@@ -218,6 +247,19 @@ class BocaPackager(BasePackager):
|
|
218
247
|
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
219
248
|
shutil.copy(str(solution.path), dest_path)
|
220
249
|
|
250
|
+
def _expand_run_script(self, run_path: pathlib.Path):
|
251
|
+
pkg = package.find_problem_package_or_die()
|
252
|
+
if pkg.type == TaskType.COMMUNICATION:
|
253
|
+
runit_content = (
|
254
|
+
get_default_app_path() / 'packagers' / 'boca' / 'interactor_run.sh'
|
255
|
+
).read_text()
|
256
|
+
run_path.write_text(
|
257
|
+
run_path.read_text().replace(
|
258
|
+
'{{runit_content}}',
|
259
|
+
runit_content,
|
260
|
+
)
|
261
|
+
)
|
262
|
+
|
221
263
|
@classmethod
|
222
264
|
def name(cls) -> str:
|
223
265
|
return 'boca'
|
@@ -263,6 +305,7 @@ class BocaPackager(BasePackager):
|
|
263
305
|
)
|
264
306
|
raise typer.Exit(1)
|
265
307
|
shutil.copyfile(run_orig_path, run_path / language)
|
308
|
+
self._expand_run_script(run_path / language)
|
266
309
|
|
267
310
|
# Prepare compile.
|
268
311
|
compile_path = into_path / 'compile'
|
@@ -86,3 +86,16 @@ async def polygon(
|
|
86
86
|
await run_contest_packager(
|
87
87
|
PolygonContestPackager, PolygonPackager, verification=verification
|
88
88
|
)
|
89
|
+
|
90
|
+
|
91
|
+
@app.command('pkg', help='Build a contest package for PKG.')
|
92
|
+
@contest_package.within_contest
|
93
|
+
@syncer.sync
|
94
|
+
async def pkg(
|
95
|
+
verification: environment.VerificationParam,
|
96
|
+
):
|
97
|
+
from rbx.box.packaging.pkg.packager import PkgContestPackager, PkgPackager
|
98
|
+
|
99
|
+
await run_contest_packager(
|
100
|
+
PkgContestPackager, PkgPackager, verification=verification
|
101
|
+
)
|
rbx/box/packaging/main.py
CHANGED
@@ -114,9 +114,9 @@ async def boca(
|
|
114
114
|
result_path = await run_packager(BocaPackager, verification=verification)
|
115
115
|
|
116
116
|
if upload:
|
117
|
-
from rbx.box.
|
117
|
+
from rbx.box.tooling.boca.scraper import get_boca_scraper
|
118
118
|
|
119
|
-
uploader =
|
119
|
+
uploader = get_boca_scraper()
|
120
120
|
uploader.login_and_upload(result_path)
|
121
121
|
|
122
122
|
|
@@ -132,3 +132,14 @@ async def moj(
|
|
132
132
|
from rbx.box.packaging.moj.packager import MojPackager
|
133
133
|
|
134
134
|
await run_packager(MojPackager, verification=verification, for_boca=for_boca)
|
135
|
+
|
136
|
+
|
137
|
+
@app.command('pkg', help='Build a package for PKG.')
|
138
|
+
@package.within_problem
|
139
|
+
@syncer.sync
|
140
|
+
async def pkg(
|
141
|
+
verification: environment.VerificationParam,
|
142
|
+
):
|
143
|
+
from rbx.box.packaging.pkg.packager import PkgPackager
|
144
|
+
|
145
|
+
await run_packager(PkgPackager, verification=verification)
|
rbx/box/packaging/packager.py
CHANGED
@@ -46,7 +46,7 @@ class BasePackager(ABC):
|
|
46
46
|
pkg = package.find_problem_package_or_die()
|
47
47
|
|
48
48
|
res = set()
|
49
|
-
for statement in pkg.
|
49
|
+
for statement in pkg.expanded_statements:
|
50
50
|
res.add(statement.language)
|
51
51
|
return list(res)
|
52
52
|
|
@@ -89,7 +89,7 @@ class BasePackager(ABC):
|
|
89
89
|
|
90
90
|
def get_statement_for_language(self, lang: str) -> Statement:
|
91
91
|
pkg = package.find_problem_package_or_die()
|
92
|
-
for statement in pkg.
|
92
|
+
for statement in pkg.expanded_statements:
|
93
93
|
if statement.language == lang:
|
94
94
|
return statement
|
95
95
|
raise
|
@@ -114,7 +114,7 @@ class BaseContestPackager(ABC):
|
|
114
114
|
pkg = contest_package.find_contest_package_or_die()
|
115
115
|
|
116
116
|
res = set()
|
117
|
-
for statement in pkg.
|
117
|
+
for statement in pkg.expanded_statements:
|
118
118
|
res.add(statement.language)
|
119
119
|
return list(res)
|
120
120
|
|
@@ -123,7 +123,7 @@ class BaseContestPackager(ABC):
|
|
123
123
|
|
124
124
|
def get_statement_for_language(self, lang: str) -> ContestStatement:
|
125
125
|
contest = contest_package.find_contest_package_or_die()
|
126
|
-
for statement in contest.
|
126
|
+
for statement in contest.expanded_statements:
|
127
127
|
if statement.language == lang:
|
128
128
|
return statement
|
129
129
|
raise
|
@@ -0,0 +1,142 @@
|
|
1
|
+
import pathlib
|
2
|
+
import shutil
|
3
|
+
from typing import List, Optional
|
4
|
+
|
5
|
+
from rbx.box import naming, package
|
6
|
+
from rbx.box.contest import contest_package
|
7
|
+
from rbx.box.contest.schema import ContestStatement
|
8
|
+
from rbx.box.packaging.packager import (
|
9
|
+
BaseContestPackager,
|
10
|
+
BasePackager,
|
11
|
+
BuiltContestStatement,
|
12
|
+
BuiltProblemPackage,
|
13
|
+
BuiltStatement,
|
14
|
+
)
|
15
|
+
from rbx.box.schema import ExpectedOutcome, TaskType
|
16
|
+
from rbx.box.statements.schema import Statement
|
17
|
+
|
18
|
+
|
19
|
+
class PkgPackager(BasePackager):
|
20
|
+
@classmethod
|
21
|
+
def task_types(cls) -> List[TaskType]:
|
22
|
+
return [TaskType.BATCH, TaskType.COMMUNICATION]
|
23
|
+
|
24
|
+
@classmethod
|
25
|
+
def name(cls) -> str:
|
26
|
+
return 'pkg'
|
27
|
+
|
28
|
+
def _get_problem_basename(self) -> str:
|
29
|
+
shortname = naming.get_problem_shortname()
|
30
|
+
if shortname is not None:
|
31
|
+
return shortname
|
32
|
+
return self.package_basename()
|
33
|
+
|
34
|
+
def _get_main_statement(self) -> Optional[Statement]:
|
35
|
+
pkg = package.find_problem_package_or_die()
|
36
|
+
if not pkg.expanded_statements:
|
37
|
+
return None
|
38
|
+
return pkg.expanded_statements[0]
|
39
|
+
|
40
|
+
def _get_main_built_statement(
|
41
|
+
self, built_statements: List[BuiltStatement]
|
42
|
+
) -> Optional[BuiltStatement]:
|
43
|
+
statement = self._get_main_statement()
|
44
|
+
if statement is None:
|
45
|
+
return None
|
46
|
+
for built_statement in built_statements:
|
47
|
+
if built_statement.statement == statement:
|
48
|
+
return built_statement
|
49
|
+
return None
|
50
|
+
|
51
|
+
def _copy_accepted_solutions(self, into_path: pathlib.Path):
|
52
|
+
for solution in package.get_solutions():
|
53
|
+
if solution.outcome != ExpectedOutcome.ACCEPTED:
|
54
|
+
continue
|
55
|
+
dest_path = into_path / solution.path.name
|
56
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
57
|
+
shutil.copy(str(solution.path), dest_path)
|
58
|
+
|
59
|
+
def package(
|
60
|
+
self,
|
61
|
+
build_path: pathlib.Path,
|
62
|
+
into_path: pathlib.Path,
|
63
|
+
built_statements: List[BuiltStatement],
|
64
|
+
) -> pathlib.Path:
|
65
|
+
into_path.mkdir(parents=True, exist_ok=True)
|
66
|
+
|
67
|
+
main_statement = self._get_main_built_statement(built_statements)
|
68
|
+
if main_statement is not None:
|
69
|
+
statement_path = into_path / 'statement.pdf'
|
70
|
+
shutil.copyfile(main_statement.path, statement_path)
|
71
|
+
|
72
|
+
# Prepare tests
|
73
|
+
tests_path = into_path / 'tests'
|
74
|
+
tests_path.mkdir(parents=True, exist_ok=True)
|
75
|
+
|
76
|
+
testcases = self.get_flattened_built_testcases()
|
77
|
+
for i, testcase in enumerate(testcases):
|
78
|
+
shutil.copyfile(testcase.inputPath, tests_path / f'{i + 1:03d}.in')
|
79
|
+
if testcase.outputPath is not None:
|
80
|
+
shutil.copyfile(testcase.outputPath, tests_path / f'{i + 1:03d}.ans')
|
81
|
+
else:
|
82
|
+
(tests_path / f'{i + 1:03d}.ans').touch()
|
83
|
+
|
84
|
+
# Copy solutions.
|
85
|
+
solutions_path = into_path / 'solutions'
|
86
|
+
solutions_path.mkdir(parents=True, exist_ok=True)
|
87
|
+
self._copy_accepted_solutions(solutions_path)
|
88
|
+
|
89
|
+
# Zip all.
|
90
|
+
shutil.make_archive(
|
91
|
+
str(build_path / self._get_problem_basename()), 'zip', into_path
|
92
|
+
)
|
93
|
+
|
94
|
+
return (build_path / self._get_problem_basename()).with_suffix('.zip')
|
95
|
+
|
96
|
+
|
97
|
+
class PkgContestPackager(BaseContestPackager):
|
98
|
+
@classmethod
|
99
|
+
def name(cls) -> str:
|
100
|
+
return 'pkg'
|
101
|
+
|
102
|
+
def _get_main_statement(self) -> Optional[ContestStatement]:
|
103
|
+
pkg = contest_package.find_contest_package_or_die()
|
104
|
+
if not pkg.expanded_statements:
|
105
|
+
return None
|
106
|
+
return pkg.expanded_statements[0]
|
107
|
+
|
108
|
+
def _get_main_built_statement(
|
109
|
+
self, built_statements: List[BuiltContestStatement]
|
110
|
+
) -> Optional[BuiltContestStatement]:
|
111
|
+
statement = self._get_main_statement()
|
112
|
+
if statement is None:
|
113
|
+
return None
|
114
|
+
for built_statement in built_statements:
|
115
|
+
if built_statement.statement == statement:
|
116
|
+
return built_statement
|
117
|
+
return None
|
118
|
+
|
119
|
+
def package(
|
120
|
+
self,
|
121
|
+
built_packages: List[BuiltProblemPackage],
|
122
|
+
build_path: pathlib.Path,
|
123
|
+
into_path: pathlib.Path,
|
124
|
+
built_statements: List[BuiltContestStatement],
|
125
|
+
) -> pathlib.Path:
|
126
|
+
into_path.mkdir(parents=True, exist_ok=True)
|
127
|
+
|
128
|
+
# Add contest-level statement.
|
129
|
+
main_statement = self._get_main_built_statement(built_statements)
|
130
|
+
if main_statement is not None:
|
131
|
+
statement_path = into_path / 'statement.pdf'
|
132
|
+
shutil.copyfile(main_statement.path, statement_path)
|
133
|
+
|
134
|
+
# Add problems.
|
135
|
+
for built_package in built_packages:
|
136
|
+
pkg_path = into_path / built_package.problem.short_name
|
137
|
+
shutil.unpack_archive(built_package.path, pkg_path, format='zip')
|
138
|
+
|
139
|
+
# Zip all.
|
140
|
+
shutil.make_archive(str(build_path / 'contest'), 'zip', into_path)
|
141
|
+
|
142
|
+
return (build_path / 'contest').with_suffix('.zip')
|
@@ -1,13 +1,12 @@
|
|
1
|
-
import functools
|
2
1
|
import pathlib
|
3
2
|
import shutil
|
4
3
|
from typing import List, Optional
|
5
4
|
|
6
|
-
import iso639
|
7
5
|
import typer
|
8
6
|
|
9
7
|
from rbx import console
|
10
8
|
from rbx.box import header, package
|
9
|
+
from rbx.box.lang import code_to_langs, is_valid_lang_code
|
11
10
|
from rbx.box.packaging.packager import (
|
12
11
|
BaseContestPackager,
|
13
12
|
BasePackager,
|
@@ -28,27 +27,6 @@ DAT_TEMPLATE = """
|
|
28
27
|
"""
|
29
28
|
|
30
29
|
|
31
|
-
def langs_to_code(langs: List[str]) -> List[str]:
|
32
|
-
return [iso639.Language.from_name(lang).part1 for lang in langs]
|
33
|
-
|
34
|
-
|
35
|
-
def code_to_langs(langs: List[str]) -> List[str]:
|
36
|
-
return [iso639.Language.from_part1(lang).name.lower() for lang in langs]
|
37
|
-
|
38
|
-
|
39
|
-
@functools.cache
|
40
|
-
def is_valid_lang_code(lang: str) -> bool:
|
41
|
-
try:
|
42
|
-
code_to_langs([lang])
|
43
|
-
except iso639.LanguageNotFoundError:
|
44
|
-
console.console.print(
|
45
|
-
f'[warning]Language [item]{lang}[/item] is being skipped because it is not a iso639 language.[/warning]'
|
46
|
-
)
|
47
|
-
return False
|
48
|
-
|
49
|
-
return True
|
50
|
-
|
51
|
-
|
52
30
|
class PolygonPackager(BasePackager):
|
53
31
|
@classmethod
|
54
32
|
def task_types(cls) -> List[TaskType]:
|
@@ -59,7 +37,7 @@ class PolygonPackager(BasePackager):
|
|
59
37
|
pkg = package.find_problem_package_or_die()
|
60
38
|
|
61
39
|
lang_codes = set()
|
62
|
-
for statement in pkg.
|
40
|
+
for statement in pkg.expanded_statements:
|
63
41
|
lang_codes.add(statement.title)
|
64
42
|
|
65
43
|
for lang in langs:
|