nodeocc 1.0.25__tar.gz → 1.0.26__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.
Files changed (23) hide show
  1. {nodeocc-1.0.25 → nodeocc-1.0.26}/PKG-INFO +1 -1
  2. {nodeocc-1.0.25 → nodeocc-1.0.26}/pyproject.toml +1 -1
  3. {nodeocc-1.0.25 → nodeocc-1.0.26}/src/controller/controller.py +29 -29
  4. {nodeocc-1.0.25 → nodeocc-1.0.26}/src/nodeocc.egg-info/PKG-INFO +1 -1
  5. {nodeocc-1.0.25 → nodeocc-1.0.26}/src/readers/slurmreader.py +16 -6
  6. {nodeocc-1.0.25 → nodeocc-1.0.26}/src/view/__init__.py +7 -1
  7. {nodeocc-1.0.25 → nodeocc-1.0.26}/src/view/curses_multiwindow.py +17 -6
  8. {nodeocc-1.0.25 → nodeocc-1.0.26}/src/view/slurm_list.py +1 -1
  9. {nodeocc-1.0.25 → nodeocc-1.0.26}/src/view/slurm_viz.py +21 -6
  10. {nodeocc-1.0.25 → nodeocc-1.0.26}/README.md +0 -0
  11. {nodeocc-1.0.25 → nodeocc-1.0.26}/setup.cfg +0 -0
  12. {nodeocc-1.0.25 → nodeocc-1.0.26}/setup.py +0 -0
  13. {nodeocc-1.0.25 → nodeocc-1.0.26}/src/__init__.py +0 -0
  14. {nodeocc-1.0.25 → nodeocc-1.0.26}/src/model/infrastructure.py +0 -0
  15. {nodeocc-1.0.25 → nodeocc-1.0.26}/src/model/job.py +0 -0
  16. {nodeocc-1.0.25 → nodeocc-1.0.26}/src/nodeocc.egg-info/SOURCES.txt +0 -0
  17. {nodeocc-1.0.25 → nodeocc-1.0.26}/src/nodeocc.egg-info/dependency_links.txt +0 -0
  18. {nodeocc-1.0.25 → nodeocc-1.0.26}/src/nodeocc.egg-info/entry_points.txt +0 -0
  19. {nodeocc-1.0.25 → nodeocc-1.0.26}/src/nodeocc.egg-info/requires.txt +0 -0
  20. {nodeocc-1.0.25 → nodeocc-1.0.26}/src/nodeocc.egg-info/top_level.txt +0 -0
  21. {nodeocc-1.0.25 → nodeocc-1.0.26}/src/readers/compute_wait_time.py +0 -0
  22. {nodeocc-1.0.25 → nodeocc-1.0.26}/src/view/styles.py +0 -0
  23. {nodeocc-1.0.25 → nodeocc-1.0.26}/src/view/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nodeocc
3
- Version: 1.0.25
3
+ Version: 1.0.26
4
4
  Summary: TUI application for viewing the status of GPU allocations on a Slurm cluster
5
5
  Author: Matteo Boschini
6
6
  Maintainer: Lorenzo Bonicelli, Aniello Panariello
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nodeocc"
7
- version = "1.0.25"
7
+ version = "1.0.26"
8
8
  authors = [{ name = "Matteo Boschini" }]
9
9
  maintainers = [{ name = "Lorenzo Bonicelli" }, { name = "Aniello Panariello" }]
10
10
  description = "TUI application for viewing the status of GPU allocations on a Slurm cluster"
@@ -24,6 +24,8 @@ import json
24
24
  import argparse
25
25
  import traceback
26
26
  import socket
27
+ import re
28
+ from urllib.request import urlopen
27
29
  from model.infrastructure import Infrastructure
28
30
  from model.job import Job
29
31
  import asyncio
@@ -31,36 +33,28 @@ import importlib.metadata
31
33
 
32
34
  program_name = 'nodeocc'
33
35
  version_number = importlib.metadata.version(program_name)
34
- updated = False
35
36
 
36
- BBLUE = '\033[1;34m'
37
- END = '\033[0m'
38
- try:
39
- url = "https://pypi.python.org/pypi/nodeocc/json"
40
- import requests
41
- newest_version = json.loads(requests.get(url).text)['info']['version']
42
- print(BBLUE, end='')
43
- if int(newest_version.split('.')[0]) > int(version_number.split('.')[0]):
44
- print(f"New major version available: {newest_version} (current: {version_number})")
45
- print(f"Please update with 'pip install {program_name} --upgrade'")
46
- time.sleep(7)
47
- elif int(newest_version.split('.')[1]) > int(version_number.split('.')[1]):
48
- print(f"New minor version available: {newest_version} (current: {version_number})")
49
- print(f"Please update with 'pip install {program_name} --upgrade'")
50
- time.sleep(7)
51
- elif int(newest_version.split('.')[2]) > int(version_number.split('.')[2]):
52
- print(f"New patch version available: {newest_version} (current: {version_number})")
53
- print(f"Please update with 'pip install {program_name} --upgrade'")
54
- time.sleep(7)
55
- else:
56
- print(f"Up to date: {version_number}")
57
- updated = True
58
- time.sleep(3)
59
- print(END, end='')
60
- except Exception as e:
61
- print(f"{BBLUE}Could not get newest version from pip, check if {program_name} is up to date{END}")
62
- newest_version = '?.?.?'
63
- time.sleep(5)
37
+
38
+ def _version_tuple(v):
39
+ return tuple(int(x) for x in re.findall(r'\d+', str(v)))
40
+
41
+
42
+ def check_latest_version(cur_version, timeout_sec=1.2):
43
+ """
44
+ Fast, non-blocking-ish version check:
45
+ - short timeout
46
+ - no startup sleeps/pauses
47
+ - graceful fallback to current version on errors
48
+ """
49
+ try:
50
+ url = f"https://pypi.python.org/pypi/{program_name}/json"
51
+ with urlopen(url, timeout=timeout_sec) as response:
52
+ payload = json.loads(response.read().decode('utf-8'))
53
+ newest = payload['info']['version']
54
+ is_updated = _version_tuple(cur_version) >= _version_tuple(newest)
55
+ return newest, is_updated
56
+ except Exception:
57
+ return cur_version, True
64
58
 
65
59
  BEGIN_DELIM = "!{$"
66
60
  END_DELIM_ENCODED = "!}$".encode('utf-8')
@@ -81,6 +75,12 @@ args = parser.parse_args()
81
75
  # check basepath is readable
82
76
  assert os.access(args.basepath, os.R_OK), f"Base path {args.basepath} not readable"
83
77
 
78
+ # Keep startup snappy for daemon/diagnostics; check updates only in interactive mode.
79
+ if args.master_only or args.diag:
80
+ newest_version, updated = version_number, True
81
+ else:
82
+ newest_version, updated = check_latest_version(version_number)
83
+
84
84
  # export args
85
85
  orig_instance = Singleton.getInstance(args)
86
86
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nodeocc
3
- Version: 1.0.25
3
+ Version: 1.0.26
4
4
  Summary: TUI application for viewing the status of GPU allocations on a Slurm cluster
5
5
  Author: Matteo Boschini
6
6
  Maintainer: Lorenzo Bonicelli, Aniello Panariello
@@ -57,9 +57,16 @@ def _parse_gpu_count(gres):
57
57
  token = token.strip()
58
58
  if not token.lower().startswith('gpu'):
59
59
  continue
60
- last_field = token.split(':')[-1].split('(')[0]
61
- match = re.search(r'\d+', last_field)
62
- n_gpu += int(match.group(0)) if match else 1
60
+ token_core = token.split('(')[0]
61
+ parts = token_core.split(':')
62
+ count = None
63
+
64
+ if len(parts) >= 2 and re.fullmatch(r'\d+', parts[-1]):
65
+ count = int(parts[-1])
66
+ elif len(parts) >= 3 and re.fullmatch(r'\d+', parts[-2]):
67
+ count = int(parts[-2])
68
+
69
+ n_gpu += count if count is not None else 1
63
70
  return n_gpu
64
71
 
65
72
 
@@ -75,10 +82,13 @@ def _parse_gpu_models(gres):
75
82
  token = token.strip()
76
83
  if not token.lower().startswith('gpu'):
77
84
  continue
78
- parts = token.split(':')
79
- if len(parts) >= 3:
85
+ token_core = token.split('(')[0]
86
+ parts = token_core.split(':')
87
+ if len(parts) >= 3 and not re.fullmatch(r'\d+', parts[1]):
88
+ model = parts[1]
89
+ elif len(parts) == 2 and not re.fullmatch(r'\d+', parts[1]):
80
90
  model = parts[1]
81
- elif len(parts) == 2:
91
+ elif len(parts) == 2 and re.fullmatch(r'\d+', parts[1]):
82
92
  model = 'gpu'
83
93
  else:
84
94
  model = 'n/a'
@@ -13,5 +13,11 @@ def update_views(stdscr, instance, filter):
13
13
  instance, 'job_id_type') else 'agg')
14
14
  instance.timeme(f"- print job list")
15
15
 
16
- instance.nocc = slurm_viz.view_viz(instance.inf, instance.jobs, stylefn=styles.crsstyler, mode=instance.view_mode if hasattr(instance, 'view_mode') else 'gpu')
16
+ instance.nocc = slurm_viz.view_viz(
17
+ instance.inf,
18
+ instance.jobs,
19
+ stylefn=styles.crsstyler,
20
+ mode=instance.view_mode if hasattr(instance, 'view_mode') else 'gpu',
21
+ panel_width=instance.right_width if hasattr(instance, 'right_width') else None,
22
+ )
17
23
  instance.timeme(f"- view nodeocc")
@@ -35,6 +35,11 @@ BUTTON_ACTIONS = {
35
35
  'info': [ord('i')],
36
36
  }
37
37
 
38
+ RIGHT_WIDTH_GPU = 33
39
+ RIGHT_WIDTH_INFO = 39
40
+ # Keep a single stable width across modes; use the widest one so info fields are not cut.
41
+ RIGHT_WIDTH_UNIFIED = max(RIGHT_WIDTH_GPU, RIGHT_WIDTH_INFO)
42
+
38
43
 
39
44
  def is_file_writable_byall(file_path):
40
45
  return os.stat(file_path).st_mode & stat.S_IWOTH
@@ -476,7 +481,7 @@ def handle_keys(stdscr, instance):
476
481
  instance.sort_by_prio = not instance.sort_by_prio
477
482
  elif k in BUTTON_ACTIONS['res_view_mode']:
478
483
  instance.view_mode = {"gpu": "ram", "ram": "cpu", "cpu": "gpu", "info": "gpu"}[instance.view_mode]
479
- instance.right_width = 33 if instance.view_mode != 'info' else 39
484
+ instance.right_width = RIGHT_WIDTH_UNIFIED
480
485
  stdscr.clear()
481
486
  elif k in BUTTON_ACTIONS['job_id_type']:
482
487
  instance.job_id_type = "true" if instance.job_id_type == "agg" else "agg"
@@ -489,7 +494,7 @@ def handle_keys(stdscr, instance):
489
494
  instance.show_prio = not instance.show_prio
490
495
  elif k in BUTTON_ACTIONS['info']:
491
496
  instance.view_mode = 'info' if instance.view_mode != 'info' else 'gpu'
492
- instance.right_width = 33 if instance.view_mode != 'info' else 39
497
+ instance.right_width = RIGHT_WIDTH_UNIFIED
493
498
  stdscr.clear()
494
499
  elif k in BUTTON_ACTIONS['up_tab']:
495
500
  # get screen size
@@ -513,7 +518,7 @@ def update_screen(stdscr, instance):
513
518
  if instance.k in BUTTON_ACTIONS['refresh']:
514
519
  stdscr.clear()
515
520
 
516
- totsize = 106
521
+ totsize = 106 + max(0, RIGHT_WIDTH_UNIFIED - RIGHT_WIDTH_GPU)
517
522
  if instance.show_account:
518
523
  totsize += 10
519
524
  if instance.show_prio:
@@ -606,8 +611,14 @@ def update_screen(stdscr, instance):
606
611
  # get slurm user partition
607
612
  # stdscr.addstr(lines - 1, xoffset + 62 + 2, f'(Avg time {instance.avg_wait_time})', curses.color_pair(2))
608
613
 
609
- signature = instance.signature if instance.updated else f'{instance.version} -> {instance.newest_version}'
610
- stdscr.addstr(lines - 1, columns - 2 - len(signature), signature, curses.color_pair(2) if instance.updated else curses.color_pair(3))
614
+ if instance.updated:
615
+ signature = instance.signature
616
+ sig_style = curses.color_pair(2)
617
+ else:
618
+ signature = f'{instance.signature} (new {instance.newest_version})'
619
+ sig_style = curses.color_pair(4)
620
+
621
+ stdscr.addstr(lines - 1, columns - 2 - len(signature), signature, sig_style)
611
622
 
612
623
  stdscr.refresh()
613
624
  curses.doupdate()
@@ -712,7 +723,7 @@ async def main(stdscr):
712
723
  # status
713
724
 
714
725
  instance.a_filter = 0
715
- instance.right_width = 33
726
+ instance.right_width = RIGHT_WIDTH_UNIFIED
716
727
 
717
728
  stdscr.clear()
718
729
 
@@ -44,7 +44,7 @@ def _joblet_format(instance, job, width=74, jobid_type='agg'):
44
44
  mem = round(joblet.mem / 1024)
45
45
  joblet_repr += _format_to(f'{mem}G', 4, 'right')
46
46
  joblet_repr += ' '
47
- if instance.view_mode in ('gpu', 'ram'):
47
+ if instance.view_mode in ('gpu', 'ram', 'info'):
48
48
  joblet_repr += _format_to((f'{joblet.n_gpus}gp' if joblet.n_gpus > 0 else " - "), 3, 'left')
49
49
  elif instance.view_mode == 'cpu':
50
50
  joblet_repr += _format_to((f'{joblet.cpus}cp' if joblet.cpus > 0 else " - "), 3, 'left')
@@ -101,6 +101,20 @@ cpu_paused = '▚'
101
101
  cpu_down = '⨯'
102
102
 
103
103
  GLOBAL_NODES = None
104
+ BASE_PANEL_WIDTH = 33
105
+
106
+
107
+ def _center_panel_content(text, panel_width, base_width=BASE_PANEL_WIDTH):
108
+ if panel_width is None or panel_width <= base_width:
109
+ return text
110
+
111
+ pad = max(0, (panel_width - base_width) // 2)
112
+ if pad == 0:
113
+ return text
114
+
115
+ lines = text.split('\n')
116
+ lines = [(' ' * pad + line) if line else line for line in lines]
117
+ return '\n'.join(lines)
104
118
 
105
119
 
106
120
  def get_ram_block(megs):
@@ -138,15 +152,15 @@ def get_node_status(node, jobs):
138
152
  return None # white?
139
153
 
140
154
 
141
- def view_viz(infrastructure, jobs, work=True, stylefn=cmdstyle, current_user=None, mode='gpu'):
155
+ def view_viz(infrastructure, jobs, work=True, stylefn=cmdstyle, current_user=None, mode='gpu', panel_width=None):
142
156
  if mode == 'gpu':
143
- return view_viz_gpu(infrastructure, jobs, work, stylefn, current_user)
157
+ return _center_panel_content(view_viz_gpu(infrastructure, jobs, work, stylefn, current_user), panel_width)
144
158
  elif mode == 'ram':
145
- return view_viz_ram(infrastructure, jobs, work, stylefn, current_user)
159
+ return _center_panel_content(view_viz_ram(infrastructure, jobs, work, stylefn, current_user), panel_width)
146
160
  elif mode == 'cpu':
147
- return view_viz_cpu(infrastructure, jobs, work, stylefn, current_user)
161
+ return _center_panel_content(view_viz_cpu(infrastructure, jobs, work, stylefn, current_user), panel_width)
148
162
  elif mode == 'info':
149
- return view_viz_info(infrastructure, jobs, work, stylefn, current_user)
163
+ return view_viz_info(infrastructure, jobs, work, stylefn, current_user, panel_width)
150
164
 
151
165
 
152
166
  def view_viz_ram(infrastructure, jobs, work=True, stylefn=cmdstyle, current_user=None):
@@ -519,7 +533,7 @@ def view_viz_cpu(infrastructure, jobs, work=True, stylefn=cmdstyle, current_user
519
533
  return RetScope.return_string
520
534
 
521
535
 
522
- def view_viz_info(infrastructure, jobs, work=True, stylefn=cmdstyle, current_user=None):
536
+ def view_viz_info(infrastructure, jobs, work=True, stylefn=cmdstyle, current_user=None, panel_width=None):
523
537
  # this is for hot reload
524
538
  if not work:
525
539
  return "UPDATE IN PROGRESS - PLZ W8 M8 B8"
@@ -555,6 +569,7 @@ def view_viz_info(infrastructure, jobs, work=True, stylefn=cmdstyle, current_use
555
569
  # max_ram_len = max([len(str(n.mem // 1024)) for n in nodes])
556
570
  feat_str = stylefn("GREEN", "FEATURES")
557
571
  RetScope.return_string += f'{"":{gpu_name_chars}} GPU {feat_str}\n'
572
+
558
573
  for i, n in GLOBAL_NODES.iterrows():
559
574
  gpu_model = n.m_gpu # if n.m_gpu is not None else 'N/A'
560
575
  # gpu_mem = n.gpu_mem if n.gpu_mem is not None else 'N/A'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes