richqueue 1.4__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.4 → richqueue-1.5}/PKG-INFO +1 -1
- {richqueue-1.4 → richqueue-1.5}/pyproject.toml +2 -1
- richqueue-1.5/richqueue/log.py +157 -0
- {richqueue-1.4 → richqueue-1.5}/richqueue/slurm.py +8 -4
- {richqueue-1.4 → richqueue-1.5}/richqueue/table.py +35 -0
- {richqueue-1.4 → richqueue-1.5}/.gitignore +0 -0
- {richqueue-1.4 → richqueue-1.5}/LICENSE +0 -0
- {richqueue-1.4 → richqueue-1.5}/README.md +0 -0
- {richqueue-1.4 → richqueue-1.5}/richqueue/__init__.py +0 -0
- {richqueue-1.4 → richqueue-1.5}/richqueue/console.py +0 -0
- {richqueue-1.4 → richqueue-1.5}/richqueue/layout.py +0 -0
- {richqueue-1.4 → richqueue-1.5}/richqueue/main.py +0 -0
- {richqueue-1.4 → richqueue-1.5}/richqueue/tools.py +0 -0
- {richqueue-1.4 → richqueue-1.5}/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.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"
|
|
@@ -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
|
|
|
@@ -223,7 +227,7 @@ def get_squeue(
|
|
|
223
227
|
|
|
224
228
|
extract_list(df, "job_state")
|
|
225
229
|
extract_list(df, "exclusive")
|
|
226
|
-
extract_json(df, "exclusive")
|
|
230
|
+
# extract_json(df, "exclusive")
|
|
227
231
|
|
|
228
232
|
return df
|
|
229
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
|
|
|
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
|