hardpy 0.4.0__py3-none-any.whl → 0.5.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.
- hardpy/__init__.py +23 -3
- hardpy/hardpy_panel/frontend/dist/asset-manifest.json +3 -3
- hardpy/hardpy_panel/frontend/dist/index.html +1 -1
- hardpy/hardpy_panel/frontend/dist/static/js/main.da686f40.js +3 -0
- hardpy/hardpy_panel/frontend/dist/static/js/main.da686f40.js.map +1 -0
- hardpy/pytest_hardpy/plugin.py +46 -11
- hardpy/pytest_hardpy/pytest_call.py +9 -9
- hardpy/pytest_hardpy/result/couchdb_config.py +78 -4
- hardpy/pytest_hardpy/utils/__init__.py +17 -8
- hardpy/pytest_hardpy/utils/dialog_box.py +274 -71
- hardpy/pytest_hardpy/utils/exception.py +7 -0
- {hardpy-0.4.0.dist-info → hardpy-0.5.1.dist-info}/METADATA +7 -4
- {hardpy-0.4.0.dist-info → hardpy-0.5.1.dist-info}/RECORD +17 -17
- hardpy/hardpy_panel/frontend/dist/static/js/main.37744128.js +0 -3
- hardpy/hardpy_panel/frontend/dist/static/js/main.37744128.js.map +0 -1
- /hardpy/hardpy_panel/frontend/dist/static/js/{main.37744128.js.LICENSE.txt → main.da686f40.js.LICENSE.txt} +0 -0
- {hardpy-0.4.0.dist-info → hardpy-0.5.1.dist-info}/WHEEL +0 -0
- {hardpy-0.4.0.dist-info → hardpy-0.5.1.dist-info}/entry_points.txt +0 -0
- {hardpy-0.4.0.dist-info → hardpy-0.5.1.dist-info}/licenses/LICENSE +0 -0
hardpy/pytest_hardpy/plugin.py
CHANGED
|
@@ -6,6 +6,7 @@ from typing import Any, Callable
|
|
|
6
6
|
from logging import getLogger
|
|
7
7
|
from pathlib import Path, PurePath
|
|
8
8
|
from platform import system
|
|
9
|
+
from re import compile as re_compile
|
|
9
10
|
|
|
10
11
|
from natsort import natsorted
|
|
11
12
|
from pytest import (
|
|
@@ -19,6 +20,13 @@ from pytest import (
|
|
|
19
20
|
fixture,
|
|
20
21
|
ExitCode,
|
|
21
22
|
)
|
|
23
|
+
from _pytest._code.code import (
|
|
24
|
+
ExceptionRepr,
|
|
25
|
+
ReprFileLocation,
|
|
26
|
+
ExceptionInfo,
|
|
27
|
+
ReprExceptionInfo,
|
|
28
|
+
TerminalRepr,
|
|
29
|
+
)
|
|
22
30
|
|
|
23
31
|
from hardpy.pytest_hardpy.reporter import HookReporter
|
|
24
32
|
from hardpy.pytest_hardpy.utils import (
|
|
@@ -187,8 +195,9 @@ class HardpyPlugin(object):
|
|
|
187
195
|
|
|
188
196
|
def pytest_runtest_logreport(self, report: TestReport):
|
|
189
197
|
"""Call after call of each test item."""
|
|
190
|
-
if report.when != "call":
|
|
198
|
+
if report.when != "call" and report.failed is False:
|
|
191
199
|
# ignore setup and teardown phase
|
|
200
|
+
# or continue processing setup and teardown failure (fixture exception handler)
|
|
192
201
|
return True
|
|
193
202
|
|
|
194
203
|
module_id = Path(report.fspath).stem
|
|
@@ -204,7 +213,7 @@ class HardpyPlugin(object):
|
|
|
204
213
|
case_id,
|
|
205
214
|
)
|
|
206
215
|
|
|
207
|
-
assertion_msg = self._decode_assertion_msg(report.
|
|
216
|
+
assertion_msg = self._decode_assertion_msg(report.longrepr)
|
|
208
217
|
self._reporter.set_assertion_msg(module_id, case_id, assertion_msg)
|
|
209
218
|
self._reporter.set_progress(self._progress.calculate(report.nodeid))
|
|
210
219
|
self._results[module_id][case_id] = report.outcome # noqa: WPS204
|
|
@@ -265,15 +274,41 @@ class HardpyPlugin(object):
|
|
|
265
274
|
case _:
|
|
266
275
|
return RunStatus.ERROR
|
|
267
276
|
|
|
268
|
-
def _decode_assertion_msg(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
+
def _decode_assertion_msg(
|
|
278
|
+
self,
|
|
279
|
+
error: ( # noqa: WPS320
|
|
280
|
+
ExceptionInfo[BaseException] # noqa: DAR101,DAR201
|
|
281
|
+
| tuple[str, int, str]
|
|
282
|
+
| str
|
|
283
|
+
| TerminalRepr
|
|
284
|
+
| None
|
|
285
|
+
),
|
|
286
|
+
) -> str | None:
|
|
287
|
+
"""Parse pytest assertion error message."""
|
|
288
|
+
if error is None:
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
match error:
|
|
292
|
+
case str():
|
|
293
|
+
return error
|
|
294
|
+
case tuple() if len(error) == 3:
|
|
295
|
+
return error[2]
|
|
296
|
+
case ExceptionInfo():
|
|
297
|
+
error_repr = error.getrepr()
|
|
298
|
+
if isinstance(error_repr, ReprExceptionInfo) and error_repr.reprcrash:
|
|
299
|
+
return error_repr.reprcrash.message
|
|
300
|
+
case TerminalRepr():
|
|
301
|
+
if isinstance(error, ExceptionRepr) and isinstance( # noqa: WPS337
|
|
302
|
+
error.reprcrash, ReprFileLocation
|
|
303
|
+
):
|
|
304
|
+
# remove ansi codes
|
|
305
|
+
ansi_pattern = re_compile(
|
|
306
|
+
r"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])" # noqa: E501
|
|
307
|
+
)
|
|
308
|
+
return ansi_pattern.sub("", error.reprcrash.message)
|
|
309
|
+
return str(error)
|
|
310
|
+
case _:
|
|
311
|
+
return None
|
|
277
312
|
|
|
278
313
|
def _handle_dependency(self, node_info: NodeInfo):
|
|
279
314
|
dependency = self._dependencies.get(
|
|
@@ -20,8 +20,6 @@ from hardpy.pytest_hardpy.utils import (
|
|
|
20
20
|
DuplicateDialogBoxError,
|
|
21
21
|
DialogBox,
|
|
22
22
|
ConfigData,
|
|
23
|
-
generate_dialog_box_dict,
|
|
24
|
-
get_dialog_box_data,
|
|
25
23
|
)
|
|
26
24
|
from hardpy.pytest_hardpy.reporter import RunnerReporter
|
|
27
25
|
|
|
@@ -221,7 +219,7 @@ def run_dialog_box(dialog_box_data: DialogBox) -> Any:
|
|
|
221
219
|
|
|
222
220
|
- dialog_text (str): The text of the dialog box.
|
|
223
221
|
- title_bar (str | None): The title bar of the dialog box.
|
|
224
|
-
|
|
222
|
+
If the title_bar field is missing, it is the case name.
|
|
225
223
|
- widget (DialogBoxWidget | None): Widget information.
|
|
226
224
|
|
|
227
225
|
Returns:
|
|
@@ -229,9 +227,13 @@ def run_dialog_box(dialog_box_data: DialogBox) -> Any:
|
|
|
229
227
|
|
|
230
228
|
The type of the return value depends on the widget type:
|
|
231
229
|
|
|
232
|
-
-
|
|
230
|
+
- BASE: bool.
|
|
233
231
|
- TEXT_INPUT: str.
|
|
234
232
|
- NUMERIC_INPUT: float.
|
|
233
|
+
- RADIOBUTTON: str.
|
|
234
|
+
- CHECKBOX: list[str].
|
|
235
|
+
- IMAGE: bool.
|
|
236
|
+
- MULTISTEP: bool.
|
|
235
237
|
|
|
236
238
|
Raises:
|
|
237
239
|
ValueError: If the 'message' argument is empty.
|
|
@@ -251,14 +253,12 @@ def run_dialog_box(dialog_box_data: DialogBox) -> Any:
|
|
|
251
253
|
)
|
|
252
254
|
if reporter.get_field(key):
|
|
253
255
|
raise DuplicateDialogBoxError
|
|
254
|
-
data_dict = generate_dialog_box_dict(dialog_box_data)
|
|
255
256
|
|
|
256
|
-
reporter.set_doc_value(key,
|
|
257
|
+
reporter.set_doc_value(key, dialog_box_data.to_dict(), statestore_only=True)
|
|
257
258
|
reporter.update_db_by_doc()
|
|
258
259
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return get_dialog_box_data(dialog_raw_data, dialog_box_data.widget)
|
|
260
|
+
input_dbx_data = _get_socket_raw_data()
|
|
261
|
+
return dialog_box_data.widget.convert_data(input_dbx_data)
|
|
262
262
|
|
|
263
263
|
|
|
264
264
|
def _get_current_test() -> CurrentTestInfo:
|
|
@@ -1,22 +1,96 @@
|
|
|
1
1
|
# Copyright (c) 2024 Everypin
|
|
2
2
|
# GNU General Public License v3.0 (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
3
|
|
|
4
|
+
import ast
|
|
5
|
+
import requests
|
|
6
|
+
import socket
|
|
7
|
+
|
|
4
8
|
from dataclasses import dataclass
|
|
9
|
+
from urllib3 import disable_warnings
|
|
5
10
|
|
|
6
11
|
|
|
7
12
|
@dataclass
|
|
8
13
|
class CouchdbConfig: # noqa: WPS306
|
|
9
|
-
"""CouchDB loader config.
|
|
14
|
+
"""CouchDB loader config.
|
|
15
|
+
|
|
16
|
+
If `connection_str` arg is not set, it will be created from other args.
|
|
17
|
+
"""
|
|
10
18
|
|
|
11
19
|
db_name: str = "report"
|
|
12
20
|
user: str = "dev"
|
|
13
21
|
password: str = "dev"
|
|
14
22
|
host: str = "localhost"
|
|
15
23
|
port: int = 5984
|
|
24
|
+
connection_str: str | None = None
|
|
25
|
+
|
|
26
|
+
def __post_init__(self):
|
|
27
|
+
"""Disable urllib3 warnings.
|
|
28
|
+
|
|
29
|
+
More info: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
|
|
30
|
+
"""
|
|
31
|
+
disable_warnings()
|
|
16
32
|
|
|
17
33
|
@property
|
|
18
34
|
def connection_string(self) -> str:
|
|
19
|
-
"""Get couchdb connection string.
|
|
35
|
+
"""Get couchdb connection string.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
RuntimeError: CouchDB server is not available
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
str: Database connection string.
|
|
42
|
+
"""
|
|
43
|
+
if self.connection_str:
|
|
44
|
+
return self.connection_str
|
|
45
|
+
|
|
46
|
+
# TODO: Modify connection string creating based on protocol.
|
|
47
|
+
# Some problems with http and https, different ports, local
|
|
48
|
+
# and cloud databases.
|
|
49
|
+
protocol = self._get_protocol()
|
|
50
|
+
|
|
51
|
+
if protocol == "http":
|
|
52
|
+
host_url = f"http://{self.host}:{self.port}"
|
|
53
|
+
uri = f"{self.host}:{str(self.port)}" # noqa: WPS237
|
|
54
|
+
elif protocol == "https":
|
|
55
|
+
host_url = f"https://{self.host}"
|
|
56
|
+
uri = f"{self.host}"
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
response = requests.get(host_url, timeout=5)
|
|
60
|
+
except requests.exceptions.RequestException:
|
|
61
|
+
raise RuntimeError(f"Error CouchDB connecting to {host_url}.")
|
|
62
|
+
|
|
63
|
+
# fmt: off
|
|
64
|
+
try:
|
|
65
|
+
couchdb_dict = ast.literal_eval(response._content.decode("utf-8")) # noqa: WPS437,E501
|
|
66
|
+
couchdb_dict.get("couchdb", False)
|
|
67
|
+
except Exception:
|
|
68
|
+
raise RuntimeError(f"Address {host_url} does not provide CouchDB attributes.")
|
|
69
|
+
# fmt: on
|
|
70
|
+
|
|
20
71
|
credentials = f"{self.user}:{self.password}"
|
|
21
|
-
|
|
22
|
-
|
|
72
|
+
return f"{protocol}://{credentials}@{uri}/"
|
|
73
|
+
|
|
74
|
+
def _get_protocol(self) -> str: # noqa: WPS231
|
|
75
|
+
success = 200
|
|
76
|
+
try:
|
|
77
|
+
# HTTPS attempt
|
|
78
|
+
sock = socket.create_connection((self.host, self.port))
|
|
79
|
+
sock.close()
|
|
80
|
+
request = f"https://{self.host}"
|
|
81
|
+
if requests.get(request, timeout=5).status_code == success:
|
|
82
|
+
return "https"
|
|
83
|
+
raise OSError
|
|
84
|
+
except OSError:
|
|
85
|
+
try: # noqa: WPS505
|
|
86
|
+
# HTTP attempt
|
|
87
|
+
sock = socket.create_connection((self.host, self.port))
|
|
88
|
+
sock.close()
|
|
89
|
+
request = f"http://{self.host}:{self.port}"
|
|
90
|
+
if requests.get(request, timeout=5).status_code == success:
|
|
91
|
+
return "http"
|
|
92
|
+
raise OSError
|
|
93
|
+
except OSError:
|
|
94
|
+
raise RuntimeError(
|
|
95
|
+
f"Error connecting to couchdb server {self.host}:{self.port}."
|
|
96
|
+
)
|
|
@@ -9,15 +9,20 @@ from hardpy.pytest_hardpy.utils.config_data import ConfigData
|
|
|
9
9
|
from hardpy.pytest_hardpy.utils.exception import (
|
|
10
10
|
DuplicateSerialNumberError,
|
|
11
11
|
DuplicateDialogBoxError,
|
|
12
|
+
WidgetInfoError,
|
|
12
13
|
)
|
|
13
14
|
from hardpy.pytest_hardpy.utils.dialog_box import (
|
|
14
15
|
DialogBox,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
TextInputWidget,
|
|
17
|
+
NumericInputWidget,
|
|
18
|
+
CheckboxWidget,
|
|
19
|
+
RadiobuttonWidget,
|
|
20
|
+
ImageWidget,
|
|
21
|
+
MultistepWidget,
|
|
22
|
+
StepWidget,
|
|
19
23
|
)
|
|
20
24
|
|
|
25
|
+
|
|
21
26
|
__all__ = [
|
|
22
27
|
"NodeInfo",
|
|
23
28
|
"ProgressCalculator",
|
|
@@ -27,9 +32,13 @@ __all__ = [
|
|
|
27
32
|
"ConfigData",
|
|
28
33
|
"DuplicateSerialNumberError",
|
|
29
34
|
"DuplicateDialogBoxError",
|
|
35
|
+
"WidgetInfoError",
|
|
30
36
|
"DialogBox",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
37
|
+
"TextInputWidget",
|
|
38
|
+
"NumericInputWidget",
|
|
39
|
+
"CheckboxWidget",
|
|
40
|
+
"RadiobuttonWidget",
|
|
41
|
+
"ImageWidget",
|
|
42
|
+
"MultistepWidget",
|
|
43
|
+
"StepWidget"
|
|
35
44
|
]
|
|
@@ -1,31 +1,275 @@
|
|
|
1
1
|
# Copyright (c) 2024 Everypin
|
|
2
2
|
# GNU General Public License v3.0 (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
import base64
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from ast import literal_eval
|
|
7
|
+
from copy import deepcopy
|
|
5
8
|
from dataclasses import dataclass
|
|
6
|
-
from
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, Final
|
|
11
|
+
|
|
12
|
+
from hardpy.pytest_hardpy.utils.exception import WidgetInfoError
|
|
7
13
|
|
|
8
14
|
|
|
9
|
-
class
|
|
15
|
+
class WidgetType(Enum):
|
|
10
16
|
"""Dialog box widget type."""
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
CHECKBOX = "checkbox"
|
|
18
|
+
BASE = "base"
|
|
14
19
|
TEXT_INPUT = "textinput"
|
|
15
20
|
NUMERIC_INPUT = "numericinput"
|
|
21
|
+
RADIOBUTTON = "radiobutton"
|
|
22
|
+
CHECKBOX = "checkbox"
|
|
23
|
+
IMAGE = "image"
|
|
24
|
+
STEP = "step"
|
|
25
|
+
MULTISTEP = "multistep"
|
|
16
26
|
|
|
17
27
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
class IWidget(ABC):
|
|
29
|
+
"""Dialog box widget interface."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, widget_type: WidgetType):
|
|
32
|
+
self.type: Final[str] = widget_type.value
|
|
33
|
+
self.info: dict = {}
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def convert_data(self, input_data: str | None) -> Any | None:
|
|
37
|
+
"""Get the widget data in the correct format.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
input_data (str | None): input string or nothing.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Any: Widget data in the correct format
|
|
44
|
+
"""
|
|
45
|
+
raise NotImplementedError
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BaseWidget(IWidget):
|
|
49
|
+
"""Widget info interface."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, widget_type: WidgetType = WidgetType.BASE):
|
|
52
|
+
super().__init__(WidgetType.BASE)
|
|
53
|
+
|
|
54
|
+
def convert_data(self, input_data: str | None = None) -> bool: # noqa: WPS324
|
|
55
|
+
"""Get base widget data, i.e. None.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
input_data (str): input string
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
bool: True if confirm button is pressed
|
|
62
|
+
"""
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TextInputWidget(IWidget):
|
|
67
|
+
"""Text input widget."""
|
|
68
|
+
|
|
69
|
+
def __init__(self):
|
|
70
|
+
"""Initialize the TextInputWidget."""
|
|
71
|
+
super().__init__(WidgetType.TEXT_INPUT)
|
|
72
|
+
|
|
73
|
+
def convert_data(self, input_data: str) -> str:
|
|
74
|
+
"""Get the text input data in the string format.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
input_data (str): input string
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
str: Text input string data
|
|
81
|
+
"""
|
|
82
|
+
return input_data
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class NumericInputWidget(IWidget):
|
|
86
|
+
"""Numeric input widget."""
|
|
87
|
+
|
|
88
|
+
def __init__(self):
|
|
89
|
+
"""Initialize the NumericInputWidget."""
|
|
90
|
+
super().__init__(WidgetType.NUMERIC_INPUT)
|
|
91
|
+
|
|
92
|
+
def convert_data(self, input_data: str) -> float | None:
|
|
93
|
+
"""Get the numeric widget data in the correct format.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
input_data (str): input string
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
float | None: Numeric data or None if the input is not a number
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
return float(input_data)
|
|
103
|
+
except ValueError:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class RadiobuttonWidget(IWidget):
|
|
108
|
+
"""Radiobutton widget."""
|
|
109
|
+
|
|
110
|
+
def __init__(self, fields: list[str]):
|
|
111
|
+
"""Initialize the RadiobuttonWidget.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
fields (list[str]): Radiobutton fields.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ValueError: If the fields list is empty.
|
|
118
|
+
"""
|
|
119
|
+
super().__init__(WidgetType.RADIOBUTTON)
|
|
120
|
+
if not fields:
|
|
121
|
+
raise ValueError("RadiobuttonWidget must have at least one field")
|
|
122
|
+
self.info["fields"] = fields
|
|
123
|
+
|
|
124
|
+
def convert_data(self, input_data: str) -> str:
|
|
125
|
+
"""Get the radiobutton widget data in the correct format.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
input_data (str): input string
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
str: Radiobutton string data
|
|
132
|
+
"""
|
|
133
|
+
return input_data
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class CheckboxWidget(IWidget):
|
|
137
|
+
"""Checkbox widget."""
|
|
138
|
+
|
|
139
|
+
def __init__(self, fields: list[str]):
|
|
140
|
+
"""Initialize the CheckboxWidget.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
fields (list[str]): Checkbox fields.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
ValueError: If the fields list is empty.
|
|
147
|
+
"""
|
|
148
|
+
super().__init__(WidgetType.CHECKBOX)
|
|
149
|
+
if not fields:
|
|
150
|
+
raise ValueError("RadiobuttonWidget must have at least one field")
|
|
151
|
+
self.info["fields"] = fields
|
|
152
|
+
|
|
153
|
+
def convert_data(self, input_data: str) -> list[str] | None:
|
|
154
|
+
"""Get the checkbox widget data in the correct format.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
input_data (str): input string
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
(list[str] | None): Checkbox string data or None if the input is not a list
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
return literal_eval(input_data)
|
|
164
|
+
except ValueError:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class ImageWidget(IWidget):
|
|
169
|
+
"""Image widget."""
|
|
170
|
+
|
|
171
|
+
def __init__(self, address: str, format: str = "image", width: int = 100):
|
|
172
|
+
"""Validate the image fields and defines the base64 if it does not exist.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
address (str): image address
|
|
176
|
+
format (str): image format
|
|
177
|
+
width (int): image width
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
WidgetInfoError: If both address and base64 are specified.
|
|
181
|
+
"""
|
|
182
|
+
super().__init__(WidgetType.IMAGE)
|
|
183
|
+
|
|
184
|
+
if width < 1:
|
|
185
|
+
raise WidgetInfoError("Width must be positive")
|
|
186
|
+
|
|
187
|
+
self.info["address"] = address
|
|
188
|
+
self.info["format"] = format
|
|
189
|
+
self.info["width"] = width
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
with open(address, "rb") as file:
|
|
193
|
+
file_data = file.read()
|
|
194
|
+
except FileNotFoundError:
|
|
195
|
+
raise WidgetInfoError("The image address is invalid")
|
|
196
|
+
self.info["base64"] = base64.b64encode(file_data).decode("utf-8")
|
|
197
|
+
|
|
198
|
+
def convert_data(self, input_data: str | None = None) -> bool:
|
|
199
|
+
"""Get the image widget data, i.e. None.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
input_data (str | None): input string or nothing.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
bool: True if confirm button is pressed
|
|
206
|
+
"""
|
|
207
|
+
return True
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class StepWidget(IWidget):
|
|
211
|
+
"""Step widget.
|
|
21
212
|
|
|
22
213
|
Args:
|
|
23
|
-
|
|
24
|
-
|
|
214
|
+
title (str): Step title
|
|
215
|
+
text (str | None): Step text
|
|
216
|
+
widget (ImageWidget | None): Step widget
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
WidgetInfoError: If the text or widget are not provided.
|
|
25
220
|
"""
|
|
26
221
|
|
|
27
|
-
|
|
28
|
-
|
|
222
|
+
def __init__(self, title: str, text: str | None, widget: ImageWidget | None):
|
|
223
|
+
super().__init__(WidgetType.STEP)
|
|
224
|
+
if text is None and widget is None:
|
|
225
|
+
raise WidgetInfoError("Text or widget must be provided")
|
|
226
|
+
self.info["title"] = title
|
|
227
|
+
if isinstance(text, str):
|
|
228
|
+
self.info["text"] = text
|
|
229
|
+
if isinstance(widget, ImageWidget):
|
|
230
|
+
self.info["widget"] = widget.__dict__
|
|
231
|
+
|
|
232
|
+
def convert_data(self, input_data: str) -> bool:
|
|
233
|
+
"""Get the step widget data in the correct format.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
input_data (str): input string
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
bool: True if confirm button is pressed
|
|
240
|
+
"""
|
|
241
|
+
return True
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class MultistepWidget(IWidget):
|
|
245
|
+
"""Multistep widget."""
|
|
246
|
+
|
|
247
|
+
def __init__(self, steps: list[StepWidget]):
|
|
248
|
+
"""Initialize the MultistepWidget.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
steps (list[StepWidget]): A list with info about the steps.
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
ValueError: If the provided list of steps is empty.
|
|
255
|
+
"""
|
|
256
|
+
super().__init__(WidgetType.MULTISTEP)
|
|
257
|
+
if not steps:
|
|
258
|
+
raise ValueError("MultistepWidget must have at least one step")
|
|
259
|
+
self.info["steps"] = []
|
|
260
|
+
for step in steps:
|
|
261
|
+
self.info["steps"].append(step.__dict__)
|
|
262
|
+
|
|
263
|
+
def convert_data(self, input_data: str) -> bool:
|
|
264
|
+
"""Get the multistep widget data in the correct format.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
input_data (str): input string
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
bool: True if confirm button is pressed
|
|
271
|
+
"""
|
|
272
|
+
return True
|
|
29
273
|
|
|
30
274
|
|
|
31
275
|
@dataclass
|
|
@@ -35,66 +279,25 @@ class DialogBox:
|
|
|
35
279
|
Args:
|
|
36
280
|
dialog_text (str): dialog text
|
|
37
281
|
title_bar (str | None): title bar
|
|
38
|
-
widget (
|
|
282
|
+
widget (IWidget | None): widget info
|
|
39
283
|
"""
|
|
40
284
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
dialog_box_data (DialogBox): dialog box data
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
dict: dialog box dictionary
|
|
54
|
-
"""
|
|
55
|
-
if dialog_box_data.widget is None:
|
|
56
|
-
data_dict = {
|
|
57
|
-
"title_bar": dialog_box_data.title_bar,
|
|
58
|
-
"dialog_text": dialog_box_data.dialog_text,
|
|
59
|
-
"widget": None,
|
|
60
|
-
}
|
|
61
|
-
else:
|
|
62
|
-
data_dict = {
|
|
63
|
-
"title_bar": dialog_box_data.title_bar,
|
|
64
|
-
"dialog_text": dialog_box_data.dialog_text,
|
|
65
|
-
"widget": {
|
|
66
|
-
"info": dialog_box_data.widget.info,
|
|
67
|
-
"type": dialog_box_data.widget.type.value,
|
|
68
|
-
},
|
|
69
|
-
}
|
|
70
|
-
return data_dict
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def get_dialog_box_data(
|
|
74
|
-
input_data: str, widget: DialogBoxWidget | None
|
|
75
|
-
) -> Any:
|
|
76
|
-
"""Get the dialog box data in the correct format.
|
|
285
|
+
def __init__(
|
|
286
|
+
self,
|
|
287
|
+
dialog_text: str,
|
|
288
|
+
title_bar: str | None = None,
|
|
289
|
+
widget: IWidget | None = None,
|
|
290
|
+
):
|
|
291
|
+
self.widget: IWidget = BaseWidget() if widget is None else widget
|
|
292
|
+
self.dialog_text: str = dialog_text
|
|
293
|
+
self.title_bar: str | None = title_bar
|
|
77
294
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
widget (DialogBoxWidget | None): widget info
|
|
295
|
+
def to_dict(self) -> dict:
|
|
296
|
+
"""Convert DialogBox to dictionary.
|
|
81
297
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if widget.type is None:
|
|
89
|
-
raise ValueError("Widget type is `None`, but widget data is not empty")
|
|
90
|
-
|
|
91
|
-
match widget.type:
|
|
92
|
-
case DialogBoxWidgetType.NUMERIC_INPUT:
|
|
93
|
-
try:
|
|
94
|
-
return float(input_data)
|
|
95
|
-
except ValueError:
|
|
96
|
-
return None
|
|
97
|
-
case DialogBoxWidgetType.TEXT_INPUT:
|
|
98
|
-
return input_data
|
|
99
|
-
case _:
|
|
100
|
-
return None
|
|
298
|
+
Returns:
|
|
299
|
+
dict: DialogBox dictionary.
|
|
300
|
+
"""
|
|
301
|
+
dbx_dict = deepcopy(self.__dict__)
|
|
302
|
+
dbx_dict["widget"] = deepcopy(self.widget.__dict__)
|
|
303
|
+
return dbx_dict
|
|
@@ -21,3 +21,10 @@ class DuplicateDialogBoxError(HardpyError):
|
|
|
21
21
|
|
|
22
22
|
def __init__(self):
|
|
23
23
|
super().__init__(self.__doc__) # type: ignore
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class WidgetInfoError(HardpyError):
|
|
27
|
+
"""The widget info is not correct."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, message):
|
|
30
|
+
super().__init__(message)
|