rbx.cp 0.5.50__py3-none-any.whl → 0.5.52__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.
@@ -0,0 +1,336 @@
1
+ import os
2
+ import pathlib
3
+ import tempfile
4
+ from typing import Any, Dict, Optional
5
+
6
+ import rich
7
+ import rich.progress
8
+ import typer
9
+
10
+ from rbx import console
11
+ from rbx.box import header, package
12
+ from rbx.box.generators import get_all_built_testcases
13
+ from rbx.box.packaging.polygon import polygon_api as api
14
+ from rbx.box.packaging.polygon.packager import code_to_langs, is_valid_lang_code
15
+ from rbx.box.schema import CodeItem, ExpectedOutcome, Solution, TaskType, Testcase
16
+ from rbx.box.statements.build_statements import get_relative_assets
17
+ from rbx.box.statements.builders import (
18
+ StatementBlocks,
19
+ StatementBuilderProblem,
20
+ render_jinja_blocks,
21
+ )
22
+ from rbx.box.statements.schema import Statement, StatementType
23
+ from rbx.box.testcase_utils import get_alternate_interaction_texts, parse_interaction
24
+
25
+ _API_URL = 'https://polygon.codeforces.com/api'
26
+
27
+ POLY = api.Polygon(
28
+ _API_URL,
29
+ os.environ.get('POLYGON_API_KEY', '').strip(),
30
+ os.environ.get('POLYGON_API_SECRET', '').strip(),
31
+ )
32
+
33
+
34
+ def _get_source_type(code: CodeItem):
35
+ return None
36
+
37
+
38
+ def _get_solution_tag(solution: Solution, is_first: bool = False) -> api.SolutionTag:
39
+ if solution.outcome == ExpectedOutcome.ACCEPTED:
40
+ return api.SolutionTag.OK if not is_first else api.SolutionTag.MA
41
+ if solution.outcome == ExpectedOutcome.ACCEPTED_OR_TLE:
42
+ return api.SolutionTag.TO
43
+ if solution.outcome == ExpectedOutcome.WRONG_ANSWER:
44
+ return api.SolutionTag.WA
45
+ if solution.outcome == ExpectedOutcome.TIME_LIMIT_EXCEEDED:
46
+ return api.SolutionTag.TL
47
+ if solution.outcome == ExpectedOutcome.MEMORY_LIMIT_EXCEEDED:
48
+ return api.SolutionTag.ML
49
+ if solution.outcome == ExpectedOutcome.RUNTIME_ERROR:
50
+ return api.SolutionTag.RE
51
+ return api.SolutionTag.RJ
52
+
53
+
54
+ def _find_or_create_problem(problem_name: str) -> api.Problem:
55
+ results = POLY.problems_list(name=problem_name)
56
+ for result in results:
57
+ if result.name == problem_name:
58
+ console.console.print(
59
+ f'Found already existing problem [item]{problem_name}[/item].'
60
+ )
61
+ return result
62
+ console.console.print(f'Creating new problem [item]{problem_name}[/item].')
63
+ return POLY.problem_create(problem_name)
64
+
65
+
66
+ def _update_problem_info(problem: api.Problem):
67
+ pkg = package.find_problem_package_or_die()
68
+
69
+ problem.update_info(
70
+ api.ProblemInfo(
71
+ interactive=pkg.type == TaskType.COMMUNICATION,
72
+ time_limit=pkg.timeLimit,
73
+ memory_limit=pkg.memoryLimit,
74
+ )
75
+ )
76
+
77
+
78
+ def _get_checker_name() -> str:
79
+ checker = package.get_checker()
80
+ return checker.path.with_stem('checker').name
81
+
82
+
83
+ def _get_interactor_name() -> str:
84
+ interactor = package.get_interactor()
85
+ return interactor.path.with_stem('interactor').name
86
+
87
+
88
+ def _get_validator_name() -> str:
89
+ validator = package.get_validator()
90
+ return validator.path.with_stem('validator').name
91
+
92
+
93
+ def _update_rbx_header(problem: api.Problem):
94
+ console.console.print('Uploading rbx.h...')
95
+ rbx_header = header.get_header()
96
+ problem.save_file(
97
+ type=api.FileType.RESOURCE,
98
+ name='rbx.h',
99
+ file=rbx_header.read_bytes(),
100
+ source_type=None,
101
+ )
102
+
103
+
104
+ def _update_checker(problem: api.Problem):
105
+ console.console.print('Uploading checker...')
106
+ checker = package.get_checker()
107
+ problem.save_file(
108
+ type=api.FileType.SOURCE,
109
+ name=_get_checker_name(),
110
+ file=checker.path.read_bytes(),
111
+ source_type=_get_source_type(checker),
112
+ )
113
+
114
+ problem.set_checker(_get_checker_name())
115
+
116
+
117
+ def _update_interactor(problem: api.Problem):
118
+ console.console.print('Uploading interactor...')
119
+ interactor = package.get_interactor()
120
+ problem.save_file(
121
+ type=api.FileType.SOURCE,
122
+ name=_get_interactor_name(),
123
+ file=interactor.path.read_bytes(),
124
+ source_type=_get_source_type(interactor),
125
+ )
126
+
127
+ problem.set_interactor(_get_interactor_name())
128
+
129
+
130
+ def _upload_validator(problem: api.Problem):
131
+ console.console.print('Uploading validator...')
132
+ validator = package.get_validator()
133
+ problem.save_file(
134
+ type=api.FileType.SOURCE,
135
+ name=_get_validator_name(),
136
+ file=validator.path.read_bytes(),
137
+ source_type=_get_source_type(validator),
138
+ )
139
+
140
+ problem.set_validator(_get_validator_name())
141
+
142
+
143
+ def _save_skip_coinciding_testcases(problem: api.Problem, *args, **kwargs) -> bool:
144
+ try:
145
+ problem.save_test(*args, **kwargs)
146
+ except api.PolygonRequestFailedException as e:
147
+ if 'test coincides with' in e.comment.lower():
148
+ return False
149
+ raise
150
+ return True
151
+
152
+
153
+ def _get_test_params_for_statement(
154
+ testcase: Testcase, is_sample: bool
155
+ ) -> Dict[str, Any]:
156
+ if not is_sample:
157
+ return {}
158
+ res: Dict[str, Any] = {'test_use_in_statements': True}
159
+ if testcase.outputPath is not None:
160
+ res['test_output_for_statements'] = testcase.outputPath.read_text()
161
+ else:
162
+ return res
163
+
164
+ pio_path = testcase.outputPath.with_suffix('.pio')
165
+ if pio_path.is_file():
166
+ interaction = parse_interaction(pio_path)
167
+ res['test_input_for_statements'], res['test_output_for_statements'] = (
168
+ get_alternate_interaction_texts(interaction)
169
+ )
170
+ else:
171
+ pin_path = testcase.outputPath.with_suffix('.pin')
172
+ if pin_path.is_file():
173
+ res['test_input_for_statements'] = pin_path.read_text()
174
+ pout_path = testcase.outputPath.with_suffix('.pout')
175
+ if pout_path.is_file():
176
+ res['test_output_for_statements'] = pout_path.read_text()
177
+ return res
178
+
179
+
180
+ def _upload_testcases(problem: api.Problem):
181
+ pkg = package.find_problem_package_or_die()
182
+ testcases = get_all_built_testcases()
183
+ i = 0
184
+
185
+ with rich.progress.Progress(speed_estimate_period=5) as progress:
186
+ total_len = 0
187
+ for group in pkg.testcases:
188
+ total_len += len(testcases[group.name])
189
+ task_id = progress.add_task('Uploading testcases...', total=total_len)
190
+ for group in pkg.testcases:
191
+ for testcase in testcases[group.name]:
192
+ is_sample = group.name == 'samples'
193
+ saved = _save_skip_coinciding_testcases(
194
+ problem,
195
+ testset='tests',
196
+ test_index=i + 1,
197
+ test_input=testcase.inputPath.read_text(),
198
+ **_get_test_params_for_statement(testcase, is_sample),
199
+ )
200
+ progress.update(task_id, advance=1)
201
+ if saved:
202
+ i += 1
203
+
204
+
205
+ def _upload_solutions(problem: api.Problem):
206
+ console.console.print('Uploading main solution...')
207
+ pkg = package.find_problem_package_or_die()
208
+ main_solution = pkg.solutions[0]
209
+ if main_solution is None or main_solution.outcome != ExpectedOutcome.ACCEPTED:
210
+ return
211
+ problem.save_solution(
212
+ main_solution.path.name,
213
+ main_solution.path.read_bytes(),
214
+ source_type=_get_source_type(main_solution),
215
+ tag=api.SolutionTag.MA,
216
+ )
217
+
218
+ for i, solution in enumerate(pkg.solutions):
219
+ console.console.print(
220
+ f'Uploading solution [item]{solution.path.name}[/item] (tag: [item]{_get_solution_tag(solution, is_first=i == 0)}[/item])...'
221
+ )
222
+ problem.save_solution(
223
+ solution.path.name,
224
+ solution.path.read_bytes(),
225
+ source_type=_get_source_type(solution),
226
+ tag=_get_solution_tag(solution, is_first=i == 0),
227
+ )
228
+
229
+
230
+ def _get_statement_for_language(language: str) -> Optional[Statement]:
231
+ pkg = package.find_problem_package_or_die()
232
+ for statement in pkg.statements:
233
+ if statement.language == language:
234
+ return statement
235
+ return None
236
+
237
+
238
+ def _get_statement_blocks(statement: Statement) -> StatementBlocks:
239
+ # TODO: actually try to convert to rbxTeX
240
+ assert statement.type == StatementType.rbxTeX
241
+ builder_problem = StatementBuilderProblem(
242
+ package=package.find_problem_package_or_die(),
243
+ statement=statement,
244
+ )
245
+ with tempfile.TemporaryDirectory() as temp_dir:
246
+ return render_jinja_blocks(
247
+ pathlib.Path(temp_dir),
248
+ statement.path.read_bytes(),
249
+ **builder_problem.build_inner_jinja_kwargs(),
250
+ )
251
+
252
+
253
+ def _upload_statement_resources(problem: api.Problem, statement: Statement):
254
+ assets = get_relative_assets(statement.path, statement.assets)
255
+ for asset, relative_asset in assets:
256
+ console.console.print(
257
+ f'Uploading statement resource [item]{relative_asset}[/item]...'
258
+ )
259
+ resource_bytes = asset.read_bytes()
260
+ if len(resource_bytes) >= 1024 * 1024: # >= 1mb
261
+ console.console.print(
262
+ f'[error]Statement resource [item]{relative_asset}[/item] is too large to upload (more than 1MB).[/error]'
263
+ )
264
+ raise typer.Exit(1)
265
+ problem.save_statement_resource(
266
+ name=str(relative_asset),
267
+ file=resource_bytes,
268
+ )
269
+
270
+
271
+ def _upload_statement(problem: api.Problem):
272
+ pkg = package.find_problem_package_or_die()
273
+
274
+ languages = set()
275
+ for statement in pkg.statements:
276
+ if not is_valid_lang_code(statement.language):
277
+ continue
278
+ languages.add(statement.language)
279
+ for language in languages:
280
+ statement = _get_statement_for_language(language)
281
+ if statement is None:
282
+ continue
283
+ if statement.type != StatementType.rbxTeX:
284
+ continue
285
+ console.console.print(
286
+ f'Uploading statement for language [item]{language}[/item] (polygon language: [item]{code_to_langs([language])[0]}[/item])...'
287
+ )
288
+ blocks = _get_statement_blocks(statement)
289
+ polygon_statement = api.Statement(
290
+ encoding='utf-8',
291
+ name=statement.title,
292
+ legend=blocks.blocks.get('legend'),
293
+ input=blocks.blocks.get('input'),
294
+ output=blocks.blocks.get('output'),
295
+ interaction=blocks.blocks.get('interaction'),
296
+ notes=blocks.blocks.get('notes'),
297
+ )
298
+ problem.save_statement(
299
+ lang=code_to_langs([language])[0], problem_statement=polygon_statement
300
+ )
301
+
302
+ _upload_statement_resources(problem, statement)
303
+
304
+
305
+ def _normalize_problem_name(name: str) -> str:
306
+ return name.replace(' ', '-').replace('_', '-').lower()
307
+
308
+
309
+ async def upload_problem(name: str):
310
+ pkg = package.find_problem_package_or_die()
311
+ name = _normalize_problem_name(name)
312
+ problem = _find_or_create_problem(name)
313
+ _update_problem_info(problem)
314
+ _update_checker(problem)
315
+ _update_rbx_header(problem)
316
+
317
+ if (
318
+ pkg.type == TaskType.COMMUNICATION
319
+ and package.get_interactor_or_nil() is not None
320
+ ):
321
+ _update_interactor(problem)
322
+
323
+ # if pkg.validator is not None:
324
+ # _upload_validator(problem)
325
+
326
+ _upload_solutions(problem)
327
+ _upload_testcases(problem)
328
+ _upload_statement(problem)
329
+
330
+ # Commit.
331
+ console.console.print('Committing changes...')
332
+ problem.commit_changes()
333
+
334
+ console.console.print(
335
+ f'[success]Problem [item]{name}[/item] uploaded successfully![/success]'
336
+ )
@@ -65,6 +65,10 @@ class Checker(BaseXmlModel):
65
65
  testset: Optional[Testset] = element(default=None)
66
66
 
67
67
 
68
+ class Interactor(BaseXmlModel):
69
+ source: File = element()
70
+
71
+
68
72
  class Problem(BaseXmlModel, tag='problem'):
69
73
  names: List[Name] = wrapped('names', element(tag='name'), default_factory=list)
70
74
 
@@ -84,6 +88,8 @@ class Problem(BaseXmlModel, tag='problem'):
84
88
 
85
89
  checker: Checker = wrapped('assets', element(tag='checker'))
86
90
 
91
+ interactor: Optional[Interactor] = wrapped('assets', element(tag='interactor'))
92
+
87
93
 
88
94
  class ContestProblem(BaseXmlModel):
89
95
  index: str = attr()
rbx/box/solutions.py CHANGED
@@ -5,7 +5,7 @@ import dataclasses
5
5
  import pathlib
6
6
  import shutil
7
7
  from collections.abc import Iterator
8
- from typing import Dict, Iterable, List, Optional, Set, Tuple
8
+ from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
9
9
 
10
10
  import rich
11
11
  import rich.live
@@ -16,7 +16,7 @@ import typer
16
16
  from pydantic import BaseModel
17
17
 
18
18
  from rbx import console, utils
19
- from rbx.box import checkers, package
19
+ from rbx.box import checkers, environment, package
20
20
  from rbx.box.code import (
21
21
  SanitizationLevel,
22
22
  compile_item,
@@ -889,29 +889,37 @@ def _print_solution_header(
889
889
  console.print(f'({solution_testdir})')
890
890
 
891
891
 
892
+ @dataclasses.dataclass
893
+ class SolutionTiming:
894
+ time: int
895
+ solution: Solution
896
+
897
+
892
898
  @dataclasses.dataclass
893
899
  class TimingSummary:
894
- slowest_good: Optional[int] = None
895
- fastest_slow: Optional[int] = None
900
+ slowest_good: Optional[SolutionTiming] = None
901
+ fastest_slow: Optional[SolutionTiming] = None
896
902
 
897
- def add_good(self, time: int):
898
- if self.slowest_good is None or time > self.slowest_good:
899
- self.slowest_good = time
903
+ def add_good(self, time: int, solution: Solution):
904
+ if self.slowest_good is None or time > self.slowest_good.time:
905
+ self.slowest_good = SolutionTiming(time, solution)
900
906
 
901
- def add_slow(self, time: int):
902
- if self.fastest_slow is None or time < self.fastest_slow:
903
- self.fastest_slow = time
907
+ def add_slow(self, time: int, solution: Solution):
908
+ if self.fastest_slow is None or time < self.fastest_slow.time:
909
+ self.fastest_slow = SolutionTiming(time, solution)
904
910
 
905
911
  def print(self, console: rich.console.Console, tl: Optional[int] = None):
906
912
  if self.slowest_good is not None:
907
913
  console.print(
908
- f'Slowest [success]OK[/success] solution: {self.slowest_good} ms'
914
+ f'Slowest [success]OK[/success] solution: {self.slowest_good.time} ms, [item]{self.slowest_good.solution.path}[/item]'
909
915
  )
910
916
  if self.fastest_slow is not None:
911
- fastest_slow = self.fastest_slow
912
- if tl is not None and self.fastest_slow > tl:
917
+ fastest_slow = self.fastest_slow.time
918
+ if tl is not None and self.fastest_slow.time > tl:
913
919
  fastest_slow = f'>{tl}'
914
- console.print(f'Fastest [error]slow[/error] solution: {fastest_slow} ms')
920
+ console.print(
921
+ f'Fastest [error]slow[/error] solution: {fastest_slow} ms, [item]{self.fastest_slow.solution.path}[/item]'
922
+ )
915
923
 
916
924
 
917
925
  async def _print_timing(
@@ -953,11 +961,11 @@ async def _print_timing(
953
961
 
954
962
  # Get solution timings.
955
963
  if solution.outcome.match(Outcome.ACCEPTED):
956
- summary.add_good(solution_time)
957
- summary_per_language[solution.language].add_good(solution_time)
964
+ summary.add_good(solution_time, solution)
965
+ summary_per_language[solution.language].add_good(solution_time, solution)
958
966
  if solution.outcome.is_slow():
959
- summary.add_slow(solution_time)
960
- summary_per_language[solution.language].add_slow(solution_time)
967
+ summary.add_slow(solution_time, solution)
968
+ summary_per_language[solution.language].add_slow(solution_time, solution)
961
969
 
962
970
  if summary.slowest_good is None and summary.fastest_slow is None:
963
971
  return
@@ -1236,6 +1244,16 @@ async def print_run_report(
1236
1244
  return ok
1237
1245
 
1238
1246
 
1247
+ def _step_up(x: Any, step: int) -> int:
1248
+ x = int(x)
1249
+ return (x + step - 1) // step * step
1250
+
1251
+
1252
+ def _step_down(x: Any, step: int) -> int:
1253
+ x = int(x)
1254
+ return x // step * step
1255
+
1256
+
1239
1257
  async def estimate_time_limit(
1240
1258
  console: rich.console.Console,
1241
1259
  result: RunSolutionResult,
@@ -1254,12 +1272,12 @@ async def estimate_time_limit(
1254
1272
  for solution in result.skeleton.solutions:
1255
1273
  timings = []
1256
1274
  for evals in structured_evaluations[str(solution.path)].values():
1257
- for eval in evals:
1258
- if eval is None:
1275
+ for ev in evals:
1276
+ if ev is None:
1259
1277
  continue
1260
- eval = await eval()
1261
- if eval.log.time is not None:
1262
- timings.append(int(eval.log.time * 1000))
1278
+ ev = await ev()
1279
+ if ev.log.time is not None:
1280
+ timings.append(int(ev.log.time * 1000))
1263
1281
 
1264
1282
  if not timings:
1265
1283
  console.print(
@@ -1293,7 +1311,19 @@ async def estimate_time_limit(
1293
1311
  f'Slowest language: {slowest_language} ({slowest_language_time} ms)'
1294
1312
  )
1295
1313
 
1296
- estimated_tl = int(max(fastest_time * 3, slowest_time * 1.5))
1297
- console.print(f'[success]Estimated time limit:[/success] {estimated_tl} ms')
1314
+ env = environment.get_environment()
1315
+ estimated_tl = int(
1316
+ eval(
1317
+ env.timing.formula,
1318
+ {
1319
+ 'fastest': fastest_time,
1320
+ 'slowest': slowest_time,
1321
+ 'step_up': _step_up,
1322
+ 'step_down': _step_down,
1323
+ },
1324
+ )
1325
+ )
1298
1326
 
1327
+ console.print(f'Using formula: {env.timing.formula}')
1328
+ console.print(f'[success]Estimated time limit:[/success] {estimated_tl} ms')
1299
1329
  return estimated_tl
rbx/box/stresses.py CHANGED
@@ -1,8 +1,8 @@
1
- import functools
2
1
  import time
3
2
  from shutil import rmtree
4
3
  from typing import List, Optional
5
4
 
5
+ import async_lru
6
6
  import syncer
7
7
  import typer
8
8
  from pydantic import BaseModel
@@ -147,7 +147,7 @@ async def run_stress(
147
147
  else None,
148
148
  )
149
149
 
150
- @functools.cache
150
+ @async_lru.alru_cache
151
151
  async def run_solution_fn(
152
152
  solution: str,
153
153
  retry_index: Optional[int] = None,
@@ -197,7 +197,7 @@ async def run_stress(
197
197
  raise typer.Exit(1)
198
198
  expected_output_path = main_testcase_log.stdout_absolute_path
199
199
 
200
- @functools.cache
200
+ @async_lru.alru_cache
201
201
  async def run_solution_and_checker_fn(
202
202
  call: finder_parser.FinderCall,
203
203
  input_path=input_path,
@@ -242,9 +242,12 @@ async def run_stress(
242
242
  checker_result=eval.result,
243
243
  )
244
244
 
245
- runner = finder_parser.FinderTreeRunner(
246
- runner=syncer.sync(run_solution_and_checker_fn)
247
- )
245
+ @syncer.sync
246
+ async def run_fn(*args, **kwargs):
247
+ # Wrap the runner in a syncer.sync to make it work with the finder parser.
248
+ return await run_solution_and_checker_fn(*args, **kwargs)
249
+
250
+ runner = finder_parser.FinderTreeRunner(runner=run_fn)
248
251
  finder_outcome: finder_parser.FinderOutcome = runner.transform(parsed_finder)
249
252
 
250
253
  internal_error_results = [
rbx/box/testcase_utils.py CHANGED
@@ -213,6 +213,21 @@ def parse_interaction(file: pathlib.Path) -> TestcaseInteraction:
213
213
  )
214
214
 
215
215
 
216
+ def get_alternate_interaction_texts(
217
+ interaction: TestcaseInteraction,
218
+ ) -> Tuple[str, str]:
219
+ interactor_entries = []
220
+ solution_entries = []
221
+ for entry in interaction.entries:
222
+ if entry.pipe == 1:
223
+ solution_entries.append(entry.data)
224
+ interactor_entries.extend(['\n'] * entry.data.count('\n'))
225
+ else:
226
+ interactor_entries.append(entry.data)
227
+ solution_entries.extend(['\n'] * entry.data.count('\n'))
228
+ return ''.join(interactor_entries), ''.join(solution_entries)
229
+
230
+
216
231
  def print_interaction(interaction: TestcaseInteraction):
217
232
  for entry in interaction.entries:
218
233
  text = rich.text.Text(entry.data)
@@ -8,7 +8,7 @@ import re
8
8
  import signal
9
9
  import struct
10
10
  import termios
11
- from typing import Callable, List, Optional
11
+ from typing import Callable, List, Optional, Tuple
12
12
 
13
13
  import pyte
14
14
  import textual
@@ -321,10 +321,14 @@ class LogDisplay(ScrollView, can_focus=True):
321
321
  loop.remove_reader(pout)
322
322
  event.set()
323
323
 
324
- async def cleanup():
324
+ async def cleanup(wait_tp: Optional[Tuple[int, int]] = None):
325
+ if self.exitcode is not None:
326
+ return
325
327
  try:
326
328
  loop.remove_reader(pout)
327
- _, exitstatus = os.waitpid(pid, os.WNOHANG)
329
+ if wait_tp is None:
330
+ wait_tp = os.waitpid(pid, os.WNOHANG)
331
+ _, exitstatus = wait_tp
328
332
  exitcode = os.waitstatus_to_exitcode(exitstatus)
329
333
  self.exitcode = exitcode
330
334
  except ChildProcessError:
@@ -352,8 +356,8 @@ class LogDisplay(ScrollView, can_focus=True):
352
356
  async def wait():
353
357
  while True:
354
358
  try:
355
- if os.waitpid(pid, os.WNOHANG) != (0, 0):
356
- await cleanup()
359
+ if (wait_tp := os.waitpid(pid, os.WNOHANG)) != (0, 0):
360
+ await cleanup(wait_tp)
357
361
  except ChildProcessError:
358
362
  break
359
363
  await asyncio.sleep(0.5)
rbx/box/ui/css/app.tcss CHANGED
@@ -3,7 +3,7 @@ Screen {
3
3
  color: $text;
4
4
  }
5
5
 
6
- rbxApp > Screen {
6
+ Screen {
7
7
  align: center middle;
8
8
  }
9
9