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.
Files changed (47) hide show
  1. hardpy/__init__.py +49 -49
  2. hardpy/cli/cli.py +8 -9
  3. hardpy/cli/template.py +6 -6
  4. hardpy/common/config.py +19 -18
  5. hardpy/hardpy_panel/api.py +9 -9
  6. hardpy/hardpy_panel/frontend/dist/asset-manifest.json +3 -3
  7. hardpy/hardpy_panel/frontend/dist/index.html +1 -1
  8. hardpy/hardpy_panel/frontend/dist/logo192.png +0 -0
  9. hardpy/hardpy_panel/frontend/dist/static/css/main.e8a862f1.css.map +1 -1
  10. hardpy/hardpy_panel/frontend/dist/static/js/main.6f09d61a.js +3 -0
  11. hardpy/hardpy_panel/frontend/dist/static/js/{main.7c954faf.js.map → main.6f09d61a.js.map} +1 -1
  12. hardpy/pytest_hardpy/db/__init__.py +4 -5
  13. hardpy/pytest_hardpy/db/base_connector.py +6 -5
  14. hardpy/pytest_hardpy/db/base_server.py +1 -1
  15. hardpy/pytest_hardpy/db/base_store.py +23 -9
  16. hardpy/pytest_hardpy/db/const.py +3 -1
  17. hardpy/pytest_hardpy/db/runstore.py +13 -15
  18. hardpy/pytest_hardpy/db/schema/__init__.py +9 -0
  19. hardpy/pytest_hardpy/db/{schema.py → schema/v1.py} +120 -79
  20. hardpy/pytest_hardpy/db/statestore.py +7 -20
  21. hardpy/pytest_hardpy/plugin.py +128 -85
  22. hardpy/pytest_hardpy/pytest_call.py +80 -32
  23. hardpy/pytest_hardpy/pytest_wrapper.py +8 -8
  24. hardpy/pytest_hardpy/reporter/__init__.py +2 -2
  25. hardpy/pytest_hardpy/reporter/base.py +32 -7
  26. hardpy/pytest_hardpy/reporter/hook_reporter.py +66 -37
  27. hardpy/pytest_hardpy/reporter/runner_reporter.py +6 -8
  28. hardpy/pytest_hardpy/result/__init__.py +2 -2
  29. hardpy/pytest_hardpy/result/couchdb_config.py +20 -16
  30. hardpy/pytest_hardpy/result/report_loader/couchdb_loader.py +2 -2
  31. hardpy/pytest_hardpy/result/report_reader/couchdb_reader.py +36 -20
  32. hardpy/pytest_hardpy/utils/__init__.py +34 -29
  33. hardpy/pytest_hardpy/utils/connection_data.py +6 -8
  34. hardpy/pytest_hardpy/utils/const.py +1 -1
  35. hardpy/pytest_hardpy/utils/dialog_box.py +105 -66
  36. hardpy/pytest_hardpy/utils/exception.py +14 -8
  37. hardpy/pytest_hardpy/utils/machineid.py +15 -0
  38. hardpy/pytest_hardpy/utils/node_info.py +45 -16
  39. hardpy/pytest_hardpy/utils/progress_calculator.py +4 -3
  40. hardpy/pytest_hardpy/utils/singleton.py +23 -16
  41. {hardpy-0.6.1.dist-info → hardpy-0.8.0.dist-info}/METADATA +26 -33
  42. {hardpy-0.6.1.dist-info → hardpy-0.8.0.dist-info}/RECORD +46 -43
  43. hardpy/hardpy_panel/frontend/dist/static/js/main.7c954faf.js +0 -3
  44. /hardpy/hardpy_panel/frontend/dist/static/js/{main.7c954faf.js.LICENSE.txt → main.6f09d61a.js.LICENSE.txt} +0 -0
  45. {hardpy-0.6.1.dist-info → hardpy-0.8.0.dist-info}/WHEEL +0 -0
  46. {hardpy-0.6.1.dist-info → hardpy-0.8.0.dist-info}/entry_points.txt +0 -0
  47. {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 typing import List, Optional
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
- from hardpy.pytest_hardpy.result.couchdb_config import CouchdbConfig
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: Optional[str]
26
- first_failed_test_id: Optional[str]
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
- raise ValueError("Start time and end time must be positive values")
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
- raise ValueError("Invalid report status")
93
+ msg = "Invalid report status"
94
+ raise ValueError(msg)
89
95
  return status
90
96
 
91
- def get_report_infos(self) -> List[ReportInfo]:
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, start_time: int, end_time: int
106
- ) -> List[ReportInfo]:
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
- raise ValueError("Start time and end time must be positive values")
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
- raise RuntimeError("Error initializing database") from exc
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 _module_name, module_info in report_doc[DF.MODULES].items():
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(self, start, end, timeframe_start, timeframe_end):
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.exception import (
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
- TextInputWidget,
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
- "NodeInfo",
30
- "ProgressCalculator",
31
- "TestStatus",
32
- "Singleton",
31
+ "BaseWidget",
32
+ "CheckboxWidget",
33
33
  "ConnectionData",
34
- "DuplicateSerialNumberError",
34
+ "DialogBox",
35
35
  "DuplicatePartNumberError",
36
+ "DuplicateSerialNumberError",
37
+ "DuplicateTestStandLocationError",
36
38
  "DuplicateTestStandNameError",
37
- "DuplicateDialogBoxError",
38
- "WidgetInfoError",
39
- "DialogBox",
40
- "TextInputWidget",
39
+ "ImageComponent",
40
+ "ImageError",
41
+ "MultistepWidget",
42
+ "NodeInfo",
41
43
  "NumericInputWidget",
42
- "CheckboxWidget",
44
+ "ProgressCalculator",
43
45
  "RadiobuttonWidget",
44
- "ImageWidget",
45
- "MultistepWidget",
46
- "StepWidget"
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 Singleton
6
+ from hardpy.pytest_hardpy.utils.singleton import SingletonMeta
7
7
 
8
8
 
9
- class ConnectionData(Singleton):
9
+ class ConnectionData(metaclass=SingletonMeta):
10
10
  """Connection data storage."""
11
11
 
12
- def __init__(self):
13
- if not self._initialized:
14
- self.database_url: str = "http://dev:dev@localhost:5984/"
15
- self.socket_host: str = gethostname()
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
@@ -4,7 +4,7 @@
4
4
  from enum import Enum
5
5
 
6
6
 
7
- class TestStatus(str, Enum): # noqa: WPS600
7
+ class TestStatus(str, Enum):
8
8
  """Pytest test status.
9
9
 
10
10
  Statuses, that can be returned by Pytest.
@@ -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__(self, widget_type: WidgetType = WidgetType.BASE):
52
- super().__init__(WidgetType.BASE)
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: WPS324
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
- raise ValueError("RadiobuttonWidget must have at least one field")
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
- raise ValueError("Checkbox must have at least one field")
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
- widget (ImageWidget | None): Step widget
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__(self, title: str, text: str | None, widget: ImageWidget | None):
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 widget is None:
225
- raise WidgetInfoError("Text or widget must be provided")
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(widget, ImageWidget):
230
- self.info["widget"] = widget.__dict__
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
- raise ValueError("MultistepWidget must have at least one step")
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 DuplicateDialogBoxError(HardpyError):
34
- """The dialog box has already been determined."""
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()