Skip to content

Commit ab46165

Browse files
ebretontiangolo
authored andcommitted
✨ Add base class to simplify CRUD (#23)
1 parent 1c975c7 commit ab46165

File tree

33 files changed

+321
-282
lines changed

33 files changed

+321
-282
lines changed

test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ cookiecutter --config-file ./testing-config.yml --no-input -f ./
99

1010
cd ./testing-project
1111

12-
bash ./scripts/test.sh
12+
bash ./scripts/test.sh "$@"
1313

1414
cd ../

{{cookiecutter.project_slug}}/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ If your Docker is not running in `localhost` (the URLs above wouldn't work) chec
5555

5656
Open your editor at `./backend/app/` (instead of the project root: `./`), so that you see an `./app/` directory with your code inside. That way, your editor will be able to find all the imports, etc.
5757

58-
Modify or add SQLAlchemy models in `./backend/app/app/db_models/`, Pydantic models in `./backend/app/app/models/`, API endpoints in `./backend/app/app/api/`, CRUD (Create, Read, Update, Delete) utils in `./backend/app/app/crud/`. The easiest might be to copy the ones for Items (models, endpoints, and CRUD utils) and update them to your needs.
58+
Modify or add SQLAlchemy models in `./backend/app/app/models/`, Pydantic schemas in `./backend/app/app/schemas/`, API endpoints in `./backend/app/app/api/`, CRUD (Create, Read, Update, Delete) utils in `./backend/app/app/crud/`. The easiest might be to copy the ones for Items (models, endpoints, and CRUD utils) and update them to your needs.
5959

6060
Add and modify tasks to the Celery worker in `./backend/app/app/worker.py`.
6161

@@ -205,7 +205,7 @@ Make sure you create a "revision" of your models and that you "upgrade" your dat
205205
docker-compose exec backend bash
206206
```
207207

208-
* If you created a new model in `./backend/app/app/db_models/`, make sure to import it in `./backend/app/app/db/base.py`, that Python module (`base.py`) that imports all the models will be used by Alembic.
208+
* If you created a new model in `./backend/app/app/models/`, make sure to import it in `./backend/app/app/db/base.py`, that Python module (`base.py`) that imports all the models will be used by Alembic.
209209

210210
* After changing a model (for example, adding a column), inside the container, create a revision, e.g.:
211211

{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/items.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
from app import crud
77
from app.api.utils.db import get_db
88
from app.api.utils.security import get_current_active_user
9-
from app.db_models.user import User as DBUser
10-
from app.models.item import Item, ItemCreate, ItemUpdate
9+
from app.models.user import User as DBUser
10+
from app.schemas.item import Item, ItemCreate, ItemUpdate
1111

1212
router = APIRouter()
1313

@@ -41,7 +41,9 @@ def create_item(
4141
"""
4242
Create new item.
4343
"""
44-
item = crud.item.create(db_session=db, item_in=item_in, owner_id=current_user.id)
44+
item = crud.item.create_with_owner(
45+
db_session=db, obj_in=item_in, owner_id=current_user.id
46+
)
4547
return item
4648

4749

@@ -61,7 +63,7 @@ def update_item(
6163
raise HTTPException(status_code=404, detail="Item not found")
6264
if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id):
6365
raise HTTPException(status_code=400, detail="Not enough permissions")
64-
item = crud.item.update(db_session=db, item=item, item_in=item_in)
66+
item = crud.item.update(db_session=db, db_obj=item, obj_in=item_in)
6567
return item
6668

6769

{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
from app.core import config
1111
from app.core.jwt import create_access_token
1212
from app.core.security import get_password_hash
13-
from app.db_models.user import User as DBUser
14-
from app.models.msg import Msg
15-
from app.models.token import Token
16-
from app.models.user import User
13+
from app.models.user import User as DBUser
14+
from app.schemas.msg import Msg
15+
from app.schemas.token import Token
16+
from app.schemas.user import User
1717
from app.utils import (
1818
generate_password_reset_token,
1919
send_reset_password_email,

{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
from fastapi import APIRouter, Body, Depends, HTTPException
44
from fastapi.encoders import jsonable_encoder
5-
from pydantic.types import EmailStr
5+
from pydantic.networks import EmailStr
66
from sqlalchemy.orm import Session
77

88
from app import crud
99
from app.api.utils.db import get_db
1010
from app.api.utils.security import get_current_active_superuser, get_current_active_user
1111
from app.core import config
12-
from app.db_models.user import User as DBUser
13-
from app.models.user import User, UserCreate, UserInDB, UserUpdate
12+
from app.models.user import User as DBUser
13+
from app.schemas.user import User, UserCreate, UserUpdate
1414
from app.utils import send_new_account_email
1515

1616
router = APIRouter()
@@ -46,7 +46,7 @@ def create_user(
4646
status_code=400,
4747
detail="The user with this username already exists in the system.",
4848
)
49-
user = crud.user.create(db, user_in=user_in)
49+
user = crud.user.create(db, obj_in=user_in)
5050
if config.EMAILS_ENABLED and user_in.email:
5151
send_new_account_email(
5252
email_to=user_in.email, username=user_in.email, password=user_in.password
@@ -74,7 +74,7 @@ def update_user_me(
7474
user_in.full_name = full_name
7575
if email is not None:
7676
user_in.email = email
77-
user = crud.user.update(db, user=current_user, user_in=user_in)
77+
user = crud.user.update(db, db_obj=current_user, obj_in=user_in)
7878
return user
7979

8080

@@ -103,7 +103,7 @@ def create_user_open(
103103
if not config.USERS_OPEN_REGISTRATION:
104104
raise HTTPException(
105105
status_code=403,
106-
detail="Open user resgistration is forbidden on this server",
106+
detail="Open user registration is forbidden on this server",
107107
)
108108
user = crud.user.get_by_email(db, email=email)
109109
if user:
@@ -112,7 +112,7 @@ def create_user_open(
112112
detail="The user with this username already exists in the system",
113113
)
114114
user_in = UserCreate(password=password, email=email, full_name=full_name)
115-
user = crud.user.create(db, user_in=user_in)
115+
user = crud.user.create(db, obj_in=user_in)
116116
return user
117117

118118

@@ -125,7 +125,7 @@ def read_user_by_id(
125125
"""
126126
Get a specific user by id.
127127
"""
128-
user = crud.user.get(db, user_id=user_id)
128+
user = crud.user.get(db, id=user_id)
129129
if user == current_user:
130130
return user
131131
if not crud.user.is_superuser(current_user):
@@ -141,16 +141,16 @@ def update_user(
141141
db: Session = Depends(get_db),
142142
user_id: int,
143143
user_in: UserUpdate,
144-
current_user: UserInDB = Depends(get_current_active_superuser),
144+
current_user: DBUser = Depends(get_current_active_superuser),
145145
):
146146
"""
147147
Update a user.
148148
"""
149-
user = crud.user.get(db, user_id=user_id)
149+
user = crud.user.get(db, id=user_id)
150150
if not user:
151151
raise HTTPException(
152152
status_code=404,
153153
detail="The user with this username does not exist in the system",
154154
)
155-
user = crud.user.update(db, user=user, user_in=user_in)
155+
user = crud.user.update(db, db_obj=user, obj_in=user_in)
156156
return user

{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/utils.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
from fastapi import APIRouter, Depends
2-
from pydantic.types import EmailStr
2+
from pydantic.networks import EmailStr
33

44
from app.api.utils.security import get_current_active_superuser
55
from app.core.celery_app import celery_app
6-
from app.models.msg import Msg
7-
from app.models.user import UserInDB
6+
from app.schemas.msg import Msg
7+
from app.schemas.user import User
8+
from app.models.user import User as DBUser
89
from app.utils import send_test_email
910

1011
router = APIRouter()
1112

1213

1314
@router.post("/test-celery/", response_model=Msg, status_code=201)
1415
def test_celery(
15-
msg: Msg, current_user: UserInDB = Depends(get_current_active_superuser)
16+
msg: Msg, current_user: DBUser = Depends(get_current_active_superuser)
1617
):
1718
"""
1819
Test Celery worker.
@@ -23,7 +24,7 @@ def test_celery(
2324

2425
@router.post("/test-email/", response_model=Msg, status_code=201)
2526
def test_email(
26-
email_to: EmailStr, current_user: UserInDB = Depends(get_current_active_superuser)
27+
email_to: EmailStr, current_user: DBUser = Depends(get_current_active_superuser)
2728
):
2829
"""
2930
Test emails.

{{cookiecutter.project_slug}}/backend/app/app/api/utils/security.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
from app.api.utils.db import get_db
1010
from app.core import config
1111
from app.core.jwt import ALGORITHM
12-
from app.db_models.user import User
13-
from app.models.token import TokenPayload
12+
from app.models.user import User
13+
from app.schemas.token import TokenPayload
1414

1515
reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v1/login/access-token")
1616

@@ -25,7 +25,7 @@ def get_current_user(
2525
raise HTTPException(
2626
status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
2727
)
28-
user = crud.user.get(db, user_id=token_data.user_id)
28+
user = crud.user.get(db, id=token_data.user_id)
2929
if not user:
3030
raise HTTPException(status_code=404, detail="User not found")
3131
return user
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
1-
from . import item, user
1+
from .crud_user import user
2+
from .crud_item import item
3+
4+
# For a new basic set of CRUD operations you could just do
5+
6+
# from .base import CRUDBase
7+
# from app.models.item import Item
8+
# from app.schemas.item import ItemCreate, ItemUpdate
9+
10+
# item = CRUDBase[Item, ItemCreate, ItemUpdate](Item)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from typing import List, Optional, Generic, TypeVar, Type
2+
3+
from fastapi.encoders import jsonable_encoder
4+
from pydantic import BaseModel
5+
from sqlalchemy.orm import Session
6+
7+
from app.db.base_class import Base
8+
9+
ModelType = TypeVar("ModelType", bound=Base)
10+
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
11+
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
12+
13+
14+
class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
15+
def __init__(self, model: Type[ModelType]):
16+
"""
17+
CRUD object with default methods to Create, Read, Update, Delete (CRUD).
18+
19+
**Parameters**
20+
21+
* `model`: A SQLAlchemy model class
22+
* `schema`: A Pydantic model (schema) class
23+
"""
24+
self.model = model
25+
26+
def get(self, db_session: Session, id: int) -> Optional[ModelType]:
27+
return db_session.query(self.model).filter(self.model.id == id).first()
28+
29+
def get_multi(self, db_session: Session, *, skip=0, limit=100) -> List[ModelType]:
30+
return db_session.query(self.model).offset(skip).limit(limit).all()
31+
32+
def create(self, db_session: Session, *, obj_in: CreateSchemaType) -> ModelType:
33+
obj_in_data = jsonable_encoder(obj_in)
34+
db_obj = self.model(**obj_in_data)
35+
db_session.add(db_obj)
36+
db_session.commit()
37+
db_session.refresh(db_obj)
38+
return db_obj
39+
40+
def update(
41+
self, db_session: Session, *, db_obj: ModelType, obj_in: UpdateSchemaType
42+
) -> ModelType:
43+
obj_data = jsonable_encoder(db_obj)
44+
update_data = obj_in.dict(skip_defaults=True)
45+
for field in obj_data:
46+
if field in update_data:
47+
setattr(db_obj, field, update_data[field])
48+
db_session.add(db_obj)
49+
db_session.commit()
50+
db_session.refresh(db_obj)
51+
return db_obj
52+
53+
def remove(self, db_session: Session, *, id: int) -> ModelType:
54+
obj = db_session.query(self.model).get(id)
55+
db_session.delete(obj)
56+
db_session.commit()
57+
return obj
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from typing import List
2+
3+
from fastapi.encoders import jsonable_encoder
4+
from sqlalchemy.orm import Session
5+
6+
from app.models.item import Item
7+
from app.schemas.item import ItemCreate, ItemUpdate
8+
from app.crud.base import CRUDBase
9+
10+
11+
class CRUDItem(CRUDBase[Item, ItemCreate, ItemUpdate]):
12+
def create_with_owner(
13+
self, db_session: Session, *, obj_in: ItemCreate, owner_id: int
14+
) -> Item:
15+
obj_in_data = jsonable_encoder(obj_in)
16+
db_obj = self.model(**obj_in_data, owner_id=owner_id)
17+
db_session.add(db_obj)
18+
db_session.commit()
19+
db_session.refresh(db_obj)
20+
return db_obj
21+
22+
def get_multi_by_owner(
23+
self, db_session: Session, *, owner_id: int, skip=0, limit=100
24+
) -> List[Item]:
25+
return (
26+
db_session.query(self.model)
27+
.filter(Item.owner_id == owner_id)
28+
.offset(skip)
29+
.limit(limit)
30+
.all()
31+
)
32+
33+
34+
item = CRUDItem(Item)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from typing import Optional
2+
3+
from sqlalchemy.orm import Session
4+
5+
from app.models.user import User
6+
from app.schemas.user import UserCreate, UserUpdate
7+
from app.core.security import verify_password, get_password_hash
8+
from app.crud.base import CRUDBase
9+
10+
11+
class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
12+
def get_by_email(self, db_session: Session, *, email: str) -> Optional[User]:
13+
return db_session.query(User).filter(User.email == email).first()
14+
15+
def create(self, db_session: Session, *, obj_in: UserCreate) -> User:
16+
db_obj = User(
17+
email=obj_in.email,
18+
hashed_password=get_password_hash(obj_in.password),
19+
full_name=obj_in.full_name,
20+
is_superuser=obj_in.is_superuser,
21+
)
22+
db_session.add(db_obj)
23+
db_session.commit()
24+
db_session.refresh(db_obj)
25+
return db_obj
26+
27+
def authenticate(
28+
self, db_session: Session, *, email: str, password: str
29+
) -> Optional[User]:
30+
user = self.get_by_email(db_session, email=email)
31+
if not user:
32+
return None
33+
if not verify_password(password, user.hashed_password):
34+
return None
35+
return user
36+
37+
def is_active(self, user: User) -> bool:
38+
return user.is_active
39+
40+
def is_superuser(self, user: User) -> bool:
41+
return user.is_superuser
42+
43+
44+
user = CRUDUser(User)

0 commit comments

Comments
 (0)