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
rbx/box/validators.py ADDED
@@ -0,0 +1,245 @@
1
+ import pathlib
2
+ import shlex
3
+ from typing import Dict, List, Optional, Set, Tuple
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from rbx import console
8
+ from rbx.box import package
9
+ from rbx.box.code import compile_item, run_item
10
+ from rbx.box.schema import CodeItem, Primitive
11
+ from rbx.box.testcases import find_built_testcase_inputs
12
+ from rbx.grading.steps import (
13
+ DigestHolder,
14
+ DigestOrDest,
15
+ DigestOrSource,
16
+ GradingFileOutput,
17
+ )
18
+ from rbx.utils import StatusProgress
19
+
20
+ HitBounds = Dict[str, Tuple[bool, bool]]
21
+
22
+
23
+ class TestcaseValidationInfo(BaseModel):
24
+ group: str
25
+ path: pathlib.Path
26
+ ok: bool
27
+ hit_bounds: HitBounds
28
+ message: Optional[str] = None
29
+
30
+
31
+ def _compile_validator(validator: CodeItem) -> str:
32
+ try:
33
+ digest = compile_item(validator)
34
+ except:
35
+ console.console.print(
36
+ f'[error]Failed compiling validator [item]{validator.path}[/item].[/error]'
37
+ )
38
+ raise
39
+ return digest
40
+
41
+
42
+ def _bounds_or(lhs: Tuple[bool, bool], rhs: Tuple[bool, bool]) -> Tuple[bool, bool]:
43
+ return (lhs[0] or rhs[0], lhs[1] or rhs[1])
44
+
45
+
46
+ def _process_bounds(log: str) -> HitBounds:
47
+ bounds: HitBounds = {}
48
+ for line in log.splitlines():
49
+ items = line.split(':')
50
+ if len(items) != 2:
51
+ continue
52
+ k, v = items
53
+ v = v.strip()
54
+
55
+ hit = ('min-value-hit' in v, 'max-value-hit' in v)
56
+ if k not in bounds:
57
+ bounds[k] = hit
58
+ continue
59
+ bounds[k] = _bounds_or(bounds[k], hit)
60
+ return bounds
61
+
62
+
63
+ def _merge_hit_bounds(hit_bounds: List[HitBounds]) -> HitBounds:
64
+ res: HitBounds = {}
65
+ for hb in hit_bounds:
66
+ for k, hit in hb.items():
67
+ if k not in res:
68
+ res[k] = hit
69
+ continue
70
+ res[k] = _bounds_or(res[k], hit)
71
+ return res
72
+
73
+
74
+ def _has_group_specific_validator() -> bool:
75
+ pkg = package.find_problem_package_or_die()
76
+
77
+ return any(group.validator is not None for group in pkg.testcases)
78
+
79
+
80
+ def _validate_testcase(
81
+ testcase: pathlib.Path,
82
+ validator: CodeItem,
83
+ validator_digest: str,
84
+ vars: Optional[Dict[str, Primitive]] = None,
85
+ ) -> Tuple[bool, Optional[str], HitBounds]:
86
+ vars = vars or {}
87
+ for var in vars:
88
+ assert (
89
+ var.isidentifier()
90
+ ), f'Variable {var} should be a valid Python identifier.'
91
+ # TODO: check if needs to do some escaping
92
+ var_args = [f'--{k}={v}' for k, v in vars.items()]
93
+ var_args.extend(['--testOverviewLogFileName', 'validator.log'])
94
+
95
+ message_digest = DigestHolder()
96
+ log_digest = DigestHolder()
97
+ run_log = run_item(
98
+ validator,
99
+ DigestOrSource.create(validator_digest),
100
+ stdin=DigestOrSource.create(testcase),
101
+ stderr=DigestOrDest.create(message_digest),
102
+ outputs=[
103
+ GradingFileOutput(
104
+ src=pathlib.Path('validator.log'),
105
+ digest=log_digest,
106
+ optional=True,
107
+ )
108
+ ],
109
+ extra_args=shlex.join(var_args) if var_args else None,
110
+ )
111
+ log_overview = ''
112
+ if log_digest.value is not None:
113
+ log_overview = package.get_digest_as_string(log_digest.value or '')
114
+ message = package.get_digest_as_string(message_digest.value or '')
115
+ return (
116
+ run_log is not None and run_log.exitcode == 0,
117
+ message,
118
+ _process_bounds(log_overview or ''),
119
+ )
120
+
121
+
122
+ def validate_test(
123
+ testcase: pathlib.Path,
124
+ validator: CodeItem,
125
+ validator_digest: str,
126
+ ) -> Tuple[bool, Optional[str], HitBounds]:
127
+ pkg = package.find_problem_package_or_die()
128
+ return _validate_testcase(
129
+ testcase, validator, validator_digest, vars=pkg.expanded_vars
130
+ )
131
+
132
+
133
+ def compile_main_validator() -> Optional[Tuple[CodeItem, str]]:
134
+ pkg = package.find_problem_package_or_die()
135
+ if pkg.validator is None:
136
+ return None
137
+
138
+ return pkg.validator, _compile_validator(pkg.validator)
139
+
140
+
141
+ def compile_validators(
142
+ progress: Optional[StatusProgress] = None,
143
+ ) -> Dict[str, str]:
144
+ pkg = package.find_problem_package_or_die()
145
+
146
+ group_to_compiled_digest = {}
147
+
148
+ for group in pkg.testcases:
149
+ validator = group.validator or pkg.validator
150
+ if validator is None:
151
+ continue
152
+ if progress:
153
+ progress.update(
154
+ f'Compiling validator for group [item]{group.name}[/item]...'
155
+ )
156
+ group_to_compiled_digest[group.name] = _compile_validator(validator)
157
+
158
+ return group_to_compiled_digest
159
+
160
+
161
+ def validate_testcases(
162
+ progress: Optional[StatusProgress] = None,
163
+ groups: Optional[Set[str]] = None,
164
+ ) -> List[TestcaseValidationInfo]:
165
+ def step():
166
+ if progress is not None:
167
+ progress.step()
168
+
169
+ pkg = package.find_problem_package_or_die()
170
+
171
+ group_to_compiled_digest = compile_validators(progress)
172
+
173
+ validation_info = []
174
+
175
+ for group in pkg.testcases:
176
+ validator = group.validator or pkg.validator
177
+ if validator is None:
178
+ continue
179
+ if group.name not in group_to_compiled_digest:
180
+ continue
181
+ if groups is not None and group.name not in groups:
182
+ continue
183
+ compiled_digest = group_to_compiled_digest[group.name]
184
+
185
+ testcases = find_built_testcase_inputs(group)
186
+
187
+ for testcase in testcases:
188
+ ok, message, hit_bounds = validate_test(
189
+ testcase, validator, compiled_digest
190
+ )
191
+ validation_info.append(
192
+ TestcaseValidationInfo(
193
+ group=group.name,
194
+ path=testcase,
195
+ ok=ok,
196
+ hit_bounds=hit_bounds,
197
+ message=message,
198
+ )
199
+ )
200
+ step()
201
+
202
+ return validation_info
203
+
204
+
205
+ def print_validation_report(infos: List[TestcaseValidationInfo]):
206
+ console.console.rule('Validation report', style='status')
207
+ hit_bounds_per_group: Dict[str, HitBounds] = {}
208
+ for info in infos:
209
+ if not info.ok:
210
+ console.console.print(
211
+ f'[error]Testcase [item]{info.path}[/item] failed verification:[/error]\n{info.message}'
212
+ )
213
+ continue
214
+
215
+ if info.group not in hit_bounds_per_group:
216
+ hit_bounds_per_group[info.group] = {}
217
+ hit_bounds_per_group[info.group] = _merge_hit_bounds(
218
+ [hit_bounds_per_group[info.group], info.hit_bounds]
219
+ )
220
+
221
+ if not _has_group_specific_validator():
222
+ hit_bounds_per_group = {None: _merge_hit_bounds(hit_bounds_per_group.values())}
223
+
224
+ for group, hit_bounds in hit_bounds_per_group.items():
225
+ if group == 'samples':
226
+ # Skip samples.
227
+ continue
228
+ if group is None:
229
+ console.console.print('Hit bounds:')
230
+ else:
231
+ console.console.print(f'Group [item]{group}[/item] hit bounds:')
232
+
233
+ for k, v in hit_bounds.items():
234
+ if all(v):
235
+ continue
236
+
237
+ if not v[0]:
238
+ console.console.print(
239
+ f' - {k}: [warning]min-value not hit[/warning]',
240
+ )
241
+ if not v[1]:
242
+ console.console.print(
243
+ f' - {k}: [warning]max-value not hit[/warning]',
244
+ )
245
+ console.console.print()
@@ -0,0 +1,15 @@
1
+ import pathlib
2
+
3
+ import pytest
4
+
5
+ from rbx.box.generators import generate_testcases
6
+ from rbx.box.validators import validate_testcases
7
+
8
+
9
+ @pytest.mark.test_pkg('box1')
10
+ def test_validators(pkg_from_testdata: pathlib.Path):
11
+ generate_testcases()
12
+ validation_infos = validate_testcases()
13
+
14
+ for info in validation_infos:
15
+ assert info.ok
rbx/checker.py ADDED
@@ -0,0 +1,128 @@
1
+ import pathlib
2
+ from typing import Optional
3
+
4
+ import typer
5
+ from typing_extensions import Annotated
6
+
7
+ from rbx import annotations, config, metadata, utils
8
+ from rbx.config import get_builtin_checker, get_testlib
9
+ from rbx.console import console
10
+
11
+ app = typer.Typer(no_args_is_help=True)
12
+
13
+
14
+ @app.command('add, a')
15
+ def add(
16
+ problem: annotations.Problem,
17
+ template: Annotated[
18
+ Optional[str],
19
+ typer.Option(
20
+ '--template', '-t', help='Checker that should be used as template.'
21
+ ),
22
+ ] = None,
23
+ ):
24
+ """
25
+ Add a new checker for the problem.
26
+ """
27
+ dumped_problem = metadata.find_problem_by_anything(problem)
28
+ if dumped_problem is None:
29
+ console.print(f'[error]Problem [item]{problem}[/item] not found.[/error]')
30
+ return
31
+
32
+ template_path = get_builtin_checker(template or 'boilerplate.cpp')
33
+
34
+ if not template_path.is_file():
35
+ console.print(f'[error]Template file {template} not found.[/error]')
36
+ return
37
+
38
+ testlib_path = get_testlib()
39
+ if not testlib_path.is_file():
40
+ console.print('[error]Testlib file not found.[/error]')
41
+ return
42
+
43
+ checker_name = f'{dumped_problem.code}.checker.cpp'
44
+ checker_path = pathlib.Path() / checker_name
45
+
46
+ # Create both files.
47
+ checker_path.write_text(template_path.read_text())
48
+ (checker_path.parent / 'testlib.h').write_text(testlib_path.read_text())
49
+
50
+ # Set checker.
51
+ problem_to_dump = dumped_problem.model_copy()
52
+ problem_to_dump.checker = checker_name
53
+ problem_path = metadata.find_problem_path_by_code(dumped_problem.code)
54
+ if not problem_path:
55
+ raise typer.Exit(1)
56
+ problem_path.write_text(utils.model_json(problem_to_dump))
57
+ console.print(
58
+ f'Checker [item]{checker_name}[/item] added to problem [item]{dumped_problem.pretty_name()}[/item].'
59
+ )
60
+
61
+
62
+ @app.command('set, s')
63
+ def set(problem: annotations.Problem, checker: annotations.Checker):
64
+ """
65
+ Set a checker for the problem.
66
+ """
67
+ dumped_problem = metadata.find_problem_by_anything(problem)
68
+ if dumped_problem is None:
69
+ console.print(f'[error]Problem [item]{problem}[/item] not found.[/error]')
70
+ return
71
+
72
+ problem_to_dump = dumped_problem.model_copy()
73
+ problem_to_dump.checker = checker
74
+ problem_path = metadata.find_problem_path_by_code(dumped_problem.code)
75
+ if not problem_path:
76
+ raise typer.Exit(1)
77
+ problem_path.write_text(utils.model_json(problem_to_dump))
78
+ console.print(
79
+ f'Checker [item]{checker}[/item] will be used for problem [item]{dumped_problem.pretty_name()}[/item].'
80
+ )
81
+
82
+
83
+ @app.command('unset, u')
84
+ def unset(problem: annotations.Problem):
85
+ """
86
+ Use the default checker for a problem.
87
+ """
88
+ dumped_problem = metadata.find_problem_by_anything(problem)
89
+ if dumped_problem is None:
90
+ console.print(f'[error]Problem [item]{problem}[/item] not found.[/error]')
91
+ return
92
+
93
+ problem_to_dump = dumped_problem.model_copy()
94
+ problem_to_dump.checker = None
95
+ problem_path = metadata.find_problem_path_by_code(dumped_problem.code)
96
+ if not problem_path:
97
+ raise typer.Exit(1)
98
+ problem_path.write_text(utils.model_json(problem_to_dump))
99
+ console.print(
100
+ f'Default checker will be used for problem [item]{dumped_problem.pretty_name()}[/item].'
101
+ )
102
+
103
+
104
+ @app.command('edit, e')
105
+ def edit(problem: annotations.Problem):
106
+ """
107
+ Edit the checker for a problem.
108
+ """
109
+ dumped_problem = metadata.find_problem_by_anything(problem)
110
+ if dumped_problem is None:
111
+ console.print(f'[error]Problem [item]{problem}[/item] not found.[/error]')
112
+ return
113
+
114
+ checker = dumped_problem.checker
115
+ if checker is None:
116
+ console.print(
117
+ f'[error]No checker set for problem [item]{dumped_problem.pretty_name()}[/item].[/error]'
118
+ )
119
+ return
120
+
121
+ checker_path = pathlib.Path() / checker
122
+ if not checker_path.is_file():
123
+ console.print(
124
+ f'[error]Checker [item]{checker}[/item] not found in the problems folder. You cannot edit a builtin checker.[/error]'
125
+ )
126
+ return
127
+
128
+ config.open_editor(checker_path)
rbx/clone.py ADDED
@@ -0,0 +1,197 @@
1
+ import logging
2
+ import pathlib
3
+ import threading
4
+ import time
5
+ from typing import List, Optional
6
+
7
+ import fastapi
8
+ import jinja2
9
+ import rich
10
+ import rich.prompt
11
+ import rich.status
12
+ import uvicorn
13
+
14
+ from rbx import hydration, metadata, providers, utils
15
+ from rbx.config import Language, get_config
16
+ from rbx.console import console
17
+ from rbx.schema import DumpedProblem, Problem
18
+
19
+
20
+ def clear_loggers():
21
+ for logger_name in [
22
+ 'uvicorn',
23
+ 'uvicorn.access',
24
+ 'uvicorn.asgi',
25
+ ]:
26
+ logging.getLogger(logger_name).handlers.clear()
27
+ logging.getLogger(logger_name).propagate = False
28
+
29
+
30
+ def create_problem_structure(
31
+ root: pathlib.Path,
32
+ problem: Problem,
33
+ lang: Language,
34
+ status: Optional[rich.status.Status],
35
+ should_simplify: bool = False,
36
+ verbose: bool = False,
37
+ ) -> Optional[DumpedProblem]:
38
+ # Create directory structure.
39
+ root.parent.mkdir(parents=True, exist_ok=True)
40
+
41
+ problem_to_dump = DumpedProblem.from_problem(
42
+ problem,
43
+ code=providers.get_code(problem, simplify=should_simplify),
44
+ aliases=providers.get_aliases(problem),
45
+ )
46
+
47
+ if verbose:
48
+ console.print(
49
+ f'Creating problem structure for [item]{problem_to_dump.pretty_name()}[/item]...'
50
+ )
51
+
52
+ code_path = root / lang.get_file(problem_to_dump.code)
53
+ json_path = root / f'{problem_to_dump.code}.rbx.json'
54
+
55
+ existing_problem = metadata.find_problem_by_code(problem_to_dump.code, root)
56
+ if existing_problem:
57
+ console.print(
58
+ f'[error]Problem with identifier [item]{problem_to_dump.code}[/item] already exists in this folder.[/error]'
59
+ )
60
+ if not utils.confirm_on_status(
61
+ status, 'Do you want to overwrite it?', default=False
62
+ ):
63
+ console.print(
64
+ f'Skipping problem [item]{problem_to_dump.pretty_name()}[/item].'
65
+ )
66
+ return None
67
+
68
+ json_path.write_text(utils.model_json(problem_to_dump))
69
+ code = jinja2.Template(lang.get_template()).render(**problem_to_dump.get_vars())
70
+ code_path.write_text(code)
71
+
72
+ if verbose:
73
+ console.print(
74
+ f'Problem structure for [item]{problem_to_dump.pretty_name()}[/item] created successfully.'
75
+ )
76
+ return problem_to_dump
77
+
78
+
79
+ def process_problems(
80
+ problems: List[Problem], lang: Language, status: rich.status.Status
81
+ ):
82
+ console.print(
83
+ f'Creating problem structure for [item]{len(problems)}[/item] problems...'
84
+ )
85
+
86
+ should_simplify = False
87
+ if providers.should_simplify_contest_problems(problems):
88
+ console.print('Detected the parsed problems are from a contest.')
89
+ if utils.confirm_on_status(
90
+ status,
91
+ 'Do you want to identify these problems by their letters?',
92
+ default=True,
93
+ ):
94
+ should_simplify = True
95
+
96
+ root = pathlib.Path()
97
+ dumped_problems = []
98
+ for problem in problems:
99
+ dumped_problem = create_problem_structure(
100
+ root, problem, lang, status, should_simplify=should_simplify
101
+ )
102
+ if dumped_problem:
103
+ dumped_problems.append(dumped_problem)
104
+ console.print(f'Hydrating [item]{len(dumped_problems)}[/item] problems...')
105
+ for problem in dumped_problems:
106
+ hydration.hydrate_problem(root, problem)
107
+
108
+
109
+ def main(lang: Optional[str] = None):
110
+ if get_config().get_language(lang) is None:
111
+ console.print(
112
+ f'[error]Language {lang or get_config().defaultLanguage} not found in config. Please check your configuration.[/error]'
113
+ )
114
+ return
115
+
116
+ app = fastapi.FastAPI()
117
+
118
+ async def shutdown():
119
+ server.should_exit = True
120
+
121
+ batch_to_left_lock = threading.Lock()
122
+ batch_to_left = {}
123
+ ignored = set()
124
+ saved_status = None
125
+ problems_to_process = []
126
+
127
+ def process_batch_item(problem: Problem):
128
+ batch_to_left_lock.acquire()
129
+ if problem.batch.id in ignored:
130
+ batch_to_left_lock.release()
131
+ return True
132
+ if problem.batch.id not in batch_to_left:
133
+ if len(batch_to_left) > 0:
134
+ console.print(
135
+ f'[error]Ignoring extra batch [item]{problem.batch.id}[/item] since other batch is being parsed.[/error]'
136
+ )
137
+ ignored.add(problem.batch.id)
138
+ batch_to_left_lock.release()
139
+ return True
140
+ if problem.batch.size > 1 and saved_status:
141
+ saved_status.update(
142
+ f'[rbx]rbx[/rbx] is parsing problems from group [item]{problem.group}[/item]'
143
+ )
144
+ elif saved_status:
145
+ saved_status.update('[rbx]rbx[/rbx] is parsing problems...')
146
+ console.print(
147
+ f'Started parsing batch [item]{problem.batch.id}[/item] with size [item]{problem.batch.size}[/item].'
148
+ )
149
+ batch_to_left[problem.batch.id] = problem.batch.size
150
+ console.print(f'Parsing problem [item]{problem.name}[/item]...')
151
+ problems_to_process.append(problem)
152
+ finished = False
153
+ if batch_to_left[problem.batch.id] == 1:
154
+ finished = True
155
+ if problem.batch.size > 1:
156
+ console.print(
157
+ f'[status][rbx]rbx[/rbx] parsed all problems from group [item]{problem.group}[/item].[/status]'
158
+ )
159
+ else:
160
+ console.print(
161
+ f'[status][rbx]rbx[/rbx] parsed problem from [item]{problem.url}[/item].[/status]'
162
+ )
163
+ else:
164
+ batch_to_left[problem.batch.id] -= 1
165
+ batch_to_left_lock.release()
166
+ return not finished
167
+
168
+ clock = None
169
+
170
+ @app.post('/')
171
+ async def parse(problem: Problem, background_tasks: fastapi.BackgroundTasks):
172
+ nonlocal clock
173
+ if clock is None:
174
+ clock = time.monotonic()
175
+ if not process_batch_item(problem):
176
+ duration = time.monotonic() - clock
177
+ console.print(
178
+ f'Parsed all problems in [item]{duration:.2f}[/item] seconds.'
179
+ )
180
+ background_tasks.add_task(shutdown)
181
+ return {}
182
+
183
+ config = uvicorn.Config(app, port=1327)
184
+ server = uvicorn.Server(config=config)
185
+ clear_loggers()
186
+ with console.status('Waiting for Competitive Companion request...') as status:
187
+ saved_status = status
188
+ server.run()
189
+
190
+ with console.status('Processing parsed problems...') as status:
191
+ language = get_config().get_language(lang)
192
+ if not language:
193
+ console.print(
194
+ f'[error]Language {lang or get_config().defaultLanguage} not found in config. Please check your configuration.[/error]'
195
+ )
196
+ return
197
+ process_problems(problems_to_process, language, status)