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.
Files changed (40) hide show
  1. rbx/box/builder.py +3 -3
  2. rbx/box/cli.py +9 -0
  3. rbx/box/contest/build_contest_statements.py +5 -6
  4. rbx/box/contest/statements.py +0 -1
  5. rbx/box/generators.py +93 -23
  6. rbx/box/header.py +5 -0
  7. rbx/box/lang.py +25 -12
  8. rbx/box/package.py +56 -4
  9. rbx/box/packaging/contest_main.py +40 -7
  10. rbx/box/packaging/importer.py +37 -0
  11. rbx/box/packaging/main.py +18 -65
  12. rbx/box/packaging/packager.py +95 -2
  13. rbx/box/packaging/polygon/importer.py +232 -0
  14. rbx/box/packaging/polygon/packager.py +36 -5
  15. rbx/box/packaging/polygon/upload.py +34 -14
  16. rbx/box/packaging/polygon/xml_schema.py +15 -6
  17. rbx/box/schema.py +3 -3
  18. rbx/box/solutions.py +8 -12
  19. rbx/box/statements/build_statements.py +0 -1
  20. rbx/box/statements/latex.py +11 -0
  21. rbx/box/stresses.py +1 -1
  22. rbx/box/tooling/converter.py +76 -0
  23. rbx/box/tooling/main.py +54 -1
  24. rbx/grading/caching.py +1 -0
  25. rbx/grading/judge/sandbox.py +1 -0
  26. rbx/grading/steps.py +1 -0
  27. rbx/resources/presets/default/contest/.gitignore +15 -0
  28. rbx/resources/presets/default/contest/contest.rbx.yml +2 -2
  29. rbx/resources/presets/default/problem/.gitignore +15 -0
  30. rbx/resources/presets/default/problem/problem.rbx.yml +1 -4
  31. rbx/resources/presets/default/problem/testplan/random.py +1 -1
  32. rbx/resources/presets/default/problem/testplan/random.txt +2 -4
  33. rbx/resources/presets/default/problem/validator.cpp +2 -1
  34. rbx/resources/presets/default/shared/icpc.sty +1 -1
  35. rbx/utils.py +13 -0
  36. {rbx_cp-0.11.2.dist-info → rbx_cp-0.13.2.dist-info}/METADATA +2 -2
  37. {rbx_cp-0.11.2.dist-info → rbx_cp-0.13.2.dist-info}/RECORD +40 -37
  38. {rbx_cp-0.11.2.dist-info → rbx_cp-0.13.2.dist-info}/LICENSE +0 -0
  39. {rbx_cp-0.11.2.dist-info → rbx_cp-0.13.2.dist-info}/WHEEL +0 -0
  40. {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[Literal['UTF-8']] = attr(default=None)
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
- answerPattern: str = element('answer-path-pattern')
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('tests', element(tag='test'), default_factory=list)
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('assets', element(tag='checker'))
94
+ checker: Optional[Checker] = wrapped(
95
+ 'assets', element(tag='checker', default=None), default=None
96
+ )
90
97
 
91
- interactor: Optional[Interactor] = wrapped('assets', element(tag='interactor'))
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 FNameField, NameField
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 = FNameField(description='The name of the generator to call.')
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 = NameField(description="""The name of the generator.""")
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 pkg.solutions)
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 pkg.solutions
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 pkg.solutions
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
- pkg = package.find_problem_package_or_die()
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 pkg.solutions
786
+ for sol in solutions
791
787
  ]
792
788
 
793
- seen_sols = set(str(sol.path) for sol in pkg.solutions)
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.
@@ -339,7 +339,6 @@ async def build(
339
339
  vars: Annotated[
340
340
  Optional[List[str]],
341
341
  typer.Option(
342
- '-v',
343
342
  '--vars',
344
343
  help='Variables to be used in the statements.',
345
344
  ),
@@ -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
@@ -67,7 +67,7 @@ async def run_stress(
67
67
  raise typer.Exit(1)
68
68
  generator = generators.get_call_from_string(generator_call)
69
69
  stress = Stress(
70
- name=f'{generator.name}',
70
+ name=f'{pathlib.Path(generator.name).stem}',
71
71
  generator=generator,
72
72
  finder=finder,
73
73
  )
@@ -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.
@@ -469,6 +469,7 @@ class SandboxBase(abc.ABC):
469
469
  if override:
470
470
  real_path.unlink(missing_ok=True)
471
471
  try:
472
+ real_path.parent.mkdir(parents=True, exist_ok=True)
472
473
  real_path.symlink_to(utils.abspath(from_path))
473
474
  except NotImplementedError:
474
475
  return None
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,6 +1,21 @@
1
1
  .box/
2
2
  build/
3
+ __pycache__/
3
4
 
5
+ .DS_Store
6
+ .vscode/
7
+
8
+ a.out
9
+ *.exe
10
+ *.pyc
4
11
  *.o
12
+ *~
13
+
14
+ *.fdb_latexmk
15
+ *.fls
16
+ *.log
17
+ *.synctex.gz
5
18
  *.aux
6
19
  *.log
20
+
21
+ *.un~
@@ -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 <problem-name> <short-name>`
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: "jinja-tex"
10
+ type: JinjaTeX
11
11
  assets:
12
12
  - "statement/icpc.sty"
13
13
  - "statement/*.png"
@@ -1,6 +1,21 @@
1
1
  .box/
2
2
  build/
3
+ __pycache__/
3
4
 
5
+ .DS_Store
6
+ .vscode/
7
+
8
+ a.out
9
+ *.exe
10
+ *.pyc
4
11
  *.o
12
+ *~
13
+
14
+ *.fdb_latexmk
15
+ *.fls
16
+ *.log
17
+ *.synctex.gz
5
18
  *.aux
6
19
  *.log
20
+
21
+ *.un~
@@ -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,3 @@
1
1
  # Generate 10 random testcases with n <= 1e9
2
2
  for i in range(10):
3
- print(f'gen 1000000000 {i}')
3
+ print(f'gens/gen 1000000000 {i}')
@@ -1,4 +1,2 @@
1
- gen 123456
2
- gen 12345678
3
- # Obtained by running `rbx stress -g 'gen [1..<MAX_N>] @' -f '[sols/wa.cpp] ~ INCORRECT'`
4
- gen 149403982 b139a2bd
1
+ gens/gen 123456
2
+ gens/gen 12345678
@@ -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 = opt<int>("MAX_N"); // Read from package vars.
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();
@@ -220,7 +220,7 @@
220
220
  \end{minipage}
221
221
  \end{tabular}
222
222
  \end{center}
223
- \vspace{-0.5cm}
223
+ \vspace{-0.1cm}
224
224
  }
225
225
  } % exampleInteractive
226
226
 
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.11.2
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)