kscale 0.1.1__tar.gz → 0.1.3__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. {kscale-0.1.1/kscale.egg-info → kscale-0.1.3}/PKG-INFO +1 -1
  2. {kscale-0.1.1 → kscale-0.1.3}/kscale/__init__.py +1 -1
  3. {kscale-0.1.1 → kscale-0.1.3}/kscale/conf.py +1 -0
  4. {kscale-0.1.1 → kscale-0.1.3}/kscale/web/clients/base.py +94 -36
  5. {kscale-0.1.1 → kscale-0.1.3}/kscale/web/clients/robot_class.py +23 -3
  6. {kscale-0.1.1 → kscale-0.1.3}/kscale/web/utils.py +6 -0
  7. {kscale-0.1.1 → kscale-0.1.3/kscale.egg-info}/PKG-INFO +1 -1
  8. {kscale-0.1.1 → kscale-0.1.3}/LICENSE +0 -0
  9. {kscale-0.1.1 → kscale-0.1.3}/MANIFEST.in +0 -0
  10. {kscale-0.1.1 → kscale-0.1.3}/README.md +0 -0
  11. {kscale-0.1.1 → kscale-0.1.3}/kscale/api.py +0 -0
  12. {kscale-0.1.1 → kscale-0.1.3}/kscale/artifacts/__init__.py +0 -0
  13. {kscale-0.1.1 → kscale-0.1.3}/kscale/artifacts/plane.obj +0 -0
  14. {kscale-0.1.1 → kscale-0.1.3}/kscale/artifacts/plane.urdf +0 -0
  15. {kscale-0.1.1 → kscale-0.1.3}/kscale/cli.py +0 -0
  16. {kscale-0.1.1 → kscale-0.1.3}/kscale/py.typed +0 -0
  17. {kscale-0.1.1 → kscale-0.1.3}/kscale/requirements-dev.txt +0 -0
  18. {kscale-0.1.1 → kscale-0.1.3}/kscale/requirements.txt +0 -0
  19. {kscale-0.1.1 → kscale-0.1.3}/kscale/utils/__init__.py +0 -0
  20. {kscale-0.1.1 → kscale-0.1.3}/kscale/utils/api_base.py +0 -0
  21. {kscale-0.1.1 → kscale-0.1.3}/kscale/utils/checksum.py +0 -0
  22. {kscale-0.1.1 → kscale-0.1.3}/kscale/utils/cli.py +0 -0
  23. {kscale-0.1.1 → kscale-0.1.3}/kscale/web/__init__.py +0 -0
  24. {kscale-0.1.1 → kscale-0.1.3}/kscale/web/api.py +0 -0
  25. {kscale-0.1.1 → kscale-0.1.3}/kscale/web/cli/__init__.py +0 -0
  26. {kscale-0.1.1 → kscale-0.1.3}/kscale/web/cli/robot.py +0 -0
  27. {kscale-0.1.1 → kscale-0.1.3}/kscale/web/cli/robot_class.py +0 -0
  28. {kscale-0.1.1 → kscale-0.1.3}/kscale/web/cli/token.py +0 -0
  29. {kscale-0.1.1 → kscale-0.1.3}/kscale/web/cli/user.py +0 -0
  30. {kscale-0.1.1 → kscale-0.1.3}/kscale/web/clients/__init__.py +0 -0
  31. {kscale-0.1.1 → kscale-0.1.3}/kscale/web/clients/client.py +0 -0
  32. {kscale-0.1.1 → kscale-0.1.3}/kscale/web/clients/robot.py +0 -0
  33. {kscale-0.1.1 → kscale-0.1.3}/kscale/web/clients/user.py +0 -0
  34. {kscale-0.1.1 → kscale-0.1.3}/kscale/web/gen/__init__.py +0 -0
  35. {kscale-0.1.1 → kscale-0.1.3}/kscale/web/gen/api.py +0 -0
  36. {kscale-0.1.1 → kscale-0.1.3}/kscale.egg-info/SOURCES.txt +0 -0
  37. {kscale-0.1.1 → kscale-0.1.3}/kscale.egg-info/dependency_links.txt +0 -0
  38. {kscale-0.1.1 → kscale-0.1.3}/kscale.egg-info/entry_points.txt +0 -0
  39. {kscale-0.1.1 → kscale-0.1.3}/kscale.egg-info/not-zip-safe +0 -0
  40. {kscale-0.1.1 → kscale-0.1.3}/kscale.egg-info/requires.txt +0 -0
  41. {kscale-0.1.1 → kscale-0.1.3}/kscale.egg-info/top_level.txt +0 -0
  42. {kscale-0.1.1 → kscale-0.1.3}/pyproject.toml +0 -0
  43. {kscale-0.1.1 → kscale-0.1.3}/setup.cfg +0 -0
  44. {kscale-0.1.1 → kscale-0.1.3}/setup.py +0 -0
  45. {kscale-0.1.1 → kscale-0.1.3}/tests/test_dummy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: kscale
3
- Version: 0.1.1
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,6 +1,6 @@
1
1
  """Defines the common interface for the K-Scale Python API."""
2
2
 
3
- __version__ = "0.1.1"
3
+ __version__ = "0.1.3"
4
4
 
5
5
  from pathlib import Path
6
6
 
@@ -22,6 +22,7 @@ def get_path() -> Path:
22
22
  class WWWSettings:
23
23
  api_root: str = field(default=DEFAULT_API_ROOT)
24
24
  cache_dir: str = field(default=II("oc.env:KSCALE_CACHE_DIR,'~/.kscale/cache/'"))
25
+ refresh_interval_minutes: int = field(default=60 * 24)
25
26
 
26
27
 
27
28
  @dataclass
@@ -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()
@@ -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 RobotClass, RobotDownloadURDFResponse, RobotUploadURDFResponse
11
- from kscale.web.utils import get_cache_dir
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
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: kscale
3
- Version: 0.1.1
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes