kscale 0.3.15__py3-none-any.whl → 0.3.17__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/web/cli/robot_class.py +4 -4
- kscale/web/cli/user.py +0 -9
- kscale/web/clients/base.py +4 -303
- kscale/web/clients/user.py +0 -4
- kscale/web/gen/api.py +78 -2
- {kscale-0.3.15.dist-info → kscale-0.3.17.dist-info}/METADATA +1 -1
- {kscale-0.3.15.dist-info → kscale-0.3.17.dist-info}/RECORD +12 -12
- {kscale-0.3.15.dist-info → kscale-0.3.17.dist-info}/WHEEL +1 -1
- {kscale-0.3.15.dist-info → kscale-0.3.17.dist-info}/entry_points.txt +0 -0
- {kscale-0.3.15.dist-info → kscale-0.3.17.dist-info}/licenses/LICENSE +0 -0
- {kscale-0.3.15.dist-info → kscale-0.3.17.dist-info}/top_level.txt +0 -0
kscale/__init__.py
CHANGED
kscale/web/cli/robot_class.py
CHANGED
@@ -192,7 +192,7 @@ async def run_pybullet(
|
|
192
192
|
) -> None:
|
193
193
|
"""Shows the URDF file for a robot class in PyBullet."""
|
194
194
|
try:
|
195
|
-
import pybullet as p
|
195
|
+
import pybullet as p # noqa: PLC0415
|
196
196
|
except ImportError:
|
197
197
|
click.echo(click.style("PyBullet is not installed; install it with `pip install pybullet`", fg="red"))
|
198
198
|
return
|
@@ -453,14 +453,14 @@ async def run_mujoco(class_name: str, scene: str, no_cache: bool) -> None:
|
|
453
453
|
launches the Mujoco viewer using the provided MJCF file.
|
454
454
|
"""
|
455
455
|
try:
|
456
|
-
from mujoco_scenes.errors import TemplateNotFoundError
|
457
|
-
from mujoco_scenes.mjcf import list_scenes, load_mjmodel
|
456
|
+
from mujoco_scenes.errors import TemplateNotFoundError # noqa: PLC0415
|
457
|
+
from mujoco_scenes.mjcf import list_scenes, load_mjmodel # noqa: PLC0415
|
458
458
|
except ImportError:
|
459
459
|
click.echo(click.style("Mujoco Scenes is required; install with `pip install mujoco-scenes`", fg="red"))
|
460
460
|
return
|
461
461
|
|
462
462
|
try:
|
463
|
-
import mujoco.viewer
|
463
|
+
import mujoco.viewer # noqa: PLC0415
|
464
464
|
except ImportError:
|
465
465
|
click.echo(click.style("Mujoco is required; install with `pip install mujoco`", fg="red"))
|
466
466
|
return
|
kscale/web/cli/user.py
CHANGED
@@ -39,14 +39,5 @@ async def me() -> None:
|
|
39
39
|
)
|
40
40
|
|
41
41
|
|
42
|
-
@cli.command()
|
43
|
-
@coro
|
44
|
-
async def key() -> None:
|
45
|
-
"""Get an API key for the currently-authenticated user."""
|
46
|
-
client = UserClient()
|
47
|
-
api_key = await client.get_api_key()
|
48
|
-
click.echo(f"API key: {click.style(api_key, fg='green')}")
|
49
|
-
|
50
|
-
|
51
42
|
if __name__ == "__main__":
|
52
43
|
cli()
|
kscale/web/clients/base.py
CHANGED
@@ -1,33 +1,19 @@
|
|
1
1
|
"""Defines a base client for the K-Scale WWW API client."""
|
2
2
|
|
3
|
-
import asyncio
|
4
|
-
import json
|
5
3
|
import logging
|
6
4
|
import os
|
7
|
-
import secrets
|
8
5
|
import sys
|
9
|
-
import time
|
10
|
-
import webbrowser
|
11
6
|
from types import TracebackType
|
12
7
|
from typing import Any, Mapping, Self, Type
|
13
8
|
from urllib.parse import urljoin
|
14
9
|
|
15
|
-
import aiohttp
|
16
10
|
import httpx
|
17
|
-
from aiohttp import web
|
18
|
-
from async_lru import alru_cache
|
19
|
-
from jwt import ExpiredSignatureError, PyJWKClient, decode as jwt_decode
|
20
11
|
from pydantic import BaseModel
|
21
|
-
from yarl import URL
|
22
12
|
|
23
|
-
from kscale.web.
|
24
|
-
from kscale.web.utils import DEFAULT_UPLOAD_TIMEOUT, get_api_root, get_auth_dir
|
13
|
+
from kscale.web.utils import DEFAULT_UPLOAD_TIMEOUT, get_api_root
|
25
14
|
|
26
15
|
logger = logging.getLogger(__name__)
|
27
16
|
|
28
|
-
# This port matches the available port for the OAuth callback.
|
29
|
-
OAUTH_PORT = 16821
|
30
|
-
|
31
17
|
# This is the name of the API key header for the K-Scale WWW API.
|
32
18
|
HEADER_NAME = "x-kscale-api-key"
|
33
19
|
|
@@ -36,126 +22,6 @@ def verbose_error() -> bool:
|
|
36
22
|
return os.environ.get("KSCALE_VERBOSE_ERROR", "0") == "1"
|
37
23
|
|
38
24
|
|
39
|
-
class OAuthCallback:
|
40
|
-
def __init__(self) -> None:
|
41
|
-
self.token_type: str | None = None
|
42
|
-
self.access_token: str | None = None
|
43
|
-
self.id_token: str | None = None
|
44
|
-
self.state: str | None = None
|
45
|
-
self.expires_in: str | None = None
|
46
|
-
self.app = web.Application()
|
47
|
-
self.app.router.add_get("/token", self.handle_token)
|
48
|
-
self.app.router.add_get("/callback", self.handle_callback)
|
49
|
-
|
50
|
-
async def handle_token(self, request: web.Request) -> web.Response:
|
51
|
-
"""Handle the token extraction."""
|
52
|
-
self.token_type = request.query.get("token_type")
|
53
|
-
self.access_token = request.query.get("access_token")
|
54
|
-
self.id_token = request.query.get("id_token")
|
55
|
-
self.state = request.query.get("state")
|
56
|
-
self.expires_in = request.query.get("expires_in")
|
57
|
-
return web.Response(text="OK")
|
58
|
-
|
59
|
-
async def handle_callback(self, request: web.Request) -> web.Response:
|
60
|
-
"""Handle the OAuth callback with token in URL fragment."""
|
61
|
-
return web.Response(
|
62
|
-
text="""
|
63
|
-
<!DOCTYPE html>
|
64
|
-
<html lang="en">
|
65
|
-
<head>
|
66
|
-
<meta charset="UTF-8">
|
67
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
68
|
-
<title>Authentication successful</title>
|
69
|
-
<style>
|
70
|
-
body {
|
71
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
72
|
-
display: flex;
|
73
|
-
justify-content: center;
|
74
|
-
align-items: center;
|
75
|
-
min-height: 100vh;
|
76
|
-
margin: 0;
|
77
|
-
background: #f5f5f5;
|
78
|
-
color: #333;
|
79
|
-
}
|
80
|
-
.container {
|
81
|
-
background: white;
|
82
|
-
padding: 2rem;
|
83
|
-
border-radius: 8px;
|
84
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
85
|
-
max-width: 600px;
|
86
|
-
width: 90%;
|
87
|
-
}
|
88
|
-
h1 {
|
89
|
-
color: #2c3e50;
|
90
|
-
margin-bottom: 1rem;
|
91
|
-
}
|
92
|
-
.token-info {
|
93
|
-
background: #f8f9fa;
|
94
|
-
border: 1px solid #dee2e6;
|
95
|
-
border-radius: 4px;
|
96
|
-
padding: 1rem;
|
97
|
-
margin: 1rem 0;
|
98
|
-
word-break: break-all;
|
99
|
-
}
|
100
|
-
.token-label {
|
101
|
-
font-weight: bold;
|
102
|
-
color: #6c757d;
|
103
|
-
margin-bottom: 0.5rem;
|
104
|
-
}
|
105
|
-
.success-icon {
|
106
|
-
color: #28a745;
|
107
|
-
font-size: 48px;
|
108
|
-
margin-bottom: 1rem;
|
109
|
-
}
|
110
|
-
</style>
|
111
|
-
</head>
|
112
|
-
<body>
|
113
|
-
<div class="container">
|
114
|
-
<div class="success-icon">✓</div>
|
115
|
-
<h1>Authentication successful!</h1>
|
116
|
-
<p>Your authentication tokens are shown below. You can now close this window.</p>
|
117
|
-
|
118
|
-
<div class="token-info">
|
119
|
-
<div class="token-label">Access Token:</div>
|
120
|
-
<div id="accessTokenDisplay"></div>
|
121
|
-
</div>
|
122
|
-
|
123
|
-
<div class="token-info">
|
124
|
-
<div class="token-label">ID Token:</div>
|
125
|
-
<div id="idTokenDisplay"></div>
|
126
|
-
</div>
|
127
|
-
</div>
|
128
|
-
|
129
|
-
<script>
|
130
|
-
const params = new URLSearchParams(window.location.hash.substring(1));
|
131
|
-
const tokenType = params.get('token_type');
|
132
|
-
const accessToken = params.get('access_token');
|
133
|
-
const idToken = params.get('id_token');
|
134
|
-
const state = params.get('state');
|
135
|
-
const expiresIn = params.get('expires_in');
|
136
|
-
|
137
|
-
// Display tokens
|
138
|
-
document.getElementById('accessTokenDisplay').textContent = accessToken || 'Not provided';
|
139
|
-
document.getElementById('idTokenDisplay').textContent = idToken || 'Not provided';
|
140
|
-
|
141
|
-
if (accessToken) {
|
142
|
-
const tokenUrl = new URL(window.location.href);
|
143
|
-
tokenUrl.pathname = '/token';
|
144
|
-
tokenUrl.searchParams.set('access_token', accessToken);
|
145
|
-
tokenUrl.searchParams.set('token_type', tokenType);
|
146
|
-
tokenUrl.searchParams.set('id_token', idToken);
|
147
|
-
tokenUrl.searchParams.set('state', state);
|
148
|
-
tokenUrl.searchParams.set('expires_in', expiresIn);
|
149
|
-
fetch(tokenUrl.toString());
|
150
|
-
}
|
151
|
-
</script>
|
152
|
-
</body>
|
153
|
-
</html>
|
154
|
-
""",
|
155
|
-
content_type="text/html",
|
156
|
-
)
|
157
|
-
|
158
|
-
|
159
25
|
class BaseClient:
|
160
26
|
def __init__(
|
161
27
|
self,
|
@@ -169,179 +35,14 @@ class BaseClient:
|
|
169
35
|
self._client: httpx.AsyncClient | None = None
|
170
36
|
self._client_no_auth: httpx.AsyncClient | None = None
|
171
37
|
|
172
|
-
@alru_cache
|
173
|
-
async def _get_oicd_info(self) -> OICDInfo:
|
174
|
-
cache_path = get_auth_dir() / "oicd_info.json"
|
175
|
-
if self.use_cache and cache_path.exists():
|
176
|
-
with open(cache_path, "r") as f:
|
177
|
-
return OICDInfo(**json.load(f))
|
178
|
-
data = await self._request("GET", "/auth/oicd", auth=False)
|
179
|
-
if self.use_cache:
|
180
|
-
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
181
|
-
with open(cache_path, "w") as f:
|
182
|
-
json.dump(data, f)
|
183
|
-
return OICDInfo(**data)
|
184
|
-
|
185
|
-
@alru_cache
|
186
|
-
async def _get_oicd_metadata(self) -> dict:
|
187
|
-
"""Returns the OpenID Connect server configuration.
|
188
|
-
|
189
|
-
Returns:
|
190
|
-
The OpenID Connect server configuration.
|
191
|
-
"""
|
192
|
-
cache_path = get_auth_dir() / "oicd_metadata.json"
|
193
|
-
if self.use_cache and cache_path.exists():
|
194
|
-
with open(cache_path, "r") as f:
|
195
|
-
return json.load(f)
|
196
|
-
oicd_info = await self._get_oicd_info()
|
197
|
-
oicd_config_url = f"{oicd_info.authority}/.well-known/openid-configuration"
|
198
|
-
async with aiohttp.ClientSession() as session:
|
199
|
-
async with session.get(oicd_config_url) as response:
|
200
|
-
metadata = await response.json()
|
201
|
-
if self.use_cache:
|
202
|
-
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
203
|
-
with open(cache_path, "w") as f:
|
204
|
-
json.dump(metadata, f, indent=2)
|
205
|
-
logger.info("Cached OpenID Connect metadata to %s", cache_path)
|
206
|
-
return metadata
|
207
|
-
|
208
|
-
async def _get_bearer_token(self) -> str:
|
209
|
-
"""Get a bearer token using the OAuth2 implicit flow.
|
210
|
-
|
211
|
-
Returns:
|
212
|
-
A bearer token to use with the K-Scale WWW API.
|
213
|
-
"""
|
214
|
-
# Check if we are in a headless environment.
|
215
|
-
error_message = (
|
216
|
-
"Cannot perform browser-based authentication in a headless environment. "
|
217
|
-
"Please use 'kscale user key' to generate an API key locally and set "
|
218
|
-
"the KSCALE_API_KEY environment variable instead."
|
219
|
-
)
|
220
|
-
try:
|
221
|
-
if not webbrowser.get().name != "null":
|
222
|
-
raise RuntimeError(error_message)
|
223
|
-
except webbrowser.Error:
|
224
|
-
raise RuntimeError(error_message)
|
225
|
-
|
226
|
-
oicd_info = await self._get_oicd_info()
|
227
|
-
metadata = await self._get_oicd_metadata()
|
228
|
-
auth_endpoint = metadata["authorization_endpoint"]
|
229
|
-
|
230
|
-
# Use the cached state and nonce if available, otherwise generate.
|
231
|
-
state_file = get_auth_dir() / "oauth_state.json"
|
232
|
-
state: str | None = None
|
233
|
-
nonce: str | None = None
|
234
|
-
if state_file.exists():
|
235
|
-
with open(state_file, "r") as f:
|
236
|
-
state_data = json.load(f)
|
237
|
-
state = state_data.get("state")
|
238
|
-
nonce = state_data.get("nonce")
|
239
|
-
if state is None:
|
240
|
-
state = secrets.token_urlsafe(32)
|
241
|
-
if nonce is None:
|
242
|
-
nonce = secrets.token_urlsafe(32)
|
243
|
-
|
244
|
-
# Change /oauth2/authorize to /login to use the login endpoint.
|
245
|
-
auth_endpoint = auth_endpoint.replace("/oauth2/authorize", "/login")
|
246
|
-
|
247
|
-
auth_url = str(
|
248
|
-
URL(auth_endpoint).with_query(
|
249
|
-
{
|
250
|
-
"response_type": "token",
|
251
|
-
"redirect_uri": f"http://localhost:{OAUTH_PORT}/callback",
|
252
|
-
"state": state,
|
253
|
-
"nonce": nonce,
|
254
|
-
"scope": "openid profile email",
|
255
|
-
"client_id": oicd_info.client_id,
|
256
|
-
}
|
257
|
-
)
|
258
|
-
)
|
259
|
-
|
260
|
-
# Start local server to receive callback
|
261
|
-
callback_handler = OAuthCallback()
|
262
|
-
runner = web.AppRunner(callback_handler.app)
|
263
|
-
await runner.setup()
|
264
|
-
site = web.TCPSite(runner, "localhost", OAUTH_PORT)
|
265
|
-
|
266
|
-
try:
|
267
|
-
await site.start()
|
268
|
-
except OSError as e:
|
269
|
-
raise OSError(
|
270
|
-
f"The command line interface requires access to local port {OAUTH_PORT} in order to authenticate with "
|
271
|
-
"OpenID Connect. Please ensure that no other application is using this port."
|
272
|
-
) from e
|
273
|
-
|
274
|
-
# Open browser for user authentication
|
275
|
-
webbrowser.open(auth_url)
|
276
|
-
|
277
|
-
# Wait for the callback with timeout
|
278
|
-
try:
|
279
|
-
start_time = time.time()
|
280
|
-
while callback_handler.access_token is None:
|
281
|
-
if time.time() - start_time > 30:
|
282
|
-
raise TimeoutError("Authentication timed out after 30 seconds")
|
283
|
-
await asyncio.sleep(0.1)
|
284
|
-
|
285
|
-
# Save the state and nonce to the cache.
|
286
|
-
state = callback_handler.state
|
287
|
-
state_file.parent.mkdir(parents=True, exist_ok=True)
|
288
|
-
state_file.write_text(json.dumps({"state": state, "nonce": nonce}))
|
289
|
-
|
290
|
-
return callback_handler.access_token
|
291
|
-
finally:
|
292
|
-
await runner.cleanup()
|
293
|
-
|
294
|
-
@alru_cache
|
295
|
-
async def _get_jwk_client(self) -> PyJWKClient:
|
296
|
-
"""Returns a JWK client for the OpenID Connect server."""
|
297
|
-
oicd_info = await self._get_oicd_info()
|
298
|
-
jwks_uri = f"{oicd_info.authority}/.well-known/jwks.json"
|
299
|
-
return PyJWKClient(uri=jwks_uri)
|
300
|
-
|
301
|
-
async def _is_token_expired(self, token: str) -> bool:
|
302
|
-
"""Check if a token is expired."""
|
303
|
-
jwk_client = await self._get_jwk_client()
|
304
|
-
signing_key = jwk_client.get_signing_key_from_jwt(token)
|
305
|
-
|
306
|
-
try:
|
307
|
-
claims = jwt_decode(
|
308
|
-
token,
|
309
|
-
signing_key.key,
|
310
|
-
algorithms=["RS256"],
|
311
|
-
options={"verify_aud": False},
|
312
|
-
)
|
313
|
-
except ExpiredSignatureError:
|
314
|
-
return True
|
315
|
-
|
316
|
-
return claims["exp"] < time.time()
|
317
|
-
|
318
|
-
@alru_cache
|
319
|
-
async def get_bearer_token(self) -> str:
|
320
|
-
"""Get a bearer token from OpenID Connect.
|
321
|
-
|
322
|
-
Returns:
|
323
|
-
A bearer token to use with the K-Scale WWW API.
|
324
|
-
"""
|
325
|
-
cache_path = get_auth_dir() / "bearer_token.txt"
|
326
|
-
if self.use_cache and cache_path.exists():
|
327
|
-
token = cache_path.read_text()
|
328
|
-
if not await self._is_token_expired(token):
|
329
|
-
return token
|
330
|
-
token = await self._get_bearer_token()
|
331
|
-
if self.use_cache:
|
332
|
-
cache_path.write_text(token)
|
333
|
-
cache_path.chmod(0o600)
|
334
|
-
return token
|
335
|
-
|
336
38
|
async def get_client(self, *, auth: bool = True) -> httpx.AsyncClient:
|
337
39
|
client = self._client if auth else self._client_no_auth
|
338
40
|
if client is None:
|
339
41
|
headers: dict[str, str] = {}
|
340
42
|
if auth:
|
341
|
-
if "KSCALE_API_KEY" in os.environ:
|
342
|
-
|
343
|
-
|
344
|
-
headers["Authorization"] = f"Bearer {await self.get_bearer_token()}"
|
43
|
+
if "KSCALE_API_KEY" not in os.environ:
|
44
|
+
raise ValueError("KSCALE_API_KEY is not set! Obtain one here: https://kscale.dev/dashboard/keys")
|
45
|
+
headers[HEADER_NAME] = os.environ["KSCALE_API_KEY"]
|
345
46
|
|
346
47
|
client = httpx.AsyncClient(
|
347
48
|
base_url=self.base_url,
|
kscale/web/clients/user.py
CHANGED
@@ -8,7 +8,3 @@ class UserClient(BaseClient):
|
|
8
8
|
async def get_profile_info(self) -> ProfileResponse:
|
9
9
|
data = await self._request("GET", "/auth/profile", auth=True)
|
10
10
|
return ProfileResponse(**data)
|
11
|
-
|
12
|
-
async def get_api_key(self, num_hours: int = 24) -> str:
|
13
|
-
data = await self._request("POST", "/auth/key", auth=True, data={"num_hours": num_hours})
|
14
|
-
return data["api_key"]
|
kscale/web/gen/api.py
CHANGED
@@ -2,15 +2,30 @@
|
|
2
2
|
|
3
3
|
# generated by datamodel-codegen:
|
4
4
|
# filename: openapi.json
|
5
|
-
# timestamp: 2025-
|
5
|
+
# timestamp: 2025-07-10T20:02:21+00:00
|
6
6
|
|
7
7
|
from __future__ import annotations
|
8
8
|
|
9
|
+
from datetime import datetime
|
9
10
|
from typing import Dict, List, Optional, Union
|
10
11
|
|
11
12
|
from pydantic import BaseModel, Field
|
12
13
|
|
13
14
|
|
15
|
+
class APIKey(BaseModel):
|
16
|
+
id: str = Field(..., title="Id")
|
17
|
+
user_id: str = Field(..., title="User Id")
|
18
|
+
name: str = Field(..., title="Name")
|
19
|
+
key_hash: str = Field(..., title="Key Hash")
|
20
|
+
permissions: List[str] = Field(..., title="Permissions")
|
21
|
+
email: str = Field(..., title="Email")
|
22
|
+
email_verified: bool = Field(..., title="Email Verified")
|
23
|
+
created_at: datetime = Field(..., title="Created At")
|
24
|
+
last_used_at: Optional[datetime] = Field(None, title="Last Used At")
|
25
|
+
expires_at: Optional[datetime] = Field(None, title="Expires At")
|
26
|
+
is_active: Optional[bool] = Field(True, title="Is Active")
|
27
|
+
|
28
|
+
|
14
29
|
class APIKeyRequest(BaseModel):
|
15
30
|
num_hours: Optional[int] = Field(24, title="Num Hours")
|
16
31
|
|
@@ -19,6 +34,16 @@ class APIKeyResponse(BaseModel):
|
|
19
34
|
api_key: str = Field(..., title="Api Key")
|
20
35
|
|
21
36
|
|
37
|
+
class APIKeySummaryResponse(BaseModel):
|
38
|
+
id: str = Field(..., title="Id")
|
39
|
+
name: str = Field(..., title="Name")
|
40
|
+
permissions: List[str] = Field(..., title="Permissions")
|
41
|
+
created_at: datetime = Field(..., title="Created At")
|
42
|
+
last_used_at: Optional[datetime] = Field(..., title="Last Used At")
|
43
|
+
expires_at: Optional[datetime] = Field(..., title="Expires At")
|
44
|
+
is_active: bool = Field(..., title="Is Active")
|
45
|
+
|
46
|
+
|
22
47
|
class ActuatorMetadataInput(BaseModel):
|
23
48
|
actuator_type: Optional[str] = Field(None, title="Actuator Type")
|
24
49
|
sysid: Optional[str] = Field(None, title="Sysid")
|
@@ -62,6 +87,45 @@ class AddRobotRequest(BaseModel):
|
|
62
87
|
class_name: str = Field(..., title="Class Name")
|
63
88
|
|
64
89
|
|
90
|
+
class Agent(BaseModel):
|
91
|
+
id: str = Field(..., title="Id")
|
92
|
+
user_id: str = Field(..., title="User Id")
|
93
|
+
upload_time: str = Field(..., title="Upload Time")
|
94
|
+
description: Optional[str] = Field(None, title="Description")
|
95
|
+
num_downloads: Optional[int] = Field(0, title="Num Downloads")
|
96
|
+
|
97
|
+
|
98
|
+
class AgentDownloadResponse(BaseModel):
|
99
|
+
url: str = Field(..., title="Url")
|
100
|
+
md5_hash: str = Field(..., title="Md5 Hash")
|
101
|
+
|
102
|
+
|
103
|
+
class AgentUploadRequest(BaseModel):
|
104
|
+
filename: str = Field(..., title="Filename")
|
105
|
+
content_type: str = Field(..., title="Content Type")
|
106
|
+
|
107
|
+
|
108
|
+
class AgentUploadResponse(BaseModel):
|
109
|
+
url: str = Field(..., title="Url")
|
110
|
+
filename: str = Field(..., title="Filename")
|
111
|
+
content_type: str = Field(..., title="Content Type")
|
112
|
+
|
113
|
+
|
114
|
+
class CreateAPIKeyRequest(BaseModel):
|
115
|
+
name: str = Field(..., title="Name")
|
116
|
+
permissions: List[str] = Field(..., title="Permissions")
|
117
|
+
expires_at: Optional[datetime] = Field(None, title="Expires At")
|
118
|
+
|
119
|
+
|
120
|
+
class CreateAPIKeyResponse(BaseModel):
|
121
|
+
api_key: APIKey
|
122
|
+
plain_key: str = Field(..., title="Plain Key")
|
123
|
+
|
124
|
+
|
125
|
+
class CreateAgentRequest(BaseModel):
|
126
|
+
description: Optional[str] = Field(None, title="Description")
|
127
|
+
|
128
|
+
|
65
129
|
class JointMetadataInput(BaseModel):
|
66
130
|
id: Optional[int] = Field(None, title="Id")
|
67
131
|
kp: Optional[Union[float, str]] = Field(None, title="Kp")
|
@@ -73,6 +137,8 @@ class JointMetadataInput(BaseModel):
|
|
73
137
|
actuator_type: Optional[str] = Field(None, title="Actuator Type")
|
74
138
|
nn_id: Optional[int] = Field(None, title="Nn Id")
|
75
139
|
soft_torque_limit: Optional[Union[float, str]] = Field(None, title="Soft Torque Limit")
|
140
|
+
min_angle_deg: Optional[Union[float, str]] = Field(None, title="Min Angle Deg")
|
141
|
+
max_angle_deg: Optional[Union[float, str]] = Field(None, title="Max Angle Deg")
|
76
142
|
|
77
143
|
|
78
144
|
class JointMetadataOutput(BaseModel):
|
@@ -86,9 +152,11 @@ class JointMetadataOutput(BaseModel):
|
|
86
152
|
actuator_type: Optional[str] = Field(None, title="Actuator Type")
|
87
153
|
nn_id: Optional[int] = Field(None, title="Nn Id")
|
88
154
|
soft_torque_limit: Optional[str] = Field(None, title="Soft Torque Limit")
|
155
|
+
min_angle_deg: Optional[str] = Field(None, title="Min Angle Deg")
|
156
|
+
max_angle_deg: Optional[str] = Field(None, title="Max Angle Deg")
|
89
157
|
|
90
158
|
|
91
|
-
class
|
159
|
+
class OIDCInfo(BaseModel):
|
92
160
|
authority: str = Field(..., title="Authority")
|
93
161
|
client_id: str = Field(..., title="Client Id")
|
94
162
|
|
@@ -141,6 +209,14 @@ class RobotUploadURDFResponse(BaseModel):
|
|
141
209
|
content_type: str = Field(..., title="Content Type")
|
142
210
|
|
143
211
|
|
212
|
+
class UpdateAPIKeyPermissionsRequest(BaseModel):
|
213
|
+
permissions: List[str] = Field(..., title="Permissions")
|
214
|
+
|
215
|
+
|
216
|
+
class UpdateAgentRequest(BaseModel):
|
217
|
+
description: Optional[str] = Field(None, title="Description")
|
218
|
+
|
219
|
+
|
144
220
|
class UpdateRobotClassRequest(BaseModel):
|
145
221
|
new_class_name: Optional[str] = Field(None, title="New Class Name")
|
146
222
|
new_description: Optional[str] = Field(None, title="New Description")
|
@@ -1,4 +1,4 @@
|
|
1
|
-
kscale/__init__.py,sha256=
|
1
|
+
kscale/__init__.py,sha256=6HuPRxAl_xLwIYPRTPkAJ6Hz0BgwBehLHoHw2OWOtyQ,201
|
2
2
|
kscale/cli.py,sha256=JvaPtmWvF7s0D4I3K98eZAItf3oOi2ULsn5aPGxDcu4,795
|
3
3
|
kscale/conf.py,sha256=dm35XSnzJp93St-ixVtYN4Nvqvb5upPGBrWkSI6Yb-4,1743
|
4
4
|
kscale/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -15,19 +15,19 @@ kscale/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
15
|
kscale/web/utils.py,sha256=Mme-FAQ0_zbjjOQeX8wyq8F4kL4i9fH7ytri16U6qOA,1046
|
16
16
|
kscale/web/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
17
|
kscale/web/cli/robot.py,sha256=rI-A4_0uvJPeA71Apl4Z3mV5fIfWkgmzT9JRmJYxz3A,3307
|
18
|
-
kscale/web/cli/robot_class.py,sha256=
|
19
|
-
kscale/web/cli/user.py,sha256=
|
18
|
+
kscale/web/cli/robot_class.py,sha256=lmA0gxXiFTMoUtq6FBPwr8gRfM_hkAMJ7KOog5RUZII,19715
|
19
|
+
kscale/web/cli/user.py,sha256=SLugjzUkfhxBTd8-pLnyA2uTcuaX9RqmllVaR-rs9w8,1049
|
20
20
|
kscale/web/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
21
|
-
kscale/web/clients/base.py,sha256=
|
21
|
+
kscale/web/clients/base.py,sha256=bd2ifjLtMGIO7wQ4PHdFvc13EDSEO5GnwiiknwSjX8o,4119
|
22
22
|
kscale/web/clients/client.py,sha256=rzW2s8T7bKVuybOSQ65-ghl02rcXBoOxnx_nUDwgEPw,362
|
23
23
|
kscale/web/clients/robot.py,sha256=PI8HHkU-4Re9I5rLpp6dGbekRE-rBNVfXZxR_mO2MqE,1485
|
24
24
|
kscale/web/clients/robot_class.py,sha256=LCKje6nNsDBeKDxZAGCO9vQXdyOwtx0woMmB5vDoUmE,7230
|
25
|
-
kscale/web/clients/user.py,sha256=
|
25
|
+
kscale/web/clients/user.py,sha256=9iv8J-ROm_yBIwi-0oqldReLkNBFktdHRv3UCOxBzjY,377
|
26
26
|
kscale/web/gen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
27
|
-
kscale/web/gen/api.py,sha256=
|
28
|
-
kscale-0.3.
|
29
|
-
kscale-0.3.
|
30
|
-
kscale-0.3.
|
31
|
-
kscale-0.3.
|
32
|
-
kscale-0.3.
|
33
|
-
kscale-0.3.
|
27
|
+
kscale/web/gen/api.py,sha256=4xUbD_6lDrqB5jrgr1_SK9hAtU6t-EApFsazhxNiWmA,10101
|
28
|
+
kscale-0.3.17.dist-info/licenses/LICENSE,sha256=HCN2bImAzUOXldAZZI7JZ9PYq6OwMlDAP_PpX1HnuN0,1071
|
29
|
+
kscale-0.3.17.dist-info/METADATA,sha256=P-ZBO_i0zw_6G2kXGO2b8mnrtGMURjYl46RZbYtxJGc,2350
|
30
|
+
kscale-0.3.17.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
31
|
+
kscale-0.3.17.dist-info/entry_points.txt,sha256=N_0pCpPnwGDYVzOeuaSOrbJkS5L3lS9d8CxpJF1f8UI,62
|
32
|
+
kscale-0.3.17.dist-info/top_level.txt,sha256=C2ynjYwopg6YjgttnI2dJjasyq3EKNmYp-IfQg9Xms4,7
|
33
|
+
kscale-0.3.17.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|