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