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
@@ -25,7 +25,11 @@ from rbx.box.statements.schema import (
25
25
  TexToPDF,
26
26
  rbxToTeX,
27
27
  )
28
- from rbx.box.testcase_utils import TestcaseInteraction, parse_interaction
28
+ from rbx.box.testcase_utils import (
29
+ TestcaseInteraction,
30
+ TestcaseInteractionParsingError,
31
+ parse_interaction,
32
+ )
29
33
 
30
34
 
31
35
  @dataclasses.dataclass
@@ -37,20 +41,22 @@ class StatementCodeLanguage:
37
41
 
38
42
  @dataclasses.dataclass
39
43
  class StatementBuilderContext:
44
+ lang: str
40
45
  languages: List[StatementCodeLanguage]
41
46
  params: ConversionStep
42
47
  root: pathlib.Path
43
- editorial: bool
48
+ custom_vars: Optional[Dict[str, Any]] = None
44
49
  vars: Optional[Dict[str, Primitive]] = None
45
50
 
46
51
  def build_jinja_kwargs(self) -> Dict[str, Any]:
47
52
  res = {
53
+ 'lang': self.lang,
48
54
  'languages': self.languages,
49
55
  'keyed_languages': {lang.id: lang for lang in self.languages},
50
- 'is_editorial': self.editorial,
51
56
  }
52
- if self.vars is not None:
53
- res['vars'] = self.vars
57
+ if self.vars is not None or self.custom_vars is not None:
58
+ res['vars'] = self.vars or {}
59
+ res['vars'].update(self.custom_vars or {})
54
60
  return res
55
61
 
56
62
 
@@ -82,7 +88,13 @@ class StatementSample(BaseModel):
82
88
 
83
89
  interaction = None
84
90
  if pio_path.is_file():
85
- interaction = parse_interaction(pio_path)
91
+ try:
92
+ interaction = parse_interaction(pio_path)
93
+ except TestcaseInteractionParsingError as e:
94
+ console.console.print(
95
+ f'Error parsing interactive sample: [error]{e}[/error]'
96
+ )
97
+ raise typer.Exit(1) from e
86
98
 
87
99
  return StatementSample(
88
100
  inputPath=input_path,
@@ -325,10 +337,6 @@ class rbxTeXBuilder(StatementBuilder):
325
337
  )
326
338
  blocks = statement_blocks.blocks
327
339
 
328
- # Remove editorial block when not editorial.
329
- if not context.editorial and 'editorial' in blocks:
330
- del blocks['editorial']
331
-
332
340
  problem_kwargs = problem.build_jinja_kwargs()
333
341
  problem_kwargs['problem']['blocks'] = blocks
334
342
  if statement_blocks.explanations is not None:
@@ -0,0 +1,49 @@
1
+ import collections
2
+ from typing import Any, List, TypeVar
3
+
4
+ from rbx.box.fields import merge_pydantic_models
5
+
6
+ TypeVarT = TypeVar('TypeVarT', bound=Any)
7
+
8
+
9
+ def expand_statements(statements: List[TypeVarT]) -> List[TypeVarT]:
10
+ deg = collections.defaultdict(int)
11
+ dependencies = collections.defaultdict(list)
12
+ for statement in statements:
13
+ if statement.extends is not None:
14
+ deg[statement.name] += 1
15
+ dependencies[statement.extends].append(statement.name)
16
+
17
+ # Topological sort.
18
+ # - We need to expand statements in the order of dependencies.
19
+ # - This is a simple topological sort.
20
+ # - If there are multiple statements with indegree 0, we choose the first one.
21
+ st_per_name = {}
22
+ expanded = {}
23
+ st = []
24
+ for statement in statements:
25
+ st_per_name[statement.name] = statement
26
+ if deg[statement.name] == 0:
27
+ st.append(statement)
28
+
29
+ while st:
30
+ statement = st.pop()
31
+ expanded_statement = statement.model_copy()
32
+ if statement.extends is not None:
33
+ expanded_statement = merge_pydantic_models(
34
+ expanded[statement.extends], statement
35
+ )
36
+
37
+ expanded[statement.name] = expanded_statement
38
+
39
+ for dep_name in dependencies[statement.name]:
40
+ deg[dep_name] -= 1
41
+ if deg[dep_name] == 0:
42
+ st.append(st_per_name[dep_name])
43
+
44
+ if len(expanded) != len(statements):
45
+ raise ValueError(
46
+ f'Failed to expand statements: only {len(expanded)} out of {len(statements)} were expanded. This means there is a cycle introduced by the `extends` field.'
47
+ )
48
+
49
+ return [expanded[statement.name] for statement in statements]
@@ -6,9 +6,10 @@ with Latex.
6
6
  import pathlib
7
7
  import re
8
8
  import typing
9
- from typing import Dict, Tuple, Union
9
+ from typing import Any, Dict, Tuple, Union
10
10
 
11
11
  import jinja2
12
+ import jinja2.runtime
12
13
  import typer
13
14
 
14
15
  from rbx import console
@@ -131,11 +132,56 @@ def path_stem(path: pathlib.Path) -> str:
131
132
  return path.stem
132
133
 
133
134
 
135
+ @jinja2.pass_context
136
+ def test_var_truthy(ctx: jinja2.runtime.Context, value: Any):
137
+ if isinstance(value, jinja2.Undefined):
138
+ return False
139
+ if value is None:
140
+ return False
141
+ return bool(value)
142
+
143
+
144
+ @jinja2.pass_context
145
+ def test_var_falsy(ctx: jinja2.runtime.Context, value: Any):
146
+ return not test_var_truthy(ctx, value)
147
+
148
+
149
+ @jinja2.pass_context
150
+ def test_var_null(ctx: jinja2.runtime.Context, value: Any):
151
+ if isinstance(value, jinja2.Undefined):
152
+ return True
153
+ if value is None:
154
+ return True
155
+ return False
156
+
157
+
158
+ @jinja2.pass_context
159
+ def test_var_nonnull(ctx: jinja2.runtime.Context, value: Any):
160
+ return not test_var_null(ctx, value)
161
+
162
+
134
163
  ######################################################################
135
164
  # Declare module functions
136
165
  ######################################################################
137
166
 
138
167
 
168
+ class StrictChainableUndefined(jinja2.StrictUndefined):
169
+ def __getattr__(self, name: str) -> 'StrictChainableUndefined':
170
+ # Raise AttributeError on requests for names that appear to be unimplemented
171
+ # dunder methods to avoid confusing Python with truthy non-method objects that
172
+ # do not implement the protocol being probed for. e.g., copy.copy(Undefined())
173
+ # fails spectacularly if getattr(Undefined(), '__setstate__') returns an
174
+ # Undefined object instead of raising AttributeError to signal that it does not
175
+ # support that style of object initialization.
176
+ if name[:2] == '__' and name[-2:] == '__':
177
+ raise AttributeError(name)
178
+
179
+ return self
180
+
181
+ def __getitem__(self, _name: str) -> 'StrictChainableUndefined': # type: ignore[override]
182
+ return self
183
+
184
+
139
185
  class JinjaDictWrapper(dict):
140
186
  def __init__(self, *args, key='dict object', **kwargs):
141
187
  super().__init__(*args, **kwargs)
@@ -145,7 +191,9 @@ class JinjaDictWrapper(dict):
145
191
  try:
146
192
  return super().__getitem__(key)
147
193
  except KeyError:
148
- return jinja2.StrictUndefined(hint=f'"{key}" was not found in "{self.key}"')
194
+ return StrictChainableUndefined(
195
+ hint=f'"{key}" was not found in "{self.key}"'
196
+ )
149
197
 
150
198
 
151
199
  def add_builtin_filters(j2_env: jinja2.Environment):
@@ -155,6 +203,13 @@ def add_builtin_filters(j2_env: jinja2.Environment):
155
203
  j2_env.filters['stem'] = path_stem
156
204
 
157
205
 
206
+ def add_builtin_tests(j2_env: jinja2.Environment):
207
+ j2_env.tests['truthy'] = test_var_truthy
208
+ j2_env.tests['falsy'] = test_var_falsy
209
+ j2_env.tests['null'] = test_var_null
210
+ j2_env.tests['nonnull'] = test_var_nonnull
211
+
212
+
158
213
  def render_latex_template(path_templates, template_filename, template_vars=None) -> str:
159
214
  """Render a latex template, filling in its template variables
160
215
 
@@ -168,9 +223,10 @@ def render_latex_template(path_templates, template_filename, template_vars=None)
168
223
  j2_env = jinja2.Environment(
169
224
  loader=jinja2.FileSystemLoader(path_templates),
170
225
  **J2_ARGS,
171
- undefined=jinja2.StrictUndefined,
226
+ undefined=StrictChainableUndefined,
172
227
  )
173
228
  add_builtin_filters(j2_env)
229
+ add_builtin_tests(j2_env)
174
230
  template = j2_env.get_template(template_filename)
175
231
  try:
176
232
  return template.render(**var_dict) # type: ignore
@@ -198,9 +254,10 @@ def render_latex_template_blocks(
198
254
  j2_env = jinja2.Environment(
199
255
  loader=jinja2.FileSystemLoader(path_templates),
200
256
  **J2_ARGS,
201
- undefined=jinja2.StrictUndefined,
257
+ undefined=StrictChainableUndefined,
202
258
  )
203
259
  add_builtin_filters(j2_env)
260
+ add_builtin_tests(j2_env)
204
261
  template = j2_env.get_template(template_filename)
205
262
  ctx = template.new_context(var_dict) # type: ignore
206
263
  try:
@@ -2,11 +2,24 @@ from __future__ import annotations
2
2
 
3
3
  import pathlib
4
4
  from enum import Enum
5
- from typing import List, Literal, Union
5
+ from typing import Annotated, List, Literal, Optional, Union
6
6
 
7
- from pydantic import BaseModel, ConfigDict, Field
7
+ from pydantic import AfterValidator, BaseModel, ConfigDict, Field
8
8
 
9
9
  from rbx.autoenum import AutoEnum, alias
10
+ from rbx.box.fields import FNameField
11
+ from rbx.box.lang import is_valid_lang_code
12
+
13
+
14
+ def validate_statement_language(lang: str):
15
+ if not is_valid_lang_code(lang) or not lang.islower():
16
+ raise ValueError(
17
+ f'Invalid statement language: {lang}. Language must be a valid lowercase ISO 639-1 code.'
18
+ )
19
+ return lang
20
+
21
+
22
+ StatementLanguage = Annotated[str, AfterValidator(validate_statement_language)]
10
23
 
11
24
 
12
25
  ### Conversion types
@@ -95,13 +108,28 @@ class StatementType(AutoEnum):
95
108
  class Statement(BaseModel):
96
109
  model_config = ConfigDict(extra='forbid')
97
110
 
111
+ name: str = FNameField(description='Name of this statement.')
112
+
113
+ extends: Optional[str] = FNameField(
114
+ default=None,
115
+ description='Name of the statement that this statement extends.',
116
+ )
117
+
118
+ language: StatementLanguage = Field(
119
+ default='en', description='Language code of this statement (ISO 639-1).'
120
+ )
121
+
98
122
  title: str = Field(
99
- description='Name of the problem, as it appears in the statement.'
123
+ default='', description='Name of the problem, as it appears in the statement.'
100
124
  )
101
125
 
102
- path: pathlib.Path = Field(description='Path to the input statement file.')
126
+ path: pathlib.Path = Field(
127
+ default_factory=pathlib.Path, description='Path to the input statement file.'
128
+ )
103
129
 
104
- type: StatementType = Field(description='Type of the input statement file.')
130
+ type: StatementType = Field(
131
+ default=StatementType.rbxTeX, description='Type of the input statement file.'
132
+ )
105
133
 
106
134
  steps: List[ConversionStep] = Field(
107
135
  default=[],
@@ -134,7 +162,3 @@ the statement. Files will be included in the same folder as the statement file,
134
162
  their relativeness. Can be glob pattern as well, such as `imgs/*.png`.
135
163
  """,
136
164
  )
137
-
138
- language: str = Field(
139
- default='en', description='Language this is statement is written in.'
140
- )
rbx/box/stats.py ADDED
@@ -0,0 +1,92 @@
1
+ import pathlib
2
+ from typing import Iterable, List, Tuple
3
+
4
+ from rbx import console
5
+ from rbx.box.cd import (
6
+ find_all_ancestor_packages,
7
+ is_contest_package,
8
+ is_problem_package,
9
+ )
10
+ from rbx.box.contest.contest_package import find_contest, find_contest_package_or_die
11
+ from rbx.box.formatting import get_formatted_memory
12
+
13
+
14
+ def find_problem_packages_from_contest(
15
+ root: pathlib.Path = pathlib.Path(),
16
+ ) -> Iterable[pathlib.Path]:
17
+ contest_path = find_contest(root)
18
+ contest = find_contest_package_or_die(contest_path)
19
+ for problem in contest.problems:
20
+ yield contest_path / problem.get_path()
21
+
22
+
23
+ def find_all_reachable_packages(
24
+ root: pathlib.Path = pathlib.Path(),
25
+ ) -> List[pathlib.Path]:
26
+ packages = find_all_ancestor_packages(root)
27
+
28
+ for package in list(packages):
29
+ if is_contest_package(package):
30
+ packages.extend(find_problem_packages_from_contest(package))
31
+ return packages
32
+
33
+
34
+ def find_and_group_all_reachable_packages(
35
+ root: pathlib.Path = pathlib.Path(),
36
+ ) -> Tuple[List[pathlib.Path], List[pathlib.Path]]:
37
+ packages = find_all_reachable_packages(root)
38
+ contest_packages = set(pkg for pkg in packages if is_contest_package(pkg))
39
+ problem_packages = set(pkg for pkg in packages if is_problem_package(pkg))
40
+ return sorted(contest_packages), sorted(problem_packages)
41
+
42
+
43
+ def get_dir_size(path: pathlib.Path) -> int:
44
+ if not path.is_dir():
45
+ return 0
46
+ return sum(
47
+ f.stat().st_size
48
+ for f in path.glob('**/*')
49
+ if f.is_file() and not f.is_symlink()
50
+ )
51
+
52
+
53
+ def get_cache_size(root: pathlib.Path = pathlib.Path()) -> int:
54
+ cache_dir = root / '.box'
55
+ return get_dir_size(cache_dir)
56
+
57
+
58
+ def get_build_size(root: pathlib.Path = pathlib.Path()) -> int:
59
+ build_dir = root / 'build'
60
+ return get_dir_size(build_dir)
61
+
62
+
63
+ def print_package_stats(root: pathlib.Path = pathlib.Path()) -> int:
64
+ if is_contest_package(root):
65
+ console.console.print(f'[status]Contest package[/status]: [item]{root}[/item]')
66
+ else:
67
+ console.console.print(f'[status]Problem package[/status]: [item]{root}[/item]')
68
+
69
+ cache_size = get_cache_size(root)
70
+ build_size = get_build_size(root)
71
+ console.console.print(
72
+ f'[status]Cache size[/status]: [item]{get_formatted_memory(cache_size)}[/item]'
73
+ )
74
+ console.console.print(
75
+ f'[status]Build size[/status]: [item]{get_formatted_memory(build_size)}[/item]'
76
+ )
77
+
78
+ return cache_size + build_size
79
+
80
+
81
+ def print_reachable_package_stats(root: pathlib.Path = pathlib.Path()) -> None:
82
+ contest_packages, problem_packages = find_and_group_all_reachable_packages(root)
83
+ total_size = 0
84
+ for pkg in contest_packages:
85
+ total_size += print_package_stats(pkg)
86
+ console.console.print()
87
+ for pkg in problem_packages:
88
+ total_size += print_package_stats(pkg)
89
+ console.console.print()
90
+ console.console.print(
91
+ f'[status]Total size[/status]: [item]{get_formatted_memory(total_size)}[/item]'
92
+ )
rbx/box/tasks.py CHANGED
@@ -4,7 +4,7 @@ from typing import Optional
4
4
  from rbx.box import checkers, package, state
5
5
  from rbx.box.code import CommunicationItem, run_communication, run_item
6
6
  from rbx.box.environment import EnvironmentSandbox, ExecutionConfig, VerificationLevel
7
- from rbx.box.retries import Retrier
7
+ from rbx.box.retries import Retrier, get_retrier_config
8
8
  from rbx.box.schema import Solution, Testcase
9
9
  from rbx.grading.judge.sandbox import SandboxBase
10
10
  from rbx.grading.limits import Limits
@@ -51,6 +51,7 @@ async def run_solution_on_testcase(
51
51
  use_retries: bool = True,
52
52
  use_timelimit: bool = True,
53
53
  capture_pipes: bool = False,
54
+ nruns: int = 0,
54
55
  ) -> Evaluation:
55
56
  if interactor_digest is not None:
56
57
  return await _run_communication_solution_on_testcase(
@@ -66,6 +67,7 @@ async def run_solution_on_testcase(
66
67
  use_retries=use_retries,
67
68
  use_timelimit=use_timelimit,
68
69
  capture_pipes=capture_pipes,
70
+ nruns=nruns,
69
71
  )
70
72
 
71
73
  async def run_fn(retry_index: int) -> Evaluation:
@@ -132,7 +134,7 @@ async def run_solution_on_testcase(
132
134
  if not use_retries:
133
135
  return await run_fn(0)
134
136
 
135
- retrier = Retrier()
137
+ retrier = Retrier(get_retrier_config(nruns))
136
138
  return await retrier.repeat(run_fn)
137
139
 
138
140
 
@@ -166,6 +168,7 @@ async def _run_communication_solution_on_testcase(
166
168
  use_retries: bool = True,
167
169
  use_timelimit: bool = True,
168
170
  capture_pipes: bool = False,
171
+ nruns: int = 0,
169
172
  ) -> Evaluation:
170
173
  capture_pipes = capture_pipes or state.STATE.debug_logs
171
174
 
@@ -291,5 +294,5 @@ async def _run_communication_solution_on_testcase(
291
294
  if not use_retries:
292
295
  return await run_fn(0)
293
296
 
294
- retrier = Retrier()
297
+ retrier = Retrier(get_retrier_config(nruns))
295
298
  return await retrier.repeat(run_fn)
rbx/box/testcase_utils.py CHANGED
@@ -160,6 +160,10 @@ def fill_output_for_defined_testcase(testcase: Testcase) -> Testcase:
160
160
  return res
161
161
 
162
162
 
163
+ class TestcaseInteractionParsingError(Exception):
164
+ pass
165
+
166
+
163
167
  def parse_interaction(file: pathlib.Path) -> TestcaseInteraction:
164
168
  entries = []
165
169
  with file.open('r') as f:
@@ -167,53 +171,21 @@ def parse_interaction(file: pathlib.Path) -> TestcaseInteraction:
167
171
  interactor_prefix = f.readline().strip()
168
172
  solution_prefix = f.readline().strip()
169
173
  except Exception:
170
- console.console.print(
171
- f'[error]Failed to read interaction file [item]{file}[/item]. Expected the first two lines to be the interactor and solution prefixes.[/error]'
172
- )
173
- raise typer.Exit(1) from None
174
-
175
- # Crop file.
176
- rest = f.read()
177
- start = 0
178
-
179
- def _find_next_prefix(start: int) -> Optional[Tuple[int, int]]:
180
- interactor_idx = rest.find(interactor_prefix, start)
181
- solution_idx = rest.find(solution_prefix, start)
182
- if interactor_idx == -1 and solution_idx == -1:
183
- return None
184
- if interactor_idx == -1:
185
- return (solution_idx, solution_idx + len(solution_prefix))
186
- if solution_idx == -1:
187
- return (interactor_idx, interactor_idx + len(interactor_prefix))
188
- if interactor_idx < solution_idx:
189
- return (interactor_idx, interactor_idx + len(interactor_prefix))
190
- return (solution_idx, solution_idx + len(solution_prefix))
191
-
192
- def _find_next_block() -> Optional[Tuple[int, Tuple[int, int]]]:
193
- prefix = _find_next_prefix(start)
194
- if prefix is None:
195
- return None
196
- prefix_start, prefix_end = prefix
197
- prefix = rest[prefix_start:prefix_end]
198
- pipe = 1 if prefix == solution_prefix else 0
199
-
200
- nxt = _find_next_prefix(prefix_end)
201
- if nxt is None:
202
- return (pipe, (prefix_end, len(rest)))
203
- nxt_start, _ = nxt
204
- return (pipe, (prefix_end, nxt_start))
205
-
206
- # TODO: optimize
207
- blocks = 0
208
- MAX_BLOCKS = 1024
209
- while blocks < MAX_BLOCKS:
210
- block = _find_next_block()
211
- if block is None:
212
- break
213
- pipe, (st, nd) = block
214
- entries.append(TestcaseInteractionEntry(data=rest[st:nd], pipe=pipe))
215
- start = nd
216
- blocks += 1
174
+ raise TestcaseInteractionParsingError(
175
+ f'Failed to read interaction file {file}. Expected the first two lines to be the interactor and solution prefixes.'
176
+ ) from None
177
+
178
+ while line := f.readline().strip():
179
+ if line.startswith(interactor_prefix):
180
+ stripped = line[len(interactor_prefix) :].strip()
181
+ entries.append(TestcaseInteractionEntry(data=stripped, pipe=0))
182
+ elif line.startswith(solution_prefix):
183
+ stripped = line[len(solution_prefix) :].strip()
184
+ entries.append(TestcaseInteractionEntry(data=stripped, pipe=1))
185
+ else:
186
+ raise TestcaseInteractionParsingError(
187
+ f'Invalid line in interaction file {file}. Expected the line to start with the interactor or solution prefix ({interactor_prefix} or {solution_prefix}).'
188
+ ) from None
217
189
 
218
190
  return TestcaseInteraction(
219
191
  prefixes=(interactor_prefix, solution_prefix),
File without changes
File without changes
@@ -0,0 +1,13 @@
1
+ import pathlib
2
+
3
+ import typer
4
+
5
+ from rbx import annotations
6
+ from rbx.box.tooling.boca.scrape import scrape_boca
7
+
8
+ app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
9
+
10
+
11
+ @app.command('scrape', help='Scrape runs from BOCA.')
12
+ def scrape() -> None:
13
+ scrape_boca(pathlib.Path())
@@ -0,0 +1,34 @@
1
+ import pathlib
2
+ from concurrent.futures import ThreadPoolExecutor
3
+
4
+ from rich.progress import MofNCompleteColumn, Progress, SpinnerColumn
5
+
6
+ from rbx.box.tooling.boca.scraper import BocaRun, BocaScraper
7
+
8
+
9
+ def scrape_boca(into_path: pathlib.Path):
10
+ scraper = BocaScraper()
11
+ scraper.login()
12
+ runs = scraper.list_runs()
13
+
14
+ progress = Progress(
15
+ SpinnerColumn(),
16
+ *Progress.get_default_columns(),
17
+ MofNCompleteColumn(),
18
+ transient=True,
19
+ )
20
+ scrape_task = progress.add_task('Scraping runs...', total=len(runs))
21
+ with progress:
22
+
23
+ def work(run: BocaRun):
24
+ scraper.download_run(
25
+ run.run_number,
26
+ run.site_number,
27
+ pathlib.Path(into_path) / run.problem_shortname,
28
+ name=f'{run.run_number}-{run.site_number}-{run.outcome.short_name().lower()}',
29
+ )
30
+
31
+ progress.update(scrape_task, advance=1)
32
+
33
+ with ThreadPoolExecutor(max_workers=10) as executor:
34
+ executor.map(work, runs)