qq_lib.nodes
Provides node presentation utilities.
This module organizes and formats information about compute nodes as reported by the batch system, preparing it for human-readable terminal output.
Internal grouping logic clusters nodes with similar naming patterns, extracts
shared attributes, and aggregates resource and property data. These groups are
then rendered by NodesPresenter, which produces a unified panel showing node
availability, CPU/GPU capacities, scratch resources, and other relevant metrics.
1# Released under MIT License. 2# Copyright (c) 2025-2026 Ladislav Bartos and Robert Vacha Lab 3 4""" 5Provides node presentation utilities. 6 7This module organizes and formats information about compute nodes as reported by 8the batch system, preparing it for human-readable terminal output. 9 10Internal grouping logic clusters nodes with similar naming patterns, extracts 11shared attributes, and aggregates resource and property data. These groups are 12then rendered by `NodesPresenter`, which produces a unified panel showing node 13availability, CPU/GPU capacities, scratch resources, and other relevant metrics. 14""" 15 16from .presenter import NodesPresenter 17 18__all__ = ["NodesPresenter"]
396class NodesPresenter: 397 """ 398 Presenter class for displaying information about batch system nodes. 399 """ 400 401 def __init__( 402 self, nodes: list[BatchNodeInterface], user: str, all: bool, server: str | None 403 ): 404 """ 405 Initialize the presenter with a list of nodes. 406 407 Args: 408 nodes (list[BatchNodeInterface]): List of node information objects 409 to be presented. 410 user (str): Name of the user for which the nodes are displayed. 411 all (boolean): Display all nodes or only those that are available. 412 server (str | None): Batch server for which the nodes were collected. 413 `None` = default server. 414 """ 415 self._nodes = nodes 416 self._user = user 417 self._display_all = all 418 self._server = server 419 420 self._node_groups = self._create_node_groups() 421 422 def dump_yaml(self) -> None: 423 """ 424 Print the YAML representation of all nodes to stdout. 425 """ 426 for node in self._nodes: 427 print(node.to_yaml()) 428 429 def create_nodes_info_panel(self, console: Console | None = None) -> Group: 430 """ 431 Build a complete Rich panel summarizing all node groups. 432 433 Args: 434 console (Console | None): Optional Rich console instance used to 435 determine available terminal width. If None, a new console 436 is created. 437 438 Returns: 439 Group: A Rich `Group` containing the formatted node information panel. 440 """ 441 console = console or Console() 442 443 groups = self._node_groups 444 parts = [g.create_full_info_panel() for g in self._node_groups] 445 seps = [NodesPresenter._create_separator(g.name) for g in self._node_groups[1:]] 446 447 content: list[Group] = NodesPresenter._interleave(parts, seps) 448 449 if len(groups) > 1: 450 content.append(self._create_metadata_panel()) 451 452 panel = Panel( 453 Group(*content), 454 title=Text( 455 f"NODE GROUP: {groups[0].name}", 456 style=CFG.nodes_presenter.title_style, 457 justify="center", 458 ), 459 subtitle=Text( 460 f"{self._server}", 461 style=CFG.jobs_presenter.subtitle_style, 462 justify="center", 463 ) 464 if self._server is not None 465 else None, 466 border_style=CFG.nodes_presenter.border_style, 467 padding=(1, 1), 468 width=get_panel_width( 469 console, 1, CFG.nodes_presenter.min_width, CFG.nodes_presenter.max_width 470 ), 471 expand=False, 472 ) 473 474 return Group(Text(""), panel, Text("")) 475 476 def _create_metadata_panel(self) -> Group: 477 """ 478 Create a summary panel displaying overall statistics across all node groups. 479 480 Returns: 481 Group: A Rich `Group` containing the overall statistics summary. 482 """ 483 total_stats = NodeGroupStats.sum_stats(*(g.stats for g in self._node_groups)) 484 485 return Group( 486 "", 487 Rule( 488 title=Text( 489 "OVERALL STATISTICS", 490 style=CFG.nodes_presenter.title_style, 491 ), 492 style=CFG.nodes_presenter.rule_style, 493 ), 494 "", 495 NodesPresenter._format_metadata_table( 496 list(total_stats.properties), "All properties", total_stats 497 ), 498 ) 499 500 def _create_node_groups(self) -> list[NodeGroup]: 501 """ 502 Organize nodes into logical groups based on common name prefixes. 503 504 Nodes sharing the same alphabetic prefix are grouped together (e.g., 505 `node1`, `node2`, `node3` form one group). Groups with fewer than three 506 nodes are merged into a generic "others" group. 507 508 Returns: 509 list[NodeGroup]: A list of node groups created from the input nodes. 510 """ 511 raw_groups = defaultdict(list) 512 unassigned = [] 513 514 # get nodes with same names 515 for node in self._nodes: 516 name = node.get_name() 517 match = re.match(r"[A-Za-z]+", name) 518 prefix = match.group(0) if match else "" 519 raw_groups[prefix].append(node) 520 521 # create node groups; each node group must have at least 3 members 522 groups: list[NodeGroup] = [] 523 for prefix, nodes in raw_groups.items(): 524 if len(nodes) >= 3: 525 groups.append(NodeGroup(prefix, nodes, self._user)) 526 else: 527 unassigned.extend(nodes) 528 529 # create a node group for the unassigned nodes 530 if unassigned: 531 groups.append( 532 NodeGroup( 533 CFG.nodes_presenter.others_group_name 534 if len(groups) > 0 535 else CFG.nodes_presenter.all_nodes_group_name, 536 unassigned, 537 self._user, 538 ) 539 ) 540 541 return groups 542 543 def _create_separator(title: str) -> Group: 544 """ 545 Create a visual separator between node group panels. 546 547 Args: 548 title (str): The title to display within the rule separator. 549 550 Returns: 551 Group: A Rich `Group` containing a rule with the given title. 552 """ 553 return Group( 554 "", 555 Rule( 556 title=Text( 557 f"NODE GROUP: {title}", style=CFG.nodes_presenter.title_style 558 ), 559 style=CFG.nodes_presenter.rule_style, 560 ), 561 "", 562 ) 563 564 def _interleave(sections: list[Group], seps: list[Group]) -> list[Group]: 565 """ 566 Interleave content sections with separator elements. 567 568 Args: 569 sections (list[Group]): Panels or sections to display. 570 seps (list[Group]): Separators to insert between sections. 571 572 Returns: 573 list[Group]: Combined ordered list of sections and separators. 574 """ 575 chunks = [] 576 for part, sep in zip_longest(sections, seps, fillvalue=None): 577 if part is not None: 578 chunks.append(part) 579 if sep is not None: 580 chunks.append(sep) 581 return chunks 582 583 @staticmethod 584 def _format_processing_units(free: int, total: int, available: bool) -> Text: 585 """ 586 Format numbers of free and total CPUs or GPUs as a styled Rich text element. 587 588 Args: 589 free (int): Number of free units (e.g., CPUs or GPUs). 590 total (int): Total number of units. 591 available (bool): Whether the node is available to the user. 592 593 Returns: 594 Text: A styled Rich text element showing free and total counts. 595 """ 596 if not available: 597 style = CFG.nodes_presenter.unavailable_node_style 598 elif total == 0: 599 style = CFG.nodes_presenter.main_text_style 600 elif total == free: 601 style = CFG.nodes_presenter.free_node_style 602 elif free > 0: 603 style = CFG.nodes_presenter.part_free_node_style 604 else: 605 style = CFG.nodes_presenter.busy_node_style 606 607 return Text(f"{free} / {total}", style=style) 608 609 @staticmethod 610 def _format_size_property(free: Size, total: Size, style: str) -> Text: 611 """ 612 Format a memory or storage property as a styled text string. 613 614 Args: 615 free (Size): Available memory or storage. 616 total (Size): Total memory or storage. 617 style (str): Rich style string to apply to the text. 618 619 Returns: 620 Text: A formatted text element showing free and total capacity. 621 """ 622 return Text(f"{free} / {total}", style=style) 623 624 @staticmethod 625 def _format_node_properties( 626 props: list[str], shared_props: list[str], style: str 627 ) -> Text: 628 """ 629 Format node-specific properties for display, excluding shared ones. 630 631 Args: 632 props (list[str]): All properties of the node. 633 shared_props (list[str]): Properties common to all nodes in the group. 634 style (str): Rich text style for formatting. 635 636 Returns: 637 Text: Comma-separated list of unique node properties. 638 """ 639 return Text(", ".join(x for x in props if x not in shared_props), style=style) 640 641 @staticmethod 642 def _format_state_mark( 643 free_cpus: int, 644 total_cpus: int, 645 free_gpus: int, 646 total_gpus: int, 647 available: bool, 648 ) -> Text: 649 """ 650 Generate a state mark symbol indicating node utilization and availability. 651 652 Args: 653 free_cpus (int): Number of free CPU cores. 654 total_cpus (int): Total number of CPU cores. 655 free_gpus (int): Number of free GPUs. 656 total_gpus (int): Total number of GPUs. 657 available (bool): Whether the node is accessible to the user. 658 659 Returns: 660 Text: A styled Rich text symbol representing node state. 661 """ 662 if not available: 663 style = CFG.nodes_presenter.unavailable_node_style 664 elif free_cpus == total_cpus and free_gpus == total_gpus: 665 style = CFG.nodes_presenter.free_node_style 666 elif free_cpus != 0 or free_gpus != 0: 667 style = CFG.nodes_presenter.part_free_node_style 668 else: 669 style = CFG.nodes_presenter.busy_node_style 670 671 return Text(CFG.nodes_presenter.state_mark, style=style) 672 673 @staticmethod 674 def _format_properties_section(props: list[str], title: str) -> Text: 675 """ 676 Create a formatted text section showing a list of properties. 677 678 Args: 679 props (list[str]): Properties to display. 680 title (str): Section label to prefix before the property list. 681 682 Returns: 683 Text: A Rich text object combining the title and formatted property list. 684 """ 685 return Text( 686 f"{title}: ", 687 style=f"{CFG.nodes_presenter.main_text_style} bold", 688 ) + Text( 689 (", ".join(sorted(props))).ljust(CFG.nodes_presenter.max_props_panel_width), 690 style=f"{CFG.nodes_presenter.main_text_style} not bold", 691 ) 692 693 @staticmethod 694 def _format_metadata_table( 695 props: list[str], title: str, stats: NodeGroupStats 696 ) -> Table: 697 """ 698 Create a metadata table showing a list of (shared/all) properties 699 and an aggregated statistics summary (CPU, GPU, node counts). 700 701 Args: 702 props (list[str]): List of properties to display. 703 title (str): Title label for the property section. 704 stats (NodeGroupStats): Aggregated statistics for the corresponding node group. 705 706 Returns: 707 Table: A Rich grid table with property and statistics columns. 708 """ 709 grid = Table.grid(expand=False, padding=(0, 5)) 710 grid.add_column(max_width=CFG.nodes_presenter.max_props_panel_width) 711 grid.add_column() 712 713 grid.add_row( 714 NodesPresenter._format_properties_section(props, title), 715 stats.create_stats_table(), 716 ) 717 718 return grid
Presenter class for displaying information about batch system nodes.
401 def __init__( 402 self, nodes: list[BatchNodeInterface], user: str, all: bool, server: str | None 403 ): 404 """ 405 Initialize the presenter with a list of nodes. 406 407 Args: 408 nodes (list[BatchNodeInterface]): List of node information objects 409 to be presented. 410 user (str): Name of the user for which the nodes are displayed. 411 all (boolean): Display all nodes or only those that are available. 412 server (str | None): Batch server for which the nodes were collected. 413 `None` = default server. 414 """ 415 self._nodes = nodes 416 self._user = user 417 self._display_all = all 418 self._server = server 419 420 self._node_groups = self._create_node_groups()
Initialize the presenter with a list of nodes.
Arguments:
- nodes (list[BatchNodeInterface]): List of node information objects to be presented.
- user (str): Name of the user for which the nodes are displayed.
- all (boolean): Display all nodes or only those that are available.
- server (str | None): Batch server for which the nodes were collected.
None= default server.
422 def dump_yaml(self) -> None: 423 """ 424 Print the YAML representation of all nodes to stdout. 425 """ 426 for node in self._nodes: 427 print(node.to_yaml())
Print the YAML representation of all nodes to stdout.
429 def create_nodes_info_panel(self, console: Console | None = None) -> Group: 430 """ 431 Build a complete Rich panel summarizing all node groups. 432 433 Args: 434 console (Console | None): Optional Rich console instance used to 435 determine available terminal width. If None, a new console 436 is created. 437 438 Returns: 439 Group: A Rich `Group` containing the formatted node information panel. 440 """ 441 console = console or Console() 442 443 groups = self._node_groups 444 parts = [g.create_full_info_panel() for g in self._node_groups] 445 seps = [NodesPresenter._create_separator(g.name) for g in self._node_groups[1:]] 446 447 content: list[Group] = NodesPresenter._interleave(parts, seps) 448 449 if len(groups) > 1: 450 content.append(self._create_metadata_panel()) 451 452 panel = Panel( 453 Group(*content), 454 title=Text( 455 f"NODE GROUP: {groups[0].name}", 456 style=CFG.nodes_presenter.title_style, 457 justify="center", 458 ), 459 subtitle=Text( 460 f"{self._server}", 461 style=CFG.jobs_presenter.subtitle_style, 462 justify="center", 463 ) 464 if self._server is not None 465 else None, 466 border_style=CFG.nodes_presenter.border_style, 467 padding=(1, 1), 468 width=get_panel_width( 469 console, 1, CFG.nodes_presenter.min_width, CFG.nodes_presenter.max_width 470 ), 471 expand=False, 472 ) 473 474 return Group(Text(""), panel, Text(""))
Build a complete Rich panel summarizing all node groups.
Arguments:
- console (Console | None): Optional Rich console instance used to determine available terminal width. If None, a new console is created.
Returns:
Group: A Rich
Groupcontaining the formatted node information panel.