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
@@ -0,0 +1,503 @@
|
|
1
|
+
import pathlib
|
2
|
+
import shutil
|
3
|
+
import tempfile
|
4
|
+
from typing import Annotated, Iterable, List, Optional, Sequence, Union
|
5
|
+
|
6
|
+
import git
|
7
|
+
import rich
|
8
|
+
import rich.prompt
|
9
|
+
import typer
|
10
|
+
from iso639.language import functools
|
11
|
+
|
12
|
+
from rbx import console, utils
|
13
|
+
from rbx.box import cd
|
14
|
+
from rbx.box.environment import get_environment_path
|
15
|
+
from rbx.box.presets.fetch import PresetFetchInfo, get_preset_fetch_info
|
16
|
+
from rbx.box.presets.lock_schema import LockedAsset, PresetLock
|
17
|
+
from rbx.box.presets.schema import Preset, TrackedAsset
|
18
|
+
from rbx.config import get_default_app_path
|
19
|
+
from rbx.grading.judge.digester import digest_cooperatively
|
20
|
+
|
21
|
+
app = typer.Typer(no_args_is_help=True)
|
22
|
+
|
23
|
+
LOCAL = 'local'
|
24
|
+
|
25
|
+
|
26
|
+
def find_preset_yaml(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.Path]:
|
27
|
+
found = root / 'preset.rbx.yml'
|
28
|
+
if found.exists():
|
29
|
+
return found
|
30
|
+
return None
|
31
|
+
|
32
|
+
|
33
|
+
def get_preset_yaml(root: pathlib.Path = pathlib.Path()) -> Preset:
|
34
|
+
found = find_preset_yaml(root)
|
35
|
+
if not found:
|
36
|
+
console.console.print(
|
37
|
+
f'[error][item]preset.rbx.yml[/item] not found in [item]{root.absolute()}[/item].[/error]'
|
38
|
+
)
|
39
|
+
raise typer.Exit(1)
|
40
|
+
return utils.model_from_yaml(Preset, found.read_text())
|
41
|
+
|
42
|
+
|
43
|
+
def find_preset_lock(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.Path]:
|
44
|
+
root = root.resolve()
|
45
|
+
problem_yaml_path = root / '.preset-lock.yml'
|
46
|
+
if not problem_yaml_path.is_file():
|
47
|
+
return None
|
48
|
+
return problem_yaml_path
|
49
|
+
|
50
|
+
|
51
|
+
def get_preset_lock(root: pathlib.Path = pathlib.Path()) -> Optional[PresetLock]:
|
52
|
+
found = find_preset_lock(root)
|
53
|
+
if not found:
|
54
|
+
return None
|
55
|
+
return utils.model_from_yaml(PresetLock, found.read_text())
|
56
|
+
|
57
|
+
|
58
|
+
def _find_nested_preset(root: pathlib.Path) -> Optional[pathlib.Path]:
|
59
|
+
root = root.resolve()
|
60
|
+
problem_yaml_path = root / 'preset.rbx.yml'
|
61
|
+
while root != pathlib.PosixPath('/') and not problem_yaml_path.is_file():
|
62
|
+
root = root.parent
|
63
|
+
problem_yaml_path = root / 'preset.rbx.yml'
|
64
|
+
if not problem_yaml_path.is_file():
|
65
|
+
return None
|
66
|
+
return problem_yaml_path.parent
|
67
|
+
|
68
|
+
|
69
|
+
def _find_local_preset(root: pathlib.Path) -> Optional[pathlib.Path]:
|
70
|
+
root = root.resolve()
|
71
|
+
problem_yaml_path = root / '.local.rbx' / 'preset.rbx.yml'
|
72
|
+
while root != pathlib.PosixPath('/') and not problem_yaml_path.is_file():
|
73
|
+
root = root.parent
|
74
|
+
problem_yaml_path = root / '.local.rbx' / 'preset.rbx.yml'
|
75
|
+
if not problem_yaml_path.is_file():
|
76
|
+
return _find_nested_preset(root)
|
77
|
+
return problem_yaml_path.parent
|
78
|
+
|
79
|
+
|
80
|
+
def get_preset_installation_path(
|
81
|
+
name: str, root: pathlib.Path = pathlib.Path()
|
82
|
+
) -> pathlib.Path:
|
83
|
+
if name == LOCAL:
|
84
|
+
local_path = _find_local_preset(root)
|
85
|
+
if local_path is None:
|
86
|
+
console.console.print('[error]Local preset was not found.[/error]')
|
87
|
+
raise typer.Exit(1)
|
88
|
+
return local_path
|
89
|
+
nested_preset_path = _find_nested_preset(root)
|
90
|
+
if nested_preset_path is not None:
|
91
|
+
nested_preset = utils.model_from_yaml(
|
92
|
+
Preset, (nested_preset_path / 'preset.rbx.yml').read_text()
|
93
|
+
)
|
94
|
+
if nested_preset.name == name:
|
95
|
+
return nested_preset_path
|
96
|
+
return utils.get_app_path() / 'presets' / name
|
97
|
+
|
98
|
+
|
99
|
+
def _find_installed_presets() -> List[str]:
|
100
|
+
folder = utils.get_app_path() / 'presets'
|
101
|
+
|
102
|
+
res = []
|
103
|
+
if get_preset_installation_path('local'):
|
104
|
+
res.append('local')
|
105
|
+
for yml in folder.glob('*/preset.rbx.yml'):
|
106
|
+
res.append(yml.parent.name)
|
107
|
+
return res
|
108
|
+
|
109
|
+
|
110
|
+
def _is_contest(root: pathlib.Path = pathlib.Path()) -> bool:
|
111
|
+
return (root / 'contest.rbx.yml').is_file()
|
112
|
+
|
113
|
+
|
114
|
+
def _is_problem(root: pathlib.Path = pathlib.Path()) -> bool:
|
115
|
+
return (root / 'problem.rbx.yml').is_file()
|
116
|
+
|
117
|
+
|
118
|
+
def _check_is_valid_package(root: pathlib.Path = pathlib.Path()):
|
119
|
+
if not _is_contest(root) and not _is_problem(root):
|
120
|
+
console.console.print('[error]Not a valid rbx package directory.[/error]')
|
121
|
+
raise typer.Exit(1)
|
122
|
+
|
123
|
+
|
124
|
+
def _get_preset_package_path(
|
125
|
+
name: str, is_contest: bool, root: pathlib.Path = pathlib.Path()
|
126
|
+
) -> pathlib.Path:
|
127
|
+
preset_path = get_preset_installation_path(name, root)
|
128
|
+
preset = get_installed_preset(name, root)
|
129
|
+
|
130
|
+
if is_contest:
|
131
|
+
assert (
|
132
|
+
preset.contest is not None
|
133
|
+
), 'Preset does not have a contest package definition.'
|
134
|
+
return preset_path / preset.contest
|
135
|
+
|
136
|
+
assert (
|
137
|
+
preset.problem is not None
|
138
|
+
), 'Preset does not have a problem package definition,'
|
139
|
+
return preset_path / preset.problem
|
140
|
+
|
141
|
+
|
142
|
+
def _process_globbing(
|
143
|
+
assets: Iterable[TrackedAsset], preset_dir: pathlib.Path
|
144
|
+
) -> List[TrackedAsset]:
|
145
|
+
res = []
|
146
|
+
for asset in assets:
|
147
|
+
if '*' in str(asset.path):
|
148
|
+
glb = str(asset.path)
|
149
|
+
files = preset_dir.glob(glb)
|
150
|
+
relative_files = [file.relative_to(preset_dir) for file in files]
|
151
|
+
res.extend([TrackedAsset(path=path) for path in relative_files])
|
152
|
+
continue
|
153
|
+
res.append(asset)
|
154
|
+
return res
|
155
|
+
|
156
|
+
|
157
|
+
def _get_preset_tracked_assets(name: str, is_contest: bool) -> List[TrackedAsset]:
|
158
|
+
preset = get_installed_preset(name)
|
159
|
+
preset_path = get_preset_installation_path(name)
|
160
|
+
|
161
|
+
if is_contest:
|
162
|
+
assert (
|
163
|
+
preset.contest is not None
|
164
|
+
), 'Preset does not have a contest package definition.'
|
165
|
+
return _process_globbing(preset.tracking.contest, preset_path)
|
166
|
+
|
167
|
+
assert (
|
168
|
+
preset.problem is not None
|
169
|
+
), 'Preset does not have a problem package definition,'
|
170
|
+
return _process_globbing(preset.tracking.problem, preset_path)
|
171
|
+
|
172
|
+
|
173
|
+
def _build_package_locked_assets(
|
174
|
+
tracked_assets: Sequence[Union[TrackedAsset, LockedAsset]],
|
175
|
+
root: pathlib.Path = pathlib.Path(),
|
176
|
+
) -> List[LockedAsset]:
|
177
|
+
res = []
|
178
|
+
for tracked_asset in tracked_assets:
|
179
|
+
asset_path = root / tracked_asset.path
|
180
|
+
if not asset_path.is_file():
|
181
|
+
continue
|
182
|
+
with asset_path.open('rb') as f:
|
183
|
+
res.append(
|
184
|
+
LockedAsset(path=tracked_asset.path, hash=digest_cooperatively(f))
|
185
|
+
)
|
186
|
+
return res
|
187
|
+
|
188
|
+
|
189
|
+
def _find_non_modified_assets(
|
190
|
+
reference: List[LockedAsset], current: List[LockedAsset]
|
191
|
+
) -> List[LockedAsset]:
|
192
|
+
current_by_path = {asset.path: asset for asset in current}
|
193
|
+
|
194
|
+
res = []
|
195
|
+
for asset in reference:
|
196
|
+
if (
|
197
|
+
asset.path in current_by_path
|
198
|
+
and current_by_path[asset.path].hash != asset.hash
|
199
|
+
):
|
200
|
+
# This is a file that was modified.
|
201
|
+
continue
|
202
|
+
res.append(asset)
|
203
|
+
return res
|
204
|
+
|
205
|
+
|
206
|
+
def _find_modified_assets(
|
207
|
+
reference: List[LockedAsset],
|
208
|
+
current: List[LockedAsset],
|
209
|
+
):
|
210
|
+
reference_by_path = {asset.path: asset for asset in reference}
|
211
|
+
|
212
|
+
res = []
|
213
|
+
for asset in current:
|
214
|
+
if (
|
215
|
+
asset.path in reference_by_path
|
216
|
+
and reference_by_path[asset.path].hash == asset.hash
|
217
|
+
):
|
218
|
+
# This is a file that was not modified.
|
219
|
+
continue
|
220
|
+
res.append(asset)
|
221
|
+
return res
|
222
|
+
|
223
|
+
|
224
|
+
def _copy_updated_assets(
|
225
|
+
preset_name: str,
|
226
|
+
preset_lock: PresetLock,
|
227
|
+
is_contest: bool,
|
228
|
+
root: pathlib.Path = pathlib.Path(),
|
229
|
+
):
|
230
|
+
current_package_snapshot = _build_package_locked_assets(preset_lock.assets)
|
231
|
+
non_modified_assets = _find_non_modified_assets(
|
232
|
+
preset_lock.assets, current_package_snapshot
|
233
|
+
)
|
234
|
+
|
235
|
+
preset_package_path = _get_preset_package_path(preset_name, is_contest=is_contest)
|
236
|
+
preset_tracked_assets = _get_preset_tracked_assets(
|
237
|
+
preset_name, is_contest=is_contest
|
238
|
+
)
|
239
|
+
current_preset_snapshot = _build_package_locked_assets(
|
240
|
+
preset_tracked_assets, preset_package_path
|
241
|
+
)
|
242
|
+
assets_to_copy = _find_modified_assets(non_modified_assets, current_preset_snapshot)
|
243
|
+
|
244
|
+
for asset in assets_to_copy:
|
245
|
+
src_path = preset_package_path / asset.path
|
246
|
+
dst_path = root / asset.path
|
247
|
+
shutil.copyfile(str(src_path), str(dst_path))
|
248
|
+
console.console.print(
|
249
|
+
f'Updated [item]{asset.path}[/item] from preset [item]{preset_name}[/item].'
|
250
|
+
)
|
251
|
+
|
252
|
+
|
253
|
+
def _try_installing_from_resources(name: str) -> bool:
|
254
|
+
if name == LOCAL:
|
255
|
+
return False
|
256
|
+
rsrc_preset_path = get_default_app_path() / 'presets' / name
|
257
|
+
if not rsrc_preset_path.exists():
|
258
|
+
return False
|
259
|
+
yaml_path = rsrc_preset_path / 'preset.rbx.yml'
|
260
|
+
if not yaml_path.is_file():
|
261
|
+
return False
|
262
|
+
console.console.print(f'Installing preset [item]{name}[/item] from resources...')
|
263
|
+
_install(rsrc_preset_path, force=True)
|
264
|
+
return True
|
265
|
+
|
266
|
+
|
267
|
+
@functools.cache
|
268
|
+
def _install_from_resources_just_once(name: str) -> bool:
|
269
|
+
# Send all output to the void since we don't wanna be verbose here.
|
270
|
+
with console.console.capture():
|
271
|
+
return _try_installing_from_resources(name)
|
272
|
+
|
273
|
+
|
274
|
+
def get_installed_preset_or_null(
|
275
|
+
name: str, root: pathlib.Path = pathlib.Path()
|
276
|
+
) -> Optional[Preset]:
|
277
|
+
installation_path = get_preset_installation_path(name, root) / 'preset.rbx.yml'
|
278
|
+
if not installation_path.is_file():
|
279
|
+
if not _try_installing_from_resources(name):
|
280
|
+
return None
|
281
|
+
elif name == 'default':
|
282
|
+
_install_from_resources_just_once(name)
|
283
|
+
|
284
|
+
return utils.model_from_yaml(Preset, installation_path.read_text())
|
285
|
+
|
286
|
+
|
287
|
+
def get_installed_preset(name: str, root: pathlib.Path = pathlib.Path()) -> Preset:
|
288
|
+
preset = get_installed_preset_or_null(name, root)
|
289
|
+
if preset is None:
|
290
|
+
console.console.print(
|
291
|
+
f'[error]Preset [item]{name}[/item] is not installed.[/error]'
|
292
|
+
)
|
293
|
+
raise typer.Exit(1)
|
294
|
+
return preset
|
295
|
+
|
296
|
+
|
297
|
+
def _install(root: pathlib.Path = pathlib.Path(), force: bool = False):
|
298
|
+
preset = get_preset_yaml(root)
|
299
|
+
|
300
|
+
if preset.name == LOCAL:
|
301
|
+
console.console.print('[error]Naming a preset "local" is prohibited.[/error]')
|
302
|
+
|
303
|
+
console.console.print(f'Installing preset [item]{preset.name}[/item]...')
|
304
|
+
|
305
|
+
if preset.env is not None:
|
306
|
+
console.console.print(
|
307
|
+
f'[item]{preset.name}[/item]: Copying environment file...'
|
308
|
+
)
|
309
|
+
should_copy_env = True
|
310
|
+
if get_environment_path(preset.name).exists():
|
311
|
+
res = force or rich.prompt.Confirm.ask(
|
312
|
+
f'Environment [item]{preset.name}[/item] already exists. Overwrite?',
|
313
|
+
console=console.console,
|
314
|
+
)
|
315
|
+
if not res:
|
316
|
+
should_copy_env = False
|
317
|
+
|
318
|
+
if should_copy_env:
|
319
|
+
shutil.rmtree(get_environment_path(preset.name), ignore_errors=True)
|
320
|
+
shutil.copyfile(str(root / preset.env), get_environment_path(preset.name))
|
321
|
+
|
322
|
+
console.console.print(f'[item]{preset.name}[/item]: Copying preset folder...')
|
323
|
+
installation_path = get_preset_installation_path(preset.name)
|
324
|
+
installation_path.parent.mkdir(parents=True, exist_ok=True)
|
325
|
+
if installation_path.exists():
|
326
|
+
res = force or rich.prompt.Confirm.ask(
|
327
|
+
f'Preset [item]{preset.name}[/item] is already installed. Overwrite?',
|
328
|
+
console=console.console,
|
329
|
+
)
|
330
|
+
if not res:
|
331
|
+
raise typer.Exit(1)
|
332
|
+
shutil.rmtree(str(installation_path), ignore_errors=True)
|
333
|
+
shutil.copytree(str(root), str(installation_path))
|
334
|
+
shutil.rmtree(str(installation_path / 'build'), ignore_errors=True)
|
335
|
+
shutil.rmtree(str(installation_path / '.box'), ignore_errors=True)
|
336
|
+
|
337
|
+
|
338
|
+
def install_from_remote(fetch_info: PresetFetchInfo, force: bool = False) -> str:
|
339
|
+
assert fetch_info.fetch_uri is not None
|
340
|
+
with tempfile.TemporaryDirectory() as d:
|
341
|
+
console.console.print(
|
342
|
+
f'Cloning preset from [item]{fetch_info.fetch_uri}[/item]...'
|
343
|
+
)
|
344
|
+
git.Repo.clone_from(fetch_info.fetch_uri, d)
|
345
|
+
pd = pathlib.Path(d)
|
346
|
+
if fetch_info.inner_dir:
|
347
|
+
pd = pd / fetch_info.inner_dir
|
348
|
+
console.console.print(
|
349
|
+
f'Installing preset from [item]{fetch_info.inner_dir}[/item].'
|
350
|
+
)
|
351
|
+
preset = get_preset_yaml(pd)
|
352
|
+
preset.uri = fetch_info.uri
|
353
|
+
|
354
|
+
(pd / 'preset.rbx.yml').write_text(utils.model_to_yaml(preset))
|
355
|
+
_install(pd, force=force)
|
356
|
+
return preset.name
|
357
|
+
|
358
|
+
|
359
|
+
def generate_lock(
|
360
|
+
preset_name: Optional[str] = None, root: pathlib.Path = pathlib.Path()
|
361
|
+
):
|
362
|
+
if preset_name is None:
|
363
|
+
preset_lock = get_preset_lock(root)
|
364
|
+
if preset_lock is None:
|
365
|
+
console.console.print(
|
366
|
+
'[error][item].preset-lock.yml[/item] not found. '
|
367
|
+
'Specify a preset argument to this function to create a lock from scratch.[/error]'
|
368
|
+
)
|
369
|
+
raise typer.Exit(1)
|
370
|
+
preset_name = preset_lock.preset_name
|
371
|
+
|
372
|
+
preset = get_installed_preset(preset_name, root)
|
373
|
+
|
374
|
+
tracked_assets = _get_preset_tracked_assets(preset_name, is_contest=_is_contest())
|
375
|
+
preset_lock = PresetLock(
|
376
|
+
name=preset.name if preset_name != LOCAL else LOCAL,
|
377
|
+
uri=preset.uri,
|
378
|
+
assets=_build_package_locked_assets(tracked_assets, root),
|
379
|
+
)
|
380
|
+
|
381
|
+
(root / '.preset-lock.yml').write_text(utils.model_to_yaml(preset_lock))
|
382
|
+
console.console.print(
|
383
|
+
'[success][item].preset-lock.yml[/item] was created.[/success]'
|
384
|
+
)
|
385
|
+
|
386
|
+
|
387
|
+
def _sync(update: bool = False):
|
388
|
+
preset_lock = get_preset_lock()
|
389
|
+
if preset_lock is None:
|
390
|
+
console.console.print(
|
391
|
+
'[error]Package does not have a [item].preset.lock.yml[/item] file and thus cannot be synced.[/error]'
|
392
|
+
)
|
393
|
+
console.console.print(
|
394
|
+
'[error]Ensure this package was created through a preset, or manually associate one with [item]rbx presets lock [PRESET][/item].[/error]'
|
395
|
+
)
|
396
|
+
raise typer.Exit(1)
|
397
|
+
|
398
|
+
should_update = update and preset_lock.uri is not None
|
399
|
+
installed_preset = get_installed_preset_or_null(preset_lock.preset_name)
|
400
|
+
if installed_preset is None:
|
401
|
+
if not update or preset_lock.uri is None:
|
402
|
+
console.console.print(
|
403
|
+
f'[error]Preset [item]{preset_lock.preset_name}[/item] is not installed. Install it before trying to update.'
|
404
|
+
)
|
405
|
+
raise typer.Exit(1)
|
406
|
+
should_update = True
|
407
|
+
|
408
|
+
if should_update:
|
409
|
+
install(uri=preset_lock.uri)
|
410
|
+
|
411
|
+
_copy_updated_assets(
|
412
|
+
preset_lock.preset_name,
|
413
|
+
preset_lock,
|
414
|
+
is_contest=_is_contest(),
|
415
|
+
)
|
416
|
+
generate_lock(preset_lock.preset_name)
|
417
|
+
|
418
|
+
|
419
|
+
@app.command(
|
420
|
+
'install', help='Install preset from current directory or from the given URI.'
|
421
|
+
)
|
422
|
+
def install(
|
423
|
+
uri: Optional[str] = typer.Argument(
|
424
|
+
None, help='GitHub URI for the preset to install.'
|
425
|
+
),
|
426
|
+
):
|
427
|
+
if uri is None:
|
428
|
+
_install()
|
429
|
+
return
|
430
|
+
|
431
|
+
fetch_info = get_preset_fetch_info(uri)
|
432
|
+
if fetch_info is None:
|
433
|
+
console.console.print(f'[error] Preset with URI {uri} not found.[/error]')
|
434
|
+
raise typer.Exit(1)
|
435
|
+
if fetch_info.fetch_uri is None:
|
436
|
+
console.console.print(f'[error]URI {uri} is invalid.[/error]')
|
437
|
+
install_from_remote(fetch_info)
|
438
|
+
|
439
|
+
|
440
|
+
@app.command('update', help='Update installed remote presets')
|
441
|
+
def update(
|
442
|
+
name: Annotated[
|
443
|
+
Optional[str], typer.Argument(help='If set, update only this preset.')
|
444
|
+
] = None,
|
445
|
+
):
|
446
|
+
if not name:
|
447
|
+
presets = _find_installed_presets()
|
448
|
+
else:
|
449
|
+
presets = [name]
|
450
|
+
|
451
|
+
for preset_name in presets:
|
452
|
+
preset = get_installed_preset_or_null(preset_name)
|
453
|
+
if preset is None:
|
454
|
+
console.console.print(
|
455
|
+
f'[error]Preset [item]{preset_name}[/item] is not installed.'
|
456
|
+
)
|
457
|
+
continue
|
458
|
+
if preset.uri is None:
|
459
|
+
console.console.print(
|
460
|
+
f'Skipping preset [item]{preset_name}[/item], not remote.'
|
461
|
+
)
|
462
|
+
continue
|
463
|
+
install_from_remote(preset.fetch_info, force=True)
|
464
|
+
|
465
|
+
|
466
|
+
@app.command(
|
467
|
+
'sync',
|
468
|
+
help='Sync current package assets with those provided by the installed preset.',
|
469
|
+
)
|
470
|
+
@cd.within_closest_package
|
471
|
+
def sync(
|
472
|
+
update: Annotated[
|
473
|
+
bool,
|
474
|
+
typer.Option(
|
475
|
+
'--update',
|
476
|
+
'-u',
|
477
|
+
help='Whether to fetch an up-to-date version of the installed preset from remote, if available.',
|
478
|
+
),
|
479
|
+
] = False,
|
480
|
+
):
|
481
|
+
_check_is_valid_package()
|
482
|
+
_sync(update=update)
|
483
|
+
|
484
|
+
|
485
|
+
@app.command(
|
486
|
+
'lock', help='Generate a lock for this package, based on a existing preset.'
|
487
|
+
)
|
488
|
+
@cd.within_closest_package
|
489
|
+
def lock(
|
490
|
+
preset: Annotated[
|
491
|
+
Optional[str],
|
492
|
+
typer.Argument(
|
493
|
+
help='Preset to generate a lock for. If unset, will default to the one in the existing .preset-lock.yml.',
|
494
|
+
),
|
495
|
+
] = None,
|
496
|
+
):
|
497
|
+
_check_is_valid_package()
|
498
|
+
generate_lock(preset)
|
499
|
+
|
500
|
+
|
501
|
+
@app.callback()
|
502
|
+
def callback():
|
503
|
+
pass
|
rbx/box/presets/fetch.py
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from pydantic import BaseModel
|
5
|
+
|
6
|
+
|
7
|
+
class PresetFetchInfo(BaseModel):
|
8
|
+
# The actual name of this preset.
|
9
|
+
name: str
|
10
|
+
|
11
|
+
# The URI to associate with this preset.
|
12
|
+
uri: Optional[str] = None
|
13
|
+
|
14
|
+
# The actual URI from where to fetch the repo.
|
15
|
+
fetch_uri: Optional[str] = None
|
16
|
+
|
17
|
+
# Inner directory from where to pull the preset.
|
18
|
+
inner_dir: str = ''
|
19
|
+
|
20
|
+
|
21
|
+
def get_preset_fetch_info(uri: Optional[str]) -> Optional[PresetFetchInfo]:
|
22
|
+
if uri is None:
|
23
|
+
return None
|
24
|
+
|
25
|
+
def get_github_fetch_info(s: str) -> Optional[PresetFetchInfo]:
|
26
|
+
pattern = r'(https:\/\/(?:[\w\-]+\.)?github\.com\/([\w\-]+\/[\w\.\-]+))(?:\.git)?(?:\/(.*))?'
|
27
|
+
compiled = re.compile(pattern)
|
28
|
+
match = compiled.match(s)
|
29
|
+
if match is None:
|
30
|
+
return None
|
31
|
+
return PresetFetchInfo(
|
32
|
+
name=match.group(2),
|
33
|
+
uri=match.group(0),
|
34
|
+
fetch_uri=match.group(1),
|
35
|
+
inner_dir=match.group(3) or '',
|
36
|
+
)
|
37
|
+
|
38
|
+
def get_short_github_fetch_info(s: str) -> Optional[PresetFetchInfo]:
|
39
|
+
pattern = r'([\w\-]+\/[\w\.\-]+)(?:\/(.*))?'
|
40
|
+
compiled = re.compile(pattern)
|
41
|
+
match = compiled.match(s)
|
42
|
+
if match is None:
|
43
|
+
return None
|
44
|
+
return PresetFetchInfo(
|
45
|
+
name=match.group(1),
|
46
|
+
uri=match.group(0),
|
47
|
+
fetch_uri=f'https://github.com/{match.group(1)}',
|
48
|
+
inner_dir=match.group(2) or '',
|
49
|
+
)
|
50
|
+
|
51
|
+
def get_local_fetch_info(s: str) -> Optional[PresetFetchInfo]:
|
52
|
+
pattern = r'[\w\-]+'
|
53
|
+
compiled = re.compile(pattern)
|
54
|
+
match = compiled.match(s)
|
55
|
+
if match is None:
|
56
|
+
return None
|
57
|
+
return PresetFetchInfo(name=s)
|
58
|
+
|
59
|
+
extractors = [
|
60
|
+
get_github_fetch_info,
|
61
|
+
get_short_github_fetch_info,
|
62
|
+
get_local_fetch_info,
|
63
|
+
]
|
64
|
+
|
65
|
+
for extract in extractors:
|
66
|
+
res = extract(uri)
|
67
|
+
if res is not None:
|
68
|
+
return res
|
69
|
+
|
70
|
+
return None
|
@@ -0,0 +1,20 @@
|
|
1
|
+
from typing import List, Optional
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
5
|
+
from rbx.box.presets.schema import TrackedAsset
|
6
|
+
|
7
|
+
|
8
|
+
class LockedAsset(TrackedAsset):
|
9
|
+
hash: str
|
10
|
+
|
11
|
+
|
12
|
+
class PresetLock(BaseModel):
|
13
|
+
name: str
|
14
|
+
uri: Optional[str] = None
|
15
|
+
|
16
|
+
@property
|
17
|
+
def preset_name(self) -> str:
|
18
|
+
return self.name
|
19
|
+
|
20
|
+
assets: List[LockedAsset] = []
|
@@ -0,0 +1,59 @@
|
|
1
|
+
import pathlib
|
2
|
+
from typing import List, Optional
|
3
|
+
|
4
|
+
from pydantic import BaseModel, Field
|
5
|
+
|
6
|
+
from rbx.box.presets.fetch import PresetFetchInfo, get_preset_fetch_info
|
7
|
+
|
8
|
+
|
9
|
+
def NameField(**kwargs):
|
10
|
+
return Field(
|
11
|
+
pattern=r'^[a-zA-Z0-9][a-zA-Z0-9\-]*$', min_length=3, max_length=32, **kwargs
|
12
|
+
)
|
13
|
+
|
14
|
+
|
15
|
+
class TrackedAsset(BaseModel):
|
16
|
+
# Path of the asset relative to the root of the problem/contest that should
|
17
|
+
# be tracked. Can also be a glob, when specified in the preset config.
|
18
|
+
path: pathlib.Path
|
19
|
+
|
20
|
+
|
21
|
+
class Tracking(BaseModel):
|
22
|
+
# Problem assets that should be tracked and updated by rbx
|
23
|
+
# when the preset has an update.
|
24
|
+
problem: List[TrackedAsset] = []
|
25
|
+
|
26
|
+
# Contest assets that should be tracked and updated by rbx
|
27
|
+
# when the preset has an update.
|
28
|
+
contest: List[TrackedAsset] = []
|
29
|
+
|
30
|
+
|
31
|
+
class Preset(BaseModel):
|
32
|
+
# Name of the preset, or a GitHub repository containing it.
|
33
|
+
name: str = NameField()
|
34
|
+
|
35
|
+
# URI of the preset to be fetched.
|
36
|
+
uri: Optional[str] = None
|
37
|
+
|
38
|
+
# Path to the environment file that will be installed with this preset.
|
39
|
+
# When copied to the box environment, the environment will be named `name`.
|
40
|
+
env: Optional[pathlib.Path] = None
|
41
|
+
|
42
|
+
# Path to the contest preset directory, relative to the preset directory.
|
43
|
+
problem: Optional[pathlib.Path] = None
|
44
|
+
|
45
|
+
# Path to the problem preset directory, relative to the preset directory.
|
46
|
+
contest: Optional[pathlib.Path] = None
|
47
|
+
|
48
|
+
# Configures how preset assets should be tracked and updated when the
|
49
|
+
# preset has an update. Usually useful when a common library used by the
|
50
|
+
# package changes in the preset, or when a latex template is changed.
|
51
|
+
tracking: Tracking = Field(default_factory=Tracking)
|
52
|
+
|
53
|
+
@property
|
54
|
+
def fetch_info(self) -> PresetFetchInfo:
|
55
|
+
if self.uri is None:
|
56
|
+
return PresetFetchInfo(name=self.name)
|
57
|
+
res = get_preset_fetch_info(self.uri)
|
58
|
+
assert res is not None
|
59
|
+
return res
|