rbx.cp 0.5.72__py3-none-any.whl → 0.6.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 (71) hide show
  1. rbx/annotations.py +21 -1
  2. rbx/box/cli.py +24 -8
  3. rbx/box/code.py +140 -3
  4. rbx/box/contest/build_contest_statements.py +44 -34
  5. rbx/box/contest/contest_utils.py +25 -0
  6. rbx/box/contest/main.py +24 -0
  7. rbx/box/contest/schema.py +52 -8
  8. rbx/box/contest/statements.py +53 -25
  9. rbx/box/download.py +19 -1
  10. rbx/box/fields.py +35 -0
  11. rbx/box/lang.py +27 -0
  12. rbx/box/package.py +1 -1
  13. rbx/box/packaging/boca/packager.py +48 -5
  14. rbx/box/packaging/contest_main.py +13 -0
  15. rbx/box/packaging/main.py +13 -2
  16. rbx/box/packaging/packager.py +4 -4
  17. rbx/box/packaging/pkg/packager.py +142 -0
  18. rbx/box/packaging/polygon/packager.py +2 -24
  19. rbx/box/packaging/polygon/upload.py +35 -17
  20. rbx/box/remote.py +2 -2
  21. rbx/box/schema.py +68 -18
  22. rbx/box/solutions.py +6 -1
  23. rbx/box/statements/build_statements.py +44 -27
  24. rbx/box/statements/builders.py +18 -10
  25. rbx/box/statements/expander.py +49 -0
  26. rbx/box/statements/latex_jinja.py +61 -4
  27. rbx/box/statements/schema.py +33 -9
  28. rbx/box/testcase_utils.py +19 -47
  29. rbx/box/tooling/__init__.py +0 -0
  30. rbx/box/tooling/boca/__init__.py +0 -0
  31. rbx/box/tooling/boca/main.py +13 -0
  32. rbx/box/tooling/boca/scrape.py +34 -0
  33. rbx/box/{packaging/boca/upload.py → tooling/boca/scraper.py} +77 -8
  34. rbx/box/tooling/main.py +8 -0
  35. rbx/box/ui/screens/run_explorer.py +1 -1
  36. rbx/box/ui/widgets/interaction_box.py +19 -1
  37. rbx/grading/caching.py +18 -2
  38. rbx/grading/judge/sandbox.py +48 -5
  39. rbx/grading/judge/sandboxes/isolate.py +1 -0
  40. rbx/grading/judge/sandboxes/stupid_sandbox.py +11 -5
  41. rbx/grading/judge/sandboxes/timeit.py +36 -15
  42. rbx/grading/processing_context.py +62 -78
  43. rbx/grading/steps.py +91 -40
  44. rbx/resources/packagers/boca/checker.sh +4 -1
  45. rbx/resources/packagers/boca/compile/c +2 -6
  46. rbx/resources/packagers/boca/compile/cc +2 -6
  47. rbx/resources/packagers/boca/compile/cpp +2 -6
  48. rbx/resources/packagers/boca/compile/java +1 -6
  49. rbx/resources/packagers/boca/compile/kt +24 -28
  50. rbx/resources/packagers/boca/compile/py2 +2 -6
  51. rbx/resources/packagers/boca/compile/py3 +2 -6
  52. rbx/resources/packagers/boca/interactive/c +15 -62
  53. rbx/resources/packagers/boca/interactive/cc +15 -62
  54. rbx/resources/packagers/boca/interactive/cpp +15 -61
  55. rbx/resources/packagers/boca/interactive/java +15 -67
  56. rbx/resources/packagers/boca/interactive/kt +15 -67
  57. rbx/resources/packagers/boca/interactive/py2 +15 -67
  58. rbx/resources/packagers/boca/interactive/py3 +15 -65
  59. rbx/resources/packagers/boca/interactor_compile.sh +5 -2
  60. rbx/resources/packagers/boca/interactor_run.sh +174 -0
  61. rbx/resources/packagers/boca/safeexec.c +530 -0
  62. rbx/resources/packagers/boca/safeexec_compile.sh +49 -0
  63. rbx/resources/presets/default/contest/contest.rbx.yml +9 -8
  64. rbx/resources/presets/default/problem/problem.rbx.yml +27 -26
  65. rbx/resources/templates/rbx.h +2 -3
  66. {rbx_cp-0.5.72.dist-info → rbx_cp-0.6.0.dist-info}/METADATA +2 -1
  67. {rbx_cp-0.5.72.dist-info → rbx_cp-0.6.0.dist-info}/RECORD +70 -59
  68. rbx/resources/packagers/boca/compile/pas +0 -172
  69. {rbx_cp-0.5.72.dist-info → rbx_cp-0.6.0.dist-info}/LICENSE +0 -0
  70. {rbx_cp-0.5.72.dist-info → rbx_cp-0.6.0.dist-info}/WHEEL +0 -0
  71. {rbx_cp-0.5.72.dist-info → rbx_cp-0.6.0.dist-info}/entry_points.txt +0 -0
@@ -10,6 +10,9 @@ from rbx.box.contest.contest_package import (
10
10
  find_contest_package_or_die,
11
11
  within_contest,
12
12
  )
13
+ from rbx.box.contest.schema import ContestStatement
14
+ from rbx.box.formatting import href
15
+ from rbx.box.schema import expand_any_vars
13
16
  from rbx.box.statements.schema import StatementType
14
17
 
15
18
  app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
@@ -20,13 +23,18 @@ app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
20
23
  @syncer.sync
21
24
  async def build(
22
25
  verification: environment.VerificationParam,
26
+ names: Annotated[
27
+ Optional[List[str]],
28
+ typer.Argument(
29
+ help='Names of statements to build.',
30
+ ),
31
+ ] = None,
23
32
  languages: Annotated[
24
33
  Optional[List[str]],
25
34
  typer.Option(
26
- default_factory=list,
27
35
  help='Languages to build statements for. If not specified, build statements for all available languages.',
28
36
  ),
29
- ],
37
+ ] = None,
30
38
  output: Annotated[
31
39
  Optional[StatementType],
32
40
  typer.Option(
@@ -38,10 +46,14 @@ async def build(
38
46
  bool,
39
47
  typer.Option(help='Whether to build the statement with samples or not.'),
40
48
  ] = True,
41
- editorial: Annotated[
42
- bool,
43
- typer.Option(help='Whether to add editorial blocks to the statements or not.'),
44
- ] = False,
49
+ vars: Annotated[
50
+ Optional[List[str]],
51
+ typer.Option(
52
+ '-v',
53
+ '--vars',
54
+ help='Variables to be used in the statements.',
55
+ ),
56
+ ] = None,
45
57
  ):
46
58
  contest = find_contest_package_or_die()
47
59
  # At most run the validators, only in samples.
@@ -64,26 +76,42 @@ async def build(
64
76
  raise typer.Exit(1)
65
77
 
66
78
  contest = find_contest_package_or_die()
67
- candidate_languages = languages
68
- if not candidate_languages:
69
- candidate_languages = sorted(set([st.language for st in contest.statements]))
70
-
71
- for language in candidate_languages:
72
- candidates_for_lang = [
73
- st for st in contest.statements if st.language == language
74
- ]
75
- if not candidates_for_lang:
76
- console.console.print(
77
- f'[error]No contest-level statement found for language [item]{language}[/item].[/error]',
79
+
80
+ candidate_languages = set(languages or [])
81
+ candidate_names = set(names or [])
82
+
83
+ def should_process(st: ContestStatement) -> bool:
84
+ if candidate_languages and st.language not in candidate_languages:
85
+ return False
86
+ if candidate_names and st.name not in candidate_names:
87
+ return False
88
+ return True
89
+
90
+ valid_statements = [st for st in contest.expanded_statements if should_process(st)]
91
+
92
+ if not valid_statements:
93
+ console.console.print(
94
+ '[error]No statement found according to the specified criteria.[/error]',
95
+ )
96
+ raise typer.Exit(1)
97
+
98
+ built_statements = []
99
+
100
+ for statement in valid_statements:
101
+ built_statements.append(
102
+ build_statement(
103
+ statement,
104
+ contest,
105
+ output_type=output,
106
+ use_samples=samples,
107
+ custom_vars=expand_any_vars(annotations.parse_dictionary_items(vars)),
78
108
  )
79
- raise typer.Exit(1)
80
-
81
- build_statement(
82
- candidates_for_lang[0],
83
- contest,
84
- output_type=output,
85
- use_samples=samples,
86
- is_editorial=editorial,
109
+ )
110
+
111
+ console.console.rule(title='Built statements')
112
+ for statement, built_path in zip(valid_statements, built_statements):
113
+ console.console.print(
114
+ f'[item]{statement.name} {statement.language}[/item] -> {href(built_path)}'
87
115
  )
88
116
 
89
117
 
rbx/box/download.py CHANGED
@@ -5,7 +5,7 @@ from typing import Optional
5
5
  import typer
6
6
 
7
7
  from rbx import annotations, console
8
- from rbx.box import header, package
8
+ from rbx.box import header, package, remote
9
9
  from rbx.box.schema import CodeItem
10
10
  from rbx.config import get_builtin_checker, get_jngen, get_testlib
11
11
  from rbx.grading import steps
@@ -72,3 +72,21 @@ def checker(name: str):
72
72
  console.console.print(
73
73
  f'[success]Downloaded [item]{name}[/item] into current package.[/success]'
74
74
  )
75
+
76
+
77
+ @app.command('remote, r', help='Download a remote code.')
78
+ @package.within_problem
79
+ def remote_cmd(
80
+ name: str,
81
+ output: Optional[str] = typer.Option(
82
+ None,
83
+ '-o',
84
+ '--output',
85
+ help='Whether to not build outputs for tests and run checker.',
86
+ ),
87
+ ):
88
+ path = remote.expand_file(name)
89
+
90
+ if output is not None:
91
+ pathlib.Path(output).parent.mkdir(parents=True, exist_ok=True)
92
+ shutil.copy(str(path), output)
rbx/box/fields.py ADDED
@@ -0,0 +1,35 @@
1
+ from typing import TypeVar
2
+
3
+ from deepmerge import always_merger
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ def NameField(**kwargs):
8
+ return Field(
9
+ pattern=r'^[a-zA-Z0-9][a-zA-Z0-9\-_]*$', min_length=3, max_length=32, **kwargs
10
+ )
11
+
12
+
13
+ def FNameField(**kwargs):
14
+ return Field(
15
+ pattern=r'^[a-zA-Z0-9][a-zA-Z0-9\-_]*$', min_length=3, max_length=128, **kwargs
16
+ )
17
+
18
+
19
+ T = TypeVar('T', bound=BaseModel)
20
+
21
+
22
+ def merge_pydantic_models(base: T, nxt: T) -> T:
23
+ """Merge two Pydantic model instances.
24
+
25
+ The attributes of 'base' and 'nxt' that weren't explicitly set are dumped into dicts
26
+ using '.model_dump(exclude_unset=True)', which are then merged using 'deepmerge',
27
+ and the merged result is turned into a model instance using '.model_validate'.
28
+
29
+ For attributes set on both 'base' and 'nxt', the value from 'nxt' will be used in
30
+ the output result.
31
+ """
32
+ base_dict = base.model_dump(exclude_unset=True)
33
+ nxt_dict = nxt.model_dump(exclude_unset=True)
34
+ merged_dict = always_merger.merge(base_dict, nxt_dict)
35
+ return base.model_validate(merged_dict)
rbx/box/lang.py ADDED
@@ -0,0 +1,27 @@
1
+ import functools
2
+ from typing import List
3
+
4
+ import iso639
5
+
6
+ from rbx import console
7
+
8
+
9
+ def code_to_langs(langs: List[str]) -> List[str]:
10
+ return [iso639.Language.from_part1(lang).name.lower() for lang in langs]
11
+
12
+
13
+ @functools.cache
14
+ def is_valid_lang_code(lang: str) -> bool:
15
+ try:
16
+ code_to_langs([lang])
17
+ except iso639.LanguageNotFoundError:
18
+ console.console.print(
19
+ f'[warning]Language [item]{lang}[/item] is being skipped because it is not a iso639 language.[/warning]'
20
+ )
21
+ return False
22
+
23
+ return True
24
+
25
+
26
+ def langs_to_code(langs: List[str]) -> List[str]:
27
+ return [iso639.Language.from_name(lang).part1 for lang in langs]
rbx/box/package.py CHANGED
@@ -459,7 +459,7 @@ def get_merged_capture_path(root: pathlib.Path = pathlib.Path()) -> pathlib.Path
459
459
  def is_cache_valid(root: pathlib.Path = pathlib.Path()):
460
460
  cache_dir = find_problem(root) / '.box'
461
461
  if not cache_dir.is_dir():
462
- return
462
+ return True
463
463
 
464
464
  fingerprint_file = cache_dir / 'fingerprint'
465
465
  if not fingerprint_file.is_file():
@@ -32,11 +32,11 @@ class BocaPackager(BasePackager):
32
32
  def _get_main_statement(self) -> Statement:
33
33
  pkg = package.find_problem_package_or_die()
34
34
 
35
- if not pkg.statements:
35
+ if not pkg.expanded_statements:
36
36
  console.console.print('[error]No statements found.[/error]')
37
37
  raise typer.Exit(1)
38
38
 
39
- return pkg.statements[0]
39
+ return pkg.expanded_statements[0]
40
40
 
41
41
  def _get_main_built_statement(
42
42
  self, built_statements: List[BuiltStatement]
@@ -119,10 +119,18 @@ class BocaPackager(BasePackager):
119
119
 
120
120
  def _get_limits(self, language: BocaLanguage) -> str:
121
121
  pkg = package.find_problem_package_or_die()
122
- no_of_runs = self._get_number_of_runs(language)
122
+ if pkg.type == TaskType.COMMUNICATION:
123
+ # Interactive tasks only support a single run.
124
+ no_of_runs = 1
125
+ time_limit = f'{self._get_pkg_timelimit(language) / 1000:.2f}'
126
+ else:
127
+ no_of_runs = self._get_number_of_runs(language)
128
+ time_limit = test_time(
129
+ self._get_pkg_timelimit(language) / 1000 * no_of_runs
130
+ )
123
131
  return (
124
132
  '#!/bin/bash\n'
125
- f'echo {test_time(self._get_pkg_timelimit(language) / 1000 * no_of_runs)}\n'
133
+ f'echo {time_limit}\n'
126
134
  f'echo {no_of_runs}\n'
127
135
  f'echo {self._get_pkg_memorylimit(language)}\n'
128
136
  f'echo {pkg.outputLimit}\n'
@@ -176,6 +184,25 @@ class BocaPackager(BasePackager):
176
184
  '{{rbxFlags}}', extension.flags_with_defaults()['cc']
177
185
  ).replace('{{interactor_content}}', interactor)
178
186
 
187
+ def _get_safeexec(self) -> str:
188
+ safeexec_script_path = (
189
+ get_default_app_path() / 'packagers' / 'boca' / 'safeexec_compile.sh'
190
+ )
191
+ safeexec_path = get_default_app_path() / 'packagers' / 'boca' / 'safeexec.c'
192
+ if not safeexec_script_path.exists():
193
+ console.console.print(
194
+ '[error]BOCA template safeexec compile script not found.[/error]'
195
+ )
196
+ raise typer.Exit(1)
197
+ if not safeexec_path.exists():
198
+ console.console.print(
199
+ '[error]BOCA template safeexec source code not found.[/error]'
200
+ )
201
+ raise typer.Exit(1)
202
+ return safeexec_script_path.read_text().replace(
203
+ '{{safeexec_content}}', safeexec_path.read_text()
204
+ )
205
+
179
206
  def _get_compile(self, language: BocaLanguage) -> str:
180
207
  pkg = package.find_problem_package_or_die()
181
208
  extension = get_extension_or_default('boca', BocaExtension)
@@ -196,6 +223,9 @@ class BocaPackager(BasePackager):
196
223
  compile_text = compile_text.replace(
197
224
  'umask 0022', 'umask 0022\n\n' + self._get_interactor()
198
225
  )
226
+ compile_text = compile_text.replace(
227
+ 'umask 0022', 'umask 0022\n\n' + self._get_safeexec()
228
+ )
199
229
  compile_text = compile_text.replace(
200
230
  'umask 0022', 'umask 0022\n\n' + self._get_checker()
201
231
  )
@@ -206,7 +236,6 @@ class BocaPackager(BasePackager):
206
236
  return compile_text
207
237
 
208
238
  def _copy_solutions(self, into_path: pathlib.Path):
209
- into_path = into_path / 'solutions'
210
239
  for solution in package.get_solutions():
211
240
  dest_path = (
212
241
  into_path
@@ -218,6 +247,19 @@ class BocaPackager(BasePackager):
218
247
  dest_path.parent.mkdir(parents=True, exist_ok=True)
219
248
  shutil.copy(str(solution.path), dest_path)
220
249
 
250
+ def _expand_run_script(self, run_path: pathlib.Path):
251
+ pkg = package.find_problem_package_or_die()
252
+ if pkg.type == TaskType.COMMUNICATION:
253
+ runit_content = (
254
+ get_default_app_path() / 'packagers' / 'boca' / 'interactor_run.sh'
255
+ ).read_text()
256
+ run_path.write_text(
257
+ run_path.read_text().replace(
258
+ '{{runit_content}}',
259
+ runit_content,
260
+ )
261
+ )
262
+
221
263
  @classmethod
222
264
  def name(cls) -> str:
223
265
  return 'boca'
@@ -263,6 +305,7 @@ class BocaPackager(BasePackager):
263
305
  )
264
306
  raise typer.Exit(1)
265
307
  shutil.copyfile(run_orig_path, run_path / language)
308
+ self._expand_run_script(run_path / language)
266
309
 
267
310
  # Prepare compile.
268
311
  compile_path = into_path / 'compile'
@@ -86,3 +86,16 @@ async def polygon(
86
86
  await run_contest_packager(
87
87
  PolygonContestPackager, PolygonPackager, verification=verification
88
88
  )
89
+
90
+
91
+ @app.command('pkg', help='Build a contest package for PKG.')
92
+ @contest_package.within_contest
93
+ @syncer.sync
94
+ async def pkg(
95
+ verification: environment.VerificationParam,
96
+ ):
97
+ from rbx.box.packaging.pkg.packager import PkgContestPackager, PkgPackager
98
+
99
+ await run_contest_packager(
100
+ PkgContestPackager, PkgPackager, verification=verification
101
+ )
rbx/box/packaging/main.py CHANGED
@@ -114,9 +114,9 @@ async def boca(
114
114
  result_path = await run_packager(BocaPackager, verification=verification)
115
115
 
116
116
  if upload:
117
- from rbx.box.packaging.boca.upload import get_boca_uploader
117
+ from rbx.box.tooling.boca.scraper import get_boca_scraper
118
118
 
119
- uploader = get_boca_uploader()
119
+ uploader = get_boca_scraper()
120
120
  uploader.login_and_upload(result_path)
121
121
 
122
122
 
@@ -132,3 +132,14 @@ async def moj(
132
132
  from rbx.box.packaging.moj.packager import MojPackager
133
133
 
134
134
  await run_packager(MojPackager, verification=verification, for_boca=for_boca)
135
+
136
+
137
+ @app.command('pkg', help='Build a package for PKG.')
138
+ @package.within_problem
139
+ @syncer.sync
140
+ async def pkg(
141
+ verification: environment.VerificationParam,
142
+ ):
143
+ from rbx.box.packaging.pkg.packager import PkgPackager
144
+
145
+ await run_packager(PkgPackager, verification=verification)
@@ -46,7 +46,7 @@ class BasePackager(ABC):
46
46
  pkg = package.find_problem_package_or_die()
47
47
 
48
48
  res = set()
49
- for statement in pkg.statements:
49
+ for statement in pkg.expanded_statements:
50
50
  res.add(statement.language)
51
51
  return list(res)
52
52
 
@@ -89,7 +89,7 @@ class BasePackager(ABC):
89
89
 
90
90
  def get_statement_for_language(self, lang: str) -> Statement:
91
91
  pkg = package.find_problem_package_or_die()
92
- for statement in pkg.statements:
92
+ for statement in pkg.expanded_statements:
93
93
  if statement.language == lang:
94
94
  return statement
95
95
  raise
@@ -114,7 +114,7 @@ class BaseContestPackager(ABC):
114
114
  pkg = contest_package.find_contest_package_or_die()
115
115
 
116
116
  res = set()
117
- for statement in pkg.statements:
117
+ for statement in pkg.expanded_statements:
118
118
  res.add(statement.language)
119
119
  return list(res)
120
120
 
@@ -123,7 +123,7 @@ class BaseContestPackager(ABC):
123
123
 
124
124
  def get_statement_for_language(self, lang: str) -> ContestStatement:
125
125
  contest = contest_package.find_contest_package_or_die()
126
- for statement in contest.statements:
126
+ for statement in contest.expanded_statements:
127
127
  if statement.language == lang:
128
128
  return statement
129
129
  raise
@@ -0,0 +1,142 @@
1
+ import pathlib
2
+ import shutil
3
+ from typing import List, Optional
4
+
5
+ from rbx.box import naming, package
6
+ from rbx.box.contest import contest_package
7
+ from rbx.box.contest.schema import ContestStatement
8
+ from rbx.box.packaging.packager import (
9
+ BaseContestPackager,
10
+ BasePackager,
11
+ BuiltContestStatement,
12
+ BuiltProblemPackage,
13
+ BuiltStatement,
14
+ )
15
+ from rbx.box.schema import ExpectedOutcome, TaskType
16
+ from rbx.box.statements.schema import Statement
17
+
18
+
19
+ class PkgPackager(BasePackager):
20
+ @classmethod
21
+ def task_types(cls) -> List[TaskType]:
22
+ return [TaskType.BATCH, TaskType.COMMUNICATION]
23
+
24
+ @classmethod
25
+ def name(cls) -> str:
26
+ return 'pkg'
27
+
28
+ def _get_problem_basename(self) -> str:
29
+ shortname = naming.get_problem_shortname()
30
+ if shortname is not None:
31
+ return shortname
32
+ return self.package_basename()
33
+
34
+ def _get_main_statement(self) -> Optional[Statement]:
35
+ pkg = package.find_problem_package_or_die()
36
+ if not pkg.expanded_statements:
37
+ return None
38
+ return pkg.expanded_statements[0]
39
+
40
+ def _get_main_built_statement(
41
+ self, built_statements: List[BuiltStatement]
42
+ ) -> Optional[BuiltStatement]:
43
+ statement = self._get_main_statement()
44
+ if statement is None:
45
+ return None
46
+ for built_statement in built_statements:
47
+ if built_statement.statement == statement:
48
+ return built_statement
49
+ return None
50
+
51
+ def _copy_accepted_solutions(self, into_path: pathlib.Path):
52
+ for solution in package.get_solutions():
53
+ if solution.outcome != ExpectedOutcome.ACCEPTED:
54
+ continue
55
+ dest_path = into_path / solution.path.name
56
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
57
+ shutil.copy(str(solution.path), dest_path)
58
+
59
+ def package(
60
+ self,
61
+ build_path: pathlib.Path,
62
+ into_path: pathlib.Path,
63
+ built_statements: List[BuiltStatement],
64
+ ) -> pathlib.Path:
65
+ into_path.mkdir(parents=True, exist_ok=True)
66
+
67
+ main_statement = self._get_main_built_statement(built_statements)
68
+ if main_statement is not None:
69
+ statement_path = into_path / 'statement.pdf'
70
+ shutil.copyfile(main_statement.path, statement_path)
71
+
72
+ # Prepare tests
73
+ tests_path = into_path / 'tests'
74
+ tests_path.mkdir(parents=True, exist_ok=True)
75
+
76
+ testcases = self.get_flattened_built_testcases()
77
+ for i, testcase in enumerate(testcases):
78
+ shutil.copyfile(testcase.inputPath, tests_path / f'{i + 1:03d}.in')
79
+ if testcase.outputPath is not None:
80
+ shutil.copyfile(testcase.outputPath, tests_path / f'{i + 1:03d}.ans')
81
+ else:
82
+ (tests_path / f'{i + 1:03d}.ans').touch()
83
+
84
+ # Copy solutions.
85
+ solutions_path = into_path / 'solutions'
86
+ solutions_path.mkdir(parents=True, exist_ok=True)
87
+ self._copy_accepted_solutions(solutions_path)
88
+
89
+ # Zip all.
90
+ shutil.make_archive(
91
+ str(build_path / self._get_problem_basename()), 'zip', into_path
92
+ )
93
+
94
+ return (build_path / self._get_problem_basename()).with_suffix('.zip')
95
+
96
+
97
+ class PkgContestPackager(BaseContestPackager):
98
+ @classmethod
99
+ def name(cls) -> str:
100
+ return 'pkg'
101
+
102
+ def _get_main_statement(self) -> Optional[ContestStatement]:
103
+ pkg = contest_package.find_contest_package_or_die()
104
+ if not pkg.expanded_statements:
105
+ return None
106
+ return pkg.expanded_statements[0]
107
+
108
+ def _get_main_built_statement(
109
+ self, built_statements: List[BuiltContestStatement]
110
+ ) -> Optional[BuiltContestStatement]:
111
+ statement = self._get_main_statement()
112
+ if statement is None:
113
+ return None
114
+ for built_statement in built_statements:
115
+ if built_statement.statement == statement:
116
+ return built_statement
117
+ return None
118
+
119
+ def package(
120
+ self,
121
+ built_packages: List[BuiltProblemPackage],
122
+ build_path: pathlib.Path,
123
+ into_path: pathlib.Path,
124
+ built_statements: List[BuiltContestStatement],
125
+ ) -> pathlib.Path:
126
+ into_path.mkdir(parents=True, exist_ok=True)
127
+
128
+ # Add contest-level statement.
129
+ main_statement = self._get_main_built_statement(built_statements)
130
+ if main_statement is not None:
131
+ statement_path = into_path / 'statement.pdf'
132
+ shutil.copyfile(main_statement.path, statement_path)
133
+
134
+ # Add problems.
135
+ for built_package in built_packages:
136
+ pkg_path = into_path / built_package.problem.short_name
137
+ shutil.unpack_archive(built_package.path, pkg_path, format='zip')
138
+
139
+ # Zip all.
140
+ shutil.make_archive(str(build_path / 'contest'), 'zip', into_path)
141
+
142
+ return (build_path / 'contest').with_suffix('.zip')
@@ -1,13 +1,12 @@
1
- import functools
2
1
  import pathlib
3
2
  import shutil
4
3
  from typing import List, Optional
5
4
 
6
- import iso639
7
5
  import typer
8
6
 
9
7
  from rbx import console
10
8
  from rbx.box import header, package
9
+ from rbx.box.lang import code_to_langs, is_valid_lang_code
11
10
  from rbx.box.packaging.packager import (
12
11
  BaseContestPackager,
13
12
  BasePackager,
@@ -28,27 +27,6 @@ DAT_TEMPLATE = """
28
27
  """
29
28
 
30
29
 
31
- def langs_to_code(langs: List[str]) -> List[str]:
32
- return [iso639.Language.from_name(lang).part1 for lang in langs]
33
-
34
-
35
- def code_to_langs(langs: List[str]) -> List[str]:
36
- return [iso639.Language.from_part1(lang).name.lower() for lang in langs]
37
-
38
-
39
- @functools.cache
40
- def is_valid_lang_code(lang: str) -> bool:
41
- try:
42
- code_to_langs([lang])
43
- except iso639.LanguageNotFoundError:
44
- console.console.print(
45
- f'[warning]Language [item]{lang}[/item] is being skipped because it is not a iso639 language.[/warning]'
46
- )
47
- return False
48
-
49
- return True
50
-
51
-
52
30
  class PolygonPackager(BasePackager):
53
31
  @classmethod
54
32
  def task_types(cls) -> List[TaskType]:
@@ -59,7 +37,7 @@ class PolygonPackager(BasePackager):
59
37
  pkg = package.find_problem_package_or_die()
60
38
 
61
39
  lang_codes = set()
62
- for statement in pkg.statements:
40
+ for statement in pkg.expanded_statements:
63
41
  lang_codes.add(statement.title)
64
42
 
65
43
  for lang in langs: