rbx.cp 0.11.2__py3-none-any.whl → 0.13.2__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/box/builder.py +3 -3
- rbx/box/cli.py +9 -0
- rbx/box/contest/build_contest_statements.py +5 -6
- rbx/box/contest/statements.py +0 -1
- rbx/box/generators.py +93 -23
- rbx/box/header.py +5 -0
- rbx/box/lang.py +25 -12
- rbx/box/package.py +56 -4
- rbx/box/packaging/contest_main.py +40 -7
- rbx/box/packaging/importer.py +37 -0
- rbx/box/packaging/main.py +18 -65
- rbx/box/packaging/packager.py +95 -2
- rbx/box/packaging/polygon/importer.py +232 -0
- rbx/box/packaging/polygon/packager.py +36 -5
- rbx/box/packaging/polygon/upload.py +34 -14
- rbx/box/packaging/polygon/xml_schema.py +15 -6
- rbx/box/schema.py +3 -3
- rbx/box/solutions.py +8 -12
- rbx/box/statements/build_statements.py +0 -1
- rbx/box/statements/latex.py +11 -0
- rbx/box/stresses.py +1 -1
- rbx/box/tooling/converter.py +76 -0
- rbx/box/tooling/main.py +54 -1
- rbx/grading/caching.py +1 -0
- rbx/grading/judge/sandbox.py +1 -0
- rbx/grading/steps.py +1 -0
- rbx/resources/presets/default/contest/.gitignore +15 -0
- rbx/resources/presets/default/contest/contest.rbx.yml +2 -2
- rbx/resources/presets/default/problem/.gitignore +15 -0
- rbx/resources/presets/default/problem/problem.rbx.yml +1 -4
- rbx/resources/presets/default/problem/testplan/random.py +1 -1
- rbx/resources/presets/default/problem/testplan/random.txt +2 -4
- rbx/resources/presets/default/problem/validator.cpp +2 -1
- rbx/resources/presets/default/shared/icpc.sty +1 -1
- rbx/utils.py +13 -0
- {rbx_cp-0.11.2.dist-info → rbx_cp-0.13.2.dist-info}/METADATA +2 -2
- {rbx_cp-0.11.2.dist-info → rbx_cp-0.13.2.dist-info}/RECORD +40 -37
- {rbx_cp-0.11.2.dist-info → rbx_cp-0.13.2.dist-info}/LICENSE +0 -0
- {rbx_cp-0.11.2.dist-info → rbx_cp-0.13.2.dist-info}/WHEEL +0 -0
- {rbx_cp-0.11.2.dist-info → rbx_cp-0.13.2.dist-info}/entry_points.txt +0 -0
@@ -10,7 +10,7 @@ class Name(BaseXmlModel):
|
|
10
10
|
|
11
11
|
|
12
12
|
class Statement(BaseXmlModel):
|
13
|
-
charset: Optional[
|
13
|
+
charset: Optional[str] = attr(default=None)
|
14
14
|
|
15
15
|
language: str = attr()
|
16
16
|
|
@@ -43,9 +43,12 @@ class Testset(BaseXmlModel):
|
|
43
43
|
size: int = element('test-count', default=None)
|
44
44
|
|
45
45
|
inputPattern: str = element('input-path-pattern')
|
46
|
-
|
46
|
+
outputPattern: Optional[str] = element('output-path-pattern', default=None)
|
47
|
+
answerPattern: Optional[str] = element('answer-path-pattern', default=None)
|
47
48
|
|
48
|
-
tests: List[Test] = wrapped(
|
49
|
+
tests: List[Test] = wrapped(
|
50
|
+
'tests', element(tag='test', default=None), default_factory=list
|
51
|
+
)
|
49
52
|
|
50
53
|
|
51
54
|
class Judging(BaseXmlModel):
|
@@ -56,7 +59,7 @@ class Judging(BaseXmlModel):
|
|
56
59
|
|
57
60
|
|
58
61
|
class Checker(BaseXmlModel):
|
59
|
-
name: str = attr()
|
62
|
+
name: Optional[str] = attr(default=None)
|
60
63
|
type: Literal['testlib'] = attr()
|
61
64
|
source: File = element()
|
62
65
|
binary: Optional[File] = element(default=None)
|
@@ -70,6 +73,8 @@ class Interactor(BaseXmlModel):
|
|
70
73
|
|
71
74
|
|
72
75
|
class Problem(BaseXmlModel, tag='problem'):
|
76
|
+
short_name: str = attr('short-name')
|
77
|
+
|
73
78
|
names: List[Name] = wrapped('names', element(tag='name'), default_factory=list)
|
74
79
|
|
75
80
|
statements: List[Statement] = wrapped(
|
@@ -86,9 +91,13 @@ class Problem(BaseXmlModel, tag='problem'):
|
|
86
91
|
default=[],
|
87
92
|
)
|
88
93
|
|
89
|
-
checker: Checker = wrapped(
|
94
|
+
checker: Optional[Checker] = wrapped(
|
95
|
+
'assets', element(tag='checker', default=None), default=None
|
96
|
+
)
|
90
97
|
|
91
|
-
interactor: Optional[Interactor] = wrapped(
|
98
|
+
interactor: Optional[Interactor] = wrapped(
|
99
|
+
'assets', element(tag='interactor', default=None), default=None
|
100
|
+
)
|
92
101
|
|
93
102
|
|
94
103
|
class ContestProblem(BaseXmlModel):
|
rbx/box/schema.py
CHANGED
@@ -9,7 +9,7 @@ from pydantic import AfterValidator, BaseModel, ConfigDict, Field, model_validat
|
|
9
9
|
from pydantic_core import PydanticCustomError
|
10
10
|
|
11
11
|
from rbx.autoenum import AutoEnum, alias
|
12
|
-
from rbx.box.fields import
|
12
|
+
from rbx.box.fields import NameField
|
13
13
|
from rbx.box.statements.expander import expand_statements
|
14
14
|
from rbx.box.statements.schema import Statement
|
15
15
|
from rbx.grading.steps import Outcome
|
@@ -257,7 +257,7 @@ class Testcase(BaseModel):
|
|
257
257
|
class GeneratorCall(BaseModel):
|
258
258
|
model_config = ConfigDict(extra='forbid')
|
259
259
|
|
260
|
-
name: str =
|
260
|
+
name: str = Field(description='The name of the generator to call.')
|
261
261
|
|
262
262
|
args: Optional[str] = Field(
|
263
263
|
default=None, description='The arguments to pass to the generator.'
|
@@ -355,7 +355,7 @@ problems that have points.
|
|
355
355
|
class Generator(CodeItem):
|
356
356
|
model_config = ConfigDict(extra='forbid')
|
357
357
|
|
358
|
-
name: str =
|
358
|
+
name: str = Field(description="""The name of the generator.""")
|
359
359
|
|
360
360
|
|
361
361
|
class Solution(CodeItem):
|
rbx/box/solutions.py
CHANGED
@@ -158,12 +158,10 @@ def compile_solutions(
|
|
158
158
|
tracked_solutions: Optional[Set[str]] = None,
|
159
159
|
sanitized: bool = False,
|
160
160
|
) -> Dict[pathlib.Path, str]:
|
161
|
-
pkg = package.find_problem_package_or_die()
|
162
|
-
|
163
161
|
compiled_solutions = {}
|
164
162
|
|
165
163
|
if tracked_solutions is None:
|
166
|
-
tracked_solutions = set(str(sol.path) for sol in
|
164
|
+
tracked_solutions = set(str(sol.path) for sol in package.get_solutions())
|
167
165
|
|
168
166
|
for solution in expand_solutions(list(tracked_solutions)):
|
169
167
|
if progress:
|
@@ -232,9 +230,8 @@ async def convert_list_of_solution_evaluations_to_dict(
|
|
232
230
|
skeleton: SolutionReportSkeleton,
|
233
231
|
items: Iterable[EvaluationItem],
|
234
232
|
) -> List[Dict[str, List[Evaluation]]]:
|
235
|
-
pkg = package.find_problem_package_or_die()
|
236
233
|
res: List[Dict[str, List[Evaluation]]] = [
|
237
|
-
collections.defaultdict(list) for _ in
|
234
|
+
collections.defaultdict(list) for _ in package.get_solutions()
|
238
235
|
]
|
239
236
|
|
240
237
|
for item in items:
|
@@ -250,10 +247,9 @@ def _get_solutions_for_skeleton(
|
|
250
247
|
tracked_solutions: Optional[Iterable[str]] = None,
|
251
248
|
verification: VerificationLevel = VerificationLevel.NONE,
|
252
249
|
) -> List[Solution]:
|
253
|
-
pkg = package.find_problem_package_or_die()
|
254
250
|
solutions = [
|
255
251
|
sol
|
256
|
-
for sol in
|
252
|
+
for sol in package.get_solutions()
|
257
253
|
if verification.value >= VerificationLevel.ALL_SOLUTIONS.value or is_fast(sol)
|
258
254
|
]
|
259
255
|
if tracked_solutions is not None:
|
@@ -739,8 +735,7 @@ def _get_solution_repr(sol: Solution) -> List[Tuple[str, str]]:
|
|
739
735
|
|
740
736
|
|
741
737
|
def expand_solutions_with_source(sols: List[str]) -> List[Tuple[Solution, bool]]:
|
742
|
-
|
743
|
-
pkg_sols = {str(sol.path): sol for sol in pkg.solutions}
|
738
|
+
pkg_sols = {str(sol.path): sol for sol in package.get_solutions()}
|
744
739
|
|
745
740
|
# Download remote sols.
|
746
741
|
path_sols = remote.expand_files(sols)
|
@@ -777,20 +772,21 @@ async def pick_solutions(
|
|
777
772
|
tracked_solutions: Optional[OrderedSet[str]],
|
778
773
|
extra_solutions: Optional[List[str]] = None,
|
779
774
|
) -> List[str]:
|
780
|
-
pkg = package.find_problem_package_or_die()
|
781
775
|
# Store in a separate list to maintain order with the package declaration.
|
782
776
|
import questionary
|
783
777
|
|
778
|
+
solutions = package.get_solutions()
|
779
|
+
|
784
780
|
choices = [
|
785
781
|
questionary.Choice(
|
786
782
|
title=_get_solution_repr(sol),
|
787
783
|
value=str(sol.path),
|
788
784
|
checked=tracked_solutions is None or str(sol.path) in tracked_solutions,
|
789
785
|
)
|
790
|
-
for sol in
|
786
|
+
for sol in solutions
|
791
787
|
]
|
792
788
|
|
793
|
-
seen_sols = set(str(sol.path) for sol in
|
789
|
+
seen_sols = set(str(sol.path) for sol in solutions)
|
794
790
|
|
795
791
|
if extra_solutions is not None:
|
796
792
|
# Add only new solutions.
|
rbx/box/statements/latex.py
CHANGED
@@ -5,6 +5,8 @@ from typing import Optional
|
|
5
5
|
|
6
6
|
import chardet
|
7
7
|
|
8
|
+
from rbx.utils import command_exists
|
9
|
+
|
8
10
|
MAX_PDFLATEX_RUNS = 3
|
9
11
|
|
10
12
|
|
@@ -45,3 +47,12 @@ class Latex:
|
|
45
47
|
return LatexResult(result=completed, pdf=None)
|
46
48
|
|
47
49
|
return LatexResult(result=completed, pdf=output_path.read_bytes())
|
50
|
+
|
51
|
+
|
52
|
+
def install_tex_packages(path: pathlib.Path, cwd: pathlib.Path):
|
53
|
+
if not command_exists('texliveonfly'):
|
54
|
+
return
|
55
|
+
subprocess.run(
|
56
|
+
['texliveonfly', path],
|
57
|
+
cwd=cwd,
|
58
|
+
)
|
rbx/box/stresses.py
CHANGED
@@ -0,0 +1,76 @@
|
|
1
|
+
import pathlib
|
2
|
+
import tempfile
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
import typer
|
6
|
+
|
7
|
+
from rbx import console
|
8
|
+
from rbx.box import builder, cd, package
|
9
|
+
from rbx.box.environment import VerificationLevel
|
10
|
+
from rbx.box.packaging.boca.packager import BocaPackager
|
11
|
+
from rbx.box.packaging.importer import BaseImporter
|
12
|
+
from rbx.box.packaging.moj.packager import MojPackager
|
13
|
+
from rbx.box.packaging.packager import BasePackager, BuiltStatement
|
14
|
+
from rbx.box.packaging.polygon.importer import PolygonImporter
|
15
|
+
from rbx.box.packaging.polygon.packager import PolygonPackager
|
16
|
+
from rbx.box.statements.build_statements import build_statement
|
17
|
+
|
18
|
+
PACKAGER_REGISTRY = {
|
19
|
+
'polygon': PolygonPackager,
|
20
|
+
'boca': BocaPackager,
|
21
|
+
'moj': MojPackager,
|
22
|
+
}
|
23
|
+
|
24
|
+
IMPORTER_REGISTRY = {
|
25
|
+
'polygon': PolygonImporter,
|
26
|
+
}
|
27
|
+
|
28
|
+
|
29
|
+
def get_packager(source: str, **kwargs) -> BasePackager:
|
30
|
+
if source not in PACKAGER_REGISTRY:
|
31
|
+
console.console.print(f'Unknown packager: {source}')
|
32
|
+
raise typer.Exit(1)
|
33
|
+
return PACKAGER_REGISTRY[source](**kwargs)
|
34
|
+
|
35
|
+
|
36
|
+
def get_importer(source: str, **kwargs) -> BaseImporter:
|
37
|
+
if source not in IMPORTER_REGISTRY:
|
38
|
+
console.console.print(f'Unknown importer: {source}')
|
39
|
+
raise typer.Exit(1)
|
40
|
+
return IMPORTER_REGISTRY[source](**kwargs)
|
41
|
+
|
42
|
+
|
43
|
+
async def convert(
|
44
|
+
pkg_dir: pathlib.Path,
|
45
|
+
into_dir: pathlib.Path,
|
46
|
+
source: str,
|
47
|
+
destination: str,
|
48
|
+
main_language: Optional[str] = None,
|
49
|
+
) -> pathlib.Path:
|
50
|
+
importer = get_importer(source, main_language=main_language)
|
51
|
+
packager = get_packager(destination)
|
52
|
+
await importer.import_package(pkg_dir, into_dir)
|
53
|
+
|
54
|
+
with cd.new_package_cd(into_dir):
|
55
|
+
package.clear_package_cache()
|
56
|
+
|
57
|
+
pkg = package.find_problem_package_or_die()
|
58
|
+
|
59
|
+
if not await builder.build(VerificationLevel.NONE.value):
|
60
|
+
console.console.print('[error]Failed to build the problem.[/error]')
|
61
|
+
raise typer.Exit(1)
|
62
|
+
|
63
|
+
built_statements = []
|
64
|
+
for statement_type in packager.statement_types():
|
65
|
+
for language in packager.languages():
|
66
|
+
statement = packager.get_statement_for_language(language)
|
67
|
+
statement_path = build_statement(statement, pkg, statement_type)
|
68
|
+
built_statements.append(
|
69
|
+
BuiltStatement(statement, statement_path, statement_type)
|
70
|
+
)
|
71
|
+
|
72
|
+
with tempfile.TemporaryDirectory() as td:
|
73
|
+
result_path = packager.package(
|
74
|
+
package.get_build_path(), pathlib.Path(td), built_statements
|
75
|
+
)
|
76
|
+
return result_path
|
rbx/box/tooling/main.py
CHANGED
@@ -1,8 +1,61 @@
|
|
1
|
+
import atexit
|
2
|
+
import pathlib
|
3
|
+
import shutil
|
4
|
+
import tempfile
|
5
|
+
import zipfile
|
6
|
+
from typing import Annotated, Optional
|
7
|
+
|
8
|
+
import syncer
|
1
9
|
import typer
|
2
10
|
|
3
|
-
from rbx import annotations
|
11
|
+
from rbx import annotations, console
|
12
|
+
from rbx.box.tooling import converter
|
4
13
|
from rbx.box.tooling.boca import main as boca_main
|
5
14
|
|
6
15
|
app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
|
7
16
|
|
8
17
|
app.add_typer(boca_main.app, name='boca')
|
18
|
+
|
19
|
+
|
20
|
+
@app.command('convert')
|
21
|
+
@syncer.sync
|
22
|
+
async def convert(
|
23
|
+
pkg: Annotated[pathlib.Path, typer.Argument(help='The package to convert.')],
|
24
|
+
source: Annotated[
|
25
|
+
str, typer.Option('-s', '--source', help='The format to convert from.')
|
26
|
+
],
|
27
|
+
dest: Annotated[
|
28
|
+
str, typer.Option('-d', '--dest', help='The format to convert to.')
|
29
|
+
],
|
30
|
+
output: Annotated[str, typer.Option('-o', '--output', help='The output path.')],
|
31
|
+
language: Annotated[
|
32
|
+
Optional[str],
|
33
|
+
typer.Option('--language', '-l', help='The main language of the problem.'),
|
34
|
+
] = None,
|
35
|
+
):
|
36
|
+
if pkg.suffix == '.zip':
|
37
|
+
temp_dir = tempfile.TemporaryDirectory()
|
38
|
+
with zipfile.ZipFile(pkg, 'r') as zip_ref:
|
39
|
+
zip_ref.extractall(temp_dir.name)
|
40
|
+
pkg = pathlib.Path(temp_dir.name)
|
41
|
+
|
42
|
+
atexit.register(temp_dir.cleanup)
|
43
|
+
|
44
|
+
if not pkg.is_dir():
|
45
|
+
console.console.print(f'[error]Package {pkg} is not a directory.[/error]')
|
46
|
+
raise typer.Exit(1)
|
47
|
+
|
48
|
+
with tempfile.TemporaryDirectory() as td:
|
49
|
+
result_path = await converter.convert(
|
50
|
+
pkg, pathlib.Path(td), source, dest, main_language=language
|
51
|
+
)
|
52
|
+
output_path = pathlib.Path(output)
|
53
|
+
if output_path.suffix == '.zip':
|
54
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
55
|
+
shutil.copy(result_path, output_path)
|
56
|
+
else:
|
57
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
58
|
+
shutil.unpack_archive(result_path, output_path)
|
59
|
+
console.console.print(
|
60
|
+
f'[success]Converted package to [item]{output_path}[/item].[/success]'
|
61
|
+
)
|
rbx/grading/caching.py
CHANGED
@@ -241,6 +241,7 @@ def _copy_hashed_files(artifact_list: List[GradingArtifacts], cacher: FileCacher
|
|
241
241
|
) is not None:
|
242
242
|
# Use a symlink to the file in the persistent cache, if available.
|
243
243
|
output.dest.unlink(missing_ok=True)
|
244
|
+
output.dest.parent.mkdir(parents=True, exist_ok=True)
|
244
245
|
output.dest.symlink_to(path_to_symlink)
|
245
246
|
else:
|
246
247
|
# Otherwise, copy it.
|
rbx/grading/judge/sandbox.py
CHANGED
rbx/grading/steps.py
CHANGED
@@ -355,6 +355,7 @@ def _process_output_artifacts(
|
|
355
355
|
):
|
356
356
|
# File is in the persistent cache, store a symlink to it.
|
357
357
|
dst.unlink(missing_ok=True)
|
358
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
358
359
|
dst.symlink_to(path_to_symlink)
|
359
360
|
else:
|
360
361
|
# File is not in the persistent cache, copy it.
|
@@ -1,13 +1,13 @@
|
|
1
1
|
---
|
2
2
|
# yaml-language-server: $schema=https://rsalesc.github.io/rbx/schemas/Contest.json
|
3
|
-
# Add problems by running `rbx contest add
|
3
|
+
# Add problems by running `rbx contest add`
|
4
4
|
name: "new-contest"
|
5
5
|
statements:
|
6
6
|
- name: "statement-en"
|
7
7
|
title: "New contest"
|
8
8
|
language: "en"
|
9
9
|
path: "statement/contest.rbx.tex"
|
10
|
-
type:
|
10
|
+
type: JinjaTeX
|
11
11
|
assets:
|
12
12
|
- "statement/icpc.sty"
|
13
13
|
- "statement/*.png"
|
@@ -5,9 +5,6 @@ timeLimit: 1000 # ms
|
|
5
5
|
memoryLimit: 256 # MiB
|
6
6
|
checker: {path: "wcmp.cpp"} # Download others from testlib with `rbx download checker`
|
7
7
|
validator: {path: "validator.cpp"}
|
8
|
-
generators:
|
9
|
-
- path: "gens/gen.cpp"
|
10
|
-
name: "gen"
|
11
8
|
testcases:
|
12
9
|
- name: "samples"
|
13
10
|
testcaseGlob: "manual_tests/samples/*.in" # Pattern for the sample inputs.
|
@@ -39,7 +36,7 @@ statements:
|
|
39
36
|
stresses:
|
40
37
|
- name: "stress"
|
41
38
|
generator:
|
42
|
-
name: "gen"
|
39
|
+
name: "gens/gen"
|
43
40
|
args: "[1..<MAX_N>] @" # `@` generates a random string
|
44
41
|
finder: "[sols/wa.cpp] ~ INCORRECT"
|
45
42
|
unitTests:
|
@@ -1,3 +1,4 @@
|
|
1
|
+
#include "rbx.h"
|
1
2
|
#include "testlib.h"
|
2
3
|
|
3
4
|
using namespace std;
|
@@ -6,7 +7,7 @@ int main(int argc, char *argv[]) {
|
|
6
7
|
registerValidation(argc, argv);
|
7
8
|
prepareOpts(argc, argv);
|
8
9
|
|
9
|
-
int MAX_N =
|
10
|
+
int MAX_N = getVar<int>("MAX_N"); // Read from package vars.
|
10
11
|
|
11
12
|
inf.readInt(1, MAX_N, "A");
|
12
13
|
inf.readSpace();
|
rbx/utils.py
CHANGED
@@ -6,6 +6,7 @@ import os
|
|
6
6
|
import os.path
|
7
7
|
import pathlib
|
8
8
|
import resource
|
9
|
+
import subprocess
|
9
10
|
from typing import Any, Optional, Type, TypeVar
|
10
11
|
|
11
12
|
import rich
|
@@ -145,6 +146,18 @@ def get_open_fds():
|
|
145
146
|
return fds
|
146
147
|
|
147
148
|
|
149
|
+
def command_exists(command):
|
150
|
+
try:
|
151
|
+
subprocess.run(
|
152
|
+
[command], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
153
|
+
)
|
154
|
+
return True
|
155
|
+
except FileNotFoundError:
|
156
|
+
return False
|
157
|
+
except subprocess.CalledProcessError:
|
158
|
+
return True
|
159
|
+
|
160
|
+
|
148
161
|
@contextlib.contextmanager
|
149
162
|
def new_cd(x: pathlib.Path):
|
150
163
|
d = os.getcwd()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: rbx.cp
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.13.2
|
4
4
|
Summary:
|
5
5
|
Author: Roberto Sales
|
6
6
|
Requires-Python: >=3.9.1,<4.0.0
|
@@ -20,6 +20,7 @@ Requires-Dist: fastapi (>=0.115.8,<0.116.0)
|
|
20
20
|
Requires-Dist: filelock (>=3.14.0,<4.0.0)
|
21
21
|
Requires-Dist: gitignore-parser (>=0.1.12,<0.2.0)
|
22
22
|
Requires-Dist: gitpython (>=3.1.43,<4.0.0)
|
23
|
+
Requires-Dist: iso639-lang (>=2.6.1,<3.0.0)
|
23
24
|
Requires-Dist: jinja2 (>=3.1.4,<4.0.0)
|
24
25
|
Requires-Dist: lark (>=1.2.2,<2.0.0)
|
25
26
|
Requires-Dist: latexbuild (>=0.2.2,<0.3.0)
|
@@ -33,7 +34,6 @@ Requires-Dist: pydantic (==2.8.2)
|
|
33
34
|
Requires-Dist: pydantic-xml[lxml] (>=2.11.0,<3.0.0)
|
34
35
|
Requires-Dist: pypandoc (>=1.15,<2.0)
|
35
36
|
Requires-Dist: pyte (>=0.8.2,<0.9.0)
|
36
|
-
Requires-Dist: python-iso639 (>=2024.4.27,<2025.0.0)
|
37
37
|
Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
|
38
38
|
Requires-Dist: questionary (>=2.1.0,<3.0.0)
|
39
39
|
Requires-Dist: requests (>=2.32.3,<3.0.0)
|