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
rbx/box/schema.py
ADDED
@@ -0,0 +1,394 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import pathlib
|
4
|
+
from typing import Dict, List, Optional, Union
|
5
|
+
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
7
|
+
from pydantic_core import PydanticCustomError
|
8
|
+
|
9
|
+
from rbx.autoenum import AutoEnum, alias
|
10
|
+
from rbx.box.statements.schema import Statement
|
11
|
+
from rbx.grading.steps import Outcome
|
12
|
+
|
13
|
+
Primitive = Union[str, int, float, bool]
|
14
|
+
|
15
|
+
|
16
|
+
def NameField(**kwargs):
|
17
|
+
return Field(
|
18
|
+
pattern=r'^[a-zA-Z0-9][a-zA-Z0-9\-_]*$', min_length=3, max_length=32, **kwargs
|
19
|
+
)
|
20
|
+
|
21
|
+
|
22
|
+
def _check_oneof(model_obj: BaseModel, fields: List[str]):
|
23
|
+
has = []
|
24
|
+
for field in fields:
|
25
|
+
if hasattr(model_obj, field) and getattr(model_obj, field):
|
26
|
+
has.append(field)
|
27
|
+
if len(has) <= 1:
|
28
|
+
return
|
29
|
+
raise ValueError(
|
30
|
+
f'fields {has} were specified at the same time '
|
31
|
+
'in a testgroup; only one of them can be specified'
|
32
|
+
)
|
33
|
+
|
34
|
+
|
35
|
+
def expand_var(value: Primitive) -> Primitive:
|
36
|
+
if not isinstance(value, str):
|
37
|
+
return value
|
38
|
+
if value.startswith('\\'):
|
39
|
+
return value[1:]
|
40
|
+
if not value.startswith('py`') or not value.endswith('`'):
|
41
|
+
return value
|
42
|
+
res = eval(value[3:-1])
|
43
|
+
for supported_type in [str, int, float, bool]:
|
44
|
+
if isinstance(res, supported_type):
|
45
|
+
return res
|
46
|
+
|
47
|
+
raise TypeError(
|
48
|
+
f'Variable with backticks should evaluate to a primitive Python type: {value}'
|
49
|
+
)
|
50
|
+
|
51
|
+
|
52
|
+
class ExpectedOutcome(AutoEnum):
|
53
|
+
ACCEPTED = alias('accepted', 'ac', 'correct') # type: ignore
|
54
|
+
"""Expected outcome for correct solutions (AC)."""
|
55
|
+
|
56
|
+
WRONG_ANSWER = alias('wrong answer', 'wa') # type: ignore
|
57
|
+
"""Expected outcome for solutions that finish successfully,
|
58
|
+
but the produced output are incorrect (WA)."""
|
59
|
+
|
60
|
+
INCORRECT = alias('fail', 'incorrect') # type: ignore
|
61
|
+
"""Expected outcome for solutions that finish with any non-AC verdict."""
|
62
|
+
|
63
|
+
RUNTIME_ERROR = alias('runtime error', 'rte', 're') # type: ignore
|
64
|
+
"""Expected outcome solutions that finish with non-zero code (RTE)."""
|
65
|
+
|
66
|
+
TIME_LIMIT_EXCEEDED = alias('time limit exceeded', 'timeout', 'tle') # type: ignore
|
67
|
+
"""Expected outcome for solutions that do not finish in time."""
|
68
|
+
|
69
|
+
MEMORY_LIMIT_EXCEEDED = alias('memory limit exceeded', 'mle') # type: ignore
|
70
|
+
"""Expected outcome for solutions that use more memory than allowed."""
|
71
|
+
|
72
|
+
OUTPUT_LIMIT_EXCEEDED = alias('output limit exceeded', 'ole') # type: ignore
|
73
|
+
"""Expected outcome for solutions that use more output than allowed."""
|
74
|
+
|
75
|
+
TLE_OR_RTE = alias('tle or rte', 'tle/rte', 'tle+rte') # type: ignore
|
76
|
+
"""Expected outcome for solutions that finish with either TLE or RTE.
|
77
|
+
|
78
|
+
Especially useful for environments where TLE and RTE are indistinguishable."""
|
79
|
+
|
80
|
+
def style(self) -> str:
|
81
|
+
if self == ExpectedOutcome.ACCEPTED:
|
82
|
+
return 'green'
|
83
|
+
if self == ExpectedOutcome.WRONG_ANSWER:
|
84
|
+
return 'red'
|
85
|
+
if self == ExpectedOutcome.INCORRECT:
|
86
|
+
return 'red'
|
87
|
+
if self.match(Outcome.TIME_LIMIT_EXCEEDED):
|
88
|
+
return 'yellow'
|
89
|
+
if self.match(Outcome.RUNTIME_ERROR):
|
90
|
+
return 'lnumber'
|
91
|
+
if self.match(Outcome.MEMORY_LIMIT_EXCEEDED):
|
92
|
+
return 'cyan'
|
93
|
+
return 'magenta'
|
94
|
+
|
95
|
+
def is_slow(self) -> bool:
|
96
|
+
return self in [ExpectedOutcome.TIME_LIMIT_EXCEEDED, ExpectedOutcome.TLE_OR_RTE]
|
97
|
+
|
98
|
+
def match(self, outcome: Outcome) -> bool:
|
99
|
+
if self == ExpectedOutcome.ACCEPTED:
|
100
|
+
return outcome == Outcome.ACCEPTED
|
101
|
+
if self == ExpectedOutcome.WRONG_ANSWER:
|
102
|
+
return outcome == Outcome.WRONG_ANSWER
|
103
|
+
if self == ExpectedOutcome.INCORRECT:
|
104
|
+
return outcome in {
|
105
|
+
Outcome.WRONG_ANSWER,
|
106
|
+
Outcome.RUNTIME_ERROR,
|
107
|
+
Outcome.MEMORY_LIMIT_EXCEEDED,
|
108
|
+
Outcome.TIME_LIMIT_EXCEEDED,
|
109
|
+
Outcome.OUTPUT_LIMIT_EXCEEDED,
|
110
|
+
}
|
111
|
+
if self == ExpectedOutcome.RUNTIME_ERROR:
|
112
|
+
return outcome == Outcome.RUNTIME_ERROR
|
113
|
+
if self == ExpectedOutcome.TIME_LIMIT_EXCEEDED:
|
114
|
+
return outcome == Outcome.TIME_LIMIT_EXCEEDED
|
115
|
+
if self == ExpectedOutcome.MEMORY_LIMIT_EXCEEDED:
|
116
|
+
return outcome == Outcome.MEMORY_LIMIT_EXCEEDED
|
117
|
+
if self == ExpectedOutcome.TLE_OR_RTE:
|
118
|
+
return outcome in {Outcome.TIME_LIMIT_EXCEEDED, Outcome.RUNTIME_ERROR}
|
119
|
+
if self == ExpectedOutcome.OUTPUT_LIMIT_EXCEEDED:
|
120
|
+
return outcome == Outcome.OUTPUT_LIMIT_EXCEEDED
|
121
|
+
return False
|
122
|
+
|
123
|
+
def get_matches(self) -> List[Outcome]:
|
124
|
+
return [outcome for outcome in Outcome if self.match(outcome)]
|
125
|
+
|
126
|
+
def intersect(self, rhs: 'ExpectedOutcome') -> bool:
|
127
|
+
return bool(set(self.get_matches()) & set(rhs.get_matches()))
|
128
|
+
|
129
|
+
|
130
|
+
class CodeItem(BaseModel):
|
131
|
+
model_config = ConfigDict(extra='forbid')
|
132
|
+
|
133
|
+
path: pathlib.Path = Field(
|
134
|
+
description="""The path to the code file, relative to the package directory."""
|
135
|
+
)
|
136
|
+
|
137
|
+
language: Optional[str] = Field(
|
138
|
+
None, description="""The language of the code file."""
|
139
|
+
)
|
140
|
+
|
141
|
+
compilationFiles: Optional[List[str]] = Field(
|
142
|
+
[],
|
143
|
+
description="""
|
144
|
+
Extra files that should be placed alongside the code file during its compilation,
|
145
|
+
such as testlib.h, jngen.h, etc.
|
146
|
+
|
147
|
+
The paths should be given relative to the package directory, but will be included
|
148
|
+
relative to the `path` directory.
|
149
|
+
|
150
|
+
Testlib and jngen are already included by default.
|
151
|
+
""",
|
152
|
+
)
|
153
|
+
|
154
|
+
|
155
|
+
class Testcase(BaseModel):
|
156
|
+
model_config = ConfigDict(extra='forbid')
|
157
|
+
|
158
|
+
inputPath: pathlib.Path = Field(description="""The path of the input file.""")
|
159
|
+
|
160
|
+
outputPath: Optional[pathlib.Path] = Field(
|
161
|
+
None, description="""The path of the output file."""
|
162
|
+
)
|
163
|
+
|
164
|
+
|
165
|
+
class GeneratorCall(BaseModel):
|
166
|
+
model_config = ConfigDict(extra='forbid')
|
167
|
+
|
168
|
+
name: str = NameField(description='The name of the generator to call.')
|
169
|
+
|
170
|
+
args: Optional[str] = Field(
|
171
|
+
None, description='The arguments to pass to the generator.'
|
172
|
+
)
|
173
|
+
|
174
|
+
|
175
|
+
class TestcaseSubgroup(BaseModel):
|
176
|
+
model_config = ConfigDict(extra='forbid')
|
177
|
+
|
178
|
+
name: str = NameField(description='The name of the test group.')
|
179
|
+
|
180
|
+
testcases: List[Testcase] = Field(
|
181
|
+
[],
|
182
|
+
description="""
|
183
|
+
The path of testcases to add to this group,
|
184
|
+
in the order they're defined.""",
|
185
|
+
)
|
186
|
+
|
187
|
+
testcaseGlob: Optional[str] = Field(
|
188
|
+
None,
|
189
|
+
description="""
|
190
|
+
A Python glob that matches input file paths relative to the
|
191
|
+
package directory. The globbed files should end with the extension
|
192
|
+
".in", and their corresponding outputs, if defined, should have the same file name,
|
193
|
+
but ending with ".out".
|
194
|
+
""",
|
195
|
+
)
|
196
|
+
|
197
|
+
generators: List[GeneratorCall] = Field(
|
198
|
+
[],
|
199
|
+
description="""
|
200
|
+
A list of generators to call to generate testcases for this group.
|
201
|
+
""",
|
202
|
+
)
|
203
|
+
|
204
|
+
generatorScript: Optional[CodeItem] = Field(
|
205
|
+
None,
|
206
|
+
description="""
|
207
|
+
A generator script to call to generate testcases for this group.
|
208
|
+
""",
|
209
|
+
)
|
210
|
+
|
211
|
+
@model_validator(mode='after')
|
212
|
+
def check_oneof(self) -> 'TestcaseSubgroup':
|
213
|
+
_check_oneof(
|
214
|
+
self,
|
215
|
+
[
|
216
|
+
'testcases',
|
217
|
+
'testcaseGlob',
|
218
|
+
'generators',
|
219
|
+
'generatorScript',
|
220
|
+
],
|
221
|
+
)
|
222
|
+
return self
|
223
|
+
|
224
|
+
|
225
|
+
class TestcaseGroup(TestcaseSubgroup):
|
226
|
+
model_config = ConfigDict(extra='forbid')
|
227
|
+
|
228
|
+
subgroups: List[TestcaseSubgroup] = Field(
|
229
|
+
[],
|
230
|
+
description="""
|
231
|
+
A list of test subgroups to define for this group.
|
232
|
+
""",
|
233
|
+
)
|
234
|
+
|
235
|
+
validator: Optional[CodeItem] = Field(
|
236
|
+
None,
|
237
|
+
description="""
|
238
|
+
A validator to use to validate the testcases of this group.
|
239
|
+
If not specified, will use the package-level validator.
|
240
|
+
Useful in cases where the constraints vary across test groups.
|
241
|
+
""",
|
242
|
+
)
|
243
|
+
|
244
|
+
weight: Optional[float] = Field(
|
245
|
+
1.0,
|
246
|
+
description="""
|
247
|
+
The weight of this group in the final score. Useful for
|
248
|
+
problems that have points.
|
249
|
+
""",
|
250
|
+
)
|
251
|
+
|
252
|
+
|
253
|
+
class Generator(CodeItem):
|
254
|
+
model_config = ConfigDict(extra='forbid')
|
255
|
+
|
256
|
+
name: str = NameField(description="""The name of the generator.""")
|
257
|
+
|
258
|
+
|
259
|
+
class Solution(CodeItem):
|
260
|
+
model_config = ConfigDict(extra='forbid')
|
261
|
+
|
262
|
+
outcome: ExpectedOutcome = Field(
|
263
|
+
description="""The expected outcome of this solution."""
|
264
|
+
)
|
265
|
+
|
266
|
+
|
267
|
+
class Stress(BaseModel):
|
268
|
+
model_config = ConfigDict(extra='forbid')
|
269
|
+
|
270
|
+
name: str = NameField(description='The name of the stress test.')
|
271
|
+
|
272
|
+
generator: GeneratorCall = Field(
|
273
|
+
description='Generator pattern to call during stress-test.'
|
274
|
+
)
|
275
|
+
|
276
|
+
finder: str = Field(
|
277
|
+
description='Finder expression to be used to match against generated tests.'
|
278
|
+
)
|
279
|
+
|
280
|
+
|
281
|
+
class LimitModifiers(BaseModel):
|
282
|
+
timeMultiplier: Optional[float] = Field(
|
283
|
+
None, description='Multiplier for time limit.'
|
284
|
+
)
|
285
|
+
time: Optional[int] = Field(
|
286
|
+
None, description='Value to override time limit with, in milliseconds.'
|
287
|
+
)
|
288
|
+
memory: Optional[int] = Field(
|
289
|
+
None, description='Value to override memory limit with, in MB.'
|
290
|
+
)
|
291
|
+
|
292
|
+
|
293
|
+
class Package(BaseModel):
|
294
|
+
model_config = ConfigDict(extra='forbid')
|
295
|
+
|
296
|
+
# Name of the problem.
|
297
|
+
name: str = NameField(description='The name of the problem.')
|
298
|
+
|
299
|
+
timeLimit: int = Field(description='Time limit of the problem, in milliseconds.')
|
300
|
+
|
301
|
+
memoryLimit: int = Field(description='Memory limit of the problem, in MB.')
|
302
|
+
|
303
|
+
outputLimit: int = Field(
|
304
|
+
4 * 1024, description='Output limit of the problem, in KB.'
|
305
|
+
)
|
306
|
+
|
307
|
+
modifiers: Dict[str, LimitModifiers] = Field(
|
308
|
+
{},
|
309
|
+
description="""
|
310
|
+
Limit modifiers that can be specified per language.
|
311
|
+
""",
|
312
|
+
)
|
313
|
+
|
314
|
+
checker: Optional[CodeItem] = Field(
|
315
|
+
None, description='The checker for this problem.'
|
316
|
+
)
|
317
|
+
|
318
|
+
validator: Optional[CodeItem] = Field(
|
319
|
+
None, description='The validator for this problem.'
|
320
|
+
)
|
321
|
+
|
322
|
+
generators: List[Generator] = Field([], description='Generators for this problem.')
|
323
|
+
|
324
|
+
solutions: List[Solution] = Field(
|
325
|
+
[],
|
326
|
+
description="""
|
327
|
+
All tested solutions for this problem.
|
328
|
+
|
329
|
+
The first solution in this list should be the main solution -- the one
|
330
|
+
that is correct and used as reference -- and should have the `accepted` outcome.
|
331
|
+
""",
|
332
|
+
)
|
333
|
+
|
334
|
+
testcases: List[TestcaseGroup] = Field([], description='Testcases for the problem.')
|
335
|
+
|
336
|
+
stresses: List[Stress] = Field([], description='Stress tests for the problem.')
|
337
|
+
|
338
|
+
statements: List[Statement] = Field([], description='Statements for the problem.')
|
339
|
+
|
340
|
+
# Vars to be re-used across the package.
|
341
|
+
# - It will be passed as --key=value arguments to the validator.
|
342
|
+
# - It will be available as \VAR{key} variables in the rbx statement.
|
343
|
+
vars: Dict[str, Primitive] = Field(
|
344
|
+
{}, description='Variables to be re-used across the package.'
|
345
|
+
)
|
346
|
+
|
347
|
+
@property
|
348
|
+
def expanded_vars(self) -> Dict[str, Primitive]:
|
349
|
+
return {key: expand_var(value) for key, value in self.vars.items()}
|
350
|
+
|
351
|
+
def timelimit_for_language(self, language: Optional[str]) -> int:
|
352
|
+
res = self.timeLimit
|
353
|
+
if language is None:
|
354
|
+
return res
|
355
|
+
if language not in self.modifiers:
|
356
|
+
return res
|
357
|
+
modifier = self.modifiers[language]
|
358
|
+
if modifier.time is not None:
|
359
|
+
return modifier.time
|
360
|
+
if modifier.timeMultiplier is not None:
|
361
|
+
return int(res * float(modifier.timeMultiplier))
|
362
|
+
return res
|
363
|
+
|
364
|
+
def memorylimit_for_language(self, language: Optional[str]) -> int:
|
365
|
+
res = self.memoryLimit
|
366
|
+
if language is None:
|
367
|
+
return res
|
368
|
+
if language not in self.modifiers:
|
369
|
+
return res
|
370
|
+
modifier = self.modifiers[language]
|
371
|
+
if modifier.memory is not None:
|
372
|
+
return modifier.memory
|
373
|
+
return res
|
374
|
+
|
375
|
+
@model_validator(mode='after')
|
376
|
+
def check_first_solution_is_main(self):
|
377
|
+
if self.solutions:
|
378
|
+
if self.solutions[0].outcome != ExpectedOutcome.ACCEPTED:
|
379
|
+
raise PydanticCustomError(
|
380
|
+
'MISSING_MAIN_SOLUTION',
|
381
|
+
'The first solution in the package must have the "ACCEPTED" outcome.',
|
382
|
+
)
|
383
|
+
return self
|
384
|
+
|
385
|
+
@model_validator(mode='after')
|
386
|
+
def samples_come_first(self):
|
387
|
+
for i, group in enumerate(self.testcases):
|
388
|
+
if group.name == 'samples' and i > 0:
|
389
|
+
raise PydanticCustomError(
|
390
|
+
'SAMPLES_NOT_FIRST',
|
391
|
+
'The "samples" group must be the first group in the package, but is actually the {i}-th',
|
392
|
+
{'i': i + 1},
|
393
|
+
)
|
394
|
+
return self
|