From af6f36f6bd9decceb9c311f7fcc7bcbb5c62556b Mon Sep 17 00:00:00 2001 From: Benjamin Bolte Date: Mon, 20 Jan 2025 23:16:06 -0800 Subject: [PATCH 1/4] update api --- kscale/web/cli/robot_class.py | 18 ++++++++ kscale/web/clients/base.py | 7 +-- kscale/web/clients/robot.py | 10 ++--- kscale/web/clients/robot_class.py | 22 +++++---- kscale/web/gen/api.py | 74 +++++++++++++++++++++++++++---- 5 files changed, 105 insertions(+), 26 deletions(-) diff --git a/kscale/web/cli/robot_class.py b/kscale/web/cli/robot_class.py index bd11075..44ee9a8 100644 --- a/kscale/web/cli/robot_class.py +++ b/kscale/web/cli/robot_class.py @@ -1,5 +1,6 @@ """Defines the CLI for getting information about robot classes.""" +import json import logging import math import time @@ -10,6 +11,7 @@ from kscale.utils.cli import coro from kscale.web.clients.robot_class import RobotClassClient +from kscale.web.gen.api import RobotURDFMetadataInput logger = logging.getLogger(__name__) @@ -73,6 +75,22 @@ async def update(current_name: str, name: str | None = None, description: str | click.echo(f" Description: {click.style(robot_class.description or 'N/A', fg='yellow')}") +@cli.command() +@click.argument("name") +@click.argument("json_path", type=click.Path(exists=True)) +@coro +async def update_metadata(name: str, json_path: str) -> None: + """Updates the metadata of a robot class.""" + with open(json_path, "r", encoding="utf-8") as f: + raw_metadata = json.load(f) + metadata = RobotURDFMetadataInput.model_validate(raw_metadata) + async with RobotClassClient() as client: + robot_class = await client.update_robot_class(name, new_metadata=metadata) + click.echo("Robot class metadata updated:") + click.echo(f" ID: {click.style(robot_class.id, fg='blue')}") + click.echo(f" Name: {click.style(robot_class.class_name, fg='green')}") + + @cli.command() @click.argument("name") @coro diff --git a/kscale/web/clients/base.py b/kscale/web/clients/base.py index 7407907..183a962 100644 --- a/kscale/web/clients/base.py +++ b/kscale/web/clients/base.py @@ -360,9 +360,10 @@ async def _request( files: dict[str, Any] | None = None, ) -> dict[str, Any]: url = urljoin(self.base_url, endpoint) - kwargs: dict[str, Any] = {"params": params} - - if data: + kwargs: dict[str, Any] = {} + if params is not None: + kwargs["params"] = params + if data is not None: if isinstance(data, BaseModel): kwargs["json"] = data.model_dump(exclude_unset=True) else: diff --git a/kscale/web/clients/robot.py b/kscale/web/clients/robot.py index cee7fda..aeecee8 100644 --- a/kscale/web/clients/robot.py +++ b/kscale/web/clients/robot.py @@ -19,16 +19,16 @@ async def add_robot( class_name: str, description: str | None = None, ) -> RobotResponse: - params = {"class_name": class_name} + data = {"class_name": class_name} if description is not None: - params["description"] = description - data = await self._request( + data["description"] = description + response = await self._request( "PUT", f"/robot/{robot_name}", - params=params, + data=data, auth=True, ) - return RobotResponse.model_validate(data) + return RobotResponse.model_validate(response) async def get_robot_by_id(self, robot_id: str) -> RobotResponse: data = await self._request("GET", f"/robot/id/{robot_id}", auth=True) diff --git a/kscale/web/clients/robot_class.py b/kscale/web/clients/robot_class.py index 8ab4aff..f330544 100644 --- a/kscale/web/clients/robot_class.py +++ b/kscale/web/clients/robot_class.py @@ -13,6 +13,7 @@ RobotClass, RobotDownloadURDFResponse, RobotUploadURDFResponse, + RobotURDFMetadataInput, ) from kscale.web.utils import get_robots_dir, should_refresh_file @@ -34,13 +35,13 @@ async def get_robot_classes(self) -> list[RobotClass]: return [RobotClass.model_validate(item) for item in data] async def create_robot_class(self, class_name: str, description: str | None = None) -> RobotClass: - params = {} + data = {} if description is not None: - params["description"] = description + data["description"] = description data = await self._request( "PUT", f"/robots/{class_name}", - params=params, + data=data, auth=True, ) return RobotClass.model_validate(data) @@ -50,18 +51,21 @@ async def update_robot_class( class_name: str, new_class_name: str | None = None, new_description: str | None = None, + new_metadata: RobotURDFMetadataInput | None = None, ) -> RobotClass: - params = {} + data = {} if new_class_name is not None: - params["new_class_name"] = new_class_name + data["new_class_name"] = new_class_name if new_description is not None: - params["new_description"] = new_description - if not params: + data["new_description"] = new_description + if new_metadata is not None: + data["new_metadata"] = new_metadata.model_dump() + if not data: raise ValueError("No parameters to update") data = await self._request( "POST", f"/robots/{class_name}", - params=params, + data=data, auth=True, ) return RobotClass.model_validate(data) @@ -84,7 +88,7 @@ async def upload_robot_class_urdf(self, class_name: str, urdf_file: str | Path) data = await self._request( "PUT", f"/robots/urdf/{class_name}", - params={"filename": urdf_file.name, "content_type": content_type}, + data={"filename": urdf_file.name, "content_type": content_type}, auth=True, ) response = RobotUploadURDFResponse.model_validate(data) diff --git a/kscale/web/gen/api.py b/kscale/web/gen/api.py index 8fdd830..1415114 100644 --- a/kscale/web/gen/api.py +++ b/kscale/web/gen/api.py @@ -2,15 +2,46 @@ # generated by datamodel-codegen: # filename: openapi.json -# timestamp: 2025-01-15T22:35:42+00:00 +# timestamp: 2025-01-21T07:10:22+00:00 from __future__ import annotations -from typing import List, Optional, Union +from typing import Dict, List, Optional, Union from pydantic import BaseModel, Field +class APIKeyResponse(BaseModel): + api_key: str = Field(..., title="Api Key") + + +class AddRobotClassRequest(BaseModel): + description: Optional[str] = Field(None, title="Description") + + +class AddRobotRequest(BaseModel): + description: Optional[str] = Field(None, title="Description") + class_name: str = Field(..., title="Class Name") + + +class JointMetadataInput(BaseModel): + id: Optional[int] = Field(None, title="Id") + kp: Optional[Union[float, str]] = Field(None, title="Kp") + kd: Optional[Union[float, str]] = Field(None, title="Kd") + offset: Optional[Union[float, str]] = Field(None, title="Offset") + lower_limit: Optional[Union[float, str]] = Field(None, title="Lower Limit") + upper_limit: Optional[Union[float, str]] = Field(None, title="Upper Limit") + + +class JointMetadataOutput(BaseModel): + id: Optional[int] = Field(None, title="Id") + kp: Optional[str] = Field(None, title="Kp") + kd: Optional[str] = Field(None, title="Kd") + offset: Optional[str] = Field(None, title="Offset") + lower_limit: Optional[str] = Field(None, title="Lower Limit") + upper_limit: Optional[str] = Field(None, title="Upper Limit") + + class OICDInfo(BaseModel): authority: str = Field(..., title="Authority") client_id: str = Field(..., title="Client Id") @@ -24,13 +55,6 @@ class Robot(BaseModel): class_id: str = Field(..., title="Class Id") -class RobotClass(BaseModel): - id: str = Field(..., title="Id") - class_name: str = Field(..., title="Class Name") - description: str = Field(..., title="Description") - user_id: str = Field(..., title="User Id") - - class RobotDownloadURDFResponse(BaseModel): url: str = Field(..., title="Url") md5_hash: str = Field(..., title="Md5 Hash") @@ -44,12 +68,36 @@ class RobotResponse(BaseModel): class_name: str = Field(..., title="Class Name") +class RobotURDFMetadataInput(BaseModel): + joint_name_to_metadata: Optional[Dict[str, JointMetadataInput]] = Field(None, title="Joint Name To Metadata") + + +class RobotURDFMetadataOutput(BaseModel): + joint_name_to_metadata: Optional[Dict[str, JointMetadataOutput]] = Field(None, title="Joint Name To Metadata") + + +class RobotUploadURDFRequest(BaseModel): + filename: str = Field(..., title="Filename") + content_type: str = Field(..., title="Content Type") + + class RobotUploadURDFResponse(BaseModel): url: str = Field(..., title="Url") filename: str = Field(..., title="Filename") content_type: str = Field(..., title="Content Type") +class UpdateRobotClassRequest(BaseModel): + new_class_name: Optional[str] = Field(None, title="New Class Name") + new_description: Optional[str] = Field(None, title="New Description") + new_metadata: Optional[RobotURDFMetadataInput] = None + + +class UpdateRobotRequest(BaseModel): + new_robot_name: Optional[str] = Field(None, title="New Robot Name") + new_description: Optional[str] = Field(None, title="New Description") + + class UserResponse(BaseModel): user_id: str = Field(..., title="User Id") is_admin: bool = Field(..., title="Is Admin") @@ -71,3 +119,11 @@ class ProfileResponse(BaseModel): email: str = Field(..., title="Email") email_verified: bool = Field(..., title="Email Verified") user: UserResponse + + +class RobotClass(BaseModel): + id: str = Field(..., title="Id") + class_name: str = Field(..., title="Class Name") + description: str = Field(..., title="Description") + user_id: str = Field(..., title="User Id") + metadata: Optional[RobotURDFMetadataOutput] = None From e40717a4cc7565fcca681130baefd1149b9b9ef6 Mon Sep 17 00:00:00 2001 From: Benjamin Bolte Date: Mon, 20 Jan 2025 23:16:30 -0800 Subject: [PATCH 2/4] format --- kscale/web/clients/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kscale/web/clients/base.py b/kscale/web/clients/base.py index 183a962..eea6bf8 100644 --- a/kscale/web/clients/base.py +++ b/kscale/web/clients/base.py @@ -15,7 +15,8 @@ import httpx from aiohttp import web from async_lru import alru_cache -from jwt import ExpiredSignatureError, PyJWKClient, decode as jwt_decode +from jwt import ExpiredSignatureError, PyJWKClient +from jwt import decode as jwt_decode from pydantic import BaseModel from yarl import URL From e4603f1e576a69b30e0936bad2a0f09d21ea9a76 Mon Sep 17 00:00:00 2001 From: Benjamin Bolte Date: Mon, 20 Jan 2025 23:24:28 -0800 Subject: [PATCH 3/4] lint --- kscale/web/clients/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kscale/web/clients/base.py b/kscale/web/clients/base.py index eea6bf8..183a962 100644 --- a/kscale/web/clients/base.py +++ b/kscale/web/clients/base.py @@ -15,8 +15,7 @@ import httpx from aiohttp import web from async_lru import alru_cache -from jwt import ExpiredSignatureError, PyJWKClient -from jwt import decode as jwt_decode +from jwt import ExpiredSignatureError, PyJWKClient, decode as jwt_decode from pydantic import BaseModel from yarl import URL From 2bac72726d56fe41d07629c61a4ec8f8c9f7fa9c Mon Sep 17 00:00:00 2001 From: Benjamin Bolte Date: Tue, 21 Jan 2025 15:37:27 -0800 Subject: [PATCH 4/4] lint --- kscale/web/clients/robot_class.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kscale/web/clients/robot_class.py b/kscale/web/clients/robot_class.py index f330544..567aa56 100644 --- a/kscale/web/clients/robot_class.py +++ b/kscale/web/clients/robot_class.py @@ -5,6 +5,7 @@ import logging import tarfile from pathlib import Path +from typing import Any import httpx @@ -53,7 +54,7 @@ async def update_robot_class( new_description: str | None = None, new_metadata: RobotURDFMetadataInput | None = None, ) -> RobotClass: - data = {} + data: dict[str, Any] = {} if new_class_name is not None: data["new_class_name"] = new_class_name if new_description is not None: