kscale 0.1.1__py3-none-any.whl → 0.1.3__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.
- kscale/__init__.py +1 -1
- kscale/conf.py +1 -0
- kscale/web/clients/base.py +94 -36
- kscale/web/clients/robot_class.py +23 -3
- kscale/web/utils.py +6 -0
- {kscale-0.1.1.dist-info → kscale-0.1.3.dist-info}/METADATA +1 -1
- {kscale-0.1.1.dist-info → kscale-0.1.3.dist-info}/RECORD +11 -11
- {kscale-0.1.1.dist-info → kscale-0.1.3.dist-info}/LICENSE +0 -0
- {kscale-0.1.1.dist-info → kscale-0.1.3.dist-info}/WHEEL +0 -0
- {kscale-0.1.1.dist-info → kscale-0.1.3.dist-info}/entry_points.txt +0 -0
- {kscale-0.1.1.dist-info → kscale-0.1.3.dist-info}/top_level.txt +0 -0
kscale/__init__.py
CHANGED
kscale/conf.py
CHANGED
kscale/web/clients/base.py
CHANGED
@@ -29,14 +29,22 @@ OAUTH_PORT = 16821
|
|
29
29
|
|
30
30
|
class OAuthCallback:
|
31
31
|
def __init__(self) -> None:
|
32
|
+
self.token_type: str | None = None
|
32
33
|
self.access_token: str | None = None
|
34
|
+
self.id_token: str | None = None
|
35
|
+
self.state: str | None = None
|
36
|
+
self.expires_in: str | None = None
|
33
37
|
self.app = web.Application()
|
34
38
|
self.app.router.add_get("/token", self.handle_token)
|
35
39
|
self.app.router.add_get("/callback", self.handle_callback)
|
36
40
|
|
37
41
|
async def handle_token(self, request: web.Request) -> web.Response:
|
38
42
|
"""Handle the token extraction."""
|
43
|
+
self.token_type = request.query.get("token_type")
|
39
44
|
self.access_token = request.query.get("access_token")
|
45
|
+
self.id_token = request.query.get("id_token")
|
46
|
+
self.state = request.query.get("state")
|
47
|
+
self.expires_in = request.query.get("expires_in")
|
40
48
|
return web.Response(text="OK")
|
41
49
|
|
42
50
|
async def handle_callback(self, request: web.Request) -> web.Response:
|
@@ -45,62 +53,92 @@ class OAuthCallback:
|
|
45
53
|
text="""
|
46
54
|
<!DOCTYPE html>
|
47
55
|
<html lang="en">
|
48
|
-
|
49
56
|
<head>
|
50
57
|
<meta charset="UTF-8">
|
51
|
-
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
52
58
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
53
59
|
<title>Authentication successful</title>
|
54
60
|
<style>
|
55
61
|
body {
|
62
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
56
63
|
display: flex;
|
57
64
|
justify-content: center;
|
58
65
|
align-items: center;
|
59
66
|
min-height: 100vh;
|
60
67
|
margin: 0;
|
61
|
-
|
68
|
+
background: #f5f5f5;
|
69
|
+
color: #333;
|
70
|
+
}
|
71
|
+
.container {
|
72
|
+
background: white;
|
73
|
+
padding: 2rem;
|
74
|
+
border-radius: 8px;
|
75
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
76
|
+
max-width: 600px;
|
77
|
+
width: 90%;
|
78
|
+
}
|
79
|
+
h1 {
|
80
|
+
color: #2c3e50;
|
81
|
+
margin-bottom: 1rem;
|
62
82
|
}
|
63
|
-
|
64
|
-
|
83
|
+
.token-info {
|
84
|
+
background: #f8f9fa;
|
85
|
+
border: 1px solid #dee2e6;
|
86
|
+
border-radius: 4px;
|
87
|
+
padding: 1rem;
|
88
|
+
margin: 1rem 0;
|
89
|
+
word-break: break-all;
|
65
90
|
}
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
margin-
|
70
|
-
|
71
|
-
|
72
|
-
|
91
|
+
.token-label {
|
92
|
+
font-weight: bold;
|
93
|
+
color: #6c757d;
|
94
|
+
margin-bottom: 0.5rem;
|
95
|
+
}
|
96
|
+
.success-icon {
|
97
|
+
color: #28a745;
|
98
|
+
font-size: 48px;
|
99
|
+
margin-bottom: 1rem;
|
73
100
|
}
|
74
101
|
</style>
|
75
102
|
</head>
|
76
|
-
|
77
103
|
<body>
|
78
|
-
<div
|
104
|
+
<div class="container">
|
105
|
+
<div class="success-icon">✓</div>
|
79
106
|
<h1>Authentication successful!</h1>
|
80
|
-
<p>
|
81
|
-
|
107
|
+
<p>Your authentication tokens are shown below. You can now close this window.</p>
|
108
|
+
|
109
|
+
<div class="token-info">
|
110
|
+
<div class="token-label">Access Token:</div>
|
111
|
+
<div id="accessTokenDisplay"></div>
|
112
|
+
</div>
|
113
|
+
|
114
|
+
<div class="token-info">
|
115
|
+
<div class="token-label">ID Token:</div>
|
116
|
+
<div id="idTokenDisplay"></div>
|
117
|
+
</div>
|
82
118
|
</div>
|
119
|
+
|
83
120
|
<script>
|
84
121
|
const params = new URLSearchParams(window.location.hash.substring(1));
|
85
|
-
const
|
86
|
-
|
87
|
-
|
122
|
+
const tokenType = params.get('token_type');
|
123
|
+
const accessToken = params.get('access_token');
|
124
|
+
const idToken = params.get('id_token');
|
125
|
+
const state = params.get('state');
|
126
|
+
const expiresIn = params.get('expires_in');
|
127
|
+
|
128
|
+
// Display tokens
|
129
|
+
document.getElementById('accessTokenDisplay').textContent = accessToken || 'Not provided';
|
130
|
+
document.getElementById('idTokenDisplay').textContent = idToken || 'Not provided';
|
131
|
+
|
132
|
+
if (accessToken) {
|
133
|
+
const tokenUrl = new URL(window.location.href);
|
134
|
+
tokenUrl.pathname = '/token';
|
135
|
+
tokenUrl.searchParams.set('access_token', accessToken);
|
136
|
+
tokenUrl.searchParams.set('token_type', tokenType);
|
137
|
+
tokenUrl.searchParams.set('id_token', idToken);
|
138
|
+
tokenUrl.searchParams.set('state', state);
|
139
|
+
tokenUrl.searchParams.set('expires_in', expiresIn);
|
140
|
+
fetch(tokenUrl.toString());
|
88
141
|
}
|
89
|
-
|
90
|
-
let timeLeft = 3;
|
91
|
-
const countdownElement = document.getElementById('countdown');
|
92
|
-
const closeNotification = document.getElementById('closeNotification');
|
93
|
-
const timer = setInterval(() => {
|
94
|
-
timeLeft--;
|
95
|
-
countdownElement.textContent = timeLeft;
|
96
|
-
if (timeLeft <= 0) {
|
97
|
-
clearInterval(timer);
|
98
|
-
window.close();
|
99
|
-
setTimeout(() => {
|
100
|
-
closeNotification.style.display = 'block';
|
101
|
-
}, 500);
|
102
|
-
}
|
103
|
-
}, 1000);
|
104
142
|
</script>
|
105
143
|
</body>
|
106
144
|
</html>
|
@@ -167,8 +205,23 @@ class BaseClient:
|
|
167
205
|
oicd_info = await self._get_oicd_info()
|
168
206
|
metadata = await self._get_oicd_metadata()
|
169
207
|
auth_endpoint = metadata["authorization_endpoint"]
|
170
|
-
|
171
|
-
nonce
|
208
|
+
|
209
|
+
# Use the cached state and nonce if available, otherwise generate.
|
210
|
+
state_file = get_cache_dir() / "oauth_state.json"
|
211
|
+
state: str | None = None
|
212
|
+
nonce: str | None = None
|
213
|
+
if state_file.exists():
|
214
|
+
with open(state_file, "r") as f:
|
215
|
+
state_data = json.load(f)
|
216
|
+
state = state_data.get("state")
|
217
|
+
nonce = state_data.get("nonce")
|
218
|
+
if state is None:
|
219
|
+
state = secrets.token_urlsafe(32)
|
220
|
+
if nonce is None:
|
221
|
+
nonce = secrets.token_urlsafe(32)
|
222
|
+
|
223
|
+
# Change /oauth2/authorize to /login to use the login endpoint.
|
224
|
+
auth_endpoint = auth_endpoint.replace("/oauth2/authorize", "/login")
|
172
225
|
|
173
226
|
auth_url = str(
|
174
227
|
URL(auth_endpoint).with_query(
|
@@ -208,6 +261,11 @@ class BaseClient:
|
|
208
261
|
raise TimeoutError("Authentication timed out after 30 seconds")
|
209
262
|
await asyncio.sleep(0.1)
|
210
263
|
|
264
|
+
# Save the state and nonce to the cache.
|
265
|
+
state = callback_handler.state
|
266
|
+
state_file.parent.mkdir(parents=True, exist_ok=True)
|
267
|
+
state_file.write_text(json.dumps({"state": state, "nonce": nonce}))
|
268
|
+
|
211
269
|
return callback_handler.access_token
|
212
270
|
finally:
|
213
271
|
await runner.cleanup()
|
@@ -1,14 +1,19 @@
|
|
1
1
|
"""Defines the client for interacting with the K-Scale robot class endpoints."""
|
2
2
|
|
3
3
|
import hashlib
|
4
|
+
import json
|
4
5
|
import logging
|
5
6
|
from pathlib import Path
|
6
7
|
|
7
8
|
import httpx
|
8
9
|
|
9
10
|
from kscale.web.clients.base import BaseClient
|
10
|
-
from kscale.web.gen.api import
|
11
|
-
|
11
|
+
from kscale.web.gen.api import (
|
12
|
+
RobotClass,
|
13
|
+
RobotDownloadURDFResponse,
|
14
|
+
RobotUploadURDFResponse,
|
15
|
+
)
|
16
|
+
from kscale.web.utils import get_cache_dir, should_refresh_file
|
12
17
|
|
13
18
|
logger = logging.getLogger(__name__)
|
14
19
|
|
@@ -94,13 +99,22 @@ class RobotClassClient(BaseClient):
|
|
94
99
|
|
95
100
|
async def download_robot_class_urdf(self, class_name: str, *, cache: bool = True) -> Path:
|
96
101
|
cache_path = get_cache_dir() / class_name / "robot.tgz"
|
97
|
-
if cache and cache_path.exists():
|
102
|
+
if cache and cache_path.exists() and not should_refresh_file(cache_path):
|
98
103
|
return cache_path
|
99
104
|
data = await self._request("GET", f"/robots/urdf/{class_name}", auth=True)
|
100
105
|
response = RobotDownloadURDFResponse.model_validate(data)
|
101
106
|
expected_hash = response.md5_hash
|
102
107
|
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
103
108
|
|
109
|
+
# Checks the md5 hash of the file.
|
110
|
+
cache_path_info = cache_path.parent / "info.json"
|
111
|
+
if cache_path_info.exists():
|
112
|
+
with open(cache_path_info, "r") as f:
|
113
|
+
info = json.load(f)
|
114
|
+
if info["md5_hash"] == expected_hash:
|
115
|
+
cache_path.touch()
|
116
|
+
return cache_path
|
117
|
+
|
104
118
|
logger.info("Downloading URDF file from %s", response.url)
|
105
119
|
async with httpx.AsyncClient(
|
106
120
|
timeout=httpx.Timeout(DOWNLOAD_TIMEOUT),
|
@@ -118,4 +132,10 @@ class RobotClassClient(BaseClient):
|
|
118
132
|
if hash_value_hex != expected_hash:
|
119
133
|
raise ValueError(f"MD5 hash mismatch: {hash_value_hex} != {expected_hash}")
|
120
134
|
|
135
|
+
# Updates the info file.
|
136
|
+
logger.info("Updating downloaded file information")
|
137
|
+
info = {"md5_hash": hash_value_hex}
|
138
|
+
with open(cache_path_info, "w") as f:
|
139
|
+
json.dump(info, f)
|
140
|
+
|
121
141
|
return cache_path
|
kscale/web/utils.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
import functools
|
4
4
|
import logging
|
5
|
+
import time
|
5
6
|
from pathlib import Path
|
6
7
|
|
7
8
|
from kscale.conf import Settings
|
@@ -17,6 +18,11 @@ def get_cache_dir() -> Path:
|
|
17
18
|
return Path(Settings.load().www.cache_dir).expanduser().resolve()
|
18
19
|
|
19
20
|
|
21
|
+
def should_refresh_file(file: Path) -> bool:
|
22
|
+
"""Returns whether the file should be refreshed."""
|
23
|
+
return file.exists() and file.stat().st_mtime < time.time() - Settings.load().www.refresh_interval_minutes * 60
|
24
|
+
|
25
|
+
|
20
26
|
@functools.lru_cache
|
21
27
|
def get_artifact_dir(artifact_id: str) -> Path:
|
22
28
|
"""Returns the directory for a specific artifact."""
|
@@ -1,7 +1,7 @@
|
|
1
|
-
kscale/__init__.py,sha256=
|
1
|
+
kscale/__init__.py,sha256=jCPBaF41wGta1VKhadMOzjzqpwaKVoU5lWY21BAvCZY,172
|
2
2
|
kscale/api.py,sha256=jmiuFurTN_Gj_-k-6asqxw8wp-_bgJUXgMPFgJ4lqHA,230
|
3
3
|
kscale/cli.py,sha256=PMHLKR5UwdbbReVmqHXpJ-K9-mGHv_0I7KQkwxmFcUA,881
|
4
|
-
kscale/conf.py,sha256=
|
4
|
+
kscale/conf.py,sha256=x9jJ8pmSweUsLScN741B21SySCrnRZioV-1ZkhmERbI,1585
|
5
5
|
kscale/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
6
|
kscale/requirements-dev.txt,sha256=WI7-ea4IRJakmqVMN8QKhOsDGrghwtvk03aIsFaNSIw,130
|
7
7
|
kscale/requirements.txt,sha256=_BGbnKTQaXKx0bNEG0wguod9swsiCb2mF6rLm7sFJ2Q,214
|
@@ -14,23 +14,23 @@ kscale/utils/checksum.py,sha256=jt6QmmQND9zrOEnUtOfZpLYROhgto4Gh3OpdUWk4tZA,1093
|
|
14
14
|
kscale/utils/cli.py,sha256=JoaY5x5SdUx97KmMM9j5AjRRUqqrTlJ9qVckZptEsYA,827
|
15
15
|
kscale/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
16
|
kscale/web/api.py,sha256=YioIdruq7LCKSBf9SvOGkv914W36_zBmpTzsJqKc0wE,439
|
17
|
-
kscale/web/utils.py,sha256=
|
17
|
+
kscale/web/utils.py,sha256=l_m8GETCThhnXbHfXq53j1b-E__uZ5GuUbsQ8HR0WA0,1037
|
18
18
|
kscale/web/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
19
|
kscale/web/cli/robot.py,sha256=rI-A4_0uvJPeA71Apl4Z3mV5fIfWkgmzT9JRmJYxz3A,3307
|
20
20
|
kscale/web/cli/robot_class.py,sha256=ymC5phUqofvOXv5P6f51b9lMK5eRDaavvnzS0x9rDbU,3574
|
21
21
|
kscale/web/cli/token.py,sha256=1rFC8MYKtqbNsQa2KIqwW1tqpaMtFaxuNsallwejXTU,787
|
22
22
|
kscale/web/cli/user.py,sha256=qO0z2K5uA48hEiOOYEzv6BO2nOlCpITTDZFuiNl6d34,817
|
23
23
|
kscale/web/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
24
|
-
kscale/web/clients/base.py,sha256=
|
24
|
+
kscale/web/clients/base.py,sha256=voOgOGlrYy-fyFJiLVNVwL4osOo53-ARBsqdhBp4BWA,14263
|
25
25
|
kscale/web/clients/client.py,sha256=QjBicdHQYNoUG9XRjAYmGu3THae9DzWa_hQox3OO1Gw,214
|
26
26
|
kscale/web/clients/robot.py,sha256=HMfJnkDxaJ_o7X2vdYYS9iob1JRoBG2qiGmQpCQZpAk,1485
|
27
|
-
kscale/web/clients/robot_class.py,sha256=
|
27
|
+
kscale/web/clients/robot_class.py,sha256=yP-GBToyTyNc9eZzsoVmQuNC98aQHmgScxwsxRiM2v8,5067
|
28
28
|
kscale/web/clients/user.py,sha256=9iv8J-ROm_yBIwi-0oqldReLkNBFktdHRv3UCOxBzjY,377
|
29
29
|
kscale/web/gen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
30
30
|
kscale/web/gen/api.py,sha256=SovcII36JFgK9jd2CXlLPMjiUROGB4vEnapOsYMUrkU,2188
|
31
|
-
kscale-0.1.
|
32
|
-
kscale-0.1.
|
33
|
-
kscale-0.1.
|
34
|
-
kscale-0.1.
|
35
|
-
kscale-0.1.
|
36
|
-
kscale-0.1.
|
31
|
+
kscale-0.1.3.dist-info/LICENSE,sha256=HCN2bImAzUOXldAZZI7JZ9PYq6OwMlDAP_PpX1HnuN0,1071
|
32
|
+
kscale-0.1.3.dist-info/METADATA,sha256=Aw4Q4bvhclIV1tG_1a8Gk-ZylIPe84vUUtIkQLXBa68,2340
|
33
|
+
kscale-0.1.3.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
34
|
+
kscale-0.1.3.dist-info/entry_points.txt,sha256=N_0pCpPnwGDYVzOeuaSOrbJkS5L3lS9d8CxpJF1f8UI,62
|
35
|
+
kscale-0.1.3.dist-info/top_level.txt,sha256=C2ynjYwopg6YjgttnI2dJjasyq3EKNmYp-IfQg9Xms4,7
|
36
|
+
kscale-0.1.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|