richqueue 0.7__tar.gz → 0.9__tar.gz
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.
- {richqueue-0.7 → richqueue-0.9}/PKG-INFO +1 -1
- {richqueue-0.7 → richqueue-0.9}/pyproject.toml +2 -2
- {richqueue-0.7 → richqueue-0.9}/richqueue/main.py +9 -5
- {richqueue-0.7 → richqueue-0.9}/richqueue/slurm.py +70 -10
- {richqueue-0.7 → richqueue-0.9}/richqueue/table.py +150 -0
- {richqueue-0.7 → richqueue-0.9}/.gitignore +0 -0
- {richqueue-0.7 → richqueue-0.9}/LICENSE +0 -0
- {richqueue-0.7 → richqueue-0.9}/README.md +0 -0
- {richqueue-0.7 → richqueue-0.9}/richqueue/__init__.py +0 -0
- {richqueue-0.7 → richqueue-0.9}/richqueue/console.py +0 -0
- {richqueue-0.7 → richqueue-0.9}/richqueue/layout.py +0 -0
- {richqueue-0.7 → richqueue-0.9}/richqueue/tools.py +0 -0
|
@@ -3,7 +3,7 @@ requires = ["hatchling"]
|
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
[project]
|
|
5
5
|
name = "richqueue"
|
|
6
|
-
version = "0.
|
|
6
|
+
version = "0.9"
|
|
7
7
|
authors = [
|
|
8
8
|
{ name = "Max Winokan", email = "max@winokan.com" },
|
|
9
9
|
]
|
|
@@ -30,4 +30,4 @@ include = [
|
|
|
30
30
|
]
|
|
31
31
|
|
|
32
32
|
[project.scripts]
|
|
33
|
-
rq = "richqueue.
|
|
33
|
+
rq = "richqueue.main:main"
|
|
@@ -4,7 +4,7 @@ from rich.live import Live
|
|
|
4
4
|
import time
|
|
5
5
|
from .console import console
|
|
6
6
|
from .tools import curry
|
|
7
|
-
from .slurm import get_layout_pair, get_node_layout, get_hist_layout
|
|
7
|
+
from .slurm import get_layout_pair, get_node_layout, get_hist_layout, get_job_layout
|
|
8
8
|
|
|
9
9
|
# set up singletons
|
|
10
10
|
app = Typer()
|
|
@@ -20,6 +20,7 @@ def show(
|
|
|
20
20
|
loop: bool = True,
|
|
21
21
|
hist: int | None = None,
|
|
22
22
|
hist_unit: str = "weeks",
|
|
23
|
+
job: int | None = None,
|
|
23
24
|
# screen: bool = True,
|
|
24
25
|
# disappear: bool = True,
|
|
25
26
|
):
|
|
@@ -36,18 +37,21 @@ def show(
|
|
|
36
37
|
"hist_unit": hist_unit,
|
|
37
38
|
"screen": screen,
|
|
38
39
|
"disappear": disappear,
|
|
40
|
+
"job": job,
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
# console.print(kwargs)
|
|
42
44
|
|
|
43
|
-
match (bool(idle), bool(hist)):
|
|
44
|
-
case (True, False):
|
|
45
|
+
match (bool(idle), bool(hist), bool(job)):
|
|
46
|
+
case (True, False, False):
|
|
45
47
|
loop = False
|
|
46
48
|
layout_func = get_node_layout
|
|
47
|
-
case (False, True):
|
|
49
|
+
case (False, True, False):
|
|
48
50
|
loop = False
|
|
49
51
|
layout_func = get_hist_layout
|
|
50
|
-
case (False, False):
|
|
52
|
+
case (False, False, True):
|
|
53
|
+
layout_func = get_job_layout
|
|
54
|
+
case (False, False, False):
|
|
51
55
|
layout_func = curry(dual_layout, get_layout_pair)
|
|
52
56
|
case _:
|
|
53
57
|
raise Exception("Unsupported CLI options")
|
|
@@ -6,7 +6,7 @@ import json
|
|
|
6
6
|
from .console import console
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
import datetime
|
|
9
|
-
from .table import running_job_table, history_job_table, node_table
|
|
9
|
+
from .table import running_job_table, history_job_table, node_table, job_table
|
|
10
10
|
from .tools import human_timedelta
|
|
11
11
|
|
|
12
12
|
# from numpy import isnat
|
|
@@ -119,12 +119,32 @@ def get_node_layout(idle: bool = True, **kwargs):
|
|
|
119
119
|
return Panel(table, expand=False)
|
|
120
120
|
|
|
121
121
|
|
|
122
|
+
def get_job_layout(job: int, **kwargs):
|
|
123
|
+
|
|
124
|
+
df = get_squeue(job=job, **kwargs)
|
|
125
|
+
|
|
126
|
+
if "end_time" not in df:
|
|
127
|
+
df["end_time"] = None
|
|
128
|
+
df["run_time"] = add_run_time(df)
|
|
129
|
+
|
|
130
|
+
assert len(df) == 1
|
|
131
|
+
|
|
132
|
+
table = job_table(df.iloc[0], job=job, **kwargs)
|
|
133
|
+
|
|
134
|
+
return Panel(table, expand=False)
|
|
135
|
+
|
|
136
|
+
|
|
122
137
|
### GET QUEUE DFs
|
|
123
138
|
|
|
124
139
|
|
|
125
|
-
def get_squeue(
|
|
140
|
+
def get_squeue(
|
|
141
|
+
user: str | None = None, job: int | None = None, **kwargs
|
|
142
|
+
) -> "pandas.DataFrame":
|
|
126
143
|
|
|
127
|
-
if
|
|
144
|
+
if job:
|
|
145
|
+
assert isinstance(job, int)
|
|
146
|
+
command = f"squeue --job={job} --json"
|
|
147
|
+
elif user:
|
|
128
148
|
command = f"squeue -u {user} --json"
|
|
129
149
|
else:
|
|
130
150
|
command = f"squeue --json"
|
|
@@ -134,13 +154,22 @@ def get_squeue(user: str | None = None, **kwargs) -> "pandas.DataFrame":
|
|
|
134
154
|
[command], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
135
155
|
)
|
|
136
156
|
output = process.communicate()
|
|
157
|
+
|
|
158
|
+
if job and "slurm_load_jobs error: Invalid job id specified" in str(output[1]):
|
|
159
|
+
return get_sacct(user=user, job=job, **kwargs)
|
|
160
|
+
|
|
137
161
|
payload = json.loads(output[0])
|
|
162
|
+
|
|
138
163
|
except json.JSONDecodeError:
|
|
139
164
|
# console.print('[orange1 bold]Warning: using example data')
|
|
165
|
+
|
|
166
|
+
if job:
|
|
167
|
+
example = "squeue_job.json"
|
|
168
|
+
else:
|
|
169
|
+
example = "squeue_long.json"
|
|
170
|
+
|
|
140
171
|
payload = json.load(
|
|
141
|
-
open(
|
|
142
|
-
Path(__file__).parent.parent / "example_data" / "squeue_long.json", "rt"
|
|
143
|
-
)
|
|
172
|
+
open(Path(__file__).parent.parent / "example_data" / example, "rt")
|
|
144
173
|
)
|
|
145
174
|
|
|
146
175
|
global METADATA
|
|
@@ -168,23 +197,35 @@ def get_squeue(user: str | None = None, **kwargs) -> "pandas.DataFrame":
|
|
|
168
197
|
extract_inner(df, "node_count", "number")
|
|
169
198
|
extract_inner(df, "cpus_per_task", "number")
|
|
170
199
|
extract_inner(df, "threads_per_core", "number")
|
|
200
|
+
extract_inner(df, "memory_per_node", "number")
|
|
201
|
+
extract_inner(df, "memory_per_cpu", "number")
|
|
202
|
+
extract_inner(df, "tasks", "number")
|
|
171
203
|
|
|
172
204
|
extract_time(df, "start_time")
|
|
173
205
|
extract_time(df, "submit_time")
|
|
174
|
-
|
|
206
|
+
# extract_inner(df, "time_limit", "number")
|
|
207
|
+
extract_time_limit(df, "time_limit")
|
|
175
208
|
|
|
176
209
|
extract_list(df, "job_state")
|
|
210
|
+
extract_list(df, "exclusive")
|
|
211
|
+
extract_json(df, "exclusive")
|
|
177
212
|
|
|
178
213
|
return df
|
|
179
214
|
|
|
180
215
|
|
|
181
216
|
def get_sacct(
|
|
182
|
-
user: str | None = None,
|
|
217
|
+
user: str | None = None,
|
|
218
|
+
hist: int | None = 4,
|
|
219
|
+
job: int | None = None,
|
|
220
|
+
hist_unit: str = "weeks",
|
|
221
|
+
**kwargs,
|
|
183
222
|
) -> "pandas.DataFrame":
|
|
184
223
|
|
|
185
224
|
hist = hist or 4
|
|
186
225
|
|
|
187
|
-
if
|
|
226
|
+
if job:
|
|
227
|
+
command = f"sacct --job={job} --json"
|
|
228
|
+
elif user:
|
|
188
229
|
command = f"sacct -u {user} --json -S now-{hist}{hist_unit}"
|
|
189
230
|
else:
|
|
190
231
|
command = f"sacct --json -S now-{hist}{hist_unit}"
|
|
@@ -197,8 +238,13 @@ def get_sacct(
|
|
|
197
238
|
payload = json.loads(output[0])
|
|
198
239
|
except json.JSONDecodeError:
|
|
199
240
|
# console.print('[orange1 bold]Warning: using example data')
|
|
241
|
+
if job:
|
|
242
|
+
example = "sacct_job.json"
|
|
243
|
+
else:
|
|
244
|
+
example = "sacct.json"
|
|
245
|
+
|
|
200
246
|
payload = json.load(
|
|
201
|
-
open(Path(__file__).parent.parent / "example_data" /
|
|
247
|
+
open(Path(__file__).parent.parent / "example_data" / example, "rt")
|
|
202
248
|
)
|
|
203
249
|
|
|
204
250
|
global METADATA
|
|
@@ -348,12 +394,26 @@ def extract_inner(df, key, inner):
|
|
|
348
394
|
df[key] = df.apply(_inner, axis=1)
|
|
349
395
|
|
|
350
396
|
|
|
397
|
+
def extract_json(df, key):
|
|
398
|
+
|
|
399
|
+
def _inner(x):
|
|
400
|
+
return json.loads(x[key])
|
|
401
|
+
|
|
402
|
+
df[key] = df.apply(_inner, axis=1)
|
|
403
|
+
|
|
404
|
+
|
|
351
405
|
def extract_time(df, key):
|
|
352
406
|
df[key] = df.apply(
|
|
353
407
|
lambda x: datetime.datetime.fromtimestamp(x[key]["number"]), axis=1
|
|
354
408
|
)
|
|
355
409
|
|
|
356
410
|
|
|
411
|
+
def extract_time_limit(df, key):
|
|
412
|
+
df[key] = df.apply(
|
|
413
|
+
lambda x: human_timedelta(datetime.timedelta(minutes=x[key]["number"])), axis=1
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
357
417
|
def extract_sacct_times(df):
|
|
358
418
|
df["start_time"] = df.apply(
|
|
359
419
|
lambda x: datetime.datetime.fromtimestamp(x["time"]["start"]), axis=1
|
|
@@ -145,6 +145,61 @@ def node_table(df, long: bool = False, **kwargs):
|
|
|
145
145
|
return table
|
|
146
146
|
|
|
147
147
|
|
|
148
|
+
def job_table(row, job: int, long: bool = True, **kwargs):
|
|
149
|
+
|
|
150
|
+
from .slurm import METADATA
|
|
151
|
+
|
|
152
|
+
title = f"[bold]{job=} on {METADATA['cluster_name']}"
|
|
153
|
+
|
|
154
|
+
table = Table(title=title, box=None, header_style="")
|
|
155
|
+
|
|
156
|
+
columns = JOB_COLUMNS[long]
|
|
157
|
+
|
|
158
|
+
table.add_column("[bold underline]Key", style="bold")
|
|
159
|
+
table.add_column("[bold underline]Value")
|
|
160
|
+
|
|
161
|
+
for col in columns:
|
|
162
|
+
if not col:
|
|
163
|
+
table.add_row()
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
col_data = COLUMNS[col]
|
|
167
|
+
|
|
168
|
+
key = col_data.get("header") or col
|
|
169
|
+
|
|
170
|
+
if (
|
|
171
|
+
"standard_output" in row
|
|
172
|
+
and col == "standard_error"
|
|
173
|
+
and row["standard_output"] == row[col]
|
|
174
|
+
):
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
if "]" in key:
|
|
178
|
+
key = key.split("]")[1]
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
value = row[col]
|
|
182
|
+
|
|
183
|
+
if long or value:
|
|
184
|
+
formatter = FORMATTERS.get(col, str)
|
|
185
|
+
value = formatter(value)
|
|
186
|
+
style = col_data.get("style")
|
|
187
|
+
|
|
188
|
+
if style:
|
|
189
|
+
value = f"[{style}]{value}"
|
|
190
|
+
|
|
191
|
+
except KeyError:
|
|
192
|
+
if long:
|
|
193
|
+
value = "[red]Unknown"
|
|
194
|
+
else:
|
|
195
|
+
value = None
|
|
196
|
+
|
|
197
|
+
if value or long:
|
|
198
|
+
table.add_row(key, value)
|
|
199
|
+
|
|
200
|
+
return table
|
|
201
|
+
|
|
202
|
+
|
|
148
203
|
### FORMATTERS
|
|
149
204
|
|
|
150
205
|
|
|
@@ -181,6 +236,78 @@ def mem_string(mb):
|
|
|
181
236
|
|
|
182
237
|
### SPECIFICATION
|
|
183
238
|
|
|
239
|
+
JOB_COLUMNS = {
|
|
240
|
+
True: [
|
|
241
|
+
None,
|
|
242
|
+
"job_id",
|
|
243
|
+
"name",
|
|
244
|
+
"job_state",
|
|
245
|
+
"nodes",
|
|
246
|
+
None,
|
|
247
|
+
"user_name",
|
|
248
|
+
"group_name",
|
|
249
|
+
"partition",
|
|
250
|
+
"qos",
|
|
251
|
+
None,
|
|
252
|
+
"node_count",
|
|
253
|
+
"cpus",
|
|
254
|
+
"tasks",
|
|
255
|
+
"cpus_per_task",
|
|
256
|
+
"memory_per_cpu",
|
|
257
|
+
"memory_per_node",
|
|
258
|
+
"threads_per_core",
|
|
259
|
+
None,
|
|
260
|
+
"submit_time",
|
|
261
|
+
"start_time",
|
|
262
|
+
"run_time",
|
|
263
|
+
"time_limit",
|
|
264
|
+
None,
|
|
265
|
+
"command",
|
|
266
|
+
"current_working_directory",
|
|
267
|
+
"standard_output",
|
|
268
|
+
"standard_error",
|
|
269
|
+
None,
|
|
270
|
+
"exclusive",
|
|
271
|
+
"requeue",
|
|
272
|
+
"dependency",
|
|
273
|
+
"restart_cnt",
|
|
274
|
+
# "derived_exit_code",
|
|
275
|
+
# # "end_time",
|
|
276
|
+
],
|
|
277
|
+
False: [
|
|
278
|
+
"job_id",
|
|
279
|
+
"name",
|
|
280
|
+
"job_state",
|
|
281
|
+
# "nodes",
|
|
282
|
+
"user_name",
|
|
283
|
+
# "group_name",
|
|
284
|
+
"partition",
|
|
285
|
+
# "qos",
|
|
286
|
+
"node_count",
|
|
287
|
+
"cpus",
|
|
288
|
+
# "tasks",
|
|
289
|
+
# "cpus_per_task",
|
|
290
|
+
# "memory_per_cpu",
|
|
291
|
+
# "memory_per_node",
|
|
292
|
+
# "threads_per_core",
|
|
293
|
+
# "submit_time",
|
|
294
|
+
"start_time",
|
|
295
|
+
"run_time",
|
|
296
|
+
"time_limit",
|
|
297
|
+
"command",
|
|
298
|
+
"current_working_directory",
|
|
299
|
+
"standard_output",
|
|
300
|
+
"standard_error",
|
|
301
|
+
"exclusive",
|
|
302
|
+
"requeue",
|
|
303
|
+
"dependency",
|
|
304
|
+
"restart_cnt",
|
|
305
|
+
# None,
|
|
306
|
+
# "derived_exit_code",
|
|
307
|
+
# # "end_time",
|
|
308
|
+
],
|
|
309
|
+
}
|
|
310
|
+
|
|
184
311
|
RUNNING_JOB_COLUMNS = {
|
|
185
312
|
True: [
|
|
186
313
|
"job_id",
|
|
@@ -355,6 +482,29 @@ COLUMNS = {
|
|
|
355
482
|
"style": "magenta",
|
|
356
483
|
"no_wrap": True,
|
|
357
484
|
},
|
|
485
|
+
"command": {"header": "Script", "style": "bright_yellow"},
|
|
486
|
+
"current_working_directory": {"header": "Directory", "style": "bright_yellow"},
|
|
487
|
+
"standard_output": {"header": "Log", "style": "bright_yellow"},
|
|
488
|
+
"standard_error": {"header": "Error Log", "style": "bright_yellow"},
|
|
489
|
+
"cpus_per_task": {"header": "#CPUs/Task", "style": "magenta"},
|
|
490
|
+
"dependency": {"header": "Dependencies", "style": "green_yellow"},
|
|
491
|
+
"derived_exit_code": {},
|
|
492
|
+
"group_name": {"header": "User Group", "style": "green_yellow"},
|
|
493
|
+
"tasks": {"header": "#Tasks", "style": "magenta"},
|
|
494
|
+
"memory_per_cpu": {"header": "RAM/CPU", "style": "magenta"},
|
|
495
|
+
"memory_per_node": {"header": "RAM/Node", "style": "magenta"},
|
|
496
|
+
"qos": {"header": "QoS", "style": "green_yellow"},
|
|
497
|
+
"restart_cnt": {"header": "#Restarts", "style": "green_yellow"},
|
|
498
|
+
"requeue": {"header": "Requeue?", "style": "green_yellow"},
|
|
499
|
+
"exclusive": {"header": "Exclusive?", "style": "green_yellow"},
|
|
500
|
+
"time_limit": {
|
|
501
|
+
"header": "[underline dodger_blue2]Time Limit",
|
|
502
|
+
"justify": "right",
|
|
503
|
+
"style": "dodger_blue2",
|
|
504
|
+
"no_wrap": True,
|
|
505
|
+
},
|
|
506
|
+
"threads_per_core": {"header": "#Threads/core", "style": "magenta"},
|
|
507
|
+
"user_name": {"header": "User Group", "style": "green_yellow"},
|
|
358
508
|
}
|
|
359
509
|
|
|
360
510
|
FORMATTERS = {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|