hardpy 0.10.1__py3-none-any.whl → 0.11.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 (28) hide show
  1. hardpy/__init__.py +2 -0
  2. hardpy/cli/cli.py +0 -12
  3. hardpy/cli/template.py +0 -4
  4. hardpy/common/config.py +11 -21
  5. hardpy/hardpy_panel/frontend/dist/asset-manifest.json +3 -3
  6. hardpy/hardpy_panel/frontend/dist/index.html +1 -1
  7. hardpy/hardpy_panel/frontend/dist/static/js/main.114c5914.js +3 -0
  8. hardpy/hardpy_panel/frontend/dist/static/js/main.114c5914.js.map +1 -0
  9. hardpy/pytest_hardpy/db/base_store.py +7 -2
  10. hardpy/pytest_hardpy/db/const.py +3 -0
  11. hardpy/pytest_hardpy/db/schema/v1.py +22 -0
  12. hardpy/pytest_hardpy/plugin.py +4 -12
  13. hardpy/pytest_hardpy/pytest_call.py +30 -41
  14. hardpy/pytest_hardpy/pytest_wrapper.py +7 -17
  15. hardpy/pytest_hardpy/reporter/base.py +9 -4
  16. hardpy/pytest_hardpy/reporter/hook_reporter.py +7 -0
  17. hardpy/pytest_hardpy/utils/__init__.py +2 -0
  18. hardpy/pytest_hardpy/utils/connection_data.py +0 -4
  19. hardpy/pytest_hardpy/utils/dialog_box.py +59 -6
  20. hardpy/pytest_hardpy/utils/exception.py +1 -0
  21. {hardpy-0.10.1.dist-info → hardpy-0.11.0.dist-info}/METADATA +1 -1
  22. {hardpy-0.10.1.dist-info → hardpy-0.11.0.dist-info}/RECORD +26 -26
  23. hardpy/hardpy_panel/frontend/dist/static/js/main.8a7d8f7d.js +0 -3
  24. hardpy/hardpy_panel/frontend/dist/static/js/main.8a7d8f7d.js.map +0 -1
  25. /hardpy/hardpy_panel/frontend/dist/static/js/{main.8a7d8f7d.js.LICENSE.txt → main.114c5914.js.LICENSE.txt} +0 -0
  26. {hardpy-0.10.1.dist-info → hardpy-0.11.0.dist-info}/WHEEL +0 -0
  27. {hardpy-0.10.1.dist-info → hardpy-0.11.0.dist-info}/entry_points.txt +0 -0
  28. {hardpy-0.10.1.dist-info → hardpy-0.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -36,8 +36,8 @@ class BaseStore(BaseConnector):
36
36
  """
37
37
  return glom(self._doc, key)
38
38
 
39
- def update_doc(self, key: str, value: Any) -> None: # noqa: ANN401
40
- """Update document.
39
+ def update_doc_value(self, key: str, value: Any) -> None: # noqa: ANN401
40
+ """Update document value.
41
41
 
42
42
  HardPy collecting uses a simple key without dots.
43
43
  Assign is used to update a document.
@@ -60,6 +60,11 @@ class BaseStore(BaseConnector):
60
60
  self._doc["_rev"] = self._db.get(self._doc_id)["_rev"]
61
61
  self._doc = self._db.save(self._doc)
62
62
 
63
+ def update_doc(self) -> None:
64
+ """Update current document by database."""
65
+ self._doc["_rev"] = self._db.get(self._doc_id)["_rev"]
66
+ self._doc = self._db.get(self._doc_id)
67
+
63
68
  def get_document(self) -> ModelMetaclass:
64
69
  """Get document by schema.
65
70
 
@@ -32,6 +32,9 @@ class DatabaseField(str, Enum):
32
32
  TITLE = "title"
33
33
  VISIBLE = "visible"
34
34
  IMAGE = "image"
35
+ HTML = "html"
35
36
  ID = "id"
36
37
  FONT_SIZE = "font_size"
37
38
  ALERT = "alert"
39
+ OPERATOR_DATA = "operator_data"
40
+ DIALOG = "dialog"
@@ -204,6 +204,24 @@ class TestStand(BaseModel):
204
204
  location: str | None = None
205
205
 
206
206
 
207
+ class OperatorData(BaseModel):
208
+ """Operator data from operator panel.
209
+
210
+ Example:
211
+ ```
212
+ {
213
+ "operator_data": {
214
+ "dialog": "hello",
215
+ }
216
+ }
217
+ ```
218
+ """
219
+
220
+ model_config = ConfigDict(extra="forbid")
221
+
222
+ dialog: str | None = None
223
+
224
+
207
225
  class ResultStateStore(IBaseResult):
208
226
  """Test run description.
209
227
 
@@ -218,6 +236,9 @@ class ResultStateStore(IBaseResult):
218
236
  "status": "failed",
219
237
  "name": "hardpy-stand",
220
238
  "alert": "",
239
+ "operator_data": {
240
+ "dialog": ""
241
+ },
221
242
  "dut": {
222
243
  "serial_number": "92c5a4bb-ecb0-42c5-89ac-e0caca0919fd",
223
244
  "part_number": "part_1",
@@ -305,6 +326,7 @@ class ResultStateStore(IBaseResult):
305
326
  modules: dict[str, ModuleStateStore] = {}
306
327
  operator_msg: dict = {}
307
328
  alert: str
329
+ operator_data: OperatorData
308
330
 
309
331
 
310
332
  class ResultRunStore(IBaseResult):
@@ -56,17 +56,17 @@ def pytest_addoption(parser: Parser) -> None:
56
56
  default=con_data.database_url,
57
57
  help="database url",
58
58
  )
59
+ # TODO (xorialexandrov): Remove --hardpy-sp and --hardpy-sh in HardPy major version.
60
+ # Addoptions left for compatibility with version 0.10.1 and below
59
61
  parser.addoption(
60
62
  "--hardpy-sp",
61
63
  action="store",
62
- default=con_data.socket_port,
63
- help="internal socket port",
64
+ help="DEPRECATED, UNUSED: internal socket port",
64
65
  )
65
66
  parser.addoption(
66
67
  "--hardpy-sh",
67
68
  action="store",
68
- default=con_data.socket_host,
69
- help="internal socket host",
69
+ help="DEPRECATED, UNUSED: internal socket host",
70
70
  )
71
71
  parser.addoption(
72
72
  "--hardpy-clear-database",
@@ -136,14 +136,6 @@ class HardpyPlugin:
136
136
 
137
137
  is_clear_database = config.getoption("--hardpy-clear-database")
138
138
 
139
- socket_port = config.getoption("--hardpy-sp")
140
- if socket_port:
141
- con_data.socket_port = int(socket_port) # type: ignore
142
-
143
- socket_host = config.getoption("--hardpy-sh")
144
- if socket_host:
145
- con_data.socket_host = str(socket_host) # type: ignore
146
-
147
139
  sc_address = config.getoption("--sc-address")
148
140
  if sc_address:
149
141
  con_data.sc_address = str(sc_address) # type: ignore
@@ -2,10 +2,9 @@
2
2
  # GNU General Public License v3.0 (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
3
3
  from __future__ import annotations
4
4
 
5
- import socket
6
5
  from dataclasses import dataclass
7
6
  from os import environ
8
- from select import select
7
+ from time import sleep
9
8
  from typing import Any
10
9
  from uuid import uuid4
11
10
 
@@ -19,12 +18,12 @@ from hardpy.pytest_hardpy.db import (
19
18
  )
20
19
  from hardpy.pytest_hardpy.reporter import RunnerReporter
21
20
  from hardpy.pytest_hardpy.utils import (
22
- ConnectionData,
23
21
  DialogBox,
24
22
  DuplicatePartNumberError,
25
23
  DuplicateSerialNumberError,
26
24
  DuplicateTestStandLocationError,
27
25
  DuplicateTestStandNameError,
26
+ HTMLComponent,
28
27
  ImageComponent,
29
28
  )
30
29
 
@@ -279,6 +278,7 @@ def run_dialog_box(dialog_box_data: DialogBox) -> Any: # noqa: ANN401
279
278
  If the title_bar field is missing, it is the case name.
280
279
  - widget (DialogBoxWidget | None): Widget information.
281
280
  - image (ImageComponent | None): Image information.
281
+ - html (HTMLComponent | None): HTML information.
282
282
 
283
283
  Returns:
284
284
  Any: An object containing the user's response.
@@ -312,18 +312,18 @@ def run_dialog_box(dialog_box_data: DialogBox) -> Any: # noqa: ANN401
312
312
  reporter.set_doc_value(key, dialog_box_data.to_dict(), statestore_only=True)
313
313
  reporter.update_db_by_doc()
314
314
 
315
- # get socket data
316
- input_dbx_data = _get_socket_raw_data()
315
+ input_dbx_data = _get_operator_data()
317
316
 
318
317
  _cleanup_widget(reporter, key)
319
318
  return dialog_box_data.widget.convert_data(input_dbx_data)
320
319
 
321
320
 
322
- def set_operator_message(
321
+ def set_operator_message( # noqa: PLR0913
323
322
  msg: str,
324
323
  title: str | None = None,
325
324
  block: bool = True,
326
325
  image: ImageComponent | None = None,
326
+ html: HTMLComponent | None = None,
327
327
  font_size: int = 14,
328
328
  ) -> None:
329
329
  """Set operator message.
@@ -334,7 +334,8 @@ def set_operator_message(
334
334
  Args:
335
335
  msg (str): message
336
336
  title (str | None): title
337
- image (ImageComponent | None): operator message info
337
+ image (ImageComponent | None): operator message image
338
+ html (HTMLComponent | None): operator message html page
338
339
  block (bool): if True, the function will block until the message is closed
339
340
  font_size (int): font size
340
341
  """
@@ -351,6 +352,7 @@ def set_operator_message(
351
352
  DF.TITLE: title,
352
353
  DF.VISIBLE: True,
353
354
  DF.IMAGE: image.to_dict() if image else None,
355
+ DF.HTML: html.to_dict() if html else None,
354
356
  DF.ID: str(uuid4()),
355
357
  DF.FONT_SIZE: int(font_size),
356
358
  }
@@ -358,10 +360,9 @@ def set_operator_message(
358
360
  reporter.update_db_by_doc()
359
361
 
360
362
  if block:
361
- # get socket data
362
- is_msg_visible = _get_socket_raw_data()
363
-
363
+ is_msg_visible = _get_operator_data()
364
364
  msg_data[DF.VISIBLE] = is_msg_visible
365
+
365
366
  reporter.set_doc_value(key, msg_data, statestore_only=True)
366
367
  reporter.update_db_by_doc()
367
368
 
@@ -414,37 +415,25 @@ def _get_current_test() -> CurrentTestInfo:
414
415
  return CurrentTestInfo(module_id=module_id, case_id=case_id)
415
416
 
416
417
 
417
- def _get_socket_raw_data() -> str:
418
- # create socket connection
419
- with socket.socket() as server:
420
- server.setblocking(False)
421
- server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
422
-
423
- con_data = ConnectionData()
424
- try:
425
- server.bind((con_data.socket_host, con_data.socket_port))
426
- except OSError as exc:
427
- msg = "Socket creating error"
428
- server.close()
429
- raise RuntimeError(msg) from exc
430
- server.listen(1)
431
-
432
- client = None
433
- while True:
434
- ready_to_read, _, _ = select([server], [], [], 0.5)
435
- if ready_to_read:
436
- client, _ = server.accept()
437
- break
438
-
439
- # receive data
440
- max_input_data_len = 1024
441
- socket_data = client.recv(max_input_data_len).decode("utf-8")
442
-
443
- # close connection
444
- client.close()
445
- server.close()
446
-
447
- return socket_data
418
+ def _get_operator_data() -> str:
419
+ """Get operator panel data.
420
+
421
+ Returns:
422
+ str: operator panel data
423
+ """
424
+ reporter = RunnerReporter()
425
+
426
+ data = ""
427
+ key = reporter.generate_key(DF.OPERATOR_DATA, DF.DIALOG)
428
+ while not data:
429
+ reporter.update_doc_by_db()
430
+
431
+ data = reporter.get_field(key)
432
+ if data:
433
+ reporter.set_doc_value(key, "", statestore_only=True)
434
+ break
435
+ sleep(0.1)
436
+ return data
448
437
 
449
438
 
450
439
  def _cleanup_widget(reporter: RunnerReporter, key: str) -> None:
@@ -6,9 +6,10 @@ import signal
6
6
  import subprocess
7
7
  import sys
8
8
  from platform import system
9
- from socket import socket
10
9
 
11
10
  from hardpy.common.config import ConfigManager
11
+ from hardpy.pytest_hardpy.db import DatabaseField as DF # noqa: N817
12
+ from hardpy.pytest_hardpy.reporter import RunnerReporter
12
13
 
13
14
 
14
15
  class PyTestWrapper:
@@ -16,6 +17,7 @@ class PyTestWrapper:
16
17
 
17
18
  def __init__(self) -> None:
18
19
  self._proc = None
20
+ self._reporter = RunnerReporter()
19
21
  self.python_executable = sys.executable
20
22
 
21
23
  # Make sure test structure is stored in DB
@@ -43,10 +45,6 @@ class PyTestWrapper:
43
45
  "pytest",
44
46
  "--hardpy-db-url",
45
47
  self.config.database.connection_url(),
46
- "--hardpy-sp",
47
- str(self.config.socket.port),
48
- "--hardpy-sh",
49
- self.config.socket.host,
50
48
  "--sc-address",
51
49
  self.config.stand_cloud.address,
52
50
  "--sc-connection-only"
@@ -64,10 +62,6 @@ class PyTestWrapper:
64
62
  "pytest",
65
63
  "--hardpy-db-url",
66
64
  self.config.database.connection_url(),
67
- "--hardpy-sp",
68
- str(self.config.socket.port),
69
- "--hardpy-sh",
70
- self.config.socket.host,
71
65
  "--sc-address",
72
66
  self.config.stand_cloud.address,
73
67
  "--sc-connection-only"
@@ -117,10 +111,6 @@ class PyTestWrapper:
117
111
  "--collect-only",
118
112
  "--hardpy-db-url",
119
113
  self.config.database.connection_url(),
120
- "--hardpy-sp",
121
- str(self.config.socket.port),
122
- "--hardpy-sh",
123
- self.config.socket.host,
124
114
  "--hardpy-pt",
125
115
  ]
126
116
 
@@ -144,10 +134,10 @@ class PyTestWrapper:
144
134
  bool: True if dialog box was confirmed/closed, else False
145
135
  """
146
136
  try:
147
- with socket() as client:
148
- client.connect((self.config.socket.host, self.config.socket.port))
149
- client.sendall(data.encode("utf-8"))
150
- client.close()
137
+ self._reporter.update_doc_by_db()
138
+ key = self._reporter.generate_key(DF.OPERATOR_DATA, DF.DIALOG)
139
+ self._reporter.set_doc_value(key, data, statestore_only=True)
140
+ self._reporter.update_db_by_doc()
151
141
  except Exception: # noqa: BLE001
152
142
  return False
153
143
  return True
@@ -45,19 +45,24 @@ class BaseReporter:
45
45
  msg = "Both runstore_only and statestore_only cannot be True"
46
46
  raise ValueError(msg)
47
47
  if runstore_only:
48
- self._runstore.update_doc(key, value)
48
+ self._runstore.update_doc_value(key, value)
49
49
  return
50
50
  if statestore_only:
51
- self._statestore.update_doc(key, value)
51
+ self._statestore.update_doc_value(key, value)
52
52
  return
53
- self._runstore.update_doc(key, value)
54
- self._statestore.update_doc(key, value)
53
+ self._runstore.update_doc_value(key, value)
54
+ self._statestore.update_doc_value(key, value)
55
55
 
56
56
  def update_db_by_doc(self) -> None:
57
57
  """Update database by current document."""
58
58
  self._statestore.update_db()
59
59
  self._runstore.update_db()
60
60
 
61
+ def update_doc_by_db(self) -> None:
62
+ """Update document by current database."""
63
+ self._statestore.update_doc()
64
+ self._runstore.update_doc()
65
+
61
66
  def generate_key(self, *args: Any) -> str: # noqa: ANN401
62
67
  """Generate key for database.
63
68
 
@@ -38,6 +38,7 @@ class HookReporter(BaseReporter):
38
38
  self.set_doc_value(DF.ARTIFACT, {}, runstore_only=True)
39
39
  self.set_doc_value(DF.OPERATOR_MSG, {}, statestore_only=True)
40
40
  self.set_doc_value(DF.ALERT, "", statestore_only=True)
41
+ self.set_doc_value(DF.OPERATOR_DATA, {}, statestore_only=True)
41
42
 
42
43
  test_stand_tz = self.generate_key(DF.TEST_STAND, DF.TIMEZONE)
43
44
  self.set_doc_value(test_stand_tz, str(get_localzone().key))
@@ -45,6 +46,9 @@ class HookReporter(BaseReporter):
45
46
  test_stand_id_key = self.generate_key(DF.TEST_STAND, DF.HW_ID)
46
47
  self.set_doc_value(test_stand_id_key, machine_id())
47
48
 
49
+ operator_data_key = self.generate_key(DF.OPERATOR_DATA, DF.DIALOG)
50
+ self.set_doc_value(operator_data_key, "", statestore_only=True)
51
+
48
52
  def start(self) -> None:
49
53
  """Start test."""
50
54
  self._log.debug("Starting test run.")
@@ -54,6 +58,9 @@ class HookReporter(BaseReporter):
54
58
  self.set_doc_value(DF.PROGRESS, 0, statestore_only=True)
55
59
  self.set_doc_value(DF.ALERT, "", statestore_only=True)
56
60
 
61
+ operator_data_key = self.generate_key(DF.OPERATOR_DATA, DF.DIALOG)
62
+ self.set_doc_value(operator_data_key, "", statestore_only=True)
63
+
57
64
  def finish(self, status: TestStatus) -> None:
58
65
  """Finish test.
59
66
 
@@ -7,6 +7,7 @@ from hardpy.pytest_hardpy.utils.dialog_box import (
7
7
  BaseWidget,
8
8
  CheckboxWidget,
9
9
  DialogBox,
10
+ HTMLComponent,
10
11
  ImageComponent,
11
12
  MultistepWidget,
12
13
  NumericInputWidget,
@@ -36,6 +37,7 @@ __all__ = [
36
37
  "DuplicateSerialNumberError",
37
38
  "DuplicateTestStandLocationError",
38
39
  "DuplicateTestStandNameError",
40
+ "HTMLComponent",
39
41
  "ImageComponent",
40
42
  "ImageError",
41
43
  "MultistepWidget",
@@ -1,8 +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
3
 
4
- from socket import gethostname
5
-
6
4
  from hardpy.pytest_hardpy.utils.singleton import SingletonMeta
7
5
 
8
6
 
@@ -11,7 +9,5 @@ class ConnectionData(metaclass=SingletonMeta):
11
9
 
12
10
  def __init__(self) -> None:
13
11
  self.database_url: str = "http://dev:dev@localhost:5984/"
14
- self.socket_host: str = gethostname()
15
- self.socket_port: int = 6525
16
12
  self.sc_address: str = ""
17
13
  self.sc_connection_only: bool = False
@@ -184,6 +184,7 @@ class StepWidget(IWidget):
184
184
  title (str): Step title
185
185
  text (str | None): Step text
186
186
  image (ImageComponent | None): Step image
187
+ html (HTMLComponent | None): Step html
187
188
 
188
189
  Raises:
189
190
  WidgetInfoError: If the text or widget are not provided.
@@ -194,16 +195,19 @@ class StepWidget(IWidget):
194
195
  title: str,
195
196
  text: str | None,
196
197
  image: ImageComponent | None = None,
198
+ html: HTMLComponent | None = None,
197
199
  ) -> None:
198
200
  super().__init__(WidgetType.STEP)
199
- if text is None and image is None:
200
- msg = "Text or image must be provided"
201
+ if text is None and image is None and html is None:
202
+ msg = "Text, image and html must be provided"
201
203
  raise WidgetInfoError(msg)
202
204
  self.info["title"] = title
203
205
  if isinstance(text, str):
204
206
  self.info["text"] = text
205
207
  if isinstance(image, ImageComponent):
206
208
  self.info["image"] = image.__dict__
209
+ if isinstance(html, HTMLComponent):
210
+ self.info["html"] = html.__dict__
207
211
 
208
212
  def convert_data(self, input_data: str) -> bool: # noqa: ARG002
209
213
  """Get the step widget data in the correct format.
@@ -264,7 +268,7 @@ class ImageComponent:
264
268
  width: int = 100,
265
269
  border: int = 0,
266
270
  ) -> None:
267
- """Validate the image fields and defines the base64 if it does not exist.
271
+ """Initialize the image component.
268
272
 
269
273
  Args:
270
274
  address (str): image address
@@ -272,7 +276,7 @@ class ImageComponent:
272
276
  border (int): image border
273
277
 
274
278
  Raises:
275
- ImageError: If both address and base64data are specified.
279
+ ImageError: If both address and base64data are specified
276
280
  """
277
281
  if width < 1:
278
282
  msg = "Width must be positive"
@@ -297,7 +301,7 @@ class ImageComponent:
297
301
  """Convert ImageComponent to dictionary.
298
302
 
299
303
  Returns:
300
- dict: ImageComponent dictionary.
304
+ dict: ImageComponent dictionary
301
305
  """
302
306
  return {
303
307
  "address": self.address,
@@ -307,6 +311,51 @@ class ImageComponent:
307
311
  }
308
312
 
309
313
 
314
+ class HTMLComponent:
315
+ """HTML component."""
316
+
317
+ def __init__(
318
+ self,
319
+ html: str,
320
+ width: int = 100,
321
+ border: int = 0,
322
+ is_raw_html: bool = True,
323
+ ) -> None:
324
+ """Initialize the HTML component.
325
+
326
+ Args:
327
+ html (str): html string or URL
328
+ width (int): html component width
329
+ border (int): html component border
330
+ is_raw_html (bool): True if the html code is raw, else False
331
+ """
332
+ if width < 1:
333
+ msg = "Width must be positive"
334
+ raise WidgetInfoError(msg)
335
+
336
+ if border < 0:
337
+ msg = "Border must be non-negative"
338
+ raise WidgetInfoError(msg)
339
+
340
+ self.code_or_url = html
341
+ self.width = width
342
+ self.border = border
343
+ self.is_raw_html = is_raw_html
344
+
345
+ def to_dict(self) -> dict:
346
+ """Convert HtmlComponent to dictionary.
347
+
348
+ Returns:
349
+ dict: HtmlComponent dictionary.
350
+ """
351
+ return {
352
+ "code_or_url": self.code_or_url,
353
+ "width": self.width,
354
+ "border": self.border,
355
+ "is_raw_html": self.is_raw_html,
356
+ }
357
+
358
+
310
359
  @dataclass
311
360
  class DialogBox:
312
361
  """Dialog box data.
@@ -319,16 +368,18 @@ class DialogBox:
319
368
  font_size (int): font size
320
369
  """
321
370
 
322
- def __init__(
371
+ def __init__( # noqa: PLR0913
323
372
  self,
324
373
  dialog_text: str,
325
374
  title_bar: str | None = None,
326
375
  widget: IWidget | None = None,
327
376
  image: ImageComponent | None = None,
377
+ html: HTMLComponent | None = None,
328
378
  font_size: int = 14,
329
379
  ) -> None:
330
380
  self.widget: IWidget = BaseWidget() if widget is None else widget
331
381
  self.image: ImageComponent | None = image
382
+ self.html: HTMLComponent | None = html
332
383
  self.dialog_text: str = dialog_text
333
384
  self.title_bar: str | None = title_bar
334
385
  self.visible: bool = True
@@ -349,4 +400,6 @@ class DialogBox:
349
400
  dbx_dict["widget"] = deepcopy(self.widget.__dict__)
350
401
  if self.image:
351
402
  dbx_dict["image"] = deepcopy(self.image.__dict__)
403
+ if self.html:
404
+ dbx_dict["html"] = deepcopy(self.html.__dict__)
352
405
  return dbx_dict
@@ -43,6 +43,7 @@ class WidgetInfoError(HardpyError):
43
43
  def __init__(self, message: str) -> None:
44
44
  super().__init__(message)
45
45
 
46
+
46
47
  class ImageError(HardpyError):
47
48
  """The image info is not correct."""
48
49
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hardpy
3
- Version: 0.10.1
3
+ Version: 0.11.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/