44from dataclasses import dataclass
55from datetime import datetime , timedelta , timezone
66from typing import List , Optional , Set , Tuple
7+ from ipydatagrid import DataGrid , TextRenderer , Expr
78from ipywidgets import Box , HTML , Layout
89from IPython .display import display
10+ import pandas as pd
911
1012import requests
1113
@@ -291,6 +293,9 @@ def get_nodes(
291293 return nodes
292294
293295
296+
297+
298+
294299def _parse_blazar_dt (datetime_string ):
295300 d = datetime .strptime (datetime_string , "%Y-%m-%dT%H:%M:%S.%f" )
296301 return d .replace (tzinfo = timezone .utc )
@@ -313,6 +318,85 @@ def get_node_types() -> List[str]:
313318 get_nodes ()
314319 return list (set (node_types ))
315320
321+ def _reservable_color (cell ):
322+ return "#a2d9fe" if cell .value else "#f69084"
323+
324+ def _gpu_background_color (cell ):
325+ return "#d3d3d3" if not cell .value else None
326+
327+ def show_nodes (
328+ nodes : Optional [List [Node ]] = None
329+ ) -> None :
330+ """
331+ Display a sortable, filterable table of available nodes.
332+
333+ Args:
334+ nodes (Optional[List[Node]], optional): A list of Node objects to display.
335+ If not provided, defaults to the output of hardware.get_nodes().
336+
337+ Returns:
338+ None
339+ """
340+
341+ def estimate_column_width (df , column , char_px = 7 , padding = 0 ):
342+ if column not in df .columns :
343+ raise ValueError (f"Column '{ column } ' not found in DataFrame." )
344+ max_chars = df [column ].astype (str ).map (len ).max ()
345+ return max (max_chars * char_px + padding , 80 )
346+
347+ if not nodes :
348+ nodes = get_nodes ()
349+
350+ rows = []
351+ for n in nodes :
352+ rows .append ({
353+ "Node Name" : n .name ,
354+ "Type" : n .type ,
355+ "Clock Speed (GHz)" : round (n .cpu .get ('clock_speed' , 0 ) / 1e9 , 2 ),
356+ "RAM" : n .main_memory .get ('humanized_ram_size' , 'N/A' ),
357+ "GPU Model" : (n .gpu or {}).get ('gpu_model' ) or "" ,
358+ "GPU Count" : (n .gpu or {}).get ('gpu_count' ) or "" ,
359+ "Storage Size" : n .storage_devices [0 ].get ('humanized_size' , 'N/A' ) if n .storage_devices else 'N/A' ,
360+ "Site" : n .site ,
361+ "Reservable" : bool (n .reservable ),
362+ })
363+
364+ df = pd .DataFrame (rows )
365+ renderers = {
366+ "Reservable" : TextRenderer (
367+ text_color = "black" ,
368+ background_color = Expr (_reservable_color ),
369+ ),
370+ "GPU Model" : TextRenderer (
371+ background_color = Expr (_gpu_background_color ),
372+ ),
373+ "GPU Count" : TextRenderer (
374+ background_color = Expr (_gpu_background_color ),
375+ )
376+ }
377+
378+ grid = DataGrid (
379+ df ,
380+ layout = Layout (height = "400px" ),
381+ selection_mode = "row" ,
382+ renderers = renderers ,
383+ column_widths = {
384+ "Node Name" : int (estimate_column_width (df , "Node Name" )),
385+ "Site" : int (estimate_column_width (df , "Site" )),
386+ "Type" : int (estimate_column_width (df , "Type" )),
387+ "RAM" : int (estimate_column_width (df , "RAM" )),
388+ "Storage Size" : int (estimate_column_width (df , "Storage Size" )),
389+ "Clock Speed (GHz)" : 55 ,
390+ "GPU Model" : 90 ,
391+ "GPU Count" : 30 ,
392+ "key" : 30 ,
393+ "Reservable" : 55
394+ },
395+
396+ df = pd .DataFrame (rows )
397+ )
398+
399+ display (grid )
316400
317401@dataclass
318402class Device :
0 commit comments