rbx.cp 0.5.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/__init__.py +0 -0
- rbx/annotations.py +127 -0
- rbx/autoenum.py +333 -0
- rbx/box/__init__.py +0 -0
- rbx/box/builder.py +77 -0
- rbx/box/cd.py +37 -0
- rbx/box/checkers.py +134 -0
- rbx/box/code.py +185 -0
- rbx/box/compile.py +56 -0
- rbx/box/conftest.py +42 -0
- rbx/box/contest/__init__.py +0 -0
- rbx/box/contest/build_contest_statements.py +347 -0
- rbx/box/contest/contest_package.py +76 -0
- rbx/box/contest/contest_utils.py +20 -0
- rbx/box/contest/main.py +179 -0
- rbx/box/contest/schema.py +155 -0
- rbx/box/contest/statements.py +82 -0
- rbx/box/creation.py +72 -0
- rbx/box/download.py +64 -0
- rbx/box/environment.py +345 -0
- rbx/box/extensions.py +26 -0
- rbx/box/generators.py +478 -0
- rbx/box/generators_test.py +63 -0
- rbx/box/main.py +449 -0
- rbx/box/package.py +316 -0
- rbx/box/packaging/boca/extension.py +27 -0
- rbx/box/packaging/boca/packager.py +245 -0
- rbx/box/packaging/contest_main.py +82 -0
- rbx/box/packaging/main.py +68 -0
- rbx/box/packaging/packager.py +117 -0
- rbx/box/packaging/polygon/packager.py +320 -0
- rbx/box/packaging/polygon/test.py +81 -0
- rbx/box/packaging/polygon/xml_schema.py +106 -0
- rbx/box/presets/__init__.py +503 -0
- rbx/box/presets/fetch.py +70 -0
- rbx/box/presets/lock_schema.py +20 -0
- rbx/box/presets/schema.py +59 -0
- rbx/box/schema.py +394 -0
- rbx/box/solutions.py +792 -0
- rbx/box/solutions_test.py +41 -0
- rbx/box/statements/__init__.py +0 -0
- rbx/box/statements/build_statements.py +359 -0
- rbx/box/statements/builders.py +375 -0
- rbx/box/statements/joiners.py +113 -0
- rbx/box/statements/latex.py +47 -0
- rbx/box/statements/latex_jinja.py +214 -0
- rbx/box/statements/schema.py +138 -0
- rbx/box/stresses.py +292 -0
- rbx/box/stressing/__init__.py +0 -0
- rbx/box/stressing/finder_parser.py +359 -0
- rbx/box/stressing/generator_parser.py +258 -0
- rbx/box/testcases.py +54 -0
- rbx/box/ui/__init__.py +0 -0
- rbx/box/ui/captured_log.py +372 -0
- rbx/box/ui/css/app.tcss +48 -0
- rbx/box/ui/main.py +38 -0
- rbx/box/ui/run.py +209 -0
- rbx/box/validators.py +245 -0
- rbx/box/validators_test.py +15 -0
- rbx/checker.py +128 -0
- rbx/clone.py +197 -0
- rbx/config.py +271 -0
- rbx/conftest.py +38 -0
- rbx/console.py +27 -0
- rbx/create.py +37 -0
- rbx/edit.py +24 -0
- rbx/grading/__init__.py +0 -0
- rbx/grading/caching.py +356 -0
- rbx/grading/conftest.py +33 -0
- rbx/grading/judge/__init__.py +0 -0
- rbx/grading/judge/cacher.py +503 -0
- rbx/grading/judge/digester.py +35 -0
- rbx/grading/judge/sandbox.py +748 -0
- rbx/grading/judge/sandboxes/__init__.py +0 -0
- rbx/grading/judge/sandboxes/isolate.py +683 -0
- rbx/grading/judge/sandboxes/stupid_sandbox.py +310 -0
- rbx/grading/judge/sandboxes/timeit.py +217 -0
- rbx/grading/judge/storage.py +284 -0
- rbx/grading/judge/test.py +38 -0
- rbx/grading/judge/testiso.py +54 -0
- rbx/grading/steps.py +522 -0
- rbx/grading/steps_with_caching.py +59 -0
- rbx/grading/steps_with_caching_run_test.py +429 -0
- rbx/grading_utils.py +148 -0
- rbx/hydration.py +101 -0
- rbx/main.py +122 -0
- rbx/metadata.py +105 -0
- rbx/providers/__init__.py +43 -0
- rbx/providers/codeforces.py +73 -0
- rbx/providers/provider.py +26 -0
- rbx/resources/checkers/boilerplate.cpp +20 -0
- rbx/resources/default_config.json +48 -0
- rbx/resources/envs/default.rbx.yml +37 -0
- rbx/resources/envs/isolate.rbx.yml +37 -0
- rbx/resources/packagers/boca/checker.sh +43 -0
- rbx/resources/packagers/boca/compare +53 -0
- rbx/resources/packagers/boca/compile/c +172 -0
- rbx/resources/packagers/boca/compile/cc +173 -0
- rbx/resources/packagers/boca/compile/cpp +172 -0
- rbx/resources/packagers/boca/compile/java +194 -0
- rbx/resources/packagers/boca/compile/kt +155 -0
- rbx/resources/packagers/boca/compile/pas +172 -0
- rbx/resources/packagers/boca/compile/py2 +173 -0
- rbx/resources/packagers/boca/compile/py3 +173 -0
- rbx/resources/packagers/boca/run/c +128 -0
- rbx/resources/packagers/boca/run/cc +128 -0
- rbx/resources/packagers/boca/run/cpp +128 -0
- rbx/resources/packagers/boca/run/java +194 -0
- rbx/resources/packagers/boca/run/kt +159 -0
- rbx/resources/packagers/boca/run/py2 +166 -0
- rbx/resources/packagers/boca/run/py3 +166 -0
- rbx/resources/presets/default/contest/contest.rbx.yml +14 -0
- rbx/resources/presets/default/contest/statement/contest.rbx.tex +97 -0
- rbx/resources/presets/default/contest/statement/olymp.sty +250 -0
- rbx/resources/presets/default/contest/statement/template.rbx.tex +42 -0
- rbx/resources/presets/default/preset.rbx.yml +12 -0
- rbx/resources/presets/default/problem/.gitignore +6 -0
- rbx/resources/presets/default/problem/gen.cpp +9 -0
- rbx/resources/presets/default/problem/problem.rbx.yml +44 -0
- rbx/resources/presets/default/problem/random.py +3 -0
- rbx/resources/presets/default/problem/random.txt +2 -0
- rbx/resources/presets/default/problem/sols/main.cpp +9 -0
- rbx/resources/presets/default/problem/sols/slow.cpp +15 -0
- rbx/resources/presets/default/problem/sols/wa.cpp +9 -0
- rbx/resources/presets/default/problem/statement/olymp.sty +250 -0
- rbx/resources/presets/default/problem/statement/projecao.png +0 -0
- rbx/resources/presets/default/problem/statement/statement.rbx.tex +18 -0
- rbx/resources/presets/default/problem/statement/template.rbx.tex +89 -0
- rbx/resources/presets/default/problem/tests/samples/000.in +1 -0
- rbx/resources/presets/default/problem/tests/samples/001.in +1 -0
- rbx/resources/presets/default/problem/validator.cpp +16 -0
- rbx/resources/presets/default/problem/wcmp.cpp +34 -0
- rbx/resources/templates/template.cpp +19 -0
- rbx/run.py +45 -0
- rbx/schema.py +64 -0
- rbx/submit.py +61 -0
- rbx/submitors/__init__.py +18 -0
- rbx/submitors/codeforces.py +120 -0
- rbx/submitors/submitor.py +25 -0
- rbx/test.py +347 -0
- rbx/testcase.py +70 -0
- rbx/testcase_rendering.py +79 -0
- rbx/testdata/box1/gen1.cpp +7 -0
- rbx/testdata/box1/gen2.cpp +9 -0
- rbx/testdata/box1/genScript.py +2 -0
- rbx/testdata/box1/hard-tle.sol.cpp +26 -0
- rbx/testdata/box1/ole.cpp +17 -0
- rbx/testdata/box1/problem.rbx.yml +39 -0
- rbx/testdata/box1/re.sol.cpp +23 -0
- rbx/testdata/box1/sol.cpp +22 -0
- rbx/testdata/box1/tests/1.in +1 -0
- rbx/testdata/box1/tle-and-incorrect.sol.cpp +33 -0
- rbx/testdata/box1/tle.sol.cpp +35 -0
- rbx/testdata/box1/validator.cpp +11 -0
- rbx/testdata/box1/wa.sol.cpp +22 -0
- rbx/testdata/caching/executable.py +1 -0
- rbx/testdata/compatible +0 -0
- rbx/testing_utils.py +65 -0
- rbx/utils.py +162 -0
- rbx_cp-0.5.0.dist-info/LICENSE +201 -0
- rbx_cp-0.5.0.dist-info/METADATA +89 -0
- rbx_cp-0.5.0.dist-info/RECORD +164 -0
- rbx_cp-0.5.0.dist-info/WHEEL +4 -0
- rbx_cp-0.5.0.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,375 @@
|
|
1
|
+
import dataclasses
|
2
|
+
import pathlib
|
3
|
+
import re
|
4
|
+
import shutil
|
5
|
+
import typing
|
6
|
+
from abc import ABC, abstractmethod
|
7
|
+
from typing import Any, Dict, List, Optional, Tuple
|
8
|
+
|
9
|
+
import typer
|
10
|
+
|
11
|
+
from rbx import console
|
12
|
+
from rbx.box.schema import Package, Primitive, Testcase
|
13
|
+
from rbx.box.statements.latex import (
|
14
|
+
MAX_PDFLATEX_RUNS,
|
15
|
+
Latex,
|
16
|
+
decode_latex_output,
|
17
|
+
should_rerun,
|
18
|
+
)
|
19
|
+
from rbx.box.statements.latex_jinja import (
|
20
|
+
JinjaDictWrapper,
|
21
|
+
render_latex_template,
|
22
|
+
render_latex_template_blocks,
|
23
|
+
)
|
24
|
+
from rbx.box.statements.schema import (
|
25
|
+
ConversionStep,
|
26
|
+
ConversionType,
|
27
|
+
JinjaTeX,
|
28
|
+
Statement,
|
29
|
+
StatementType,
|
30
|
+
TexToPDF,
|
31
|
+
rbxToTeX,
|
32
|
+
)
|
33
|
+
|
34
|
+
|
35
|
+
@dataclasses.dataclass
|
36
|
+
class StatementCodeLanguage:
|
37
|
+
id: str
|
38
|
+
name: str
|
39
|
+
command: str
|
40
|
+
|
41
|
+
|
42
|
+
@dataclasses.dataclass
|
43
|
+
class StatementBuilderContext:
|
44
|
+
languages: List[StatementCodeLanguage]
|
45
|
+
params: ConversionStep
|
46
|
+
root: pathlib.Path
|
47
|
+
editorial: bool
|
48
|
+
vars: Optional[Dict[str, Primitive]] = None
|
49
|
+
|
50
|
+
def build_jinja_kwargs(self) -> Dict[str, Any]:
|
51
|
+
res = {
|
52
|
+
'languages': self.languages,
|
53
|
+
'keyed_languages': {lang.id: lang for lang in self.languages},
|
54
|
+
'is_editorial': self.editorial,
|
55
|
+
}
|
56
|
+
if self.vars is not None:
|
57
|
+
res['vars'] = self.vars
|
58
|
+
return res
|
59
|
+
|
60
|
+
|
61
|
+
class StatementBuilderItem(ABC):
|
62
|
+
@abstractmethod
|
63
|
+
def build_jinja_kwargs(self) -> Dict[str, Any]:
|
64
|
+
pass
|
65
|
+
|
66
|
+
|
67
|
+
class StatementSample(Testcase):
|
68
|
+
explanation: Optional[str] = None
|
69
|
+
|
70
|
+
|
71
|
+
@dataclasses.dataclass
|
72
|
+
class StatementBuilderProblem(StatementBuilderItem):
|
73
|
+
package: Package
|
74
|
+
statement: Statement
|
75
|
+
samples: List[Testcase] = dataclasses.field(default_factory=list)
|
76
|
+
short_name: Optional[str] = None
|
77
|
+
|
78
|
+
# Will only be filled by contests.
|
79
|
+
io_path: Optional[pathlib.Path] = None
|
80
|
+
|
81
|
+
def build_inner_jinja_kwargs(self) -> Dict[str, Any]:
|
82
|
+
kwargs = {
|
83
|
+
'package': self.package,
|
84
|
+
'statement': self.statement,
|
85
|
+
'samples': self.samples,
|
86
|
+
'vars': JinjaDictWrapper(self.package.expanded_vars, key='vars'),
|
87
|
+
'title': self.statement.title or self.package.name,
|
88
|
+
}
|
89
|
+
if self.short_name is not None:
|
90
|
+
kwargs['short_name'] = self.short_name
|
91
|
+
if self.io_path is not None:
|
92
|
+
kwargs['path'] = self.io_path
|
93
|
+
return kwargs
|
94
|
+
|
95
|
+
def build_jinja_kwargs(self) -> Dict[str, Any]:
|
96
|
+
inner = self.build_inner_jinja_kwargs()
|
97
|
+
return {
|
98
|
+
'problem': inner,
|
99
|
+
}
|
100
|
+
|
101
|
+
|
102
|
+
@dataclasses.dataclass
|
103
|
+
class StatementBuilderContest(StatementBuilderItem):
|
104
|
+
title: str
|
105
|
+
location: Optional[str] = None
|
106
|
+
date: Optional[str] = None
|
107
|
+
problems: List[StatementBuilderProblem] = dataclasses.field(default_factory=list)
|
108
|
+
|
109
|
+
def build_inner_jinja_kwargs(self) -> Dict[str, Any]:
|
110
|
+
res = {'title': self.title}
|
111
|
+
if self.location:
|
112
|
+
res['location'] = self.location
|
113
|
+
if self.date:
|
114
|
+
res['date'] = self.date
|
115
|
+
return res
|
116
|
+
|
117
|
+
def build_jinja_kwargs(self) -> Dict[str, Any]:
|
118
|
+
res = {
|
119
|
+
'contest': self.build_inner_jinja_kwargs(),
|
120
|
+
'problems': [
|
121
|
+
problem.build_inner_jinja_kwargs() for problem in self.problems
|
122
|
+
],
|
123
|
+
}
|
124
|
+
return res
|
125
|
+
|
126
|
+
|
127
|
+
@dataclasses.dataclass
|
128
|
+
class StatementBlocks:
|
129
|
+
blocks: Dict[str, str] = dataclasses.field(default_factory=dict)
|
130
|
+
explanations: Dict[int, str] = dataclasses.field(default_factory=dict)
|
131
|
+
|
132
|
+
|
133
|
+
def prepare_assets(
|
134
|
+
assets: List[Tuple[pathlib.Path, pathlib.Path]],
|
135
|
+
dest_dir: pathlib.Path,
|
136
|
+
):
|
137
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
138
|
+
|
139
|
+
for asset_in, asset_out in assets:
|
140
|
+
if not asset_in.is_file():
|
141
|
+
console.console.print(
|
142
|
+
f'[error]Asset [item]{asset_in}[/item] does not exist in your package.[/error]'
|
143
|
+
)
|
144
|
+
raise typer.Exit(1)
|
145
|
+
|
146
|
+
# dest_path = dest_dir / asset.resolve().relative_to(statement_dir)
|
147
|
+
dest_path = dest_dir / asset_out
|
148
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
149
|
+
shutil.copyfile(str(asset_in), str(dest_path))
|
150
|
+
|
151
|
+
|
152
|
+
def render_jinja(root: pathlib.Path, content: bytes, **kwargs) -> bytes:
|
153
|
+
temp_file = '__input__.tex'
|
154
|
+
temp_path = root / temp_file
|
155
|
+
temp_path.write_bytes(content)
|
156
|
+
|
157
|
+
result: str = render_latex_template(
|
158
|
+
str(root),
|
159
|
+
temp_file,
|
160
|
+
kwargs,
|
161
|
+
)
|
162
|
+
return result.encode()
|
163
|
+
|
164
|
+
|
165
|
+
def render_jinja_blocks(
|
166
|
+
root: pathlib.Path, content: bytes, **kwargs
|
167
|
+
) -> StatementBlocks:
|
168
|
+
temp_file = '__input__.tex'
|
169
|
+
temp_path = root / temp_file
|
170
|
+
temp_path.write_bytes(content)
|
171
|
+
|
172
|
+
result: Dict[str, str] = render_latex_template_blocks(
|
173
|
+
str(root),
|
174
|
+
temp_file,
|
175
|
+
kwargs,
|
176
|
+
)
|
177
|
+
|
178
|
+
pattern = re.compile(r'explanation_(\d+)')
|
179
|
+
explanation_keys = []
|
180
|
+
for key in result:
|
181
|
+
if match := pattern.match(key):
|
182
|
+
explanation_keys.append((key, int(match.group(1))))
|
183
|
+
|
184
|
+
explanations = {value: result[key] for key, value in explanation_keys}
|
185
|
+
return StatementBlocks(blocks=result, explanations=explanations)
|
186
|
+
|
187
|
+
|
188
|
+
class StatementBuilder(ABC):
|
189
|
+
@abstractmethod
|
190
|
+
def name(self) -> ConversionType:
|
191
|
+
pass
|
192
|
+
|
193
|
+
@abstractmethod
|
194
|
+
def default_params(self) -> ConversionStep:
|
195
|
+
pass
|
196
|
+
|
197
|
+
@abstractmethod
|
198
|
+
def input_type(self) -> StatementType:
|
199
|
+
pass
|
200
|
+
|
201
|
+
@abstractmethod
|
202
|
+
def output_type(self) -> StatementType:
|
203
|
+
pass
|
204
|
+
|
205
|
+
def handles_contest(self) -> bool:
|
206
|
+
return True
|
207
|
+
|
208
|
+
def handles_problem(self) -> bool:
|
209
|
+
return True
|
210
|
+
|
211
|
+
def inject_assets(
|
212
|
+
self, root: pathlib.Path, params: ConversionStep
|
213
|
+
) -> List[Tuple[pathlib.Path, pathlib.Path]]:
|
214
|
+
return []
|
215
|
+
|
216
|
+
@abstractmethod
|
217
|
+
def build(
|
218
|
+
self,
|
219
|
+
input: bytes,
|
220
|
+
context: StatementBuilderContext,
|
221
|
+
item: StatementBuilderItem,
|
222
|
+
verbose: bool = False,
|
223
|
+
) -> bytes:
|
224
|
+
pass
|
225
|
+
|
226
|
+
|
227
|
+
class JinjaTeXBuilder(StatementBuilder):
|
228
|
+
def name(self) -> ConversionType:
|
229
|
+
return ConversionType.JinjaTeX
|
230
|
+
|
231
|
+
def default_params(self) -> ConversionStep:
|
232
|
+
return JinjaTeX(type=ConversionType.JinjaTeX)
|
233
|
+
|
234
|
+
def input_type(self) -> StatementType:
|
235
|
+
return StatementType.JinjaTeX
|
236
|
+
|
237
|
+
def output_type(self) -> StatementType:
|
238
|
+
return StatementType.TeX
|
239
|
+
|
240
|
+
def build(
|
241
|
+
self,
|
242
|
+
input: bytes,
|
243
|
+
context: StatementBuilderContext,
|
244
|
+
item: StatementBuilderItem,
|
245
|
+
verbose: bool = False,
|
246
|
+
) -> bytes:
|
247
|
+
return render_jinja(
|
248
|
+
context.root,
|
249
|
+
input,
|
250
|
+
**context.build_jinja_kwargs(),
|
251
|
+
**item.build_jinja_kwargs(),
|
252
|
+
)
|
253
|
+
|
254
|
+
|
255
|
+
class rbxTeXBuilder(StatementBuilder):
|
256
|
+
def name(self) -> ConversionType:
|
257
|
+
return ConversionType.rbxToTex
|
258
|
+
|
259
|
+
def default_params(self) -> ConversionStep:
|
260
|
+
return rbxToTeX(type=ConversionType.rbxToTex)
|
261
|
+
|
262
|
+
def input_type(self) -> StatementType:
|
263
|
+
return StatementType.rbxTeX
|
264
|
+
|
265
|
+
def output_type(self) -> StatementType:
|
266
|
+
return StatementType.TeX
|
267
|
+
|
268
|
+
def handles_contest(self) -> bool:
|
269
|
+
# This builder cannot build contest statements.
|
270
|
+
return False
|
271
|
+
|
272
|
+
def inject_assets(
|
273
|
+
self, root: pathlib.Path, params: ConversionStep
|
274
|
+
) -> List[Tuple[pathlib.Path, pathlib.Path]]:
|
275
|
+
params = typing.cast(rbxToTeX, params)
|
276
|
+
if not params.template:
|
277
|
+
return []
|
278
|
+
return [((root / params.template).resolve(), params.template)]
|
279
|
+
|
280
|
+
def build(
|
281
|
+
self,
|
282
|
+
input: bytes,
|
283
|
+
context: StatementBuilderContext,
|
284
|
+
item: StatementBuilderItem,
|
285
|
+
verbose: bool = False,
|
286
|
+
) -> bytes:
|
287
|
+
params = typing.cast(rbxToTeX, context.params)
|
288
|
+
assert params.template is not None
|
289
|
+
problem = typing.cast(StatementBuilderProblem, item)
|
290
|
+
|
291
|
+
statement_blocks = render_jinja_blocks(
|
292
|
+
context.root, input, **problem.build_inner_jinja_kwargs()
|
293
|
+
)
|
294
|
+
blocks = statement_blocks.blocks
|
295
|
+
|
296
|
+
# Remove editorial block when not editorial.
|
297
|
+
if not context.editorial and 'editorial' in blocks:
|
298
|
+
del blocks['editorial']
|
299
|
+
|
300
|
+
problem_kwargs = problem.build_jinja_kwargs()
|
301
|
+
problem_kwargs['problem']['blocks'] = blocks
|
302
|
+
if statement_blocks.explanations is not None:
|
303
|
+
problem_kwargs['problem']['samples'] = [
|
304
|
+
StatementSample(
|
305
|
+
**typing.cast(Testcase, sample).model_dump(),
|
306
|
+
explanation=statement_blocks.explanations.get(i),
|
307
|
+
)
|
308
|
+
for i, sample in enumerate(problem_kwargs['problem']['samples'])
|
309
|
+
]
|
310
|
+
|
311
|
+
return render_jinja(
|
312
|
+
context.root,
|
313
|
+
f'%- extends "{params.template}"'.encode(),
|
314
|
+
**context.build_jinja_kwargs(),
|
315
|
+
**problem_kwargs,
|
316
|
+
)
|
317
|
+
|
318
|
+
|
319
|
+
class TeX2PDFBuilder(StatementBuilder):
|
320
|
+
def name(self) -> ConversionType:
|
321
|
+
return ConversionType.TexToPDF
|
322
|
+
|
323
|
+
def default_params(self) -> ConversionStep:
|
324
|
+
return TexToPDF(type=ConversionType.TexToPDF)
|
325
|
+
|
326
|
+
def input_type(self) -> StatementType:
|
327
|
+
return StatementType.TeX
|
328
|
+
|
329
|
+
def output_type(self) -> StatementType:
|
330
|
+
return StatementType.PDF
|
331
|
+
|
332
|
+
def build(
|
333
|
+
self,
|
334
|
+
input: bytes,
|
335
|
+
context: StatementBuilderContext,
|
336
|
+
item: StatementBuilderItem,
|
337
|
+
verbose: bool = False,
|
338
|
+
) -> bytes:
|
339
|
+
latex = Latex(input.decode())
|
340
|
+
latex_result = latex.build_pdf(context.root)
|
341
|
+
pdf = latex_result.pdf
|
342
|
+
logs = decode_latex_output(latex_result.result.stdout)
|
343
|
+
runs = 1
|
344
|
+
|
345
|
+
while pdf is not None and should_rerun(logs) and runs < MAX_PDFLATEX_RUNS:
|
346
|
+
console.console.print(
|
347
|
+
'Re-running pdfLaTeX to get cross-references right...'
|
348
|
+
)
|
349
|
+
latex_result = latex.build_pdf(context.root)
|
350
|
+
pdf = latex_result.pdf
|
351
|
+
logs = decode_latex_output(latex_result.result.stdout)
|
352
|
+
runs += 1
|
353
|
+
|
354
|
+
if pdf is None:
|
355
|
+
console.console.print(f'{logs}')
|
356
|
+
console.console.print('[error]PdfLaTeX compilation failed.[/error]')
|
357
|
+
raise typer.Exit(1)
|
358
|
+
|
359
|
+
if verbose:
|
360
|
+
console.console.print(f'{logs}')
|
361
|
+
|
362
|
+
return pdf
|
363
|
+
|
364
|
+
|
365
|
+
BUILDER_LIST: List[StatementBuilder] = [
|
366
|
+
TeX2PDFBuilder(),
|
367
|
+
JinjaTeXBuilder(),
|
368
|
+
rbxTeXBuilder(),
|
369
|
+
]
|
370
|
+
PROBLEM_BUILDER_LIST = [
|
371
|
+
builder for builder in BUILDER_LIST if builder.handles_problem()
|
372
|
+
]
|
373
|
+
CONTEST_BUILDER_LIST = [
|
374
|
+
builder for builder in BUILDER_LIST if builder.handles_contest()
|
375
|
+
]
|
@@ -0,0 +1,113 @@
|
|
1
|
+
import dataclasses
|
2
|
+
import pathlib
|
3
|
+
from abc import ABC, abstractmethod
|
4
|
+
from typing import Any, Dict, List
|
5
|
+
|
6
|
+
import typer
|
7
|
+
|
8
|
+
from rbx import console
|
9
|
+
from rbx.box.statements.builders import (
|
10
|
+
StatementBuilderContest,
|
11
|
+
StatementCodeLanguage,
|
12
|
+
)
|
13
|
+
from rbx.box.statements.latex import (
|
14
|
+
MAX_PDFLATEX_RUNS,
|
15
|
+
Latex,
|
16
|
+
decode_latex_output,
|
17
|
+
should_rerun,
|
18
|
+
)
|
19
|
+
from rbx.box.statements.schema import Joiner, JoinerType, JoinTexToPDF, StatementType
|
20
|
+
|
21
|
+
|
22
|
+
@dataclasses.dataclass
|
23
|
+
class StatementJoinerContext:
|
24
|
+
languages: List[StatementCodeLanguage]
|
25
|
+
params: Joiner
|
26
|
+
root: pathlib.Path
|
27
|
+
|
28
|
+
def build_jinja_kwargs(self) -> Dict[str, Any]:
|
29
|
+
return {'languages': self.languages}
|
30
|
+
|
31
|
+
|
32
|
+
class StatementJoiner(ABC):
|
33
|
+
@abstractmethod
|
34
|
+
def name(self) -> JoinerType:
|
35
|
+
pass
|
36
|
+
|
37
|
+
@abstractmethod
|
38
|
+
def default_params(self) -> Joiner:
|
39
|
+
pass
|
40
|
+
|
41
|
+
@abstractmethod
|
42
|
+
def input_type(self) -> StatementType:
|
43
|
+
pass
|
44
|
+
|
45
|
+
@abstractmethod
|
46
|
+
def output_type(self) -> StatementType:
|
47
|
+
pass
|
48
|
+
|
49
|
+
@abstractmethod
|
50
|
+
def joined_type(self) -> StatementType:
|
51
|
+
pass
|
52
|
+
|
53
|
+
@abstractmethod
|
54
|
+
def build(
|
55
|
+
self,
|
56
|
+
input: bytes,
|
57
|
+
context: StatementJoinerContext,
|
58
|
+
contest: StatementBuilderContest,
|
59
|
+
verbose: bool = False,
|
60
|
+
) -> bytes:
|
61
|
+
pass
|
62
|
+
|
63
|
+
|
64
|
+
class TeX2PDFJoiner(StatementJoiner):
|
65
|
+
def name(self) -> JoinerType:
|
66
|
+
return JoinerType.TexToPDF
|
67
|
+
|
68
|
+
def default_params(self) -> Joiner:
|
69
|
+
return JoinTexToPDF(type=JoinerType.TexToPDF)
|
70
|
+
|
71
|
+
def input_type(self) -> StatementType:
|
72
|
+
return StatementType.TeX
|
73
|
+
|
74
|
+
def output_type(self) -> StatementType:
|
75
|
+
return StatementType.PDF
|
76
|
+
|
77
|
+
def joined_type(self) -> StatementType:
|
78
|
+
return StatementType.TeX
|
79
|
+
|
80
|
+
def build(
|
81
|
+
self,
|
82
|
+
input: bytes,
|
83
|
+
context: StatementJoinerContext,
|
84
|
+
contest: StatementBuilderContest,
|
85
|
+
verbose: bool = False,
|
86
|
+
) -> bytes:
|
87
|
+
latex = Latex(input.decode())
|
88
|
+
latex_result = latex.build_pdf(context.root)
|
89
|
+
pdf = latex_result.pdf
|
90
|
+
logs = decode_latex_output(latex_result.result.stdout)
|
91
|
+
runs = 1
|
92
|
+
|
93
|
+
while pdf is not None and should_rerun(logs) and runs < MAX_PDFLATEX_RUNS:
|
94
|
+
console.console.print(
|
95
|
+
'Re-running pdfLaTeX to get cross-references right...'
|
96
|
+
)
|
97
|
+
latex_result = latex.build_pdf(context.root)
|
98
|
+
pdf = latex_result.pdf
|
99
|
+
logs = decode_latex_output(latex_result.result.stdout)
|
100
|
+
runs += 1
|
101
|
+
|
102
|
+
if pdf is None:
|
103
|
+
console.console.print(f'{logs}')
|
104
|
+
console.console.print('[error]PdfLaTeX compilation failed.[/error]')
|
105
|
+
raise typer.Exit(1)
|
106
|
+
|
107
|
+
if verbose:
|
108
|
+
console.console.print(f'{logs}')
|
109
|
+
|
110
|
+
return pdf
|
111
|
+
|
112
|
+
|
113
|
+
JOINER_LIST: List[StatementJoiner] = [TeX2PDFJoiner()]
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import dataclasses
|
2
|
+
import pathlib
|
3
|
+
import subprocess
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
import chardet
|
7
|
+
|
8
|
+
MAX_PDFLATEX_RUNS = 3
|
9
|
+
|
10
|
+
|
11
|
+
def should_rerun(logs: str) -> bool:
|
12
|
+
logs = logs.lower()
|
13
|
+
for line in logs.splitlines():
|
14
|
+
if 'rerun to get cross-references right' in line:
|
15
|
+
return True
|
16
|
+
if 'rerun' in line and 'warning' in line:
|
17
|
+
return True
|
18
|
+
return False
|
19
|
+
|
20
|
+
|
21
|
+
def decode_latex_output(output: bytes) -> str:
|
22
|
+
# Latex output can be tricky with decoding
|
23
|
+
encoding = chardet.detect(output)['encoding'] or 'utf-8'
|
24
|
+
return output.decode(encoding)
|
25
|
+
|
26
|
+
|
27
|
+
@dataclasses.dataclass
|
28
|
+
class LatexResult:
|
29
|
+
result: subprocess.CompletedProcess[bytes]
|
30
|
+
pdf: Optional[bytes]
|
31
|
+
|
32
|
+
|
33
|
+
class Latex:
|
34
|
+
def __init__(self, latex: str):
|
35
|
+
self.latex = latex
|
36
|
+
|
37
|
+
def build_pdf(self, temp_dir: pathlib.Path) -> LatexResult:
|
38
|
+
temp_path = temp_dir / 'statement.tex'
|
39
|
+
output_path = temp_path.with_suffix('.pdf')
|
40
|
+
args = ['pdflatex', '-interaction', 'nonstopmode', str(temp_path)]
|
41
|
+
temp_path.write_text(self.latex)
|
42
|
+
|
43
|
+
completed = subprocess.run(args, timeout=15, capture_output=True, cwd=temp_dir)
|
44
|
+
if completed.returncode != 0 or not output_path.exists():
|
45
|
+
return LatexResult(result=completed, pdf=None)
|
46
|
+
|
47
|
+
return LatexResult(result=completed, pdf=output_path.read_bytes())
|