hardpy 0.6.1__py3-none-any.whl → 0.7.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 (46) hide show
  1. hardpy/__init__.py +28 -26
  2. hardpy/cli/cli.py +8 -8
  3. hardpy/cli/template.py +6 -6
  4. hardpy/common/config.py +15 -14
  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/static/js/main.942e57d4.js +3 -0
  9. hardpy/hardpy_panel/frontend/dist/static/js/main.942e57d4.js.map +1 -0
  10. hardpy/pytest_hardpy/db/__init__.py +3 -4
  11. hardpy/pytest_hardpy/db/base_connector.py +6 -5
  12. hardpy/pytest_hardpy/db/base_server.py +1 -1
  13. hardpy/pytest_hardpy/db/base_store.py +14 -9
  14. hardpy/pytest_hardpy/db/const.py +3 -1
  15. hardpy/pytest_hardpy/db/runstore.py +13 -15
  16. hardpy/pytest_hardpy/db/schema/__init__.py +9 -0
  17. hardpy/pytest_hardpy/db/{schema.py → schema/v1.py} +120 -79
  18. hardpy/pytest_hardpy/db/statestore.py +8 -10
  19. hardpy/pytest_hardpy/plugin.py +103 -48
  20. hardpy/pytest_hardpy/pytest_call.py +75 -30
  21. hardpy/pytest_hardpy/pytest_wrapper.py +8 -7
  22. hardpy/pytest_hardpy/reporter/__init__.py +1 -1
  23. hardpy/pytest_hardpy/reporter/base.py +32 -7
  24. hardpy/pytest_hardpy/reporter/hook_reporter.py +65 -37
  25. hardpy/pytest_hardpy/reporter/runner_reporter.py +6 -8
  26. hardpy/pytest_hardpy/result/__init__.py +1 -1
  27. hardpy/pytest_hardpy/result/couchdb_config.py +20 -16
  28. hardpy/pytest_hardpy/result/report_loader/couchdb_loader.py +2 -2
  29. hardpy/pytest_hardpy/result/report_reader/couchdb_reader.py +36 -20
  30. hardpy/pytest_hardpy/utils/__init__.py +20 -19
  31. hardpy/pytest_hardpy/utils/connection_data.py +6 -8
  32. hardpy/pytest_hardpy/utils/const.py +1 -1
  33. hardpy/pytest_hardpy/utils/dialog_box.py +34 -22
  34. hardpy/pytest_hardpy/utils/exception.py +8 -8
  35. hardpy/pytest_hardpy/utils/machineid.py +15 -0
  36. hardpy/pytest_hardpy/utils/node_info.py +45 -16
  37. hardpy/pytest_hardpy/utils/progress_calculator.py +4 -3
  38. hardpy/pytest_hardpy/utils/singleton.py +23 -16
  39. {hardpy-0.6.1.dist-info → hardpy-0.7.0.dist-info}/METADATA +19 -28
  40. {hardpy-0.6.1.dist-info → hardpy-0.7.0.dist-info}/RECORD +44 -42
  41. hardpy/hardpy_panel/frontend/dist/static/js/main.7c954faf.js +0 -3
  42. hardpy/hardpy_panel/frontend/dist/static/js/main.7c954faf.js.map +0 -1
  43. /hardpy/hardpy_panel/frontend/dist/static/js/{main.7c954faf.js.LICENSE.txt → main.942e57d4.js.LICENSE.txt} +0 -0
  44. {hardpy-0.6.1.dist-info → hardpy-0.7.0.dist-info}/WHEEL +0 -0
  45. {hardpy-0.6.1.dist-info → hardpy-0.7.0.dist-info}/entry_points.txt +0 -0
  46. {hardpy-0.6.1.dist-info → hardpy-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,40 +1,40 @@
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 (
17
- DialogBox,
18
- TextInputWidget,
19
- NumericInputWidget,
20
7
  CheckboxWidget,
21
- RadiobuttonWidget,
8
+ DialogBox,
22
9
  ImageWidget,
23
10
  MultistepWidget,
11
+ NumericInputWidget,
12
+ RadiobuttonWidget,
24
13
  StepWidget,
14
+ TextInputWidget,
25
15
  )
26
-
16
+ from hardpy.pytest_hardpy.utils.exception import (
17
+ DuplicatePartNumberError,
18
+ DuplicateSerialNumberError,
19
+ DuplicateTestStandLocationError,
20
+ DuplicateTestStandNameError,
21
+ WidgetInfoError,
22
+ )
23
+ from hardpy.pytest_hardpy.utils.machineid import machine_id
24
+ from hardpy.pytest_hardpy.utils.node_info import NodeInfo
25
+ from hardpy.pytest_hardpy.utils.progress_calculator import ProgressCalculator
26
+ from hardpy.pytest_hardpy.utils.singleton import SingletonMeta
27
27
 
28
28
  __all__ = [
29
29
  "NodeInfo",
30
30
  "ProgressCalculator",
31
31
  "TestStatus",
32
- "Singleton",
32
+ "SingletonMeta",
33
33
  "ConnectionData",
34
34
  "DuplicateSerialNumberError",
35
35
  "DuplicatePartNumberError",
36
+ "DuplicateTestStandLocationError",
36
37
  "DuplicateTestStandNameError",
37
- "DuplicateDialogBoxError",
38
38
  "WidgetInfoError",
39
39
  "DialogBox",
40
40
  "TextInputWidget",
@@ -43,5 +43,6 @@ __all__ = [
43
43
  "RadiobuttonWidget",
44
44
  "ImageWidget",
45
45
  "MultistepWidget",
46
- "StepWidget"
46
+ "StepWidget",
47
+ "machine_id",
47
48
  ]
@@ -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
@@ -28,12 +29,12 @@ class WidgetType(Enum):
28
29
  class IWidget(ABC):
29
30
  """Dialog box widget interface."""
30
31
 
31
- def __init__(self, widget_type: WidgetType):
32
+ def __init__(self, widget_type: WidgetType) -> None:
32
33
  self.type: Final[str] = widget_type.value
33
34
  self.info: dict = {}
34
35
 
35
36
  @abstractmethod
36
- def convert_data(self, input_data: str | None) -> Any | None:
37
+ def convert_data(self, input_data: str | None) -> Any | None: # noqa: ANN401
37
38
  """Get the widget data in the correct format.
38
39
 
39
40
  Args:
@@ -48,10 +49,10 @@ class IWidget(ABC):
48
49
  class BaseWidget(IWidget):
49
50
  """Widget info interface."""
50
51
 
51
- def __init__(self, widget_type: WidgetType = WidgetType.BASE):
52
+ def __init__(self, widget_type: WidgetType = WidgetType.BASE) -> None: # noqa: ARG002
52
53
  super().__init__(WidgetType.BASE)
53
54
 
54
- def convert_data(self, input_data: str | None = None) -> bool: # noqa: WPS324
55
+ def convert_data(self, input_data: str | None = None) -> bool: # noqa: ARG002
55
56
  """Get base widget data, i.e. None.
56
57
 
57
58
  Args:
@@ -66,7 +67,7 @@ class BaseWidget(IWidget):
66
67
  class TextInputWidget(IWidget):
67
68
  """Text input widget."""
68
69
 
69
- def __init__(self):
70
+ def __init__(self) -> None:
70
71
  """Initialize the TextInputWidget."""
71
72
  super().__init__(WidgetType.TEXT_INPUT)
72
73
 
@@ -85,7 +86,7 @@ class TextInputWidget(IWidget):
85
86
  class NumericInputWidget(IWidget):
86
87
  """Numeric input widget."""
87
88
 
88
- def __init__(self):
89
+ def __init__(self) -> None:
89
90
  """Initialize the NumericInputWidget."""
90
91
  super().__init__(WidgetType.NUMERIC_INPUT)
91
92
 
@@ -107,7 +108,7 @@ class NumericInputWidget(IWidget):
107
108
  class RadiobuttonWidget(IWidget):
108
109
  """Radiobutton widget."""
109
110
 
110
- def __init__(self, fields: list[str]):
111
+ def __init__(self, fields: list[str]) -> None:
111
112
  """Initialize the RadiobuttonWidget.
112
113
 
113
114
  Args:
@@ -118,7 +119,8 @@ class RadiobuttonWidget(IWidget):
118
119
  """
119
120
  super().__init__(WidgetType.RADIOBUTTON)
120
121
  if not fields:
121
- raise ValueError("RadiobuttonWidget must have at least one field")
122
+ msg = "RadiobuttonWidget must have at least one field"
123
+ raise ValueError(msg)
122
124
  self.info["fields"] = fields
123
125
 
124
126
  def convert_data(self, input_data: str) -> str:
@@ -136,7 +138,7 @@ class RadiobuttonWidget(IWidget):
136
138
  class CheckboxWidget(IWidget):
137
139
  """Checkbox widget."""
138
140
 
139
- def __init__(self, fields: list[str]):
141
+ def __init__(self, fields: list[str]) -> None:
140
142
  """Initialize the CheckboxWidget.
141
143
 
142
144
  Args:
@@ -147,7 +149,8 @@ class CheckboxWidget(IWidget):
147
149
  """
148
150
  super().__init__(WidgetType.CHECKBOX)
149
151
  if not fields:
150
- raise ValueError("Checkbox must have at least one field")
152
+ msg = "Checkbox must have at least one field"
153
+ raise ValueError(msg)
151
154
  self.info["fields"] = fields
152
155
 
153
156
  def convert_data(self, input_data: str) -> list[str] | None:
@@ -168,7 +171,7 @@ class CheckboxWidget(IWidget):
168
171
  class ImageWidget(IWidget):
169
172
  """Image widget."""
170
173
 
171
- def __init__(self, address: str, format: str = "image", width: int = 100):
174
+ def __init__(self, address: str, format: str = "image", width: int = 100) -> None: # noqa: A002
172
175
  """Validate the image fields and defines the base64 if it does not exist.
173
176
 
174
177
  Args:
@@ -182,20 +185,22 @@ class ImageWidget(IWidget):
182
185
  super().__init__(WidgetType.IMAGE)
183
186
 
184
187
  if width < 1:
185
- raise WidgetInfoError("Width must be positive")
188
+ msg = "Width must be positive"
189
+ raise WidgetInfoError(msg)
186
190
 
187
191
  self.info["address"] = address
188
192
  self.info["format"] = format
189
193
  self.info["width"] = width
190
194
 
191
195
  try:
192
- with open(address, "rb") as file:
196
+ with open(address, "rb") as file: # noqa: PTH123
193
197
  file_data = file.read()
194
198
  except FileNotFoundError:
195
- raise WidgetInfoError("The image address is invalid")
199
+ msg = "The image address is invalid"
200
+ raise WidgetInfoError(msg) # noqa: B904
196
201
  self.info["base64"] = base64.b64encode(file_data).decode("utf-8")
197
202
 
198
- def convert_data(self, input_data: str | None = None) -> bool:
203
+ def convert_data(self, input_data: str | None = None) -> bool: # noqa: ARG002
199
204
  """Get the image widget data, i.e. None.
200
205
 
201
206
  Args:
@@ -219,17 +224,23 @@ class StepWidget(IWidget):
219
224
  WidgetInfoError: If the text or widget are not provided.
220
225
  """
221
226
 
222
- def __init__(self, title: str, text: str | None, widget: ImageWidget | None):
227
+ def __init__(
228
+ self,
229
+ title: str,
230
+ text: str | None,
231
+ widget: ImageWidget | None,
232
+ ) -> None:
223
233
  super().__init__(WidgetType.STEP)
224
234
  if text is None and widget is None:
225
- raise WidgetInfoError("Text or widget must be provided")
235
+ msg = "Text or widget must be provided"
236
+ raise WidgetInfoError(msg)
226
237
  self.info["title"] = title
227
238
  if isinstance(text, str):
228
239
  self.info["text"] = text
229
240
  if isinstance(widget, ImageWidget):
230
241
  self.info["widget"] = widget.__dict__
231
242
 
232
- def convert_data(self, input_data: str) -> bool:
243
+ def convert_data(self, input_data: str) -> bool: # noqa: ARG002
233
244
  """Get the step widget data in the correct format.
234
245
 
235
246
  Args:
@@ -244,7 +255,7 @@ class StepWidget(IWidget):
244
255
  class MultistepWidget(IWidget):
245
256
  """Multistep widget."""
246
257
 
247
- def __init__(self, steps: list[StepWidget]):
258
+ def __init__(self, steps: list[StepWidget]) -> None:
248
259
  """Initialize the MultistepWidget.
249
260
 
250
261
  Args:
@@ -255,12 +266,13 @@ class MultistepWidget(IWidget):
255
266
  """
256
267
  super().__init__(WidgetType.MULTISTEP)
257
268
  if not steps:
258
- raise ValueError("MultistepWidget must have at least one step")
269
+ msg = "MultistepWidget must have at least one step"
270
+ raise ValueError(msg)
259
271
  self.info["steps"] = []
260
272
  for step in steps:
261
273
  self.info["steps"].append(step.__dict__)
262
274
 
263
- def convert_data(self, input_data: str) -> bool:
275
+ def convert_data(self, input_data: str) -> bool: # noqa: ARG002
264
276
  """Get the multistep widget data in the correct format.
265
277
 
266
278
  Args:
@@ -287,7 +299,7 @@ class DialogBox:
287
299
  dialog_text: str,
288
300
  title_bar: str | None = None,
289
301
  widget: IWidget | None = None,
290
- ):
302
+ ) -> None:
291
303
  self.widget: IWidget = BaseWidget() if widget is None else widget
292
304
  self.dialog_text: str = dialog_text
293
305
  self.title_bar: str | None = title_bar
@@ -5,40 +5,40 @@
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
44
  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()
@@ -1,12 +1,14 @@
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 re
5
6
  from logging import getLogger
6
7
  from pathlib import Path
7
- from typing import NamedTuple
8
+ from typing import TYPE_CHECKING, NamedTuple
8
9
 
9
- from pytest import Item, Mark
10
+ if TYPE_CHECKING:
11
+ from pytest import Item, Mark
10
12
 
11
13
 
12
14
  class TestDependencyInfo(NamedTuple):
@@ -22,7 +24,7 @@ class TestDependencyInfo(NamedTuple):
22
24
  class NodeInfo:
23
25
  """Test node info."""
24
26
 
25
- def __init__(self, item: Item):
27
+ def __init__(self, item: Item) -> None:
26
28
  self._item = item
27
29
  self._log = getLogger(__name__)
28
30
 
@@ -31,19 +33,21 @@ class NodeInfo:
31
33
  "case_name",
32
34
  )
33
35
  self._module_name = self._get_human_name(
34
- item.parent.own_markers,
36
+ item.parent.own_markers, # type: ignore
35
37
  "module_name",
36
38
  )
37
39
 
38
40
  self._dependency = self._get_dependency_info(
39
- item.own_markers + item.parent.own_markers
41
+ item.own_markers + item.parent.own_markers, # type: ignore
40
42
  )
41
43
 
42
- self._module_id = Path(item.parent.nodeid).stem
44
+ self._attempt = self._get_attempt(item.own_markers)
45
+
46
+ self._module_id = Path(item.parent.nodeid).stem # type: ignore
43
47
  self._case_id = item.name
44
48
 
45
49
  @property
46
- def module_id(self):
50
+ def module_id(self) -> str:
47
51
  """Get module id.
48
52
 
49
53
  Returns:
@@ -52,7 +56,7 @@ class NodeInfo:
52
56
  return self._module_id
53
57
 
54
58
  @property
55
- def case_id(self):
59
+ def case_id(self) -> str:
56
60
  """Get case id.
57
61
 
58
62
  Returns:
@@ -61,7 +65,7 @@ class NodeInfo:
61
65
  return self._case_id
62
66
 
63
67
  @property
64
- def module_name(self):
68
+ def module_name(self) -> str:
65
69
  """Get module name.
66
70
 
67
71
  Returns:
@@ -70,7 +74,7 @@ class NodeInfo:
70
74
  return self._module_name
71
75
 
72
76
  @property
73
- def case_name(self):
77
+ def case_name(self) -> str:
74
78
  """Get case name.
75
79
 
76
80
  Returns:
@@ -79,7 +83,7 @@ class NodeInfo:
79
83
  return self._case_name
80
84
 
81
85
  @property
82
- def dependency(self):
86
+ def dependency(self) -> TestDependencyInfo | str:
83
87
  """Get dependency information.
84
88
 
85
89
  Returns:
@@ -87,6 +91,15 @@ class NodeInfo:
87
91
  """
88
92
  return self._dependency
89
93
 
94
+ @property
95
+ def attempt(self) -> int:
96
+ """Get attempt.
97
+
98
+ Returns:
99
+ int: attempt number
100
+ """
101
+ return self._attempt
102
+
90
103
  def _get_human_name(self, markers: list[Mark], marker_name: str) -> str:
91
104
  """Get human name from markers.
92
105
 
@@ -98,10 +111,8 @@ class NodeInfo:
98
111
  str: human name by user
99
112
  """
100
113
  for marker in markers:
101
- if marker.name == marker_name:
102
- if marker.args:
103
- return marker.args[0]
104
-
114
+ if marker.name == marker_name and marker.args:
115
+ return marker.args[0]
105
116
  return ""
106
117
 
107
118
  def _get_dependency_info(self, markers: list[Mark]) -> TestDependencyInfo | str:
@@ -118,8 +129,26 @@ class NodeInfo:
118
129
  dependency_data = re.search(r"(\w+)::(\w+)", dependency_value)
119
130
  if dependency_data:
120
131
  return TestDependencyInfo(*dependency_data.groups())
121
- elif re.search(r"^\w+$", dependency_value):
132
+ elif re.search(r"^\w+$", dependency_value): # noqa: RET505
122
133
  return TestDependencyInfo(dependency_value, None)
123
134
  elif dependency_data is None and dependency_value == "":
124
135
  return ""
125
136
  raise ValueError
137
+
138
+ def _get_attempt(self, markers: list[Mark]) -> int:
139
+ """Get the number of attempts.
140
+
141
+ Args:
142
+ markers (list[Mark]): item markers list
143
+
144
+ Returns:
145
+ int: number of attempts
146
+ """
147
+ attempt: int = 1
148
+ for marker in markers:
149
+ if marker.name == "attempt" and marker.args:
150
+ attempt = marker.args[0]
151
+ if not isinstance(attempt, int) or attempt < 1:
152
+ msg = "The 'attempt' marker value must be a positive integer."
153
+ raise ValueError(msg)
154
+ return attempt
@@ -7,12 +7,12 @@ from logging import getLogger
7
7
  class ProgressCalculator:
8
8
  """Test run progress calculator."""
9
9
 
10
- def __init__(self):
10
+ def __init__(self) -> None:
11
11
  self._progress_nodeids: set[str] = set()
12
12
  self._tests_amount: int = 1
13
13
  self._log = getLogger(__name__)
14
14
 
15
- def set_test_amount(self, amount: int):
15
+ def set_test_amount(self, amount: int) -> None:
16
16
  """Set test amount.
17
17
 
18
18
  Raises:
@@ -22,7 +22,8 @@ class ProgressCalculator:
22
22
  amount (int): test amount
23
23
  """
24
24
  if amount <= 0:
25
- raise ValueError("Test amount must be greater than 0.")
25
+ msg = "Test amount must be greater than 0."
26
+ raise ValueError(msg)
26
27
  self._tests_amount = amount
27
28
 
28
29
  def calculate(self, nodeid: str) -> int:
@@ -1,23 +1,30 @@
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
- class Singleton:
5
- """Singleton class.
5
+ from typing import Generic, TypeVar
6
6
 
7
- In the child class must be used constructor of type:
7
+ T = TypeVar("T")
8
8
 
9
- def __init__(self):
10
- if not self._initialized:
11
- ...
12
- your code
13
- ...
14
- self._initialized = True
15
- """
16
9
 
17
- _instance = None
18
- _initialized = False
10
+ class SingletonMeta(type, Generic[T]): # noqa: D101
11
+ _instances: dict[SingletonMeta[T], T] = {} # noqa: RUF012
19
12
 
20
- def __new__(cls, *args, **kwargs): # noqa: D102
21
- if not cls._instance:
22
- cls._instance = super().__new__(cls, *args, **kwargs)
23
- return cls._instance
13
+ def __call__(cls, *args, **kwargs) -> T: # noqa: ANN002, ANN003
14
+ """Magic method to create an instance of the class.
15
+
16
+ Args:
17
+ *args: Variable length argument list
18
+ **kwargs: Arbitrary keyword arguments
19
+ Returns:
20
+ object: An instance of the class
21
+ """
22
+ if cls not in cls._instances:
23
+ cls._instances[cls] = super().__call__(
24
+ *args,
25
+ **kwargs,
26
+ )
27
+ return cls._instances[cls]
28
+
29
+ def __instancecheck__(cls, instance: object) -> bool:
30
+ return cls.__subclasscheck__(type(instance))
@@ -1,17 +1,17 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hardpy
3
- Version: 0.6.1
3
+ Version: 0.7.0
4
4
  Summary: HardPy library for device testing
5
5
  Project-URL: Homepage, https://github.com/everypinio/hardpy/
6
6
  Project-URL: Documentation, https://everypinio.github.io/hardpy/
7
7
  Project-URL: Repository, https://github.com/everypinio/hardpy/
8
8
  Project-URL: Changelog, https://everypinio.github.io/hardpy/changelog/
9
- Author: Everypin
9
+ Author-email: Everypin <info@everypin.io>
10
+ License-Expression: GPL-3.0-or-later
10
11
  License-File: LICENSE
11
12
  Classifier: Development Status :: 4 - Beta
12
13
  Classifier: Framework :: Pytest
13
14
  Classifier: Intended Audience :: Developers
14
- Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
15
15
  Classifier: Operating System :: OS Independent
16
16
  Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -22,10 +22,11 @@ Classifier: Topic :: Software Development :: Libraries
22
22
  Classifier: Topic :: Software Development :: Testing
23
23
  Classifier: Topic :: Software Development :: Testing :: Acceptance
24
24
  Classifier: Topic :: Utilities
25
- Requires-Python: <4,>=3.10
25
+ Requires-Python: >=3.10
26
26
  Requires-Dist: fastapi>=0.100.1
27
27
  Requires-Dist: glom>=23.3.0
28
28
  Requires-Dist: natsort>=8.4.0
29
+ Requires-Dist: py-machineid~=0.6.0
29
30
  Requires-Dist: pycouchdb>=1.14.2
30
31
  Requires-Dist: pydantic>=2.4.0
31
32
  Requires-Dist: pytest<9,>=7
@@ -35,30 +36,27 @@ Requires-Dist: uvicorn>=0.23.2
35
36
  Provides-Extra: build
36
37
  Requires-Dist: build==1.0.3; extra == 'build'
37
38
  Provides-Extra: dev
38
- Requires-Dist: black; extra == 'dev'
39
- Requires-Dist: mypy; extra == 'dev'
40
- Requires-Dist: wemake-python-styleguide; extra == 'dev'
39
+ Requires-Dist: mypy>=1.11.0; extra == 'dev'
40
+ Requires-Dist: ruff>=0.5.6; extra == 'dev'
41
+ Requires-Dist: wemake-python-styleguide>=0.19.2; extra == 'dev'
41
42
  Description-Content-Type: text/markdown
42
43
 
43
44
  <h1 align="center">
44
- <img src="https://everypinio.github.io/hardpy/img/logo256.png" alt="HardPy" style="width:200px;">
45
- </h1>
46
-
47
- <h1 align="center">
48
- <b>HardPy</b>
45
+ <img src="https://raw.githubusercontent.com/everypinio/hardpy/main/docs/img/logo256.png" alt="HardPy" style="width:150px;">
49
46
  </h1>
50
47
 
51
48
  <p align="center">
52
49
  HardPy is a python library for creating a test bench for devices.
53
50
  </p>
54
51
 
55
- ---
56
-
57
- **Documentation**: <a href=https://everypinio.github.io/hardpy/ target="_blank">https://everypinio.github.io/hardpy/</a>
52
+ <div align="center">
58
53
 
59
- **Source Code**: <a href=https://github.com/everypinio/hardpy target="_blank">https://github.com/everypinio/hardpy</a>
54
+ [![PyPI version](https://badge.fury.io/py/hardpy.svg)](https://badge.fury.io/py/hardpy)
55
+ ![python versions](https://img.shields.io/pypi/pyversions/hardpy.svg)
56
+ [![pytest versions](https://img.shields.io/badge/pytest-%3E%3D7.0%20%3C9.0-blue)](https://docs.pytest.org/en/latest/)
57
+ [![Documentation](https://img.shields.io/badge/Documentation%20-Overview%20-%20%23007ec6)](https://everypinio.github.io/hardpy/)
60
58
 
61
- **PyPi**: <a href=https://pypi.org/project/hardpy/ target="_blank">https://pypi.org/project/hardpy/</a>
59
+ </div>
62
60
 
63
61
  ---
64
62
 
@@ -70,6 +68,10 @@ HardPy allows you to:
70
68
  * Use a browser to view, start, stop, and interact with tests;
71
69
  * Store test results in the [CouchDB](https://couchdb.apache.org/) database.
72
70
 
71
+ <h1 align="center">
72
+ <img src="https://raw.githubusercontent.com/everypinio/hardpy/main/docs/img/hardpy_panel.gif" alt="hardpy panel" style="width:550px;">
73
+ </h1>
74
+
73
75
  ## To Install
74
76
 
75
77
  ```bash
@@ -92,21 +94,10 @@ docker compose up -d
92
94
  hardpy run
93
95
  ```
94
96
  4. View operator panel in browser: http://localhost:8000/
95
-
96
- <h1 align="center">
97
- <img src="https://everypinio.github.io/hardpy/img/hardpy_operator_panel_hello_hardpy.png"
98
- alt="hardpy operator panel" style="width:600px;">
99
- </h1>
100
-
101
97
  5. View the latest test report: http://localhost:5984/_utils
102
98
 
103
99
  Login and password: **dev**, database - **runstore**, document - **current**.
104
100
 
105
- <h1 align="center">
106
- <img src="https://everypinio.github.io/hardpy/img/runstore_hello_hardpy.png"
107
- alt="hardpy runstore" style="width:500px;">
108
- </h1>
109
-
110
101
  ## Examples
111
102
 
112
103
  Find more examples of using the **HardPy** in the [examples](https://github.com/everypinio/hardpy/tree/main/examples) folder.