rbx.cp 0.5.73__py3-none-any.whl → 0.6.1__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/cd.py +11 -1
- rbx/box/checkers.py +9 -1
- rbx/box/cli.py +59 -46
- rbx/box/code.py +142 -3
- rbx/box/contest/build_contest_statements.py +44 -34
- rbx/box/contest/contest_package.py +4 -7
- rbx/box/contest/main.py +7 -58
- rbx/box/contest/schema.py +52 -8
- rbx/box/contest/statements.py +53 -25
- rbx/box/creation.py +3 -36
- rbx/box/environment.py +21 -9
- rbx/box/fields.py +35 -0
- rbx/box/lang.py +27 -0
- rbx/box/linting.py +26 -0
- rbx/box/package.py +4 -35
- 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/presets/__init__.py +362 -281
- rbx/box/presets/lock_schema.py +1 -2
- rbx/box/presets/schema.py +13 -5
- rbx/box/remote.py +2 -2
- rbx/box/retries.py +8 -0
- rbx/box/schema.py +82 -19
- rbx/box/solutions.py +77 -15
- 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/stats.py +92 -0
- rbx/box/tasks.py +6 -3
- 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/utils/run_ui.py +1 -1
- rbx/box/ui/widgets/interaction_box.py +19 -1
- rbx/grading/caching.py +18 -2
- rbx/grading/judge/sandbox.py +60 -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 +92 -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 -83
- rbx/resources/packagers/boca/interactive/cc +15 -83
- rbx/resources/packagers/boca/interactive/cpp +15 -83
- rbx/resources/packagers/boca/interactive/java +15 -88
- rbx/resources/packagers/boca/interactive/kt +15 -88
- rbx/resources/packagers/boca/interactive/py2 +15 -88
- rbx/resources/packagers/boca/interactive/py3 +15 -88
- 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 +38 -26
- rbx/resources/presets/default/problem/random.txt +3 -1
- rbx/resources/presets/default/problem/rbx.h +92 -0
- rbx/resources/presets/default/problem/statement/statement.rbx.tex +4 -7
- rbx/resources/presets/default/problem/validator.cpp +8 -8
- rbx/resources/templates/rbx.h +2 -3
- {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.1.dist-info}/METADATA +23 -6
- {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.1.dist-info}/RECORD +84 -71
- {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.1.dist-info}/WHEEL +1 -1
- rbx/resources/packagers/boca/compile/pas +0 -172
- rbx/resources/presets/default/problem/statement/projecao.png +0 -0
- {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.1.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.1.dist-info}/entry_points.txt +0 -0
rbx/box/statements/builders.py
CHANGED
@@ -25,7 +25,11 @@ from rbx.box.statements.schema import (
|
|
25
25
|
TexToPDF,
|
26
26
|
rbxToTeX,
|
27
27
|
)
|
28
|
-
from rbx.box.testcase_utils import
|
28
|
+
from rbx.box.testcase_utils import (
|
29
|
+
TestcaseInteraction,
|
30
|
+
TestcaseInteractionParsingError,
|
31
|
+
parse_interaction,
|
32
|
+
)
|
29
33
|
|
30
34
|
|
31
35
|
@dataclasses.dataclass
|
@@ -37,20 +41,22 @@ class StatementCodeLanguage:
|
|
37
41
|
|
38
42
|
@dataclasses.dataclass
|
39
43
|
class StatementBuilderContext:
|
44
|
+
lang: str
|
40
45
|
languages: List[StatementCodeLanguage]
|
41
46
|
params: ConversionStep
|
42
47
|
root: pathlib.Path
|
43
|
-
|
48
|
+
custom_vars: Optional[Dict[str, Any]] = None
|
44
49
|
vars: Optional[Dict[str, Primitive]] = None
|
45
50
|
|
46
51
|
def build_jinja_kwargs(self) -> Dict[str, Any]:
|
47
52
|
res = {
|
53
|
+
'lang': self.lang,
|
48
54
|
'languages': self.languages,
|
49
55
|
'keyed_languages': {lang.id: lang for lang in self.languages},
|
50
|
-
'is_editorial': self.editorial,
|
51
56
|
}
|
52
|
-
if self.vars is not None:
|
53
|
-
res['vars'] = self.vars
|
57
|
+
if self.vars is not None or self.custom_vars is not None:
|
58
|
+
res['vars'] = self.vars or {}
|
59
|
+
res['vars'].update(self.custom_vars or {})
|
54
60
|
return res
|
55
61
|
|
56
62
|
|
@@ -82,7 +88,13 @@ class StatementSample(BaseModel):
|
|
82
88
|
|
83
89
|
interaction = None
|
84
90
|
if pio_path.is_file():
|
85
|
-
|
91
|
+
try:
|
92
|
+
interaction = parse_interaction(pio_path)
|
93
|
+
except TestcaseInteractionParsingError as e:
|
94
|
+
console.console.print(
|
95
|
+
f'Error parsing interactive sample: [error]{e}[/error]'
|
96
|
+
)
|
97
|
+
raise typer.Exit(1) from e
|
86
98
|
|
87
99
|
return StatementSample(
|
88
100
|
inputPath=input_path,
|
@@ -325,10 +337,6 @@ class rbxTeXBuilder(StatementBuilder):
|
|
325
337
|
)
|
326
338
|
blocks = statement_blocks.blocks
|
327
339
|
|
328
|
-
# Remove editorial block when not editorial.
|
329
|
-
if not context.editorial and 'editorial' in blocks:
|
330
|
-
del blocks['editorial']
|
331
|
-
|
332
340
|
problem_kwargs = problem.build_jinja_kwargs()
|
333
341
|
problem_kwargs['problem']['blocks'] = blocks
|
334
342
|
if statement_blocks.explanations is not None:
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import collections
|
2
|
+
from typing import Any, List, TypeVar
|
3
|
+
|
4
|
+
from rbx.box.fields import merge_pydantic_models
|
5
|
+
|
6
|
+
TypeVarT = TypeVar('TypeVarT', bound=Any)
|
7
|
+
|
8
|
+
|
9
|
+
def expand_statements(statements: List[TypeVarT]) -> List[TypeVarT]:
|
10
|
+
deg = collections.defaultdict(int)
|
11
|
+
dependencies = collections.defaultdict(list)
|
12
|
+
for statement in statements:
|
13
|
+
if statement.extends is not None:
|
14
|
+
deg[statement.name] += 1
|
15
|
+
dependencies[statement.extends].append(statement.name)
|
16
|
+
|
17
|
+
# Topological sort.
|
18
|
+
# - We need to expand statements in the order of dependencies.
|
19
|
+
# - This is a simple topological sort.
|
20
|
+
# - If there are multiple statements with indegree 0, we choose the first one.
|
21
|
+
st_per_name = {}
|
22
|
+
expanded = {}
|
23
|
+
st = []
|
24
|
+
for statement in statements:
|
25
|
+
st_per_name[statement.name] = statement
|
26
|
+
if deg[statement.name] == 0:
|
27
|
+
st.append(statement)
|
28
|
+
|
29
|
+
while st:
|
30
|
+
statement = st.pop()
|
31
|
+
expanded_statement = statement.model_copy()
|
32
|
+
if statement.extends is not None:
|
33
|
+
expanded_statement = merge_pydantic_models(
|
34
|
+
expanded[statement.extends], statement
|
35
|
+
)
|
36
|
+
|
37
|
+
expanded[statement.name] = expanded_statement
|
38
|
+
|
39
|
+
for dep_name in dependencies[statement.name]:
|
40
|
+
deg[dep_name] -= 1
|
41
|
+
if deg[dep_name] == 0:
|
42
|
+
st.append(st_per_name[dep_name])
|
43
|
+
|
44
|
+
if len(expanded) != len(statements):
|
45
|
+
raise ValueError(
|
46
|
+
f'Failed to expand statements: only {len(expanded)} out of {len(statements)} were expanded. This means there is a cycle introduced by the `extends` field.'
|
47
|
+
)
|
48
|
+
|
49
|
+
return [expanded[statement.name] for statement in statements]
|
@@ -6,9 +6,10 @@ with Latex.
|
|
6
6
|
import pathlib
|
7
7
|
import re
|
8
8
|
import typing
|
9
|
-
from typing import Dict, Tuple, Union
|
9
|
+
from typing import Any, Dict, Tuple, Union
|
10
10
|
|
11
11
|
import jinja2
|
12
|
+
import jinja2.runtime
|
12
13
|
import typer
|
13
14
|
|
14
15
|
from rbx import console
|
@@ -131,11 +132,56 @@ def path_stem(path: pathlib.Path) -> str:
|
|
131
132
|
return path.stem
|
132
133
|
|
133
134
|
|
135
|
+
@jinja2.pass_context
|
136
|
+
def test_var_truthy(ctx: jinja2.runtime.Context, value: Any):
|
137
|
+
if isinstance(value, jinja2.Undefined):
|
138
|
+
return False
|
139
|
+
if value is None:
|
140
|
+
return False
|
141
|
+
return bool(value)
|
142
|
+
|
143
|
+
|
144
|
+
@jinja2.pass_context
|
145
|
+
def test_var_falsy(ctx: jinja2.runtime.Context, value: Any):
|
146
|
+
return not test_var_truthy(ctx, value)
|
147
|
+
|
148
|
+
|
149
|
+
@jinja2.pass_context
|
150
|
+
def test_var_null(ctx: jinja2.runtime.Context, value: Any):
|
151
|
+
if isinstance(value, jinja2.Undefined):
|
152
|
+
return True
|
153
|
+
if value is None:
|
154
|
+
return True
|
155
|
+
return False
|
156
|
+
|
157
|
+
|
158
|
+
@jinja2.pass_context
|
159
|
+
def test_var_nonnull(ctx: jinja2.runtime.Context, value: Any):
|
160
|
+
return not test_var_null(ctx, value)
|
161
|
+
|
162
|
+
|
134
163
|
######################################################################
|
135
164
|
# Declare module functions
|
136
165
|
######################################################################
|
137
166
|
|
138
167
|
|
168
|
+
class StrictChainableUndefined(jinja2.StrictUndefined):
|
169
|
+
def __getattr__(self, name: str) -> 'StrictChainableUndefined':
|
170
|
+
# Raise AttributeError on requests for names that appear to be unimplemented
|
171
|
+
# dunder methods to avoid confusing Python with truthy non-method objects that
|
172
|
+
# do not implement the protocol being probed for. e.g., copy.copy(Undefined())
|
173
|
+
# fails spectacularly if getattr(Undefined(), '__setstate__') returns an
|
174
|
+
# Undefined object instead of raising AttributeError to signal that it does not
|
175
|
+
# support that style of object initialization.
|
176
|
+
if name[:2] == '__' and name[-2:] == '__':
|
177
|
+
raise AttributeError(name)
|
178
|
+
|
179
|
+
return self
|
180
|
+
|
181
|
+
def __getitem__(self, _name: str) -> 'StrictChainableUndefined': # type: ignore[override]
|
182
|
+
return self
|
183
|
+
|
184
|
+
|
139
185
|
class JinjaDictWrapper(dict):
|
140
186
|
def __init__(self, *args, key='dict object', **kwargs):
|
141
187
|
super().__init__(*args, **kwargs)
|
@@ -145,7 +191,9 @@ class JinjaDictWrapper(dict):
|
|
145
191
|
try:
|
146
192
|
return super().__getitem__(key)
|
147
193
|
except KeyError:
|
148
|
-
return
|
194
|
+
return StrictChainableUndefined(
|
195
|
+
hint=f'"{key}" was not found in "{self.key}"'
|
196
|
+
)
|
149
197
|
|
150
198
|
|
151
199
|
def add_builtin_filters(j2_env: jinja2.Environment):
|
@@ -155,6 +203,13 @@ def add_builtin_filters(j2_env: jinja2.Environment):
|
|
155
203
|
j2_env.filters['stem'] = path_stem
|
156
204
|
|
157
205
|
|
206
|
+
def add_builtin_tests(j2_env: jinja2.Environment):
|
207
|
+
j2_env.tests['truthy'] = test_var_truthy
|
208
|
+
j2_env.tests['falsy'] = test_var_falsy
|
209
|
+
j2_env.tests['null'] = test_var_null
|
210
|
+
j2_env.tests['nonnull'] = test_var_nonnull
|
211
|
+
|
212
|
+
|
158
213
|
def render_latex_template(path_templates, template_filename, template_vars=None) -> str:
|
159
214
|
"""Render a latex template, filling in its template variables
|
160
215
|
|
@@ -168,9 +223,10 @@ def render_latex_template(path_templates, template_filename, template_vars=None)
|
|
168
223
|
j2_env = jinja2.Environment(
|
169
224
|
loader=jinja2.FileSystemLoader(path_templates),
|
170
225
|
**J2_ARGS,
|
171
|
-
undefined=
|
226
|
+
undefined=StrictChainableUndefined,
|
172
227
|
)
|
173
228
|
add_builtin_filters(j2_env)
|
229
|
+
add_builtin_tests(j2_env)
|
174
230
|
template = j2_env.get_template(template_filename)
|
175
231
|
try:
|
176
232
|
return template.render(**var_dict) # type: ignore
|
@@ -198,9 +254,10 @@ def render_latex_template_blocks(
|
|
198
254
|
j2_env = jinja2.Environment(
|
199
255
|
loader=jinja2.FileSystemLoader(path_templates),
|
200
256
|
**J2_ARGS,
|
201
|
-
undefined=
|
257
|
+
undefined=StrictChainableUndefined,
|
202
258
|
)
|
203
259
|
add_builtin_filters(j2_env)
|
260
|
+
add_builtin_tests(j2_env)
|
204
261
|
template = j2_env.get_template(template_filename)
|
205
262
|
ctx = template.new_context(var_dict) # type: ignore
|
206
263
|
try:
|
rbx/box/statements/schema.py
CHANGED
@@ -2,11 +2,24 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import pathlib
|
4
4
|
from enum import Enum
|
5
|
-
from typing import List, Literal, Union
|
5
|
+
from typing import Annotated, List, Literal, Optional, Union
|
6
6
|
|
7
|
-
from pydantic import BaseModel, ConfigDict, Field
|
7
|
+
from pydantic import AfterValidator, BaseModel, ConfigDict, Field
|
8
8
|
|
9
9
|
from rbx.autoenum import AutoEnum, alias
|
10
|
+
from rbx.box.fields import FNameField
|
11
|
+
from rbx.box.lang import is_valid_lang_code
|
12
|
+
|
13
|
+
|
14
|
+
def validate_statement_language(lang: str):
|
15
|
+
if not is_valid_lang_code(lang) or not lang.islower():
|
16
|
+
raise ValueError(
|
17
|
+
f'Invalid statement language: {lang}. Language must be a valid lowercase ISO 639-1 code.'
|
18
|
+
)
|
19
|
+
return lang
|
20
|
+
|
21
|
+
|
22
|
+
StatementLanguage = Annotated[str, AfterValidator(validate_statement_language)]
|
10
23
|
|
11
24
|
|
12
25
|
### Conversion types
|
@@ -95,13 +108,28 @@ class StatementType(AutoEnum):
|
|
95
108
|
class Statement(BaseModel):
|
96
109
|
model_config = ConfigDict(extra='forbid')
|
97
110
|
|
111
|
+
name: str = FNameField(description='Name of this statement.')
|
112
|
+
|
113
|
+
extends: Optional[str] = FNameField(
|
114
|
+
default=None,
|
115
|
+
description='Name of the statement that this statement extends.',
|
116
|
+
)
|
117
|
+
|
118
|
+
language: StatementLanguage = Field(
|
119
|
+
default='en', description='Language code of this statement (ISO 639-1).'
|
120
|
+
)
|
121
|
+
|
98
122
|
title: str = Field(
|
99
|
-
description='Name of the problem, as it appears in the statement.'
|
123
|
+
default='', description='Name of the problem, as it appears in the statement.'
|
100
124
|
)
|
101
125
|
|
102
|
-
path: pathlib.Path = Field(
|
126
|
+
path: pathlib.Path = Field(
|
127
|
+
default_factory=pathlib.Path, description='Path to the input statement file.'
|
128
|
+
)
|
103
129
|
|
104
|
-
type: StatementType = Field(
|
130
|
+
type: StatementType = Field(
|
131
|
+
default=StatementType.rbxTeX, description='Type of the input statement file.'
|
132
|
+
)
|
105
133
|
|
106
134
|
steps: List[ConversionStep] = Field(
|
107
135
|
default=[],
|
@@ -134,7 +162,3 @@ the statement. Files will be included in the same folder as the statement file,
|
|
134
162
|
their relativeness. Can be glob pattern as well, such as `imgs/*.png`.
|
135
163
|
""",
|
136
164
|
)
|
137
|
-
|
138
|
-
language: str = Field(
|
139
|
-
default='en', description='Language this is statement is written in.'
|
140
|
-
)
|
rbx/box/stats.py
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
import pathlib
|
2
|
+
from typing import Iterable, List, Tuple
|
3
|
+
|
4
|
+
from rbx import console
|
5
|
+
from rbx.box.cd import (
|
6
|
+
find_all_ancestor_packages,
|
7
|
+
is_contest_package,
|
8
|
+
is_problem_package,
|
9
|
+
)
|
10
|
+
from rbx.box.contest.contest_package import find_contest, find_contest_package_or_die
|
11
|
+
from rbx.box.formatting import get_formatted_memory
|
12
|
+
|
13
|
+
|
14
|
+
def find_problem_packages_from_contest(
|
15
|
+
root: pathlib.Path = pathlib.Path(),
|
16
|
+
) -> Iterable[pathlib.Path]:
|
17
|
+
contest_path = find_contest(root)
|
18
|
+
contest = find_contest_package_or_die(contest_path)
|
19
|
+
for problem in contest.problems:
|
20
|
+
yield contest_path / problem.get_path()
|
21
|
+
|
22
|
+
|
23
|
+
def find_all_reachable_packages(
|
24
|
+
root: pathlib.Path = pathlib.Path(),
|
25
|
+
) -> List[pathlib.Path]:
|
26
|
+
packages = find_all_ancestor_packages(root)
|
27
|
+
|
28
|
+
for package in list(packages):
|
29
|
+
if is_contest_package(package):
|
30
|
+
packages.extend(find_problem_packages_from_contest(package))
|
31
|
+
return packages
|
32
|
+
|
33
|
+
|
34
|
+
def find_and_group_all_reachable_packages(
|
35
|
+
root: pathlib.Path = pathlib.Path(),
|
36
|
+
) -> Tuple[List[pathlib.Path], List[pathlib.Path]]:
|
37
|
+
packages = find_all_reachable_packages(root)
|
38
|
+
contest_packages = set(pkg for pkg in packages if is_contest_package(pkg))
|
39
|
+
problem_packages = set(pkg for pkg in packages if is_problem_package(pkg))
|
40
|
+
return sorted(contest_packages), sorted(problem_packages)
|
41
|
+
|
42
|
+
|
43
|
+
def get_dir_size(path: pathlib.Path) -> int:
|
44
|
+
if not path.is_dir():
|
45
|
+
return 0
|
46
|
+
return sum(
|
47
|
+
f.stat().st_size
|
48
|
+
for f in path.glob('**/*')
|
49
|
+
if f.is_file() and not f.is_symlink()
|
50
|
+
)
|
51
|
+
|
52
|
+
|
53
|
+
def get_cache_size(root: pathlib.Path = pathlib.Path()) -> int:
|
54
|
+
cache_dir = root / '.box'
|
55
|
+
return get_dir_size(cache_dir)
|
56
|
+
|
57
|
+
|
58
|
+
def get_build_size(root: pathlib.Path = pathlib.Path()) -> int:
|
59
|
+
build_dir = root / 'build'
|
60
|
+
return get_dir_size(build_dir)
|
61
|
+
|
62
|
+
|
63
|
+
def print_package_stats(root: pathlib.Path = pathlib.Path()) -> int:
|
64
|
+
if is_contest_package(root):
|
65
|
+
console.console.print(f'[status]Contest package[/status]: [item]{root}[/item]')
|
66
|
+
else:
|
67
|
+
console.console.print(f'[status]Problem package[/status]: [item]{root}[/item]')
|
68
|
+
|
69
|
+
cache_size = get_cache_size(root)
|
70
|
+
build_size = get_build_size(root)
|
71
|
+
console.console.print(
|
72
|
+
f'[status]Cache size[/status]: [item]{get_formatted_memory(cache_size)}[/item]'
|
73
|
+
)
|
74
|
+
console.console.print(
|
75
|
+
f'[status]Build size[/status]: [item]{get_formatted_memory(build_size)}[/item]'
|
76
|
+
)
|
77
|
+
|
78
|
+
return cache_size + build_size
|
79
|
+
|
80
|
+
|
81
|
+
def print_reachable_package_stats(root: pathlib.Path = pathlib.Path()) -> None:
|
82
|
+
contest_packages, problem_packages = find_and_group_all_reachable_packages(root)
|
83
|
+
total_size = 0
|
84
|
+
for pkg in contest_packages:
|
85
|
+
total_size += print_package_stats(pkg)
|
86
|
+
console.console.print()
|
87
|
+
for pkg in problem_packages:
|
88
|
+
total_size += print_package_stats(pkg)
|
89
|
+
console.console.print()
|
90
|
+
console.console.print(
|
91
|
+
f'[status]Total size[/status]: [item]{get_formatted_memory(total_size)}[/item]'
|
92
|
+
)
|
rbx/box/tasks.py
CHANGED
@@ -4,7 +4,7 @@ from typing import Optional
|
|
4
4
|
from rbx.box import checkers, package, state
|
5
5
|
from rbx.box.code import CommunicationItem, run_communication, run_item
|
6
6
|
from rbx.box.environment import EnvironmentSandbox, ExecutionConfig, VerificationLevel
|
7
|
-
from rbx.box.retries import Retrier
|
7
|
+
from rbx.box.retries import Retrier, get_retrier_config
|
8
8
|
from rbx.box.schema import Solution, Testcase
|
9
9
|
from rbx.grading.judge.sandbox import SandboxBase
|
10
10
|
from rbx.grading.limits import Limits
|
@@ -51,6 +51,7 @@ async def run_solution_on_testcase(
|
|
51
51
|
use_retries: bool = True,
|
52
52
|
use_timelimit: bool = True,
|
53
53
|
capture_pipes: bool = False,
|
54
|
+
nruns: int = 0,
|
54
55
|
) -> Evaluation:
|
55
56
|
if interactor_digest is not None:
|
56
57
|
return await _run_communication_solution_on_testcase(
|
@@ -66,6 +67,7 @@ async def run_solution_on_testcase(
|
|
66
67
|
use_retries=use_retries,
|
67
68
|
use_timelimit=use_timelimit,
|
68
69
|
capture_pipes=capture_pipes,
|
70
|
+
nruns=nruns,
|
69
71
|
)
|
70
72
|
|
71
73
|
async def run_fn(retry_index: int) -> Evaluation:
|
@@ -132,7 +134,7 @@ async def run_solution_on_testcase(
|
|
132
134
|
if not use_retries:
|
133
135
|
return await run_fn(0)
|
134
136
|
|
135
|
-
retrier = Retrier()
|
137
|
+
retrier = Retrier(get_retrier_config(nruns))
|
136
138
|
return await retrier.repeat(run_fn)
|
137
139
|
|
138
140
|
|
@@ -166,6 +168,7 @@ async def _run_communication_solution_on_testcase(
|
|
166
168
|
use_retries: bool = True,
|
167
169
|
use_timelimit: bool = True,
|
168
170
|
capture_pipes: bool = False,
|
171
|
+
nruns: int = 0,
|
169
172
|
) -> Evaluation:
|
170
173
|
capture_pipes = capture_pipes or state.STATE.debug_logs
|
171
174
|
|
@@ -291,5 +294,5 @@ async def _run_communication_solution_on_testcase(
|
|
291
294
|
if not use_retries:
|
292
295
|
return await run_fn(0)
|
293
296
|
|
294
|
-
retrier = Retrier()
|
297
|
+
retrier = Retrier(get_retrier_config(nruns))
|
295
298
|
return await retrier.repeat(run_fn)
|
rbx/box/testcase_utils.py
CHANGED
@@ -160,6 +160,10 @@ def fill_output_for_defined_testcase(testcase: Testcase) -> Testcase:
|
|
160
160
|
return res
|
161
161
|
|
162
162
|
|
163
|
+
class TestcaseInteractionParsingError(Exception):
|
164
|
+
pass
|
165
|
+
|
166
|
+
|
163
167
|
def parse_interaction(file: pathlib.Path) -> TestcaseInteraction:
|
164
168
|
entries = []
|
165
169
|
with file.open('r') as f:
|
@@ -167,53 +171,21 @@ def parse_interaction(file: pathlib.Path) -> TestcaseInteraction:
|
|
167
171
|
interactor_prefix = f.readline().strip()
|
168
172
|
solution_prefix = f.readline().strip()
|
169
173
|
except Exception:
|
170
|
-
|
171
|
-
f'
|
172
|
-
)
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
return (solution_idx, solution_idx + len(solution_prefix))
|
186
|
-
if solution_idx == -1:
|
187
|
-
return (interactor_idx, interactor_idx + len(interactor_prefix))
|
188
|
-
if interactor_idx < solution_idx:
|
189
|
-
return (interactor_idx, interactor_idx + len(interactor_prefix))
|
190
|
-
return (solution_idx, solution_idx + len(solution_prefix))
|
191
|
-
|
192
|
-
def _find_next_block() -> Optional[Tuple[int, Tuple[int, int]]]:
|
193
|
-
prefix = _find_next_prefix(start)
|
194
|
-
if prefix is None:
|
195
|
-
return None
|
196
|
-
prefix_start, prefix_end = prefix
|
197
|
-
prefix = rest[prefix_start:prefix_end]
|
198
|
-
pipe = 1 if prefix == solution_prefix else 0
|
199
|
-
|
200
|
-
nxt = _find_next_prefix(prefix_end)
|
201
|
-
if nxt is None:
|
202
|
-
return (pipe, (prefix_end, len(rest)))
|
203
|
-
nxt_start, _ = nxt
|
204
|
-
return (pipe, (prefix_end, nxt_start))
|
205
|
-
|
206
|
-
# TODO: optimize
|
207
|
-
blocks = 0
|
208
|
-
MAX_BLOCKS = 1024
|
209
|
-
while blocks < MAX_BLOCKS:
|
210
|
-
block = _find_next_block()
|
211
|
-
if block is None:
|
212
|
-
break
|
213
|
-
pipe, (st, nd) = block
|
214
|
-
entries.append(TestcaseInteractionEntry(data=rest[st:nd], pipe=pipe))
|
215
|
-
start = nd
|
216
|
-
blocks += 1
|
174
|
+
raise TestcaseInteractionParsingError(
|
175
|
+
f'Failed to read interaction file {file}. Expected the first two lines to be the interactor and solution prefixes.'
|
176
|
+
) from None
|
177
|
+
|
178
|
+
while line := f.readline().strip():
|
179
|
+
if line.startswith(interactor_prefix):
|
180
|
+
stripped = line[len(interactor_prefix) :].strip()
|
181
|
+
entries.append(TestcaseInteractionEntry(data=stripped, pipe=0))
|
182
|
+
elif line.startswith(solution_prefix):
|
183
|
+
stripped = line[len(solution_prefix) :].strip()
|
184
|
+
entries.append(TestcaseInteractionEntry(data=stripped, pipe=1))
|
185
|
+
else:
|
186
|
+
raise TestcaseInteractionParsingError(
|
187
|
+
f'Invalid line in interaction file {file}. Expected the line to start with the interactor or solution prefix ({interactor_prefix} or {solution_prefix}).'
|
188
|
+
) from None
|
217
189
|
|
218
190
|
return TestcaseInteraction(
|
219
191
|
prefixes=(interactor_prefix, solution_prefix),
|
File without changes
|
File without changes
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import pathlib
|
2
|
+
|
3
|
+
import typer
|
4
|
+
|
5
|
+
from rbx import annotations
|
6
|
+
from rbx.box.tooling.boca.scrape import scrape_boca
|
7
|
+
|
8
|
+
app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
|
9
|
+
|
10
|
+
|
11
|
+
@app.command('scrape', help='Scrape runs from BOCA.')
|
12
|
+
def scrape() -> None:
|
13
|
+
scrape_boca(pathlib.Path())
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import pathlib
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
3
|
+
|
4
|
+
from rich.progress import MofNCompleteColumn, Progress, SpinnerColumn
|
5
|
+
|
6
|
+
from rbx.box.tooling.boca.scraper import BocaRun, BocaScraper
|
7
|
+
|
8
|
+
|
9
|
+
def scrape_boca(into_path: pathlib.Path):
|
10
|
+
scraper = BocaScraper()
|
11
|
+
scraper.login()
|
12
|
+
runs = scraper.list_runs()
|
13
|
+
|
14
|
+
progress = Progress(
|
15
|
+
SpinnerColumn(),
|
16
|
+
*Progress.get_default_columns(),
|
17
|
+
MofNCompleteColumn(),
|
18
|
+
transient=True,
|
19
|
+
)
|
20
|
+
scrape_task = progress.add_task('Scraping runs...', total=len(runs))
|
21
|
+
with progress:
|
22
|
+
|
23
|
+
def work(run: BocaRun):
|
24
|
+
scraper.download_run(
|
25
|
+
run.run_number,
|
26
|
+
run.site_number,
|
27
|
+
pathlib.Path(into_path) / run.problem_shortname,
|
28
|
+
name=f'{run.run_number}-{run.site_number}-{run.outcome.short_name().lower()}',
|
29
|
+
)
|
30
|
+
|
31
|
+
progress.update(scrape_task, advance=1)
|
32
|
+
|
33
|
+
with ThreadPoolExecutor(max_workers=10) as executor:
|
34
|
+
executor.map(work, runs)
|