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