Skip to content

Commit

Permalink
Refactor the API to use a sample in-memory User model
Browse files Browse the repository at this point in the history
  • Loading branch information
IbraheemTuffaha committed Aug 18, 2024
1 parent f475bc3 commit 18348b2
Show file tree
Hide file tree
Showing 13 changed files with 191 additions and 25 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,14 @@ docker run -p 8000:8000 -it app
- If you face an issue with **git ssh access** while pushing new changes, run `ssh-add $HOME/.ssh/<your ssh key>` in terminal outside the devcontainer.

- If you face an issue during **devcontainer build**, make sure the repo is marked as trusted in VSCode. Check `Source Control` tab in the sidebar to mark the repo safe, then rebuild the devcontainer.

## Sample CRUD API

The `/v1` directory contains a sample API router demonstrating basic CRUD operations for users:

- Endpoints: Create, Read, Update, Delete users
- Router setup: `app/v1/routers/base.py` and `app/v1/routers/users.py`
- User model: `app/v1/models/user.py`
- User management: `app/v1/services/user/user_manager.py`

Use the samples as a starting point for your own API endpoints. View available endpoints at `http://localhost:8000/docs`.
3 changes: 3 additions & 0 deletions app/v1/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__all__ = ["User"]

from app.v1.models.user import User
6 changes: 6 additions & 0 deletions app/v1/models/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pydantic import BaseModel, EmailStr


class User(BaseModel):
username: str
email: EmailStr
4 changes: 2 additions & 2 deletions app/v1/routers/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from fastapi import APIRouter

from app.v1.routers.dummy import router as dummy_router
from app.v1.routers.users import router as users_router


router = APIRouter(prefix="/v1")
router.include_router(dummy_router, prefix="/dummy", tags=["Dummy"])
router.include_router(users_router, prefix="/users", tags=["Users"])
11 changes: 0 additions & 11 deletions app/v1/routers/dummy.py

This file was deleted.

44 changes: 44 additions & 0 deletions app/v1/routers/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from fastapi import APIRouter, Depends, HTTPException, status

from app.v1.models import User
from app.v1.services import UserManager, get_user_manager


router = APIRouter()


@router.post("/")
async def create_user(user: User, user_manager: UserManager = Depends(get_user_manager)) -> User:
try:
return user_manager.create_user(user)
except ValueError as ex:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(ex))


@router.get("/{username}")
async def get_user(username: str, user_manager: UserManager = Depends(get_user_manager)) -> User:
try:
return user_manager.get_user(username)
except ValueError as ex:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(ex))


@router.get("/")
async def list_users(user_manager: UserManager = Depends(get_user_manager)) -> list[User]:
return user_manager.list_users()


@router.put("/{username}")
async def update_user(username: str, user: User, user_manager: UserManager = Depends(get_user_manager)) -> User:
try:
return user_manager.update_user(username, user)
except ValueError as ex:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(ex))


@router.delete("/{username}")
async def delete_user(username: str, user_manager: UserManager = Depends(get_user_manager)) -> User:
try:
return user_manager.delete_user(username)
except ValueError as ex:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(ex))
5 changes: 0 additions & 5 deletions app/v1/serializers/dummy.py

This file was deleted.

4 changes: 4 additions & 0 deletions app/v1/services/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__all__ = ["get_user_manager", "UserManager"]

from app.v1.services.user.dependency import get_user_manager
from app.v1.services.user.user_manager import UserManager
File renamed without changes.
5 changes: 5 additions & 0 deletions app/v1/services/user/dependency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from app.v1.services.user.user_manager import UserManager


def get_user_manager() -> UserManager:
return UserManager()
30 changes: 30 additions & 0 deletions app/v1/services/user/user_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from app.v1.models import User


class UserManager:
_users: dict[str, User] = {}

def create_user(self, user: User) -> User:
if user.username in self._users:
raise ValueError(f"User with username '{user.username}' already exists")
self._users[user.username] = user
return user

def get_user(self, username: str) -> User:
if username not in self._users:
raise ValueError(f"User with username '{username}' does not exist")
return self._users[username]

def list_users(self) -> list[User]:
return list(self._users.values())

def update_user(self, username: str, user: User) -> User:
if username not in self._users:
raise ValueError(f"User with username '{username}' does not exist")
self._users[username] = user
return user

def delete_user(self, username: str) -> User:
if username not in self._users:
raise ValueError(f"User with username '{username}' does not exist")
return self._users.pop(username)
7 changes: 0 additions & 7 deletions tests/v1/routers/test_dummy.py

This file was deleted.

86 changes: 86 additions & 0 deletions tests/v1/routers/test_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from fastapi.testclient import TestClient


def test_create_user(client: TestClient) -> None:
user_data = {"username": "testuser", "email": "[email protected]"}
response = client.post("/v1/users/", json=user_data)
assert response.status_code == 200
assert response.json() == user_data


def test_get_user(client: TestClient) -> None:
# First, create a user
user_data = {"username": "getuser", "email": "[email protected]"}
client.post("/v1/users/", json=user_data)

# Then, retrieve the user
response = client.get("/v1/users/getuser")
assert response.status_code == 200
assert response.json() == user_data


def test_list_users(client: TestClient) -> None:
# Create two users
user1 = {"username": "user1", "email": "[email protected]"}
user2 = {"username": "user2", "email": "[email protected]"}
client.post("/v1/users/", json=user1)
client.post("/v1/users/", json=user2)

# List all users
response = client.get("/v1/users/")
assert response.status_code == 200
assert len(response.json()) >= 2
assert user1 in response.json()
assert user2 in response.json()


def test_update_user(client: TestClient) -> None:
# First, create a user
original_data = {"username": "updateuser", "email": "[email protected]"}
client.post("/v1/users/", json=original_data)

# Then, update the user
updated_data = {"username": "updateuser", "email": "[email protected]"}
response = client.put("/v1/users/updateuser", json=updated_data)
assert response.status_code == 200
assert response.json() == updated_data


def test_delete_user(client: TestClient) -> None:
# First, create a user
user_data = {"username": "deleteuser", "email": "[email protected]"}
client.post("/v1/users/", json=user_data)

# Then, delete the user
response = client.delete("/v1/users/deleteuser")
assert response.status_code == 200
assert response.json() == user_data

# Verify the user is deleted
response = client.get("/v1/users/deleteuser")
assert response.status_code == 404


def test_create_duplicate_user(client: TestClient) -> None:
user_data = {"username": "duplicate", "email": "[email protected]"}
client.post("/v1/users/", json=user_data)

# Try to create a user with the same username
response = client.post("/v1/users/", json=user_data)
assert response.status_code == 409


def test_get_nonexistent_user(client: TestClient) -> None:
response = client.get("/v1/users/nonexistent")
assert response.status_code == 404


def test_update_nonexistent_user(client: TestClient) -> None:
user_data = {"username": "nonexistent", "email": "[email protected]"}
response = client.put("/v1/users/nonexistent", json=user_data)
assert response.status_code == 404


def test_delete_nonexistent_user(client: TestClient) -> None:
response = client.delete("/v1/users/nonexistent")
assert response.status_code == 404

0 comments on commit 18348b2

Please sign in to comment.