kscale 0.2.2__py3-none-any.whl → 0.3.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.
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.2.2"
3
+ __version__ = "0.3.0"
4
4
 
5
5
  from pathlib import Path
6
6
 
@@ -1,12 +1,17 @@
1
1
  """Defines the CLI for getting information about robot classes."""
2
2
 
3
+ import json
3
4
  import logging
5
+ import math
6
+ import time
7
+ from typing import Sequence
4
8
 
5
9
  import click
6
10
  from tabulate import tabulate
7
11
 
8
12
  from kscale.utils.cli import coro
9
13
  from kscale.web.clients.robot_class import RobotClassClient
14
+ from kscale.web.gen.api import RobotURDFMetadataInput
10
15
 
11
16
  logger = logging.getLogger(__name__)
12
17
 
@@ -70,6 +75,22 @@ async def update(current_name: str, name: str | None = None, description: str |
70
75
  click.echo(f" Description: {click.style(robot_class.description or 'N/A', fg='yellow')}")
71
76
 
72
77
 
78
+ @cli.command()
79
+ @click.argument("name")
80
+ @click.argument("json_path", type=click.Path(exists=True))
81
+ @coro
82
+ async def update_metadata(name: str, json_path: str) -> None:
83
+ """Updates the metadata of a robot class."""
84
+ with open(json_path, "r", encoding="utf-8") as f:
85
+ raw_metadata = json.load(f)
86
+ metadata = RobotURDFMetadataInput.model_validate(raw_metadata)
87
+ async with RobotClassClient() as client:
88
+ robot_class = await client.update_robot_class(name, new_metadata=metadata)
89
+ click.echo("Robot class metadata updated:")
90
+ click.echo(f" ID: {click.style(robot_class.id, fg='blue')}")
91
+ click.echo(f" Name: {click.style(robot_class.class_name, fg='green')}")
92
+
93
+
73
94
  @cli.command()
74
95
  @click.argument("name")
75
96
  @coro
@@ -109,5 +130,283 @@ async def download(class_name: str, no_cache: bool) -> None:
109
130
  click.echo(f"URDF downloaded: {click.style(urdf_file, fg='green')}")
110
131
 
111
132
 
133
+ @urdf.command()
134
+ @click.argument("class_name")
135
+ @click.option("--no-cache", is_flag=True, default=False)
136
+ @click.option("--hide-gui", is_flag=True, default=False)
137
+ @click.option("--hide-origin", is_flag=True, default=False)
138
+ @click.option("--see-thru", is_flag=True, default=False)
139
+ @click.option("--show-collision", is_flag=True, default=False)
140
+ @click.option("--show-inertia", is_flag=True, default=False)
141
+ @click.option("--fixed-base", is_flag=True, default=False)
142
+ @click.option("--no-merge", is_flag=True, default=False)
143
+ @click.option("--dt", type=float, default=0.01)
144
+ @click.option("--start-height", type=float, default=0.0)
145
+ @click.option("--cycle-duration", type=float, default=2.0)
146
+ @coro
147
+ async def pybullet(
148
+ class_name: str,
149
+ no_cache: bool,
150
+ hide_gui: bool,
151
+ hide_origin: bool,
152
+ see_thru: bool,
153
+ show_collision: bool,
154
+ show_inertia: bool,
155
+ fixed_base: bool,
156
+ no_merge: bool,
157
+ dt: float,
158
+ start_height: float,
159
+ cycle_duration: float,
160
+ ) -> None:
161
+ """Shows the URDF file for a robot class in PyBullet."""
162
+ try:
163
+ import pybullet as p
164
+ except ImportError:
165
+ click.echo(click.style("PyBullet is not installed; install it with `pip install pybullet`", fg="red"))
166
+ return
167
+ async with RobotClassClient() as client:
168
+ urdf_base = await client.download_and_extract_urdf(class_name, cache=not no_cache)
169
+ try:
170
+ urdf_path = next(urdf_base.glob("*.urdf"))
171
+ except StopIteration:
172
+ click.echo(click.style(f"No URDF file found in {urdf_base}", fg="red"))
173
+ return
174
+
175
+ # Connect to PyBullet.
176
+ p.connect(p.GUI)
177
+ p.setGravity(0, 0, -9.81)
178
+ p.setRealTimeSimulation(0)
179
+
180
+ # Create floor plane
181
+ floor = p.createCollisionShape(p.GEOM_PLANE)
182
+ p.createMultiBody(0, floor)
183
+
184
+ # Turn off panels.
185
+ if hide_gui:
186
+ p.configureDebugVisualizer(p.COV_ENABLE_GUI, 0)
187
+ p.configureDebugVisualizer(p.COV_ENABLE_SEGMENTATION_MARK_PREVIEW, 0)
188
+ p.configureDebugVisualizer(p.COV_ENABLE_DEPTH_BUFFER_PREVIEW, 0)
189
+ p.configureDebugVisualizer(p.COV_ENABLE_RGB_BUFFER_PREVIEW, 0)
190
+
191
+ # Enable mouse picking.
192
+ p.configureDebugVisualizer(p.COV_ENABLE_MOUSE_PICKING, 1)
193
+
194
+ # Load the robot URDF.
195
+ start_position = [0.0, 0.0, start_height]
196
+ start_orientation = p.getQuaternionFromEuler([0.0, 0.0, 0.0])
197
+ flags = p.URDF_USE_INERTIA_FROM_FILE
198
+ if not no_merge:
199
+ flags |= p.URDF_MERGE_FIXED_LINKS
200
+
201
+ robot = p.loadURDF(
202
+ str(urdf_path.resolve().absolute()),
203
+ start_position,
204
+ start_orientation,
205
+ flags=flags,
206
+ useFixedBase=fixed_base,
207
+ )
208
+
209
+ # Display collision meshes as separate object.
210
+ if show_collision:
211
+ collision_flags = p.URDF_USE_INERTIA_FROM_FILE | p.URDF_USE_SELF_COLLISION_EXCLUDE_ALL_PARENTS
212
+ collision = p.loadURDF(
213
+ str(urdf_path.resolve().absolute()),
214
+ start_position,
215
+ start_orientation,
216
+ flags=collision_flags,
217
+ useFixedBase=0,
218
+ )
219
+
220
+ # Make collision shapes semi-transparent.
221
+ joint_ids = [i for i in range(p.getNumJoints(collision))] + [-1]
222
+ for i in joint_ids:
223
+ p.changeVisualShape(collision, i, rgbaColor=[1, 0, 0, 0.5])
224
+
225
+ # Initializes physics parameters.
226
+ p.changeDynamics(floor, -1, lateralFriction=1, spinningFriction=-1, rollingFriction=-1)
227
+ p.setPhysicsEngineParameter(fixedTimeStep=dt, maxNumCmdPer1ms=1000)
228
+
229
+ # Shows the origin of the robot.
230
+ if not hide_origin:
231
+ p.addUserDebugLine([0, 0, 0], [0.1, 0, 0], [1, 0, 0], parentObjectUniqueId=robot, parentLinkIndex=-1)
232
+ p.addUserDebugLine([0, 0, 0], [0, 0.1, 0], [0, 1, 0], parentObjectUniqueId=robot, parentLinkIndex=-1)
233
+ p.addUserDebugLine([0, 0, 0], [0, 0, 0.1], [0, 0, 1], parentObjectUniqueId=robot, parentLinkIndex=-1)
234
+
235
+ # Make the robot see-through.
236
+ joint_ids = [i for i in range(p.getNumJoints(robot))] + [-1]
237
+ if see_thru:
238
+ shape_data = p.getVisualShapeData(robot)
239
+ for i in joint_ids:
240
+ prev_color = shape_data[i][-1]
241
+ p.changeVisualShape(robot, i, rgbaColor=prev_color[:3] + (0.9,))
242
+
243
+ def draw_box(pt: Sequence[Sequence[float]], color: tuple[float, float, float], obj_id: int, link_id: int) -> None:
244
+ """Draw a box in PyBullet debug visualization.
245
+
246
+ Args:
247
+ pt: List of 8 points defining box vertices, each point is [x,y,z]
248
+ color: RGB color tuple for the box lines
249
+ obj_id: PyBullet object ID to attach box to
250
+ link_id: Link ID on the object to attach box to
251
+ """
252
+ assert len(pt) == 8
253
+ assert all(len(p) == 3 for p in pt)
254
+
255
+ p.addUserDebugLine(pt[0], pt[1], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
256
+ p.addUserDebugLine(pt[1], pt[3], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
257
+ p.addUserDebugLine(pt[3], pt[2], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
258
+ p.addUserDebugLine(pt[2], pt[0], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
259
+
260
+ p.addUserDebugLine(pt[0], pt[4], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
261
+ p.addUserDebugLine(pt[1], pt[5], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
262
+ p.addUserDebugLine(pt[2], pt[6], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
263
+ p.addUserDebugLine(pt[3], pt[7], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
264
+
265
+ p.addUserDebugLine(pt[4 + 0], pt[4 + 1], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
266
+ p.addUserDebugLine(pt[4 + 1], pt[4 + 3], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
267
+ p.addUserDebugLine(pt[4 + 3], pt[4 + 2], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
268
+ p.addUserDebugLine(pt[4 + 2], pt[4 + 0], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
269
+
270
+ # Shows bounding boxes around each part of the robot representing the inertia frame.
271
+ if show_inertia:
272
+ for i in joint_ids:
273
+ dynamics_info = p.getDynamicsInfo(robot, i)
274
+ mass = dynamics_info[0]
275
+ if mass <= 0:
276
+ continue
277
+ inertia = dynamics_info[2]
278
+
279
+ # Calculate box dimensions.
280
+ ixx, iyy, izz = inertia[0], inertia[1], inertia[2]
281
+ box_scale_x = math.sqrt(6 * (iyy + izz - ixx) / mass) / 2
282
+ box_scale_y = math.sqrt(6 * (ixx + izz - iyy) / mass) / 2
283
+ box_scale_z = math.sqrt(6 * (ixx + iyy - izz) / mass) / 2
284
+ half_extents = [box_scale_x, box_scale_y, box_scale_z]
285
+
286
+ # Create box vertices in local inertia frame
287
+ pt = [
288
+ [half_extents[0], half_extents[1], half_extents[2]],
289
+ [-half_extents[0], half_extents[1], half_extents[2]],
290
+ [half_extents[0], -half_extents[1], half_extents[2]],
291
+ [-half_extents[0], -half_extents[1], half_extents[2]],
292
+ [half_extents[0], half_extents[1], -half_extents[2]],
293
+ [-half_extents[0], half_extents[1], -half_extents[2]],
294
+ [half_extents[0], -half_extents[1], -half_extents[2]],
295
+ [-half_extents[0], -half_extents[1], -half_extents[2]],
296
+ ]
297
+
298
+ draw_box(pt, (1, 0, 0), robot, i)
299
+
300
+ # Show joint controller.
301
+ joints: dict[str, int] = {}
302
+ controls: dict[str, float] = {}
303
+ for i in range(p.getNumJoints(robot)):
304
+ joint_info = p.getJointInfo(robot, i)
305
+ name = joint_info[1].decode("utf-8")
306
+ joint_type = joint_info[2]
307
+ joints[name] = i
308
+ if joint_type == p.JOINT_PRISMATIC:
309
+ joint_min, joint_max = joint_info[8:10]
310
+ controls[name] = p.addUserDebugParameter(name, joint_min, joint_max, 0.0)
311
+ elif joint_type == p.JOINT_REVOLUTE:
312
+ joint_min, joint_max = joint_info[8:10]
313
+ controls[name] = p.addUserDebugParameter(name, joint_min, joint_max, 0.0)
314
+
315
+ def reset_joints_to_zero(robot: int, joints: dict[str, int]) -> None:
316
+ for joint_id in joints.values():
317
+ joint_info = p.getJointInfo(robot, joint_id)
318
+ joint_min, joint_max = joint_info[8:10]
319
+ zero_position = (joint_min + joint_max) / 2
320
+ p.setJointMotorControl2(robot, joint_id, p.POSITION_CONTROL, zero_position)
321
+
322
+ def reset_camera(position: int) -> None:
323
+ height = start_height if fixed_base else 0
324
+ camera_positions = {
325
+ 1: (2.0, 0, -30, [0, 0, height]), # Default view
326
+ 2: (2.0, 90, -30, [0, 0, height]), # Side view
327
+ 3: (2.0, 180, -30, [0, 0, height]), # Back view
328
+ 4: (2.0, 270, -30, [0, 0, height]), # Other side view
329
+ 5: (2.0, 0, 0, [0, 0, height]), # Front level view
330
+ 6: (2.0, 0, -80, [0, 0, height]), # Top-down view
331
+ 7: (1.5, 45, -45, [0, 0, height]), # Closer angled view
332
+ 8: (3.0, 30, -30, [0, 0, height]), # Further angled view
333
+ 9: (2.0, 0, 30, [0, 0, height]), # Low angle view
334
+ }
335
+
336
+ if position in camera_positions:
337
+ distance, yaw, pitch, target = camera_positions[position]
338
+ p.resetDebugVisualizerCamera(
339
+ cameraDistance=distance,
340
+ cameraYaw=yaw,
341
+ cameraPitch=pitch,
342
+ cameraTargetPosition=target,
343
+ )
344
+
345
+ # Run the simulation until the user closes the window.
346
+ last_time = time.time()
347
+ prev_control_values = {k: 0.0 for k in controls}
348
+ cycle_joints = False
349
+ cycle_start_time = 0.0
350
+
351
+ while p.isConnected():
352
+ # Reset the simulation if "r" was pressed.
353
+ keys = p.getKeyboardEvents()
354
+ if ord("r") in keys and keys[ord("r")] & p.KEY_WAS_TRIGGERED:
355
+ p.resetBasePositionAndOrientation(robot, start_position, start_orientation)
356
+ p.setJointMotorControlArray(
357
+ robot,
358
+ range(p.getNumJoints(robot)),
359
+ p.POSITION_CONTROL,
360
+ targetPositions=[0] * p.getNumJoints(robot),
361
+ )
362
+
363
+ # Reset joints to zero position if "z" was pressed
364
+ if ord("z") in keys and keys[ord("z")] & p.KEY_WAS_TRIGGERED:
365
+ reset_joints_to_zero(robot, joints)
366
+ cycle_joints = False # Stop joint cycling if it was active
367
+
368
+ # Reset camera if number keys 1-9 are pressed
369
+ for i in range(1, 10):
370
+ if ord(str(i)) in keys and keys[ord(str(i))] & p.KEY_WAS_TRIGGERED:
371
+ reset_camera(i)
372
+
373
+ # Start/stop joint cycling if "c" was pressed
374
+ if ord("c") in keys and keys[ord("c")] & p.KEY_WAS_TRIGGERED:
375
+ cycle_joints = not cycle_joints
376
+ if cycle_joints:
377
+ cycle_start_time = time.time()
378
+ else:
379
+ # When stopping joint cycling, set joints to their current positions
380
+ for k, v in controls.items():
381
+ current_position = p.getJointState(robot, joints[k])[0]
382
+ p.setJointMotorControl2(robot, joints[k], p.POSITION_CONTROL, current_position)
383
+
384
+ # Set joint positions.
385
+ if cycle_joints:
386
+ elapsed_time = time.time() - cycle_start_time
387
+ cycle_progress = (elapsed_time % cycle_duration) / cycle_duration
388
+ for k, v in controls.items():
389
+ joint_info = p.getJointInfo(robot, joints[k])
390
+ joint_min, joint_max = joint_info[8:10]
391
+ target_position = joint_min + (joint_max - joint_min) * math.sin(cycle_progress * math.pi)
392
+ p.setJointMotorControl2(robot, joints[k], p.POSITION_CONTROL, target_position)
393
+ else:
394
+ for k, v in controls.items():
395
+ try:
396
+ target_position = p.readUserDebugParameter(v)
397
+ if target_position != prev_control_values[k]:
398
+ prev_control_values[k] = target_position
399
+ p.setJointMotorControl2(robot, joints[k], p.POSITION_CONTROL, target_position)
400
+ except p.error:
401
+ logger.debug("Failed to set joint %s", k)
402
+ pass
403
+
404
+ # Step simulation.
405
+ p.stepSimulation()
406
+ cur_time = time.time()
407
+ time.sleep(max(0, dt - (cur_time - last_time)))
408
+ last_time = cur_time
409
+
410
+
112
411
  if __name__ == "__main__":
113
412
  cli()
@@ -360,9 +360,10 @@ class BaseClient:
360
360
  files: dict[str, Any] | None = None,
361
361
  ) -> dict[str, Any]:
362
362
  url = urljoin(self.base_url, endpoint)
363
- kwargs: dict[str, Any] = {"params": params}
364
-
365
- if data:
363
+ kwargs: dict[str, Any] = {}
364
+ if params is not None:
365
+ kwargs["params"] = params
366
+ if data is not None:
366
367
  if isinstance(data, BaseModel):
367
368
  kwargs["json"] = data.model_dump(exclude_unset=True)
368
369
  else:
@@ -19,16 +19,16 @@ class RobotClient(BaseClient):
19
19
  class_name: str,
20
20
  description: str | None = None,
21
21
  ) -> RobotResponse:
22
- params = {"class_name": class_name}
22
+ data = {"class_name": class_name}
23
23
  if description is not None:
24
- params["description"] = description
25
- data = await self._request(
24
+ data["description"] = description
25
+ response = await self._request(
26
26
  "PUT",
27
27
  f"/robot/{robot_name}",
28
- params=params,
28
+ data=data,
29
29
  auth=True,
30
30
  )
31
- return RobotResponse.model_validate(data)
31
+ return RobotResponse.model_validate(response)
32
32
 
33
33
  async def get_robot_by_id(self, robot_id: str) -> RobotResponse:
34
34
  data = await self._request("GET", f"/robot/id/{robot_id}", auth=True)
@@ -5,6 +5,7 @@ import json
5
5
  import logging
6
6
  import tarfile
7
7
  from pathlib import Path
8
+ from typing import Any
8
9
 
9
10
  import httpx
10
11
 
@@ -13,6 +14,7 @@ from kscale.web.gen.api import (
13
14
  RobotClass,
14
15
  RobotDownloadURDFResponse,
15
16
  RobotUploadURDFResponse,
17
+ RobotURDFMetadataInput,
16
18
  )
17
19
  from kscale.web.utils import get_robots_dir, should_refresh_file
18
20
 
@@ -34,13 +36,13 @@ class RobotClassClient(BaseClient):
34
36
  return [RobotClass.model_validate(item) for item in data]
35
37
 
36
38
  async def create_robot_class(self, class_name: str, description: str | None = None) -> RobotClass:
37
- params = {}
39
+ data = {}
38
40
  if description is not None:
39
- params["description"] = description
41
+ data["description"] = description
40
42
  data = await self._request(
41
43
  "PUT",
42
44
  f"/robots/{class_name}",
43
- params=params,
45
+ data=data,
44
46
  auth=True,
45
47
  )
46
48
  return RobotClass.model_validate(data)
@@ -50,18 +52,21 @@ class RobotClassClient(BaseClient):
50
52
  class_name: str,
51
53
  new_class_name: str | None = None,
52
54
  new_description: str | None = None,
55
+ new_metadata: RobotURDFMetadataInput | None = None,
53
56
  ) -> RobotClass:
54
- params = {}
57
+ data: dict[str, Any] = {}
55
58
  if new_class_name is not None:
56
- params["new_class_name"] = new_class_name
59
+ data["new_class_name"] = new_class_name
57
60
  if new_description is not None:
58
- params["new_description"] = new_description
59
- if not params:
61
+ data["new_description"] = new_description
62
+ if new_metadata is not None:
63
+ data["new_metadata"] = new_metadata.model_dump()
64
+ if not data:
60
65
  raise ValueError("No parameters to update")
61
66
  data = await self._request(
62
67
  "POST",
63
68
  f"/robots/{class_name}",
64
- params=params,
69
+ data=data,
65
70
  auth=True,
66
71
  )
67
72
  return RobotClass.model_validate(data)
@@ -84,7 +89,7 @@ class RobotClassClient(BaseClient):
84
89
  data = await self._request(
85
90
  "PUT",
86
91
  f"/robots/urdf/{class_name}",
87
- params={"filename": urdf_file.name, "content_type": content_type},
92
+ data={"filename": urdf_file.name, "content_type": content_type},
88
93
  auth=True,
89
94
  )
90
95
  response = RobotUploadURDFResponse.model_validate(data)
kscale/web/gen/api.py CHANGED
@@ -2,15 +2,46 @@
2
2
 
3
3
  # generated by datamodel-codegen:
4
4
  # filename: openapi.json
5
- # timestamp: 2025-01-15T22:35:42+00:00
5
+ # timestamp: 2025-01-21T07:10:22+00:00
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from typing import List, Optional, Union
9
+ from typing import Dict, List, Optional, Union
10
10
 
11
11
  from pydantic import BaseModel, Field
12
12
 
13
13
 
14
+ class APIKeyResponse(BaseModel):
15
+ api_key: str = Field(..., title="Api Key")
16
+
17
+
18
+ class AddRobotClassRequest(BaseModel):
19
+ description: Optional[str] = Field(None, title="Description")
20
+
21
+
22
+ class AddRobotRequest(BaseModel):
23
+ description: Optional[str] = Field(None, title="Description")
24
+ class_name: str = Field(..., title="Class Name")
25
+
26
+
27
+ class JointMetadataInput(BaseModel):
28
+ id: Optional[int] = Field(None, title="Id")
29
+ kp: Optional[Union[float, str]] = Field(None, title="Kp")
30
+ kd: Optional[Union[float, str]] = Field(None, title="Kd")
31
+ offset: Optional[Union[float, str]] = Field(None, title="Offset")
32
+ lower_limit: Optional[Union[float, str]] = Field(None, title="Lower Limit")
33
+ upper_limit: Optional[Union[float, str]] = Field(None, title="Upper Limit")
34
+
35
+
36
+ class JointMetadataOutput(BaseModel):
37
+ id: Optional[int] = Field(None, title="Id")
38
+ kp: Optional[str] = Field(None, title="Kp")
39
+ kd: Optional[str] = Field(None, title="Kd")
40
+ offset: Optional[str] = Field(None, title="Offset")
41
+ lower_limit: Optional[str] = Field(None, title="Lower Limit")
42
+ upper_limit: Optional[str] = Field(None, title="Upper Limit")
43
+
44
+
14
45
  class OICDInfo(BaseModel):
15
46
  authority: str = Field(..., title="Authority")
16
47
  client_id: str = Field(..., title="Client Id")
@@ -24,13 +55,6 @@ class Robot(BaseModel):
24
55
  class_id: str = Field(..., title="Class Id")
25
56
 
26
57
 
27
- class RobotClass(BaseModel):
28
- id: str = Field(..., title="Id")
29
- class_name: str = Field(..., title="Class Name")
30
- description: str = Field(..., title="Description")
31
- user_id: str = Field(..., title="User Id")
32
-
33
-
34
58
  class RobotDownloadURDFResponse(BaseModel):
35
59
  url: str = Field(..., title="Url")
36
60
  md5_hash: str = Field(..., title="Md5 Hash")
@@ -44,12 +68,36 @@ class RobotResponse(BaseModel):
44
68
  class_name: str = Field(..., title="Class Name")
45
69
 
46
70
 
71
+ class RobotURDFMetadataInput(BaseModel):
72
+ joint_name_to_metadata: Optional[Dict[str, JointMetadataInput]] = Field(None, title="Joint Name To Metadata")
73
+
74
+
75
+ class RobotURDFMetadataOutput(BaseModel):
76
+ joint_name_to_metadata: Optional[Dict[str, JointMetadataOutput]] = Field(None, title="Joint Name To Metadata")
77
+
78
+
79
+ class RobotUploadURDFRequest(BaseModel):
80
+ filename: str = Field(..., title="Filename")
81
+ content_type: str = Field(..., title="Content Type")
82
+
83
+
47
84
  class RobotUploadURDFResponse(BaseModel):
48
85
  url: str = Field(..., title="Url")
49
86
  filename: str = Field(..., title="Filename")
50
87
  content_type: str = Field(..., title="Content Type")
51
88
 
52
89
 
90
+ class UpdateRobotClassRequest(BaseModel):
91
+ new_class_name: Optional[str] = Field(None, title="New Class Name")
92
+ new_description: Optional[str] = Field(None, title="New Description")
93
+ new_metadata: Optional[RobotURDFMetadataInput] = None
94
+
95
+
96
+ class UpdateRobotRequest(BaseModel):
97
+ new_robot_name: Optional[str] = Field(None, title="New Robot Name")
98
+ new_description: Optional[str] = Field(None, title="New Description")
99
+
100
+
53
101
  class UserResponse(BaseModel):
54
102
  user_id: str = Field(..., title="User Id")
55
103
  is_admin: bool = Field(..., title="Is Admin")
@@ -71,3 +119,11 @@ class ProfileResponse(BaseModel):
71
119
  email: str = Field(..., title="Email")
72
120
  email_verified: bool = Field(..., title="Email Verified")
73
121
  user: UserResponse
122
+
123
+
124
+ class RobotClass(BaseModel):
125
+ id: str = Field(..., title="Id")
126
+ class_name: str = Field(..., title="Class Name")
127
+ description: str = Field(..., title="Description")
128
+ user_id: str = Field(..., title="User Id")
129
+ metadata: Optional[RobotURDFMetadataOutput] = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: kscale
3
- Version: 0.2.2
3
+ Version: 0.3.0
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=A1hh6cfKSMUSIDxYuPny2OkMm4-WcRlZsRYow_TO9Vo,200
1
+ kscale/__init__.py,sha256=ApqkdyLhaMQ1u0TopxCm4T5hZTDoweO2b2gUg0djui8,200
2
2
  kscale/cli.py,sha256=PMHLKR5UwdbbReVmqHXpJ-K9-mGHv_0I7KQkwxmFcUA,881
3
3
  kscale/conf.py,sha256=dm35XSnzJp93St-ixVtYN4Nvqvb5upPGBrWkSI6Yb-4,1743
4
4
  kscale/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -15,20 +15,20 @@ 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=8igY6oWUDafb644QzmqC5cX6Az0rNJau_jvLr7YkCd0,3573
18
+ kscale/web/cli/robot_class.py,sha256=Za5WZmyySgpY-hhR1BqyF1k1wr9bGbXLAE6uFlTOZkk,16706
19
19
  kscale/web/cli/token.py,sha256=1rFC8MYKtqbNsQa2KIqwW1tqpaMtFaxuNsallwejXTU,787
20
20
  kscale/web/cli/user.py,sha256=aaJJCL1P5lfhK6ZC9OwOHXKA-I3MWqVZ_k7TYnx33CY,1303
21
21
  kscale/web/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- kscale/web/clients/base.py,sha256=uovIxtkotRxrIFL0PhLYDIhhfXrIGjXxNY2FLVO3L18,15110
22
+ kscale/web/clients/base.py,sha256=A70BF9ZnHEBds0RKrowbeUNsX_WwsgV349csUlM4iMc,15174
23
23
  kscale/web/clients/client.py,sha256=rzW2s8T7bKVuybOSQ65-ghl02rcXBoOxnx_nUDwgEPw,362
24
- kscale/web/clients/robot.py,sha256=HMfJnkDxaJ_o7X2vdYYS9iob1JRoBG2qiGmQpCQZpAk,1485
25
- kscale/web/clients/robot_class.py,sha256=zCFL-Y_jIfO9_qOwzBAHzfgFxzpltMte4LM5haCC24U,6385
24
+ kscale/web/clients/robot.py,sha256=PI8HHkU-4Re9I5rLpp6dGbekRE-rBNVfXZxR_mO2MqE,1485
25
+ kscale/web/clients/robot_class.py,sha256=_oAEUglLmNXCspnACC40XLclLL3Z2rF88o1p0tk-JUM,6588
26
26
  kscale/web/clients/user.py,sha256=jsa1_s6qXRM-AGBbHlPhd1NierUtynjY9tVAPNr6_Os,568
27
27
  kscale/web/gen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- kscale/web/gen/api.py,sha256=SovcII36JFgK9jd2CXlLPMjiUROGB4vEnapOsYMUrkU,2188
29
- kscale-0.2.2.dist-info/LICENSE,sha256=HCN2bImAzUOXldAZZI7JZ9PYq6OwMlDAP_PpX1HnuN0,1071
30
- kscale-0.2.2.dist-info/METADATA,sha256=c0duCLdAcxXv6SXhRpkoSO99-ahyAwV2HRhYiBZXK1k,2340
31
- kscale-0.2.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
32
- kscale-0.2.2.dist-info/entry_points.txt,sha256=N_0pCpPnwGDYVzOeuaSOrbJkS5L3lS9d8CxpJF1f8UI,62
33
- kscale-0.2.2.dist-info/top_level.txt,sha256=C2ynjYwopg6YjgttnI2dJjasyq3EKNmYp-IfQg9Xms4,7
34
- kscale-0.2.2.dist-info/RECORD,,
28
+ kscale/web/gen/api.py,sha256=7Uyzy4V2Ovh70M5lLSaJhwKkBr5SYyTfrNjr180JQhY,4304
29
+ kscale-0.3.0.dist-info/LICENSE,sha256=HCN2bImAzUOXldAZZI7JZ9PYq6OwMlDAP_PpX1HnuN0,1071
30
+ kscale-0.3.0.dist-info/METADATA,sha256=H3CQZAwUTZBfScRQVKAArLadR62aw-rWhFMUBkTby84,2340
31
+ kscale-0.3.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
32
+ kscale-0.3.0.dist-info/entry_points.txt,sha256=N_0pCpPnwGDYVzOeuaSOrbJkS5L3lS9d8CxpJF1f8UI,62
33
+ kscale-0.3.0.dist-info/top_level.txt,sha256=C2ynjYwopg6YjgttnI2dJjasyq3EKNmYp-IfQg9Xms4,7
34
+ kscale-0.3.0.dist-info/RECORD,,
File without changes