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/client/direct.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Direct execution client (no scheduler)."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import shlex
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Union
|
|
9
|
+
|
|
10
|
+
from hpcpy.client.base import BaseClient
|
|
11
|
+
from hpcpy.constants.direct import COMMANDS, DIRECTIVES, STATUSES
|
|
12
|
+
from hpcpy.utilities import shell
|
|
13
|
+
from hpcpy.job import Job
|
|
14
|
+
import hpcpy.constants as hc
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DirectClient(BaseClient):
|
|
18
|
+
"""Direct execution interface for systems without a scheduler.
|
|
19
|
+
|
|
20
|
+
Runs scripts directly as local processes, using the process ID as the
|
|
21
|
+
job ID. Status is tracked via ps(1).
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
*args
|
|
26
|
+
Positional arguments forwarded to the base class.
|
|
27
|
+
**kwargs
|
|
28
|
+
Keyword arguments forwarded to the base class.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, *args, **kwargs):
|
|
32
|
+
super().__init__(
|
|
33
|
+
cmd_templates=COMMANDS,
|
|
34
|
+
directive_templates=DIRECTIVES,
|
|
35
|
+
statuses=STATUSES,
|
|
36
|
+
status_attribute="short",
|
|
37
|
+
*args,
|
|
38
|
+
**kwargs,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def status(self, job_id):
|
|
42
|
+
"""Get the status of a directly-executed process.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
job_id : str
|
|
47
|
+
Process ID.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
tuple
|
|
52
|
+
Generic status code and native status dictionary.
|
|
53
|
+
"""
|
|
54
|
+
cmd = self.cmd_templates["status"].format(job_id=job_id)
|
|
55
|
+
|
|
56
|
+
# ps returns non-zero when the process does not exist, so use check=False
|
|
57
|
+
result = shell(cmd, check=False)
|
|
58
|
+
raw = result.stdout.decode("utf8").strip()
|
|
59
|
+
|
|
60
|
+
return self._parse_status(raw, job_id)
|
|
61
|
+
|
|
62
|
+
def submit(
|
|
63
|
+
self,
|
|
64
|
+
job_script: Union[str, Path],
|
|
65
|
+
directives: list = None,
|
|
66
|
+
render: bool = False,
|
|
67
|
+
dry_run: bool = False,
|
|
68
|
+
variables: dict = dict(),
|
|
69
|
+
**context,
|
|
70
|
+
):
|
|
71
|
+
"""Submit a job by running it directly as a local process.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
job_script : Union[str, Path]
|
|
76
|
+
Path to the script to execute.
|
|
77
|
+
directives : list, optional
|
|
78
|
+
Ignored for direct execution.
|
|
79
|
+
render : bool, optional
|
|
80
|
+
Render the job script from a template, by default False
|
|
81
|
+
dry_run : bool, optional
|
|
82
|
+
Return the command rather than executing it, by default False
|
|
83
|
+
variables : dict, optional
|
|
84
|
+
Environment variables to set for the process.
|
|
85
|
+
**context :
|
|
86
|
+
Additional key/value pairs for job script rendering.
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
Union[Job, str]
|
|
91
|
+
Submitted job object, or the command string if dry_run=True.
|
|
92
|
+
"""
|
|
93
|
+
# Sanitize the variables, if there are any, then create string version and add to context
|
|
94
|
+
variables_sanitized = self._sanitize_dict(variables)
|
|
95
|
+
variables_str = []
|
|
96
|
+
for key, value in variables_sanitized.items():
|
|
97
|
+
variables_str.append(f"{key}={value}")
|
|
98
|
+
|
|
99
|
+
context["variables_str"] = (
|
|
100
|
+
" ".join(variables_str) + " " if variables_str else ""
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Delegate command assembly (directive rendering, job script rendering) to the parent
|
|
104
|
+
cmd = super().submit(
|
|
105
|
+
job_script=job_script,
|
|
106
|
+
directives=directives if isinstance(directives, list) else [],
|
|
107
|
+
render=render,
|
|
108
|
+
dry_run=True,
|
|
109
|
+
variables=self._sanitize_dict(variables),
|
|
110
|
+
**context,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if dry_run:
|
|
114
|
+
return cmd
|
|
115
|
+
|
|
116
|
+
_env = os.environ.copy()
|
|
117
|
+
if isinstance(variables_sanitized, dict) and variables_sanitized:
|
|
118
|
+
_env.update({k: str(v) for k, v in variables_sanitized.items()})
|
|
119
|
+
|
|
120
|
+
proc = subprocess.Popen(shlex.split(cmd), env=_env)
|
|
121
|
+
job_id = str(proc.pid)
|
|
122
|
+
|
|
123
|
+
job = Job(job_id, client=None, auto_update=False)
|
|
124
|
+
job.set_client(self)
|
|
125
|
+
job._auto_update = True
|
|
126
|
+
|
|
127
|
+
return job
|
|
128
|
+
|
|
129
|
+
def _parse_status(self, raw, job_id):
|
|
130
|
+
"""Extract the status from the raw ps output.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
raw : str
|
|
135
|
+
Raw output from ps(1). Empty string means the process has finished.
|
|
136
|
+
job_id : str
|
|
137
|
+
Process ID.
|
|
138
|
+
|
|
139
|
+
Returns
|
|
140
|
+
-------
|
|
141
|
+
tuple
|
|
142
|
+
Generic status code and native status dictionary.
|
|
143
|
+
"""
|
|
144
|
+
# Empty output means the process no longer exists
|
|
145
|
+
if not raw:
|
|
146
|
+
return hc.STATUS_FINISHED, {"pid": job_id, "state": "FINISHED"}
|
|
147
|
+
|
|
148
|
+
# ps -o stat= returns one or more characters; the first is the main state
|
|
149
|
+
state_char = raw[0]
|
|
150
|
+
generic_status = None
|
|
151
|
+
|
|
152
|
+
for s in self.statuses:
|
|
153
|
+
if state_char == s.short:
|
|
154
|
+
generic_status = s.status
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
return generic_status, {"pid": job_id, "state": raw}
|
|
158
|
+
|
|
159
|
+
def _sanitize_dict(self, user_dict: dict) -> dict:
|
|
160
|
+
"""Sanitize user-supplied dict for use with subprocess.run.
|
|
161
|
+
|
|
162
|
+
Parameters
|
|
163
|
+
----------
|
|
164
|
+
user_dict : dict
|
|
165
|
+
Key/value pairs.
|
|
166
|
+
|
|
167
|
+
Returns
|
|
168
|
+
-------
|
|
169
|
+
dict
|
|
170
|
+
Sanitized version of the dict.
|
|
171
|
+
|
|
172
|
+
Raises
|
|
173
|
+
------
|
|
174
|
+
ValueError
|
|
175
|
+
When a key is invalid.
|
|
176
|
+
When a value is invalid.
|
|
177
|
+
When a null byte is provided.
|
|
178
|
+
"""
|
|
179
|
+
VALID_KEY = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
180
|
+
sanitized = dict()
|
|
181
|
+
|
|
182
|
+
for key, value in user_dict.items():
|
|
183
|
+
|
|
184
|
+
# Validate key
|
|
185
|
+
if not isinstance(key, str):
|
|
186
|
+
raise ValueError(f"Key {key!r} must be a string")
|
|
187
|
+
|
|
188
|
+
if not VALID_KEY.match(key):
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"Key {key!r} is invalid: must start with a letter or underscore "
|
|
191
|
+
f"and contain only alphanumeric characters and underscores"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Validate and sanitize value
|
|
195
|
+
if not isinstance(value, (str, int, float, bool)):
|
|
196
|
+
raise ValueError(
|
|
197
|
+
f"Value for key {key!r} must be a string, int, float, or bool; "
|
|
198
|
+
f"got {type(value).__name__}"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
str_value = str(value)
|
|
202
|
+
|
|
203
|
+
# Reject null bytes — they silently truncate strings in most shells
|
|
204
|
+
if "\x00" in str_value:
|
|
205
|
+
raise ValueError(f"Value for key {key!r} contains a null byte")
|
|
206
|
+
|
|
207
|
+
# Add to dict and quote for safety
|
|
208
|
+
sanitized[key] = shlex.quote(str_value)
|
|
209
|
+
|
|
210
|
+
return sanitized
|
hpcpy/client/pbs.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""PBS implementation."""
|
|
2
|
+
|
|
3
|
+
from hpcpy.client.base import BaseClient
|
|
4
|
+
from hpcpy.constants.pbs import COMMANDS, DIRECTIVES, STATUSES, DELAY_DIRECTIVE_FMT
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from typing import Union
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from shlex import quote
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PBSClient(BaseClient):
|
|
13
|
+
"""PBS interface.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
*args
|
|
18
|
+
Positional arguments forwarded to the base class.
|
|
19
|
+
**kwargs
|
|
20
|
+
Keyword arguments forwarded to the base class.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, *args, **kwargs):
|
|
24
|
+
# Set up the templates
|
|
25
|
+
super().__init__(
|
|
26
|
+
cmd_templates=COMMANDS,
|
|
27
|
+
directive_templates=DIRECTIVES,
|
|
28
|
+
statuses=STATUSES,
|
|
29
|
+
status_attribute="short",
|
|
30
|
+
*args,
|
|
31
|
+
**kwargs,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def status(self, job_id):
|
|
35
|
+
"""Get the status of a job.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
job_id : str
|
|
40
|
+
Job ID.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
str
|
|
45
|
+
Generic status code.
|
|
46
|
+
"""
|
|
47
|
+
# Get the raw response
|
|
48
|
+
raw = super().status(job_id=job_id)
|
|
49
|
+
|
|
50
|
+
# Parse the status as per this implementation
|
|
51
|
+
generic_status, native_full = self._parse_status(raw, job_id)
|
|
52
|
+
|
|
53
|
+
# Return the generic status
|
|
54
|
+
return generic_status, native_full
|
|
55
|
+
|
|
56
|
+
def _render_variables(self, variables):
|
|
57
|
+
"""Render the variables flag for PBS.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
variables : dict
|
|
62
|
+
Dictionary of variables
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
str
|
|
67
|
+
String formatted variables for PBS
|
|
68
|
+
"""
|
|
69
|
+
formatted = list()
|
|
70
|
+
|
|
71
|
+
for k, v in variables.items():
|
|
72
|
+
|
|
73
|
+
# Fix for broken quotes #52
|
|
74
|
+
if isinstance(v, str) and " " in v:
|
|
75
|
+
v = quote(v)
|
|
76
|
+
line = f'"{k}={v}"'
|
|
77
|
+
formatted.append(line)
|
|
78
|
+
else:
|
|
79
|
+
formatted.append(f"{k}={v}")
|
|
80
|
+
|
|
81
|
+
formatted = ",".join(formatted)
|
|
82
|
+
return f"-v {formatted}"
|
|
83
|
+
|
|
84
|
+
def submit(
|
|
85
|
+
self,
|
|
86
|
+
job_script: Union[str, Path],
|
|
87
|
+
directives: list = None,
|
|
88
|
+
render: bool = False,
|
|
89
|
+
dry_run: bool = False,
|
|
90
|
+
depends_on: list = None,
|
|
91
|
+
delay: Union[datetime, timedelta] = None,
|
|
92
|
+
queue: str = None,
|
|
93
|
+
walltime: timedelta = None,
|
|
94
|
+
storage: list = None,
|
|
95
|
+
variables: dict = None,
|
|
96
|
+
**context,
|
|
97
|
+
):
|
|
98
|
+
"""Submit a job to the scheduler.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
job_script : Union[str, Path]
|
|
103
|
+
Path to the script.
|
|
104
|
+
directives : list, optional
|
|
105
|
+
List of complete directives to submit, by default list()
|
|
106
|
+
render : bool, optional
|
|
107
|
+
Render the job script from a template, by default False
|
|
108
|
+
dry_run : bool, optional
|
|
109
|
+
Return rather than executing the command, by default False
|
|
110
|
+
depends_on : list, optional
|
|
111
|
+
List of job IDs with successful exit on which this job depends, by default list()
|
|
112
|
+
delay: Union[datetime, timedelta]
|
|
113
|
+
Delay the start of this job until specific date or interval, by default None
|
|
114
|
+
queue: str, optional
|
|
115
|
+
Queue on which to submit the job, by default None
|
|
116
|
+
walltime: timedelta, optional
|
|
117
|
+
Walltime expressed as a timedelta, by default None
|
|
118
|
+
storage: list, optional
|
|
119
|
+
List of storage mounts to apply, by default None
|
|
120
|
+
variables: dict, optional
|
|
121
|
+
Key/value environment variable pairs added to the qsub command.
|
|
122
|
+
**context:
|
|
123
|
+
Additional key/value pairs to be added to command/jobscript interpolation
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
Job : hpcpy.job.Job
|
|
128
|
+
Job object.
|
|
129
|
+
"""
|
|
130
|
+
# Initialise the directives to an empty list
|
|
131
|
+
directives = directives if isinstance(directives, list) else []
|
|
132
|
+
|
|
133
|
+
# Add job depends
|
|
134
|
+
if depends_on:
|
|
135
|
+
|
|
136
|
+
# Normalise to a list of strs
|
|
137
|
+
depends_on = super()._normalise_depends_on(depends_on)
|
|
138
|
+
|
|
139
|
+
directives = self._interpolate_directive(
|
|
140
|
+
directives,
|
|
141
|
+
"depends_on",
|
|
142
|
+
depends_on_str=":".join(depends_on),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Add delay (specified time or delta)
|
|
146
|
+
if delay:
|
|
147
|
+
delay_directive = self._assemble_delay_directive(delay, DELAY_DIRECTIVE_FMT)
|
|
148
|
+
directives.append(delay_directive)
|
|
149
|
+
|
|
150
|
+
# Add queue
|
|
151
|
+
if queue:
|
|
152
|
+
directives = self._interpolate_directive(directives, "queue", queue=queue)
|
|
153
|
+
context["queue"] = queue
|
|
154
|
+
|
|
155
|
+
# Add walltime
|
|
156
|
+
if walltime:
|
|
157
|
+
_walltime = str(walltime)
|
|
158
|
+
directives.append(f"-l walltime={_walltime}")
|
|
159
|
+
context["walltime"] = _walltime
|
|
160
|
+
|
|
161
|
+
# Add storage
|
|
162
|
+
if storage:
|
|
163
|
+
storage_str = "+".join(storage)
|
|
164
|
+
directives.append(f"-l storage={storage_str}")
|
|
165
|
+
context["storage"] = storage
|
|
166
|
+
context["storage_str"] = storage_str
|
|
167
|
+
|
|
168
|
+
# Add variables
|
|
169
|
+
if isinstance(variables, dict) and len(variables) > 0:
|
|
170
|
+
directives.append(self._render_variables(variables))
|
|
171
|
+
|
|
172
|
+
# Call the super
|
|
173
|
+
job_or_cmd = super().submit(
|
|
174
|
+
job_script=job_script,
|
|
175
|
+
directives=directives,
|
|
176
|
+
render=render,
|
|
177
|
+
dry_run=dry_run,
|
|
178
|
+
**context,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Return the command if requested
|
|
182
|
+
if dry_run:
|
|
183
|
+
return job_or_cmd
|
|
184
|
+
|
|
185
|
+
# Point to this client from the job and switch on auto_update
|
|
186
|
+
job_or_cmd.set_client(self)
|
|
187
|
+
job_or_cmd._auto_update = True
|
|
188
|
+
|
|
189
|
+
# Return the job object
|
|
190
|
+
return job_or_cmd
|
|
191
|
+
|
|
192
|
+
def _parse_status(self, raw, job_id):
|
|
193
|
+
"""Extract the status from the raw response.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
raw : str
|
|
198
|
+
Raw response from the scheduler.
|
|
199
|
+
job_id : str
|
|
200
|
+
Job ID required to extract status from the response.
|
|
201
|
+
|
|
202
|
+
Returns
|
|
203
|
+
-------
|
|
204
|
+
str
|
|
205
|
+
Status code.
|
|
206
|
+
"""
|
|
207
|
+
# Convert to JSON
|
|
208
|
+
parsed = json.loads(raw)
|
|
209
|
+
|
|
210
|
+
# Get the status out of the job ID
|
|
211
|
+
native_full = parsed.get("Jobs").get(job_id)
|
|
212
|
+
native_status = native_full.get("job_state")
|
|
213
|
+
|
|
214
|
+
# Set the generic status attribute
|
|
215
|
+
generic_status = None
|
|
216
|
+
|
|
217
|
+
for s in self.statuses:
|
|
218
|
+
if native_status == s.short:
|
|
219
|
+
generic_status = s.status
|
|
220
|
+
break
|
|
221
|
+
|
|
222
|
+
# Return the generic status
|
|
223
|
+
return generic_status, native_full
|
hpcpy/client/slurm.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""SLURM Client."""
|
|
2
|
+
|
|
3
|
+
from hpcpy.client.base import BaseClient
|
|
4
|
+
from hpcpy.constants.slurm import COMMANDS, STATUSES, DIRECTIVES, DELAY_DIRECTIVE_FMT
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from typing import Union
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SlurmClient(BaseClient):
|
|
13
|
+
"""SLURM interface.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
*args
|
|
18
|
+
Positional arguments forwarded to the base class.
|
|
19
|
+
**kwargs
|
|
20
|
+
Keyword arguments forwarded to the base class.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, *args, **kwargs):
|
|
24
|
+
# Set up the templates
|
|
25
|
+
super().__init__(
|
|
26
|
+
cmd_templates=COMMANDS,
|
|
27
|
+
directive_templates=DIRECTIVES,
|
|
28
|
+
statuses=STATUSES,
|
|
29
|
+
status_attribute="short",
|
|
30
|
+
*args,
|
|
31
|
+
**kwargs,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def status(self, job_id):
|
|
35
|
+
"""Get the status of a job.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
job_id : str
|
|
40
|
+
Job ID.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
str
|
|
45
|
+
Generic status code.
|
|
46
|
+
"""
|
|
47
|
+
# Get the raw response
|
|
48
|
+
raw = super().status(job_id=job_id)
|
|
49
|
+
|
|
50
|
+
# Parse the status as per this implementation
|
|
51
|
+
generic_status, native_full = self._parse_status(raw, job_id)
|
|
52
|
+
|
|
53
|
+
# Return the generic status
|
|
54
|
+
return generic_status, native_full
|
|
55
|
+
|
|
56
|
+
def submit(
|
|
57
|
+
self,
|
|
58
|
+
job_script: Union[str, Path],
|
|
59
|
+
directives: list = None,
|
|
60
|
+
render: bool = False,
|
|
61
|
+
dry_run: bool = False,
|
|
62
|
+
depends_on: list = None,
|
|
63
|
+
delay: Union[datetime, timedelta] = None,
|
|
64
|
+
queue: str = None,
|
|
65
|
+
walltime: timedelta = None,
|
|
66
|
+
variables: dict = dict(),
|
|
67
|
+
**context,
|
|
68
|
+
):
|
|
69
|
+
"""Submit a job to the scheduler.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
job_script : Union[str, Path]
|
|
74
|
+
Path to the script.
|
|
75
|
+
directives : list, optional
|
|
76
|
+
List of complete directives to submit, by default list()
|
|
77
|
+
render : bool, optional
|
|
78
|
+
Render the job script from a template, by default False
|
|
79
|
+
dry_run : bool, optional
|
|
80
|
+
Return rather than executing the command, by default False
|
|
81
|
+
depends_on : list, optional
|
|
82
|
+
List of job IDs with successful exit on which this job depends, by default list()
|
|
83
|
+
delay: Union[datetime, timedelta]
|
|
84
|
+
Delay the start of this job until specific date or interval, by default None
|
|
85
|
+
queue: str, optional
|
|
86
|
+
Queue on which to submit the job, by default None
|
|
87
|
+
walltime: timedelta, optional
|
|
88
|
+
Walltime expressed as a timedelta, by default None
|
|
89
|
+
variables: dict, optional
|
|
90
|
+
Key/value environment variable pairs added to the qsub command.
|
|
91
|
+
**context:
|
|
92
|
+
Additional key/value pairs to be added to command/jobscript interpolation
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
Job : hpcpy.job.Job
|
|
97
|
+
Job object.
|
|
98
|
+
"""
|
|
99
|
+
# Get a logger from the parent
|
|
100
|
+
self._logger.debug(f"Submitting {job_script}")
|
|
101
|
+
|
|
102
|
+
# Initialise directives
|
|
103
|
+
directives = directives if isinstance(directives, list) else []
|
|
104
|
+
|
|
105
|
+
# Add job depends
|
|
106
|
+
if depends_on:
|
|
107
|
+
|
|
108
|
+
self._logger.debug("Job dependency specified.")
|
|
109
|
+
|
|
110
|
+
# Normalise depends_on to strs
|
|
111
|
+
depends_on = super()._normalise_depends_on(depends_on)
|
|
112
|
+
|
|
113
|
+
directives = self._interpolate_directive(
|
|
114
|
+
directives,
|
|
115
|
+
"depends_on",
|
|
116
|
+
depends_on_str=":".join(depends_on),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Add delay (specified time or delta)
|
|
120
|
+
if delay:
|
|
121
|
+
self._logger.debug("Delay specified.")
|
|
122
|
+
delay_directive = self._assemble_delay_directive(delay, DELAY_DIRECTIVE_FMT)
|
|
123
|
+
directives.append(delay_directive)
|
|
124
|
+
|
|
125
|
+
# Add queue
|
|
126
|
+
if queue:
|
|
127
|
+
self._logger.debug(f"Queue specified ({queue})")
|
|
128
|
+
directives = self._interpolate_directive(directives, "queue", queue=queue)
|
|
129
|
+
context["queue"] = queue
|
|
130
|
+
|
|
131
|
+
# Add walltime
|
|
132
|
+
if walltime:
|
|
133
|
+
walltime_str = str(int(walltime.total_seconds() / 60.0))
|
|
134
|
+
self._logger.debug(f"Walltime specified ({walltime_str})")
|
|
135
|
+
directives = self._interpolate_directive(
|
|
136
|
+
directives, "walltime", walltime_str=walltime_str
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Update the environment with the variables (which is how Slurm does job variables)
|
|
140
|
+
self._logger.debug("Updating environment.")
|
|
141
|
+
_env = os.environ.copy()
|
|
142
|
+
_env.update(variables)
|
|
143
|
+
|
|
144
|
+
# Call the super submit
|
|
145
|
+
self._logger.debug("Submitting via parent class.")
|
|
146
|
+
job_or_cmd = super().submit(
|
|
147
|
+
job_script=job_script,
|
|
148
|
+
directives=directives,
|
|
149
|
+
render=render,
|
|
150
|
+
dry_run=dry_run,
|
|
151
|
+
env=_env,
|
|
152
|
+
**context,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Return the command from the super
|
|
156
|
+
if dry_run:
|
|
157
|
+
self._logger.debug("Dry run requested, returning fully-formed command.")
|
|
158
|
+
return job_or_cmd
|
|
159
|
+
|
|
160
|
+
# Point to this client in the job object and switch on auto_update
|
|
161
|
+
job_or_cmd.set_client(self)
|
|
162
|
+
job_or_cmd._auto_update = True
|
|
163
|
+
|
|
164
|
+
# Get the job ID out of the return string
|
|
165
|
+
self._logger.debug(f"job_id={job_or_cmd.id}")
|
|
166
|
+
|
|
167
|
+
# Return the job object
|
|
168
|
+
return job_or_cmd
|
|
169
|
+
|
|
170
|
+
def _parse_status(self, raw, job_id):
|
|
171
|
+
"""Extract the statue from the raw response.
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
raw : str
|
|
176
|
+
Raw response from the scheduler.
|
|
177
|
+
job_id : str
|
|
178
|
+
Job ID.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
generic_status : str
|
|
183
|
+
Generic status code.
|
|
184
|
+
native_full : dict
|
|
185
|
+
Full native status dictionary.
|
|
186
|
+
"""
|
|
187
|
+
# Parse the response
|
|
188
|
+
parsed = json.loads(raw)
|
|
189
|
+
|
|
190
|
+
# Get the status from the first job
|
|
191
|
+
native_full = parsed.get("jobs")[0]
|
|
192
|
+
native_status = native_full.get("job_state")[0]
|
|
193
|
+
|
|
194
|
+
# Set the default generic status to None
|
|
195
|
+
generic_status = None
|
|
196
|
+
|
|
197
|
+
# Look up the generic status
|
|
198
|
+
for s in self.statuses:
|
|
199
|
+
if native_status == s.long:
|
|
200
|
+
generic_status = s.status
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
return generic_status, native_full
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Common constants."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
# Location for rendered job scripts
|
|
6
|
+
JOB_SCRIPT_DIR = Path.home() / ".hpcpy" / "job_scripts"
|
|
7
|
+
JOB_SCRIPT_DIR.mkdir(parents=True, exist_ok=True)
|
|
8
|
+
|
|
9
|
+
# Generic Statuses (actually just PBS statuses, but used as a generic status)
|
|
10
|
+
STATUS_CYCLE_HARVESTING = "U"
|
|
11
|
+
STATUS_EXITING = "E"
|
|
12
|
+
STATUS_FINISHED = "F"
|
|
13
|
+
STATUS_HAS_SUBJOB = "B"
|
|
14
|
+
STATUS_HELD = "H"
|
|
15
|
+
STATUS_MOVED = "M"
|
|
16
|
+
STATUS_MOVING = "T"
|
|
17
|
+
STATUS_QUEUED = "Q"
|
|
18
|
+
STATUS_RUNNING = "R"
|
|
19
|
+
STATUS_SUBJOB_COMPLETED = "X"
|
|
20
|
+
STATUS_SUSPENDED = "S"
|
|
21
|
+
STATUS_WAITING = "W"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Constants for direct execution (no scheduler)."""
|
|
2
|
+
|
|
3
|
+
from hpcpy.status import Status
|
|
4
|
+
import hpcpy.constants as hc
|
|
5
|
+
|
|
6
|
+
# Commands
|
|
7
|
+
COMMANDS = dict(
|
|
8
|
+
submit="{variables_str}bash {job_script}",
|
|
9
|
+
status="ps -p {job_id} -o stat=",
|
|
10
|
+
delete="kill {job_id}",
|
|
11
|
+
hold="kill -s STOP {job_id}",
|
|
12
|
+
release="kill -s CONT {job_id}",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Directives (none for direct execution)
|
|
16
|
+
DIRECTIVES = dict()
|
|
17
|
+
|
|
18
|
+
# Statuses - mapped from ps(1) stat codes
|
|
19
|
+
STATUSES = [
|
|
20
|
+
Status("R", "RUNNING", "Process is running.", generic=hc.STATUS_RUNNING),
|
|
21
|
+
Status(
|
|
22
|
+
"S",
|
|
23
|
+
"SLEEPING",
|
|
24
|
+
"Process is sleeping (interruptible).",
|
|
25
|
+
generic=hc.STATUS_RUNNING,
|
|
26
|
+
),
|
|
27
|
+
Status(
|
|
28
|
+
"D",
|
|
29
|
+
"DISK_SLEEP",
|
|
30
|
+
"Process is in uninterruptible disk sleep.",
|
|
31
|
+
generic=hc.STATUS_RUNNING,
|
|
32
|
+
),
|
|
33
|
+
Status("I", "IDLE", "Process is idle.", generic=hc.STATUS_RUNNING),
|
|
34
|
+
Status(
|
|
35
|
+
"T", "STOPPED", "Process has been stopped by a signal.", generic=hc.STATUS_HELD
|
|
36
|
+
),
|
|
37
|
+
Status(
|
|
38
|
+
"Z",
|
|
39
|
+
"ZOMBIE",
|
|
40
|
+
"Process is a zombie (awaiting reaping).",
|
|
41
|
+
generic=hc.STATUS_EXITING,
|
|
42
|
+
),
|
|
43
|
+
Status(
|
|
44
|
+
"F",
|
|
45
|
+
"FINISHED",
|
|
46
|
+
"Job has finished, or never existed",
|
|
47
|
+
generic=hc.STATUS_FINISHED,
|
|
48
|
+
),
|
|
49
|
+
Status(
|
|
50
|
+
"E",
|
|
51
|
+
"EXITING",
|
|
52
|
+
"Job has finished, or never existed",
|
|
53
|
+
generic=hc.STATUS_EXITING,
|
|
54
|
+
),
|
|
55
|
+
]
|