From 3faee98ec80041a27e4c1f1156178de6f9dcdc63 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Wed, 26 Jun 2024 16:19:19 +0200 Subject: [PATCH] feat(widgets): added simple bec queue widget --- bec_widgets/cli/client.py | 19 +++ bec_widgets/widgets/bec_queue/__init__.py | 0 bec_widgets/widgets/bec_queue/bec_queue.py | 111 ++++++++++++++++++ .../widgets/bec_queue/bec_queue.pyproject | 1 + .../widgets/bec_queue/bec_queue_plugin.py | 54 +++++++++ .../widgets/bec_queue/register_bec_queue.py | 15 +++ tests/unit_tests/test_bec_queue.py | 111 ++++++++++++++++++ 7 files changed, 311 insertions(+) create mode 100644 bec_widgets/widgets/bec_queue/__init__.py create mode 100644 bec_widgets/widgets/bec_queue/bec_queue.py create mode 100644 bec_widgets/widgets/bec_queue/bec_queue.pyproject create mode 100644 bec_widgets/widgets/bec_queue/bec_queue_plugin.py create mode 100644 bec_widgets/widgets/bec_queue/register_bec_queue.py create mode 100644 tests/unit_tests/test_bec_queue.py diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py index eb1e7c0d..7175a7a1 100644 --- a/bec_widgets/cli/client.py +++ b/bec_widgets/cli/client.py @@ -13,6 +13,7 @@ class Widgets(str, enum.Enum): Enum for the available widgets. """ + BECQueue = "BECQueue" BECStatusBox = "BECStatusBox" BECDock = "BECDock" BECDockArea = "BECDockArea" @@ -1446,6 +1447,24 @@ class BECPlotBase(RPCBase): """ +class BECQueue(RPCBase): + @property + @rpc_call + def config_dict(self) -> "dict": + """ + Get the configuration of the widget. + + Returns: + dict: The configuration of the widget. + """ + + @rpc_call + def get_all_rpc(self) -> "dict": + """ + Get all registered RPC objects. + """ + + class BECStatusBox(RPCBase): @property @rpc_call diff --git a/bec_widgets/widgets/bec_queue/__init__.py b/bec_widgets/widgets/bec_queue/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bec_widgets/widgets/bec_queue/bec_queue.py b/bec_widgets/widgets/bec_queue/bec_queue.py new file mode 100644 index 00000000..52cd1b97 --- /dev/null +++ b/bec_widgets/widgets/bec_queue/bec_queue.py @@ -0,0 +1,111 @@ +from bec_lib.endpoints import MessageEndpoints +from qtpy.QtCore import Qt, Slot +from qtpy.QtWidgets import QHeaderView, QTableWidget, QTableWidgetItem, QWidget + +from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig + + +class BECQueue(BECConnector, QTableWidget): + """ + Widget to display the BEC queue. + """ + + def __init__( + self, + parent: QWidget | None = None, + client=None, + config: ConnectionConfig = None, + gui_id: str = None, + ): + super().__init__(client, config, gui_id) + QTableWidget.__init__(self, parent=parent) + self.setColumnCount(3) + self.setHorizontalHeaderLabels(["Scan Number", "Type", "Status"]) + header = self.horizontalHeader() + header.setSectionResizeMode(QHeaderView.Stretch) + self.bec_dispatcher.connect_slot(self.update_queue, MessageEndpoints.scan_queue_status()) + self.reset_content() + + @Slot(dict, dict) + def update_queue(self, content, _metadata): + """ + Update the queue table with the latest queue information. + + Args: + content (dict): The queue content. + _metadata (dict): The metadata. + """ + # only show the primary queue for now + queue_info = content.get("queue", {}).get("primary", {}).get("info", []) + self.setRowCount(len(queue_info)) + self.clearContents() + + if not queue_info: + self.reset_content() + return + + for index, item in enumerate(queue_info): + blocks = item.get("request_blocks", []) + scan_types = [] + scan_numbers = [] + status = item.get("status", "") + for request_block in blocks: + scan_type = request_block.get("content", {}).get("scan_type", "") + if scan_type: + scan_types.append(scan_type) + scan_number = request_block.get("scan_number", "") + if scan_number: + scan_numbers.append(str(scan_number)) + if scan_types: + scan_types = ", ".join(scan_types) + if scan_numbers: + scan_numbers = ", ".join(scan_numbers) + self.set_row(index, scan_numbers, scan_types, status) + + def format_item(self, content: str) -> QTableWidgetItem: + """ + Format the content of the table item. + + Args: + content (str): The content to be formatted. + + Returns: + QTableWidgetItem: The formatted item. + """ + item = QTableWidgetItem(content) + item.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter) + return item + + def set_row(self, index: int, scan_number: str, scan_type: str, status: str): + """ + Set the row of the table. + + Args: + index (int): The index of the row. + scan_number (str): The scan number. + scan_type (str): The scan type. + status (str): The status. + """ + + self.setItem(index, 0, self.format_item(scan_number)) + self.setItem(index, 1, self.format_item(scan_type)) + self.setItem(index, 2, self.format_item(status)) + + def reset_content(self): + """ + Reset the content of the table. + """ + + self.setRowCount(1) + self.set_row(0, "", "", "") + + +if __name__ == "__main__": # pragma: no cover + import sys + + from qtpy.QtWidgets import QApplication + + app = QApplication(sys.argv) + widget = BECQueue() + widget.show() + sys.exit(app.exec_()) diff --git a/bec_widgets/widgets/bec_queue/bec_queue.pyproject b/bec_widgets/widgets/bec_queue/bec_queue.pyproject new file mode 100644 index 00000000..0d5f0e5c --- /dev/null +++ b/bec_widgets/widgets/bec_queue/bec_queue.pyproject @@ -0,0 +1 @@ +{'files': ['bec_queue.py']} \ No newline at end of file diff --git a/bec_widgets/widgets/bec_queue/bec_queue_plugin.py b/bec_widgets/widgets/bec_queue/bec_queue_plugin.py new file mode 100644 index 00000000..8dcd6d9b --- /dev/null +++ b/bec_widgets/widgets/bec_queue/bec_queue_plugin.py @@ -0,0 +1,54 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from qtpy.QtDesigner import QDesignerCustomWidgetInterface +from qtpy.QtGui import QIcon + +from bec_widgets.widgets.bec_queue.bec_queue import BECQueue + +DOM_XML = """ + + + + +""" + + +class BECQueuePlugin(QDesignerCustomWidgetInterface): # pragma: no cover + def __init__(self): + super().__init__() + self._form_editor = None + + def createWidget(self, parent): + t = BECQueue(parent) + return t + + def domXml(self): + return DOM_XML + + def group(self): + return "" + + def icon(self): + return QIcon() + + def includeFile(self): + return "bec_queue" + + def initialize(self, form_editor): + self._form_editor = form_editor + + def isContainer(self): + return False + + def isInitialized(self): + return self._form_editor is not None + + def name(self): + return "BECQueue" + + def toolTip(self): + return "Widget to display the BEC queue." + + def whatsThis(self): + return self.toolTip() diff --git a/bec_widgets/widgets/bec_queue/register_bec_queue.py b/bec_widgets/widgets/bec_queue/register_bec_queue.py new file mode 100644 index 00000000..31bdacb1 --- /dev/null +++ b/bec_widgets/widgets/bec_queue/register_bec_queue.py @@ -0,0 +1,15 @@ +def main(): # pragma: no cover + from qtpy import PYSIDE6 + + if not PYSIDE6: + print("PYSIDE6 is not available in the environment. Cannot patch designer.") + return + from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection + + from bec_widgets.widgets.bec_queue.bec_queue_plugin import BECQueuePlugin + + QPyDesignerCustomWidgetCollection.addCustomWidget(BECQueuePlugin()) + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/tests/unit_tests/test_bec_queue.py b/tests/unit_tests/test_bec_queue.py new file mode 100644 index 00000000..5883a401 --- /dev/null +++ b/tests/unit_tests/test_bec_queue.py @@ -0,0 +1,111 @@ +import pytest +from bec_lib import messages + +from bec_widgets.widgets.bec_queue.bec_queue import BECQueue + +from .client_mocks import mocked_client + + +@pytest.fixture +def bec_queue_msg_full(): + content = { + "primary": { + "info": [ + { + "active_request_block": None, + "is_scan": [True], + "queue_id": "600163fc-5e56-4901-af25-14e9ee76817c", + "request_blocks": [ + { + "RID": "89a76021-28c0-4297-828e-74ae40b941e5", + "content": { + "parameter": { + "args": {"samx": [-0.1, 0.1]}, + "kwargs": { + "exp_time": 0.5, + "relative": True, + "steps": 20, + "system_config": { + "file_directory": None, + "file_suffix": None, + }, + }, + }, + "queue": "primary", + "scan_type": "line_scan", + }, + "is_scan": True, + "metadata": { + "RID": "89a76021-28c0-4297-828e-74ae40b941e5", + "file_directory": None, + "file_suffix": None, + "user_metadata": {"sample_name": "testA"}, + }, + "msg": messages.ScanQueueMessage( + metadata={ + "file_suffix": None, + "file_directory": None, + "user_metadata": {"sample_name": "testA"}, + "RID": "89a76021-28c0-4297-828e-74ae40b941e5", + }, + scan_type="line_scan", + parameter={ + "args": {"samx": [-0.1, 0.1]}, + "kwargs": { + "steps": 20, + "exp_time": 0.5, + "relative": True, + "system_config": { + "file_suffix": None, + "file_directory": None, + }, + }, + }, + queue="primary", + ), + "readout_priority": { + "async": [], + "baseline": [], + "monitored": ["samx"], + "on_request": [], + }, + "report_instructions": [{"scan_progress": 20}], + "scan_id": "2d704cc3-c172-404c-866d-608ce09fce40", + "scan_motors": ["samx"], + "scan_number": 1289, + } + ], + "scan_id": ["2d704cc3-c172-404c-866d-608ce09fce40"], + "scan_number": [1289], + "status": "COMPLETED", + } + ], + "status": "RUNNING", + } + } + msg = messages.ScanQueueStatusMessage(metadata={}, queue=content) + return msg + + +@pytest.fixture +def bec_queue(qtbot, mocked_client): + widget = BECQueue(client=mocked_client) + qtbot.addWidget(widget) + qtbot.waitExposed(widget) + yield widget + + +def test_bec_queue(bec_queue, bec_queue_msg_full): + bec_queue.update_queue(bec_queue_msg_full.content, {}) + assert bec_queue.rowCount() == 1 + assert bec_queue.item(0, 0).text() == "1289" + assert bec_queue.item(0, 1).text() == "line_scan" + assert bec_queue.item(0, 2).text() == "COMPLETED" + + +def test_bec_queue_empty(bec_queue): + bec_queue.update_queue({}, {}) + assert bec_queue.rowCount() == 1 + assert bec_queue.item(0, 0).text() == "" + assert bec_queue.item(0, 1).text() == "" + assert bec_queue.item(0, 2).text() == ""