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.
Files changed (164) hide show
  1. rbx/__init__.py +0 -0
  2. rbx/annotations.py +127 -0
  3. rbx/autoenum.py +333 -0
  4. rbx/box/__init__.py +0 -0
  5. rbx/box/builder.py +77 -0
  6. rbx/box/cd.py +37 -0
  7. rbx/box/checkers.py +134 -0
  8. rbx/box/code.py +185 -0
  9. rbx/box/compile.py +56 -0
  10. rbx/box/conftest.py +42 -0
  11. rbx/box/contest/__init__.py +0 -0
  12. rbx/box/contest/build_contest_statements.py +347 -0
  13. rbx/box/contest/contest_package.py +76 -0
  14. rbx/box/contest/contest_utils.py +20 -0
  15. rbx/box/contest/main.py +179 -0
  16. rbx/box/contest/schema.py +155 -0
  17. rbx/box/contest/statements.py +82 -0
  18. rbx/box/creation.py +72 -0
  19. rbx/box/download.py +64 -0
  20. rbx/box/environment.py +345 -0
  21. rbx/box/extensions.py +26 -0
  22. rbx/box/generators.py +478 -0
  23. rbx/box/generators_test.py +63 -0
  24. rbx/box/main.py +449 -0
  25. rbx/box/package.py +316 -0
  26. rbx/box/packaging/boca/extension.py +27 -0
  27. rbx/box/packaging/boca/packager.py +245 -0
  28. rbx/box/packaging/contest_main.py +82 -0
  29. rbx/box/packaging/main.py +68 -0
  30. rbx/box/packaging/packager.py +117 -0
  31. rbx/box/packaging/polygon/packager.py +320 -0
  32. rbx/box/packaging/polygon/test.py +81 -0
  33. rbx/box/packaging/polygon/xml_schema.py +106 -0
  34. rbx/box/presets/__init__.py +503 -0
  35. rbx/box/presets/fetch.py +70 -0
  36. rbx/box/presets/lock_schema.py +20 -0
  37. rbx/box/presets/schema.py +59 -0
  38. rbx/box/schema.py +394 -0
  39. rbx/box/solutions.py +792 -0
  40. rbx/box/solutions_test.py +41 -0
  41. rbx/box/statements/__init__.py +0 -0
  42. rbx/box/statements/build_statements.py +359 -0
  43. rbx/box/statements/builders.py +375 -0
  44. rbx/box/statements/joiners.py +113 -0
  45. rbx/box/statements/latex.py +47 -0
  46. rbx/box/statements/latex_jinja.py +214 -0
  47. rbx/box/statements/schema.py +138 -0
  48. rbx/box/stresses.py +292 -0
  49. rbx/box/stressing/__init__.py +0 -0
  50. rbx/box/stressing/finder_parser.py +359 -0
  51. rbx/box/stressing/generator_parser.py +258 -0
  52. rbx/box/testcases.py +54 -0
  53. rbx/box/ui/__init__.py +0 -0
  54. rbx/box/ui/captured_log.py +372 -0
  55. rbx/box/ui/css/app.tcss +48 -0
  56. rbx/box/ui/main.py +38 -0
  57. rbx/box/ui/run.py +209 -0
  58. rbx/box/validators.py +245 -0
  59. rbx/box/validators_test.py +15 -0
  60. rbx/checker.py +128 -0
  61. rbx/clone.py +197 -0
  62. rbx/config.py +271 -0
  63. rbx/conftest.py +38 -0
  64. rbx/console.py +27 -0
  65. rbx/create.py +37 -0
  66. rbx/edit.py +24 -0
  67. rbx/grading/__init__.py +0 -0
  68. rbx/grading/caching.py +356 -0
  69. rbx/grading/conftest.py +33 -0
  70. rbx/grading/judge/__init__.py +0 -0
  71. rbx/grading/judge/cacher.py +503 -0
  72. rbx/grading/judge/digester.py +35 -0
  73. rbx/grading/judge/sandbox.py +748 -0
  74. rbx/grading/judge/sandboxes/__init__.py +0 -0
  75. rbx/grading/judge/sandboxes/isolate.py +683 -0
  76. rbx/grading/judge/sandboxes/stupid_sandbox.py +310 -0
  77. rbx/grading/judge/sandboxes/timeit.py +217 -0
  78. rbx/grading/judge/storage.py +284 -0
  79. rbx/grading/judge/test.py +38 -0
  80. rbx/grading/judge/testiso.py +54 -0
  81. rbx/grading/steps.py +522 -0
  82. rbx/grading/steps_with_caching.py +59 -0
  83. rbx/grading/steps_with_caching_run_test.py +429 -0
  84. rbx/grading_utils.py +148 -0
  85. rbx/hydration.py +101 -0
  86. rbx/main.py +122 -0
  87. rbx/metadata.py +105 -0
  88. rbx/providers/__init__.py +43 -0
  89. rbx/providers/codeforces.py +73 -0
  90. rbx/providers/provider.py +26 -0
  91. rbx/resources/checkers/boilerplate.cpp +20 -0
  92. rbx/resources/default_config.json +48 -0
  93. rbx/resources/envs/default.rbx.yml +37 -0
  94. rbx/resources/envs/isolate.rbx.yml +37 -0
  95. rbx/resources/packagers/boca/checker.sh +43 -0
  96. rbx/resources/packagers/boca/compare +53 -0
  97. rbx/resources/packagers/boca/compile/c +172 -0
  98. rbx/resources/packagers/boca/compile/cc +173 -0
  99. rbx/resources/packagers/boca/compile/cpp +172 -0
  100. rbx/resources/packagers/boca/compile/java +194 -0
  101. rbx/resources/packagers/boca/compile/kt +155 -0
  102. rbx/resources/packagers/boca/compile/pas +172 -0
  103. rbx/resources/packagers/boca/compile/py2 +173 -0
  104. rbx/resources/packagers/boca/compile/py3 +173 -0
  105. rbx/resources/packagers/boca/run/c +128 -0
  106. rbx/resources/packagers/boca/run/cc +128 -0
  107. rbx/resources/packagers/boca/run/cpp +128 -0
  108. rbx/resources/packagers/boca/run/java +194 -0
  109. rbx/resources/packagers/boca/run/kt +159 -0
  110. rbx/resources/packagers/boca/run/py2 +166 -0
  111. rbx/resources/packagers/boca/run/py3 +166 -0
  112. rbx/resources/presets/default/contest/contest.rbx.yml +14 -0
  113. rbx/resources/presets/default/contest/statement/contest.rbx.tex +97 -0
  114. rbx/resources/presets/default/contest/statement/olymp.sty +250 -0
  115. rbx/resources/presets/default/contest/statement/template.rbx.tex +42 -0
  116. rbx/resources/presets/default/preset.rbx.yml +12 -0
  117. rbx/resources/presets/default/problem/.gitignore +6 -0
  118. rbx/resources/presets/default/problem/gen.cpp +9 -0
  119. rbx/resources/presets/default/problem/problem.rbx.yml +44 -0
  120. rbx/resources/presets/default/problem/random.py +3 -0
  121. rbx/resources/presets/default/problem/random.txt +2 -0
  122. rbx/resources/presets/default/problem/sols/main.cpp +9 -0
  123. rbx/resources/presets/default/problem/sols/slow.cpp +15 -0
  124. rbx/resources/presets/default/problem/sols/wa.cpp +9 -0
  125. rbx/resources/presets/default/problem/statement/olymp.sty +250 -0
  126. rbx/resources/presets/default/problem/statement/projecao.png +0 -0
  127. rbx/resources/presets/default/problem/statement/statement.rbx.tex +18 -0
  128. rbx/resources/presets/default/problem/statement/template.rbx.tex +89 -0
  129. rbx/resources/presets/default/problem/tests/samples/000.in +1 -0
  130. rbx/resources/presets/default/problem/tests/samples/001.in +1 -0
  131. rbx/resources/presets/default/problem/validator.cpp +16 -0
  132. rbx/resources/presets/default/problem/wcmp.cpp +34 -0
  133. rbx/resources/templates/template.cpp +19 -0
  134. rbx/run.py +45 -0
  135. rbx/schema.py +64 -0
  136. rbx/submit.py +61 -0
  137. rbx/submitors/__init__.py +18 -0
  138. rbx/submitors/codeforces.py +120 -0
  139. rbx/submitors/submitor.py +25 -0
  140. rbx/test.py +347 -0
  141. rbx/testcase.py +70 -0
  142. rbx/testcase_rendering.py +79 -0
  143. rbx/testdata/box1/gen1.cpp +7 -0
  144. rbx/testdata/box1/gen2.cpp +9 -0
  145. rbx/testdata/box1/genScript.py +2 -0
  146. rbx/testdata/box1/hard-tle.sol.cpp +26 -0
  147. rbx/testdata/box1/ole.cpp +17 -0
  148. rbx/testdata/box1/problem.rbx.yml +39 -0
  149. rbx/testdata/box1/re.sol.cpp +23 -0
  150. rbx/testdata/box1/sol.cpp +22 -0
  151. rbx/testdata/box1/tests/1.in +1 -0
  152. rbx/testdata/box1/tle-and-incorrect.sol.cpp +33 -0
  153. rbx/testdata/box1/tle.sol.cpp +35 -0
  154. rbx/testdata/box1/validator.cpp +11 -0
  155. rbx/testdata/box1/wa.sol.cpp +22 -0
  156. rbx/testdata/caching/executable.py +1 -0
  157. rbx/testdata/compatible +0 -0
  158. rbx/testing_utils.py +65 -0
  159. rbx/utils.py +162 -0
  160. rbx_cp-0.5.0.dist-info/LICENSE +201 -0
  161. rbx_cp-0.5.0.dist-info/METADATA +89 -0
  162. rbx_cp-0.5.0.dist-info/RECORD +164 -0
  163. rbx_cp-0.5.0.dist-info/WHEEL +4 -0
  164. 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())