bec-widgets 1.19.2__py3-none-any.whl → 1.21.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.
@@ -0,0 +1,196 @@
1
+ from __future__ import annotations
2
+
3
+ from decimal import Decimal
4
+ from typing import TYPE_CHECKING
5
+
6
+ from bec_lib.logger import bec_logger
7
+ from bec_lib.metadata_schema import get_metadata_schema_for_scan
8
+ from bec_qthemes import material_icon
9
+ from pydantic import Field, ValidationError
10
+ from qtpy.QtWidgets import (
11
+ QApplication,
12
+ QComboBox,
13
+ QGridLayout,
14
+ QLabel,
15
+ QLayout,
16
+ QVBoxLayout,
17
+ QWidget,
18
+ )
19
+
20
+ from bec_widgets.qt_utils.compact_popup import CompactPopupWidget
21
+ from bec_widgets.qt_utils.error_popups import SafeSlot
22
+ from bec_widgets.utils.bec_widget import BECWidget
23
+ from bec_widgets.widgets.editors.scan_metadata._metadata_widgets import widget_from_type
24
+ from bec_widgets.widgets.editors.scan_metadata.additional_metadata_table import (
25
+ AdditionalMetadataTable,
26
+ )
27
+
28
+ if TYPE_CHECKING:
29
+ from pydantic.fields import FieldInfo
30
+
31
+ logger = bec_logger.logger
32
+
33
+
34
+ class ScanMetadata(BECWidget, QWidget):
35
+ """Dynamically generates a form for inclusion of metadata for a scan. Uses the
36
+ metadata schema registry supplied in the plugin repo to find pydantic models
37
+ associated with the scan type. Sets limits for numerical values if specified."""
38
+
39
+ def __init__(
40
+ self,
41
+ parent=None,
42
+ client=None,
43
+ scan_name: str | None = None,
44
+ initial_extras: list[list[str]] | None = None,
45
+ ):
46
+ super().__init__(client=client)
47
+ QWidget.__init__(self, parent=parent)
48
+
49
+ self.set_schema(scan_name)
50
+
51
+ self._layout = QVBoxLayout()
52
+ self._layout.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
53
+ self.setLayout(self._layout)
54
+ self._layout.addWidget(QLabel("<b>Required scan metadata:</b>"))
55
+ self._md_grid = QWidget()
56
+ self._layout.addWidget(self._md_grid)
57
+ self._grid_container = QVBoxLayout()
58
+ self._md_grid.setLayout(self._grid_container)
59
+ self._new_grid_layout()
60
+ self._grid_container.addLayout(self._md_grid_layout)
61
+ self._layout.addWidget(QLabel("<b>Additional metadata:</b>"))
62
+ self._additional_metadata = AdditionalMetadataTable(initial_extras or [])
63
+ self._layout.addWidget(self._additional_metadata)
64
+
65
+ self._validity = CompactPopupWidget()
66
+ self._validity.compact_view = True # type: ignore
67
+ self._validity.label = "Validity" # type: ignore
68
+ self._validity.compact_show_popup.setIcon(
69
+ material_icon(icon_name="info", size=(10, 10), convert_to_pixmap=False)
70
+ )
71
+ self._validity_message = QLabel("Not yet validated")
72
+ self._validity.addWidget(self._validity_message)
73
+ self._layout.addWidget(self._validity)
74
+
75
+ self.populate()
76
+
77
+ @SafeSlot(str)
78
+ def update_with_new_scan(self, scan_name: str):
79
+ self.set_schema(scan_name)
80
+ self.populate()
81
+ self.validate_form()
82
+
83
+ def validate_form(self, *_):
84
+ try:
85
+ self._md_schema.model_validate(self.get_full_model_dict())
86
+ self._validity.set_global_state("success")
87
+ self._validity_message.setText("No errors!")
88
+ except ValidationError as e:
89
+ self._validity.set_global_state("emergency")
90
+ self._validity_message.setText(str(e))
91
+
92
+ def get_full_model_dict(self):
93
+ """Get the entered metadata as a dict"""
94
+ return self._additional_metadata.dump_dict() | self._dict_from_grid()
95
+
96
+ def set_schema(self, scan_name: str | None = None):
97
+ self._scan_name = scan_name or ""
98
+ self._md_schema = get_metadata_schema_for_scan(self._scan_name)
99
+
100
+ def populate(self):
101
+ self._clear_grid()
102
+ self._populate()
103
+
104
+ def _populate(self):
105
+ self._additional_metadata.update_disallowed_keys(list(self._md_schema.model_fields.keys()))
106
+ for i, (field_name, info) in enumerate(self._md_schema.model_fields.items()):
107
+ self._add_griditem(field_name, info, i)
108
+
109
+ def _add_griditem(self, field_name: str, info: FieldInfo, row: int):
110
+ grid = self._md_grid_layout
111
+ label = QLabel(info.title or field_name)
112
+ label.setProperty("_model_field_name", field_name)
113
+ label.setToolTip(info.description or field_name)
114
+ grid.addWidget(label, row, 0)
115
+ widget = widget_from_type(info.annotation)(info)
116
+ widget.valueChanged.connect(self.validate_form)
117
+ grid.addWidget(widget, row, 1)
118
+
119
+ def _dict_from_grid(self) -> dict[str, str | int | float | Decimal | bool]:
120
+ grid = self._md_grid_layout
121
+ return {
122
+ grid.itemAtPosition(i, 0).widget().property("_model_field_name"): grid.itemAtPosition(i, 1).widget().getValue() # type: ignore # we only add 'MetadataWidget's here
123
+ for i in range(grid.rowCount())
124
+ }
125
+
126
+ def _clear_grid(self):
127
+ while self._md_grid_layout.count():
128
+ item = self._md_grid_layout.takeAt(0)
129
+ widget = item.widget()
130
+ if widget is not None:
131
+ widget.deleteLater()
132
+ self._md_grid_layout.deleteLater()
133
+ self._new_grid_layout()
134
+ self._grid_container.addLayout(self._md_grid_layout)
135
+ self._md_grid.adjustSize()
136
+ self.adjustSize()
137
+
138
+ def _new_grid_layout(self):
139
+ self._md_grid_layout = QGridLayout()
140
+ self._md_grid_layout.setContentsMargins(0, 0, 0, 0)
141
+ self._md_grid_layout.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
142
+
143
+
144
+ if __name__ == "__main__": # pragma: no cover
145
+ from unittest.mock import patch
146
+
147
+ from bec_lib.metadata_schema import BasicScanMetadata
148
+
149
+ from bec_widgets.utils.colors import set_theme
150
+
151
+ class ExampleSchema1(BasicScanMetadata):
152
+ abc: int = Field(gt=0, lt=2000, description="Heating temperature abc", title="A B C")
153
+ foo: str = Field(max_length=12, description="Sample database code", default="DEF123")
154
+ xyz: Decimal = Field(decimal_places=4)
155
+ baz: bool
156
+
157
+ class ExampleSchema2(BasicScanMetadata):
158
+ checkbox_up_top: bool
159
+ checkbox_again: bool = Field(
160
+ title="Checkbox Again", description="this one defaults to True", default=True
161
+ )
162
+ different_items: int | None = Field(
163
+ None, description="This is just one different item...", gt=-100, lt=0
164
+ )
165
+ length_limited_string: str = Field(max_length=32)
166
+ float_with_2dp: Decimal = Field(decimal_places=2)
167
+
168
+ class ExampleSchema3(BasicScanMetadata):
169
+ optional_with_regex: str | None = Field(None, pattern=r"^\d+-\d+$")
170
+
171
+ with patch(
172
+ "bec_lib.metadata_schema._get_metadata_schema_registry",
173
+ lambda: {"scan1": ExampleSchema1, "scan2": ExampleSchema2, "scan3": ExampleSchema3},
174
+ ):
175
+
176
+ app = QApplication([])
177
+ w = QWidget()
178
+ selection = QComboBox()
179
+ selection.addItems(["grid_scan", "scan1", "scan2", "scan3"])
180
+
181
+ layout = QVBoxLayout()
182
+ w.setLayout(layout)
183
+
184
+ scan_metadata = ScanMetadata(
185
+ scan_name="grid_scan",
186
+ initial_extras=[["key1", "value1"], ["key2", "value2"], ["key3", "value3"]],
187
+ )
188
+ selection.currentTextChanged.connect(scan_metadata.update_with_new_scan)
189
+
190
+ layout.addWidget(selection)
191
+ layout.addWidget(scan_metadata)
192
+
193
+ set_theme("dark")
194
+ window = w
195
+ window.show()
196
+ app.exec()
@@ -5,9 +5,9 @@ from html.parser import HTMLParser
5
5
 
6
6
  from bec_lib.logger import bec_logger
7
7
  from pydantic import Field
8
- from qtpy.QtCore import Property, Slot
9
8
  from qtpy.QtWidgets import QTextEdit, QVBoxLayout, QWidget
10
9
 
10
+ from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
11
11
  from bec_widgets.utils.bec_connector import ConnectionConfig
12
12
  from bec_widgets.utils.bec_widget import BECWidget
13
13
 
@@ -66,7 +66,7 @@ class TextBox(BECWidget, QWidget):
66
66
  else:
67
67
  self.set_html_text(DEFAULT_TEXT)
68
68
 
69
- @Slot(str)
69
+ @SafeSlot(str)
70
70
  def set_plain_text(self, text: str) -> None:
71
71
  """Set the plain text of the widget.
72
72
 
@@ -77,7 +77,7 @@ class TextBox(BECWidget, QWidget):
77
77
  self.config.text = text
78
78
  self.config.is_html = False
79
79
 
80
- @Slot(str)
80
+ @SafeSlot(str)
81
81
  def set_html_text(self, text: str) -> None:
82
82
  """Set the HTML text of the widget.
83
83
 
@@ -88,7 +88,7 @@ class TextBox(BECWidget, QWidget):
88
88
  self.config.text = text
89
89
  self.config.is_html = True
90
90
 
91
- @Property(str)
91
+ @SafeProperty(str)
92
92
  def plain_text(self) -> str:
93
93
  """Get the text of the widget.
94
94
 
@@ -106,7 +106,7 @@ class TextBox(BECWidget, QWidget):
106
106
  """
107
107
  self.set_plain_text(text)
108
108
 
109
- @Property(str)
109
+ @SafeProperty(str)
110
110
  def html_text(self) -> str:
111
111
  """Get the HTML text of the widget.
112
112
 
@@ -0,0 +1,3 @@
1
+ from bec_widgets.widgets.utility.logpanel.logpanel import LogPanel
2
+
3
+ __ALL__ = ["LogPanel"]
@@ -0,0 +1,58 @@
1
+ """ Utilities for filtering and formatting in the LogPanel"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections import deque
7
+ from typing import Callable, Iterator
8
+
9
+ from bec_lib.logger import LogLevel
10
+ from bec_lib.messages import LogMessage
11
+ from qtpy.QtCore import QDateTime
12
+
13
+ LinesHtmlFormatter = Callable[[deque[LogMessage]], Iterator[str]]
14
+ LineFormatter = Callable[[LogMessage], str]
15
+ LineFilter = Callable[[LogMessage], bool] | None
16
+
17
+ ANSI_ESCAPE_REGEX = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
18
+
19
+
20
+ def replace_escapes(s: str):
21
+ s = ANSI_ESCAPE_REGEX.sub("", s)
22
+ return s.replace(" ", "&nbsp;").replace("\n", "<br />").replace("\t", " ")
23
+
24
+
25
+ def level_filter(msg: LogMessage, thresh: int):
26
+ return LogLevel[msg.content["log_type"].upper()].value >= thresh
27
+
28
+
29
+ def noop_format(line: LogMessage):
30
+ _textline = line.log_msg if isinstance(line.log_msg, str) else line.log_msg["text"]
31
+ return replace_escapes(_textline.strip()) + "<br />"
32
+
33
+
34
+ def simple_color_format(line: LogMessage, colors: dict[LogLevel, str]):
35
+ color = colors.get(LogLevel[line.content["log_type"].upper()]) or colors[LogLevel.INFO]
36
+ return f'<font color="{color}">{noop_format(line)}</font>'
37
+
38
+
39
+ def create_formatter(line_format: LineFormatter, line_filter: LineFilter) -> LinesHtmlFormatter:
40
+ def _formatter(data: deque[LogMessage]):
41
+ if line_filter is not None:
42
+ return (line_format(line) for line in data if line_filter(line))
43
+ else:
44
+ return (line_format(line) for line in data)
45
+
46
+ return _formatter
47
+
48
+
49
+ def log_txt(line):
50
+ return line.log_msg if isinstance(line.log_msg, str) else line.log_msg["text"]
51
+
52
+
53
+ def log_time(line):
54
+ return QDateTime.fromMSecsSinceEpoch(int(line.log_msg["record"]["time"]["timestamp"] * 1000))
55
+
56
+
57
+ def log_svc(line):
58
+ return line.log_msg["service_name"]
@@ -0,0 +1 @@
1
+ {'files': ['logpanel.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.logpanel.logpanel import LogPanel
8
+
9
+ DOM_XML = """
10
+ <ui language='c++'>
11
+ <widget class='LogPanel' name='log_panel'>
12
+ </widget>
13
+ </ui>
14
+ """
15
+
16
+
17
+ class LogPanelPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
18
+ def __init__(self):
19
+ super().__init__()
20
+ self._form_editor = None
21
+
22
+ def createWidget(self, parent):
23
+ t = LogPanel(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(LogPanel.ICON_NAME)
34
+
35
+ def includeFile(self):
36
+ return "log_panel"
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 "LogPanel"
49
+
50
+ def toolTip(self):
51
+ return "Displays a log panel"
52
+
53
+ def whatsThis(self):
54
+ return self.toolTip()