richqueue 1.3__tar.gz → 1.5__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.3 → richqueue-1.5}/PKG-INFO +1 -1
- {richqueue-1.3 → richqueue-1.5}/pyproject.toml +2 -1
- {richqueue-1.3 → richqueue-1.5}/richqueue/layout.py +2 -2
- richqueue-1.5/richqueue/log.py +157 -0
- {richqueue-1.3 → richqueue-1.5}/richqueue/slurm.py +20 -8
- {richqueue-1.3 → richqueue-1.5}/richqueue/table.py +37 -0
- {richqueue-1.3 → richqueue-1.5}/richqueue/wisdom.py +3 -1
- {richqueue-1.3 → richqueue-1.5}/.gitignore +0 -0
- {richqueue-1.3 → richqueue-1.5}/LICENSE +0 -0
- {richqueue-1.3 → richqueue-1.5}/README.md +0 -0
- {richqueue-1.3 → richqueue-1.5}/richqueue/__init__.py +0 -0
- {richqueue-1.3 → richqueue-1.5}/richqueue/console.py +0 -0
- {richqueue-1.3 → richqueue-1.5}/richqueue/main.py +0 -0
- {richqueue-1.3 → richqueue-1.5}/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 = "1.
|
|
6
|
+
version = "1.5"
|
|
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"
|
|
@@ -16,12 +16,12 @@ def dual_layout(function, **kwargs):
|
|
|
16
16
|
try:
|
|
17
17
|
upper.size = upper.renderable.renderable.row_count + PANEL_PADDING
|
|
18
18
|
except AttributeError:
|
|
19
|
-
upper.size = 3
|
|
19
|
+
upper.size = 3 # + PANEL_PADDING
|
|
20
20
|
|
|
21
21
|
try:
|
|
22
22
|
lower.size = lower.renderable.renderable.row_count + PANEL_PADDING
|
|
23
23
|
except AttributeError:
|
|
24
|
-
lower.size = 3
|
|
24
|
+
lower.size = 3 # + PANEL_PADDING
|
|
25
25
|
|
|
26
26
|
layout.split_column(
|
|
27
27
|
upper,
|
|
@@ -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
|
|
|
@@ -40,20 +44,22 @@ def get_layout_pair(user: str | None, **kwargs):
|
|
|
40
44
|
hide_pending = False
|
|
41
45
|
|
|
42
46
|
console_height = console.size.height
|
|
43
|
-
|
|
47
|
+
|
|
44
48
|
history_df = df[~df["job_state"].isin(["RUNNING", "PENDING"])]
|
|
45
49
|
|
|
46
50
|
if n_running + n_pending > 0:
|
|
47
51
|
|
|
48
52
|
running_df = df[df["job_state"].isin(["RUNNING", "PENDING"])]
|
|
49
53
|
|
|
54
|
+
active_height = n_running + n_pending + PANEL_PADDING
|
|
55
|
+
|
|
50
56
|
max_rows = console_height - 2 * PANEL_PADDING
|
|
51
57
|
|
|
52
58
|
if n_rows > max_rows:
|
|
53
59
|
|
|
54
60
|
# hide history?
|
|
55
|
-
if
|
|
56
|
-
history_limit = 0
|
|
61
|
+
if active_height < console_height - PANEL_PADDING:
|
|
62
|
+
history_limit = max(0, console_height - PANEL_PADDING - active_height)
|
|
57
63
|
running_limit = None
|
|
58
64
|
|
|
59
65
|
# hide pending?
|
|
@@ -89,7 +95,13 @@ def get_layout_pair(user: str | None, **kwargs):
|
|
|
89
95
|
running = Panel(Text("No active jobs", style="bold"), expand=False)
|
|
90
96
|
|
|
91
97
|
if history_limit == 0:
|
|
92
|
-
history = Panel(
|
|
98
|
+
history = Panel(
|
|
99
|
+
Text(
|
|
100
|
+
"history hidden, resize window or use smaller --hist value",
|
|
101
|
+
style="bold",
|
|
102
|
+
),
|
|
103
|
+
expand=False,
|
|
104
|
+
)
|
|
93
105
|
else:
|
|
94
106
|
history = Panel(
|
|
95
107
|
history_job_table(history_df, limit=history_limit, user=user, **kwargs),
|
|
@@ -215,7 +227,7 @@ def get_squeue(
|
|
|
215
227
|
|
|
216
228
|
extract_list(df, "job_state")
|
|
217
229
|
extract_list(df, "exclusive")
|
|
218
|
-
extract_json(df, "exclusive")
|
|
230
|
+
# extract_json(df, "exclusive")
|
|
219
231
|
|
|
220
232
|
return df
|
|
221
233
|
|
|
@@ -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
|
|
|
@@ -213,6 +248,8 @@ def color_by_state(state):
|
|
|
213
248
|
return "[bright_yellow]Pending"
|
|
214
249
|
elif state == "MIXED":
|
|
215
250
|
return "[bright_yellow]Mixed"
|
|
251
|
+
elif state == "TIMEOUT":
|
|
252
|
+
return "[bold bright_yellow]Timed Out"
|
|
216
253
|
elif state == "CANCELLED":
|
|
217
254
|
return "[orange3]Cancelled"
|
|
218
255
|
elif state == "FAILED":
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|