diff --git a/README.md b/README.md
index 36c2d72..fff5fa6 100644
--- a/README.md
+++ b/README.md
@@ -136,6 +136,17 @@ After using this generator, your new project (the directory created) will contai
## Release Notes
+* PR #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.
+
### 0.2.1
* Fix frontend hijacking /docs in development. Using latest https://github.com/tiangolo/node-frontend with custom Nginx configs in frontend. PR #14.
diff --git a/{{cookiecutter.project_slug}}/README.md b/{{cookiecutter.project_slug}}/README.md
index 3123508..86f5855 100644
--- a/{{cookiecutter.project_slug}}/README.md
+++ b/{{cookiecutter.project_slug}}/README.md
@@ -53,7 +53,9 @@ If your Docker is not running in `localhost` (the URLs above wouldn't work) chec
### General workflow
-Modify or add Pydantic models in `./backend/app/app/models` and API endpoints in `./backend/app/app/api/`.
+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.
+
+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.
Add and modify tasks to the Celery worker in `./backend/app/app/worker.py`.
diff --git a/{{cookiecutter.project_slug}}/backend/app/Pipfile b/{{cookiecutter.project_slug}}/backend/app/Pipfile
index 4a6a12e..224ff57 100644
--- a/{{cookiecutter.project_slug}}/backend/app/Pipfile
+++ b/{{cookiecutter.project_slug}}/backend/app/Pipfile
@@ -20,7 +20,7 @@ pyjwt = "*"
python-multipart = "*"
email-validator = "*"
requests = "*"
-celery = "==4.2.1"
+celery = "~=4.3"
passlib = {extras = ["bcrypt"],version = "*"}
tenacity = "*"
pydantic = "*"
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/api.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/api.py
index 6803319..f4ac95f 100644
--- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/api.py
+++ b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/api.py
@@ -1,9 +1,10 @@
from fastapi import APIRouter
-from app.api.api_v1.endpoints import role, token, user, utils
+from app.api.api_v1.endpoints import items, login, roles, users, utils
api_router = APIRouter()
-api_router.include_router(role.router)
-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(roles.router, prefix="/roles", tags=["roles"])
+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"])
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/items.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/items.py
new file mode 100644
index 0000000..194ee2b
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/items.py
@@ -0,0 +1,134 @@
+from typing import List
+
+from fastapi import APIRouter, Depends, HTTPException
+
+from app import crud
+from app.api.utils.security import get_current_active_user
+from app.db.database import get_default_bucket
+from app.models.item import Item, ItemCreate, ItemUpdate
+from app.models.user import UserInDB
+
+router = APIRouter()
+
+
+@router.get("/", response_model=List[Item])
+def read_items(
+ skip: int = 0,
+ limit: int = 100,
+ current_user: UserInDB = Depends(get_current_active_user),
+):
+ """
+ Retrieve items.
+
+ If superuser, all the items.
+
+ If normal user, the items owned by this user.
+ """
+ bucket = get_default_bucket()
+ if crud.user.is_superuser(current_user):
+ docs = crud.item.get_multi(bucket, skip=skip, limit=limit)
+ else:
+ docs = crud.item.get_multi_by_owner(
+ bucket=bucket, owner_username=current_user.username, skip=skip, limit=limit
+ )
+ return docs
+
+
+@router.get("/search/", response_model=List[Item])
+def search_items(
+ q: str,
+ skip: int = 0,
+ limit: int = 100,
+ current_user: UserInDB = Depends(get_current_active_user),
+):
+ """
+ Search items, use Bleve Query String syntax:
+ http://blevesearch.com/docs/Query-String-Query/
+
+ For typeahead suffix with `*`. For example, a query with: `title:foo*` will match
+ items containing `football`, `fool proof`, etc.
+ """
+ bucket = get_default_bucket()
+ if crud.user.is_superuser(current_user):
+ docs = crud.item.search(bucket=bucket, query_string=q, skip=skip, limit=limit)
+ else:
+ docs = crud.item.search_with_owner(
+ bucket=bucket,
+ query_string=q,
+ username=current_user.username,
+ skip=skip,
+ limit=limit,
+ )
+ return docs
+
+
+@router.post("/", response_model=Item)
+def create_item(
+ *, item_in: ItemCreate, current_user: UserInDB = Depends(get_current_active_user)
+):
+ """
+ Create new item.
+ """
+ bucket = get_default_bucket()
+ id = crud.utils.generate_new_id()
+ doc = crud.item.upsert(
+ bucket=bucket, id=id, doc_in=item_in, owner_username=current_user.username
+ )
+ return doc
+
+
+@router.put("/{id}", response_model=Item)
+def update_item(
+ *,
+ id: str,
+ item_in: ItemUpdate,
+ current_user: UserInDB = Depends(get_current_active_user),
+):
+ """
+ Update an item.
+ """
+ bucket = get_default_bucket()
+ doc = crud.item.get(bucket=bucket, id=id)
+ if not doc:
+ raise HTTPException(status_code=404, detail="Item not found")
+ if not crud.user.is_superuser(current_user) and (
+ doc.owner_username != current_user.username
+ ):
+ raise HTTPException(status_code=400, detail="Not enough permissions")
+ doc = crud.item.update(
+ bucket=bucket, id=id, doc_in=item_in, owner_username=doc.owner_username
+ )
+ return doc
+
+
+@router.get("/{id}", response_model=Item)
+def read_item(id: str, current_user: UserInDB = Depends(get_current_active_user)):
+ """
+ Get item by ID.
+ """
+ bucket = get_default_bucket()
+ doc = crud.item.get(bucket=bucket, id=id)
+ if not doc:
+ raise HTTPException(status_code=404, detail="Item not found")
+ if not crud.user.is_superuser(current_user) and (
+ doc.owner_username != current_user.username
+ ):
+ raise HTTPException(status_code=400, detail="Not enough permissions")
+ return doc
+
+
+@router.delete("/{id}", response_model=Item)
+def delete_item(id: str, current_user: UserInDB = Depends(get_current_active_user)):
+ """
+ Delete an item by ID.
+ """
+ bucket = get_default_bucket()
+ doc = crud.item.get(bucket=bucket, id=id)
+ if not doc:
+ raise HTTPException(status_code=404, detail="Item not found")
+ if not crud.user.is_superuser(current_user) and (
+ doc.owner_username != current_user.username
+ ):
+ raise HTTPException(status_code=400, detail="Not enough permissions")
+ doc = crud.item.remove(bucket=bucket, id=id)
+ return doc
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/token.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py
similarity index 84%
rename from {{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/token.py
rename to {{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py
index 6ddbf00..7d6e060 100644
--- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/token.py
+++ b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py
@@ -10,7 +10,7 @@
from app.db.database import get_default_bucket
from app.models.msg import Msg
from app.models.token import Token
-from app.models.user import User, UserInDB, UserInUpdate
+from app.models.user import User, UserInDB, UserUpdate
from app.utils import (
generate_password_reset_token,
send_reset_password_email,
@@ -20,10 +20,10 @@
router = APIRouter()
-@router.post("/login/access-token", response_model=Token, tags=["login"])
+@router.post("/login/access-token", response_model=Token)
def login(form_data: OAuth2PasswordRequestForm = Depends()):
"""
- OAuth2 compatible token login, get an access token for future requests
+ OAuth2 compatible token login, get an access token for future requests.
"""
bucket = get_default_bucket()
user = crud.user.authenticate(
@@ -42,18 +42,18 @@ def login(form_data: OAuth2PasswordRequestForm = Depends()):
}
-@router.post("/login/test-token", tags=["login"], response_model=User)
+@router.post("/login/test-token", response_model=User)
def test_token(current_user: UserInDB = Depends(get_current_user)):
"""
- Test access token
+ Test access token.
"""
return current_user
-@router.post("/password-recovery/{username}", tags=["login"], response_model=Msg)
+@router.post("/password-recovery/{username}", response_model=Msg)
def recover_password(username: str):
"""
- Password Recovery
+ Password Recovery.
"""
bucket = get_default_bucket()
user = crud.user.get(bucket, username=username)
@@ -70,10 +70,10 @@ def recover_password(username: str):
return {"msg": "Password recovery email sent"}
-@router.post("/reset-password/", tags=["login"], response_model=Msg)
+@router.post("/reset-password/", response_model=Msg)
def reset_password(token: str, new_password: str):
"""
- Reset password
+ Reset password.
"""
username = verify_password_reset_token(token)
if not username:
@@ -87,6 +87,6 @@ def reset_password(token: str, new_password: str):
)
elif not crud.user.is_active(user):
raise HTTPException(status_code=400, detail="Inactive user")
- user_in = UserInUpdate(name=username, password=new_password)
+ user_in = UserUpdate(name=username, password=new_password)
user = crud.user.update(bucket, username=username, user_in=user_in)
return {"msg": "Password updated successfully"}
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/role.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/roles.py
similarity index 86%
rename from {{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/role.py
rename to {{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/roles.py
index e2dcfea..a5c0c62 100644
--- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/role.py
+++ b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/roles.py
@@ -8,10 +8,10 @@
router = APIRouter()
-@router.get("/roles/", response_model=Roles)
+@router.get("/", response_model=Roles)
def read_roles(current_user: UserInDB = Depends(get_current_active_superuser)):
"""
- Retrieve roles
+ Retrieve roles.
"""
roles = crud.utils.ensure_enums_to_strs(RoleEnum)
return {"roles": roles}
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/user.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py
similarity index 82%
rename from {{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/user.py
rename to {{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py
index 815bee2..3526bdd 100644
--- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/user.py
+++ b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py
@@ -7,27 +7,27 @@
from app.api.utils.security import get_current_active_superuser, get_current_active_user
from app.core import config
from app.db.database import get_default_bucket
-from app.models.user import User, UserInCreate, UserInDB, UserInUpdate
+from app.models.user import User, UserCreate, UserInDB, UserUpdate
from app.utils import send_new_account_email
router = APIRouter()
-@router.get("/users/", tags=["users"], response_model=List[User])
+@router.get("/", response_model=List[User])
def read_users(
skip: int = 0,
limit: int = 100,
current_user: UserInDB = Depends(get_current_active_superuser),
):
"""
- Retrieve users
+ Retrieve users.
"""
bucket = get_default_bucket()
users = crud.user.get_multi(bucket, skip=skip, limit=limit)
return users
-@router.get("/users/search/", tags=["users"], response_model=List[User])
+@router.get("/search/", response_model=List[User])
def search_users(
q: str,
skip: int = 0,
@@ -46,14 +46,14 @@ def search_users(
return users
-@router.post("/users/", tags=["users"], response_model=User)
+@router.post("/", response_model=User)
def create_user(
*,
- user_in: UserInCreate,
+ user_in: UserCreate,
current_user: UserInDB = Depends(get_current_active_superuser),
):
"""
- Create new user
+ Create new user.
"""
bucket = get_default_bucket()
user = crud.user.get(bucket, username=user_in.username)
@@ -70,7 +70,7 @@ def create_user(
return user
-@router.put("/users/me", tags=["users"], response_model=User)
+@router.put("/me", response_model=User)
def update_user_me(
*,
password: str = Body(None),
@@ -79,9 +79,9 @@ def update_user_me(
current_user: UserInDB = Depends(get_current_active_user),
):
"""
- Update own user
+ Update own user.
"""
- user_in = UserInUpdate(**current_user.dict())
+ user_in = UserUpdate(**current_user.dict())
if password is not None:
user_in.password = password
if full_name is not None:
@@ -93,15 +93,15 @@ def update_user_me(
return user
-@router.get("/users/me", tags=["users"], response_model=User)
+@router.get("/me", response_model=User)
def read_user_me(current_user: UserInDB = Depends(get_current_active_user)):
"""
- Get current user
+ Get current user.
"""
return current_user
-@router.post("/users/open", tags=["users"], response_model=User)
+@router.post("/open", response_model=User)
def create_user_open(
*,
username: str = Body(...),
@@ -110,7 +110,7 @@ def create_user_open(
full_name: str = Body(None),
):
"""
- Create new user without the need to be logged in
+ Create new user without the need to be logged in.
"""
if not config.USERS_OPEN_REGISTRATION:
raise HTTPException(
@@ -124,7 +124,7 @@ def create_user_open(
status_code=400,
detail="The user with this username already exists in the system",
)
- user_in = UserInCreate(
+ user_in = UserCreate(
username=username, password=password, email=email, full_name=full_name
)
user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
@@ -135,10 +135,10 @@ def create_user_open(
return user
-@router.get("/users/{username}", tags=["users"], response_model=User)
+@router.get("/{username}", response_model=User)
def read_user(username: str, current_user: UserInDB = Depends(get_current_active_user)):
"""
- Get a specific user by username (email)
+ Get a specific user by username (email).
"""
bucket = get_default_bucket()
user = crud.user.get(bucket, username=username)
@@ -151,15 +151,15 @@ def read_user(username: str, current_user: UserInDB = Depends(get_current_active
return user
-@router.put("/users/{username}", tags=["users"], response_model=User)
+@router.put("/{username}", response_model=User)
def update_user(
*,
username: str,
- user_in: UserInUpdate,
+ user_in: UserUpdate,
current_user: UserInDB = Depends(get_current_active_superuser),
):
"""
- Update a user
+ Update a user.
"""
bucket = get_default_bucket()
user = crud.user.get(bucket, username=username)
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/utils.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/utils.py
index 18995ce..cc43abe 100644
--- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/utils.py
+++ b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/utils.py
@@ -10,23 +10,23 @@
router = APIRouter()
-@router.post("/test-celery/", tags=["utils"], response_model=Msg, status_code=201)
+@router.post("/test-celery/", response_model=Msg, status_code=201)
def test_celery(
msg: Msg, current_user: UserInDB = Depends(get_current_active_superuser)
):
"""
- Test Celery worker
+ Test Celery worker.
"""
celery_app.send_task("app.worker.test_celery", args=[msg.msg])
return {"msg": "Word received"}
-@router.post("/test-email/", tags=["utils"], response_model=Msg, status_code=201)
+@router.post("/test-email/", response_model=Msg, status_code=201)
def test_email(
email_to: EmailStr, current_user: UserInDB = Depends(get_current_active_superuser)
):
"""
- Test emails
+ Test emails.
"""
send_test_email(email_to=email_to)
return {"msg": "Test email sent"}
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/__init__.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/__init__.py
index f3b5b99..1bc5b1a 100644
--- a/{{cookiecutter.project_slug}}/backend/app/app/crud/__init__.py
+++ b/{{cookiecutter.project_slug}}/backend/app/app/crud/__init__.py
@@ -1 +1 @@
-from . import user, utils
+from . import item, user, utils
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/item.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/item.py
new file mode 100644
index 0000000..fe142ce
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/backend/app/app/crud/item.py
@@ -0,0 +1,131 @@
+from couchbase.bucket import Bucket
+from couchbase.n1ql import CONSISTENCY_REQUEST, N1QLQuery
+
+from app.core import config
+from app.models.config import ITEM_DOC_TYPE
+from app.models.item import ItemCreate, ItemInDB, ItemUpdate
+
+from . import utils
+
+# Same as file name /app/app/search_index_definitions/items.json
+full_text_index_name = "items"
+
+
+def get_doc_id(id: str):
+ return f"{ITEM_DOC_TYPE}::{id}"
+
+
+def get(bucket: Bucket, *, id: str):
+ doc_id = get_doc_id(id)
+ return utils.get_doc(bucket=bucket, doc_id=doc_id, doc_model=ItemInDB)
+
+
+def upsert(
+ bucket: Bucket,
+ *,
+ id: str,
+ doc_in: ItemCreate,
+ owner_username: str,
+ persist_to=0,
+ ttl=0,
+):
+ doc_id = get_doc_id(id)
+ doc = ItemInDB(**doc_in.dict(), id=id, owner_username=owner_username)
+ return utils.upsert(
+ bucket=bucket, doc_id=doc_id, doc_in=doc, persist_to=persist_to, ttl=ttl
+ )
+
+
+def update(
+ bucket: Bucket,
+ *,
+ id: str,
+ doc_in: ItemUpdate,
+ owner_username=None,
+ persist_to=0,
+ ttl=0,
+):
+ doc_id = get_doc_id(id=id)
+ doc = get(bucket, id=id)
+ doc = doc.copy(update=doc_in.dict(skip_defaults=True))
+ if owner_username is not None:
+ doc.owner_username = owner_username
+ return utils.upsert(
+ bucket=bucket, doc_id=doc_id, doc_in=doc, persist_to=persist_to, ttl=ttl
+ )
+
+
+def remove(bucket: Bucket, *, id: str, persist_to=0):
+ doc_id = get_doc_id(id)
+ return utils.remove(
+ bucket=bucket, doc_id=doc_id, doc_model=ItemInDB, persist_to=persist_to
+ )
+
+
+def get_multi(bucket: Bucket, *, skip=0, limit=100):
+ return utils.get_docs(
+ bucket=bucket,
+ doc_type=ITEM_DOC_TYPE,
+ doc_model=ItemInDB,
+ skip=skip,
+ limit=limit,
+ )
+
+
+def get_multi_by_owner(bucket: Bucket, *, owner_username: str, skip=0, limit=100):
+ query_str = f"SELECT *, META().id as doc_id FROM {config.COUCHBASE_BUCKET_NAME} WHERE type = $type AND owner_username = $owner_username LIMIT $limit OFFSET $skip;"
+ q = N1QLQuery(
+ query_str,
+ bucket=config.COUCHBASE_BUCKET_NAME,
+ type=ITEM_DOC_TYPE,
+ owner_username=owner_username,
+ limit=limit,
+ skip=skip,
+ )
+ q.consistency = CONSISTENCY_REQUEST
+ doc_results = bucket.n1ql_query(q)
+ return utils.doc_results_to_model(doc_results, doc_model=ItemInDB)
+
+
+def search(bucket: Bucket, *, query_string: str, skip=0, limit=100):
+ docs = utils.search_get_docs(
+ bucket=bucket,
+ query_string=query_string,
+ index_name=full_text_index_name,
+ doc_model=ItemInDB,
+ skip=skip,
+ limit=limit,
+ )
+ return docs
+
+
+def search_with_owner(
+ bucket: Bucket, *query_string: str, username: str, skip=0, limit=100
+):
+ username_filter = f"owner_username:{username}"
+ if username_filter not in query_string:
+ query_string = f"{query_string} {username_filter}"
+ docs = utils.search_get_docs(
+ bucket=bucket,
+ query_string=query_string,
+ index_name=full_text_index_name,
+ doc_model=ItemInDB,
+ skip=skip,
+ limit=limit,
+ )
+ return docs
+
+
+def search_get_search_results_to_docs(
+ bucket: Bucket, *, query_string: str, skip=0, limit=100
+):
+ docs = utils.search_by_type_get_results_to_docs(
+ bucket=bucket,
+ query_string=query_string,
+ index_name=full_text_index_name,
+ doc_type=ITEM_DOC_TYPE,
+ doc_model=ItemInDB,
+ skip=skip,
+ limit=limit,
+ )
+ return docs
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/user.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/user.py
index a26c6c4..d85104c 100644
--- a/{{cookiecutter.project_slug}}/backend/app/app/crud/user.py
+++ b/{{cookiecutter.project_slug}}/backend/app/app/crud/user.py
@@ -7,15 +7,16 @@
from app.core.security import get_password_hash, verify_password
from app.models.config import USERPROFILE_DOC_TYPE
from app.models.role import RoleEnum
-from app.models.user import UserInCreate, UserInDB, UserInUpdate, UserSyncIn
+from app.models.user import UserCreate, UserInDB, UserSyncIn, UserUpdate
from . import utils
+# Same as file name /app/app/search_index_definitions/users.json
full_text_index_name = "users"
-def get_doc_id(username):
- return f"userprofile::{username}"
+def get_doc_id(username: str):
+ return f"{USERPROFILE_DOC_TYPE}::{username}"
def get(bucket: Bucket, *, username: str):
@@ -33,7 +34,7 @@ def get_by_email(bucket: Bucket, *, email: str):
)
q.consistency = CONSISTENCY_REQUEST
doc_results = bucket.n1ql_query(q)
- users = utils.results_to_model(doc_results, doc_model=UserInDB)
+ users = utils.doc_results_to_model(doc_results, doc_model=UserInDB)
if not users:
return None
return users[0]
@@ -42,7 +43,6 @@ def get_by_email(bucket: Bucket, *, email: str):
def insert_sync_gateway(user: UserSyncIn):
name = user.name
url = f"http://{config.COUCHBASE_SYNC_GATEWAY_HOST}:{config.COUCHBASE_SYNC_GATEWAY_PORT}/{config.COUCHBASE_SYNC_GATEWAY_DATABASE}/_user/{name}"
-
data = jsonable_encoder(user)
response = requests.put(url, json=data)
return response.status_code == 200 or response.status_code == 201
@@ -59,10 +59,9 @@ def update_sync_gateway(user: UserSyncIn):
return response.status_code == 200 or response.status_code == 201
-def upsert_in_db(bucket: Bucket, *, user_in: UserInCreate, persist_to=0):
+def upsert_in_db(bucket: Bucket, *, user_in: UserCreate, persist_to=0):
user_doc_id = get_doc_id(user_in.username)
passwordhash = get_password_hash(user_in.password)
-
user = UserInDB(**user_in.dict(), hashed_password=passwordhash)
doc_data = jsonable_encoder(user)
with bucket.durability(
@@ -72,14 +71,10 @@ def upsert_in_db(bucket: Bucket, *, user_in: UserInCreate, persist_to=0):
return user
-def update_in_db(bucket: Bucket, *, username: str, user_in: UserInUpdate, persist_to=0):
+def update_in_db(bucket: Bucket, *, username: str, user_in: UserUpdate, persist_to=0):
user_doc_id = get_doc_id(username)
stored_user = get(bucket, username=username)
- for field in stored_user.fields:
- if field in user_in.fields:
- value_in = getattr(user_in, field)
- if value_in is not None:
- setattr(stored_user, field, value_in)
+ stored_user = stored_user.copy(update=user_in.dict(skip_defaults=True))
if user_in.password:
passwordhash = get_password_hash(user_in.password)
stored_user.hashed_password = passwordhash
@@ -91,14 +86,14 @@ def update_in_db(bucket: Bucket, *, username: str, user_in: UserInUpdate, persis
return stored_user
-def upsert(bucket: Bucket, *, user_in: UserInCreate, persist_to=0):
+def upsert(bucket: Bucket, *, user_in: UserCreate, persist_to=0):
user = upsert_in_db(bucket, user_in=user_in, persist_to=persist_to)
user_in_sync = UserSyncIn(**user_in.dict(), name=user_in.username)
assert insert_sync_gateway(user_in_sync)
return user
-def update(bucket: Bucket, *, username: str, user_in: UserInUpdate, persist_to=0):
+def update(bucket: Bucket, *, username: str, user_in: UserUpdate, persist_to=0):
user = update_in_db(
bucket, username=username, user_in=user_in, persist_to=persist_to
)
@@ -141,20 +136,23 @@ def get_multi(bucket: Bucket, *, skip=0, limit=100):
return users
-def search_docs(bucket: Bucket, *, query_string: str, skip=0, limit=100):
- users = utils.search_docs(
+def search(bucket: Bucket, *, query_string: str, skip=0, limit=100):
+ users = utils.search_get_docs(
bucket=bucket,
query_string=query_string,
index_name=full_text_index_name,
doc_model=UserInDB,
+ doc_type=USERPROFILE_DOC_TYPE,
skip=skip,
limit=limit,
)
return users
-def search(bucket: Bucket, *, query_string: str, skip=0, limit=100):
- users = utils.search_results_by_type(
+def search_get_search_results_to_docs(
+ bucket: Bucket, *, query_string: str, skip=0, limit=100
+):
+ users = utils.search_by_type_get_results_to_docs(
bucket=bucket,
query_string=query_string,
index_name=full_text_index_name,
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/utils.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/utils.py
index 74d18a8..dd22f93 100644
--- a/{{cookiecutter.project_slug}}/backend/app/app/crud/utils.py
+++ b/{{cookiecutter.project_slug}}/backend/app/app/crud/utils.py
@@ -1,14 +1,17 @@
import uuid
from enum import Enum
-from typing import List, Sequence, Type, Union
+from typing import List, Optional, Sequence, Type, TypeVar, Union
from couchbase.bucket import Bucket
from couchbase.fulltext import MatchAllQuery, QueryStringQuery
from couchbase.n1ql import CONSISTENCY_REQUEST, N1QLQuery
+from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
from pydantic.fields import Field, Shape
-from app.core.config import COUCHBASE_BUCKET_NAME
+from app.core import config
+
+PydanticModel = TypeVar("PydanticModel", bound=BaseModel)
def generate_new_id():
@@ -25,19 +28,23 @@ def ensure_enums_to_strs(items: Union[Sequence[Union[Enum, str]], Type[Enum]]):
return str_items
-def get_all_documents_by_type(bucket: Bucket, *, doc_type: str, skip=0, limit=100):
- query_str = f"SELECT *, META().id as id FROM {COUCHBASE_BUCKET_NAME} WHERE type = $type LIMIT $limit OFFSET $skip;"
+def get_doc_results_by_type(bucket: Bucket, *, doc_type: str, skip=0, limit=100):
+ query_str = f"SELECT *, META().id as doc_id FROM {config.COUCHBASE_BUCKET_NAME} WHERE type = $type LIMIT $limit OFFSET $skip;"
q = N1QLQuery(
- query_str, bucket=COUCHBASE_BUCKET_NAME, type=doc_type, limit=limit, skip=skip
+ query_str,
+ bucket=config.COUCHBASE_BUCKET_NAME,
+ type=doc_type,
+ limit=limit,
+ skip=skip,
)
q.consistency = CONSISTENCY_REQUEST
result = bucket.n1ql_query(q)
return result
-def get_documents_by_keys(
- bucket: Bucket, *, keys: List[str], doc_model=Type[BaseModel]
-):
+def get_docs_by_keys(
+ bucket: Bucket, *, keys: List[str], doc_model=Type[PydanticModel]
+) -> List[PydanticModel]:
results = bucket.get_multi(keys, quiet=True)
docs = []
for result in results.values():
@@ -46,18 +53,28 @@ def get_documents_by_keys(
return docs
-def results_to_model(results_from_couchbase: list, *, doc_model: Type[BaseModel]):
+def doc_result_to_model(
+ couchbase_result, *, doc_model: Type[PydanticModel]
+) -> PydanticModel:
+ data = couchbase_result[config.COUCHBASE_BUCKET_NAME]
+ doc = doc_model(**data)
+ return doc
+
+
+def doc_results_to_model(
+ results_from_couchbase: list, *, doc_model: Type[PydanticModel]
+) -> List[PydanticModel]:
items = []
for doc in results_from_couchbase:
- data = doc[COUCHBASE_BUCKET_NAME]
+ data = doc[config.COUCHBASE_BUCKET_NAME]
doc = doc_model(**data)
items.append(doc)
return items
def search_results_to_model(
- results_from_couchbase: list, *, doc_model: Type[BaseModel]
-):
+ results_from_couchbase: list, *, doc_model: Type[PydanticModel]
+) -> List[PydanticModel]:
items = []
for doc in results_from_couchbase:
data = doc.get("fields")
@@ -79,15 +96,17 @@ def search_results_to_model(
def get_docs(
- bucket: Bucket, *, doc_type: str, doc_model=Type[BaseModel], skip=0, limit=100
-):
- doc_results = get_all_documents_by_type(
+ bucket: Bucket, *, doc_type: str, doc_model=Type[PydanticModel], skip=0, limit=100
+) -> List[PydanticModel]:
+ doc_results = get_doc_results_by_type(
bucket, doc_type=doc_type, skip=skip, limit=limit
)
- return results_to_model(doc_results, doc_model=doc_model)
+ return doc_results_to_model(doc_results, doc_model=doc_model)
-def get_doc(bucket: Bucket, *, doc_id: str, doc_model: Type[BaseModel]):
+def get_doc(
+ bucket: Bucket, *, doc_id: str, doc_model: Type[PydanticModel]
+) -> Optional[PydanticModel]:
result = bucket.get(doc_id, quiet=True)
if not result.value:
return None
@@ -95,14 +114,65 @@ def get_doc(bucket: Bucket, *, doc_id: str, doc_model: Type[BaseModel]):
return model
-def search_docs_get_doc_ids(
+def upsert(
+ bucket: Bucket, *, doc_id: str, doc_in: PydanticModel, persist_to=0, ttl=0
+) -> Optional[PydanticModel]:
+ doc_data = jsonable_encoder(doc_in)
+ with bucket.durability(
+ persist_to=persist_to, timeout=config.COUCHBASE_DURABILITY_TIMEOUT_SECS
+ ):
+ result = bucket.upsert(doc_id, doc_data, ttl=ttl)
+ if result.success:
+ return doc_in
+ return None
+
+
+def update(
+ bucket: Bucket,
+ *,
+ doc_id: str,
+ doc: PydanticModel,
+ doc_updated: PydanticModel,
+ persist_to=0,
+ ttl=0,
+):
+ doc_updated = doc.copy(update=doc_updated.dict(skip_defaults=True))
+ data = jsonable_encoder(doc_updated)
+ with bucket.durability(
+ persist_to=persist_to, timeout=config.COUCHBASE_DURABILITY_TIMEOUT_SECS
+ ):
+ result = bucket.upsert(doc_id, data, ttl=ttl)
+ if result.success:
+ return doc_updated
+
+
+def remove(
+ bucket: Bucket, *, doc_id: str, doc_model: Type[PydanticModel] = None, persist_to=0
+) -> Optional[Union[PydanticModel, bool]]:
+ result = bucket.get(doc_id, quiet=True)
+ if not result.value:
+ return None
+ if doc_model:
+ model = doc_model(**result.value)
+ with bucket.durability(
+ persist_to=persist_to, timeout=config.COUCHBASE_DURABILITY_TIMEOUT_SECS
+ ):
+ result = bucket.remove(doc_id)
+ if not result.success:
+ return None
+ if doc_model:
+ return model
+ return True
+
+
+def search_get_doc_ids(
bucket: Bucket,
*,
query_string: str,
index_name: str,
skip: int = 0,
limit: int = 100,
-):
+) -> List[str]:
query = QueryStringQuery(query_string)
hits = bucket.search(index_name, query, skip=skip, limit=limit)
doc_ids = []
@@ -111,7 +181,7 @@ def search_docs_get_doc_ids(
return doc_ids
-def search_get_results(
+def search_get_search_results(
bucket: Bucket,
*,
query_string: str,
@@ -130,7 +200,7 @@ def search_get_results(
return docs
-def search_get_results_by_type(
+def search_by_type_get_search_results(
bucket: Bucket,
*,
query_string: str,
@@ -152,16 +222,23 @@ def search_get_results_by_type(
return docs
-def search_docs(
+def search_get_docs(
bucket: Bucket,
*,
query_string: str,
index_name: str,
- doc_model: Type[BaseModel],
+ doc_model: Type[PydanticModel],
+ doc_type: str = None,
skip=0,
limit=100,
-):
- keys = search_docs_get_doc_ids(
+) -> List[PydanticModel]:
+ if doc_type is not None:
+ type_filter = f"type:{doc_type}"
+ if not query_string:
+ query_string = type_filter
+ if query_string and type_filter not in query_string:
+ query_string += f" {type_filter}"
+ keys = search_get_doc_ids(
bucket=bucket,
query_string=query_string,
index_name=index_name,
@@ -170,20 +247,19 @@ def search_docs(
)
if not keys:
return []
- doc_results = get_documents_by_keys(bucket=bucket, keys=keys, doc_model=doc_model)
- return doc_results
+ return get_docs_by_keys(bucket=bucket, keys=keys, doc_model=doc_model)
-def search_results(
+def search_get_search_results_to_docs(
bucket: Bucket,
*,
query_string: str,
index_name: str,
- doc_model: Type[BaseModel],
+ doc_model: Type[PydanticModel],
skip=0,
limit=100,
-):
- doc_results = search_get_results(
+) -> List[PydanticModel]:
+ doc_results = search_get_search_results(
bucket=bucket,
query_string=query_string,
index_name=index_name,
@@ -193,17 +269,17 @@ def search_results(
return search_results_to_model(doc_results, doc_model=doc_model)
-def search_results_by_type(
+def search_by_type_get_results_to_docs(
bucket: Bucket,
*,
query_string: str,
index_name: str,
doc_type: str,
- doc_model: Type[BaseModel],
+ doc_model: Type[PydanticModel],
skip=0,
limit=100,
-):
- doc_results = search_get_results_by_type(
+) -> List[PydanticModel]:
+ doc_results = search_by_type_get_search_results(
bucket=bucket,
query_string=query_string,
index_name=index_name,
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py b/{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py
index 750921a..fa74bea 100644
--- a/{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py
+++ b/{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py
@@ -15,7 +15,7 @@
)
from app.db.full_text_search_utils import ensure_create_full_text_indexes
from app.models.role import RoleEnum
-from app.models.user import UserInCreate
+from app.models.user import UserCreate
def init_db():
@@ -75,7 +75,7 @@ def init_db():
)
logging.info("after ensure_create_couchbase_app_user sync")
logging.info("before upsert_user first superuser")
- user_in = UserInCreate(
+ user_in = UserCreate(
username=config.FIRST_SUPERUSER,
password=config.FIRST_SUPERUSER_PASSWORD,
email=config.FIRST_SUPERUSER,
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/models/config.py b/{{cookiecutter.project_slug}}/backend/app/app/models/config.py
index 7c55d2c..810612e 100644
--- a/{{cookiecutter.project_slug}}/backend/app/app/models/config.py
+++ b/{{cookiecutter.project_slug}}/backend/app/app/models/config.py
@@ -1 +1,2 @@
USERPROFILE_DOC_TYPE = "userprofile"
+ITEM_DOC_TYPE = "item"
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/models/item.py b/{{cookiecutter.project_slug}}/backend/app/app/models/item.py
new file mode 100644
index 0000000..63855bf
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/backend/app/app/models/item.py
@@ -0,0 +1,34 @@
+from pydantic import BaseModel
+
+from app.models.config import ITEM_DOC_TYPE
+
+
+# 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 to return to client
+class Item(ItemBase):
+ id: str
+ title: str
+ owner_username: str
+
+
+# Properties properties stored in DB
+class ItemInDB(ItemBase):
+ type: str = ITEM_DOC_TYPE
+ id: str
+ title: str
+ owner_username: str
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/models/user.py b/{{cookiecutter.project_slug}}/backend/app/app/models/user.py
index 7a71cfc..328783b 100644
--- a/{{cookiecutter.project_slug}}/backend/app/app/models/user.py
+++ b/{{cookiecutter.project_slug}}/backend/app/app/models/user.py
@@ -6,7 +6,7 @@
from app.models.role import RoleEnum
-# Shared properties
+# Shared properties in Couchbase and Sync Gateway
class UserBase(BaseModel):
email: Optional[str] = None
admin_roles: Optional[List[Union[str, RoleEnum]]] = None
@@ -14,13 +14,14 @@ class UserBase(BaseModel):
disabled: Optional[bool] = None
+# Shared properties in Couchbase
class UserBaseInDB(UserBase):
username: Optional[str] = None
full_name: Optional[str] = None
# Properties to receive via API on creation
-class UserInCreate(UserBaseInDB):
+class UserCreate(UserBaseInDB):
username: str
password: str
admin_roles: List[Union[str, RoleEnum]] = []
@@ -29,7 +30,7 @@ class UserInCreate(UserBaseInDB):
# Properties to receive via API on update
-class UserInUpdate(UserBaseInDB):
+class UserUpdate(UserBaseInDB):
password: Optional[str] = None
@@ -42,8 +43,10 @@ class User(UserBaseInDB):
class UserInDB(UserBaseInDB):
type: str = USERPROFILE_DOC_TYPE
hashed_password: str
+ username: str
+# Additional properties in Sync Gateway
class UserSyncIn(UserBase):
name: str
password: Optional[str] = None
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/search_index_definitions/items.json b/{{cookiecutter.project_slug}}/backend/app/app/search_index_definitions/items.json
new file mode 100644
index 0000000..870a716
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/backend/app/app/search_index_definitions/items.json
@@ -0,0 +1,15 @@
+{
+ "name": "items",
+ "type": "fulltext-alias",
+ "params": {
+ "targets": {
+ "items_01": {}
+ }
+ },
+ "sourceType": "nil",
+ "sourceName": "",
+ "sourceUUID": "",
+ "sourceParams": null,
+ "planParams": {},
+ "uuid": ""
+}
\ No newline at end of file
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/search_index_definitions/items_01.json b/{{cookiecutter.project_slug}}/backend/app/app/search_index_definitions/items_01.json
new file mode 100644
index 0000000..34f60df
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/backend/app/app/search_index_definitions/items_01.json
@@ -0,0 +1,116 @@
+{
+ "name": "items_01",
+ "type": "fulltext-index",
+ "params": {
+ "mapping": {
+ "types": {
+ "item": {
+ "enabled": true,
+ "dynamic": false,
+ "properties": {
+ "owner_username": {
+ "enabled": true,
+ "dynamic": false,
+ "fields": [
+ {
+ "name": "owner_username",
+ "type": "text",
+ "analyzer": "keyword",
+ "store": false,
+ "index": true,
+ "include_term_vectors": true,
+ "include_in_all": true
+ }
+ ]
+ },
+ "description": {
+ "enabled": true,
+ "dynamic": false,
+ "fields": [
+ {
+ "name": "description",
+ "type": "text",
+ "store": false,
+ "index": true,
+ "include_term_vectors": true,
+ "include_in_all": true
+ }
+ ]
+ },
+ "title": {
+ "enabled": true,
+ "dynamic": false,
+ "fields": [
+ {
+ "name": "title",
+ "type": "text",
+ "store": false,
+ "index": true,
+ "include_term_vectors": true,
+ "include_in_all": true
+ }
+ ]
+ },
+ "type": {
+ "enabled": true,
+ "dynamic": false,
+ "fields": [
+ {
+ "name": "type",
+ "type": "text",
+ "store": false,
+ "index": true,
+ "include_term_vectors": false,
+ "include_in_all": false
+ }
+ ]
+ },
+ "id": {
+ "enabled": true,
+ "dynamic": false,
+ "fields": [
+ {
+ "name": "id",
+ "type": "text",
+ "store": false,
+ "index": true,
+ "include_term_vectors": false,
+ "include_in_all": false
+ }
+ ]
+ }
+ }
+ }
+ },
+ "default_mapping": {
+ "enabled": false,
+ "dynamic": true
+ },
+ "default_type": "_default",
+ "default_analyzer": "standard",
+ "default_datetime_parser": "dateTimeOptional",
+ "default_field": "_all",
+ "store_dynamic": false,
+ "index_dynamic": true
+ },
+ "store": {
+ "indexType": "scorch",
+ "kvStoreName": ""
+ },
+ "doc_config": {
+ "mode": "type_field",
+ "type_field": "type",
+ "docid_prefix_delim": "",
+ "docid_regexp": ""
+ }
+ },
+ "sourceType": "couchbase",
+ "sourceName": "app",
+ "sourceUUID": "",
+ "sourceParams": {},
+ "planParams": {
+ "maxPartitionsPerPIndex": 171,
+ "numReplicas": 0
+ },
+ "uuid": ""
+}
\ No newline at end of file
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_celery.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_celery.py
index eb48030..2270243 100644
--- a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_celery.py
+++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_celery.py
@@ -8,7 +8,7 @@ def test_celery_worker_test(superuser_token_headers):
server_api = get_server_api()
data = {"msg": "test"}
r = requests.post(
- f"{server_api}{config.API_V1_STR}/test-celery/",
+ f"{server_api}{config.API_V1_STR}/utils/test-celery/",
json=data,
headers=superuser_token_headers,
)
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_items.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_items.py
new file mode 100644
index 0000000..c148afa
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_items.py
@@ -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_username" 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_username"] == item.owner_username
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_token.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_login.py
similarity index 100%
rename from {{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_token.py
rename to {{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_login.py
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_user.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_users.py
similarity index 88%
rename from {{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_user.py
rename to {{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_users.py
index 8dae7d9..01e4ebc 100644
--- a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_user.py
+++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_users.py
@@ -3,7 +3,7 @@
from app import crud
from app.core import config
from app.db.database import get_default_bucket
-from app.models.user import UserInCreate
+from app.models.user import UserCreate
from app.tests.utils.user import user_authentication_headers
from app.tests.utils.utils import get_server_api, random_lower_string
@@ -41,7 +41,7 @@ def test_get_existing_user(superuser_token_headers):
server_api = get_server_api()
username = random_lower_string()
password = random_lower_string()
- user_in = UserInCreate(username=username, email=username, password=password)
+ user_in = UserCreate(username=username, email=username, password=password)
bucket = get_default_bucket()
user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
r = requests.get(
@@ -59,7 +59,7 @@ def test_create_user_existing_username(superuser_token_headers):
username = random_lower_string()
# username = email
password = random_lower_string()
- user_in = UserInCreate(username=username, email=username, password=password)
+ user_in = UserCreate(username=username, email=username, password=password)
bucket = get_default_bucket()
user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
data = {"username": username, "password": password}
@@ -77,7 +77,7 @@ def test_create_user_by_normal_user():
server_api = get_server_api()
username = random_lower_string()
password = random_lower_string()
- user_in = UserInCreate(username=username, email=username, password=password)
+ user_in = UserCreate(username=username, email=username, password=password)
bucket = get_default_bucket()
user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
user_token_headers = user_authentication_headers(server_api, username, password)
@@ -92,13 +92,13 @@ def test_retrieve_users(superuser_token_headers):
server_api = get_server_api()
username = random_lower_string()
password = random_lower_string()
- user_in = UserInCreate(username=username, email=username, password=password)
+ user_in = UserCreate(username=username, email=username, password=password)
bucket = get_default_bucket()
user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
username2 = random_lower_string()
password2 = random_lower_string()
- user_in2 = UserInCreate(username=username2, email=username2, password=password2)
+ user_in2 = UserCreate(username=username2, email=username2, password=password2)
user2 = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
r = requests.get(
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_item.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_item.py
new file mode 100644
index 0000000..c2d36fb
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_item.py
@@ -0,0 +1,85 @@
+from app import crud
+from app.db.database import get_default_bucket
+from app.models.config import ITEM_DOC_TYPE
+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
+
+
+def test_create_item():
+ title = random_lower_string()
+ description = random_lower_string()
+ id = crud.utils.generate_new_id()
+ item_in = ItemCreate(title=title, description=description)
+ bucket = get_default_bucket()
+ user = create_random_user()
+ item = crud.item.upsert(
+ bucket=bucket, id=id, doc_in=item_in, owner_username=user.username, persist_to=1
+ )
+ assert item.id == id
+ assert item.type == ITEM_DOC_TYPE
+ assert item.title == title
+ assert item.description == description
+ assert item.owner_username == user.username
+
+
+def test_get_item():
+ title = random_lower_string()
+ description = random_lower_string()
+ id = crud.utils.generate_new_id()
+ item_in = ItemCreate(title=title, description=description)
+ bucket = get_default_bucket()
+ user = create_random_user()
+ item = crud.item.upsert(
+ bucket=bucket, id=id, doc_in=item_in, owner_username=user.username, persist_to=1
+ )
+ stored_item = crud.item.get(bucket=bucket, id=id)
+ assert item.id == stored_item.id
+ assert item.title == stored_item.title
+ assert item.description == stored_item.description
+ assert item.owner_username == stored_item.owner_username
+
+
+def test_update_item():
+ title = random_lower_string()
+ description = random_lower_string()
+ id = crud.utils.generate_new_id()
+ item_in = ItemCreate(title=title, description=description)
+ bucket = get_default_bucket()
+ user = create_random_user()
+ item = crud.item.upsert(
+ bucket=bucket, id=id, doc_in=item_in, owner_username=user.username, persist_to=1
+ )
+ description2 = random_lower_string()
+ item_update = ItemUpdate(description=description2)
+ item2 = crud.item.update(
+ bucket=bucket,
+ id=id,
+ doc_in=item_update,
+ owner_username=item.owner_username,
+ persist_to=1,
+ )
+ assert item.id == item2.id
+ assert item.title == item2.title
+ assert item.description == description
+ assert item2.description == description2
+ assert item.owner_username == item2.owner_username
+
+
+def test_delete_item():
+ title = random_lower_string()
+ description = random_lower_string()
+ id = crud.utils.generate_new_id()
+ item_in = ItemCreate(title=title, description=description)
+ bucket = get_default_bucket()
+ user = create_random_user()
+ item = crud.item.upsert(
+ bucket=bucket, id=id, doc_in=item_in, owner_username=user.username, persist_to=1
+ )
+ item2 = crud.item.remove(bucket=bucket, id=id, persist_to=1)
+ item3 = crud.item.get(bucket=bucket, id=id)
+ assert item3 is None
+ assert item2.id == id
+ assert item2.title == title
+ assert item2.description == description
+ assert item2.owner_username == user.username
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_user.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_user.py
index 454a80a..5a45a23 100644
--- a/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_user.py
+++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_user.py
@@ -3,14 +3,14 @@
from app import crud
from app.db.database import get_default_bucket
from app.models.role import RoleEnum
-from app.models.user import UserInCreate
+from app.models.user import UserCreate
from app.tests.utils.utils import random_lower_string
def test_create_user():
email = random_lower_string()
password = random_lower_string()
- user_in = UserInCreate(username=email, email=email, password=password)
+ user_in = UserCreate(username=email, email=email, password=password)
bucket = get_default_bucket()
user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
assert hasattr(user, "username")
@@ -23,7 +23,7 @@ def test_create_user():
def test_authenticate_user():
email = random_lower_string()
password = random_lower_string()
- user_in = UserInCreate(username=email, email=email, password=password)
+ user_in = UserCreate(username=email, email=email, password=password)
bucket = get_default_bucket()
user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
authenticated_user = crud.user.authenticate(
@@ -44,7 +44,7 @@ def test_not_authenticate_user():
def test_check_if_user_is_active():
email = random_lower_string()
password = random_lower_string()
- user_in = UserInCreate(username=email, email=email, password=password)
+ user_in = UserCreate(username=email, email=email, password=password)
bucket = get_default_bucket()
user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
is_active = crud.user.is_active(user)
@@ -54,9 +54,7 @@ def test_check_if_user_is_active():
def test_check_if_user_is_active_inactive():
email = random_lower_string()
password = random_lower_string()
- user_in = UserInCreate(
- username=email, email=email, password=password, disabled=True
- )
+ user_in = UserCreate(username=email, email=email, password=password, disabled=True)
bucket = get_default_bucket()
user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
is_active = crud.user.is_active(user)
@@ -66,7 +64,7 @@ def test_check_if_user_is_active_inactive():
def test_check_if_user_is_superuser():
email = random_lower_string()
password = random_lower_string()
- user_in = UserInCreate(
+ user_in = UserCreate(
username=email, email=email, password=password, admin_roles=[RoleEnum.superuser]
)
bucket = get_default_bucket()
@@ -78,7 +76,7 @@ def test_check_if_user_is_superuser():
def test_check_if_user_is_superuser_normal_user():
username = random_lower_string()
password = random_lower_string()
- user_in = UserInCreate(username=username, email=username, password=password)
+ user_in = UserCreate(username=username, email=username, password=password)
bucket = get_default_bucket()
user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
is_superuser = crud.user.is_superuser(user)
@@ -88,14 +86,14 @@ def test_check_if_user_is_superuser_normal_user():
def test_get_user():
password = random_lower_string()
username = random_lower_string()
- user_in = UserInCreate(
+ user_in = UserCreate(
username=username,
email=username,
password=password,
admin_roles=[RoleEnum.superuser],
)
bucket = get_default_bucket()
- user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
+ user = crud.user.upsert(bucket=bucket, user_in=user_in, persist_to=1)
user_2 = crud.user.get(bucket, username=username)
assert user.username == user_2.username
assert jsonable_encoder(user) == jsonable_encoder(user_2)
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py
new file mode 100644
index 0000000..304c15c
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py
@@ -0,0 +1,23 @@
+from app import crud
+from app.db.database import get_default_bucket
+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_username: str = None):
+ if owner_username is None:
+ user = create_random_user()
+ owner_username = user.username
+ title = random_lower_string()
+ description = random_lower_string()
+ id = crud.utils.generate_new_id()
+ item_in = ItemCreate(title=title, description=description, id=id)
+ bucket = get_default_bucket()
+ return crud.item.upsert(
+ bucket=bucket,
+ id=id,
+ doc_in=item_in,
+ owner_username=owner_username,
+ persist_to=1,
+ )
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/user.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/user.py
index 9f8009f..1e9f696 100644
--- a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/user.py
+++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/user.py
@@ -1,6 +1,10 @@
import requests
+from app import crud
from app.core import config
+from app.db.database import get_default_bucket
+from app.models.user import UserCreate
+from app.tests.utils.utils import random_lower_string
def user_authentication_headers(server_api, email, password):
@@ -11,3 +15,12 @@ def user_authentication_headers(server_api, email, password):
auth_token = response["access_token"]
headers = {"Authorization": f"Bearer {auth_token}"}
return headers
+
+
+def create_random_user():
+ email = random_lower_string()
+ password = random_lower_string()
+ user_in = UserCreate(username=email, email=email, password=password)
+ bucket = get_default_bucket()
+ user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
+ return user
diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests_pre_start.py b/{{cookiecutter.project_slug}}/backend/app/app/tests_pre_start.py
index e623af8..eaae325 100644
--- a/{{cookiecutter.project_slug}}/backend/app/app/tests_pre_start.py
+++ b/{{cookiecutter.project_slug}}/backend/app/app/tests_pre_start.py
@@ -3,7 +3,7 @@
from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed
from app.db.database import get_default_bucket
-from app.tests.api.api_v1.test_token import test_get_access_token
+from app.tests.api.api_v1.test_login import test_get_access_token
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
diff --git a/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh b/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh
index 08bb841..11184af 100644
--- a/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh
+++ b/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh
@@ -5,4 +5,4 @@ set -x
autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place app --exclude=__init__.py
isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --apply app
black app
-vulture app
+vulture app --min-confidence 70
diff --git a/{{cookiecutter.project_slug}}/backend/backend.dockerfile b/{{cookiecutter.project_slug}}/backend/backend.dockerfile
index f8ecd34..593e1d7 100644
--- a/{{cookiecutter.project_slug}}/backend/backend.dockerfile
+++ b/{{cookiecutter.project_slug}}/backend/backend.dockerfile
@@ -5,7 +5,7 @@ RUN wget -O - http://packages.couchbase.com/ubuntu/couchbase.key | apt-key add -
RUN echo "deb http://packages.couchbase.com/ubuntu stretch stretch/main" > /etc/apt/sources.list.d/couchbase.list
RUN apt-get update && apt-get install -y libcouchbase-dev libcouchbase2-bin build-essential
-RUN pip install celery==4.2.1 passlib[bcrypt] tenacity requests couchbase emails "fastapi>=0.7.1" uvicorn gunicorn pyjwt python-multipart email_validator jinja2
+RUN pip install celery~=4.3 passlib[bcrypt] tenacity requests couchbase emails "fastapi>=0.16.0" uvicorn gunicorn pyjwt python-multipart email_validator jinja2
# For development, Jupyter remote kernel, Hydrogen
# Using inside the container:
diff --git a/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile b/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile
index 7c4076e..92d2084 100644
--- a/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile
+++ b/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile
@@ -5,7 +5,7 @@ RUN wget -O - http://packages.couchbase.com/ubuntu/couchbase.key | apt-key add -
RUN echo "deb http://packages.couchbase.com/ubuntu stretch stretch/main" > /etc/apt/sources.list.d/couchbase.list
RUN apt-get update && apt-get install -y libcouchbase-dev build-essential
-RUN pip install raven celery==4.2.1 passlib[bcrypt] tenacity requests "fastapi>=0.7.1" couchbase emails pyjwt email_validator jinja2
+RUN pip install raven celery~=4.3 passlib[bcrypt] tenacity requests "fastapi>=0.16.0" couchbase emails pyjwt email_validator jinja2
# For development, Jupyter remote kernel, Hydrogen
# Using inside the container:
diff --git a/{{cookiecutter.project_slug}}/backend/tests.dockerfile b/{{cookiecutter.project_slug}}/backend/tests.dockerfile
index 15aeeab..684d4ad 100644
--- a/{{cookiecutter.project_slug}}/backend/tests.dockerfile
+++ b/{{cookiecutter.project_slug}}/backend/tests.dockerfile
@@ -4,7 +4,7 @@ RUN wget -O - http://packages.couchbase.com/ubuntu/couchbase.key | apt-key add -
RUN echo "deb http://packages.couchbase.com/ubuntu stretch stretch/main" > /etc/apt/sources.list.d/couchbase.list
RUN apt-get update && apt-get install -y libcouchbase-dev build-essential
-RUN pip install requests pytest tenacity passlib[bcrypt] couchbase "fastapi>=0.7.1"
+RUN pip install requests pytest tenacity passlib[bcrypt] couchbase "fastapi>=0.16.0"
# For development, Jupyter remote kernel, Hydrogen
# Using inside the container: