Browse Source
* Update CRUD utils to use types better. * Simplify Pydantic model names, from `UserInCreate` to `UserCreate`, etc. * Upgrade packages. * Add new generic "Items" models, crud utils, endpoints, and tests. To facilitate re-using them to create new functionality. As they are simple and generic (not like Users), it's easier to copy-paste and adapt them to each use case. * Update endpoints/*path operations* to simplify code and use new utilities, prefix and tags in `include_router`. * Update testing utils. * Update linting rules, relax vulture to reduce false positives. * Update migrations to include new Items. * Update project README.md with tips about how to start with backend.pull/13907/head
committed by
GitHub
32 changed files with 426 additions and 1091 deletions
File diff suppressed because it is too large
@ -1,8 +1,9 @@ |
|||||
from fastapi import APIRouter |
from fastapi import APIRouter |
||||
|
|
||||
from app.api.api_v1.endpoints import token, user, utils |
from app.api.api_v1.endpoints import items, login, users, utils |
||||
|
|
||||
api_router = APIRouter() |
api_router = APIRouter() |
||||
api_router.include_router(token.router) |
api_router.include_router(login.router, tags=["login"]) |
||||
api_router.include_router(user.router) |
api_router.include_router(users.router, prefix="/users", tags=["users"]) |
||||
api_router.include_router(utils.router) |
api_router.include_router(utils.router, prefix="/utils", tags=["utils"]) |
||||
|
api_router.include_router(items.router, prefix="/items", tags=["items"]) |
||||
|
@ -0,0 +1,102 @@ |
|||||
|
from typing import List |
||||
|
|
||||
|
from fastapi import APIRouter, Depends, HTTPException |
||||
|
from sqlalchemy.orm import Session |
||||
|
|
||||
|
from app import crud |
||||
|
from app.api.utils.db import get_db |
||||
|
from app.api.utils.security import get_current_active_user |
||||
|
from app.db_models.user import User as DBUser |
||||
|
from app.models.item import Item, ItemCreate, ItemUpdate |
||||
|
|
||||
|
router = APIRouter() |
||||
|
|
||||
|
|
||||
|
@router.get("/", response_model=List[Item]) |
||||
|
def read_items( |
||||
|
db: Session = Depends(get_db), |
||||
|
skip: int = 0, |
||||
|
limit: int = 100, |
||||
|
current_user: DBUser = Depends(get_current_active_user), |
||||
|
): |
||||
|
""" |
||||
|
Retrieve items. |
||||
|
""" |
||||
|
if crud.user.is_superuser(current_user): |
||||
|
items = crud.item.get_multi(db, skip=skip, limit=limit) |
||||
|
else: |
||||
|
items = crud.item.get_multi_by_owner( |
||||
|
db_session=db, owner_id=current_user.id, skip=skip, limit=limit |
||||
|
) |
||||
|
return items |
||||
|
|
||||
|
|
||||
|
@router.post("/", response_model=Item) |
||||
|
def create_item( |
||||
|
*, |
||||
|
db: Session = Depends(get_db), |
||||
|
item_in: ItemCreate, |
||||
|
current_user: DBUser = Depends(get_current_active_user), |
||||
|
): |
||||
|
""" |
||||
|
Create new item. |
||||
|
""" |
||||
|
item = crud.item.create(db_session=db, item_in=item_in, owner_id=current_user.id) |
||||
|
return item |
||||
|
|
||||
|
|
||||
|
@router.put("/{id}", response_model=Item) |
||||
|
def update_item( |
||||
|
*, |
||||
|
db: Session = Depends(get_db), |
||||
|
id: int, |
||||
|
item_in: ItemUpdate, |
||||
|
current_user: DBUser = Depends(get_current_active_user), |
||||
|
): |
||||
|
""" |
||||
|
Update an item. |
||||
|
""" |
||||
|
item = crud.item.get(db_session=db, id=id) |
||||
|
if not item: |
||||
|
raise HTTPException(status_code=404, detail="Item not found") |
||||
|
if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id): |
||||
|
raise HTTPException(status_code=400, detail="Not enough permissions") |
||||
|
item = crud.item.update(db_session=db, item=item, item_in=item_in) |
||||
|
return item |
||||
|
|
||||
|
|
||||
|
@router.get("/{id}", response_model=Item) |
||||
|
def read_user_me( |
||||
|
*, |
||||
|
db: Session = Depends(get_db), |
||||
|
id: int, |
||||
|
current_user: DBUser = Depends(get_current_active_user), |
||||
|
): |
||||
|
""" |
||||
|
Get item by ID. |
||||
|
""" |
||||
|
item = crud.item.get(db_session=db, id=id) |
||||
|
if not item: |
||||
|
raise HTTPException(status_code=400, detail="Item not found") |
||||
|
if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id): |
||||
|
raise HTTPException(status_code=400, detail="Not enough permissions") |
||||
|
return item |
||||
|
|
||||
|
|
||||
|
@router.delete("/{id}", response_model=Item) |
||||
|
def delete_item( |
||||
|
*, |
||||
|
db: Session = Depends(get_db), |
||||
|
id: int, |
||||
|
current_user: DBUser = Depends(get_current_active_user), |
||||
|
): |
||||
|
""" |
||||
|
Delete an item. |
||||
|
""" |
||||
|
item = crud.item.get(db_session=db, id=id) |
||||
|
if not item: |
||||
|
raise HTTPException(status_code=404, detail="Item not found") |
||||
|
if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id): |
||||
|
raise HTTPException(status_code=400, detail="Not enough permissions") |
||||
|
item = crud.item.remove(db_session=db, id=id) |
||||
|
return item |
@ -1 +1 @@ |
|||||
from . import user |
from . import item, user |
||||
|
@ -0,0 +1,54 @@ |
|||||
|
from typing import List, Optional |
||||
|
|
||||
|
from fastapi.encoders import jsonable_encoder |
||||
|
from sqlalchemy.orm import Session |
||||
|
|
||||
|
from app.db_models.item import Item |
||||
|
from app.models.item import ItemCreate, ItemUpdate |
||||
|
|
||||
|
|
||||
|
def get(db_session: Session, *, id: int) -> Optional[Item]: |
||||
|
return db_session.query(Item).filter(Item.id == id).first() |
||||
|
|
||||
|
|
||||
|
def get_multi(db_session: Session, *, skip=0, limit=100) -> List[Optional[Item]]: |
||||
|
return db_session.query(Item).offset(skip).limit(limit).all() |
||||
|
|
||||
|
|
||||
|
def get_multi_by_owner( |
||||
|
db_session: Session, *, owner_id: int, skip=0, limit=100 |
||||
|
) -> List[Optional[Item]]: |
||||
|
return ( |
||||
|
db_session.query(Item) |
||||
|
.filter(Item.owner_id == owner_id) |
||||
|
.offset(skip) |
||||
|
.limit(limit) |
||||
|
.all() |
||||
|
) |
||||
|
|
||||
|
|
||||
|
def create(db_session: Session, *, item_in: ItemCreate, owner_id: int) -> Item: |
||||
|
item = Item(title=item_in.title, description=item_in.description, owner_id=owner_id) |
||||
|
db_session.add(item) |
||||
|
db_session.commit() |
||||
|
db_session.refresh(item) |
||||
|
return item |
||||
|
|
||||
|
|
||||
|
def update(db_session: Session, *, item: Item, item_in: ItemUpdate) -> Item: |
||||
|
item_data = jsonable_encoder(item) |
||||
|
update_data = item_in.dict(skip_defaults=True) |
||||
|
for field in item_data: |
||||
|
if field in update_data: |
||||
|
setattr(item, field, update_data[field]) |
||||
|
db_session.add(item) |
||||
|
db_session.commit() |
||||
|
db_session.refresh(item) |
||||
|
return item |
||||
|
|
||||
|
|
||||
|
def remove(db_session: Session, *, id: int): |
||||
|
item = db_session.query(Item).filter(Item.id == id).first() |
||||
|
db_session.delete(item) |
||||
|
db_session.commit() |
||||
|
return item |
@ -0,0 +1,12 @@ |
|||||
|
from sqlalchemy import Column, ForeignKey, Integer, String |
||||
|
from sqlalchemy.orm import relationship |
||||
|
|
||||
|
from app.db.base_class import Base |
||||
|
|
||||
|
|
||||
|
class Item(Base): |
||||
|
id = Column(Integer, primary_key=True, index=True) |
||||
|
title = Column(String, index=True) |
||||
|
description = Column(String, index=True) |
||||
|
owner_id = Column(Integer, ForeignKey("user.id")) |
||||
|
owner = relationship("User", back_populates="items") |
@ -0,0 +1,34 @@ |
|||||
|
from pydantic import BaseModel |
||||
|
|
||||
|
|
||||
|
# Shared properties |
||||
|
class ItemBase(BaseModel): |
||||
|
title: str = None |
||||
|
description: str = None |
||||
|
|
||||
|
|
||||
|
# Properties to receive on item creation |
||||
|
class ItemCreate(ItemBase): |
||||
|
title: str |
||||
|
|
||||
|
|
||||
|
# Properties to receive on item update |
||||
|
class ItemUpdate(ItemBase): |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
# Properties shared by models stored in DB |
||||
|
class ItemInDBBase(ItemBase): |
||||
|
id: int |
||||
|
title: str |
||||
|
owner_id: int |
||||
|
|
||||
|
|
||||
|
# Properties to return to client |
||||
|
class Item(ItemInDBBase): |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
# Properties properties stored in DB |
||||
|
class ItemInDB(ItemInDBBase): |
||||
|
pass |
@ -0,0 +1,34 @@ |
|||||
|
import requests |
||||
|
|
||||
|
from app.core import config |
||||
|
from app.tests.utils.item import create_random_item |
||||
|
from app.tests.utils.utils import get_server_api |
||||
|
|
||||
|
|
||||
|
def test_create_item(superuser_token_headers): |
||||
|
server_api = get_server_api() |
||||
|
data = {"title": "Foo", "description": "Fighters"} |
||||
|
response = requests.post( |
||||
|
f"{server_api}{config.API_V1_STR}/items/", |
||||
|
headers=superuser_token_headers, |
||||
|
json=data, |
||||
|
) |
||||
|
content = response.json() |
||||
|
assert content["title"] == data["title"] |
||||
|
assert content["description"] == data["description"] |
||||
|
assert "id" in content |
||||
|
assert "owner_id" in content |
||||
|
|
||||
|
|
||||
|
def test_read_item(superuser_token_headers): |
||||
|
item = create_random_item() |
||||
|
server_api = get_server_api() |
||||
|
response = requests.get( |
||||
|
f"{server_api}{config.API_V1_STR}/items/{item.id}", |
||||
|
headers=superuser_token_headers, |
||||
|
) |
||||
|
content = response.json() |
||||
|
assert content["title"] == item.title |
||||
|
assert content["description"] == item.description |
||||
|
assert content["id"] == item.id |
||||
|
assert content["owner_id"] == item.owner_id |
@ -0,0 +1,61 @@ |
|||||
|
from app import crud |
||||
|
from app.models.item import ItemCreate, ItemUpdate |
||||
|
from app.tests.utils.user import create_random_user |
||||
|
from app.tests.utils.utils import random_lower_string |
||||
|
from app.db.session import db_session |
||||
|
|
||||
|
|
||||
|
def test_create_item(): |
||||
|
title = random_lower_string() |
||||
|
description = random_lower_string() |
||||
|
item_in = ItemCreate(title=title, description=description) |
||||
|
user = create_random_user() |
||||
|
item = crud.item.create(db_session=db_session, item_in=item_in, owner_id=user.id) |
||||
|
assert item.title == title |
||||
|
assert item.description == description |
||||
|
assert item.owner_id == user.id |
||||
|
|
||||
|
|
||||
|
def test_get_item(): |
||||
|
title = random_lower_string() |
||||
|
description = random_lower_string() |
||||
|
item_in = ItemCreate(title=title, description=description) |
||||
|
user = create_random_user() |
||||
|
item = crud.item.create(db_session=db_session, item_in=item_in, owner_id=user.id) |
||||
|
stored_item = crud.item.get(db_session=db_session, id=item.id) |
||||
|
assert item.id == stored_item.id |
||||
|
assert item.title == stored_item.title |
||||
|
assert item.description == stored_item.description |
||||
|
assert item.owner_id == stored_item.owner_id |
||||
|
|
||||
|
|
||||
|
def test_update_item(): |
||||
|
title = random_lower_string() |
||||
|
description = random_lower_string() |
||||
|
item_in = ItemCreate(title=title, description=description) |
||||
|
user = create_random_user() |
||||
|
item = crud.item.create(db_session=db_session, item_in=item_in, owner_id=user.id) |
||||
|
description2 = random_lower_string() |
||||
|
item_update = ItemUpdate(description=description2) |
||||
|
item2 = crud.item.update( |
||||
|
db_session=db_session, item=item, item_in=item_update |
||||
|
) |
||||
|
assert item.id == item2.id |
||||
|
assert item.title == item2.title |
||||
|
assert item2.description == description2 |
||||
|
assert item.owner_id == item2.owner_id |
||||
|
|
||||
|
|
||||
|
def test_delete_item(): |
||||
|
title = random_lower_string() |
||||
|
description = random_lower_string() |
||||
|
item_in = ItemCreate(title=title, description=description) |
||||
|
user = create_random_user() |
||||
|
item = crud.item.create(db_session=db_session, item_in=item_in, owner_id=user.id) |
||||
|
item2 = crud.item.remove(db_session=db_session, id=item.id) |
||||
|
item3 = crud.item.get(db_session=db_session, id=item.id) |
||||
|
assert item3 is None |
||||
|
assert item2.id == item.id |
||||
|
assert item2.title == title |
||||
|
assert item2.description == description |
||||
|
assert item2.owner_id == user.id |
@ -0,0 +1,17 @@ |
|||||
|
from app import crud |
||||
|
from app.db.session import db_session |
||||
|
from app.models.item import ItemCreate |
||||
|
from app.tests.utils.user import create_random_user |
||||
|
from app.tests.utils.utils import random_lower_string |
||||
|
|
||||
|
|
||||
|
def create_random_item(owner_id: int = None): |
||||
|
if owner_id is None: |
||||
|
user = create_random_user() |
||||
|
owner_id = user.id |
||||
|
title = random_lower_string() |
||||
|
description = random_lower_string() |
||||
|
item_in = ItemCreate(title=title, description=description, id=id) |
||||
|
return crud.item.create( |
||||
|
db_session=db_session, item_in=item_in, owner_id=owner_id |
||||
|
) |
Loading…
Reference in new issue