rbx.cp 0.13.8__py3-none-any.whl → 0.14.0__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 (71) hide show
  1. rbx/box/cli.py +74 -70
  2. rbx/box/code.py +3 -0
  3. rbx/box/contest/build_contest_statements.py +65 -23
  4. rbx/box/contest/contest_package.py +8 -1
  5. rbx/box/contest/main.py +9 -3
  6. rbx/box/contest/schema.py +17 -13
  7. rbx/box/contest/statements.py +12 -8
  8. rbx/box/dump_schemas.py +2 -1
  9. rbx/box/environment.py +1 -1
  10. rbx/box/fields.py +22 -4
  11. rbx/box/generators.py +32 -13
  12. rbx/box/limits_info.py +161 -0
  13. rbx/box/package.py +18 -1
  14. rbx/box/packaging/boca/boca_language_utils.py +26 -0
  15. rbx/box/packaging/boca/boca_outcome_utils.py +10 -0
  16. rbx/box/packaging/boca/packager.py +7 -5
  17. rbx/box/packaging/contest_main.py +20 -12
  18. rbx/box/packaging/packager.py +24 -14
  19. rbx/box/packaging/polygon/packager.py +7 -3
  20. rbx/box/packaging/polygon/upload.py +2 -1
  21. rbx/box/presets/__init__.py +64 -64
  22. rbx/box/remote.py +3 -3
  23. rbx/box/sanitizers/issue_stack.py +124 -0
  24. rbx/box/schema.py +87 -27
  25. rbx/box/solutions.py +74 -117
  26. rbx/box/statements/build_statements.py +12 -1
  27. rbx/box/statements/builders.py +5 -3
  28. rbx/box/statements/latex_jinja.py +73 -23
  29. rbx/box/statements/schema.py +7 -9
  30. rbx/box/stressing/generator_parser.py +3 -1
  31. rbx/box/tasks.py +10 -10
  32. rbx/box/testcase_extractors.py +8 -0
  33. rbx/box/testing/testing_preset.py +129 -2
  34. rbx/box/testing/testing_shared.py +3 -1
  35. rbx/box/timing.py +305 -0
  36. rbx/box/tooling/boca/debug_utils.py +88 -0
  37. rbx/box/tooling/boca/manual_scrape.py +20 -0
  38. rbx/box/tooling/boca/scraper.py +660 -57
  39. rbx/box/unit.py +0 -2
  40. rbx/box/validators.py +0 -4
  41. rbx/grading/judge/cacher.py +36 -0
  42. rbx/grading/judge/program.py +12 -2
  43. rbx/grading/judge/sandbox.py +1 -1
  44. rbx/grading/judge/sandboxes/stupid_sandbox.py +2 -1
  45. rbx/grading/judge/storage.py +36 -3
  46. rbx/grading/limits.py +4 -0
  47. rbx/grading/steps.py +3 -2
  48. rbx/resources/presets/default/contest/contest.rbx.yml +7 -1
  49. rbx/resources/presets/default/contest/statement/info.rbx.tex +54 -0
  50. rbx/resources/presets/default/problem/.gitignore +1 -0
  51. rbx/resources/presets/default/problem/problem.rbx.yml +19 -3
  52. rbx/resources/presets/default/problem/rbx.h +52 -5
  53. rbx/resources/presets/default/problem/statement/statement.rbx.tex +6 -2
  54. rbx/resources/presets/default/problem/testlib.h +6299 -0
  55. rbx/resources/presets/default/problem/validator.cpp +4 -3
  56. rbx/resources/presets/default/shared/contest_template.rbx.tex +8 -4
  57. rbx/resources/presets/default/shared/icpc.sty +16 -1
  58. rbx/resources/presets/default/shared/problem_template.rbx.tex +4 -1
  59. rbx/testing_utils.py +17 -1
  60. {rbx_cp-0.13.8.dist-info → rbx_cp-0.14.0.dist-info}/METADATA +4 -2
  61. {rbx_cp-0.13.8.dist-info → rbx_cp-0.14.0.dist-info}/RECORD +65 -62
  62. {rbx_cp-0.13.8.dist-info → rbx_cp-0.14.0.dist-info}/WHEEL +1 -1
  63. {rbx_cp-0.13.8.dist-info → rbx_cp-0.14.0.dist-info}/entry_points.txt +0 -1
  64. rbx/providers/__init__.py +0 -43
  65. rbx/providers/codeforces.py +0 -73
  66. rbx/providers/provider.py +0 -26
  67. rbx/submitors/__init__.py +0 -18
  68. rbx/submitors/codeforces.py +0 -121
  69. rbx/submitors/submitor.py +0 -25
  70. /rbx/resources/presets/default/problem/sols/{wa.cpp → wa-overflow.cpp} +0 -0
  71. {rbx_cp-0.13.8.dist-info → rbx_cp-0.14.0.dist-info}/LICENSE +0 -0
rbx/box/timing.py ADDED
@@ -0,0 +1,305 @@
1
+ import pathlib
2
+ from typing import Any, Dict, Optional
3
+
4
+ import questionary
5
+ import rich
6
+ import rich.console
7
+ import typer
8
+ from ordered_set import OrderedSet
9
+ from pydantic import BaseModel, Field
10
+
11
+ from rbx import console, utils
12
+ from rbx.box import environment, limits_info, package, schema
13
+ from rbx.box.code import find_language_name
14
+ from rbx.box.environment import VerificationLevel
15
+ from rbx.box.formatting import href
16
+ from rbx.box.schema import ExpectedOutcome
17
+ from rbx.box.solutions import (
18
+ RunSolutionResult,
19
+ consume_and_key_evaluation_items,
20
+ get_exact_matching_solutions,
21
+ print_run_report,
22
+ run_solutions,
23
+ )
24
+
25
+
26
+ class TimingProfile(BaseModel):
27
+ timeLimit: int
28
+ formula: Optional[str] = None
29
+ timeLimitPerLanguage: Dict[str, int] = Field(default_factory=dict)
30
+
31
+ def to_limits(self):
32
+ return schema.LimitsProfile(
33
+ timeLimit=self.timeLimit,
34
+ formula=self.formula,
35
+ modifiers={
36
+ lang: schema.LimitModifiers(time=tl)
37
+ for lang, tl in self.timeLimitPerLanguage.items()
38
+ },
39
+ )
40
+
41
+
42
+ def get_timing_profile(
43
+ profile: str, root: pathlib.Path = pathlib.Path()
44
+ ) -> Optional[TimingProfile]:
45
+ path = package.get_limits_file(profile, root)
46
+ if not path.exists():
47
+ return None
48
+ return utils.model_from_yaml(TimingProfile, path.read_text())
49
+
50
+
51
+ def step_down(x: Any, step: int) -> int:
52
+ x = int(x)
53
+ return x // step * step
54
+
55
+
56
+ def step_up(x: Any, step: int) -> int:
57
+ x = int(x)
58
+ return (x + step - 1) // step * step
59
+
60
+
61
+ async def estimate_time_limit(
62
+ console: rich.console.Console,
63
+ result: RunSolutionResult,
64
+ formula: Optional[str] = None,
65
+ ) -> Optional[TimingProfile]:
66
+ structured_evaluations = consume_and_key_evaluation_items(
67
+ result.items, result.skeleton
68
+ )
69
+
70
+ timing_per_solution = {}
71
+ timing_per_solution_per_language = {}
72
+
73
+ if not result.skeleton.solutions:
74
+ console.print('[error]No solutions to estimate time limit from.[/error]')
75
+ return None
76
+
77
+ for solution in result.skeleton.solutions:
78
+ timings = []
79
+ for evals in structured_evaluations[str(solution.path)].values():
80
+ for ev in evals:
81
+ if ev is None:
82
+ continue
83
+ ev = await ev()
84
+ if ev.log.time is not None:
85
+ timings.append(int(ev.log.time * 1000))
86
+
87
+ if not timings:
88
+ console.print(
89
+ f'[warning]No timings for solution {href(solution.path)}.[/warning]'
90
+ )
91
+ continue
92
+
93
+ timing_per_solution[str(solution.path)] = max(timings)
94
+ lang = find_language_name(solution)
95
+ if lang not in timing_per_solution_per_language:
96
+ timing_per_solution_per_language[lang] = {}
97
+ timing_per_solution_per_language[lang][str(solution.path)] = max(timings)
98
+
99
+ console.rule('[status]Time report[/status]', style='status')
100
+
101
+ fastest_time = min(timing_per_solution.values())
102
+ slowest_time = max(timing_per_solution.values())
103
+
104
+ console.print(f'Fastest solution: {fastest_time} ms')
105
+ console.print(f'Slowest solution: {slowest_time} ms')
106
+
107
+ def _get_lang_fastest(lang: str) -> int:
108
+ return min(timing_per_solution_per_language[lang].values())
109
+
110
+ def _get_lang_slowest(lang: str) -> int:
111
+ return max(timing_per_solution_per_language[lang].values())
112
+
113
+ env = environment.get_environment()
114
+ if formula is None:
115
+ formula = env.timing.formula
116
+
117
+ def _eval(fastest_time: int, slowest_time: int) -> int:
118
+ return int(
119
+ eval(
120
+ formula,
121
+ {
122
+ 'fastest': fastest_time,
123
+ 'slowest': slowest_time,
124
+ 'step_up': step_up,
125
+ 'step_down': step_down,
126
+ },
127
+ )
128
+ )
129
+
130
+ if len(timing_per_solution_per_language) > 1:
131
+ timing_language_list = [
132
+ (_get_lang_fastest(lang), lang) for lang in timing_per_solution_per_language
133
+ ]
134
+ fastest_language_time, fastest_language = min(timing_language_list)
135
+ slowest_language_time, slowest_language = max(timing_language_list)
136
+
137
+ console.print(
138
+ f'Fastest language: {fastest_language} ({fastest_language_time} ms)'
139
+ )
140
+ console.print(
141
+ f'Slowest language: {slowest_language} ({slowest_language_time} ms)'
142
+ )
143
+
144
+ console.print()
145
+ console.rule('[status]Time estimation[/status]', style='status')
146
+
147
+ console.print(f'Using formula: {formula}')
148
+
149
+ estimated_tl = _eval(fastest_time, slowest_time)
150
+ console.print(f'[success]Estimated time limit:[/success] {estimated_tl} ms')
151
+
152
+ estimated_tl_per_language = {}
153
+ if len(timing_per_solution_per_language) > 1:
154
+ for lang in timing_per_solution_per_language:
155
+ estimated_tl_per_language[lang] = _eval(
156
+ _get_lang_fastest(lang), _get_lang_slowest(lang)
157
+ )
158
+
159
+ final_estimated_tls_per_language = {}
160
+ if estimated_tl_per_language:
161
+ for lang, tl in estimated_tl_per_language.items():
162
+ console.print(f'Estimated time limit for {lang}: {tl} ms')
163
+
164
+ all_distinct_tls = set(estimated_tl_per_language.values())
165
+ if len(all_distinct_tls) > 1:
166
+ console.print()
167
+ console.print('It seems your problem has solutions for multiple languages!')
168
+ selected_langs = await questionary.checkbox(
169
+ 'Please select which languages you want to have a specific time limit for '
170
+ '(or leave all unselected if you want to use a single global time limit)',
171
+ choices=list(estimated_tl_per_language.keys()),
172
+ ).ask_async()
173
+ if selected_langs:
174
+ for lang in selected_langs:
175
+ final_estimated_tls_per_language[lang] = estimated_tl_per_language[
176
+ lang
177
+ ]
178
+
179
+ return TimingProfile(
180
+ timeLimit=estimated_tl,
181
+ formula=formula,
182
+ timeLimitPerLanguage=final_estimated_tls_per_language,
183
+ )
184
+
185
+
186
+ async def compute_time_limits(
187
+ check: bool,
188
+ detailed: bool,
189
+ runs: int = 0,
190
+ profile: str = 'local',
191
+ formula: Optional[str] = None,
192
+ ):
193
+ if package.get_main_solution() is None:
194
+ console.console.print(
195
+ '[warning]No main solution found, so cannot estimate a time limit.[/warning]'
196
+ )
197
+ return None
198
+
199
+ verification = VerificationLevel.ALL_SOLUTIONS.value
200
+
201
+ with utils.StatusProgress('Running ACCEPTED solutions...') as s:
202
+ tracked_solutions = OrderedSet(
203
+ str(solution.path)
204
+ for solution in get_exact_matching_solutions(ExpectedOutcome.ACCEPTED)
205
+ )
206
+ solution_result = run_solutions(
207
+ progress=s,
208
+ tracked_solutions=tracked_solutions,
209
+ check=check,
210
+ verification=VerificationLevel(verification),
211
+ timelimit_override=-1, # Unlimited for time limit estimation
212
+ nruns=runs,
213
+ )
214
+
215
+ console.console.print()
216
+ console.console.rule(
217
+ '[status]Run report (for time estimation)[/status]', style='status'
218
+ )
219
+ ok = await print_run_report(
220
+ solution_result,
221
+ console.console,
222
+ VerificationLevel(verification),
223
+ detailed=detailed,
224
+ skip_printing_limits=True,
225
+ )
226
+
227
+ if not ok:
228
+ console.console.print(
229
+ '[error]Failed to run ACCEPTED solutions, so cannot estimate a reliable time limit.[/error]'
230
+ )
231
+ return None
232
+
233
+ estimated_tl = await estimate_time_limit(console.console, solution_result, formula)
234
+ if estimated_tl is None:
235
+ return None
236
+
237
+ limits_path = package.get_limits_file(profile)
238
+ console.console.print(
239
+ f'[green]Writing the following timing profile to [item]{href(limits_path)}[/item].[/green]'
240
+ )
241
+ console.console.print(estimated_tl, highlight=True)
242
+ limits_path.write_text(utils.model_to_yaml(estimated_tl.to_limits()))
243
+
244
+ return estimated_tl
245
+
246
+
247
+ def inherit_time_limits(profile: str = 'local'):
248
+ limits_path = package.get_limits_file(profile)
249
+ limits = schema.LimitsProfile(inheritFromPackage=True)
250
+ limits_path.write_text(utils.model_to_yaml(limits))
251
+
252
+ console.console.print(
253
+ f'[green]Inherit time limits from package for profile [item]{profile}[/item].[/green]'
254
+ )
255
+
256
+
257
+ def set_time_limit(timelimit: int, profile: str = 'local'):
258
+ limits = schema.LimitsProfile(timeLimit=timelimit)
259
+ limits_path = package.get_limits_file(profile)
260
+ limits_path.write_text(utils.model_to_yaml(limits))
261
+
262
+ console.console.print(
263
+ f'[green]Set time limit for profile [item]{profile}[/item] to [item]{timelimit} ms[/item].[/green]'
264
+ )
265
+
266
+
267
+ def integrate(profile: str = 'local'):
268
+ limits_profile = limits_info.get_saved_limits_profile(profile)
269
+ if limits_profile is None:
270
+ console.console.print(
271
+ f'[error]No limits profile found for profile [item]{profile}[/item].[/error]'
272
+ )
273
+ raise typer.Exit(1)
274
+
275
+ if limits_profile.inheritFromPackage:
276
+ console.console.print(
277
+ f'[warning]Limits profile [item]{profile}[/item] already inherits from package.[/warning]'
278
+ )
279
+ console.console.print('[warning]This operation is a no-op.[/warning]')
280
+ return
281
+
282
+ ru, pkg = package.get_ruyaml()
283
+
284
+ if limits_profile.timeLimit is not None:
285
+ pkg['timeLimit'] = limits_profile.timeLimit
286
+ if limits_profile.memoryLimit is not None:
287
+ pkg['memoryLimit'] = limits_profile.memoryLimit
288
+ if limits_profile.outputLimit is not None:
289
+ pkg['outputLimit'] = limits_profile.outputLimit
290
+
291
+ for lang, limits in limits_profile.modifiers.items():
292
+ if limits.time is not None:
293
+ pkg['modifiers'][lang]['time'] = limits.time
294
+ if limits.memory is not None:
295
+ pkg['modifiers'][lang]['memory'] = limits.memory
296
+ if limits.timeMultiplier is not None:
297
+ pkg['modifiers'][lang]['timeMultiplier'] = limits.timeMultiplier
298
+
299
+ dest_yml = package.find_problem_yaml()
300
+ assert dest_yml is not None
301
+ utils.save_ruyaml(dest_yml, ru, pkg)
302
+
303
+ console.console.print(
304
+ f'[green]Integrated limits profile [item]{profile}[/item] into package.[/green]'
305
+ )
@@ -0,0 +1,88 @@
1
+ import urllib.parse
2
+ from typing import Any
3
+
4
+ from rbx import console
5
+
6
+
7
+ def pretty_print_request_data(req: Any) -> None:
8
+ """
9
+ Pretty print the POST data from a mechanize request.
10
+
11
+ Args:
12
+ req: A mechanize.Request object containing POST data
13
+ """
14
+ if hasattr(req, 'data') and req.data:
15
+ console.console.print('\n[bold blue]POST Data:[/bold blue]')
16
+ if isinstance(req.data, bytes):
17
+ try:
18
+ # Try to decode the data
19
+ decoded_data = req.data.decode('utf-8')
20
+
21
+ # Check if it's multipart form data
22
+ if 'Content-Disposition: form-data' in decoded_data:
23
+ console.console.print(' [cyan]Format:[/cyan] multipart/form-data')
24
+ # Parse multipart form data
25
+ parts = decoded_data.split('-----------------------------')
26
+ for part in parts[1:-1]: # Skip first empty part and last boundary
27
+ if 'Content-Disposition: form-data' in part:
28
+ lines = part.strip().split('\r\n')
29
+ for line in lines:
30
+ if line.startswith(
31
+ 'Content-Disposition: form-data; name='
32
+ ):
33
+ # Extract field name
34
+ name_start = line.find('name="') + 6
35
+ name_end = line.find('"', name_start)
36
+ if name_end == -1:
37
+ name_end = line.find('\r', name_start)
38
+ field_name = line[name_start:name_end]
39
+
40
+ # Find the value (after empty line)
41
+ try:
42
+ empty_line_idx = lines.index('')
43
+ if empty_line_idx + 1 < len(lines):
44
+ field_value = lines[empty_line_idx + 1]
45
+ # Handle file uploads
46
+ if 'filename=' in line:
47
+ filename_start = (
48
+ line.find('filename="') + 10
49
+ )
50
+ filename_end = line.find(
51
+ '"', filename_start
52
+ )
53
+ filename = line[
54
+ filename_start:filename_end
55
+ ]
56
+ console.console.print(
57
+ f' [green]{field_name}[/green]: [yellow](file: {filename})[/yellow]'
58
+ )
59
+ else:
60
+ console.console.print(
61
+ f' [green]{field_name}[/green]: [white]{field_value}[/white]'
62
+ )
63
+ except (ValueError, IndexError):
64
+ console.console.print(
65
+ f' [green]{field_name}[/green]: [dim](could not parse value)[/dim]'
66
+ )
67
+ break
68
+ else:
69
+ # Try to parse as URL-encoded data
70
+ console.console.print(
71
+ ' [cyan]Format:[/cyan] application/x-www-form-urlencoded'
72
+ )
73
+ parsed_data = urllib.parse.parse_qs(decoded_data)
74
+ for key, values in parsed_data.items():
75
+ # Show first value if list has one item, otherwise show the list
76
+ display_value = values[0] if len(values) == 1 else values
77
+ console.console.print(
78
+ f' [green]{key}[/green]: [white]{display_value}[/white]'
79
+ )
80
+
81
+ except UnicodeDecodeError:
82
+ console.console.print(
83
+ f" [yellow]Raw bytes ({len(req.data)} bytes):[/yellow] {req.data[:100]}{'...' if len(req.data) > 100 else ''}"
84
+ )
85
+ else:
86
+ console.console.print(f' [yellow]Data:[/yellow] {req.data}')
87
+ else:
88
+ console.console.print('\n[yellow]No POST data found in request[/yellow]')
@@ -0,0 +1,20 @@
1
+ import datetime
2
+
3
+ if __name__ == '__main__':
4
+ from rbx.box.tooling.boca.scraper import BocaScraper
5
+
6
+ # BASE_URL = 'http://137.184.1.39/boca'
7
+ BASE_URL = 'http://localhost:8000/boca'
8
+
9
+ system_scraper = BocaScraper(base_url=BASE_URL, username='system', password='boca')
10
+ system_scraper.login()
11
+ assert system_scraper.loggedIn
12
+ system_scraper.create_and_activate_contest()
13
+
14
+ admin_scraper = BocaScraper(base_url=BASE_URL, username='admin', password='boca')
15
+ admin_scraper.login()
16
+ assert admin_scraper.loggedIn
17
+ admin_scraper.configure_contest(
18
+ start_time=datetime.datetime.now() - datetime.timedelta(minutes=1)
19
+ )
20
+ admin_scraper.configure_main_site(autojudge=True, chief='judge')