Skip to content

Commit

Permalink
kernel images (#671)
Browse files Browse the repository at this point in the history
* kernel images

* more consistent api

* better crud

* description parameter

* better auth

* fix broken query

* expect tgz

* remove kernel stuff for now
  • Loading branch information
codekansas authored Jan 15, 2025
1 parent b9a1e90 commit 5999940
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 70 deletions.
25 changes: 15 additions & 10 deletions tests/routers/test_robot.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@
@pytest.mark.asyncio
async def test_robots(test_client: TestClient) -> None:
# First create a robot class that we'll use
response = test_client.put("/robots/add", params={"class_name": "test_class"}, headers=HEADERS)
response = test_client.put("/robots/test_class", params={"description": "Test description"}, headers=HEADERS)
assert response.status_code == status.HTTP_200_OK, response.text
robot_class_data = response.json()
assert robot_class_data["id"] is not None

# Adds a robot
response = test_client.put(
"/robot/add", params={"robot_name": "test_robot", "class_name": "test_class"}, headers=HEADERS
"/robot/test_robot",
params={"description": "Test description", "class_name": "test_class"},
headers=HEADERS,
)
assert response.status_code == status.HTTP_200_OK, response.text
data = response.json()
Expand All @@ -28,7 +30,9 @@ async def test_robots(test_client: TestClient) -> None:

# Attempts to add a second robot with the same name
response = test_client.put(
"/robot/add", params={"robot_name": "test_robot", "class_name": "test_class"}, headers=HEADERS
"/robot/test_robot",
params={"description": "Test description", "class_name": "test_class"},
headers=HEADERS,
)
assert response.status_code == status.HTTP_400_BAD_REQUEST, response.text

Expand All @@ -53,15 +57,16 @@ async def test_robots(test_client: TestClient) -> None:

# Adds a second robot
response = test_client.put(
"/robot/add", params={"robot_name": "other_robot", "class_name": "test_class"}, headers=HEADERS
"/robot/other_robot",
params={"description": "Test description", "class_name": "test_class"},
headers=HEADERS,
)
assert response.status_code == status.HTTP_200_OK, response.text

# Updates the first robot
response = test_client.put(
"/robot/update",
response = test_client.post(
"/robot/test_robot",
params={
"robot_name": "test_robot",
"new_robot_name": "updated_robot",
"new_description": "new description",
},
Expand All @@ -80,7 +85,7 @@ async def test_robots(test_client: TestClient) -> None:
assert all(robot["robot_name"] in ("updated_robot", "other_robot") for robot in data)

# Deletes the robots
response = test_client.delete("/robot/delete", params={"robot_name": "updated_robot"}, headers=HEADERS)
response = test_client.delete("/robot/updated_robot", headers=HEADERS)
assert response.status_code == status.HTTP_200_OK, response.text

# Lists my robots again
Expand All @@ -96,8 +101,8 @@ async def test_robots(test_client: TestClient) -> None:
data = response.json()
assert data["id"] is not None

response = test_client.delete("/robot/delete", params={"robot_name": "other_robot"}, headers=HEADERS)
response = test_client.delete("/robot/other_robot", headers=HEADERS)
assert response.status_code == status.HTTP_200_OK, response.text

response = test_client.delete("/robots/delete", params={"class_name": "test_class"}, headers=HEADERS)
response = test_client.delete("/robots/test_class", headers=HEADERS)
assert response.status_code == status.HTTP_200_OK, response.text
32 changes: 15 additions & 17 deletions tests/routers/test_robot_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@
@pytest.mark.asyncio
async def test_robot_classes(test_client: TestClient) -> None:
# Adds a robot class.
response = test_client.put("/robots/add", params={"class_name": "test"}, headers=HEADERS)
response = test_client.put("/robots/test", params={"description": "Test description"}, headers=HEADERS)
assert response.status_code == status.HTTP_200_OK, response.text

# Attempts to add a second robot class with the same name.
response = test_client.put("/robots/add", params={"class_name": "test"}, headers=HEADERS)
response = test_client.put("/robots/test", params={"description": "Test description"}, headers=HEADERS)
assert response.status_code == status.HTTP_400_BAD_REQUEST, response.text

# Gets the added robot class.
response = test_client.get("/robots", headers=HEADERS)
response = test_client.get("/robots/", headers=HEADERS)
assert response.status_code == status.HTTP_200_OK, response.text
data = response.json()
assert len(data) == 1
Expand All @@ -34,14 +34,13 @@ async def test_robot_classes(test_client: TestClient) -> None:
assert data["class_name"] == "test"

# Adds a second robot class.
response = test_client.put("/robots/add", params={"class_name": "othertest"}, headers=HEADERS)
response = test_client.put("/robots/othertest", params={"description": "Test description"}, headers=HEADERS)
assert response.status_code == status.HTTP_200_OK, response.text

# Updates the robot class.
response = test_client.put(
"/robots/update",
response = test_client.post(
"/robots/test",
params={
"class_name": "test",
"new_class_name": "othertest",
"new_description": "new description",
},
Expand All @@ -50,10 +49,9 @@ async def test_robot_classes(test_client: TestClient) -> None:
assert response.status_code == status.HTTP_400_BAD_REQUEST, response.text

# Updates the robot class.
response = test_client.put(
"/robots/update",
response = test_client.post(
"/robots/test",
params={
"class_name": "test",
"new_class_name": "newtest",
"new_description": "new description",
},
Expand All @@ -69,10 +67,10 @@ async def test_robot_classes(test_client: TestClient) -> None:
assert all(robot_class["class_name"] in ("newtest", "othertest") for robot_class in data)

# Deletes the robot classes.
response = test_client.delete("/robots/delete", params={"class_name": "newtest"}, headers=HEADERS)
response = test_client.delete("/robots/newtest", headers=HEADERS)
assert response.status_code == status.HTTP_200_OK, response.text

response = test_client.delete("/robots/delete", params={"class_name": "othertest"}, headers=HEADERS)
response = test_client.delete("/robots/othertest", headers=HEADERS)
assert response.status_code == status.HTTP_200_OK, response.text

# Lists my robot classes.
Expand All @@ -84,15 +82,15 @@ async def test_robot_classes(test_client: TestClient) -> None:
@pytest.mark.asyncio
async def test_urdf(test_client: TestClient) -> None:
# Adds a robot class.
response = test_client.put("/robots/add", params={"class_name": "test"}, headers=HEADERS)
response = test_client.put("/robots/test", params={"description": "Test description"}, headers=HEADERS)
assert response.status_code == status.HTTP_200_OK, response.text

# Uploads a URDF for the robot class.
response = test_client.put(
"/robots/urdf/test",
params={
"filename": "robot.urdf",
"content_type": "application/gzip",
"filename": "robot_files.tgz",
"content_type": "application/x-compressed-tar",
},
headers=HEADERS,
)
Expand All @@ -104,7 +102,7 @@ async def test_urdf(test_client: TestClient) -> None:
async with httpx.AsyncClient() as client:
response = await client.put(
url=data["url"],
files={"file": ("robot.urdf", b"test", data["content_type"])},
files={"file": ("robot_files.tgz", b"test", data["content_type"])},
headers={"Content-Type": data["content_type"]},
)
assert response.status_code == status.HTTP_200_OK, response.text
Expand All @@ -123,7 +121,7 @@ async def test_urdf(test_client: TestClient) -> None:
assert data["md5_hash"] == f'"{hashlib.md5(content).hexdigest()}"'

# Deletes the robot classes.
response = test_client.delete("/robots/delete", params={"class_name": "test"}, headers=HEADERS)
response = test_client.delete("/robots/test", headers=HEADERS)
assert response.status_code == status.HTTP_200_OK, response.text

# Check that the URDF is deleted.
Expand Down
16 changes: 11 additions & 5 deletions www/crud/robot.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ async def add_robot(
)

# Check if the robot already exists.
existing_robot = await self.get_robot_by_name(robot_name)
existing_robot = await self.get_robot_by_name(robot_name, user_id)
if existing_robot is not None:
raise ValueError(f"Robot with name '{robot_name}' already exists")

Expand All @@ -93,13 +93,15 @@ async def add_robot(
async def update_robot(
self,
robot: Robot,
user_id: str,
new_robot_name: str | None = None,
new_description: str | None = None,
) -> Robot:
"""Updates a robot in the database.
Args:
robot: The robot to update.
user_id: The ID of the user who owns the robot.
new_robot_name: The new name of the robot.
new_description: The new description of the robot.
Expand All @@ -117,7 +119,7 @@ async def update_robot(
old_robot_name = robot.robot_name
if new_robot_name is not None:
if old_robot_name != new_robot_name:
if (await self.get_robot_by_name(new_robot_name)) is not None:
if (await self.get_robot_by_name(new_robot_name, user_id)) is not None:
raise ValueError(f"Robot with name '{new_robot_name}' already exists")
robot.robot_name = new_robot_name

Expand Down Expand Up @@ -145,26 +147,30 @@ async def delete_robot(self, robot: Robot) -> None:
table = await self.table
await table.delete_item(Key={"id": robot.id})

async def get_robot_by_name(self, robot_name: str) -> Robot | None:
async def get_robot_by_name(self, robot_name: str, user_id: str) -> Robot | None:
"""Gets a robot by name."""
table = await self.table
response = await table.query(
IndexName=self.get_gsi_index_name("robot_name"),
KeyConditionExpression=Key("robot_name").eq(robot_name),
FilterExpression=Key("user_id").eq(user_id),
)
if (items := response.get("Items", [])) == []:
return None
if len(items) > 1:
raise ValueError(f"Multiple robots with name '{robot_name}' found")
return Robot.model_validate(items[0])

async def get_robot_by_id(self, id: str) -> Robot | None:
async def get_robot_by_id(self, id: str, user_id: str) -> Robot | None:
"""Gets a robot by ID."""
table = await self.table
response = await table.get_item(Key={"id": id})
if (item := response.get("Item")) is None:
return None
return Robot.model_validate(item)
robot = Robot.model_validate(item)
if robot.user_id != user_id:
return None
return robot

async def list_robots(self, user_id: str | None = None) -> list[Robot]:
"""Gets all robots."""
Expand Down
29 changes: 19 additions & 10 deletions www/routers/robot.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ async def get_robots(

async def _get_robot_and_class_by_id(
id: str,
user: Annotated[User, Depends(require_user)],
crud: Annotated[RobotCrud, Depends(robot_crud)],
cls_crud: Annotated[RobotClassCrud, Depends(robot_class_crud)],
) -> tuple[Robot, RobotClass]:
robot = await crud.get_robot_by_id(id)
robot = await crud.get_robot_by_id(id, user.id)
if robot is None:
raise ItemNotFoundError(f"Robot '{id}' not found")
robot_class = await cls_crud.get_robot_class_by_id(robot.class_id)
Expand All @@ -55,20 +56,22 @@ async def _get_robot_and_class_by_id(

async def _get_base_robot_by_name(
robot_name: str,
user: Annotated[User, Depends(require_user)],
crud: Annotated[RobotCrud, Depends(robot_crud)],
) -> Robot:
robot = await crud.get_robot_by_name(robot_name)
robot = await crud.get_robot_by_name(robot_name, user.id)
if robot is None:
raise ItemNotFoundError(f"Robot '{robot_name}' not found")
return robot


async def _get_robot_and_class_by_name(
robot_name: str,
user: Annotated[User, Depends(require_user)],
crud: Annotated[RobotCrud, Depends(robot_crud)],
cls_crud: Annotated[RobotClassCrud, Depends(robot_class_crud)],
) -> tuple[Robot, RobotClass]:
robot = await crud.get_robot_by_name(robot_name)
robot = await crud.get_robot_by_name(robot_name, user.id)
if robot is None:
raise ItemNotFoundError(f"Robot '{robot_name}' not found")
robot_class = await cls_crud.get_robot_class_by_id(robot.class_id)
Expand All @@ -80,20 +83,22 @@ async def _get_robot_and_class_by_name(
@router.get("/name/{robot_name}")
async def get_robot_by_name(
robot_name: str,
user: Annotated[User, Depends(require_user)],
crud: Annotated[RobotCrud, Depends(robot_crud)],
cls_crud: Annotated[RobotClassCrud, Depends(robot_class_crud)],
) -> RobotResponse:
robot, robot_class = await _get_robot_and_class_by_name(robot_name, crud, cls_crud)
robot, robot_class = await _get_robot_and_class_by_name(robot_name, user, crud, cls_crud)
return RobotResponse.from_robot(robot, robot_class)


@router.get("/id/{id}")
async def get_robot_by_id(
id: str,
user: Annotated[User, Depends(require_user)],
crud: Annotated[RobotCrud, Depends(robot_crud)],
cls_crud: Annotated[RobotClassCrud, Depends(robot_class_crud)],
) -> RobotResponse:
robot, robot_class = await _get_robot_and_class_by_id(id, crud, cls_crud)
robot, robot_class = await _get_robot_and_class_by_id(id, user, crud, cls_crud)
return RobotResponse.from_robot(robot, robot_class)


Expand All @@ -109,18 +114,22 @@ async def get_robots_for_user(
return await crud.list_robots(user_id)


@router.put("/add")
@router.put("/{robot_name}")
async def add_robot(
robot_name: str,
user: Annotated[User, Depends(require_permissions({"upload"}))],
robot_class: Annotated[RobotClass, Depends(get_robot_class_by_name)],
crud: Annotated[RobotCrud, Depends(robot_crud)],
description: str | None = Query(
default=None,
description="The description of the robot",
),
) -> RobotResponse:
robot = await crud.add_robot(robot_name, user.id, robot_class.id)
robot = await crud.add_robot(robot_name, user.id, robot_class.id, description)
return RobotResponse.from_robot(robot, robot_class)


@router.put("/update")
@router.post("/{robot_name}")
async def update_robot(
user: Annotated[User, Depends(require_permissions({"upload"}))],
existing_robot_tuple: Annotated[tuple[Robot, RobotClass], Depends(_get_robot_and_class_by_name)],
Expand All @@ -137,11 +146,11 @@ async def update_robot(
existing_robot, existing_robot_class = existing_robot_tuple
if existing_robot.user_id != user.id:
raise ActionNotAllowedError("You are not the owner of this robot")
robot = await crud.update_robot(existing_robot, new_robot_name, new_description)
robot = await crud.update_robot(existing_robot, user.id, new_robot_name, new_description)
return RobotResponse.from_robot(robot, existing_robot_class)


@router.delete("/delete")
@router.delete("/{robot_name}")
async def delete_robot(
user: Annotated[User, Depends(require_user)],
robot: Annotated[Robot, Depends(_get_base_robot_by_name)],
Expand Down
Loading

0 comments on commit 5999940

Please sign in to comment.