rbx.cp 0.5.73__py3-none-any.whl → 0.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. rbx/annotations.py +21 -1
  2. rbx/box/cd.py +11 -1
  3. rbx/box/checkers.py +9 -1
  4. rbx/box/cli.py +59 -46
  5. rbx/box/code.py +142 -3
  6. rbx/box/contest/build_contest_statements.py +44 -34
  7. rbx/box/contest/contest_package.py +4 -7
  8. rbx/box/contest/main.py +7 -58
  9. rbx/box/contest/schema.py +52 -8
  10. rbx/box/contest/statements.py +53 -25
  11. rbx/box/creation.py +3 -36
  12. rbx/box/environment.py +21 -9
  13. rbx/box/fields.py +35 -0
  14. rbx/box/lang.py +27 -0
  15. rbx/box/linting.py +26 -0
  16. rbx/box/package.py +4 -35
  17. rbx/box/packaging/boca/packager.py +48 -5
  18. rbx/box/packaging/contest_main.py +13 -0
  19. rbx/box/packaging/main.py +13 -2
  20. rbx/box/packaging/packager.py +4 -4
  21. rbx/box/packaging/pkg/packager.py +142 -0
  22. rbx/box/packaging/polygon/packager.py +2 -24
  23. rbx/box/packaging/polygon/upload.py +35 -17
  24. rbx/box/presets/__init__.py +362 -281
  25. rbx/box/presets/lock_schema.py +1 -2
  26. rbx/box/presets/schema.py +13 -5
  27. rbx/box/remote.py +2 -2
  28. rbx/box/retries.py +8 -0
  29. rbx/box/schema.py +82 -19
  30. rbx/box/solutions.py +77 -15
  31. rbx/box/statements/build_statements.py +44 -27
  32. rbx/box/statements/builders.py +18 -10
  33. rbx/box/statements/expander.py +49 -0
  34. rbx/box/statements/latex_jinja.py +61 -4
  35. rbx/box/statements/schema.py +33 -9
  36. rbx/box/stats.py +92 -0
  37. rbx/box/tasks.py +6 -3
  38. rbx/box/testcase_utils.py +19 -47
  39. rbx/box/tooling/__init__.py +0 -0
  40. rbx/box/tooling/boca/__init__.py +0 -0
  41. rbx/box/tooling/boca/main.py +13 -0
  42. rbx/box/tooling/boca/scrape.py +34 -0
  43. rbx/box/{packaging/boca/upload.py → tooling/boca/scraper.py} +77 -8
  44. rbx/box/tooling/main.py +8 -0
  45. rbx/box/ui/utils/run_ui.py +1 -1
  46. rbx/box/ui/widgets/interaction_box.py +19 -1
  47. rbx/grading/caching.py +18 -2
  48. rbx/grading/judge/sandbox.py +60 -5
  49. rbx/grading/judge/sandboxes/isolate.py +1 -0
  50. rbx/grading/judge/sandboxes/stupid_sandbox.py +11 -5
  51. rbx/grading/judge/sandboxes/timeit.py +36 -15
  52. rbx/grading/processing_context.py +62 -78
  53. rbx/grading/steps.py +92 -40
  54. rbx/resources/packagers/boca/checker.sh +4 -1
  55. rbx/resources/packagers/boca/compile/c +2 -6
  56. rbx/resources/packagers/boca/compile/cc +2 -6
  57. rbx/resources/packagers/boca/compile/cpp +2 -6
  58. rbx/resources/packagers/boca/compile/java +1 -6
  59. rbx/resources/packagers/boca/compile/kt +24 -28
  60. rbx/resources/packagers/boca/compile/py2 +2 -6
  61. rbx/resources/packagers/boca/compile/py3 +2 -6
  62. rbx/resources/packagers/boca/interactive/c +15 -83
  63. rbx/resources/packagers/boca/interactive/cc +15 -83
  64. rbx/resources/packagers/boca/interactive/cpp +15 -83
  65. rbx/resources/packagers/boca/interactive/java +15 -88
  66. rbx/resources/packagers/boca/interactive/kt +15 -88
  67. rbx/resources/packagers/boca/interactive/py2 +15 -88
  68. rbx/resources/packagers/boca/interactive/py3 +15 -88
  69. rbx/resources/packagers/boca/interactor_compile.sh +5 -2
  70. rbx/resources/packagers/boca/interactor_run.sh +174 -0
  71. rbx/resources/packagers/boca/safeexec.c +530 -0
  72. rbx/resources/packagers/boca/safeexec_compile.sh +49 -0
  73. rbx/resources/presets/default/contest/contest.rbx.yml +9 -8
  74. rbx/resources/presets/default/problem/problem.rbx.yml +38 -26
  75. rbx/resources/presets/default/problem/random.txt +3 -1
  76. rbx/resources/presets/default/problem/rbx.h +92 -0
  77. rbx/resources/presets/default/problem/statement/statement.rbx.tex +4 -7
  78. rbx/resources/presets/default/problem/validator.cpp +8 -8
  79. rbx/resources/templates/rbx.h +2 -3
  80. {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.1.dist-info}/METADATA +23 -6
  81. {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.1.dist-info}/RECORD +84 -71
  82. {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.1.dist-info}/WHEEL +1 -1
  83. rbx/resources/packagers/boca/compile/pas +0 -172
  84. rbx/resources/presets/default/problem/statement/projecao.png +0 -0
  85. {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.1.dist-info}/LICENSE +0 -0
  86. {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.1.dist-info}/entry_points.txt +0 -0
@@ -9,7 +9,7 @@ from pydantic import ValidationError
9
9
  from rbx import console, utils
10
10
  from rbx.box import cd
11
11
  from rbx.box.contest.schema import Contest
12
- from rbx.box.package import find_problem_package_or_die, warn_preset_deactivated
12
+ from rbx.box.package import find_problem_package_or_die
13
13
  from rbx.box.schema import Package
14
14
 
15
15
  YAML_NAME = 'contest.rbx.yml'
@@ -24,7 +24,6 @@ def find_contest_yaml(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.P
24
24
  contest_yaml_path = root / YAML_NAME
25
25
  if not contest_yaml_path.is_file():
26
26
  return None
27
- warn_preset_deactivated(root)
28
27
  return contest_yaml_path
29
28
 
30
29
 
@@ -84,12 +83,10 @@ def get_problems(contest: Contest) -> List[Package]:
84
83
  return problems
85
84
 
86
85
 
87
- def get_ruyaml() -> Tuple[ruyaml.YAML, ruyaml.Any]:
88
- contest_yaml_path = find_contest_yaml()
86
+ def get_ruyaml(root: pathlib.Path = pathlib.Path()) -> Tuple[ruyaml.YAML, ruyaml.Any]:
87
+ contest_yaml_path = find_contest_yaml(root)
89
88
  if contest_yaml_path is None:
90
- console.console.print(
91
- f'Contest not found in {pathlib.Path().absolute()}', style='error'
92
- )
89
+ console.console.print(f'[error]Contest not found in {root.absolute()}[/error]')
93
90
  raise typer.Exit(1)
94
91
  res = ruyaml.YAML()
95
92
  return res, res.load(contest_yaml_path.read_text())
rbx/box/contest/main.py CHANGED
@@ -17,7 +17,6 @@ from rbx.box.contest.contest_package import (
17
17
  )
18
18
  from rbx.box.contest.schema import ContestProblem
19
19
  from rbx.box.packaging import contest_main as packaging
20
- from rbx.box.presets.fetch import get_preset_fetch_info
21
20
  from rbx.box.schema import Package
22
21
  from rbx.config import open_editor
23
22
 
@@ -40,53 +39,18 @@ app.add_typer(
40
39
  def create(
41
40
  path: str,
42
41
  preset: Annotated[
43
- str,
42
+ Optional[str],
44
43
  typer.Option(
45
44
  '--preset',
46
45
  '-p',
47
- help='Which preset to use to create this package. Can be a named of an already installed preset, or an URI, in which case the preset will be downloaded.',
46
+ help='Which preset to use to create this package. Can be a named of an already installed preset, or an URI, in which case the preset will be downloaded.\n'
47
+ 'If not provided, the default preset will be used, or the active preset if any.',
48
48
  ),
49
- ] = 'default',
50
- local: bool = typer.Option(
51
- False,
52
- '--local',
53
- '-l',
54
- help='Whether to inline the installed preset within the contest folder.',
55
- ),
49
+ ] = None,
56
50
  ):
57
51
  console.console.print(f'Creating new contest at [item]{path}[/item]...')
58
52
 
59
- fetch_info = get_preset_fetch_info(preset)
60
- if fetch_info is None:
61
- console.console.print(
62
- f'[error]Invalid preset name/URI [item]{preset}[/item][/error]'
63
- )
64
- raise typer.Exit(1)
65
-
66
- if fetch_info.is_remote():
67
- preset = presets.install_from_remote(fetch_info)
68
- elif fetch_info.is_local_dir():
69
- preset = presets.install_from_local_dir(fetch_info)
70
-
71
- preset_cfg = presets.get_installed_preset(preset)
72
- preset_path = (
73
- presets.get_preset_installation_path(preset)
74
- if preset_cfg.contest is not None
75
- else presets.get_preset_installation_path('default')
76
- )
77
-
78
- contest_path = (
79
- presets.get_preset_installation_path(preset) / preset_cfg.contest
80
- if preset_cfg.contest is not None
81
- else presets.get_preset_installation_path('default') / 'contest'
82
- )
83
-
84
- if not contest_path.is_dir():
85
- console.console.print(
86
- f'[error]Contest template [item]{contest_path}[/item] does not exist.[/error]'
87
- )
88
- raise typer.Exit(1)
89
-
53
+ fetch_info = presets.get_preset_fetch_info_with_fallback(preset)
90
54
  dest_path = pathlib.Path(path)
91
55
 
92
56
  if dest_path.exists():
@@ -100,23 +64,11 @@ def create(
100
64
  )
101
65
  raise typer.Exit(1)
102
66
 
103
- dest_path.mkdir(parents=True, exist_ok=True)
104
- shutil.copytree(str(contest_path), str(dest_path), dirs_exist_ok=True)
105
- shutil.rmtree(str(dest_path / 'build'), ignore_errors=True)
106
- shutil.rmtree(str(dest_path / '.box'), ignore_errors=True)
107
- shutil.rmtree(str(dest_path / '.local.rbx'), ignore_errors=True)
108
- # TODO: consider clearing build and .box recursively for nested problem directories
109
- for lock in dest_path.rglob('.preset-lock.yml'):
110
- lock.unlink(missing_ok=True)
111
-
112
- if local:
113
- presets.copy_local_preset(
114
- preset_path, dest_path, remote_uri=fetch_info.uri or preset_cfg.uri
115
- )
67
+ presets.install_contest(dest_path, fetch_info)
116
68
 
117
69
  with cd.new_package_cd(dest_path):
118
70
  contest_utils.clear_all_caches()
119
- presets.generate_lock(preset if not local else presets.LOCAL)
71
+ presets.generate_lock()
120
72
 
121
73
 
122
74
  @app.command('edit, e', help='Open contest.rbx.yml in your default editor.')
@@ -143,9 +95,6 @@ def add(path: str, short_name: str, preset: Optional[str] = None):
143
95
  )
144
96
  raise typer.Exit(1)
145
97
 
146
- preset_lock = presets.get_preset_lock()
147
- if preset is None and preset_lock is not None:
148
- preset = preset_lock.preset_name
149
98
  creation.create(name, preset=preset, path=pathlib.Path(path))
150
99
 
151
100
  contest = find_contest_package_or_die()
rbx/box/contest/schema.py CHANGED
@@ -1,12 +1,15 @@
1
1
  import pathlib
2
- from typing import Dict, List, Optional
2
+ from typing import Annotated, Dict, List, Optional
3
3
 
4
- from pydantic import BaseModel, ConfigDict, Field, model_validator
4
+ from pydantic import AfterValidator, BaseModel, ConfigDict, Field, model_validator
5
5
 
6
- from rbx.box.schema import NameField, Primitive, expand_var
6
+ from rbx.box.fields import FNameField, NameField
7
+ from rbx.box.schema import Primitive, expand_var
8
+ from rbx.box.statements.expander import expand_statements
7
9
  from rbx.box.statements.schema import (
8
10
  ConversionStep,
9
11
  Joiner,
12
+ StatementLanguage,
10
13
  StatementType,
11
14
  )
12
15
 
@@ -15,6 +18,13 @@ def ShortNameField(**kwargs):
15
18
  return Field(pattern=r'^[A-Z]+[0-9]*$', min_length=1, max_length=4, **kwargs)
16
19
 
17
20
 
21
+ def is_unique_by_name(statements: List['ContestStatement']) -> List['ContestStatement']:
22
+ names = {st.name for st in statements}
23
+ if len(names) != len(statements):
24
+ raise ValueError('Statement names must be unique.')
25
+ return statements
26
+
27
+
18
28
  class ProblemStatementOverride(BaseModel):
19
29
  model_config = ConfigDict(extra='forbid')
20
30
 
@@ -29,13 +39,26 @@ configure them in case they are applied.
29
39
  """,
30
40
  )
31
41
 
42
+ vars: Dict[str, Primitive] = Field(
43
+ default={},
44
+ description='Variables to be merged into the problem statement vars.',
45
+ )
46
+
32
47
 
33
48
  class ContestStatement(BaseModel):
34
49
  model_config = ConfigDict(extra='forbid')
35
50
 
36
- language: str = Field(default='en', description='Language code for this statement.')
51
+ name: str = FNameField(description='Name of this statement.')
52
+
53
+ extends: Optional[str] = FNameField(
54
+ default=None, description='Name of the statement to inherit from.'
55
+ )
56
+
57
+ language: StatementLanguage = Field(
58
+ default='en', description='Language code for this statement (ISO 639-1).'
59
+ )
37
60
 
38
- title: str = Field(description='Title of the contest in this language.')
61
+ title: str = Field(default='', description='Title of the contest in this language.')
39
62
 
40
63
  location: Optional[str] = Field(
41
64
  default=None, description='Location of the contest in this language.'
@@ -45,9 +68,14 @@ class ContestStatement(BaseModel):
45
68
  default=None, description='Date of the contest in this language.'
46
69
  )
47
70
 
48
- path: pathlib.Path = Field(description='Path to the input statement file.')
71
+ path: pathlib.Path = Field(
72
+ default_factory=pathlib.Path,
73
+ description='Path to the input statement file.',
74
+ )
49
75
 
50
- type: StatementType = Field(description='Type of the input statement file.')
76
+ type: StatementType = Field(
77
+ default=StatementType.rbxTeX, description='Type of the input statement file.'
78
+ )
51
79
 
52
80
  joiner: Optional[Joiner] = Field(
53
81
  default=None,
@@ -95,6 +123,15 @@ Can be glob pattern as well, such as `imgs/*.png`.
95
123
  default=None, description='Override configuration for problem statements.'
96
124
  )
97
125
 
126
+ match: Optional[str] = FNameField(
127
+ default=None,
128
+ description="""
129
+ Name of the problem-level statement to match this statement against.
130
+
131
+ If not specified, will match against the first statement of the same language.
132
+ """,
133
+ )
134
+
98
135
  # Vars to be re-used in the statement.
99
136
  # - It will be available as \VAR{vars} variable in the contest-level box statement.
100
137
  vars: Dict[str, Primitive] = Field(
@@ -188,7 +225,10 @@ class Contest(BaseModel):
188
225
  default=[], description='List of problems in this contest.'
189
226
  )
190
227
 
191
- statements: List[ContestStatement] = Field(
228
+ statements: Annotated[
229
+ List[ContestStatement],
230
+ AfterValidator(is_unique_by_name),
231
+ ] = Field(
192
232
  default=None,
193
233
  description='Configure statements in this contest, per language.',
194
234
  )
@@ -199,6 +239,10 @@ class Contest(BaseModel):
199
239
  default={}, description='Variables to be re-used across the package.'
200
240
  )
201
241
 
242
+ @property
243
+ def expanded_statements(self) -> List[ContestStatement]:
244
+ return expand_statements(self.statements)
245
+
202
246
  @property
203
247
  def expanded_vars(self) -> Dict[str, Primitive]:
204
248
  return {key: expand_var(value) for key, value in self.vars.items()}
@@ -10,6 +10,9 @@ from rbx.box.contest.contest_package import (
10
10
  find_contest_package_or_die,
11
11
  within_contest,
12
12
  )
13
+ from rbx.box.contest.schema import ContestStatement
14
+ from rbx.box.formatting import href
15
+ from rbx.box.schema import expand_any_vars
13
16
  from rbx.box.statements.schema import StatementType
14
17
 
15
18
  app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
@@ -20,13 +23,18 @@ app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
20
23
  @syncer.sync
21
24
  async def build(
22
25
  verification: environment.VerificationParam,
26
+ names: Annotated[
27
+ Optional[List[str]],
28
+ typer.Argument(
29
+ help='Names of statements to build.',
30
+ ),
31
+ ] = None,
23
32
  languages: Annotated[
24
33
  Optional[List[str]],
25
34
  typer.Option(
26
- default_factory=list,
27
35
  help='Languages to build statements for. If not specified, build statements for all available languages.',
28
36
  ),
29
- ],
37
+ ] = None,
30
38
  output: Annotated[
31
39
  Optional[StatementType],
32
40
  typer.Option(
@@ -38,10 +46,14 @@ async def build(
38
46
  bool,
39
47
  typer.Option(help='Whether to build the statement with samples or not.'),
40
48
  ] = True,
41
- editorial: Annotated[
42
- bool,
43
- typer.Option(help='Whether to add editorial blocks to the statements or not.'),
44
- ] = False,
49
+ vars: Annotated[
50
+ Optional[List[str]],
51
+ typer.Option(
52
+ '-v',
53
+ '--vars',
54
+ help='Variables to be used in the statements.',
55
+ ),
56
+ ] = None,
45
57
  ):
46
58
  contest = find_contest_package_or_die()
47
59
  # At most run the validators, only in samples.
@@ -64,26 +76,42 @@ async def build(
64
76
  raise typer.Exit(1)
65
77
 
66
78
  contest = find_contest_package_or_die()
67
- candidate_languages = languages
68
- if not candidate_languages:
69
- candidate_languages = sorted(set([st.language for st in contest.statements]))
70
-
71
- for language in candidate_languages:
72
- candidates_for_lang = [
73
- st for st in contest.statements if st.language == language
74
- ]
75
- if not candidates_for_lang:
76
- console.console.print(
77
- f'[error]No contest-level statement found for language [item]{language}[/item].[/error]',
79
+
80
+ candidate_languages = set(languages or [])
81
+ candidate_names = set(names or [])
82
+
83
+ def should_process(st: ContestStatement) -> bool:
84
+ if candidate_languages and st.language not in candidate_languages:
85
+ return False
86
+ if candidate_names and st.name not in candidate_names:
87
+ return False
88
+ return True
89
+
90
+ valid_statements = [st for st in contest.expanded_statements if should_process(st)]
91
+
92
+ if not valid_statements:
93
+ console.console.print(
94
+ '[error]No statement found according to the specified criteria.[/error]',
95
+ )
96
+ raise typer.Exit(1)
97
+
98
+ built_statements = []
99
+
100
+ for statement in valid_statements:
101
+ built_statements.append(
102
+ build_statement(
103
+ statement,
104
+ contest,
105
+ output_type=output,
106
+ use_samples=samples,
107
+ custom_vars=expand_any_vars(annotations.parse_dictionary_items(vars)),
78
108
  )
79
- raise typer.Exit(1)
80
-
81
- build_statement(
82
- candidates_for_lang[0],
83
- contest,
84
- output_type=output,
85
- use_samples=samples,
86
- is_editorial=editorial,
109
+ )
110
+
111
+ console.console.rule(title='Built statements')
112
+ for statement, built_path in zip(valid_statements, built_statements):
113
+ console.console.print(
114
+ f'[item]{statement.name} {statement.language}[/item] -> {href(built_path)}'
87
115
  )
88
116
 
89
117
 
rbx/box/creation.py CHANGED
@@ -1,12 +1,10 @@
1
1
  import pathlib
2
- import shutil
3
2
  from typing import Annotated, Optional
4
3
 
5
4
  import typer
6
5
 
7
6
  from rbx import console, utils
8
7
  from rbx.box import package, presets
9
- from rbx.box.presets.fetch import get_preset_fetch_info
10
8
 
11
9
 
12
10
  def create(
@@ -26,33 +24,9 @@ def create(
26
24
  ] = None,
27
25
  path: Optional[pathlib.Path] = None,
28
26
  ):
29
- preset = preset or 'default'
30
27
  console.console.print(f'Creating new problem [item]{name}[/item]...')
31
28
 
32
- fetch_info = get_preset_fetch_info(preset)
33
- if fetch_info is None:
34
- console.console.print(
35
- f'[error]Invalid preset name/URI [item]{preset}[/item].[/error]'
36
- )
37
- raise typer.Exit(1)
38
-
39
- if fetch_info.fetch_uri is not None:
40
- preset = presets.install_from_remote(fetch_info)
41
-
42
- preset_cfg = presets.get_installed_preset(preset)
43
-
44
- problem_path = (
45
- presets.get_preset_installation_path(preset) / preset_cfg.problem
46
- if preset_cfg.problem is not None
47
- else presets.get_preset_installation_path('default') / 'problem'
48
- )
49
-
50
- if not problem_path.is_dir():
51
- console.console.print(
52
- f'[error]Problem template [item]{problem_path}[/item] does not exist.[/error]'
53
- )
54
- raise typer.Exit(1)
55
-
29
+ fetch_info = presets.get_preset_fetch_info_with_fallback(preset)
56
30
  dest_path = path or pathlib.Path(name)
57
31
 
58
32
  if dest_path.exists():
@@ -61,18 +35,11 @@ def create(
61
35
  )
62
36
  raise typer.Exit(1)
63
37
 
64
- dest_path.parent.mkdir(parents=True, exist_ok=True)
65
- shutil.copytree(str(problem_path), str(dest_path))
66
-
67
- # Remove a few left overs.
68
- shutil.rmtree(str(dest_path / 'build'), ignore_errors=True)
69
- shutil.rmtree(str(dest_path / '.box'), ignore_errors=True)
70
- for lock in dest_path.rglob('.preset-lock.yml'):
71
- lock.unlink(missing_ok=True)
38
+ presets.install_problem(dest_path, fetch_info)
72
39
 
73
40
  # Change problem name.
74
41
  ru, problem = package.get_ruyaml(dest_path)
75
42
  problem['name'] = name
76
43
  utils.save_ruyaml(dest_path / 'problem.rbx.yml', ru, problem)
77
44
 
78
- presets.generate_lock(preset, root=dest_path)
45
+ presets.generate_lock(dest_path)
rbx/box/environment.py CHANGED
@@ -7,6 +7,7 @@ import typer
7
7
  from pydantic import BaseModel, ConfigDict, Field, ValidationError
8
8
 
9
9
  from rbx import config, console, utils
10
+ from rbx.box import presets
10
11
  from rbx.box.extensions import Extensions, LanguageExtensions
11
12
  from rbx.grading.judge.sandbox import SandboxBase, SandboxParams
12
13
  from rbx.grading.judge.sandboxes.isolate import IsolateSandbox
@@ -185,22 +186,33 @@ class Environment(BaseModel):
185
186
  extensions: Optional[Extensions] = None
186
187
 
187
188
 
188
- def get_environment_path(env: str) -> pathlib.Path:
189
+ def get_app_environment_path(env: str) -> pathlib.Path:
189
190
  return config.get_app_file(pathlib.PosixPath('envs') / f'{env}.rbx.yml')
190
191
 
191
192
 
192
- def get_local_environment_path() -> Optional[pathlib.Path]:
193
- # TODO: implement logic to get env from local preset
194
- return None
193
+ def get_active_environment_path() -> pathlib.Path:
194
+ env_path = presets.get_preset_environment_path()
195
+ if env_path is None:
196
+ env_path = get_app_environment_path(config.get_config().boxEnvironment)
197
+ return env_path
198
+
199
+
200
+ @functools.cache
201
+ def get_active_environment_description() -> str:
202
+ env_path = presets.get_preset_environment_path()
203
+ if env_path is None:
204
+ return config.get_config().boxEnvironment
205
+ preset = presets.get_active_preset()
206
+ return f'preset - {preset.name}'
195
207
 
196
208
 
197
209
  @functools.cache
198
210
  def get_environment(env: Optional[str] = None) -> Environment:
199
211
  env_path = (
200
- get_environment_path(env) if env is not None else get_local_environment_path()
212
+ get_app_environment_path(env)
213
+ if env is not None
214
+ else get_active_environment_path()
201
215
  )
202
- if env_path is None:
203
- env_path = get_environment_path(config.get_config().boxEnvironment)
204
216
  if not env_path.is_file():
205
217
  console.console.print(
206
218
  f'Environment file [item]{env_path}[/item] not found.', style='error'
@@ -232,8 +244,8 @@ def install_environment(name: str, file: pathlib.Path):
232
244
  )
233
245
  raise typer.Exit(1)
234
246
 
235
- get_environment_path(name).parent.mkdir(parents=True, exist_ok=True)
236
- get_environment_path(name).write_bytes(file.read_bytes())
247
+ get_app_environment_path(name).parent.mkdir(parents=True, exist_ok=True)
248
+ get_app_environment_path(name).write_bytes(file.read_bytes())
237
249
  console.console.print(
238
250
  f'[success]Environment [item]{name}[/item] was installed from [item]{file}[/item]'
239
251
  )
rbx/box/fields.py ADDED
@@ -0,0 +1,35 @@
1
+ from typing import TypeVar
2
+
3
+ from deepmerge import always_merger
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ def NameField(**kwargs):
8
+ return Field(
9
+ pattern=r'^[a-zA-Z0-9][a-zA-Z0-9\-_]*$', min_length=3, max_length=32, **kwargs
10
+ )
11
+
12
+
13
+ def FNameField(**kwargs):
14
+ return Field(
15
+ pattern=r'^[a-zA-Z0-9][a-zA-Z0-9\-_]*$', min_length=3, max_length=128, **kwargs
16
+ )
17
+
18
+
19
+ T = TypeVar('T', bound=BaseModel)
20
+
21
+
22
+ def merge_pydantic_models(base: T, nxt: T) -> T:
23
+ """Merge two Pydantic model instances.
24
+
25
+ The attributes of 'base' and 'nxt' that weren't explicitly set are dumped into dicts
26
+ using '.model_dump(exclude_unset=True)', which are then merged using 'deepmerge',
27
+ and the merged result is turned into a model instance using '.model_validate'.
28
+
29
+ For attributes set on both 'base' and 'nxt', the value from 'nxt' will be used in
30
+ the output result.
31
+ """
32
+ base_dict = base.model_dump(exclude_unset=True)
33
+ nxt_dict = nxt.model_dump(exclude_unset=True)
34
+ merged_dict = always_merger.merge(base_dict, nxt_dict)
35
+ return base.model_validate(merged_dict)
rbx/box/lang.py ADDED
@@ -0,0 +1,27 @@
1
+ import functools
2
+ from typing import List
3
+
4
+ import iso639
5
+
6
+ from rbx import console
7
+
8
+
9
+ def code_to_langs(langs: List[str]) -> List[str]:
10
+ return [iso639.Language.from_part1(lang).name.lower() for lang in langs]
11
+
12
+
13
+ @functools.cache
14
+ def is_valid_lang_code(lang: str) -> bool:
15
+ try:
16
+ code_to_langs([lang])
17
+ except iso639.LanguageNotFoundError:
18
+ console.console.print(
19
+ f'[warning]Language [item]{lang}[/item] is being skipped because it is not a iso639 language.[/warning]'
20
+ )
21
+ return False
22
+
23
+ return True
24
+
25
+
26
+ def langs_to_code(langs: List[str]) -> List[str]:
27
+ return [iso639.Language.from_name(lang).part1 for lang in langs]
rbx/box/linting.py ADDED
@@ -0,0 +1,26 @@
1
+ import pathlib
2
+
3
+ import yamlfix
4
+ import yamlfix.model
5
+
6
+ from rbx import console
7
+ from rbx.box.cd import is_contest_package, is_problem_package
8
+ from rbx.box.stats import find_problem_packages_from_contest
9
+
10
+
11
+ def fix_yaml(path: pathlib.Path, verbose: bool = True):
12
+ config = yamlfix.model.YamlfixConfig(quote_basic_values=True)
13
+ _, changed = yamlfix.fix_files([str(path)], dry_run=False, config=config)
14
+ if changed and verbose:
15
+ console.console.print(
16
+ f'Formatting [item]{path}[/item].',
17
+ )
18
+
19
+
20
+ def fix_package(root: pathlib.Path = pathlib.Path()):
21
+ if is_problem_package(root):
22
+ fix_yaml(root / 'problem.rbx.yml')
23
+ if is_contest_package(root):
24
+ fix_yaml(root / 'contest.rbx.yml')
25
+ for problem in find_problem_packages_from_contest(root):
26
+ fix_yaml(problem / 'problem.rbx.yml')
rbx/box/package.py CHANGED
@@ -10,10 +10,9 @@ import ruyaml
10
10
  import typer
11
11
  from pydantic import ValidationError
12
12
 
13
- from rbx import config, console, utils
14
- from rbx.box import cd, environment
13
+ from rbx import console, utils
14
+ from rbx.box import cd
15
15
  from rbx.box.environment import get_sandbox_type
16
- from rbx.box.presets import get_installed_preset_or_null, get_preset_lock
17
16
  from rbx.box.schema import (
18
17
  CodeItem,
19
18
  ExpectedOutcome,
@@ -38,33 +37,6 @@ TEMP_DIR = None
38
37
  CACHE_STEP_VERSION = 1
39
38
 
40
39
 
41
- def warn_preset_deactivated(root: pathlib.Path = pathlib.Path()):
42
- preset_lock = get_preset_lock(root)
43
- if preset_lock is None:
44
- return
45
-
46
- preset = get_installed_preset_or_null(preset_lock.preset_name)
47
- if preset is None:
48
- console.console.print(
49
- f'[warning]WARNING: [item]{preset_lock.preset_name}[/item] is not installed. '
50
- 'Run [item]rbx presets sync && rbx activate[/item] to install and activate this preset.'
51
- )
52
- console.console.print()
53
- return
54
-
55
- if preset.env is not None and (
56
- not environment.get_environment_path(preset.name).is_file()
57
- or config.get_config().boxEnvironment != preset.name
58
- ):
59
- console.console.print(
60
- '[warning]WARNING: This package uses a preset that configures a custom environment, '
61
- f' but instead you are using the environment [item]{config.get_config().boxEnvironment}[/item]. '
62
- 'Run [item]rbx activate[/item] to use the environment configured by your preset.'
63
- )
64
- console.console.print()
65
- return
66
-
67
-
68
40
  @functools.cache
69
41
  def find_problem_yaml(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.Path]:
70
42
  root = root.resolve()
@@ -74,7 +46,6 @@ def find_problem_yaml(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.P
74
46
  problem_yaml_path = root / YAML_NAME
75
47
  if not problem_yaml_path.is_file():
76
48
  return None
77
- warn_preset_deactivated(root)
78
49
  return problem_yaml_path
79
50
 
80
51
 
@@ -130,9 +101,7 @@ def save_package(
130
101
  def get_ruyaml(root: pathlib.Path = pathlib.Path()) -> Tuple[ruyaml.YAML, ruyaml.Any]:
131
102
  problem_yaml_path = find_problem_yaml(root)
132
103
  if problem_yaml_path is None:
133
- console.console.print(
134
- f'Problem not found in {pathlib.Path().absolute()}', style='error'
135
- )
104
+ console.console.print(f'[error]Problem not found in {root.absolute()}[/error]')
136
105
  raise typer.Exit(1)
137
106
  res = ruyaml.YAML()
138
107
  return res, res.load(problem_yaml_path.read_text())
@@ -459,7 +428,7 @@ def get_merged_capture_path(root: pathlib.Path = pathlib.Path()) -> pathlib.Path
459
428
  def is_cache_valid(root: pathlib.Path = pathlib.Path()):
460
429
  cache_dir = find_problem(root) / '.box'
461
430
  if not cache_dir.is_dir():
462
- return
431
+ return True
463
432
 
464
433
  fingerprint_file = cache_dir / 'fingerprint'
465
434
  if not fingerprint_file.is_file():