Skip to content

Commit

Permalink
crud (#670)
Browse files Browse the repository at this point in the history
* crud

* creation script

* fix tests

* robot classes crud

* robot router / crud implementation

* urdf uploading and downloading

* remove environment

* fix tests

* robot fixes

* nit fix
  • Loading branch information
codekansas authored Jan 15, 2025
1 parent 680ef5d commit b9a1e90
Show file tree
Hide file tree
Showing 27 changed files with 1,340 additions and 592 deletions.
1 change: 0 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ jobs:
run-tests:
timeout-minutes: 10
runs-on: ubuntu-latest
environment: ${{ github.ref == 'refs/heads/master' && 'production' || 'staging' }}

steps:
- name: Check out repository
Expand Down
57 changes: 16 additions & 41 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,63 +1,38 @@
# Makefile

# ------------------------ #
# Serve #
# ------------------------ #
ENV_VARS = \
ENVIRONMENT=local \
AWS_REGION=us-east-1 \
AWS_DEFAULT_REGION=us-east-1 \
AWS_ACCESS_KEY_ID=test \
AWS_SECRET_ACCESS_KEY=test \
AWS_ENDPOINT_URL=http://localhost:4566

start:
@if [ -f env.sh ]; then source env.sh; fi; fastapi dev 'www/main.py' --host localhost --port 8080
$(ENV_VARS) fastapi dev 'www/main.py' --host localhost --port 8080
.PHONY: start

start-docker-dynamodb:
@docker kill www-db || true
@docker rm www-db || true
@docker run --name www-db -d -p 8000:8000 amazon/dynamodb-local
.PHONY: start-docker-dynamodb
start-localstack:
@docker compose --file docker/docker-compose-localstack.yml down --remove-orphans
@docker rm -f www-localstack 2>/dev/null || true
@docker compose --file docker/docker-compose-localstack.yml up -d localstack --force-recreate
.PHONY: start-localstack

start-docker-backend:
@docker kill www-backend || true
@docker rm www-backend || true
@docker build -t www-backend .
@docker run --name www-backend -d -p 8080:8080 www-backend
.PHONY: start-docker-backend

start-docker-localstack:
@docker kill www-localstack || true
@docker rm www-localstack || true
@docker run -d --name www-localstack -p 4566:4566 -p 4571:4571 localstack/localstack
.PHONY: start-docker-localstack

# ------------------------ #
# Install #
# ------------------------ #

install:
@pip install -e '[.dev]'
.PHONY: install

# ------------------------ #
# Code Formatting #
# ------------------------ #
create-db:
$(ENV_VARS) python -m scripts.create_db --s3 --db
.PHONY: create-db

format:
@black www tests
@ruff check --fix www tests
.PHONY: format

# ------------------------ #
# Static Checks #
# ------------------------ #

lint:
@black --diff --check www tests
@ruff check www tests
@mypy --install-types --non-interactive www tests
.PHONY: lint

# ------------------------ #
# Unit tests #
# ------------------------ #

test:
@python -m pytest
.PHONY: test-backend
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,19 @@
# K-Scale Website

This is the codebase for K-Scale's web infrastructure.

## Getting Started

First, pull the repository and install the project:

```bash
git clone https://github.com/kscalelabs/www.git
cd www
pip install -e '.[dev]'
```

Next, start localstack:

```bash
make start-localstack
```
9 changes: 9 additions & 0 deletions docker/docker-compose-localstack.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
services:
localstack:
image: localstack/localstack
container_name: www-localstack
ports:
- "4566:4566"
- "4571:4571"
restart: always
pull_policy: always
47 changes: 47 additions & 0 deletions scripts/create_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Defines the script to create the database.
This script is meant to be run locally, to create the initial database tables
in localstack. To run it, use:
```bash
python -m scripts.create_db --s3
```
"""

import argparse
import asyncio
import logging

import colorlogging

from www.crud.base.db import DBCrud
from www.crud.base.s3 import create_s3_bucket
from www.crud.robot import robot_crud
from www.crud.robot_class import robot_class_crud

logger = logging.getLogger(__name__)

CRUDS: list[DBCrud] = [robot_class_crud, robot_crud]


async def main() -> None:
colorlogging.configure()

parser = argparse.ArgumentParser()
parser.add_argument("--s3", action="store_true", help="Create the S3 bucket.")
parser.add_argument("--db", action="store_true", help="Create the DynamoDB tables.")
args = parser.parse_args()

if args.s3:
logger.info("Creating S3 bucket...")
await create_s3_bucket()

if args.db:
for crud in CRUDS:
async with crud:
logger.info("Creating %s table...", crud.table_name)
await crud.create_table()


if __name__ == "__main__":
asyncio.run(main())
13 changes: 12 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from moto.server import ThreadedMotoServer
from pytest_mock.plugin import AsyncMockType, MockerFixture, MockType

from www.crud import create

os.environ["ENVIRONMENT"] = "local"


Expand All @@ -20,13 +22,19 @@ def pytest_collection_modifyitems(items: list[Function]) -> None:


@pytest.fixture(autouse=True)
def mock_aws() -> Generator[None, None, None]:
async def mock_aws() -> AsyncGenerator[None, None]:
server: ThreadedMotoServer | None = None

# logging.getLogger("botocore").setLevel(logging.DEBUG)
logging.getLogger("botocore").setLevel(logging.WARN)

try:
# Sets required AWS environment variables.
os.environ["AWS_ACCESS_KEY_ID"] = "test"
os.environ["AWS_SECRET_ACCESS_KEY"] = "test"
os.environ["AWS_SESSION_TOKEN"] = "test"
os.environ["AWS_DEFAULT_REGION"] = os.environ["AWS_REGION"] = "us-east-1"

# Starts a local AWS server.
server = ThreadedMotoServer(port=0)
server.start()
Expand All @@ -36,6 +44,9 @@ def mock_aws() -> Generator[None, None, None]:
os.environ["AWS_ENDPOINT_URL_DYNAMODB"] = endpoint
os.environ["AWS_ENDPOINT_URL_S3"] = endpoint

# Create the S3 bucket and DynamoDB tables.
await create()

yield

finally:
Expand Down
6 changes: 4 additions & 2 deletions tests/routers/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from fastapi import status
from fastapi.testclient import TestClient

HEADERS = {"Authorization": "Bearer test"}


@pytest.mark.asyncio
async def test_user_endpoint(test_client: TestClient) -> None:
Expand All @@ -27,7 +29,7 @@ async def test_profile_endpoint(test_client: TestClient) -> None:
response = test_client.get("/auth/profile")
assert response.status_code == status.HTTP_401_UNAUTHORIZED, response.text

response = test_client.get("/auth/profile", headers={"Authorization": "Bearer test"})
response = test_client.get("/auth/profile", headers=HEADERS)
assert response.status_code == status.HTTP_200_OK, response.text

# Matches test user data.
Expand All @@ -45,5 +47,5 @@ async def test_profile_endpoint(test_client: TestClient) -> None:

@pytest.mark.asyncio
async def test_logout_endpoint(test_client: TestClient) -> None:
response = test_client.get("/auth/logout", headers={"Authorization": "Bearer test"})
response = test_client.get("/auth/logout", headers=HEADERS)
assert response.status_code == status.HTTP_200_OK, response.text
103 changes: 103 additions & 0 deletions tests/routers/test_robot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""Unit tests for the robot router."""

import pytest
from fastapi import status
from fastapi.testclient import TestClient

HEADERS = {"Authorization": "Bearer test"}


@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)
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
)
assert response.status_code == status.HTTP_200_OK, response.text
data = response.json()
robot_id = data["id"]
assert robot_id is not None
assert data["robot_name"] == "test_robot"
assert data["class_name"] == "test_class"

# 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
)
assert response.status_code == status.HTTP_400_BAD_REQUEST, response.text

# Gets all robots
response = test_client.get("/robot", headers=HEADERS)
assert response.status_code == status.HTTP_200_OK, response.text
data = response.json()
assert len(data) == 1
assert data[0]["robot_name"] == "test_robot"

# Gets the robot by name
response = test_client.get("/robot/name/test_robot", headers=HEADERS)
assert response.status_code == status.HTTP_200_OK, response.text
data = response.json()
assert data["robot_name"] == "test_robot"

# Gets the robot by ID
response = test_client.get(f"/robot/id/{robot_id}", headers=HEADERS)
assert response.status_code == status.HTTP_200_OK, response.text
data = response.json()
assert data["robot_name"] == "test_robot"

# Adds a second robot
response = test_client.put(
"/robot/add", params={"robot_name": "other_robot", "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",
params={
"robot_name": "test_robot",
"new_robot_name": "updated_robot",
"new_description": "new description",
},
headers=HEADERS,
)
assert response.status_code == status.HTTP_200_OK, response.text
data = response.json()
assert data["robot_name"] == "updated_robot"
assert data["description"] == "new description"

# Lists my robots
response = test_client.get("/robot/user/me", headers=HEADERS)
assert response.status_code == status.HTTP_200_OK, response.text
data = response.json()
assert len(data) == 2
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)
assert response.status_code == status.HTTP_200_OK, response.text

# Lists my robots again
response = test_client.get("/robot/user/me", headers=HEADERS)
assert response.status_code == status.HTTP_200_OK, response.text
data = response.json()
assert len(data) == 1
assert data[0]["robot_name"] == "other_robot"

# Clean up - delete remaining robot and robot class
response = test_client.get("/robot/name/other_robot", headers=HEADERS)
assert response.status_code == status.HTTP_200_OK, response.text
data = response.json()
assert data["id"] is not None

response = test_client.delete("/robot/delete", params={"robot_name": "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)
assert response.status_code == status.HTTP_200_OK, response.text
Loading

0 comments on commit b9a1e90

Please sign in to comment.