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.
- {richqueue-1.4 → richqueue-1.6}/PKG-INFO +1 -1
- {richqueue-1.4 → richqueue-1.6}/pyproject.toml +2 -1
- richqueue-1.6/richqueue/log.py +157 -0
- {richqueue-1.4 → richqueue-1.6}/richqueue/slurm.py +23 -13
- {richqueue-1.4 → richqueue-1.6}/richqueue/table.py +35 -0
- {richqueue-1.4 → richqueue-1.6}/.gitignore +0 -0
- {richqueue-1.4 → richqueue-1.6}/LICENSE +0 -0
- {richqueue-1.4 → richqueue-1.6}/README.md +0 -0
- {richqueue-1.4 → richqueue-1.6}/richqueue/__init__.py +0 -0
- {richqueue-1.4 → richqueue-1.6}/richqueue/console.py +0 -0
- {richqueue-1.4 → richqueue-1.6}/richqueue/layout.py +0 -0
- {richqueue-1.4 → richqueue-1.6}/richqueue/main.py +0 -0
- {richqueue-1.4 → richqueue-1.6}/richqueue/tools.py +0 -0
- {richqueue-1.4 → richqueue-1.6}/richqueue/wisdom.py +0 -0
|
@@ -3,7 +3,7 @@ requires = ["hatchling"]
|
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
[project]
|
|
5
5
|
name = "richqueue"
|
|
6
|
-
version = "1.
|
|
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
|
-
|
|
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":
|
|
191
|
-
"user":
|
|
192
|
-
"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":
|
|
271
|
-
"user":
|
|
272
|
-
"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":
|
|
321
|
-
"user":
|
|
322
|
-
"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
|