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.
Files changed (77) hide show
  1. rbx/__version__.py +1 -0
  2. rbx/box/cli.py +74 -70
  3. rbx/box/code.py +3 -0
  4. rbx/box/contest/build_contest_statements.py +65 -23
  5. rbx/box/contest/contest_package.py +8 -1
  6. rbx/box/contest/main.py +9 -3
  7. rbx/box/contest/schema.py +17 -13
  8. rbx/box/contest/statements.py +12 -8
  9. rbx/box/dump_schemas.py +2 -1
  10. rbx/box/environment.py +1 -1
  11. rbx/box/fields.py +22 -4
  12. rbx/box/generators.py +32 -13
  13. rbx/box/git_utils.py +29 -1
  14. rbx/box/limits_info.py +161 -0
  15. rbx/box/package.py +18 -1
  16. rbx/box/packaging/boca/boca_language_utils.py +26 -0
  17. rbx/box/packaging/boca/boca_outcome_utils.py +10 -0
  18. rbx/box/packaging/boca/packager.py +7 -5
  19. rbx/box/packaging/contest_main.py +20 -12
  20. rbx/box/packaging/packager.py +24 -14
  21. rbx/box/packaging/polygon/packager.py +7 -3
  22. rbx/box/packaging/polygon/upload.py +2 -1
  23. rbx/box/presets/__init__.py +143 -78
  24. rbx/box/presets/fetch.py +10 -2
  25. rbx/box/presets/schema.py +16 -1
  26. rbx/box/remote.py +3 -3
  27. rbx/box/sanitizers/issue_stack.py +124 -0
  28. rbx/box/schema.py +87 -27
  29. rbx/box/solutions.py +74 -117
  30. rbx/box/statements/build_statements.py +12 -1
  31. rbx/box/statements/builders.py +5 -3
  32. rbx/box/statements/latex_jinja.py +73 -23
  33. rbx/box/statements/schema.py +7 -9
  34. rbx/box/stressing/generator_parser.py +3 -1
  35. rbx/box/tasks.py +10 -10
  36. rbx/box/testcase_extractors.py +8 -0
  37. rbx/box/testing/testing_preset.py +129 -2
  38. rbx/box/testing/testing_shared.py +3 -1
  39. rbx/box/timing.py +305 -0
  40. rbx/box/tooling/boca/debug_utils.py +88 -0
  41. rbx/box/tooling/boca/manual_scrape.py +20 -0
  42. rbx/box/tooling/boca/scraper.py +660 -57
  43. rbx/box/unit.py +0 -2
  44. rbx/box/validators.py +0 -4
  45. rbx/grading/judge/cacher.py +36 -0
  46. rbx/grading/judge/program.py +12 -2
  47. rbx/grading/judge/sandbox.py +1 -1
  48. rbx/grading/judge/sandboxes/stupid_sandbox.py +2 -1
  49. rbx/grading/judge/storage.py +36 -3
  50. rbx/grading/limits.py +4 -0
  51. rbx/grading/steps.py +3 -2
  52. rbx/resources/presets/default/contest/contest.rbx.yml +7 -1
  53. rbx/resources/presets/default/contest/statement/info.rbx.tex +46 -0
  54. rbx/resources/presets/default/preset.rbx.yml +1 -0
  55. rbx/resources/presets/default/problem/.gitignore +1 -0
  56. rbx/resources/presets/default/problem/problem.rbx.yml +19 -3
  57. rbx/resources/presets/default/problem/rbx.h +52 -5
  58. rbx/resources/presets/default/problem/statement/statement.rbx.tex +6 -2
  59. rbx/resources/presets/default/problem/testlib.h +6299 -0
  60. rbx/resources/presets/default/problem/validator.cpp +4 -3
  61. rbx/resources/presets/default/shared/contest_template.rbx.tex +8 -4
  62. rbx/resources/presets/default/shared/icpc.sty +18 -3
  63. rbx/resources/presets/default/shared/problem_template.rbx.tex +4 -1
  64. rbx/testing_utils.py +17 -1
  65. rbx/utils.py +45 -0
  66. {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/METADATA +5 -2
  67. {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/RECORD +71 -67
  68. {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/entry_points.txt +0 -1
  69. rbx/providers/__init__.py +0 -43
  70. rbx/providers/codeforces.py +0 -73
  71. rbx/providers/provider.py +0 -26
  72. rbx/submitors/__init__.py +0 -18
  73. rbx/submitors/codeforces.py +0 -121
  74. rbx/submitors/submitor.py +0 -25
  75. /rbx/resources/presets/default/problem/sols/{wa.cpp → wa-overflow.cpp} +0 -0
  76. {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/LICENSE +0 -0
  77. {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/WHEEL +0 -0
@@ -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.presets.fetch import PresetFetchInfo, get_preset_fetch_info
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
- _FALLBACK_PRESET_URI = 'rsalesc/rbx/rbx/resources/presets/default'
28
+ _FALLBACK_PRESET_NAME = 'default'
21
29
 
22
30
 
23
- def _find_preset_yaml(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.Path]:
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 = _find_preset_yaml(root)
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
- return utils.model_from_yaml(Preset, found.read_text())
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 _get_preset_lock(root: pathlib.Path = pathlib.Path()) -> Optional[PresetLock]:
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 _find_nested_preset(root: pathlib.Path) -> Optional[pathlib.Path]:
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 _find_local_preset(root: pathlib.Path) -> Optional[pathlib.Path]:
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 _find_nested_preset(original_root)
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 = _find_local_preset(root)
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 = _find_local_preset(root)
121
+ preset_path = find_local_preset(root)
88
122
  if preset_path is None:
89
123
  return False
90
- nested_preset_path = _find_nested_preset(root)
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 _is_contest(root: pathlib.Path = pathlib.Path()) -> bool:
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 _is_problem(root: pathlib.Path = pathlib.Path()) -> bool:
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 _check_is_valid_package(root: pathlib.Path = pathlib.Path()):
105
- if not _is_contest(root) and not _is_problem(root):
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 _process_globbing(
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 _dedup_tracked_assets(assets: List[TrackedAsset]) -> List[TrackedAsset]:
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 _get_preset_tracked_assets(
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 = _find_local_preset(root)
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 = _process_globbing(preset.tracking.contest, preset_pkg_path)
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 = _process_globbing(preset.tracking.problem, preset_pkg_path)
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 _dedup_tracked_assets(res)
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 _get_symlink_info(
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 _build_package_locked_assets(
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=_get_symlink_info(tracked_asset, root),
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=_get_symlink_info(tracked_asset, root),
283
+ symlink_info=get_symlink_info(tracked_asset, root),
250
284
  )
251
285
  )
252
286
  return res
253
287
 
254
288
 
255
- def _find_non_modified_assets(
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 _find_modified_assets(
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 _copy_preset_file(
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 = _get_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 = _build_package_locked_assets(
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 = _build_package_locked_assets(current_preset_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 = _find_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 = _find_modified_assets(
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
- _copy_preset_file(
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 = _find_local_preset(root)
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 = _find_local_preset(root)
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 = _find_local_preset(root)
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(_FALLBACK_PRESET_URI)
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 _clean_copied_package_dir(dest: pathlib.Path):
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 _clean_copied_contest_dir(dest: pathlib.Path, delete_local_rbx: bool = True):
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
- _clean_copied_package_dir(dest)
536
+ clean_copied_package_dir(dest)
503
537
 
504
538
 
505
- def _clean_copied_problem_dir(dest: pathlib.Path):
539
+ def clean_copied_problem_dir(dest: pathlib.Path):
506
540
  shutil.rmtree(str(dest / 'build'), ignore_errors=True)
507
- _clean_copied_package_dir(dest)
541
+ clean_copied_package_dir(dest)
508
542
 
509
543
 
510
- def _install_preset_from_dir(
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
- _clean_copied_contest_dir(dest / preset.contest)
587
+ clean_copied_contest_dir(dest / preset.contest)
545
588
  if preset.problem is not None:
546
- _clean_copied_problem_dir(dest / preset.problem)
589
+ clean_copied_problem_dir(dest / preset.problem)
547
590
 
548
- _clean_copied_package_dir(dest)
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
- _install_preset_from_dir(
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
- _install_preset_from_dir(
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
- _install_preset_from_dir(
686
+ install_preset_from_dir(
621
687
  rsrc_preset_path,
622
688
  dest,
623
689
  ensure_contest,
624
690
  ensure_problem,
625
- override_uri=str(utils.abspath(rsrc_preset_path)),
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
- # NOTE: Disabled for now.
657
- # if _install_preset_from_resources(
658
- # fetch_info,
659
- # dest,
660
- # ensure_contest=ensure_contest,
661
- # ensure_problem=ensure_problem,
662
- # update=update,
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
- _copy_preset_file(
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
- _copy_preset_file(
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 = _find_local_preset(dest_pkg)
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
- _clean_copied_contest_dir(dest_pkg, delete_local_rbx=False)
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 = _find_local_preset(dest_pkg)
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
- _clean_copied_problem_dir(dest_pkg)
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
- _install_preset_from_dir(get_active_preset_path(), dest_pkg)
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 = _get_preset_tracked_assets(root, is_contest=_is_contest(root))
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=_build_package_locked_assets(tracked_assets, root),
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 = _get_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=_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 = _find_local_preset(pathlib.Path.cwd())
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
- _check_is_valid_package()
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
- _check_is_valid_package()
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 = _find_local_preset(pathlib.Path.cwd())
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 get_local_fetch_info(s: str) -> Optional[PresetFetchInfo]:
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
- get_local_fetch_info,
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)