hpcpy 0.7.3__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.
- hpcpy/__init__.py +22 -0
- hpcpy/_version.py +21 -0
- hpcpy/client/__init__.py +1 -0
- hpcpy/client/base.py +445 -0
- hpcpy/client/client_factory.py +38 -0
- hpcpy/client/direct.py +210 -0
- hpcpy/client/pbs.py +223 -0
- hpcpy/client/slurm.py +203 -0
- hpcpy/constants/__init__.py +21 -0
- hpcpy/constants/direct.py +55 -0
- hpcpy/constants/pbs.py +60 -0
- hpcpy/constants/slurm.py +108 -0
- hpcpy/data/test/scripts/slurm_get_states.py +37 -0
- hpcpy/exceptions.py +41 -0
- hpcpy/job.py +96 -0
- hpcpy/status.py +23 -0
- hpcpy/utilities.py +209 -0
- hpcpy-0.7.3.dist-info/METADATA +55 -0
- hpcpy-0.7.3.dist-info/RECORD +22 -0
- hpcpy-0.7.3.dist-info/WHEEL +5 -0
- hpcpy-0.7.3.dist-info/licenses/LICENSE +16 -0
- hpcpy-0.7.3.dist-info/top_level.txt +1 -0
hpcpy/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Top-level package for hpcpy."""
|
|
2
|
+
|
|
3
|
+
from . import _version
|
|
4
|
+
from hpcpy.client.client_factory import ClientFactory
|
|
5
|
+
from hpcpy.client.direct import DirectClient
|
|
6
|
+
from hpcpy.client.pbs import PBSClient
|
|
7
|
+
from hpcpy.client.slurm import SlurmClient
|
|
8
|
+
from typing import Union
|
|
9
|
+
|
|
10
|
+
__version__ = _version.get_versions()["version"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_client(*args, **kwargs) -> Union[PBSClient, SlurmClient, DirectClient]:
|
|
14
|
+
"""Get a client object specific for the current scheduler.
|
|
15
|
+
|
|
16
|
+
Returns
|
|
17
|
+
-------
|
|
18
|
+
Union[PBSClient, SlurmClient, DirectClient]
|
|
19
|
+
Client object for this scheduler.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
return ClientFactory().get_client(*args, **kwargs)
|
hpcpy/_version.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
# This file was generated by 'versioneer.py' (0.29) from
|
|
3
|
+
# revision-control system data, or from the parent directory name of an
|
|
4
|
+
# unpacked source archive. Distribution tarballs contain a pre-generated copy
|
|
5
|
+
# of this file.
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
version_json = '''
|
|
10
|
+
{
|
|
11
|
+
"date": "2026-06-25T11:05:52+1000",
|
|
12
|
+
"dirty": false,
|
|
13
|
+
"error": null,
|
|
14
|
+
"full-revisionid": "1e88a763b5dea9ec2589f39ffcd71332a3c6f709",
|
|
15
|
+
"version": "0.7.3"
|
|
16
|
+
}
|
|
17
|
+
''' # END VERSION_JSON
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_versions():
|
|
21
|
+
return json.loads(version_json)
|
hpcpy/client/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Top-level client objects."""
|
hpcpy/client/base.py
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"""Base client object."""
|
|
2
|
+
|
|
3
|
+
from hpcpy.utilities import shell, interpolate_file_template, get_logger, ensure_list
|
|
4
|
+
from hpcpy.job import Job
|
|
5
|
+
import hpcpy.constants as hc
|
|
6
|
+
from random import choice
|
|
7
|
+
from string import ascii_uppercase
|
|
8
|
+
import os
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from pandas import to_timedelta
|
|
11
|
+
from typing import Union
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseClient:
|
|
15
|
+
"""A base class from which all others inherit.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
cmd_templates : dict
|
|
20
|
+
Dictionary of command templates.
|
|
21
|
+
directive_templates : dict
|
|
22
|
+
Dictionary of directive templates.
|
|
23
|
+
statuses : list
|
|
24
|
+
List of statuses.
|
|
25
|
+
status_attribute : str
|
|
26
|
+
Attribute to use for status lookup.
|
|
27
|
+
job_script_expiry : str, optional
|
|
28
|
+
Job script expiry interval, by default "1H"
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
cmd_templates,
|
|
34
|
+
directive_templates,
|
|
35
|
+
statuses,
|
|
36
|
+
status_attribute,
|
|
37
|
+
job_script_expiry="1H",
|
|
38
|
+
):
|
|
39
|
+
# Set the command templates etc.
|
|
40
|
+
self.cmd_templates = cmd_templates
|
|
41
|
+
self.job_script_expiry = job_script_expiry
|
|
42
|
+
self.statuses = statuses
|
|
43
|
+
self.status_attribute = status_attribute
|
|
44
|
+
self.directive_templates = directive_templates
|
|
45
|
+
|
|
46
|
+
# Set up a shared logger
|
|
47
|
+
self._logger = get_logger()
|
|
48
|
+
|
|
49
|
+
def _clean_rendered_job_scripts(self, force=False) -> None:
|
|
50
|
+
"""Clean the rendered job scripts from the JOB_SCRIPT_DIR.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
force : bool, optional
|
|
55
|
+
Clean regardless of expiry, by default False
|
|
56
|
+
"""
|
|
57
|
+
# Disable option
|
|
58
|
+
if self.job_script_expiry is None and force is False:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
self._logger.debug(f"Cleaning rendered jobscripts (force={force}).")
|
|
62
|
+
|
|
63
|
+
# List the rendered files.
|
|
64
|
+
rendered_job_scripts = self.list_rendered_job_scripts()
|
|
65
|
+
|
|
66
|
+
# Work out the threshold
|
|
67
|
+
now = datetime.now()
|
|
68
|
+
threshold = now - to_timedelta(self.job_script_expiry).to_pytimedelta()
|
|
69
|
+
|
|
70
|
+
for rjs in rendered_job_scripts:
|
|
71
|
+
|
|
72
|
+
# Ensure actually a file
|
|
73
|
+
if not os.path.isfile(rjs):
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
# Get the modified time of the file, check threshold and delete
|
|
77
|
+
mod_time = datetime.fromtimestamp(os.path.getmtime(rjs))
|
|
78
|
+
|
|
79
|
+
# Check if the file has expired
|
|
80
|
+
if mod_time <= threshold or force:
|
|
81
|
+
self._logger.debug(f"Removing {rjs}")
|
|
82
|
+
os.remove(rjs)
|
|
83
|
+
|
|
84
|
+
def list_rendered_job_scripts(self) -> list:
|
|
85
|
+
"""List the rendered job scripts in the JOB_SCRIPT_DIR.
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
list
|
|
90
|
+
List of paths to job scripts.
|
|
91
|
+
"""
|
|
92
|
+
return [hc.JOB_SCRIPT_DIR / rjs for rjs in os.listdir(hc.JOB_SCRIPT_DIR)]
|
|
93
|
+
|
|
94
|
+
def submit(
|
|
95
|
+
self,
|
|
96
|
+
job_script,
|
|
97
|
+
directives=list(),
|
|
98
|
+
render=False,
|
|
99
|
+
dry_run=False,
|
|
100
|
+
env=dict(),
|
|
101
|
+
**context,
|
|
102
|
+
) -> Union[Job, str]:
|
|
103
|
+
"""Submit the job script.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
job_script : path-like
|
|
108
|
+
Path to the job script or template if render=True
|
|
109
|
+
render : bool
|
|
110
|
+
Use the job_script as a template and render **context into it.
|
|
111
|
+
directives : list
|
|
112
|
+
List of directives to add to the command.
|
|
113
|
+
dry_run : bool
|
|
114
|
+
Return the command that would have been executed.
|
|
115
|
+
env : dict
|
|
116
|
+
Append the specified dictionary to the execution environment
|
|
117
|
+
**context :
|
|
118
|
+
Additional key/value pairs interpolated into the command and job script.
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
Union[Job, str]
|
|
123
|
+
Submitted job object, or the command to execute (if dry_run=True).
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
if render:
|
|
127
|
+
self._logger.debug("Rendering job script.")
|
|
128
|
+
_job_script = self._render_job_script(job_script, **context)
|
|
129
|
+
else:
|
|
130
|
+
_job_script = job_script
|
|
131
|
+
|
|
132
|
+
self._logger.debug(f"Using job script as {_job_script}")
|
|
133
|
+
|
|
134
|
+
# Add the directives to the interpolation context (will return blank string if nothing there)
|
|
135
|
+
_directives = self._render_directives(directives)
|
|
136
|
+
context["directives"] = _directives
|
|
137
|
+
self._logger.debug(f"Directives rendered as {directives}")
|
|
138
|
+
|
|
139
|
+
# Add the job script to the context
|
|
140
|
+
context["job_script"] = _job_script
|
|
141
|
+
|
|
142
|
+
# Assemble the submit command
|
|
143
|
+
cmd = self.cmd_templates["submit"].format(**context)
|
|
144
|
+
self._logger.debug("Command rendered as:")
|
|
145
|
+
self._logger.debug(cmd)
|
|
146
|
+
|
|
147
|
+
# Just return the command string for the user without submitting
|
|
148
|
+
if dry_run:
|
|
149
|
+
self._logger.debug("dry_run requested, returning submission command.")
|
|
150
|
+
return cmd
|
|
151
|
+
|
|
152
|
+
# Attach the user environment to the call
|
|
153
|
+
self._logger.debug("Updating user environment.")
|
|
154
|
+
_env = os.environ
|
|
155
|
+
_env.update(env)
|
|
156
|
+
|
|
157
|
+
# Submit
|
|
158
|
+
self._logger.debug("Submitting to the shell.")
|
|
159
|
+
result = self._shell(cmd, env=_env)
|
|
160
|
+
|
|
161
|
+
# Job ID is the last part of the returned string for either client
|
|
162
|
+
job_id = result.split()[-1]
|
|
163
|
+
|
|
164
|
+
# Return the job object, but switch off auto_update so it doesn't attempt to get status
|
|
165
|
+
return Job(job_id, client=None, auto_update=False)
|
|
166
|
+
|
|
167
|
+
def status(self, job_id):
|
|
168
|
+
"""Check the status of a job.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
job_id : str
|
|
173
|
+
Job ID.
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
str
|
|
178
|
+
Command response text.
|
|
179
|
+
"""
|
|
180
|
+
cmd = self.cmd_templates["status"].format(job_id=job_id)
|
|
181
|
+
result = self._shell(cmd)
|
|
182
|
+
return result
|
|
183
|
+
|
|
184
|
+
def delete(self, job_id):
|
|
185
|
+
"""Delete/cancel a job.
|
|
186
|
+
|
|
187
|
+
Parameters
|
|
188
|
+
----------
|
|
189
|
+
job_id : str
|
|
190
|
+
Job ID.
|
|
191
|
+
|
|
192
|
+
Returns
|
|
193
|
+
-------
|
|
194
|
+
str
|
|
195
|
+
Command response text.
|
|
196
|
+
"""
|
|
197
|
+
cmd = self.cmd_templates["delete"].format(job_id=job_id)
|
|
198
|
+
result = self._shell(cmd)
|
|
199
|
+
return result
|
|
200
|
+
|
|
201
|
+
def is_queued(self, job_id):
|
|
202
|
+
"""Check if the job is queued.
|
|
203
|
+
|
|
204
|
+
Parameters
|
|
205
|
+
----------
|
|
206
|
+
job_id : str
|
|
207
|
+
Job ID.
|
|
208
|
+
|
|
209
|
+
Returns
|
|
210
|
+
-------
|
|
211
|
+
bool
|
|
212
|
+
True if queued, False otherwise.
|
|
213
|
+
"""
|
|
214
|
+
return self.status(job_id) == hc.STATUS_QUEUED
|
|
215
|
+
|
|
216
|
+
def is_running(self, job_id):
|
|
217
|
+
"""Check if the job is running.
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
job_id : str
|
|
222
|
+
Job ID.
|
|
223
|
+
|
|
224
|
+
Returns
|
|
225
|
+
-------
|
|
226
|
+
bool
|
|
227
|
+
True if running, False otherwise.
|
|
228
|
+
"""
|
|
229
|
+
return self.status(job_id) == hc.STATUS_RUNNING
|
|
230
|
+
|
|
231
|
+
def _shell(self, cmd, decode=True, env=None):
|
|
232
|
+
"""Run the shell utility for the given command.
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
cmd : str
|
|
237
|
+
Command to run.
|
|
238
|
+
decode : bool
|
|
239
|
+
Automatically decode response with utf-8, defaults to True
|
|
240
|
+
env : dict, optional
|
|
241
|
+
Add environment variables to the command.
|
|
242
|
+
|
|
243
|
+
Raises
|
|
244
|
+
------
|
|
245
|
+
hpcpy.exceptions.ShellException :
|
|
246
|
+
When the underlying shell call fails.
|
|
247
|
+
|
|
248
|
+
Returns
|
|
249
|
+
-------
|
|
250
|
+
str
|
|
251
|
+
Result from the underlying called command.
|
|
252
|
+
"""
|
|
253
|
+
result = shell(cmd, env=env)
|
|
254
|
+
|
|
255
|
+
if decode:
|
|
256
|
+
result = result.stdout.decode("utf8").strip()
|
|
257
|
+
|
|
258
|
+
return result
|
|
259
|
+
|
|
260
|
+
def _get_job_script_filename(self, filepath, hash_length=8) -> str:
|
|
261
|
+
"""Generate a script filename with a random suffix.
|
|
262
|
+
|
|
263
|
+
Parameters
|
|
264
|
+
----------
|
|
265
|
+
filepath : str
|
|
266
|
+
Path to the file.
|
|
267
|
+
hash_length : int, optional
|
|
268
|
+
Length of the random hash, by default 8
|
|
269
|
+
|
|
270
|
+
Returns
|
|
271
|
+
-------
|
|
272
|
+
str
|
|
273
|
+
Hash-suffixed filename.
|
|
274
|
+
"""
|
|
275
|
+
filename, ext = os.path.splitext(filepath)
|
|
276
|
+
filename = os.path.basename(filename)
|
|
277
|
+
_hash = "".join(choice(ascii_uppercase) for i in range(hash_length))
|
|
278
|
+
return f"{filename}_{_hash}{ext}"
|
|
279
|
+
|
|
280
|
+
def _render_job_script(self, template, **context):
|
|
281
|
+
"""Render a job script.
|
|
282
|
+
|
|
283
|
+
Parameters
|
|
284
|
+
----------
|
|
285
|
+
template : str, path-like.
|
|
286
|
+
Path to the template.
|
|
287
|
+
**context :
|
|
288
|
+
Key/value pairs to be interpolated into the script.
|
|
289
|
+
|
|
290
|
+
Returns
|
|
291
|
+
-------
|
|
292
|
+
str
|
|
293
|
+
Path to the rendered job script.
|
|
294
|
+
"""
|
|
295
|
+
# Render the template
|
|
296
|
+
_rendered = interpolate_file_template(template, **context)
|
|
297
|
+
|
|
298
|
+
# Generate the output filepath
|
|
299
|
+
os.makedirs(hc.JOB_SCRIPT_DIR, exist_ok=True)
|
|
300
|
+
output_filename = self._get_job_script_filename(template)
|
|
301
|
+
output_filepath = hc.JOB_SCRIPT_DIR / output_filename
|
|
302
|
+
|
|
303
|
+
# Write it out
|
|
304
|
+
with open(output_filepath, "w") as fo:
|
|
305
|
+
fo.write(_rendered)
|
|
306
|
+
|
|
307
|
+
return output_filepath
|
|
308
|
+
|
|
309
|
+
def _render_directives(self, directives):
|
|
310
|
+
"""Render the directives into a single string for command interpolation.
|
|
311
|
+
|
|
312
|
+
Parameters
|
|
313
|
+
----------
|
|
314
|
+
directives : list
|
|
315
|
+
List of scheduler-compliant directives. One per item.
|
|
316
|
+
|
|
317
|
+
Returns
|
|
318
|
+
-------
|
|
319
|
+
str
|
|
320
|
+
Rendered directives, or blank string.
|
|
321
|
+
"""
|
|
322
|
+
# Render blank directives if empty
|
|
323
|
+
if not directives:
|
|
324
|
+
return ""
|
|
325
|
+
|
|
326
|
+
return " " + " ".join(directives)
|
|
327
|
+
|
|
328
|
+
def hold(self, job_id):
|
|
329
|
+
"""Hold a job.
|
|
330
|
+
|
|
331
|
+
Parameters
|
|
332
|
+
----------
|
|
333
|
+
job_id : str
|
|
334
|
+
Job ID.
|
|
335
|
+
|
|
336
|
+
Returns
|
|
337
|
+
-------
|
|
338
|
+
str
|
|
339
|
+
Command response text.
|
|
340
|
+
"""
|
|
341
|
+
cmd = self.cmd_templates["hold"].format(job_id=job_id)
|
|
342
|
+
result = self._shell(cmd)
|
|
343
|
+
return result
|
|
344
|
+
|
|
345
|
+
def release(self, job_id):
|
|
346
|
+
"""Release a job.
|
|
347
|
+
|
|
348
|
+
Parameters
|
|
349
|
+
----------
|
|
350
|
+
job_id : str
|
|
351
|
+
Job ID.
|
|
352
|
+
|
|
353
|
+
Returns
|
|
354
|
+
-------
|
|
355
|
+
str
|
|
356
|
+
Command response text.
|
|
357
|
+
"""
|
|
358
|
+
cmd = self.cmd_templates["release"].format(job_id=job_id)
|
|
359
|
+
result = self._shell(cmd)
|
|
360
|
+
return result
|
|
361
|
+
|
|
362
|
+
def _assemble_delay_directive(self, delay, delay_directive_fmt) -> str:
|
|
363
|
+
"""Assemble the delay directive for the scheduler.
|
|
364
|
+
|
|
365
|
+
Parameters
|
|
366
|
+
----------
|
|
367
|
+
delay : datetime or timedelta
|
|
368
|
+
Either a specific datetime or delta from now.
|
|
369
|
+
delay_directive_fmt : str
|
|
370
|
+
Delay directive format.
|
|
371
|
+
|
|
372
|
+
Returns
|
|
373
|
+
-------
|
|
374
|
+
str
|
|
375
|
+
Formatted delay directive for the current scheduler.
|
|
376
|
+
|
|
377
|
+
Raises
|
|
378
|
+
------
|
|
379
|
+
ValueError
|
|
380
|
+
When the delay argument is incorrect or puts the job in the past.
|
|
381
|
+
"""
|
|
382
|
+
current_time = datetime.now()
|
|
383
|
+
delay_str = None
|
|
384
|
+
|
|
385
|
+
if isinstance(delay, datetime) and delay > current_time:
|
|
386
|
+
delay_str = delay.strftime(delay_directive_fmt)
|
|
387
|
+
|
|
388
|
+
elif isinstance(delay, timedelta) and (current_time + delay) > current_time:
|
|
389
|
+
delay_str = (current_time + delay).strftime(delay_directive_fmt)
|
|
390
|
+
else:
|
|
391
|
+
raise ValueError(
|
|
392
|
+
"Job submission delay argument either incorrect or puts the job in the past."
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
return self.directive_templates["delay"].format(delay_str=delay_str)
|
|
396
|
+
|
|
397
|
+
def _interpolate_directive(self, directives, key, **kwargs):
|
|
398
|
+
"""Interpolate a directive into the keyed template and add to the list.
|
|
399
|
+
|
|
400
|
+
Parameters
|
|
401
|
+
----------
|
|
402
|
+
directives : list
|
|
403
|
+
Directives
|
|
404
|
+
key : str
|
|
405
|
+
Key to the template to interpolate into.
|
|
406
|
+
**kwargs :
|
|
407
|
+
Key/value pairs to use in interpolation.
|
|
408
|
+
|
|
409
|
+
Returns
|
|
410
|
+
-------
|
|
411
|
+
list
|
|
412
|
+
Updated directives list.
|
|
413
|
+
"""
|
|
414
|
+
directives.append(self.directive_templates[key].format(**kwargs))
|
|
415
|
+
return directives
|
|
416
|
+
|
|
417
|
+
def _normalise_depends_on(self, jobs: Union[str, Job, list]) -> list:
|
|
418
|
+
"""Normalise the jobs supplied by a depends_on argument into strings.
|
|
419
|
+
|
|
420
|
+
Parameters
|
|
421
|
+
----------
|
|
422
|
+
jobs : Union[str, Job, list]
|
|
423
|
+
Job ID, Job object or a list containing either.
|
|
424
|
+
|
|
425
|
+
Returns
|
|
426
|
+
-------
|
|
427
|
+
list
|
|
428
|
+
List of Job IDs
|
|
429
|
+
|
|
430
|
+
Raises
|
|
431
|
+
------
|
|
432
|
+
TypeError
|
|
433
|
+
When any of the objects is neither a str or a Job object.
|
|
434
|
+
"""
|
|
435
|
+
normalised = list()
|
|
436
|
+
for ix, _job in enumerate(ensure_list(jobs)):
|
|
437
|
+
|
|
438
|
+
if isinstance(_job, str):
|
|
439
|
+
normalised.append(_job)
|
|
440
|
+
elif isinstance(_job, Job):
|
|
441
|
+
normalised.append(_job.id)
|
|
442
|
+
else:
|
|
443
|
+
raise TypeError(f"Object at index {ix} is neither a str or Job object.")
|
|
444
|
+
|
|
445
|
+
return normalised
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Client Factory."""
|
|
2
|
+
|
|
3
|
+
from hpcpy.client.direct import DirectClient
|
|
4
|
+
from hpcpy.client.pbs import PBSClient
|
|
5
|
+
from hpcpy.client.slurm import SlurmClient
|
|
6
|
+
from hpcpy.utilities import shell
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ClientFactory:
|
|
11
|
+
"""An object that agnostically selects a scheduler."""
|
|
12
|
+
|
|
13
|
+
def get_client(
|
|
14
|
+
self, *args, **kwargs
|
|
15
|
+
) -> Union[PBSClient, SlurmClient, DirectClient]:
|
|
16
|
+
"""Get a client object based on what kind of scheduler we are using.
|
|
17
|
+
|
|
18
|
+
Falls back to DirectClient when no scheduler can be detected.
|
|
19
|
+
|
|
20
|
+
Arguments:
|
|
21
|
+
----------
|
|
22
|
+
**kwargs
|
|
23
|
+
Arguments for the specific client.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
Union[PBSClient, SlurmClient, DirectClient]
|
|
28
|
+
Client object suitable for the detected scheduler.
|
|
29
|
+
"""
|
|
30
|
+
clients = dict(qsub=PBSClient, sbatch=SlurmClient)
|
|
31
|
+
|
|
32
|
+
# Loop through the clients in order, looking for a valid scheduler
|
|
33
|
+
for cmd, client in clients.items():
|
|
34
|
+
if shell(f"which {cmd}", check=False).returncode == 0:
|
|
35
|
+
return client(*args, **kwargs)
|
|
36
|
+
|
|
37
|
+
# No scheduler found; fall back to direct execution
|
|
38
|
+
return DirectClient(*args, **kwargs)
|