rbx.cp 0.5.12__py3-none-any.whl → 0.5.14__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/box/builder.py CHANGED
@@ -9,14 +9,23 @@ from rbx.box.solutions import (
9
9
  print_run_report,
10
10
  run_solutions,
11
11
  )
12
- from rbx.box.validators import print_validation_report, validate_testcases
12
+ from rbx.box.validators import (
13
+ has_validation_errors,
14
+ print_validation_report,
15
+ validate_testcases,
16
+ )
13
17
 
14
18
 
15
19
  def build(
16
20
  verification: environment.VerificationParam,
17
21
  groups: Optional[Set[str]] = None,
18
- output: bool = True,
19
- ) -> None:
22
+ output: Optional[bool] = True,
23
+ ) -> bool:
24
+ no_main_solution_report = False
25
+ if output is None:
26
+ output = package.get_main_solution() is not None
27
+ no_main_solution_report = not output
28
+
20
29
  with utils.StatusProgress(
21
30
  'Building testcases...',
22
31
  'Built [item]{processed}[/item] testcases...',
@@ -24,6 +33,28 @@ def build(
24
33
  ) as s:
25
34
  generate_testcases(s, groups=groups)
26
35
 
36
+ if verification > 0:
37
+ validator = package.get_validator_or_nil()
38
+ if validator is None:
39
+ console.console.print(
40
+ '[warning]No validator found, skipping validation.[/warning]'
41
+ )
42
+
43
+ if validator is not None:
44
+ with utils.StatusProgress(
45
+ 'Validating testcases...',
46
+ 'Validated [item]{processed}[/item] testcases...',
47
+ keep=True,
48
+ ) as s:
49
+ infos = validate_testcases(s, groups=groups)
50
+ print_validation_report(infos)
51
+
52
+ if has_validation_errors(infos):
53
+ console.console.print(
54
+ '[error]Validation failed, check the report above.[/error]'
55
+ )
56
+ return False
57
+
27
58
  with utils.StatusProgress(
28
59
  'Building outputs for testcases...',
29
60
  'Built [item]{processed}[/item] outputs...',
@@ -32,23 +63,22 @@ def build(
32
63
  if output:
33
64
  generate_outputs_for_testcases(s, groups=groups)
34
65
 
35
- if verification > 0:
36
- with utils.StatusProgress(
37
- 'Validating testcases...',
38
- 'Validated [item]{processed}[/item] testcases...',
39
- keep=True,
40
- ) as s:
41
- infos = validate_testcases(s, groups=groups)
42
- print_validation_report(infos)
43
-
44
66
  console.console.print(
45
67
  '[success]Problem built.[/success] '
46
68
  '[warning]Check the output for verification errors![/warning]'
47
69
  )
48
70
 
71
+ if no_main_solution_report:
72
+ console.console.print(
73
+ '[warning]No main solution found, skipping generating samples for the statement.[/warning]'
74
+ )
75
+
76
+ return True
77
+
49
78
 
50
79
  def verify(verification: environment.VerificationParam) -> bool:
51
- build(verification=verification)
80
+ if not build(verification=verification):
81
+ return False
52
82
 
53
83
  if verification < VerificationLevel.FAST_SOLUTIONS.value:
54
84
  return True
rbx/box/cd.py CHANGED
@@ -3,9 +3,8 @@ import pathlib
3
3
  from typing import Optional
4
4
 
5
5
  import typer
6
- from rich import console
7
6
 
8
- from rbx import utils
7
+ from rbx import console, utils
9
8
 
10
9
 
11
10
  def find_package(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.Path]:
rbx/box/checkers.py CHANGED
@@ -119,6 +119,31 @@ def check(
119
119
  )
120
120
  message = package.get_digest_as_string(error.value or '') or ''
121
121
 
122
+ if (
123
+ checker_run_log is not None
124
+ and checker_run_log.exitcode != 0
125
+ and (
126
+ checker_run_log.exitstatus != SandboxBase.EXIT_NONZERO_RETURN
127
+ or checker_run_log.exitcode not in [0, 1, 2, 3]
128
+ )
129
+ ):
130
+ console.console.print(
131
+ f'[error]Checker [item]{package.get_checker().path}[/item] failed unexpectedly.[/error]'
132
+ )
133
+ console.console.print(
134
+ f'[error]Summary:[/error] {checker_run_log.get_summary()}'
135
+ )
136
+ console.console.print(
137
+ f'[error]Testcase input:[/error] [item]{testcase.inputPath}[/item]'
138
+ )
139
+ console.console.print(
140
+ f'[error]Testcase output:[/error] [item]{testcase.outputPath}[/item]'
141
+ )
142
+ console.console.print(
143
+ f'[error]Program output:[/error] [item]{program_output}[/item]'
144
+ )
145
+ raise typer.Exit(1)
146
+
122
147
  if checker_run_log is None or checker_run_log.exitcode not in [0, 1, 2, 3]:
123
148
  return CheckerResult(outcome=Outcome.INTERNAL_ERROR)
124
149
 
rbx/box/compile.py CHANGED
@@ -22,35 +22,29 @@ def _compile(item: CodeItem):
22
22
  out_path.chmod(0o755)
23
23
 
24
24
  console.console.print(
25
- f'[success]Compiled file written at [item]{out_path}[/item].[/success]'
25
+ f'[success]Compiled file written at [item]{out_path}[/item][/success]'
26
26
  )
27
27
 
28
28
 
29
- @app.command('any, a', help='Compile an asset given its path.')
30
- @package.within_problem
31
29
  def any(path: str):
32
- _compile(CodeItem(path=pathlib.Path(path)))
33
-
34
-
35
- @app.command('solution, s', help='Compile a solution given its path.')
36
- @package.within_problem
37
- def solution(path: str):
38
- _compile(package.get_solution(path))
30
+ pkg = package.find_problem_package_or_die()
39
31
 
32
+ solution = package.get_solution_or_nil(path)
33
+ if solution is not None:
34
+ _compile(solution)
35
+ return
40
36
 
41
- @app.command('generator, gen, g', help='Compile a generator given its name.')
42
- @package.within_problem
43
- def generator(name: str):
44
- _compile(package.get_generator(name))
37
+ for generator in pkg.generators:
38
+ if generator.path == pathlib.Path(path) or generator.name == path:
39
+ _compile(generator)
40
+ return
45
41
 
42
+ if pkg.checker is not None and pkg.checker.path == pathlib.Path(path):
43
+ _compile(pkg.checker)
44
+ return
46
45
 
47
- @app.command('checker, c', help='Compile the checker.')
48
- @package.within_problem
49
- def checker():
50
- _compile(package.get_checker())
46
+ if pkg.validator is not None and pkg.validator.path == pathlib.Path(path):
47
+ _compile(pkg.validator)
48
+ return
51
49
 
52
-
53
- @app.command('validator, v', help='Compile the main validator.')
54
- @package.within_problem
55
- def validator():
56
- _compile(package.get_validator())
50
+ _compile(CodeItem(path=pathlib.Path(path)))
@@ -3,6 +3,7 @@ import pathlib
3
3
  from typing import List, Optional
4
4
 
5
5
  import typer
6
+ from pydantic import ValidationError
6
7
 
7
8
  from rbx import console, utils
8
9
  from rbx.box.contest.schema import Contest
@@ -30,7 +31,12 @@ def find_contest_package(root: pathlib.Path = pathlib.Path()) -> Optional[Contes
30
31
  contest_yaml_path = find_contest_yaml(root)
31
32
  if not contest_yaml_path:
32
33
  return None
33
- return utils.model_from_yaml(Contest, contest_yaml_path.read_text())
34
+ try:
35
+ return utils.model_from_yaml(Contest, contest_yaml_path.read_text())
36
+ except ValidationError as e:
37
+ console.console.print(e)
38
+ console.console.print('[error]Error parsing contest.rbx.yml.[/error]')
39
+ raise typer.Exit(1) from e
34
40
 
35
41
 
36
42
  def find_contest_package_or_die(root: pathlib.Path = pathlib.Path()) -> Contest:
rbx/box/contest/main.py CHANGED
@@ -160,6 +160,32 @@ def add(path: str, short_name: str, preset: Optional[str] = None):
160
160
  )
161
161
 
162
162
 
163
+ @app.command('remove, r', help='Remove problem from contest.')
164
+ @within_contest
165
+ def remove(path_or_short_name: str):
166
+ contest = find_contest_package_or_die()
167
+
168
+ kept_problems = []
169
+ removed_problems = []
170
+ for problem in contest.problems:
171
+ if (
172
+ problem.path == pathlib.Path(path_or_short_name)
173
+ or problem.short_name == path_or_short_name
174
+ ):
175
+ removed_problems.append(problem)
176
+ else:
177
+ kept_problems.append(problem)
178
+
179
+ contest.problems = kept_problems
180
+ save_contest(contest)
181
+
182
+ for problem in removed_problems:
183
+ shutil.rmtree(str(problem.path), ignore_errors=True)
184
+ console.console.print(
185
+ f'Problem [item]{problem.short_name}[/item] removed from contest at [item]{problem.path}[/item].'
186
+ )
187
+
188
+
163
189
  @app.command(
164
190
  'each',
165
191
  help='Run a command for each problem in the contest.',
@@ -51,7 +51,14 @@ def build(
51
51
  )
52
52
  with utils.new_cd(problem.get_path()):
53
53
  contest_utils.clear_package_cache()
54
- builder.build(verification=verification, groups=set(['samples']))
54
+
55
+ if not builder.build(
56
+ verification=verification, groups=set(['samples']), output=None
57
+ ):
58
+ console.console.print(
59
+ '[error]Failed to build statements with samples, aborting.[/error]'
60
+ )
61
+ raise typer.Exit(1)
55
62
 
56
63
  contest = find_contest_package_or_die()
57
64
  candidate_languages = languages
rbx/box/environment.py CHANGED
@@ -4,7 +4,7 @@ from enum import Enum
4
4
  from typing import Annotated, List, Optional, Type, TypeVar
5
5
 
6
6
  import typer
7
- from pydantic import BaseModel, ConfigDict
7
+ from pydantic import BaseModel, ConfigDict, ValidationError
8
8
 
9
9
  from rbx import config, console, utils
10
10
  from rbx.box.extensions import Extensions, LanguageExtensions
@@ -19,9 +19,8 @@ class VerificationLevel(Enum):
19
19
  NONE = 0
20
20
  VALIDATE = 1
21
21
  FAST_SOLUTIONS = 2
22
- ASAN = 3
23
- ALL_SOLUTIONS = 4
24
- FULL = 5
22
+ ALL_SOLUTIONS = 3
23
+ FULL = 4
25
24
 
26
25
 
27
26
  VerificationParam = Annotated[
@@ -31,7 +30,7 @@ VerificationParam = Annotated[
31
30
  '--verification',
32
31
  '-v',
33
32
  help='Verification level to use when building package.',
34
- default_factory=lambda: VerificationLevel.ALL_SOLUTIONS.value,
33
+ default_factory=lambda: VerificationLevel.FULL.value,
35
34
  ),
36
35
  ]
37
36
 
@@ -193,7 +192,14 @@ def get_environment(env: Optional[str] = None) -> Environment:
193
192
  f'Environment file [item]{env_path}[/item] not found.', style='error'
194
193
  )
195
194
  raise typer.Exit()
196
- return utils.model_from_yaml(Environment, env_path.read_text())
195
+ try:
196
+ return utils.model_from_yaml(Environment, env_path.read_text())
197
+ except ValidationError as e:
198
+ console.console.print(e)
199
+ console.console.print(
200
+ f'[error]Error parsing environment file [item]{env_path}[/item][/error]'
201
+ )
202
+ raise typer.Exit(1) from e
197
203
 
198
204
 
199
205
  @functools.cache
rbx/box/generators.py CHANGED
@@ -82,6 +82,8 @@ def _run_generator(
82
82
  console.console.print(
83
83
  f'[error]Failed generating test {i} from group path {group_path}[/error]',
84
84
  )
85
+ if run_log is not None:
86
+ console.console.print(f'[error]Summary:[/error] {run_log.get_summary()}')
85
87
  if generation_stderr.value is not None:
86
88
  console.console.print('[error]Stderr:[/error]')
87
89
  console.console.print(
@@ -142,9 +144,7 @@ def generate_output_for_testcase(
142
144
  f'[error]Failed generating output for [item]{testcase.inputPath}[/item][/error]',
143
145
  )
144
146
  if run_log is not None:
145
- console.console.print(
146
- f'[error]Main solution exited with code [item]{-run_log.exitcode}[/item][/error]',
147
- )
147
+ console.console.print(f'[error]Summary:[/error] {run_log.get_summary()}')
148
148
  checker_result = checkers.check_with_no_output(run_log)
149
149
  console.console.print(
150
150
  f'[warning]Time: [item]{run_log.time:.2f}s[/item][/warning]',
@@ -238,7 +238,7 @@ def _run_generator_script(testcase: TestcaseSubgroup, cacher: FileCacher) -> str
238
238
  )
239
239
  if run_log is not None:
240
240
  console.console.print(
241
- f'[error]Script exited with code [item]{-run_log.exitcode}[/item][/error]',
241
+ f'[error]Summary:[/error] {run_log.get_summary()}'
242
242
  )
243
243
  if run_stderr.value is not None:
244
244
  console.console.print('[error]Stderr:[/error]')
@@ -339,8 +339,12 @@ def generate_standalone(
339
339
  )
340
340
  if not generation_log or generation_log.exitcode != 0:
341
341
  console.console.print(
342
- f'[error]Failed generating test using generator call [info]{call.name} {expanded_args_str}[/info].[/error]',
342
+ f'[error]Failed generating test using generator call [info]{call.name} {expanded_args_str}[/info][/error]',
343
343
  )
344
+ if generation_log is not None:
345
+ console.console.print(
346
+ f'[error]Summary:[/error] {generation_log.get_summary()}'
347
+ )
344
348
  if generation_stderr.value is not None:
345
349
  console.console.print('[error]Stderr:[/error]')
346
350
  console.console.print(
@@ -357,7 +361,7 @@ def generate_standalone(
357
361
  ok, message, *_ = validators.validate_test(output, validator, validator_digest)
358
362
  if not ok:
359
363
  console.console.print(
360
- f'[error]Failed validating testcase generated by call [info]{call.name} {expanded_args_str}[/info].[/error]'
364
+ f'[error]Failed validating testcase generated by call [info]{call.name} {expanded_args_str}[/info][/error]'
361
365
  )
362
366
  console.console.print(f'[error]Message:[/error] {message}')
363
367
  console.console.print(f'Testcase written at [item]{output}[/item]')
rbx/box/main.py CHANGED
@@ -3,11 +3,12 @@ from gevent import monkey
3
3
 
4
4
  monkey.patch_all()
5
5
 
6
+ import tempfile
6
7
  import shlex
7
8
  import sys
8
9
  import typing
9
10
 
10
- from rbx.box.schema import CodeItem, ExpectedOutcome
11
+ from rbx.box.schema import CodeItem, ExpectedOutcome, TestcaseGroup
11
12
 
12
13
 
13
14
  import pathlib
@@ -31,6 +32,7 @@ from rbx.box import (
31
32
  compile,
32
33
  presets,
33
34
  stresses,
35
+ validators,
34
36
  )
35
37
  from rbx.box.contest import main as contest
36
38
  from rbx.box.environment import VerificationLevel, get_environment_path
@@ -42,7 +44,6 @@ from rbx.box.solutions import (
42
44
  run_solutions,
43
45
  )
44
46
  from rbx.box.statements import build_statements
45
- from rbx.box.ui import main as ui_pkg
46
47
 
47
48
  app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
48
49
  app.add_typer(
@@ -69,15 +70,12 @@ app.add_typer(
69
70
  app.add_typer(
70
71
  contest.app, name='contest', cls=annotations.AliasGroup, help='Contest management.'
71
72
  )
72
- app.add_typer(
73
- compile.app, name='compile', cls=annotations.AliasGroup, help='Compile assets.'
74
- )
75
73
 
76
74
 
77
- @app.command('ui', hidden=True)
78
- @package.within_problem
79
- def ui():
80
- ui_pkg.start()
75
+ # @app.command('ui', hidden=True)
76
+ # @package.within_problem
77
+ # def ui():
78
+ # ui_pkg.start()
81
79
 
82
80
 
83
81
  @app.command('edit, e', help='Open problem.rbx.yml in your default editor.')
@@ -96,13 +94,6 @@ def build(verification: environment.VerificationParam):
96
94
  builder.build(verification=verification)
97
95
 
98
96
 
99
- @app.command('verify, v', help='Build and verify all the tests for the problem.')
100
- @package.within_problem
101
- def verify(verification: environment.VerificationParam):
102
- if not builder.verify(verification=verification):
103
- console.console.print('[error]Verification failed, check the report.[/error]')
104
-
105
-
106
97
  @app.command('run, r', help='Build and run solution(s).')
107
98
  @package.within_problem
108
99
  def run(
@@ -139,7 +130,14 @@ def run(
139
130
  )
140
131
  check = False
141
132
 
142
- builder.build(verification=verification, output=check)
133
+ if not builder.build(verification=verification, output=check):
134
+ return
135
+
136
+ if verification <= VerificationLevel.VALIDATE.value:
137
+ console.console.print(
138
+ '[warning]Verification level is set to [item]validate (-v1)[/item], so rbx only build tests and validated them.[/warning]'
139
+ )
140
+ return
143
141
 
144
142
  with utils.StatusProgress('Running solutions...') as s:
145
143
  tracked_solutions = None
@@ -206,6 +204,12 @@ def irun(
206
204
  console.console.print(
207
205
  '[warning]Outputs will be written to files. If you wish to print them to the terminal, use the "-p" parameter.'
208
206
  )
207
+ if verification < VerificationLevel.ALL_SOLUTIONS.value:
208
+ console.console.print(
209
+ '[warning]Verification level should be at least [item]all solutions (-v4)[/item] to run solutions interactively.'
210
+ )
211
+ return
212
+
209
213
  main_solution = package.get_main_solution()
210
214
  if check and main_solution is None:
211
215
  console.console.print(
@@ -248,7 +252,13 @@ def create(
248
252
  @app.command('stress', help='Run a stress test.')
249
253
  @package.within_problem
250
254
  def stress(
251
- name: str,
255
+ name: Annotated[
256
+ str,
257
+ typer.Argument(
258
+ help='Name of the stress test to run (specified in problem.rbx.yml), '
259
+ 'or the generator to run, in case -g is specified.'
260
+ ),
261
+ ],
252
262
  generator_args: Annotated[
253
263
  Optional[str],
254
264
  typer.Option(
@@ -325,9 +335,27 @@ def stress(
325
335
 
326
336
  testgroup = questionary.select(
327
337
  'Choose the testgroup to add the tests to.\nOnly test groups that have a .txt generatorScript are shown below: ',
328
- choices=list(groups_by_name) + ['(skip)'],
338
+ choices=list(groups_by_name) + ['(create new script)', '(skip)'],
329
339
  ).ask()
330
340
 
341
+ if testgroup == '(create new script)':
342
+ new_script_name = questionary.text(
343
+ 'Enter the name of the new .txt generatorScript file: '
344
+ ).ask()
345
+ new_script_path = pathlib.Path(new_script_name).with_suffix('.txt')
346
+ new_script_path.parent.mkdir(parents=True, exist_ok=True)
347
+ new_script_path.touch()
348
+
349
+ # Temporarily create a new testgroup with the new script.
350
+ testgroup = new_script_path.stem
351
+ groups_by_name[testgroup] = TestcaseGroup(
352
+ name=testgroup, generatorScript=CodeItem(path=new_script_path)
353
+ )
354
+ console.console.print(
355
+ f'[warning]A testgroup for [item]{new_script_path}[/item] will not be automatically added to the problem.rbx.yml file for you.\n'
356
+ 'Please add it manually. [/warning]'
357
+ )
358
+
331
359
  if testgroup not in groups_by_name:
332
360
  break
333
361
  try:
@@ -355,6 +383,47 @@ def stress(
355
383
  break
356
384
 
357
385
 
386
+ @app.command('compile', help='Compile an asset given its path.')
387
+ @package.within_problem
388
+ def compile_command(
389
+ path: Annotated[str, typer.Argument(help='Path to the asset to compile.')],
390
+ ):
391
+ compile.any(path)
392
+
393
+
394
+ @app.command('validate', help='Run the validator in a one-off fashion, interactively.')
395
+ @package.within_problem
396
+ def validate(
397
+ path: Annotated[
398
+ Optional[str],
399
+ typer.Option('--path', '-p', help='Path to the testcase to validate.'),
400
+ ] = None,
401
+ ):
402
+ validator_tuple = validators.compile_main_validator()
403
+ if validator_tuple is None:
404
+ console.console.print('[error]No validator found for this problem.[/error]')
405
+ raise typer.Exit(1)
406
+
407
+ validator, validator_digest = validator_tuple
408
+
409
+ input = console.multiline_prompt('Testcase input')
410
+
411
+ if path is None:
412
+ with tempfile.TemporaryDirectory() as tmpdir:
413
+ tmppath = pathlib.Path(tmpdir) / '000.in'
414
+ tmppath.write_text(input)
415
+
416
+ info = validators.validate_one_off(
417
+ pathlib.Path(tmppath), validator, validator_digest
418
+ )
419
+ else:
420
+ info = validators.validate_one_off(
421
+ pathlib.Path(path), validator, validator_digest
422
+ )
423
+
424
+ validators.print_validation_report([info])
425
+
426
+
358
427
  @app.command('environment, env', help='Set or show the current box environment.')
359
428
  def environment_command(
360
429
  env: Annotated[Optional[str], typer.Argument()] = None,
@@ -417,10 +486,17 @@ def activate():
417
486
  console.console.print(
418
487
  '[error]Preset is not installed. Install it manually, or specify a URI in [item].preset-lock.yml[/item].[/error]'
419
488
  )
420
- raise
489
+ raise typer.Exit(1)
421
490
  presets.install(preset_lock.uri)
422
491
 
423
492
  preset = presets.get_installed_preset(preset_lock.preset_name)
493
+
494
+ # Install the environment from the preset if it's not already installed.
495
+ presets.optionally_install_environment_from_preset(
496
+ preset, root=presets.get_preset_installation_path(preset_lock.name)
497
+ )
498
+
499
+ # Activate the environment.
424
500
  if preset.env is not None:
425
501
  environment_command(preset.name)
426
502
 
rbx/box/package.py CHANGED
@@ -3,6 +3,7 @@ import pathlib
3
3
  from typing import Dict, List, Optional, Tuple
4
4
 
5
5
  import typer
6
+ from pydantic import ValidationError
6
7
 
7
8
  from rbx import config, console, utils
8
9
  from rbx.box import environment
@@ -74,7 +75,12 @@ def find_problem_package(root: pathlib.Path = pathlib.Path()) -> Optional[Packag
74
75
  problem_yaml_path = find_problem_yaml(root)
75
76
  if not problem_yaml_path:
76
77
  return None
77
- return utils.model_from_yaml(Package, problem_yaml_path.read_text())
78
+ try:
79
+ return utils.model_from_yaml(Package, problem_yaml_path.read_text())
80
+ except ValidationError as e:
81
+ console.console.print(e)
82
+ console.console.print('[error]Error parsing problem.rbx.yml.[/error]')
83
+ raise typer.Exit(1) from e
78
84
 
79
85
 
80
86
  def find_problem_package_or_die(root: pathlib.Path = pathlib.Path()) -> Package:
@@ -4,6 +4,7 @@ import tempfile
4
4
  from typing import Annotated, Iterable, List, Optional, Sequence, Union
5
5
 
6
6
  import git
7
+ import questionary
7
8
  import rich
8
9
  import rich.prompt
9
10
  import typer
@@ -16,7 +17,7 @@ from rbx.box.presets.fetch import PresetFetchInfo, get_preset_fetch_info
16
17
  from rbx.box.presets.lock_schema import LockedAsset, PresetLock
17
18
  from rbx.box.presets.schema import Preset, TrackedAsset
18
19
  from rbx.config import get_default_app_path
19
- from rbx.grading.judge.digester import digest_cooperatively
20
+ from rbx.grading.judge.digester import digest_cooperatively, digest_file
20
21
 
21
22
  app = typer.Typer(no_args_is_help=True)
22
23
 
@@ -34,7 +35,7 @@ def get_preset_yaml(root: pathlib.Path = pathlib.Path()) -> Preset:
34
35
  found = find_preset_yaml(root)
35
36
  if not found:
36
37
  console.console.print(
37
- f'[error][item]preset.rbx.yml[/item] not found in [item]{root.absolute()}[/item].[/error]'
38
+ f'[error][item]preset.rbx.yml[/item] not found in [item]{root.absolute()}[/item][/error]'
38
39
  )
39
40
  raise typer.Exit(1)
40
41
  return utils.model_from_yaml(Preset, found.read_text())
@@ -294,6 +295,30 @@ def get_installed_preset(name: str, root: pathlib.Path = pathlib.Path()) -> Pres
294
295
  return preset
295
296
 
296
297
 
298
+ def optionally_install_environment_from_preset(
299
+ preset: Preset, root: pathlib.Path = pathlib.Path()
300
+ ):
301
+ if preset.env is None:
302
+ return
303
+ env_path = get_environment_path(preset.name)
304
+ preset_env_path = root / preset.env
305
+ if env_path.is_file():
306
+ if digest_file(preset_env_path) == digest_file(env_path):
307
+ return
308
+ overwrite = questionary.confirm(
309
+ 'Preset environment file has changed. Overwrite?',
310
+ default=False,
311
+ ).ask()
312
+ if not overwrite:
313
+ return
314
+
315
+ console.console.print(
316
+ f'[success]Overwriting the existing environment based on [item]{preset.env}[/item].'
317
+ )
318
+ env_path.parent.mkdir(parents=True, exist_ok=True)
319
+ shutil.copyfile(str(preset_env_path), env_path)
320
+
321
+
297
322
  def _install(root: pathlib.Path = pathlib.Path(), force: bool = False):
298
323
  preset = get_preset_yaml(root)
299
324
 
@@ -394,7 +419,7 @@ def _sync(update: bool = False):
394
419
  '[error]Package does not have a [item].preset.lock.yml[/item] file and thus cannot be synced.[/error]'
395
420
  )
396
421
  console.console.print(
397
- '[error]Ensure this package was created through a preset, or manually associate one with [item]rbx presets lock [PRESET][/item].[/error]'
422
+ '[error]Ensure this package was created through a preset, or manually associate one with [item]rbx presets lock [PRESET][/item][/error]'
398
423
  )
399
424
  raise typer.Exit(1)
400
425
 
rbx/box/solutions.py CHANGED
@@ -114,7 +114,7 @@ def compile_solutions(
114
114
  compiled_solutions[solution.path] = compile_item(solution)
115
115
  except:
116
116
  console.console.print(
117
- f'[error]Failed compiling solution [item]{solution.path}[/item].[/error]'
117
+ f'[error]Failed compiling solution [item]{solution.path}[/item][/error]'
118
118
  )
119
119
  raise
120
120
 
@@ -480,10 +480,14 @@ def get_testcase_markup_verdict(eval: Evaluation) -> str:
480
480
 
481
481
 
482
482
  def _get_evals_time_in_ms(evals: List[Evaluation]) -> int:
483
+ if not evals:
484
+ return 0
483
485
  return max(int((eval.log.time or 0.0) * 1000) for eval in evals)
484
486
 
485
487
 
486
488
  def _get_evals_memory_in_mb(evals: List[Evaluation]) -> int:
489
+ if not evals:
490
+ return 0
487
491
  return max(int(eval.log.memory or 0) // (1024 * 1024) for eval in evals)
488
492
 
489
493
 
@@ -505,6 +509,7 @@ def _print_solution_outcome(
505
509
  ) -> bool:
506
510
  pkg = package.find_problem_package_or_die()
507
511
 
512
+ has_plain_tle = False
508
513
  bad_verdicts = set()
509
514
  no_tle_bad_verdicts = set()
510
515
  for eval in evals:
@@ -515,6 +520,10 @@ def _print_solution_outcome(
515
520
  and eval.result.no_tle_outcome != Outcome.ACCEPTED
516
521
  ):
517
522
  no_tle_bad_verdicts.add(eval.result.no_tle_outcome)
523
+ has_plain_tle = has_plain_tle or (
524
+ eval.result.outcome == Outcome.TIME_LIMIT_EXCEEDED
525
+ and eval.result.no_tle_outcome is None
526
+ )
518
527
 
519
528
  unmatched_bad_verdicts = set(
520
529
  v for v in bad_verdicts if not solution.outcome.match(v)
@@ -543,16 +552,27 @@ def _print_solution_outcome(
543
552
  verification.value >= VerificationLevel.FULL.value
544
553
  # Solution expects a TLE.
545
554
  and expected_outcome_is_tle
546
- # A TLE (or similar) has happened.
547
- and matched_bad_verdicts
548
- # The solution has no other bad verdicts except for TLEs in double TL.
549
- and not ((bad_verdicts | no_tle_bad_verdicts) - {Outcome.TIME_LIMIT_EXCEEDED})
550
- # The solution passes in double TL.
555
+ # Solution does not have a plain TLE.
556
+ and not has_plain_tle
557
+ # A TLE has happened.
558
+ and Outcome.TIME_LIMIT_EXCEEDED in matched_bad_verdicts
559
+ # The solution runs in double TL.
551
560
  and evals_time < pkg.timelimit_for_language(solution.language) * 2
552
561
  ):
553
- console.print(
554
- '[yellow]WARNING[/yellow] The solution still passed in double TL.'
555
- )
562
+ other_verdicts = (bad_verdicts | no_tle_bad_verdicts) - {
563
+ Outcome.TIME_LIMIT_EXCEEDED
564
+ }
565
+ if not other_verdicts:
566
+ # The solution has no other bad verdicts except for TLEs in double TL.
567
+ console.print(
568
+ '[yellow]WARNING[/yellow] The solution still passed in double TL.'
569
+ )
570
+ elif not (bad_verdicts - {Outcome.TIME_LIMIT_EXCEEDED}):
571
+ # The solution has other bad soft TLE outcomes.
572
+ other_verdicts_names = ' '.join(v.name for v in other_verdicts)
573
+ console.print(
574
+ f'[yellow]WARNING[/yellow] The solution could still run under double TL, but failed with [item]{other_verdicts_names}[/item].'
575
+ )
556
576
  console.print(f'Time: {get_evals_formatted_time(evals)}')
557
577
  console.print(f'Memory: {get_evals_formatted_memory(evals)}')
558
578
  return len(unmatched_bad_verdicts) == 0
@@ -758,7 +778,10 @@ def print_run_report(
758
778
  is_closing_group = last_group is not None and is_new_group
759
779
 
760
780
  if is_closing_group:
761
- console.print(f'({get_evals_formatted_time(group_evals)})', end='')
781
+ console.print(
782
+ f'({get_evals_formatted_time(group_evals)}, {get_evals_formatted_memory(group_evals)})',
783
+ end='',
784
+ )
762
785
  console.print()
763
786
 
764
787
  if is_new_solution:
@@ -788,6 +811,9 @@ def print_run_report(
788
811
  print_last_solution()
789
812
 
790
813
  items.seek(0)
791
- _print_timing(console, result.skeleton, list(structured_evaluations)[-1])
814
+ structured_evaluations_list = list(structured_evaluations)
815
+
816
+ if structured_evaluations_list:
817
+ _print_timing(console, result.skeleton, structured_evaluations_list[-1])
792
818
 
793
819
  return ok
@@ -330,7 +330,15 @@ def build(
330
330
  ):
331
331
  # At most run the validators, only in samples.
332
332
  if samples:
333
- builder.build(verification=verification, groups=set(['samples']))
333
+ if not builder.build(
334
+ verification=verification,
335
+ groups=set(['samples']),
336
+ output=None,
337
+ ):
338
+ console.console.print(
339
+ '[error]Failed to build statements with samples, aborting.[/error]'
340
+ )
341
+ raise typer.Exit(1)
334
342
 
335
343
  pkg = package.find_problem_package_or_die()
336
344
  candidate_languages = languages
rbx/box/stresses.py CHANGED
@@ -35,7 +35,7 @@ def _compile_finder(finder: CodeItem) -> str:
35
35
  digest = compile_item(finder)
36
36
  except Exception as e:
37
37
  console.console.print(
38
- f'[error]Failed compiling checker [item]{finder.path}[/item].[/error]'
38
+ f'[error]Failed compiling checker [item]{finder.path}[/item][/error]'
39
39
  )
40
40
  raise typer.Exit(1) from e
41
41
  return digest
@@ -104,6 +104,8 @@ def run_stress(
104
104
  if time.monotonic() - startTime > timeoutInSeconds:
105
105
  break
106
106
 
107
+ executed += 1
108
+
107
109
  if progress:
108
110
  seconds = timeoutInSeconds - int(time.monotonic() - startTime)
109
111
  progress.update(
@@ -212,7 +214,7 @@ def run_stress(
212
214
 
213
215
  if internal_error_results:
214
216
  console.console.print(
215
- f'[error]Checkers failed during stress test [item]{name}[/item] with args [info]{expanded_generator_call.name} {expanded_generator_call.args}[/info].[/error]'
217
+ f'[error]Checkers failed during stress test [item]{name}[/item] with args [info]{expanded_generator_call.name} {expanded_generator_call.args}[/info][/error]'
216
218
  )
217
219
  for internal_error_result in internal_error_results:
218
220
  assert internal_error_result.checker is not None
@@ -259,7 +261,6 @@ def run_stress(
259
261
  )
260
262
 
261
263
  # Be cooperative.
262
- executed += 1
263
264
  time.sleep(0.001)
264
265
 
265
266
  return StressReport(findings=findings, executed=executed)
@@ -300,7 +300,7 @@ def validate(tree: lark.ParseTree):
300
300
  for checker in all_checkers:
301
301
  if not pathlib.Path(checker).is_file():
302
302
  console.console.print(
303
- f'[error]Finder expression references non-existing checker [item]{checker}[/item].[/error]'
303
+ f'[error]Finder expression references non-existing checker [item]{checker}[/item][/error]'
304
304
  )
305
305
  raise typer.Exit(1)
306
306
 
@@ -308,7 +308,7 @@ def validate(tree: lark.ParseTree):
308
308
  for solution in all_solutions:
309
309
  if not pathlib.Path(solution).is_file():
310
310
  console.console.print(
311
- f'[error]Finder expression references non-existing solution [item]{solution}[/item].[/error]'
311
+ f'[error]Finder expression references non-existing solution [item]{solution}[/item][/error]'
312
312
  )
313
313
  raise typer.Exit(1)
314
314
 
rbx/box/validators.py CHANGED
@@ -2,6 +2,7 @@ import pathlib
2
2
  import shlex
3
3
  from typing import Dict, List, Optional, Set, Tuple
4
4
 
5
+ import typer
5
6
  from pydantic import BaseModel
6
7
 
7
8
  from rbx import console
@@ -9,6 +10,7 @@ from rbx.box import package
9
10
  from rbx.box.code import compile_item, run_item
10
11
  from rbx.box.schema import CodeItem, Primitive
11
12
  from rbx.box.testcases import find_built_testcase_inputs
13
+ from rbx.grading.judge.sandbox import SandboxBase
12
14
  from rbx.grading.steps import (
13
15
  DigestHolder,
14
16
  DigestOrDest,
@@ -33,7 +35,7 @@ def _compile_validator(validator: CodeItem) -> str:
33
35
  digest = compile_item(validator)
34
36
  except:
35
37
  console.console.print(
36
- f'[error]Failed compiling validator [item]{validator.path}[/item].[/error]'
38
+ f'[error]Failed compiling validator [item]{validator.path}[/item][/error]'
37
39
  )
38
40
  raise
39
41
  return digest
@@ -52,6 +54,9 @@ def _process_bounds(log: str) -> HitBounds:
52
54
  k, v = items
53
55
  v = v.strip()
54
56
 
57
+ if 'constant-bounds' in k:
58
+ continue
59
+
55
60
  hit = ('min-value-hit' in v, 'max-value-hit' in v)
56
61
  if k not in bounds:
57
62
  bounds[k] = hit
@@ -108,6 +113,18 @@ def _validate_testcase(
108
113
  ],
109
114
  extra_args=shlex.join(var_args) if var_args else None,
110
115
  )
116
+
117
+ if (
118
+ run_log is not None
119
+ and run_log.exitcode != 0
120
+ and run_log.exitstatus != SandboxBase.EXIT_NONZERO_RETURN
121
+ ):
122
+ console.console.print(
123
+ f'[error]Validator [item]{validator.path}[/item] failed unexpectedly.[/error]'
124
+ )
125
+ console.console.print(f'[error]Summary:[/error] {run_log.get_summary()}')
126
+ raise typer.Exit(1)
127
+
111
128
  log_overview = ''
112
129
  if log_digest.value is not None:
113
130
  log_overview = package.get_digest_as_string(log_digest.value or '')
@@ -138,6 +155,22 @@ def compile_main_validator() -> Optional[Tuple[CodeItem, str]]:
138
155
  return pkg.validator, _compile_validator(pkg.validator)
139
156
 
140
157
 
158
+ def validate_one_off(
159
+ testcase: pathlib.Path,
160
+ validator: CodeItem,
161
+ validator_digest: str,
162
+ ) -> TestcaseValidationInfo:
163
+ ok, message, _ = validate_test(testcase, validator, validator_digest)
164
+ info = TestcaseValidationInfo(
165
+ group='interactive',
166
+ path=testcase,
167
+ ok=ok,
168
+ hit_bounds={},
169
+ message=message,
170
+ )
171
+ return info
172
+
173
+
141
174
  def compile_validators(
142
175
  progress: Optional[StatusProgress] = None,
143
176
  ) -> Dict[str, str]:
@@ -202,6 +235,10 @@ def validate_testcases(
202
235
  return validation_info
203
236
 
204
237
 
238
+ def has_validation_errors(infos: List[TestcaseValidationInfo]) -> bool:
239
+ return any(not info.ok for info in infos)
240
+
241
+
205
242
  def print_validation_report(infos: List[TestcaseValidationInfo]):
206
243
  console.console.rule('Validation report', style='status')
207
244
  hit_bounds_per_group: Dict[str, HitBounds] = {}
@@ -218,6 +255,10 @@ def print_validation_report(infos: List[TestcaseValidationInfo]):
218
255
  [hit_bounds_per_group[info.group], info.hit_bounds]
219
256
  )
220
257
 
258
+ if not hit_bounds_per_group:
259
+ console.console.print()
260
+ return
261
+
221
262
  if not _has_group_specific_validator():
222
263
  hit_bounds_per_group = {None: _merge_hit_bounds(hit_bounds_per_group.values())}
223
264
 
rbx/clone.py CHANGED
@@ -158,7 +158,7 @@ def main(lang: Optional[str] = None):
158
158
  )
159
159
  else:
160
160
  console.print(
161
- f'[status][rbx]rbx[/rbx] parsed problem from [item]{problem.url}[/item].[/status]'
161
+ f'[status][rbx]rbx[/rbx] parsed problem from [item]{problem.url}[/item][/status]'
162
162
  )
163
163
  else:
164
164
  batch_to_left[problem.batch.id] -= 1
@@ -1,4 +1,5 @@
1
1
  import hashlib
2
+ import pathlib
2
3
  from typing import IO
3
4
 
4
5
  import gevent
@@ -33,3 +34,8 @@ def digest_cooperatively(f: IO[bytes], chunk_size: int = 2**20):
33
34
  d = Digester()
34
35
  digest_cooperatively_into_digester(f, d, chunk_size)
35
36
  return d.digest()
37
+
38
+
39
+ def digest_file(path: pathlib.Path):
40
+ with open(path, 'rb') as f:
41
+ return digest_cooperatively(f)
rbx/grading/steps.py CHANGED
@@ -1,5 +1,8 @@
1
+ import functools
2
+ import os
1
3
  import pathlib
2
4
  import shlex
5
+ import shutil
3
6
  import subprocess
4
7
  from enum import Enum
5
8
  from typing import Any, Dict, List, Optional, Tuple, Union
@@ -129,12 +132,6 @@ class TestcaseIO(BaseModel):
129
132
  output: Optional[pathlib.Path] = None
130
133
 
131
134
 
132
- class PreprocessLog(BaseModel):
133
- cmd: List[str]
134
- exitcode: int
135
- log: str
136
-
137
-
138
135
  class RunLogMetadata(BaseModel):
139
136
  language: Optional[str] = None
140
137
 
@@ -151,6 +148,18 @@ class RunLog(BaseModel):
151
148
  return None
152
149
  return self.metadata.language
153
150
 
151
+ def get_summary(self) -> str:
152
+ if self.exitcode == 0:
153
+ return 'OK'
154
+ time = self.time or 0.0
155
+ memory = self.memory or 0
156
+ return f'FAILED with exit code {self.exitcode} and sandbox status {self.exitstatus} (time: {time}s, memory: {memory//(1024*1024)}MB)'
157
+
158
+
159
+ class PreprocessLog(RunLog):
160
+ cmd: List[str]
161
+ log: str
162
+
154
163
 
155
164
  class TestcaseLog(RunLog):
156
165
  stdout_absolute_path: Optional[pathlib.Path] = None
@@ -250,10 +259,26 @@ def _split_and_expand(command: str, sandbox: SandboxBase) -> List[str]:
250
259
  return res
251
260
 
252
261
 
262
+ def _is_c_command(exe_command: str) -> bool:
263
+ return exe_command.endswith('gcc') or exe_command.endswith('clang')
264
+
265
+
253
266
  def _is_cpp_command(exe_command: str) -> bool:
254
267
  return exe_command.endswith('g++') or exe_command.endswith('clang++')
255
268
 
256
269
 
270
+ @functools.cache
271
+ def _complain_about_clang() -> None:
272
+ console.print(
273
+ '[warning]Notice your C++ files are being compiled with [item]clang[/item] instead of [item]g++[/item].[/warning]'
274
+ )
275
+ console.print('[warning]This may lead to unexpected behavior.[/warning]')
276
+ console.print('[warning]Consider using [item]g++[/item] instead.[/warning]')
277
+ console.print(
278
+ '[warning]See [item]https://rsalesc.github.io/rbx/cpp-on-macos[/item] for instructions on how to use [item]g++[/item] on MacOS.'
279
+ )
280
+
281
+
257
282
  def _maybe_get_bits_stdcpp_for_clang(command: str) -> Optional[GradingFileInput]:
258
283
  cmds = shlex.split(command)
259
284
  if not cmds:
@@ -274,6 +299,7 @@ def _maybe_get_bits_stdcpp_for_clang(command: str) -> Optional[GradingFileInput]
274
299
  if 'clang' not in lines[0]:
275
300
  return None
276
301
 
302
+ _complain_about_clang()
277
303
  bits = get_bits_stdcpp()
278
304
  return GradingFileInput(src=bits, dest=pathlib.Path('bits/stdc++.h'))
279
305
 
@@ -288,12 +314,45 @@ def _maybe_get_bits_stdcpp_for_commands(
288
314
  return None
289
315
 
290
316
 
317
+ @functools.cache
318
+ def _try_following_alias_for_exe(exe: str) -> Optional[str]:
319
+ if _is_c_command(exe) and os.environ.get('RBX_C_PATH'):
320
+ return os.environ['RBX_C_PATH']
321
+ if _is_cpp_command(exe) and os.environ.get('RBX_CXX_PATH'):
322
+ return os.environ['RBX_CXX_PATH']
323
+ output = subprocess.run(
324
+ f'which {exe}', shell=True, executable=shutil.which('bash'), capture_output=True
325
+ )
326
+ if output.returncode != 0:
327
+ return None
328
+ return output.stdout.decode().strip()
329
+
330
+
331
+ def _try_following_alias_for_command(command: str) -> str:
332
+ cmds = shlex.split(command)
333
+ if not cmds:
334
+ return command
335
+ exe = cmds[0]
336
+ new_exe = _try_following_alias_for_exe(exe)
337
+ if new_exe is None:
338
+ return command
339
+ return shlex.join([new_exe, *cmds[1:]])
340
+
341
+
342
+ def _try_following_alias_for_commands(commands: List[str]) -> List[str]:
343
+ res: List[str] = []
344
+ for command in commands:
345
+ res.append(_try_following_alias_for_command(command))
346
+ return res
347
+
348
+
291
349
  def compile(
292
350
  commands: List[str],
293
351
  params: SandboxParams,
294
352
  sandbox: SandboxBase,
295
353
  artifacts: GradingArtifacts,
296
354
  ) -> bool:
355
+ commands = _try_following_alias_for_commands(commands)
297
356
  bits_artifact = _maybe_get_bits_stdcpp_for_commands(commands)
298
357
  if bits_artifact is not None:
299
358
  _process_input_artifacts(GradingArtifacts(inputs=[bits_artifact]), sandbox)
@@ -337,6 +396,9 @@ def compile(
337
396
  log = PreprocessLog(
338
397
  cmd=cmd,
339
398
  exitcode=sandbox.get_exit_code(),
399
+ exitstatus=sandbox.get_exit_status(),
400
+ time=sandbox.get_execution_time(),
401
+ memory=sandbox.get_memory_used(),
340
402
  log='\n'.join(std_outputs),
341
403
  )
342
404
  logs.append(log)
@@ -346,10 +408,10 @@ def compile(
346
408
 
347
409
  if logs and logs[-1].exitcode != 0:
348
410
  console.print(
349
- 'Preprocessing [error]failed[/error] with command',
411
+ '[error]FAILED[/error] Preprocessing failed with command',
350
412
  utils.highlight_json_obj(logs[-1].cmd),
351
413
  )
352
- console.print(f'Exit code: [error]{logs[-1].exitcode}[/error]')
414
+ console.print(f'[error]Summary:[/error] {logs[-1].get_summary()}')
353
415
  console.print(Text.from_ansi(logs[-1].log), style='default')
354
416
  return False
355
417
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rbx.cp
3
- Version: 0.5.12
3
+ Version: 0.5.14
4
4
  Summary:
5
5
  Author: Roberto Sales
6
6
  Requires-Python: >=3.9,<4.0
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Requires-Dist: chardet (>=5.2.0,<6.0.0)
13
- Requires-Dist: fastapi (>=0.111.0,<0.112.0)
13
+ Requires-Dist: fastapi (>=0.115.8,<0.116.0)
14
14
  Requires-Dist: filelock (>=3.14.0,<4.0.0)
15
15
  Requires-Dist: gevent (>=24.2.1,<25.0.0)
16
16
  Requires-Dist: gitpython (>=3.1.43,<4.0.0)
@@ -26,9 +26,9 @@ Requires-Dist: python-iso639 (>=2024.4.27,<2025.0.0)
26
26
  Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
27
27
  Requires-Dist: questionary (>=2.1.0,<3.0.0)
28
28
  Requires-Dist: requests (>=2.32.3,<3.0.0)
29
- Requires-Dist: rich (>=13.7.1,<14.0.0)
29
+ Requires-Dist: rich (>=13.9.4,<14.0.0)
30
30
  Requires-Dist: textual (>=0.79.1,<0.80.0)
31
- Requires-Dist: typer[all] (>=0.12.3,<0.13.0)
31
+ Requires-Dist: typer (>=0.15.1,<0.16.0)
32
32
  Description-Content-Type: text/markdown
33
33
 
34
34
  <p align="center">
@@ -2,27 +2,27 @@ rbx/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  rbx/annotations.py,sha256=Z3jBUyZoXkrz34jko3Rft0bnMME6nWb0vsV5I3HlgR0,3064
3
3
  rbx/autoenum.py,sha256=cusv8ClXRlDVvhZ8eDrtYcL_2peXlHugAey_ht8roXk,12025
4
4
  rbx/box/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- rbx/box/builder.py,sha256=rClPi3p4dYDHAjy8nE6YZtXwB19In1xvMBxvZQY0up4,2330
6
- rbx/box/cd.py,sha256=2MJjcO-4sxF7HoAI9Suxny7veR-27fA4pP1DxQNQkBI,972
7
- rbx/box/checkers.py,sha256=3luGMotNvDzqwbnYaxlAWzsUL7iW24gGG0wy28re0bo,4484
5
+ rbx/box/builder.py,sha256=QVKp7VL7YC8xs4oevcsam8DT_tjkSqu76nUoad1EwVY,3229
6
+ rbx/box/cd.py,sha256=NF0gqQso7a1IZzzZ6YJU8ip744AYAQ8Hp7vuzBRmZak,956
7
+ rbx/box/checkers.py,sha256=u5Pk-xViWxb_-KqAdEvnydbfppkYg0lBYoRQl5m_U_4,5380
8
8
  rbx/box/code.py,sha256=0UfxzemrObXQq-cvAtSAImCm4sKV_4Xog-TMUZBRFwc,5761
9
- rbx/box/compile.py,sha256=2xc8c6yDcEof-z0XmUdQHN4PrR9fXDGWeHKuXxDUKj8,1436
9
+ rbx/box/compile.py,sha256=DHf9MNhfigXufe3Q5LghuTNsAYcpCoFho-6nG8F0t38,1315
10
10
  rbx/box/conftest.py,sha256=sEmciXSeDC-wmrZ1JSxbsUenKNP_VWW32mrCun2pY3I,1070
11
11
  rbx/box/contest/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  rbx/box/contest/build_contest_statements.py,sha256=qar6h5u1xAlOKHl3P7-A1d0aYtaqu4PspGw_DYUGOLY,11260
13
- rbx/box/contest/contest_package.py,sha256=-eHk6CfJ083jZptWCjR0F4x_r9qsmKeSOUvK0QnDyA4,2393
13
+ rbx/box/contest/contest_package.py,sha256=GhZZSOC95wbypyeKFDgUIaaTjhldASudQBvjvurHbDw,2623
14
14
  rbx/box/contest/contest_utils.py,sha256=vWv4iNWdJT5motAznfdNzl8o-tEoCU4xmdyaPTPJZuY,490
15
- rbx/box/contest/main.py,sha256=BKq_n60BstfSHakvNDUP9lu5LNptx38EN8fYPduS8pU,6171
15
+ rbx/box/contest/main.py,sha256=FeTe_Qz9t233dTe1QQ6DxMDq5mcXHx59x9vgaLM8vTY,6976
16
16
  rbx/box/contest/schema.py,sha256=rxzjJasMWPKKhvSJs4eW4A2oCiA4gXgfF-MzqsbPslQ,4914
17
- rbx/box/contest/statements.py,sha256=iG_E4bqhc7hof4LlAliw4m9VO8HcDzvnhsWmxzVRQM4,2690
17
+ rbx/box/contest/statements.py,sha256=cRK5ndsywotUm66QqvtReqVAc-fW4c-2OZufK3Xd8_c,2947
18
18
  rbx/box/creation.py,sha256=mVHVVj8ozX9D5qpkLewE5WSjF6HtTm74Pkwubk-bATg,2259
19
19
  rbx/box/download.py,sha256=MFP-R26JiYGAP89I0TK-0fYc69Fsd20tsBqgtRCy5AE,2234
20
- rbx/box/environment.py,sha256=i-IGHDyFDaYF1fyr0QHdGZ3z4NWp5sj6nxsbnxhAYq4,10924
20
+ rbx/box/environment.py,sha256=GjwnJwtkTdkHmUh1b23zagjLsTiJQAOpP36A93mA-zc,11159
21
21
  rbx/box/extensions.py,sha256=p0iLaU28KswOBDX2HGVO_dR2gk-JSAWb6sXC6GZ1d0w,738
22
- rbx/box/generators.py,sha256=27Q8sVWTG4d8UT6Si-YJ83zZ3jEnu1Urmk-audRYoFk,16111
22
+ rbx/box/generators.py,sha256=lKX_50Yf4bretHxgGzEZxKV7LFjpweaADC-22WEpLY4,16302
23
23
  rbx/box/generators_test.py,sha256=mQqHepAMYa6zV_PseQALI0nIX6AdQktt6lh94muFhNw,1758
24
- rbx/box/main.py,sha256=DA0B_OmWmeWj_xpNEKs5syQEmg2_zbOrTGZVVK3eW0o,13639
25
- rbx/box/package.py,sha256=UR3BxVbTZ6eYJfdsTTx4l8FfdZ1wsnaHAmAdMr07lww,10302
24
+ rbx/box/main.py,sha256=id1YpYKb7GHWoEMdiRAwfT08kY-ol52ulGcaCaPhtqw,16411
25
+ rbx/box/package.py,sha256=Hds0WIvhAWXnYOmIXGULcLLVeFM1YkllOPSKHB5QSkk,10532
26
26
  rbx/box/packaging/boca/extension.py,sha256=hQhcbocNfW2ESv5RalS1wf6uvOoOfOnR_gHvbXUbSzY,852
27
27
  rbx/box/packaging/boca/packager.py,sha256=FOhSRg5K5Y4qNB0WyTR3DKgrpObf9I0JbyGpJHOtxpo,10673
28
28
  rbx/box/packaging/contest_main.py,sha256=ypiBS8dd0yCqoFJIqiK1Fo02dQmUB_G-Z7G926jomrk,2746
@@ -31,23 +31,23 @@ rbx/box/packaging/packager.py,sha256=suCT_SLnWa915rV2j8VFqzH43HGKRTr9mGGlrvj45aw
31
31
  rbx/box/packaging/polygon/packager.py,sha256=CcjHzDr4MwSyp270gsPY6RWoz8bUJeykDqXPvQ3XZ1U,10773
32
32
  rbx/box/packaging/polygon/test.py,sha256=bgEju5PwudgyfwxXJagm8fM6CJVlWM6l_-2q1V-oKaQ,3069
33
33
  rbx/box/packaging/polygon/xml_schema.py,sha256=-r24bCeRMGLrGGoT9FIgmqr87xHL-JzrFaR6bztbYtw,2703
34
- rbx/box/presets/__init__.py,sha256=6IHzqczfiuCdvuXWInimYd9sVMYUQR12BlFaxdr39hI,16721
34
+ rbx/box/presets/__init__.py,sha256=Wiegp1onXPaZs8RE1J3PKT5j3PFWKw2U2rkgOSbnYeM,17529
35
35
  rbx/box/presets/fetch.py,sha256=F-BCOlvEBEyDqtOhiDuGPn4EDtA4Bwm-fqHJ7zZGlW8,1975
36
36
  rbx/box/presets/lock_schema.py,sha256=6sRPnyePOC8yy-5WcD5JRZdDJHf8loqbvpQ1IPiOU9s,349
37
37
  rbx/box/presets/schema.py,sha256=mZmSPkQsw7eQM0lQN6er1MO_LiW1ObwwAZFDK0F5fxE,1962
38
38
  rbx/box/schema.py,sha256=RBQmFxr35LfaRwbxR6WezeQQ3twqh9VS3tn20rlRm_I,12580
39
- rbx/box/solutions.py,sha256=KJVflmvNudvuT8Tra-V6L7iGfPLCk5HbKQb3rMqk0aY,26439
39
+ rbx/box/solutions.py,sha256=zfRawpEufWOI2uw7jNFb86bx8HwU5UaiwQMUj9KlDBE,27447
40
40
  rbx/box/solutions_test.py,sha256=DetQj7ZVnV3tBXBpCrFeK_Yv3XUtdEf29y_6qnyeyfY,1496
41
41
  rbx/box/statements/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
- rbx/box/statements/build_statements.py,sha256=YInK5Upc8rT-DCVCXe_3ZBosHn0Zw19m3Pv4wUmqKL4,11678
42
+ rbx/box/statements/build_statements.py,sha256=24ZUM5H33NhmqnDL0ofA1anUKWxuZG1paA1tCV4AKns,11911
43
43
  rbx/box/statements/builders.py,sha256=W3VkmtjfzrT5MkIVXgfR9fM-OWK007ihm5hfzvp9cfc,10474
44
44
  rbx/box/statements/joiners.py,sha256=ZbxomnMjEFT8yf5WSWUB4tBa3DL3AhjGEuh8uqHyDdg,2837
45
45
  rbx/box/statements/latex.py,sha256=LkcHwXjMFxbw--Gj9T1VkFKQFsXhY9dN7xZHpZycNW8,1346
46
46
  rbx/box/statements/latex_jinja.py,sha256=7WBfn1h8DpqCAmSE6Av64HfURMnJ2AO4QX1CD72sz5E,7096
47
47
  rbx/box/statements/schema.py,sha256=g3KgBn4nIqx-0utH8R2FCqPmJP969chhYfn96chQgd4,3851
48
- rbx/box/stresses.py,sha256=mbKa70QyPOmgXfhGcArTKsdXjH0PFDVmydY3W8bgZlk,10596
48
+ rbx/box/stresses.py,sha256=DUc8TWFlVXUvh-iaBnvwVNmgoCi1puOJiKTHMP8COds,10595
49
49
  rbx/box/stressing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
- rbx/box/stressing/finder_parser.py,sha256=Hw9m2viRGnVqVgO39D51s2qAEjZK1LKWe94rkqCx7VA,11919
50
+ rbx/box/stressing/finder_parser.py,sha256=syZGg_Wm762jAe2eN0oBCNeiM0H8lfZRWT5HKOXpkuQ,11917
51
51
  rbx/box/stressing/generator_parser.py,sha256=oHZryjR3YohgaSO9WEirQ7b2e-98WgZStF0N99W4Thw,7380
52
52
  rbx/box/testcases.py,sha256=bi7T5xXkwyWOkoI6ILcaf2gSSVuuNtZjhP5yL0DJAu4,1452
53
53
  rbx/box/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -55,10 +55,10 @@ rbx/box/ui/captured_log.py,sha256=ptICDPViVnz-_2NfrcB0SSBXNW5L74zI-vAZNN7kSok,11
55
55
  rbx/box/ui/css/app.tcss,sha256=apd5PkPEvl5jK3kE2qrxPyVED1VnvSsj08QQwzUPwEA,786
56
56
  rbx/box/ui/main.py,sha256=b0rHcBF42W4AOCv7WhtiGf_rUnY0yxpqO5oj3wfR4R4,984
57
57
  rbx/box/ui/run.py,sha256=wMEXrEFdQvMHz2hRKAFIithTnTtaL0kNQZu0jKmb8jI,7060
58
- rbx/box/validators.py,sha256=KFpBFtyTfIwjQjW3_qvyBoGQqJ06ELo5kuQGNz4Pi0s,7222
58
+ rbx/box/validators.py,sha256=pRBYCJRbA_7FrthJG9tNHQyTxq7yBNuu8lTgd7irc3s,8309
59
59
  rbx/box/validators_test.py,sha256=hriR6rD32Ouu64eKYYTPLZVvqMxXj7Q2h1l_JAefL7U,344
60
60
  rbx/checker.py,sha256=pj1jO3my48ru-qugbER5onccANCjoR0-PaFe3H3VGEY,4118
61
- rbx/clone.py,sha256=Fyjnr3Bp0xmUIZH6DkAGcGc2n3P5PDdoVszIYJbUmwM,6745
61
+ rbx/clone.py,sha256=wpHyED0_7ST7LD3vj7HjXhzqEzlwh6dRQvKQVDYhGeU,6744
62
62
  rbx/config.py,sha256=2B0PwgDaLjfs5PI8-kfDia6UVOAJq4rpWvb8VmweSXg,7360
63
63
  rbx/conftest.py,sha256=ouilbOIpvX8jTEdCAiWT85CbdBQKUUf41BjmDI82u-Y,967
64
64
  rbx/console.py,sha256=l0iulQH3_jQEm455W66TbDtC4a8owkWTHIIQpJaXofQ,715
@@ -69,7 +69,7 @@ rbx/grading/caching.py,sha256=oGPnKpk9NIUJKwDMsPbEF0bMUtjHN8CEeAOvCzfM5dk,11848
69
69
  rbx/grading/conftest.py,sha256=iN9LUG1IQqhK5JjkctcP68v6675oYsiD2sQSgyLMTqw,960
70
70
  rbx/grading/judge/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
71
  rbx/grading/judge/cacher.py,sha256=W4bdPIvI8tXOl8A7B4Ncuzi9-zXCp0DX40i1XKQGOAU,17761
72
- rbx/grading/judge/digester.py,sha256=hsstwtj302SYgQPeHVtA-7qOupMN-pMUGC6pNuWsrgs,826
72
+ rbx/grading/judge/digester.py,sha256=m6o-kjwyFOXKdImUXtVbdMHhwrgrXk8FDnJFVefnTIw,951
73
73
  rbx/grading/judge/sandbox.py,sha256=0h3YCmGabf9OfORJgx6v2Bed4kE-i8FyuZkPux-sDVk,23569
74
74
  rbx/grading/judge/sandboxes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
75
  rbx/grading/judge/sandboxes/isolate.py,sha256=9xgBuNfAvGtO2zME1FXRah2rcPvzDShsPG0TTuX_UDU,25649
@@ -78,7 +78,7 @@ rbx/grading/judge/sandboxes/timeit.py,sha256=rOQnxf90hhTrWOQ1Mt57HNhZAAn1jDULtMj
78
78
  rbx/grading/judge/storage.py,sha256=FirqjwDqb0m0h2OTFyWrZL7CQ4XjZNxhqB4JpnDIhZY,9485
79
79
  rbx/grading/judge/test.py,sha256=ll0Iw7zyOpGdKPD_PGH7dvUkb4stQLu-ikbQnqJvuAc,944
80
80
  rbx/grading/judge/testiso.py,sha256=v14DtkWiZFJ9AKMzrb0_vZKPWDt8jz8iIw1Z2O-Advk,1397
81
- rbx/grading/steps.py,sha256=VyJnbWl7S0rpkMupN2Os5zjfvEvI2XlcaSt8cNRjgUQ,16398
81
+ rbx/grading/steps.py,sha256=pJM3Xnog6ZXia2xvWMeJmNij_ipRg2hec_WBf1n5O44,18598
82
82
  rbx/grading/steps_with_caching.py,sha256=C_IA_dStxp6poJyGggFFyEouU9y_739UOLKUiJITb8M,1489
83
83
  rbx/grading/steps_with_caching_run_test.py,sha256=nRzB4OcXkb-kQ4WCj0iTGVfBACllxZ0Ek5RSwfoJRgo,15262
84
84
  rbx/grading_utils.py,sha256=lL2KtSkOsMElqrRoApQTbFcqVOeHVWUDTMCa3IsLpC4,4484
@@ -157,8 +157,8 @@ rbx/testdata/caching/executable.py,sha256=WKRHNf_fprFJd1Fq1ubmQtR3mZzTYVNwKPLWuZ
157
157
  rbx/testdata/compatible,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
158
158
  rbx/testing_utils.py,sha256=ZZLKMUHlZ4HwsuNY50jqSBJ9HhpnFdba7opjDsvXE1U,2084
159
159
  rbx/utils.py,sha256=wy8zQRb97n3lptilK7UxM4A44KjXb1W5Z1tD61a-K4A,4173
160
- rbx_cp-0.5.12.dist-info/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
161
- rbx_cp-0.5.12.dist-info/METADATA,sha256=vSW5k13xLbM7Ud4mL8yxEF3Ehp8ZRmJ4bC0WNyLZNYs,3254
162
- rbx_cp-0.5.12.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
163
- rbx_cp-0.5.12.dist-info/entry_points.txt,sha256=qBTLBOeifT1F00LWaEewRRE_jQPgvH7BUdJfZ-dYsFU,57
164
- rbx_cp-0.5.12.dist-info/RECORD,,
160
+ rbx_cp-0.5.14.dist-info/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
161
+ rbx_cp-0.5.14.dist-info/METADATA,sha256=1dncSdLwt6q_zdoeysRdfAlZVk4wfHXA4cDojM5d3kA,3249
162
+ rbx_cp-0.5.14.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
163
+ rbx_cp-0.5.14.dist-info/entry_points.txt,sha256=qBTLBOeifT1F00LWaEewRRE_jQPgvH7BUdJfZ-dYsFU,57
164
+ rbx_cp-0.5.14.dist-info/RECORD,,