richqueue 0.8__tar.gz → 1.0__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: 0.8
3
+ Version: 1.0
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 = "0.8"
6
+ version = "1.0"
7
7
  authors = [
8
8
  { name = "Max Winokan", email = "max@winokan.com" },
9
9
  ]
@@ -31,3 +31,4 @@ include = [
31
31
 
32
32
  [project.scripts]
33
33
  rq = "richqueue.main:main"
34
+ confucius = "richqueue.wisdom:main"
@@ -0,0 +1,142 @@
1
+ from typer import Typer, Option
2
+ from typing import Optional
3
+ from typing_extensions import Annotated
4
+ from .layout import dual_layout
5
+ from rich.live import Live
6
+ import time
7
+ from .console import console
8
+ from .tools import curry
9
+ from .slurm import (
10
+ get_layout_pair,
11
+ get_node_layout,
12
+ get_hist_layout,
13
+ get_job_layout,
14
+ NotOnClusterError,
15
+ )
16
+
17
+ # set up singletons
18
+ app = Typer()
19
+
20
+
21
+ # main CLI command
22
+ @app.callback(invoke_without_command=True)
23
+ def show(
24
+ user: Annotated[
25
+ str,
26
+ Option(
27
+ "-u",
28
+ "--user",
29
+ help="Query jobs for this user. Use 'all' to see all jobs",
30
+ ),
31
+ ] = None,
32
+ long: Annotated[bool, Option("-v", "--long", help="More detailed output")] = False,
33
+ idle: Annotated[bool, Option("-i", "--idle", help="Show available nodes")] = False,
34
+ loop: Annotated[
35
+ bool, Option("-l", "--loop", help="Show a live-updating screen")
36
+ ] = True,
37
+ hist: Annotated[
38
+ int, Option("-h", "--hist", help="Show historical jobs from the last X weeks")
39
+ ] = None,
40
+ hist_unit: Annotated[
41
+ str,
42
+ Option("-hu", "--hist-unit", help="Show historical jobs from the last X weeks"),
43
+ ] = "weeks",
44
+ job: Annotated[
45
+ int, Option("-j", "--job", help="Show details for a specific job")
46
+ ] = None,
47
+ wisdom: Annotated[
48
+ bool, Option("-w", "--wisdom", help="Show a confucius quote on exit")
49
+ ] = False,
50
+ ):
51
+
52
+ screen = True
53
+ disappear = not screen
54
+
55
+ kwargs = {
56
+ "user": user,
57
+ "long": long,
58
+ "idle": idle,
59
+ "loop": loop,
60
+ "hist": hist,
61
+ "hist_unit": hist_unit,
62
+ "screen": screen,
63
+ "disappear": disappear,
64
+ "job": job,
65
+ }
66
+
67
+ # console.print(kwargs)
68
+
69
+ match (bool(idle), bool(hist), bool(job)):
70
+ case (True, False, False):
71
+ loop = False
72
+ layout_func = get_node_layout
73
+ case (False, True, False):
74
+ loop = False
75
+ layout_func = get_hist_layout
76
+ case (False, False, True):
77
+ layout_func = get_job_layout
78
+ case (False, False, False):
79
+ layout_func = curry(dual_layout, get_layout_pair)
80
+ case _:
81
+ raise Exception("Unsupported CLI options")
82
+
83
+ try:
84
+
85
+ # live updating layout
86
+ if loop:
87
+
88
+ layout = layout_func(**kwargs)
89
+
90
+ with Live(
91
+ layout,
92
+ refresh_per_second=4,
93
+ screen=screen,
94
+ transient=disappear,
95
+ vertical_overflow="visible",
96
+ ) as live:
97
+
98
+ try:
99
+ while True:
100
+ layout = layout_func(**kwargs)
101
+ live.update(layout)
102
+ time.sleep(1)
103
+ except KeyboardInterrupt:
104
+ live.stop()
105
+ if wisdom:
106
+ from .wisdom import print_random_quote
107
+
108
+ print_random_quote()
109
+
110
+ # static layout
111
+ else:
112
+
113
+ layout = layout_func(**kwargs)
114
+ console.print(layout)
115
+
116
+ except NotOnClusterError as e:
117
+ console.print(f"[red bold]{e}[reset]")
118
+
119
+
120
+ @app.command()
121
+ def log(job: Annotated[int, Option(help="Show logs from a specific job")]):
122
+ raise NotImplementedError
123
+
124
+
125
+ @app.command()
126
+ def dir(job: Annotated[int, Option(help="Get job directory")]):
127
+ raise NotImplementedError
128
+
129
+
130
+ @app.command()
131
+ def nickname():
132
+ raise NotImplementedError
133
+
134
+
135
+ # start Typer app
136
+ def main():
137
+ app()
138
+
139
+
140
+ # start Typer app
141
+ if __name__ == "__main__":
142
+ app()
@@ -6,7 +6,7 @@ import json
6
6
  from .console import console
7
7
  from pathlib import Path
8
8
  import datetime
9
- from .table import running_job_table, history_job_table, node_table
9
+ from .table import running_job_table, history_job_table, node_table, job_table
10
10
  from .tools import human_timedelta
11
11
 
12
12
  # from numpy import isnat
@@ -119,12 +119,32 @@ def get_node_layout(idle: bool = True, **kwargs):
119
119
  return Panel(table, expand=False)
120
120
 
121
121
 
122
+ def get_job_layout(job: int, **kwargs):
123
+
124
+ df = get_squeue(job=job, **kwargs)
125
+
126
+ if "end_time" not in df:
127
+ df["end_time"] = None
128
+ df["run_time"] = add_run_time(df)
129
+
130
+ assert len(df) == 1
131
+
132
+ table = job_table(df.iloc[0], job=job, **kwargs)
133
+
134
+ return Panel(table, expand=False)
135
+
136
+
122
137
  ### GET QUEUE DFs
123
138
 
124
139
 
125
- def get_squeue(user: str | None = None, **kwargs) -> "pandas.DataFrame":
140
+ def get_squeue(
141
+ user: str | None = None, job: int | None = None, **kwargs
142
+ ) -> "pandas.DataFrame":
126
143
 
127
- if user:
144
+ if job:
145
+ assert isinstance(job, int)
146
+ command = f"squeue --job={job} --json"
147
+ elif user:
128
148
  command = f"squeue -u {user} --json"
129
149
  else:
130
150
  command = f"squeue --json"
@@ -134,14 +154,14 @@ def get_squeue(user: str | None = None, **kwargs) -> "pandas.DataFrame":
134
154
  [command], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
135
155
  )
136
156
  output = process.communicate()
157
+
158
+ if job and "slurm_load_jobs error: Invalid job id specified" in str(output[1]):
159
+ return get_sacct(user=user, job=job, **kwargs)
160
+
137
161
  payload = json.loads(output[0])
162
+
138
163
  except json.JSONDecodeError:
139
- # console.print('[orange1 bold]Warning: using example data')
140
- payload = json.load(
141
- open(
142
- Path(__file__).parent.parent / "example_data" / "squeue_long.json", "rt"
143
- )
144
- )
164
+ raise NotOnClusterError("Could not get sinfo JSON. Not on a SLURM cluster?")
145
165
 
146
166
  global METADATA
147
167
 
@@ -168,23 +188,35 @@ def get_squeue(user: str | None = None, **kwargs) -> "pandas.DataFrame":
168
188
  extract_inner(df, "node_count", "number")
169
189
  extract_inner(df, "cpus_per_task", "number")
170
190
  extract_inner(df, "threads_per_core", "number")
191
+ extract_inner(df, "memory_per_node", "number")
192
+ extract_inner(df, "memory_per_cpu", "number")
193
+ extract_inner(df, "tasks", "number")
171
194
 
172
195
  extract_time(df, "start_time")
173
196
  extract_time(df, "submit_time")
174
- extract_time(df, "time_limit")
197
+ # extract_inner(df, "time_limit", "number")
198
+ extract_time_limit(df, "time_limit")
175
199
 
176
200
  extract_list(df, "job_state")
201
+ extract_list(df, "exclusive")
202
+ extract_json(df, "exclusive")
177
203
 
178
204
  return df
179
205
 
180
206
 
181
207
  def get_sacct(
182
- user: str | None = None, hist: int | None = 4, hist_unit: str = "weeks", **kwargs
208
+ user: str | None = None,
209
+ hist: int | None = 4,
210
+ job: int | None = None,
211
+ hist_unit: str = "weeks",
212
+ **kwargs,
183
213
  ) -> "pandas.DataFrame":
184
214
 
185
215
  hist = hist or 4
186
216
 
187
- if user:
217
+ if job:
218
+ command = f"sacct --job={job} --json"
219
+ elif user:
188
220
  command = f"sacct -u {user} --json -S now-{hist}{hist_unit}"
189
221
  else:
190
222
  command = f"sacct --json -S now-{hist}{hist_unit}"
@@ -196,10 +228,17 @@ def get_sacct(
196
228
  output = process.communicate()
197
229
  payload = json.loads(output[0])
198
230
  except json.JSONDecodeError:
231
+ raise NotOnClusterError("Could not get sacct JSON. Not on a SLURM cluster?")
232
+
199
233
  # console.print('[orange1 bold]Warning: using example data')
200
- payload = json.load(
201
- open(Path(__file__).parent.parent / "example_data" / "sacct.json", "rt")
202
- )
234
+ # if job:
235
+ # example = "sacct_job.json"
236
+ # else:
237
+ # example = "sacct.json"
238
+
239
+ # payload = json.load(
240
+ # open(Path(__file__).parent.parent / "example_data" / example, "rt")
241
+ # )
203
242
 
204
243
  global METADATA
205
244
 
@@ -249,9 +288,7 @@ def get_sinfo(**kwargs) -> "pandas.DataFrame":
249
288
  output = process.communicate()
250
289
  payload = json.loads(output[0])
251
290
  except json.JSONDecodeError:
252
- payload = json.load(
253
- open(Path(__file__).parent.parent / "example_data" / "sinfo_N.json", "rt")
254
- )
291
+ raise NotOnClusterError("Could not get sinfo JSON. Not on a SLURM cluster?")
255
292
 
256
293
  global METADATA
257
294
 
@@ -348,12 +385,26 @@ def extract_inner(df, key, inner):
348
385
  df[key] = df.apply(_inner, axis=1)
349
386
 
350
387
 
388
+ def extract_json(df, key):
389
+
390
+ def _inner(x):
391
+ return json.loads(x[key])
392
+
393
+ df[key] = df.apply(_inner, axis=1)
394
+
395
+
351
396
  def extract_time(df, key):
352
397
  df[key] = df.apply(
353
398
  lambda x: datetime.datetime.fromtimestamp(x[key]["number"]), axis=1
354
399
  )
355
400
 
356
401
 
402
+ def extract_time_limit(df, key):
403
+ df[key] = df.apply(
404
+ lambda x: human_timedelta(datetime.timedelta(minutes=x[key]["number"])), axis=1
405
+ )
406
+
407
+
357
408
  def extract_sacct_times(df):
358
409
  df["start_time"] = df.apply(
359
410
  lambda x: datetime.datetime.fromtimestamp(x["time"]["start"]), axis=1
@@ -416,3 +467,6 @@ COLUMNS = {
416
467
  "current_working_directory",
417
468
  ],
418
469
  }
470
+
471
+
472
+ class NotOnClusterError(Exception): ...
@@ -145,6 +145,61 @@ def node_table(df, long: bool = False, **kwargs):
145
145
  return table
146
146
 
147
147
 
148
+ def job_table(row, job: int, long: bool = True, **kwargs):
149
+
150
+ from .slurm import METADATA
151
+
152
+ title = f"[bold]{job=} on {METADATA['cluster_name']}"
153
+
154
+ table = Table(title=title, box=None, header_style="")
155
+
156
+ columns = JOB_COLUMNS[long]
157
+
158
+ table.add_column("[bold underline]Key", style="bold")
159
+ table.add_column("[bold underline]Value")
160
+
161
+ for col in columns:
162
+ if not col:
163
+ table.add_row()
164
+ continue
165
+
166
+ col_data = COLUMNS[col]
167
+
168
+ key = col_data.get("header") or col
169
+
170
+ if (
171
+ "standard_output" in row
172
+ and col == "standard_error"
173
+ and row["standard_output"] == row[col]
174
+ ):
175
+ continue
176
+
177
+ if "]" in key:
178
+ key = key.split("]")[1]
179
+
180
+ try:
181
+ value = row[col]
182
+
183
+ if long or value:
184
+ formatter = FORMATTERS.get(col, str)
185
+ value = formatter(value)
186
+ style = col_data.get("style")
187
+
188
+ if style:
189
+ value = f"[{style}]{value}"
190
+
191
+ except KeyError:
192
+ if long:
193
+ value = "[red]Unknown"
194
+ else:
195
+ value = None
196
+
197
+ if value or long:
198
+ table.add_row(key, value)
199
+
200
+ return table
201
+
202
+
148
203
  ### FORMATTERS
149
204
 
150
205
 
@@ -181,6 +236,78 @@ def mem_string(mb):
181
236
 
182
237
  ### SPECIFICATION
183
238
 
239
+ JOB_COLUMNS = {
240
+ True: [
241
+ None,
242
+ "job_id",
243
+ "name",
244
+ "job_state",
245
+ "nodes",
246
+ None,
247
+ "user_name",
248
+ "group_name",
249
+ "partition",
250
+ "qos",
251
+ None,
252
+ "node_count",
253
+ "cpus",
254
+ "tasks",
255
+ "cpus_per_task",
256
+ "memory_per_cpu",
257
+ "memory_per_node",
258
+ "threads_per_core",
259
+ None,
260
+ "submit_time",
261
+ "start_time",
262
+ "run_time",
263
+ "time_limit",
264
+ None,
265
+ "command",
266
+ "current_working_directory",
267
+ "standard_output",
268
+ "standard_error",
269
+ None,
270
+ "exclusive",
271
+ "requeue",
272
+ "dependency",
273
+ "restart_cnt",
274
+ # "derived_exit_code",
275
+ # # "end_time",
276
+ ],
277
+ False: [
278
+ "job_id",
279
+ "name",
280
+ "job_state",
281
+ # "nodes",
282
+ "user_name",
283
+ # "group_name",
284
+ "partition",
285
+ # "qos",
286
+ "node_count",
287
+ "cpus",
288
+ # "tasks",
289
+ # "cpus_per_task",
290
+ # "memory_per_cpu",
291
+ # "memory_per_node",
292
+ # "threads_per_core",
293
+ # "submit_time",
294
+ "start_time",
295
+ "run_time",
296
+ "time_limit",
297
+ "command",
298
+ "current_working_directory",
299
+ "standard_output",
300
+ "standard_error",
301
+ "exclusive",
302
+ "requeue",
303
+ "dependency",
304
+ "restart_cnt",
305
+ # None,
306
+ # "derived_exit_code",
307
+ # # "end_time",
308
+ ],
309
+ }
310
+
184
311
  RUNNING_JOB_COLUMNS = {
185
312
  True: [
186
313
  "job_id",
@@ -190,6 +317,7 @@ RUNNING_JOB_COLUMNS = {
190
317
  "submit_time",
191
318
  "start_time",
192
319
  "run_time",
320
+ "time_limit",
193
321
  "partition",
194
322
  "nodes",
195
323
  "job_state",
@@ -201,6 +329,7 @@ RUNNING_JOB_COLUMNS = {
201
329
  "cpus",
202
330
  "start_time",
203
331
  "run_time",
332
+ "time_limit",
204
333
  "job_state",
205
334
  ],
206
335
  }
@@ -355,6 +484,29 @@ COLUMNS = {
355
484
  "style": "magenta",
356
485
  "no_wrap": True,
357
486
  },
487
+ "command": {"header": "Script", "style": "bright_yellow"},
488
+ "current_working_directory": {"header": "Directory", "style": "bright_yellow"},
489
+ "standard_output": {"header": "Log", "style": "bright_yellow"},
490
+ "standard_error": {"header": "Error Log", "style": "bright_yellow"},
491
+ "cpus_per_task": {"header": "#CPUs/Task", "style": "magenta"},
492
+ "dependency": {"header": "Dependencies", "style": "green_yellow"},
493
+ "derived_exit_code": {},
494
+ "group_name": {"header": "User Group", "style": "green_yellow"},
495
+ "tasks": {"header": "#Tasks", "style": "magenta"},
496
+ "memory_per_cpu": {"header": "RAM/CPU", "style": "magenta"},
497
+ "memory_per_node": {"header": "RAM/Node", "style": "magenta"},
498
+ "qos": {"header": "QoS", "style": "green_yellow"},
499
+ "restart_cnt": {"header": "#Restarts", "style": "green_yellow"},
500
+ "requeue": {"header": "Requeue?", "style": "green_yellow"},
501
+ "exclusive": {"header": "Exclusive?", "style": "green_yellow"},
502
+ "time_limit": {
503
+ "header": "[underline dodger_blue2]Limit",
504
+ "justify": "right",
505
+ "style": "dodger_blue2",
506
+ "no_wrap": True,
507
+ },
508
+ "threads_per_core": {"header": "#Threads/core", "style": "magenta"},
509
+ "user_name": {"header": "User Group", "style": "green_yellow"},
358
510
  }
359
511
 
360
512
  FORMATTERS = {
@@ -12,6 +12,9 @@ def human_timedelta(delta):
12
12
 
13
13
  bits = []
14
14
 
15
+ if delta.days < 0 or delta.seconds < 0:
16
+ return ""
17
+
15
18
  days = delta.days
16
19
 
17
20
  s = delta.seconds
@@ -0,0 +1,259 @@
1
+
2
+ QUOTES = [
3
+ "DO IT TOMORROW",
4
+ "DO DFT TOMORROW",
5
+ "HOW 'BOUT NAH",
6
+ "STYLE OVER SUBSTANCE",
7
+ "I'M NOT CHINESE",
8
+ "TO DRIVE A TANK\nSIMPLY USE DRUIDRY\nTO TRANSFORM INTO\nA TANK DRIVER",
9
+ "IF THE GAUSSIAN FAILS TWICE\nCONSECUTIVELY, GIVE UP",
10
+ "THE FEWER HOURS YOU WORK ON A FIXED SALARY\nTHE HIGHER YOUR HOURLY PAY", # MW
11
+ "COMP. CHEM IS TEMPORARY.\nCONFUCIUS IS FOREVER",
12
+ "BETTER TO ASK FOR FORGIVENESS THAN PERMISSION",
13
+ "DO YOU THINK I WANT TO BE HERE?",
14
+ "READ THE FUCKING MANUAL",
15
+ "NEVER GIVE A COW\nFREE MILK",
16
+ "FOR BETTER OR FOR WORSE\nI'LL ALWAYS HAVE ANOTHER COFFEE", # MW
17
+ "I'M A BIG PROPONENT FOR HAVING FUN", # MW
18
+ "I WOULDN'T BET MY LIFE ON IT",
19
+ "DEATH DOES NOT PROTECT YOU FROM CRITICISM",
20
+ "SOMETIMES YOU HAVE TO SPEND MONEY TO MAKE MONEY.\n\nSOMETIMES YOU HAVE TO SPEND MONEY TO LOSE MONEY.\n\nONE THING IS FOR CERTAIN, YOU HAVE TO SPEND MONEY.", # Rust Valley Restorers
21
+ "PREMATURE JOB FAILURE IS NOTHING TO BE ASHAMED OF",
22
+ "LOUIE TOLD ME WHAT TO DO\nAND I DID MY BEST TO FUCK IT UP",
23
+ "IT IS IMPOSSIBLE TO REVERSE\nTHE CREATION OF A CAKE",
24
+ "CAKE CAN BE CREATED AND DESTROYED",
25
+ "I CAN HEAR BUT I CAN'T LISTEN",
26
+ "I'M BETTER THAN THE MANUAL",
27
+ "BEFORE TRYING, CONSULT ME",
28
+ "CODE FIRST, THINK LATER",
29
+ "WHEN IN DOUBT, CROSS PRODUCT",
30
+ "CHEMIS[bold]DO[/bold] OR CHEMIS[bold]DON'T[/bold]\nTHERE IS NO CHEMIS[bold]TRY[/bold]",
31
+ "I HATE IT HERE",
32
+ "I DIDN'T SIGN UP FOR THIS",
33
+ "I DON'T UNDERSTAND WHY PEOPLE\nWOULD CHOOSE TO BE A BIOLOGIST",
34
+ "I HATE PAST ME",
35
+ "SOUNDS LIKE A FUTURE ME PROBLEM",
36
+ "I STRUGGLE TO FIND PEACE",
37
+ "I HEREBY CLASSIFY THIS AS A ROYAL BALL ACHE",
38
+ "I'M ALL OUT OF PASSION",
39
+ "WHAT IS A DECIMAL PLACE BETWEEN FRIENDS?",
40
+ "###SPICY###OF COURSE",
41
+ "[dodger_blue2 bold] 🇬🇷 OF COURSE [/bold /dodger_blue2]",
42
+ "ΩΦ KΩΥΡΣΕ",
43
+ "FRENCH? SO YOU'RE EASY TO SATISFY",
44
+ "YOU DON'T WANT TO FINISH TOO EARLY",
45
+ "BOLD AND UNPROVEN",
46
+ "WHERE IS MY GIRAFFE??", # CV
47
+ "THIS IS A HIPPOPOTAMUS",
48
+ "MONOGAMOUS ENTANGLEMENT",
49
+ "INVEST IN A STRAP-ON",
50
+ "###SPICY###FRIZZ OFF", # AC
51
+ "I'M FRIZZED OFF", # AC
52
+ "I'M GONNA GO TO THAT BUS STATION TOMORROW\nAND KICK MIKE'S ASS",
53
+ "STRUGGLING FROM SUCCESS",
54
+ "I'D SHOW YOU MINE;\nBUT I DON'T THINK YOU'D LIKE IT",
55
+ "I'M SEVERAL RAKI'S DEEP",
56
+ "DON'T ASK ME QUESTIONS THAT COULD HAVE CONSEQUENCES",
57
+ "ENORMOUS DRAGON OF FAECES,\nDANCING INSIDE ME", # FB
58
+ "I HAD A CRISIS OF FAITH IN MY RESEARCH,\nBUT THEN I REALISED THAT IT'S FINE",
59
+ "IT DEPENDS",
60
+ "KIND OF A PROOF",
61
+ "THE DEAD CAT CAN BE A RESOURCE",
62
+ "WITH A TALK LIKE THAT YOU CAN EXPECT TO MEET A PERSON\nWHO TOTALLY DISAGREES WITH EVERYTHING YOU SAID.\nYOU WILL FIND THAT PERSON IN ME", # Moritz @ QuEBS 2022
63
+ "MIDNIGHT MEAT",
64
+ "EXISTENCE IS CANCER",
65
+ "IT'S A PITY THEY FOUND A CURE FOR LEPROSY", # QuEBS 2022 Excursion
66
+ "###SPICY###MEOW", # RG
67
+ "DON’T KNOW\nDON’T CARE",
68
+ "INTERESTING TRANSFER DOWN THESE RODS",
69
+ # "DISSEMINATE THROUGHOUT THE UK",
70
+ "DUMPING MY PUMP ON",
71
+ "KINDA TOXIC, BUT DON’T WORRY ABOUT THAT",
72
+ "WHACKING GREAT ABSORBER", # AM
73
+ "I KNOW YOU’RE THEORISTS, AND THIS IS VERY MUDDY",
74
+ "I HAVE A PROPENSITY FOR THE DENSITY", # AB @ QuEBS 2022
75
+ "OH MY [bold]GOODNESS[/bold]", # GF
76
+ "YOU ARE UNDERMINING MY AUTHORITY", # unknown @ QuEBS 2022
77
+ "TO BE WEAK, IS ACTUALLY AN ADVANTAGE ",
78
+ "SPEEDY BOARDING",
79
+ "THE PHILOSOPHICAL SIGNIFICANCE OF POO",
80
+ "THE CAFFEINE IS A MATTER OF LIFE AND DEATH ",
81
+ "IT’S ALMOST CERTAINLY WRONG, BUT FUN",
82
+ "THAT WOULD HAVE BEEN WISE,\nHOWEVER,\nI AM NOT A WISE MAN",
83
+ "THAT MAKES YOU THINK SOFT",
84
+ "I WOULD ARGUE THAT BARE LEGS IS BETTER",
85
+ "ANYTHING IS MICROSCOPIC IF IT’S FAR AWAY ENOUGH",
86
+ "AN ERROR OCCURRED,\nHOW IS THAT EVEN SCIENTIFICALLY POSSIBLE",
87
+ "YOU HEAR THAT?\nTHAT’S A TARDIGRADE",
88
+ "I SEEM TO HAVE LOST CONTROL [OF MY MOUSE]",
89
+ "THERE’S NO ANSWER,\nBY THE WAY",
90
+ "BECAUSE OF SOME PHYSICS ",
91
+ "PSEUDO-WEAK",
92
+ "THESE BALLS ARE JUST REGULAR SHINY", # MW
93
+ "THE LAST COUP KIND OF FIZZLED OUT",
94
+ "I THINK IT’S STRAIGHT SO IT MUST BE ERADICATED",
95
+ "KOBE BEEF IS ALSO FAMOUSLY DEAD COW",
96
+ "I’VE ALSO DECIDED I AM NOT A MORAL PERSON",
97
+ "IT WAS ONLY AN EMPTY THREAT", # JJMF
98
+ "THREATS ARE A PART OF WORKING LIFE", # JJMF
99
+ "WELL THE THREAT WORKED!", # JJMF
100
+ "AN EMPTY THREAT IS STILL A THREAT",
101
+ "WHAT DO YOU THINK I’M SOME SORT OF PEPSI CRACK WHORE?",
102
+ "LOW IMPACT FACTOR?\nI WANT LOW IMPACT ON MY LIFE!",
103
+ "NOT F*CK YO MAMA\n\nF*CK YO-YO MA!",
104
+ "I FORGOT ABOUT THE PASSAGE OF TIME",
105
+ "EVERYTHING IS JUST A BIG WEIRD LIGAND",
106
+ "IT NEEDS TO TASTE GOOD, AND KILL YOU", # CV
107
+ "MY ABILITY TO SEE FUNKINESS REQUIRES EXPERIENCE", # ISQBP 2022
108
+ "HISTIDINE IS A PROBLEM",
109
+ "I HATE THE FACT THAT THEY SPENT SO MUCH TIME ON THESE QUOTES,\nINSTEAD OF IMPROVING THEIR SIMULATION SOFTWARE", # Gromacs
110
+ "BUTTER MUSS SEIN",
111
+ "I DON’T NEED A CONTROL BECAUSE I DON’T HAVE ANY RESULTS",
112
+ "LET THE ANIMAL EAT FIBRE,\nAND THEN EAT THE ANIMAL",
113
+ "THAT’S NOT A SHOT MATE!!",
114
+ "FUCK AROUND AND FIND OUT",
115
+ "♫♬♪[bold]OH MON BEBE[/bold]", # Les Kaira
116
+ "WHY DID TOM CHEATHAM KILL US LAST NIGHT?", # ISQBP 2022
117
+ "NOSE-HOOVER? SO LIKE A COCAINE ADDICT?",
118
+ "YOU CAN SLEEP ON A BANANA",
119
+ "AMBER AND I BROKE UP", # MW
120
+ "WE WENT TOO FAR",
121
+ "THEY GOT JIGGLY PHYSICS?",
122
+ "DO YOU WANT TO SEE MY STUFF JIGGLING?",
123
+ "WE DON’T SEE COLOURS HERE,\nWE JUST SEE BALLS",
124
+ "IN MY OPINION THIS IS EXACTLY\nWHAT IS WRONG WITH SCIENCE",
125
+ "I DIDN’T REALISE YOU WERE A NONCE",
126
+ "I HEARD YOU LIKE FINISHING EARLY",
127
+ "JOIN THE QUANTUM BIOLOGY DISCORD SERVER! #GAMERSRISEUP",
128
+ "WE SHOULD GET SOMETHING VENOMOUS LIKE A PLATYPUS",
129
+ "THERE IS TOO MUCH ON YOUTUBE ABOUT PLATYPUS CUSTARD",
130
+ "I DON'T BELIEVE IN GRANDCHILDREN",
131
+ "WINNING IS NEVER AN OPTION, ONLY REVENGE",
132
+ "FEDERICO’S ALLEYWAY BOUQUET", # MW
133
+ "SENT FROM MY GALAXY",
134
+ " _______\n / 12 \\ \n| | |\n|9 | 3|\n| \\ |\n| |\n \\___6___/\n\nGOOD HEAVENS, LOOK AT THE TIME!",
135
+ "Har🌳-Frick ✝️ ",
136
+ "BRANEURISM\n\nSymptoms:\tlight-headed, delirium,\n\t\tdyslexic, autism,\n\t\t\t\tchicken sounds,\n\t\t\t\tflowcharts\n",
137
+ f" LISTEN TO PHAT BRAHMS\n | \\\n V \\\n EAT KATSU \\ IS IT FRIDAY? ----> WAIT\n | \\ | Yes ^ No |\n V \\ V L-----------\n SPIN MESMERISING SPIN ORB | IS HILLSIDE OPEN? ---> KATSU\n | | | \n V ^ V \n [bold]BRANEURYSM[/bold] | IS YOUNG'S OPEN ---> KATSU\n / \\ | | \n /YES \\ | V\n L NO------->--------| IS MAX IN THE OFFICE?\nKATSU COMA | | Yes \\ No\n ----<---- \\\n END THE DAY\n",
138
+ "I WOULDN’T PAY £100 FOR A WET DREAM,\nFOR £100 I CAN GET A WET REALITY",
139
+ "I KNOW YOUR FUTURE…\n\nBUT I’LL TELL YOU AFTER I GO TO THE LOO",
140
+ "YOU ARE HEADING FOR A SMACK",
141
+ "[bold]F*CK!!! WHY ARE THESE GUYS SUCH *$#@'S?![/bold]\n\n...\n\nI NEED ANOTHER COFFEE, I'M NOT ON EDGE ENOUGH.", # MW
142
+ "RESULTS ARE A SOCIAL CONSTRUCT",
143
+ "AUTISOMANCY",
144
+ "IS A HANGING MAN STANDING ON\nA BOX THAT HE IS HOLDING\nBENEATH HIS FEET A\nPERPETUAL MOTION MACHINE?",
145
+ "HAVE YOU READ KARL MARX'S LATEST TWEET?",
146
+ "WE NEED MORE GAY SCIENCE",
147
+ "IF WE OVERCOME THIS I WILL JERK FROM NOW UNTIL THE NEXT YEAR", # FB
148
+ "DO LOOK A THIEVING STUFFED DOUGHNUT IN THE ASS",
149
+ "YOU'RE NOT HUMAN UNTIL YOU'VE BEEN CITED", # LS
150
+ "TRUE [bold]AND[/bold] FALSE",
151
+ "EINSTEIN? LARGELY A B*TCH",
152
+ "I’M SCARED TO GO TO TEXAS, PEOPLE JUST SHOOTING ME", # MS
153
+ "YOU’RE WRONG WITH THAT, YOU’RE ABSOLUTELY WRONG WITH THAT", # MS, supervisory meeting
154
+ "WE SHOULD GET AN OFFICE DILDO",
155
+ "AIDS SALIVA IS A-OK\nSO YOU CAN GIVE THAT WHISTLE A SUCK",
156
+ "NO ONE EVER SAID NOT TO EAT YELLOW RICE",
157
+ "WALRUSES RAPING PENGUINS FOR REASONS THAT ARE NOT YET CLEAR",
158
+ "THEY NEVER SAID DON'T EAT YELLOW RICE",
159
+ "RUINING PEOPLE'S CAREERS BY KEEPING THEM IN THE SAME PLACE\nJUST BECAUSE I DID", # JAK
160
+ "WHEN I FIRST STARTED LECTURING\nI DID A LOT OF BUM WIGGLING", # JAK
161
+ "I DON'T UNDERSTAND HAVING VALUES",
162
+ "PISS",
163
+ "THAT'S THE ONLY REASON I'M NOT\nEATING YOUR FACE OFF RIGHT NOW",
164
+ "DOUBLE-TEAMED BY GERIATRICS",
165
+ "LIMPINGCHAN",
166
+ "LET'S HAVE A QUICK CHAT?\nI NEED TO VENT SOME ANGER!",
167
+ "THE FRENCHMAN BURNS EASILY",
168
+ "WHY IS THE BROWN ALWAYS CAUSING PROBLEMS",
169
+ "QUANTUM NATIONALISM",
170
+ "THERE IS NO SUCH THING AS TIME",
171
+ "IS OUR BANANA CORRECT?",
172
+ "WOULD YOU RATHER:\n\nWALLACE HAMMERING FOR AN HOUR\n\nOR\n\nA BRIEF AL-QAEDA MOMENT?",
173
+ "THE CONSPIRACY GROWS",
174
+ "YOU'RE LOOKING GUILTIER BY THE SECOND",
175
+ "I HAD TO MAKE SOMETHING, BUT I ONLY DESTROY",
176
+ "QUANTUM CRY FOR HELP",
177
+ "QB SOS",
178
+ "UNFORUNATELY THE JAVA VERSION ON EUREKA IS TOO OLD TO RUN MINECRAFT", # MW
179
+ "NOBODY CARES, ESPECIALLY ME",
180
+ "GEOGRAPHICALLY-CHALLENGED",
181
+ "I GUESS THAT'S ONE GOOD THING ABOUT FASCISTS",
182
+ "TESTICLES HAVE SO MUCH POTENTIAL",
183
+ "WHENEVER IN DOUBT, GIVE LOUIE A SHOUT",
184
+ "IF IN MOGADISHU, TELL LOUIE 'I MISS YOU'",
185
+ "IF YOU'RE IN A PICKLE,\nGIVE LOUIE A TICKLE",
186
+ "DOORS - UNLIKE PEOPLE - CAN BE UNFUCKED", # MW
187
+ "ARE YOU GOING TO GO TO HIS OFFICE, LIFT HIS ARMS UP\nAND GET YOUR FINGERS IN THERE?",
188
+ "I CANNOT RIPEN MY OWN BANANA",
189
+ "I DO BELIEVE!",
190
+ "GREAT MINDS THINK THE SAME JUNK",
191
+ """HI EVERYONE.\nHOW DO YOU KEEP WORKING WITH A COLLABORATOR IF\nYOUR LAST MEETING ENDED WITH YOU SAYING\n\n"SPARE ME THIS USELESS AND AGGRESSIVE RHETORIC,\n'CAUSE I DON'T NEED IT, OR CARE ABOUT IT"?""",
192
+ "LUCKILY SCIENCE AND DFT CALCULATIONS,\nDO NOT CARE ABOUT YOUR IMPRESSION OF THEM", # FB
193
+ "I WAS ONLY DOING AN IMPRESSION OF A NONCE!",
194
+ "I GUESS WE ALL LEAVE AS FLESH",
195
+ "SHOULD I BUY AN END OF LIFE PLANNER FOR MY PHD?", # EN
196
+ "WE NEED MORE MONEY TO\nMAKE STUDENTS SUFFER",
197
+ "I WON'T TELL YOU WHAT TO DO,\nI'LL JUST JUDGE YOU",
198
+ "NO, YOU DO NOT NEED TO STAY IN QB.\nAND YOU MIGHT NOT WANT TO DO SO",
199
+ "SHIT CODE IS THE FIRST STEP TO NOT SHIT CODE", # MW
200
+ "GRANTS MAKE IT RAIN!",
201
+ "YOU CAN'T GET DRUNK ON PISS",
202
+ "CAN'T GET CANCELLED IF YOU'RE NOT FAMOUS",
203
+ "I'M STARTING TO BELIEVE THESE ARE NOT REAL CONFUCIUS QUOTES", # MS
204
+ "WOULD YOU RATHER MAKE LOVE TO AN EYE OR A FOOT?", # unknown @ MMC 2023
205
+ "QUANTUM PHYSICS MEANS THAT\nANYTHING CAN HAPPEN AT ANY TIME,\nFOR NO REASON", # Futurama
206
+ "I WOULD BE SCHLOBBIN ON SOMEONE'S KNOB FOR THAT", # LS
207
+ "THERE IS NO GREAT LOVE, WITHOUT GREAT JEALOUSY", # Futurama
208
+ "THE BAD BOYS WON'T WAIT ALL NIGHT", # MW @ ABC 2023
209
+ "YOU WON'T BELIEVE WHAT THIS OLD ITALIAN MAN PUT IN MY MIND",
210
+ "THERE'S NOTHING NATURAL ABOUT AVERAGING COORDINATES", # ABC 2023
211
+ "ONIONS DON'T HURT ME",
212
+ "THAT WAS BEFORE I DISCOVERED TURKISH MEN", # RG @ ABC 2023
213
+ "ALL COSTS ARE SUBSTANTIAL",
214
+ "ALWAYS A GOOD IDEA TO BLAME THE FRENCH",
215
+ "CHEESE, SEX AND ALIENS", # about a museum in Basel
216
+ "YOU HAVE TO ACCEPT YOUR MISERY",
217
+ "COSTA RICA WAS A SIMULATION WITH THE WRONG FORCE FIELD", # Leandro @ ABC 2023
218
+ "LOW ENTROPY REGION OF NERDS",
219
+ "THANK YOU FOR BEING WILLING", # MW @ ABC 2023
220
+ "HELLO STALKERS, COME TO ME",
221
+ "YOU'RE ALWAYS ON THE RECEIVING END OF THE MOOSE SHAFTING",
222
+ "IT'S A COCKTAIL OF SHIT",
223
+ "F*CK HIM, CARVE YOUR OWN PATH", # LS about Löwdin
224
+ "I AM A STRAIGHT WHITE WHALE",
225
+ "TO KILL A MONKEYBIRD",
226
+ "LAZINESS LEADS TO THE ULTIMATE CREATIVITY",
227
+ "DO YOU KNOW HOW TO READ [bold]FACIAL[/bold] LIPS?",
228
+ "THERE'S TOO MANY BALD BASTARDS",
229
+ "MAYBE YOU'RE THE D&D CHARACTER OF SOMEONE ELSE,\nAND THEY ARE DOING A REALLY BAD JOB",
230
+ "THIS IS NOT AN ACT OF GOD,\nIT IS PITY",
231
+ "FREUD'S THEORY ON GRAPEFRUITING YOUR DAD",
232
+ "NORMALLY IT'S SHIT.\n\nBUT TODAY, IT'S [bold]EXCEPTIONALLY[/bold] SHIT",
233
+ "WHAT ARE THESE REVOLUTIONARY LOOKING FIGURES?\nAH YES, HEXAGONS",
234
+ "IF YOU ABUSE THEM, THEY'LL MOVE",
235
+ "IT'S NOT SEXUAL HARASSMENT, IT'S A COMPLIMENT",
236
+ "NOT A GOOD CRAZY",
237
+ "EVENTUALLY YOU HAVE TO CARE ABOUT [bold]SOMETHING[/bold]",
238
+ "DON'T PUSH THE RIVER", # MW
239
+ "THE DILDO OF CONSEQUENCES RARELY ARRIVES PRE-LUBED", # REDDIT
240
+ "IT'S ALL UNDER CONTROL", # TH
241
+ "THERE IS NO FUN WITHOUT VIOLENCE", # GF
242
+ "AN UBER IS TRANSIENT BUT SWIM TRUNKS ARE FOREVER", # MW
243
+ "IT'S NOT STEALING IF YOU PIONEER SOMETHING", # MW
244
+ "THE SUFFERING IS OPTIONAL",
245
+ "THAT'S THANKS TO LOUIE, AIMEE,\nAND - IT PAINS ME TO SAY IT - GEORGE", # alex jones @ QuEBS 2023
246
+ "HOME IS WHERE YOUR CHEEKS IMPRESS", # MW
247
+ "YOUR NUTS ARE DOING A REAL JOB ON MY THROAT", # MW
248
+ "VICIOUS CIRCLE OF BOLLOCKING", # RL, XChem, Aug 2023
249
+ "CROWDS GATHER TO WITNESS FIRST PANINI ARRIVE IN IRELAND", # Waterford Whispers News
250
+ "BLESSED ARE THE TOASTED SANDWICH MAKERS", # Waterford Whispers News
251
+ "BEEF JESUS DIED FOR OUR SINS", # MW
252
+ "I'M NOT IN A PICKLE, I'M IN A POTENTIAL WELL",
253
+ "THE ELECTRON IDENTITY CRISIS",
254
+ "TOY STORY IS A SOCIAL COMMENTARY ON THE MEASUREMENT PROBLEM",
255
+ "DO YOU THINK SOME STUDENTS JUST AREN'T INTELLIGENT ENOUGH?",
256
+ "YOU DON'T NEED A DEBUGGER\nYOU NEED A PRIEST", # MW
257
+ "IF THE PAPER YOU WANT TO READ DOESN'T EXIST, WRITE IT", # MW
258
+ "THE WORLD IS YOUR HOSTAGE", #
259
+ ]
@@ -1,90 +0,0 @@
1
- from typer import Typer
2
- from .layout import dual_layout
3
- from rich.live import Live
4
- import time
5
- from .console import console
6
- from .tools import curry
7
- from .slurm import get_layout_pair, get_node_layout, get_hist_layout
8
-
9
- # set up singletons
10
- app = Typer()
11
-
12
- # main CLI command
13
-
14
-
15
- @app.command()
16
- def show(
17
- user: None | str = None,
18
- long: bool = False,
19
- idle: bool = False,
20
- loop: bool = True,
21
- hist: int | None = None,
22
- hist_unit: str = "weeks",
23
- # screen: bool = True,
24
- # disappear: bool = True,
25
- ):
26
-
27
- screen = True
28
- disappear = not screen
29
-
30
- kwargs = {
31
- "user": user,
32
- "long": long,
33
- "idle": idle,
34
- "loop": loop,
35
- "hist": hist,
36
- "hist_unit": hist_unit,
37
- "screen": screen,
38
- "disappear": disappear,
39
- }
40
-
41
- # console.print(kwargs)
42
-
43
- match (bool(idle), bool(hist)):
44
- case (True, False):
45
- loop = False
46
- layout_func = get_node_layout
47
- case (False, True):
48
- loop = False
49
- layout_func = get_hist_layout
50
- case (False, False):
51
- layout_func = curry(dual_layout, get_layout_pair)
52
- case _:
53
- raise Exception("Unsupported CLI options")
54
-
55
- # live updating layout
56
- if loop:
57
-
58
- layout = layout_func(**kwargs)
59
-
60
- with Live(
61
- layout,
62
- refresh_per_second=4,
63
- screen=screen,
64
- transient=disappear,
65
- vertical_overflow="visible",
66
- ) as live:
67
-
68
- try:
69
- while True:
70
- layout = layout_func(**kwargs)
71
- live.update(layout)
72
- time.sleep(1)
73
- except KeyboardInterrupt:
74
- live.stop()
75
-
76
- # static layout
77
- else:
78
-
79
- layout = layout_func(**kwargs)
80
- console.print(layout)
81
-
82
-
83
- # start Typer app
84
- def main():
85
- app()
86
-
87
-
88
- # start Typer app
89
- if __name__ == "__main__":
90
- app()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes