Skip to content
This repository was archived by the owner on Nov 14, 2022. It is now read-only.

Commit 87ccdc7

Browse files
authored
✨ Add Items (crud, models, endpoints), utils, refactor (#15)
* 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. * Add full text search for items. * Update project README.md with tips about how to start with backend.
1 parent aa801a0 commit 87ccdc7

File tree

32 files changed

+801
-126
lines changed

32 files changed

+801
-126
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,17 @@ After using this generator, your new project (the directory created) will contai
136136

137137
## Release Notes
138138

139+
* PR <a href="https://github.com/tiangolo/full-stack-fastapi-couchbase/pull/15" target="_blank">#15</a>:
140+
* Update CRUD utils to use types better.
141+
* Simplify Pydantic model names, from `UserInCreate` to `UserCreate`, etc.
142+
* Upgrade packages.
143+
* 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.
144+
* Update endpoints/*path operations* to simplify code and use new utilities, prefix and tags in `include_router`.
145+
* Update testing utils.
146+
* Update linting rules, relax vulture to reduce false positives.
147+
* Add full text search for items.
148+
* Update project README.md with tips about how to start with backend.
149+
139150
### 0.2.1
140151

141152
* Fix frontend hijacking /docs in development. Using latest https://github.com/tiangolo/node-frontend with custom Nginx configs in frontend. <a href="https://github.com/tiangolo/full-stack-fastapi-couchbase/pull/14" target="_blank">PR #14</a>.

{{cookiecutter.project_slug}}/README.md

Lines changed: 3 additions & 1 deletion
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-
Modify or add 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 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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pyjwt = "*"
2020
python-multipart = "*"
2121
email-validator = "*"
2222
requests = "*"
23-
celery = "==4.2.1"
23+
celery = "~=4.3"
2424
passlib = {extras = ["bcrypt"],version = "*"}
2525
tenacity = "*"
2626
pydantic = "*"
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from fastapi import APIRouter
22

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

55
api_router = APIRouter()
6-
api_router.include_router(role.router)
7-
api_router.include_router(token.router)
8-
api_router.include_router(user.router)
9-
api_router.include_router(utils.router)
6+
api_router.include_router(login.router, tags=["login"])
7+
api_router.include_router(roles.router, prefix="/roles", tags=["roles"])
8+
api_router.include_router(users.router, prefix="/users", tags=["users"])
9+
api_router.include_router(utils.router, prefix="/utils", tags=["utils"])
10+
api_router.include_router(items.router, prefix="/items", tags=["items"])
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
from typing import List
2+
3+
from fastapi import APIRouter, Depends, HTTPException
4+
5+
from app import crud
6+
from app.api.utils.security import get_current_active_user
7+
from app.db.database import get_default_bucket
8+
from app.models.item import Item, ItemCreate, ItemUpdate
9+
from app.models.user import UserInDB
10+
11+
router = APIRouter()
12+
13+
14+
@router.get("/", response_model=List[Item])
15+
def read_items(
16+
skip: int = 0,
17+
limit: int = 100,
18+
current_user: UserInDB = Depends(get_current_active_user),
19+
):
20+
"""
21+
Retrieve items.
22+
23+
If superuser, all the items.
24+
25+
If normal user, the items owned by this user.
26+
"""
27+
bucket = get_default_bucket()
28+
if crud.user.is_superuser(current_user):
29+
docs = crud.item.get_multi(bucket, skip=skip, limit=limit)
30+
else:
31+
docs = crud.item.get_multi_by_owner(
32+
bucket=bucket, owner_username=current_user.username, skip=skip, limit=limit
33+
)
34+
return docs
35+
36+
37+
@router.get("/search/", response_model=List[Item])
38+
def search_items(
39+
q: str,
40+
skip: int = 0,
41+
limit: int = 100,
42+
current_user: UserInDB = Depends(get_current_active_user),
43+
):
44+
"""
45+
Search items, use Bleve Query String syntax:
46+
http://blevesearch.com/docs/Query-String-Query/
47+
48+
For typeahead suffix with `*`. For example, a query with: `title:foo*` will match
49+
items containing `football`, `fool proof`, etc.
50+
"""
51+
bucket = get_default_bucket()
52+
if crud.user.is_superuser(current_user):
53+
docs = crud.item.search(bucket=bucket, query_string=q, skip=skip, limit=limit)
54+
else:
55+
docs = crud.item.search_with_owner(
56+
bucket=bucket,
57+
query_string=q,
58+
username=current_user.username,
59+
skip=skip,
60+
limit=limit,
61+
)
62+
return docs
63+
64+
65+
@router.post("/", response_model=Item)
66+
def create_item(
67+
*, item_in: ItemCreate, current_user: UserInDB = Depends(get_current_active_user)
68+
):
69+
"""
70+
Create new item.
71+
"""
72+
bucket = get_default_bucket()
73+
id = crud.utils.generate_new_id()
74+
doc = crud.item.upsert(
75+
bucket=bucket, id=id, doc_in=item_in, owner_username=current_user.username
76+
)
77+
return doc
78+
79+
80+
@router.put("/{id}", response_model=Item)
81+
def update_item(
82+
*,
83+
id: str,
84+
item_in: ItemUpdate,
85+
current_user: UserInDB = Depends(get_current_active_user),
86+
):
87+
"""
88+
Update an item.
89+
"""
90+
bucket = get_default_bucket()
91+
doc = crud.item.get(bucket=bucket, id=id)
92+
if not doc:
93+
raise HTTPException(status_code=404, detail="Item not found")
94+
if not crud.user.is_superuser(current_user) and (
95+
doc.owner_username != current_user.username
96+
):
97+
raise HTTPException(status_code=400, detail="Not enough permissions")
98+
doc = crud.item.update(
99+
bucket=bucket, id=id, doc_in=item_in, owner_username=doc.owner_username
100+
)
101+
return doc
102+
103+
104+
@router.get("/{id}", response_model=Item)
105+
def read_item(id: str, current_user: UserInDB = Depends(get_current_active_user)):
106+
"""
107+
Get item by ID.
108+
"""
109+
bucket = get_default_bucket()
110+
doc = crud.item.get(bucket=bucket, id=id)
111+
if not doc:
112+
raise HTTPException(status_code=404, detail="Item not found")
113+
if not crud.user.is_superuser(current_user) and (
114+
doc.owner_username != current_user.username
115+
):
116+
raise HTTPException(status_code=400, detail="Not enough permissions")
117+
return doc
118+
119+
120+
@router.delete("/{id}", response_model=Item)
121+
def delete_item(id: str, current_user: UserInDB = Depends(get_current_active_user)):
122+
"""
123+
Delete an item by ID.
124+
"""
125+
bucket = get_default_bucket()
126+
doc = crud.item.get(bucket=bucket, id=id)
127+
if not doc:
128+
raise HTTPException(status_code=404, detail="Item not found")
129+
if not crud.user.is_superuser(current_user) and (
130+
doc.owner_username != current_user.username
131+
):
132+
raise HTTPException(status_code=400, detail="Not enough permissions")
133+
doc = crud.item.remove(bucket=bucket, id=id)
134+
return doc

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from app.db.database import get_default_bucket
1111
from app.models.msg import Msg
1212
from app.models.token import Token
13-
from app.models.user import User, UserInDB, UserInUpdate
13+
from app.models.user import User, UserInDB, UserUpdate
1414
from app.utils import (
1515
generate_password_reset_token,
1616
send_reset_password_email,
@@ -20,10 +20,10 @@
2020
router = APIRouter()
2121

2222

23-
@router.post("/login/access-token", response_model=Token, tags=["login"])
23+
@router.post("/login/access-token", response_model=Token)
2424
def login(form_data: OAuth2PasswordRequestForm = Depends()):
2525
"""
26-
OAuth2 compatible token login, get an access token for future requests
26+
OAuth2 compatible token login, get an access token for future requests.
2727
"""
2828
bucket = get_default_bucket()
2929
user = crud.user.authenticate(
@@ -42,18 +42,18 @@ def login(form_data: OAuth2PasswordRequestForm = Depends()):
4242
}
4343

4444

45-
@router.post("/login/test-token", tags=["login"], response_model=User)
45+
@router.post("/login/test-token", response_model=User)
4646
def test_token(current_user: UserInDB = Depends(get_current_user)):
4747
"""
48-
Test access token
48+
Test access token.
4949
"""
5050
return current_user
5151

5252

53-
@router.post("/password-recovery/{username}", tags=["login"], response_model=Msg)
53+
@router.post("/password-recovery/{username}", response_model=Msg)
5454
def recover_password(username: str):
5555
"""
56-
Password Recovery
56+
Password Recovery.
5757
"""
5858
bucket = get_default_bucket()
5959
user = crud.user.get(bucket, username=username)
@@ -70,10 +70,10 @@ def recover_password(username: str):
7070
return {"msg": "Password recovery email sent"}
7171

7272

73-
@router.post("/reset-password/", tags=["login"], response_model=Msg)
73+
@router.post("/reset-password/", response_model=Msg)
7474
def reset_password(token: str, new_password: str):
7575
"""
76-
Reset password
76+
Reset password.
7777
"""
7878
username = verify_password_reset_token(token)
7979
if not username:
@@ -87,6 +87,6 @@ def reset_password(token: str, new_password: str):
8787
)
8888
elif not crud.user.is_active(user):
8989
raise HTTPException(status_code=400, detail="Inactive user")
90-
user_in = UserInUpdate(name=username, password=new_password)
90+
user_in = UserUpdate(name=username, password=new_password)
9191
user = crud.user.update(bucket, username=username, user_in=user_in)
9292
return {"msg": "Password updated successfully"}

{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/role.py renamed to {{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/roles.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
router = APIRouter()
99

1010

11-
@router.get("/roles/", response_model=Roles)
11+
@router.get("/", response_model=Roles)
1212
def read_roles(current_user: UserInDB = Depends(get_current_active_superuser)):
1313
"""
14-
Retrieve roles
14+
Retrieve roles.
1515
"""
1616
roles = crud.utils.ensure_enums_to_strs(RoleEnum)
1717
return {"roles": roles}

0 commit comments

Comments
 (0)