Skip to content

Commit

Permalink
feat: add Storage endpoints (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
bathienle authored Aug 1, 2024
1 parent 2457f60 commit 4749fd2
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 1 deletion.
96 changes: 96 additions & 0 deletions cbir/api/storages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Storage API"""

import shutil
from pathlib import Path

from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import JSONResponse

from cbir.api.utils.models import Storage

router = APIRouter()


@router.get("/storages")
async def get_storages(request: Request) -> JSONResponse:
"""Get all storages."""

settings = request.app.state.database.settings
base_path = Path(settings.data_path)

if not base_path.is_dir():
raise HTTPException(
status_code=404,
detail="Base path not found or is not a directory",
)

folder_names = [f.name for f in base_path.iterdir() if f.is_dir()]

return JSONResponse(content=folder_names)


@router.post("/storages")
async def create_storage(request: Request, body: Storage) -> JSONResponse:
"""Create a new storage."""

settings = request.app.state.database.settings
base_path = Path(settings.data_path)
storage_path = base_path / body.name

if storage_path.exists():
raise HTTPException(
status_code=409,
detail=f"Storage with name '{body.name}' already exists.",
)

try:
storage_path.mkdir()
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to create storage: {str(e)}",
) from e

return JSONResponse(content={"message": f"Created storage with name: {body.name}"})


@router.get("/storages/{name}")
async def get_storage(request: Request, name: str) -> JSONResponse:
"""Get a specific storage."""

settings = request.app.state.database.settings
base_path = Path(settings.data_path)
storage_path = base_path / name

if not storage_path.is_dir():
raise HTTPException(
status_code=404,
detail=f"Storage with name '{name}' not found.",
)

return JSONResponse(content={"name": name})


@router.delete("/storages/{name}")
async def delete_storage(request: Request, name: str) -> JSONResponse:
"""Delete a specific storage and its content."""

settings = request.app.state.database.settings
base_path = Path(settings.data_path)
storage_path = base_path / name

if not storage_path.is_dir():
raise HTTPException(
status_code=404,
detail=f"Storage with name '{name}' not found.",
)

try:
shutil.rmtree(storage_path)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to delete storage: {str(e)}",
) from e

return JSONResponse(content={"message": f"Deleted storage with name: {name}"})
Empty file added cbir/api/utils/__init__.py
Empty file.
11 changes: 11 additions & 0 deletions cbir/api/utils/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""DTO Models"""

from pydantic import BaseModel


class Storage(BaseModel):
"""
Storage model.
"""

name: str
3 changes: 2 additions & 1 deletion cbir/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from fastapi import FastAPI

from cbir import __version__
from cbir.api import image
from cbir.api import image, storages
from cbir.config import DatabaseSetting, ModelSetting
from cbir.models.model import Model
from cbir.models.resnet import Resnet
Expand Down Expand Up @@ -71,3 +71,4 @@ async def lifespan(local_app: FastAPI) -> AsyncGenerator[None, None]:
},
)
app.include_router(router=image.router, prefix="/api")
app.include_router(router=storages.router, prefix="/api")
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ line_length = 79
ignore_missing_imports = true
plugins = ["pydantic.mypy"]

[tool.pylint]
disable = ["redefined-outer-name"]
output-format = "colorized"

[tool.poetry]
name = "cytomine-cbir"
version = "0.1.0"
Expand Down
170 changes: 170 additions & 0 deletions tests/test_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
"""Storage tests"""

import shutil
import tempfile
from typing import Any, Generator

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

from cbir.api.storages import router


@pytest.fixture
def app() -> Generator[Any, Any, Any]:
"""
Create and provide a FastAPI application instance for testing.
The application is configured with:
- A router included in the FastAPI instance.
- A mock database with temporary settings stored in a temporary directory.
Yields:
local_app: The FastAPI app instance with router and mock database.
"""

local_app = FastAPI()
local_app.include_router(router)

settings = type("Settings", (object,), {"data_path": tempfile.mkdtemp()})
local_app.state.database = type("Database", (object,), {"settings": settings})

yield local_app

# Cleanup after tests
shutil.rmtree(local_app.state.database.settings.data_path) # type: ignore


@pytest.fixture
def client(app: FastAPI) -> TestClient:
"""
Provide a test client for the FastAPI application.
Args:
app (FastAPI): The FastAPI application instance to be tested.
Returns:
TestClient: An instance of `TestClient` that can be used to send
requests to the FastAPI application and inspect the responses.
"""
return TestClient(app)


def test_get_storages(client: TestClient) -> None:
"""
Test the GET /storages endpoint.
Args:
client: A test client instance used to send requests to the application.
"""

response = client.get("/storages")
assert response.status_code == 200
assert response.json() == []


def test_create_storage(client: TestClient) -> None:
"""
Test the POST /storages endpoint.
Args:
client: A test client instance used to send requests to the application.
"""

storage_name = "test_storage"

response = client.post("/storages", json={"name": storage_name})
assert response.status_code == 200
assert response.json() == {"message": f"Created storage with name: {storage_name}"}

response = client.get("/storages")
assert response.status_code == 200
assert storage_name in response.json()


def test_get_storage(client: TestClient) -> None:
"""
Test the GET /storages/{name} endpoint.
Args:
client: A test client instance used to send requests to the application.
"""

storage_name = "test_storage"
client.post("/storages", json={"name": storage_name})

response = client.get(f"/storages/{storage_name}")
assert response.status_code == 200
assert response.json() == {"name": storage_name}


def test_get_storage_not_found(client: TestClient) -> None:
"""
Test the GET /storages/{name} endpoint for non-existent storage.
Args:
client: A test client instance used to send requests to the application.
"""

storage_name = "non_existent_storage"

response = client.get(f"/storages/{storage_name}")
assert response.status_code == 404
assert response.json() == {
"detail": f"Storage with name '{storage_name}' not found."
}


def test_create_existing_storage(client: TestClient) -> None:
"""
Test the POST /storages/{name} endpoint for an existing storage.
Args:
client: A test client instance used to send requests to the application.
"""

storage_name = "test_storage"
client.post("/storages", json={"name": storage_name})

response = client.post("/storages", json={"name": storage_name})
assert response.status_code == 409
assert response.json() == {
"detail": f"Storage with name '{storage_name}' already exists."
}


def test_delete_storage(client: TestClient) -> None:
"""
Test the DELETE /storages/{name} endpoint.
Args:
client: A test client instance used to send requests to the application.
"""

storage_name = "test_storage"
client.post("/storages", json={"name": storage_name})

response = client.delete(f"/storages/{storage_name}")
assert response.status_code == 200
assert response.json() == {"message": f"Deleted storage with name: {storage_name}"}

response = client.get(f"/storages/{storage_name}")
assert response.status_code == 404


def test_delete_storage_not_found(client: TestClient) -> None:
"""
Test the DELETE /storages/{name} endpoint when the storage does not exist.
Args:
client: A test client instance used to send requests to the application.
"""

storage_name = "non_existent_storage"

response = client.delete(f"/storages/{storage_name}")
assert response.status_code == 404
assert response.json() == {
"detail": f"Storage with name '{storage_name}' not found."
}

0 comments on commit 4749fd2

Please sign in to comment.