Skip to content

Commit 3f66485

Browse files
committed
♻️ Cleanup of xtl.jobs.Job
xtl.jobs.config - Abstraction of JobConfig to _BaseJobConfig - JobConfig now holds configuration of multiple steps - If `job_directory` is provided for a JobConfig, it will be propagated to all of its steps xtl.jobs.config2 - BatchConfig now inherits from _BaseJobConfig xtl.jobs.jobs:Job - Removed all of the batch execution functionality, which is now handled by the compute site - Logging of tracebacks upon exceptions in the .run() method are now toggleable with the option settings.jobs.tracebacks - Fixed a bug where the config class was not properly propagated to new subclasses of BatchJob xtl.jobs.jobs:BatchJob - Positional arguments can now be passed to the shell when executing a BatchJob - These can be passed as `batch_args` in the .with_config() method xtl.config.settings - Fixed a bug where the Settings class was not hashable - Added option `tracebacks` to JobSettings
1 parent 4f87034 commit 3f66485

File tree

6 files changed

+142
-198
lines changed

6 files changed

+142
-198
lines changed

src/xtl/config/settings.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ class Settings(Options):
5151
# instantiated without any input file. Any unknown options will then be stored
5252
# in the model's `__pydantic_extra__` attribute.
5353

54+
# Make Settings hashable so that it can be used in sets or as dict keys
55+
__hash__ = object.__hash__
56+
5457

5558
class UnitsSettings(Settings):
5659
"""
@@ -141,6 +144,12 @@ class JobsSettings(Settings):
141144
desc='Default compute site for job execution'
142145
)
143146

147+
tracebacks: bool = \
148+
Option(
149+
default=False,
150+
desc='Whether to include tracebacks in job logs when exceptions occur'
151+
)
152+
144153

145154
class DependencySettings(Settings):
146155
"""
@@ -178,7 +187,7 @@ class DependenciesSettings(Settings):
178187
resolution: str = \
179188
Option(
180189
default='loose',
181-
choices=('strict', 'loose'),
190+
choices={'strict', 'loose'},
182191
desc='Dependency resolution strategy. '
183192
'If "strict", jobs requiring missing dependencies will fail to run. '
184193
'If "loose", missing dependencies will be ignored.'

src/xtl/exceptions/base.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,11 @@ def __init__(self, message: str, command: Any, raiser=None):
5454
self.message = message
5555
self.command = command
5656
self.raiser = raiser
57+
58+
59+
class StderrError(Error):
60+
61+
def __init__(self, message: str, stderr: str, raiser=None):
62+
self.message = message
63+
self.stderr = stderr
64+
self.raiser = raiser

src/xtl/jobs/config.py

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from pathlib import Path
2-
from typing import Optional
2+
from typing import Optional, get_origin
33
import tempfile
44
from typing import TYPE_CHECKING
5+
from typing_extensions import TypedDict
56

67
from pydantic import computed_field, PrivateAttr, model_validator
78

@@ -158,27 +159,73 @@ def get_batch(self) -> 'BatchFile':
158159
return batch
159160

160161

161-
class JobConfig(Options):
162+
# Setting total=False to make all keys optional during initialization
163+
# NB: This needs to happen on every subclass of JobStepsConfig
164+
class JobStepsConfig(TypedDict, total=True):
162165
"""
163-
Base class for passing configuration to a job.
166+
Base class for defining the steps of a job. Each key is the name of a step, and the value is the configuration for
167+
that step, which must be a subclass of JobConfig.
164168
"""
165-
job_directory: Optional[Path] = Option(default=None,
166-
desc='Directory for job execution and '
167-
'results')
169+
...
168170

169-
# TODO: Remove all below when BatchJob/BatchJobConfig2 is fully implemented
170-
batch: Optional[BatchConfig] = Option(default=None,
171-
desc='Configuration for execution of batch'
172-
'files')
173171

174-
_include_default_dependencies: bool = PrivateAttr(True)
175-
"""Whether to include the default dependencies in the job."""
172+
class _BaseJobConfig(Options):
173+
"""
174+
Base class for job configuration, containing fields common to all jobs.
175+
"""
176+
177+
job_directory: Optional[Path] = \
178+
Option(
179+
default=None,
180+
desc='Directory for job execution and results'
181+
)
182+
183+
184+
class JobConfig(_BaseJobConfig):
185+
"""
186+
Class for passing configuration to a job.
187+
"""
188+
189+
steps: JobStepsConfig = \
190+
Option(
191+
default_factory=JobStepsConfig,
192+
desc='Configuration for each step of the job.'
193+
)
194+
195+
def _get_steps_class(self) -> type[TypedDict]:
196+
"""
197+
Get the class of the steps field.
198+
"""
199+
# Get the `steps` field
200+
step_field = self.model_fields.get('steps', None)
201+
if step_field is None:
202+
raise ValueError('JobConfig must have a steps field')
203+
204+
# Get the type of the `steps` field and ensure it's a simple type
205+
# Will raise for e.g. list[int], Union[str, int], etc.
206+
StepsClass: type[TypedDict] = step_field.annotation
207+
if get_origin(StepsClass) is not None:
208+
raise TypeError(f'`steps` must be a simple type, not {StepsClass}')
209+
210+
return StepsClass
211+
212+
@property
213+
def steps_list(self) -> list[str]:
214+
"""
215+
Returns the list of steps in the job, in the order they are defined.
216+
"""
217+
return list(self._get_steps_class().__annotations__.keys())
176218

177219
@model_validator(mode='after')
178220
def _propagate_job_directory(self):
179221
"""
180222
Propagate job_directory to batch.batch_directory if both are set
181223
"""
182-
if self.job_directory is not None and self.batch is not None:
183-
self.batch.directory = self.job_directory
224+
if self.job_directory:
225+
for i, step in enumerate(self.steps_list):
226+
config = self.steps.get(step, {})
227+
if not hasattr(config, 'job_directory'):
228+
continue
229+
config.job_directory = self.job_directory / f'{i+1}_{step}'
230+
184231
return self

src/xtl/jobs/config2.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from xtl.common.serializers import PermissionOctal
1111
from xtl.common.validators import cast_as_temp_dir_if_none
1212
from xtl.jobs import Shell
13-
from xtl.jobs.config import JobConfig
13+
from xtl.jobs.config import _BaseJobConfig
1414
from xtl.jobs.sites import ComputeSite
1515

1616

@@ -21,26 +21,28 @@ class ResourcesConfig(Options):
2121

2222
# TODO: Custom formatters & aliases for SLURM
2323
# TODO: Custom validators for SLURM-like input
24+
# Formatting should be handled within different methods, e.g. to_slurm()
25+
# Maybe we implement a _aliases and _formatters PrivateAttr in the ResourcesConfig for this purpose?
2426
cpus: int = \
2527
Option(
2628
default=1, ge=1,
2729
desc='Number of CPU cores required for the job',
28-
alias='cpus-per-task'
30+
# alias='cpus-per-task'
2931
)
3032

3133
memory: float = \
3234
Option(
3335
default=1.0, ge=0.0,
3436
desc='Amount of memory (in GB) required for the job',
35-
alias='mem',
36-
formatter=lambda x: f'{x}G'
37+
# alias='mem',
38+
# formatter=lambda x: f'{x}G'
3739
)
3840

3941
timeout: float | str | timedelta | None = \
4042
Option(
4143
default=None,
4244
desc='Maximum runtime for the job (in minutes or D-HH:MM:SS format)',
43-
alias='time',
45+
# alias='time',
4446
# cast_as=...,
4547
# formatter=...
4648
)
@@ -49,21 +51,20 @@ class ResourcesConfig(Options):
4951
Option(
5052
default=0, ge=0,
5153
desc='Number of GPUs required for the job',
52-
alias='gpus'
5354
)
5455

5556
no_tasks: int = \
5657
Option(
5758
default=1, ge=1,
5859
desc='Number of tasks required for the job (only used in MPI jobs)',
59-
alias='ntasks'
60+
# alias='ntasks'
6061
)
6162

6263
no_nodes: int = \
6364
Option(
6465
default=1, ge=1,
6566
desc='Number of nodes required for the job (only used in MPI jobs)',
66-
alias='nodes'
67+
# alias='nodes'
6768
)
6869

6970
def to_slurm(self) -> list[str]:
@@ -74,13 +75,14 @@ def to_slurm(self) -> list[str]:
7475
return args
7576

7677

77-
class BatchJobConfig(JobConfig):
78+
class BatchJobConfig(_BaseJobConfig):
7879

79-
# Generate a temporary directory if not provided
80-
job_directory: Optional[Path] = Option(
81-
default_factory=lambda: cast_as_temp_dir_if_none(None, prefix='xtl_batch_'),
82-
desc='Directory for job execution and results',
83-
cast_as=lambda x: cast_as_temp_dir_if_none(x, prefix='xtl_batch_'),
80+
# Batch jobs always require a job directory
81+
job_directory: Optional[Path] = \
82+
Option(
83+
default_factory=lambda: cast_as_temp_dir_if_none(None, prefix='xtl_batch_'),
84+
desc='Directory for job execution and results',
85+
cast_as=lambda x: cast_as_temp_dir_if_none(x, prefix='xtl_batch_'),
8486
)
8587
filename: str = \
8688
Option(

0 commit comments

Comments
 (0)