Skip to content

Commit 7ec0382

Browse files
committed
add program config view
1 parent 82619dd commit 7ec0382

2 files changed

Lines changed: 111 additions & 75 deletions

File tree

algobattle/match.py

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@
66
from itertools import combinations
77
from pathlib import Path
88
import tomllib
9-
from types import EllipsisType
10-
from typing import Annotated, Any, ClassVar, Literal, Self, TypeAlias, TypeVar, TypedDict, cast, overload
9+
from typing import Annotated, Any, ClassVar, Self, TypeAlias, TypeVar, TypedDict, cast, overload
1110

1211
from pydantic import AfterValidator, ByteSize, ConfigDict, Field, GetCoreSchemaHandler, ValidationInfo, model_validator
1312
from pydantic.types import PathType
1413
from pydantic_core import CoreSchema
15-
from pydantic_core.core_schema import no_info_after_validator_function, tagged_union_schema, union_schema
14+
from pydantic_core.core_schema import no_info_after_validator_function, union_schema
1615
from anyio import create_task_group, CapacityLimiter
1716
from anyio.to_thread import current_default_thread_limiter
1817
from docker.types import LogConfig, Ulimit
@@ -533,24 +532,6 @@ class DockerConfig(BaseModel):
533532
run: AdvancedRunArgs = AdvancedRunArgs()
534533

535534

536-
class RunConfigOverride(TypedDict, total=False):
537-
"""Run parameters that were overriden by the battle type."""
538-
539-
timeout: float | None
540-
space: int | None
541-
cpus: int
542-
543-
544-
@dataclass(frozen=True, slots=True)
545-
class RunSpecs:
546-
"""Actual specification of a program run."""
547-
548-
timeout: float | None
549-
space: int | None
550-
cpus: int
551-
overriden: RunConfigOverride
552-
553-
554535
class RunConfig(BaseModel):
555536
"""Parameters determining how a program is run."""
556537

@@ -565,28 +546,6 @@ class RunConfig(BaseModel):
565546
cpus: int = 1
566547
"""Number of cpu cores available."""
567548

568-
def reify(
569-
self,
570-
timeout: float | None | EllipsisType,
571-
space: int | None | EllipsisType,
572-
cpus: int | EllipsisType,
573-
) -> RunSpecs:
574-
"""Merges the overriden config options with the parsed ones."""
575-
overriden = RunConfigOverride()
576-
if timeout is ...:
577-
timeout = self.timeout
578-
else:
579-
overriden["timeout"] = timeout
580-
if space is ...:
581-
space = self.space
582-
else:
583-
overriden["space"] = space
584-
if cpus is ...:
585-
cpus = self.cpus
586-
else:
587-
overriden["cpus"] = cpus
588-
return RunSpecs(timeout=timeout, space=space, cpus=cpus, overriden=overriden)
589-
590549

591550
class MatchConfig(BaseModel):
592551
"""Parameters determining the match execution.

algobattle/program.py

Lines changed: 109 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from timeit import default_timer
99
from types import EllipsisType
1010
from typing import Any, ClassVar, Iterator, Protocol, Self, TypeVar, cast, Generator as PyGenerator
11+
from typing_extensions import TypedDict
1112
from uuid import uuid4
1213
import json
1314
from dataclasses import dataclass
@@ -22,7 +23,6 @@
2223
from pydantic import Field
2324
from anyio.to_thread import run_sync
2425
from urllib3.exceptions import ReadTimeoutError
25-
from algobattle.config import DockerConfig, RunConfig, RunConfigOverride, RunSpecs
2626

2727
from algobattle.util import (
2828
BuildError,
@@ -36,7 +36,7 @@
3636
Role,
3737
BaseModel,
3838
)
39-
from algobattle.problem import AnyProblem, Instance, Problem, Solution
39+
from algobattle.problem import AnyProblem, Instance, Solution
4040

4141

4242
AnySolution = Solution[Instance]
@@ -61,6 +61,68 @@ def client() -> DockerClient:
6161
return _client_var
6262

6363

64+
class RunConfigOverride(TypedDict, total=False):
65+
"""Run parameters that were overriden by the battle type."""
66+
67+
timeout: float | None
68+
space: int | None
69+
cpus: int
70+
71+
72+
@dataclass(frozen=True, slots=True)
73+
class RunSpecs:
74+
"""Actual specification of a program run."""
75+
76+
timeout: float | None
77+
space: int | None
78+
cpus: int
79+
overriden: RunConfigOverride
80+
81+
82+
@dataclass(frozen=True, slots=True)
83+
class RunConfigView:
84+
"""Config view for single runs."""
85+
86+
timeout: float | None
87+
space: int | None
88+
cpus: int
89+
90+
def reify(
91+
self,
92+
timeout: float | None | EllipsisType,
93+
space: int | None | EllipsisType,
94+
cpus: int | EllipsisType,
95+
) -> RunSpecs:
96+
"""Merges the overriden config options with the parsed ones."""
97+
overriden = RunConfigOverride()
98+
if timeout is ...:
99+
timeout = self.timeout
100+
else:
101+
overriden["timeout"] = timeout
102+
if space is ...:
103+
space = self.space
104+
else:
105+
overriden["space"] = space
106+
if cpus is ...:
107+
cpus = self.cpus
108+
else:
109+
overriden["cpus"] = cpus
110+
return RunSpecs(timeout=timeout, space=space, cpus=cpus, overriden=overriden)
111+
112+
113+
@dataclass(frozen=True, slots=True)
114+
class ProgramConfigView:
115+
"""Config settings relevant to the program module."""
116+
117+
build_timeout: float | None
118+
max_image_size: int | None
119+
strict_timeouts: bool
120+
build_kwargs: dict[str, Any]
121+
run_kwargs: dict[str, Any]
122+
generator: RunConfigView
123+
solver: RunConfigView
124+
125+
64126
class ProgramUi(Protocol):
65127
"""Provides an interface for :class:`Program` to update the Ui."""
66128

@@ -166,14 +228,10 @@ class Program(ABC):
166228

167229
id: str
168230
"""The id of the Docker image."""
169-
problem: Problem[Instance, Solution[Instance]]
170-
"""The problem this program creates instances and/or solutions for."""
171-
config: RunConfig
172-
"""Config settings this program will use."""
173-
strict_timeouts: bool
174-
"""Wether this program will raise an exception if the container times out."""
175-
docker_config: DockerConfig
176-
"""Advanced config settings this program will use to run."""
231+
problem: AnyProblem
232+
"""The problem this program generates/solves."""
233+
config: ProgramConfigView
234+
"""Config settings used for this program."""
177235

178236
role: ClassVar[Role]
179237
"""Role of this program."""
@@ -209,25 +267,17 @@ async def build(
209267
cls,
210268
path: Path,
211269
*,
212-
timeout: float | None = 600,
213-
max_size: int | None = None,
214-
team_name: str | None = None,
215270
problem: AnyProblem,
216-
config: RunConfig = RunConfig(),
217-
strict_timeouts: bool = False,
218-
docker_config: DockerConfig = DockerConfig(),
271+
config: ProgramConfigView,
272+
team_name: str | None = None,
219273
) -> Self:
220274
"""Creates a program by building the specified docker image.
221275
222276
Args:
223277
path: Path to a Dockerfile (or folder containing one) from which to build the image.
224-
timeout: Build timeout.
225-
max_size: Maximum size of the built image.
278+
problem: The problem this program generates/solves.
279+
config: Settings for this program.
226280
team_name: If set the image will be given a descriptive name.
227-
problem: Problem this program is solving/generating instances for.
228-
config: Run config for this program.
229-
strict_timeouts: Wether to raise an error if the container times out but produces valid output.
230-
docker_config: Docker config that will be used to build and run this program.
231281
232282
Returns:
233283
The built Program.
@@ -251,9 +301,9 @@ async def build(
251301
cls._build_daemon_call,
252302
str(path),
253303
name,
254-
timeout,
304+
config.build_timeout,
255305
dockerfile,
256-
docker_config.build.kwargs,
306+
config.build_kwargs,
257307
)
258308
if old_image is not None:
259309
old_image.reload()
@@ -271,15 +321,15 @@ async def build(
271321
cast(str, image.id),
272322
problem=problem,
273323
config=config,
274-
strict_timeouts=strict_timeouts,
275-
docker_config=docker_config,
276324
)
277325
used_size = cast(dict[str, Any], image.attrs).get("Size", 0)
278-
if max_size is not None and used_size > max_size:
326+
if config.max_image_size is not None and used_size > config.max_image_size:
279327
try:
280328
self.remove()
281329
finally:
282-
raise BuildError("Built image is too large.", detail=f"Size: {used_size}B, limit: {max_size}B.")
330+
raise BuildError(
331+
"Built image is too large.", detail=f"Size: {used_size}B, limit: {config.max_image_size}B."
332+
)
283333
return self
284334

285335
@classmethod
@@ -298,6 +348,33 @@ def _build_daemon_call(
298348
)
299349
return image
300350

351+
def run_specs(
352+
self,
353+
timeout: float | None | EllipsisType,
354+
space: int | None | EllipsisType,
355+
cpus: int | EllipsisType,
356+
) -> RunSpecs:
357+
"""Merges the overriden config options with the parsed ones."""
358+
overriden = RunConfigOverride()
359+
match self.role:
360+
case Role.generator:
361+
config = self.config.generator
362+
case Role.solver:
363+
config = self.config.solver
364+
if timeout is ...:
365+
timeout = config.timeout
366+
else:
367+
overriden["timeout"] = timeout
368+
if space is ...:
369+
space = config.space
370+
else:
371+
overriden["space"] = space
372+
if cpus is ...:
373+
cpus = config.cpus
374+
else:
375+
overriden["cpus"] = cpus
376+
return RunSpecs(timeout=timeout, space=space, cpus=cpus, overriden=overriden)
377+
301378
async def _run_inner(
302379
self,
303380
*,
@@ -338,7 +415,7 @@ async def _run_inner(
338415
detach=True,
339416
mounts=io.mounts if io else None,
340417
cpuset_cpus=set_cpus,
341-
**self.docker_config.run.kwargs,
418+
**self.config.run_kwargs,
342419
),
343420
)
344421

@@ -397,7 +474,7 @@ def _run_daemon_call(self, container: DockerContainer, timeout: float | None = N
397474
elapsed_time = round(default_timer() - start_time, 2)
398475
if len(e.args) != 1 or not isinstance(e.args[0], ReadTimeoutError):
399476
raise
400-
if self.strict_timeouts:
477+
if self.config.strict_timeouts:
401478
raise ExecutionTimeout("The docker container exceeded the time limit.", runtime=elapsed_time)
402479
return elapsed_time
403480

@@ -457,7 +534,7 @@ async def run(
457534
Returns:
458535
Datastructure containing all info about the generator execution and the created problem instance.
459536
"""
460-
specs = self.config.reify(timeout, space, cpus)
537+
specs = self.run_specs(timeout, space, cpus)
461538
runtime = 0
462539
battle_data = None
463540
instance = None
@@ -574,7 +651,7 @@ async def run(
574651
Returns:
575652
Datastructure containing all info about the solver execution and the solution it computed.
576653
"""
577-
specs = self.config.reify(timeout, space, cpus)
654+
specs = self.run_specs(timeout, space, cpus)
578655
runtime = 0
579656
battle_data = None
580657
solution = None

0 commit comments

Comments
 (0)