hardpy 0.17.1__py3-none-any.whl → 0.18.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.
- hardpy/cli/cli.py +50 -7
- hardpy/common/config.py +14 -11
- hardpy/common/stand_cloud/connector.py +20 -1
- hardpy/common/stand_cloud/token_manager.py +26 -1
- hardpy/hardpy_panel/api.py +93 -2
- hardpy/hardpy_panel/frontend/dist/assets/{allPaths-LQifhvrX.js → allPaths-31ulJ0tA.js} +1 -1
- hardpy/hardpy_panel/frontend/dist/assets/{allPathsLoader-C-JecT3u.js → allPathsLoader-HPn4WHWu.js} +2 -2
- hardpy/hardpy_panel/frontend/dist/assets/{browser-ponyfill-B-CPdmQc.js → browser-ponyfill-BQ1ipruI.js} +1 -1
- hardpy/hardpy_panel/frontend/dist/assets/{index-B2oxSaK6.js → index-BK2y65ib.js} +119 -119
- hardpy/hardpy_panel/frontend/dist/assets/{splitPathsBySizeLoader-CBTnepth.js → splitPathsBySizeLoader-ev1ZiRR9.js} +1 -1
- hardpy/hardpy_panel/frontend/dist/index.html +1 -1
- hardpy/pytest_hardpy/db/__init__.py +3 -1
- hardpy/pytest_hardpy/db/tempstore.py +54 -0
- hardpy/pytest_hardpy/plugin.py +34 -1
- hardpy/pytest_hardpy/pytest_wrapper.py +2 -0
- hardpy/pytest_hardpy/result/report_loader/stand_cloud_loader.py +8 -4
- hardpy/pytest_hardpy/result/report_synchronizer/__init__.py +10 -0
- hardpy/pytest_hardpy/result/report_synchronizer/synchronizer.py +121 -0
- {hardpy-0.17.1.dist-info → hardpy-0.18.0.dist-info}/METADATA +3 -2
- {hardpy-0.17.1.dist-info → hardpy-0.18.0.dist-info}/RECORD +23 -20
- {hardpy-0.17.1.dist-info → hardpy-0.18.0.dist-info}/WHEEL +0 -0
- {hardpy-0.17.1.dist-info → hardpy-0.18.0.dist-info}/entry_points.txt +0 -0
- {hardpy-0.17.1.dist-info → hardpy-0.18.0.dist-info}/licenses/LICENSE +0 -0
hardpy/cli/cli.py
CHANGED
|
@@ -67,13 +67,21 @@ def init( # noqa: PLR0913
|
|
|
67
67
|
help="Specify a frontend port.",
|
|
68
68
|
),
|
|
69
69
|
sc_address: str = typer.Option(
|
|
70
|
-
default=
|
|
70
|
+
default=default_config.stand_cloud.address,
|
|
71
71
|
help="Specify a StandCloud address.",
|
|
72
72
|
),
|
|
73
73
|
sc_connection_only: bool = typer.Option(
|
|
74
|
-
default=
|
|
74
|
+
default=default_config.stand_cloud.connection_only,
|
|
75
75
|
help="Check StandCloud service availability before start.",
|
|
76
76
|
),
|
|
77
|
+
sc_autosync: bool = typer.Option(
|
|
78
|
+
default=default_config.stand_cloud.autosync,
|
|
79
|
+
help="Enable StandCloud auto syncronization.",
|
|
80
|
+
),
|
|
81
|
+
sc_api_key: str | None = typer.Option(
|
|
82
|
+
default=default_config.stand_cloud.api_key,
|
|
83
|
+
help="Specify a StandCloud API key.",
|
|
84
|
+
),
|
|
77
85
|
) -> None:
|
|
78
86
|
"""Initialize HardPy tests directory.
|
|
79
87
|
|
|
@@ -90,6 +98,8 @@ def init( # noqa: PLR0913
|
|
|
90
98
|
frontend_language (str): Panel operator language
|
|
91
99
|
sc_address (str): StandCloud address
|
|
92
100
|
sc_connection_only (bool): Flag to check StandCloud service availability
|
|
101
|
+
sc_autosync (bool): Flag to enable StandCloud auto syncronization
|
|
102
|
+
sc_api_key (str | None): StandCloud API key
|
|
93
103
|
"""
|
|
94
104
|
dir_path = Path(Path.cwd() / tests_dir if tests_dir else "tests")
|
|
95
105
|
config_manager = ConfigManager()
|
|
@@ -104,6 +114,8 @@ def init( # noqa: PLR0913
|
|
|
104
114
|
frontend_language=default_config.frontend.language,
|
|
105
115
|
sc_address=sc_address,
|
|
106
116
|
sc_connection_only=sc_connection_only,
|
|
117
|
+
sc_autosync=sc_autosync,
|
|
118
|
+
sc_api_key=sc_api_key,
|
|
107
119
|
)
|
|
108
120
|
# create tests directory
|
|
109
121
|
Path.mkdir(dir_path, exist_ok=True, parents=True)
|
|
@@ -142,6 +154,7 @@ def run(tests_dir: Annotated[Optional[str], typer.Argument()] = None) -> None:
|
|
|
142
154
|
tests_dir (Optional[str]): Test directory. Current directory by default
|
|
143
155
|
"""
|
|
144
156
|
config = _get_config(tests_dir)
|
|
157
|
+
_validate_config(config)
|
|
145
158
|
|
|
146
159
|
print("\nLaunch the HardPy operator panel...")
|
|
147
160
|
|
|
@@ -263,6 +276,30 @@ def sc_logout(address: Annotated[str, typer.Argument()]) -> None:
|
|
|
263
276
|
print(f"HardPy logout failed from {address}")
|
|
264
277
|
|
|
265
278
|
|
|
279
|
+
@cli.command()
|
|
280
|
+
def sc_sync(
|
|
281
|
+
tests_dir: Annotated[Optional[str], typer.Argument()] = None,
|
|
282
|
+
timeout: int = typer.Option(
|
|
283
|
+
default="60",
|
|
284
|
+
help="Specify a synchronization timeout.",
|
|
285
|
+
),
|
|
286
|
+
) -> str:
|
|
287
|
+
"""Synchronize HardPy tests with StandCloud.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
tests_dir (Optional[str]): Test directory. Current directory by default
|
|
291
|
+
timeout (int): Synchronization timeout
|
|
292
|
+
"""
|
|
293
|
+
try:
|
|
294
|
+
_timeout = int(timeout)
|
|
295
|
+
except ValueError:
|
|
296
|
+
print("Timeout must be a number.")
|
|
297
|
+
sys.exit()
|
|
298
|
+
config = _get_config(tests_dir, validate=True)
|
|
299
|
+
url = f"http://{config.frontend.host}:{config.frontend.port}/api/stand_cloud_sync"
|
|
300
|
+
return _request_hardpy(url, timeout=_timeout)
|
|
301
|
+
|
|
302
|
+
|
|
266
303
|
def _get_config(tests_dir: str | None = None, validate: bool = False) -> HardpyConfig:
|
|
267
304
|
dir_path = Path.cwd() / tests_dir if tests_dir else Path.cwd()
|
|
268
305
|
config_manager = ConfigManager()
|
|
@@ -273,12 +310,12 @@ def _get_config(tests_dir: str | None = None, validate: bool = False) -> HardpyC
|
|
|
273
310
|
sys.exit()
|
|
274
311
|
|
|
275
312
|
if validate:
|
|
276
|
-
|
|
313
|
+
_validate_running_config(config, dir_path)
|
|
277
314
|
|
|
278
315
|
return config
|
|
279
316
|
|
|
280
317
|
|
|
281
|
-
def
|
|
318
|
+
def _validate_running_config(config: HardpyConfig, tests_dir: str) -> None:
|
|
282
319
|
url = f"http://{config.frontend.host}:{config.frontend.port}/api/hardpy_config"
|
|
283
320
|
error_msg = f"HardPy in directory {tests_dir} does not run."
|
|
284
321
|
try:
|
|
@@ -292,19 +329,25 @@ def _validate_config(config: HardpyConfig, tests_dir: str) -> None:
|
|
|
292
329
|
print(error_msg)
|
|
293
330
|
sys.exit()
|
|
294
331
|
|
|
332
|
+
def _validate_config(config: HardpyConfig) -> None:
|
|
333
|
+
if config.stand_cloud.autosync_timeout < 1:
|
|
334
|
+
print("StandCloud autosync timeout must be greater than 0.")
|
|
335
|
+
sys.exit()
|
|
336
|
+
|
|
295
337
|
|
|
296
|
-
def _request_hardpy(url: str) ->
|
|
338
|
+
def _request_hardpy(url: str, timeout: int = 5) -> str:
|
|
297
339
|
try:
|
|
298
|
-
response = requests.get(url, timeout=
|
|
340
|
+
response = requests.get(url, timeout=timeout)
|
|
299
341
|
except Exception:
|
|
300
342
|
print("HardPy operator panel is not running.")
|
|
301
343
|
sys.exit()
|
|
302
344
|
try:
|
|
303
345
|
status: dict = response.json().get("status", "ERROR")
|
|
304
346
|
except ValueError:
|
|
305
|
-
print(f"
|
|
347
|
+
print(f"HardPy internal error: {response}.")
|
|
306
348
|
sys.exit()
|
|
307
349
|
print(f"HardPy status: {status}.")
|
|
350
|
+
return status
|
|
308
351
|
|
|
309
352
|
|
|
310
353
|
if __name__ == "__main__":
|
hardpy/common/config.py
CHANGED
|
@@ -51,6 +51,7 @@ class FrontendConfig(BaseModel):
|
|
|
51
51
|
language: str = "en"
|
|
52
52
|
full_size_button: bool = False
|
|
53
53
|
sound_on: bool = False
|
|
54
|
+
measurement_display: bool = True
|
|
54
55
|
modal_result: ModalResultConfig = Field(default_factory=lambda: ModalResultConfig())
|
|
55
56
|
|
|
56
57
|
|
|
@@ -69,8 +70,11 @@ class StandCloudConfig(BaseModel):
|
|
|
69
70
|
|
|
70
71
|
model_config = ConfigDict(extra="forbid")
|
|
71
72
|
|
|
72
|
-
address: str = ""
|
|
73
|
+
address: str = "standcloud.io"
|
|
73
74
|
connection_only: bool = False
|
|
75
|
+
autosync: bool = False
|
|
76
|
+
autosync_timeout: int = 30 # 30 minutes
|
|
77
|
+
api_key: str = ""
|
|
74
78
|
|
|
75
79
|
|
|
76
80
|
class HardpyConfig(BaseModel, extra="allow"):
|
|
@@ -128,8 +132,10 @@ class ConfigManager(metaclass=SingletonMeta):
|
|
|
128
132
|
frontend_host: str,
|
|
129
133
|
frontend_port: int,
|
|
130
134
|
frontend_language: str,
|
|
131
|
-
sc_address: str
|
|
132
|
-
sc_connection_only: bool
|
|
135
|
+
sc_address: str,
|
|
136
|
+
sc_connection_only: bool,
|
|
137
|
+
sc_autosync: bool,
|
|
138
|
+
sc_api_key: str,
|
|
133
139
|
) -> None:
|
|
134
140
|
"""Initialize the HardPy configuration.
|
|
135
141
|
|
|
@@ -146,6 +152,8 @@ class ConfigManager(metaclass=SingletonMeta):
|
|
|
146
152
|
frontend_language (str): Operator panel language.
|
|
147
153
|
sc_address (str): StandCloud address.
|
|
148
154
|
sc_connection_only (bool): StandCloud check availability.
|
|
155
|
+
sc_autosync (bool): StandCloud auto syncronization.
|
|
156
|
+
sc_api_key (str): StandCloud API key.
|
|
149
157
|
"""
|
|
150
158
|
self._config.tests_name = tests_name
|
|
151
159
|
self._config.frontend.host = frontend_host
|
|
@@ -159,6 +167,8 @@ class ConfigManager(metaclass=SingletonMeta):
|
|
|
159
167
|
self._config.database.url = self._config.database.get_url()
|
|
160
168
|
self._config.stand_cloud.address = sc_address
|
|
161
169
|
self._config.stand_cloud.connection_only = sc_connection_only
|
|
170
|
+
self._config.stand_cloud.autosync = sc_autosync
|
|
171
|
+
self._config.stand_cloud.api_key = sc_api_key
|
|
162
172
|
|
|
163
173
|
def create_config(self, parent_dir: Path) -> None:
|
|
164
174
|
"""Create HardPy configuration.
|
|
@@ -166,14 +176,7 @@ class ConfigManager(metaclass=SingletonMeta):
|
|
|
166
176
|
Args:
|
|
167
177
|
parent_dir (Path): Configuration file parent directory.
|
|
168
178
|
"""
|
|
169
|
-
|
|
170
|
-
if not self._config.stand_cloud.address:
|
|
171
|
-
del config.stand_cloud
|
|
172
|
-
if not self._config.tests_name:
|
|
173
|
-
del config.tests_name
|
|
174
|
-
if not self._config.database.doc_id:
|
|
175
|
-
del config.database.doc_id
|
|
176
|
-
config_str = tomli_w.dumps(config.model_dump())
|
|
179
|
+
config_str = tomli_w.dumps(self._config.model_dump())
|
|
177
180
|
with Path.open(parent_dir / "hardpy.toml", "w") as file:
|
|
178
181
|
file.write(config_str)
|
|
179
182
|
|
|
@@ -29,9 +29,10 @@ class StandCloudConnector:
|
|
|
29
29
|
|
|
30
30
|
def __init__(
|
|
31
31
|
self,
|
|
32
|
-
addr: str,
|
|
32
|
+
addr: str = "standcloud.io",
|
|
33
33
|
api_mode: StandCloudAPIMode = StandCloudAPIMode.HARDPY,
|
|
34
34
|
api_version: int = 1,
|
|
35
|
+
api_key: str | None = None,
|
|
35
36
|
) -> None:
|
|
36
37
|
"""Create StandCloud API connector.
|
|
37
38
|
|
|
@@ -42,6 +43,8 @@ class StandCloudConnector:
|
|
|
42
43
|
Default: StandCloudAPIMode.HARDPY.
|
|
43
44
|
api_version (int): StandCloud API version.
|
|
44
45
|
Default: 1.
|
|
46
|
+
api_key (str | None): StandCloud API key.
|
|
47
|
+
Default: None.
|
|
45
48
|
"""
|
|
46
49
|
https_prefix = "https://"
|
|
47
50
|
auth_addr = addr + "/auth"
|
|
@@ -57,6 +60,10 @@ class StandCloudConnector:
|
|
|
57
60
|
self._client_id = "hardpy-report-uploader"
|
|
58
61
|
self._verify_ssl = not __debug__
|
|
59
62
|
self._token_manager = TokenManager(self._addr.domain)
|
|
63
|
+
self._token_manager.save_token(api_key)
|
|
64
|
+
# TODO(xorialexandrov): Change the self._token logic for storing the
|
|
65
|
+
# API key in the file system to secure storage.
|
|
66
|
+
# Remove device flow process.
|
|
60
67
|
self._token: BearerToken = self.get_access_token()
|
|
61
68
|
self._log = getLogger(__name__)
|
|
62
69
|
|
|
@@ -84,6 +91,8 @@ class StandCloudConnector:
|
|
|
84
91
|
Returns:
|
|
85
92
|
bool: True if token is valid, False otherwise.
|
|
86
93
|
"""
|
|
94
|
+
if self._token_manager.api_key:
|
|
95
|
+
return False
|
|
87
96
|
try:
|
|
88
97
|
OAuth2(
|
|
89
98
|
sc_addr=self._addr,
|
|
@@ -208,12 +217,22 @@ class StandCloudConnector:
|
|
|
208
217
|
return new_token
|
|
209
218
|
|
|
210
219
|
def _get_api(self, endpoint: str) -> ApiClient:
|
|
220
|
+
if self._token_manager.api_key:
|
|
221
|
+
session = requests.Session()
|
|
222
|
+
session.headers["Authorization"] = f"Bearer {self._token_manager.api_key}"
|
|
223
|
+
return ApiClient(
|
|
224
|
+
f"{self._addr.api}/{endpoint}",
|
|
225
|
+
session=session,
|
|
226
|
+
timeout=10,
|
|
227
|
+
)
|
|
228
|
+
|
|
211
229
|
if self._token is None:
|
|
212
230
|
msg = (
|
|
213
231
|
f"Access token to {self._addr.domain} is not set."
|
|
214
232
|
f"Login to {self._addr.domain} first"
|
|
215
233
|
)
|
|
216
234
|
raise StandCloudError(msg)
|
|
235
|
+
|
|
217
236
|
try:
|
|
218
237
|
auth = OAuth2(
|
|
219
238
|
sc_addr=self._addr,
|
|
@@ -27,6 +27,16 @@ class TokenManager:
|
|
|
27
27
|
|
|
28
28
|
def __init__(self, service_name: str) -> None:
|
|
29
29
|
self._service_name = f"HardPy_{service_name}"
|
|
30
|
+
self._api_key = None
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def api_key(self) -> str | None:
|
|
34
|
+
"""Get personal access token.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
str | None: personal access token
|
|
38
|
+
"""
|
|
39
|
+
return self._api_key
|
|
30
40
|
|
|
31
41
|
def remove_token(self) -> bool:
|
|
32
42
|
"""Remove token from keyring storage.
|
|
@@ -47,6 +57,14 @@ class TokenManager:
|
|
|
47
57
|
storage_keyring.delete_password(self._service_name, "refresh_token")
|
|
48
58
|
return True
|
|
49
59
|
|
|
60
|
+
def save_token(self, token: str) -> None:
|
|
61
|
+
"""Save personal access token.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
token (str): personal access token
|
|
65
|
+
"""
|
|
66
|
+
self._api_key = token
|
|
67
|
+
|
|
50
68
|
def save_token_info(self, token: BearerToken | dict) -> None:
|
|
51
69
|
"""Save token to keyring storage.
|
|
52
70
|
|
|
@@ -74,8 +92,15 @@ class TokenManager:
|
|
|
74
92
|
Returns:
|
|
75
93
|
BearerToken: access token
|
|
76
94
|
"""
|
|
77
|
-
|
|
95
|
+
storage_keyring, mem_keyring = self._get_store()
|
|
96
|
+
pat = storage_keyring.get_password(self._service_name, "pat")
|
|
97
|
+
if pat:
|
|
98
|
+
return BearerToken(access_token=pat)
|
|
99
|
+
|
|
78
100
|
token_info = mem_keyring.get_password(self._service_name, "access_token")
|
|
101
|
+
if not token_info:
|
|
102
|
+
msg = "Access token not found"
|
|
103
|
+
raise FileNotFoundError(msg)
|
|
79
104
|
secret = self._add_expires_in(json.loads(token_info)) # type: ignore
|
|
80
105
|
return BearerToken(**secret)
|
|
81
106
|
|
hardpy/hardpy_panel/api.py
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
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 asyncio
|
|
6
|
+
import contextlib
|
|
5
7
|
import json
|
|
8
|
+
import logging
|
|
6
9
|
import os
|
|
7
10
|
import re
|
|
11
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
8
12
|
from enum import Enum
|
|
9
13
|
from pathlib import Path
|
|
10
|
-
from typing import Annotated
|
|
14
|
+
from typing import TYPE_CHECKING, Annotated, Any, Final
|
|
11
15
|
from urllib.parse import unquote
|
|
12
16
|
|
|
13
17
|
from fastapi import FastAPI, Query
|
|
@@ -15,9 +19,46 @@ from fastapi.staticfiles import StaticFiles
|
|
|
15
19
|
|
|
16
20
|
from hardpy.common.config import ConfigManager
|
|
17
21
|
from hardpy.pytest_hardpy.pytest_wrapper import PyTestWrapper
|
|
22
|
+
from hardpy.pytest_hardpy.result.report_synchronizer import StandCloudSynchronizer
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import AsyncGenerator
|
|
26
|
+
|
|
27
|
+
# TODO (xorialexandrov): Move logging to own module
|
|
28
|
+
logging.basicConfig(
|
|
29
|
+
level=logging.INFO,
|
|
30
|
+
format="%(asctime)s %(levelname)s:\t %(message)s",
|
|
31
|
+
)
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@contextlib.asynccontextmanager
|
|
36
|
+
async def lifespan_sync_scheduler(app: FastAPI) -> AsyncGenerator[Any, Any]:
|
|
37
|
+
"""Manages the lifecycle events (startup and shutdown) for background tasks."""
|
|
38
|
+
config_manager = ConfigManager()
|
|
39
|
+
sc_autosync = config_manager.config.stand_cloud.autosync
|
|
40
|
+
if sc_autosync:
|
|
41
|
+
autosync_timeout = config_manager.config.stand_cloud.autosync_timeout
|
|
42
|
+
app.state.sync_task = asyncio.create_task(sync_stand_cloud(autosync_timeout))
|
|
43
|
+
|
|
44
|
+
yield
|
|
45
|
+
|
|
46
|
+
if sc_autosync:
|
|
47
|
+
if hasattr(app.state, "sync_task"):
|
|
48
|
+
app.state.sync_task.cancel()
|
|
49
|
+
# Wait for the task to be cancelled/finish its current cycle
|
|
50
|
+
await asyncio.gather(app.state.sync_task, return_exceptions=True)
|
|
51
|
+
logger.info("Cancelled StandCloud synchronization task.")
|
|
52
|
+
|
|
53
|
+
if hasattr(app.state, "executor"):
|
|
54
|
+
app.state.executor.shutdown(wait=False)
|
|
55
|
+
logger.info("Shut down ThreadPoolExecutor.")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
app = FastAPI(lifespan=lifespan_sync_scheduler)
|
|
20
59
|
app.state.pytest_wrp = PyTestWrapper()
|
|
60
|
+
app.state.sc_synchronizer = StandCloudSynchronizer()
|
|
61
|
+
app.state.executor = ThreadPoolExecutor(max_workers=1)
|
|
21
62
|
|
|
22
63
|
|
|
23
64
|
class Status(str, Enum):
|
|
@@ -34,6 +75,30 @@ class Status(str, Enum):
|
|
|
34
75
|
ERROR = "error"
|
|
35
76
|
|
|
36
77
|
|
|
78
|
+
async def sync_stand_cloud(sc_sync_interval_minutes: int) -> None:
|
|
79
|
+
"""Periodically calls the blocking stand_cloud_sync logic in a separate thread."""
|
|
80
|
+
sc_sync_interval: Final[int] = sc_sync_interval_minutes * 60
|
|
81
|
+
initial_pause: Final[int] = 30
|
|
82
|
+
|
|
83
|
+
loop = asyncio.get_event_loop()
|
|
84
|
+
await asyncio.sleep(initial_pause)
|
|
85
|
+
|
|
86
|
+
while True:
|
|
87
|
+
try:
|
|
88
|
+
sync_result = await loop.run_in_executor(
|
|
89
|
+
app.state.executor,
|
|
90
|
+
app.state.sc_synchronizer.sync,
|
|
91
|
+
)
|
|
92
|
+
logger.info(f"StandCloud synchronization status: {sync_result}")
|
|
93
|
+
except asyncio.CancelledError:
|
|
94
|
+
logger.info("StandCloud synchronization task cancelled.")
|
|
95
|
+
break
|
|
96
|
+
except Exception: # noqa: BLE001
|
|
97
|
+
# TODO (xorialexandrov): add more information to logging
|
|
98
|
+
logger.info("Error during StandCloud synchronization")
|
|
99
|
+
await asyncio.sleep(sc_sync_interval)
|
|
100
|
+
|
|
101
|
+
|
|
37
102
|
@app.get("/api/hardpy_config")
|
|
38
103
|
def hardpy_config() -> dict:
|
|
39
104
|
"""Get config of HardPy.
|
|
@@ -60,7 +125,9 @@ def start_pytest(args: Annotated[list[str] | None, Query()] = None) -> dict:
|
|
|
60
125
|
args_dict = dict(arg.split("=", 1) for arg in args if "=" in arg)
|
|
61
126
|
|
|
62
127
|
if app.state.pytest_wrp.start(start_args=args_dict):
|
|
128
|
+
logger.info("Start testing process.")
|
|
63
129
|
return {"status": Status.STARTED}
|
|
130
|
+
logger.info("Testing process is already running.")
|
|
64
131
|
return {"status": Status.BUSY}
|
|
65
132
|
|
|
66
133
|
|
|
@@ -72,7 +139,9 @@ def stop_pytest() -> dict:
|
|
|
72
139
|
dict[str, RunStatus]: run status
|
|
73
140
|
"""
|
|
74
141
|
if app.state.pytest_wrp.stop():
|
|
142
|
+
logger.info("Stop testing process.")
|
|
75
143
|
return {"status": Status.STOPPED}
|
|
144
|
+
logger.info("Testing process is not running.")
|
|
76
145
|
return {"status": Status.READY}
|
|
77
146
|
|
|
78
147
|
|
|
@@ -126,6 +195,28 @@ def database_document_id() -> dict:
|
|
|
126
195
|
return {"document_id": config_manager.config.database.doc_id}
|
|
127
196
|
|
|
128
197
|
|
|
198
|
+
@app.get("/api/stand_cloud_sync")
|
|
199
|
+
async def stand_cloud_sync() -> dict:
|
|
200
|
+
"""Stop pytest subprocess.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
dict[status, str]: synchronization status
|
|
204
|
+
"""
|
|
205
|
+
loop = asyncio.get_event_loop()
|
|
206
|
+
try:
|
|
207
|
+
sync_result = await loop.run_in_executor(
|
|
208
|
+
app.state.executor,
|
|
209
|
+
app.state.sc_synchronizer.sync,
|
|
210
|
+
)
|
|
211
|
+
except Exception: # noqa: BLE001
|
|
212
|
+
msg = "Error during StandCloud synchronization"
|
|
213
|
+
# TODO (xorialexandrov): add more information to logging
|
|
214
|
+
logger.info("Error during StandCloud synchronization")
|
|
215
|
+
return {"status": msg}
|
|
216
|
+
logger.info(f"StandCloud sucnronization status: {sync_result}")
|
|
217
|
+
return {"status": sync_result}
|
|
218
|
+
|
|
219
|
+
|
|
129
220
|
@app.post("/api/confirm_dialog_box")
|
|
130
221
|
def confirm_dialog_box(dbx_data: dict) -> dict:
|
|
131
222
|
"""Confirm dialog box with unified JSON structure.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{I as n}from"./index-DLOviMB1.js";import{I as e}from"./index-B-fsa5Ru.js";import{p as r,I as s}from"./index-
|
|
1
|
+
import{I as n}from"./index-DLOviMB1.js";import{I as e}from"./index-B-fsa5Ru.js";import{p as r,I as s}from"./index-BK2y65ib.js";function I(o,t){var a=r(o);return t===s.STANDARD?n[a]:e[a]}function p(o){return r(o)}export{n as IconSvgPaths16,e as IconSvgPaths20,I as getIconPaths,p as iconNameToPathsRecordKey};
|
hardpy/hardpy_panel/frontend/dist/assets/{allPathsLoader-C-JecT3u.js → allPathsLoader-HPn4WHWu.js}
RENAMED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/allPaths-
|
|
2
|
-
import{_ as o,a as n,b as i}from"./index-
|
|
1
|
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/allPaths-31ulJ0tA.js","assets/index-DLOviMB1.js","assets/index-B-fsa5Ru.js","assets/index-BK2y65ib.js","assets/index-B7T9xvaW.css"])))=>i.map(i=>d[i]);
|
|
2
|
+
import{_ as o,a as n,b as i}from"./index-BK2y65ib.js";var _=function(e,a){return o(void 0,void 0,void 0,function(){var t;return n(this,function(r){switch(r.label){case 0:return[4,i(()=>import("./allPaths-31ulJ0tA.js"),__vite__mapDeps([0,1,2,3,4]))];case 1:return t=r.sent().getIconPaths,[2,t(e,a)]}})})};export{_ as allPathsLoader};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{g as G}from"./index-
|
|
1
|
+
import{g as G}from"./index-BK2y65ib.js";function $(w,c){for(var m=0;m<c.length;m++){const d=c[m];if(typeof d!="string"&&!Array.isArray(d)){for(const y in d)if(y!=="default"&&!(y in w)){const p=Object.getOwnPropertyDescriptor(d,y);p&&Object.defineProperty(w,y,p.get?p:{enumerable:!0,get:()=>d[y]})}}}return Object.freeze(Object.defineProperty(w,Symbol.toStringTag,{value:"Module"}))}var E={exports:{}},U;function X(){return U||(U=1,(function(w,c){var m={},d=typeof globalThis<"u"&&globalThis||typeof self<"u"&&self||typeof m<"u"&&m,y=(function(){function v(){this.fetch=!1,this.DOMException=d.DOMException}return v.prototype=d,new v})();(function(v){(function(u){var a=typeof v<"u"&&v||typeof self<"u"&&self||typeof a<"u"&&a,f={searchParams:"URLSearchParams"in a,iterable:"Symbol"in a&&"iterator"in Symbol,blob:"FileReader"in a&&"Blob"in a&&(function(){try{return new Blob,!0}catch{return!1}})(),formData:"FormData"in a,arrayBuffer:"ArrayBuffer"in a};function S(e){return e&&DataView.prototype.isPrototypeOf(e)}if(f.arrayBuffer)var F=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],I=ArrayBuffer.isView||function(e){return e&&F.indexOf(Object.prototype.toString.call(e))>-1};function _(e){if(typeof e!="string"&&(e=String(e)),/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(e)||e==="")throw new TypeError('Invalid character in header field name: "'+e+'"');return e.toLowerCase()}function T(e){return typeof e!="string"&&(e=String(e)),e}function B(e){var t={next:function(){var r=e.shift();return{done:r===void 0,value:r}}};return f.iterable&&(t[Symbol.iterator]=function(){return t}),t}function s(e){this.map={},e instanceof s?e.forEach(function(t,r){this.append(r,t)},this):Array.isArray(e)?e.forEach(function(t){this.append(t[0],t[1])},this):e&&Object.getOwnPropertyNames(e).forEach(function(t){this.append(t,e[t])},this)}s.prototype.append=function(e,t){e=_(e),t=T(t);var r=this.map[e];this.map[e]=r?r+", "+t:t},s.prototype.delete=function(e){delete this.map[_(e)]},s.prototype.get=function(e){return e=_(e),this.has(e)?this.map[e]:null},s.prototype.has=function(e){return this.map.hasOwnProperty(_(e))},s.prototype.set=function(e,t){this.map[_(e)]=T(t)},s.prototype.forEach=function(e,t){for(var r in this.map)this.map.hasOwnProperty(r)&&e.call(t,this.map[r],r,this)},s.prototype.keys=function(){var e=[];return this.forEach(function(t,r){e.push(r)}),B(e)},s.prototype.values=function(){var e=[];return this.forEach(function(t){e.push(t)}),B(e)},s.prototype.entries=function(){var e=[];return this.forEach(function(t,r){e.push([r,t])}),B(e)},f.iterable&&(s.prototype[Symbol.iterator]=s.prototype.entries);function O(e){if(e.bodyUsed)return Promise.reject(new TypeError("Already read"));e.bodyUsed=!0}function D(e){return new Promise(function(t,r){e.onload=function(){t(e.result)},e.onerror=function(){r(e.error)}})}function M(e){var t=new FileReader,r=D(t);return t.readAsArrayBuffer(e),r}function q(e){var t=new FileReader,r=D(t);return t.readAsText(e),r}function H(e){for(var t=new Uint8Array(e),r=new Array(t.length),n=0;n<t.length;n++)r[n]=String.fromCharCode(t[n]);return r.join("")}function x(e){if(e.slice)return e.slice(0);var t=new Uint8Array(e.byteLength);return t.set(new Uint8Array(e)),t.buffer}function R(){return this.bodyUsed=!1,this._initBody=function(e){this.bodyUsed=this.bodyUsed,this._bodyInit=e,e?typeof e=="string"?this._bodyText=e:f.blob&&Blob.prototype.isPrototypeOf(e)?this._bodyBlob=e:f.formData&&FormData.prototype.isPrototypeOf(e)?this._bodyFormData=e:f.searchParams&&URLSearchParams.prototype.isPrototypeOf(e)?this._bodyText=e.toString():f.arrayBuffer&&f.blob&&S(e)?(this._bodyArrayBuffer=x(e.buffer),this._bodyInit=new Blob([this._bodyArrayBuffer])):f.arrayBuffer&&(ArrayBuffer.prototype.isPrototypeOf(e)||I(e))?this._bodyArrayBuffer=x(e):this._bodyText=e=Object.prototype.toString.call(e):this._bodyText="",this.headers.get("content-type")||(typeof e=="string"?this.headers.set("content-type","text/plain;charset=UTF-8"):this._bodyBlob&&this._bodyBlob.type?this.headers.set("content-type",this._bodyBlob.type):f.searchParams&&URLSearchParams.prototype.isPrototypeOf(e)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))},f.blob&&(this.blob=function(){var e=O(this);if(e)return e;if(this._bodyBlob)return Promise.resolve(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(new Blob([this._bodyArrayBuffer]));if(this._bodyFormData)throw new Error("could not read FormData body as blob");return Promise.resolve(new Blob([this._bodyText]))},this.arrayBuffer=function(){if(this._bodyArrayBuffer){var e=O(this);return e||(ArrayBuffer.isView(this._bodyArrayBuffer)?Promise.resolve(this._bodyArrayBuffer.buffer.slice(this._bodyArrayBuffer.byteOffset,this._bodyArrayBuffer.byteOffset+this._bodyArrayBuffer.byteLength)):Promise.resolve(this._bodyArrayBuffer))}else return this.blob().then(M)}),this.text=function(){var e=O(this);if(e)return e;if(this._bodyBlob)return q(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(H(this._bodyArrayBuffer));if(this._bodyFormData)throw new Error("could not read FormData body as text");return Promise.resolve(this._bodyText)},f.formData&&(this.formData=function(){return this.text().then(k)}),this.json=function(){return this.text().then(JSON.parse)},this}var L=["DELETE","GET","HEAD","OPTIONS","POST","PUT"];function C(e){var t=e.toUpperCase();return L.indexOf(t)>-1?t:e}function b(e,t){if(!(this instanceof b))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');t=t||{};var r=t.body;if(e instanceof b){if(e.bodyUsed)throw new TypeError("Already read");this.url=e.url,this.credentials=e.credentials,t.headers||(this.headers=new s(e.headers)),this.method=e.method,this.mode=e.mode,this.signal=e.signal,!r&&e._bodyInit!=null&&(r=e._bodyInit,e.bodyUsed=!0)}else this.url=String(e);if(this.credentials=t.credentials||this.credentials||"same-origin",(t.headers||!this.headers)&&(this.headers=new s(t.headers)),this.method=C(t.method||this.method||"GET"),this.mode=t.mode||this.mode||null,this.signal=t.signal||this.signal,this.referrer=null,(this.method==="GET"||this.method==="HEAD")&&r)throw new TypeError("Body not allowed for GET or HEAD requests");if(this._initBody(r),(this.method==="GET"||this.method==="HEAD")&&(t.cache==="no-store"||t.cache==="no-cache")){var n=/([?&])_=[^&]*/;if(n.test(this.url))this.url=this.url.replace(n,"$1_="+new Date().getTime());else{var i=/\?/;this.url+=(i.test(this.url)?"&":"?")+"_="+new Date().getTime()}}}b.prototype.clone=function(){return new b(this,{body:this._bodyInit})};function k(e){var t=new FormData;return e.trim().split("&").forEach(function(r){if(r){var n=r.split("="),i=n.shift().replace(/\+/g," "),o=n.join("=").replace(/\+/g," ");t.append(decodeURIComponent(i),decodeURIComponent(o))}}),t}function N(e){var t=new s,r=e.replace(/\r?\n[\t ]+/g," ");return r.split("\r").map(function(n){return n.indexOf(`
|
|
2
2
|
`)===0?n.substr(1,n.length):n}).forEach(function(n){var i=n.split(":"),o=i.shift().trim();if(o){var g=i.join(":").trim();t.append(o,g)}}),t}R.call(b.prototype);function l(e,t){if(!(this instanceof l))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');t||(t={}),this.type="default",this.status=t.status===void 0?200:t.status,this.ok=this.status>=200&&this.status<300,this.statusText=t.statusText===void 0?"":""+t.statusText,this.headers=new s(t.headers),this.url=t.url||"",this._initBody(e)}R.call(l.prototype),l.prototype.clone=function(){return new l(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new s(this.headers),url:this.url})},l.error=function(){var e=new l(null,{status:0,statusText:""});return e.type="error",e};var V=[301,302,303,307,308];l.redirect=function(e,t){if(V.indexOf(t)===-1)throw new RangeError("Invalid status code");return new l(null,{status:t,headers:{location:e}})},u.DOMException=a.DOMException;try{new u.DOMException}catch{u.DOMException=function(t,r){this.message=t,this.name=r;var n=Error(t);this.stack=n.stack},u.DOMException.prototype=Object.create(Error.prototype),u.DOMException.prototype.constructor=u.DOMException}function P(e,t){return new Promise(function(r,n){var i=new b(e,t);if(i.signal&&i.signal.aborted)return n(new u.DOMException("Aborted","AbortError"));var o=new XMLHttpRequest;function g(){o.abort()}o.onload=function(){var h={status:o.status,statusText:o.statusText,headers:N(o.getAllResponseHeaders()||"")};h.url="responseURL"in o?o.responseURL:h.headers.get("X-Request-URL");var A="response"in o?o.response:o.responseText;setTimeout(function(){r(new l(A,h))},0)},o.onerror=function(){setTimeout(function(){n(new TypeError("Network request failed"))},0)},o.ontimeout=function(){setTimeout(function(){n(new TypeError("Network request failed"))},0)},o.onabort=function(){setTimeout(function(){n(new u.DOMException("Aborted","AbortError"))},0)};function z(h){try{return h===""&&a.location.href?a.location.href:h}catch{return h}}o.open(i.method,z(i.url),!0),i.credentials==="include"?o.withCredentials=!0:i.credentials==="omit"&&(o.withCredentials=!1),"responseType"in o&&(f.blob?o.responseType="blob":f.arrayBuffer&&i.headers.get("Content-Type")&&i.headers.get("Content-Type").indexOf("application/octet-stream")!==-1&&(o.responseType="arraybuffer")),t&&typeof t.headers=="object"&&!(t.headers instanceof s)?Object.getOwnPropertyNames(t.headers).forEach(function(h){o.setRequestHeader(h,T(t.headers[h]))}):i.headers.forEach(function(h,A){o.setRequestHeader(A,h)}),i.signal&&(i.signal.addEventListener("abort",g),o.onreadystatechange=function(){o.readyState===4&&i.signal.removeEventListener("abort",g)}),o.send(typeof i._bodyInit>"u"?null:i._bodyInit)})}return P.polyfill=!0,a.fetch||(a.fetch=P,a.Headers=s,a.Request=b,a.Response=l),u.Headers=s,u.Request=b,u.Response=l,u.fetch=P,u})({})})(y),y.fetch.ponyfill=!0,delete y.fetch.polyfill;var p=d.fetch?d:y;c=p.fetch,c.default=p.fetch,c.fetch=p.fetch,c.Headers=p.Headers,c.Request=p.Request,c.Response=p.Response,w.exports=c})(E,E.exports)),E.exports}var j=X();const J=G(j),Q=$({__proto__:null,default:J},[j]);export{Q as b};
|