hardpy 0.6.1__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 +49 -49
- hardpy/cli/cli.py +8 -9
- hardpy/cli/template.py +6 -6
- hardpy/common/config.py +19 -18
- hardpy/hardpy_panel/api.py +9 -9
- 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.7c954faf.js.map → main.6f09d61a.js.map} +1 -1
- hardpy/pytest_hardpy/db/__init__.py +4 -5
- hardpy/pytest_hardpy/db/base_connector.py +6 -5
- hardpy/pytest_hardpy/db/base_server.py +1 -1
- hardpy/pytest_hardpy/db/base_store.py +23 -9
- hardpy/pytest_hardpy/db/const.py +3 -1
- hardpy/pytest_hardpy/db/runstore.py +13 -15
- hardpy/pytest_hardpy/db/schema/__init__.py +9 -0
- hardpy/pytest_hardpy/db/{schema.py → schema/v1.py} +120 -79
- hardpy/pytest_hardpy/db/statestore.py +7 -20
- hardpy/pytest_hardpy/plugin.py +128 -85
- hardpy/pytest_hardpy/pytest_call.py +80 -32
- hardpy/pytest_hardpy/pytest_wrapper.py +8 -8
- hardpy/pytest_hardpy/reporter/__init__.py +2 -2
- hardpy/pytest_hardpy/reporter/base.py +32 -7
- hardpy/pytest_hardpy/reporter/hook_reporter.py +66 -37
- hardpy/pytest_hardpy/reporter/runner_reporter.py +6 -8
- hardpy/pytest_hardpy/result/__init__.py +2 -2
- hardpy/pytest_hardpy/result/couchdb_config.py +20 -16
- hardpy/pytest_hardpy/result/report_loader/couchdb_loader.py +2 -2
- hardpy/pytest_hardpy/result/report_reader/couchdb_reader.py +36 -20
- hardpy/pytest_hardpy/utils/__init__.py +34 -29
- hardpy/pytest_hardpy/utils/connection_data.py +6 -8
- hardpy/pytest_hardpy/utils/const.py +1 -1
- hardpy/pytest_hardpy/utils/dialog_box.py +105 -66
- hardpy/pytest_hardpy/utils/exception.py +14 -8
- hardpy/pytest_hardpy/utils/machineid.py +15 -0
- hardpy/pytest_hardpy/utils/node_info.py +45 -16
- hardpy/pytest_hardpy/utils/progress_calculator.py +4 -3
- hardpy/pytest_hardpy/utils/singleton.py +23 -16
- {hardpy-0.6.1.dist-info → hardpy-0.8.0.dist-info}/METADATA +26 -33
- {hardpy-0.6.1.dist-info → hardpy-0.8.0.dist-info}/RECORD +46 -43
- hardpy/hardpy_panel/frontend/dist/static/js/main.7c954faf.js +0 -3
- /hardpy/hardpy_panel/frontend/dist/static/js/{main.7c954faf.js.LICENSE.txt → main.6f09d61a.js.LICENSE.txt} +0 -0
- {hardpy-0.6.1.dist-info → hardpy-0.8.0.dist-info}/WHEEL +0 -0
- {hardpy-0.6.1.dist-info → hardpy-0.8.0.dist-info}/entry_points.txt +0 -0
- {hardpy-0.6.1.dist-info → hardpy-0.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,17 +1,21 @@
|
|
|
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
|
+
from __future__ import annotations
|
|
3
4
|
|
|
4
|
-
from logging import getLogger
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from
|
|
6
|
+
from logging import getLogger
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
7
8
|
|
|
8
9
|
from pycouchdb import Server as DbServer
|
|
9
|
-
from pycouchdb.client import Database
|
|
10
10
|
from pycouchdb.exceptions import NotFound
|
|
11
11
|
|
|
12
|
-
from hardpy.pytest_hardpy.db import DatabaseField as DF
|
|
12
|
+
from hardpy.pytest_hardpy.db import DatabaseField as DF # noqa: N817
|
|
13
13
|
from hardpy.pytest_hardpy.utils.const import TestStatus
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pycouchdb.client import Database
|
|
17
|
+
|
|
18
|
+
from hardpy.pytest_hardpy.result.couchdb_config import CouchdbConfig
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
@dataclass
|
|
@@ -22,14 +26,14 @@ class ReportInfo:
|
|
|
22
26
|
status: str
|
|
23
27
|
start_time: str
|
|
24
28
|
end_time: str
|
|
25
|
-
first_failed_test_name:
|
|
26
|
-
first_failed_test_id:
|
|
29
|
+
first_failed_test_name: str | None
|
|
30
|
+
first_failed_test_id: str | None
|
|
27
31
|
|
|
28
32
|
|
|
29
33
|
class CouchdbReader:
|
|
30
34
|
"""CouchDB report info reader."""
|
|
31
35
|
|
|
32
|
-
def __init__(self, config: CouchdbConfig):
|
|
36
|
+
def __init__(self, config: CouchdbConfig) -> None:
|
|
33
37
|
self._log = getLogger(__name__)
|
|
34
38
|
self._config = config
|
|
35
39
|
self._db_srv = DbServer(config.connection_string)
|
|
@@ -58,13 +62,14 @@ class CouchdbReader:
|
|
|
58
62
|
int: number of reports
|
|
59
63
|
"""
|
|
60
64
|
if start_time < 0 or end_time < 0:
|
|
61
|
-
|
|
65
|
+
msg = "Start time and end time must be positive values"
|
|
66
|
+
raise ValueError(msg)
|
|
62
67
|
return sum(
|
|
63
68
|
1
|
|
64
69
|
for report in self._db.all()
|
|
65
70
|
if self._is_in_timeframe(
|
|
66
|
-
self._get_start_time_from_db(report[self._doc_id]),
|
|
67
|
-
self._get_stop_time_from_db(report[self._doc_id]),
|
|
71
|
+
self._get_start_time_from_db(report[self._doc_id]), # type: ignore
|
|
72
|
+
self._get_stop_time_from_db(report[self._doc_id]), # type: ignore
|
|
68
73
|
start_time,
|
|
69
74
|
end_time,
|
|
70
75
|
)
|
|
@@ -85,10 +90,11 @@ class CouchdbReader:
|
|
|
85
90
|
doc = self._db.get(report_name)
|
|
86
91
|
status = doc[DF.STATUS]
|
|
87
92
|
if status not in {TestStatus.PASSED, TestStatus.FAILED, TestStatus.SKIPPED}:
|
|
88
|
-
|
|
93
|
+
msg = "Invalid report status"
|
|
94
|
+
raise ValueError(msg)
|
|
89
95
|
return status
|
|
90
96
|
|
|
91
|
-
def get_report_infos(self) ->
|
|
97
|
+
def get_report_infos(self) -> list[ReportInfo]:
|
|
92
98
|
"""Get a list of information about all reports in the database.
|
|
93
99
|
|
|
94
100
|
Returns:
|
|
@@ -102,8 +108,10 @@ class CouchdbReader:
|
|
|
102
108
|
return reports_info
|
|
103
109
|
|
|
104
110
|
def get_report_infos_in_timeframe(
|
|
105
|
-
self,
|
|
106
|
-
|
|
111
|
+
self,
|
|
112
|
+
start_time: int,
|
|
113
|
+
end_time: int,
|
|
114
|
+
) -> list[ReportInfo]:
|
|
107
115
|
"""Get a list of information about reports in a timeframe in the database.
|
|
108
116
|
|
|
109
117
|
Args:
|
|
@@ -117,14 +125,15 @@ class CouchdbReader:
|
|
|
117
125
|
List[ReportInfo]: list of report information
|
|
118
126
|
"""
|
|
119
127
|
if start_time < 0 or end_time < 0:
|
|
120
|
-
|
|
128
|
+
msg = "Start time and end time must be positive values"
|
|
129
|
+
raise ValueError(msg)
|
|
121
130
|
|
|
122
131
|
reports_info = []
|
|
123
132
|
reports = self._db.all()
|
|
124
133
|
for report in reports:
|
|
125
134
|
start_t_db = self._get_start_time_from_db(report[self._doc_id])
|
|
126
135
|
stop_t_db = self._get_stop_time_from_db(report[self._doc_id])
|
|
127
|
-
if self._is_in_timeframe(start_t_db, stop_t_db, start_time, end_time):
|
|
136
|
+
if self._is_in_timeframe(start_t_db, stop_t_db, start_time, end_time): # type: ignore
|
|
128
137
|
report_info = self._get_single_report_info(report)
|
|
129
138
|
reports_info.append(report_info)
|
|
130
139
|
return reports_info
|
|
@@ -134,7 +143,8 @@ class CouchdbReader:
|
|
|
134
143
|
return self._db_srv.database(self._config.db_name)
|
|
135
144
|
except NotFound as exc:
|
|
136
145
|
self._log.error(f"Error initializing database: {exc}")
|
|
137
|
-
|
|
146
|
+
msg = "Error initializing database"
|
|
147
|
+
raise RuntimeError(msg) from exc
|
|
138
148
|
|
|
139
149
|
def _get_start_time_from_db(self, doc: dict) -> str:
|
|
140
150
|
return doc[DF.START_TIME]
|
|
@@ -146,7 +156,7 @@ class CouchdbReader:
|
|
|
146
156
|
first_failed_test_name = None
|
|
147
157
|
first_failed_test_id = None
|
|
148
158
|
report_doc = report[self._doc_id]
|
|
149
|
-
for
|
|
159
|
+
for module_info in report_doc[DF.MODULES].values():
|
|
150
160
|
for case_name, case_info in module_info[DF.CASES].items():
|
|
151
161
|
if case_info[DF.STATUS] == TestStatus.FAILED:
|
|
152
162
|
first_failed_test_name = case_info[DF.NAME]
|
|
@@ -160,5 +170,11 @@ class CouchdbReader:
|
|
|
160
170
|
first_failed_test_id=first_failed_test_id,
|
|
161
171
|
)
|
|
162
172
|
|
|
163
|
-
def _is_in_timeframe(
|
|
173
|
+
def _is_in_timeframe(
|
|
174
|
+
self,
|
|
175
|
+
start: int,
|
|
176
|
+
end: int,
|
|
177
|
+
timeframe_start: int,
|
|
178
|
+
timeframe_end: int,
|
|
179
|
+
) -> bool:
|
|
164
180
|
return timeframe_start <= start and end <= timeframe_end
|
|
@@ -1,47 +1,52 @@
|
|
|
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
|
-
from hardpy.pytest_hardpy.utils.node_info import NodeInfo
|
|
5
|
-
from hardpy.pytest_hardpy.utils.progress_calculator import ProgressCalculator
|
|
6
|
-
from hardpy.pytest_hardpy.utils.const import TestStatus
|
|
7
|
-
from hardpy.pytest_hardpy.utils.singleton import Singleton
|
|
8
4
|
from hardpy.pytest_hardpy.utils.connection_data import ConnectionData
|
|
9
|
-
from hardpy.pytest_hardpy.utils.
|
|
10
|
-
DuplicateSerialNumberError,
|
|
11
|
-
DuplicatePartNumberError,
|
|
12
|
-
DuplicateTestStandNameError,
|
|
13
|
-
DuplicateDialogBoxError,
|
|
14
|
-
WidgetInfoError,
|
|
15
|
-
)
|
|
5
|
+
from hardpy.pytest_hardpy.utils.const import TestStatus
|
|
16
6
|
from hardpy.pytest_hardpy.utils.dialog_box import (
|
|
7
|
+
BaseWidget,
|
|
8
|
+
CheckboxWidget,
|
|
17
9
|
DialogBox,
|
|
18
|
-
|
|
10
|
+
ImageComponent,
|
|
11
|
+
MultistepWidget,
|
|
19
12
|
NumericInputWidget,
|
|
20
|
-
CheckboxWidget,
|
|
21
13
|
RadiobuttonWidget,
|
|
22
|
-
ImageWidget,
|
|
23
|
-
MultistepWidget,
|
|
24
14
|
StepWidget,
|
|
15
|
+
TextInputWidget,
|
|
25
16
|
)
|
|
26
|
-
|
|
17
|
+
from hardpy.pytest_hardpy.utils.exception import (
|
|
18
|
+
DuplicatePartNumberError,
|
|
19
|
+
DuplicateSerialNumberError,
|
|
20
|
+
DuplicateTestStandLocationError,
|
|
21
|
+
DuplicateTestStandNameError,
|
|
22
|
+
ImageError,
|
|
23
|
+
WidgetInfoError,
|
|
24
|
+
)
|
|
25
|
+
from hardpy.pytest_hardpy.utils.machineid import machine_id
|
|
26
|
+
from hardpy.pytest_hardpy.utils.node_info import NodeInfo
|
|
27
|
+
from hardpy.pytest_hardpy.utils.progress_calculator import ProgressCalculator
|
|
28
|
+
from hardpy.pytest_hardpy.utils.singleton import SingletonMeta
|
|
27
29
|
|
|
28
30
|
__all__ = [
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"TestStatus",
|
|
32
|
-
"Singleton",
|
|
31
|
+
"BaseWidget",
|
|
32
|
+
"CheckboxWidget",
|
|
33
33
|
"ConnectionData",
|
|
34
|
-
"
|
|
34
|
+
"DialogBox",
|
|
35
35
|
"DuplicatePartNumberError",
|
|
36
|
+
"DuplicateSerialNumberError",
|
|
37
|
+
"DuplicateTestStandLocationError",
|
|
36
38
|
"DuplicateTestStandNameError",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
39
|
+
"ImageComponent",
|
|
40
|
+
"ImageError",
|
|
41
|
+
"MultistepWidget",
|
|
42
|
+
"NodeInfo",
|
|
41
43
|
"NumericInputWidget",
|
|
42
|
-
"
|
|
44
|
+
"ProgressCalculator",
|
|
43
45
|
"RadiobuttonWidget",
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
46
|
+
"SingletonMeta",
|
|
47
|
+
"StepWidget",
|
|
48
|
+
"TestStatus",
|
|
49
|
+
"TextInputWidget",
|
|
50
|
+
"WidgetInfoError",
|
|
51
|
+
"machine_id",
|
|
47
52
|
]
|
|
@@ -3,15 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
from socket import gethostname
|
|
5
5
|
|
|
6
|
-
from hardpy.pytest_hardpy.utils.singleton import
|
|
6
|
+
from hardpy.pytest_hardpy.utils.singleton import SingletonMeta
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class ConnectionData(
|
|
9
|
+
class ConnectionData(metaclass=SingletonMeta):
|
|
10
10
|
"""Connection data storage."""
|
|
11
11
|
|
|
12
|
-
def __init__(self):
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
self.socket_port: int = 6525
|
|
17
|
-
self._initialized = True
|
|
12
|
+
def __init__(self) -> None:
|
|
13
|
+
self.database_url: str = "http://dev:dev@localhost:5984/"
|
|
14
|
+
self.socket_host: str = gethostname()
|
|
15
|
+
self.socket_port: int = 6525
|
|
@@ -1,5 +1,6 @@
|
|
|
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
|
+
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import base64
|
|
5
6
|
from abc import ABC, abstractmethod
|
|
@@ -9,7 +10,7 @@ from dataclasses import dataclass
|
|
|
9
10
|
from enum import Enum
|
|
10
11
|
from typing import Any, Final
|
|
11
12
|
|
|
12
|
-
from hardpy.pytest_hardpy.utils.exception import WidgetInfoError
|
|
13
|
+
from hardpy.pytest_hardpy.utils.exception import ImageError, WidgetInfoError
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class WidgetType(Enum):
|
|
@@ -20,7 +21,6 @@ class WidgetType(Enum):
|
|
|
20
21
|
NUMERIC_INPUT = "numericinput"
|
|
21
22
|
RADIOBUTTON = "radiobutton"
|
|
22
23
|
CHECKBOX = "checkbox"
|
|
23
|
-
IMAGE = "image"
|
|
24
24
|
STEP = "step"
|
|
25
25
|
MULTISTEP = "multistep"
|
|
26
26
|
|
|
@@ -28,12 +28,12 @@ class WidgetType(Enum):
|
|
|
28
28
|
class IWidget(ABC):
|
|
29
29
|
"""Dialog box widget interface."""
|
|
30
30
|
|
|
31
|
-
def __init__(self, widget_type: WidgetType):
|
|
31
|
+
def __init__(self, widget_type: WidgetType) -> None:
|
|
32
32
|
self.type: Final[str] = widget_type.value
|
|
33
33
|
self.info: dict = {}
|
|
34
34
|
|
|
35
35
|
@abstractmethod
|
|
36
|
-
def convert_data(self, input_data: str | None) -> Any | None:
|
|
36
|
+
def convert_data(self, input_data: str | None) -> Any | None: # noqa: ANN401
|
|
37
37
|
"""Get the widget data in the correct format.
|
|
38
38
|
|
|
39
39
|
Args:
|
|
@@ -48,10 +48,13 @@ class IWidget(ABC):
|
|
|
48
48
|
class BaseWidget(IWidget):
|
|
49
49
|
"""Widget info interface."""
|
|
50
50
|
|
|
51
|
-
def __init__(
|
|
52
|
-
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
widget_type: WidgetType = WidgetType.BASE,
|
|
54
|
+
) -> None:
|
|
55
|
+
super().__init__(widget_type)
|
|
53
56
|
|
|
54
|
-
def convert_data(self, input_data: str | None = None) -> bool: # noqa:
|
|
57
|
+
def convert_data(self, input_data: str | None = None) -> bool: # noqa: ARG002
|
|
55
58
|
"""Get base widget data, i.e. None.
|
|
56
59
|
|
|
57
60
|
Args:
|
|
@@ -66,7 +69,7 @@ class BaseWidget(IWidget):
|
|
|
66
69
|
class TextInputWidget(IWidget):
|
|
67
70
|
"""Text input widget."""
|
|
68
71
|
|
|
69
|
-
def __init__(self):
|
|
72
|
+
def __init__(self) -> None:
|
|
70
73
|
"""Initialize the TextInputWidget."""
|
|
71
74
|
super().__init__(WidgetType.TEXT_INPUT)
|
|
72
75
|
|
|
@@ -85,7 +88,7 @@ class TextInputWidget(IWidget):
|
|
|
85
88
|
class NumericInputWidget(IWidget):
|
|
86
89
|
"""Numeric input widget."""
|
|
87
90
|
|
|
88
|
-
def __init__(self):
|
|
91
|
+
def __init__(self) -> None:
|
|
89
92
|
"""Initialize the NumericInputWidget."""
|
|
90
93
|
super().__init__(WidgetType.NUMERIC_INPUT)
|
|
91
94
|
|
|
@@ -107,7 +110,7 @@ class NumericInputWidget(IWidget):
|
|
|
107
110
|
class RadiobuttonWidget(IWidget):
|
|
108
111
|
"""Radiobutton widget."""
|
|
109
112
|
|
|
110
|
-
def __init__(self, fields: list[str]):
|
|
113
|
+
def __init__(self, fields: list[str]) -> None:
|
|
111
114
|
"""Initialize the RadiobuttonWidget.
|
|
112
115
|
|
|
113
116
|
Args:
|
|
@@ -118,7 +121,11 @@ class RadiobuttonWidget(IWidget):
|
|
|
118
121
|
"""
|
|
119
122
|
super().__init__(WidgetType.RADIOBUTTON)
|
|
120
123
|
if not fields:
|
|
121
|
-
|
|
124
|
+
msg = "RadiobuttonWidget must have at least one field"
|
|
125
|
+
raise ValueError(msg)
|
|
126
|
+
if len(fields) != len(set(fields)):
|
|
127
|
+
msg = "RadiobuttonWidget fields must be unique"
|
|
128
|
+
raise ValueError(msg)
|
|
122
129
|
self.info["fields"] = fields
|
|
123
130
|
|
|
124
131
|
def convert_data(self, input_data: str) -> str:
|
|
@@ -136,7 +143,7 @@ class RadiobuttonWidget(IWidget):
|
|
|
136
143
|
class CheckboxWidget(IWidget):
|
|
137
144
|
"""Checkbox widget."""
|
|
138
145
|
|
|
139
|
-
def __init__(self, fields: list[str]):
|
|
146
|
+
def __init__(self, fields: list[str]) -> None:
|
|
140
147
|
"""Initialize the CheckboxWidget.
|
|
141
148
|
|
|
142
149
|
Args:
|
|
@@ -147,7 +154,11 @@ class CheckboxWidget(IWidget):
|
|
|
147
154
|
"""
|
|
148
155
|
super().__init__(WidgetType.CHECKBOX)
|
|
149
156
|
if not fields:
|
|
150
|
-
|
|
157
|
+
msg = "Checkbox must have at least one field"
|
|
158
|
+
raise ValueError(msg)
|
|
159
|
+
if len(fields) != len(set(fields)):
|
|
160
|
+
msg = "CheckboxWidget fields must be unique"
|
|
161
|
+
raise ValueError(msg)
|
|
151
162
|
self.info["fields"] = fields
|
|
152
163
|
|
|
153
164
|
def convert_data(self, input_data: str) -> list[str] | None:
|
|
@@ -165,71 +176,35 @@ class CheckboxWidget(IWidget):
|
|
|
165
176
|
return None
|
|
166
177
|
|
|
167
178
|
|
|
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
179
|
class StepWidget(IWidget):
|
|
211
180
|
"""Step widget.
|
|
212
181
|
|
|
213
182
|
Args:
|
|
214
183
|
title (str): Step title
|
|
215
184
|
text (str | None): Step text
|
|
216
|
-
|
|
185
|
+
image (ImageComponent | None): Step image
|
|
217
186
|
|
|
218
187
|
Raises:
|
|
219
188
|
WidgetInfoError: If the text or widget are not provided.
|
|
220
189
|
"""
|
|
221
190
|
|
|
222
|
-
def __init__(
|
|
191
|
+
def __init__(
|
|
192
|
+
self,
|
|
193
|
+
title: str,
|
|
194
|
+
text: str | None,
|
|
195
|
+
image: ImageComponent | None = None,
|
|
196
|
+
) -> None:
|
|
223
197
|
super().__init__(WidgetType.STEP)
|
|
224
|
-
if text is None and
|
|
225
|
-
|
|
198
|
+
if text is None and image is None:
|
|
199
|
+
msg = "Text or image must be provided"
|
|
200
|
+
raise WidgetInfoError(msg)
|
|
226
201
|
self.info["title"] = title
|
|
227
202
|
if isinstance(text, str):
|
|
228
203
|
self.info["text"] = text
|
|
229
|
-
if isinstance(
|
|
230
|
-
self.info["
|
|
204
|
+
if isinstance(image, ImageComponent):
|
|
205
|
+
self.info["image"] = image.__dict__
|
|
231
206
|
|
|
232
|
-
def convert_data(self, input_data: str) -> bool:
|
|
207
|
+
def convert_data(self, input_data: str) -> bool: # noqa: ARG002
|
|
233
208
|
"""Get the step widget data in the correct format.
|
|
234
209
|
|
|
235
210
|
Args:
|
|
@@ -244,7 +219,7 @@ class StepWidget(IWidget):
|
|
|
244
219
|
class MultistepWidget(IWidget):
|
|
245
220
|
"""Multistep widget."""
|
|
246
221
|
|
|
247
|
-
def __init__(self, steps: list[StepWidget]):
|
|
222
|
+
def __init__(self, steps: list[StepWidget]) -> None:
|
|
248
223
|
"""Initialize the MultistepWidget.
|
|
249
224
|
|
|
250
225
|
Args:
|
|
@@ -255,12 +230,19 @@ class MultistepWidget(IWidget):
|
|
|
255
230
|
"""
|
|
256
231
|
super().__init__(WidgetType.MULTISTEP)
|
|
257
232
|
if not steps:
|
|
258
|
-
|
|
233
|
+
msg = "MultistepWidget must have at least one step"
|
|
234
|
+
raise ValueError(msg)
|
|
235
|
+
title_set = set()
|
|
259
236
|
self.info["steps"] = []
|
|
260
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)
|
|
261
243
|
self.info["steps"].append(step.__dict__)
|
|
262
244
|
|
|
263
|
-
def convert_data(self, input_data: str) -> bool:
|
|
245
|
+
def convert_data(self, input_data: str) -> bool: # noqa: ARG002
|
|
264
246
|
"""Get the multistep widget data in the correct format.
|
|
265
247
|
|
|
266
248
|
Args:
|
|
@@ -272,6 +254,58 @@ class MultistepWidget(IWidget):
|
|
|
272
254
|
return True
|
|
273
255
|
|
|
274
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
|
+
|
|
275
309
|
@dataclass
|
|
276
310
|
class DialogBox:
|
|
277
311
|
"""Dialog box data.
|
|
@@ -280,6 +314,7 @@ class DialogBox:
|
|
|
280
314
|
dialog_text (str): dialog text
|
|
281
315
|
title_bar (str | None): title bar
|
|
282
316
|
widget (IWidget | None): widget info
|
|
317
|
+
image (ImageComponent | None): image
|
|
283
318
|
"""
|
|
284
319
|
|
|
285
320
|
def __init__(
|
|
@@ -287,8 +322,10 @@ class DialogBox:
|
|
|
287
322
|
dialog_text: str,
|
|
288
323
|
title_bar: str | None = None,
|
|
289
324
|
widget: IWidget | None = None,
|
|
290
|
-
|
|
325
|
+
image: ImageComponent | None = None,
|
|
326
|
+
) -> None:
|
|
291
327
|
self.widget: IWidget = BaseWidget() if widget is None else widget
|
|
328
|
+
self.image: ImageComponent | None = image
|
|
292
329
|
self.dialog_text: str = dialog_text
|
|
293
330
|
self.title_bar: str | None = title_bar
|
|
294
331
|
|
|
@@ -300,4 +337,6 @@ class DialogBox:
|
|
|
300
337
|
"""
|
|
301
338
|
dbx_dict = deepcopy(self.__dict__)
|
|
302
339
|
dbx_dict["widget"] = deepcopy(self.widget.__dict__)
|
|
340
|
+
if self.image:
|
|
341
|
+
dbx_dict["image"] = deepcopy(self.image.__dict__)
|
|
303
342
|
return dbx_dict
|
|
@@ -5,40 +5,46 @@
|
|
|
5
5
|
class HardpyError(Exception):
|
|
6
6
|
"""Base HardPy exception."""
|
|
7
7
|
|
|
8
|
-
def __init__(self, msg: str):
|
|
8
|
+
def __init__(self, msg: str) -> None:
|
|
9
9
|
super().__init__(f"HardPy error: {msg}")
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class DuplicateSerialNumberError(HardpyError):
|
|
13
13
|
"""The serial number has already been determined."""
|
|
14
14
|
|
|
15
|
-
def __init__(self):
|
|
15
|
+
def __init__(self) -> None:
|
|
16
16
|
super().__init__(self.__doc__) # type: ignore
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class DuplicatePartNumberError(HardpyError):
|
|
20
20
|
"""The part number has already been determined."""
|
|
21
21
|
|
|
22
|
-
def __init__(self):
|
|
22
|
+
def __init__(self) -> None:
|
|
23
23
|
super().__init__(self.__doc__) # type: ignore
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class DuplicateTestStandNameError(HardpyError):
|
|
27
27
|
"""The test stand name has already been determined."""
|
|
28
28
|
|
|
29
|
-
def __init__(self):
|
|
29
|
+
def __init__(self) -> None:
|
|
30
30
|
super().__init__(self.__doc__) # type: ignore
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
class
|
|
34
|
-
"""The
|
|
33
|
+
class DuplicateTestStandLocationError(HardpyError):
|
|
34
|
+
"""The test stand location has already been determined."""
|
|
35
35
|
|
|
36
|
-
def __init__(self):
|
|
36
|
+
def __init__(self) -> None:
|
|
37
37
|
super().__init__(self.__doc__) # type: ignore
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
class WidgetInfoError(HardpyError):
|
|
41
41
|
"""The widget info is not correct."""
|
|
42
42
|
|
|
43
|
-
def __init__(self, message):
|
|
43
|
+
def __init__(self, message: str) -> None:
|
|
44
|
+
super().__init__(message)
|
|
45
|
+
|
|
46
|
+
class ImageError(HardpyError):
|
|
47
|
+
"""The image info is not correct."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, message: str) -> None:
|
|
44
50
|
super().__init__(message)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from platform import node as platform_node
|
|
2
|
+
|
|
3
|
+
import machineid
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def machine_id() -> str:
|
|
7
|
+
"""Get machine id.
|
|
8
|
+
|
|
9
|
+
Returns:
|
|
10
|
+
str: id, if available, otherwise MAC address
|
|
11
|
+
"""
|
|
12
|
+
try:
|
|
13
|
+
return machineid.id()
|
|
14
|
+
except machineid.MachineIdNotFound:
|
|
15
|
+
return platform_node()
|