Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify CRUD definitions, make a clearer distinction between schemas and models #23

Merged
merged 33 commits into from Jan 19, 2020
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7abb977
removed postgres_password from alembic.ini, read it from env var instead
Apr 12, 2019
c23eb50
:twisted_rightwards_arrows: Merge remote
tiangolo Apr 20, 2019
14fe548
:recycle: use f-strings for PostgreSQL URL
tiangolo Apr 20, 2019
059046b
Merge pull request #1 from tiangolo/master
ebreton Apr 27, 2019
900a278
Merge pull request #2 from tiangolo/master
ebreton May 3, 2019
d18d065
Add CrudBase along with SubItem for the showcase
May 3, 2019
c0123bb
Merge pull request #3 from tiangolo/master
ebreton Jun 18, 2019
8033e6a
Add subitem
Sep 5, 2019
7b2ceb9
Merge pull request #4 from tiangolo/master
ebreton Sep 9, 2019
5e93adc
merged master in
Sep 9, 2019
c10da2f
Add orm_mode
Sep 9, 2019
5efdecc
Follow comments on PR
Sep 9, 2019
9db15d8
Renamed models into schemas
Sep 9, 2019
f6a5bf6
Rename db_models into models
Sep 9, 2019
9ce0921
Rename db_models to models
Sep 9, 2019
5f8a300
Forward args passed to test.sh down to test-start.sh
Sep 10, 2019
3acade8
ignore cache, Pilfile.lock and docker-stack.yml
Sep 10, 2019
e464bd3
Fix tests
Sep 10, 2019
8b2f559
Update tests
Sep 19, 2019
fa7adb9
Rename test-backend.sh to test-again.sh, improve doc
Sep 19, 2019
efa4d85
Fix typo and missing argument in CrudBase docstring
Dec 4, 2019
92ad76c
:wrench: Update testing scripts
tiangolo Jan 19, 2020
470661f
:recycle: Refactor CRUD utils to use generics and types
tiangolo Jan 19, 2020
359581f
:rewind: Revert model changes, to have the minimum changes
tiangolo Jan 19, 2020
4d6de8c
:rewind: Revert DB base and changes, separate CRUD from DB models
tiangolo Jan 19, 2020
cc2a769
:rewind: Revert changes in code line order
tiangolo Jan 19, 2020
f4f7d71
:recycle: Refactor Pydantic models, revert changes not related to the…
tiangolo Jan 19, 2020
f7615dd
:sparkles: Use new CRUD utils, revert changes not related to PR
tiangolo Jan 19, 2020
79f0169
:sparkles: Use new CRUD utils in security utils
tiangolo Jan 19, 2020
2a45871
:white_check_mark: Use new CRUD utils in tests
tiangolo Jan 19, 2020
e6f6a86
:arrow_up: Upgrade FastAPI and Uvicorn version
tiangolo Jan 19, 2020
43129b8
:twisted_rightwards_arrows: Merge master
tiangolo Jan 19, 2020
a4b8c89
:recycle: Update files, refactor, simplify
tiangolo Jan 19, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
@@ -1,3 +1,7 @@
.vscode
testing-project
.mypy_cache
.pytest_cache
Pipfile.lock

testing-project
docker-stack.yml
19 changes: 19 additions & 0 deletions test-backend.sh
@@ -0,0 +1,19 @@
#! /usr/bin/env bash

# pre-requisite: use test.sh script at least once before using this script

# this script produces the same result as test.sh, but faster
# because it rsyncs the latest modification into testing-project
# instead of removing it and generating it again

# Exit in case of error
set -e

# push new src files
rsync -av \{\{cookiecutter.project_slug\}\}/backend testing-project/

# restart backend container
docker-compose -f testing-project/docker-stack.yml restart backend

# run tests
docker-compose -f testing-project/docker-stack.yml exec -T backend-tests /tests-start.sh $*
2 changes: 1 addition & 1 deletion test.sh
Expand Up @@ -9,6 +9,6 @@ cookiecutter --config-file ./testing-config.yml --no-input -f ./

cd ./testing-project

bash ./scripts/test.sh
bash ./scripts/test.sh $*

cd ../
4 changes: 2 additions & 2 deletions {{cookiecutter.project_slug}}/README.md
Expand Up @@ -55,7 +55,7 @@ If your Docker is not running in `localhost` (the URLs above wouldn't work) chec

Open your editor at `./backend/app/` (instead of the project root: `./`), so that you see an `./app/` directory with your code inside. That way, your editor will be able to find all the imports, etc.

Modify or add SQLAlchemy models in `./backend/app/app/db_models/`, Pydantic models in `./backend/app/app/models/`, API endpoints in `./backend/app/app/api/`, CRUD (Create, Read, Update, Delete) utils in `./backend/app/app/crud/`. The easiest might be to copy the ones for Items (models, endpoints, and CRUD utils) and update them to your needs.
Modify or add SQLAlchemy models in `./backend/app/app/models/`, Pydantic schemas in `./backend/app/app/schemas/`, API endpoints in `./backend/app/app/api/`, CRUD (Create, Read, Update, Delete) utils in `./backend/app/app/crud/`. The easiest might be to copy the ones for Items (models, endpoints, and CRUD utils) and update them to your needs.

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

Expand Down Expand Up @@ -205,7 +205,7 @@ Make sure you create a "revision" of your models and that you "upgrade" your dat
docker-compose exec backend bash
```

* If you created a new model in `./backend/app/app/db_models/`, make sure to import it in `./backend/app/app/db/base.py`, that Python module (`base.py`) that imports all the models will be used by Alembic.
* If you created a new model in `./backend/app/app/models/`, make sure to import it in `./backend/app/app/db/base.py`, that Python module (`base.py`) that imports all the models will be used by Alembic.

* After changing a model (for example, adding a column), inside the container, create a revision, e.g.:

Expand Down
Expand Up @@ -19,33 +19,52 @@
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('full_name', sa.String(), nullable=True),
sa.Column('email', sa.String(), nullable=True),
sa.Column('hashed_password', sa.String(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('is_superuser', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id')
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('full_name', sa.String(), nullable=True),
sa.Column('email', sa.String(), nullable=True),
sa.Column('hashed_password', sa.String(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('is_superuser', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
op.create_index(op.f('ix_user_full_name'), 'user', ['full_name'], unique=False)
op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False)

op.create_table('item',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(), nullable=True),
sa.Column('description', sa.String(), nullable=True),
sa.Column('owner_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(), nullable=True),
sa.Column('description', sa.String(), nullable=True),
sa.Column('owner_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_item_description'), 'item', ['description'], unique=False)
op.create_index(op.f('ix_item_id'), 'item', ['id'], unique=False)
op.create_index(op.f('ix_item_title'), 'item', ['title'], unique=False)

op.create_table('subitem',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(), nullable=True),
sa.Column('description', sa.String(), nullable=True),
sa.Column('item_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['item_id'], ['item.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_subitem_description'), 'subitem', ['description'], unique=False)
op.create_index(op.f('ix_subitem_id'), 'subitem', ['id'], unique=False)
op.create_index(op.f('ix_subitem_title'), 'subitem', ['title'], unique=False)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_subitem_title'), table_name='subitem')
op.drop_index(op.f('ix_subitem_id'), table_name='subitem')
op.drop_index(op.f('ix_subitem_description'), table_name='subitem')
op.drop_table('subitem')
op.drop_index(op.f('ix_item_title'), table_name='item')
op.drop_index(op.f('ix_item_id'), table_name='item')
op.drop_index(op.f('ix_item_description'), table_name='item')
Expand Down
Expand Up @@ -6,8 +6,8 @@
from app import crud
from app.api.utils.db import get_db
from app.api.utils.security import get_current_active_user
from app.db_models.user import User as DBUser
from app.models.item import Item, ItemCreate, ItemUpdate
from app.models.user import User as DBUser
from app.schemas.item import Item, ItemCreate, ItemUpdate

router = APIRouter()

Expand All @@ -22,7 +22,7 @@ def read_items(
"""
Retrieve items.
"""
if crud.user.is_superuser(current_user):
if current_user.is_superuser:
items = crud.item.get_multi(db, skip=skip, limit=limit)
else:
items = crud.item.get_multi_by_owner(
Expand All @@ -41,7 +41,8 @@ def create_item(
"""
Create new item.
"""
item = crud.item.create(db_session=db, item_in=item_in, owner_id=current_user.id)
item_in.owner_id = current_user.id
item = crud.item.create(db_session=db, item_in=item_in)
return item


Expand All @@ -59,7 +60,7 @@ def update_item(
item = crud.item.get(db_session=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):
if not current_user.is_superuser and (item.owner_id != current_user.id):
raise HTTPException(status_code=400, detail="Not enough permissions")
item = crud.item.update(db_session=db, item=item, item_in=item_in)
return item
Expand All @@ -78,7 +79,7 @@ def read_user_me(
item = crud.item.get(db_session=db, id=id)
if not item:
raise HTTPException(status_code=400, detail="Item not found")
if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id):
if not current_user.is_superuser and (item.owner_id != current_user.id):
raise HTTPException(status_code=400, detail="Not enough permissions")
return item

Expand All @@ -96,7 +97,7 @@ def delete_item(
item = crud.item.get(db_session=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):
if not current_user.is_superuser 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)
return item
Expand Up @@ -10,10 +10,10 @@
from app.core import config
from app.core.jwt import create_access_token
from app.core.security import get_password_hash
from app.db_models.user import User as DBUser
from app.models.msg import Msg
from app.models.token import Token
from app.models.user import User
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,
Expand All @@ -35,7 +35,7 @@ def login_access_token(
)
if not user:
raise HTTPException(status_code=400, detail="Incorrect email or password")
elif not crud.user.is_active(user):
elif not user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
access_token_expires = timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES)
return {
Expand Down Expand Up @@ -87,7 +87,7 @@ def reset_password(token: str = Body(...), new_password: str = Body(...), db: Se
status_code=404,
detail="The user with this username does not exist in the system.",
)
elif not crud.user.is_active(user):
elif not user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
hashed_password = get_password_hash(new_password)
user.hashed_password = hashed_password
Expand Down
Expand Up @@ -9,13 +9,43 @@
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 import config
from app.db_models.user import User as DBUser
from app.models.user import User, UserCreate, UserInDB, UserUpdate
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("/me", response_model=User)
def read_user_me(
db: Session = Depends(get_db),
current_user: DBUser = Depends(get_current_active_user),
):
"""
Get current user.
"""
return current_user


@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),
):
"""
Get a specific user by id.
"""
user = crud.user.get(db, obj_id=user_id)
if user == current_user:
return user
if not current_user.is_superuser:
raise HTTPException(
status_code=400, detail="The user doesn't have enough privileges"
)
return user


@router.get("/", response_model=List[User])
def read_users(
db: Session = Depends(get_db),
Expand All @@ -30,6 +60,33 @@ def read_users(
return users


@router.post("/open", response_model=User)
def create_user_open(
*,
db: Session = Depends(get_db),
password: str = Body(...),
email: EmailStr = Body(...),
full_name: str = Body(None),
):
"""
Create new user without the need to be logged in.
"""
if not config.USERS_OPEN_REGISTRATION:
raise HTTPException(
status_code=403,
detail="Open user resgistration is forbidden on this server",
)
user = 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 = UserCreate(password=password, email=email, full_name=full_name)
user = crud.user.create(db, obj_in=user_in)
return user


@router.post("/", response_model=User)
def create_user(
*,
Expand All @@ -46,7 +103,7 @@ def create_user(
status_code=400,
detail="The user with this username already exists in the system.",
)
user = crud.user.create(db, user_in=user_in)
user = crud.user.create(db, obj_in=user_in)
if config.EMAILS_ENABLED and user_in.email:
send_new_account_email(
email_to=user_in.email, username=user_in.email, password=user_in.password
Expand Down Expand Up @@ -74,64 +131,7 @@ def update_user_me(
user_in.full_name = full_name
if email is not None:
user_in.email = email
user = crud.user.update(db, user=current_user, user_in=user_in)
return user


@router.get("/me", response_model=User)
def read_user_me(
db: Session = Depends(get_db),
current_user: DBUser = Depends(get_current_active_user),
):
"""
Get current user.
"""
return current_user


@router.post("/open", response_model=User)
def create_user_open(
*,
db: Session = Depends(get_db),
password: str = Body(...),
email: EmailStr = Body(...),
full_name: str = Body(None),
):
"""
Create new user without the need to be logged in.
"""
if not config.USERS_OPEN_REGISTRATION:
raise HTTPException(
status_code=403,
detail="Open user resgistration is forbidden on this server",
)
user = 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 = UserCreate(password=password, email=email, full_name=full_name)
user = crud.user.create(db, user_in=user_in)
return user


@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),
):
"""
Get a specific user by id.
"""
user = crud.user.get(db, user_id=user_id)
if user == current_user:
return user
if not crud.user.is_superuser(current_user):
raise HTTPException(
status_code=400, detail="The user doesn't have enough privileges"
)
user = crud.user.update(db, obj=current_user, obj_in=user_in)
return user


Expand All @@ -141,16 +141,16 @@ def update_user(
db: Session = Depends(get_db),
user_id: int,
user_in: UserUpdate,
current_user: UserInDB = Depends(get_current_active_superuser),
current_user: User = Depends(get_current_active_superuser),
):
"""
Update a user.
"""
user = crud.user.get(db, user_id=user_id)
user = crud.user.get(db, obj_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, user=user, user_in=user_in)
user = crud.user.update(db, obj=user, obj_in=user_in)
return user