bec-widgets 2.8.4__py3-none-any.whl → 2.9.1__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,6 +1,42 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v2.9.1 (2025-05-30)
5
+
6
+ ### Bug Fixes
7
+
8
+ - Make registry update log message debug level
9
+ ([`12f8c82`](https://github.com/bec-project/bec_widgets/commit/12f8c82eb59ed6a7273b57126efe340bf37b65cc))
10
+
11
+
12
+ ## v2.9.0 (2025-05-30)
13
+
14
+ ### Bug Fixes
15
+
16
+ - **DeviceSignalInput**: Improve robustness
17
+ ([`91195ae`](https://github.com/bec-project/bec_widgets/commit/91195ae0fdf024daf2daaa4ea2963992b4e40e04))
18
+
19
+ use set for storing filter properties to allow multiple set to true or false
20
+
21
+ ### Code Style
22
+
23
+ - Typing in bec_dispatcher
24
+ ([`a6c5c21`](https://github.com/bec-project/bec_widgets/commit/a6c5c21afaa6dcf33ce71027e8730354ee34e3b4))
25
+
26
+ ### Documentation
27
+
28
+ - Add usage docs for signal label widget
29
+ ([`2b9919b`](https://github.com/bec-project/bec_widgets/commit/2b9919bb34a66708f4b910ffc17dc253e9b7f70d))
30
+
31
+ ### Features
32
+
33
+ - (#569) add signal label widget
34
+ ([`822e7d0`](https://github.com/bec-project/bec_widgets/commit/822e7d06ff7479d006ae99942fed5e2c836831ce))
35
+
36
+ add a widget which shows the current value of a signal from BEC. configurable with many properties
37
+ in designer. intended for use mainly in static GUIs.
38
+
39
+
4
40
  ## v2.8.4 (2025-05-30)
5
41
 
6
42
  ### Bug Fixes
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 2.8.4
3
+ Version: 2.9.1
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
@@ -52,6 +52,7 @@ _Widgets = {
52
52
  "ScanControl": "ScanControl",
53
53
  "ScatterWaveform": "ScatterWaveform",
54
54
  "SignalComboBox": "SignalComboBox",
55
+ "SignalLabel": "SignalLabel",
55
56
  "SignalLineEdit": "SignalLineEdit",
56
57
  "StopButton": "StopButton",
57
58
  "TextBox": "TextBox",
@@ -3459,6 +3460,78 @@ class SignalComboBox(RPCBase):
3459
3460
  """
3460
3461
 
3461
3462
 
3463
+ class SignalLabel(RPCBase):
3464
+ @property
3465
+ @rpc_call
3466
+ def custom_label(self) -> "str":
3467
+ """
3468
+ Use a cusom label rather than the signal name
3469
+ """
3470
+
3471
+ @property
3472
+ @rpc_call
3473
+ def custom_units(self) -> "str":
3474
+ """
3475
+ Use a custom unit string
3476
+ """
3477
+
3478
+ @custom_label.setter
3479
+ @rpc_call
3480
+ def custom_label(self) -> "str":
3481
+ """
3482
+ Use a cusom label rather than the signal name
3483
+ """
3484
+
3485
+ @custom_units.setter
3486
+ @rpc_call
3487
+ def custom_units(self) -> "str":
3488
+ """
3489
+ Use a custom unit string
3490
+ """
3491
+
3492
+ @property
3493
+ @rpc_call
3494
+ def decimal_places(self) -> "int":
3495
+ """
3496
+ Format to a given number of decimal_places. Set to 0 to disable.
3497
+ """
3498
+
3499
+ @decimal_places.setter
3500
+ @rpc_call
3501
+ def decimal_places(self) -> "int":
3502
+ """
3503
+ Format to a given number of decimal_places. Set to 0 to disable.
3504
+ """
3505
+
3506
+ @property
3507
+ @rpc_call
3508
+ def show_default_units(self) -> "bool":
3509
+ """
3510
+ Show default units obtained from the signal alongside it
3511
+ """
3512
+
3513
+ @show_default_units.setter
3514
+ @rpc_call
3515
+ def show_default_units(self) -> "bool":
3516
+ """
3517
+ Show default units obtained from the signal alongside it
3518
+ """
3519
+
3520
+ @property
3521
+ @rpc_call
3522
+ def show_select_button(self) -> "bool":
3523
+ """
3524
+ Show the button to select the signal to display
3525
+ """
3526
+
3527
+ @show_select_button.setter
3528
+ @rpc_call
3529
+ def show_select_button(self) -> "bool":
3530
+ """
3531
+ Show the button to select the signal to display
3532
+ """
3533
+
3534
+
3462
3535
  class SignalLineEdit(RPCBase):
3463
3536
  """Line edit widget for device input with autocomplete for device names."""
3464
3537
 
@@ -163,7 +163,7 @@ class BECDispatcher:
163
163
  def connect_slot(
164
164
  self,
165
165
  slot: Callable,
166
- topics: Union[EndpointInfo, str, list[Union[EndpointInfo, str]]],
166
+ topics: EndpointInfo | str | list[EndpointInfo] | list[str],
167
167
  cb_info: dict | None = None,
168
168
  **kwargs,
169
169
  ) -> None:
@@ -172,7 +172,7 @@ class BECDispatcher:
172
172
  Args:
173
173
  slot (Callable): A slot method/function that accepts two inputs: content and metadata of
174
174
  the corresponding pub/sub message
175
- topics (EndpointInfo | str | list): A topic or list of topics that can typically be acquired via bec_lib.MessageEndpoints
175
+ topics EndpointInfo | str | list[EndpointInfo] | list[str]: A topic or list of topics that can typically be acquired via bec_lib.MessageEndpoints
176
176
  cb_info (dict | None): A dictionary containing information about the callback. Defaults to None.
177
177
  """
178
178
  qt_slot = QtThreadSafeCallback(cb=slot, cb_info=cb_info)
@@ -183,13 +183,15 @@ class BECDispatcher:
183
183
  topics_str, _ = self.client.connector._convert_endpointinfo(topics)
184
184
  qt_slot.topics.update(set(topics_str))
185
185
 
186
- def disconnect_slot(self, slot: Callable, topics: Union[str, list]):
186
+ def disconnect_slot(
187
+ self, slot: Callable, topics: EndpointInfo | str | list[EndpointInfo] | list[str]
188
+ ):
187
189
  """
188
190
  Disconnect a slot from a topic.
189
191
 
190
192
  Args:
191
193
  slot(Callable): The slot to disconnect
192
- topics(Union[str, list]): The topic(s) to disconnect from
194
+ topics EndpointInfo | str | list[EndpointInfo] | list[str]: A topic or list of topics to unsub from.
193
195
  """
194
196
  # find the right slot to disconnect from ;
195
197
  # slot callbacks are wrapped in QtThreadSafeCallback objects,
@@ -195,7 +195,7 @@ class RPCServer:
195
195
  return
196
196
  self._broadcasted_data = data
197
197
 
198
- logger.info(f"Broadcasting registry update: {data} for {self.gui_id}")
198
+ logger.debug(f"Broadcasting registry update: {data} for {self.gui_id}")
199
199
  self.client.connector.xadd(
200
200
  MessageEndpoints.gui_registry_state(self.gui_id),
201
201
  msg_dict={"data": messages.GUIRegistryStateMessage(state=data)},
@@ -1,10 +1,11 @@
1
1
  from bec_lib.callback_handler import EventType
2
2
  from bec_lib.device import Signal
3
3
  from bec_lib.logger import bec_logger
4
- from qtpy.QtCore import Property, Slot
4
+ from qtpy.QtCore import Property
5
5
 
6
6
  from bec_widgets.utils import ConnectionConfig
7
7
  from bec_widgets.utils.bec_widget import BECWidget
8
+ from bec_widgets.utils.error_popups import SafeSlot
8
9
  from bec_widgets.utils.filter_io import FilterIO
9
10
  from bec_widgets.utils.ophyd_kind_util import Kind
10
11
  from bec_widgets.utils.widget_io import WidgetIO
@@ -49,7 +50,7 @@ class DeviceSignalInputBase(BECWidget):
49
50
 
50
51
  self._device = None
51
52
  self.get_bec_shortcuts()
52
- self._signal_filter = []
53
+ self._signal_filter = set()
53
54
  self._signals = []
54
55
  self._hinted_signals = []
55
56
  self._normal_signals = []
@@ -60,7 +61,7 @@ class DeviceSignalInputBase(BECWidget):
60
61
 
61
62
  ### Qt Slots ###
62
63
 
63
- @Slot(str)
64
+ @SafeSlot(str)
64
65
  def set_signal(self, signal: str):
65
66
  """
66
67
  Set the signal.
@@ -76,7 +77,7 @@ class DeviceSignalInputBase(BECWidget):
76
77
  f"Signal {signal} not found for device {self.device} and filtered selection {self.signal_filter}."
77
78
  )
78
79
 
79
- @Slot(str)
80
+ @SafeSlot(str)
80
81
  def set_device(self, device: str | None):
81
82
  """
82
83
  Set the device. If device is not valid, device will be set to None which happens
@@ -90,8 +91,8 @@ class DeviceSignalInputBase(BECWidget):
90
91
  self._device = device
91
92
  self.update_signals_from_filters()
92
93
 
93
- @Slot(dict, dict)
94
- @Slot()
94
+ @SafeSlot(dict, dict)
95
+ @SafeSlot()
95
96
  def update_signals_from_filters(
96
97
  self, content: dict | None = None, metadata: dict | None = None
97
98
  ):
@@ -158,9 +159,9 @@ class DeviceSignalInputBase(BECWidget):
158
159
  @include_hinted_signals.setter
159
160
  def include_hinted_signals(self, value: bool):
160
161
  if value:
161
- self._signal_filter.append(Kind.hinted)
162
+ self._signal_filter.add(Kind.hinted)
162
163
  else:
163
- self._signal_filter.remove(Kind.hinted)
164
+ self._signal_filter.discard(Kind.hinted)
164
165
  self.update_signals_from_filters()
165
166
 
166
167
  @Property(bool)
@@ -171,9 +172,9 @@ class DeviceSignalInputBase(BECWidget):
171
172
  @include_normal_signals.setter
172
173
  def include_normal_signals(self, value: bool):
173
174
  if value:
174
- self._signal_filter.append(Kind.normal)
175
+ self._signal_filter.add(Kind.normal)
175
176
  else:
176
- self._signal_filter.remove(Kind.normal)
177
+ self._signal_filter.discard(Kind.normal)
177
178
  self.update_signals_from_filters()
178
179
 
179
180
  @Property(bool)
@@ -184,9 +185,9 @@ class DeviceSignalInputBase(BECWidget):
184
185
  @include_config_signals.setter
185
186
  def include_config_signals(self, value: bool):
186
187
  if value:
187
- self._signal_filter.append(Kind.config)
188
+ self._signal_filter.add(Kind.config)
188
189
  else:
189
- self._signal_filter.remove(Kind.config)
190
+ self._signal_filter.discard(Kind.config)
190
191
  self.update_signals_from_filters()
191
192
 
192
193
  ### Properties and Methods ###
@@ -1,11 +1,13 @@
1
1
  from bec_lib.device import Positioner
2
- from qtpy.QtCore import QSize, Signal, Slot
2
+ from qtpy.QtCore import QSize, Signal
3
3
  from qtpy.QtWidgets import QComboBox, QSizePolicy
4
4
 
5
+ from bec_widgets.utils.error_popups import SafeSlot
5
6
  from bec_widgets.utils.filter_io import ComboBoxFilterHandler, FilterIO
6
7
  from bec_widgets.utils.ophyd_kind_util import Kind
7
8
  from bec_widgets.widgets.control.device_input.base_classes.device_signal_input_base import (
8
9
  DeviceSignalInputBase,
10
+ DeviceSignalInputBaseConfig,
9
11
  )
10
12
 
11
13
 
@@ -35,7 +37,7 @@ class SignalComboBox(DeviceSignalInputBase, QComboBox):
35
37
  self,
36
38
  parent=None,
37
39
  client=None,
38
- config: DeviceSignalInputBase = None,
40
+ config: DeviceSignalInputBaseConfig | None = None,
39
41
  gui_id: str | None = None,
40
42
  device: str | None = None,
41
43
  signal_filter: str | list[str] | None = None,
@@ -65,9 +67,13 @@ class SignalComboBox(DeviceSignalInputBase, QComboBox):
65
67
  if default is not None:
66
68
  self.set_signal(default)
67
69
 
68
- def update_signals_from_filters(self):
70
+ @SafeSlot()
71
+ @SafeSlot(dict, dict)
72
+ def update_signals_from_filters(
73
+ self, content: dict | None = None, metadata: dict | None = None
74
+ ):
69
75
  """Update the filters for the combobox"""
70
- super().update_signals_from_filters()
76
+ super().update_signals_from_filters(content, metadata)
71
77
  # pylint: disable=protected-access
72
78
  if FilterIO._find_handler(self) is ComboBoxFilterHandler:
73
79
  if len(self._config_signals) > 0:
@@ -84,7 +90,7 @@ class SignalComboBox(DeviceSignalInputBase, QComboBox):
84
90
  self.insertItem(0, "Hinted Signals")
85
91
  self.model().item(0).setEnabled(False)
86
92
 
87
- @Slot(str)
93
+ @SafeSlot(str)
88
94
  def on_text_changed(self, text: str):
89
95
  """Slot for text changed. If a device is selected and the signal is changed and valid it emits a signal.
90
96
  For a positioner, the readback value has to be renamed to the device name.
@@ -0,0 +1,15 @@
1
+ def main(): # pragma: no cover
2
+ from qtpy import PYSIDE6
3
+
4
+ if not PYSIDE6:
5
+ print("PYSIDE6 is not available in the environment. Cannot patch designer.")
6
+ return
7
+ from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
8
+
9
+ from bec_widgets.widgets.utility.signal_label.signal_label_plugin import SignalLabelPlugin
10
+
11
+ QPyDesignerCustomWidgetCollection.addCustomWidget(SignalLabelPlugin())
12
+
13
+
14
+ if __name__ == "__main__": # pragma: no cover
15
+ main()
@@ -0,0 +1,456 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ import traceback
5
+ from typing import TYPE_CHECKING
6
+
7
+ from bec_lib.device import Device, Signal
8
+ from bec_lib.endpoints import MessageEndpoints
9
+ from bec_qthemes import material_icon
10
+ from qtpy.QtCore import Signal as QSignal
11
+ from qtpy.QtWidgets import (
12
+ QApplication,
13
+ QComboBox,
14
+ QDialog,
15
+ QDialogButtonBox,
16
+ QGroupBox,
17
+ QHBoxLayout,
18
+ QLabel,
19
+ QLineEdit,
20
+ QToolButton,
21
+ QVBoxLayout,
22
+ QWidget,
23
+ )
24
+
25
+ from bec_widgets.utils.bec_connector import ConnectionConfig
26
+ from bec_widgets.utils.bec_widget import BECWidget
27
+ from bec_widgets.utils.colors import get_accent_colors
28
+ from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
29
+ from bec_widgets.utils.ophyd_kind_util import Kind
30
+ from bec_widgets.widgets.control.device_input.base_classes.device_input_base import (
31
+ DeviceInputConfig,
32
+ )
33
+ from bec_widgets.widgets.control.device_input.base_classes.device_signal_input_base import (
34
+ DeviceSignalInputBaseConfig,
35
+ )
36
+ from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit import (
37
+ DeviceLineEdit,
38
+ )
39
+ from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import SignalComboBox
40
+
41
+ if TYPE_CHECKING:
42
+ from bec_lib.client import BECClient
43
+
44
+
45
+ class ChoiceDialog(QDialog):
46
+ accepted_output = QSignal(str, str)
47
+
48
+ CONNECTION_ERROR_STR = "Error: client is not connected!"
49
+
50
+ def __init__(
51
+ self,
52
+ parent: QWidget | None = None,
53
+ config: ConnectionConfig | None = None,
54
+ client: BECClient | None = None,
55
+ show_hinted: bool = True,
56
+ show_normal: bool = False,
57
+ show_config: bool = False,
58
+ ):
59
+ if not client or not client.started:
60
+ self._display_error()
61
+ return
62
+ super().__init__(parent=parent)
63
+ self.setWindowTitle("Choose device and signal...")
64
+ self._accent_colors = get_accent_colors()
65
+
66
+ layout = QHBoxLayout()
67
+
68
+ config_dict = config.model_dump() if config is not None else {}
69
+ self._device_config = DeviceInputConfig.model_validate(config_dict)
70
+ self._signal_config = DeviceSignalInputBaseConfig.model_validate(config_dict)
71
+ self._device_field = DeviceLineEdit(
72
+ config=self._device_config, parent=parent, client=client
73
+ )
74
+ self._signal_field = SignalComboBox(
75
+ config=self._signal_config,
76
+ device=self._signal_config.device,
77
+ parent=parent,
78
+ client=client,
79
+ )
80
+ layout.addWidget(self._device_field)
81
+ layout.addWidget(self._signal_field)
82
+
83
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
84
+ self.button_box.accepted.connect(self.accept)
85
+ self.button_box.rejected.connect(self.reject)
86
+ layout.addWidget(self.button_box)
87
+
88
+ self._signal_field.include_hinted_signals = show_hinted
89
+ self._signal_field.include_normal_signals = show_normal
90
+ self._signal_field.include_config_signals = show_config
91
+
92
+ self.setLayout(layout)
93
+ self._device_field.textChanged.connect(self._update_device)
94
+ self._device_field.setText(config.device if config is not None else "")
95
+
96
+ def _display_error(self):
97
+ try:
98
+ super().__init__()
99
+ except Exception:
100
+ ...
101
+ layout = QHBoxLayout()
102
+ layout.addWidget(QLabel(self.CONNECTION_ERROR_STR))
103
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Cancel)
104
+ self.button_box.accepted.connect(self.reject)
105
+ layout.addWidget(self.button_box)
106
+ self.setLayout(layout)
107
+
108
+ @SafeSlot(str)
109
+ def _update_device(self, device: str):
110
+ if device in self._device_field.dev:
111
+ self._device_field.set_device(device)
112
+ self._signal_field.set_device(device)
113
+ self._device_field.setStyleSheet(
114
+ f"QLineEdit {{ border-style: solid; border-width: 2px; border-color: {self._accent_colors.success.name() if self._accent_colors else 'green'}}}"
115
+ )
116
+ self.button_box.button(QDialogButtonBox.Ok).setEnabled(True)
117
+ else:
118
+ self._device_field.setStyleSheet(
119
+ f"QLineEdit {{ border-style: solid; border-width: 2px; border-color: {self._accent_colors.emergency.name() if self._accent_colors else 'red'}}}"
120
+ )
121
+ self.button_box.button(QDialogButtonBox.Ok).setEnabled(False)
122
+ self._signal_field.clear()
123
+
124
+ def accept(self):
125
+ self.accepted_output.emit(self._device_field.text(), self._signal_field.currentText())
126
+ return super().accept()
127
+
128
+
129
+ class SignalLabel(BECWidget, QWidget):
130
+
131
+ ICON_NAME = "scoreboard"
132
+ RPC = True
133
+ PLUGIN = True
134
+
135
+ USER_ACCESS = [
136
+ "custom_label",
137
+ "custom_units",
138
+ "custom_label.setter",
139
+ "custom_units.setter",
140
+ "decimal_places",
141
+ "decimal_places.setter",
142
+ "show_default_units",
143
+ "show_default_units.setter",
144
+ "show_select_button",
145
+ "show_select_button.setter",
146
+ ]
147
+
148
+ def __init__(
149
+ self,
150
+ parent: QWidget | None = None,
151
+ client: BECClient | None = None,
152
+ device: str | None = None,
153
+ signal: str | None = None,
154
+ show_select_button: bool = True,
155
+ show_default_units: bool = False,
156
+ custom_label: str = "",
157
+ custom_units: str = "",
158
+ **kwargs,
159
+ ):
160
+ """Initialize the SignalLabel widget.
161
+
162
+ Args:
163
+ parent (QWidget, optional): The parent widget. Defaults to None.
164
+ client (BECClient, optional): The BEC client. Defaults to None.
165
+ device (str, optional): The device name. Defaults to None.
166
+ signal (str, optional): The signal name. Defaults to None.
167
+ selection_dialog_config (DeviceSignalInputBaseConfig | dict, optional): Configuration for the signal selection dialog.
168
+ show_select_button (bool, optional): Whether to show the select button. Defaults to True.
169
+ show_default_units (bool, optional): Whether to show default units. Defaults to False.
170
+ custom_label (str, optional): Custom label for the widget. Defaults to "".
171
+ custom_units (str, optional): Custom units for the widget. Defaults to "".
172
+ """
173
+ self._config = DeviceSignalInputBaseConfig(default=signal, device=device)
174
+ super().__init__(parent=parent, client=client, **kwargs)
175
+
176
+ self._device = device
177
+ self._signal = signal
178
+
179
+ self._custom_label: str = custom_label
180
+ self._custom_units: str = custom_units
181
+ self._show_default_units: bool = show_default_units
182
+ self._decimal_places = 3
183
+
184
+ self._show_hinted_signals: bool = True
185
+ self._show_normal_signals: bool = False
186
+ self._show_config_signals: bool = False
187
+
188
+ self._outer_layout = QHBoxLayout()
189
+ self._layout = QHBoxLayout()
190
+ self._outer_layout.setContentsMargins(0, 0, 0, 0)
191
+ self._layout.setContentsMargins(0, 0, 0, 0)
192
+ self.setLayout(self._outer_layout)
193
+
194
+ self._label = QGroupBox(custom_label)
195
+ self._outer_layout.addWidget(self._label)
196
+ self._update_label()
197
+ self._label.setLayout(self._layout)
198
+
199
+ self._value: str = ""
200
+ self._display = QLabel()
201
+ self._layout.addWidget(self._display)
202
+
203
+ self._select_button = QToolButton()
204
+ self._select_button.setIcon(material_icon(icon_name="settings", size=(20, 20)))
205
+ self._show_select_button: bool = show_select_button
206
+ self._layout.addWidget(self._select_button)
207
+ self._display.setMinimumHeight(self._select_button.sizeHint().height())
208
+ self.show_select_button = self._show_select_button
209
+
210
+ self._select_button.clicked.connect(self.show_choice_dialog)
211
+ self.get_bec_shortcuts()
212
+
213
+ self._connected: bool = False
214
+ self.connect_device()
215
+
216
+ def _create_dialog(self):
217
+ return ChoiceDialog(
218
+ config=self._config,
219
+ parent=self,
220
+ client=self.client,
221
+ show_config=self.show_config_signals,
222
+ show_normal=self.show_normal_signals,
223
+ show_hinted=self.show_hinted_signals,
224
+ )
225
+
226
+ @SafeSlot()
227
+ def _process_dialog(self, device: str, signal: str):
228
+ self.disconnect_device()
229
+ self.device = device
230
+ self.signal = signal
231
+ self._update_label()
232
+ self.connect_device()
233
+
234
+ def show_choice_dialog(self):
235
+ dialog = self._create_dialog()
236
+ dialog.accepted_output.connect(self._process_dialog)
237
+ dialog.open()
238
+ return dialog
239
+
240
+ def connect_device(self):
241
+ """Subscribe to the Redis topic for the device to display"""
242
+ if not self._connected and self._device and self._device in self.dev:
243
+ self._connected = True
244
+ self._readback_endpoint = MessageEndpoints.device_readback(self._device)
245
+ self.bec_dispatcher.connect_slot(self.on_device_readback, self._readback_endpoint)
246
+ self._manual_read()
247
+ self.set_display_value(self._value)
248
+
249
+ def disconnect_device(self):
250
+ """Unsubscribe from the Redis topic for the device to display"""
251
+ if self._connected:
252
+ self._connected = False
253
+ self.bec_dispatcher.disconnect_slot(self.on_device_readback, self._readback_endpoint)
254
+
255
+ def _manual_read(self):
256
+ if self._device is None or not isinstance(
257
+ (device := self.dev.get(self._device)), Device | Signal
258
+ ):
259
+ self._units = ""
260
+ self._value = "__"
261
+ return
262
+ signal: Signal = (
263
+ getattr(device, self.signal, None) if isinstance(device, Device) else device
264
+ )
265
+ if not isinstance(signal, Signal): # Avoid getting other attributes of device, e.g. methods
266
+ signal = None
267
+ if signal is None:
268
+ self._units = ""
269
+ self._value = "__"
270
+ return
271
+ self._value = signal.get()
272
+ self._units = signal.get_device_config().get("egu", "")
273
+
274
+ @SafeSlot(dict, dict)
275
+ def on_device_readback(self, msg: dict, metadata: dict) -> None:
276
+ """
277
+ Update the display with the new value.
278
+ """
279
+ try:
280
+ signal_to_read = self._patch_hinted_signal()
281
+ self._value = msg["signals"][signal_to_read]["value"]
282
+ self.set_display_value(self._value)
283
+ except Exception as e:
284
+ self._display.setText("ERROR!")
285
+ self._display.setToolTip(
286
+ f"Error processing incoming reading: {msg}, handled with exception: {''.join(traceback.format_exception(e))}"
287
+ )
288
+
289
+ def _patch_hinted_signal(self):
290
+ if self.dev[self._device]._info["signals"] == {}:
291
+ return self._signal
292
+ signal_info = self.dev[self._device]._info["signals"][self._signal]
293
+ return (
294
+ signal_info["obj_name"] if signal_info["kind_str"] == Kind.hinted.name else self._signal
295
+ )
296
+
297
+ @SafeProperty(str)
298
+ def device(self) -> str:
299
+ """The device from which to select a signal"""
300
+ return self._device or "Not set!"
301
+
302
+ @device.setter
303
+ def device(self, value: str) -> None:
304
+ self.disconnect_device()
305
+ self._device = value
306
+ self._config.device = value
307
+ self.connect_device()
308
+ self._update_label()
309
+
310
+ @SafeProperty(str)
311
+ def signal(self) -> str:
312
+ """The signal to display"""
313
+ return self._signal or "Not set!"
314
+
315
+ @signal.setter
316
+ def signal(self, value: str) -> None:
317
+ self.disconnect_device()
318
+ self._signal = value
319
+ self._config.default = value
320
+ self.connect_device()
321
+ self._update_label()
322
+
323
+ @SafeProperty(bool)
324
+ def show_select_button(self) -> bool:
325
+ """Show the button to select the signal to display"""
326
+ return self._show_select_button
327
+
328
+ @show_select_button.setter
329
+ def show_select_button(self, value: bool) -> None:
330
+ self._show_select_button = value
331
+ self._select_button.setVisible(value)
332
+
333
+ @SafeProperty(bool)
334
+ def show_default_units(self) -> bool:
335
+ """Show default units obtained from the signal alongside it"""
336
+ return self._show_default_units
337
+
338
+ @show_default_units.setter
339
+ def show_default_units(self, value: bool) -> None:
340
+ self._show_default_units = value
341
+ self.set_display_value(self._value)
342
+
343
+ @SafeProperty(str)
344
+ def custom_label(self) -> str:
345
+ """Use a cusom label rather than the signal name"""
346
+ return self._custom_label
347
+
348
+ @custom_label.setter
349
+ def custom_label(self, value: str) -> None:
350
+ self._custom_label = value
351
+ self._update_label()
352
+
353
+ @SafeProperty(str)
354
+ def custom_units(self) -> str:
355
+ """Use a custom unit string"""
356
+ return self._custom_units
357
+
358
+ @custom_units.setter
359
+ def custom_units(self, value: str) -> None:
360
+ self._custom_units = value
361
+ self.set_display_value(self._value)
362
+
363
+ @SafeProperty(int)
364
+ def decimal_places(self) -> int:
365
+ """Format to a given number of decimal_places. Set to 0 to disable."""
366
+ return self._decimal_places
367
+
368
+ @decimal_places.setter
369
+ def decimal_places(self, value: int) -> None:
370
+ self._decimal_places = value
371
+ self._update_label()
372
+
373
+ @SafeProperty(bool)
374
+ def show_hinted_signals(self) -> bool:
375
+ """In the signal selection menu, show hinted signals"""
376
+ return self._show_hinted_signals
377
+
378
+ @show_hinted_signals.setter
379
+ def show_hinted_signals(self, value: bool) -> None:
380
+ self._show_hinted_signals = value
381
+
382
+ @SafeProperty(bool)
383
+ def show_config_signals(self) -> bool:
384
+ """In the signal selection menu, show config signals"""
385
+ return self._show_config_signals
386
+
387
+ @show_config_signals.setter
388
+ def show_config_signals(self, value: bool) -> None:
389
+ self._show_config_signals = value
390
+
391
+ @SafeProperty(bool)
392
+ def show_normal_signals(self) -> bool:
393
+ """In the signal selection menu, show normal signals"""
394
+ return self._show_normal_signals
395
+
396
+ @show_normal_signals.setter
397
+ def show_normal_signals(self, value: bool) -> None:
398
+ self._show_normal_signals = value
399
+
400
+ def _format_value(self, value: str):
401
+ if self._decimal_places == 0:
402
+ return value
403
+ try:
404
+ return f"{float(value):0.{self._decimal_places}f}"
405
+ except ValueError:
406
+ return value
407
+
408
+ @SafeSlot(str)
409
+ def set_display_value(self, value: str):
410
+ """Set the display to a given value, appending the units if specified"""
411
+ self._display.setText(f"{self._format_value(value)}{self._units_string}")
412
+ self._display.setToolTip("")
413
+
414
+ @property
415
+ def _units_string(self):
416
+ if self.custom_units or self._show_default_units:
417
+ return f" {self.custom_units or self._default_units or ''}"
418
+ return ""
419
+
420
+ @property
421
+ def _default_units(self) -> str:
422
+ return self._units
423
+
424
+ @property
425
+ def _default_label(self) -> str:
426
+ return (
427
+ str(self._signal) if self._device == self._signal else f"{self._device} {self._signal}"
428
+ )
429
+
430
+ def _update_label(self):
431
+ self._label.setTitle(
432
+ self._custom_label if self._custom_label else f"{self._default_label}:"
433
+ )
434
+
435
+
436
+ if __name__ == "__main__":
437
+
438
+ app = QApplication(sys.argv)
439
+ w = QWidget()
440
+ w.setLayout(QVBoxLayout())
441
+ w.layout().addWidget(
442
+ SignalLabel(
443
+ device="samx",
444
+ signal="readback",
445
+ custom_label="custom label:",
446
+ custom_units=" m/s/s",
447
+ show_select_button=False,
448
+ )
449
+ )
450
+ w.layout().addWidget(SignalLabel(device="samy", signal="readback", show_default_units=True))
451
+ l = SignalLabel()
452
+ l.device = "bpm4i"
453
+ l.signal = "bpm4i"
454
+ w.layout().addWidget(l)
455
+ w.show()
456
+ sys.exit(app.exec_())
@@ -0,0 +1 @@
1
+ {'files': ['signal_label.py']}
@@ -0,0 +1,54 @@
1
+ # Copyright (C) 2022 The Qt Company Ltd.
2
+ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
3
+
4
+ from qtpy.QtDesigner import QDesignerCustomWidgetInterface
5
+
6
+ from bec_widgets.utils.bec_designer import designer_material_icon
7
+ from bec_widgets.widgets.utility.signal_label.signal_label import SignalLabel
8
+
9
+ DOM_XML = """
10
+ <ui language='c++'>
11
+ <widget class='SignalLabel' name='signal_label'>
12
+ </widget>
13
+ </ui>
14
+ """
15
+
16
+
17
+ class SignalLabelPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
18
+ def __init__(self):
19
+ super().__init__()
20
+ self._form_editor = None
21
+
22
+ def createWidget(self, parent):
23
+ t = SignalLabel(parent)
24
+ return t
25
+
26
+ def domXml(self):
27
+ return DOM_XML
28
+
29
+ def group(self):
30
+ return "BEC Utils"
31
+
32
+ def icon(self):
33
+ return designer_material_icon(SignalLabel.ICON_NAME)
34
+
35
+ def includeFile(self):
36
+ return "signal_label"
37
+
38
+ def initialize(self, form_editor):
39
+ self._form_editor = form_editor
40
+
41
+ def isContainer(self):
42
+ return False
43
+
44
+ def isInitialized(self):
45
+ return self._form_editor is not None
46
+
47
+ def name(self):
48
+ return "SignalLabel"
49
+
50
+ def toolTip(self):
51
+ return "Display the live value of any signal"
52
+
53
+ def whatsThis(self):
54
+ return self.toolTip()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 2.8.4
3
+ Version: 2.9.1
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=1nMYldzVk0tFkBWYTcUjumOrdSADASheWOAc0kOFDYs,9509
3
3
  .pylintrc,sha256=eeY8YwSI74oFfq6IYIbCqnx3Vk8ZncKaatv96n_Y8Rs,18544
4
4
  .readthedocs.yaml,sha256=ivqg3HTaOxNbEW3bzWh9MXAkrekuGoNdj0Mj3SdRYuw,639
5
- CHANGELOG.md,sha256=zrzPgv4SdkfI87Bynz1AKCww3iy1vOyt7iaEzOy2M4Q,289871
5
+ CHANGELOG.md,sha256=M__eaV2qFZ_rzZPL5yHSuCg6ymh4veC-MLWH7I20Qyc,290970
6
6
  LICENSE,sha256=Daeiu871NcAp8uYi4eB_qHgvypG-HX0ioRQyQxFwjeg,1531
7
- PKG-INFO,sha256=i_3fowZja6vwAtG6rabvccdxkcoUynup_6DHrxGLYuw,1273
7
+ PKG-INFO,sha256=JmSUIffA_-E4bhT7es7Y81PRNsG76VEYuRCqWw_DbcY,1273
8
8
  README.md,sha256=oY5Jc1uXehRASuwUJ0umin2vfkFh7tHF-LLruHTaQx0,3560
9
- pyproject.toml,sha256=s2v1q74HdC09fmE082F9bwYEDALJFrH_W0PPKu7Ykgo,2902
9
+ pyproject.toml,sha256=xInUTzdYljzVQdPaETaRqGblNXIaAZuFvCK3yhJsLog,2902
10
10
  .git_hooks/pre-commit,sha256=n3RofIZHJl8zfJJIUomcMyYGFi_rwq4CC19z0snz3FI,286
11
11
  .github/pull_request_template.md,sha256=F_cJXzooWMFgMGtLK-7KeGcQt0B4AYFse5oN0zQ9p6g,801
12
12
  .github/ISSUE_TEMPLATE/bug_report.yml,sha256=WdRnt7HGxvsIBLzhkaOWNfg8IJQYa_oV9_F08Ym6znQ,1081
@@ -35,7 +35,7 @@ bec_widgets/assets/app_icons/bec_widgets_icon.png,sha256=K8dgGwIjalDh9PRHUsSQBqg
35
35
  bec_widgets/assets/app_icons/ui_loader_tile.png,sha256=qSK3XHqvnAVGV9Q0ulORcGFbXJ9LDq2uz8l9uTtMsNk,1812476
36
36
  bec_widgets/assets/app_icons/widget_launch_tile.png,sha256=bWsICHFfSe9-ESUj3AwlE95dDOea-f6M-s9fBapsxB4,2252911
37
37
  bec_widgets/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
- bec_widgets/cli/client.py,sha256=qtAdEDsNub66VLi0uInX4RUQnSaFW86a-I_0bj4um_A,95077
38
+ bec_widgets/cli/client.py,sha256=Pl93U3IyN5OisaEDCrS2ytY5CppRfuyIiCppXNMf8yg,96703
39
39
  bec_widgets/cli/client_utils.py,sha256=F2hyt--jL53bN8NoWifNUMqwwx5FbpS6I1apERdTRzM,18114
40
40
  bec_widgets/cli/generate_cli.py,sha256=K_wMxo2XBUn92SnY3dSrlyUn8ax6Y20QBGCuP284DsQ,10986
41
41
  bec_widgets/cli/server.py,sha256=h7QyBOOGjyrP_fxJIIOSEMc4E06cLG0JyaofjNV6oCA,5671
@@ -62,7 +62,7 @@ bec_widgets/tests/utils.py,sha256=DSzi6Z70fospjfyx0Uz5bWIDwaAzKbzcHfWPW0YyxzQ,71
62
62
  bec_widgets/utils/__init__.py,sha256=1930ji1Jj6dVuY81Wd2kYBhHYNV-2R0bN_L4o9zBj1U,533
63
63
  bec_widgets/utils/bec_connector.py,sha256=ATOSyZqryn1QHPc7aotiDnUtzFhlj_gmcukMT_pqjHQ,19272
64
64
  bec_widgets/utils/bec_designer.py,sha256=ehNl_i743rijmhPiIGNd1bihE7-l4oJzTVoa4yjPjls,5426
65
- bec_widgets/utils/bec_dispatcher.py,sha256=y9EFIgU3JqIs7R10XnLh0I1_Skts9sbPe3ijOVJumYs,9837
65
+ bec_widgets/utils/bec_dispatcher.py,sha256=qYyd0SjCCUDwihxUFYzlKVxvZwSqQpDDPxF7_xt-sz0,9948
66
66
  bec_widgets/utils/bec_plugin_helper.py,sha256=tLXEyzh0LWuRp-1XhJg32m-hUSLfRStC0YRUWKdhY5Q,3565
67
67
  bec_widgets/utils/bec_signal_proxy.py,sha256=Yc08fdBEDABrowwNPSngT9-28R8FD4ml7oTL6BoMyEE,3263
68
68
  bec_widgets/utils/bec_table.py,sha256=nA2b8ukSeUfquFMAxGrUVOqdrzMoDYD6O_4EYbOG2zk,717
@@ -89,7 +89,7 @@ bec_widgets/utils/redis_message_waiter.py,sha256=fvL_QgC0cTDv_FPJdRyp5AKjf401EJU
89
89
  bec_widgets/utils/reference_utils.py,sha256=8pq06TOvZBZdim0G6hvPJXzVDib7ve4o-Ptvfp563nk,2859
90
90
  bec_widgets/utils/round_frame.py,sha256=SLtoPi8sfJvjfK7G_a4_sRBMGivF5fTHNwMLq93-cVM,4920
91
91
  bec_widgets/utils/rpc_decorator.py,sha256=pIvtqySQLnuS7l2Ti_UAe4WX7CRivZnsE5ZdKAihxh0,479
92
- bec_widgets/utils/rpc_server.py,sha256=iOIEy8s5DAL_aG6RC9G5DScBfyolpfDVeR2a_3h46JM,10310
92
+ bec_widgets/utils/rpc_server.py,sha256=6FqSsjndXwLoL5d-zIHHLMg_U3GyK0aSk5gc1833UGw,10311
93
93
  bec_widgets/utils/serialization.py,sha256=_SO8q0ylC0MWckT9yTbCtF0QNsRoT6ysL8WnN8-GQ-U,1174
94
94
  bec_widgets/utils/settings_dialog.py,sha256=1z6noC9k6BSIGAw12DvHHiW3LmvPquaGMjWSe5wdmek,4307
95
95
  bec_widgets/utils/side_panel.py,sha256=enxcQOwJOMfD3MxgPA9mQSKSX_F6Omy2zePncYDW1FA,14193
@@ -177,7 +177,7 @@ bec_widgets/widgets/control/device_control/positioner_group/register_positioner_
177
177
  bec_widgets/widgets/control/device_input/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
178
178
  bec_widgets/widgets/control/device_input/base_classes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
179
179
  bec_widgets/widgets/control/device_input/base_classes/device_input_base.py,sha256=r4DwWQz2wwNQ3Uswzdy12MGycV7pFrE_Zv4h_2G5IRA,15915
180
- bec_widgets/widgets/control/device_input/base_classes/device_signal_input_base.py,sha256=oylgVQ2XyN7CWrI_Aj4xtKT5tac41JRpGvccaY0SUHw,9271
180
+ bec_widgets/widgets/control/device_input/base_classes/device_signal_input_base.py,sha256=wLEzPfEqaCPL3EpvMgFL5aZqSwuGj1OEkiyKaOQ7M1I,9330
181
181
  bec_widgets/widgets/control/device_input/device_combobox/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
182
182
  bec_widgets/widgets/control/device_input/device_combobox/device_combo_box.pyproject,sha256=wI2eXR5ky_IM9-BCHJnH_9CEqYcZwIuLcgitSEr8OJU,40
183
183
  bec_widgets/widgets/control/device_input/device_combobox/device_combo_box_plugin.py,sha256=E8LD9T4O2w621q25uHqBqZLDiQ6zpMR25ZDuf51jrPw,1434
@@ -192,7 +192,7 @@ bec_widgets/widgets/control/device_input/signal_combobox/__init__.py,sha256=47DE
192
192
  bec_widgets/widgets/control/device_input/signal_combobox/register_signal_combo_box.py,sha256=VEdFRUfLph7JE2arcnzHw8etsE-4wZkwyzlNLMJBsZk,526
193
193
  bec_widgets/widgets/control/device_input/signal_combobox/signal_combo_box.pyproject,sha256=xod6iyRD-WD0Uk6LWXjSxFJCQy-831pvTkKcw2FAdnM,33
194
194
  bec_widgets/widgets/control/device_input/signal_combobox/signal_combo_box_plugin.py,sha256=sstqm2KtyR5wwOIYJRbzOqHMq5_9ExKP-YS5qV5ACrA,1373
195
- bec_widgets/widgets/control/device_input/signal_combobox/signal_combobox.py,sha256=NCT0ql6KCe-YspoYKSv2py7JeqKBGJFU97q6UoG-Oxg,4641
195
+ bec_widgets/widgets/control/device_input/signal_combobox/signal_combobox.py,sha256=vM4PsYDl8RBHb9g083fRpeUZZkW1u1m7uXfaVMGUpyY,4869
196
196
  bec_widgets/widgets/control/device_input/signal_line_edit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
197
197
  bec_widgets/widgets/control/device_input/signal_line_edit/register_signal_line_edit.py,sha256=aQLTy_3gbji0vq5VvvAddHFimpwGGaMYJy5iGgX23aM,527
198
198
  bec_widgets/widgets/control/device_input/signal_line_edit/signal_line_edit.py,sha256=-y_Oy8A7pQVQbzjvHznGxTX-wCisP-4l5py7WOm1_EY,6008
@@ -363,6 +363,10 @@ bec_widgets/widgets/utility/logpanel/log_panel.pyproject,sha256=2ncs1bsu-wICstR1
363
363
  bec_widgets/widgets/utility/logpanel/log_panel_plugin.py,sha256=KY7eS1uGZzLYtDAdBv6S2mw8UjcDGVt3UklN_D5M06A,1250
364
364
  bec_widgets/widgets/utility/logpanel/logpanel.py,sha256=tnjczAwtfe1biL-u9h9tntoQerWo3iLVD9RTSLOvd5o,20651
365
365
  bec_widgets/widgets/utility/logpanel/register_log_panel.py,sha256=LFUE5JzCYvIwJQtTqZASLVAHYy3gO1nrHzPVH_kpCEY,470
366
+ bec_widgets/widgets/utility/signal_label/register_signal_label.py,sha256=wDB4Q3dSbZ51hsxnuB74oXdMRoLgDRd-XfhaomYY2OA,483
367
+ bec_widgets/widgets/utility/signal_label/signal_label.py,sha256=bht1zpHKxrslfFCknnLe3Q9FeF8Do0j6onWAiLXZan0,15875
368
+ bec_widgets/widgets/utility/signal_label/signal_label.pyproject,sha256=DXgt__AGnPCqXo5A92aTPH0SxbWlvgyNzKraWUuumWg,30
369
+ bec_widgets/widgets/utility/signal_label/signal_label_plugin.py,sha256=ZXR8oYl4NkPcXJ8pgDcdXcg3teB0MHtoNZoiGDmgCoU,1298
366
370
  bec_widgets/widgets/utility/spinbox/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
367
371
  bec_widgets/widgets/utility/spinbox/bec_spin_box.pyproject,sha256=RIg1SZuCltuZZuK1O4Djg0TpCInhoCw8KeqNaf1_K0A,33
368
372
  bec_widgets/widgets/utility/spinbox/bec_spin_box_plugin.py,sha256=-XNrUAz1LZQPhJrH1sszfGrpBfpHUIfNO4bw7MPcc3k,1255
@@ -404,8 +408,8 @@ bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py,sha256=O
404
408
  bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.pyproject,sha256=Lbi9zb6HNlIq14k6hlzR-oz6PIFShBuF7QxE6d87d64,34
405
409
  bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button_plugin.py,sha256=CzChz2SSETYsR8-36meqWnsXCT-FIy_J_xeU5coWDY8,1350
406
410
  bec_widgets/widgets/utility/visual/dark_mode_button/register_dark_mode_button.py,sha256=rMpZ1CaoucwobgPj1FuKTnt07W82bV1GaSYdoqcdMb8,521
407
- bec_widgets-2.8.4.dist-info/METADATA,sha256=i_3fowZja6vwAtG6rabvccdxkcoUynup_6DHrxGLYuw,1273
408
- bec_widgets-2.8.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
409
- bec_widgets-2.8.4.dist-info/entry_points.txt,sha256=dItMzmwA1wizJ1Itx15qnfJ0ZzKVYFLVJ1voxT7K7D4,214
410
- bec_widgets-2.8.4.dist-info/licenses/LICENSE,sha256=Daeiu871NcAp8uYi4eB_qHgvypG-HX0ioRQyQxFwjeg,1531
411
- bec_widgets-2.8.4.dist-info/RECORD,,
411
+ bec_widgets-2.9.1.dist-info/METADATA,sha256=JmSUIffA_-E4bhT7es7Y81PRNsG76VEYuRCqWw_DbcY,1273
412
+ bec_widgets-2.9.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
413
+ bec_widgets-2.9.1.dist-info/entry_points.txt,sha256=dItMzmwA1wizJ1Itx15qnfJ0ZzKVYFLVJ1voxT7K7D4,214
414
+ bec_widgets-2.9.1.dist-info/licenses/LICENSE,sha256=Daeiu871NcAp8uYi4eB_qHgvypG-HX0ioRQyQxFwjeg,1531
415
+ bec_widgets-2.9.1.dist-info/RECORD,,
pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bec_widgets"
7
- version = "2.8.4"
7
+ version = "2.9.1"
8
8
  description = "BEC Widgets"
9
9
  requires-python = ">=3.10"
10
10
  classifiers = [