rbx.cp 0.13.8__py3-none-any.whl → 0.16.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.
- rbx/__version__.py +1 -0
- rbx/box/cli.py +74 -70
- rbx/box/code.py +3 -0
- rbx/box/contest/build_contest_statements.py +65 -23
- rbx/box/contest/contest_package.py +8 -1
- rbx/box/contest/main.py +9 -3
- rbx/box/contest/schema.py +17 -13
- rbx/box/contest/statements.py +12 -8
- rbx/box/dump_schemas.py +2 -1
- rbx/box/environment.py +1 -1
- rbx/box/fields.py +22 -4
- rbx/box/generators.py +32 -13
- rbx/box/git_utils.py +29 -1
- rbx/box/limits_info.py +161 -0
- rbx/box/package.py +18 -1
- rbx/box/packaging/boca/boca_language_utils.py +26 -0
- rbx/box/packaging/boca/boca_outcome_utils.py +10 -0
- rbx/box/packaging/boca/packager.py +7 -5
- rbx/box/packaging/contest_main.py +20 -12
- rbx/box/packaging/packager.py +24 -14
- rbx/box/packaging/polygon/packager.py +7 -3
- rbx/box/packaging/polygon/upload.py +2 -1
- rbx/box/presets/__init__.py +143 -78
- rbx/box/presets/fetch.py +10 -2
- rbx/box/presets/schema.py +16 -1
- rbx/box/remote.py +3 -3
- rbx/box/sanitizers/issue_stack.py +124 -0
- rbx/box/schema.py +87 -27
- rbx/box/solutions.py +74 -117
- rbx/box/statements/build_statements.py +12 -1
- rbx/box/statements/builders.py +5 -3
- rbx/box/statements/latex_jinja.py +73 -23
- rbx/box/statements/schema.py +7 -9
- rbx/box/stressing/generator_parser.py +3 -1
- rbx/box/tasks.py +10 -10
- rbx/box/testcase_extractors.py +8 -0
- rbx/box/testing/testing_preset.py +129 -2
- rbx/box/testing/testing_shared.py +3 -1
- rbx/box/timing.py +305 -0
- rbx/box/tooling/boca/debug_utils.py +88 -0
- rbx/box/tooling/boca/manual_scrape.py +20 -0
- rbx/box/tooling/boca/scraper.py +660 -57
- rbx/box/unit.py +0 -2
- rbx/box/validators.py +0 -4
- rbx/grading/judge/cacher.py +36 -0
- rbx/grading/judge/program.py +12 -2
- rbx/grading/judge/sandbox.py +1 -1
- rbx/grading/judge/sandboxes/stupid_sandbox.py +2 -1
- rbx/grading/judge/storage.py +36 -3
- rbx/grading/limits.py +4 -0
- rbx/grading/steps.py +3 -2
- rbx/resources/presets/default/contest/contest.rbx.yml +7 -1
- rbx/resources/presets/default/contest/statement/info.rbx.tex +46 -0
- rbx/resources/presets/default/preset.rbx.yml +1 -0
- rbx/resources/presets/default/problem/.gitignore +1 -0
- rbx/resources/presets/default/problem/problem.rbx.yml +19 -3
- rbx/resources/presets/default/problem/rbx.h +52 -5
- rbx/resources/presets/default/problem/statement/statement.rbx.tex +6 -2
- rbx/resources/presets/default/problem/testlib.h +6299 -0
- rbx/resources/presets/default/problem/validator.cpp +4 -3
- rbx/resources/presets/default/shared/contest_template.rbx.tex +8 -4
- rbx/resources/presets/default/shared/icpc.sty +18 -3
- rbx/resources/presets/default/shared/problem_template.rbx.tex +4 -1
- rbx/testing_utils.py +17 -1
- rbx/utils.py +45 -0
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/METADATA +5 -2
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/RECORD +71 -67
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/entry_points.txt +0 -1
- rbx/providers/__init__.py +0 -43
- rbx/providers/codeforces.py +0 -73
- rbx/providers/provider.py +0 -26
- rbx/submitors/__init__.py +0 -18
- rbx/submitors/codeforces.py +0 -121
- rbx/submitors/submitor.py +0 -25
- /rbx/resources/presets/default/problem/sols/{wa.cpp → wa-overflow.cpp} +0 -0
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/LICENSE +0 -0
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/WHEEL +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')
|