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.
- CHANGELOG.md +20 -0
- PKG-INFO +2 -3
- bec_widgets/cli/client.py +21 -0
- bec_widgets/widgets/__init__.py +0 -1
- bec_widgets/widgets/containers/dock/dock_area.py +7 -0
- bec_widgets/widgets/editors/scan_metadata/__init__.py +7 -0
- bec_widgets/widgets/editors/scan_metadata/_metadata_widgets.py +275 -0
- bec_widgets/widgets/editors/scan_metadata/_util.py +67 -0
- bec_widgets/widgets/editors/scan_metadata/additional_metadata_table.py +146 -0
- bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +196 -0
- bec_widgets/widgets/editors/text_box/text_box.py +5 -5
- bec_widgets/widgets/utility/logpanel/__init__.py +3 -0
- bec_widgets/widgets/utility/logpanel/_util.py +58 -0
- bec_widgets/widgets/utility/logpanel/log_panel.pyproject +1 -0
- bec_widgets/widgets/utility/logpanel/log_panel_plugin.py +54 -0
- bec_widgets/widgets/utility/logpanel/logpanel.py +529 -0
- bec_widgets/widgets/utility/logpanel/register_log_panel.py +15 -0
- {bec_widgets-1.19.2.dist-info → bec_widgets-1.21.0.dist-info}/METADATA +2 -3
- {bec_widgets-1.19.2.dist-info → bec_widgets-1.21.0.dist-info}/RECORD +23 -12
- pyproject.toml +4 -4
- {bec_widgets-1.19.2.dist-info → bec_widgets-1.21.0.dist-info}/WHEEL +0 -0
- {bec_widgets-1.19.2.dist-info → bec_widgets-1.21.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-1.19.2.dist-info → bec_widgets-1.21.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
109
|
+
@SafeProperty(str)
|
110
110
|
def html_text(self) -> str:
|
111
111
|
"""Get the HTML text of the widget.
|
112
112
|
|
@@ -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(" ", " ").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()
|