rbx.cp 0.5.65__py3-none-any.whl → 0.5.67__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/checkers.py CHANGED
@@ -91,8 +91,10 @@ def _check_pre_output(run_log: Optional[RunLog]) -> CheckerResult:
91
91
 
92
92
  if run_log.exitstatus in [SandboxBase.EXIT_SIGNAL, SandboxBase.EXIT_NONZERO_RETURN]:
93
93
  return CheckerResult(outcome=Outcome.RUNTIME_ERROR)
94
- if run_log.exitstatus in [SandboxBase.EXIT_TIMEOUT, SandboxBase.EXIT_TIMEOUT_WALL]:
94
+ if run_log.exitstatus == SandboxBase.EXIT_TIMEOUT:
95
95
  return CheckerResult(outcome=Outcome.TIME_LIMIT_EXCEEDED)
96
+ if run_log.exitstatus == SandboxBase.EXIT_TIMEOUT_WALL:
97
+ return CheckerResult(outcome=Outcome.IDLENESS_LIMIT_EXCEEDED)
96
98
  if run_log.exitstatus == SandboxBase.EXIT_MEMORY_LIMIT_EXCEEDED:
97
99
  return CheckerResult(outcome=Outcome.MEMORY_LIMIT_EXCEEDED)
98
100
  if run_log.exitstatus == SandboxBase.EXIT_SANDBOX_ERROR:
@@ -103,7 +105,7 @@ def _check_pre_output(run_log: Optional[RunLog]) -> CheckerResult:
103
105
 
104
106
 
105
107
  def _convert_tle(result: CheckerResult, run_log: Optional[RunLog]) -> CheckerResult:
106
- if result.outcome == Outcome.TIME_LIMIT_EXCEEDED:
108
+ if result.outcome.is_slow():
107
109
  # This already is a TLE outcome.
108
110
  return result
109
111
  is_sanitized = (
rbx/box/cli.py CHANGED
@@ -32,7 +32,6 @@ from rbx.box.packaging import main as packaging
32
32
  from rbx.box.schema import CodeItem, ExpectedOutcome, TestcaseGroup
33
33
  from rbx.box.solutions import (
34
34
  estimate_time_limit,
35
- expand_solutions,
36
35
  get_exact_matching_solutions,
37
36
  get_matching_solutions,
38
37
  pick_solutions,
@@ -137,6 +136,13 @@ def ui():
137
136
  ui_pkg.start()
138
137
 
139
138
 
139
+ @app.command('diff', hidden=True)
140
+ def diff(path1: pathlib.Path, path2: pathlib.Path):
141
+ from rbx.box.ui import main as ui_pkg
142
+
143
+ ui_pkg.start_differ(path1, path2)
144
+
145
+
140
146
  @app.command('serve', hidden=True)
141
147
  def serve():
142
148
  from textual_serve.server import Server
@@ -243,7 +249,7 @@ async def run(
243
249
  tracked_solutions = set(
244
250
  await pick_solutions(
245
251
  tracked_solutions,
246
- extra_solutions=await expand_solutions(solutions or []),
252
+ extra_solutions=solutions,
247
253
  )
248
254
  )
249
255
  if not tracked_solutions:
@@ -479,7 +485,7 @@ async def irun(
479
485
  tracked_solutions = set(
480
486
  await pick_solutions(
481
487
  tracked_solutions,
482
- extra_solutions=await expand_solutions(solutions or []),
488
+ extra_solutions=solutions,
483
489
  )
484
490
  )
485
491
  if not tracked_solutions:
@@ -546,18 +552,17 @@ def create(
546
552
  @syncer.sync
547
553
  async def stress(
548
554
  name: Annotated[
549
- str,
555
+ Optional[str],
550
556
  typer.Argument(
551
- help='Name of the stress test to run (specified in problem.rbx.yml), '
552
- 'or the generator to run, in case -g is specified.'
557
+ help='Name of the stress test to run (specified in problem.rbx.yml).'
553
558
  ),
554
- ],
559
+ ] = None,
555
560
  generator_args: Annotated[
556
561
  Optional[str],
557
562
  typer.Option(
558
563
  '--generator',
559
564
  '-g',
560
- help='Run generator [name] with these args.',
565
+ help='Generator call to use to generate a single test for execution.',
561
566
  ),
562
567
  ] = None,
563
568
  finder: Annotated[
@@ -604,9 +609,9 @@ async def stress(
604
609
 
605
610
  with utils.StatusProgress('Running stress...') as s:
606
611
  report = await stresses.run_stress(
607
- name,
608
612
  timeout,
609
- args=generator_args,
613
+ name=name,
614
+ generator_call=generator_args,
610
615
  finder=finder,
611
616
  findingsLimit=findings,
612
617
  progress=s,
rbx/box/compile.py CHANGED
@@ -3,7 +3,7 @@ import pathlib
3
3
  import typer
4
4
 
5
5
  from rbx import annotations, console
6
- from rbx.box import code, package
6
+ from rbx.box import code, package, remote
7
7
  from rbx.box.code import SanitizationLevel
8
8
  from rbx.box.sanitizers import warning_stack
9
9
  from rbx.box.schema import CodeItem
@@ -35,6 +35,8 @@ def _compile(item: CodeItem, sanitized: SanitizationLevel, warnings: bool):
35
35
  def any(path: str, sanitized: bool = False, warnings: bool = False):
36
36
  pkg = package.find_problem_package_or_die()
37
37
 
38
+ path = str(remote.expand_file(path))
39
+
38
40
  solution = package.get_solution_or_nil(path)
39
41
  if solution is not None:
40
42
  _compile(
rbx/box/formatting.py CHANGED
@@ -1,10 +1,14 @@
1
1
  import os
2
2
  import pathlib
3
- from typing import Optional
3
+ from typing import Any, Optional
4
4
 
5
5
  from rbx.box import setter_config
6
6
 
7
7
 
8
+ def ref(text: Any) -> str:
9
+ return f'[item]{text}[/item]'
10
+
11
+
8
12
  def href(url: os.PathLike[str], text: Optional[str] = None, style: str = 'item') -> str:
9
13
  custom_text = False
10
14
  if text is None:
rbx/box/generators.py CHANGED
@@ -110,7 +110,10 @@ def get_all_built_testcases() -> Dict[str, List[Testcase]]:
110
110
 
111
111
 
112
112
  def get_call_from_string(call_str: str) -> GeneratorCall:
113
- name, args = call_str.split(None, 1)
113
+ try:
114
+ name, args = call_str.split(None, 1)
115
+ except ValueError:
116
+ return GeneratorCall(name=call_str, args='')
114
117
  return GeneratorCall(name=name, args=args)
115
118
 
116
119
 
@@ -317,10 +320,7 @@ async def generate_output_for_testcase(
317
320
  capture_pipes=True,
318
321
  )
319
322
 
320
- if (
321
- eval.result.outcome == Outcome.TIME_LIMIT_EXCEEDED
322
- and eval.result.no_tle_outcome == Outcome.ACCEPTED
323
- ):
323
+ if eval.result.outcome.is_slow() and eval.result.no_tle_outcome == Outcome.ACCEPTED:
324
324
  console.console.print(
325
325
  f'[warning]Testcase [item]{testcase.inputPath}[/item] finished in TLE, but test was generated successfully.[/warning]'
326
326
  )
rbx/box/package.py CHANGED
@@ -21,6 +21,7 @@ from rbx.box.schema import (
21
21
  Package,
22
22
  Solution,
23
23
  Stress,
24
+ TaskType,
24
25
  TestcaseGroup,
25
26
  TestcaseSubgroup,
26
27
  )
@@ -32,6 +33,7 @@ from rbx.grading.judge.storage import FilesystemStorage, Storage
32
33
 
33
34
  YAML_NAME = 'problem.rbx.yml'
34
35
  _DEFAULT_CHECKER = 'wcmp.cpp'
36
+ _NOOP_CHECKER = 'noop.cpp'
35
37
  TEMP_DIR = None
36
38
  CACHE_STEP_VERSION = 1
37
39
 
@@ -150,6 +152,19 @@ def get_problem_cache_dir(root: pathlib.Path = pathlib.Path()) -> pathlib.Path:
150
152
  return cache_dir
151
153
 
152
154
 
155
+ @functools.cache
156
+ def get_problem_remote_dir(
157
+ platform: Optional[str] = None, root: pathlib.Path = pathlib.Path()
158
+ ) -> pathlib.Path:
159
+ remote_dir = get_problem_cache_dir(root) / '.remote'
160
+ remote_dir.mkdir(parents=True, exist_ok=True)
161
+
162
+ if platform is not None:
163
+ remote_dir = remote_dir / platform
164
+ remote_dir.mkdir(parents=True, exist_ok=True)
165
+ return remote_dir
166
+
167
+
153
168
  @functools.cache
154
169
  def get_problem_storage_dir(root: pathlib.Path = pathlib.Path()) -> pathlib.Path:
155
170
  storage_dir = get_problem_cache_dir(root) / '.storage'
@@ -277,13 +292,19 @@ def get_validator_or_nil(root: pathlib.Path = pathlib.Path()) -> Optional[CodeIt
277
292
  return package.validator
278
293
 
279
294
 
295
+ @functools.cache
296
+ def get_default_checker(root: pathlib.Path = pathlib.Path()) -> CodeItem:
297
+ package = find_problem_package_or_die(root)
298
+ if package.type == TaskType.COMMUNICATION:
299
+ return CodeItem(path=get_builtin_checker(_NOOP_CHECKER).absolute())
300
+ return CodeItem(path=get_builtin_checker(_DEFAULT_CHECKER).absolute())
301
+
302
+
280
303
  @functools.cache
281
304
  def get_checker(root: pathlib.Path = pathlib.Path()) -> CodeItem:
282
305
  package = find_problem_package_or_die(root)
283
306
 
284
- return package.checker or CodeItem(
285
- path=get_builtin_checker(_DEFAULT_CHECKER).absolute()
286
- )
307
+ return package.checker or get_default_checker(root)
287
308
 
288
309
 
289
310
  @functools.cache
@@ -4,6 +4,7 @@ import hashlib
4
4
  import os
5
5
  import pathlib
6
6
  import re
7
+ import shutil
7
8
  import typing
8
9
  from typing import Any, NoReturn, Optional, Tuple
9
10
 
@@ -248,13 +249,51 @@ class BocaUploader:
248
249
  '[error]Persistent error while uploading problem to BOCA website.[/error]'
249
250
  )
250
251
  console.console.print(
251
- '[warning]This might be caused by PHP max upload size limit (which usually defaults to 2MBF).[/warning]'
252
+ '[warning]This might be caused by PHP max upload size limit (which usually defaults to 2MB).[/warning]'
252
253
  )
253
254
  console.console.print(
254
255
  '[warning]Check [item]https://www.php.net/manual/en/ini.core.php#ini.sect.file-uploads[/item] for more information.[/warning]'
255
256
  )
256
257
  raise typer.Exit(1)
257
258
 
259
+ def download_run(self, run_number: int, site_number: int, into_dir: pathlib.Path):
260
+ url = f'{self.base_url}/admin/runedit.php?runnumber={run_number}&runsitenumber={site_number}'
261
+ _, html = self.open(
262
+ url,
263
+ error_msg=f'Error while downloading BOCA run [item]{run_number}-{site_number}[/item]',
264
+ )
265
+
266
+ soup = BeautifulSoup(html, 'html.parser')
267
+ rows = soup.select('tr')
268
+
269
+ href: Optional[str] = None
270
+ filename: Optional[pathlib.Path] = None
271
+
272
+ for row in rows:
273
+ row_text = row.select('td')[0].text.strip().lower()
274
+ if row_text != "team's code:":
275
+ continue
276
+ link_col = row.select_one('td:nth-of-type(2) a:nth-of-type(1)')
277
+ if link_col is None:
278
+ continue
279
+ href = str(link_col.attrs['href'])
280
+ filename = pathlib.Path(link_col.text.strip())
281
+ break
282
+
283
+ if href is None or filename is None:
284
+ self.raw_error(
285
+ "Error while downloading run:\nNo link to team's code found."
286
+ )
287
+
288
+ link = self.br.find_link(url=href)
289
+ tmp_file, _ = self.br.retrieve(link.absolute_url)
290
+ if tmp_file is None:
291
+ self.raw_error('Error while downloading run:\nDownloaded file is None.')
292
+ final_path = into_dir / filename.with_stem(f'{run_number}-{site_number}')
293
+ final_path.parent.mkdir(parents=True, exist_ok=True)
294
+ shutil.move(tmp_file, final_path)
295
+ return final_path
296
+
258
297
 
259
298
  @functools.lru_cache
260
299
  def get_boca_uploader(
@@ -3,7 +3,6 @@ import shutil
3
3
  import tempfile
4
4
  from typing import Annotated, Iterable, List, Optional, Sequence, Union
5
5
 
6
- import questionary
7
6
  import rich
8
7
  import rich.prompt
9
8
  import typer
@@ -498,6 +497,8 @@ def copy_local_preset(
498
497
  if preset_remote_uri is None:
499
498
  return
500
499
 
500
+ import questionary
501
+
501
502
  add_submodule = questionary.confirm(
502
503
  'The preset is installed from a remote Git repository. Do you want to add it as a submodule of your project?',
503
504
  default=False,
rbx/box/remote.py ADDED
@@ -0,0 +1,151 @@
1
+ import pathlib
2
+ import re
3
+ from abc import ABC, abstractmethod
4
+ from typing import List, Optional, Tuple, Union
5
+
6
+ import typer
7
+
8
+ from rbx import console
9
+ from rbx.box import cd, package
10
+ from rbx.box.formatting import href, ref
11
+
12
+ PathLike = Union[str, pathlib.Path]
13
+
14
+
15
+ class Expander(ABC):
16
+ def get_remote_path(self, path: pathlib.Path) -> pathlib.Path:
17
+ return package.get_problem_remote_dir() / path
18
+
19
+ def cacheable_paths(self, path: pathlib.Path) -> List[pathlib.Path]:
20
+ return []
21
+
22
+ def cacheable_globs(self, path: pathlib.Path) -> List[str]:
23
+ return []
24
+
25
+ @abstractmethod
26
+ def expand(self, path: pathlib.Path) -> Optional[pathlib.Path]:
27
+ pass
28
+
29
+
30
+ class BocaExpander(Expander):
31
+ BOCA_REGEX = re.compile(r'\@boca\/(\d+)(?:\-(\d+))?')
32
+
33
+ def get_match(self, path_str: str) -> Optional[Tuple[int, int]]:
34
+ match = self.BOCA_REGEX.match(path_str)
35
+ if match is None:
36
+ return None
37
+ run_number = int(match.group(1))
38
+ site_number = int(match.group(2)) if match.group(2) is not None else 1
39
+ return run_number, site_number
40
+
41
+ def get_boca_folder(self) -> pathlib.Path:
42
+ return self.get_remote_path(pathlib.Path('boca'))
43
+
44
+ def get_boca_path(self, run_number: int, site_number: int) -> pathlib.Path:
45
+ return self.get_boca_folder() / f'{run_number}-{site_number}'
46
+
47
+ def cacheable_globs(self, path: pathlib.Path) -> List[str]:
48
+ match = self.get_match(str(path))
49
+ if match is None:
50
+ return []
51
+ run_number, site_number = match
52
+ return [str(self.get_boca_path(run_number, site_number)) + '.*']
53
+
54
+ def expand(self, path: pathlib.Path) -> Optional[pathlib.Path]:
55
+ from rbx.box.packaging.boca import upload as boca_upload
56
+
57
+ match = self.get_match(str(path))
58
+ if match is None:
59
+ return None
60
+ run_number, site_number = match
61
+
62
+ boca_uploader = boca_upload.get_boca_uploader()
63
+ boca_uploader.login()
64
+ sol_path = boca_uploader.download_run(
65
+ run_number, site_number, self.get_boca_folder()
66
+ )
67
+ console.console.print(f'Downloaded {href(sol_path)} from BOCA...')
68
+ return sol_path
69
+
70
+
71
+ REGISTERED_EXPANDERS: List['Expander'] = [
72
+ BocaExpander(),
73
+ ]
74
+
75
+
76
+ def _relative_to_pkg(path: pathlib.Path) -> pathlib.Path:
77
+ return path.resolve().relative_to(pathlib.Path.cwd())
78
+
79
+
80
+ def _try_cacheable_paths(
81
+ path: pathlib.Path, expander: Expander
82
+ ) -> Optional[pathlib.Path]:
83
+ cached_paths = expander.cacheable_paths(path)
84
+ for cached_path in cached_paths:
85
+ if cached_path.exists():
86
+ return _relative_to_pkg(cached_path)
87
+ return None
88
+
89
+
90
+ def _try_cacheable_globs(
91
+ path: pathlib.Path, expander: Expander
92
+ ) -> Optional[pathlib.Path]:
93
+ cached_globs = expander.cacheable_globs(path)
94
+ for cached_glob in cached_globs:
95
+ rel_glob = _relative_to_pkg(pathlib.Path(cached_glob))
96
+ globbed = list(pathlib.Path.cwd().glob(str(rel_glob)))
97
+ if not globbed:
98
+ continue
99
+ return _relative_to_pkg(globbed[0])
100
+ return None
101
+
102
+
103
+ def _try_cache(path: pathlib.Path, expander: Expander) -> Optional[pathlib.Path]:
104
+ cached = _try_cacheable_paths(path, expander)
105
+ if cached is not None:
106
+ return cached
107
+ return _try_cacheable_globs(path, expander)
108
+
109
+
110
+ def _expand_path(path: pathlib.Path) -> Optional[pathlib.Path]:
111
+ if not cd.is_problem_package():
112
+ console.console.print(
113
+ f'Skipping expansion of {ref(path)} because we are not in a problem package.'
114
+ )
115
+ raise typer.Exit(1)
116
+
117
+ for expander in REGISTERED_EXPANDERS:
118
+ cached = _try_cache(path, expander)
119
+ if cached is not None:
120
+ return cached
121
+ expanded = expander.expand(path)
122
+ if expanded is not None:
123
+ return _relative_to_pkg(expanded)
124
+ return None
125
+
126
+
127
+ def _expand_paths(paths: List[pathlib.Path]) -> List[pathlib.Path]:
128
+ res = []
129
+ for path in paths:
130
+ if not str(path).startswith('@'):
131
+ res.append(path)
132
+ continue
133
+ expanded = _expand_path(path)
134
+ if expanded is None:
135
+ continue
136
+ res.append(expanded)
137
+ return res
138
+
139
+
140
+ def expand_files(files: List[str]) -> List[pathlib.Path]:
141
+ return _expand_paths([pathlib.Path(file) for file in files])
142
+
143
+
144
+ def expand_file(file: str) -> pathlib.Path:
145
+ res = expand_files([file])
146
+ if len(res) != 1:
147
+ console.console.print(
148
+ f'Could not expand {ref(file)} because it is not a valid expansion.'
149
+ )
150
+ raise typer.Exit(1)
151
+ return res[0]
rbx/box/retries.py CHANGED
@@ -18,10 +18,7 @@ def _both_accepted(eval_a: Evaluation, eval_b: Evaluation) -> bool:
18
18
 
19
19
 
20
20
  def _any_tle(eval_a: Evaluation, eval_b: Evaluation) -> bool:
21
- return (
22
- eval_a.result.outcome == Outcome.TIME_LIMIT_EXCEEDED
23
- or eval_b.result.outcome == Outcome.TIME_LIMIT_EXCEEDED
24
- )
21
+ return eval_a.result.outcome.is_slow() or eval_b.result.outcome.is_slow()
25
22
 
26
23
 
27
24
  def _get_faster(eval_a: Evaluation, eval_b: Evaluation) -> Evaluation:
@@ -130,13 +127,10 @@ class Retrier:
130
127
 
131
128
  def should_repeat(self, eval: Evaluation) -> bool:
132
129
  if self.is_stress:
133
- if (
134
- eval.result.outcome == Outcome.TIME_LIMIT_EXCEEDED
135
- and self.retries_for_stress > 0
136
- ):
130
+ if eval.result.outcome.is_slow() and self.retries_for_stress > 0:
137
131
  self.retries_for_stress -= 1
138
132
  return True
139
- if eval.result.outcome == Outcome.TIME_LIMIT_EXCEEDED and self.retries > 0:
133
+ if eval.result.outcome.is_slow() and self.retries > 0:
140
134
  self.retries -= 1
141
135
  return True
142
136
  if self.reps > 0:
rbx/box/schema.py CHANGED
@@ -57,6 +57,9 @@ def expand_var(value: Primitive) -> Primitive:
57
57
 
58
58
 
59
59
  class ExpectedOutcome(AutoEnum):
60
+ ANY = alias('any') # type: ignore
61
+ """Expected outcome for any outcome."""
62
+
60
63
  ACCEPTED = alias('accepted', 'ac', 'correct') # type: ignore
61
64
  """Expected outcome for correct solutions (AC)."""
62
65
 
@@ -97,6 +100,8 @@ class ExpectedOutcome(AutoEnum):
97
100
  Especially useful for environments where TLE and RTE are indistinguishable."""
98
101
 
99
102
  def style(self) -> str:
103
+ if self == ExpectedOutcome.ANY:
104
+ return 'orange'
100
105
  if self == ExpectedOutcome.ACCEPTED:
101
106
  return 'green'
102
107
  if self == ExpectedOutcome.WRONG_ANSWER:
@@ -114,29 +119,39 @@ class ExpectedOutcome(AutoEnum):
114
119
  def is_slow(self) -> bool:
115
120
  return self in [ExpectedOutcome.TIME_LIMIT_EXCEEDED, ExpectedOutcome.TLE_OR_RTE]
116
121
 
122
+ def matches_tle_and_is_incorrect(self) -> bool:
123
+ return self.match(Outcome.TIME_LIMIT_EXCEEDED) and not self.match(
124
+ Outcome.ACCEPTED
125
+ )
126
+
117
127
  def match(self, outcome: Outcome) -> bool:
128
+ if self == ExpectedOutcome.ANY:
129
+ return True
118
130
  if self == ExpectedOutcome.ACCEPTED:
119
131
  return outcome == Outcome.ACCEPTED
120
132
  if self == ExpectedOutcome.ACCEPTED_OR_TLE:
121
- return outcome in {Outcome.ACCEPTED, Outcome.TIME_LIMIT_EXCEEDED}
133
+ return outcome in {Outcome.ACCEPTED} or outcome.is_slow()
122
134
  if self == ExpectedOutcome.WRONG_ANSWER:
123
135
  return outcome == Outcome.WRONG_ANSWER
124
136
  if self == ExpectedOutcome.INCORRECT:
125
- return outcome in {
126
- Outcome.WRONG_ANSWER,
127
- Outcome.RUNTIME_ERROR,
128
- Outcome.MEMORY_LIMIT_EXCEEDED,
129
- Outcome.TIME_LIMIT_EXCEEDED,
130
- Outcome.OUTPUT_LIMIT_EXCEEDED,
131
- }
137
+ return (
138
+ outcome
139
+ in {
140
+ Outcome.WRONG_ANSWER,
141
+ Outcome.RUNTIME_ERROR,
142
+ Outcome.MEMORY_LIMIT_EXCEEDED,
143
+ Outcome.OUTPUT_LIMIT_EXCEEDED,
144
+ }
145
+ or outcome.is_slow()
146
+ )
132
147
  if self == ExpectedOutcome.RUNTIME_ERROR:
133
148
  return outcome == Outcome.RUNTIME_ERROR
134
149
  if self == ExpectedOutcome.TIME_LIMIT_EXCEEDED:
135
- return outcome == Outcome.TIME_LIMIT_EXCEEDED
150
+ return outcome.is_slow()
136
151
  if self == ExpectedOutcome.MEMORY_LIMIT_EXCEEDED:
137
152
  return outcome == Outcome.MEMORY_LIMIT_EXCEEDED
138
153
  if self == ExpectedOutcome.TLE_OR_RTE:
139
- return outcome in {Outcome.TIME_LIMIT_EXCEEDED, Outcome.RUNTIME_ERROR}
154
+ return outcome in {Outcome.RUNTIME_ERROR} or outcome.is_slow()
140
155
  if self == ExpectedOutcome.OUTPUT_LIMIT_EXCEEDED:
141
156
  return outcome == Outcome.OUTPUT_LIMIT_EXCEEDED
142
157
  return False