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
@@ -9,7 +9,7 @@ from pydantic import ValidationError
|
|
9
9
|
from rbx import console, utils
|
10
10
|
from rbx.box import cd
|
11
11
|
from rbx.box.contest.schema import Contest
|
12
|
-
from rbx.box.package import find_problem_package_or_die
|
12
|
+
from rbx.box.package import find_problem_package_or_die
|
13
13
|
from rbx.box.schema import Package
|
14
14
|
|
15
15
|
YAML_NAME = 'contest.rbx.yml'
|
@@ -24,7 +24,6 @@ def find_contest_yaml(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.P
|
|
24
24
|
contest_yaml_path = root / YAML_NAME
|
25
25
|
if not contest_yaml_path.is_file():
|
26
26
|
return None
|
27
|
-
warn_preset_deactivated(root)
|
28
27
|
return contest_yaml_path
|
29
28
|
|
30
29
|
|
@@ -84,12 +83,10 @@ def get_problems(contest: Contest) -> List[Package]:
|
|
84
83
|
return problems
|
85
84
|
|
86
85
|
|
87
|
-
def get_ruyaml() -> Tuple[ruyaml.YAML, ruyaml.Any]:
|
88
|
-
contest_yaml_path = find_contest_yaml()
|
86
|
+
def get_ruyaml(root: pathlib.Path = pathlib.Path()) -> Tuple[ruyaml.YAML, ruyaml.Any]:
|
87
|
+
contest_yaml_path = find_contest_yaml(root)
|
89
88
|
if contest_yaml_path is None:
|
90
|
-
console.console.print(
|
91
|
-
f'Contest not found in {pathlib.Path().absolute()}', style='error'
|
92
|
-
)
|
89
|
+
console.console.print(f'[error]Contest not found in {root.absolute()}[/error]')
|
93
90
|
raise typer.Exit(1)
|
94
91
|
res = ruyaml.YAML()
|
95
92
|
return res, res.load(contest_yaml_path.read_text())
|
rbx/box/contest/main.py
CHANGED
@@ -17,7 +17,6 @@ from rbx.box.contest.contest_package import (
|
|
17
17
|
)
|
18
18
|
from rbx.box.contest.schema import ContestProblem
|
19
19
|
from rbx.box.packaging import contest_main as packaging
|
20
|
-
from rbx.box.presets.fetch import get_preset_fetch_info
|
21
20
|
from rbx.box.schema import Package
|
22
21
|
from rbx.config import open_editor
|
23
22
|
|
@@ -40,53 +39,18 @@ app.add_typer(
|
|
40
39
|
def create(
|
41
40
|
path: str,
|
42
41
|
preset: Annotated[
|
43
|
-
str,
|
42
|
+
Optional[str],
|
44
43
|
typer.Option(
|
45
44
|
'--preset',
|
46
45
|
'-p',
|
47
|
-
help='Which preset to use to create this package. Can be a named of an already installed preset, or an URI, in which case the preset will be downloaded
|
46
|
+
help='Which preset to use to create this package. Can be a named of an already installed preset, or an URI, in which case the preset will be downloaded.\n'
|
47
|
+
'If not provided, the default preset will be used, or the active preset if any.',
|
48
48
|
),
|
49
|
-
] =
|
50
|
-
local: bool = typer.Option(
|
51
|
-
False,
|
52
|
-
'--local',
|
53
|
-
'-l',
|
54
|
-
help='Whether to inline the installed preset within the contest folder.',
|
55
|
-
),
|
49
|
+
] = None,
|
56
50
|
):
|
57
51
|
console.console.print(f'Creating new contest at [item]{path}[/item]...')
|
58
52
|
|
59
|
-
fetch_info =
|
60
|
-
if fetch_info is None:
|
61
|
-
console.console.print(
|
62
|
-
f'[error]Invalid preset name/URI [item]{preset}[/item][/error]'
|
63
|
-
)
|
64
|
-
raise typer.Exit(1)
|
65
|
-
|
66
|
-
if fetch_info.is_remote():
|
67
|
-
preset = presets.install_from_remote(fetch_info)
|
68
|
-
elif fetch_info.is_local_dir():
|
69
|
-
preset = presets.install_from_local_dir(fetch_info)
|
70
|
-
|
71
|
-
preset_cfg = presets.get_installed_preset(preset)
|
72
|
-
preset_path = (
|
73
|
-
presets.get_preset_installation_path(preset)
|
74
|
-
if preset_cfg.contest is not None
|
75
|
-
else presets.get_preset_installation_path('default')
|
76
|
-
)
|
77
|
-
|
78
|
-
contest_path = (
|
79
|
-
presets.get_preset_installation_path(preset) / preset_cfg.contest
|
80
|
-
if preset_cfg.contest is not None
|
81
|
-
else presets.get_preset_installation_path('default') / 'contest'
|
82
|
-
)
|
83
|
-
|
84
|
-
if not contest_path.is_dir():
|
85
|
-
console.console.print(
|
86
|
-
f'[error]Contest template [item]{contest_path}[/item] does not exist.[/error]'
|
87
|
-
)
|
88
|
-
raise typer.Exit(1)
|
89
|
-
|
53
|
+
fetch_info = presets.get_preset_fetch_info_with_fallback(preset)
|
90
54
|
dest_path = pathlib.Path(path)
|
91
55
|
|
92
56
|
if dest_path.exists():
|
@@ -100,23 +64,11 @@ def create(
|
|
100
64
|
)
|
101
65
|
raise typer.Exit(1)
|
102
66
|
|
103
|
-
|
104
|
-
shutil.copytree(str(contest_path), str(dest_path), dirs_exist_ok=True)
|
105
|
-
shutil.rmtree(str(dest_path / 'build'), ignore_errors=True)
|
106
|
-
shutil.rmtree(str(dest_path / '.box'), ignore_errors=True)
|
107
|
-
shutil.rmtree(str(dest_path / '.local.rbx'), ignore_errors=True)
|
108
|
-
# TODO: consider clearing build and .box recursively for nested problem directories
|
109
|
-
for lock in dest_path.rglob('.preset-lock.yml'):
|
110
|
-
lock.unlink(missing_ok=True)
|
111
|
-
|
112
|
-
if local:
|
113
|
-
presets.copy_local_preset(
|
114
|
-
preset_path, dest_path, remote_uri=fetch_info.uri or preset_cfg.uri
|
115
|
-
)
|
67
|
+
presets.install_contest(dest_path, fetch_info)
|
116
68
|
|
117
69
|
with cd.new_package_cd(dest_path):
|
118
70
|
contest_utils.clear_all_caches()
|
119
|
-
presets.generate_lock(
|
71
|
+
presets.generate_lock()
|
120
72
|
|
121
73
|
|
122
74
|
@app.command('edit, e', help='Open contest.rbx.yml in your default editor.')
|
@@ -143,9 +95,6 @@ def add(path: str, short_name: str, preset: Optional[str] = None):
|
|
143
95
|
)
|
144
96
|
raise typer.Exit(1)
|
145
97
|
|
146
|
-
preset_lock = presets.get_preset_lock()
|
147
|
-
if preset is None and preset_lock is not None:
|
148
|
-
preset = preset_lock.preset_name
|
149
98
|
creation.create(name, preset=preset, path=pathlib.Path(path))
|
150
99
|
|
151
100
|
contest = find_contest_package_or_die()
|
rbx/box/contest/schema.py
CHANGED
@@ -1,12 +1,15 @@
|
|
1
1
|
import pathlib
|
2
|
-
from typing import Dict, List, Optional
|
2
|
+
from typing import Annotated, Dict, List, Optional
|
3
3
|
|
4
|
-
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
4
|
+
from pydantic import AfterValidator, BaseModel, ConfigDict, Field, model_validator
|
5
5
|
|
6
|
-
from rbx.box.
|
6
|
+
from rbx.box.fields import FNameField, NameField
|
7
|
+
from rbx.box.schema import Primitive, expand_var
|
8
|
+
from rbx.box.statements.expander import expand_statements
|
7
9
|
from rbx.box.statements.schema import (
|
8
10
|
ConversionStep,
|
9
11
|
Joiner,
|
12
|
+
StatementLanguage,
|
10
13
|
StatementType,
|
11
14
|
)
|
12
15
|
|
@@ -15,6 +18,13 @@ def ShortNameField(**kwargs):
|
|
15
18
|
return Field(pattern=r'^[A-Z]+[0-9]*$', min_length=1, max_length=4, **kwargs)
|
16
19
|
|
17
20
|
|
21
|
+
def is_unique_by_name(statements: List['ContestStatement']) -> List['ContestStatement']:
|
22
|
+
names = {st.name for st in statements}
|
23
|
+
if len(names) != len(statements):
|
24
|
+
raise ValueError('Statement names must be unique.')
|
25
|
+
return statements
|
26
|
+
|
27
|
+
|
18
28
|
class ProblemStatementOverride(BaseModel):
|
19
29
|
model_config = ConfigDict(extra='forbid')
|
20
30
|
|
@@ -29,13 +39,26 @@ configure them in case they are applied.
|
|
29
39
|
""",
|
30
40
|
)
|
31
41
|
|
42
|
+
vars: Dict[str, Primitive] = Field(
|
43
|
+
default={},
|
44
|
+
description='Variables to be merged into the problem statement vars.',
|
45
|
+
)
|
46
|
+
|
32
47
|
|
33
48
|
class ContestStatement(BaseModel):
|
34
49
|
model_config = ConfigDict(extra='forbid')
|
35
50
|
|
36
|
-
|
51
|
+
name: str = FNameField(description='Name of this statement.')
|
52
|
+
|
53
|
+
extends: Optional[str] = FNameField(
|
54
|
+
default=None, description='Name of the statement to inherit from.'
|
55
|
+
)
|
56
|
+
|
57
|
+
language: StatementLanguage = Field(
|
58
|
+
default='en', description='Language code for this statement (ISO 639-1).'
|
59
|
+
)
|
37
60
|
|
38
|
-
title: str = Field(description='Title of the contest in this language.')
|
61
|
+
title: str = Field(default='', description='Title of the contest in this language.')
|
39
62
|
|
40
63
|
location: Optional[str] = Field(
|
41
64
|
default=None, description='Location of the contest in this language.'
|
@@ -45,9 +68,14 @@ class ContestStatement(BaseModel):
|
|
45
68
|
default=None, description='Date of the contest in this language.'
|
46
69
|
)
|
47
70
|
|
48
|
-
path: pathlib.Path = Field(
|
71
|
+
path: pathlib.Path = Field(
|
72
|
+
default_factory=pathlib.Path,
|
73
|
+
description='Path to the input statement file.',
|
74
|
+
)
|
49
75
|
|
50
|
-
type: StatementType = Field(
|
76
|
+
type: StatementType = Field(
|
77
|
+
default=StatementType.rbxTeX, description='Type of the input statement file.'
|
78
|
+
)
|
51
79
|
|
52
80
|
joiner: Optional[Joiner] = Field(
|
53
81
|
default=None,
|
@@ -95,6 +123,15 @@ Can be glob pattern as well, such as `imgs/*.png`.
|
|
95
123
|
default=None, description='Override configuration for problem statements.'
|
96
124
|
)
|
97
125
|
|
126
|
+
match: Optional[str] = FNameField(
|
127
|
+
default=None,
|
128
|
+
description="""
|
129
|
+
Name of the problem-level statement to match this statement against.
|
130
|
+
|
131
|
+
If not specified, will match against the first statement of the same language.
|
132
|
+
""",
|
133
|
+
)
|
134
|
+
|
98
135
|
# Vars to be re-used in the statement.
|
99
136
|
# - It will be available as \VAR{vars} variable in the contest-level box statement.
|
100
137
|
vars: Dict[str, Primitive] = Field(
|
@@ -188,7 +225,10 @@ class Contest(BaseModel):
|
|
188
225
|
default=[], description='List of problems in this contest.'
|
189
226
|
)
|
190
227
|
|
191
|
-
statements:
|
228
|
+
statements: Annotated[
|
229
|
+
List[ContestStatement],
|
230
|
+
AfterValidator(is_unique_by_name),
|
231
|
+
] = Field(
|
192
232
|
default=None,
|
193
233
|
description='Configure statements in this contest, per language.',
|
194
234
|
)
|
@@ -199,6 +239,10 @@ class Contest(BaseModel):
|
|
199
239
|
default={}, description='Variables to be re-used across the package.'
|
200
240
|
)
|
201
241
|
|
242
|
+
@property
|
243
|
+
def expanded_statements(self) -> List[ContestStatement]:
|
244
|
+
return expand_statements(self.statements)
|
245
|
+
|
202
246
|
@property
|
203
247
|
def expanded_vars(self) -> Dict[str, Primitive]:
|
204
248
|
return {key: expand_var(value) for key, value in self.vars.items()}
|
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/creation.py
CHANGED
@@ -1,12 +1,10 @@
|
|
1
1
|
import pathlib
|
2
|
-
import shutil
|
3
2
|
from typing import Annotated, Optional
|
4
3
|
|
5
4
|
import typer
|
6
5
|
|
7
6
|
from rbx import console, utils
|
8
7
|
from rbx.box import package, presets
|
9
|
-
from rbx.box.presets.fetch import get_preset_fetch_info
|
10
8
|
|
11
9
|
|
12
10
|
def create(
|
@@ -26,33 +24,9 @@ def create(
|
|
26
24
|
] = None,
|
27
25
|
path: Optional[pathlib.Path] = None,
|
28
26
|
):
|
29
|
-
preset = preset or 'default'
|
30
27
|
console.console.print(f'Creating new problem [item]{name}[/item]...')
|
31
28
|
|
32
|
-
fetch_info =
|
33
|
-
if fetch_info is None:
|
34
|
-
console.console.print(
|
35
|
-
f'[error]Invalid preset name/URI [item]{preset}[/item].[/error]'
|
36
|
-
)
|
37
|
-
raise typer.Exit(1)
|
38
|
-
|
39
|
-
if fetch_info.fetch_uri is not None:
|
40
|
-
preset = presets.install_from_remote(fetch_info)
|
41
|
-
|
42
|
-
preset_cfg = presets.get_installed_preset(preset)
|
43
|
-
|
44
|
-
problem_path = (
|
45
|
-
presets.get_preset_installation_path(preset) / preset_cfg.problem
|
46
|
-
if preset_cfg.problem is not None
|
47
|
-
else presets.get_preset_installation_path('default') / 'problem'
|
48
|
-
)
|
49
|
-
|
50
|
-
if not problem_path.is_dir():
|
51
|
-
console.console.print(
|
52
|
-
f'[error]Problem template [item]{problem_path}[/item] does not exist.[/error]'
|
53
|
-
)
|
54
|
-
raise typer.Exit(1)
|
55
|
-
|
29
|
+
fetch_info = presets.get_preset_fetch_info_with_fallback(preset)
|
56
30
|
dest_path = path or pathlib.Path(name)
|
57
31
|
|
58
32
|
if dest_path.exists():
|
@@ -61,18 +35,11 @@ def create(
|
|
61
35
|
)
|
62
36
|
raise typer.Exit(1)
|
63
37
|
|
64
|
-
|
65
|
-
shutil.copytree(str(problem_path), str(dest_path))
|
66
|
-
|
67
|
-
# Remove a few left overs.
|
68
|
-
shutil.rmtree(str(dest_path / 'build'), ignore_errors=True)
|
69
|
-
shutil.rmtree(str(dest_path / '.box'), ignore_errors=True)
|
70
|
-
for lock in dest_path.rglob('.preset-lock.yml'):
|
71
|
-
lock.unlink(missing_ok=True)
|
38
|
+
presets.install_problem(dest_path, fetch_info)
|
72
39
|
|
73
40
|
# Change problem name.
|
74
41
|
ru, problem = package.get_ruyaml(dest_path)
|
75
42
|
problem['name'] = name
|
76
43
|
utils.save_ruyaml(dest_path / 'problem.rbx.yml', ru, problem)
|
77
44
|
|
78
|
-
presets.generate_lock(
|
45
|
+
presets.generate_lock(dest_path)
|
rbx/box/environment.py
CHANGED
@@ -7,6 +7,7 @@ import typer
|
|
7
7
|
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
8
8
|
|
9
9
|
from rbx import config, console, utils
|
10
|
+
from rbx.box import presets
|
10
11
|
from rbx.box.extensions import Extensions, LanguageExtensions
|
11
12
|
from rbx.grading.judge.sandbox import SandboxBase, SandboxParams
|
12
13
|
from rbx.grading.judge.sandboxes.isolate import IsolateSandbox
|
@@ -185,22 +186,33 @@ class Environment(BaseModel):
|
|
185
186
|
extensions: Optional[Extensions] = None
|
186
187
|
|
187
188
|
|
188
|
-
def
|
189
|
+
def get_app_environment_path(env: str) -> pathlib.Path:
|
189
190
|
return config.get_app_file(pathlib.PosixPath('envs') / f'{env}.rbx.yml')
|
190
191
|
|
191
192
|
|
192
|
-
def
|
193
|
-
|
194
|
-
|
193
|
+
def get_active_environment_path() -> pathlib.Path:
|
194
|
+
env_path = presets.get_preset_environment_path()
|
195
|
+
if env_path is None:
|
196
|
+
env_path = get_app_environment_path(config.get_config().boxEnvironment)
|
197
|
+
return env_path
|
198
|
+
|
199
|
+
|
200
|
+
@functools.cache
|
201
|
+
def get_active_environment_description() -> str:
|
202
|
+
env_path = presets.get_preset_environment_path()
|
203
|
+
if env_path is None:
|
204
|
+
return config.get_config().boxEnvironment
|
205
|
+
preset = presets.get_active_preset()
|
206
|
+
return f'preset - {preset.name}'
|
195
207
|
|
196
208
|
|
197
209
|
@functools.cache
|
198
210
|
def get_environment(env: Optional[str] = None) -> Environment:
|
199
211
|
env_path = (
|
200
|
-
|
212
|
+
get_app_environment_path(env)
|
213
|
+
if env is not None
|
214
|
+
else get_active_environment_path()
|
201
215
|
)
|
202
|
-
if env_path is None:
|
203
|
-
env_path = get_environment_path(config.get_config().boxEnvironment)
|
204
216
|
if not env_path.is_file():
|
205
217
|
console.console.print(
|
206
218
|
f'Environment file [item]{env_path}[/item] not found.', style='error'
|
@@ -232,8 +244,8 @@ def install_environment(name: str, file: pathlib.Path):
|
|
232
244
|
)
|
233
245
|
raise typer.Exit(1)
|
234
246
|
|
235
|
-
|
236
|
-
|
247
|
+
get_app_environment_path(name).parent.mkdir(parents=True, exist_ok=True)
|
248
|
+
get_app_environment_path(name).write_bytes(file.read_bytes())
|
237
249
|
console.console.print(
|
238
250
|
f'[success]Environment [item]{name}[/item] was installed from [item]{file}[/item]'
|
239
251
|
)
|
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/linting.py
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
import pathlib
|
2
|
+
|
3
|
+
import yamlfix
|
4
|
+
import yamlfix.model
|
5
|
+
|
6
|
+
from rbx import console
|
7
|
+
from rbx.box.cd import is_contest_package, is_problem_package
|
8
|
+
from rbx.box.stats import find_problem_packages_from_contest
|
9
|
+
|
10
|
+
|
11
|
+
def fix_yaml(path: pathlib.Path, verbose: bool = True):
|
12
|
+
config = yamlfix.model.YamlfixConfig(quote_basic_values=True)
|
13
|
+
_, changed = yamlfix.fix_files([str(path)], dry_run=False, config=config)
|
14
|
+
if changed and verbose:
|
15
|
+
console.console.print(
|
16
|
+
f'Formatting [item]{path}[/item].',
|
17
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
def fix_package(root: pathlib.Path = pathlib.Path()):
|
21
|
+
if is_problem_package(root):
|
22
|
+
fix_yaml(root / 'problem.rbx.yml')
|
23
|
+
if is_contest_package(root):
|
24
|
+
fix_yaml(root / 'contest.rbx.yml')
|
25
|
+
for problem in find_problem_packages_from_contest(root):
|
26
|
+
fix_yaml(problem / 'problem.rbx.yml')
|
rbx/box/package.py
CHANGED
@@ -10,10 +10,9 @@ import ruyaml
|
|
10
10
|
import typer
|
11
11
|
from pydantic import ValidationError
|
12
12
|
|
13
|
-
from rbx import
|
14
|
-
from rbx.box import cd
|
13
|
+
from rbx import console, utils
|
14
|
+
from rbx.box import cd
|
15
15
|
from rbx.box.environment import get_sandbox_type
|
16
|
-
from rbx.box.presets import get_installed_preset_or_null, get_preset_lock
|
17
16
|
from rbx.box.schema import (
|
18
17
|
CodeItem,
|
19
18
|
ExpectedOutcome,
|
@@ -38,33 +37,6 @@ TEMP_DIR = None
|
|
38
37
|
CACHE_STEP_VERSION = 1
|
39
38
|
|
40
39
|
|
41
|
-
def warn_preset_deactivated(root: pathlib.Path = pathlib.Path()):
|
42
|
-
preset_lock = get_preset_lock(root)
|
43
|
-
if preset_lock is None:
|
44
|
-
return
|
45
|
-
|
46
|
-
preset = get_installed_preset_or_null(preset_lock.preset_name)
|
47
|
-
if preset is None:
|
48
|
-
console.console.print(
|
49
|
-
f'[warning]WARNING: [item]{preset_lock.preset_name}[/item] is not installed. '
|
50
|
-
'Run [item]rbx presets sync && rbx activate[/item] to install and activate this preset.'
|
51
|
-
)
|
52
|
-
console.console.print()
|
53
|
-
return
|
54
|
-
|
55
|
-
if preset.env is not None and (
|
56
|
-
not environment.get_environment_path(preset.name).is_file()
|
57
|
-
or config.get_config().boxEnvironment != preset.name
|
58
|
-
):
|
59
|
-
console.console.print(
|
60
|
-
'[warning]WARNING: This package uses a preset that configures a custom environment, '
|
61
|
-
f' but instead you are using the environment [item]{config.get_config().boxEnvironment}[/item]. '
|
62
|
-
'Run [item]rbx activate[/item] to use the environment configured by your preset.'
|
63
|
-
)
|
64
|
-
console.console.print()
|
65
|
-
return
|
66
|
-
|
67
|
-
|
68
40
|
@functools.cache
|
69
41
|
def find_problem_yaml(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.Path]:
|
70
42
|
root = root.resolve()
|
@@ -74,7 +46,6 @@ def find_problem_yaml(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.P
|
|
74
46
|
problem_yaml_path = root / YAML_NAME
|
75
47
|
if not problem_yaml_path.is_file():
|
76
48
|
return None
|
77
|
-
warn_preset_deactivated(root)
|
78
49
|
return problem_yaml_path
|
79
50
|
|
80
51
|
|
@@ -130,9 +101,7 @@ def save_package(
|
|
130
101
|
def get_ruyaml(root: pathlib.Path = pathlib.Path()) -> Tuple[ruyaml.YAML, ruyaml.Any]:
|
131
102
|
problem_yaml_path = find_problem_yaml(root)
|
132
103
|
if problem_yaml_path is None:
|
133
|
-
console.console.print(
|
134
|
-
f'Problem not found in {pathlib.Path().absolute()}', style='error'
|
135
|
-
)
|
104
|
+
console.console.print(f'[error]Problem not found in {root.absolute()}[/error]')
|
136
105
|
raise typer.Exit(1)
|
137
106
|
res = ruyaml.YAML()
|
138
107
|
return res, res.load(problem_yaml_path.read_text())
|
@@ -459,7 +428,7 @@ def get_merged_capture_path(root: pathlib.Path = pathlib.Path()) -> pathlib.Path
|
|
459
428
|
def is_cache_valid(root: pathlib.Path = pathlib.Path()):
|
460
429
|
cache_dir = find_problem(root) / '.box'
|
461
430
|
if not cache_dir.is_dir():
|
462
|
-
return
|
431
|
+
return True
|
463
432
|
|
464
433
|
fingerprint_file = cache_dir / 'fingerprint'
|
465
434
|
if not fingerprint_file.is_file():
|