richqueue 1.4__tar.gz → 1.6__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: 1.4
3
+ Version: 1.6
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 = "1.4"
6
+ version = "1.6"
7
7
  authors = [
8
8
  { name = "Max Winokan", email = "max@winokan.com" },
9
9
  ]
@@ -31,4 +31,5 @@ include = [
31
31
 
32
32
  [project.scripts]
33
33
  rq = "richqueue.main:main"
34
+ res = "richqueue.log:main"
34
35
  confucius = "richqueue.wisdom:main"
@@ -0,0 +1,157 @@
1
+ import time
2
+
3
+ from rich.live import Live
4
+ from rich.panel import Panel
5
+ from rich.layout import Layout
6
+
7
+ from typing_extensions import Annotated
8
+ from typer import Typer, Argument, Option
9
+
10
+ from .console import console
11
+ from .layout import dual_layout
12
+ from .slurm import combined_df, get_user, PANEL_PADDING
13
+ from .table import job_table, log_table
14
+ from .tools import curry
15
+
16
+ # access logs
17
+
18
+ JOB_DF = None
19
+
20
+ app = Typer()
21
+
22
+
23
+ @app.callback(invoke_without_command=True)
24
+ def show_log(
25
+ job: Annotated[int, Argument(help="Show logs for this job")] = None,
26
+ user: Annotated[
27
+ str,
28
+ Option(
29
+ "-u",
30
+ "--user",
31
+ help="Query jobs for another user",
32
+ ),
33
+ ] = None,
34
+ long: Annotated[bool, Option("-v", "--long", help="More detailed output")] = False,
35
+ wisdom: Annotated[
36
+ bool, Option("-w", "--wisdom", help="Show a confucius quote on exit")
37
+ ] = False,
38
+ ):
39
+
40
+ if user is None:
41
+ user = get_user()
42
+
43
+ if job is None:
44
+
45
+ def job_getter():
46
+ return guess_current_job(user=user)
47
+
48
+ else:
49
+
50
+ def job_getter():
51
+ return get_specific_job(job=job, user=None)
52
+
53
+ job = job_getter()
54
+
55
+ if job is None:
56
+ return None
57
+
58
+ layout_func = curry(dual_layout, log_layout_pair)
59
+
60
+ layout = layout_func(job_getter=job_getter, long=long)
61
+
62
+ with Live(
63
+ layout,
64
+ refresh_per_second=1,
65
+ screen=True,
66
+ transient=True,
67
+ vertical_overflow="visible",
68
+ ) as live:
69
+
70
+ try:
71
+ while True:
72
+ layout = layout_func(job_getter=job_getter, long=long)
73
+ live.update(layout)
74
+ time.sleep(1)
75
+ except KeyboardInterrupt:
76
+ live.stop()
77
+ if wisdom:
78
+ from .wisdom import print_random_quote
79
+
80
+ print_random_quote()
81
+
82
+
83
+ def log_layout_pair(job_getter, **kwargs):
84
+ job = job_getter()
85
+ # console.print(job)
86
+
87
+ upper = job_table(row=job, job=job.job_id, **kwargs)
88
+ upper = Panel(upper, expand=False)
89
+
90
+ limit = console.size.height - 2 * PANEL_PADDING - upper.renderable.row_count
91
+
92
+ lower = log_table(
93
+ stdout=job.standard_output, stderr=job.standard_error, limit=limit
94
+ )
95
+ lower = Panel(lower, expand=False)
96
+
97
+ return upper, lower
98
+
99
+
100
+ def guess_current_job(user: str, **kwargs):
101
+
102
+ global JOB_DF
103
+ JOB_DF = combined_df(user=user, **kwargs)
104
+
105
+ assert len(JOB_DF), "No jobs found"
106
+
107
+ pending = JOB_DF[JOB_DF["job_state"] == "PENDING"]
108
+ running = JOB_DF[JOB_DF["job_state"] == "RUNNING"]
109
+ history = JOB_DF[~JOB_DF["job_state"].isin(["PENDING", "RUNNING"])]
110
+
111
+ # pick the single active job
112
+ if len(running) == 1:
113
+ return running.iloc[0]
114
+
115
+ # pick the most recently submitted active job
116
+ elif len(running) > 1:
117
+ return running.sort_values(by="submit_time", ascending=False).iloc[0]
118
+
119
+ # pick the single pending job
120
+ elif len(pending) == 1:
121
+ return pending.iloc[0]
122
+
123
+ # pick the most recently submitted pending job
124
+ elif len(pending) > 1:
125
+ return pending.sort_values(by="submit_time", ascending=False).iloc[0]
126
+
127
+ # pick the most recently submitted pending job
128
+ else:
129
+ return pending.sort_values(by="submit_time", ascending=False).iloc[0]
130
+
131
+ return job
132
+
133
+
134
+ def get_specific_job(job: int, user: str, **kwargs):
135
+
136
+ global JOB_DF
137
+ JOB_DF = combined_df(user=user, **kwargs)
138
+
139
+ assert len(JOB_DF), "No jobs found"
140
+
141
+ matches = JOB_DF[JOB_DF["job_id"] == job]
142
+
143
+ if not len(matches):
144
+ console.print("[red bold]Could not find job with ID {job_id=}")
145
+ return None
146
+
147
+ assert len(matches) == 1
148
+
149
+ return matches.iloc[0]
150
+
151
+
152
+ def main():
153
+ app()
154
+
155
+
156
+ if __name__ == "__main__":
157
+ main()
@@ -18,14 +18,18 @@ PANEL_PADDING = 4
18
18
  ### CONSTRUCT LAYOUT
19
19
 
20
20
 
21
+ def get_user() -> str:
22
+ x = subprocess.Popen(["whoami"], shell=True, stdout=subprocess.PIPE)
23
+ output = x.communicate()
24
+ return output[0].strip().decode("utf-8")
25
+
26
+
21
27
  def get_layout_pair(user: str | None, **kwargs):
22
28
 
23
29
  if user == "all":
24
30
  user = None
25
31
  elif user is None:
26
- x = subprocess.Popen(["whoami"], shell=True, stdout=subprocess.PIPE)
27
- output = x.communicate()
28
- user = output[0].strip().decode("utf-8")
32
+ user = get_user()
29
33
 
30
34
  df = combined_df(user=user, **kwargs)
31
35
 
@@ -186,10 +190,12 @@ def get_squeue(
186
190
 
187
191
  global METADATA
188
192
 
193
+ meta = payload["meta"]
194
+
189
195
  METADATA = {
190
- "cluster_name": payload["meta"]["slurm"]["cluster"],
191
- "user": payload["meta"]["client"]["user"],
192
- "group": payload["meta"]["client"]["group"],
196
+ "cluster_name": meta.get("slurm", {}).get("cluster", "unknown HPC"),
197
+ "user": meta.get("client", {}).get("user", "unknown user"),
198
+ "group": meta.get("client", {}).get("group", "unknown group"),
193
199
  }
194
200
 
195
201
  # parse payload
@@ -223,7 +229,7 @@ def get_squeue(
223
229
 
224
230
  extract_list(df, "job_state")
225
231
  extract_list(df, "exclusive")
226
- extract_json(df, "exclusive")
232
+ # extract_json(df, "exclusive")
227
233
 
228
234
  return df
229
235
 
@@ -266,10 +272,12 @@ def get_sacct(
266
272
 
267
273
  global METADATA
268
274
 
275
+ meta = payload["meta"]
276
+
269
277
  METADATA = {
270
- "cluster_name": payload["meta"]["slurm"]["cluster"],
271
- "user": payload["meta"]["client"]["user"],
272
- "group": payload["meta"]["client"]["group"],
278
+ "cluster_name": meta.get("slurm", {}).get("cluster", "unknown HPC"),
279
+ "user": meta.get("client", {}).get("user", "unknown user"),
280
+ "group": meta.get("client", {}).get("group", "unknown group"),
273
281
  "hist": hist,
274
282
  "hist_unit": hist_unit,
275
283
  }
@@ -316,10 +324,12 @@ def get_sinfo(**kwargs) -> "pandas.DataFrame":
316
324
 
317
325
  global METADATA
318
326
 
327
+ meta = payload["meta"]
328
+
319
329
  METADATA = {
320
- "cluster_name": payload["meta"]["slurm"]["cluster"],
321
- "user": payload["meta"]["client"]["user"],
322
- "group": payload["meta"]["client"]["group"],
330
+ "cluster_name": meta.get("slurm", {}).get("cluster", "unknown HPC"),
331
+ "user": meta.get("client", {}).get("user", "unknown user"),
332
+ "group": meta.get("client", {}).get("group", "unknown group"),
323
333
  }
324
334
 
325
335
  df = DataFrame(payload["sinfo"])
@@ -2,6 +2,7 @@ from rich.table import Table
2
2
  from .tools import human_datetime
3
3
  from pandas import isnull
4
4
  from .console import console
5
+ import subprocess
5
6
 
6
7
  ### TABLES
7
8
 
@@ -200,6 +201,40 @@ def job_table(row, job: int, long: bool = True, **kwargs):
200
201
  return table
201
202
 
202
203
 
204
+ def log_table(stdout, stderr, limit):
205
+
206
+ from rich.text import Text
207
+
208
+ dual = stdout != stderr
209
+
210
+ if dual:
211
+ raise NotImplementedError("separate log files not yet supported")
212
+
213
+ else:
214
+
215
+ table = Table(title="stdout", box=None, header_style="")
216
+
217
+ table.add_column("[bold underline]#", style="bold")
218
+ table.add_column("[bold underline]Log", no_wrap=True)
219
+
220
+ lines = tail(stdout, limit)
221
+
222
+ for i, line in enumerate(lines):
223
+ line = line.decode("utf-8")
224
+
225
+ line = line.split("\r")[-1]
226
+
227
+ table.add_row(str(i), Text.from_ansi(line))
228
+
229
+ return table
230
+
231
+
232
+ def tail(path, n):
233
+ proc = subprocess.Popen(["tail", "-n", str(n), path], stdout=subprocess.PIPE)
234
+ lines = proc.stdout.readlines()
235
+ return lines
236
+
237
+
203
238
  ### FORMATTERS
204
239
 
205
240
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes