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
rbx/box/validators.py
ADDED
@@ -0,0 +1,245 @@
|
|
1
|
+
import pathlib
|
2
|
+
import shlex
|
3
|
+
from typing import Dict, List, Optional, Set, Tuple
|
4
|
+
|
5
|
+
from pydantic import BaseModel
|
6
|
+
|
7
|
+
from rbx import console
|
8
|
+
from rbx.box import package
|
9
|
+
from rbx.box.code import compile_item, run_item
|
10
|
+
from rbx.box.schema import CodeItem, Primitive
|
11
|
+
from rbx.box.testcases import find_built_testcase_inputs
|
12
|
+
from rbx.grading.steps import (
|
13
|
+
DigestHolder,
|
14
|
+
DigestOrDest,
|
15
|
+
DigestOrSource,
|
16
|
+
GradingFileOutput,
|
17
|
+
)
|
18
|
+
from rbx.utils import StatusProgress
|
19
|
+
|
20
|
+
HitBounds = Dict[str, Tuple[bool, bool]]
|
21
|
+
|
22
|
+
|
23
|
+
class TestcaseValidationInfo(BaseModel):
|
24
|
+
group: str
|
25
|
+
path: pathlib.Path
|
26
|
+
ok: bool
|
27
|
+
hit_bounds: HitBounds
|
28
|
+
message: Optional[str] = None
|
29
|
+
|
30
|
+
|
31
|
+
def _compile_validator(validator: CodeItem) -> str:
|
32
|
+
try:
|
33
|
+
digest = compile_item(validator)
|
34
|
+
except:
|
35
|
+
console.console.print(
|
36
|
+
f'[error]Failed compiling validator [item]{validator.path}[/item].[/error]'
|
37
|
+
)
|
38
|
+
raise
|
39
|
+
return digest
|
40
|
+
|
41
|
+
|
42
|
+
def _bounds_or(lhs: Tuple[bool, bool], rhs: Tuple[bool, bool]) -> Tuple[bool, bool]:
|
43
|
+
return (lhs[0] or rhs[0], lhs[1] or rhs[1])
|
44
|
+
|
45
|
+
|
46
|
+
def _process_bounds(log: str) -> HitBounds:
|
47
|
+
bounds: HitBounds = {}
|
48
|
+
for line in log.splitlines():
|
49
|
+
items = line.split(':')
|
50
|
+
if len(items) != 2:
|
51
|
+
continue
|
52
|
+
k, v = items
|
53
|
+
v = v.strip()
|
54
|
+
|
55
|
+
hit = ('min-value-hit' in v, 'max-value-hit' in v)
|
56
|
+
if k not in bounds:
|
57
|
+
bounds[k] = hit
|
58
|
+
continue
|
59
|
+
bounds[k] = _bounds_or(bounds[k], hit)
|
60
|
+
return bounds
|
61
|
+
|
62
|
+
|
63
|
+
def _merge_hit_bounds(hit_bounds: List[HitBounds]) -> HitBounds:
|
64
|
+
res: HitBounds = {}
|
65
|
+
for hb in hit_bounds:
|
66
|
+
for k, hit in hb.items():
|
67
|
+
if k not in res:
|
68
|
+
res[k] = hit
|
69
|
+
continue
|
70
|
+
res[k] = _bounds_or(res[k], hit)
|
71
|
+
return res
|
72
|
+
|
73
|
+
|
74
|
+
def _has_group_specific_validator() -> bool:
|
75
|
+
pkg = package.find_problem_package_or_die()
|
76
|
+
|
77
|
+
return any(group.validator is not None for group in pkg.testcases)
|
78
|
+
|
79
|
+
|
80
|
+
def _validate_testcase(
|
81
|
+
testcase: pathlib.Path,
|
82
|
+
validator: CodeItem,
|
83
|
+
validator_digest: str,
|
84
|
+
vars: Optional[Dict[str, Primitive]] = None,
|
85
|
+
) -> Tuple[bool, Optional[str], HitBounds]:
|
86
|
+
vars = vars or {}
|
87
|
+
for var in vars:
|
88
|
+
assert (
|
89
|
+
var.isidentifier()
|
90
|
+
), f'Variable {var} should be a valid Python identifier.'
|
91
|
+
# TODO: check if needs to do some escaping
|
92
|
+
var_args = [f'--{k}={v}' for k, v in vars.items()]
|
93
|
+
var_args.extend(['--testOverviewLogFileName', 'validator.log'])
|
94
|
+
|
95
|
+
message_digest = DigestHolder()
|
96
|
+
log_digest = DigestHolder()
|
97
|
+
run_log = run_item(
|
98
|
+
validator,
|
99
|
+
DigestOrSource.create(validator_digest),
|
100
|
+
stdin=DigestOrSource.create(testcase),
|
101
|
+
stderr=DigestOrDest.create(message_digest),
|
102
|
+
outputs=[
|
103
|
+
GradingFileOutput(
|
104
|
+
src=pathlib.Path('validator.log'),
|
105
|
+
digest=log_digest,
|
106
|
+
optional=True,
|
107
|
+
)
|
108
|
+
],
|
109
|
+
extra_args=shlex.join(var_args) if var_args else None,
|
110
|
+
)
|
111
|
+
log_overview = ''
|
112
|
+
if log_digest.value is not None:
|
113
|
+
log_overview = package.get_digest_as_string(log_digest.value or '')
|
114
|
+
message = package.get_digest_as_string(message_digest.value or '')
|
115
|
+
return (
|
116
|
+
run_log is not None and run_log.exitcode == 0,
|
117
|
+
message,
|
118
|
+
_process_bounds(log_overview or ''),
|
119
|
+
)
|
120
|
+
|
121
|
+
|
122
|
+
def validate_test(
|
123
|
+
testcase: pathlib.Path,
|
124
|
+
validator: CodeItem,
|
125
|
+
validator_digest: str,
|
126
|
+
) -> Tuple[bool, Optional[str], HitBounds]:
|
127
|
+
pkg = package.find_problem_package_or_die()
|
128
|
+
return _validate_testcase(
|
129
|
+
testcase, validator, validator_digest, vars=pkg.expanded_vars
|
130
|
+
)
|
131
|
+
|
132
|
+
|
133
|
+
def compile_main_validator() -> Optional[Tuple[CodeItem, str]]:
|
134
|
+
pkg = package.find_problem_package_or_die()
|
135
|
+
if pkg.validator is None:
|
136
|
+
return None
|
137
|
+
|
138
|
+
return pkg.validator, _compile_validator(pkg.validator)
|
139
|
+
|
140
|
+
|
141
|
+
def compile_validators(
|
142
|
+
progress: Optional[StatusProgress] = None,
|
143
|
+
) -> Dict[str, str]:
|
144
|
+
pkg = package.find_problem_package_or_die()
|
145
|
+
|
146
|
+
group_to_compiled_digest = {}
|
147
|
+
|
148
|
+
for group in pkg.testcases:
|
149
|
+
validator = group.validator or pkg.validator
|
150
|
+
if validator is None:
|
151
|
+
continue
|
152
|
+
if progress:
|
153
|
+
progress.update(
|
154
|
+
f'Compiling validator for group [item]{group.name}[/item]...'
|
155
|
+
)
|
156
|
+
group_to_compiled_digest[group.name] = _compile_validator(validator)
|
157
|
+
|
158
|
+
return group_to_compiled_digest
|
159
|
+
|
160
|
+
|
161
|
+
def validate_testcases(
|
162
|
+
progress: Optional[StatusProgress] = None,
|
163
|
+
groups: Optional[Set[str]] = None,
|
164
|
+
) -> List[TestcaseValidationInfo]:
|
165
|
+
def step():
|
166
|
+
if progress is not None:
|
167
|
+
progress.step()
|
168
|
+
|
169
|
+
pkg = package.find_problem_package_or_die()
|
170
|
+
|
171
|
+
group_to_compiled_digest = compile_validators(progress)
|
172
|
+
|
173
|
+
validation_info = []
|
174
|
+
|
175
|
+
for group in pkg.testcases:
|
176
|
+
validator = group.validator or pkg.validator
|
177
|
+
if validator is None:
|
178
|
+
continue
|
179
|
+
if group.name not in group_to_compiled_digest:
|
180
|
+
continue
|
181
|
+
if groups is not None and group.name not in groups:
|
182
|
+
continue
|
183
|
+
compiled_digest = group_to_compiled_digest[group.name]
|
184
|
+
|
185
|
+
testcases = find_built_testcase_inputs(group)
|
186
|
+
|
187
|
+
for testcase in testcases:
|
188
|
+
ok, message, hit_bounds = validate_test(
|
189
|
+
testcase, validator, compiled_digest
|
190
|
+
)
|
191
|
+
validation_info.append(
|
192
|
+
TestcaseValidationInfo(
|
193
|
+
group=group.name,
|
194
|
+
path=testcase,
|
195
|
+
ok=ok,
|
196
|
+
hit_bounds=hit_bounds,
|
197
|
+
message=message,
|
198
|
+
)
|
199
|
+
)
|
200
|
+
step()
|
201
|
+
|
202
|
+
return validation_info
|
203
|
+
|
204
|
+
|
205
|
+
def print_validation_report(infos: List[TestcaseValidationInfo]):
|
206
|
+
console.console.rule('Validation report', style='status')
|
207
|
+
hit_bounds_per_group: Dict[str, HitBounds] = {}
|
208
|
+
for info in infos:
|
209
|
+
if not info.ok:
|
210
|
+
console.console.print(
|
211
|
+
f'[error]Testcase [item]{info.path}[/item] failed verification:[/error]\n{info.message}'
|
212
|
+
)
|
213
|
+
continue
|
214
|
+
|
215
|
+
if info.group not in hit_bounds_per_group:
|
216
|
+
hit_bounds_per_group[info.group] = {}
|
217
|
+
hit_bounds_per_group[info.group] = _merge_hit_bounds(
|
218
|
+
[hit_bounds_per_group[info.group], info.hit_bounds]
|
219
|
+
)
|
220
|
+
|
221
|
+
if not _has_group_specific_validator():
|
222
|
+
hit_bounds_per_group = {None: _merge_hit_bounds(hit_bounds_per_group.values())}
|
223
|
+
|
224
|
+
for group, hit_bounds in hit_bounds_per_group.items():
|
225
|
+
if group == 'samples':
|
226
|
+
# Skip samples.
|
227
|
+
continue
|
228
|
+
if group is None:
|
229
|
+
console.console.print('Hit bounds:')
|
230
|
+
else:
|
231
|
+
console.console.print(f'Group [item]{group}[/item] hit bounds:')
|
232
|
+
|
233
|
+
for k, v in hit_bounds.items():
|
234
|
+
if all(v):
|
235
|
+
continue
|
236
|
+
|
237
|
+
if not v[0]:
|
238
|
+
console.console.print(
|
239
|
+
f' - {k}: [warning]min-value not hit[/warning]',
|
240
|
+
)
|
241
|
+
if not v[1]:
|
242
|
+
console.console.print(
|
243
|
+
f' - {k}: [warning]max-value not hit[/warning]',
|
244
|
+
)
|
245
|
+
console.console.print()
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import pathlib
|
2
|
+
|
3
|
+
import pytest
|
4
|
+
|
5
|
+
from rbx.box.generators import generate_testcases
|
6
|
+
from rbx.box.validators import validate_testcases
|
7
|
+
|
8
|
+
|
9
|
+
@pytest.mark.test_pkg('box1')
|
10
|
+
def test_validators(pkg_from_testdata: pathlib.Path):
|
11
|
+
generate_testcases()
|
12
|
+
validation_infos = validate_testcases()
|
13
|
+
|
14
|
+
for info in validation_infos:
|
15
|
+
assert info.ok
|
rbx/checker.py
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
import pathlib
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
import typer
|
5
|
+
from typing_extensions import Annotated
|
6
|
+
|
7
|
+
from rbx import annotations, config, metadata, utils
|
8
|
+
from rbx.config import get_builtin_checker, get_testlib
|
9
|
+
from rbx.console import console
|
10
|
+
|
11
|
+
app = typer.Typer(no_args_is_help=True)
|
12
|
+
|
13
|
+
|
14
|
+
@app.command('add, a')
|
15
|
+
def add(
|
16
|
+
problem: annotations.Problem,
|
17
|
+
template: Annotated[
|
18
|
+
Optional[str],
|
19
|
+
typer.Option(
|
20
|
+
'--template', '-t', help='Checker that should be used as template.'
|
21
|
+
),
|
22
|
+
] = None,
|
23
|
+
):
|
24
|
+
"""
|
25
|
+
Add a new checker for the problem.
|
26
|
+
"""
|
27
|
+
dumped_problem = metadata.find_problem_by_anything(problem)
|
28
|
+
if dumped_problem is None:
|
29
|
+
console.print(f'[error]Problem [item]{problem}[/item] not found.[/error]')
|
30
|
+
return
|
31
|
+
|
32
|
+
template_path = get_builtin_checker(template or 'boilerplate.cpp')
|
33
|
+
|
34
|
+
if not template_path.is_file():
|
35
|
+
console.print(f'[error]Template file {template} not found.[/error]')
|
36
|
+
return
|
37
|
+
|
38
|
+
testlib_path = get_testlib()
|
39
|
+
if not testlib_path.is_file():
|
40
|
+
console.print('[error]Testlib file not found.[/error]')
|
41
|
+
return
|
42
|
+
|
43
|
+
checker_name = f'{dumped_problem.code}.checker.cpp'
|
44
|
+
checker_path = pathlib.Path() / checker_name
|
45
|
+
|
46
|
+
# Create both files.
|
47
|
+
checker_path.write_text(template_path.read_text())
|
48
|
+
(checker_path.parent / 'testlib.h').write_text(testlib_path.read_text())
|
49
|
+
|
50
|
+
# Set checker.
|
51
|
+
problem_to_dump = dumped_problem.model_copy()
|
52
|
+
problem_to_dump.checker = checker_name
|
53
|
+
problem_path = metadata.find_problem_path_by_code(dumped_problem.code)
|
54
|
+
if not problem_path:
|
55
|
+
raise typer.Exit(1)
|
56
|
+
problem_path.write_text(utils.model_json(problem_to_dump))
|
57
|
+
console.print(
|
58
|
+
f'Checker [item]{checker_name}[/item] added to problem [item]{dumped_problem.pretty_name()}[/item].'
|
59
|
+
)
|
60
|
+
|
61
|
+
|
62
|
+
@app.command('set, s')
|
63
|
+
def set(problem: annotations.Problem, checker: annotations.Checker):
|
64
|
+
"""
|
65
|
+
Set a checker for the problem.
|
66
|
+
"""
|
67
|
+
dumped_problem = metadata.find_problem_by_anything(problem)
|
68
|
+
if dumped_problem is None:
|
69
|
+
console.print(f'[error]Problem [item]{problem}[/item] not found.[/error]')
|
70
|
+
return
|
71
|
+
|
72
|
+
problem_to_dump = dumped_problem.model_copy()
|
73
|
+
problem_to_dump.checker = checker
|
74
|
+
problem_path = metadata.find_problem_path_by_code(dumped_problem.code)
|
75
|
+
if not problem_path:
|
76
|
+
raise typer.Exit(1)
|
77
|
+
problem_path.write_text(utils.model_json(problem_to_dump))
|
78
|
+
console.print(
|
79
|
+
f'Checker [item]{checker}[/item] will be used for problem [item]{dumped_problem.pretty_name()}[/item].'
|
80
|
+
)
|
81
|
+
|
82
|
+
|
83
|
+
@app.command('unset, u')
|
84
|
+
def unset(problem: annotations.Problem):
|
85
|
+
"""
|
86
|
+
Use the default checker for a problem.
|
87
|
+
"""
|
88
|
+
dumped_problem = metadata.find_problem_by_anything(problem)
|
89
|
+
if dumped_problem is None:
|
90
|
+
console.print(f'[error]Problem [item]{problem}[/item] not found.[/error]')
|
91
|
+
return
|
92
|
+
|
93
|
+
problem_to_dump = dumped_problem.model_copy()
|
94
|
+
problem_to_dump.checker = None
|
95
|
+
problem_path = metadata.find_problem_path_by_code(dumped_problem.code)
|
96
|
+
if not problem_path:
|
97
|
+
raise typer.Exit(1)
|
98
|
+
problem_path.write_text(utils.model_json(problem_to_dump))
|
99
|
+
console.print(
|
100
|
+
f'Default checker will be used for problem [item]{dumped_problem.pretty_name()}[/item].'
|
101
|
+
)
|
102
|
+
|
103
|
+
|
104
|
+
@app.command('edit, e')
|
105
|
+
def edit(problem: annotations.Problem):
|
106
|
+
"""
|
107
|
+
Edit the checker for a problem.
|
108
|
+
"""
|
109
|
+
dumped_problem = metadata.find_problem_by_anything(problem)
|
110
|
+
if dumped_problem is None:
|
111
|
+
console.print(f'[error]Problem [item]{problem}[/item] not found.[/error]')
|
112
|
+
return
|
113
|
+
|
114
|
+
checker = dumped_problem.checker
|
115
|
+
if checker is None:
|
116
|
+
console.print(
|
117
|
+
f'[error]No checker set for problem [item]{dumped_problem.pretty_name()}[/item].[/error]'
|
118
|
+
)
|
119
|
+
return
|
120
|
+
|
121
|
+
checker_path = pathlib.Path() / checker
|
122
|
+
if not checker_path.is_file():
|
123
|
+
console.print(
|
124
|
+
f'[error]Checker [item]{checker}[/item] not found in the problems folder. You cannot edit a builtin checker.[/error]'
|
125
|
+
)
|
126
|
+
return
|
127
|
+
|
128
|
+
config.open_editor(checker_path)
|
rbx/clone.py
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
import logging
|
2
|
+
import pathlib
|
3
|
+
import threading
|
4
|
+
import time
|
5
|
+
from typing import List, Optional
|
6
|
+
|
7
|
+
import fastapi
|
8
|
+
import jinja2
|
9
|
+
import rich
|
10
|
+
import rich.prompt
|
11
|
+
import rich.status
|
12
|
+
import uvicorn
|
13
|
+
|
14
|
+
from rbx import hydration, metadata, providers, utils
|
15
|
+
from rbx.config import Language, get_config
|
16
|
+
from rbx.console import console
|
17
|
+
from rbx.schema import DumpedProblem, Problem
|
18
|
+
|
19
|
+
|
20
|
+
def clear_loggers():
|
21
|
+
for logger_name in [
|
22
|
+
'uvicorn',
|
23
|
+
'uvicorn.access',
|
24
|
+
'uvicorn.asgi',
|
25
|
+
]:
|
26
|
+
logging.getLogger(logger_name).handlers.clear()
|
27
|
+
logging.getLogger(logger_name).propagate = False
|
28
|
+
|
29
|
+
|
30
|
+
def create_problem_structure(
|
31
|
+
root: pathlib.Path,
|
32
|
+
problem: Problem,
|
33
|
+
lang: Language,
|
34
|
+
status: Optional[rich.status.Status],
|
35
|
+
should_simplify: bool = False,
|
36
|
+
verbose: bool = False,
|
37
|
+
) -> Optional[DumpedProblem]:
|
38
|
+
# Create directory structure.
|
39
|
+
root.parent.mkdir(parents=True, exist_ok=True)
|
40
|
+
|
41
|
+
problem_to_dump = DumpedProblem.from_problem(
|
42
|
+
problem,
|
43
|
+
code=providers.get_code(problem, simplify=should_simplify),
|
44
|
+
aliases=providers.get_aliases(problem),
|
45
|
+
)
|
46
|
+
|
47
|
+
if verbose:
|
48
|
+
console.print(
|
49
|
+
f'Creating problem structure for [item]{problem_to_dump.pretty_name()}[/item]...'
|
50
|
+
)
|
51
|
+
|
52
|
+
code_path = root / lang.get_file(problem_to_dump.code)
|
53
|
+
json_path = root / f'{problem_to_dump.code}.rbx.json'
|
54
|
+
|
55
|
+
existing_problem = metadata.find_problem_by_code(problem_to_dump.code, root)
|
56
|
+
if existing_problem:
|
57
|
+
console.print(
|
58
|
+
f'[error]Problem with identifier [item]{problem_to_dump.code}[/item] already exists in this folder.[/error]'
|
59
|
+
)
|
60
|
+
if not utils.confirm_on_status(
|
61
|
+
status, 'Do you want to overwrite it?', default=False
|
62
|
+
):
|
63
|
+
console.print(
|
64
|
+
f'Skipping problem [item]{problem_to_dump.pretty_name()}[/item].'
|
65
|
+
)
|
66
|
+
return None
|
67
|
+
|
68
|
+
json_path.write_text(utils.model_json(problem_to_dump))
|
69
|
+
code = jinja2.Template(lang.get_template()).render(**problem_to_dump.get_vars())
|
70
|
+
code_path.write_text(code)
|
71
|
+
|
72
|
+
if verbose:
|
73
|
+
console.print(
|
74
|
+
f'Problem structure for [item]{problem_to_dump.pretty_name()}[/item] created successfully.'
|
75
|
+
)
|
76
|
+
return problem_to_dump
|
77
|
+
|
78
|
+
|
79
|
+
def process_problems(
|
80
|
+
problems: List[Problem], lang: Language, status: rich.status.Status
|
81
|
+
):
|
82
|
+
console.print(
|
83
|
+
f'Creating problem structure for [item]{len(problems)}[/item] problems...'
|
84
|
+
)
|
85
|
+
|
86
|
+
should_simplify = False
|
87
|
+
if providers.should_simplify_contest_problems(problems):
|
88
|
+
console.print('Detected the parsed problems are from a contest.')
|
89
|
+
if utils.confirm_on_status(
|
90
|
+
status,
|
91
|
+
'Do you want to identify these problems by their letters?',
|
92
|
+
default=True,
|
93
|
+
):
|
94
|
+
should_simplify = True
|
95
|
+
|
96
|
+
root = pathlib.Path()
|
97
|
+
dumped_problems = []
|
98
|
+
for problem in problems:
|
99
|
+
dumped_problem = create_problem_structure(
|
100
|
+
root, problem, lang, status, should_simplify=should_simplify
|
101
|
+
)
|
102
|
+
if dumped_problem:
|
103
|
+
dumped_problems.append(dumped_problem)
|
104
|
+
console.print(f'Hydrating [item]{len(dumped_problems)}[/item] problems...')
|
105
|
+
for problem in dumped_problems:
|
106
|
+
hydration.hydrate_problem(root, problem)
|
107
|
+
|
108
|
+
|
109
|
+
def main(lang: Optional[str] = None):
|
110
|
+
if get_config().get_language(lang) is None:
|
111
|
+
console.print(
|
112
|
+
f'[error]Language {lang or get_config().defaultLanguage} not found in config. Please check your configuration.[/error]'
|
113
|
+
)
|
114
|
+
return
|
115
|
+
|
116
|
+
app = fastapi.FastAPI()
|
117
|
+
|
118
|
+
async def shutdown():
|
119
|
+
server.should_exit = True
|
120
|
+
|
121
|
+
batch_to_left_lock = threading.Lock()
|
122
|
+
batch_to_left = {}
|
123
|
+
ignored = set()
|
124
|
+
saved_status = None
|
125
|
+
problems_to_process = []
|
126
|
+
|
127
|
+
def process_batch_item(problem: Problem):
|
128
|
+
batch_to_left_lock.acquire()
|
129
|
+
if problem.batch.id in ignored:
|
130
|
+
batch_to_left_lock.release()
|
131
|
+
return True
|
132
|
+
if problem.batch.id not in batch_to_left:
|
133
|
+
if len(batch_to_left) > 0:
|
134
|
+
console.print(
|
135
|
+
f'[error]Ignoring extra batch [item]{problem.batch.id}[/item] since other batch is being parsed.[/error]'
|
136
|
+
)
|
137
|
+
ignored.add(problem.batch.id)
|
138
|
+
batch_to_left_lock.release()
|
139
|
+
return True
|
140
|
+
if problem.batch.size > 1 and saved_status:
|
141
|
+
saved_status.update(
|
142
|
+
f'[rbx]rbx[/rbx] is parsing problems from group [item]{problem.group}[/item]'
|
143
|
+
)
|
144
|
+
elif saved_status:
|
145
|
+
saved_status.update('[rbx]rbx[/rbx] is parsing problems...')
|
146
|
+
console.print(
|
147
|
+
f'Started parsing batch [item]{problem.batch.id}[/item] with size [item]{problem.batch.size}[/item].'
|
148
|
+
)
|
149
|
+
batch_to_left[problem.batch.id] = problem.batch.size
|
150
|
+
console.print(f'Parsing problem [item]{problem.name}[/item]...')
|
151
|
+
problems_to_process.append(problem)
|
152
|
+
finished = False
|
153
|
+
if batch_to_left[problem.batch.id] == 1:
|
154
|
+
finished = True
|
155
|
+
if problem.batch.size > 1:
|
156
|
+
console.print(
|
157
|
+
f'[status][rbx]rbx[/rbx] parsed all problems from group [item]{problem.group}[/item].[/status]'
|
158
|
+
)
|
159
|
+
else:
|
160
|
+
console.print(
|
161
|
+
f'[status][rbx]rbx[/rbx] parsed problem from [item]{problem.url}[/item].[/status]'
|
162
|
+
)
|
163
|
+
else:
|
164
|
+
batch_to_left[problem.batch.id] -= 1
|
165
|
+
batch_to_left_lock.release()
|
166
|
+
return not finished
|
167
|
+
|
168
|
+
clock = None
|
169
|
+
|
170
|
+
@app.post('/')
|
171
|
+
async def parse(problem: Problem, background_tasks: fastapi.BackgroundTasks):
|
172
|
+
nonlocal clock
|
173
|
+
if clock is None:
|
174
|
+
clock = time.monotonic()
|
175
|
+
if not process_batch_item(problem):
|
176
|
+
duration = time.monotonic() - clock
|
177
|
+
console.print(
|
178
|
+
f'Parsed all problems in [item]{duration:.2f}[/item] seconds.'
|
179
|
+
)
|
180
|
+
background_tasks.add_task(shutdown)
|
181
|
+
return {}
|
182
|
+
|
183
|
+
config = uvicorn.Config(app, port=1327)
|
184
|
+
server = uvicorn.Server(config=config)
|
185
|
+
clear_loggers()
|
186
|
+
with console.status('Waiting for Competitive Companion request...') as status:
|
187
|
+
saved_status = status
|
188
|
+
server.run()
|
189
|
+
|
190
|
+
with console.status('Processing parsed problems...') as status:
|
191
|
+
language = get_config().get_language(lang)
|
192
|
+
if not language:
|
193
|
+
console.print(
|
194
|
+
f'[error]Language {lang or get_config().defaultLanguage} not found in config. Please check your configuration.[/error]'
|
195
|
+
)
|
196
|
+
return
|
197
|
+
process_problems(problems_to_process, language, status)
|