algobattle-base 4.3.2__tar.gz → 4.3.3__tar.gz
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.
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/PKG-INFO +2 -4
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/battle.py +25 -35
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/cli.py +105 -97
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/match.py +31 -30
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/problem.py +16 -26
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/program.py +49 -51
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/__init__.py +5 -5
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/types.py +43 -53
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/util.py +11 -16
- algobattle_base-4.3.3/pyproject.toml +156 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/test_battles.py +7 -4
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/test_docker.py +16 -3
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/test_match.py +7 -6
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/test_types.py +2 -2
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/test_util.py +4 -4
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/problem.py +2 -1
- algobattle_base-4.3.2/pyproject.toml +0 -63
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/LICENSE +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/README.md +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/__init__.py +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/py.typed +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/c/.gitignore +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/c/Dockerfile +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/c/src/CMakeLists.txt +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/c/src/main.c +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/cpp/.gitignore +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/cpp/Dockerfile +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/cpp/src/CMakeLists.txt +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/cpp/src/main.cpp +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/csharp/Dockerfile.jinja +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/csharp/Program.cs +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/csharp/{{program.capitalize()}}.csproj +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/go/.gitignore +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/go/Dockerfile +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/go/go.mod.jinja +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/go/main.go +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/java/.dockerignore +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/java/.gitignore +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/java/Dockerfile.jinja +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/java/pom.xml.jinja +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/java/src/main/java/com/{{team_normalized}}/{{program.capitalize()}}.java.jinja +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/javascript/.gitignore +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/javascript/Dockerfile.jinja +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/javascript/package-lock.json.jinja +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/javascript/package.json.jinja +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/javascript/{{program}}.mjs.jinja +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/plain/Dockerfile.jinja +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/problem.py.jinja +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/python/.gitignore +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/python/Dockerfile.jinja +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/python/pyproject.toml.jinja +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/python/{{program}}.py.jinja +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/rust/.gitignore +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/rust/Cargo.toml.jinja +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/rust/Dockerfile.jinja +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/rust/src/main.rs.jinja +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/typescript/.gitignore +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/typescript/Dockerfile +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/typescript/package-lock.json.jinja +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/typescript/package.json.jinja +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/typescript/src/main.ts +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/typescript/tsconfig.json +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/README.md +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/__init__.py +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/configs/empty.toml +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/configs/teams.toml +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/configs/teams_incorrect.toml +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/configs/test.toml +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/README.md +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/__init__.py +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/build_error/Dockerfile +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/build_timeout/Dockerfile +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/generator/Dockerfile +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/generator/instance.json +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/generator_execution_error/Dockerfile +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/generator_semantics_error/Dockerfile +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/generator_semantics_error/instance.json +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/generator_syntax_error/Dockerfile +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/generator_syntax_error/instance.json +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/generator_timeout/Dockerfile +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/generator_timeout/instance.json +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/solver/Dockerfile +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/solver/solution.json +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/solver_execution_error/Dockerfile +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/solver_semantics_error/Dockerfile +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/solver_semantics_error/solution.json +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/solver_syntax_error/Dockerfile +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/solver_syntax_error/solution.json +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/solver_timeout/Dockerfile +0 -0
- {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/solver_timeout/solution.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: algobattle-base
|
|
3
|
-
Version: 4.3.
|
|
3
|
+
Version: 4.3.3
|
|
4
4
|
Summary: The Algobattle lab course package.
|
|
5
5
|
Author: Imogen Hergeth, Jan Dreier, Henri Lotze
|
|
6
6
|
License: MIT
|
|
@@ -22,9 +22,7 @@ Requires-Dist: typing-extensions>=4.15.0
|
|
|
22
22
|
Requires-Dist: tomlkit>=0.13.3
|
|
23
23
|
Requires-Dist: jinja2>=3.1.6
|
|
24
24
|
Provides-Extra: dev
|
|
25
|
-
Requires-Dist:
|
|
26
|
-
Requires-Dist: flake8>=6.1.0; extra == "dev"
|
|
27
|
-
Requires-Dist: flake8-docstrings>=1.7.0; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff>=0.13.2; extra == "dev"
|
|
28
26
|
Requires-Dist: mkdocs>=1.5.3; extra == "dev"
|
|
29
27
|
Requires-Dist: mkdocs-material>=9.5.3; extra == "dev"
|
|
30
28
|
Requires-Dist: pymdown-extensions>=10.7; extra == "dev"
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
This module contains the :class:`Battle` class, which speciefies how each type of battle is fought and scored,
|
|
4
4
|
some basic battle types, and related classed.
|
|
5
5
|
"""
|
|
6
|
+
from abc import abstractmethod
|
|
7
|
+
from collections.abc import Iterable
|
|
6
8
|
from dataclasses import dataclass, field
|
|
7
9
|
from enum import StrEnum
|
|
8
10
|
from importlib.metadata import entry_points
|
|
9
|
-
from abc import abstractmethod
|
|
10
11
|
from inspect import isclass
|
|
11
12
|
from itertools import count
|
|
12
13
|
from pathlib import Path
|
|
@@ -16,7 +17,6 @@ from typing import (
|
|
|
16
17
|
Annotated,
|
|
17
18
|
Any,
|
|
18
19
|
ClassVar,
|
|
19
|
-
Iterable,
|
|
20
20
|
Literal,
|
|
21
21
|
ParamSpec,
|
|
22
22
|
Protocol,
|
|
@@ -26,9 +26,8 @@ from typing import (
|
|
|
26
26
|
Unpack,
|
|
27
27
|
overload,
|
|
28
28
|
)
|
|
29
|
-
from typing_extensions import TypedDict
|
|
30
|
-
from annotated_types import Ge
|
|
31
29
|
|
|
30
|
+
from annotated_types import Ge
|
|
32
31
|
from pydantic import (
|
|
33
32
|
ConfigDict,
|
|
34
33
|
Field,
|
|
@@ -39,11 +38,10 @@ from pydantic import (
|
|
|
39
38
|
ValidatorFunctionWrapHandler,
|
|
40
39
|
)
|
|
41
40
|
from pydantic_core import CoreSchema
|
|
42
|
-
from pydantic_core.core_schema import
|
|
43
|
-
|
|
44
|
-
with_info_wrap_validator_function,
|
|
45
|
-
)
|
|
41
|
+
from pydantic_core.core_schema import tagged_union_schema, with_info_wrap_validator_function
|
|
42
|
+
from typing_extensions import TypedDict
|
|
46
43
|
|
|
44
|
+
from algobattle.problem import InstanceModel, Problem, SolutionModel
|
|
47
45
|
from algobattle.program import (
|
|
48
46
|
Generator,
|
|
49
47
|
GeneratorResult,
|
|
@@ -53,15 +51,7 @@ from algobattle.program import (
|
|
|
53
51
|
Solver,
|
|
54
52
|
SolverResult,
|
|
55
53
|
)
|
|
56
|
-
from algobattle.
|
|
57
|
-
from algobattle.util import (
|
|
58
|
-
Encodable,
|
|
59
|
-
EncodableModel,
|
|
60
|
-
ExceptionInfo,
|
|
61
|
-
BaseModel,
|
|
62
|
-
Role,
|
|
63
|
-
)
|
|
64
|
-
|
|
54
|
+
from algobattle.util import BaseModel, Encodable, EncodableModel, ExceptionInfo, Role
|
|
65
55
|
|
|
66
56
|
_BattleConfig: TypeAlias = Any
|
|
67
57
|
"""Type alias used to generate correct typings when subclassing :class:`Battle`.
|
|
@@ -73,7 +63,7 @@ the new battle type directly.
|
|
|
73
63
|
"""
|
|
74
64
|
T = TypeVar("T")
|
|
75
65
|
P = ParamSpec("P")
|
|
76
|
-
|
|
66
|
+
_type = type
|
|
77
67
|
|
|
78
68
|
|
|
79
69
|
class ProgramLogConfigTime(StrEnum):
|
|
@@ -91,7 +81,7 @@ class ProgramLogConfigLocation(StrEnum):
|
|
|
91
81
|
inline = "inline"
|
|
92
82
|
|
|
93
83
|
|
|
94
|
-
class ProgramLogConfigView(Protocol):
|
|
84
|
+
class ProgramLogConfigView(Protocol):
|
|
95
85
|
when: ProgramLogConfigTime = ProgramLogConfigTime.error
|
|
96
86
|
output: ProgramLogConfigLocation = ProgramLogConfigLocation.inline
|
|
97
87
|
|
|
@@ -100,7 +90,7 @@ class ProgramRunInfo(BaseModel):
|
|
|
100
90
|
"""Data about a program's execution."""
|
|
101
91
|
|
|
102
92
|
runtime: float = 0
|
|
103
|
-
overriden: RunConfigOverride = Field(default_factory=
|
|
93
|
+
overriden: RunConfigOverride = Field(default_factory=RunConfigOverride, validate_default=True)
|
|
104
94
|
error: ExceptionInfo | None = None
|
|
105
95
|
battle_data: SerializeAsAny[EncodableModel] | None = None
|
|
106
96
|
instance: SerializeAsAny[InstanceModel] | None = None
|
|
@@ -177,7 +167,7 @@ class FightUi(ProgramUi, Protocol):
|
|
|
177
167
|
"""Informs the ui that the fight has finished running and has been added to the battle's `.fight_results`."""
|
|
178
168
|
|
|
179
169
|
|
|
180
|
-
class RunKwargs(TypedDict, total=False):
|
|
170
|
+
class RunKwargs(TypedDict, total=False, closed=True):
|
|
181
171
|
"""The keyword arguments used by the FightHandler.run family of functions."""
|
|
182
172
|
|
|
183
173
|
timeout_generator: float | None
|
|
@@ -394,7 +384,7 @@ class Battle(BaseModel):
|
|
|
394
384
|
"""Type of battle that will be used."""
|
|
395
385
|
|
|
396
386
|
@classmethod
|
|
397
|
-
def __get_pydantic_core_schema__(cls, source:
|
|
387
|
+
def __get_pydantic_core_schema__(cls, source: _type, handler: GetCoreSchemaHandler) -> CoreSchema:
|
|
398
388
|
# there's two bugs we need to catch:
|
|
399
389
|
# 1. this function is called during the pydantic BaseModel metaclass's __new__, so the BattleConfig class
|
|
400
390
|
# won't be ready at that point and be missing in the namespace
|
|
@@ -446,7 +436,7 @@ class Battle(BaseModel):
|
|
|
446
436
|
installed = ", ".join(b.name() for b in Battle._battle_types.values())
|
|
447
437
|
raise ValueError(
|
|
448
438
|
f"The specified battle type '{passed}' is not installed. Installed types are: {installed}"
|
|
449
|
-
)
|
|
439
|
+
) from e
|
|
450
440
|
|
|
451
441
|
return with_info_wrap_validator_function(check_installed, subclass_schema)
|
|
452
442
|
|
|
@@ -459,7 +449,7 @@ class Battle(BaseModel):
|
|
|
459
449
|
|
|
460
450
|
if TYPE_CHECKING:
|
|
461
451
|
# to hint that we're gonna fill this with arbitrary data belonging to some supposed battle type
|
|
462
|
-
def __getattr__(self,
|
|
452
|
+
def __getattr__(self, attr: str, /) -> Any:
|
|
463
453
|
...
|
|
464
454
|
|
|
465
455
|
class UiData(BaseModel):
|
|
@@ -560,7 +550,7 @@ class Iterated(Battle):
|
|
|
560
550
|
exit after that many failures, or `"unlimited"` to never exit early.
|
|
561
551
|
"""
|
|
562
552
|
|
|
563
|
-
class UiData(Battle.UiData):
|
|
553
|
+
class UiData(Battle.UiData):
|
|
564
554
|
reached: list[int]
|
|
565
555
|
cap: int
|
|
566
556
|
note: str
|
|
@@ -620,7 +610,7 @@ class Iterated(Battle):
|
|
|
620
610
|
return 0 if len(self.results) == 0 else sum(self.results) / len(self.results)
|
|
621
611
|
|
|
622
612
|
@staticmethod
|
|
623
|
-
def format_score(score: float) -> str:
|
|
613
|
+
def format_score(score: float) -> str:
|
|
624
614
|
return str(int(score))
|
|
625
615
|
|
|
626
616
|
|
|
@@ -637,7 +627,7 @@ class Averaged(Battle):
|
|
|
637
627
|
num_fights: int = 10
|
|
638
628
|
"""Number of iterations in each round."""
|
|
639
629
|
|
|
640
|
-
class UiData(Battle.UiData):
|
|
630
|
+
class UiData(Battle.UiData):
|
|
641
631
|
round: int
|
|
642
632
|
|
|
643
633
|
async def run_battle(self, fight: FightHandler, config: Config, min_size: int, ui: BattleUi) -> None:
|
|
@@ -659,7 +649,7 @@ class Averaged(Battle):
|
|
|
659
649
|
return sum(f.score for f in self.fights) / len(self.fights)
|
|
660
650
|
|
|
661
651
|
@staticmethod
|
|
662
|
-
def format_score(score: float) -> str:
|
|
652
|
+
def format_score(score: float) -> str:
|
|
663
653
|
return format(score, ".0%")
|
|
664
654
|
|
|
665
655
|
|
|
@@ -681,7 +671,7 @@ class FightHistory(Encodable):
|
|
|
681
671
|
gen_sols: set[Role]
|
|
682
672
|
sol_sols: set[Role]
|
|
683
673
|
|
|
684
|
-
def encode(self, target: Path, role: Role) -> None:
|
|
674
|
+
def encode(self, target: Path, role: Role) -> None:
|
|
685
675
|
target.mkdir()
|
|
686
676
|
for i, fight in enumerate(self.history):
|
|
687
677
|
fight_dir = target / str(i)
|
|
@@ -715,16 +705,16 @@ class Improving(Battle):
|
|
|
715
705
|
"""Number of fights that will be fought."""
|
|
716
706
|
weighting: Annotated[float, Ge(0)] = 1.1
|
|
717
707
|
"""How much each successive fight should be weighted more than the previous."""
|
|
718
|
-
scores: set[Role] = {Role.generator, Role.solver}
|
|
708
|
+
scores: set[Role] = {Role.generator, Role.solver} # noqa: RUF012
|
|
719
709
|
"""Who to show each fight's scores to."""
|
|
720
|
-
instances: set[Role] = {Role.generator, Role.solver}
|
|
710
|
+
instances: set[Role] = {Role.generator, Role.solver} # noqa: RUF012
|
|
721
711
|
"""Who to show the instances to."""
|
|
722
|
-
generator_solutions: set[Role] = {Role.generator}
|
|
712
|
+
generator_solutions: set[Role] = {Role.generator} # noqa: RUF012
|
|
723
713
|
"""Who to show the generator's solutions to, if the problem requires them."""
|
|
724
|
-
solver_solutions: set[Role] = {Role.solver}
|
|
714
|
+
solver_solutions: set[Role] = {Role.solver} # noqa: RUF012
|
|
725
715
|
"""Who to show the solver's solutions to."""
|
|
726
716
|
|
|
727
|
-
class UiData(Battle.UiData):
|
|
717
|
+
class UiData(Battle.UiData):
|
|
728
718
|
round: int
|
|
729
719
|
|
|
730
720
|
async def run_battle(self, fight: FightHandler, config: Config, min_size: int, ui: BattleUi) -> None:
|
|
@@ -760,5 +750,5 @@ class Improving(Battle):
|
|
|
760
750
|
return total / quotient
|
|
761
751
|
|
|
762
752
|
@staticmethod
|
|
763
|
-
def format_score(score: float) -> str:
|
|
753
|
+
def format_score(score: float) -> str:
|
|
764
754
|
return format(score, ".0%")
|
|
@@ -2,68 +2,69 @@
|
|
|
2
2
|
|
|
3
3
|
Provides a command line interface to start matches and observe them. See `battle --help` for further options.
|
|
4
4
|
"""
|
|
5
|
+
|
|
6
|
+
import operator
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Iterable
|
|
5
10
|
from enum import StrEnum
|
|
6
11
|
from functools import cached_property
|
|
7
|
-
import
|
|
12
|
+
from importlib.metadata import version as pkg_version
|
|
8
13
|
from os import environ
|
|
9
14
|
from pathlib import Path
|
|
10
15
|
from random import choice
|
|
11
16
|
from shutil import rmtree
|
|
12
17
|
from subprocess import PIPE, Popen
|
|
13
|
-
import
|
|
14
|
-
from typing import Annotated, Any, ClassVar, Iterable, Literal, Optional, Self, cast
|
|
15
|
-
from typing_extensions import override
|
|
16
|
-
from importlib.metadata import version as pkg_version
|
|
18
|
+
from typing import Annotated, Any, ClassVar, Literal, Self, cast
|
|
17
19
|
from zipfile import ZipFile
|
|
18
|
-
import shutil
|
|
19
20
|
|
|
20
21
|
from anyio import run as run_async_fn
|
|
21
22
|
from click import Choice
|
|
22
|
-
from click.core import Parameter
|
|
23
|
+
from click.core import Context, Parameter
|
|
23
24
|
from pydantic import Field, TypeAdapter, ValidationError
|
|
24
|
-
from
|
|
25
|
-
from rich.console import Group, RenderableType
|
|
25
|
+
from rich.columns import Columns
|
|
26
|
+
from rich.console import Console, Group, RenderableType
|
|
26
27
|
from rich.live import Live
|
|
27
|
-
from rich.
|
|
28
|
+
from rich.padding import Padding
|
|
29
|
+
from rich.panel import Panel
|
|
28
30
|
from rich.progress import (
|
|
29
|
-
Progress,
|
|
30
|
-
TextColumn,
|
|
31
|
-
SpinnerColumn,
|
|
32
31
|
BarColumn,
|
|
33
32
|
MofNCompleteColumn,
|
|
34
|
-
|
|
33
|
+
Progress,
|
|
35
34
|
ProgressColumn,
|
|
35
|
+
SpinnerColumn,
|
|
36
36
|
Task,
|
|
37
|
+
TextColumn,
|
|
38
|
+
TimeElapsedColumn,
|
|
37
39
|
)
|
|
38
|
-
from rich.
|
|
40
|
+
from rich.prompt import Confirm, Prompt
|
|
41
|
+
from rich.rule import Rule
|
|
42
|
+
from rich.table import Column, Table
|
|
39
43
|
from rich.text import Text
|
|
40
|
-
from rich.columns import Columns
|
|
41
|
-
from rich.prompt import Prompt, Confirm
|
|
42
44
|
from rich.theme import Theme
|
|
43
|
-
from rich.rule import Rule
|
|
44
|
-
from rich.padding import Padding
|
|
45
45
|
from rich.traceback import Traceback
|
|
46
|
-
from tomlkit import TOMLDocument, comment,
|
|
46
|
+
from tomlkit import TOMLDocument, comment, dumps as dumps_toml, nl as toml_newline, parse as parse_toml, table
|
|
47
47
|
from tomlkit.exceptions import ParseError
|
|
48
48
|
from tomlkit.items import Table as TomlTable
|
|
49
|
+
from typer import Abort, Argument, Option, Typer, get_app_dir, launch
|
|
50
|
+
from typing_extensions import override
|
|
49
51
|
|
|
50
52
|
from algobattle.battle import Battle
|
|
51
|
-
from algobattle.match import AlgobattleConfig, EmptyUi, Match, MatchConfig, MatchupStr, TeamInfo, Ui
|
|
53
|
+
from algobattle.match import AlgobattleConfig, EmptyUi, Match, MatchConfig, MatchupStr, ProjectConfig, TeamInfo, Ui
|
|
52
54
|
from algobattle.problem import Instance, Problem, Solution
|
|
53
55
|
from algobattle.program import Generator, Matchup, Solver
|
|
56
|
+
from algobattle.templates import Language, PartialTemplateArgs, TemplateArgs, write_problem_template, write_templates
|
|
54
57
|
from algobattle.util import (
|
|
58
|
+
BaseModel,
|
|
55
59
|
BuildError,
|
|
56
60
|
DockerNotRunning,
|
|
57
61
|
EncodableModel,
|
|
58
62
|
ExceptionInfo,
|
|
59
63
|
Role,
|
|
60
64
|
RunningTimer,
|
|
61
|
-
BaseModel,
|
|
62
65
|
TempDir,
|
|
63
66
|
timestamp,
|
|
64
67
|
)
|
|
65
|
-
from algobattle.templates import Language, PartialTemplateArgs, TemplateArgs, write_problem_template, write_templates
|
|
66
|
-
|
|
67
68
|
|
|
68
69
|
__all__ = ("app",)
|
|
69
70
|
|
|
@@ -75,16 +76,14 @@ For more detailed documentation, visit our website at http://algobattle.org/docs
|
|
|
75
76
|
app = Typer(pretty_exceptions_show_locals=True, help=help_message)
|
|
76
77
|
packager = Typer(help="Subcommands to package problems and programs into `.algo` files.")
|
|
77
78
|
app.add_typer(packager, name="package")
|
|
78
|
-
theme = Theme(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
)
|
|
79
|
+
theme = Theme({
|
|
80
|
+
"success": "green",
|
|
81
|
+
"warning": "orange3",
|
|
82
|
+
"error": "red",
|
|
83
|
+
"attention": "magenta2",
|
|
84
|
+
"heading": "blue",
|
|
85
|
+
"info": "dim cyan",
|
|
86
|
+
})
|
|
88
87
|
console = Console(theme=theme)
|
|
89
88
|
|
|
90
89
|
|
|
@@ -101,7 +100,7 @@ class _General(BaseModel):
|
|
|
101
100
|
|
|
102
101
|
|
|
103
102
|
class CliConfig(BaseModel):
|
|
104
|
-
general: _General = Field(default_factory=
|
|
103
|
+
general: _General = Field(default_factory=_General, validate_default=True)
|
|
105
104
|
default_project_table: ProjectConfig | None = Field(default=None)
|
|
106
105
|
|
|
107
106
|
_doc: TOMLDocument
|
|
@@ -138,8 +137,7 @@ class CliConfig(BaseModel):
|
|
|
138
137
|
@property
|
|
139
138
|
def default_project_doc(self) -> TomlTable | None:
|
|
140
139
|
"""The default exec config for each problem."""
|
|
141
|
-
|
|
142
|
-
return exec
|
|
140
|
+
return self._doc.get("default_project_table", None)
|
|
143
141
|
|
|
144
142
|
@cached_property
|
|
145
143
|
def install_cmd(self) -> list[str]:
|
|
@@ -167,6 +165,7 @@ class CliConfig(BaseModel):
|
|
|
167
165
|
|
|
168
166
|
@app.command("run")
|
|
169
167
|
def run_match(
|
|
168
|
+
*,
|
|
170
169
|
path: Annotated[
|
|
171
170
|
Path, Argument(exists=True, help="Path to either a config file or a directory containing one.")
|
|
172
171
|
] = Path(),
|
|
@@ -184,27 +183,25 @@ def run_match(
|
|
|
184
183
|
save = False
|
|
185
184
|
except KeyboardInterrupt:
|
|
186
185
|
console.print("[error]Stopping match execution")
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
except KeyboardInterrupt:
|
|
207
|
-
raise Abort
|
|
186
|
+
try:
|
|
187
|
+
if config.project.points > 0 and result.active_teams:
|
|
188
|
+
points = result.calculate_points()
|
|
189
|
+
leaderboard = Table(
|
|
190
|
+
Column("Team", justify="center"),
|
|
191
|
+
Column("Points", justify="right"),
|
|
192
|
+
title="[heading]Leaderboard",
|
|
193
|
+
)
|
|
194
|
+
for team, pts in sorted(points.items(), key=operator.itemgetter(1)):
|
|
195
|
+
leaderboard.add_row(team, f"{pts:.1f}")
|
|
196
|
+
console.print(Padding(leaderboard, (1, 0, 0, 0)))
|
|
197
|
+
if save:
|
|
198
|
+
out_path = config.project.results.joinpath(f"match-{timestamp()}.json")
|
|
199
|
+
config.project.results.mkdir(parents=True, exist_ok=True)
|
|
200
|
+
out_path.write_text(result.format(error_detail=config.project.error_detail))
|
|
201
|
+
console.print("Saved match result to ", out_path)
|
|
202
|
+
except KeyboardInterrupt as e:
|
|
203
|
+
raise Abort from e
|
|
204
|
+
return result
|
|
208
205
|
|
|
209
206
|
|
|
210
207
|
def _init_program(target: Path, lang: Language, args: PartialTemplateArgs, role: Role) -> None:
|
|
@@ -233,17 +230,15 @@ class ClickLanguage(Choice):
|
|
|
233
230
|
def __init__(self, case_sensitive: bool = True) -> None:
|
|
234
231
|
super().__init__([lang.value for lang in Language], case_sensitive)
|
|
235
232
|
|
|
236
|
-
def get_metavar(self, param: Parameter) -> str:
|
|
233
|
+
def get_metavar(self, param: Parameter, ctx: Context) -> str:
|
|
237
234
|
return "LANGUAGE"
|
|
238
235
|
|
|
239
236
|
|
|
240
237
|
@app.command(epilog=f"Supported languages are: {', '.join(Language)}.")
|
|
241
238
|
def init(
|
|
242
|
-
target: Annotated[
|
|
243
|
-
Optional[Path], Argument(file_okay=False, writable=True, help="The folder to initialize.")
|
|
244
|
-
] = None,
|
|
239
|
+
target: Annotated[Path | None, Argument(file_okay=False, writable=True, help="The folder to initialize.")] = None,
|
|
245
240
|
problem_: Annotated[
|
|
246
|
-
|
|
241
|
+
str | None,
|
|
247
242
|
Option(
|
|
248
243
|
"--problem",
|
|
249
244
|
"-p",
|
|
@@ -251,15 +246,15 @@ def init(
|
|
|
251
246
|
),
|
|
252
247
|
] = None,
|
|
253
248
|
language: Annotated[
|
|
254
|
-
|
|
249
|
+
Language | None,
|
|
255
250
|
Option("--language", "-l", help="The language to use for the programs.", click_type=ClickLanguage()),
|
|
256
251
|
] = None,
|
|
257
252
|
generator: Annotated[
|
|
258
|
-
|
|
253
|
+
Language | None,
|
|
259
254
|
Option("--generator", "-g", help="The language to use for the generator.", click_type=ClickLanguage()),
|
|
260
255
|
] = None,
|
|
261
256
|
solver: Annotated[
|
|
262
|
-
|
|
257
|
+
Language | None,
|
|
263
258
|
Option("--solver", "-s", help="The language to use for the solver.", click_type=ClickLanguage()),
|
|
264
259
|
] = None,
|
|
265
260
|
schemas: Annotated[bool, Option(help="Whether to also save the problem's IO schemas.")] = False,
|
|
@@ -282,10 +277,28 @@ def init(
|
|
|
282
277
|
if language:
|
|
283
278
|
generator = solver = language
|
|
284
279
|
config = CliConfig.load()
|
|
285
|
-
team_name = config.general.team_name or choice(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
280
|
+
team_name = config.general.team_name or choice((
|
|
281
|
+
"Dogs",
|
|
282
|
+
"Cats",
|
|
283
|
+
"Otters",
|
|
284
|
+
"Red Pandas",
|
|
285
|
+
"Crows",
|
|
286
|
+
"Rats",
|
|
287
|
+
"Cockatoos",
|
|
288
|
+
"Dingos",
|
|
289
|
+
"Penguins",
|
|
290
|
+
"Kiwis",
|
|
291
|
+
"Orcas",
|
|
292
|
+
"Bearded Dragons",
|
|
293
|
+
"Macaws",
|
|
294
|
+
"Wombats",
|
|
295
|
+
"Wallabies",
|
|
296
|
+
"Owls",
|
|
297
|
+
"Seals",
|
|
298
|
+
"Octopuses",
|
|
299
|
+
"Frogs",
|
|
300
|
+
"Jellyfish",
|
|
301
|
+
))
|
|
289
302
|
|
|
290
303
|
if new: # create a new problem
|
|
291
304
|
if problem_ is None:
|
|
@@ -304,12 +317,12 @@ def init(
|
|
|
304
317
|
target = Path()
|
|
305
318
|
try:
|
|
306
319
|
parsed_config = AlgobattleConfig.from_file(target, relativize_paths=False)
|
|
307
|
-
except FileNotFoundError:
|
|
320
|
+
except FileNotFoundError as e:
|
|
308
321
|
console.print("[error]You must use a problem spec file or target a directory with an existing config.")
|
|
309
|
-
raise Abort
|
|
322
|
+
raise Abort from e
|
|
310
323
|
except ValueError as e:
|
|
311
324
|
console.print("[error]The Algobattle config file is not formatted properly\n", e)
|
|
312
|
-
raise Abort
|
|
325
|
+
raise Abort from e
|
|
313
326
|
console.print("Using existing project data")
|
|
314
327
|
if len(parsed_config.teams) == 1:
|
|
315
328
|
team_name = next(iter(parsed_config.teams.keys()))
|
|
@@ -323,9 +336,8 @@ def init(
|
|
|
323
336
|
|
|
324
337
|
elif (problem := Path(problem_)).is_file(): # use a problem spec file
|
|
325
338
|
with TempDir() as unpack_dir:
|
|
326
|
-
with console.status("Extracting problem data"):
|
|
327
|
-
|
|
328
|
-
problem_zip.extractall(unpack_dir)
|
|
339
|
+
with console.status("Extracting problem data"), ZipFile(problem) as problem_zip:
|
|
340
|
+
problem_zip.extractall(unpack_dir)
|
|
329
341
|
|
|
330
342
|
parsed_config = AlgobattleConfig.from_file(unpack_dir, relativize_paths=False)
|
|
331
343
|
if target is None:
|
|
@@ -333,7 +345,7 @@ def init(
|
|
|
333
345
|
|
|
334
346
|
target.mkdir(parents=True, exist_ok=True)
|
|
335
347
|
problem_data = list(unpack_dir.iterdir())
|
|
336
|
-
if any((
|
|
348
|
+
if any(target.joinpath(path.name).exists() for path in problem_data):
|
|
337
349
|
copy_problem_data = Confirm.ask(
|
|
338
350
|
"[attention]The target directory already contains an algobattle project, "
|
|
339
351
|
"do you want to replace it?",
|
|
@@ -363,9 +375,10 @@ def init(
|
|
|
363
375
|
problem_name = parsed_config.match.problem
|
|
364
376
|
if deps := parsed_config.problem.dependencies:
|
|
365
377
|
cmd = config.install_cmd
|
|
366
|
-
with
|
|
367
|
-
|
|
368
|
-
|
|
378
|
+
with (
|
|
379
|
+
console.status(f"Installing {problem_name}'s dependencies"),
|
|
380
|
+
Popen(cmd + deps, env=environ.copy(), stdout=PIPE, stderr=PIPE, text=True) as installer,
|
|
381
|
+
):
|
|
369
382
|
assert installer.stdout is not None
|
|
370
383
|
assert installer.stderr is not None
|
|
371
384
|
for line in installer.stdout:
|
|
@@ -506,7 +519,7 @@ def test_team(config: AlgobattleConfig, team: str, size: int | None = None) -> T
|
|
|
506
519
|
@app.command()
|
|
507
520
|
def test(
|
|
508
521
|
project: Annotated[Path, Argument(help="The project folder to use.")] = Path(),
|
|
509
|
-
size: Annotated[
|
|
522
|
+
size: Annotated[int | None, Option(help="The size of instance the generator will be asked to create.")] = None,
|
|
510
523
|
) -> Literal["success", "error"]:
|
|
511
524
|
"""Tests whether the programs install successfully and run on dummy instances without crashing."""
|
|
512
525
|
if not (project.is_file() or project.joinpath("algobattle.toml").is_file()):
|
|
@@ -542,10 +555,10 @@ def config() -> None:
|
|
|
542
555
|
def package_problem(
|
|
543
556
|
project: Annotated[Path, Argument(exists=True, resolve_path=True, help="Path to the project directory.")] = Path(),
|
|
544
557
|
description: Annotated[
|
|
545
|
-
|
|
558
|
+
Path | None, Option(exists=True, dir_okay=False, help="Path to a problem description file.")
|
|
546
559
|
] = None,
|
|
547
560
|
out: Annotated[
|
|
548
|
-
|
|
561
|
+
Path | None, Option("--out", "-o", dir_okay=False, file_okay=False, help="Location of the output.")
|
|
549
562
|
] = None,
|
|
550
563
|
) -> None:
|
|
551
564
|
"""Packages problem data into an `.algo` file."""
|
|
@@ -571,22 +584,23 @@ def package_problem(
|
|
|
571
584
|
parsed_config = AlgobattleConfig.from_file(config)
|
|
572
585
|
except (ValidationError, ParseError) as e:
|
|
573
586
|
console.print(f"[error]Improperly formatted config file[/]\nError: {e}")
|
|
574
|
-
raise Abort
|
|
587
|
+
raise Abort from e
|
|
575
588
|
problem_name = parsed_config.match.problem
|
|
576
589
|
try:
|
|
577
590
|
with console.status("Loading problem"):
|
|
578
|
-
|
|
591
|
+
# we need to access the property so that it gets loaded
|
|
592
|
+
parsed_config.loaded_problem # noqa: B018
|
|
579
593
|
except ValueError as e:
|
|
580
594
|
console.print(f"[error]Couldn't load the problem file[/]\nError: {e}")
|
|
581
|
-
raise Abort
|
|
595
|
+
raise Abort from e
|
|
582
596
|
except RuntimeError as e:
|
|
583
597
|
error = e.__cause__
|
|
584
598
|
if error is None:
|
|
585
599
|
console.print(f"[error]Couldn't load the problem file[/]\nError: {e}")
|
|
586
|
-
raise Abort
|
|
600
|
+
raise Abort from e
|
|
587
601
|
trace = Traceback.from_exception(error.__class__, error, error.__traceback__)
|
|
588
602
|
console.print("[error]Couldn't execute the problem file[/]\nError:", trace)
|
|
589
|
-
raise Abort
|
|
603
|
+
raise Abort from e
|
|
590
604
|
|
|
591
605
|
if "project" in config_doc:
|
|
592
606
|
config_doc.remove("project")
|
|
@@ -613,7 +627,7 @@ def package_problem(
|
|
|
613
627
|
def package_programs(
|
|
614
628
|
project: Annotated[Path, Argument(help="The project folder to use.")] = Path(),
|
|
615
629
|
team: Annotated[
|
|
616
|
-
|
|
630
|
+
str | None,
|
|
617
631
|
Option(
|
|
618
632
|
help="Name of team whose programs should be packaged. If None are specified, every team's are packaged."
|
|
619
633
|
),
|
|
@@ -789,9 +803,6 @@ class CliUi(Live, Ui):
|
|
|
789
803
|
self.config = config
|
|
790
804
|
super().__init__(None, refresh_per_second=10, transient=True, console=console)
|
|
791
805
|
|
|
792
|
-
def __enter__(self) -> Self:
|
|
793
|
-
return cast(Self, super().__enter__())
|
|
794
|
-
|
|
795
806
|
def _update_renderable(self) -> None:
|
|
796
807
|
if self.build is None:
|
|
797
808
|
renderable = Group(self.display_match(self.match, self.config.match), *self.battle_panels.values())
|
|
@@ -809,10 +820,7 @@ class CliUi(Live, Ui):
|
|
|
809
820
|
title="[heading]Match overview",
|
|
810
821
|
)
|
|
811
822
|
for matchup, battle in match.battles.items():
|
|
812
|
-
if battle.runtime_error is None:
|
|
813
|
-
res = battle.format_score(battle.score(config.battle))
|
|
814
|
-
else:
|
|
815
|
-
res = ":warning:"
|
|
823
|
+
res = battle.format_score(battle.score(config.battle)) if battle.runtime_error is None else ":warning:"
|
|
816
824
|
table.add_row(matchup.generator, matchup.solver, res)
|
|
817
825
|
return Padding(table, pad=(1, 0, 0, 0))
|
|
818
826
|
|
|
@@ -868,7 +876,7 @@ class CliUi(Live, Ui):
|
|
|
868
876
|
fights = battle.fights[-1:-6:-1]
|
|
869
877
|
panel = self.battle_panels[matchup]
|
|
870
878
|
table = panel._fights_table()
|
|
871
|
-
for i, fight in zip(range(len(battle.fights), len(battle.fights) - len(fights), -1), fights):
|
|
879
|
+
for i, fight in zip(range(len(battle.fights), len(battle.fights) - len(fights), -1), fights, strict=True):
|
|
872
880
|
if fight.generator.error:
|
|
873
881
|
info = f"[error]Generator failed[/]: {fight.generator.error.message}"
|
|
874
882
|
elif fight.solver and fight.solver.error:
|