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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: richqueue
3
- Version: 1.3
3
+ Version: 1.5
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.3"
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 # + PANEL_PADDING
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 # + PANEL_PADDING
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
- 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
 
@@ -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 n_running + n_pending + PANEL_PADDING + 1 < console_height:
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(Text("history hidden, resize window or use smaller --hist value", style="bold"), expand=False)
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":
@@ -332,8 +332,10 @@ QUOTES = [
332
332
  "THE WORLD IS YOUR HOSTAGE", #
333
333
  ]
334
334
 
335
+
335
336
  def main():
336
- app()
337
+ app()
338
+
337
339
 
338
340
  if __name__ == "__main__":
339
341
  app()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes