hardpy 0.9.0__py3-none-any.whl → 0.10.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 +4 -1
- hardpy/cli/cli.py +78 -11
- hardpy/common/config.py +16 -0
- hardpy/common/stand_cloud/__init__.py +13 -0
- hardpy/common/stand_cloud/connector.py +227 -0
- hardpy/common/stand_cloud/exception.py +9 -0
- hardpy/common/stand_cloud/oauth_callback.py +95 -0
- hardpy/common/stand_cloud/registration.py +236 -0
- hardpy/common/stand_cloud/token_storage.py +27 -0
- hardpy/hardpy_panel/api.py +10 -8
- hardpy/hardpy_panel/frontend/dist/asset-manifest.json +3 -3
- hardpy/hardpy_panel/frontend/dist/index.html +1 -1
- hardpy/hardpy_panel/frontend/dist/static/js/main.8a7d8f7d.js +3 -0
- hardpy/hardpy_panel/frontend/dist/static/js/main.8a7d8f7d.js.map +1 -0
- hardpy/pytest_hardpy/db/const.py +1 -0
- hardpy/pytest_hardpy/db/schema/v1.py +2 -0
- hardpy/pytest_hardpy/plugin.py +52 -4
- hardpy/pytest_hardpy/pytest_wrapper.py +10 -0
- hardpy/pytest_hardpy/reporter/hook_reporter.py +10 -0
- hardpy/pytest_hardpy/result/__init__.py +4 -0
- hardpy/pytest_hardpy/result/report_loader/__init__.py +4 -0
- hardpy/pytest_hardpy/result/report_loader/stand_cloud_loader.py +72 -0
- hardpy/pytest_hardpy/utils/connection_data.py +2 -0
- {hardpy-0.9.0.dist-info → hardpy-0.10.1.dist-info}/METADATA +8 -2
- {hardpy-0.9.0.dist-info → hardpy-0.10.1.dist-info}/RECORD +29 -22
- {hardpy-0.9.0.dist-info → hardpy-0.10.1.dist-info}/WHEEL +1 -1
- hardpy/hardpy_panel/frontend/dist/static/js/main.403e9cd8.js +0 -3
- hardpy/hardpy_panel/frontend/dist/static/js/main.403e9cd8.js.map +0 -1
- /hardpy/hardpy_panel/frontend/dist/static/js/{main.403e9cd8.js.LICENSE.txt → main.8a7d8f7d.js.LICENSE.txt} +0 -0
- {hardpy-0.9.0.dist-info → hardpy-0.10.1.dist-info}/entry_points.txt +0 -0
- {hardpy-0.9.0.dist-info → hardpy-0.10.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Copyright (c) 2024 Everypin
|
|
2
|
+
# GNU General Public License v3.0 (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import secrets
|
|
12
|
+
import socket
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from contextlib import suppress
|
|
16
|
+
from platform import system
|
|
17
|
+
from time import sleep
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
from urllib.parse import urlencode
|
|
20
|
+
|
|
21
|
+
import requests
|
|
22
|
+
from keyring import delete_password, get_credential
|
|
23
|
+
from keyring.errors import KeyringError
|
|
24
|
+
from oauthlib.common import urldecode
|
|
25
|
+
from oauthlib.oauth2 import WebApplicationClient
|
|
26
|
+
from oauthlib.oauth2.rfc6749.errors import InvalidClientIdError
|
|
27
|
+
|
|
28
|
+
from hardpy.common.stand_cloud.connector import StandCloudConnector, StandCloudError
|
|
29
|
+
from hardpy.common.stand_cloud.token_storage import get_token_store
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from requests_oauth2client import BearerToken
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def login(addr: str) -> None:
|
|
36
|
+
"""Login HardPy in StandCloud.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
addr (str): StandCloud address
|
|
40
|
+
"""
|
|
41
|
+
# OAuth client configuration
|
|
42
|
+
client_id = "hardpy-report-uploader"
|
|
43
|
+
client = WebApplicationClient(client_id)
|
|
44
|
+
try:
|
|
45
|
+
sc_connector = StandCloudConnector(addr=addr)
|
|
46
|
+
except StandCloudError as exc:
|
|
47
|
+
print(str(exc))
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
verify_ssl = not __debug__
|
|
51
|
+
|
|
52
|
+
# Auth requests
|
|
53
|
+
port = _reserve_socket_port()
|
|
54
|
+
callback_process = _create_callback_process(port)
|
|
55
|
+
state = secrets.token_urlsafe(16)
|
|
56
|
+
code_verifier = _code_verifier()
|
|
57
|
+
data = _par_data(code_verifier, client_id, port, state, sc_connector.url.api)
|
|
58
|
+
timeout = 10
|
|
59
|
+
|
|
60
|
+
# fmt: off
|
|
61
|
+
# pushed authorization response
|
|
62
|
+
try:
|
|
63
|
+
response = json.loads(requests.post(sc_connector.url.par, data=data, verify=verify_ssl, timeout=timeout).content)
|
|
64
|
+
except Exception as e: # noqa: BLE001
|
|
65
|
+
print(f"Authentication server is unavailable: {e}")
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
url = (sc_connector.url.auth + "?" + urlencode({"client_id": client_id, "request_uri": response["request_uri"]}))
|
|
68
|
+
|
|
69
|
+
# OAuth authorization code request
|
|
70
|
+
print(f"\nOpen the provided URL and authorize HardPy to use StandCloud\n\n{url}")
|
|
71
|
+
|
|
72
|
+
# use subprocess
|
|
73
|
+
first_line = next(callback_process.stdout) # type: ignore
|
|
74
|
+
sleep(1)
|
|
75
|
+
|
|
76
|
+
# OAuth authorization code grant
|
|
77
|
+
response = json.loads(first_line)
|
|
78
|
+
callback_process.kill()
|
|
79
|
+
|
|
80
|
+
_check_incorrect_response(response, state)
|
|
81
|
+
|
|
82
|
+
code = response["code"]
|
|
83
|
+
uri = _redirect_uri(port)
|
|
84
|
+
data = client.prepare_request_body(code=code, client_id=client_id, redirect_uri=uri, code_verifier=code_verifier)
|
|
85
|
+
|
|
86
|
+
# OAuth access token request
|
|
87
|
+
data = dict(urldecode(data))
|
|
88
|
+
response = requests.post(sc_connector.url.token, data=data, verify=verify_ssl, timeout=timeout)
|
|
89
|
+
# fmt: on
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
# OAuth access token grant
|
|
93
|
+
response = client.parse_request_body_response(response.text)
|
|
94
|
+
except InvalidClientIdError as e:
|
|
95
|
+
print(e)
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
|
|
98
|
+
_store_password(client.token)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def logout() -> bool:
|
|
102
|
+
"""Logout HardPy from StandCloud.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
bool: True if successful else False
|
|
106
|
+
"""
|
|
107
|
+
service_name = "HardPy"
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
while cred := get_credential(service_name, None):
|
|
111
|
+
delete_password(service_name, cred.username)
|
|
112
|
+
except KeyringError:
|
|
113
|
+
return False
|
|
114
|
+
# TODO(xorialexandrov): fix keyring clearing
|
|
115
|
+
# Windows does not clear refresh token by itself
|
|
116
|
+
if system() == "Windows":
|
|
117
|
+
storage_keyring, _ = get_token_store()
|
|
118
|
+
with suppress(KeyringError):
|
|
119
|
+
storage_keyring.delete_password(service_name, "refresh_token")
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _redirect_uri(port: str) -> str:
|
|
124
|
+
return f"http://127.0.0.1:{port}/oauth2/callback"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _reserve_socket_port() -> str:
|
|
128
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
129
|
+
sock.bind(("127.0.0.1", 0))
|
|
130
|
+
port = sock.getsockname()[1]
|
|
131
|
+
sock.close()
|
|
132
|
+
|
|
133
|
+
return str(port)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _code_verifier() -> str:
|
|
137
|
+
code_verifier = base64.urlsafe_b64encode(os.urandom(40)).decode("utf-8")
|
|
138
|
+
return re.sub("[^a-zA-Z0-9]+", "", code_verifier)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _code_challenge(code_verifier: str) -> str:
|
|
142
|
+
code_challenge = hashlib.sha256(code_verifier.encode("utf-8")).digest()
|
|
143
|
+
code_challenge = base64.urlsafe_b64encode(code_challenge).decode("utf-8")
|
|
144
|
+
return code_challenge.replace("=", "")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _create_callback_process(port: str) -> subprocess.Popen:
|
|
148
|
+
args = [
|
|
149
|
+
sys.executable,
|
|
150
|
+
"-m",
|
|
151
|
+
"uvicorn",
|
|
152
|
+
"hardpy.common.stand_cloud.oauth_callback:app",
|
|
153
|
+
"--host=127.0.0.1",
|
|
154
|
+
f"--port={port}",
|
|
155
|
+
"--log-level=error",
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
if system() == "Windows":
|
|
159
|
+
env = os.environ.copy()
|
|
160
|
+
env["PYTHONUNBUFFERED"] = "1"
|
|
161
|
+
|
|
162
|
+
return subprocess.Popen( # noqa: S603
|
|
163
|
+
args,
|
|
164
|
+
stdout=subprocess.PIPE,
|
|
165
|
+
bufsize=1,
|
|
166
|
+
universal_newlines=True,
|
|
167
|
+
env=env,
|
|
168
|
+
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP, # type: ignore
|
|
169
|
+
)
|
|
170
|
+
return subprocess.Popen( # noqa: S603
|
|
171
|
+
args,
|
|
172
|
+
stdout=subprocess.PIPE,
|
|
173
|
+
bufsize=1,
|
|
174
|
+
universal_newlines=True,
|
|
175
|
+
env=dict(PYTHONUNBUFFERED="1"), # noqa: C408
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _store_password(token: BearerToken) -> None:
|
|
180
|
+
storage_keyring, mem_keyring = get_token_store()
|
|
181
|
+
|
|
182
|
+
storage_keyring.set_password(
|
|
183
|
+
"HardPy",
|
|
184
|
+
"refresh_token",
|
|
185
|
+
token["refresh_token"],
|
|
186
|
+
)
|
|
187
|
+
token_data = {
|
|
188
|
+
"access_token": token["access_token"],
|
|
189
|
+
"expires_at": token["expires_at"],
|
|
190
|
+
}
|
|
191
|
+
try:
|
|
192
|
+
mem_keyring.set_password("HardPy", "access_token", json.dumps(token_data))
|
|
193
|
+
except KeyringError as e:
|
|
194
|
+
print(e)
|
|
195
|
+
return
|
|
196
|
+
print("\nRegistration completed successfully")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _check_incorrect_response(response: dict, state: str) -> None:
|
|
200
|
+
if response["state"] != state:
|
|
201
|
+
print("Wrong state in response")
|
|
202
|
+
sys.exit(1)
|
|
203
|
+
|
|
204
|
+
if "error" in response:
|
|
205
|
+
error = response["error"]
|
|
206
|
+
error_description = response["error_description"]
|
|
207
|
+
print(f"{error}: {error_description}")
|
|
208
|
+
sys.exit(1)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _par_data(
|
|
212
|
+
code_verifier: str,
|
|
213
|
+
client_id: str,
|
|
214
|
+
port: str,
|
|
215
|
+
state: str,
|
|
216
|
+
api_url: str,
|
|
217
|
+
) -> dict:
|
|
218
|
+
"""Create pushed authorization request data.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
dict: pushed authorization request data
|
|
222
|
+
"""
|
|
223
|
+
# Code Challenge Data
|
|
224
|
+
code_challenge = _code_challenge(code_verifier)
|
|
225
|
+
|
|
226
|
+
# pushed authorization request
|
|
227
|
+
return {
|
|
228
|
+
"scope": "authelia.bearer.authz offline_access",
|
|
229
|
+
"response_type": "code",
|
|
230
|
+
"client_id": client_id,
|
|
231
|
+
"redirect_uri": _redirect_uri(port),
|
|
232
|
+
"code_challenge": code_challenge,
|
|
233
|
+
"code_challenge_method": "S256",
|
|
234
|
+
"state": state,
|
|
235
|
+
"audience": api_url,
|
|
236
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Copyright (c) 2024 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 platform import system
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from keyring.core import load_keyring
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from keyring.backend import KeyringBackend
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_token_store() -> tuple[KeyringBackend, KeyringBackend]:
|
|
15
|
+
"""Get token store.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
tuple[KeyringBackend, KeyringBackend]: token store
|
|
19
|
+
"""
|
|
20
|
+
if system() == "Linux":
|
|
21
|
+
storage_keyring = load_keyring("keyring.backends.SecretService.Keyring")
|
|
22
|
+
elif system() == "Windows":
|
|
23
|
+
storage_keyring = load_keyring("keyring.backends.Windows.WinVaultKeyring")
|
|
24
|
+
# TODO(xorialexandrov): add memory keyring or other store
|
|
25
|
+
mem_keyring = storage_keyring
|
|
26
|
+
|
|
27
|
+
return storage_keyring, mem_keyring
|
hardpy/hardpy_panel/api.py
CHANGED
|
@@ -1,6 +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
|
+
import os
|
|
4
5
|
import re
|
|
5
6
|
from enum import Enum
|
|
6
7
|
from pathlib import Path
|
|
@@ -119,11 +120,12 @@ def confirm_operator_msg(is_msg_visible: str) -> dict:
|
|
|
119
120
|
return {"status": Status.ERROR}
|
|
120
121
|
|
|
121
122
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
123
|
+
if "DEBUG_FRONTEND" not in os.environ:
|
|
124
|
+
app.mount(
|
|
125
|
+
"/",
|
|
126
|
+
StaticFiles(
|
|
127
|
+
directory=Path(__file__).parent / "frontend/dist",
|
|
128
|
+
html=True,
|
|
129
|
+
),
|
|
130
|
+
name="static",
|
|
131
|
+
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"files": {
|
|
3
3
|
"main.css": "/static/css/main.e8a862f1.css",
|
|
4
|
-
"main.js": "/static/js/main.
|
|
4
|
+
"main.js": "/static/js/main.8a7d8f7d.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.
|
|
24
|
+
"main.8a7d8f7d.js.map": "/static/js/main.8a7d8f7d.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.
|
|
34
|
+
"static/js/main.8a7d8f7d.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.
|
|
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.8a7d8f7d.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>
|