rbx.cp 0.13.8__py3-none-any.whl → 0.16.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/__version__.py +1 -0
- rbx/box/cli.py +74 -70
- rbx/box/code.py +3 -0
- rbx/box/contest/build_contest_statements.py +65 -23
- rbx/box/contest/contest_package.py +8 -1
- rbx/box/contest/main.py +9 -3
- rbx/box/contest/schema.py +17 -13
- rbx/box/contest/statements.py +12 -8
- rbx/box/dump_schemas.py +2 -1
- rbx/box/environment.py +1 -1
- rbx/box/fields.py +22 -4
- rbx/box/generators.py +32 -13
- rbx/box/git_utils.py +29 -1
- rbx/box/limits_info.py +161 -0
- rbx/box/package.py +18 -1
- rbx/box/packaging/boca/boca_language_utils.py +26 -0
- rbx/box/packaging/boca/boca_outcome_utils.py +10 -0
- rbx/box/packaging/boca/packager.py +7 -5
- rbx/box/packaging/contest_main.py +20 -12
- rbx/box/packaging/packager.py +24 -14
- rbx/box/packaging/polygon/packager.py +7 -3
- rbx/box/packaging/polygon/upload.py +2 -1
- rbx/box/presets/__init__.py +143 -78
- rbx/box/presets/fetch.py +10 -2
- rbx/box/presets/schema.py +16 -1
- rbx/box/remote.py +3 -3
- rbx/box/sanitizers/issue_stack.py +124 -0
- rbx/box/schema.py +87 -27
- rbx/box/solutions.py +74 -117
- rbx/box/statements/build_statements.py +12 -1
- rbx/box/statements/builders.py +5 -3
- rbx/box/statements/latex_jinja.py +73 -23
- rbx/box/statements/schema.py +7 -9
- rbx/box/stressing/generator_parser.py +3 -1
- rbx/box/tasks.py +10 -10
- rbx/box/testcase_extractors.py +8 -0
- rbx/box/testing/testing_preset.py +129 -2
- rbx/box/testing/testing_shared.py +3 -1
- rbx/box/timing.py +305 -0
- rbx/box/tooling/boca/debug_utils.py +88 -0
- rbx/box/tooling/boca/manual_scrape.py +20 -0
- rbx/box/tooling/boca/scraper.py +660 -57
- rbx/box/unit.py +0 -2
- rbx/box/validators.py +0 -4
- rbx/grading/judge/cacher.py +36 -0
- rbx/grading/judge/program.py +12 -2
- rbx/grading/judge/sandbox.py +1 -1
- rbx/grading/judge/sandboxes/stupid_sandbox.py +2 -1
- rbx/grading/judge/storage.py +36 -3
- rbx/grading/limits.py +4 -0
- rbx/grading/steps.py +3 -2
- rbx/resources/presets/default/contest/contest.rbx.yml +7 -1
- rbx/resources/presets/default/contest/statement/info.rbx.tex +46 -0
- rbx/resources/presets/default/preset.rbx.yml +1 -0
- rbx/resources/presets/default/problem/.gitignore +1 -0
- rbx/resources/presets/default/problem/problem.rbx.yml +19 -3
- rbx/resources/presets/default/problem/rbx.h +52 -5
- rbx/resources/presets/default/problem/statement/statement.rbx.tex +6 -2
- rbx/resources/presets/default/problem/testlib.h +6299 -0
- rbx/resources/presets/default/problem/validator.cpp +4 -3
- rbx/resources/presets/default/shared/contest_template.rbx.tex +8 -4
- rbx/resources/presets/default/shared/icpc.sty +18 -3
- rbx/resources/presets/default/shared/problem_template.rbx.tex +4 -1
- rbx/testing_utils.py +17 -1
- rbx/utils.py +45 -0
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/METADATA +5 -2
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/RECORD +71 -67
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/entry_points.txt +0 -1
- rbx/providers/__init__.py +0 -43
- rbx/providers/codeforces.py +0 -73
- rbx/providers/provider.py +0 -26
- rbx/submitors/__init__.py +0 -18
- rbx/submitors/codeforces.py +0 -121
- rbx/submitors/submitor.py +0 -25
- /rbx/resources/presets/default/problem/sols/{wa.cpp → wa-overflow.cpp} +0 -0
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/LICENSE +0 -0
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/WHEEL +0 -0
rbx/box/presets/__init__.py
CHANGED
@@ -1,15 +1,23 @@
|
|
1
|
+
import functools
|
1
2
|
import os
|
2
3
|
import pathlib
|
3
4
|
import shutil
|
4
5
|
import tempfile
|
5
6
|
from typing import Annotated, Iterable, List, Optional, Sequence, Set, Tuple, Union
|
6
7
|
|
8
|
+
import questionary
|
7
9
|
import ruyaml
|
10
|
+
import semver
|
8
11
|
import typer
|
9
12
|
|
10
13
|
from rbx import console, utils
|
11
14
|
from rbx.box import cd
|
12
|
-
from rbx.box.
|
15
|
+
from rbx.box.git_utils import latest_remote_tag
|
16
|
+
from rbx.box.presets.fetch import (
|
17
|
+
PresetFetchInfo,
|
18
|
+
get_preset_fetch_info,
|
19
|
+
get_remote_uri_from_tool_preset,
|
20
|
+
)
|
13
21
|
from rbx.box.presets.lock_schema import LockedAsset, PresetLock, SymlinkInfo
|
14
22
|
from rbx.box.presets.schema import Preset, TrackedAsset
|
15
23
|
from rbx.config import get_default_app_path
|
@@ -17,24 +25,50 @@ from rbx.grading.judge.digester import digest_cooperatively
|
|
17
25
|
|
18
26
|
app = typer.Typer(no_args_is_help=True)
|
19
27
|
|
20
|
-
|
28
|
+
_FALLBACK_PRESET_NAME = 'default'
|
21
29
|
|
22
30
|
|
23
|
-
def
|
31
|
+
def find_preset_yaml(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.Path]:
|
24
32
|
found = root / 'preset.rbx.yml'
|
25
33
|
if found.exists():
|
26
34
|
return found
|
27
35
|
return None
|
28
36
|
|
29
37
|
|
38
|
+
@functools.cache
|
39
|
+
def _check_preset_compatibility(preset_name: str, preset_version: str) -> None:
|
40
|
+
compatibility = utils.check_version_compatibility(preset_version)
|
41
|
+
if compatibility == utils.SemVerCompatibility.OUTDATED:
|
42
|
+
console.console.print(
|
43
|
+
f'[error]Preset [item]{preset_name}[/item] requires rbx at version [item]{preset_version}[/item], but the current version is [item]{utils.get_version()}[/item].[/error]'
|
44
|
+
)
|
45
|
+
console.console.print(
|
46
|
+
f'[error]Please update rbx.cp to the latest compatible version using [item]{utils.get_upgrade_command(preset_version)}[/item].[/error]'
|
47
|
+
)
|
48
|
+
raise typer.Exit(1)
|
49
|
+
if compatibility == utils.SemVerCompatibility.BREAKING_CHANGE:
|
50
|
+
console.console.print(
|
51
|
+
f'[error]Preset [item]{preset_name}[/item] requires rbx at version [item]{preset_version}[/item], but the current version is [item]{utils.get_version()}[/item].[/error]'
|
52
|
+
)
|
53
|
+
console.console.print(
|
54
|
+
'[error]rbx version is newer, but is in a later major version, which might have introduced breaking changes.[/error]'
|
55
|
+
)
|
56
|
+
console.console.print(
|
57
|
+
'[error]If you are sure that the preset is compatible with the current rbx version, you can change the [item]min_version[/item] field in the preset file ([item].local.rbx/preset.rbx.yml)[/item] to the current version.[/error]'
|
58
|
+
)
|
59
|
+
raise typer.Exit(1)
|
60
|
+
|
61
|
+
|
30
62
|
def get_preset_yaml(root: pathlib.Path = pathlib.Path()) -> Preset:
|
31
|
-
found =
|
63
|
+
found = find_preset_yaml(root)
|
32
64
|
if not found:
|
33
65
|
console.console.print(
|
34
66
|
f'[error][item]preset.rbx.yml[/item] not found in [item]{root.absolute()}[/item][/error]'
|
35
67
|
)
|
36
68
|
raise typer.Exit(1)
|
37
|
-
|
69
|
+
preset = utils.model_from_yaml(Preset, found.read_text())
|
70
|
+
_check_preset_compatibility(preset.name, preset.min_version)
|
71
|
+
return preset
|
38
72
|
|
39
73
|
|
40
74
|
def _find_preset_lock(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.Path]:
|
@@ -45,14 +79,14 @@ def _find_preset_lock(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.P
|
|
45
79
|
return problem_yaml_path
|
46
80
|
|
47
81
|
|
48
|
-
def
|
82
|
+
def get_preset_lock(root: pathlib.Path = pathlib.Path()) -> Optional[PresetLock]:
|
49
83
|
found = _find_preset_lock(root)
|
50
84
|
if not found:
|
51
85
|
return None
|
52
86
|
return utils.model_from_yaml(PresetLock, found.read_text())
|
53
87
|
|
54
88
|
|
55
|
-
def
|
89
|
+
def find_nested_preset(root: pathlib.Path) -> Optional[pathlib.Path]:
|
56
90
|
root = utils.abspath(root)
|
57
91
|
problem_yaml_path = root / 'preset.rbx.yml'
|
58
92
|
while root != pathlib.PosixPath('/') and not problem_yaml_path.is_file():
|
@@ -63,7 +97,7 @@ def _find_nested_preset(root: pathlib.Path) -> Optional[pathlib.Path]:
|
|
63
97
|
return problem_yaml_path.parent
|
64
98
|
|
65
99
|
|
66
|
-
def
|
100
|
+
def find_local_preset(root: pathlib.Path) -> Optional[pathlib.Path]:
|
67
101
|
original_root = root
|
68
102
|
root = utils.abspath(root)
|
69
103
|
problem_yaml_path = root / '.local.rbx' / 'preset.rbx.yml'
|
@@ -71,12 +105,12 @@ def _find_local_preset(root: pathlib.Path) -> Optional[pathlib.Path]:
|
|
71
105
|
root = root.parent
|
72
106
|
problem_yaml_path = root / '.local.rbx' / 'preset.rbx.yml'
|
73
107
|
if not problem_yaml_path.is_file():
|
74
|
-
return
|
108
|
+
return find_nested_preset(original_root)
|
75
109
|
return problem_yaml_path.parent
|
76
110
|
|
77
111
|
|
78
112
|
def _is_installed_preset(root: pathlib.Path = pathlib.Path()) -> bool:
|
79
|
-
preset_path =
|
113
|
+
preset_path = find_local_preset(root)
|
80
114
|
if preset_path is None:
|
81
115
|
return False
|
82
116
|
resolved_path = utils.abspath(preset_path)
|
@@ -84,25 +118,25 @@ def _is_installed_preset(root: pathlib.Path = pathlib.Path()) -> bool:
|
|
84
118
|
|
85
119
|
|
86
120
|
def _is_active_preset_nested(root: pathlib.Path = pathlib.Path()) -> bool:
|
87
|
-
preset_path =
|
121
|
+
preset_path = find_local_preset(root)
|
88
122
|
if preset_path is None:
|
89
123
|
return False
|
90
|
-
nested_preset_path =
|
124
|
+
nested_preset_path = find_nested_preset(root)
|
91
125
|
if nested_preset_path is None:
|
92
126
|
return False
|
93
127
|
return nested_preset_path == preset_path
|
94
128
|
|
95
129
|
|
96
|
-
def
|
130
|
+
def is_contest(root: pathlib.Path = pathlib.Path()) -> bool:
|
97
131
|
return (root / 'contest.rbx.yml').is_file()
|
98
132
|
|
99
133
|
|
100
|
-
def
|
134
|
+
def is_problem(root: pathlib.Path = pathlib.Path()) -> bool:
|
101
135
|
return (root / 'problem.rbx.yml').is_file()
|
102
136
|
|
103
137
|
|
104
|
-
def
|
105
|
-
if not
|
138
|
+
def check_is_valid_package(root: pathlib.Path = pathlib.Path()):
|
139
|
+
if not is_contest(root) and not is_problem(root):
|
106
140
|
console.console.print('[error]Not a valid rbx package directory.[/error]')
|
107
141
|
raise typer.Exit(1)
|
108
142
|
|
@@ -110,7 +144,7 @@ def _check_is_valid_package(root: pathlib.Path = pathlib.Path()):
|
|
110
144
|
def _glob_while_ignoring(
|
111
145
|
dir: pathlib.Path,
|
112
146
|
glb: str,
|
113
|
-
extra_gitignore: Optional[str] = '.box\nbuild\n',
|
147
|
+
extra_gitignore: Optional[str] = '.box\nbuild\n.limits/local.yml\n',
|
114
148
|
recursive: bool = False,
|
115
149
|
) -> Iterable[pathlib.Path]:
|
116
150
|
from gitignore_parser import parse_gitignore, parse_gitignore_str
|
@@ -133,7 +167,7 @@ def _glob_while_ignoring(
|
|
133
167
|
yield file
|
134
168
|
|
135
169
|
|
136
|
-
def
|
170
|
+
def process_globbing(
|
137
171
|
assets: Iterable[TrackedAsset], preset_pkg_dir: pathlib.Path
|
138
172
|
) -> List[TrackedAsset]:
|
139
173
|
res = []
|
@@ -156,7 +190,7 @@ def _process_globbing(
|
|
156
190
|
return res
|
157
191
|
|
158
192
|
|
159
|
-
def
|
193
|
+
def dedup_tracked_assets(assets: List[TrackedAsset]) -> List[TrackedAsset]:
|
160
194
|
seen_paths = set()
|
161
195
|
res = []
|
162
196
|
for asset in assets:
|
@@ -167,11 +201,11 @@ def _dedup_tracked_assets(assets: List[TrackedAsset]) -> List[TrackedAsset]:
|
|
167
201
|
return res
|
168
202
|
|
169
203
|
|
170
|
-
def
|
204
|
+
def get_preset_tracked_assets(
|
171
205
|
root: pathlib.Path, is_contest: bool, add_symlinks: bool = False
|
172
206
|
) -> List[TrackedAsset]:
|
173
207
|
preset = get_active_preset(root)
|
174
|
-
preset_path =
|
208
|
+
preset_path = find_local_preset(root)
|
175
209
|
assert preset_path is not None
|
176
210
|
|
177
211
|
if is_contest:
|
@@ -179,13 +213,13 @@ def _get_preset_tracked_assets(
|
|
179
213
|
preset.contest is not None
|
180
214
|
), 'Preset does not have a contest package definition.'
|
181
215
|
preset_pkg_path = preset_path / preset.contest
|
182
|
-
res =
|
216
|
+
res = process_globbing(preset.tracking.contest, preset_pkg_path)
|
183
217
|
else:
|
184
218
|
assert (
|
185
219
|
preset.problem is not None
|
186
220
|
), 'Preset does not have a problem package definition,'
|
187
221
|
preset_pkg_path = preset_path / preset.problem
|
188
|
-
res =
|
222
|
+
res = process_globbing(preset.tracking.problem, preset_pkg_path)
|
189
223
|
|
190
224
|
if add_symlinks:
|
191
225
|
for file in _glob_while_ignoring(
|
@@ -199,7 +233,7 @@ def _get_preset_tracked_assets(
|
|
199
233
|
TrackedAsset(path=file.relative_to(preset_pkg_path), symlink=True)
|
200
234
|
)
|
201
235
|
|
202
|
-
return
|
236
|
+
return dedup_tracked_assets(res)
|
203
237
|
|
204
238
|
|
205
239
|
def _get_tracked_assets_symlinks(
|
@@ -212,7 +246,7 @@ def _get_tracked_assets_symlinks(
|
|
212
246
|
return res
|
213
247
|
|
214
248
|
|
215
|
-
def
|
249
|
+
def get_symlink_info(
|
216
250
|
tracked_asset: Union[TrackedAsset, LockedAsset], root: pathlib.Path
|
217
251
|
) -> Optional[SymlinkInfo]:
|
218
252
|
asset_path = root / tracked_asset.path
|
@@ -225,7 +259,7 @@ def _get_symlink_info(
|
|
225
259
|
return SymlinkInfo(target=target, is_broken=is_broken, is_outside=is_outside)
|
226
260
|
|
227
261
|
|
228
|
-
def
|
262
|
+
def build_package_locked_assets(
|
229
263
|
tracked_assets: Sequence[Union[TrackedAsset, LockedAsset]],
|
230
264
|
root: pathlib.Path = pathlib.Path(),
|
231
265
|
) -> List[LockedAsset]:
|
@@ -237,7 +271,7 @@ def _build_package_locked_assets(
|
|
237
271
|
LockedAsset(
|
238
272
|
path=tracked_asset.path,
|
239
273
|
hash=None,
|
240
|
-
symlink_info=
|
274
|
+
symlink_info=get_symlink_info(tracked_asset, root),
|
241
275
|
)
|
242
276
|
)
|
243
277
|
continue
|
@@ -246,13 +280,13 @@ def _build_package_locked_assets(
|
|
246
280
|
LockedAsset(
|
247
281
|
path=tracked_asset.path,
|
248
282
|
hash=digest_cooperatively(f),
|
249
|
-
symlink_info=
|
283
|
+
symlink_info=get_symlink_info(tracked_asset, root),
|
250
284
|
)
|
251
285
|
)
|
252
286
|
return res
|
253
287
|
|
254
288
|
|
255
|
-
def
|
289
|
+
def find_non_modified_assets(
|
256
290
|
reference: List[LockedAsset], current: List[LockedAsset]
|
257
291
|
) -> List[LockedAsset]:
|
258
292
|
reference_by_path = {asset.path: asset for asset in reference}
|
@@ -273,7 +307,7 @@ def _find_non_modified_assets(
|
|
273
307
|
return res
|
274
308
|
|
275
309
|
|
276
|
-
def
|
310
|
+
def find_modified_assets(
|
277
311
|
reference: List[LockedAsset],
|
278
312
|
current: List[LockedAsset],
|
279
313
|
seen_symlinks: Set[pathlib.Path],
|
@@ -297,7 +331,7 @@ def _find_modified_assets(
|
|
297
331
|
return res
|
298
332
|
|
299
333
|
|
300
|
-
def
|
334
|
+
def copy_preset_file(
|
301
335
|
src: pathlib.Path,
|
302
336
|
dst: pathlib.Path,
|
303
337
|
preset_package_path: pathlib.Path,
|
@@ -358,19 +392,19 @@ def _copy_updated_assets(
|
|
358
392
|
preset_path = get_active_preset_path(root)
|
359
393
|
preset_package_path = _get_active_preset_package_path(root, is_contest)
|
360
394
|
|
361
|
-
preset_tracked_assets =
|
395
|
+
preset_tracked_assets = get_preset_tracked_assets(
|
362
396
|
preset_package_path, is_contest=is_contest, add_symlinks=symlinks
|
363
397
|
)
|
364
|
-
current_preset_snapshot =
|
398
|
+
current_preset_snapshot = build_package_locked_assets(
|
365
399
|
preset_tracked_assets, preset_package_path
|
366
400
|
)
|
367
401
|
|
368
402
|
# Build current package snapshot based on the current preset snapshot.
|
369
|
-
current_package_snapshot =
|
403
|
+
current_package_snapshot = build_package_locked_assets(current_preset_snapshot)
|
370
404
|
|
371
405
|
non_modified_assets = current_package_snapshot
|
372
406
|
if not force:
|
373
|
-
non_modified_assets =
|
407
|
+
non_modified_assets = find_non_modified_assets(
|
374
408
|
preset_lock.assets, current_package_snapshot
|
375
409
|
)
|
376
410
|
|
@@ -386,7 +420,7 @@ def _copy_updated_assets(
|
|
386
420
|
|
387
421
|
seen_symlinks = _get_tracked_assets_symlinks(preset_tracked_assets)
|
388
422
|
|
389
|
-
assets_to_copy =
|
423
|
+
assets_to_copy = find_modified_assets(
|
390
424
|
non_modified_assets, current_preset_snapshot, seen_symlinks
|
391
425
|
)
|
392
426
|
|
@@ -402,7 +436,7 @@ def _copy_updated_assets(
|
|
402
436
|
for asset in assets_to_copy:
|
403
437
|
src_path = preset_package_path / asset.path
|
404
438
|
dst_path = root / asset.path
|
405
|
-
|
439
|
+
copy_preset_file(
|
406
440
|
src_path,
|
407
441
|
dst_path,
|
408
442
|
preset_package_path,
|
@@ -415,7 +449,7 @@ def _copy_updated_assets(
|
|
415
449
|
|
416
450
|
|
417
451
|
def get_active_preset_or_null(root: pathlib.Path = pathlib.Path()) -> Optional[Preset]:
|
418
|
-
local_preset =
|
452
|
+
local_preset = find_local_preset(root)
|
419
453
|
if local_preset is not None:
|
420
454
|
return get_preset_yaml(local_preset)
|
421
455
|
return None
|
@@ -430,7 +464,7 @@ def get_active_preset(root: pathlib.Path = pathlib.Path()) -> Preset:
|
|
430
464
|
|
431
465
|
|
432
466
|
def get_active_preset_path(root: pathlib.Path = pathlib.Path()) -> pathlib.Path:
|
433
|
-
preset_path =
|
467
|
+
preset_path = find_local_preset(root)
|
434
468
|
if preset_path is None:
|
435
469
|
console.console.print('[error]No preset is active.[/error]')
|
436
470
|
raise typer.Exit(1)
|
@@ -458,7 +492,7 @@ def _get_active_preset_package_path(
|
|
458
492
|
is_contest: bool = False,
|
459
493
|
) -> pathlib.Path:
|
460
494
|
preset = get_active_preset(root)
|
461
|
-
preset_path =
|
495
|
+
preset_path = find_local_preset(root)
|
462
496
|
assert preset_path is not None
|
463
497
|
if is_contest:
|
464
498
|
assert (
|
@@ -478,7 +512,7 @@ def get_preset_fetch_info_with_fallback(
|
|
478
512
|
# Use active preset if any, otherwise use the default preset.
|
479
513
|
if get_active_preset_or_null() is not None:
|
480
514
|
return None
|
481
|
-
default_preset = get_preset_fetch_info(
|
515
|
+
default_preset = get_preset_fetch_info(_FALLBACK_PRESET_NAME)
|
482
516
|
if default_preset is None:
|
483
517
|
console.console.print(
|
484
518
|
'[error]Internal error: could not find [item]default[/item] preset.[/error]'
|
@@ -488,26 +522,26 @@ def get_preset_fetch_info_with_fallback(
|
|
488
522
|
return get_preset_fetch_info(uri)
|
489
523
|
|
490
524
|
|
491
|
-
def
|
525
|
+
def clean_copied_package_dir(dest: pathlib.Path):
|
492
526
|
for box_dir in dest.rglob('.box'):
|
493
527
|
shutil.rmtree(str(box_dir), ignore_errors=True)
|
494
528
|
for lock in dest.rglob('.preset-lock.yml'):
|
495
529
|
lock.unlink(missing_ok=True)
|
496
530
|
|
497
531
|
|
498
|
-
def
|
532
|
+
def clean_copied_contest_dir(dest: pathlib.Path, delete_local_rbx: bool = True):
|
499
533
|
shutil.rmtree(str(dest / 'build'), ignore_errors=True)
|
500
534
|
if delete_local_rbx:
|
501
535
|
shutil.rmtree(str(dest / '.local.rbx'), ignore_errors=True)
|
502
|
-
|
536
|
+
clean_copied_package_dir(dest)
|
503
537
|
|
504
538
|
|
505
|
-
def
|
539
|
+
def clean_copied_problem_dir(dest: pathlib.Path):
|
506
540
|
shutil.rmtree(str(dest / 'build'), ignore_errors=True)
|
507
|
-
|
541
|
+
clean_copied_package_dir(dest)
|
508
542
|
|
509
543
|
|
510
|
-
def
|
544
|
+
def install_preset_from_dir(
|
511
545
|
src: pathlib.Path,
|
512
546
|
dest: pathlib.Path,
|
513
547
|
ensure_contest: bool = False,
|
@@ -527,6 +561,15 @@ def _install_preset_from_dir(
|
|
527
561
|
f'[error]Preset [item]{preset.name}[/item] does not have a problem package definition.[/error]'
|
528
562
|
)
|
529
563
|
raise typer.Exit(1)
|
564
|
+
|
565
|
+
try:
|
566
|
+
_check_preset_compatibility(preset.name, preset.min_version)
|
567
|
+
except Exception:
|
568
|
+
console.console.print(
|
569
|
+
f'[error]Error updating preset [item]{preset.name}[/item] to its latest version.[/error]'
|
570
|
+
)
|
571
|
+
raise
|
572
|
+
|
530
573
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
531
574
|
copy_tree_normalizing_gitdir(src, dest, update=update)
|
532
575
|
|
@@ -541,11 +584,11 @@ def _install_preset_from_dir(
|
|
541
584
|
shutil.rmtree(str(dest / '.local.rbx'), ignore_errors=True)
|
542
585
|
|
543
586
|
if preset.contest is not None:
|
544
|
-
|
587
|
+
clean_copied_contest_dir(dest / preset.contest)
|
545
588
|
if preset.problem is not None:
|
546
|
-
|
589
|
+
clean_copied_problem_dir(dest / preset.problem)
|
547
590
|
|
548
|
-
|
591
|
+
clean_copied_package_dir(dest)
|
549
592
|
|
550
593
|
|
551
594
|
def _install_preset_from_remote(
|
@@ -569,7 +612,7 @@ def _install_preset_from_remote(
|
|
569
612
|
f'Installing preset from [item]{fetch_info.inner_dir}[/item].'
|
570
613
|
)
|
571
614
|
pd = pd / fetch_info.inner_dir
|
572
|
-
|
615
|
+
install_preset_from_dir(
|
573
616
|
pd,
|
574
617
|
dest,
|
575
618
|
ensure_contest,
|
@@ -591,7 +634,7 @@ def _install_preset_from_local_dir(
|
|
591
634
|
console.console.print(
|
592
635
|
f'Installing local preset [item]{preset.name}[/item] into [item]{dest}[/item]...'
|
593
636
|
)
|
594
|
-
|
637
|
+
install_preset_from_dir(
|
595
638
|
pd,
|
596
639
|
dest,
|
597
640
|
ensure_contest,
|
@@ -614,15 +657,38 @@ def _install_preset_from_resources(
|
|
614
657
|
yaml_path = rsrc_preset_path / 'preset.rbx.yml'
|
615
658
|
if not yaml_path.is_file():
|
616
659
|
return False
|
660
|
+
preset_uri = get_remote_uri_from_tool_preset(fetch_info.name)
|
661
|
+
remote_fetch_info = get_preset_fetch_info(preset_uri)
|
662
|
+
if remote_fetch_info is None or remote_fetch_info.fetch_uri is None:
|
663
|
+
console.console.print(
|
664
|
+
f'[error]Preset [item]{fetch_info.name}[/item] not found.[/error]'
|
665
|
+
)
|
666
|
+
raise typer.Exit(1)
|
667
|
+
|
668
|
+
# Check if the latest release has breaking changes.
|
669
|
+
latest_tag = latest_remote_tag(remote_fetch_info.fetch_uri)
|
670
|
+
latest_version = semver.VersionInfo.parse(latest_tag)
|
671
|
+
if latest_version.major > utils.get_semver().major:
|
672
|
+
console.console.print(
|
673
|
+
f'[error]You are not in rbx.cp latest major version ({latest_version.major}), but are installing a built-in preset from rbx.cp.[/error]'
|
674
|
+
)
|
675
|
+
console.console.print(
|
676
|
+
f'[error]To allow for a better experience for users that clone your repository, please update rbx.cp to the latest major version using [item]{utils.get_upgrade_command(latest_version)}[/item].[/error]'
|
677
|
+
)
|
678
|
+
if not questionary.confirm(
|
679
|
+
'If you want to proceed anyway, press [y]', default=False
|
680
|
+
).ask():
|
681
|
+
raise typer.Exit(1)
|
682
|
+
|
617
683
|
console.console.print(
|
618
684
|
f'Installing preset [item]{fetch_info.name}[/item] from resources...'
|
619
685
|
)
|
620
|
-
|
686
|
+
install_preset_from_dir(
|
621
687
|
rsrc_preset_path,
|
622
688
|
dest,
|
623
689
|
ensure_contest,
|
624
690
|
ensure_problem,
|
625
|
-
override_uri=
|
691
|
+
override_uri=preset_uri,
|
626
692
|
update=update,
|
627
693
|
)
|
628
694
|
return True
|
@@ -653,15 +719,14 @@ def _install_preset_from_fetch_info(
|
|
653
719
|
update=update,
|
654
720
|
)
|
655
721
|
return
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
# return
|
722
|
+
if _install_preset_from_resources(
|
723
|
+
fetch_info,
|
724
|
+
dest,
|
725
|
+
ensure_contest=ensure_contest,
|
726
|
+
ensure_problem=ensure_problem,
|
727
|
+
update=update,
|
728
|
+
):
|
729
|
+
return
|
665
730
|
console.console.print(
|
666
731
|
f'[error]Preset [item]{fetch_info.name}[/item] not found.[/error]'
|
667
732
|
)
|
@@ -692,7 +757,7 @@ def _install_package_from_preset(
|
|
692
757
|
):
|
693
758
|
if not file.is_file():
|
694
759
|
continue
|
695
|
-
|
760
|
+
copy_preset_file(
|
696
761
|
file,
|
697
762
|
dest_pkg / file.relative_to(preset_package_path),
|
698
763
|
preset_package_path,
|
@@ -702,7 +767,7 @@ def _install_package_from_preset(
|
|
702
767
|
for asset in tracked_assets:
|
703
768
|
if not asset.symlink:
|
704
769
|
continue
|
705
|
-
|
770
|
+
copy_preset_file(
|
706
771
|
preset_package_path / asset.path,
|
707
772
|
dest_pkg / asset.path,
|
708
773
|
preset_package_path,
|
@@ -721,7 +786,7 @@ def install_contest(
|
|
721
786
|
ensure_contest=True,
|
722
787
|
)
|
723
788
|
preset = get_active_preset(dest_pkg)
|
724
|
-
preset_path =
|
789
|
+
preset_path = find_local_preset(dest_pkg)
|
725
790
|
assert preset_path is not None
|
726
791
|
if preset.contest is None:
|
727
792
|
console.console.print(
|
@@ -735,7 +800,7 @@ def install_contest(
|
|
735
800
|
_install_package_from_preset(
|
736
801
|
preset_path, preset.contest, dest_pkg, preset.tracking.contest
|
737
802
|
)
|
738
|
-
|
803
|
+
clean_copied_contest_dir(dest_pkg, delete_local_rbx=False)
|
739
804
|
|
740
805
|
|
741
806
|
def install_problem(
|
@@ -748,7 +813,7 @@ def install_problem(
|
|
748
813
|
ensure_problem=True,
|
749
814
|
)
|
750
815
|
preset = get_active_preset(dest_pkg)
|
751
|
-
preset_path =
|
816
|
+
preset_path = find_local_preset(dest_pkg)
|
752
817
|
assert preset_path is not None
|
753
818
|
if preset.problem is None:
|
754
819
|
console.console.print(
|
@@ -762,7 +827,7 @@ def install_problem(
|
|
762
827
|
_install_package_from_preset(
|
763
828
|
preset_path, preset.problem, dest_pkg, preset.tracking.problem
|
764
829
|
)
|
765
|
-
|
830
|
+
clean_copied_problem_dir(dest_pkg)
|
766
831
|
|
767
832
|
|
768
833
|
def install_preset(
|
@@ -774,7 +839,7 @@ def install_preset(
|
|
774
839
|
)
|
775
840
|
raise typer.Exit(1)
|
776
841
|
if fetch_info is None:
|
777
|
-
|
842
|
+
install_preset_from_dir(get_active_preset_path(), dest_pkg)
|
778
843
|
else:
|
779
844
|
_install_preset_from_fetch_info(fetch_info, dest_pkg)
|
780
845
|
|
@@ -792,10 +857,10 @@ def get_ruyaml(root: pathlib.Path = pathlib.Path()) -> Tuple[ruyaml.YAML, ruyaml
|
|
792
857
|
def generate_lock(root: pathlib.Path = pathlib.Path()):
|
793
858
|
preset = get_active_preset(root)
|
794
859
|
|
795
|
-
tracked_assets =
|
860
|
+
tracked_assets = get_preset_tracked_assets(root, is_contest=is_contest(root))
|
796
861
|
preset_lock = PresetLock(
|
797
862
|
name=preset.name,
|
798
|
-
assets=
|
863
|
+
assets=build_package_locked_assets(tracked_assets, root),
|
799
864
|
)
|
800
865
|
|
801
866
|
(root / '.preset-lock.yml').write_text(utils.model_to_yaml(preset_lock))
|
@@ -805,7 +870,7 @@ def generate_lock(root: pathlib.Path = pathlib.Path()):
|
|
805
870
|
|
806
871
|
|
807
872
|
def _sync(try_update: bool = False, force: bool = False, symlinks: bool = False):
|
808
|
-
preset_lock =
|
873
|
+
preset_lock = get_preset_lock()
|
809
874
|
if preset_lock is None:
|
810
875
|
console.console.print(
|
811
876
|
'[error]Package does not have a [item].preset.lock.yml[/item] file and thus cannot be synced.[/error]'
|
@@ -820,7 +885,7 @@ def _sync(try_update: bool = False, force: bool = False, symlinks: bool = False)
|
|
820
885
|
|
821
886
|
_copy_updated_assets(
|
822
887
|
preset_lock,
|
823
|
-
is_contest=
|
888
|
+
is_contest=is_contest(),
|
824
889
|
force=force,
|
825
890
|
symlinks=symlinks,
|
826
891
|
)
|
@@ -969,7 +1034,7 @@ def update():
|
|
969
1034
|
).ask():
|
970
1035
|
return
|
971
1036
|
|
972
|
-
preset_path =
|
1037
|
+
preset_path = find_local_preset(pathlib.Path.cwd())
|
973
1038
|
assert preset_path is not None
|
974
1039
|
_install_preset_from_fetch_info(preset.fetch_info, dest=preset_path, update=True)
|
975
1040
|
console.console.print(
|
@@ -1008,7 +1073,7 @@ def sync(
|
|
1008
1073
|
),
|
1009
1074
|
] = False,
|
1010
1075
|
):
|
1011
|
-
|
1076
|
+
check_is_valid_package()
|
1012
1077
|
_sync(try_update=update, force=force, symlinks=symlinks)
|
1013
1078
|
|
1014
1079
|
|
@@ -1019,7 +1084,7 @@ def sync(
|
|
1019
1084
|
)
|
1020
1085
|
@cd.within_closest_package
|
1021
1086
|
def lock():
|
1022
|
-
|
1087
|
+
check_is_valid_package()
|
1023
1088
|
generate_lock()
|
1024
1089
|
|
1025
1090
|
|
@@ -1027,7 +1092,7 @@ def lock():
|
|
1027
1092
|
@cd.within_closest_package
|
1028
1093
|
def ls():
|
1029
1094
|
preset = get_active_preset()
|
1030
|
-
preset_path =
|
1095
|
+
preset_path = find_local_preset(pathlib.Path.cwd())
|
1031
1096
|
console.console.print(f'Preset: [item]{preset.name}[/item]')
|
1032
1097
|
console.console.print(f'Path: {preset_path}')
|
1033
1098
|
console.console.print(f'URI: {preset.uri}')
|
rbx/box/presets/fetch.py
CHANGED
@@ -25,6 +25,14 @@ class PresetFetchInfo(BaseModel):
|
|
25
25
|
return bool(self.inner_dir) and not self.is_remote()
|
26
26
|
|
27
27
|
|
28
|
+
def get_inner_dir_from_tool_preset(tool_preset: str) -> str:
|
29
|
+
return f'rbx/resources/presets/{tool_preset}'
|
30
|
+
|
31
|
+
|
32
|
+
def get_remote_uri_from_tool_preset(tool_preset: str) -> str:
|
33
|
+
return f'rsalesc/rbx/{get_inner_dir_from_tool_preset(tool_preset)}'
|
34
|
+
|
35
|
+
|
28
36
|
def get_preset_fetch_info(uri: Optional[str]) -> Optional[PresetFetchInfo]:
|
29
37
|
if uri is None:
|
30
38
|
return None
|
@@ -64,7 +72,7 @@ def get_preset_fetch_info(uri: Optional[str]) -> Optional[PresetFetchInfo]:
|
|
64
72
|
return None
|
65
73
|
return PresetFetchInfo(name=path.name, inner_dir=str(path))
|
66
74
|
|
67
|
-
def
|
75
|
+
def get_tool_fetch_info(s: str) -> Optional[PresetFetchInfo]:
|
68
76
|
pattern = r'[\w\-]+'
|
69
77
|
compiled = re.compile(pattern)
|
70
78
|
match = compiled.match(s)
|
@@ -76,7 +84,7 @@ def get_preset_fetch_info(uri: Optional[str]) -> Optional[PresetFetchInfo]:
|
|
76
84
|
get_github_fetch_info,
|
77
85
|
get_short_github_fetch_info,
|
78
86
|
get_local_dir_fetch_info,
|
79
|
-
|
87
|
+
get_tool_fetch_info,
|
80
88
|
]
|
81
89
|
|
82
90
|
for extract in extractors:
|
rbx/box/presets/schema.py
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
import pathlib
|
2
2
|
from typing import List, Optional
|
3
3
|
|
4
|
+
import semver
|
4
5
|
import typer
|
5
|
-
from pydantic import BaseModel, Field
|
6
|
+
from pydantic import BaseModel, Field, field_validator
|
6
7
|
|
7
8
|
from rbx import console
|
8
9
|
from rbx.box.presets.fetch import PresetFetchInfo, get_preset_fetch_info
|
@@ -42,6 +43,9 @@ class Preset(BaseModel):
|
|
42
43
|
# Should usually be a GitHub repository.
|
43
44
|
uri: str
|
44
45
|
|
46
|
+
# Minimum version of rbx.cp required to use this preset.
|
47
|
+
min_version: str = '0.14.0'
|
48
|
+
|
45
49
|
# Path to the environment file that will be installed with this preset.
|
46
50
|
# When copied to the box environment, the environment will be named `name`.
|
47
51
|
env: Optional[pathlib.Path] = None
|
@@ -57,6 +61,17 @@ class Preset(BaseModel):
|
|
57
61
|
# package changes in the preset, or when a latex template is changed.
|
58
62
|
tracking: Tracking = Field(default_factory=Tracking)
|
59
63
|
|
64
|
+
@field_validator('min_version')
|
65
|
+
@classmethod
|
66
|
+
def validate_min_version(cls, value: str) -> str:
|
67
|
+
try:
|
68
|
+
semver.Version.parse(value)
|
69
|
+
except ValueError as err:
|
70
|
+
raise ValueError(
|
71
|
+
"min_version must be a valid SemVer string (e.g., '1.2.3' or '1.2.3-rc.1')"
|
72
|
+
) from err
|
73
|
+
return value
|
74
|
+
|
60
75
|
@property
|
61
76
|
def fetch_info(self) -> PresetFetchInfo:
|
62
77
|
res = get_preset_fetch_info(self.uri)
|