Skip to content

Commit ecd634e

Browse files
authored
✨ Add Items (crud, models, endpoints), utils, refactor (#14)
* 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.
1 parent 1fe4908 commit ecd634e

32 files changed

+426
-1091
lines changed

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,17 @@ After using this generator, your new project (the directory created) will contai
148148

149149
### Next release
150150

151+
* PR <a href="https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/14" target="_blank">#14</a>:
152+
* Update CRUD utils to use types better.
153+
* Simplify Pydantic model names, from `UserInCreate` to `UserCreate`, etc.
154+
* Upgrade packages.
155+
* 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.
156+
* Update endpoints/*path operations* to simplify code and use new utilities, prefix and tags in `include_router`.
157+
* Update testing utils.
158+
* Update linting rules, relax vulture to reduce false positives.
159+
* Update migrations to include new Items.
160+
* Update project README.md with tips about how to start with backend.
161+
151162
* Upgrade Python to 3.7 as Celery is now compatible too. <a href="https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/10" target="_blank">PR #10</a> by <a href="https://github.com/ebreton" target="_blank">@ebreton</a>.
152163

153164
### 0.2.2

{{cookiecutter.project_slug}}/README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ If your Docker is not running in `localhost` (the URLs above wouldn't work) chec
5353

5454
### General workflow
5555

56-
Add and modify SQLAlchemy models in `./backend/app/app/db_models/`, Pydantic models in `./backend/app/app/models` and API endpoints in `./backend/app/app/api/`.
56+
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.
57+
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.
5759

5860
Add and modify tasks to the Celery worker in `./backend/app/app/worker.py`.
5961

{{cookiecutter.project_slug}}/backend/app/Pipfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pyjwt = "*"
2020
python-multipart = "*"
2121
email-validator = "*"
2222
requests = "*"
23-
celery = "~=4.3"
23+
celery = "*"
2424
passlib = {extras = ["bcrypt"],version = "*"}
2525
tenacity = "*"
2626
pydantic = "*"

{{cookiecutter.project_slug}}/backend/app/Pipfile.lock

-1,022
This file was deleted.

{{cookiecutter.project_slug}}/backend/app/alembic/versions/e6ae69e9dcb9_first_revision.py renamed to {{cookiecutter.project_slug}}/backend/app/alembic/versions/d4867f3a4c0a_first_revision.py

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
"""First revision
22
3-
Revision ID: e6ae69e9dcb9
3+
Revision ID: d4867f3a4c0a
44
Revises:
5-
Create Date: 2019-02-13 14:27:57.038583
5+
Create Date: 2019-04-17 13:53:32.978401
66
77
"""
88
from alembic import op
99
import sqlalchemy as sa
1010

1111

1212
# revision identifiers, used by Alembic.
13-
revision = 'e6ae69e9dcb9'
13+
revision = 'd4867f3a4c0a'
1414
down_revision = None
1515
branch_labels = None
1616
depends_on = None
@@ -30,11 +30,26 @@ def upgrade():
3030
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
3131
op.create_index(op.f('ix_user_full_name'), 'user', ['full_name'], unique=False)
3232
op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False)
33+
op.create_table('item',
34+
sa.Column('id', sa.Integer(), nullable=False),
35+
sa.Column('title', sa.String(), nullable=True),
36+
sa.Column('description', sa.String(), nullable=True),
37+
sa.Column('owner_id', sa.Integer(), nullable=True),
38+
sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ),
39+
sa.PrimaryKeyConstraint('id')
40+
)
41+
op.create_index(op.f('ix_item_description'), 'item', ['description'], unique=False)
42+
op.create_index(op.f('ix_item_id'), 'item', ['id'], unique=False)
43+
op.create_index(op.f('ix_item_title'), 'item', ['title'], unique=False)
3344
# ### end Alembic commands ###
3445

3546

3647
def downgrade():
3748
# ### commands auto generated by Alembic - please adjust! ###
49+
op.drop_index(op.f('ix_item_title'), table_name='item')
50+
op.drop_index(op.f('ix_item_id'), table_name='item')
51+
op.drop_index(op.f('ix_item_description'), table_name='item')
52+
op.drop_table('item')
3853
op.drop_index(op.f('ix_user_id'), table_name='user')
3954
op.drop_index(op.f('ix_user_full_name'), table_name='user')
4055
op.drop_index(op.f('ix_user_email'), table_name='user')
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from fastapi import APIRouter
22

3-
from app.api.api_v1.endpoints import token, user, utils
3+
from app.api.api_v1.endpoints import items, login, users, utils
44

55
api_router = APIRouter()
6-
api_router.include_router(token.router)
7-
api_router.include_router(user.router)
8-
api_router.include_router(utils.router)
6+
api_router.include_router(login.router, tags=["login"])
7+
api_router.include_router(users.router, prefix="/users", tags=["users"])
8+
api_router.include_router(utils.router, prefix="/utils", tags=["utils"])
9+
api_router.include_router(items.router, prefix="/items", tags=["items"])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from typing import List
2+
3+
from fastapi import APIRouter, Depends, HTTPException
4+
from sqlalchemy.orm import Session
5+
6+
from app import crud
7+
from app.api.utils.db import get_db
8+
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
11+
12+
router = APIRouter()
13+
14+
15+
@router.get("/", response_model=List[Item])
16+
def read_items(
17+
db: Session = Depends(get_db),
18+
skip: int = 0,
19+
limit: int = 100,
20+
current_user: DBUser = Depends(get_current_active_user),
21+
):
22+
"""
23+
Retrieve items.
24+
"""
25+
if crud.user.is_superuser(current_user):
26+
items = crud.item.get_multi(db, skip=skip, limit=limit)
27+
else:
28+
items = crud.item.get_multi_by_owner(
29+
db_session=db, owner_id=current_user.id, skip=skip, limit=limit
30+
)
31+
return items
32+
33+
34+
@router.post("/", response_model=Item)
35+
def create_item(
36+
*,
37+
db: Session = Depends(get_db),
38+
item_in: ItemCreate,
39+
current_user: DBUser = Depends(get_current_active_user),
40+
):
41+
"""
42+
Create new item.
43+
"""
44+
item = crud.item.create(db_session=db, item_in=item_in, owner_id=current_user.id)
45+
return item
46+
47+
48+
@router.put("/{id}", response_model=Item)
49+
def update_item(
50+
*,
51+
db: Session = Depends(get_db),
52+
id: int,
53+
item_in: ItemUpdate,
54+
current_user: DBUser = Depends(get_current_active_user),
55+
):
56+
"""
57+
Update an item.
58+
"""
59+
item = crud.item.get(db_session=db, id=id)
60+
if not item:
61+
raise HTTPException(status_code=404, detail="Item not found")
62+
if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id):
63+
raise HTTPException(status_code=400, detail="Not enough permissions")
64+
item = crud.item.update(db_session=db, item=item, item_in=item_in)
65+
return item
66+
67+
68+
@router.get("/{id}", response_model=Item)
69+
def read_user_me(
70+
*,
71+
db: Session = Depends(get_db),
72+
id: int,
73+
current_user: DBUser = Depends(get_current_active_user),
74+
):
75+
"""
76+
Get item by ID.
77+
"""
78+
item = crud.item.get(db_session=db, id=id)
79+
if not item:
80+
raise HTTPException(status_code=400, detail="Item not found")
81+
if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id):
82+
raise HTTPException(status_code=400, detail="Not enough permissions")
83+
return item
84+
85+
86+
@router.delete("/{id}", response_model=Item)
87+
def delete_item(
88+
*,
89+
db: Session = Depends(get_db),
90+
id: int,
91+
current_user: DBUser = Depends(get_current_active_user),
92+
):
93+
"""
94+
Delete an item.
95+
"""
96+
item = crud.item.get(db_session=db, id=id)
97+
if not item:
98+
raise HTTPException(status_code=404, detail="Item not found")
99+
if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id):
100+
raise HTTPException(status_code=400, detail="Not enough permissions")
101+
item = crud.item.remove(db_session=db, id=id)
102+
return item

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

+19-20
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,35 @@
1010
from app.api.utils.security import get_current_active_superuser, get_current_active_user
1111
from app.core import config
1212
from app.db_models.user import User as DBUser
13-
from app.models.user import User, UserInCreate, UserInDB, UserInUpdate
13+
from app.models.user import User, UserCreate, UserInDB, UserUpdate
1414
from app.utils import send_new_account_email
1515

1616
router = APIRouter()
1717

1818

19-
@router.get("/users/", tags=["users"], response_model=List[User])
19+
@router.get("/", response_model=List[User])
2020
def read_users(
2121
db: Session = Depends(get_db),
2222
skip: int = 0,
2323
limit: int = 100,
2424
current_user: DBUser = Depends(get_current_active_superuser),
2525
):
2626
"""
27-
Retrieve users
27+
Retrieve users.
2828
"""
2929
users = crud.user.get_multi(db, skip=skip, limit=limit)
3030
return users
3131

3232

33-
@router.post("/users/", tags=["users"], response_model=User)
33+
@router.post("/", response_model=User)
3434
def create_user(
3535
*,
3636
db: Session = Depends(get_db),
37-
user_in: UserInCreate,
37+
user_in: UserCreate,
3838
current_user: DBUser = Depends(get_current_active_superuser),
3939
):
4040
"""
41-
Create new user
41+
Create new user.
4242
"""
4343
user = crud.user.get_by_email(db, email=user_in.email)
4444
if user:
@@ -54,7 +54,7 @@ def create_user(
5454
return user
5555

5656

57-
@router.put("/users/me", tags=["users"], response_model=User)
57+
@router.put("/me", response_model=User)
5858
def update_user_me(
5959
*,
6060
db: Session = Depends(get_db),
@@ -64,10 +64,10 @@ def update_user_me(
6464
current_user: DBUser = Depends(get_current_active_user),
6565
):
6666
"""
67-
Update own user
67+
Update own user.
6868
"""
6969
current_user_data = jsonable_encoder(current_user)
70-
user_in = UserInUpdate(**current_user_data)
70+
user_in = UserUpdate(**current_user_data)
7171
if password is not None:
7272
user_in.password = password
7373
if full_name is not None:
@@ -78,18 +78,18 @@ def update_user_me(
7878
return user
7979

8080

81-
@router.get("/users/me", tags=["users"], response_model=User)
81+
@router.get("/me", response_model=User)
8282
def read_user_me(
8383
db: Session = Depends(get_db),
8484
current_user: DBUser = Depends(get_current_active_user),
8585
):
8686
"""
87-
Get current user
87+
Get current user.
8888
"""
8989
return current_user
9090

9191

92-
@router.post("/users/open", tags=["users"], response_model=User)
92+
@router.post("/open", response_model=User)
9393
def create_user_open(
9494
*,
9595
db: Session = Depends(get_db),
@@ -98,7 +98,7 @@ def create_user_open(
9898
full_name: str = Body(None),
9999
):
100100
"""
101-
Create new user without the need to be logged in
101+
Create new user without the need to be logged in.
102102
"""
103103
if not config.USERS_OPEN_REGISTRATION:
104104
raise HTTPException(
@@ -111,19 +111,19 @@ def create_user_open(
111111
status_code=400,
112112
detail="The user with this username already exists in the system",
113113
)
114-
user_in = UserInCreate(password=password, email=email, full_name=full_name)
114+
user_in = UserCreate(password=password, email=email, full_name=full_name)
115115
user = crud.user.create(db, user_in=user_in)
116116
return user
117117

118118

119-
@router.get("/users/{user_id}", tags=["users"], response_model=User)
119+
@router.get("/{user_id}", response_model=User)
120120
def read_user_by_id(
121121
user_id: int,
122122
current_user: DBUser = Depends(get_current_active_user),
123123
db: Session = Depends(get_db),
124124
):
125125
"""
126-
Get a specific user by id
126+
Get a specific user by id.
127127
"""
128128
user = crud.user.get(db, user_id=user_id)
129129
if user == current_user:
@@ -135,19 +135,18 @@ def read_user_by_id(
135135
return user
136136

137137

138-
@router.put("/users/{user_id}", tags=["users"], response_model=User)
138+
@router.put("/{user_id}", response_model=User)
139139
def update_user(
140140
*,
141141
db: Session = Depends(get_db),
142142
user_id: int,
143-
user_in: UserInUpdate,
143+
user_in: UserUpdate,
144144
current_user: UserInDB = Depends(get_current_active_superuser),
145145
):
146146
"""
147-
Update a user
147+
Update a user.
148148
"""
149149
user = crud.user.get(db, user_id=user_id)
150-
151150
if not user:
152151
raise HTTPException(
153152
status_code=404,

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,23 @@
1010
router = APIRouter()
1111

1212

13-
@router.post("/test-celery/", tags=["utils"], response_model=Msg, status_code=201)
13+
@router.post("/test-celery/", response_model=Msg, status_code=201)
1414
def test_celery(
1515
msg: Msg, current_user: UserInDB = Depends(get_current_active_superuser)
1616
):
1717
"""
18-
Test Celery worker
18+
Test Celery worker.
1919
"""
2020
celery_app.send_task("app.worker.test_celery", args=[msg.msg])
2121
return {"msg": "Word received"}
2222

2323

24-
@router.post("/test-email/", tags=["utils"], response_model=Msg, status_code=201)
24+
@router.post("/test-email/", response_model=Msg, status_code=201)
2525
def test_email(
2626
email_to: EmailStr, current_user: UserInDB = Depends(get_current_active_superuser)
2727
):
2828
"""
29-
Test emails
29+
Test emails.
3030
"""
3131
send_test_email(email_to=email_to)
3232
return {"msg": "Test email sent"}
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from . import user
1+
from . import item, user

0 commit comments

Comments
 (0)