kscale 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
kscale/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Defines the common interface for the K-Scale Python API."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.3"
4
4
 
5
5
  from pathlib import Path
6
6
 
@@ -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
- text-align: center;
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
- #content {
64
- padding: 20px;
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
- #closeNotification {
67
- display: none;
68
- padding: 10px 20px;
69
- margin-top: 20px;
70
- cursor: pointer;
71
- margin-left: auto;
72
- margin-right: auto;
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 id="content">
104
+ <div class="container">
105
+ <div class="success-icon">✓</div>
79
106
  <h1>Authentication successful!</h1>
80
- <p>This window will close in <span id="countdown">3</span> seconds.</p>
81
- <p id="closeNotification" onclick="window.close()">Please close this window manually.</p>
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 token = params.get('access_token');
86
- if (token) {
87
- fetch('/token?access_token=' + token);
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
- state = secrets.token_urlsafe(32)
171
- nonce = secrets.token_urlsafe(32)
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()
@@ -8,7 +8,11 @@ from pathlib import Path
8
8
  import httpx
9
9
 
10
10
  from kscale.web.clients.base import BaseClient
11
- from kscale.web.gen.api import RobotClass, RobotDownloadURDFResponse, RobotUploadURDFResponse
11
+ from kscale.web.gen.api import (
12
+ RobotClass,
13
+ RobotDownloadURDFResponse,
14
+ RobotUploadURDFResponse,
15
+ )
12
16
  from kscale.web.utils import get_cache_dir, should_refresh_file
13
17
 
14
18
  logger = logging.getLogger(__name__)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: kscale
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: The kscale project
5
5
  Home-page: https://github.com/kscalelabs/kscale
6
6
  Author: Benjamin Bolte
@@ -1,4 +1,4 @@
1
- kscale/__init__.py,sha256=Do_oyhXSMNLURbvLDeTx4nJqgYgPhuREBynKDoeJyX4,172
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
4
  kscale/conf.py,sha256=x9jJ8pmSweUsLScN741B21SySCrnRZioV-1ZkhmERbI,1585
@@ -21,16 +21,16 @@ kscale/web/cli/robot_class.py,sha256=ymC5phUqofvOXv5P6f51b9lMK5eRDaavvnzS0x9rDbU
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=NRqRH2JTXjJLUMDM93txKgwFu9bQRYhCYLYy7vE76Ik,11462
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=vFXlvwQBmt7zEOixAI-mByBEkXCMZIW5OZjiESMavps,5050
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.2.dist-info/LICENSE,sha256=HCN2bImAzUOXldAZZI7JZ9PYq6OwMlDAP_PpX1HnuN0,1071
32
- kscale-0.1.2.dist-info/METADATA,sha256=YEcA0zqZwXvW9BnfZn51PrTXcwemCxMuolC1jB7NG-g,2340
33
- kscale-0.1.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
34
- kscale-0.1.2.dist-info/entry_points.txt,sha256=N_0pCpPnwGDYVzOeuaSOrbJkS5L3lS9d8CxpJF1f8UI,62
35
- kscale-0.1.2.dist-info/top_level.txt,sha256=C2ynjYwopg6YjgttnI2dJjasyq3EKNmYp-IfQg9Xms4,7
36
- kscale-0.1.2.dist-info/RECORD,,
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