88from timeit import default_timer
99from types import EllipsisType
1010from typing import Any , ClassVar , Iterator , Protocol , Self , TypeVar , cast , Generator as PyGenerator
11+ from typing_extensions import TypedDict
1112from uuid import uuid4
1213import json
1314from dataclasses import dataclass
2223from pydantic import Field
2324from anyio .to_thread import run_sync
2425from urllib3 .exceptions import ReadTimeoutError
25- from algobattle .config import DockerConfig , RunConfig , RunConfigOverride , RunSpecs
2626
2727from algobattle .util import (
2828 BuildError ,
3636 Role ,
3737 BaseModel ,
3838)
39- from algobattle .problem import AnyProblem , Instance , Problem , Solution
39+ from algobattle .problem import AnyProblem , Instance , Solution
4040
4141
4242AnySolution = 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+
64126class 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