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 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.include_router(token.router) |
|||
api_router.include_router(user.router) |
|||
api_router.include_router(utils.router) |
|||
api_router.include_router(login.router, tags=["login"]) |
|||
api_router.include_router(users.router, prefix="/users", tags=["users"]) |
|||
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