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.
Files changed (164) hide show
  1. rbx/__init__.py +0 -0
  2. rbx/annotations.py +127 -0
  3. rbx/autoenum.py +333 -0
  4. rbx/box/__init__.py +0 -0
  5. rbx/box/builder.py +77 -0
  6. rbx/box/cd.py +37 -0
  7. rbx/box/checkers.py +134 -0
  8. rbx/box/code.py +185 -0
  9. rbx/box/compile.py +56 -0
  10. rbx/box/conftest.py +42 -0
  11. rbx/box/contest/__init__.py +0 -0
  12. rbx/box/contest/build_contest_statements.py +347 -0
  13. rbx/box/contest/contest_package.py +76 -0
  14. rbx/box/contest/contest_utils.py +20 -0
  15. rbx/box/contest/main.py +179 -0
  16. rbx/box/contest/schema.py +155 -0
  17. rbx/box/contest/statements.py +82 -0
  18. rbx/box/creation.py +72 -0
  19. rbx/box/download.py +64 -0
  20. rbx/box/environment.py +345 -0
  21. rbx/box/extensions.py +26 -0
  22. rbx/box/generators.py +478 -0
  23. rbx/box/generators_test.py +63 -0
  24. rbx/box/main.py +449 -0
  25. rbx/box/package.py +316 -0
  26. rbx/box/packaging/boca/extension.py +27 -0
  27. rbx/box/packaging/boca/packager.py +245 -0
  28. rbx/box/packaging/contest_main.py +82 -0
  29. rbx/box/packaging/main.py +68 -0
  30. rbx/box/packaging/packager.py +117 -0
  31. rbx/box/packaging/polygon/packager.py +320 -0
  32. rbx/box/packaging/polygon/test.py +81 -0
  33. rbx/box/packaging/polygon/xml_schema.py +106 -0
  34. rbx/box/presets/__init__.py +503 -0
  35. rbx/box/presets/fetch.py +70 -0
  36. rbx/box/presets/lock_schema.py +20 -0
  37. rbx/box/presets/schema.py +59 -0
  38. rbx/box/schema.py +394 -0
  39. rbx/box/solutions.py +792 -0
  40. rbx/box/solutions_test.py +41 -0
  41. rbx/box/statements/__init__.py +0 -0
  42. rbx/box/statements/build_statements.py +359 -0
  43. rbx/box/statements/builders.py +375 -0
  44. rbx/box/statements/joiners.py +113 -0
  45. rbx/box/statements/latex.py +47 -0
  46. rbx/box/statements/latex_jinja.py +214 -0
  47. rbx/box/statements/schema.py +138 -0
  48. rbx/box/stresses.py +292 -0
  49. rbx/box/stressing/__init__.py +0 -0
  50. rbx/box/stressing/finder_parser.py +359 -0
  51. rbx/box/stressing/generator_parser.py +258 -0
  52. rbx/box/testcases.py +54 -0
  53. rbx/box/ui/__init__.py +0 -0
  54. rbx/box/ui/captured_log.py +372 -0
  55. rbx/box/ui/css/app.tcss +48 -0
  56. rbx/box/ui/main.py +38 -0
  57. rbx/box/ui/run.py +209 -0
  58. rbx/box/validators.py +245 -0
  59. rbx/box/validators_test.py +15 -0
  60. rbx/checker.py +128 -0
  61. rbx/clone.py +197 -0
  62. rbx/config.py +271 -0
  63. rbx/conftest.py +38 -0
  64. rbx/console.py +27 -0
  65. rbx/create.py +37 -0
  66. rbx/edit.py +24 -0
  67. rbx/grading/__init__.py +0 -0
  68. rbx/grading/caching.py +356 -0
  69. rbx/grading/conftest.py +33 -0
  70. rbx/grading/judge/__init__.py +0 -0
  71. rbx/grading/judge/cacher.py +503 -0
  72. rbx/grading/judge/digester.py +35 -0
  73. rbx/grading/judge/sandbox.py +748 -0
  74. rbx/grading/judge/sandboxes/__init__.py +0 -0
  75. rbx/grading/judge/sandboxes/isolate.py +683 -0
  76. rbx/grading/judge/sandboxes/stupid_sandbox.py +310 -0
  77. rbx/grading/judge/sandboxes/timeit.py +217 -0
  78. rbx/grading/judge/storage.py +284 -0
  79. rbx/grading/judge/test.py +38 -0
  80. rbx/grading/judge/testiso.py +54 -0
  81. rbx/grading/steps.py +522 -0
  82. rbx/grading/steps_with_caching.py +59 -0
  83. rbx/grading/steps_with_caching_run_test.py +429 -0
  84. rbx/grading_utils.py +148 -0
  85. rbx/hydration.py +101 -0
  86. rbx/main.py +122 -0
  87. rbx/metadata.py +105 -0
  88. rbx/providers/__init__.py +43 -0
  89. rbx/providers/codeforces.py +73 -0
  90. rbx/providers/provider.py +26 -0
  91. rbx/resources/checkers/boilerplate.cpp +20 -0
  92. rbx/resources/default_config.json +48 -0
  93. rbx/resources/envs/default.rbx.yml +37 -0
  94. rbx/resources/envs/isolate.rbx.yml +37 -0
  95. rbx/resources/packagers/boca/checker.sh +43 -0
  96. rbx/resources/packagers/boca/compare +53 -0
  97. rbx/resources/packagers/boca/compile/c +172 -0
  98. rbx/resources/packagers/boca/compile/cc +173 -0
  99. rbx/resources/packagers/boca/compile/cpp +172 -0
  100. rbx/resources/packagers/boca/compile/java +194 -0
  101. rbx/resources/packagers/boca/compile/kt +155 -0
  102. rbx/resources/packagers/boca/compile/pas +172 -0
  103. rbx/resources/packagers/boca/compile/py2 +173 -0
  104. rbx/resources/packagers/boca/compile/py3 +173 -0
  105. rbx/resources/packagers/boca/run/c +128 -0
  106. rbx/resources/packagers/boca/run/cc +128 -0
  107. rbx/resources/packagers/boca/run/cpp +128 -0
  108. rbx/resources/packagers/boca/run/java +194 -0
  109. rbx/resources/packagers/boca/run/kt +159 -0
  110. rbx/resources/packagers/boca/run/py2 +166 -0
  111. rbx/resources/packagers/boca/run/py3 +166 -0
  112. rbx/resources/presets/default/contest/contest.rbx.yml +14 -0
  113. rbx/resources/presets/default/contest/statement/contest.rbx.tex +97 -0
  114. rbx/resources/presets/default/contest/statement/olymp.sty +250 -0
  115. rbx/resources/presets/default/contest/statement/template.rbx.tex +42 -0
  116. rbx/resources/presets/default/preset.rbx.yml +12 -0
  117. rbx/resources/presets/default/problem/.gitignore +6 -0
  118. rbx/resources/presets/default/problem/gen.cpp +9 -0
  119. rbx/resources/presets/default/problem/problem.rbx.yml +44 -0
  120. rbx/resources/presets/default/problem/random.py +3 -0
  121. rbx/resources/presets/default/problem/random.txt +2 -0
  122. rbx/resources/presets/default/problem/sols/main.cpp +9 -0
  123. rbx/resources/presets/default/problem/sols/slow.cpp +15 -0
  124. rbx/resources/presets/default/problem/sols/wa.cpp +9 -0
  125. rbx/resources/presets/default/problem/statement/olymp.sty +250 -0
  126. rbx/resources/presets/default/problem/statement/projecao.png +0 -0
  127. rbx/resources/presets/default/problem/statement/statement.rbx.tex +18 -0
  128. rbx/resources/presets/default/problem/statement/template.rbx.tex +89 -0
  129. rbx/resources/presets/default/problem/tests/samples/000.in +1 -0
  130. rbx/resources/presets/default/problem/tests/samples/001.in +1 -0
  131. rbx/resources/presets/default/problem/validator.cpp +16 -0
  132. rbx/resources/presets/default/problem/wcmp.cpp +34 -0
  133. rbx/resources/templates/template.cpp +19 -0
  134. rbx/run.py +45 -0
  135. rbx/schema.py +64 -0
  136. rbx/submit.py +61 -0
  137. rbx/submitors/__init__.py +18 -0
  138. rbx/submitors/codeforces.py +120 -0
  139. rbx/submitors/submitor.py +25 -0
  140. rbx/test.py +347 -0
  141. rbx/testcase.py +70 -0
  142. rbx/testcase_rendering.py +79 -0
  143. rbx/testdata/box1/gen1.cpp +7 -0
  144. rbx/testdata/box1/gen2.cpp +9 -0
  145. rbx/testdata/box1/genScript.py +2 -0
  146. rbx/testdata/box1/hard-tle.sol.cpp +26 -0
  147. rbx/testdata/box1/ole.cpp +17 -0
  148. rbx/testdata/box1/problem.rbx.yml +39 -0
  149. rbx/testdata/box1/re.sol.cpp +23 -0
  150. rbx/testdata/box1/sol.cpp +22 -0
  151. rbx/testdata/box1/tests/1.in +1 -0
  152. rbx/testdata/box1/tle-and-incorrect.sol.cpp +33 -0
  153. rbx/testdata/box1/tle.sol.cpp +35 -0
  154. rbx/testdata/box1/validator.cpp +11 -0
  155. rbx/testdata/box1/wa.sol.cpp +22 -0
  156. rbx/testdata/caching/executable.py +1 -0
  157. rbx/testdata/compatible +0 -0
  158. rbx/testing_utils.py +65 -0
  159. rbx/utils.py +162 -0
  160. rbx_cp-0.5.0.dist-info/LICENSE +201 -0
  161. rbx_cp-0.5.0.dist-info/METADATA +89 -0
  162. rbx_cp-0.5.0.dist-info/RECORD +164 -0
  163. rbx_cp-0.5.0.dist-info/WHEEL +4 -0
  164. 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
@@ -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