hardpy 0.11.0__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.
hardpy/__init__.py CHANGED
@@ -1,7 +1,7 @@
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.common.stand_cloud import StandCloudError
4
+ from hardpy.common.stand_cloud import StandCloudConnector, StandCloudError
5
5
  from hardpy.pytest_hardpy.pytest_call import (
6
6
  clear_operator_message,
7
7
  get_current_attempt,
@@ -20,7 +20,11 @@ from hardpy.pytest_hardpy.pytest_call import (
20
20
  set_stand_location,
21
21
  set_stand_name,
22
22
  )
23
- from hardpy.pytest_hardpy.result import CouchdbLoader, StandCloudLoader
23
+ from hardpy.pytest_hardpy.result import (
24
+ CouchdbLoader,
25
+ StandCloudLoader,
26
+ StandCloudReader,
27
+ )
24
28
  from hardpy.pytest_hardpy.result.couchdb_config import CouchdbConfig
25
29
  from hardpy.pytest_hardpy.utils import (
26
30
  BaseWidget,
@@ -54,8 +58,10 @@ __all__ = [
54
58
  "MultistepWidget",
55
59
  "NumericInputWidget",
56
60
  "RadiobuttonWidget",
61
+ "StandCloudConnector",
57
62
  "StandCloudError",
58
63
  "StandCloudLoader",
64
+ "StandCloudReader",
59
65
  "StepWidget",
60
66
  "TextInputWidget",
61
67
  "clear_operator_message",
hardpy/cli/cli.py CHANGED
@@ -6,11 +6,12 @@ import sys
6
6
  from pathlib import Path
7
7
  from typing import Annotated, Optional
8
8
 
9
+ import requests
9
10
  import typer
10
11
  from uvicorn import run as uvicorn_run
11
12
 
12
13
  from hardpy.cli.template import TemplateGenerator
13
- from hardpy.common.config import ConfigManager
14
+ from hardpy.common.config import ConfigManager, HardpyConfig
14
15
  from hardpy.common.stand_cloud import (
15
16
  StandCloudConnector,
16
17
  StandCloudError,
@@ -31,6 +32,10 @@ default_config = ConfigManager().get_config()
31
32
  @cli.command()
32
33
  def init( # noqa: PLR0913
33
34
  tests_dir: Annotated[Optional[str], typer.Argument()] = None,
35
+ tests_name: str = typer.Option(
36
+ default="",
37
+ help="Specify a tests suite name.",
38
+ ),
34
39
  create_database: bool = typer.Option(
35
40
  default=True,
36
41
  help="Create CouchDB database.",
@@ -72,6 +77,7 @@ def init( # noqa: PLR0913
72
77
 
73
78
  Args:
74
79
  tests_dir (str | None): Tests directory. Current directory + `tests` by default
80
+ tests_name (str): Tests suite name, "Tests" by default
75
81
  create_database (bool): Flag to create database
76
82
  database_user (str): Database user name
77
83
  database_password (str): Database password
@@ -83,8 +89,10 @@ def init( # noqa: PLR0913
83
89
  sc_connection_only (bool): Flag to check StandCloud service availability
84
90
  """
85
91
  _tests_dir = tests_dir if tests_dir else default_config.tests_dir
92
+ _tests_name = tests_name if tests_name else default_config.tests_name
86
93
  ConfigManager().init_config(
87
94
  tests_dir=str(_tests_dir),
95
+ tests_name=_tests_name,
88
96
  database_user=database_user,
89
97
  database_password=database_password,
90
98
  database_host=database_host,
@@ -152,6 +160,48 @@ def run(tests_dir: Annotated[Optional[str], typer.Argument()] = None) -> None:
152
160
  )
153
161
 
154
162
 
163
+ @cli.command()
164
+ def start(tests_dir: Annotated[Optional[str], typer.Argument()] = None) -> None:
165
+ """Start HardPy tests.
166
+
167
+ Args:
168
+ tests_dir (Optional[str]): Test directory. Current directory by default
169
+ """
170
+ config = _get_config(tests_dir)
171
+ _check_config(config)
172
+
173
+ url = f"http://{config.frontend.host}:{config.frontend.port}/api/start"
174
+ _request_hardpy(url)
175
+
176
+
177
+ @cli.command()
178
+ def stop(tests_dir: Annotated[Optional[str], typer.Argument()] = None) -> None:
179
+ """Stop HardPy tests.
180
+
181
+ Args:
182
+ tests_dir (Optional[str]): Test directory. Current directory by default
183
+ """
184
+ config = _get_config(tests_dir)
185
+ _check_config(config)
186
+
187
+ url = f"http://{config.frontend.host}:{config.frontend.port}/api/stop"
188
+ _request_hardpy(url)
189
+
190
+
191
+ @cli.command()
192
+ def status(tests_dir: Annotated[Optional[str], typer.Argument()] = None) -> None:
193
+ """Get HardPy test launch status.
194
+
195
+ Args:
196
+ tests_dir (Optional[str]): Test directory. Current directory by default
197
+ """
198
+ config = _get_config(tests_dir)
199
+ _check_config(config)
200
+
201
+ url = f"http://{config.frontend.host}:{config.frontend.port}/api/status"
202
+ _request_hardpy(url)
203
+
204
+
155
205
  @cli.command()
156
206
  def sc_login(
157
207
  address: Annotated[str, typer.Argument()],
@@ -195,5 +245,45 @@ def sc_logout() -> None:
195
245
  print("HardPy logout failed")
196
246
 
197
247
 
248
+ def _get_config(tests_dir: str | None = None) -> HardpyConfig:
249
+ dir_path = Path.cwd() / tests_dir if tests_dir else Path.cwd()
250
+ config = ConfigManager().read_config(dir_path)
251
+
252
+ if not config:
253
+ print(f"Config at path {dir_path} not found.")
254
+ sys.exit()
255
+
256
+ return config
257
+
258
+
259
+ def _check_config(config: HardpyConfig) -> None:
260
+ url = f"http://{config.frontend.host}:{config.frontend.port}/api/hardpy_config"
261
+ error_msg = f"HardPy in directory {config.tests_dir} does not run."
262
+ try:
263
+ response = requests.get(url, timeout=2)
264
+ except Exception:
265
+ print(error_msg)
266
+ sys.exit()
267
+
268
+ running_config: dict = response.json()
269
+ if config.model_dump() != running_config:
270
+ print(error_msg)
271
+ sys.exit()
272
+
273
+
274
+ def _request_hardpy(url: str) -> None:
275
+ try:
276
+ response = requests.get(url, timeout=2)
277
+ except Exception:
278
+ print("HardPy operator panel is not running.")
279
+ sys.exit()
280
+ try:
281
+ status: dict = response.json().get("status", "ERROR")
282
+ except ValueError:
283
+ print(f"Hardpy internal error: {response}.")
284
+ sys.exit()
285
+ print(f"HardPy status: {status}.")
286
+
287
+
198
288
  if __name__ == "__main__":
199
289
  cli()
hardpy/common/config.py CHANGED
@@ -57,6 +57,7 @@ class HardpyConfig(BaseModel, extra="allow"):
57
57
 
58
58
  title: str = "HardPy TOML config"
59
59
  tests_dir: str = "tests"
60
+ tests_name: str = ""
60
61
  database: DatabaseConfig = DatabaseConfig()
61
62
  frontend: FrontendConfig = FrontendConfig()
62
63
  stand_cloud: StandCloudConfig = StandCloudConfig()
@@ -72,6 +73,7 @@ class ConfigManager:
72
73
  def init_config( # noqa: PLR0913
73
74
  cls,
74
75
  tests_dir: str,
76
+ tests_name: str,
75
77
  database_user: str,
76
78
  database_password: str,
77
79
  database_host: str,
@@ -85,6 +87,7 @@ class ConfigManager:
85
87
 
86
88
  Args:
87
89
  tests_dir (str): Tests directory.
90
+ tests_name (str): Tests suite name.
88
91
  database_user (str): Database user name.
89
92
  database_password (str): Database password.
90
93
  database_host (str): Database host.
@@ -95,6 +98,7 @@ class ConfigManager:
95
98
  sc_connection_only (bool): StandCloud check availability.
96
99
  """
97
100
  cls.obj.tests_dir = str(tests_dir)
101
+ cls.obj.tests_name = tests_name
98
102
  cls.obj.database.user = database_user
99
103
  cls.obj.database.password = database_password
100
104
  cls.obj.database.host = database_host
@@ -113,6 +117,8 @@ class ConfigManager:
113
117
  """
114
118
  if not cls.obj.stand_cloud.address:
115
119
  del cls.obj.stand_cloud
120
+ if not cls.obj.tests_name:
121
+ del cls.obj.tests_name
116
122
  config_str = tomli_w.dumps(cls.obj.model_dump())
117
123
  with Path.open(parent_dir / "hardpy.toml", "w") as file:
118
124
  file.write(config_str)
@@ -1,11 +1,12 @@
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.common.stand_cloud.connector import StandCloudConnector
4
+ from hardpy.common.stand_cloud.connector import StandCloudAPIMode, StandCloudConnector
5
5
  from hardpy.common.stand_cloud.exception import StandCloudError
6
6
  from hardpy.common.stand_cloud.registration import login, logout
7
7
 
8
8
  __all__ = [
9
+ "StandCloudAPIMode",
9
10
  "StandCloudConnector",
10
11
  "StandCloudError",
11
12
  "login",
@@ -4,19 +4,12 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  from datetime import datetime, timedelta, timezone
7
+ from enum import Enum
7
8
  from logging import getLogger
8
9
  from typing import TYPE_CHECKING, NamedTuple
9
10
 
10
- from oauthlib.oauth2.rfc6749.errors import (
11
- InvalidGrantError,
12
- MissingTokenError,
13
- TokenExpiredError,
14
- )
15
- from requests.exceptions import (
16
- ConnectionError as RequestConnectionError,
17
- HTTPError,
18
- InvalidURL,
19
- )
11
+ from oauthlib.oauth2.rfc6749.errors import OAuth2Error
12
+ from requests.exceptions import RequestException
20
13
  from requests_oauth2client import ApiClient, BearerToken
21
14
  from requests_oauth2client.tokens import ExpiredAccessToken
22
15
  from requests_oauthlib import OAuth2Session
@@ -42,24 +35,41 @@ class StandCloudURL(NamedTuple):
42
35
  par: str
43
36
  auth: str
44
37
 
38
+
39
+ class StandCloudAPIMode(str, Enum):
40
+ """StandCloud API mode.
41
+
42
+ HARDPY for test stand, integration for third-party service.
43
+ """
44
+
45
+ HARDPY = "hardpy"
46
+ INTEGRATION = "integration"
47
+
48
+
45
49
  class StandCloudConnector:
46
50
  """StandCloud API connector."""
47
51
 
48
52
  def __init__(
49
53
  self,
50
54
  addr: str,
55
+ api_mode: StandCloudAPIMode = StandCloudAPIMode.HARDPY,
56
+ api_version: int = 1,
51
57
  ) -> None:
52
58
  """Create StandCLoud loader.
53
59
 
54
60
  Args:
55
- addr (str | None, optional): StandCloud address.
56
- The option only for development and debug. Defaults to True.
61
+ addr (str): StandCloud service name.
62
+ api_mode (StandCloudAPIMode): StandCloud API mode,
63
+ hardpy for test stand, integration for third-party service.
64
+ Default: StandCloudAPIMode.HARDPY.
65
+ api_version (int): StandCloud API version.
66
+ Default: 1.
57
67
  """
58
68
  https_prefix = "https://"
59
69
  auth_addr = addr + "/auth"
60
70
 
61
71
  self._url: StandCloudURL = StandCloudURL(
62
- api=https_prefix + addr + self._get_service_name(addr) + "/api/v1",
72
+ api=https_prefix + addr + f"/{api_mode}/api/v{api_version}",
63
73
  token=https_prefix + auth_addr + "/api/oidc/token",
64
74
  par=https_prefix + auth_addr + "/api/oidc/pushed-authorization-request",
65
75
  auth=https_prefix + auth_addr + "/api/oidc/authorization",
@@ -98,13 +108,11 @@ class StandCloudConnector:
98
108
  try:
99
109
  resp = api.get(verify=self._verify_ssl)
100
110
  except ExpiredAccessToken as exc:
101
- raise StandCloudError(str(exc))
102
- except TokenExpiredError as exc:
103
- raise StandCloudError(exc.description)
104
- except InvalidGrantError as exc:
105
- raise StandCloudError(exc.description)
106
- except HTTPError as exc:
107
- raise StandCloudError(exc.strerror) # type: ignore
111
+ raise StandCloudError(str(exc)) from exc
112
+ except OAuth2Error as exc:
113
+ raise StandCloudError(exc.description) from exc
114
+ except RequestException as exc:
115
+ raise StandCloudError(exc.strerror) from exc # type: ignore
108
116
 
109
117
  return resp
110
118
 
@@ -192,15 +200,10 @@ class StandCloudConnector:
192
200
  verify=False,
193
201
  **extra,
194
202
  )
195
- except InvalidGrantError as exc:
196
- raise StandCloudError(exc.description)
197
- except RequestConnectionError as exc:
198
- raise StandCloudError(exc.strerror) # type: ignore
199
- except MissingTokenError as exc:
200
- raise StandCloudError(exc.description)
201
- except InvalidURL:
202
- msg = "Authentication URL is not available"
203
- raise StandCloudError(msg)
203
+ except OAuth2Error as exc:
204
+ raise StandCloudError(exc.description) from exc
205
+ except RequestException as exc:
206
+ raise StandCloudError(exc.strerror) from exc # type: ignore
204
207
  self._token_update(ret) # type: ignore
205
208
 
206
209
  return ApiClient(self._url.api + "/" + endpoint, session=session, timeout=10)
@@ -216,12 +219,3 @@ class StandCloudConnector:
216
219
  expires_at=expires_at,
217
220
  expires_in=expires_in,
218
221
  )
219
-
220
- def _get_service_name(self, addr: str) -> str:
221
- addr_parts = addr.split(".")
222
- number_of_parts = 3
223
- service_position_in_address = 1
224
- if isinstance(addr_parts, list) and len(addr_parts) >= number_of_parts:
225
- return "/" + addr_parts[service_position_in_address]
226
- msg = f"Invalid StandCloud address: {addr}"
227
- raise StandCloudError(msg)
@@ -18,9 +18,9 @@ app.state.pytest_wrp = PyTestWrapper()
18
18
 
19
19
 
20
20
  class Status(str, Enum):
21
- """Pytest run status.
21
+ """HardPy status.
22
22
 
23
- Statuses, that can be returned by HardPy to frontend.
23
+ Statuses, that can be returned by HardPy API.
24
24
  """
25
25
 
26
26
  STOPPED = "stopped"
@@ -31,6 +31,16 @@ class Status(str, Enum):
31
31
  ERROR = "error"
32
32
 
33
33
 
34
+ @app.get("/api/hardpy_config")
35
+ def hardpy_config() -> dict:
36
+ """Get config of HardPy.
37
+
38
+ Returns:
39
+ dict: HardPy config
40
+ """
41
+ return app.state.pytest_wrp.get_config()
42
+
43
+
34
44
  @app.get("/api/start")
35
45
  def start_pytest() -> dict:
36
46
  """Start pytest subprocess.
@@ -68,6 +78,18 @@ def collect_pytest() -> dict:
68
78
  return {"status": Status.BUSY}
69
79
 
70
80
 
81
+ @app.get("/api/status")
82
+ def status() -> dict:
83
+ """Get pytest subprocess status.
84
+
85
+ Returns:
86
+ dict[str, RunStatus]: run status
87
+ """
88
+ is_running = app.state.pytest_wrp.is_running()
89
+ status = Status.BUSY if is_running else Status.READY
90
+ return {"status": status}
91
+
92
+
71
93
  @app.get("/api/couch")
72
94
  def couch_connection() -> dict:
73
95
  """Get couchdb connection string.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "files": {
3
3
  "main.css": "/static/css/main.e8a862f1.css",
4
- "main.js": "/static/js/main.114c5914.js",
4
+ "main.js": "/static/js/main.fb8b84a3.js",
5
5
  "blueprint-icons-all-paths-loader.js": "/static/js/blueprint-icons-all-paths-loader.0aa89747.chunk.js",
6
6
  "blueprint-icons-split-paths-by-size-loader.js": "/static/js/blueprint-icons-split-paths-by-size-loader.52a072d3.chunk.js",
7
7
  "static/js/808.ce070002.chunk.js": "/static/js/808.ce070002.chunk.js",
@@ -21,7 +21,7 @@
21
21
  "static/media/logo_smol.png": "/static/media/logo_smol.5b16f92447a4a9e80331.png",
22
22
  "index.html": "/index.html",
23
23
  "main.e8a862f1.css.map": "/static/css/main.e8a862f1.css.map",
24
- "main.114c5914.js.map": "/static/js/main.114c5914.js.map",
24
+ "main.fb8b84a3.js.map": "/static/js/main.fb8b84a3.js.map",
25
25
  "blueprint-icons-all-paths-loader.0aa89747.chunk.js.map": "/static/js/blueprint-icons-all-paths-loader.0aa89747.chunk.js.map",
26
26
  "blueprint-icons-split-paths-by-size-loader.52a072d3.chunk.js.map": "/static/js/blueprint-icons-split-paths-by-size-loader.52a072d3.chunk.js.map",
27
27
  "808.ce070002.chunk.js.map": "/static/js/808.ce070002.chunk.js.map",
@@ -31,6 +31,6 @@
31
31
  },
32
32
  "entrypoints": [
33
33
  "static/css/main.e8a862f1.css",
34
- "static/js/main.114c5914.js"
34
+ "static/js/main.fb8b84a3.js"
35
35
  ]
36
36
  }
@@ -1 +1 @@
1
- <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>HardPy Operator Panel</title><script defer="defer" src="/static/js/main.114c5914.js"></script><link href="/static/css/main.e8a862f1.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
1
+ <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>HardPy Operator Panel</title><script defer="defer" src="/static/js/main.fb8b84a3.js"></script><link href="/static/css/main.e8a862f1.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>