hardpy 0.7.0__py3-none-any.whl → 0.8.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.
- hardpy/__init__.py +25 -27
- hardpy/cli/cli.py +1 -2
- hardpy/common/config.py +8 -8
- hardpy/hardpy_panel/frontend/dist/asset-manifest.json +3 -3
- hardpy/hardpy_panel/frontend/dist/index.html +1 -1
- hardpy/hardpy_panel/frontend/dist/logo192.png +0 -0
- hardpy/hardpy_panel/frontend/dist/static/css/main.e8a862f1.css.map +1 -1
- hardpy/hardpy_panel/frontend/dist/static/js/main.6f09d61a.js +3 -0
- hardpy/hardpy_panel/frontend/dist/static/js/main.6f09d61a.js.map +1 -0
- hardpy/pytest_hardpy/db/__init__.py +1 -1
- hardpy/pytest_hardpy/db/base_connector.py +1 -1
- hardpy/pytest_hardpy/db/base_store.py +9 -0
- hardpy/pytest_hardpy/db/schema/v1.py +4 -4
- hardpy/pytest_hardpy/db/statestore.py +0 -11
- hardpy/pytest_hardpy/plugin.py +52 -64
- hardpy/pytest_hardpy/pytest_call.py +5 -2
- hardpy/pytest_hardpy/pytest_wrapper.py +0 -1
- hardpy/pytest_hardpy/reporter/__init__.py +1 -1
- hardpy/pytest_hardpy/reporter/hook_reporter.py +3 -2
- hardpy/pytest_hardpy/result/__init__.py +1 -1
- hardpy/pytest_hardpy/utils/__init__.py +16 -12
- hardpy/pytest_hardpy/utils/dialog_box.py +81 -54
- hardpy/pytest_hardpy/utils/exception.py +6 -0
- {hardpy-0.7.0.dist-info → hardpy-0.8.0.dist-info}/METADATA +10 -8
- {hardpy-0.7.0.dist-info → hardpy-0.8.0.dist-info}/RECORD +29 -28
- hardpy/hardpy_panel/frontend/dist/static/js/main.942e57d4.js +0 -3
- hardpy/hardpy_panel/frontend/dist/static/js/main.942e57d4.js.map +0 -1
- /hardpy/hardpy_panel/frontend/dist/static/js/{main.942e57d4.js.LICENSE.txt → main.6f09d61a.js.LICENSE.txt} +0 -0
- {hardpy-0.7.0.dist-info → hardpy-0.8.0.dist-info}/WHEEL +0 -0
- {hardpy-0.7.0.dist-info → hardpy-0.8.0.dist-info}/entry_points.txt +0 -0
- {hardpy-0.7.0.dist-info → hardpy-0.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
from pycouchdb.client import Database
|
|
5
5
|
from pycouchdb.exceptions import Conflict, GenericError
|
|
6
|
-
from requests.exceptions import ConnectionError
|
|
6
|
+
from requests.exceptions import ConnectionError # noqa: A004
|
|
7
7
|
|
|
8
8
|
from hardpy.pytest_hardpy.db.base_server import BaseServer
|
|
9
9
|
|
|
@@ -69,6 +69,15 @@ class BaseStore(BaseConnector):
|
|
|
69
69
|
self._doc = self._db.get(self._doc_id)
|
|
70
70
|
return self._schema(**self._doc)
|
|
71
71
|
|
|
72
|
+
def clear(self) -> None:
|
|
73
|
+
"""Clear database."""
|
|
74
|
+
try:
|
|
75
|
+
# Clear statestore and runstore databases before each launch
|
|
76
|
+
self._db.delete(self._doc_id)
|
|
77
|
+
except (Conflict, NotFound):
|
|
78
|
+
self._log.debug("Database will be created for the first time")
|
|
79
|
+
self._doc: dict = self._init_doc()
|
|
80
|
+
|
|
72
81
|
def _init_doc(self) -> dict:
|
|
73
82
|
try:
|
|
74
83
|
doc = self._db.get(self._doc_id)
|
|
@@ -6,7 +6,7 @@ from typing import ClassVar
|
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, ConfigDict, Field
|
|
8
8
|
|
|
9
|
-
from hardpy.pytest_hardpy.utils import TestStatus as Status # noqa:
|
|
9
|
+
from hardpy.pytest_hardpy.utils import TestStatus as Status # noqa: TC001
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class IBaseResult(BaseModel):
|
|
@@ -177,7 +177,7 @@ class TestStand(BaseModel):
|
|
|
177
177
|
"info": {
|
|
178
178
|
"geo": "Belgrade"
|
|
179
179
|
},
|
|
180
|
-
"timezone": "
|
|
180
|
+
"timezone": "Europe/Belgrade",
|
|
181
181
|
"drivers": {
|
|
182
182
|
"driver_1": "driver info",
|
|
183
183
|
"driver_2": {
|
|
@@ -228,7 +228,7 @@ class ResultStateStore(IBaseResult):
|
|
|
228
228
|
"info": {
|
|
229
229
|
"geo": "Belgrade"
|
|
230
230
|
},
|
|
231
|
-
"timezone": "
|
|
231
|
+
"timezone": "Europe/Belgrade",
|
|
232
232
|
"drivers": {
|
|
233
233
|
"driver_1": "driver info",
|
|
234
234
|
"driver_2": {
|
|
@@ -324,7 +324,7 @@ class ResultRunStore(IBaseResult):
|
|
|
324
324
|
"info": {
|
|
325
325
|
"geo": "Belgrade"
|
|
326
326
|
},
|
|
327
|
-
"timezone": "
|
|
327
|
+
"timezone": "Europe/Belgrade",
|
|
328
328
|
"drivers": {
|
|
329
329
|
"driver_1": "driver info",
|
|
330
330
|
"driver_2": {
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
from logging import getLogger
|
|
5
5
|
|
|
6
|
-
from pycouchdb.exceptions import Conflict, NotFound
|
|
7
|
-
|
|
8
6
|
from hardpy.pytest_hardpy.db.base_store import BaseStore
|
|
9
7
|
from hardpy.pytest_hardpy.db.schema import ResultStateStore
|
|
10
8
|
from hardpy.pytest_hardpy.utils import SingletonMeta
|
|
@@ -17,12 +15,3 @@ class StateStore(BaseStore, metaclass=SingletonMeta):
|
|
|
17
15
|
super().__init__("statestore")
|
|
18
16
|
self._log = getLogger(__name__)
|
|
19
17
|
self._schema = ResultStateStore
|
|
20
|
-
|
|
21
|
-
def clear(self) -> None:
|
|
22
|
-
"""Clear database."""
|
|
23
|
-
try:
|
|
24
|
-
# Clear the statestore database before each launch
|
|
25
|
-
self._db.delete(self._doc_id)
|
|
26
|
-
except (Conflict, NotFound):
|
|
27
|
-
self._log.debug("Statestore database will be created for the first time")
|
|
28
|
-
self._doc: dict = self._init_doc()
|
hardpy/pytest_hardpy/plugin.py
CHANGED
|
@@ -25,7 +25,7 @@ from pytest import (
|
|
|
25
25
|
Parser,
|
|
26
26
|
Session,
|
|
27
27
|
TestReport,
|
|
28
|
-
exit,
|
|
28
|
+
exit, # noqa: A004
|
|
29
29
|
fixture,
|
|
30
30
|
skip,
|
|
31
31
|
)
|
|
@@ -63,7 +63,7 @@ def pytest_addoption(parser: Parser) -> None:
|
|
|
63
63
|
)
|
|
64
64
|
parser.addoption(
|
|
65
65
|
"--hardpy-clear-database",
|
|
66
|
-
action="
|
|
66
|
+
action="store_true",
|
|
67
67
|
default=False,
|
|
68
68
|
help="clear hardpy local database",
|
|
69
69
|
)
|
|
@@ -116,7 +116,6 @@ class HardpyPlugin:
|
|
|
116
116
|
con_data.database_url = str(database_url) # type: ignore
|
|
117
117
|
|
|
118
118
|
is_clear_database = config.getoption("--hardpy-clear-database")
|
|
119
|
-
is_clear_statestore = is_clear_database == str(True)
|
|
120
119
|
|
|
121
120
|
socket_port = config.getoption("--hardpy-sp")
|
|
122
121
|
if socket_port:
|
|
@@ -133,7 +132,7 @@ class HardpyPlugin:
|
|
|
133
132
|
|
|
134
133
|
# must be init after config data is set
|
|
135
134
|
try:
|
|
136
|
-
self._reporter = HookReporter(
|
|
135
|
+
self._reporter = HookReporter(bool(is_clear_database))
|
|
137
136
|
except RuntimeError as exc:
|
|
138
137
|
exit(str(exc), 1)
|
|
139
138
|
|
|
@@ -215,21 +214,24 @@ class HardpyPlugin:
|
|
|
215
214
|
|
|
216
215
|
node_info = NodeInfo(item)
|
|
217
216
|
|
|
218
|
-
|
|
217
|
+
status = TestStatus.RUN
|
|
218
|
+
is_skip_test = self._is_skip_test(node_info)
|
|
219
|
+
if not is_skip_test:
|
|
220
|
+
self._reporter.set_module_start_time(node_info.module_id)
|
|
221
|
+
self._reporter.set_case_start_time(node_info.module_id, node_info.case_id)
|
|
222
|
+
else:
|
|
223
|
+
status = TestStatus.SKIPPED
|
|
224
|
+
self._results[node_info.module_id][node_info.case_id] = status
|
|
225
|
+
progress = self._progress.calculate(item.nodeid)
|
|
226
|
+
self._reporter.set_progress(progress)
|
|
219
227
|
|
|
220
|
-
self._reporter.set_module_status(node_info.module_id,
|
|
221
|
-
self._reporter.
|
|
222
|
-
self._reporter.set_case_status(
|
|
223
|
-
node_info.module_id,
|
|
224
|
-
node_info.case_id,
|
|
225
|
-
TestStatus.RUN,
|
|
226
|
-
)
|
|
227
|
-
self._reporter.set_case_start_time(
|
|
228
|
-
node_info.module_id,
|
|
229
|
-
node_info.case_id,
|
|
230
|
-
)
|
|
228
|
+
self._reporter.set_module_status(node_info.module_id, status)
|
|
229
|
+
self._reporter.set_case_status(node_info.module_id, node_info.case_id, status)
|
|
231
230
|
self._reporter.update_db_by_doc()
|
|
232
231
|
|
|
232
|
+
if is_skip_test:
|
|
233
|
+
skip(f"Test {item.nodeid} is skipped")
|
|
234
|
+
|
|
233
235
|
def pytest_runtest_call(self, item: Item) -> None:
|
|
234
236
|
"""Call the test item."""
|
|
235
237
|
node_info = NodeInfo(item)
|
|
@@ -242,7 +244,7 @@ class HardpyPlugin:
|
|
|
242
244
|
|
|
243
245
|
def pytest_runtest_makereport(self, item: Item, call: CallInfo) -> None:
|
|
244
246
|
"""Call after call of each test item."""
|
|
245
|
-
if call.when != "call":
|
|
247
|
+
if call.when != "call" or not call.excinfo:
|
|
246
248
|
return
|
|
247
249
|
|
|
248
250
|
node_info = NodeInfo(item)
|
|
@@ -250,25 +252,22 @@ class HardpyPlugin:
|
|
|
250
252
|
module_id = node_info.module_id
|
|
251
253
|
case_id = node_info.case_id
|
|
252
254
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if current_attempt == attempt:
|
|
270
|
-
return
|
|
271
|
-
# fmt: on
|
|
255
|
+
# first attempt was in pytest_runtest_call
|
|
256
|
+
for current_attempt in range(2, attempt + 1):
|
|
257
|
+
self._reporter.set_module_status(module_id, TestStatus.RUN)
|
|
258
|
+
self._reporter.set_case_status(module_id, case_id, TestStatus.RUN)
|
|
259
|
+
self._reporter.set_case_attempt(module_id, case_id, current_attempt)
|
|
260
|
+
self._reporter.update_db_by_doc()
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
item.runtest()
|
|
264
|
+
call.excinfo = None
|
|
265
|
+
self._reporter.set_case_status(module_id, case_id, TestStatus.PASSED)
|
|
266
|
+
break
|
|
267
|
+
except AssertionError:
|
|
268
|
+
self._reporter.set_case_status(module_id, case_id, TestStatus.FAILED)
|
|
269
|
+
if current_attempt == attempt:
|
|
270
|
+
return
|
|
272
271
|
|
|
273
272
|
# Reporting hooks
|
|
274
273
|
|
|
@@ -422,36 +421,25 @@ class HardpyPlugin:
|
|
|
422
421
|
case _:
|
|
423
422
|
return None
|
|
424
423
|
|
|
425
|
-
def
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
node_info.case_id,
|
|
430
|
-
),
|
|
424
|
+
def _is_skip_test(self, node_info: NodeInfo) -> bool:
|
|
425
|
+
"""Is need to skip a test because it depends on another test."""
|
|
426
|
+
is_dependency_test_exist = self._dependencies.get(
|
|
427
|
+
TestDependencyInfo(node_info.module_id, node_info.case_id),
|
|
431
428
|
)
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
self._progress.calculate(f"{node_info.module_id}::{node_info.case_id}"),
|
|
437
|
-
)
|
|
438
|
-
skip(f"Test {node_info.module_id}::{node_info.case_id} is skipped")
|
|
439
|
-
|
|
440
|
-
def _is_dependency_failed(self, dependency: TestDependencyInfo) -> bool:
|
|
441
|
-
if isinstance(dependency, TestDependencyInfo):
|
|
442
|
-
incorrect_status = {
|
|
443
|
-
TestStatus.FAILED,
|
|
444
|
-
TestStatus.SKIPPED,
|
|
445
|
-
TestStatus.ERROR,
|
|
446
|
-
}
|
|
447
|
-
module_id, case_id = dependency
|
|
429
|
+
is_dependency_test_failed = False
|
|
430
|
+
if is_dependency_test_exist:
|
|
431
|
+
wrong_status = {TestStatus.FAILED, TestStatus.SKIPPED, TestStatus.ERROR}
|
|
432
|
+
module_id, case_id = is_dependency_test_exist
|
|
448
433
|
if case_id is not None:
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
434
|
+
is_dependency_test_failed = (
|
|
435
|
+
self._results[module_id][case_id] in wrong_status
|
|
436
|
+
)
|
|
437
|
+
else:
|
|
438
|
+
result_set = set(self._results[module_id].values())
|
|
439
|
+
is_dependency_test_failed = any(
|
|
440
|
+
status in wrong_status for status in result_set
|
|
441
|
+
)
|
|
442
|
+
return bool(is_dependency_test_exist and is_dependency_test_failed)
|
|
455
443
|
|
|
456
444
|
def _add_dependency(self, node_info: NodeInfo, nodes: dict) -> None:
|
|
457
445
|
dependency = node_info.dependency
|
|
@@ -79,7 +79,10 @@ def set_dut_serial_number(serial_number: str) -> None:
|
|
|
79
79
|
key = reporter.generate_key(DF.DUT, DF.SERIAL_NUMBER)
|
|
80
80
|
if reporter.get_field(key):
|
|
81
81
|
raise DuplicateSerialNumberError
|
|
82
|
-
reporter.set_doc_value(
|
|
82
|
+
reporter.set_doc_value(
|
|
83
|
+
key,
|
|
84
|
+
serial_number if isinstance(serial_number, str) else str(serial_number),
|
|
85
|
+
)
|
|
83
86
|
reporter.update_db_by_doc()
|
|
84
87
|
|
|
85
88
|
|
|
@@ -274,6 +277,7 @@ def run_dialog_box(dialog_box_data: DialogBox) -> Any: # noqa: ANN401
|
|
|
274
277
|
- title_bar (str | None): The title bar of the dialog box.
|
|
275
278
|
If the title_bar field is missing, it is the case name.
|
|
276
279
|
- widget (DialogBoxWidget | None): Widget information.
|
|
280
|
+
- image (ImageComponent | None): Image information.
|
|
277
281
|
|
|
278
282
|
Returns:
|
|
279
283
|
Any: An object containing the user's response.
|
|
@@ -285,7 +289,6 @@ def run_dialog_box(dialog_box_data: DialogBox) -> Any: # noqa: ANN401
|
|
|
285
289
|
- NUMERIC_INPUT: float.
|
|
286
290
|
- RADIOBUTTON: str.
|
|
287
291
|
- CHECKBOX: list[str].
|
|
288
|
-
- IMAGE: bool.
|
|
289
292
|
- MULTISTEP: bool.
|
|
290
293
|
|
|
291
294
|
Raises:
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from copy import deepcopy
|
|
6
|
-
from datetime import datetime
|
|
7
6
|
from logging import getLogger
|
|
8
7
|
from time import time
|
|
9
8
|
|
|
10
9
|
from natsort import natsorted
|
|
10
|
+
from tzlocal import get_localzone
|
|
11
11
|
|
|
12
12
|
from hardpy.pytest_hardpy.db import DatabaseField as DF # noqa: N817
|
|
13
13
|
from hardpy.pytest_hardpy.reporter.base import BaseReporter
|
|
@@ -21,6 +21,7 @@ class HookReporter(BaseReporter):
|
|
|
21
21
|
super().__init__()
|
|
22
22
|
if is_clear_database:
|
|
23
23
|
self._statestore.clear()
|
|
24
|
+
self._runstore.clear()
|
|
24
25
|
self._log = getLogger(__name__)
|
|
25
26
|
|
|
26
27
|
def init_doc(self, doc_name: str) -> None:
|
|
@@ -38,7 +39,7 @@ class HookReporter(BaseReporter):
|
|
|
38
39
|
self.set_doc_value(DF.OPERATOR_MSG, {}, statestore_only=True)
|
|
39
40
|
|
|
40
41
|
test_stand_tz = self.generate_key(DF.TEST_STAND, DF.TIMEZONE)
|
|
41
|
-
self.set_doc_value(test_stand_tz,
|
|
42
|
+
self.set_doc_value(test_stand_tz, str(get_localzone().key))
|
|
42
43
|
|
|
43
44
|
test_stand_id_key = self.generate_key(DF.TEST_STAND, DF.HW_ID)
|
|
44
45
|
self.set_doc_value(test_stand_id_key, machine_id())
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
from hardpy.pytest_hardpy.utils.connection_data import ConnectionData
|
|
5
5
|
from hardpy.pytest_hardpy.utils.const import TestStatus
|
|
6
6
|
from hardpy.pytest_hardpy.utils.dialog_box import (
|
|
7
|
+
BaseWidget,
|
|
7
8
|
CheckboxWidget,
|
|
8
9
|
DialogBox,
|
|
9
|
-
|
|
10
|
+
ImageComponent,
|
|
10
11
|
MultistepWidget,
|
|
11
12
|
NumericInputWidget,
|
|
12
13
|
RadiobuttonWidget,
|
|
@@ -18,6 +19,7 @@ from hardpy.pytest_hardpy.utils.exception import (
|
|
|
18
19
|
DuplicateSerialNumberError,
|
|
19
20
|
DuplicateTestStandLocationError,
|
|
20
21
|
DuplicateTestStandNameError,
|
|
22
|
+
ImageError,
|
|
21
23
|
WidgetInfoError,
|
|
22
24
|
)
|
|
23
25
|
from hardpy.pytest_hardpy.utils.machineid import machine_id
|
|
@@ -26,23 +28,25 @@ from hardpy.pytest_hardpy.utils.progress_calculator import ProgressCalculator
|
|
|
26
28
|
from hardpy.pytest_hardpy.utils.singleton import SingletonMeta
|
|
27
29
|
|
|
28
30
|
__all__ = [
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"TestStatus",
|
|
32
|
-
"SingletonMeta",
|
|
31
|
+
"BaseWidget",
|
|
32
|
+
"CheckboxWidget",
|
|
33
33
|
"ConnectionData",
|
|
34
|
-
"
|
|
34
|
+
"DialogBox",
|
|
35
35
|
"DuplicatePartNumberError",
|
|
36
|
+
"DuplicateSerialNumberError",
|
|
36
37
|
"DuplicateTestStandLocationError",
|
|
37
38
|
"DuplicateTestStandNameError",
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
39
|
+
"ImageComponent",
|
|
40
|
+
"ImageError",
|
|
41
|
+
"MultistepWidget",
|
|
42
|
+
"NodeInfo",
|
|
41
43
|
"NumericInputWidget",
|
|
42
|
-
"
|
|
44
|
+
"ProgressCalculator",
|
|
43
45
|
"RadiobuttonWidget",
|
|
44
|
-
"
|
|
45
|
-
"MultistepWidget",
|
|
46
|
+
"SingletonMeta",
|
|
46
47
|
"StepWidget",
|
|
48
|
+
"TestStatus",
|
|
49
|
+
"TextInputWidget",
|
|
50
|
+
"WidgetInfoError",
|
|
47
51
|
"machine_id",
|
|
48
52
|
]
|
|
@@ -10,7 +10,7 @@ from dataclasses import dataclass
|
|
|
10
10
|
from enum import Enum
|
|
11
11
|
from typing import Any, Final
|
|
12
12
|
|
|
13
|
-
from hardpy.pytest_hardpy.utils.exception import WidgetInfoError
|
|
13
|
+
from hardpy.pytest_hardpy.utils.exception import ImageError, WidgetInfoError
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class WidgetType(Enum):
|
|
@@ -21,7 +21,6 @@ class WidgetType(Enum):
|
|
|
21
21
|
NUMERIC_INPUT = "numericinput"
|
|
22
22
|
RADIOBUTTON = "radiobutton"
|
|
23
23
|
CHECKBOX = "checkbox"
|
|
24
|
-
IMAGE = "image"
|
|
25
24
|
STEP = "step"
|
|
26
25
|
MULTISTEP = "multistep"
|
|
27
26
|
|
|
@@ -49,8 +48,11 @@ class IWidget(ABC):
|
|
|
49
48
|
class BaseWidget(IWidget):
|
|
50
49
|
"""Widget info interface."""
|
|
51
50
|
|
|
52
|
-
def __init__(
|
|
53
|
-
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
widget_type: WidgetType = WidgetType.BASE,
|
|
54
|
+
) -> None:
|
|
55
|
+
super().__init__(widget_type)
|
|
54
56
|
|
|
55
57
|
def convert_data(self, input_data: str | None = None) -> bool: # noqa: ARG002
|
|
56
58
|
"""Get base widget data, i.e. None.
|
|
@@ -121,6 +123,9 @@ class RadiobuttonWidget(IWidget):
|
|
|
121
123
|
if not fields:
|
|
122
124
|
msg = "RadiobuttonWidget must have at least one field"
|
|
123
125
|
raise ValueError(msg)
|
|
126
|
+
if len(fields) != len(set(fields)):
|
|
127
|
+
msg = "RadiobuttonWidget fields must be unique"
|
|
128
|
+
raise ValueError(msg)
|
|
124
129
|
self.info["fields"] = fields
|
|
125
130
|
|
|
126
131
|
def convert_data(self, input_data: str) -> str:
|
|
@@ -151,6 +156,9 @@ class CheckboxWidget(IWidget):
|
|
|
151
156
|
if not fields:
|
|
152
157
|
msg = "Checkbox must have at least one field"
|
|
153
158
|
raise ValueError(msg)
|
|
159
|
+
if len(fields) != len(set(fields)):
|
|
160
|
+
msg = "CheckboxWidget fields must be unique"
|
|
161
|
+
raise ValueError(msg)
|
|
154
162
|
self.info["fields"] = fields
|
|
155
163
|
|
|
156
164
|
def convert_data(self, input_data: str) -> list[str] | None:
|
|
@@ -168,57 +176,13 @@ class CheckboxWidget(IWidget):
|
|
|
168
176
|
return None
|
|
169
177
|
|
|
170
178
|
|
|
171
|
-
class ImageWidget(IWidget):
|
|
172
|
-
"""Image widget."""
|
|
173
|
-
|
|
174
|
-
def __init__(self, address: str, format: str = "image", width: int = 100) -> None: # noqa: A002
|
|
175
|
-
"""Validate the image fields and defines the base64 if it does not exist.
|
|
176
|
-
|
|
177
|
-
Args:
|
|
178
|
-
address (str): image address
|
|
179
|
-
format (str): image format
|
|
180
|
-
width (int): image width
|
|
181
|
-
|
|
182
|
-
Raises:
|
|
183
|
-
WidgetInfoError: If both address and base64 are specified.
|
|
184
|
-
"""
|
|
185
|
-
super().__init__(WidgetType.IMAGE)
|
|
186
|
-
|
|
187
|
-
if width < 1:
|
|
188
|
-
msg = "Width must be positive"
|
|
189
|
-
raise WidgetInfoError(msg)
|
|
190
|
-
|
|
191
|
-
self.info["address"] = address
|
|
192
|
-
self.info["format"] = format
|
|
193
|
-
self.info["width"] = width
|
|
194
|
-
|
|
195
|
-
try:
|
|
196
|
-
with open(address, "rb") as file: # noqa: PTH123
|
|
197
|
-
file_data = file.read()
|
|
198
|
-
except FileNotFoundError:
|
|
199
|
-
msg = "The image address is invalid"
|
|
200
|
-
raise WidgetInfoError(msg) # noqa: B904
|
|
201
|
-
self.info["base64"] = base64.b64encode(file_data).decode("utf-8")
|
|
202
|
-
|
|
203
|
-
def convert_data(self, input_data: str | None = None) -> bool: # noqa: ARG002
|
|
204
|
-
"""Get the image widget data, i.e. None.
|
|
205
|
-
|
|
206
|
-
Args:
|
|
207
|
-
input_data (str | None): input string or nothing.
|
|
208
|
-
|
|
209
|
-
Returns:
|
|
210
|
-
bool: True if confirm button is pressed
|
|
211
|
-
"""
|
|
212
|
-
return True
|
|
213
|
-
|
|
214
|
-
|
|
215
179
|
class StepWidget(IWidget):
|
|
216
180
|
"""Step widget.
|
|
217
181
|
|
|
218
182
|
Args:
|
|
219
183
|
title (str): Step title
|
|
220
184
|
text (str | None): Step text
|
|
221
|
-
|
|
185
|
+
image (ImageComponent | None): Step image
|
|
222
186
|
|
|
223
187
|
Raises:
|
|
224
188
|
WidgetInfoError: If the text or widget are not provided.
|
|
@@ -228,17 +192,17 @@ class StepWidget(IWidget):
|
|
|
228
192
|
self,
|
|
229
193
|
title: str,
|
|
230
194
|
text: str | None,
|
|
231
|
-
|
|
195
|
+
image: ImageComponent | None = None,
|
|
232
196
|
) -> None:
|
|
233
197
|
super().__init__(WidgetType.STEP)
|
|
234
|
-
if text is None and
|
|
235
|
-
msg = "Text or
|
|
198
|
+
if text is None and image is None:
|
|
199
|
+
msg = "Text or image must be provided"
|
|
236
200
|
raise WidgetInfoError(msg)
|
|
237
201
|
self.info["title"] = title
|
|
238
202
|
if isinstance(text, str):
|
|
239
203
|
self.info["text"] = text
|
|
240
|
-
if isinstance(
|
|
241
|
-
self.info["
|
|
204
|
+
if isinstance(image, ImageComponent):
|
|
205
|
+
self.info["image"] = image.__dict__
|
|
242
206
|
|
|
243
207
|
def convert_data(self, input_data: str) -> bool: # noqa: ARG002
|
|
244
208
|
"""Get the step widget data in the correct format.
|
|
@@ -268,8 +232,14 @@ class MultistepWidget(IWidget):
|
|
|
268
232
|
if not steps:
|
|
269
233
|
msg = "MultistepWidget must have at least one step"
|
|
270
234
|
raise ValueError(msg)
|
|
235
|
+
title_set = set()
|
|
271
236
|
self.info["steps"] = []
|
|
272
237
|
for step in steps:
|
|
238
|
+
title = step.info["title"]
|
|
239
|
+
if title in title_set:
|
|
240
|
+
msg = "MultistepWidget must have unique step titles"
|
|
241
|
+
raise ValueError(msg)
|
|
242
|
+
title_set.add(title)
|
|
273
243
|
self.info["steps"].append(step.__dict__)
|
|
274
244
|
|
|
275
245
|
def convert_data(self, input_data: str) -> bool: # noqa: ARG002
|
|
@@ -284,6 +254,58 @@ class MultistepWidget(IWidget):
|
|
|
284
254
|
return True
|
|
285
255
|
|
|
286
256
|
|
|
257
|
+
class ImageComponent:
|
|
258
|
+
"""Image component."""
|
|
259
|
+
|
|
260
|
+
def __init__(
|
|
261
|
+
self,
|
|
262
|
+
address: str,
|
|
263
|
+
width: int = 100,
|
|
264
|
+
border: int = 0,
|
|
265
|
+
) -> None:
|
|
266
|
+
"""Validate the image fields and defines the base64 if it does not exist.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
address (str): image address
|
|
270
|
+
width (int): image width
|
|
271
|
+
border (int): image border
|
|
272
|
+
|
|
273
|
+
Raises:
|
|
274
|
+
ImageError: If both address and base64data are specified.
|
|
275
|
+
"""
|
|
276
|
+
if width < 1:
|
|
277
|
+
msg = "Width must be positive"
|
|
278
|
+
raise WidgetInfoError(msg)
|
|
279
|
+
|
|
280
|
+
if border < 0:
|
|
281
|
+
msg = "Border must be non-negative"
|
|
282
|
+
raise WidgetInfoError(msg)
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
with open(address, "rb") as file: # noqa: PTH123
|
|
286
|
+
file_data = file.read()
|
|
287
|
+
except FileNotFoundError:
|
|
288
|
+
msg = "The image address is invalid"
|
|
289
|
+
raise ImageError(msg) # noqa: B904
|
|
290
|
+
self.address = address
|
|
291
|
+
self.width = width
|
|
292
|
+
self.border = border
|
|
293
|
+
self.base64 = base64.b64encode(file_data).decode("utf-8")
|
|
294
|
+
|
|
295
|
+
def to_dict(self) -> dict:
|
|
296
|
+
"""Convert ImageComponent to dictionary.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
dict: ImageComponent dictionary.
|
|
300
|
+
"""
|
|
301
|
+
return {
|
|
302
|
+
"address": self.address,
|
|
303
|
+
"width": self.width,
|
|
304
|
+
"base64": self.base64,
|
|
305
|
+
"border": self.border,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
|
|
287
309
|
@dataclass
|
|
288
310
|
class DialogBox:
|
|
289
311
|
"""Dialog box data.
|
|
@@ -292,6 +314,7 @@ class DialogBox:
|
|
|
292
314
|
dialog_text (str): dialog text
|
|
293
315
|
title_bar (str | None): title bar
|
|
294
316
|
widget (IWidget | None): widget info
|
|
317
|
+
image (ImageComponent | None): image
|
|
295
318
|
"""
|
|
296
319
|
|
|
297
320
|
def __init__(
|
|
@@ -299,8 +322,10 @@ class DialogBox:
|
|
|
299
322
|
dialog_text: str,
|
|
300
323
|
title_bar: str | None = None,
|
|
301
324
|
widget: IWidget | None = None,
|
|
325
|
+
image: ImageComponent | None = None,
|
|
302
326
|
) -> None:
|
|
303
327
|
self.widget: IWidget = BaseWidget() if widget is None else widget
|
|
328
|
+
self.image: ImageComponent | None = image
|
|
304
329
|
self.dialog_text: str = dialog_text
|
|
305
330
|
self.title_bar: str | None = title_bar
|
|
306
331
|
|
|
@@ -312,4 +337,6 @@ class DialogBox:
|
|
|
312
337
|
"""
|
|
313
338
|
dbx_dict = deepcopy(self.__dict__)
|
|
314
339
|
dbx_dict["widget"] = deepcopy(self.widget.__dict__)
|
|
340
|
+
if self.image:
|
|
341
|
+
dbx_dict["image"] = deepcopy(self.image.__dict__)
|
|
315
342
|
return dbx_dict
|
|
@@ -42,3 +42,9 @@ class WidgetInfoError(HardpyError):
|
|
|
42
42
|
|
|
43
43
|
def __init__(self, message: str) -> None:
|
|
44
44
|
super().__init__(message)
|
|
45
|
+
|
|
46
|
+
class ImageError(HardpyError):
|
|
47
|
+
"""The image info is not correct."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, message: str) -> None:
|
|
50
|
+
super().__init__(message)
|