bec-widgets 0.66.0__py3-none-any.whl → 0.67.0__py3-none-any.whl

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.
CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## v0.67.0 (2024-06-21)
4
+
5
+ ### Documentation
6
+
7
+ * docs: add widget to documentation ([`6fa1c06`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6fa1c06053131dabd084bb3cf13c853b5d3ce833))
8
+
9
+ ### Feature
10
+
11
+ * feat: introduce BECStatusBox Widget ([`443b6c1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/443b6c1d7b02c772fda02e2d1eefd5bd40249e0c))
12
+
13
+ ### Refactor
14
+
15
+ * refactor: Change inheritance to QTreeWidget from QWidget ([`d2f2b20`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d2f2b206bb0eab60b8a9b0d0ac60a6b7887fa6fb))
16
+
17
+ ### Test
18
+
19
+ * test: add test suite for bec_status_box and status_item ([`5d4ca81`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5d4ca816cdedec4c88aba9eb326f85392504ea1c))
20
+
21
+ ### Unknown
22
+
23
+ * Update file requirements.txt ([`505a5ec`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/505a5ec8334ff4422913b3a7b79d39bcb42ad535))
24
+
25
+ ## v0.66.1 (2024-06-20)
26
+
27
+ ### Fix
28
+
29
+ * fix: fixed shutdown for pyside ([`2718bc6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2718bc624731301756df524d0d5beef6cb1c1430))
30
+
3
31
  ## v0.66.0 (2024-06-20)
4
32
 
5
33
  ### Feature
@@ -147,30 +175,6 @@ This reverts commit abc6caa2d0b6141dfbe1f3d025f78ae14deddcb3 ([`fe04dd8`](https:
147
175
 
148
176
  ## v0.60.0 (2024-06-08)
149
177
 
150
- ### Ci
151
-
152
- * ci: added git fetch for target branch ([`fc4f4f8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fc4f4f81ad1be99cf5112f2188a46c5bed2679ee))
153
-
154
- * ci: fixed pylint-check ([`6b1d582`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6b1d5827d6599f06a3acd316060a8d25f0686d54))
155
-
156
- ### Feature
157
-
158
- * feat: added isort to bw-generate-cli ([`f0391f5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f0391f59c9eb0a51b693fccfe2e399e869d35dda))
159
-
160
178
  ### Fix
161
179
 
162
180
  * fix: removed BECConnector from rpc client interface ([`6428e38`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6428e38ab94c15a2c904e75cc6404bb6d0394e04))
163
-
164
- * fix: added bec_ipython_client as dependency; needed for jupyter widget ([`006a089`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/006a0894b85cba3b2773737ed6fe3e92c81cdee0))
165
-
166
- ### Refactor
167
-
168
- * refactor: minor cleanup ([`3adf6cf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3adf6cfd586355c8b8ce7fdc9722f868e22287c5))
169
-
170
- * refactor: disabled pylint for auto-gen client ([`b15816c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b15816ca9fd3e4ae87cca5fcfe029b4dfca570ca))
171
-
172
- * refactor(isort): added bec_widgets as known first party package ([`9c5a471`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9c5a471234ed2928e4527b079436db2a807c5f6f))
173
-
174
- ### Test
175
-
176
- * test: added missing pylint statement to header ([`f662985`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f6629852ebc2b4ee239fa560cc310a5ae2627cf7))
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bec_widgets
3
- Version: 0.66.0
3
+ Version: 0.67.0
4
4
  Summary: BEC Widgets
5
5
  Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
6
6
  Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets
bec_widgets/cli/client.py CHANGED
@@ -13,6 +13,7 @@ class Widgets(str, enum.Enum):
13
13
  Enum for the available widgets.
14
14
  """
15
15
 
16
+ BECStatusBox = "BECStatusBox"
16
17
  BECDock = "BECDock"
17
18
  BECDockArea = "BECDockArea"
18
19
  BECFigure = "BECFigure"
@@ -1388,6 +1389,24 @@ class BECPlotBase(RPCBase):
1388
1389
  """
1389
1390
 
1390
1391
 
1392
+ class BECStatusBox(RPCBase):
1393
+ @property
1394
+ @rpc_call
1395
+ def config_dict(self) -> "dict":
1396
+ """
1397
+ Get the configuration of the widget.
1398
+
1399
+ Returns:
1400
+ dict: The configuration of the widget.
1401
+ """
1402
+
1403
+ @rpc_call
1404
+ def get_all_rpc(self) -> "dict":
1405
+ """
1406
+ Get all registered RPC objects.
1407
+ """
1408
+
1409
+
1391
1410
  class BECWaveform(RPCBase):
1392
1411
  @property
1393
1412
  @rpc_call
@@ -1649,6 +1668,60 @@ class BECWaveform(RPCBase):
1649
1668
  """
1650
1669
 
1651
1670
 
1671
+ class DeviceComboBox(RPCBase):
1672
+ @property
1673
+ @rpc_call
1674
+ def config_dict(self) -> "dict":
1675
+ """
1676
+ Get the configuration of the widget.
1677
+
1678
+ Returns:
1679
+ dict: The configuration of the widget.
1680
+ """
1681
+
1682
+ @rpc_call
1683
+ def get_all_rpc(self) -> "dict":
1684
+ """
1685
+ Get all registered RPC objects.
1686
+ """
1687
+
1688
+
1689
+ class DeviceInputBase(RPCBase):
1690
+ @property
1691
+ @rpc_call
1692
+ def config_dict(self) -> "dict":
1693
+ """
1694
+ Get the configuration of the widget.
1695
+
1696
+ Returns:
1697
+ dict: The configuration of the widget.
1698
+ """
1699
+
1700
+ @rpc_call
1701
+ def get_all_rpc(self) -> "dict":
1702
+ """
1703
+ Get all registered RPC objects.
1704
+ """
1705
+
1706
+
1707
+ class DeviceLineEdit(RPCBase):
1708
+ @property
1709
+ @rpc_call
1710
+ def config_dict(self) -> "dict":
1711
+ """
1712
+ Get the configuration of the widget.
1713
+
1714
+ Returns:
1715
+ dict: The configuration of the widget.
1716
+ """
1717
+
1718
+ @rpc_call
1719
+ def get_all_rpc(self) -> "dict":
1720
+ """
1721
+ Get all registered RPC objects.
1722
+ """
1723
+
1724
+
1652
1725
  class Ring(RPCBase):
1653
1726
  @rpc_call
1654
1727
  def get_all_rpc(self) -> "dict":
@@ -1950,7 +2023,7 @@ class TextBox(RPCBase):
1950
2023
  @rpc_call
1951
2024
  def set_color(self, background_color: str, font_color: str) -> None:
1952
2025
  """
1953
- Set the background color of the Widget.
2026
+ Set the background color of the widget.
1954
2027
 
1955
2028
  Args:
1956
2029
  background_color (str): The color to set the background in HEX.
@@ -1960,13 +2033,19 @@ class TextBox(RPCBase):
1960
2033
  @rpc_call
1961
2034
  def set_text(self, text: str) -> None:
1962
2035
  """
1963
- Set the text of the Widget
2036
+ Set the text of the widget.
2037
+
2038
+ Args:
2039
+ text (str): The text to set.
1964
2040
  """
1965
2041
 
1966
2042
  @rpc_call
1967
2043
  def set_font_size(self, size: int) -> None:
1968
2044
  """
1969
- Set the font size of the text in the Widget.
2045
+ Set the font size of the text in the widget.
2046
+
2047
+ Args:
2048
+ size (int): The font size to set.
1970
2049
  """
1971
2050
 
1972
2051
 
@@ -9,7 +9,7 @@ import redis
9
9
  from bec_lib.client import BECClient
10
10
  from bec_lib.redis_connector import MessageObject, RedisConnector
11
11
  from bec_lib.service_config import ServiceConfig
12
- from qtpy.QtCore import QCoreApplication, QObject
12
+ from qtpy.QtCore import PYQT5, PYQT6, PYSIDE2, PYSIDE6, QCoreApplication, QObject
13
13
  from qtpy.QtCore import Signal as pyqtSignal
14
14
 
15
15
  if TYPE_CHECKING:
@@ -115,9 +115,16 @@ class BECDispatcher:
115
115
  def reset_singleton(cls):
116
116
  cls._instance = None
117
117
  cls._initialized = False
118
- if cls.qapp:
118
+
119
+ if not cls.qapp:
120
+ return
121
+
122
+ # shutdown QCoreApp if it exists
123
+ if PYQT5 or PYQT6:
119
124
  cls.qapp.exit()
120
- cls.qapp = None
125
+ elif PYSIDE2 or PYSIDE6:
126
+ cls.qapp.shutdown()
127
+ cls.qapp = None
121
128
 
122
129
  def connect_slot(
123
130
  self, slot: Callable, topics: Union[EndpointInfo, str, list[Union[EndpointInfo, str]]]
File without changes
@@ -0,0 +1,352 @@
1
+ """This module contains the BECStatusBox widget, which displays the status of different BEC services in a collapsible tree widget.
2
+ The widget automatically updates the status of all running BEC services, and displays their status.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import sys
8
+ from typing import TYPE_CHECKING
9
+
10
+ import qdarktheme
11
+ from bec_lib.utils.import_utils import lazy_import_from
12
+ from pydantic import BaseModel, Field, field_validator
13
+ from qtpy.QtCore import QObject, QTimer, Signal, Slot
14
+ from qtpy.QtWidgets import QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
15
+
16
+ from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
17
+ from bec_widgets.widgets.bec_status_box.status_item import StatusItem
18
+
19
+ if TYPE_CHECKING:
20
+ from bec_lib.client import BECClient
21
+
22
+ # TODO : Put normal imports back when Pydantic gets faster
23
+ BECStatus = lazy_import_from("bec_lib.messages", ("BECStatus",))
24
+
25
+
26
+ class BECStatusBoxConfig(ConnectionConfig):
27
+ pass
28
+
29
+
30
+ class BECServiceInfoContainer(BaseModel):
31
+ """Container to store information about the BEC services."""
32
+
33
+ service_name: str
34
+ status: BECStatus | str = Field(
35
+ default="NOTCONNECTED",
36
+ description="The status of the service. Can be any of the BECStatus names, or NOTCONNECTED.",
37
+ )
38
+ info: dict
39
+ metrics: dict | None
40
+ model_config: dict = {"validate_assignment": True}
41
+
42
+ @field_validator("status")
43
+ @classmethod
44
+ def validate_status(cls, v):
45
+ """Validate input for status. Accept BECStatus and NOTCONNECTED.
46
+
47
+ Args:
48
+ v (BECStatus | str): The input value.
49
+
50
+ Returns:
51
+ str: The validated status.
52
+ """
53
+ if v in list(BECStatus.__members__.values()):
54
+ return v.name
55
+ if v in list(BECStatus.__members__.keys()) or v == "NOTCONNECTED":
56
+ return v
57
+ raise ValueError(
58
+ f"Status must be one of {BECStatus.__members__.values()} or 'NOTCONNECTED'. Input {v}"
59
+ )
60
+
61
+
62
+ class BECServiceStatusMixin(QObject):
63
+ """A mixin class to update the service status, and metrics.
64
+ It emits a signal 'services_update' when the service status is updated.
65
+
66
+ Args:
67
+ client (BECClient): The client object to connect to the BEC server.
68
+ """
69
+
70
+ services_update = Signal(dict, dict)
71
+
72
+ def __init__(self, client: BECClient):
73
+ super().__init__()
74
+ self.client = client
75
+ self._service_update_timer = QTimer()
76
+ self._service_update_timer.timeout.connect(self._get_service_status)
77
+ self._service_update_timer.start(1000)
78
+
79
+ def _get_service_status(self):
80
+ """Pull latest service and metrics updates from REDIS for all services, and emit both via 'services_update' signal."""
81
+ # pylint: disable=protected-access
82
+ self.client._update_existing_services()
83
+ self.services_update.emit(self.client._services_info, self.client._services_metric)
84
+
85
+
86
+ class BECStatusBox(BECConnector, QTreeWidget):
87
+ """A widget to display the status of different BEC services.
88
+ This widget automatically updates the status of all running BEC services, and displays their status.
89
+ Information about the individual services is collapsible, and double clicking on
90
+ the individual service will display the metrics about the service.
91
+
92
+ Args:
93
+ parent Optional : The parent widget for the BECStatusBox. Defaults to None.
94
+ service_name Optional(str): The name of the top service label. Defaults to "BEC Server".
95
+ client Optional(BECClient): The client object to connect to the BEC server. Defaults to None
96
+ config Optional(BECStatusBoxConfig | dict): The configuration for the status box. Defaults to None.
97
+ gui_id Optional(str): The unique id for the widget. Defaults to None.
98
+ """
99
+
100
+ CORE_SERVICES = ["DeviceServer", "ScanServer", "SciHub", "ScanBundler", "FileWriterManager"]
101
+
102
+ service_update = Signal(dict)
103
+ bec_core_state = Signal(str)
104
+
105
+ def __init__(
106
+ self,
107
+ parent=None,
108
+ service_name: str = "BEC Server",
109
+ client: BECClient = None,
110
+ config: BECStatusBoxConfig | dict = None,
111
+ gui_id: str = None,
112
+ ):
113
+ if config is None:
114
+ config = BECStatusBoxConfig(widget_class=self.__class__.__name__)
115
+ else:
116
+ if isinstance(config, dict):
117
+ config = BECStatusBoxConfig(**config)
118
+ super().__init__(client=client, config=config, gui_id=gui_id)
119
+ QTreeWidget.__init__(self, parent=parent)
120
+
121
+ self.service_name = service_name
122
+ self.config = config
123
+
124
+ self.bec_service_info_container = {}
125
+ self.tree_items = {}
126
+ self.tree_top_item = None
127
+ self.bec_service_status = BECServiceStatusMixin(client=self.client)
128
+
129
+ self.init_ui()
130
+ self.bec_service_status.services_update.connect(self.update_service_status)
131
+ self.bec_core_state.connect(self.update_top_item_status)
132
+ self.itemDoubleClicked.connect(self.on_tree_item_double_clicked)
133
+
134
+ def init_ui(self) -> None:
135
+ """Initialize the UI for the status box, and add QTreeWidget as the basis for the status box."""
136
+ self.init_ui_tree_widget()
137
+ top_label = self._create_status_widget(self.service_name, status=BECStatus.IDLE)
138
+ self.tree_top_item = QTreeWidgetItem()
139
+ self.tree_top_item.setExpanded(True)
140
+ self.tree_top_item.setDisabled(True)
141
+ self.addTopLevelItem(self.tree_top_item)
142
+ self.setItemWidget(self.tree_top_item, 0, top_label)
143
+ self.service_update.connect(top_label.update_config)
144
+
145
+ def _create_status_widget(
146
+ self, service_name: str, status=BECStatus, info: dict = None, metrics: dict = None
147
+ ) -> StatusItem:
148
+ """Creates a StatusItem (QWidget) for the given service, and stores all relevant
149
+ information about the service in the bec_service_info_container.
150
+
151
+ Args:
152
+ service_name (str): The name of the service.
153
+ status (BECStatus): The status of the service.
154
+ info Optional(dict): The information about the service. Default is {}
155
+ metric Optional(dict): Metrics for the respective service. Default is None
156
+
157
+ Returns:
158
+ StatusItem: The status item widget.
159
+ """
160
+ if info is None:
161
+ info = {}
162
+ self._update_bec_service_container(service_name, status, info, metrics)
163
+ item = StatusItem(
164
+ parent=self,
165
+ config={
166
+ "service_name": service_name,
167
+ "status": status.name,
168
+ "info": info,
169
+ "metrics": metrics,
170
+ },
171
+ )
172
+ return item
173
+
174
+ @Slot(str)
175
+ def update_top_item_status(self, status: BECStatus) -> None:
176
+ """Method to update the status of the top item in the tree widget.
177
+ Gets the status from the Signal 'bec_core_state' and updates the StatusItem via the signal 'service_update'.
178
+
179
+ Args:
180
+ status (BECStatus): The state of the core services.
181
+ """
182
+ self.bec_service_info_container[self.service_name].status = status
183
+ self.service_update.emit(self.bec_service_info_container[self.service_name].model_dump())
184
+
185
+ def _update_bec_service_container(
186
+ self, service_name: str, status: BECStatus, info: dict, metrics: dict = None
187
+ ) -> None:
188
+ """Update the bec_service_info_container with the newest status and metrics for the BEC service.
189
+ If information about the service already exists, it will create a new entry.
190
+
191
+ Args:
192
+ service_name (str): The name of the service.
193
+ service_info (StatusMessage): A class containing the service status.
194
+ service_metric (ServiceMetricMessage): A class containing the service metrics.
195
+ """
196
+ container = self.bec_service_info_container.get(service_name, None)
197
+ if container:
198
+ container.status = status
199
+ container.info = info
200
+ container.metrics = metrics
201
+ return
202
+ service_info_item = BECServiceInfoContainer(
203
+ service_name=service_name, status=status, info=info, metrics=metrics
204
+ )
205
+ self.bec_service_info_container.update({service_name: service_info_item})
206
+
207
+ @Slot(dict, dict)
208
+ def update_service_status(self, services_info: dict, services_metric: dict) -> None:
209
+ """Callback function services_metric from BECServiceStatusMixin.
210
+ It updates the status of all services.
211
+
212
+ Args:
213
+ services_info (dict): A dictionary containing the service status for all running BEC services.
214
+ services_metric (dict): A dictionary containing the service metrics for all running BEC services.
215
+ """
216
+ checked = []
217
+ services_info = self.update_core_services(services_info, services_metric)
218
+ checked.extend(self.CORE_SERVICES)
219
+
220
+ for service_name, msg in sorted(services_info.items()):
221
+ checked.append(service_name)
222
+ metric_msg = services_metric.get(service_name, None)
223
+ metrics = metric_msg.metrics if metric_msg else None
224
+ if service_name in self.tree_items:
225
+ self._update_bec_service_container(
226
+ service_name=service_name, status=msg.status, info=msg.info, metrics=metrics
227
+ )
228
+ self.service_update.emit(self.bec_service_info_container[service_name].model_dump())
229
+ continue
230
+
231
+ item_widget = self._create_status_widget(
232
+ service_name=service_name, status=msg.status, info=msg.info, metrics=metrics
233
+ )
234
+ item = QTreeWidgetItem()
235
+ item.setDisabled(True)
236
+ self.service_update.connect(item_widget.update_config)
237
+ self.tree_top_item.addChild(item)
238
+ self.setItemWidget(item, 0, item_widget)
239
+ self.tree_items.update({service_name: (item, item_widget)})
240
+
241
+ self.check_redundant_tree_items(checked)
242
+
243
+ def update_core_services(self, services_info: dict, services_metric: dict) -> dict:
244
+ """Method to process status and metrics updates of core services (stored in CORE_SERVICES).
245
+ If a core services is not connected, it should not be removed from the status widget
246
+
247
+ Args:
248
+ services_info (dict): A dictionary containing the service status of different services.
249
+ services_metric (dict): A dictionary containing the service metrics of different services.
250
+
251
+ Returns:
252
+ dict: The services_info dictionary after removing the info updates related to the CORE_SERVICES
253
+ """
254
+ bec_core_state = "RUNNING"
255
+ for service_name in sorted(self.CORE_SERVICES):
256
+ metric_msg = services_metric.get(service_name, None)
257
+ metrics = metric_msg.metrics if metric_msg else None
258
+ if service_name not in services_info:
259
+ self.bec_service_info_container[service_name].status = "NOTCONNECTED"
260
+ bec_core_state = "ERROR"
261
+ else:
262
+ msg = services_info.pop(service_name)
263
+ self._update_bec_service_container(
264
+ service_name=service_name, status=msg.status, info=msg.info, metrics=metrics
265
+ )
266
+ bec_core_state = (
267
+ "RUNNING" if (msg.status.value > 1 and bec_core_state == "RUNNING") else "ERROR"
268
+ )
269
+
270
+ if service_name in self.tree_items:
271
+ self.service_update.emit(self.bec_service_info_container[service_name].model_dump())
272
+ continue
273
+ self.add_tree_item(service_name, msg.status, msg.info, metrics)
274
+
275
+ self.bec_core_state.emit(bec_core_state)
276
+ return services_info
277
+
278
+ def check_redundant_tree_items(self, checked: list) -> None:
279
+ """Utility method to check and remove redundant objects from the BECStatusBox.
280
+
281
+ Args:
282
+ checked (list): A list of services that are currently running.
283
+ """
284
+ to_be_deleted = [key for key in self.tree_items if key not in checked]
285
+
286
+ for key in to_be_deleted:
287
+ item, _ = self.tree_items.pop(key)
288
+ self.tree_top_item.removeChild(item)
289
+
290
+ def add_tree_item(
291
+ self, service_name: str, status: BECStatus, info: dict = None, metrics: dict = None
292
+ ) -> None:
293
+ """Method to add a new QTreeWidgetItem together with a StatusItem to the tree widget.
294
+
295
+ Args:
296
+ service_name (str): The name of the service.
297
+ service_status_msg (StatusMessage): The status of the service.
298
+ metrics (dict): The metrics of the service.
299
+ """
300
+ item_widget = self._create_status_widget(
301
+ service_name=service_name, status=status, info=info, metrics=metrics
302
+ )
303
+ item = QTreeWidgetItem()
304
+ self.service_update.connect(item_widget.update_config)
305
+ self.tree_top_item.addChild(item)
306
+ self.setItemWidget(item, 0, item_widget)
307
+ self.tree_items.update({service_name: (item, item_widget)})
308
+
309
+ def init_ui_tree_widget(self) -> None:
310
+ """Initialise the tree widget for the status box."""
311
+ self.setHeaderHidden(True)
312
+ self.setStyleSheet(
313
+ "QTreeWidget::item:!selected "
314
+ "{ "
315
+ "border: 1px solid gainsboro; "
316
+ "border-left: none; "
317
+ "border-top: none; "
318
+ "}"
319
+ "QTreeWidget::item:selected {}"
320
+ )
321
+
322
+ @Slot(QTreeWidgetItem, int)
323
+ def on_tree_item_double_clicked(self, item: QTreeWidgetItem, column: int) -> None:
324
+ """Callback function for double clicks on individual QTreeWidgetItems in the collapsed section.
325
+
326
+ Args:
327
+ item (QTreeWidgetItem): The item that was double clicked.
328
+ column (int): The column that was double clicked.
329
+ """
330
+ for _, (tree_item, status_widget) in self.tree_items.items():
331
+ if tree_item == item:
332
+ status_widget.show_popup()
333
+
334
+ def closeEvent(self, event):
335
+ super().cleanup()
336
+ QTreeWidget().closeEvent(event)
337
+
338
+
339
+ def main():
340
+ """Main method to run the BECStatusBox widget."""
341
+ # pylint: disable=import-outside-toplevel
342
+ from qtpy.QtWidgets import QApplication
343
+
344
+ app = QApplication(sys.argv)
345
+ qdarktheme.setup_theme("auto")
346
+ main_window = BECStatusBox()
347
+ main_window.show()
348
+ sys.exit(app.exec())
349
+
350
+
351
+ if __name__ == "__main__":
352
+ main()
@@ -0,0 +1,171 @@
1
+ """ Module for a StatusItem widget to display status and metrics for a BEC service.
2
+ The widget is bound to be used with the BECStatusBox widget."""
3
+
4
+ import enum
5
+ import sys
6
+ from datetime import datetime
7
+
8
+ import qdarktheme
9
+ from bec_lib.utils.import_utils import lazy_import_from
10
+ from pydantic import Field
11
+ from qtpy.QtCore import Qt, Slot
12
+ from qtpy.QtWidgets import QDialog, QHBoxLayout, QLabel, QStyle, QVBoxLayout, QWidget
13
+
14
+ from bec_widgets.utils.bec_connector import ConnectionConfig
15
+
16
+ # TODO : Put normal imports back when Pydantic gets faster
17
+ BECStatus = lazy_import_from("bec_lib.messages", ("BECStatus",))
18
+
19
+
20
+ class IconsEnum(enum.Enum):
21
+ """Enum class for icons in the status item widget."""
22
+
23
+ RUNNING = "SP_DialogApplyButton"
24
+ BUSY = "SP_BrowserReload"
25
+ IDLE = "SP_MessageBoxWarning"
26
+ ERROR = "SP_DialogCancelButton"
27
+ NOTCONNECTED = "SP_TitleBarContextHelpButton"
28
+
29
+
30
+ class StatusWidgetConfig(ConnectionConfig):
31
+ """Configuration class for the status item widget."""
32
+
33
+ service_name: str
34
+ status: str
35
+ info: dict
36
+ metrics: dict | None
37
+ icon_size: tuple = Field(default=(24, 24), description="The size of the icon in the widget.")
38
+ font_size: int = Field(16, description="The font size of the text in the widget.")
39
+
40
+
41
+ class StatusItem(QWidget):
42
+ """A widget to display the status of a service.
43
+
44
+ Args:
45
+ parent: The parent widget.
46
+ config (dict): The configuration for the service.
47
+ """
48
+
49
+ def __init__(self, parent=None, config: dict = None):
50
+ if config is None:
51
+ config = StatusWidgetConfig(widget_class=self.__class__.__name__)
52
+ else:
53
+ if isinstance(config, dict):
54
+ config = StatusWidgetConfig(**config)
55
+ self.config = config
56
+ QWidget.__init__(self, parent=parent)
57
+ self.parent = parent
58
+ self.layout = None
59
+ self.config = config
60
+ self._popup_label_ref = {}
61
+ self._label = None
62
+ self._icon = None
63
+ self.init_ui()
64
+
65
+ def init_ui(self) -> None:
66
+ """Init the UI for the status item widget."""
67
+ self.layout = QHBoxLayout()
68
+ self.layout.setContentsMargins(5, 5, 5, 5)
69
+ self.setLayout(self.layout)
70
+ self._label = QLabel()
71
+ self._icon = QLabel()
72
+ self.layout.addWidget(self._label)
73
+ self.layout.addWidget(self._icon)
74
+ self.update_ui()
75
+
76
+ @Slot(dict)
77
+ def update_config(self, config: dict) -> None:
78
+ """Update the configuration of the status item widget.
79
+ This method is invoked from the parent widget.
80
+ The UI values are later updated based on the new configuration.
81
+
82
+ Args:
83
+ config (dict): Config updates from parent widget.
84
+ """
85
+ if config["service_name"] != self.config.service_name:
86
+ return
87
+ self.config.status = config["status"]
88
+ self.config.info = config["info"]
89
+ self.config.metrics = config["metrics"]
90
+ self.update_ui()
91
+
92
+ def update_ui(self) -> None:
93
+ """Update the UI of the labels, and popup dialog."""
94
+ self.set_text()
95
+ self.set_status()
96
+ self._set_popup_text()
97
+
98
+ def set_text(self) -> None:
99
+ """Set the text of the QLabel basae on the config."""
100
+ service = self.config.service_name
101
+ status = self.config.status
102
+ if "BECClient" in service.split("/"):
103
+ service = service.split("/")[0] + "/..." + service.split("/")[1][-4:]
104
+ if status == "NOTCONNECTED":
105
+ status = "NOT CONNECTED"
106
+ text = f"{service} is {status}"
107
+ self._label.setText(text)
108
+
109
+ def set_status(self) -> None:
110
+ """Set the status icon for the status item widget."""
111
+ icon_name = IconsEnum[self.config.status].value
112
+ icon = self.style().standardIcon(getattr(QStyle.StandardPixmap, icon_name))
113
+ self._icon.setPixmap(icon.pixmap(*self.config.icon_size))
114
+ self._icon.setAlignment(Qt.AlignmentFlag.AlignRight)
115
+
116
+ def show_popup(self) -> None:
117
+ """Method that is invoked when the user double clicks on the StatusItem widget."""
118
+ dialog = QDialog(self)
119
+ dialog.setWindowTitle(f"{self.config.service_name} Details")
120
+ layout = QVBoxLayout()
121
+ popup_label = self._make_popup_label()
122
+ self._set_popup_text()
123
+ layout.addWidget(popup_label)
124
+ dialog.setLayout(layout)
125
+ dialog.finished.connect(self._cleanup_popup_label)
126
+ dialog.exec()
127
+
128
+ def _make_popup_label(self) -> QLabel:
129
+ """Create a QLabel for the popup dialog.
130
+
131
+ Returns:
132
+ QLabel: The label for the popup dialog.
133
+ """
134
+ label = QLabel()
135
+ label.setWordWrap(True)
136
+ self._popup_label_ref.update({"label": label})
137
+ return label
138
+
139
+ def _set_popup_text(self) -> None:
140
+ """Compile the metrics text for the status item widget."""
141
+ if self._popup_label_ref.get("label") is None:
142
+ return
143
+ metrics_text = (
144
+ f"<b>SERVICE:</b> {self.config.service_name}<br><b>STATUS:</b> {self.config.status}<br>"
145
+ )
146
+ if self.config.metrics:
147
+ for key, value in self.config.metrics.items():
148
+ if key == "create_time":
149
+ value = datetime.fromtimestamp(value).strftime("%Y-%m-%d %H:%M:%S")
150
+ metrics_text += f"<b>{key.upper()}:</b> {value}<br>"
151
+ self._popup_label_ref["label"].setText(metrics_text)
152
+
153
+ def _cleanup_popup_label(self) -> None:
154
+ """Cleanup the popup label."""
155
+ self._popup_label_ref.clear()
156
+
157
+
158
+ def main():
159
+ """Run the status item widget."""
160
+ # pylint: disable=import-outside-toplevel
161
+ from qtpy.QtWidgets import QApplication
162
+
163
+ app = QApplication(sys.argv)
164
+ qdarktheme.setup_theme("auto")
165
+ main_window = StatusItem()
166
+ main_window.show()
167
+ sys.exit(app.exec())
168
+
169
+
170
+ if __name__ == "__main__":
171
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bec_widgets
3
- Version: 0.66.0
3
+ Version: 0.67.0
4
4
  Summary: BEC Widgets
5
5
  Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
6
6
  Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets
@@ -2,11 +2,11 @@
2
2
  .gitlab-ci.yml,sha256=RnYDz4zKXjlqltTryprlB1s5vLXxI2-seW-Vb70NNF0,8162
3
3
  .pylintrc,sha256=OstrgmEyP0smNFBKoIN5_26-UmNZgMHnbjvAWX0UrLs,18535
4
4
  .readthedocs.yaml,sha256=aSOc277LqXcsTI6lgvm_JY80lMlr69GbPKgivua2cS0,603
5
- CHANGELOG.md,sha256=P5wRvE4GZafd9QiYxHEs_VrdmlN8nfBuhSRoIoiVbBg,7087
5
+ CHANGELOG.md,sha256=WkuQFvZMK4X4aOXLwutfGkFzmzy-SLKr8ez6vvTXiBY,6851
6
6
  LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
7
- PKG-INFO,sha256=zGYdWrGWioZkJQL84cTkSoYLziwjhiDoVOCpJ2wCdL0,1302
7
+ PKG-INFO,sha256=9Cs9V5N0bA887XBwZGWJ08opZvj8rpWinMW3R2r56rs,1302
8
8
  README.md,sha256=y4jB6wvArS7N8_iTbKWnSM_oRAqLA2GqgzUR-FMh5sU,2645
9
- pyproject.toml,sha256=yQfeKopRkLxKI3bBz3HoECNUXBRbatDouMvIoBjRej8,2162
9
+ pyproject.toml,sha256=gK6dcc2qr6EKwZzWQhdkPeABrJhhE93t6qRmseFko80,2162
10
10
  .git_hooks/pre-commit,sha256=n3RofIZHJl8zfJJIUomcMyYGFi_rwq4CC19z0snz3FI,286
11
11
  .gitlab/issue_templates/bug_report_template.md,sha256=gAuyEwl7XlnebBrkiJ9AqffSNOywmr8vygUFWKTuQeI,386
12
12
  .gitlab/issue_templates/documentation_update_template.md,sha256=FHLdb3TS_D9aL4CYZCjyXSulbaW5mrN2CmwTaeLPbNw,860
@@ -17,7 +17,7 @@ bec_widgets/assets/bec_widgets_icon.png,sha256=K8dgGwIjalDh9PRHUsSQBqgdX7a00nM3i
17
17
  bec_widgets/assets/terminal_icon.png,sha256=bJl7Tft4Fi2uxvuXI8o14uMHnI9eAWKSU2uftXCH9ws,3889
18
18
  bec_widgets/cli/__init__.py,sha256=d0Q6Fn44e7wFfLabDOBxpcJ1DPKWlFunGYDUBmO-4hA,22
19
19
  bec_widgets/cli/auto_updates.py,sha256=DyBV3HnjMSH-cvVkYNcDiYKVf0Xut4Qy2qGQqkW47Bw,4833
20
- bec_widgets/cli/client.py,sha256=GJRzys0DVHQp8X9QxociTrshPm4CvaPtEprxWEpEKCs,56446
20
+ bec_widgets/cli/client.py,sha256=Zd4oMSE5-HY3IBUIVcparGGV2Ew86gaWAFTd4OjFVmg,58005
21
21
  bec_widgets/cli/client_utils.py,sha256=D076XKwcukKBKknd11B1UyOcQN_9sN7ZKMVttyCxS9Q,11586
22
22
  bec_widgets/cli/generate_cli.py,sha256=Bi8HxHhge1I87vbdYHZUZiZwvbB-OSkLYS5Xfmwiz9M,4922
23
23
  bec_widgets/cli/rpc_register.py,sha256=QxXUZu5XNg00Yf5O3UHWOXg3-f_pzKjjoZYMOa-MOJc,2216
@@ -32,7 +32,7 @@ bec_widgets/examples/motor_movement/motor_control_compilations.py,sha256=8rpA7a2
32
32
  bec_widgets/examples/motor_movement/motor_controller.ui,sha256=83XX6NGILwntoUIghvzWnMuGf80O8khK3SduVKTAEFM,29105
33
33
  bec_widgets/utils/__init__.py,sha256=1930ji1Jj6dVuY81Wd2kYBhHYNV-2R0bN_L4o9zBj1U,533
34
34
  bec_widgets/utils/bec_connector.py,sha256=RxHJNF7JjtY5pRbTMu2eQTiRXvoyJ53QuTYxHjZba38,5357
35
- bec_widgets/utils/bec_dispatcher.py,sha256=tFRd-rfOpyijr1iCGq31kLUjHRr9Y_h2HYYEZKZCgvo,5803
35
+ bec_widgets/utils/bec_dispatcher.py,sha256=vvHpD_7ZddRmD6mHR5JWU_XEJYxognpmG7CnEWMjaZk,5989
36
36
  bec_widgets/utils/bec_table.py,sha256=nA2b8ukSeUfquFMAxGrUVOqdrzMoDYD6O_4EYbOG2zk,717
37
37
  bec_widgets/utils/colors.py,sha256=GYSDe0ZxsJSwxvuy-yG2BH17qlf_Sjq8dhDcyp9IhBI,8532
38
38
  bec_widgets/utils/container_utils.py,sha256=m3VUyAYmSWkEwApP9tBvKxPYVtc2kHw4toxIpMryJy4,1495
@@ -47,6 +47,9 @@ bec_widgets/utils/validator_delegate.py,sha256=Emj1WF6W8Ke1ruBWUfmHdVJpmOSPezuOt
47
47
  bec_widgets/utils/widget_io.py,sha256=f36198CvT_EzWQ_cg2G-4tRRsaMdJ3yVqsZWKJCQEfA,10880
48
48
  bec_widgets/utils/yaml_dialog.py,sha256=cMVif-39SB9WjwGH5FWBJcFs4tnfFJFs5cacydRyhy0,1853
49
49
  bec_widgets/widgets/__init__.py,sha256=6RE9Pot2ud6BNJc_ZKiE--U-lgVRUff2IVR91lPcCbo,214
50
+ bec_widgets/widgets/bec_status_box/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
+ bec_widgets/widgets/bec_status_box/bec_status_box.py,sha256=ARdVzwabmbT_Jti6Wp-5KrxDQIingYVC-pZQHd6uz7o,14375
52
+ bec_widgets/widgets/bec_status_box/status_item.py,sha256=wPkDm0GCGNXXpy3rR_Ljaxy0ZHeiiYcrWFqEntZnz4E,5869
50
53
  bec_widgets/widgets/buttons/__init__.py,sha256=74ucIRU6-anoqQ-zT7wbrysmxhg_3_04xGhN_kllNUI,48
51
54
  bec_widgets/widgets/buttons/stop_button/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
55
  bec_widgets/widgets/buttons/stop_button/stop_button.py,sha256=x4a7RvlMkHzOd05zKOGYkyTmBza7Me7jgOL9WIgA_c4,906
@@ -103,7 +106,7 @@ docs/Makefile,sha256=i2WHuFlgfyAPEW4ssEP8NY4cOibDJrVjvzSEU8_Ggwc,634
103
106
  docs/conf.py,sha256=HxLxupNGu0Smhwn57g1kFdjZzFuaWVREgRJKhT1zi2k,2464
104
107
  docs/index.md,sha256=8ZCgaLIbJsYvt-jwi--QxsNwnK4-k3rejIeOOLclG40,1101
105
108
  docs/make.bat,sha256=vKazJE8RW49Cy8K7hm8QYbletvAd8YkeKsaPA_dWnXs,800
106
- docs/requirements.txt,sha256=SbcWrLYX6tEZNMCUETgsE50Y7VioR1VX6YCeifuJWEc,140
109
+ docs/requirements.txt,sha256=TcjRnzVYFOg64N-lJc2sn-G6YQdhPjzp0lrLuPsPMXY,156
107
110
  docs/_static/custom.css,sha256=v4Nk7r8LZslhOV8RaSUb15bG4miwiZ4-kZyXBLnSyms,13487
108
111
  docs/_templates/custom-class-template.rst,sha256=HPuPaGJob2zXlWOl5FmA-hAZRbUTGQmdqo3HS1iIFog,711
109
112
  docs/_templates/custom-module-template.rst,sha256=MXYXAz06HP_mbblO--iFwL08xROmSBo7U4O-hPbMcZU,1228
@@ -118,7 +121,7 @@ docs/assets/rocket_launch_48dp.svg,sha256=pdrPrBcKWUa5OlgWKM0B6TA6qAW7E57d7C7YW2
118
121
  docs/developer/developer.md,sha256=VUdMnQBsSRCawYMFCe0vya5oj1MimLML7Pd58kY6fYY,765
119
122
  docs/developer/getting_started/development.md,sha256=aYLmuLMYpp5FcIXeDUqCfcStIV8veuiMBjOt5bTW_30,1406
120
123
  docs/developer/getting_started/getting_started.md,sha256=My_K_6O7LLaXVB_eINrRku5o-jVx95lsmGgHxgZhT7A,378
121
- docs/developer/widgets/widgets.md,sha256=O7v0DsgCr-IULxl0TJ7NIGN68wd5kouKz1Y5ZuEvaEU,529
124
+ docs/developer/widgets/widgets.md,sha256=aNsJgG7R-3EerumNB6GH84JLIXfZqGN5GjvpKWDi0Hk,504
122
125
  docs/introduction/introduction.md,sha256=wp7jmhkUtJnSnEnmIAZGUcau_3-5e5-FohvZb63khw4,1432
123
126
  docs/user/customisation.md,sha256=wCW8fAbqtlgGE3mURvXOrK67Xo0_B-lxfg0sYuQWB40,3186
124
127
  docs/user/user.md,sha256=uCTcjclIi6rdjYRQebko6bWFEVsjyfshsVU3BDYrC-Y,1403
@@ -128,11 +131,13 @@ docs/user/getting_started/BECDockArea.png,sha256=t3vSm_rVRk371J5LOutbolETuEjStNc
128
131
  docs/user/getting_started/auto_updates.md,sha256=Gicx3lplI6JRBlnPj_VL6IhqOIcsWjYF4_EdZSCje2A,3754
129
132
  docs/user/getting_started/getting_started.md,sha256=lxZXCr6HAkM61oo5Bu-YjINSKo4wihWhAPJdotEAAVQ,358
130
133
  docs/user/getting_started/gui_complex_gui.gif,sha256=ovv9u371BGG5GqhzyBMl4mvqMHLfJS0ylr-dR0Ydwtw,6550393
131
- docs/user/getting_started/installation.md,sha256=gqMV44lh9-wkKtAtDckvnyX_d8oTBNinQxvriFQ9Sk4,1145
134
+ docs/user/getting_started/installation.md,sha256=5_fPbmUqLGtwOskFHTlytd4PJKrMcHqHShzM9ymM0oI,1149
132
135
  docs/user/getting_started/quick_start.md,sha256=VGU880GwamcIZcBE8tjxuqX2syE-71jqZedtskCoBbA,9405
133
136
  docs/user/widgets/BECFigure.png,sha256=8dQr4u0uk_y0VV-R1Jh9yTR3Vidd9HDEno_07R0swaE,1605920
134
137
  docs/user/widgets/bec_figure.md,sha256=BwcumbhZd6a2zKmoHTvwKr8kG8WxBx9lS_QwxNiBMpQ,5155
135
- docs/user/widgets/buttons.md,sha256=LG-Csj9RL7hWur8Xgj19r-u2SuIFq912fyBVN6peLGY,1222
138
+ docs/user/widgets/bec_status_box.gif,sha256=kLxf40HbS6fjdUIQ2b9SiduBEXdBd4DDWGEnQDOFMcY,1259044
139
+ docs/user/widgets/bec_status_box.md,sha256=0ILY12UnSjiVCtd5qpC8G2dPBYhug3J_rmQLilDxulY,1164
140
+ docs/user/widgets/buttons.md,sha256=Yci21PmxlRvfKcrvY7mVI7JkECPmE6j9WyWyH3j7Y2o,1221
136
141
  docs/user/widgets/image_plot.gif,sha256=_mVFhMTXGqwDOcEtrBHMZj5Thn2sLhDAHEeL2XyHN-s,14098977
137
142
  docs/user/widgets/motor.gif,sha256=FtaWdRHx4UZaGJPpq8LNhMMgX4PFcAB6IZ93JCMEh_w,2280719
138
143
  docs/user/widgets/progress_bar.gif,sha256=5jh0Zw2BBGPuNxszV1DBLJCb4_6glIRX-U2ABjnsK2k,5263592
@@ -141,7 +146,7 @@ docs/user/widgets/spiral_progress_bar.md,sha256=QTgUDIl6XPuK_HwSfB6sNijZ4bss26bi
141
146
  docs/user/widgets/text_box.md,sha256=_ST7RQWXl67MKLm6dTa995GjoughPUyK_hLnF8SPZcM,925
142
147
  docs/user/widgets/w1D.gif,sha256=tuHbleJpl6bJFNNC2OdndF5LF7IyfvlkFCMGZajrQPs,622773
143
148
  docs/user/widgets/website.md,sha256=wfudAupdtHX-Sfritg0xMWXZLLczJ4XwMLNWvu6ww-w,705
144
- docs/user/widgets/widgets.md,sha256=NzRfrgd4LWmZHa2Cs_1G59LeY5uAlFdy5aP00AtGAjk,380
149
+ docs/user/widgets/widgets.md,sha256=6H8C8M2fFmTxFFlrAuOE-jBpOUVUrOIIzWP0l_EwMGo,397
145
150
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
146
151
  tests/end-2-end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
147
152
  tests/end-2-end/conftest.py,sha256=j1O1SxXRJ8jcrunn6dcfbZLK2Jc-VUxyh9ZuCSc6Qj4,1816
@@ -156,6 +161,7 @@ tests/unit_tests/test_bec_dispatcher.py,sha256=rYPiRizHaswhGZw55IBMneDFxmPiCCLAZ
156
161
  tests/unit_tests/test_bec_dock.py,sha256=BXKXpuyIYj-l6KSyhQtM_p3kRFCRECIoXLzvkcJZDlM,3611
157
162
  tests/unit_tests/test_bec_figure.py,sha256=aEd2R8K6fU2ON8QvPemGWpql_LaaYLipRlvnjBY2qFA,8009
158
163
  tests/unit_tests/test_bec_motor_map.py,sha256=AfD_9-x6VV3TPnkQgNfFYRndPHDsGx-a_YknFeDr6hc,4588
164
+ tests/unit_tests/test_bec_status_box.py,sha256=zZ4pe7DaBzzpRsy62yHFkUGgAGb3zZU3I6zQIPsqUTY,6070
159
165
  tests/unit_tests/test_client_utils.py,sha256=eViJ1Tz-HX9TkMvQH6W8cO-c3_1I8bUc4_Yen6LOc0E,830
160
166
  tests/unit_tests/test_color_validation.py,sha256=csdvVKAohENZIRY-JQ97Hv-TShb1erj4oKMX7QRwo78,1883
161
167
  tests/unit_tests/test_crosshair.py,sha256=3OMAJ2ZaISYXMOtkXf1rPdy94vCr8njeLi6uHblBL9Q,5045
@@ -180,8 +186,8 @@ tests/unit_tests/test_configs/config_device_no_entry.yaml,sha256=hdvue9KLc_kfNzG
180
186
  tests/unit_tests/test_configs/config_scan.yaml,sha256=vo484BbWOjA_e-h6bTjSV9k7QaQHrlAvx-z8wtY-P4E,1915
181
187
  tests/unit_tests/test_msgs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
182
188
  tests/unit_tests/test_msgs/available_scans_message.py,sha256=m_z97hIrjHXXMa2Ex-UvsPmTxOYXfjxyJaGkIY6StTY,46532
183
- bec_widgets-0.66.0.dist-info/METADATA,sha256=zGYdWrGWioZkJQL84cTkSoYLziwjhiDoVOCpJ2wCdL0,1302
184
- bec_widgets-0.66.0.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
185
- bec_widgets-0.66.0.dist-info/entry_points.txt,sha256=OvoqiNzNF9bizFQNhbAmmdc_njHrnVewLE-Kl-u9sh0,115
186
- bec_widgets-0.66.0.dist-info/licenses/LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
187
- bec_widgets-0.66.0.dist-info/RECORD,,
189
+ bec_widgets-0.67.0.dist-info/METADATA,sha256=9Cs9V5N0bA887XBwZGWJ08opZvj8rpWinMW3R2r56rs,1302
190
+ bec_widgets-0.67.0.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
191
+ bec_widgets-0.67.0.dist-info/entry_points.txt,sha256=OvoqiNzNF9bizFQNhbAmmdc_njHrnVewLE-Kl-u9sh0,115
192
+ bec_widgets-0.67.0.dist-info/licenses/LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
193
+ bec_widgets-0.67.0.dist-info/RECORD,,
@@ -8,5 +8,4 @@ maxdepth: 2
8
8
  hidden: false
9
9
  ---
10
10
 
11
- how_to_develop_a_widget/
12
11
  ```
docs/requirements.txt CHANGED
@@ -7,5 +7,6 @@ sphinx-copybutton
7
7
  myst-parser
8
8
  sphinx-design
9
9
  PyQt6
10
+ PyQt6-WebEngine
10
11
  bec-widgets
11
12
  tomli
@@ -13,13 +13,13 @@ To install BEC Widgets using the pip package manager, execute the following comm
13
13
 
14
14
 
15
15
  ```bash
16
- pip install bec_widgets[pyqt6]
16
+ pip install 'bec_widgets[pyqt6]'
17
17
  ```
18
18
 
19
19
  In case you want to use PyQt5, you can install it by using the following command:
20
20
 
21
21
  ```bash
22
- pip install bec_widgets[pyqt5]
22
+ pip install 'bec_widgets[pyqt5]'
23
23
  ```
24
24
 
25
25
  **Troubleshooting**
Binary file
@@ -0,0 +1,30 @@
1
+ (user.widgets.bec_status_box)=
2
+ # BEC Status Box
3
+ **Purpose:**
4
+
5
+ The [BECStatusBox]](/api_reference/_autosummary/bec_widgets.cli.client.BECStatusBox) Widget is a widget that allows you to monitor the status/health of the all running BEC processes. The widget generates the view automatically and updates the status of the processes in real-time. The top level indicates the overall state of the BEC core services (DeviceServer, ScanServer, SciHub, ScanBundler and FileWriter), but you can also see the status of each individual process by opening the collapsed view. In the collapsed view, you can double click on each process to get a popup window with live updates of the metrics for each process in real-time.
6
+
7
+ **Key Features:**
8
+
9
+ - monitor the state of individual BEC services.
10
+ - automatically track BEC services, i.e. additional clients connecting.
11
+ - live-updates of the metrics for each process.
12
+
13
+ **Example of Use:**
14
+ ![BECStatus](./bec_status_box.gif)
15
+
16
+ **Code example:**
17
+
18
+ The following code snipped demonstrates how to create a `BECStatusBox` widget using BEC Widgets within BEC.
19
+ ```python
20
+ bec_status_box = gui.add_dock().add_widget("BECStatusBox")
21
+ ```
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
@@ -1,5 +1,4 @@
1
1
  (user.widgets.buttons)=
2
-
3
2
  # Buttons Widgets
4
3
 
5
4
  This section consolidates various custom buttons used within the BEC GUIs, facilitating the integration of these
@@ -13,6 +13,8 @@ spiral_progress_bar/
13
13
  website/
14
14
  buttons/
15
15
  text_box/
16
+ bec_status_box/
17
+
16
18
  ```
17
19
 
18
20
 
pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bec_widgets"
7
- version = "0.66.0"
7
+ version = "0.67.0"
8
8
  description = "BEC Widgets"
9
9
  requires-python = ">=3.10"
10
10
  classifiers = [
@@ -0,0 +1,152 @@
1
+ import re
2
+ from unittest import mock
3
+
4
+ import pytest
5
+ from bec_lib.messages import BECStatus, ServiceMetricMessage, StatusMessage
6
+ from qtpy.QtCore import QMetaMethod
7
+
8
+ from bec_widgets.widgets.bec_status_box.bec_status_box import BECServiceInfoContainer, BECStatusBox
9
+
10
+ from .client_mocks import mocked_client
11
+
12
+
13
+ @pytest.fixture
14
+ def status_box(qtbot, mocked_client):
15
+ with mock.patch(
16
+ "bec_widgets.widgets.bec_status_box.bec_status_box.BECServiceStatusMixin"
17
+ ) as mock_service_status_mixin:
18
+ widget = BECStatusBox(client=mocked_client)
19
+ qtbot.addWidget(widget)
20
+ qtbot.waitExposed(widget)
21
+ yield widget
22
+
23
+
24
+ def test_status_box_init(qtbot, mocked_client):
25
+ with mock.patch(
26
+ "bec_widgets.widgets.bec_status_box.bec_status_box.BECServiceStatusMixin"
27
+ ) as mock_service_status_mixin:
28
+ name = "my test"
29
+ widget = BECStatusBox(parent=None, service_name=name, client=mocked_client)
30
+ qtbot.addWidget(widget)
31
+ qtbot.waitExposed(widget)
32
+ assert widget.headerItem().DontShowIndicator.value == 1
33
+ assert widget.children()[0].children()[0].config.service_name == name
34
+
35
+
36
+ def test_update_top_item(qtbot, mocked_client):
37
+ with (
38
+ mock.patch(
39
+ "bec_widgets.widgets.bec_status_box.bec_status_box.BECServiceStatusMixin"
40
+ ) as mock_service_status_mixin,
41
+ mock.patch(
42
+ "bec_widgets.widgets.bec_status_box.status_item.StatusItem.update_config"
43
+ ) as mock_update,
44
+ ):
45
+ name = "my test"
46
+ widget = BECStatusBox(parent=None, service_name=name, client=mocked_client)
47
+ qtbot.addWidget(widget)
48
+ qtbot.waitExposed(widget)
49
+ widget.update_top_item_status(status="RUNNING")
50
+ assert widget.bec_service_info_container[name].status == "RUNNING"
51
+ assert mock_update.call_args == mock.call(widget.bec_service_info_container[name].dict())
52
+
53
+
54
+ def test_create_status_widget(status_box):
55
+ name = "test_service"
56
+ status = BECStatus.IDLE
57
+ info = {"test": "test"}
58
+ metrics = {"metric": "test_metric"}
59
+ item = status_box._create_status_widget(name, status, info, metrics)
60
+ assert item.config.service_name == name
61
+ assert item.config.status == status.name
62
+ assert item.config.info == info
63
+ assert item.config.metrics == metrics
64
+
65
+
66
+ def test_bec_service_container(status_box):
67
+ name = "test_service"
68
+ status = BECStatus.IDLE
69
+ info = {"test": "test"}
70
+ metrics = {"metric": "test_metric"}
71
+ expected_return = BECServiceInfoContainer(
72
+ service_name=name, status=status, info=info, metrics=metrics
73
+ )
74
+ assert status_box.service_name in status_box.bec_service_info_container
75
+ assert len(status_box.bec_service_info_container) == 1
76
+ status_box._update_bec_service_container(name, status, info, metrics)
77
+ assert len(status_box.bec_service_info_container) == 2
78
+ assert status_box.bec_service_info_container[name] == expected_return
79
+
80
+
81
+ def test_add_tree_item(status_box):
82
+ name = "test_service"
83
+ status = BECStatus.IDLE
84
+ info = {"test": "test"}
85
+ metrics = {"metric": "test_metric"}
86
+ assert len(status_box.children()[0].children()) == 1
87
+ status_box.add_tree_item(name, status, info, metrics)
88
+ assert len(status_box.children()[0].children()) == 2
89
+ assert name in status_box.tree_items
90
+
91
+
92
+ def test_update_service_status(status_box):
93
+ """Also checks check redundant tree items"""
94
+ name = "test_service"
95
+ status = BECStatus.IDLE
96
+ info = {"test": "test"}
97
+ metrics = {"metric": "test_metric"}
98
+ status_box.add_tree_item(name, status, info, {})
99
+ not_connected_name = "invalid_service"
100
+ status_box.add_tree_item(not_connected_name, status, info, metrics)
101
+
102
+ services_status = {name: StatusMessage(name=name, status=status, info=info)}
103
+ services_metrics = {name: ServiceMetricMessage(name=name, metrics=metrics)}
104
+
105
+ with mock.patch.object(status_box, "update_core_services", return_value=services_status):
106
+ assert not_connected_name in status_box.tree_items
107
+ status_box.update_service_status(services_status, services_metrics)
108
+ assert status_box.tree_items[name][1].config.metrics == metrics
109
+ assert not_connected_name not in status_box.tree_items
110
+
111
+
112
+ def test_update_core_services(qtbot, mocked_client):
113
+ with (
114
+ mock.patch(
115
+ "bec_widgets.widgets.bec_status_box.bec_status_box.BECServiceStatusMixin"
116
+ ) as mock_service_status_mixin,
117
+ mock.patch(
118
+ "bec_widgets.widgets.bec_status_box.bec_status_box.BECStatusBox.update_top_item_status"
119
+ ) as mock_update,
120
+ ):
121
+ name = "my test"
122
+ status_box = BECStatusBox(parent=None, service_name=name, client=mocked_client)
123
+ qtbot.addWidget(status_box)
124
+ qtbot.waitExposed(status_box)
125
+ status_box.CORE_SERVICES = ["test_service"]
126
+ name = "test_service"
127
+ status = BECStatus.RUNNING
128
+ info = {"test": "test"}
129
+ metrics = {"metric": "test_metric"}
130
+ services_status = {name: StatusMessage(name=name, status=status, info=info)}
131
+ services_metrics = {name: ServiceMetricMessage(name=name, metrics=metrics)}
132
+
133
+ status_box.update_core_services(services_status, services_metrics)
134
+ assert mock_update.call_args == mock.call(status.name)
135
+
136
+ status = BECStatus.IDLE
137
+ services_status = {name: StatusMessage(name=name, status=status, info=info)}
138
+ services_metrics = {name: ServiceMetricMessage(name=name, metrics=metrics)}
139
+ status_box.update_core_services(services_status, services_metrics)
140
+ assert mock_update.call_args == mock.call("ERROR")
141
+
142
+
143
+ def test_double_click_item(status_box):
144
+ name = "test_service"
145
+ status = BECStatus.IDLE
146
+ info = {"test": "test"}
147
+ metrics = {"MyData": "This should be shown nicely"}
148
+ status_box.add_tree_item(name, status, info, metrics)
149
+ item, status_item = status_box.tree_items[name]
150
+ with mock.patch.object(status_item, "show_popup") as mock_show_popup:
151
+ status_box.itemDoubleClicked.emit(item, 0)
152
+ assert mock_show_popup.call_count == 1