From 3a14c0024bc7d9d98eeb049a177fae8cccc2c9c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 20 Apr 2020 14:57:55 +0200 Subject: [PATCH 01/12] :recycle: Refactor backend, update DB session handling --- .../app/app/api/api_v1/endpoints/items.py | 24 ++++---- .../app/app/api/api_v1/endpoints/login.py | 20 ++++--- .../app/app/api/api_v1/endpoints/users.py | 30 +++++----- .../app/app/api/api_v1/endpoints/utils.py | 12 ++-- .../backend/app/app/api/deps.py | 55 ++++++++++++++++++ .../backend/app/app/api/utils/__init__.py | 0 .../backend/app/app/api/utils/db.py | 5 -- .../backend/app/app/api/utils/security.py | 45 --------------- .../backend/app/app/backend_pre_start.py | 5 +- .../backend/app/app/celeryworker_pre_start.py | 5 +- .../backend/app/app/core/jwt.py | 19 ------- .../backend/app/app/core/security.py | 20 +++++++ .../backend/app/app/crud/__init__.py | 2 +- .../backend/app/app/crud/base.py | 17 ++++-- .../backend/app/app/crud/crud_item.py | 2 +- .../backend/app/app/crud/crud_user.py | 26 ++++++--- .../backend/app/app/db/base.py | 2 +- .../backend/app/app/db/base_class.py | 3 + .../backend/app/app/db/init_db.py | 4 +- .../backend/app/app/db/session.py | 7 +-- .../backend/app/app/initial_data.py | 5 +- .../backend/app/app/main.py | 16 ++---- .../backend/app/app/models/__init__.py | 2 + .../app/app/tests/api/api_v1/test_items.py | 9 +-- .../app/app/tests/api/api_v1/test_users.py | 32 ++++++----- .../backend/app/app/tests/conftest.py | 9 ++- .../backend/app/app/tests/crud/test_item.py | 44 +++++++-------- .../backend/app/app/tests/crud/test_user.py | 56 +++++++++---------- .../backend/app/app/tests/utils/item.py | 13 +++-- .../backend/app/app/tests/utils/user.py | 24 ++++---- .../backend/app/app/tests_pre_start.py | 5 +- 31 files changed, 275 insertions(+), 243 deletions(-) create mode 100644 {{cookiecutter.project_slug}}/backend/app/app/api/deps.py delete mode 100644 {{cookiecutter.project_slug}}/backend/app/app/api/utils/__init__.py delete mode 100644 {{cookiecutter.project_slug}}/backend/app/app/api/utils/db.py delete mode 100644 {{cookiecutter.project_slug}}/backend/app/app/api/utils/security.py delete mode 100644 {{cookiecutter.project_slug}}/backend/app/app/core/jwt.py 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 index bddb472375..7e362f85dd 100644 --- 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 @@ -4,20 +4,20 @@ from sqlalchemy.orm import Session from app import crud -from app.api.utils.db import get_db -from app.api.utils.security import get_current_active_user from app.models.user import User as DBUser from app.schemas.item import Item, ItemCreate, ItemUpdate +from ... import deps + router = APIRouter() @router.get("/", response_model=List[Item]) def read_items( - db: Session = Depends(get_db), + db: Session = Depends(deps.get_db), skip: int = 0, limit: int = 100, - current_user: DBUser = Depends(get_current_active_user), + current_user: DBUser = Depends(deps.get_current_active_user), ): """ Retrieve items. @@ -34,9 +34,9 @@ def read_items( @router.post("/", response_model=Item) def create_item( *, - db: Session = Depends(get_db), + db: Session = Depends(deps.get_db), item_in: ItemCreate, - current_user: DBUser = Depends(get_current_active_user), + current_user: DBUser = Depends(deps.get_current_active_user), ): """ Create new item. @@ -50,10 +50,10 @@ def create_item( @router.put("/{id}", response_model=Item) def update_item( *, - db: Session = Depends(get_db), + db: Session = Depends(deps.get_db), id: int, item_in: ItemUpdate, - current_user: DBUser = Depends(get_current_active_user), + current_user: DBUser = Depends(deps.get_current_active_user), ): """ Update an item. @@ -70,9 +70,9 @@ def update_item( @router.get("/{id}", response_model=Item) def read_item( *, - db: Session = Depends(get_db), + db: Session = Depends(deps.get_db), id: int, - current_user: DBUser = Depends(get_current_active_user), + current_user: DBUser = Depends(deps.get_current_active_user), ): """ Get item by ID. @@ -88,9 +88,9 @@ def read_item( @router.delete("/{id}", response_model=Item) def delete_item( *, - db: Session = Depends(get_db), + db: Session = Depends(deps.get_db), id: int, - current_user: DBUser = Depends(get_current_active_user), + current_user: DBUser = Depends(deps.get_current_active_user), ): """ Delete an item. diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py index 34eadc25de..08eb2fad91 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py @@ -5,10 +5,7 @@ from sqlalchemy.orm import Session from app import crud -from app.api.utils.db import get_db -from app.api.utils.security import get_current_user from app.core.config import settings -from app.core.jwt import create_access_token from app.core.security import get_password_hash from app.models.user import User as DBUser from app.schemas.msg import Msg @@ -20,12 +17,15 @@ verify_password_reset_token, ) +from ....core import security +from ... import deps + router = APIRouter() @router.post("/login/access-token", response_model=Token) def login_access_token( - db: Session = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends() + db: Session = Depends(deps.get_db), form_data: OAuth2PasswordRequestForm = Depends() ): """ OAuth2 compatible token login, get an access token for future requests @@ -39,15 +39,15 @@ def login_access_token( raise HTTPException(status_code=400, detail="Inactive user") access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) return { - "access_token": create_access_token( - data={"user_id": user.id}, expires_delta=access_token_expires + "access_token": security.create_access_token( + user.id, expires_delta=access_token_expires ), "token_type": "bearer", } @router.post("/login/test-token", response_model=User) -def test_token(current_user: DBUser = Depends(get_current_user)): +def test_token(current_user: DBUser = Depends(deps.get_current_user)): """ Test access token """ @@ -55,7 +55,7 @@ def test_token(current_user: DBUser = Depends(get_current_user)): @router.post("/password-recovery/{email}", response_model=Msg) -def recover_password(email: str, db: Session = Depends(get_db)): +def recover_password(email: str, db: Session = Depends(deps.get_db)): """ Password Recovery """ @@ -75,7 +75,9 @@ def recover_password(email: str, db: Session = Depends(get_db)): @router.post("/reset-password/", response_model=Msg) def reset_password( - token: str = Body(...), new_password: str = Body(...), db: Session = Depends(get_db) + token: str = Body(...), + new_password: str = Body(...), + db: Session = Depends(deps.get_db), ): """ Reset password diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py index 0c506eac75..cb0ebcb53f 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py @@ -6,22 +6,22 @@ from sqlalchemy.orm import Session from app import crud -from app.api.utils.db import get_db -from app.api.utils.security import get_current_active_superuser, get_current_active_user from app.core.config import settings from app.models.user import User as DBUser from app.schemas.user import User, UserCreate, UserUpdate from app.utils import send_new_account_email +from ... import deps + router = APIRouter() @router.get("/", response_model=List[User]) def read_users( - db: Session = Depends(get_db), + db: Session = Depends(deps.get_db), skip: int = 0, limit: int = 100, - current_user: DBUser = Depends(get_current_active_superuser), + current_user: DBUser = Depends(deps.get_current_active_superuser), ): """ Retrieve users. @@ -33,9 +33,9 @@ def read_users( @router.post("/", response_model=User) def create_user( *, - db: Session = Depends(get_db), + db: Session = Depends(deps.get_db), user_in: UserCreate, - current_user: DBUser = Depends(get_current_active_superuser), + current_user: DBUser = Depends(deps.get_current_active_superuser), ): """ Create new user. @@ -57,11 +57,11 @@ def create_user( @router.put("/me", response_model=User) def update_user_me( *, - db: Session = Depends(get_db), + db: Session = Depends(deps.get_db), password: str = Body(None), full_name: str = Body(None), email: EmailStr = Body(None), - current_user: DBUser = Depends(get_current_active_user), + current_user: DBUser = Depends(deps.get_current_active_user), ): """ Update own user. @@ -80,8 +80,8 @@ def update_user_me( @router.get("/me", response_model=User) def read_user_me( - db: Session = Depends(get_db), - current_user: DBUser = Depends(get_current_active_user), + db: Session = Depends(deps.get_db), + current_user: DBUser = Depends(deps.get_current_active_user), ): """ Get current user. @@ -92,7 +92,7 @@ def read_user_me( @router.post("/open", response_model=User) def create_user_open( *, - db: Session = Depends(get_db), + db: Session = Depends(deps.get_db), password: str = Body(...), email: EmailStr = Body(...), full_name: str = Body(None), @@ -119,8 +119,8 @@ def create_user_open( @router.get("/{user_id}", response_model=User) def read_user_by_id( user_id: int, - current_user: DBUser = Depends(get_current_active_user), - db: Session = Depends(get_db), + current_user: DBUser = Depends(deps.get_current_active_user), + db: Session = Depends(deps.get_db), ): """ Get a specific user by id. @@ -138,10 +138,10 @@ def read_user_by_id( @router.put("/{user_id}", response_model=User) def update_user( *, - db: Session = Depends(get_db), + db: Session = Depends(deps.get_db), user_id: int, user_in: UserUpdate, - current_user: DBUser = Depends(get_current_active_superuser), + current_user: DBUser = Depends(deps.get_current_active_superuser), ): """ Update a user. 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 91c074d451..870fd98abf 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 @@ -1,18 +1,21 @@ from fastapi import APIRouter, Depends from pydantic.networks import EmailStr -from app.api.utils.security import get_current_active_superuser from app.core.celery_app import celery_app +from app.models.user import User as DBUser from app.schemas.msg import Msg from app.schemas.user import User # noqa: F401 -from app.models.user import User as DBUser from app.utils import send_test_email +from ... import deps + router = APIRouter() @router.post("/test-celery/", response_model=Msg, status_code=201) -def test_celery(msg: Msg, current_user: DBUser = Depends(get_current_active_superuser)): +def test_celery( + msg: Msg, current_user: DBUser = Depends(deps.get_current_active_superuser) +): """ Test Celery worker. """ @@ -22,7 +25,8 @@ def test_celery(msg: Msg, current_user: DBUser = Depends(get_current_active_supe @router.post("/test-email/", response_model=Msg, status_code=201) def test_email( - email_to: EmailStr, current_user: DBUser = Depends(get_current_active_superuser) + email_to: EmailStr, + current_user: DBUser = Depends(deps.get_current_active_superuser), ): """ Test emails. diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/deps.py b/{{cookiecutter.project_slug}}/backend/app/app/api/deps.py new file mode 100644 index 0000000000..96eb4947e6 --- /dev/null +++ b/{{cookiecutter.project_slug}}/backend/app/app/api/deps.py @@ -0,0 +1,55 @@ +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from jose import jwt +from pydantic import ValidationError +from sqlalchemy.orm import Session + +from .. import crud, models, schemas +from ..core import security +from ..core.config import settings +from ..db.session import SessionLocal + +reusable_oauth2 = OAuth2PasswordBearer( + tokenUrl=f"{settings.API_V1_STR}/login/access-token" +) + + +def get_db(): + try: + db = SessionLocal() + yield db + finally: + db.close() + + +def get_current_user( + db: Session = Depends(get_db), token: str = Depends(reusable_oauth2) +): + try: + payload = jwt.decode( + token, settings.SECRET_KEY, algorithms=[security.ALGORITHM] + ) + token_data = schemas.TokenPayload(**payload) + except (jwt.JWTError, ValidationError): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Could not validate credentials", + ) + user = crud.user.get(db, id=token_data.sub) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user + + +def get_current_active_user(current_user: models.User = Depends(get_current_user)): + if not crud.user.is_active(current_user): + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +def get_current_active_superuser(current_user: models.User = Depends(get_current_user)): + if not crud.user.is_superuser(current_user): + raise HTTPException( + status_code=400, detail="The user doesn't have enough privileges" + ) + return current_user diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/utils/__init__.py b/{{cookiecutter.project_slug}}/backend/app/app/api/utils/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/utils/db.py b/{{cookiecutter.project_slug}}/backend/app/app/api/utils/db.py deleted file mode 100644 index 24a437e740..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/utils/db.py +++ /dev/null @@ -1,5 +0,0 @@ -from starlette.requests import Request - - -def get_db(request: Request): - return request.state.db diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/utils/security.py b/{{cookiecutter.project_slug}}/backend/app/app/api/utils/security.py deleted file mode 100644 index e00334cc43..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/utils/security.py +++ /dev/null @@ -1,45 +0,0 @@ -import jwt -from fastapi import Depends, HTTPException, Security -from fastapi.security import OAuth2PasswordBearer -from jwt import PyJWTError -from sqlalchemy.orm import Session -from starlette.status import HTTP_403_FORBIDDEN - -from app import crud -from app.api.utils.db import get_db -from app.core.config import settings -from app.core.jwt import ALGORITHM -from app.models.user import User -from app.schemas.token import TokenPayload - -reusable_oauth2 = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/login/access-token") - - -def get_current_user( - db: Session = Depends(get_db), token: str = Security(reusable_oauth2) -): - try: - payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM]) - token_data = TokenPayload(**payload) - except PyJWTError: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials" - ) - user = crud.user.get(db, id=token_data.user_id) - if not user: - raise HTTPException(status_code=404, detail="User not found") - return user - - -def get_current_active_user(current_user: User = Security(get_current_user)): - if not crud.user.is_active(current_user): - raise HTTPException(status_code=400, detail="Inactive user") - return current_user - - -def get_current_active_superuser(current_user: User = Security(get_current_user)): - if not crud.user.is_superuser(current_user): - raise HTTPException( - status_code=400, detail="The user doesn't have enough privileges" - ) - return current_user diff --git a/{{cookiecutter.project_slug}}/backend/app/app/backend_pre_start.py b/{{cookiecutter.project_slug}}/backend/app/app/backend_pre_start.py index e0bfa489f1..2b7b4fc134 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/backend_pre_start.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/backend_pre_start.py @@ -2,7 +2,7 @@ from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed -from app.db.session import db_session +from app.db.session import SessionLocal logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -19,8 +19,9 @@ ) def init(): try: + db = SessionLocal() # Try to create session to check if DB is awake - db_session.execute("SELECT 1") + db.execute("SELECT 1") except Exception as e: logger.error(e) raise e diff --git a/{{cookiecutter.project_slug}}/backend/app/app/celeryworker_pre_start.py b/{{cookiecutter.project_slug}}/backend/app/app/celeryworker_pre_start.py index e0bfa489f1..acfaf9d32e 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/celeryworker_pre_start.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/celeryworker_pre_start.py @@ -2,7 +2,7 @@ from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed -from app.db.session import db_session +from app.db.session import SessionLocal logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -20,7 +20,8 @@ def init(): try: # Try to create session to check if DB is awake - db_session.execute("SELECT 1") + db = SessionLocal() + db.execute("SELECT 1") except Exception as e: logger.error(e) raise e diff --git a/{{cookiecutter.project_slug}}/backend/app/app/core/jwt.py b/{{cookiecutter.project_slug}}/backend/app/app/core/jwt.py deleted file mode 100644 index 6cdf6ddaa2..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/core/jwt.py +++ /dev/null @@ -1,19 +0,0 @@ -from datetime import datetime, timedelta - -import jwt - -from app.core.config import settings - -ALGORITHM = "HS256" -access_token_jwt_subject = "access" - - -def create_access_token(*, data: dict, expires_delta: timedelta = None): - to_encode = data.copy() - if expires_delta: - expire = datetime.utcnow() + expires_delta - else: - expire = datetime.utcnow() + timedelta(minutes=15) - to_encode.update({"exp": expire, "sub": access_token_jwt_subject}) - encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM) - return encoded_jwt diff --git a/{{cookiecutter.project_slug}}/backend/app/app/core/security.py b/{{cookiecutter.project_slug}}/backend/app/app/core/security.py index 8d2a49f3a4..e02e624b0f 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/core/security.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/core/security.py @@ -1,8 +1,28 @@ +from datetime import datetime, timedelta + +from jose import jwt from passlib.context import CryptContext +from .config import settings + pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +ALGORITHM = "HS256" + + +def create_access_token(subject: str, expires_delta: timedelta = None): + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta( + minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES + ) + to_encode = {"exp": expire, "sub": str(subject)} + encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + def verify_password(plain_password: str, hashed_password: str): return pwd_context.verify(plain_password, hashed_password) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/__init__.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/__init__.py index e84e698c37..ffaa0d9301 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/crud/__init__.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/crud/__init__.py @@ -1,5 +1,5 @@ -from .crud_user import user # noqa: F401 from .crud_item import item # noqa: F401 +from .crud_user import user # noqa: F401 # For a new basic set of CRUD operations you could just do diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/base.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/base.py index 7ddf099eb5..ddf842628c 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/crud/base.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/crud/base.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Generic, TypeVar, Type +from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union from fastapi.encoders import jsonable_encoder from pydantic import BaseModel @@ -23,7 +23,7 @@ def __init__(self, model: Type[ModelType]): """ self.model = model - def get(self, db_session: Session, id: int) -> Optional[ModelType]: + def get(self, db_session: Session, id: Any) -> Optional[ModelType]: return db_session.query(self.model).filter(self.model.id == id).first() def get_multi(self, db_session: Session, *, skip=0, limit=100) -> List[ModelType]: @@ -31,17 +31,24 @@ def get_multi(self, db_session: Session, *, skip=0, limit=100) -> List[ModelType def create(self, db_session: Session, *, obj_in: CreateSchemaType) -> ModelType: obj_in_data = jsonable_encoder(obj_in) - db_obj = self.model(**obj_in_data) + db_obj = self.model(**obj_in_data) # type: ignore db_session.add(db_obj) db_session.commit() db_session.refresh(db_obj) return db_obj def update( - self, db_session: Session, *, db_obj: ModelType, obj_in: UpdateSchemaType + self, + db_session: Session, + *, + db_obj: ModelType, + obj_in: Union[UpdateSchemaType, Dict[str, Any]] ) -> ModelType: obj_data = jsonable_encoder(db_obj) - update_data = obj_in.dict(exclude_unset=True) + if isinstance(obj_in, dict): + update_data = obj_in + else: + update_data = obj_in.dict(exclude_unset=True) for field in obj_data: if field in update_data: setattr(db_obj, field, update_data[field]) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_item.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_item.py index 16db7bf999..af569343b0 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_item.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_item.py @@ -3,9 +3,9 @@ from fastapi.encoders import jsonable_encoder from sqlalchemy.orm import Session +from app.crud.base import CRUDBase from app.models.item import Item from app.schemas.item import ItemCreate, ItemUpdate -from app.crud.base import CRUDBase class CRUDItem(CRUDBase[Item, ItemCreate, ItemUpdate]): diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_user.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_user.py index e8ade85fb5..ab3d638527 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_user.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_user.py @@ -1,11 +1,11 @@ -from typing import Optional +from typing import Any, Dict, Optional, Union from sqlalchemy.orm import Session -from app.models.user import User -from app.schemas.user import UserCreate, UserUpdate, UserInDB -from app.core.security import verify_password, get_password_hash +from app.core.security import get_password_hash, verify_password from app.crud.base import CRUDBase +from app.models.user import User +from app.schemas.user import UserCreate, UserUpdate class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): @@ -24,14 +24,22 @@ def create(self, db_session: Session, *, obj_in: UserCreate) -> User: db_session.refresh(db_obj) return db_obj - def update(self, db_session: Session, *, db_obj: User, obj_in: UserUpdate) -> User: - if obj_in.password: + def update( + self, + db_session: Session, + *, + db_obj: User, + obj_in: Union[UserUpdate, Dict[str, Any]] + ) -> User: + if isinstance(obj_in, dict): + update_data = obj_in + else: update_data = obj_in.dict(exclude_unset=True) - hashed_password = get_password_hash(obj_in.password) + if update_data["password"]: + hashed_password = get_password_hash(update_data["password"]) del update_data["password"] update_data["hashed_password"] = hashed_password - use_obj_in = UserInDB.parse_obj(update_data) - return super().update(db_session, db_obj=db_obj, obj_in=use_obj_in) + return super().update(db_session, db_obj=db_obj, obj_in=update_data) def authenticate( self, db_session: Session, *, email: str, password: str diff --git a/{{cookiecutter.project_slug}}/backend/app/app/db/base.py b/{{cookiecutter.project_slug}}/backend/app/app/db/base.py index 81c92fda15..b7ec44fec1 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/db/base.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/db/base.py @@ -1,5 +1,5 @@ # Import all the models, so that Base has them before being # imported by Alembic from app.db.base_class import Base # noqa -from app.models.user import User # noqa from app.models.item import Item # noqa +from app.models.user import User # noqa diff --git a/{{cookiecutter.project_slug}}/backend/app/app/db/base_class.py b/{{cookiecutter.project_slug}}/backend/app/app/db/base_class.py index 3dadcb3316..3043759687 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/db/base_class.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/db/base_class.py @@ -1,8 +1,11 @@ +from typing import Any + from sqlalchemy.ext.declarative import as_declarative, declared_attr @as_declarative() class Base: + id: Any # Generate __tablename__ automatically @declared_attr def __tablename__(cls): 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 4c216d2162..86e3d92d30 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py @@ -1,11 +1,11 @@ from app import crud from app.core.config import settings +from app.db import base # noqa: F401 from app.schemas.user import UserCreate -# make sure all SQL Alchemy models are imported before initializing DB +# make sure all SQL Alchemy models are imported (app.db.base) before initializing DB # otherwise, SQL Alchemy might fail to initialize relationships properly # for more details: https://github.com/tiangolo/full-stack-fastapi-postgresql/issues/28 -from app.db import base # noqa: F401 def init_db(db_session): diff --git a/{{cookiecutter.project_slug}}/backend/app/app/db/session.py b/{{cookiecutter.project_slug}}/backend/app/app/db/session.py index b7bf6ea8ba..9edb2fa1d0 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/db/session.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/db/session.py @@ -1,10 +1,7 @@ from sqlalchemy import create_engine -from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.orm import sessionmaker from app.core.config import settings engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True) -db_session = scoped_session( - sessionmaker(autocommit=False, autoflush=False, bind=engine) -) -Session = sessionmaker(autocommit=False, autoflush=False, bind=engine) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/initial_data.py b/{{cookiecutter.project_slug}}/backend/app/app/initial_data.py index a572ada5d8..3bb8c89817 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/initial_data.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/initial_data.py @@ -1,14 +1,15 @@ import logging from app.db.init_db import init_db -from app.db.session import db_session +from app.db.session import SessionLocal logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def init(): - init_db(db_session) + db = SessionLocal() + init_db(db) def main(): diff --git a/{{cookiecutter.project_slug}}/backend/app/app/main.py b/{{cookiecutter.project_slug}}/backend/app/app/main.py index ed3a5c3f08..d5d0a79493 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/main.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/main.py @@ -1,12 +1,12 @@ from fastapi import FastAPI from starlette.middleware.cors import CORSMiddleware -from starlette.requests import Request from app.api.api_v1.api import api_router from app.core.config import settings -from app.db.session import Session -app = FastAPI(title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json") +app = FastAPI( + title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json" +) # Set all CORS enabled origins if settings.BACKEND_CORS_ORIGINS: @@ -16,14 +16,6 @@ allow_credentials=True, allow_methods=["*"], allow_headers=["*"], - ), + ) app.include_router(api_router, prefix=settings.API_V1_STR) - - -@app.middleware("http") -async def db_session_middleware(request: Request, call_next): - request.state.db = Session() - response = await call_next(request) - request.state.db.close() - return response diff --git a/{{cookiecutter.project_slug}}/backend/app/app/models/__init__.py b/{{cookiecutter.project_slug}}/backend/app/app/models/__init__.py index e69de29bb2..3ed4a33d24 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/models/__init__.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/models/__init__.py @@ -0,0 +1,2 @@ +from .item import Item # noqa: F401 +from .user import User # noqa: F401 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 index 0a6c996ff4..e4a759da33 100644 --- 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 @@ -1,12 +1,13 @@ import requests +from sqlalchemy.orm import Session from app.core.config import settings from app.tests.utils.item import create_random_item -from app.tests.utils.utils import get_server_api from app.tests.utils.user import create_random_user # noqa: F401 +from app.tests.utils.utils import get_server_api -def test_create_item(superuser_token_headers): +def test_create_item(superuser_token_headers: dict, db: Session): server_api = get_server_api() data = {"title": "Foo", "description": "Fighters"} response = requests.post( @@ -22,8 +23,8 @@ def test_create_item(superuser_token_headers): assert "owner_id" in content -def test_read_item(superuser_token_headers): - item = create_random_item() +def test_read_item(superuser_token_headers: dict, db: Session): + item = create_random_item(db) server_api = get_server_api() response = requests.get( f"{server_api}{settings.API_V1_STR}/items/{item.id}", diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_users.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_users.py index 0d4db33beb..237fbc5396 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_users.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_users.py @@ -1,10 +1,10 @@ import requests +from sqlalchemy.orm import Session from app import crud from app.core.config import settings -from app.db.session import db_session from app.schemas.user import UserCreate -from app.tests.utils.utils import get_server_api, random_lower_string, random_email +from app.tests.utils.utils import get_server_api, random_email, random_lower_string def test_get_users_superuser_me(superuser_token_headers): @@ -31,7 +31,7 @@ def test_get_users_normal_user_me(normal_user_token_headers): assert current_user["email"] == settings.EMAIL_TEST_USER -def test_create_user_new_email(superuser_token_headers): +def test_create_user_new_email(superuser_token_headers: dict, db: Session): server_api = get_server_api() username = random_email() password = random_lower_string() @@ -43,16 +43,17 @@ def test_create_user_new_email(superuser_token_headers): ) assert 200 <= r.status_code < 300 created_user = r.json() - user = crud.user.get_by_email(db_session, email=username) + user = crud.user.get_by_email(db, email=username) + assert user assert user.email == created_user["email"] -def test_get_existing_user(superuser_token_headers): +def test_get_existing_user(superuser_token_headers: dict, db: Session): server_api = get_server_api() username = random_email() password = random_lower_string() user_in = UserCreate(email=username, password=password) - user = crud.user.create(db_session, obj_in=user_in) + user = crud.user.create(db, obj_in=user_in) user_id = user.id r = requests.get( f"{server_api}{settings.API_V1_STR}/users/{user_id}", @@ -60,17 +61,18 @@ def test_get_existing_user(superuser_token_headers): ) assert 200 <= r.status_code < 300 api_user = r.json() - user = crud.user.get_by_email(db_session, email=username) - assert user.email == api_user["email"] + existing_user = crud.user.get_by_email(db, email=username) + assert existing_user + assert existing_user.email == api_user["email"] -def test_create_user_existing_username(superuser_token_headers): +def test_create_user_existing_username(superuser_token_headers: dict, db: Session): server_api = get_server_api() username = random_email() # username = email password = random_lower_string() user_in = UserCreate(email=username, password=password) - crud.user.create(db_session, obj_in=user_in) + crud.user.create(db, obj_in=user_in) data = {"email": username, "password": password} r = requests.post( f"{server_api}{settings.API_V1_STR}/users/", @@ -95,17 +97,17 @@ def test_create_user_by_normal_user(normal_user_token_headers): assert r.status_code == 400 -def test_retrieve_users(superuser_token_headers): +def test_retrieve_users(superuser_token_headers: dict, db: Session): server_api = get_server_api() username = random_email() password = random_lower_string() user_in = UserCreate(email=username, password=password) - user = crud.user.create(db_session, obj_in=user_in) + crud.user.create(db, obj_in=user_in) username2 = random_email() password2 = random_lower_string() user_in2 = UserCreate(email=username2, password=password2) - crud.user.create(db_session, obj_in=user_in2) + crud.user.create(db, obj_in=user_in2) r = requests.get( f"{server_api}{settings.API_V1_STR}/users/", headers=superuser_token_headers @@ -113,5 +115,5 @@ def test_retrieve_users(superuser_token_headers): all_users = r.json() assert len(all_users) > 1 - for user in all_users: - assert "email" in user + for item in all_users: + assert "email" in item diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py index 08b92ce009..dbcaab1b42 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py @@ -1,8 +1,15 @@ import pytest from app.core.config import settings -from app.tests.utils.utils import get_server_api, get_superuser_token_headers from app.tests.utils.user import authentication_token_from_email +from app.tests.utils.utils import get_server_api, get_superuser_token_headers + +from ..db.session import SessionLocal + + +@pytest.fixture(scope="session") +def db(): + yield SessionLocal() @pytest.fixture(scope="module") 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 index 739aa8e0a2..6007cc071a 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_item.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_item.py @@ -1,65 +1,59 @@ +from sqlalchemy.orm import Session + from app import crud from app.schemas.item import ItemCreate, ItemUpdate from app.tests.utils.user import create_random_user from app.tests.utils.utils import random_lower_string -from app.db.session import db_session -def test_create_item(): +def test_create_item(db: Session): title = random_lower_string() description = random_lower_string() item_in = ItemCreate(title=title, description=description) - user = create_random_user() - item = crud.item.create_with_owner( - db_session=db_session, obj_in=item_in, owner_id=user.id - ) + user = create_random_user(db) + item = crud.item.create_with_owner(db_session=db, obj_in=item_in, owner_id=user.id) assert item.title == title assert item.description == description assert item.owner_id == user.id -def test_get_item(): +def test_get_item(db: Session): title = random_lower_string() description = random_lower_string() item_in = ItemCreate(title=title, description=description) - user = create_random_user() - item = crud.item.create_with_owner( - db_session=db_session, obj_in=item_in, owner_id=user.id - ) - stored_item = crud.item.get(db_session=db_session, id=item.id) + user = create_random_user(db) + item = crud.item.create_with_owner(db_session=db, obj_in=item_in, owner_id=user.id) + stored_item = crud.item.get(db_session=db, id=item.id) + assert stored_item assert item.id == stored_item.id assert item.title == stored_item.title assert item.description == stored_item.description assert item.owner_id == stored_item.owner_id -def test_update_item(): +def test_update_item(db: Session): title = random_lower_string() description = random_lower_string() item_in = ItemCreate(title=title, description=description) - user = create_random_user() - item = crud.item.create_with_owner( - db_session=db_session, obj_in=item_in, owner_id=user.id - ) + user = create_random_user(db) + item = crud.item.create_with_owner(db_session=db, obj_in=item_in, owner_id=user.id) description2 = random_lower_string() item_update = ItemUpdate(description=description2) - item2 = crud.item.update(db_session=db_session, db_obj=item, obj_in=item_update) + item2 = crud.item.update(db_session=db, db_obj=item, obj_in=item_update) assert item.id == item2.id assert item.title == item2.title assert item2.description == description2 assert item.owner_id == item2.owner_id -def test_delete_item(): +def test_delete_item(db: Session): title = random_lower_string() description = random_lower_string() item_in = ItemCreate(title=title, description=description) - user = create_random_user() - item = crud.item.create_with_owner( - db_session=db_session, obj_in=item_in, owner_id=user.id - ) - item2 = crud.item.remove(db_session=db_session, id=item.id) - item3 = crud.item.get(db_session=db_session, id=item.id) + user = create_random_user(db) + item = crud.item.create_with_owner(db_session=db, obj_in=item_in, owner_id=user.id) + item2 = crud.item.remove(db_session=db, id=item.id) + item3 = crud.item.get(db_session=db, id=item.id) assert item3 is None assert item2.id == item.id assert item2.title == title 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 d1c14437a2..b5f759daef 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 @@ -1,94 +1,94 @@ from fastapi.encoders import jsonable_encoder +from sqlalchemy.orm import Session from app import crud -from app.core.security import get_password_hash, verify_password -from app.db.session import db_session +from app.core.security import verify_password from app.schemas.user import UserCreate, UserUpdate -from app.tests.utils.utils import random_lower_string, random_email +from app.tests.utils.utils import random_email, random_lower_string -def test_create_user(): +def test_create_user(db: Session): email = random_email() password = random_lower_string() user_in = UserCreate(email=email, password=password) - user = crud.user.create(db_session, obj_in=user_in) + user = crud.user.create(db, obj_in=user_in) assert user.email == email assert hasattr(user, "hashed_password") -def test_authenticate_user(): +def test_authenticate_user(db: Session): email = random_email() password = random_lower_string() user_in = UserCreate(email=email, password=password) - user = crud.user.create(db_session, obj_in=user_in) - authenticated_user = crud.user.authenticate( - db_session, email=email, password=password - ) + user = crud.user.create(db, obj_in=user_in) + authenticated_user = crud.user.authenticate(db, email=email, password=password) assert authenticated_user assert user.email == authenticated_user.email -def test_not_authenticate_user(): +def test_not_authenticate_user(db: Session): email = random_email() password = random_lower_string() - user = crud.user.authenticate(db_session, email=email, password=password) + user = crud.user.authenticate(db, email=email, password=password) assert user is None -def test_check_if_user_is_active(): +def test_check_if_user_is_active(db: Session): email = random_email() password = random_lower_string() user_in = UserCreate(email=email, password=password) - user = crud.user.create(db_session, obj_in=user_in) + user = crud.user.create(db, obj_in=user_in) is_active = crud.user.is_active(user) assert is_active is True -def test_check_if_user_is_active_inactive(): +def test_check_if_user_is_active_inactive(db: Session): email = random_email() password = random_lower_string() user_in = UserCreate(email=email, password=password, disabled=True) - user = crud.user.create(db_session, obj_in=user_in) + user = crud.user.create(db, obj_in=user_in) is_active = crud.user.is_active(user) assert is_active -def test_check_if_user_is_superuser(): +def test_check_if_user_is_superuser(db: Session): email = random_email() password = random_lower_string() user_in = UserCreate(email=email, password=password, is_superuser=True) - user = crud.user.create(db_session, obj_in=user_in) + user = crud.user.create(db, obj_in=user_in) is_superuser = crud.user.is_superuser(user) assert is_superuser is True -def test_check_if_user_is_superuser_normal_user(): +def test_check_if_user_is_superuser_normal_user(db: Session): username = random_email() password = random_lower_string() user_in = UserCreate(email=username, password=password) - user = crud.user.create(db_session, obj_in=user_in) + user = crud.user.create(db, obj_in=user_in) is_superuser = crud.user.is_superuser(user) assert is_superuser is False -def test_get_user(): +def test_get_user(db: Session): password = random_lower_string() username = random_email() user_in = UserCreate(email=username, password=password, is_superuser=True) - user = crud.user.create(db_session, obj_in=user_in) - user_2 = crud.user.get(db_session, id=user.id) + user = crud.user.create(db, obj_in=user_in) + user_2 = crud.user.get(db, id=user.id) + assert user_2 assert user.email == user_2.email assert jsonable_encoder(user) == jsonable_encoder(user_2) -def test_update_user(): +def test_update_user(db: Session): password = random_lower_string() email = random_email() user_in = UserCreate(email=email, password=password, is_superuser=True) - user = crud.user.create(db_session, obj_in=user_in) + user = crud.user.create(db, obj_in=user_in) new_password = random_lower_string() - user_in = UserUpdate(password=new_password, is_superuser=True) - crud.user.update(db_session, db_obj=user, obj_in=user_in) - user_2 = crud.user.get(db_session, id=user.id) + user_in_update = UserUpdate(password=new_password, is_superuser=True) + crud.user.update(db, db_obj=user, obj_in=user_in_update) + user_2 = crud.user.get(db, id=user.id) + assert user_2 assert user.email == user_2.email assert verify_password(new_password, user_2.hashed_password) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py index 95950f2e7d..d1d1dc147c 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py @@ -1,17 +1,18 @@ +from sqlalchemy.orm import Session + from app import crud -from app.db.session import db_session from app.schemas.item import ItemCreate from app.tests.utils.user import create_random_user from app.tests.utils.utils import random_lower_string +from ... import models + -def create_random_item(owner_id: int = None): +def create_random_item(db: Session, *, owner_id: int = None) -> models.Item: if owner_id is None: - user = create_random_user() + user = create_random_user(db) owner_id = user.id title = random_lower_string() description = random_lower_string() item_in = ItemCreate(title=title, description=description, id=id) - return crud.item.create_with_owner( - db_session=db_session, obj_in=item_in, owner_id=owner_id - ) + return crud.item.create_with_owner(db_session=db, obj_in=item_in, owner_id=owner_id) 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 42f80ce73b..123b28dd55 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/user.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/user.py @@ -1,43 +1,45 @@ import requests +from sqlalchemy.orm import Session from app import crud from app.core.config import settings -from app.db.session import db_session from app.schemas.user import UserCreate, UserUpdate -from app.tests.utils.utils import get_server_api, random_lower_string, random_email +from app.tests.utils.utils import get_server_api, random_email, random_lower_string def user_authentication_headers(server_api, email, password): data = {"username": email, "password": password} - r = requests.post(f"{server_api}{settings.API_V1_STR}/login/access-token", data=data) + r = requests.post( + f"{server_api}{settings.API_V1_STR}/login/access-token", data=data + ) response = r.json() auth_token = response["access_token"] headers = {"Authorization": f"Bearer {auth_token}"} return headers -def create_random_user(): +def create_random_user(db: Session): email = random_email() password = random_lower_string() user_in = UserCreate(username=email, email=email, password=password) - user = crud.user.create(db_session=db_session, obj_in=user_in) + user = crud.user.create(db_session=db, obj_in=user_in) return user -def authentication_token_from_email(email): +def authentication_token_from_email(email: str, db: Session): """ Return a valid token for the user with given email. If the user doesn't exist it is created first. """ password = random_lower_string() - user = crud.user.get_by_email(db_session, email=email) + user = crud.user.get_by_email(db, email=email) if not user: - user_in = UserCreate(username=email, email=email, password=password) - user = crud.user.create(db_session=db_session, obj_in=user_in) + user_in_create = UserCreate(username=email, email=email, password=password) + user = crud.user.create(db, obj_in=user_in_create) else: - user_in = UserUpdate(password=password) - user = crud.user.update(db_session, db_obj=user, obj_in=user_in) + user_in_update = UserUpdate(password=password) + user = crud.user.update(db, db_obj=user, obj_in=user_in_update) return user_authentication_headers(get_server_api(), email, password) 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 c1b0ccaf1a..e9febe6241 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests_pre_start.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests_pre_start.py @@ -2,7 +2,7 @@ from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed -from app.db.session import db_session +from app.db.session import SessionLocal from app.tests.api.api_v1.test_login import test_get_access_token logging.basicConfig(level=logging.INFO) @@ -21,7 +21,8 @@ def init(): try: # Try to create session to check if DB is awake - db_session.execute("SELECT 1") + db = SessionLocal() + db.execute("SELECT 1") # Wait for API to be awake, run one simple tests to authenticate test_get_access_token() except Exception as e: From cdcdc45ba77587b3287ae1c087a370f71644eac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 20 Apr 2020 14:59:23 +0200 Subject: [PATCH 02/12] :sparkles: Add mypy config and plugins --- .../backend/app/app/core/config.py | 20 +++++++++---------- .../backend/app/app/models/item.py | 5 +++++ .../backend/app/app/models/user.py | 9 +++++++-- .../backend/app/app/schemas/item.py | 6 ++++-- .../backend/app/app/schemas/token.py | 4 +++- .../backend/app/app/schemas/user.py | 4 ++-- .../backend/app/app/worker.py | 2 +- .../backend/app/mypy.ini | 4 ++++ 8 files changed, 36 insertions(+), 18 deletions(-) create mode 100644 {{cookiecutter.project_slug}}/backend/app/mypy.ini diff --git a/{{cookiecutter.project_slug}}/backend/app/app/core/config.py b/{{cookiecutter.project_slug}}/backend/app/app/core/config.py index 51a32c90aa..08fb0b1fcb 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/core/config.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/core/config.py @@ -1,5 +1,5 @@ import secrets -from typing import List +from typing import List, Optional from pydantic import AnyHttpUrl, BaseSettings, EmailStr, HttpUrl, PostgresDsn, validator @@ -26,7 +26,7 @@ def assemble_cors_origins(cls, v): return v PROJECT_NAME: str - SENTRY_DSN: HttpUrl = None + SENTRY_DSN: Optional[HttpUrl] = None @validator("SENTRY_DSN", pre=True) def sentry_dsn_can_be_blank(cls, v): @@ -38,7 +38,7 @@ def sentry_dsn_can_be_blank(cls, v): POSTGRES_USER: str POSTGRES_PASSWORD: str POSTGRES_DB: str - SQLALCHEMY_DATABASE_URI: PostgresDsn = None + SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None @validator("SQLALCHEMY_DATABASE_URI", pre=True) def assemble_db_connection(cls, v, values): @@ -53,12 +53,12 @@ def assemble_db_connection(cls, v, values): ) SMTP_TLS: bool = True - SMTP_PORT: int = None - SMTP_HOST: str = None - SMTP_USER: str = None - SMTP_PASSWORD: str = None - EMAILS_FROM_EMAIL: EmailStr = None - EMAILS_FROM_NAME: str = None + SMTP_PORT: Optional[int] = None + SMTP_HOST: Optional[str] = None + SMTP_USER: Optional[str] = None + SMTP_PASSWORD: Optional[str] = None + EMAILS_FROM_EMAIL: Optional[EmailStr] = None + EMAILS_FROM_NAME: Optional[str] = None @validator("EMAILS_FROM_NAME") def get_project_name(cls, v, values): @@ -78,7 +78,7 @@ def get_emails_enabled(cls, v, values): and values.get("EMAILS_FROM_EMAIL") ) - EMAIL_TEST_USER: EmailStr = "test@example.com" + EMAIL_TEST_USER: EmailStr = "test@example.com" # type: ignore FIRST_SUPERUSER: EmailStr FIRST_SUPERUSER_PASSWORD: str diff --git a/{{cookiecutter.project_slug}}/backend/app/app/models/item.py b/{{cookiecutter.project_slug}}/backend/app/app/models/item.py index 685687a098..205535e4b4 100755 --- a/{{cookiecutter.project_slug}}/backend/app/app/models/item.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/models/item.py @@ -1,8 +1,13 @@ +from typing import TYPE_CHECKING + from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy.orm import relationship from app.db.base_class import Base +if TYPE_CHECKING: + from .user import User # noqa: F401 + class Item(Base): id = Column(Integer, primary_key=True, index=True) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/models/user.py b/{{cookiecutter.project_slug}}/backend/app/app/models/user.py index 1052908a4b..1e8e1a011b 100755 --- a/{{cookiecutter.project_slug}}/backend/app/app/models/user.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/models/user.py @@ -1,14 +1,19 @@ +from typing import TYPE_CHECKING + from sqlalchemy import Boolean, Column, Integer, String from sqlalchemy.orm import relationship from app.db.base_class import Base +if TYPE_CHECKING: + from .item import Item # noqa: F401 + class User(Base): id = Column(Integer, primary_key=True, index=True) full_name = Column(String, index=True) - email = Column(String, unique=True, index=True) - hashed_password = Column(String) + email = Column(String, unique=True, index=True, nullable=False) + hashed_password = Column(String, nullable=False) is_active = Column(Boolean(), default=True) is_superuser = Column(Boolean(), default=False) items = relationship("Item", back_populates="owner") diff --git a/{{cookiecutter.project_slug}}/backend/app/app/schemas/item.py b/{{cookiecutter.project_slug}}/backend/app/app/schemas/item.py index 49ff3784d6..cb3c744d6b 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/schemas/item.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/schemas/item.py @@ -1,3 +1,5 @@ +from typing import Optional + from pydantic import BaseModel from .user import User # noqa: F401 @@ -5,8 +7,8 @@ # Shared properties class ItemBase(BaseModel): - title: str = None - description: str = None + title: Optional[str] = None + description: Optional[str] = None # Properties to receive on item creation diff --git a/{{cookiecutter.project_slug}}/backend/app/app/schemas/token.py b/{{cookiecutter.project_slug}}/backend/app/app/schemas/token.py index 75c0f4a4c1..ea85b460da 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/schemas/token.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/schemas/token.py @@ -1,3 +1,5 @@ +from typing import Optional + from pydantic import BaseModel @@ -7,4 +9,4 @@ class Token(BaseModel): class TokenPayload(BaseModel): - user_id: int = None + sub: Optional[int] = None diff --git a/{{cookiecutter.project_slug}}/backend/app/app/schemas/user.py b/{{cookiecutter.project_slug}}/backend/app/app/schemas/user.py index 32e3a98cf1..7f5c85ac68 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/schemas/user.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/schemas/user.py @@ -7,7 +7,7 @@ class UserBase(BaseModel): email: Optional[EmailStr] = None is_active: Optional[bool] = True - is_superuser: Optional[bool] = False + is_superuser: bool = False full_name: Optional[str] = None @@ -23,7 +23,7 @@ class UserUpdate(UserBase): class UserInDBBase(UserBase): - id: int = None + id: Optional[int] = None class Config: orm_mode = True diff --git a/{{cookiecutter.project_slug}}/backend/app/app/worker.py b/{{cookiecutter.project_slug}}/backend/app/app/worker.py index 499ed065ff..a06826d4d8 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/worker.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/worker.py @@ -1,7 +1,7 @@ from raven import Client -from app.core.config import settings from app.core.celery_app import celery_app +from app.core.config import settings client_sentry = Client(settings.SENTRY_DSN) diff --git a/{{cookiecutter.project_slug}}/backend/app/mypy.ini b/{{cookiecutter.project_slug}}/backend/app/mypy.ini new file mode 100644 index 0000000000..9813db029b --- /dev/null +++ b/{{cookiecutter.project_slug}}/backend/app/mypy.ini @@ -0,0 +1,4 @@ +[mypy] +plugins = pydantic.mypy, sqlmypy +ignore_missing_imports = True +disallow_untyped_defs = True From 99675c6853b2b8e394b6bc48bb24bc0c4eb303df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 20 Apr 2020 15:00:08 +0200 Subject: [PATCH 03/12] :heavy_plus_sign: Use Python-jose instead of PyJWT as it has some extra functionalities and features --- .../backend/app/app/utils.py | 32 +++++++------------ .../backend/app/pyproject.toml | 4 ++- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/utils.py b/{{cookiecutter.project_slug}}/backend/app/app/utils.py index a8582bc541..5ac42e9639 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/utils.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/utils.py @@ -4,16 +4,15 @@ from typing import Optional import emails -import jwt from emails.template import JinjaTemplate -from jwt.exceptions import InvalidTokenError +from jose import jwt from app.core.config import settings -password_reset_jwt_subject = "preset" - -def send_email(email_to: str, subject_template="", html_template="", environment={}): +def send_email( + email_to: str, subject_template="", html_template="", environment={} +) -> None: assert settings.EMAILS_ENABLED, "no provided configuration for email variables" message = emails.Message( subject=JinjaTemplate(subject_template), @@ -31,7 +30,7 @@ def send_email(email_to: str, subject_template="", html_template="", environment logging.info(f"send email result: {response}") -def send_test_email(email_to: str): +def send_test_email(email_to: str) -> None: project_name = settings.PROJECT_NAME subject = f"{project_name} - Test email" with open(Path(settings.EMAIL_TEMPLATES_DIR) / "test_email.html") as f: @@ -44,17 +43,13 @@ def send_test_email(email_to: str): ) -def send_reset_password_email(email_to: str, email: str, token: str): +def send_reset_password_email(email_to: str, email: str, token: str) -> None: project_name = settings.PROJECT_NAME subject = f"{project_name} - Password recovery for user {email}" with open(Path(settings.EMAIL_TEMPLATES_DIR) / "reset_password.html") as f: template_str = f.read() - if hasattr(token, "decode"): - use_token = token.decode() - else: - use_token = token server_host = settings.SERVER_HOST - link = f"{server_host}/reset-password?token={use_token}" + link = f"{server_host}/reset-password?token={token}" send_email( email_to=email_to, subject_template=subject, @@ -69,7 +64,7 @@ def send_reset_password_email(email_to: str, email: str, token: str): ) -def send_new_account_email(email_to: str, username: str, password: str): +def send_new_account_email(email_to: str, username: str, password: str) -> None: project_name = settings.PROJECT_NAME subject = f"{project_name} - New account for user {username}" with open(Path(settings.EMAIL_TEMPLATES_DIR) / "new_account.html") as f: @@ -89,23 +84,20 @@ def send_new_account_email(email_to: str, username: str, password: str): ) -def generate_password_reset_token(email): +def generate_password_reset_token(email: str) -> str: delta = timedelta(hours=settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS) now = datetime.utcnow() expires = now + delta exp = expires.timestamp() encoded_jwt = jwt.encode( - {"exp": exp, "nbf": now, "sub": password_reset_jwt_subject, "email": email}, - settings.SECRET_KEY, - algorithm="HS256", + {"exp": exp, "nbf": now, "sub": email}, settings.SECRET_KEY, algorithm="HS256", ) return encoded_jwt -def verify_password_reset_token(token) -> Optional[str]: +def verify_password_reset_token(token: str) -> Optional[str]: try: decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) - assert decoded_token["sub"] == password_reset_jwt_subject return decoded_token["email"] - except InvalidTokenError: + except jwt.JWTError: return None diff --git a/{{cookiecutter.project_slug}}/backend/app/pyproject.toml b/{{cookiecutter.project_slug}}/backend/app/pyproject.toml index 33c46fbf94..dcddaa7689 100644 --- a/{{cookiecutter.project_slug}}/backend/app/pyproject.toml +++ b/{{cookiecutter.project_slug}}/backend/app/pyproject.toml @@ -8,7 +8,6 @@ authors = ["Admin "] python = "^3.7" uvicorn = "^0.11.3" fastapi = "^0.54.1" -pyjwt = "^1.7.1" python-multipart = "^0.0.5" email-validator = "^1.0.5" requests = "^2.23.0" @@ -24,6 +23,7 @@ psycopg2-binary = "^2.8.5" alembic = "^1.4.2" sqlalchemy = "^1.3.16" pytest = "^5.4.1" +python-jose = {extras = ["cryptography"], version = "^3.1.0"} [tool.poetry.dev-dependencies] mypy = "^0.770" @@ -34,6 +34,8 @@ flake8 = "^3.7.9" pytest = "^5.4.1" jupyter = "^1.0.0" vulture = "^1.4" +sqlalchemy-stubs = "^0.3" +pytest-cov = "^2.8.1" [build-system] requires = ["poetry>=0.12"] From 54ecc6b9260665c43f894b48a7001b210c8217f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 20 Apr 2020 15:03:04 +0200 Subject: [PATCH 04/12] :sparkles: Add/update scripts for test, lint, format --- {{cookiecutter.project_slug}}/backend/app/.gitignore | 3 +++ .../backend/app/scripts/format-imports.sh | 6 ++++++ .../backend/app/scripts/format.sh | 6 ++++++ {{cookiecutter.project_slug}}/backend/app/scripts/lint.sh | 8 +++++--- .../backend/app/scripts/test-cov-html.sh | 6 ++++++ {{cookiecutter.project_slug}}/backend/app/scripts/test.sh | 6 ++++++ {{cookiecutter.project_slug}}/backend/app/tests-start.sh | 2 +- 7 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 {{cookiecutter.project_slug}}/backend/app/.gitignore create mode 100755 {{cookiecutter.project_slug}}/backend/app/scripts/format-imports.sh create mode 100755 {{cookiecutter.project_slug}}/backend/app/scripts/format.sh create mode 100755 {{cookiecutter.project_slug}}/backend/app/scripts/test-cov-html.sh create mode 100755 {{cookiecutter.project_slug}}/backend/app/scripts/test.sh diff --git a/{{cookiecutter.project_slug}}/backend/app/.gitignore b/{{cookiecutter.project_slug}}/backend/app/.gitignore new file mode 100644 index 0000000000..f511683016 --- /dev/null +++ b/{{cookiecutter.project_slug}}/backend/app/.gitignore @@ -0,0 +1,3 @@ +.mypy_cache +.coverage +htmlcov diff --git a/{{cookiecutter.project_slug}}/backend/app/scripts/format-imports.sh b/{{cookiecutter.project_slug}}/backend/app/scripts/format-imports.sh new file mode 100755 index 0000000000..68f70eea75 --- /dev/null +++ b/{{cookiecutter.project_slug}}/backend/app/scripts/format-imports.sh @@ -0,0 +1,6 @@ +#!/bin/sh -e +set -x + +# Sort imports one per line, so autoflake can remove unused imports +isort --recursive --force-single-line-imports --apply app +sh ./scripts/format.sh diff --git a/{{cookiecutter.project_slug}}/backend/app/scripts/format.sh b/{{cookiecutter.project_slug}}/backend/app/scripts/format.sh new file mode 100755 index 0000000000..fa5c9af56d --- /dev/null +++ b/{{cookiecutter.project_slug}}/backend/app/scripts/format.sh @@ -0,0 +1,6 @@ +#!/bin/sh -e +set -x + +autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place app --exclude=__init__.py +black app +isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --apply app diff --git a/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh b/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh index 11184afd99..52067abab6 100644 --- a/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh +++ b/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash +set -e 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 +mypy app +black app --check +isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --check-only app vulture app --min-confidence 70 +flake8 --max-line-length 88 --exclude=__init__.py diff --git a/{{cookiecutter.project_slug}}/backend/app/scripts/test-cov-html.sh b/{{cookiecutter.project_slug}}/backend/app/scripts/test-cov-html.sh new file mode 100755 index 0000000000..4a1fd3b75a --- /dev/null +++ b/{{cookiecutter.project_slug}}/backend/app/scripts/test-cov-html.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -e +set -x + +bash scripts/test.sh --cov-report=html "${@}" diff --git a/{{cookiecutter.project_slug}}/backend/app/scripts/test.sh b/{{cookiecutter.project_slug}}/backend/app/scripts/test.sh new file mode 100755 index 0000000000..fba8e95576 --- /dev/null +++ b/{{cookiecutter.project_slug}}/backend/app/scripts/test.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -e +set -x + +pytest --cov=app --cov-report=term-missing app/tests "${@}" diff --git a/{{cookiecutter.project_slug}}/backend/app/tests-start.sh b/{{cookiecutter.project_slug}}/backend/app/tests-start.sh index a3ee2fd863..099c2b3973 100644 --- a/{{cookiecutter.project_slug}}/backend/app/tests-start.sh +++ b/{{cookiecutter.project_slug}}/backend/app/tests-start.sh @@ -3,4 +3,4 @@ set -e python /app/app/tests_pre_start.py -pytest "$@" /app/app/tests/ +bash ./scripts/test.sh "$@" From 0bd332ee37380660aef5adffc3ba69c6295ee402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 20 Apr 2020 15:48:17 +0200 Subject: [PATCH 05/12] :wrench: Update lint and format configs --- {{cookiecutter.project_slug}}/backend/app/.flake8 | 3 +++ {{cookiecutter.project_slug}}/backend/app/pyproject.toml | 6 ++++++ {{cookiecutter.project_slug}}/backend/app/scripts/format.sh | 2 +- {{cookiecutter.project_slug}}/backend/app/scripts/lint.sh | 5 ++--- 4 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 {{cookiecutter.project_slug}}/backend/app/.flake8 diff --git a/{{cookiecutter.project_slug}}/backend/app/.flake8 b/{{cookiecutter.project_slug}}/backend/app/.flake8 new file mode 100644 index 0000000000..710dc9c030 --- /dev/null +++ b/{{cookiecutter.project_slug}}/backend/app/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 88 +exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache diff --git a/{{cookiecutter.project_slug}}/backend/app/pyproject.toml b/{{cookiecutter.project_slug}}/backend/app/pyproject.toml index dcddaa7689..35d86bccaf 100644 --- a/{{cookiecutter.project_slug}}/backend/app/pyproject.toml +++ b/{{cookiecutter.project_slug}}/backend/app/pyproject.toml @@ -40,3 +40,9 @@ pytest-cov = "^2.8.1" [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" + +[tool.isort] +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +line_length = 88 diff --git a/{{cookiecutter.project_slug}}/backend/app/scripts/format.sh b/{{cookiecutter.project_slug}}/backend/app/scripts/format.sh index fa5c9af56d..71d9779e41 100755 --- a/{{cookiecutter.project_slug}}/backend/app/scripts/format.sh +++ b/{{cookiecutter.project_slug}}/backend/app/scripts/format.sh @@ -3,4 +3,4 @@ set -x autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place app --exclude=__init__.py black app -isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --apply app +isort --recursive --apply app diff --git a/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh b/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh index 52067abab6..a00fcba632 100644 --- a/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh +++ b/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh @@ -1,10 +1,9 @@ #!/usr/bin/env bash -set -e set -x mypy app black app --check -isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --check-only app +isort --recursive --check-only app vulture app --min-confidence 70 -flake8 --max-line-length 88 --exclude=__init__.py +flake8 From 0a6f193eb06bb55cec63fcd9c7959cdc3a7ebce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 20 Apr 2020 15:49:10 +0200 Subject: [PATCH 06/12] :art: Update import format, comments, and types --- .../backend/app/app/core/security.py | 3 ++- .../backend/app/app/models/__init__.py | 4 ++-- .../backend/app/app/schemas/__init__.py | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/core/security.py b/{{cookiecutter.project_slug}}/backend/app/app/core/security.py index e02e624b0f..59366db95e 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/core/security.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/core/security.py @@ -1,4 +1,5 @@ from datetime import datetime, timedelta +from typing import Any, Union from jose import jwt from passlib.context import CryptContext @@ -11,7 +12,7 @@ ALGORITHM = "HS256" -def create_access_token(subject: str, expires_delta: timedelta = None): +def create_access_token(subject: Union[str, Any], expires_delta: timedelta = None): if expires_delta: expire = datetime.utcnow() + expires_delta else: diff --git a/{{cookiecutter.project_slug}}/backend/app/app/models/__init__.py b/{{cookiecutter.project_slug}}/backend/app/app/models/__init__.py index 3ed4a33d24..a9c6bdb1ef 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/models/__init__.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/models/__init__.py @@ -1,2 +1,2 @@ -from .item import Item # noqa: F401 -from .user import User # noqa: F401 +from .item import Item +from .user import User diff --git a/{{cookiecutter.project_slug}}/backend/app/app/schemas/__init__.py b/{{cookiecutter.project_slug}}/backend/app/app/schemas/__init__.py index e69de29bb2..6b41593dbb 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/schemas/__init__.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/schemas/__init__.py @@ -0,0 +1,4 @@ +from .item import Item, ItemCreate, ItemInDB, ItemUpdate +from .msg import Msg +from .token import Token, TokenPayload +from .user import User, UserCreate, UserInDB, UserUpdate From 39d95d7b7fb3c26146cd2c86fa6eb36b079bf9fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 20 Apr 2020 16:01:52 +0200 Subject: [PATCH 07/12] :art: Add types to config --- .../backend/app/app/core/config.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/core/config.py b/{{cookiecutter.project_slug}}/backend/app/app/core/config.py index 08fb0b1fcb..3bcd367f76 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/core/config.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/core/config.py @@ -1,5 +1,5 @@ import secrets -from typing import List, Optional +from typing import Any, Dict, List, Optional, Union from pydantic import AnyHttpUrl, BaseSettings, EmailStr, HttpUrl, PostgresDsn, validator @@ -20,7 +20,7 @@ class Settings(BaseSettings): BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [] @validator("BACKEND_CORS_ORIGINS", pre=True) - def assemble_cors_origins(cls, v): + def assemble_cors_origins(cls, v: Union[str, List[str]]) -> List[str]: if isinstance(v, str) and not v.startswith("["): return [i.strip() for i in v.split(",")] return v @@ -29,7 +29,7 @@ def assemble_cors_origins(cls, v): SENTRY_DSN: Optional[HttpUrl] = None @validator("SENTRY_DSN", pre=True) - def sentry_dsn_can_be_blank(cls, v): + def sentry_dsn_can_be_blank(cls, v: str) -> Optional[str]: if len(v) == 0: return None return v @@ -41,7 +41,7 @@ def sentry_dsn_can_be_blank(cls, v): SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None @validator("SQLALCHEMY_DATABASE_URI", pre=True) - def assemble_db_connection(cls, v, values): + def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any: if isinstance(v, str): return v return PostgresDsn.build( @@ -61,7 +61,7 @@ def assemble_db_connection(cls, v, values): EMAILS_FROM_NAME: Optional[str] = None @validator("EMAILS_FROM_NAME") - def get_project_name(cls, v, values): + def get_project_name(cls, v: Optional[str], values: Dict[str, Any]) -> str: if not v: return values["PROJECT_NAME"] return v @@ -71,7 +71,7 @@ def get_project_name(cls, v, values): EMAILS_ENABLED: bool = False @validator("EMAILS_ENABLED", pre=True) - def get_emails_enabled(cls, v, values): + def get_emails_enabled(cls, v: bool, values: Dict[str, Any]) -> bool: return bool( values.get("SMTP_HOST") and values.get("SMTP_PORT") From 43c06a365cb4dcad0dbee0bf4ecb1dd94d5375fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 20 Apr 2020 16:38:26 +0200 Subject: [PATCH 08/12] :sparkles: Add types for all the code, and small fixes --- .../app/app/api/api_v1/endpoints/items.py | 12 +++++----- .../app/app/api/api_v1/endpoints/login.py | 9 ++++---- .../app/app/api/api_v1/endpoints/users.py | 16 +++++++------- .../app/app/api/api_v1/endpoints/utils.py | 6 +++-- .../backend/app/app/api/deps.py | 22 ++++++++++++------- .../backend/app/app/backend_pre_start.py | 4 ++-- .../backend/app/app/celeryworker_pre_start.py | 4 ++-- .../backend/app/app/core/config.py | 15 +++++-------- .../backend/app/app/core/security.py | 8 ++++--- .../backend/app/app/crud/base.py | 4 +++- .../backend/app/app/crud/crud_item.py | 2 +- .../backend/app/app/db/base_class.py | 3 ++- .../backend/app/app/db/init_db.py | 13 ++++++----- .../backend/app/app/initial_data.py | 4 ++-- .../app/app/tests/api/api_v1/test_celery.py | 4 +++- .../app/app/tests/api/api_v1/test_items.py | 4 ++-- .../app/app/tests/api/api_v1/test_login.py | 6 +++-- .../app/app/tests/api/api_v1/test_users.py | 18 +++++++++------ .../backend/app/app/tests/conftest.py | 13 ++++++----- .../backend/app/app/tests/crud/test_item.py | 8 +++---- .../backend/app/app/tests/crud/test_user.py | 18 +++++++-------- .../backend/app/app/tests/utils/item.py | 4 +++- .../backend/app/app/tests/utils/user.py | 11 +++++++--- .../backend/app/app/tests/utils/utils.py | 9 ++++---- .../backend/app/app/tests_pre_start.py | 4 ++-- .../backend/app/app/utils.py | 7 ++++-- .../backend/app/app/worker.py | 2 +- 27 files changed, 132 insertions(+), 98 deletions(-) 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 index 7e362f85dd..eefeb6179c 100644 --- 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 @@ -1,4 +1,4 @@ -from typing import List +from typing import Any, List from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session @@ -18,7 +18,7 @@ def read_items( skip: int = 0, limit: int = 100, current_user: DBUser = Depends(deps.get_current_active_user), -): +) -> Any: """ Retrieve items. """ @@ -37,7 +37,7 @@ def create_item( db: Session = Depends(deps.get_db), item_in: ItemCreate, current_user: DBUser = Depends(deps.get_current_active_user), -): +) -> Any: """ Create new item. """ @@ -54,7 +54,7 @@ def update_item( id: int, item_in: ItemUpdate, current_user: DBUser = Depends(deps.get_current_active_user), -): +) -> Any: """ Update an item. """ @@ -73,7 +73,7 @@ def read_item( db: Session = Depends(deps.get_db), id: int, current_user: DBUser = Depends(deps.get_current_active_user), -): +) -> Any: """ Get item by ID. """ @@ -91,7 +91,7 @@ def delete_item( db: Session = Depends(deps.get_db), id: int, current_user: DBUser = Depends(deps.get_current_active_user), -): +) -> Any: """ Delete an item. """ diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py index 08eb2fad91..0b03267d89 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py @@ -1,4 +1,5 @@ from datetime import timedelta +from typing import Any from fastapi import APIRouter, Body, Depends, HTTPException from fastapi.security import OAuth2PasswordRequestForm @@ -26,7 +27,7 @@ @router.post("/login/access-token", response_model=Token) def login_access_token( db: Session = Depends(deps.get_db), form_data: OAuth2PasswordRequestForm = Depends() -): +) -> Any: """ OAuth2 compatible token login, get an access token for future requests """ @@ -47,7 +48,7 @@ def login_access_token( @router.post("/login/test-token", response_model=User) -def test_token(current_user: DBUser = Depends(deps.get_current_user)): +def test_token(current_user: DBUser = Depends(deps.get_current_user)) -> Any: """ Test access token """ @@ -55,7 +56,7 @@ def test_token(current_user: DBUser = Depends(deps.get_current_user)): @router.post("/password-recovery/{email}", response_model=Msg) -def recover_password(email: str, db: Session = Depends(deps.get_db)): +def recover_password(email: str, db: Session = Depends(deps.get_db)) -> Any: """ Password Recovery """ @@ -78,7 +79,7 @@ def reset_password( token: str = Body(...), new_password: str = Body(...), db: Session = Depends(deps.get_db), -): +) -> Any: """ Reset password """ diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py index cb0ebcb53f..35c0b4e4b7 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py @@ -1,4 +1,4 @@ -from typing import List +from typing import Any, List from fastapi import APIRouter, Body, Depends, HTTPException from fastapi.encoders import jsonable_encoder @@ -22,7 +22,7 @@ def read_users( skip: int = 0, limit: int = 100, current_user: DBUser = Depends(deps.get_current_active_superuser), -): +) -> Any: """ Retrieve users. """ @@ -36,7 +36,7 @@ def create_user( db: Session = Depends(deps.get_db), user_in: UserCreate, current_user: DBUser = Depends(deps.get_current_active_superuser), -): +) -> Any: """ Create new user. """ @@ -62,7 +62,7 @@ def update_user_me( full_name: str = Body(None), email: EmailStr = Body(None), current_user: DBUser = Depends(deps.get_current_active_user), -): +) -> Any: """ Update own user. """ @@ -82,7 +82,7 @@ def update_user_me( def read_user_me( db: Session = Depends(deps.get_db), current_user: DBUser = Depends(deps.get_current_active_user), -): +) -> Any: """ Get current user. """ @@ -96,7 +96,7 @@ def create_user_open( password: str = Body(...), email: EmailStr = Body(...), full_name: str = Body(None), -): +) -> Any: """ Create new user without the need to be logged in. """ @@ -121,7 +121,7 @@ def read_user_by_id( user_id: int, current_user: DBUser = Depends(deps.get_current_active_user), db: Session = Depends(deps.get_db), -): +) -> Any: """ Get a specific user by id. """ @@ -142,7 +142,7 @@ def update_user( user_id: int, user_in: UserUpdate, current_user: DBUser = Depends(deps.get_current_active_superuser), -): +) -> Any: """ Update a user. """ 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 870fd98abf..098c690aee 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 @@ -1,3 +1,5 @@ +from typing import Any + from fastapi import APIRouter, Depends from pydantic.networks import EmailStr @@ -15,7 +17,7 @@ @router.post("/test-celery/", response_model=Msg, status_code=201) def test_celery( msg: Msg, current_user: DBUser = Depends(deps.get_current_active_superuser) -): +) -> Any: """ Test Celery worker. """ @@ -27,7 +29,7 @@ def test_celery( def test_email( email_to: EmailStr, current_user: DBUser = Depends(deps.get_current_active_superuser), -): +) -> Any: """ Test emails. """ diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/deps.py b/{{cookiecutter.project_slug}}/backend/app/app/api/deps.py index 96eb4947e6..a0109afe84 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/deps.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/api/deps.py @@ -1,20 +1,22 @@ +from typing import Generator + from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import jwt from pydantic import ValidationError from sqlalchemy.orm import Session -from .. import crud, models, schemas -from ..core import security -from ..core.config import settings -from ..db.session import SessionLocal +from app import crud, models, schemas +from app.core import security +from app.core.config import settings +from app.db.session import SessionLocal reusable_oauth2 = OAuth2PasswordBearer( tokenUrl=f"{settings.API_V1_STR}/login/access-token" ) -def get_db(): +def get_db() -> Generator: try: db = SessionLocal() yield db @@ -24,7 +26,7 @@ def get_db(): def get_current_user( db: Session = Depends(get_db), token: str = Depends(reusable_oauth2) -): +) -> models.User: try: payload = jwt.decode( token, settings.SECRET_KEY, algorithms=[security.ALGORITHM] @@ -41,13 +43,17 @@ def get_current_user( return user -def get_current_active_user(current_user: models.User = Depends(get_current_user)): +def get_current_active_user( + current_user: models.User = Depends(get_current_user), +) -> models.User: if not crud.user.is_active(current_user): raise HTTPException(status_code=400, detail="Inactive user") return current_user -def get_current_active_superuser(current_user: models.User = Depends(get_current_user)): +def get_current_active_superuser( + current_user: models.User = Depends(get_current_user), +) -> models.User: if not crud.user.is_superuser(current_user): raise HTTPException( status_code=400, detail="The user doesn't have enough privileges" diff --git a/{{cookiecutter.project_slug}}/backend/app/app/backend_pre_start.py b/{{cookiecutter.project_slug}}/backend/app/app/backend_pre_start.py index 2b7b4fc134..3363a41542 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/backend_pre_start.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/backend_pre_start.py @@ -17,7 +17,7 @@ before=before_log(logger, logging.INFO), after=after_log(logger, logging.WARN), ) -def init(): +def init() -> None: try: db = SessionLocal() # Try to create session to check if DB is awake @@ -27,7 +27,7 @@ def init(): raise e -def main(): +def main() -> None: logger.info("Initializing service") init() logger.info("Service finished initializing") diff --git a/{{cookiecutter.project_slug}}/backend/app/app/celeryworker_pre_start.py b/{{cookiecutter.project_slug}}/backend/app/app/celeryworker_pre_start.py index acfaf9d32e..81de37134f 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/celeryworker_pre_start.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/celeryworker_pre_start.py @@ -17,7 +17,7 @@ before=before_log(logger, logging.INFO), after=after_log(logger, logging.WARN), ) -def init(): +def init() -> None: try: # Try to create session to check if DB is awake db = SessionLocal() @@ -27,7 +27,7 @@ def init(): raise e -def main(): +def main() -> None: logger.info("Initializing service") init() logger.info("Service finished initializing") diff --git a/{{cookiecutter.project_slug}}/backend/app/app/core/config.py b/{{cookiecutter.project_slug}}/backend/app/app/core/config.py index 3bcd367f76..8b07276dac 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/core/config.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/core/config.py @@ -5,13 +5,10 @@ class Settings(BaseSettings): - API_V1_STR: str = "/api/v1" - SECRET_KEY: str = secrets.token_urlsafe(32) - - ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 # 60 minutes * 24 hours * 8 days = 8 days - + # 60 minutes * 24 hours * 8 days = 8 days + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 SERVER_NAME: str SERVER_HOST: AnyHttpUrl # BACKEND_CORS_ORIGINS is a JSON-formatted list of origins @@ -20,10 +17,12 @@ class Settings(BaseSettings): BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [] @validator("BACKEND_CORS_ORIGINS", pre=True) - def assemble_cors_origins(cls, v: Union[str, List[str]]) -> List[str]: + def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]: if isinstance(v, str) and not v.startswith("["): return [i.strip() for i in v.split(",")] - return v + elif isinstance(v, (list, str)): + return v + raise ValueError(v) PROJECT_NAME: str SENTRY_DSN: Optional[HttpUrl] = None @@ -79,10 +78,8 @@ def get_emails_enabled(cls, v: bool, values: Dict[str, Any]) -> bool: ) EMAIL_TEST_USER: EmailStr = "test@example.com" # type: ignore - FIRST_SUPERUSER: EmailStr FIRST_SUPERUSER_PASSWORD: str - USERS_OPEN_REGISTRATION: bool = False class Config: diff --git a/{{cookiecutter.project_slug}}/backend/app/app/core/security.py b/{{cookiecutter.project_slug}}/backend/app/app/core/security.py index 59366db95e..0424c85434 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/core/security.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/core/security.py @@ -12,7 +12,9 @@ ALGORITHM = "HS256" -def create_access_token(subject: Union[str, Any], expires_delta: timedelta = None): +def create_access_token( + subject: Union[str, Any], expires_delta: timedelta = None +) -> str: if expires_delta: expire = datetime.utcnow() + expires_delta else: @@ -24,9 +26,9 @@ def create_access_token(subject: Union[str, Any], expires_delta: timedelta = Non return encoded_jwt -def verify_password(plain_password: str, hashed_password: str): +def verify_password(plain_password: str, hashed_password: str) -> bool: return pwd_context.verify(plain_password, hashed_password) -def get_password_hash(password: str): +def get_password_hash(password: str) -> str: return pwd_context.hash(password) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/base.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/base.py index ddf842628c..014d425410 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/crud/base.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/crud/base.py @@ -26,7 +26,9 @@ def __init__(self, model: Type[ModelType]): def get(self, db_session: Session, id: Any) -> Optional[ModelType]: return db_session.query(self.model).filter(self.model.id == id).first() - def get_multi(self, db_session: Session, *, skip=0, limit=100) -> List[ModelType]: + def get_multi( + self, db_session: Session, *, skip: int = 0, limit: int = 100 + ) -> List[ModelType]: return db_session.query(self.model).offset(skip).limit(limit).all() def create(self, db_session: Session, *, obj_in: CreateSchemaType) -> ModelType: diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_item.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_item.py index af569343b0..5138eddbae 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_item.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_item.py @@ -20,7 +20,7 @@ def create_with_owner( return db_obj def get_multi_by_owner( - self, db_session: Session, *, owner_id: int, skip=0, limit=100 + self, db_session: Session, *, owner_id: int, skip: int = 0, limit: int = 100 ) -> List[Item]: return ( db_session.query(self.model) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/db/base_class.py b/{{cookiecutter.project_slug}}/backend/app/app/db/base_class.py index 3043759687..3b759110b6 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/db/base_class.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/db/base_class.py @@ -6,7 +6,8 @@ @as_declarative() class Base: id: Any + __name__: str # Generate __tablename__ automatically @declared_attr - def __tablename__(cls): + def __tablename__(cls) -> str: return cls.__name__.lower() 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 86e3d92d30..bc1dd2e06c 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py @@ -1,24 +1,25 @@ -from app import crud +from sqlalchemy.orm import Session + +from app import crud, schemas from app.core.config import settings from app.db import base # noqa: F401 -from app.schemas.user import UserCreate # make sure all SQL Alchemy models are imported (app.db.base) before initializing DB # otherwise, SQL Alchemy might fail to initialize relationships properly # for more details: https://github.com/tiangolo/full-stack-fastapi-postgresql/issues/28 -def init_db(db_session): +def init_db(db: Session) -> None: # Tables should be created with Alembic migrations # But if you don't want to use migrations, create # the tables un-commenting the next line # Base.metadata.create_all(bind=engine) - user = crud.user.get_by_email(db_session, email=settings.FIRST_SUPERUSER) + user = crud.user.get_by_email(db, email=settings.FIRST_SUPERUSER) if not user: - user_in = UserCreate( + user_in = schemas.UserCreate( email=settings.FIRST_SUPERUSER, password=settings.FIRST_SUPERUSER_PASSWORD, is_superuser=True, ) - user = crud.user.create(db_session, obj_in=user_in) # noqa: F841 + user = crud.user.create(db, obj_in=user_in) # noqa: F841 diff --git a/{{cookiecutter.project_slug}}/backend/app/app/initial_data.py b/{{cookiecutter.project_slug}}/backend/app/app/initial_data.py index 3bb8c89817..c50646d2df 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/initial_data.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/initial_data.py @@ -7,12 +7,12 @@ logger = logging.getLogger(__name__) -def init(): +def init() -> None: db = SessionLocal() init_db(db) -def main(): +def main() -> None: logger.info("Creating initial data") init() logger.info("Initial data created") 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 e7eb054c0d..5bbcbf9c31 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 @@ -1,10 +1,12 @@ +from typing import Dict + import requests from app.core.config import settings from app.tests.utils.utils import get_server_api -def test_celery_worker_test(superuser_token_headers): +def test_celery_worker_test(superuser_token_headers: Dict[str, str]) -> None: server_api = get_server_api() data = {"msg": "test"} r = requests.post( 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 index e4a759da33..7e89ece1be 100644 --- 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 @@ -7,7 +7,7 @@ from app.tests.utils.utils import get_server_api -def test_create_item(superuser_token_headers: dict, db: Session): +def test_create_item(superuser_token_headers: dict, db: Session) -> None: server_api = get_server_api() data = {"title": "Foo", "description": "Fighters"} response = requests.post( @@ -23,7 +23,7 @@ def test_create_item(superuser_token_headers: dict, db: Session): assert "owner_id" in content -def test_read_item(superuser_token_headers: dict, db: Session): +def test_read_item(superuser_token_headers: dict, db: Session) -> None: item = create_random_item(db) server_api = get_server_api() response = requests.get( diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_login.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_login.py index 26506f81d9..465492c657 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_login.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_login.py @@ -1,10 +1,12 @@ +from typing import Dict + import requests from app.core.config import settings from app.tests.utils.utils import get_server_api -def test_get_access_token(): +def test_get_access_token() -> None: server_api = get_server_api() login_data = { "username": settings.FIRST_SUPERUSER, @@ -19,7 +21,7 @@ def test_get_access_token(): assert tokens["access_token"] -def test_use_access_token(superuser_token_headers): +def test_use_access_token(superuser_token_headers: Dict[str, str]) -> None: server_api = get_server_api() r = requests.post( f"{server_api}{settings.API_V1_STR}/login/test-token", diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_users.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_users.py index 237fbc5396..d4d6979155 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_users.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_users.py @@ -1,3 +1,5 @@ +from typing import Dict + import requests from sqlalchemy.orm import Session @@ -7,7 +9,7 @@ from app.tests.utils.utils import get_server_api, random_email, random_lower_string -def test_get_users_superuser_me(superuser_token_headers): +def test_get_users_superuser_me(superuser_token_headers: Dict[str, str]) -> None: server_api = get_server_api() r = requests.get( f"{server_api}{settings.API_V1_STR}/users/me", headers=superuser_token_headers @@ -19,7 +21,7 @@ def test_get_users_superuser_me(superuser_token_headers): assert current_user["email"] == settings.FIRST_SUPERUSER -def test_get_users_normal_user_me(normal_user_token_headers): +def test_get_users_normal_user_me(normal_user_token_headers: Dict[str, str]) -> None: server_api = get_server_api() r = requests.get( f"{server_api}{settings.API_V1_STR}/users/me", headers=normal_user_token_headers @@ -31,7 +33,7 @@ def test_get_users_normal_user_me(normal_user_token_headers): assert current_user["email"] == settings.EMAIL_TEST_USER -def test_create_user_new_email(superuser_token_headers: dict, db: Session): +def test_create_user_new_email(superuser_token_headers: dict, db: Session) -> None: server_api = get_server_api() username = random_email() password = random_lower_string() @@ -48,7 +50,7 @@ def test_create_user_new_email(superuser_token_headers: dict, db: Session): assert user.email == created_user["email"] -def test_get_existing_user(superuser_token_headers: dict, db: Session): +def test_get_existing_user(superuser_token_headers: dict, db: Session) -> None: server_api = get_server_api() username = random_email() password = random_lower_string() @@ -66,7 +68,9 @@ def test_get_existing_user(superuser_token_headers: dict, db: Session): assert existing_user.email == api_user["email"] -def test_create_user_existing_username(superuser_token_headers: dict, db: Session): +def test_create_user_existing_username( + superuser_token_headers: dict, db: Session +) -> None: server_api = get_server_api() username = random_email() # username = email @@ -84,7 +88,7 @@ def test_create_user_existing_username(superuser_token_headers: dict, db: Sessio assert "_id" not in created_user -def test_create_user_by_normal_user(normal_user_token_headers): +def test_create_user_by_normal_user(normal_user_token_headers: Dict[str, str]) -> None: server_api = get_server_api() username = random_email() password = random_lower_string() @@ -97,7 +101,7 @@ def test_create_user_by_normal_user(normal_user_token_headers): assert r.status_code == 400 -def test_retrieve_users(superuser_token_headers: dict, db: Session): +def test_retrieve_users(superuser_token_headers: dict, db: Session) -> None: server_api = get_server_api() username = random_email() password = random_lower_string() diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py index dbcaab1b42..3fd5614c13 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py @@ -1,4 +1,7 @@ +from typing import Dict, Iterator + import pytest +from sqlalchemy.orm import Session from app.core.config import settings from app.tests.utils.user import authentication_token_from_email @@ -8,20 +11,20 @@ @pytest.fixture(scope="session") -def db(): +def db() -> Iterator[Session]: yield SessionLocal() @pytest.fixture(scope="module") -def server_api(): +def server_api() -> str: return get_server_api() @pytest.fixture(scope="module") -def superuser_token_headers(): +def superuser_token_headers() -> Dict[str, str]: return get_superuser_token_headers() @pytest.fixture(scope="module") -def normal_user_token_headers(): - return authentication_token_from_email(settings.EMAIL_TEST_USER) +def normal_user_token_headers(db: Session) -> Dict[str, str]: + return authentication_token_from_email(email=settings.EMAIL_TEST_USER, db=db) 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 index 6007cc071a..79467a6f1d 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_item.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_item.py @@ -6,7 +6,7 @@ from app.tests.utils.utils import random_lower_string -def test_create_item(db: Session): +def test_create_item(db: Session) -> None: title = random_lower_string() description = random_lower_string() item_in = ItemCreate(title=title, description=description) @@ -17,7 +17,7 @@ def test_create_item(db: Session): assert item.owner_id == user.id -def test_get_item(db: Session): +def test_get_item(db: Session) -> None: title = random_lower_string() description = random_lower_string() item_in = ItemCreate(title=title, description=description) @@ -31,7 +31,7 @@ def test_get_item(db: Session): assert item.owner_id == stored_item.owner_id -def test_update_item(db: Session): +def test_update_item(db: Session) -> None: title = random_lower_string() description = random_lower_string() item_in = ItemCreate(title=title, description=description) @@ -46,7 +46,7 @@ def test_update_item(db: Session): assert item.owner_id == item2.owner_id -def test_delete_item(db: Session): +def test_delete_item(db: Session) -> None: title = random_lower_string() description = random_lower_string() item_in = ItemCreate(title=title, description=description) 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 b5f759daef..2caee5b870 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 @@ -7,7 +7,7 @@ from app.tests.utils.utils import random_email, random_lower_string -def test_create_user(db: Session): +def test_create_user(db: Session) -> None: email = random_email() password = random_lower_string() user_in = UserCreate(email=email, password=password) @@ -16,7 +16,7 @@ def test_create_user(db: Session): assert hasattr(user, "hashed_password") -def test_authenticate_user(db: Session): +def test_authenticate_user(db: Session) -> None: email = random_email() password = random_lower_string() user_in = UserCreate(email=email, password=password) @@ -26,14 +26,14 @@ def test_authenticate_user(db: Session): assert user.email == authenticated_user.email -def test_not_authenticate_user(db: Session): +def test_not_authenticate_user(db: Session) -> None: email = random_email() password = random_lower_string() user = crud.user.authenticate(db, email=email, password=password) assert user is None -def test_check_if_user_is_active(db: Session): +def test_check_if_user_is_active(db: Session) -> None: email = random_email() password = random_lower_string() user_in = UserCreate(email=email, password=password) @@ -42,7 +42,7 @@ def test_check_if_user_is_active(db: Session): assert is_active is True -def test_check_if_user_is_active_inactive(db: Session): +def test_check_if_user_is_active_inactive(db: Session) -> None: email = random_email() password = random_lower_string() user_in = UserCreate(email=email, password=password, disabled=True) @@ -51,7 +51,7 @@ def test_check_if_user_is_active_inactive(db: Session): assert is_active -def test_check_if_user_is_superuser(db: Session): +def test_check_if_user_is_superuser(db: Session) -> None: email = random_email() password = random_lower_string() user_in = UserCreate(email=email, password=password, is_superuser=True) @@ -60,7 +60,7 @@ def test_check_if_user_is_superuser(db: Session): assert is_superuser is True -def test_check_if_user_is_superuser_normal_user(db: Session): +def test_check_if_user_is_superuser_normal_user(db: Session) -> None: username = random_email() password = random_lower_string() user_in = UserCreate(email=username, password=password) @@ -69,7 +69,7 @@ def test_check_if_user_is_superuser_normal_user(db: Session): assert is_superuser is False -def test_get_user(db: Session): +def test_get_user(db: Session) -> None: password = random_lower_string() username = random_email() user_in = UserCreate(email=username, password=password, is_superuser=True) @@ -80,7 +80,7 @@ def test_get_user(db: Session): assert jsonable_encoder(user) == jsonable_encoder(user_2) -def test_update_user(db: Session): +def test_update_user(db: Session) -> None: password = random_lower_string() email = random_email() user_in = UserCreate(email=email, password=password, is_superuser=True) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py index d1d1dc147c..b97d64f2e6 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py @@ -1,3 +1,5 @@ +from typing import Optional + from sqlalchemy.orm import Session from app import crud @@ -8,7 +10,7 @@ from ... import models -def create_random_item(db: Session, *, owner_id: int = None) -> models.Item: +def create_random_item(db: Session, *, owner_id: Optional[int] = None) -> models.Item: if owner_id is None: user = create_random_user(db) owner_id = user.id 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 123b28dd55..fdd655ae68 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/user.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/user.py @@ -1,13 +1,18 @@ +from typing import Dict + import requests from sqlalchemy.orm import Session from app import crud from app.core.config import settings +from app.models.user import User from app.schemas.user import UserCreate, UserUpdate from app.tests.utils.utils import get_server_api, random_email, random_lower_string -def user_authentication_headers(server_api, email, password): +def user_authentication_headers( + server_api: str, email: str, password: str +) -> Dict[str, str]: data = {"username": email, "password": password} r = requests.post( @@ -19,7 +24,7 @@ def user_authentication_headers(server_api, email, password): return headers -def create_random_user(db: Session): +def create_random_user(db: Session) -> User: email = random_email() password = random_lower_string() user_in = UserCreate(username=email, email=email, password=password) @@ -27,7 +32,7 @@ def create_random_user(db: Session): return user -def authentication_token_from_email(email: str, db: Session): +def authentication_token_from_email(*, email: str, db: Session) -> Dict[str, str]: """ Return a valid token for the user with given email. diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/utils.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/utils.py index 7fb38c8478..563274e0d5 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/utils.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/utils.py @@ -1,25 +1,26 @@ import random import string +from typing import Dict import requests from app.core.config import settings -def random_lower_string(): +def random_lower_string() -> str: return "".join(random.choices(string.ascii_lowercase, k=32)) -def random_email(): +def random_email() -> str: return f"{random_lower_string()}@{random_lower_string()}.com" -def get_server_api(): +def get_server_api() -> str: server_name = f"http://{settings.SERVER_NAME}" return server_name -def get_superuser_token_headers(): +def get_superuser_token_headers() -> Dict[str, str]: server_api = get_server_api() login_data = { "username": settings.FIRST_SUPERUSER, 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 e9febe6241..70dbd4f8c8 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests_pre_start.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests_pre_start.py @@ -18,7 +18,7 @@ before=before_log(logger, logging.INFO), after=after_log(logger, logging.WARN), ) -def init(): +def init() -> None: try: # Try to create session to check if DB is awake db = SessionLocal() @@ -30,7 +30,7 @@ def init(): raise e -def main(): +def main() -> None: logger.info("Initializing service") init() logger.info("Service finished initializing") diff --git a/{{cookiecutter.project_slug}}/backend/app/app/utils.py b/{{cookiecutter.project_slug}}/backend/app/app/utils.py index 5ac42e9639..b1aba6bc00 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/utils.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/utils.py @@ -1,7 +1,7 @@ import logging from datetime import datetime, timedelta from pathlib import Path -from typing import Optional +from typing import Any, Dict, Optional import emails from emails.template import JinjaTemplate @@ -11,7 +11,10 @@ def send_email( - email_to: str, subject_template="", html_template="", environment={} + email_to: str, + subject_template: str = "", + html_template: str = "", + environment: Dict[str, Any] = {}, ) -> None: assert settings.EMAILS_ENABLED, "no provided configuration for email variables" message = emails.Message( diff --git a/{{cookiecutter.project_slug}}/backend/app/app/worker.py b/{{cookiecutter.project_slug}}/backend/app/app/worker.py index a06826d4d8..5fea53c961 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/worker.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/worker.py @@ -7,5 +7,5 @@ @celery_app.task(acks_late=True) -def test_celery(word: str): +def test_celery(word: str) -> str: return f"test task return {word}" From b6a781955eb641ccb1defd742ef053e2bca130b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 20 Apr 2020 16:41:55 +0200 Subject: [PATCH 09/12] :art: Use global imports to simplify exploring with Jupyter --- .../backend/app/app/api/api_v1/endpoints/items.py | 3 +-- .../backend/app/app/api/api_v1/endpoints/login.py | 5 ++--- .../backend/app/app/api/api_v1/endpoints/users.py | 3 +-- .../backend/app/app/api/api_v1/endpoints/utils.py | 3 +-- .../backend/app/app/core/security.py | 2 +- .../backend/app/app/tests/conftest.py | 3 +-- .../backend/app/app/tests/utils/item.py | 4 +--- 7 files changed, 8 insertions(+), 15 deletions(-) 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 index eefeb6179c..f0af0a06a1 100644 --- 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 @@ -4,11 +4,10 @@ from sqlalchemy.orm import Session from app import crud +from app.api import deps from app.models.user import User as DBUser from app.schemas.item import Item, ItemCreate, ItemUpdate -from ... import deps - router = APIRouter() diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py index 0b03267d89..d40971a101 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py @@ -6,6 +6,8 @@ from sqlalchemy.orm import Session from app import crud +from app.api import deps +from app.core import security from app.core.config import settings from app.core.security import get_password_hash from app.models.user import User as DBUser @@ -18,9 +20,6 @@ verify_password_reset_token, ) -from ....core import security -from ... import deps - router = APIRouter() diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py index 35c0b4e4b7..0e44623528 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py @@ -6,13 +6,12 @@ from sqlalchemy.orm import Session from app import crud +from app.api import deps from app.core.config import settings from app.models.user import User as DBUser from app.schemas.user import User, UserCreate, UserUpdate from app.utils import send_new_account_email -from ... import deps - router = APIRouter() 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 098c690aee..de1c6dbe36 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 @@ -3,14 +3,13 @@ from fastapi import APIRouter, Depends from pydantic.networks import EmailStr +from app.api import deps from app.core.celery_app import celery_app from app.models.user import User as DBUser from app.schemas.msg import Msg from app.schemas.user import User # noqa: F401 from app.utils import send_test_email -from ... import deps - router = APIRouter() diff --git a/{{cookiecutter.project_slug}}/backend/app/app/core/security.py b/{{cookiecutter.project_slug}}/backend/app/app/core/security.py index 0424c85434..6c6ee8bc30 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/core/security.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/core/security.py @@ -4,7 +4,7 @@ from jose import jwt from passlib.context import CryptContext -from .config import settings +from app.core.config import settings pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py index 3fd5614c13..37d20956d6 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py @@ -4,11 +4,10 @@ from sqlalchemy.orm import Session from app.core.config import settings +from app.db.session import SessionLocal from app.tests.utils.user import authentication_token_from_email from app.tests.utils.utils import get_server_api, get_superuser_token_headers -from ..db.session import SessionLocal - @pytest.fixture(scope="session") def db() -> Iterator[Session]: diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py index b97d64f2e6..1560ee23ec 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py @@ -2,13 +2,11 @@ from sqlalchemy.orm import Session -from app import crud +from app import crud, models from app.schemas.item import ItemCreate from app.tests.utils.user import create_random_user from app.tests.utils.utils import random_lower_string -from ... import models - def create_random_item(db: Session, *, owner_id: Optional[int] = None) -> models.Item: if owner_id is None: From ce16b99faf0781fa26d015fdc884703efe60b96d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 20 Apr 2020 16:49:55 +0200 Subject: [PATCH 10/12] :recycle: Import schemas and models, instead of each class --- .../app/app/api/api_v1/endpoints/items.py | 28 +++++++------- .../app/app/api/api_v1/endpoints/login.py | 16 +++----- .../app/app/api/api_v1/endpoints/users.py | 38 +++++++++---------- .../app/app/api/api_v1/endpoints/utils.py | 13 +++---- 4 files changed, 43 insertions(+), 52 deletions(-) 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 index f0af0a06a1..ed84c683ea 100644 --- 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 @@ -3,20 +3,18 @@ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session -from app import crud +from app import crud, models, schemas from app.api import deps -from app.models.user import User as DBUser -from app.schemas.item import Item, ItemCreate, ItemUpdate router = APIRouter() -@router.get("/", response_model=List[Item]) +@router.get("/", response_model=List[schemas.Item]) def read_items( db: Session = Depends(deps.get_db), skip: int = 0, limit: int = 100, - current_user: DBUser = Depends(deps.get_current_active_user), + current_user: models.User = Depends(deps.get_current_active_user), ) -> Any: """ Retrieve items. @@ -30,12 +28,12 @@ def read_items( return items -@router.post("/", response_model=Item) +@router.post("/", response_model=schemas.Item) def create_item( *, db: Session = Depends(deps.get_db), - item_in: ItemCreate, - current_user: DBUser = Depends(deps.get_current_active_user), + item_in: schemas.ItemCreate, + current_user: models.User = Depends(deps.get_current_active_user), ) -> Any: """ Create new item. @@ -46,13 +44,13 @@ def create_item( return item -@router.put("/{id}", response_model=Item) +@router.put("/{id}", response_model=schemas.Item) def update_item( *, db: Session = Depends(deps.get_db), id: int, - item_in: ItemUpdate, - current_user: DBUser = Depends(deps.get_current_active_user), + item_in: schemas.ItemUpdate, + current_user: models.User = Depends(deps.get_current_active_user), ) -> Any: """ Update an item. @@ -66,12 +64,12 @@ def update_item( return item -@router.get("/{id}", response_model=Item) +@router.get("/{id}", response_model=schemas.Item) def read_item( *, db: Session = Depends(deps.get_db), id: int, - current_user: DBUser = Depends(deps.get_current_active_user), + current_user: models.User = Depends(deps.get_current_active_user), ) -> Any: """ Get item by ID. @@ -84,12 +82,12 @@ def read_item( return item -@router.delete("/{id}", response_model=Item) +@router.delete("/{id}", response_model=schemas.Item) def delete_item( *, db: Session = Depends(deps.get_db), id: int, - current_user: DBUser = Depends(deps.get_current_active_user), + current_user: models.User = Depends(deps.get_current_active_user), ) -> Any: """ Delete an item. diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py index d40971a101..4dc3a9b248 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py @@ -5,15 +5,11 @@ from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.orm import Session -from app import crud +from app import crud, models, schemas from app.api import deps from app.core import security from app.core.config import settings from app.core.security import get_password_hash -from app.models.user import User as DBUser -from app.schemas.msg import Msg -from app.schemas.token import Token -from app.schemas.user import User from app.utils import ( generate_password_reset_token, send_reset_password_email, @@ -23,7 +19,7 @@ router = APIRouter() -@router.post("/login/access-token", response_model=Token) +@router.post("/login/access-token", response_model=schemas.Token) def login_access_token( db: Session = Depends(deps.get_db), form_data: OAuth2PasswordRequestForm = Depends() ) -> Any: @@ -46,15 +42,15 @@ def login_access_token( } -@router.post("/login/test-token", response_model=User) -def test_token(current_user: DBUser = Depends(deps.get_current_user)) -> Any: +@router.post("/login/test-token", response_model=schemas.User) +def test_token(current_user: models.User = Depends(deps.get_current_user)) -> Any: """ Test access token """ return current_user -@router.post("/password-recovery/{email}", response_model=Msg) +@router.post("/password-recovery/{email}", response_model=schemas.Msg) def recover_password(email: str, db: Session = Depends(deps.get_db)) -> Any: """ Password Recovery @@ -73,7 +69,7 @@ def recover_password(email: str, db: Session = Depends(deps.get_db)) -> Any: return {"msg": "Password recovery email sent"} -@router.post("/reset-password/", response_model=Msg) +@router.post("/reset-password/", response_model=schemas.Msg) def reset_password( token: str = Body(...), new_password: str = Body(...), diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py index 0e44623528..c8f89b63d8 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py @@ -5,22 +5,20 @@ from pydantic.networks import EmailStr from sqlalchemy.orm import Session -from app import crud +from app import crud, models, schemas from app.api import deps from app.core.config import settings -from app.models.user import User as DBUser -from app.schemas.user import User, UserCreate, UserUpdate from app.utils import send_new_account_email router = APIRouter() -@router.get("/", response_model=List[User]) +@router.get("/", response_model=List[schemas.User]) def read_users( db: Session = Depends(deps.get_db), skip: int = 0, limit: int = 100, - current_user: DBUser = Depends(deps.get_current_active_superuser), + current_user: models.User = Depends(deps.get_current_active_superuser), ) -> Any: """ Retrieve users. @@ -29,12 +27,12 @@ def read_users( return users -@router.post("/", response_model=User) +@router.post("/", response_model=schemas.User) def create_user( *, db: Session = Depends(deps.get_db), - user_in: UserCreate, - current_user: DBUser = Depends(deps.get_current_active_superuser), + user_in: schemas.UserCreate, + current_user: models.User = Depends(deps.get_current_active_superuser), ) -> Any: """ Create new user. @@ -53,20 +51,20 @@ def create_user( return user -@router.put("/me", response_model=User) +@router.put("/me", response_model=schemas.User) def update_user_me( *, db: Session = Depends(deps.get_db), password: str = Body(None), full_name: str = Body(None), email: EmailStr = Body(None), - current_user: DBUser = Depends(deps.get_current_active_user), + current_user: models.User = Depends(deps.get_current_active_user), ) -> Any: """ Update own user. """ current_user_data = jsonable_encoder(current_user) - user_in = UserUpdate(**current_user_data) + user_in = schemas.UserUpdate(**current_user_data) if password is not None: user_in.password = password if full_name is not None: @@ -77,10 +75,10 @@ def update_user_me( return user -@router.get("/me", response_model=User) +@router.get("/me", response_model=schemas.User) def read_user_me( db: Session = Depends(deps.get_db), - current_user: DBUser = Depends(deps.get_current_active_user), + current_user: models.User = Depends(deps.get_current_active_user), ) -> Any: """ Get current user. @@ -88,7 +86,7 @@ def read_user_me( return current_user -@router.post("/open", response_model=User) +@router.post("/open", response_model=schemas.User) def create_user_open( *, db: Session = Depends(deps.get_db), @@ -110,15 +108,15 @@ def create_user_open( status_code=400, detail="The user with this username already exists in the system", ) - user_in = UserCreate(password=password, email=email, full_name=full_name) + user_in = schemas.UserCreate(password=password, email=email, full_name=full_name) user = crud.user.create(db, obj_in=user_in) return user -@router.get("/{user_id}", response_model=User) +@router.get("/{user_id}", response_model=schemas.User) def read_user_by_id( user_id: int, - current_user: DBUser = Depends(deps.get_current_active_user), + current_user: models.User = Depends(deps.get_current_active_user), db: Session = Depends(deps.get_db), ) -> Any: """ @@ -134,13 +132,13 @@ def read_user_by_id( return user -@router.put("/{user_id}", response_model=User) +@router.put("/{user_id}", response_model=schemas.User) def update_user( *, db: Session = Depends(deps.get_db), user_id: int, - user_in: UserUpdate, - current_user: DBUser = Depends(deps.get_current_active_superuser), + user_in: schemas.UserUpdate, + current_user: models.User = Depends(deps.get_current_active_superuser), ) -> Any: """ Update a user. 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 de1c6dbe36..71fe68d0ce 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 @@ -3,19 +3,18 @@ from fastapi import APIRouter, Depends from pydantic.networks import EmailStr +from app import models, schemas from app.api import deps from app.core.celery_app import celery_app -from app.models.user import User as DBUser -from app.schemas.msg import Msg -from app.schemas.user import User # noqa: F401 from app.utils import send_test_email router = APIRouter() -@router.post("/test-celery/", response_model=Msg, status_code=201) +@router.post("/test-celery/", response_model=schemas.Msg, status_code=201) def test_celery( - msg: Msg, current_user: DBUser = Depends(deps.get_current_active_superuser) + msg: schemas.Msg, + current_user: models.User = Depends(deps.get_current_active_superuser), ) -> Any: """ Test Celery worker. @@ -24,10 +23,10 @@ def test_celery( return {"msg": "Word received"} -@router.post("/test-email/", response_model=Msg, status_code=201) +@router.post("/test-email/", response_model=schemas.Msg, status_code=201) def test_email( email_to: EmailStr, - current_user: DBUser = Depends(deps.get_current_active_superuser), + current_user: models.User = Depends(deps.get_current_active_superuser), ) -> Any: """ Test emails. From f9826c630b652fa43e488e347e4cf7370aacc357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 20 Apr 2020 17:06:43 +0200 Subject: [PATCH 11/12] :truck: Rename db_session to db for simplicity --- .../app/app/api/api_v1/endpoints/items.py | 16 ++++------ .../backend/app/app/crud/base.py | 32 +++++++++---------- .../backend/app/app/crud/crud_item.py | 12 +++---- .../backend/app/app/crud/crud_user.py | 26 ++++++--------- .../backend/app/app/tests/crud/test_item.py | 16 +++++----- .../backend/app/app/tests/utils/item.py | 2 +- .../backend/app/app/tests/utils/user.py | 2 +- 7 files changed, 49 insertions(+), 57 deletions(-) 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 index ed84c683ea..e88885cd80 100644 --- 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 @@ -23,7 +23,7 @@ def read_items( items = crud.item.get_multi(db, skip=skip, limit=limit) else: items = crud.item.get_multi_by_owner( - db_session=db, owner_id=current_user.id, skip=skip, limit=limit + db=db, owner_id=current_user.id, skip=skip, limit=limit ) return items @@ -38,9 +38,7 @@ def create_item( """ Create new item. """ - item = crud.item.create_with_owner( - db_session=db, obj_in=item_in, owner_id=current_user.id - ) + item = crud.item.create_with_owner(db=db, obj_in=item_in, owner_id=current_user.id) return item @@ -55,12 +53,12 @@ def update_item( """ Update an item. """ - item = crud.item.get(db_session=db, id=id) + item = crud.item.get(db=db, id=id) if not item: raise HTTPException(status_code=404, detail="Item not found") if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id): raise HTTPException(status_code=400, detail="Not enough permissions") - item = crud.item.update(db_session=db, db_obj=item, obj_in=item_in) + item = crud.item.update(db=db, db_obj=item, obj_in=item_in) return item @@ -74,7 +72,7 @@ def read_item( """ Get item by ID. """ - item = crud.item.get(db_session=db, id=id) + item = crud.item.get(db=db, id=id) if not item: raise HTTPException(status_code=404, detail="Item not found") if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id): @@ -92,10 +90,10 @@ def delete_item( """ Delete an item. """ - item = crud.item.get(db_session=db, id=id) + item = crud.item.get(db=db, id=id) if not item: raise HTTPException(status_code=404, detail="Item not found") if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id): raise HTTPException(status_code=400, detail="Not enough permissions") - item = crud.item.remove(db_session=db, id=id) + item = crud.item.remove(db=db, id=id) return item diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/base.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/base.py index 014d425410..2b6f1f10cd 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/crud/base.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/crud/base.py @@ -23,25 +23,25 @@ def __init__(self, model: Type[ModelType]): """ self.model = model - def get(self, db_session: Session, id: Any) -> Optional[ModelType]: - return db_session.query(self.model).filter(self.model.id == id).first() + def get(self, db: Session, id: Any) -> Optional[ModelType]: + return db.query(self.model).filter(self.model.id == id).first() def get_multi( - self, db_session: Session, *, skip: int = 0, limit: int = 100 + self, db: Session, *, skip: int = 0, limit: int = 100 ) -> List[ModelType]: - return db_session.query(self.model).offset(skip).limit(limit).all() + return db.query(self.model).offset(skip).limit(limit).all() - def create(self, db_session: Session, *, obj_in: CreateSchemaType) -> ModelType: + def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType: obj_in_data = jsonable_encoder(obj_in) db_obj = self.model(**obj_in_data) # type: ignore - db_session.add(db_obj) - db_session.commit() - db_session.refresh(db_obj) + db.add(db_obj) + db.commit() + db.refresh(db_obj) return db_obj def update( self, - db_session: Session, + db: Session, *, db_obj: ModelType, obj_in: Union[UpdateSchemaType, Dict[str, Any]] @@ -54,13 +54,13 @@ def update( for field in obj_data: if field in update_data: setattr(db_obj, field, update_data[field]) - db_session.add(db_obj) - db_session.commit() - db_session.refresh(db_obj) + db.add(db_obj) + db.commit() + db.refresh(db_obj) return db_obj - def remove(self, db_session: Session, *, id: int) -> ModelType: - obj = db_session.query(self.model).get(id) - db_session.delete(obj) - db_session.commit() + def remove(self, db: Session, *, id: int) -> ModelType: + obj = db.query(self.model).get(id) + db.delete(obj) + db.commit() return obj diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_item.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_item.py index 5138eddbae..dcb87cdc5c 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_item.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_item.py @@ -10,20 +10,20 @@ class CRUDItem(CRUDBase[Item, ItemCreate, ItemUpdate]): def create_with_owner( - self, db_session: Session, *, obj_in: ItemCreate, owner_id: int + self, db: Session, *, obj_in: ItemCreate, owner_id: int ) -> Item: obj_in_data = jsonable_encoder(obj_in) db_obj = self.model(**obj_in_data, owner_id=owner_id) - db_session.add(db_obj) - db_session.commit() - db_session.refresh(db_obj) + db.add(db_obj) + db.commit() + db.refresh(db_obj) return db_obj def get_multi_by_owner( - self, db_session: Session, *, owner_id: int, skip: int = 0, limit: int = 100 + self, db: Session, *, owner_id: int, skip: int = 0, limit: int = 100 ) -> List[Item]: return ( - db_session.query(self.model) + db.query(self.model) .filter(Item.owner_id == owner_id) .offset(skip) .limit(limit) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_user.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_user.py index ab3d638527..14525d326f 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_user.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_user.py @@ -9,27 +9,23 @@ class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): - def get_by_email(self, db_session: Session, *, email: str) -> Optional[User]: - return db_session.query(User).filter(User.email == email).first() + def get_by_email(self, db: Session, *, email: str) -> Optional[User]: + return db.query(User).filter(User.email == email).first() - def create(self, db_session: Session, *, obj_in: UserCreate) -> User: + def create(self, db: Session, *, obj_in: UserCreate) -> User: db_obj = User( email=obj_in.email, hashed_password=get_password_hash(obj_in.password), full_name=obj_in.full_name, is_superuser=obj_in.is_superuser, ) - db_session.add(db_obj) - db_session.commit() - db_session.refresh(db_obj) + db.add(db_obj) + db.commit() + db.refresh(db_obj) return db_obj def update( - self, - db_session: Session, - *, - db_obj: User, - obj_in: Union[UserUpdate, Dict[str, Any]] + self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]] ) -> User: if isinstance(obj_in, dict): update_data = obj_in @@ -39,12 +35,10 @@ def update( hashed_password = get_password_hash(update_data["password"]) del update_data["password"] update_data["hashed_password"] = hashed_password - return super().update(db_session, db_obj=db_obj, obj_in=update_data) + return super().update(db, db_obj=db_obj, obj_in=update_data) - def authenticate( - self, db_session: Session, *, email: str, password: str - ) -> Optional[User]: - user = self.get_by_email(db_session, email=email) + def authenticate(self, db: Session, *, email: str, password: str) -> Optional[User]: + user = self.get_by_email(db, email=email) if not user: return None if not verify_password(password, user.hashed_password): 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 index 79467a6f1d..e529144ef6 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_item.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_item.py @@ -11,7 +11,7 @@ def test_create_item(db: Session) -> None: description = random_lower_string() item_in = ItemCreate(title=title, description=description) user = create_random_user(db) - item = crud.item.create_with_owner(db_session=db, obj_in=item_in, owner_id=user.id) + item = crud.item.create_with_owner(db=db, obj_in=item_in, owner_id=user.id) assert item.title == title assert item.description == description assert item.owner_id == user.id @@ -22,8 +22,8 @@ def test_get_item(db: Session) -> None: description = random_lower_string() item_in = ItemCreate(title=title, description=description) user = create_random_user(db) - item = crud.item.create_with_owner(db_session=db, obj_in=item_in, owner_id=user.id) - stored_item = crud.item.get(db_session=db, id=item.id) + item = crud.item.create_with_owner(db=db, obj_in=item_in, owner_id=user.id) + stored_item = crud.item.get(db=db, id=item.id) assert stored_item assert item.id == stored_item.id assert item.title == stored_item.title @@ -36,10 +36,10 @@ def test_update_item(db: Session) -> None: description = random_lower_string() item_in = ItemCreate(title=title, description=description) user = create_random_user(db) - item = crud.item.create_with_owner(db_session=db, obj_in=item_in, owner_id=user.id) + item = crud.item.create_with_owner(db=db, obj_in=item_in, owner_id=user.id) description2 = random_lower_string() item_update = ItemUpdate(description=description2) - item2 = crud.item.update(db_session=db, db_obj=item, obj_in=item_update) + item2 = crud.item.update(db=db, db_obj=item, obj_in=item_update) assert item.id == item2.id assert item.title == item2.title assert item2.description == description2 @@ -51,9 +51,9 @@ def test_delete_item(db: Session) -> None: description = random_lower_string() item_in = ItemCreate(title=title, description=description) user = create_random_user(db) - item = crud.item.create_with_owner(db_session=db, obj_in=item_in, owner_id=user.id) - item2 = crud.item.remove(db_session=db, id=item.id) - item3 = crud.item.get(db_session=db, id=item.id) + item = crud.item.create_with_owner(db=db, obj_in=item_in, owner_id=user.id) + item2 = crud.item.remove(db=db, id=item.id) + item3 = crud.item.get(db=db, id=item.id) assert item3 is None assert item2.id == item.id assert item2.title == title diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py index 1560ee23ec..e28f967078 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py @@ -15,4 +15,4 @@ def create_random_item(db: Session, *, owner_id: Optional[int] = None) -> models title = random_lower_string() description = random_lower_string() item_in = ItemCreate(title=title, description=description, id=id) - return crud.item.create_with_owner(db_session=db, obj_in=item_in, owner_id=owner_id) + return crud.item.create_with_owner(db=db, obj_in=item_in, owner_id=owner_id) 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 fdd655ae68..e45d787cbb 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/user.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/user.py @@ -28,7 +28,7 @@ def create_random_user(db: Session) -> User: email = random_email() password = random_lower_string() user_in = UserCreate(username=email, email=email, password=password) - user = crud.user.create(db_session=db, obj_in=user_in) + user = crud.user.create(db=db, obj_in=user_in) return user From 00cbd9064753c35bdd5baedd4bae9de99706998a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 20 Apr 2020 18:53:26 +0200 Subject: [PATCH 12/12] :pushpin: Update dependencies installation for testing --- .../backend/app/pyproject.toml | 10 ++++------ .../backend/backend.dockerfile | 13 ++++++------- .../backend/celeryworker.dockerfile | 10 ++++++---- .../docker-compose.override.yml | 6 ++++-- {{cookiecutter.project_slug}}/docker-compose.yml | 4 ++++ {{cookiecutter.project_slug}}/scripts/build-push.sh | 2 +- {{cookiecutter.project_slug}}/scripts/test.sh | 1 + 7 files changed, 26 insertions(+), 20 deletions(-) diff --git a/{{cookiecutter.project_slug}}/backend/app/pyproject.toml b/{{cookiecutter.project_slug}}/backend/app/pyproject.toml index 35d86bccaf..ea4e04e198 100644 --- a/{{cookiecutter.project_slug}}/backend/app/pyproject.toml +++ b/{{cookiecutter.project_slug}}/backend/app/pyproject.toml @@ -32,17 +32,15 @@ isort = "^4.3.21" autoflake = "^1.3.1" flake8 = "^3.7.9" pytest = "^5.4.1" -jupyter = "^1.0.0" -vulture = "^1.4" sqlalchemy-stubs = "^0.3" pytest-cov = "^2.8.1" -[build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" - [tool.isort] multi_line_output = 3 include_trailing_comma = true force_grid_wrap = 0 line_length = 88 +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" + diff --git a/{{cookiecutter.project_slug}}/backend/backend.dockerfile b/{{cookiecutter.project_slug}}/backend/backend.dockerfile index 6108868ead..8c39c502af 100644 --- a/{{cookiecutter.project_slug}}/backend/backend.dockerfile +++ b/{{cookiecutter.project_slug}}/backend/backend.dockerfile @@ -10,17 +10,16 @@ RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get- # Copy poetry.lock* in case it doesn't exist in the repo COPY ./app/pyproject.toml ./app/poetry.lock* /app/ -RUN poetry install --no-dev --no-root + +# Allow installing dev dependencies to run tests +ARG INSTALL_DEV=false +RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install --no-root ; else poetry install --no-root --no-dev ; fi" # For development, Jupyter remote kernel, Hydrogen # Using inside the container: # jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888 -ARG env=prod -RUN bash -c "if [ $env == 'dev' ] ; then pip install jupyterlab ; fi" -EXPOSE 8888 +ARG INSTALL_JUPYTER=false +RUN bash -c "if [ $INSTALL_JUPYTER == 'true' ] ; then pip install jupyterlab ; fi" COPY ./app /app - ENV PYTHONPATH=/app - -EXPOSE 80 diff --git a/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile b/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile index 6de9b7e908..4695a7b7ec 100644 --- a/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile +++ b/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile @@ -10,14 +10,16 @@ RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get- # Copy poetry.lock* in case it doesn't exist in the repo COPY ./app/pyproject.toml ./app/poetry.lock* /app/ -RUN poetry install --no-dev --no-root + +# Allow installing dev dependencies to run tests +ARG INSTALL_DEV=false +RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install --no-root ; else poetry install --no-root --no-dev ; fi" # For development, Jupyter remote kernel, Hydrogen # Using inside the container: # jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888 -ARG env=prod -RUN bash -c "if [ $env == 'dev' ] ; then pip install jupyterlab ; fi" -EXPOSE 8888 +ARG INSTALL_JUPYTER=false +RUN bash -c "if [ $INSTALL_JUPYTER == 'true' ] ; then pip install jupyterlab ; fi" ENV C_FORCE_ROOT=1 diff --git a/{{cookiecutter.project_slug}}/docker-compose.override.yml b/{{cookiecutter.project_slug}}/docker-compose.override.yml index 931cb2a189..9683d4ba19 100644 --- a/{{cookiecutter.project_slug}}/docker-compose.override.yml +++ b/{{cookiecutter.project_slug}}/docker-compose.override.yml @@ -37,7 +37,8 @@ services: context: ./backend dockerfile: backend.dockerfile args: - env: dev + INSTALL_DEV: ${INSTALL_DEV-true} + INSTALL_JUPYTER: ${INSTALL_JUPYTER-true} # command: bash -c "while true; do sleep 1; done" # Infinite loop to keep container live doing nothing command: /start-reload.sh labels: @@ -57,7 +58,8 @@ services: context: ./backend dockerfile: celeryworker.dockerfile args: - env: dev + INSTALL_DEV: ${INSTALL_DEV-true} + INSTALL_JUPYTER: ${INSTALL_JUPYTER-true} frontend: build: diff --git a/{{cookiecutter.project_slug}}/docker-compose.yml b/{{cookiecutter.project_slug}}/docker-compose.yml index 8bdf3db11d..f15d0c629e 100644 --- a/{{cookiecutter.project_slug}}/docker-compose.yml +++ b/{{cookiecutter.project_slug}}/docker-compose.yml @@ -115,6 +115,8 @@ services: build: context: ./backend dockerfile: backend.dockerfile + args: + INSTALL_DEV: ${INSTALL_DEV-false} deploy: labels: - traefik.frontend.rule=PathPrefix:/api,/docs,/redoc @@ -137,6 +139,8 @@ services: build: context: ./backend dockerfile: celeryworker.dockerfile + args: + INSTALL_DEV: ${INSTALL_DEV-false} frontend: image: '${DOCKER_IMAGE_FRONTEND}:${TAG-latest}' diff --git a/{{cookiecutter.project_slug}}/scripts/build-push.sh b/{{cookiecutter.project_slug}}/scripts/build-push.sh index 425703de89..fce8e34f28 100644 --- a/{{cookiecutter.project_slug}}/scripts/build-push.sh +++ b/{{cookiecutter.project_slug}}/scripts/build-push.sh @@ -5,6 +5,6 @@ set -e TAG=${TAG} \ FRONTEND_ENV=${FRONTEND_ENV-production} \ -. ./scripts/build.sh +sh ./scripts/build.sh docker-compose -f docker-compose.yml push diff --git a/{{cookiecutter.project_slug}}/scripts/test.sh b/{{cookiecutter.project_slug}}/scripts/test.sh index 78b4db90e0..ef63b5e4d0 100644 --- a/{{cookiecutter.project_slug}}/scripts/test.sh +++ b/{{cookiecutter.project_slug}}/scripts/test.sh @@ -6,6 +6,7 @@ set -e DOMAIN=backend \ SMTP_HOST="" \ TRAEFIK_PUBLIC_NETWORK_IS_EXTERNAL=false \ +INSTALL_DEV=true \ docker-compose \ -f docker-compose.yml \ config > docker-stack.yml