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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: richqueue
3
- Version: 0.7
3
+ Version: 0.9
4
4
  Summary: RichQueue
5
5
  Project-URL: Homepage, https://github.com/mwinokan/RichQueue/issues
6
6
  Project-URL: Bug Tracker, https://github.com/mwinokan/RichQueue/issues
@@ -3,7 +3,7 @@ requires = ["hatchling"]
3
3
  build-backend = "hatchling.build"
4
4
  [project]
5
5
  name = "richqueue"
6
- version = "0.7"
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.slurm:main"
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(user: str | None = None, **kwargs) -> "pandas.DataFrame":
140
+ def get_squeue(
141
+ user: str | None = None, job: int | None = None, **kwargs
142
+ ) -> "pandas.DataFrame":
126
143
 
127
- if user:
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
- extract_time(df, "time_limit")
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, hist: int | None = 4, hist_unit: str = "weeks", **kwargs
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 user:
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" / "sacct.json", "rt")
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