diff --git a/{{cookiecutter.project_slug}}/backend/app/alembic/env.py b/{{cookiecutter.project_slug}}/backend/app/alembic/env.py index 3ba3420643..f646954cb9 100755 --- a/{{cookiecutter.project_slug}}/backend/app/alembic/env.py +++ b/{{cookiecutter.project_slug}}/backend/app/alembic/env.py @@ -1,9 +1,11 @@ from __future__ import with_statement +import asyncio import os from alembic import context from sqlalchemy import engine_from_config, pool +from sqlalchemy.ext.asyncio import AsyncEngine from logging.config import fileConfig # this is the Alembic Config object, which provides @@ -35,7 +37,7 @@ def get_url(): password = os.getenv("POSTGRES_PASSWORD", "") server = os.getenv("POSTGRES_SERVER", "db") db = os.getenv("POSTGRES_DB", "app") - return f"postgresql://{user}:{password}@{server}/{db}" + return f"postgresql+asyncpg://{user}:{password}@{server}/{db}" def run_migrations_offline(): @@ -59,7 +61,16 @@ def run_migrations_offline(): context.run_migrations() -def run_migrations_online(): +def do_run_migrations(connection): + context.configure( + connection=connection, target_metadata=target_metadata, compare_type=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_migrations_online(): """Run migrations in 'online' mode. In this scenario we need to create an Engine @@ -68,20 +79,17 @@ def run_migrations_online(): """ configuration = config.get_section(config.config_ini_section) configuration["sqlalchemy.url"] = get_url() - connectable = engine_from_config( - configuration, prefix="sqlalchemy.", poolclass=pool.NullPool, - ) - - with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata, compare_type=True + connectable = AsyncEngine( + engine_from_config( + configuration, prefix="sqlalchemy.", poolclass=pool.NullPool, future=True, ) + ) - with context.begin_transaction(): - context.run_migrations() + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) if context.is_offline_mode(): run_migrations_offline() else: - run_migrations_online() + asyncio.run(run_migrations_online()) \ No newline at end of file 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 e88885cd80..216df8afda 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,7 +1,7 @@ from typing import Any, List from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from app import crud, models, schemas from app.api import deps @@ -10,8 +10,8 @@ @router.get("/", response_model=List[schemas.Item]) -def read_items( - db: Session = Depends(deps.get_db), +async def read_items( + db: AsyncSession = Depends(deps.get_db), skip: int = 0, limit: int = 100, current_user: models.User = Depends(deps.get_current_active_user), @@ -20,32 +20,34 @@ def read_items( Retrieve items. """ if crud.user.is_superuser(current_user): - items = crud.item.get_multi(db, skip=skip, limit=limit) + items = await crud.item.get_multi(db, skip=skip, limit=limit) else: - items = crud.item.get_multi_by_owner( + items = await crud.item.get_multi_by_owner( db=db, owner_id=current_user.id, skip=skip, limit=limit ) return items @router.post("/", response_model=schemas.Item) -def create_item( +async def create_item( *, - db: Session = Depends(deps.get_db), + db: AsyncSession = Depends(deps.get_db), item_in: schemas.ItemCreate, current_user: models.User = Depends(deps.get_current_active_user), ) -> Any: """ Create new item. """ - item = crud.item.create_with_owner(db=db, obj_in=item_in, owner_id=current_user.id) + item = await crud.item.create_with_owner( + db=db, obj_in=item_in, owner_id=current_user.id + ) return item @router.put("/{id}", response_model=schemas.Item) -def update_item( +async def update_item( *, - db: Session = Depends(deps.get_db), + db: AsyncSession = Depends(deps.get_db), id: int, item_in: schemas.ItemUpdate, current_user: models.User = Depends(deps.get_current_active_user), @@ -53,26 +55,26 @@ def update_item( """ Update an item. """ - item = crud.item.get(db=db, id=id) + item = await 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=db, db_obj=item, obj_in=item_in) + item = await crud.item.update(db=db, db_obj=item, obj_in=item_in) return item @router.get("/{id}", response_model=schemas.Item) -def read_item( +async def read_item( *, - db: Session = Depends(deps.get_db), + db: AsyncSession = Depends(deps.get_db), id: int, current_user: models.User = Depends(deps.get_current_active_user), ) -> Any: """ Get item by ID. """ - item = crud.item.get(db=db, id=id) + item = await 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): @@ -81,19 +83,19 @@ def read_item( @router.delete("/{id}", response_model=schemas.Item) -def delete_item( +async def delete_item( *, - db: Session = Depends(deps.get_db), + db: AsyncSession = Depends(deps.get_db), id: int, current_user: models.User = Depends(deps.get_current_active_user), ) -> Any: """ Delete an item. """ - item = crud.item.get(db=db, id=id) + item = await 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=db, id=id) - return item + item = await crud.item.remove(db=db, id=id) + return item \ No newline at end of file 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 4dc3a9b248..f9d4dcb4ed 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 @@ -3,7 +3,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException from fastapi.security import OAuth2PasswordRequestForm -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from app import crud, models, schemas from app.api import deps @@ -20,13 +20,14 @@ @router.post("/login/access-token", response_model=schemas.Token) -def login_access_token( - db: Session = Depends(deps.get_db), form_data: OAuth2PasswordRequestForm = Depends() +async def login_access_token( + db: AsyncSession = Depends(deps.get_db), + form_data: OAuth2PasswordRequestForm = Depends(), ) -> Any: """ OAuth2 compatible token login, get an access token for future requests """ - user = crud.user.authenticate( + user = await crud.user.authenticate( db, email=form_data.username, password=form_data.password ) if not user: @@ -43,7 +44,7 @@ def login_access_token( @router.post("/login/test-token", response_model=schemas.User) -def test_token(current_user: models.User = Depends(deps.get_current_user)) -> Any: +async def test_token(current_user: models.User = Depends(deps.get_current_user)) -> Any: """ Test access token """ @@ -51,11 +52,11 @@ def test_token(current_user: models.User = Depends(deps.get_current_user)) -> An @router.post("/password-recovery/{email}", response_model=schemas.Msg) -def recover_password(email: str, db: Session = Depends(deps.get_db)) -> Any: +async def recover_password(email: str, db: AsyncSession = Depends(deps.get_db)) -> Any: """ Password Recovery """ - user = crud.user.get_by_email(db, email=email) + user = await crud.user.get_by_email(db, email=email) if not user: raise HTTPException( @@ -70,10 +71,10 @@ def recover_password(email: str, db: Session = Depends(deps.get_db)) -> Any: @router.post("/reset-password/", response_model=schemas.Msg) -def reset_password( +async def reset_password( token: str = Body(...), new_password: str = Body(...), - db: Session = Depends(deps.get_db), + db: AsyncSession = Depends(deps.get_db), ) -> Any: """ Reset password @@ -81,7 +82,7 @@ def reset_password( email = verify_password_reset_token(token) if not email: raise HTTPException(status_code=400, detail="Invalid token") - user = crud.user.get_by_email(db, email=email) + user = await crud.user.get_by_email(db, email=email) if not user: raise HTTPException( status_code=404, @@ -92,5 +93,5 @@ def reset_password( hashed_password = get_password_hash(new_password) user.hashed_password = hashed_password db.add(user) - db.commit() - return {"msg": "Password updated successfully"} + await db.commit() + return {"msg": "Password updated successfully"} \ No newline at end of file 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 c8f89b63d8..a1d5e83d55 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 @@ -3,7 +3,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException from fastapi.encoders import jsonable_encoder from pydantic.networks import EmailStr -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from app import crud, models, schemas from app.api import deps @@ -14,8 +14,8 @@ @router.get("/", response_model=List[schemas.User]) -def read_users( - db: Session = Depends(deps.get_db), +async def read_users( + db: AsyncSession = Depends(deps.get_db), skip: int = 0, limit: int = 100, current_user: models.User = Depends(deps.get_current_active_superuser), @@ -23,27 +23,27 @@ def read_users( """ Retrieve users. """ - users = crud.user.get_multi(db, skip=skip, limit=limit) + users = await crud.user.get_multi(db, skip=skip, limit=limit) return users @router.post("/", response_model=schemas.User) -def create_user( +async def create_user( *, - db: Session = Depends(deps.get_db), + db: AsyncSession = Depends(deps.get_db), user_in: schemas.UserCreate, current_user: models.User = Depends(deps.get_current_active_superuser), ) -> Any: """ Create new user. """ - user = crud.user.get_by_email(db, email=user_in.email) + user = await crud.user.get_by_email(db, email=user_in.email) if user: raise HTTPException( status_code=400, detail="The user with this username already exists in the system.", ) - user = crud.user.create(db, obj_in=user_in) + user = await crud.user.create(db, obj_in=user_in) if settings.EMAILS_ENABLED and user_in.email: send_new_account_email( email_to=user_in.email, username=user_in.email, password=user_in.password @@ -52,9 +52,9 @@ def create_user( @router.put("/me", response_model=schemas.User) -def update_user_me( +async def update_user_me( *, - db: Session = Depends(deps.get_db), + db: AsyncSession = Depends(deps.get_db), password: str = Body(None), full_name: str = Body(None), email: EmailStr = Body(None), @@ -71,13 +71,13 @@ def update_user_me( user_in.full_name = full_name if email is not None: user_in.email = email - user = crud.user.update(db, db_obj=current_user, obj_in=user_in) + user = await crud.user.update(db, db_obj=current_user, obj_in=user_in) return user @router.get("/me", response_model=schemas.User) -def read_user_me( - db: Session = Depends(deps.get_db), +async def read_user_me( + db: AsyncSession = Depends(deps.get_db), current_user: models.User = Depends(deps.get_current_active_user), ) -> Any: """ @@ -87,9 +87,9 @@ def read_user_me( @router.post("/open", response_model=schemas.User) -def create_user_open( +async def create_user_open( *, - db: Session = Depends(deps.get_db), + db: AsyncSession = Depends(deps.get_db), password: str = Body(...), email: EmailStr = Body(...), full_name: str = Body(None), @@ -102,27 +102,27 @@ def create_user_open( status_code=403, detail="Open user registration is forbidden on this server", ) - user = crud.user.get_by_email(db, email=email) + user = await crud.user.get_by_email(db, email=email) if user: raise HTTPException( status_code=400, detail="The user with this username already exists in the system", ) user_in = schemas.UserCreate(password=password, email=email, full_name=full_name) - user = crud.user.create(db, obj_in=user_in) + user = await crud.user.create(db, obj_in=user_in) return user @router.get("/{user_id}", response_model=schemas.User) -def read_user_by_id( +async def read_user_by_id( user_id: int, current_user: models.User = Depends(deps.get_current_active_user), - db: Session = Depends(deps.get_db), + db: AsyncSession = Depends(deps.get_db), ) -> Any: """ Get a specific user by id. """ - user = crud.user.get(db, id=user_id) + user = await crud.user.get(db, id=user_id) if user == current_user: return user if not crud.user.is_superuser(current_user): @@ -133,9 +133,9 @@ def read_user_by_id( @router.put("/{user_id}", response_model=schemas.User) -def update_user( +async def update_user( *, - db: Session = Depends(deps.get_db), + db: AsyncSession = Depends(deps.get_db), user_id: int, user_in: schemas.UserUpdate, current_user: models.User = Depends(deps.get_current_active_superuser), @@ -143,11 +143,11 @@ def update_user( """ Update a user. """ - user = crud.user.get(db, id=user_id) + user = await crud.user.get(db, id=user_id) if not user: raise HTTPException( status_code=404, detail="The user with this username does not exist in the system", ) - user = crud.user.update(db, db_obj=user, obj_in=user_in) - return user + user = await crud.user.update(db, db_obj=user, obj_in=user_in) + return user \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/deps.py b/{{cookiecutter.project_slug}}/backend/app/app/api/deps.py index a0109afe84..e134d800c9 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/deps.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/api/deps.py @@ -4,28 +4,25 @@ from fastapi.security import OAuth2PasswordBearer from jose import jwt from pydantic import ValidationError -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from app import crud, models, schemas from app.core import security from app.core.config import settings -from app.db.session import SessionLocal +from app.db.session import async_session reusable_oauth2 = OAuth2PasswordBearer( tokenUrl=f"{settings.API_V1_STR}/login/access-token" ) -def get_db() -> Generator: - try: - db = SessionLocal() - yield db - finally: - db.close() +async def get_db() -> Generator: + async with async_session() as session: + yield session -def get_current_user( - db: Session = Depends(get_db), token: str = Depends(reusable_oauth2) +async def get_current_user( + db: AsyncSession = Depends(get_db), token: str = Depends(reusable_oauth2) ) -> models.User: try: payload = jwt.decode( @@ -37,7 +34,7 @@ def get_current_user( status_code=status.HTTP_403_FORBIDDEN, detail="Could not validate credentials", ) - user = crud.user.get(db, id=token_data.sub) + user = await crud.user.get(db, id=token_data.sub) if not user: raise HTTPException(status_code=404, detail="User not found") return user @@ -58,4 +55,4 @@ def get_current_active_superuser( raise HTTPException( status_code=400, detail="The user doesn't have enough privileges" ) - return current_user + return current_user \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/backend/app/app/core/config.py b/{{cookiecutter.project_slug}}/backend/app/app/core/config.py index 8b07276dac..823275151b 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/core/config.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/core/config.py @@ -4,6 +4,10 @@ from pydantic import AnyHttpUrl, BaseSettings, EmailStr, HttpUrl, PostgresDsn, validator +class AsyncPostgresDsn(PostgresDsn): + allowed_schemes = {"postgres+asyncpg", "postgresql+asyncpg"} + + class Settings(BaseSettings): API_V1_STR: str = "/api/v1" SECRET_KEY: str = secrets.token_urlsafe(32) @@ -37,14 +41,14 @@ def sentry_dsn_can_be_blank(cls, v: str) -> Optional[str]: POSTGRES_USER: str POSTGRES_PASSWORD: str POSTGRES_DB: str - SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None + SQLALCHEMY_DATABASE_URI: Optional[AsyncPostgresDsn] = None @validator("SQLALCHEMY_DATABASE_URI", pre=True) def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any: if isinstance(v, str): return v - return PostgresDsn.build( - scheme="postgresql", + return AsyncPostgresDsn.build( + scheme="postgresql+asyncpg", user=values.get("POSTGRES_USER"), password=values.get("POSTGRES_PASSWORD"), host=values.get("POSTGRES_SERVER"), @@ -86,4 +90,4 @@ class Config: case_sensitive = True -settings = Settings() +settings = Settings() \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/base.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/base.py index 2b6f1f10cd..3452103091 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/crud/base.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/crud/base.py @@ -2,7 +2,8 @@ from fastapi.encoders import jsonable_encoder from pydantic import BaseModel -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select from app.db.base_class import Base @@ -23,25 +24,27 @@ def __init__(self, model: Type[ModelType]): """ self.model = model - def get(self, db: Session, id: Any) -> Optional[ModelType]: - return db.query(self.model).filter(self.model.id == id).first() + async def get(self, db: AsyncSession, id: Any) -> Optional[ModelType]: + result = await db.execute(select(self.model).filter(self.model.id == id)) + return result.scalars().first() - def get_multi( - self, db: Session, *, skip: int = 0, limit: int = 100 + async def get_multi( + self, db: AsyncSession, *, skip: int = 0, limit: int = 100 ) -> List[ModelType]: - return db.query(self.model).offset(skip).limit(limit).all() + result = await db.execute(select(self.model).offset(skip).limit(limit)) + return result.scalars().all() - def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType: + async def create(self, db: AsyncSession, *, obj_in: CreateSchemaType) -> ModelType: obj_in_data = jsonable_encoder(obj_in) db_obj = self.model(**obj_in_data) # type: ignore db.add(db_obj) - db.commit() - db.refresh(db_obj) + await db.commit() + await db.refresh(db_obj) return db_obj - def update( + async def update( self, - db: Session, + db: AsyncSession, *, db_obj: ModelType, obj_in: Union[UpdateSchemaType, Dict[str, Any]] @@ -55,12 +58,12 @@ def update( if field in update_data: setattr(db_obj, field, update_data[field]) db.add(db_obj) - db.commit() - db.refresh(db_obj) + await db.commit() + await db.refresh(db_obj) return db_obj - def remove(self, db: Session, *, id: int) -> ModelType: - obj = db.query(self.model).get(id) - db.delete(obj) - db.commit() - return obj + async def remove(self, db: AsyncSession, *, id: int) -> ModelType: + obj = await db.get(self.model, id) + await db.delete(obj) + await db.commit() + return obj \ No newline at end of file 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 dcb87cdc5c..780f1b47d4 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_item.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_item.py @@ -1,7 +1,9 @@ from typing import List +from app.api.api_v1.endpoints.login import reset_password from fastapi.encoders import jsonable_encoder -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select from app.crud.base import CRUDBase from app.models.item import Item @@ -9,26 +11,26 @@ class CRUDItem(CRUDBase[Item, ItemCreate, ItemUpdate]): - def create_with_owner( - self, db: Session, *, obj_in: ItemCreate, owner_id: int + async def create_with_owner( + self, db: AsyncSession, *, 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.add(db_obj) - db.commit() - db.refresh(db_obj) + await db.commit() + await db.refresh(db_obj) return db_obj - def get_multi_by_owner( - self, db: Session, *, owner_id: int, skip: int = 0, limit: int = 100 + async def get_multi_by_owner( + self, db: AsyncSession, *, owner_id: int, skip: int = 0, limit: int = 100 ) -> List[Item]: - return ( - db.query(self.model) + result = await db.execute( + select(self.model) .filter(Item.owner_id == owner_id) .offset(skip) .limit(limit) - .all() ) + return result.scalars().all() -item = CRUDItem(Item) +item = CRUDItem(Item) \ No newline at end of file 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 14525d326f..02c3e89275 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_user.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_user.py @@ -1,6 +1,7 @@ from typing import Any, Dict, Optional, Union -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select from app.core.security import get_password_hash, verify_password from app.crud.base import CRUDBase @@ -9,10 +10,11 @@ class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): - def get_by_email(self, db: Session, *, email: str) -> Optional[User]: - return db.query(User).filter(User.email == email).first() + async def get_by_email(self, db: AsyncSession, *, email: str) -> Optional[User]: + result = await db.execute(select(User).filter(User.email == email)) + return result.scalars().first() - def create(self, db: Session, *, obj_in: UserCreate) -> User: + async def create(self, db: AsyncSession, *, obj_in: UserCreate) -> User: db_obj = User( email=obj_in.email, hashed_password=get_password_hash(obj_in.password), @@ -20,12 +22,16 @@ def create(self, db: Session, *, obj_in: UserCreate) -> User: is_superuser=obj_in.is_superuser, ) db.add(db_obj) - db.commit() - db.refresh(db_obj) + await db.commit() + await db.refresh(db_obj) return db_obj - def update( - self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]] + async def update( + self, + db: AsyncSession, + *, + db_obj: User, + obj_in: Union[UserUpdate, Dict[str, Any]] ) -> User: if isinstance(obj_in, dict): update_data = obj_in @@ -35,10 +41,12 @@ def update( hashed_password = get_password_hash(update_data["password"]) del update_data["password"] update_data["hashed_password"] = hashed_password - return super().update(db, db_obj=db_obj, obj_in=update_data) + return await super().update(db, db_obj=db_obj, obj_in=update_data) - def authenticate(self, db: Session, *, email: str, password: str) -> Optional[User]: - user = self.get_by_email(db, email=email) + async def authenticate( + self, db: AsyncSession, *, email: str, password: str + ) -> Optional[User]: + user = await self.get_by_email(db, email=email) if not user: return None if not verify_password(password, user.hashed_password): @@ -52,4 +60,4 @@ def is_superuser(self, user: User) -> bool: return user.is_superuser -user = CRUDUser(User) +user = CRUDUser(User) \ No newline at end of file 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 bc1dd2e06c..dbceaa0dfa 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py @@ -1,4 +1,4 @@ -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from app import crud, schemas from app.core.config import settings @@ -9,17 +9,17 @@ # for more details: https://github.com/tiangolo/full-stack-fastapi-postgresql/issues/28 -def init_db(db: Session) -> None: +async def init_db(db: AsyncSession) -> 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, email=settings.FIRST_SUPERUSER) + user = await crud.user.get_by_email(db, email=settings.FIRST_SUPERUSER) if not user: user_in = schemas.UserCreate( email=settings.FIRST_SUPERUSER, password=settings.FIRST_SUPERUSER_PASSWORD, is_superuser=True, ) - user = crud.user.create(db, obj_in=user_in) # noqa: F841 + user = await crud.user.create(db, obj_in=user_in) # noqa: F841 \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/backend/app/app/db/session.py b/{{cookiecutter.project_slug}}/backend/app/app/db/session.py index 9edb2fa1d0..5f77fbad7f 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/db/session.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/db/session.py @@ -1,7 +1,13 @@ -from sqlalchemy import create_engine +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker from app.core.config import settings -engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +engine = create_async_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True) +async_session = sessionmaker( + bind=engine, + class_=AsyncSession, + autocommit=False, + autoflush=False, + expire_on_commit=False, +) \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/backend/app/app/initial_data.py b/{{cookiecutter.project_slug}}/backend/app/app/initial_data.py index c50646d2df..ec204e96ab 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/initial_data.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/initial_data.py @@ -1,22 +1,23 @@ +import asyncio import logging from app.db.init_db import init_db -from app.db.session import SessionLocal +from app.db.session import async_session logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -def init() -> None: - db = SessionLocal() - init_db(db) +async def init() -> None: + async with async_session() as db: + await init_db(db) def main() -> None: logger.info("Creating initial data") - init() + asyncio.run(init()) logger.info("Initial data created") if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_celery.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_celery.py index 7b10a331f2..f989addf8b 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,18 +1,22 @@ from typing import Dict -from fastapi.testclient import TestClient +import pytest +from httpx import AsyncClient from app.core.config import settings +pytestmark = pytest.mark.asyncio -def test_celery_worker_test( - client: TestClient, superuser_token_headers: Dict[str, str] + +@pytest.mark.skip +async def test_celery_worker_test( + client: AsyncClient, superuser_token_headers: Dict[str, str] ) -> None: data = {"msg": "test"} - r = client.post( + r = await client.post( f"{settings.API_V1_STR}/utils/test-celery/", json=data, headers=superuser_token_headers, ) response = r.json() - assert response["msg"] == "Word received" + assert response["msg"] == "Word received" \ No newline at end of file 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 6d799b62d4..7e135933af 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,15 +1,18 @@ -from fastapi.testclient import TestClient -from sqlalchemy.orm import Session +import pytest +from httpx import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.tests.utils.item import create_random_item +pytestmark = pytest.mark.asyncio -def test_create_item( - client: TestClient, superuser_token_headers: dict, db: Session + +async def test_create_item( + client: AsyncClient, superuser_token_headers: dict, db: AsyncSession ) -> None: data = {"title": "Foo", "description": "Fighters"} - response = client.post( + response = await client.post( f"{settings.API_V1_STR}/items/", headers=superuser_token_headers, json=data, ) assert response.status_code == 200 @@ -20,11 +23,11 @@ def test_create_item( assert "owner_id" in content -def test_read_item( - client: TestClient, superuser_token_headers: dict, db: Session +async def test_read_item( + client: AsyncClient, superuser_token_headers: dict, db: AsyncSession ) -> None: - item = create_random_item(db) - response = client.get( + item = await create_random_item(db) + response = await client.get( f"{settings.API_V1_STR}/items/{item.id}", headers=superuser_token_headers, ) assert response.status_code == 200 @@ -32,4 +35,4 @@ def test_read_item( assert content["title"] == item.title assert content["description"] == item.description assert content["id"] == item.id - assert content["owner_id"] == item.owner_id + assert content["owner_id"] == item.owner_id \ No newline at end of file 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 fd2c65a622..33c60e5afe 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,28 +1,31 @@ from typing import Dict -from fastapi.testclient import TestClient +import pytest +from httpx import AsyncClient from app.core.config import settings +pytestmark = pytest.mark.asyncio -def test_get_access_token(client: TestClient) -> None: + +async def test_get_access_token(client: AsyncClient) -> None: login_data = { "username": settings.FIRST_SUPERUSER, "password": settings.FIRST_SUPERUSER_PASSWORD, } - r = client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data) + r = await client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data) tokens = r.json() assert r.status_code == 200 assert "access_token" in tokens assert tokens["access_token"] -def test_use_access_token( - client: TestClient, superuser_token_headers: Dict[str, str] +async def test_use_access_token( + client: AsyncClient, superuser_token_headers: Dict[str, str] ) -> None: - r = client.post( + r = await client.post( f"{settings.API_V1_STR}/login/test-token", headers=superuser_token_headers, ) result = r.json() assert r.status_code == 200 - assert "email" in result + assert "email" in result \ No newline at end of file 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 ba22bfe969..f456cc5773 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,7 +1,8 @@ from typing import Dict -from fastapi.testclient import TestClient -from sqlalchemy.orm import Session +import pytest +from httpx import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession from app import crud from app.core.config import settings @@ -9,10 +10,15 @@ from app.tests.utils.utils import random_email, random_lower_string -def test_get_users_superuser_me( - client: TestClient, superuser_token_headers: Dict[str, str] +pytestmark = pytest.mark.asyncio + + +async def test_get_users_superuser_me( + client: AsyncClient, superuser_token_headers: Dict[str, str] ) -> None: - r = client.get(f"{settings.API_V1_STR}/users/me", headers=superuser_token_headers) + r = await client.get( + f"{settings.API_V1_STR}/users/me", headers=superuser_token_headers + ) current_user = r.json() assert current_user assert current_user["is_active"] is True @@ -20,10 +26,12 @@ def test_get_users_superuser_me( assert current_user["email"] == settings.FIRST_SUPERUSER -def test_get_users_normal_user_me( - client: TestClient, normal_user_token_headers: Dict[str, str] +async def test_get_users_normal_user_me( + client: AsyncClient, normal_user_token_headers: Dict[str, str] ) -> None: - r = client.get(f"{settings.API_V1_STR}/users/me", headers=normal_user_token_headers) + r = await client.get( + f"{settings.API_V1_STR}/users/me", headers=normal_user_token_headers + ) current_user = r.json() assert current_user assert current_user["is_active"] is True @@ -31,50 +39,50 @@ def test_get_users_normal_user_me( assert current_user["email"] == settings.EMAIL_TEST_USER -def test_create_user_new_email( - client: TestClient, superuser_token_headers: dict, db: Session +async def test_create_user_new_email( + client: AsyncClient, superuser_token_headers: dict, db: AsyncSession ) -> None: username = random_email() password = random_lower_string() data = {"email": username, "password": password} - r = client.post( + r = await client.post( f"{settings.API_V1_STR}/users/", headers=superuser_token_headers, json=data, ) assert 200 <= r.status_code < 300 created_user = r.json() - user = crud.user.get_by_email(db, email=username) + user = await crud.user.get_by_email(db, email=username) assert user assert user.email == created_user["email"] -def test_get_existing_user( - client: TestClient, superuser_token_headers: dict, db: Session +async def test_get_existing_user( + client: AsyncClient, superuser_token_headers: dict, db: AsyncSession ) -> None: username = random_email() password = random_lower_string() user_in = UserCreate(email=username, password=password) - user = crud.user.create(db, obj_in=user_in) + user = await crud.user.create(db, obj_in=user_in) user_id = user.id - r = client.get( + r = await client.get( f"{settings.API_V1_STR}/users/{user_id}", headers=superuser_token_headers, ) assert 200 <= r.status_code < 300 api_user = r.json() - existing_user = crud.user.get_by_email(db, email=username) + existing_user = await crud.user.get_by_email(db, email=username) assert existing_user assert existing_user.email == api_user["email"] -def test_create_user_existing_username( - client: TestClient, superuser_token_headers: dict, db: Session +async def test_create_user_existing_username( + client: AsyncClient, superuser_token_headers: dict, db: AsyncSession ) -> None: username = random_email() # username = email password = random_lower_string() user_in = UserCreate(email=username, password=password) - crud.user.create(db, obj_in=user_in) + await crud.user.create(db, obj_in=user_in) data = {"email": username, "password": password} - r = client.post( + r = await client.post( f"{settings.API_V1_STR}/users/", headers=superuser_token_headers, json=data, ) created_user = r.json() @@ -82,34 +90,36 @@ def test_create_user_existing_username( assert "_id" not in created_user -def test_create_user_by_normal_user( - client: TestClient, normal_user_token_headers: Dict[str, str] +async def test_create_user_by_normal_user( + client: AsyncClient, normal_user_token_headers: Dict[str, str] ) -> None: username = random_email() password = random_lower_string() data = {"email": username, "password": password} - r = client.post( + r = await client.post( f"{settings.API_V1_STR}/users/", headers=normal_user_token_headers, json=data, ) assert r.status_code == 400 -def test_retrieve_users( - client: TestClient, superuser_token_headers: dict, db: Session +async def test_retrieve_users( + client: AsyncClient, superuser_token_headers: dict, db: AsyncSession ) -> None: username = random_email() password = random_lower_string() user_in = UserCreate(email=username, password=password) - crud.user.create(db, obj_in=user_in) + await 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, obj_in=user_in2) + await crud.user.create(db, obj_in=user_in2) - r = client.get(f"{settings.API_V1_STR}/users/", headers=superuser_token_headers) + r = await client.get( + f"{settings.API_V1_STR}/users/", headers=superuser_token_headers + ) all_users = r.json() assert len(all_users) > 1 for item in all_users: - assert "email" in item + assert "email" in item \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py index 0304cb84dc..5c32986c3d 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py @@ -1,34 +1,43 @@ +import asyncio from typing import Dict, Generator import pytest -from fastapi.testclient import TestClient -from sqlalchemy.orm import Session +from httpx import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings -from app.db.session import SessionLocal +from app.db.session import async_session from app.main import app from app.tests.utils.user import authentication_token_from_email from app.tests.utils.utils import get_superuser_token_headers @pytest.fixture(scope="session") -def db() -> Generator: - yield SessionLocal() +def event_loop(): + yield asyncio.get_event_loop() + + +@pytest.fixture(scope="session") +async def db() -> Generator: + async with async_session() as session: + yield session @pytest.fixture(scope="module") -def client() -> Generator: - with TestClient(app) as c: +async def client() -> Generator: + async with AsyncClient(app=app, base_url="http://test") as c: yield c @pytest.fixture(scope="module") -def superuser_token_headers(client: TestClient) -> Dict[str, str]: - return get_superuser_token_headers(client) +async def superuser_token_headers(client: AsyncClient) -> Dict[str, str]: + return await get_superuser_token_headers(client) @pytest.fixture(scope="module") -def normal_user_token_headers(client: TestClient, db: Session) -> Dict[str, str]: - return authentication_token_from_email( +async def normal_user_token_headers( + client: AsyncClient, db: AsyncSession +) -> Dict[str, str]: + return await authentication_token_from_email( client=client, email=settings.EMAIL_TEST_USER, db=db - ) + ) \ No newline at end of file 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 e529144ef6..e6e09c6e8d 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,29 +1,32 @@ -from sqlalchemy.orm import Session +import pytest +from sqlalchemy.ext.asyncio import AsyncSession 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 +pytestmark = pytest.mark.asyncio -def test_create_item(db: Session) -> None: + +async def test_create_item(db: AsyncSession) -> None: title = random_lower_string() description = random_lower_string() item_in = ItemCreate(title=title, description=description) - user = create_random_user(db) - item = crud.item.create_with_owner(db=db, obj_in=item_in, owner_id=user.id) + user = await create_random_user(db) + item = await 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 -def test_get_item(db: Session) -> None: +async def test_get_item(db: AsyncSession) -> None: title = random_lower_string() description = random_lower_string() item_in = ItemCreate(title=title, description=description) - user = create_random_user(db) - 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) + user = await create_random_user(db) + item = await crud.item.create_with_owner(db=db, obj_in=item_in, owner_id=user.id) + stored_item = await crud.item.get(db=db, id=item.id) assert stored_item assert item.id == stored_item.id assert item.title == stored_item.title @@ -31,31 +34,31 @@ def test_get_item(db: Session) -> None: assert item.owner_id == stored_item.owner_id -def test_update_item(db: Session) -> None: +async def test_update_item(db: AsyncSession) -> None: title = random_lower_string() description = random_lower_string() item_in = ItemCreate(title=title, description=description) - user = create_random_user(db) - item = crud.item.create_with_owner(db=db, obj_in=item_in, owner_id=user.id) + user = await create_random_user(db) + item = await 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=db, db_obj=item, obj_in=item_update) + item2 = await 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 assert item.owner_id == item2.owner_id -def test_delete_item(db: Session) -> None: +async def test_delete_item(db: AsyncSession) -> None: title = random_lower_string() description = random_lower_string() item_in = ItemCreate(title=title, description=description) - user = create_random_user(db) - 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) + user = await create_random_user(db) + item = await crud.item.create_with_owner(db=db, obj_in=item_in, owner_id=user.id) + item2 = await crud.item.remove(db=db, id=item.id) + item3 = await crud.item.get(db=db, id=item.id) assert item3 is None assert item2.id == item.id assert item2.title == title assert item2.description == description - assert item2.owner_id == user.id + assert item2.owner_id == user.id \ No newline at end of file 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 2caee5b870..d031ce04c6 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,99 @@ +import pytest from fastapi.encoders import jsonable_encoder -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from app import crud from app.core.security import verify_password from app.schemas.user import UserCreate, UserUpdate from app.tests.utils.utils import random_email, random_lower_string +pytestmark = pytest.mark.asyncio -def test_create_user(db: Session) -> None: + +async def test_create_user(db: AsyncSession) -> None: email = random_email() password = random_lower_string() user_in = UserCreate(email=email, password=password) - user = crud.user.create(db, obj_in=user_in) + user = await crud.user.create(db, obj_in=user_in) assert user.email == email assert hasattr(user, "hashed_password") -def test_authenticate_user(db: Session) -> None: +async def test_authenticate_user(db: AsyncSession) -> None: email = random_email() password = random_lower_string() user_in = UserCreate(email=email, password=password) - user = crud.user.create(db, obj_in=user_in) - authenticated_user = crud.user.authenticate(db, email=email, password=password) + user = await crud.user.create(db, obj_in=user_in) + authenticated_user = await crud.user.authenticate( + db, email=email, password=password + ) assert authenticated_user assert user.email == authenticated_user.email -def test_not_authenticate_user(db: Session) -> None: +async def test_not_authenticate_user(db: AsyncSession) -> None: email = random_email() password = random_lower_string() - user = crud.user.authenticate(db, email=email, password=password) + user = await crud.user.authenticate(db, email=email, password=password) assert user is None -def test_check_if_user_is_active(db: Session) -> None: +async def test_check_if_user_is_active(db: AsyncSession) -> None: email = random_email() password = random_lower_string() user_in = UserCreate(email=email, password=password) - user = crud.user.create(db, obj_in=user_in) + user = await 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(db: Session) -> None: +async def test_check_if_user_is_active_inactive(db: AsyncSession) -> None: email = random_email() password = random_lower_string() user_in = UserCreate(email=email, password=password, disabled=True) - user = crud.user.create(db, obj_in=user_in) + user = await 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(db: Session) -> None: +async def test_check_if_user_is_superuser(db: AsyncSession) -> None: email = random_email() password = random_lower_string() user_in = UserCreate(email=email, password=password, is_superuser=True) - user = crud.user.create(db, obj_in=user_in) + user = await 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(db: Session) -> None: +async def test_check_if_user_is_superuser_normal_user(db: AsyncSession) -> None: username = random_email() password = random_lower_string() user_in = UserCreate(email=username, password=password) - user = crud.user.create(db, obj_in=user_in) + user = await crud.user.create(db, obj_in=user_in) is_superuser = crud.user.is_superuser(user) assert is_superuser is False -def test_get_user(db: Session) -> None: +async def test_get_user(db: AsyncSession) -> None: password = random_lower_string() username = random_email() user_in = UserCreate(email=username, password=password, is_superuser=True) - user = crud.user.create(db, obj_in=user_in) - user_2 = crud.user.get(db, id=user.id) + user = await crud.user.create(db, obj_in=user_in) + user_2 = await 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(db: Session) -> None: +async def test_update_user(db: AsyncSession) -> None: password = random_lower_string() email = random_email() user_in = UserCreate(email=email, password=password, is_superuser=True) - user = crud.user.create(db, obj_in=user_in) + user = await crud.user.create(db, obj_in=user_in) new_password = random_lower_string() 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) + await crud.user.update(db, db_obj=user, obj_in=user_in_update) + user_2 = await 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) + assert verify_password(new_password, user_2.hashed_password) \ No newline at end of file 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 e28f967078..423661c30e 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py @@ -1,6 +1,6 @@ from typing import Optional -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from app import crud, models from app.schemas.item import ItemCreate @@ -8,11 +8,13 @@ from app.tests.utils.utils import random_lower_string -def create_random_item(db: Session, *, owner_id: Optional[int] = None) -> models.Item: +async def create_random_item( + db: AsyncSession, *, owner_id: Optional[int] = None +) -> models.Item: if owner_id is None: - user = create_random_user(db) + user = await 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=db, obj_in=item_in, owner_id=owner_id) + return await crud.item.create_with_owner(db=db, obj_in=item_in, owner_id=owner_id) \ No newline at end of file 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 097056c197..552f239f6f 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/user.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/user.py @@ -1,7 +1,7 @@ from typing import Dict -from fastapi.testclient import TestClient -from sqlalchemy.orm import Session +from httpx import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession from app import crud from app.core.config import settings @@ -10,28 +10,28 @@ from app.tests.utils.utils import random_email, random_lower_string -def user_authentication_headers( - *, client: TestClient, email: str, password: str +async def user_authentication_headers( + *, client: AsyncClient, email: str, password: str ) -> Dict[str, str]: data = {"username": email, "password": password} - r = client.post(f"{settings.API_V1_STR}/login/access-token", data=data) + r = await client.post(f"{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(db: Session) -> User: +async def create_random_user(db: AsyncSession) -> User: email = random_email() password = random_lower_string() user_in = UserCreate(username=email, email=email, password=password) - user = crud.user.create(db=db, obj_in=user_in) + user = await crud.user.create(db=db, obj_in=user_in) return user -def authentication_token_from_email( - *, client: TestClient, email: str, db: Session +async def authentication_token_from_email( + *, client: AsyncClient, email: str, db: AsyncSession ) -> Dict[str, str]: """ Return a valid token for the user with given email. @@ -39,12 +39,14 @@ def authentication_token_from_email( If the user doesn't exist it is created first. """ password = random_lower_string() - user = crud.user.get_by_email(db, email=email) + user = await crud.user.get_by_email(db, email=email) if not user: user_in_create = UserCreate(username=email, email=email, password=password) - user = crud.user.create(db, obj_in=user_in_create) + user = await crud.user.create(db, obj_in=user_in_create) else: user_in_update = UserUpdate(password=password) - user = crud.user.update(db, db_obj=user, obj_in=user_in_update) + user = await crud.user.update(db, db_obj=user, obj_in=user_in_update) - return user_authentication_headers(client=client, email=email, password=password) + return await user_authentication_headers( + client=client, email=email, password=password + ) \ No newline at end of file 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 021fc22017..c5fa9a8ff3 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/utils.py +++ b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/utils.py @@ -2,7 +2,7 @@ import string from typing import Dict -from fastapi.testclient import TestClient +from httpx import AsyncClient from app.core.config import settings @@ -15,13 +15,13 @@ def random_email() -> str: return f"{random_lower_string()}@{random_lower_string()}.com" -def get_superuser_token_headers(client: TestClient) -> Dict[str, str]: +async def get_superuser_token_headers(client: AsyncClient) -> Dict[str, str]: login_data = { "username": settings.FIRST_SUPERUSER, "password": settings.FIRST_SUPERUSER_PASSWORD, } - r = client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data) + r = await client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data) tokens = r.json() a_token = tokens["access_token"] headers = {"Authorization": f"Bearer {a_token}"} - return headers + return headers \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/backend/app/pyproject.toml b/{{cookiecutter.project_slug}}/backend/app/pyproject.toml index ea4e04e198..14b9b6939a 100644 --- a/{{cookiecutter.project_slug}}/backend/app/pyproject.toml +++ b/{{cookiecutter.project_slug}}/backend/app/pyproject.toml @@ -21,9 +21,11 @@ gunicorn = "^20.0.4" jinja2 = "^2.11.2" 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"} +asyncpg = "^0.22.0" +httpx = "^0.17.1" +SQLAlchemy = "^1.4.5" [tool.poetry.dev-dependencies] mypy = "^0.770" @@ -34,6 +36,7 @@ flake8 = "^3.7.9" pytest = "^5.4.1" sqlalchemy-stubs = "^0.3" pytest-cov = "^2.8.1" +pytest-asyncio = "^0.14.0" [tool.isort] multi_line_output = 3 @@ -43,4 +46,3 @@ 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 8c39c502af..6862be2439 100644 --- a/{{cookiecutter.project_slug}}/backend/backend.dockerfile +++ b/{{cookiecutter.project_slug}}/backend/backend.dockerfile @@ -3,7 +3,7 @@ FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 WORKDIR /app/ # Install Poetry -RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python && \ +RUN curl -sSL https://install.python-poetry.org | POETRY_HOME=/opt/poetry python && \ cd /usr/local/bin && \ ln -s /opt/poetry/bin/poetry && \ poetry config virtualenvs.create false @@ -13,7 +13,7 @@ COPY ./app/pyproject.toml ./app/poetry.lock* /app/ # 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" +RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install --no-root ; else poetry install --no-root --only main ; fi" # For development, Jupyter remote kernel, Hydrogen # Using inside the container: diff --git a/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile b/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile index 4695a7b7ec..26cae08176 100644 --- a/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile +++ b/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile @@ -3,7 +3,7 @@ FROM python:3.7 WORKDIR /app/ # Install Poetry -RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python && \ +RUN curl -sSL https://install.python-poetry.org | POETRY_HOME=/opt/poetry python && \ cd /usr/local/bin && \ ln -s /opt/poetry/bin/poetry && \ poetry config virtualenvs.create false @@ -13,7 +13,7 @@ COPY ./app/pyproject.toml ./app/poetry.lock* /app/ # 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" +RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install --no-root ; else poetry install --no-root --only main ; fi" # For development, Jupyter remote kernel, Hydrogen # Using inside the container: