hardpy 0.10.1__py3-none-any.whl → 0.11.1__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 (35) hide show
  1. hardpy/__init__.py +10 -2
  2. hardpy/cli/cli.py +91 -13
  3. hardpy/cli/template.py +0 -4
  4. hardpy/common/config.py +17 -21
  5. hardpy/common/stand_cloud/__init__.py +2 -1
  6. hardpy/common/stand_cloud/connector.py +32 -38
  7. hardpy/hardpy_panel/api.py +24 -2
  8. hardpy/hardpy_panel/frontend/dist/asset-manifest.json +3 -3
  9. hardpy/hardpy_panel/frontend/dist/index.html +1 -1
  10. hardpy/hardpy_panel/frontend/dist/static/js/main.fb8b84a3.js +3 -0
  11. hardpy/hardpy_panel/frontend/dist/static/js/main.fb8b84a3.js.map +1 -0
  12. hardpy/pytest_hardpy/db/base_store.py +7 -2
  13. hardpy/pytest_hardpy/db/const.py +3 -0
  14. hardpy/pytest_hardpy/db/schema/v1.py +22 -0
  15. hardpy/pytest_hardpy/plugin.py +27 -20
  16. hardpy/pytest_hardpy/pytest_call.py +30 -41
  17. hardpy/pytest_hardpy/pytest_wrapper.py +21 -17
  18. hardpy/pytest_hardpy/reporter/base.py +9 -4
  19. hardpy/pytest_hardpy/reporter/hook_reporter.py +7 -0
  20. hardpy/pytest_hardpy/result/__init__.py +4 -0
  21. hardpy/pytest_hardpy/result/couchdb_config.py +6 -8
  22. hardpy/pytest_hardpy/result/report_loader/stand_cloud_loader.py +6 -9
  23. hardpy/pytest_hardpy/result/report_reader/stand_cloud_reader.py +84 -0
  24. hardpy/pytest_hardpy/utils/__init__.py +2 -0
  25. hardpy/pytest_hardpy/utils/connection_data.py +0 -4
  26. hardpy/pytest_hardpy/utils/dialog_box.py +61 -8
  27. hardpy/pytest_hardpy/utils/exception.py +1 -0
  28. {hardpy-0.10.1.dist-info → hardpy-0.11.1.dist-info}/METADATA +1 -1
  29. {hardpy-0.10.1.dist-info → hardpy-0.11.1.dist-info}/RECORD +33 -32
  30. hardpy/hardpy_panel/frontend/dist/static/js/main.8a7d8f7d.js +0 -3
  31. hardpy/hardpy_panel/frontend/dist/static/js/main.8a7d8f7d.js.map +0 -1
  32. /hardpy/hardpy_panel/frontend/dist/static/js/{main.8a7d8f7d.js.LICENSE.txt → main.fb8b84a3.js.LICENSE.txt} +0 -0
  33. {hardpy-0.10.1.dist-info → hardpy-0.11.1.dist-info}/WHEEL +0 -0
  34. {hardpy-0.10.1.dist-info → hardpy-0.11.1.dist-info}/entry_points.txt +0 -0
  35. {hardpy-0.10.1.dist-info → hardpy-0.11.1.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,22 @@ def pytest_addoption(parser: Parser) -> None:
56
56
  default=con_data.database_url,
57
57
  help="database url",
58
58
  )
59
+ parser.addoption(
60
+ "--hardpy-tests-name",
61
+ action="store",
62
+ help="HardPy tests suite name",
63
+ )
64
+ # TODO (xorialexandrov): Remove --hardpy-sp and --hardpy-sh in HardPy major version.
65
+ # Addoptions left for compatibility with version 0.10.1 and below
59
66
  parser.addoption(
60
67
  "--hardpy-sp",
61
68
  action="store",
62
- default=con_data.socket_port,
63
- help="internal socket port",
69
+ help="DEPRECATED, UNUSED: internal socket port",
64
70
  )
65
71
  parser.addoption(
66
72
  "--hardpy-sh",
67
73
  action="store",
68
- default=con_data.socket_host,
69
- help="internal socket host",
74
+ help="DEPRECATED, UNUSED: internal socket host",
70
75
  )
71
76
  parser.addoption(
72
77
  "--hardpy-clear-database",
@@ -117,6 +122,7 @@ class HardpyPlugin:
117
122
  self._results = {}
118
123
  self._post_run_functions: list[Callable] = []
119
124
  self._dependencies = {}
125
+ self._tests_name: str = ""
120
126
 
121
127
  if system() == "Linux":
122
128
  signal.signal(signal.SIGTERM, self._stop_handler)
@@ -134,15 +140,13 @@ class HardpyPlugin:
134
140
  if database_url:
135
141
  con_data.database_url = str(database_url) # type: ignore
136
142
 
137
- is_clear_database = config.getoption("--hardpy-clear-database")
138
-
139
- socket_port = config.getoption("--hardpy-sp")
140
- if socket_port:
141
- con_data.socket_port = int(socket_port) # type: ignore
143
+ tests_name = config.getoption("--hardpy-tests-name")
144
+ if tests_name:
145
+ self._tests_name = str(tests_name)
146
+ else:
147
+ self._tests_name = str(PurePath(config.rootpath).name)
142
148
 
143
- socket_host = config.getoption("--hardpy-sh")
144
- if socket_host:
145
- con_data.socket_host = str(socket_host) # type: ignore
149
+ is_clear_database = config.getoption("--hardpy-clear-database")
146
150
 
147
151
  sc_address = config.getoption("--sc-address")
148
152
  if sc_address:
@@ -182,11 +186,11 @@ class HardpyPlugin:
182
186
  def pytest_collection_modifyitems(
183
187
  self,
184
188
  session: Session,
185
- config: Config,
189
+ config: Config, # noqa: ARG002
186
190
  items: list[Item], # noqa: ARG002
187
191
  ) -> None:
188
192
  """Call after collection phase."""
189
- self._reporter.init_doc(str(PurePath(config.rootpath).name))
193
+ self._reporter.init_doc(self._tests_name)
190
194
 
191
195
  nodes = {}
192
196
  modules = set()
@@ -264,8 +268,8 @@ class HardpyPlugin:
264
268
 
265
269
  status = TestStatus.RUN
266
270
  is_skip_test = self._is_skip_test(node_info)
271
+ self._reporter.set_module_start_time(node_info.module_id)
267
272
  if not is_skip_test:
268
- self._reporter.set_module_start_time(node_info.module_id)
269
273
  self._reporter.set_case_start_time(node_info.module_id, node_info.case_id)
270
274
  else:
271
275
  status = TestStatus.SKIPPED
@@ -324,7 +328,11 @@ class HardpyPlugin:
324
328
 
325
329
  def pytest_runtest_logreport(self, report: TestReport) -> bool | None:
326
330
  """Call after call of each test item."""
327
- if report.when != "call" and report.failed is False:
331
+ is_skipped_by_plugin: bool = False
332
+ if report.when == "setup" and report.skipped is True:
333
+ # plugin-skipped tests should not have start and stop times
334
+ is_skipped_by_plugin = True
335
+ elif report.when != "call" and report.failed is False:
328
336
  # ignore setup and teardown phase or continue processing setup
329
337
  # and teardown failure (fixture exception handler)
330
338
  return True
@@ -337,10 +345,9 @@ class HardpyPlugin:
337
345
  case_id,
338
346
  TestStatus(report.outcome),
339
347
  )
340
- self._reporter.set_case_stop_time(
341
- module_id,
342
- case_id,
343
- )
348
+ # update case stop_time in non-skipped tests or user-skipped tests
349
+ if report.skipped is False or is_skipped_by_plugin is False:
350
+ self._reporter.set_case_stop_time(module_id, case_id)
344
351
 
345
352
  assertion_msg = self._decode_assertion_msg(report.longrepr)
346
353
  self._reporter.set_assertion_msg(module_id, case_id, assertion_msg)
@@ -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,8 @@ 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,
48
+ "--hardpy-tests-name",
49
+ self.config.tests_name,
50
50
  "--sc-address",
51
51
  self.config.stand_cloud.address,
52
52
  "--sc-connection-only"
@@ -64,10 +64,8 @@ class PyTestWrapper:
64
64
  "pytest",
65
65
  "--hardpy-db-url",
66
66
  self.config.database.connection_url(),
67
- "--hardpy-sp",
68
- str(self.config.socket.port),
69
- "--hardpy-sh",
70
- self.config.socket.host,
67
+ "--hardpy-tests-name",
68
+ self.config.tests_name,
71
69
  "--sc-address",
72
70
  self.config.stand_cloud.address,
73
71
  "--sc-connection-only"
@@ -117,10 +115,8 @@ class PyTestWrapper:
117
115
  "--collect-only",
118
116
  "--hardpy-db-url",
119
117
  self.config.database.connection_url(),
120
- "--hardpy-sp",
121
- str(self.config.socket.port),
122
- "--hardpy-sh",
123
- self.config.socket.host,
118
+ "--hardpy-tests-name",
119
+ self.config.tests_name,
124
120
  "--hardpy-pt",
125
121
  ]
126
122
 
@@ -144,10 +140,10 @@ class PyTestWrapper:
144
140
  bool: True if dialog box was confirmed/closed, else False
145
141
  """
146
142
  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()
143
+ self._reporter.update_doc_by_db()
144
+ key = self._reporter.generate_key(DF.OPERATOR_DATA, DF.DIALOG)
145
+ self._reporter.set_doc_value(key, data, statestore_only=True)
146
+ self._reporter.update_db_by_doc()
151
147
  except Exception: # noqa: BLE001
152
148
  return False
153
149
  return True
@@ -159,3 +155,11 @@ class PyTestWrapper:
159
155
  bool | None: True if self._proc is not None
160
156
  """
161
157
  return self._proc and self._proc.poll() is None
158
+
159
+ def get_config(self) -> dict:
160
+ """Get HardPy configuration.
161
+
162
+ Returns:
163
+ dict: HardPy configuration
164
+ """
165
+ return ConfigManager().get_config().model_dump()
@@ -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
 
@@ -6,9 +6,13 @@ from hardpy.pytest_hardpy.result.report_loader.stand_cloud_loader import (
6
6
  StandCloudLoader,
7
7
  )
8
8
  from hardpy.pytest_hardpy.result.report_reader.couchdb_reader import CouchdbReader
9
+ from hardpy.pytest_hardpy.result.report_reader.stand_cloud_reader import (
10
+ StandCloudReader,
11
+ )
9
12
 
10
13
  __all__ = [
11
14
  "CouchdbLoader",
12
15
  "CouchdbReader",
13
16
  "StandCloudLoader",
17
+ "StandCloudReader",
14
18
  ]
@@ -58,17 +58,17 @@ class CouchdbConfig:
58
58
 
59
59
  try:
60
60
  response = requests.get(host_url, timeout=5)
61
- except requests.exceptions.RequestException:
61
+ except requests.exceptions.RequestException as exc:
62
62
  msg = f"Error CouchDB connecting to {host_url}."
63
- raise RuntimeError(msg) # noqa: B904
63
+ raise RuntimeError(msg) from exc
64
64
 
65
65
  # fmt: off
66
66
  try:
67
67
  couchdb_dict = ast.literal_eval(response._content.decode("utf-8")) # type: ignore # noqa: SLF001
68
68
  couchdb_dict.get("couchdb", False)
69
- except Exception: # noqa: BLE001
69
+ except Exception as exc:
70
70
  msg = f"Address {host_url} does not provide CouchDB attributes."
71
- raise RuntimeError(msg) # noqa: B904
71
+ raise RuntimeError(msg) from exc
72
72
  # fmt: on
73
73
 
74
74
  credentials = f"{self.user}:{self.password}"
@@ -93,8 +93,6 @@ class CouchdbConfig:
93
93
  if requests.get(request, timeout=5).status_code == success:
94
94
  return "http"
95
95
  raise OSError # noqa: TRY301
96
- except OSError:
96
+ except OSError as exc:
97
97
  msg = f"Error connecting to couchdb server {self.host}:{self.port}."
98
- raise RuntimeError( # noqa: B904
99
- msg,
100
- )
98
+ raise RuntimeError(msg) from exc
@@ -4,9 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from oauthlib.oauth2.rfc6749.errors import InvalidGrantError, TokenExpiredError
7
+ from oauthlib.oauth2.rfc6749.errors import OAuth2Error
8
8
  from requests.exceptions import HTTPError
9
- from requests_oauth2client.tokens import ExpiredAccessToken
10
9
 
11
10
  from hardpy.common.stand_cloud.connector import StandCloudConnector, StandCloudError
12
11
  from hardpy.pytest_hardpy.utils import ConnectionData
@@ -45,16 +44,14 @@ class StandCloudLoader:
45
44
  Raises:
46
45
  StandCloudError: if report not uploaded to StandCloud
47
46
  """
48
- api = self._sc_connector.get_api("api/test_run")
47
+ api = self._sc_connector.get_api("test_report")
49
48
 
50
49
  try:
51
50
  resp = api.post(verify=self._verify_ssl, json=report.model_dump())
52
- except ExpiredAccessToken as exc:
53
- raise StandCloudError(str(exc)) # type: ignore
54
- except TokenExpiredError as exc:
55
- raise StandCloudError(exc.description)
56
- except InvalidGrantError as exc:
57
- raise StandCloudError(exc.description)
51
+ except RuntimeError as exc:
52
+ raise StandCloudError(str(exc)) from exc
53
+ except OAuth2Error as exc:
54
+ raise StandCloudError(exc.description) from exc
58
55
  except HTTPError as exc:
59
56
  return exc.response # type: ignore
60
57
 
@@ -0,0 +1,84 @@
1
+ # Copyright (c) 2025 Everypin
2
+ # GNU General Public License v3.0 (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+ from urllib.parse import urlencode
7
+
8
+ from oauthlib.oauth2.rfc6749.errors import OAuth2Error
9
+ from requests.exceptions import RequestException
10
+
11
+ from hardpy.common.stand_cloud.connector import StandCloudConnector, StandCloudError
12
+
13
+ if TYPE_CHECKING:
14
+ from typing import Any
15
+
16
+ from requests import Response
17
+ from requests_oauth2client import ApiClient
18
+
19
+
20
+ class StandCloudReader:
21
+ """StandCloud data reader.
22
+
23
+ The link to the documentation can be obtained by address:
24
+ https://service_name/integration/api/v1/docs
25
+
26
+ For example:
27
+ https://demo.standcloud.io/integration/api/v1/docs
28
+ """
29
+
30
+ def __init__(self, sc_connector: StandCloudConnector) -> None:
31
+ """Create StandCloud reader.
32
+
33
+ Args:
34
+ sc_connector (StandCloudConnector): StandCloud connector
35
+ """
36
+ self._verify_ssl = not __debug__
37
+ self._sc_connector = sc_connector
38
+
39
+ def test_run(self, run_id: str) -> Response:
40
+ """Get run data from '/test_run' endpoint.
41
+
42
+ Args:
43
+ run_id (str): UUIDv4 test run identifier.
44
+ Example: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
45
+
46
+ Returns:
47
+ Response: test run data.
48
+ """
49
+ return self._request(endpoint=f"test_run/{run_id}")
50
+
51
+ def tested_dut(self, params: dict[str, Any]) -> Response:
52
+ """Get tested DUT's data from '/tested_dut' endpoint.
53
+
54
+ Args:
55
+ params (dict[str, Any]): tested DUT filters:
56
+ Examples: {
57
+ "test_stand_name": "Stand 1",
58
+ "part_number": "part_number_1",
59
+ "firmware_version": "1.2.3",
60
+ }
61
+
62
+ Returns:
63
+ Response: tested dut data.
64
+ """
65
+ return self._request(endpoint="tested_dut", params=params)
66
+
67
+ def _request(self, endpoint: str, params: dict[str, Any] | None = None) -> Response:
68
+ api = self._build_api(endpoint=endpoint, params=params)
69
+ try:
70
+ resp = api.get(verify=self._verify_ssl)
71
+ except RuntimeError as exc:
72
+ raise StandCloudError(str(exc)) from exc
73
+ except OAuth2Error as exc:
74
+ raise StandCloudError(exc.description) from exc
75
+ except RequestException as exc:
76
+ return exc.response # type: ignore
77
+
78
+ return resp
79
+
80
+ def _build_api(self, endpoint: str, params: dict | None = None) -> ApiClient:
81
+ if params is None:
82
+ return self._sc_connector.get_api(f"{endpoint}")
83
+ encoded_params = urlencode(params)
84
+ return self._sc_connector.get_api(f"{endpoint}?{encoded_params}")
@@ -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