27 Commits

Author SHA1 Message Date
Sebastián Ramírez
44d8a4358b 🔖 Release version 0.4.0 2019-05-29 09:49:17 +04:00
Sebastián Ramírez
9b4108fdae 📝 Update release notes 2019-05-29 09:48:28 +04:00
Sebastián Ramírez
b4fa418e65 🔒 Receive token as body in reset password (#34) 2019-05-29 09:47:59 +04:00
Sebastián Ramírez
a612765b83 📝 Update release notes, clarify text 2019-05-29 09:35:13 +04:00
Sebastián Ramírez
de7140f1e7 📝 Update release notes 2019-05-29 09:27:04 +04:00
dmontagu
546dc8bdcb 🔒 Update login.py to receive password as body (#33)
Change `new_password` from a query parameter to a body parameter for security.

(Why this is problematic is discussed in the top answer to https://stackoverflow.com/questions/2629222/are-querystring-parameters-secure-in-https-http-ssl)
2019-05-29 09:24:09 +04:00
Sebastián Ramírez
eae33cda72 📝 Update release notes 2019-05-22 15:30:51 +04:00
Manu
1d30172e7a 🗃️ Fix SQLAlchemy class lookup (#29) 2019-05-22 15:29:24 +04:00
Sebastián Ramírez
6fc9a37eb5 📝 Update release notes 2019-05-22 15:21:02 +04:00
Manu
170231783a 🗃️ Fix SQLAlchemy operation error after database restarts (#32) 2019-05-22 15:18:59 +04:00
Sebastián Ramírez
5216fcfd77 📝 Update release notes 2019-05-04 00:04:30 +04:00
Manu
8bf3607d2b 📝 Fix the paths of the scripts in README (#19)
* removed postgres_password from alembic.ini, read it from env var instead

* ♻️ use f-strings for PostgreSQL URL

* fix path to scripts
2019-05-03 23:57:12 +04:00
Sebastián Ramírez
8ce745b7ef 📝 Update release notes 2019-05-03 23:52:42 +04:00
Sebastián Ramírez
6bbd58c76f 📝 Update docs for running tests live 2019-05-03 23:52:23 +04:00
Manu
1aeb3208bf Use extra pytest arguments forwarded from shell (#17)
* removed postgres_password from alembic.ini, read it from env var instead

* ♻️ use f-strings for PostgreSQL URL

* passes given args
2019-05-03 23:44:18 +04:00
Sebastián Ramírez
45317e54c7 📝 Update release notes 2019-04-24 22:46:09 +04:00
Sebastián Ramírez
47e0fe56e3 ⬆️ Upgrade Jupyter to use Lab, update util/env var for local development 2019-04-24 22:45:20 +04:00
Sebastián Ramírez
42ee0fe0ba 📝 Update release notes 2019-04-20 19:59:47 +04:00
Sebastián Ramírez
92b757fc96 ♻️ Create Item from all fields in Pydantic model 2019-04-20 19:57:34 +04:00
Manu
bece399368 ♻️ removed postgres_password from alembic.ini (#9)
♻️ removed postgres_password from alembic.ini (#9)
2019-04-20 19:56:50 +04:00
Sebastián Ramírez
5dd83c6350 🔧 Update development scripts 2019-04-20 19:24:57 +04:00
Sebastián Ramírez
f365a4d026 🔖 Release 0.3.0 2019-04-19 09:46:18 +04:00
Sebastián Ramírez
ecd634e497 Add Items (crud, models, endpoints), utils, refactor (#14)
* Update CRUD utils to use types better.
* Simplify Pydantic model names, from `UserInCreate` to `UserCreate`, etc.
* Upgrade packages.
* Add new generic "Items" models, crud utils, endpoints, and tests. To facilitate re-using them to create new functionality. As they are simple and generic (not like Users), it's easier to copy-paste and adapt them to each use case.
* Update endpoints/*path operations* to simplify code and use new utilities, prefix and tags in `include_router`.
* Update testing utils.
* Update linting rules, relax vulture to reduce false positives.
* Update migrations to include new Items.
* Update project README.md with tips about how to start with backend.
2019-04-19 09:45:23 +04:00
Sebastián Ramírez
1fe4908b0a 📝 Update release notes 2019-04-16 21:39:13 +04:00
Manu
cd86803daa Upgrade to Python 3.7, including new compatible Celery (#10) 2019-04-16 21:36:38 +04:00
Sebastián Ramírez
cf5516cda6 📝 Update release notes section 2019-04-11 21:45:52 +04:00
Sebastián Ramírez
151f7ed79b 🐛 Fix frontend hijacking /docs in development (#6)
* 🐛 Fix frontend hijacking /docs in development

* 🐛 Fix frontend Dockerfile copying Nginx config
2019-04-11 21:45:07 +04:00
38 changed files with 523 additions and 1125 deletions

View File

@@ -148,9 +148,50 @@ After using this generator, your new project (the directory created) will contai
### Next release ### Next release
### 0.4.0
* Fix security on resetting a password. Receive token as body, not query. PR [#34](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/34).
* Fix security on resetting a password. Receive it as body, not query. PR [#33](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/33) by [@dmontagu](https://github.com/dmontagu).
* Fix SQLAlchemy class lookup on initialization. PR [#29](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/29) by [@ebreton](https://github.com/ebreton).
* Fix SQLAlchemy operation errors on database restart. PR [#32](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/32) by [@ebreton](https://github.com/ebreton).
* Fix locations of scripts in generated README. PR [#19](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/19) by [@ebreton](https://github.com/ebreton).
* Forward arguments from script to `pytest` inside container. PR [#17](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/17) by [@ebreton](https://github.com/ebreton).
* Update development scripts.
* Read Alembic configs from env vars. PR <a href="https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/9" target="_blank">#9</a> by <a href="https://github.com/ebreton" target="_blank">@ebreton</a>.
* Create DB Item objects from all Pydantic model's fields.
* Update Jupyter Lab installation and util script/environment variable for local development.
### 0.3.0
* PR <a href="https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/14" target="_blank">#14</a>:
* Update CRUD utils to use types better.
* Simplify Pydantic model names, from `UserInCreate` to `UserCreate`, etc.
* Upgrade packages.
* Add new generic "Items" models, crud utils, endpoints, and tests. To facilitate re-using them to create new functionality. As they are simple and generic (not like Users), it's easier to copy-paste and adapt them to each use case.
* Update endpoints/*path operations* to simplify code and use new utilities, prefix and tags in `include_router`.
* Update testing utils.
* Update linting rules, relax vulture to reduce false positives.
* Update migrations to include new Items.
* Update project README.md with tips about how to start with backend.
* Upgrade Python to 3.7 as Celery is now compatible too. PR <a href="https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/10" target="_blank">#10</a> by <a href="https://github.com/ebreton" target="_blank">@ebreton</a>.
### 0.2.2
* Fix frontend hijacking /docs in development. Using latest https://github.com/tiangolo/node-frontend with custom Nginx configs in frontend. <a href="https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/6" target="_blank">PR #6</a>.
### 0.2.1 ### 0.2.1
* Fix documentation for *path operation* to get user by ID. <a href="https://github.com/tiangolo/fastapi/pull/97" target="_blank">PR #97</a> by <a href="https://github.com/mpclarkson" target="_blank">@mpclarkson</a>. * Fix documentation for *path operation* to get user by ID. <a href="https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/4" target="_blank">PR #4</a> by <a href="https://github.com/mpclarkson" target="_blank">@mpclarkson</a> in FastAPI.
* Set `/start-reload.sh` as a command override for development by default. * Set `/start-reload.sh` as a command override for development by default.

View File

@@ -1,4 +1,5 @@
rm -rf \{\{cookiecutter.project_slug\}\}/.git rm -rf \{\{cookiecutter.project_slug\}\}/.git
rm -rf \{\{cookiecutter.project_slug\}\}/backend/app/Pipfile.lock
rm -rf \{\{cookiecutter.project_slug\}\}/frontend/node_modules rm -rf \{\{cookiecutter.project_slug\}\}/frontend/node_modules
rm -rf \{\{cookiecutter.project_slug\}\}/frontend/dist rm -rf \{\{cookiecutter.project_slug\}\}/frontend/dist
git checkout \{\{cookiecutter.project_slug\}\}/README.md git checkout \{\{cookiecutter.project_slug\}\}/README.md
@@ -6,8 +7,8 @@ git checkout \{\{cookiecutter.project_slug\}\}/.gitlab-ci.yml
git checkout \{\{cookiecutter.project_slug\}\}/cookiecutter-config-file.yml git checkout \{\{cookiecutter.project_slug\}\}/cookiecutter-config-file.yml
git checkout \{\{cookiecutter.project_slug\}\}/docker-compose.deploy.networks.yml git checkout \{\{cookiecutter.project_slug\}\}/docker-compose.deploy.networks.yml
git checkout \{\{cookiecutter.project_slug\}\}/env-backend.env git checkout \{\{cookiecutter.project_slug\}\}/env-backend.env
git checkout \{\{cookiecutter.project_slug\}\}/env-couchbase.env
git checkout \{\{cookiecutter.project_slug\}\}/env-flower.env git checkout \{\{cookiecutter.project_slug\}\}/env-flower.env
git checkout \{\{cookiecutter.project_slug\}\}/.env git checkout \{\{cookiecutter.project_slug\}\}/.env
git checkout \{\{cookiecutter.project_slug\}\}/frontend/.env git checkout \{\{cookiecutter.project_slug\}\}/frontend/.env
git checkout \{\{cookiecutter.project_slug\}\}/env-sync-gateway.env git checkout \{\{cookiecutter.project_slug\}\}/env-pgadmin.env
git checkout \{\{cookiecutter.project_slug\}\}/env-postgres.env

View File

@@ -53,7 +53,9 @@ If your Docker is not running in `localhost` (the URLs above wouldn't work) chec
### General workflow ### General workflow
Add and modify SQLAlchemy models in `./backend/app/app/db_models/`, Pydantic models in `./backend/app/app/models` and API endpoints in `./backend/app/app/api/`. Open your editor at `./backend/app/` (instead of the project root: `./`), so that you see an `./app/` directory with your code inside. That way, your editor will be able to find all the imports, etc.
Modify or add 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.
Add and modify tasks to the Celery worker in `./backend/app/app/worker.py`. Add and modify tasks to the Celery worker in `./backend/app/app/worker.py`.
@@ -121,10 +123,10 @@ Nevertheless, if it doesn't detect a change but a syntax error, it will just sto
To test the backend run: To test the backend run:
```bash ```bash
DOMAIN=backend sh ./script-test.sh DOMAIN=backend sh ./scripts/test.sh
``` ```
The file `./script-test.sh` has the commands to generate a testing `docker-stack.yml` file from the needed Docker Compose files, start the stack and test it. The file `./scripts/test.sh` has the commands to generate a testing `docker-stack.yml` file from the needed Docker Compose files, start the stack and test it.
The tests run with Pytest, modify and add tests to `./backend/app/app/tests/`. The tests run with Pytest, modify and add tests to `./backend/app/app/tests/`.
@@ -132,6 +134,22 @@ If you need to install any additional package for the tests, add it to the file
If you use GitLab CI the tests will run automatically. If you use GitLab CI the tests will run automatically.
#### Test running stack
If your stack is already up and you just want to run the tests, you can use:
```bash
docker-compose exec backend-tests /tests-start.sh
```
That `/tests-start.sh` script inside the `backend-tests` container calls `pytest`. If you need to pass extra arguments to `pytest`, you can pass them to that command and they will be forwarded.
For example, to stop on first error:
```bash
docker-compose exec backend-tests /tests-start.sh -x
```
### Live development with Python Jupyter Notebooks ### Live development with Python Jupyter Notebooks
If you know about Python [Jupyter Notebooks](http://jupyter.org/), you can take advantage of them during local development. If you know about Python [Jupyter Notebooks](http://jupyter.org/), you can take advantage of them during local development.
@@ -382,7 +400,7 @@ Then you need to have those constraints in your deployment Docker Compose file f
To be able to use different environments, like `prod` and `stag`, you should pass the name of the stack as an environment variable. Like: To be able to use different environments, like `prod` and `stag`, you should pass the name of the stack as an environment variable. Like:
```bash ```bash
STACK_NAME={{cookiecutter.docker_swarm_stack_name_staging}} sh ./script-deploy.sh STACK_NAME={{cookiecutter.docker_swarm_stack_name_staging}} sh ./scripts/deploy.sh
``` ```
To use and expand that environment variable inside the `docker-compose.deploy.volumes-placement.yml` files you can add the constraints to the services like: To use and expand that environment variable inside the `docker-compose.deploy.volumes-placement.yml` files you can add the constraints to the services like:
@@ -399,7 +417,7 @@ services:
- node.labels.${STACK_NAME}.app-db-data == true - node.labels.${STACK_NAME}.app-db-data == true
``` ```
note the `${STACK_NAME}`. In the script `./script-deploy.sh`, that `docker-compose.deploy.volumes-placement.yml` would be converted, and saved to a file `docker-stack.yml` containing: note the `${STACK_NAME}`. In the script `./scripts/deploy.sh`, that `docker-compose.deploy.volumes-placement.yml` would be converted, and saved to a file `docker-stack.yml` containing:
```yaml ```yaml
version: '3' version: '3'
@@ -488,10 +506,10 @@ Here are the steps in detail:
* Set these environment variables, prepended to the next command: * Set these environment variables, prepended to the next command:
* `TAG=prod` * `TAG=prod`
* `FRONTEND_ENV=production` * `FRONTEND_ENV=production`
* Use the provided `script-build.sh` file with those environment variables: * Use the provided `scripts/build.sh` file with those environment variables:
```bash ```bash
TAG=prod FRONTEND_ENV=production bash ./script-build.sh TAG=prod FRONTEND_ENV=production bash ./scripts/build.sh
``` ```
2. **Optionally, push your images to a Docker Registry** 2. **Optionally, push your images to a Docker Registry**
@@ -503,10 +521,10 @@ If you are using a registry and pushing your images, you can omit running the pr
* Set these environment variables: * Set these environment variables:
* `TAG=prod` * `TAG=prod`
* `FRONTEND_ENV=production` * `FRONTEND_ENV=production`
* Use the provided `script-build-push.sh` file with those environment variables: * Use the provided `scripts/build-push.sh` file with those environment variables:
```bash ```bash
TAG=prod FRONTEND_ENV=production bash ./script-build.sh TAG=prod FRONTEND_ENV=production bash ./scripts/build-push.sh
``` ```
3. **Deploy your stack** 3. **Deploy your stack**
@@ -516,14 +534,14 @@ TAG=prod FRONTEND_ENV=production bash ./script-build.sh
* `TRAEFIK_TAG={{cookiecutter.traefik_constraint_tag}}` * `TRAEFIK_TAG={{cookiecutter.traefik_constraint_tag}}`
* `STACK_NAME={{cookiecutter.docker_swarm_stack_name_main}}` * `STACK_NAME={{cookiecutter.docker_swarm_stack_name_main}}`
* `TAG=prod` * `TAG=prod`
* Use the provided `script-deploy.sh` file with those environment variables: * Use the provided `scripts/deploy.sh` file with those environment variables:
```bash ```bash
DOMAIN={{cookiecutter.domain_main}} \ DOMAIN={{cookiecutter.domain_main}} \
TRAEFIK_TAG={{cookiecutter.traefik_constraint_tag}} \ TRAEFIK_TAG={{cookiecutter.traefik_constraint_tag}} \
STACK_NAME={{cookiecutter.docker_swarm_stack_name_main}} \ STACK_NAME={{cookiecutter.docker_swarm_stack_name_main}} \
TAG=prod \ TAG=prod \
bash ./script-deploy.sh bash ./scripts/deploy.sh
``` ```
--- ---

View File

@@ -20,7 +20,7 @@ pyjwt = "*"
python-multipart = "*" python-multipart = "*"
email-validator = "*" email-validator = "*"
requests = "*" requests = "*"
celery = "==4.2.1" celery = "*"
passlib = {extras = ["bcrypt"],version = "*"} passlib = {extras = ["bcrypt"],version = "*"}
tenacity = "*" tenacity = "*"
pydantic = "*" pydantic = "*"

File diff suppressed because it is too large Load Diff

View File

@@ -35,9 +35,6 @@ script_location = alembic
# are written from script.py.mako # are written from script.py.mako
# output_encoding = utf-8 # output_encoding = utf-8
sqlalchemy.url = postgresql://postgres:{{cookiecutter.postgres_password}}@db/app
# Logging configuration # Logging configuration
[loggers] [loggers]
keys = root,sqlalchemy,alembic keys = root,sqlalchemy,alembic

View File

@@ -1,4 +1,7 @@
from __future__ import with_statement from __future__ import with_statement
import os
from alembic import context from alembic import context
from sqlalchemy import engine_from_config, pool from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig from logging.config import fileConfig
@@ -27,6 +30,14 @@ target_metadata = Base.metadata
# ... etc. # ... etc.
def get_url():
user = os.getenv("POSTGRES_USER", "postgres")
password = os.getenv("POSTGRES_PASSWORD", "")
server = os.getenv("POSTGRES_SERVER", "db")
db = os.getenv("POSTGRES_DB", "app")
return f"postgresql://{user}:{password}@{server}/{db}"
def run_migrations_offline(): def run_migrations_offline():
"""Run migrations in 'offline' mode. """Run migrations in 'offline' mode.
@@ -39,7 +50,7 @@ def run_migrations_offline():
script output. script output.
""" """
url = config.get_main_option("sqlalchemy.url") url = get_url()
context.configure( context.configure(
url=url, target_metadata=target_metadata, literal_binds=True, compare_type=True url=url, target_metadata=target_metadata, literal_binds=True, compare_type=True
) )
@@ -55,8 +66,10 @@ def run_migrations_online():
and associate a connection with the context. and associate a connection with the context.
""" """
configuration = config.get_section(config.config_ini_section)
configuration['sqlalchemy.url'] = get_url()
connectable = engine_from_config( connectable = engine_from_config(
config.get_section(config.config_ini_section), configuration,
prefix="sqlalchemy.", prefix="sqlalchemy.",
poolclass=pool.NullPool, poolclass=pool.NullPool,
) )

View File

@@ -1,8 +1,8 @@
"""First revision """First revision
Revision ID: e6ae69e9dcb9 Revision ID: d4867f3a4c0a
Revises: Revises:
Create Date: 2019-02-13 14:27:57.038583 Create Date: 2019-04-17 13:53:32.978401
""" """
from alembic import op from alembic import op
@@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = 'e6ae69e9dcb9' revision = 'd4867f3a4c0a'
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@@ -30,11 +30,26 @@ def upgrade():
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True) 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_full_name'), 'user', ['full_name'], unique=False)
op.create_index(op.f('ix_user_id'), 'user', ['id'], 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')
)
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)
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade(): def downgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
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')
op.drop_table('item')
op.drop_index(op.f('ix_user_id'), table_name='user') op.drop_index(op.f('ix_user_id'), table_name='user')
op.drop_index(op.f('ix_user_full_name'), table_name='user') op.drop_index(op.f('ix_user_full_name'), table_name='user')
op.drop_index(op.f('ix_user_email'), table_name='user') op.drop_index(op.f('ix_user_email'), table_name='user')

View File

@@ -1,8 +1,9 @@
from fastapi import APIRouter from fastapi import APIRouter
from app.api.api_v1.endpoints import token, user, utils from app.api.api_v1.endpoints import items, login, users, utils
api_router = APIRouter() api_router = APIRouter()
api_router.include_router(token.router) api_router.include_router(login.router, tags=["login"])
api_router.include_router(user.router) api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(utils.router) api_router.include_router(utils.router, prefix="/utils", tags=["utils"])
api_router.include_router(items.router, prefix="/items", tags=["items"])

View File

@@ -0,0 +1,102 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException
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.db_models.user import User as DBUser
from app.models.item import Item, ItemCreate, ItemUpdate
router = APIRouter()
@router.get("/", response_model=List[Item])
def read_items(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: DBUser = Depends(get_current_active_user),
):
"""
Retrieve items.
"""
if crud.user.is_superuser(current_user):
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
)
return items
@router.post("/", response_model=Item)
def create_item(
*,
db: Session = Depends(get_db),
item_in: ItemCreate,
current_user: DBUser = Depends(get_current_active_user),
):
"""
Create new item.
"""
item = crud.item.create(db_session=db, item_in=item_in, owner_id=current_user.id)
return item
@router.put("/{id}", response_model=Item)
def update_item(
*,
db: Session = Depends(get_db),
id: int,
item_in: ItemUpdate,
current_user: DBUser = Depends(get_current_active_user),
):
"""
Update an 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):
raise HTTPException(status_code=400, detail="Not enough permissions")
item = crud.item.update(db_session=db, item=item, item_in=item_in)
return item
@router.get("/{id}", response_model=Item)
def read_user_me(
*,
db: Session = Depends(get_db),
id: int,
current_user: DBUser = Depends(get_current_active_user),
):
"""
Get item by ID.
"""
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):
raise HTTPException(status_code=400, detail="Not enough permissions")
return item
@router.delete("/{id}", response_model=Item)
def delete_item(
*,
db: Session = Depends(get_db),
id: int,
current_user: DBUser = Depends(get_current_active_user),
):
"""
Delete an 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):
raise HTTPException(status_code=400, detail="Not enough permissions")
item = crud.item.remove(db_session=db, id=id)
return item

View File

@@ -1,6 +1,6 @@
from datetime import timedelta from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Body, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -74,7 +74,7 @@ def recover_password(email: str, db: Session = Depends(get_db)):
@router.post("/reset-password/", tags=["login"], response_model=Msg) @router.post("/reset-password/", tags=["login"], response_model=Msg)
def reset_password(token: str, new_password: str, db: Session = Depends(get_db)): def reset_password(token: str = Body(...), new_password: str = Body(...), db: Session = Depends(get_db)):
""" """
Reset password Reset password
""" """

View File

@@ -10,13 +10,13 @@ from app.api.utils.db import get_db
from app.api.utils.security import get_current_active_superuser, get_current_active_user from app.api.utils.security import get_current_active_superuser, get_current_active_user
from app.core import config from app.core import config
from app.db_models.user import User as DBUser from app.db_models.user import User as DBUser
from app.models.user import User, UserInCreate, UserInDB, UserInUpdate from app.models.user import User, UserCreate, UserInDB, UserUpdate
from app.utils import send_new_account_email from app.utils import send_new_account_email
router = APIRouter() router = APIRouter()
@router.get("/users/", tags=["users"], response_model=List[User]) @router.get("/", response_model=List[User])
def read_users( def read_users(
db: Session = Depends(get_db), db: Session = Depends(get_db),
skip: int = 0, skip: int = 0,
@@ -24,21 +24,21 @@ def read_users(
current_user: DBUser = Depends(get_current_active_superuser), current_user: DBUser = Depends(get_current_active_superuser),
): ):
""" """
Retrieve users Retrieve users.
""" """
users = crud.user.get_multi(db, skip=skip, limit=limit) users = crud.user.get_multi(db, skip=skip, limit=limit)
return users return users
@router.post("/users/", tags=["users"], response_model=User) @router.post("/", response_model=User)
def create_user( def create_user(
*, *,
db: Session = Depends(get_db), db: Session = Depends(get_db),
user_in: UserInCreate, user_in: UserCreate,
current_user: DBUser = Depends(get_current_active_superuser), current_user: DBUser = Depends(get_current_active_superuser),
): ):
""" """
Create new user Create new user.
""" """
user = crud.user.get_by_email(db, email=user_in.email) user = crud.user.get_by_email(db, email=user_in.email)
if user: if user:
@@ -54,7 +54,7 @@ def create_user(
return user return user
@router.put("/users/me", tags=["users"], response_model=User) @router.put("/me", response_model=User)
def update_user_me( def update_user_me(
*, *,
db: Session = Depends(get_db), db: Session = Depends(get_db),
@@ -64,10 +64,10 @@ def update_user_me(
current_user: DBUser = Depends(get_current_active_user), current_user: DBUser = Depends(get_current_active_user),
): ):
""" """
Update own user Update own user.
""" """
current_user_data = jsonable_encoder(current_user) current_user_data = jsonable_encoder(current_user)
user_in = UserInUpdate(**current_user_data) user_in = UserUpdate(**current_user_data)
if password is not None: if password is not None:
user_in.password = password user_in.password = password
if full_name is not None: if full_name is not None:
@@ -78,18 +78,18 @@ def update_user_me(
return user return user
@router.get("/users/me", tags=["users"], response_model=User) @router.get("/me", response_model=User)
def read_user_me( def read_user_me(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: DBUser = Depends(get_current_active_user), current_user: DBUser = Depends(get_current_active_user),
): ):
""" """
Get current user Get current user.
""" """
return current_user return current_user
@router.post("/users/open", tags=["users"], response_model=User) @router.post("/open", response_model=User)
def create_user_open( def create_user_open(
*, *,
db: Session = Depends(get_db), db: Session = Depends(get_db),
@@ -98,7 +98,7 @@ def create_user_open(
full_name: str = Body(None), full_name: str = Body(None),
): ):
""" """
Create new user without the need to be logged in Create new user without the need to be logged in.
""" """
if not config.USERS_OPEN_REGISTRATION: if not config.USERS_OPEN_REGISTRATION:
raise HTTPException( raise HTTPException(
@@ -111,19 +111,19 @@ def create_user_open(
status_code=400, status_code=400,
detail="The user with this username already exists in the system", detail="The user with this username already exists in the system",
) )
user_in = UserInCreate(password=password, email=email, full_name=full_name) user_in = UserCreate(password=password, email=email, full_name=full_name)
user = crud.user.create(db, user_in=user_in) user = crud.user.create(db, user_in=user_in)
return user return user
@router.get("/users/{user_id}", tags=["users"], response_model=User) @router.get("/{user_id}", response_model=User)
def read_user_by_id( def read_user_by_id(
user_id: int, user_id: int,
current_user: DBUser = Depends(get_current_active_user), current_user: DBUser = Depends(get_current_active_user),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
Get a specific user by id Get a specific user by id.
""" """
user = crud.user.get(db, user_id=user_id) user = crud.user.get(db, user_id=user_id)
if user == current_user: if user == current_user:
@@ -135,19 +135,18 @@ def read_user_by_id(
return user return user
@router.put("/users/{user_id}", tags=["users"], response_model=User) @router.put("/{user_id}", response_model=User)
def update_user( def update_user(
*, *,
db: Session = Depends(get_db), db: Session = Depends(get_db),
user_id: int, user_id: int,
user_in: UserInUpdate, user_in: UserUpdate,
current_user: UserInDB = Depends(get_current_active_superuser), current_user: UserInDB = Depends(get_current_active_superuser),
): ):
""" """
Update a user Update a user.
""" """
user = crud.user.get(db, user_id=user_id) user = crud.user.get(db, user_id=user_id)
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,

View File

@@ -10,23 +10,23 @@ from app.utils import send_test_email
router = APIRouter() router = APIRouter()
@router.post("/test-celery/", tags=["utils"], response_model=Msg, status_code=201) @router.post("/test-celery/", response_model=Msg, status_code=201)
def test_celery( def test_celery(
msg: Msg, current_user: UserInDB = Depends(get_current_active_superuser) msg: Msg, current_user: UserInDB = Depends(get_current_active_superuser)
): ):
""" """
Test Celery worker Test Celery worker.
""" """
celery_app.send_task("app.worker.test_celery", args=[msg.msg]) celery_app.send_task("app.worker.test_celery", args=[msg.msg])
return {"msg": "Word received"} return {"msg": "Word received"}
@router.post("/test-email/", tags=["utils"], response_model=Msg, status_code=201) @router.post("/test-email/", response_model=Msg, status_code=201)
def test_email( def test_email(
email_to: EmailStr, current_user: UserInDB = Depends(get_current_active_superuser) email_to: EmailStr, current_user: UserInDB = Depends(get_current_active_superuser)
): ):
""" """
Test emails Test emails.
""" """
send_test_email(email_to=email_to) send_test_email(email_to=email_to)
return {"msg": "Test email sent"} return {"msg": "Test email sent"}

View File

@@ -1 +1 @@
from . import user from . import item, user

View File

@@ -0,0 +1,55 @@
from typing import List, Optional
from fastapi.encoders import jsonable_encoder
from sqlalchemy.orm import Session
from app.db_models.item import Item
from app.models.item import ItemCreate, ItemUpdate
def get(db_session: Session, *, id: int) -> Optional[Item]:
return db_session.query(Item).filter(Item.id == id).first()
def get_multi(db_session: Session, *, skip=0, limit=100) -> List[Optional[Item]]:
return db_session.query(Item).offset(skip).limit(limit).all()
def get_multi_by_owner(
db_session: Session, *, owner_id: int, skip=0, limit=100
) -> List[Optional[Item]]:
return (
db_session.query(Item)
.filter(Item.owner_id == owner_id)
.offset(skip)
.limit(limit)
.all()
)
def create(db_session: Session, *, item_in: ItemCreate, owner_id: int) -> Item:
item_in_data = jsonable_encoder(item_in)
item = Item(**item_in_data, owner_id=owner_id)
db_session.add(item)
db_session.commit()
db_session.refresh(item)
return item
def update(db_session: Session, *, item: Item, item_in: ItemUpdate) -> Item:
item_data = jsonable_encoder(item)
update_data = item_in.dict(skip_defaults=True)
for field in item_data:
if field in update_data:
setattr(item, field, update_data[field])
db_session.add(item)
db_session.commit()
db_session.refresh(item)
return item
def remove(db_session: Session, *, id: int):
item = db_session.query(Item).filter(Item.id == id).first()
db_session.delete(item)
db_session.commit()
return item

View File

@@ -1,21 +1,22 @@
from typing import List, Optional from typing import List, Optional
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from sqlalchemy.orm import Session
from app.core.security import get_password_hash, verify_password from app.core.security import get_password_hash, verify_password
from app.db_models.user import User from app.db_models.user import User
from app.models.user import UserInCreate, UserInUpdate from app.models.user import UserCreate, UserUpdate
def get(db_session, *, user_id: int) -> Optional[User]: def get(db_session: Session, *, user_id: int) -> Optional[User]:
return db_session.query(User).filter(User.id == user_id).first() return db_session.query(User).filter(User.id == user_id).first()
def get_by_email(db_session, *, email: str) -> Optional[User]: def get_by_email(db_session: Session, *, email: str) -> Optional[User]:
return db_session.query(User).filter(User.email == email).first() return db_session.query(User).filter(User.email == email).first()
def authenticate(db_session, *, email: str, password: str) -> Optional[User]: def authenticate(db_session: Session, *, email: str, password: str) -> Optional[User]:
user = get_by_email(db_session, email=email) user = get_by_email(db_session, email=email)
if not user: if not user:
return None return None
@@ -32,11 +33,11 @@ def is_superuser(user) -> bool:
return user.is_superuser return user.is_superuser
def get_multi(db_session, *, skip=0, limit=100) -> List[Optional[User]]: def get_multi(db_session: Session, *, skip=0, limit=100) -> List[Optional[User]]:
return db_session.query(User).offset(skip).limit(limit).all() return db_session.query(User).offset(skip).limit(limit).all()
def create(db_session, *, user_in: UserInCreate) -> User: def create(db_session: Session, *, user_in: UserCreate) -> User:
user = User( user = User(
email=user_in.email, email=user_in.email,
hashed_password=get_password_hash(user_in.password), hashed_password=get_password_hash(user_in.password),
@@ -49,13 +50,12 @@ def create(db_session, *, user_in: UserInCreate) -> User:
return user return user
def update(db_session, *, user: User, user_in: UserInUpdate) -> User: def update(db_session: Session, *, user: User, user_in: UserUpdate) -> User:
user_data = jsonable_encoder(user) user_data = jsonable_encoder(user)
update_data = user_in.dict(skip_defaults=True)
for field in user_data: for field in user_data:
if field in user_in.fields: if field in update_data:
value_in = getattr(user_in, field) setattr(user, field, update_data[field])
if value_in is not None:
setattr(user, field, value_in)
if user_in.password: if user_in.password:
passwordhash = get_password_hash(user_in.password) passwordhash = get_password_hash(user_in.password)
user.hashed_password = passwordhash user.hashed_password = passwordhash

View File

@@ -2,3 +2,4 @@
# imported by Alembic # imported by Alembic
from app.db.base_class import Base # noqa from app.db.base_class import Base # noqa
from app.db_models.user import User # noqa from app.db_models.user import User # noqa
from app.db_models.item import Item # noqa

View File

@@ -1,6 +1,11 @@
from app import crud from app import crud
from app.core import config from app.core import config
from app.models.user import UserInCreate from app.models.user import UserCreate
# make sure all SQL Alchemy models are imported before initializing DB
# otherwise, SQL Alchemy might fail to initialize properly relationships
# for more details: https://github.com/tiangolo/full-stack-fastapi-postgresql/issues/28
from app.db import base
def init_db(db_session): def init_db(db_session):
@@ -11,7 +16,7 @@ def init_db(db_session):
user = crud.user.get_by_email(db_session, email=config.FIRST_SUPERUSER) user = crud.user.get_by_email(db_session, email=config.FIRST_SUPERUSER)
if not user: if not user:
user_in = UserInCreate( user_in = UserCreate(
email=config.FIRST_SUPERUSER, email=config.FIRST_SUPERUSER,
password=config.FIRST_SUPERUSER_PASSWORD, password=config.FIRST_SUPERUSER_PASSWORD,
is_superuser=True, is_superuser=True,

View File

@@ -3,7 +3,7 @@ from sqlalchemy.orm import scoped_session, sessionmaker
from app.core import config from app.core import config
engine = create_engine(config.SQLALCHEMY_DATABASE_URI) engine = create_engine(config.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True)
db_session = scoped_session( db_session = scoped_session(
sessionmaker(autocommit=False, autoflush=False, bind=engine) sessionmaker(autocommit=False, autoflush=False, bind=engine)
) )

View File

@@ -0,0 +1,12 @@
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from app.db.base_class import Base
class Item(Base):
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
description = Column(String, index=True)
owner_id = Column(Integer, ForeignKey("user.id"))
owner = relationship("User", back_populates="items")

View File

@@ -1,4 +1,5 @@
from sqlalchemy import Boolean, Column, Integer, String from sqlalchemy import Boolean, Column, Integer, String
from sqlalchemy.orm import relationship
from app.db.base_class import Base from app.db.base_class import Base
@@ -10,3 +11,4 @@ class User(Base):
hashed_password = Column(String) hashed_password = Column(String)
is_active = Column(Boolean(), default=True) is_active = Column(Boolean(), default=True)
is_superuser = Column(Boolean(), default=False) is_superuser = Column(Boolean(), default=False)
items = relationship("Item", back_populates="owner")

View File

@@ -0,0 +1,34 @@
from pydantic import BaseModel
# Shared properties
class ItemBase(BaseModel):
title: str = None
description: str = None
# Properties to receive on item creation
class ItemCreate(ItemBase):
title: str
# Properties to receive on item update
class ItemUpdate(ItemBase):
pass
# Properties shared by models stored in DB
class ItemInDBBase(ItemBase):
id: int
title: str
owner_id: int
# Properties to return to client
class Item(ItemInDBBase):
pass
# Properties properties stored in DB
class ItemInDB(ItemInDBBase):
pass

View File

@@ -16,13 +16,13 @@ class UserBaseInDB(UserBase):
# Properties to receive via API on creation # Properties to receive via API on creation
class UserInCreate(UserBaseInDB): class UserCreate(UserBaseInDB):
email: str email: str
password: str password: str
# Properties to receive via API on update # Properties to receive via API on update
class UserInUpdate(UserBaseInDB): class UserUpdate(UserBaseInDB):
password: Optional[str] = None password: Optional[str] = None

View File

@@ -8,7 +8,7 @@ def test_celery_worker_test(superuser_token_headers):
server_api = get_server_api() server_api = get_server_api()
data = {"msg": "test"} data = {"msg": "test"}
r = requests.post( r = requests.post(
f"{server_api}{config.API_V1_STR}/test-celery/", f"{server_api}{config.API_V1_STR}/utils/test-celery/",
json=data, json=data,
headers=superuser_token_headers, headers=superuser_token_headers,
) )

View File

@@ -0,0 +1,34 @@
import requests
from app.core import config
from app.tests.utils.item import create_random_item
from app.tests.utils.utils import get_server_api
def test_create_item(superuser_token_headers):
server_api = get_server_api()
data = {"title": "Foo", "description": "Fighters"}
response = requests.post(
f"{server_api}{config.API_V1_STR}/items/",
headers=superuser_token_headers,
json=data,
)
content = response.json()
assert content["title"] == data["title"]
assert content["description"] == data["description"]
assert "id" in content
assert "owner_id" in content
def test_read_item(superuser_token_headers):
item = create_random_item()
server_api = get_server_api()
response = requests.get(
f"{server_api}{config.API_V1_STR}/items/{item.id}",
headers=superuser_token_headers,
)
content = response.json()
assert content["title"] == item.title
assert content["description"] == item.description
assert content["id"] == item.id
assert content["owner_id"] == item.owner_id

View File

@@ -3,7 +3,7 @@ import requests
from app import crud from app import crud
from app.core import config from app.core import config
from app.db.session import db_session from app.db.session import db_session
from app.models.user import UserInCreate from app.models.user import UserCreate
from app.tests.utils.user import user_authentication_headers from app.tests.utils.user import user_authentication_headers
from app.tests.utils.utils import get_server_api, random_lower_string from app.tests.utils.utils import get_server_api, random_lower_string
@@ -40,7 +40,7 @@ def test_get_existing_user(superuser_token_headers):
server_api = get_server_api() server_api = get_server_api()
username = random_lower_string() username = random_lower_string()
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate(email=username, password=password) user_in = UserCreate(email=username, password=password)
user = crud.user.create(db_session, user_in=user_in) user = crud.user.create(db_session, user_in=user_in)
user_id = user.id user_id = user.id
r = requests.get( r = requests.get(
@@ -58,7 +58,7 @@ def test_create_user_existing_username(superuser_token_headers):
username = random_lower_string() username = random_lower_string()
# username = email # username = email
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate(email=username, password=password) user_in = UserCreate(email=username, password=password)
user = crud.user.create(db_session, user_in=user_in) user = crud.user.create(db_session, user_in=user_in)
data = {"email": username, "password": password} data = {"email": username, "password": password}
r = requests.post( r = requests.post(
@@ -75,7 +75,7 @@ def test_create_user_by_normal_user():
server_api = get_server_api() server_api = get_server_api()
username = random_lower_string() username = random_lower_string()
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate(email=username, password=password) user_in = UserCreate(email=username, password=password)
user = crud.user.create(db_session, user_in=user_in) user = crud.user.create(db_session, user_in=user_in)
user_token_headers = user_authentication_headers(server_api, username, password) user_token_headers = user_authentication_headers(server_api, username, password)
data = {"email": username, "password": password} data = {"email": username, "password": password}
@@ -89,12 +89,12 @@ def test_retrieve_users(superuser_token_headers):
server_api = get_server_api() server_api = get_server_api()
username = random_lower_string() username = random_lower_string()
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate(email=username, password=password) user_in = UserCreate(email=username, password=password)
user = crud.user.create(db_session, user_in=user_in) user = crud.user.create(db_session, user_in=user_in)
username2 = random_lower_string() username2 = random_lower_string()
password2 = random_lower_string() password2 = random_lower_string()
user_in2 = UserInCreate(email=username2, password=password2) user_in2 = UserCreate(email=username2, password=password2)
user2 = crud.user.create(db_session, user_in=user_in2) user2 = crud.user.create(db_session, user_in=user_in2)
r = requests.get( r = requests.get(

View File

@@ -0,0 +1,61 @@
from app import crud
from app.models.item import ItemCreate, ItemUpdate
from app.tests.utils.user import create_random_user
from app.tests.utils.utils import random_lower_string
from app.db.session import db_session
def test_create_item():
title = random_lower_string()
description = random_lower_string()
item_in = ItemCreate(title=title, description=description)
user = create_random_user()
item = crud.item.create(db_session=db_session, item_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():
title = random_lower_string()
description = random_lower_string()
item_in = ItemCreate(title=title, description=description)
user = create_random_user()
item = crud.item.create(db_session=db_session, item_in=item_in, owner_id=user.id)
stored_item = crud.item.get(db_session=db_session, id=item.id)
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():
title = random_lower_string()
description = random_lower_string()
item_in = ItemCreate(title=title, description=description)
user = create_random_user()
item = crud.item.create(db_session=db_session, item_in=item_in, owner_id=user.id)
description2 = random_lower_string()
item_update = ItemUpdate(description=description2)
item2 = crud.item.update(
db_session=db_session, item=item, item_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():
title = random_lower_string()
description = random_lower_string()
item_in = ItemCreate(title=title, description=description)
user = create_random_user()
item = crud.item.create(db_session=db_session, item_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)
assert item3 is None
assert item2.id == item.id
assert item2.title == title
assert item2.description == description
assert item2.owner_id == user.id

View File

@@ -2,14 +2,14 @@ from fastapi.encoders import jsonable_encoder
from app import crud from app import crud
from app.db.session import db_session from app.db.session import db_session
from app.models.user import UserInCreate from app.models.user import UserCreate
from app.tests.utils.utils import random_lower_string from app.tests.utils.utils import random_lower_string
def test_create_user(): def test_create_user():
email = random_lower_string() email = random_lower_string()
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate(email=email, password=password) user_in = UserCreate(email=email, password=password)
user = crud.user.create(db_session, user_in=user_in) user = crud.user.create(db_session, user_in=user_in)
assert user.email == email assert user.email == email
assert hasattr(user, "hashed_password") assert hasattr(user, "hashed_password")
@@ -18,7 +18,7 @@ def test_create_user():
def test_authenticate_user(): def test_authenticate_user():
email = random_lower_string() email = random_lower_string()
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate(email=email, password=password) user_in = UserCreate(email=email, password=password)
user = crud.user.create(db_session, user_in=user_in) user = crud.user.create(db_session, user_in=user_in)
authenticated_user = crud.user.authenticate( authenticated_user = crud.user.authenticate(
db_session, email=email, password=password db_session, email=email, password=password
@@ -37,7 +37,7 @@ def test_not_authenticate_user():
def test_check_if_user_is_active(): def test_check_if_user_is_active():
email = random_lower_string() email = random_lower_string()
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate(email=email, password=password) user_in = UserCreate(email=email, password=password)
user = crud.user.create(db_session, user_in=user_in) user = crud.user.create(db_session, user_in=user_in)
is_active = crud.user.is_active(user) is_active = crud.user.is_active(user)
assert is_active is True assert is_active is True
@@ -46,7 +46,7 @@ def test_check_if_user_is_active():
def test_check_if_user_is_active_inactive(): def test_check_if_user_is_active_inactive():
email = random_lower_string() email = random_lower_string()
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate(email=email, password=password, disabled=True) user_in = UserCreate(email=email, password=password, disabled=True)
print(user_in) print(user_in)
user = crud.user.create(db_session, user_in=user_in) user = crud.user.create(db_session, user_in=user_in)
print(user) print(user)
@@ -58,7 +58,7 @@ def test_check_if_user_is_active_inactive():
def test_check_if_user_is_superuser(): def test_check_if_user_is_superuser():
email = random_lower_string() email = random_lower_string()
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate(email=email, password=password, is_superuser=True) user_in = UserCreate(email=email, password=password, is_superuser=True)
user = crud.user.create(db_session, user_in=user_in) user = crud.user.create(db_session, user_in=user_in)
is_superuser = crud.user.is_superuser(user) is_superuser = crud.user.is_superuser(user)
assert is_superuser is True assert is_superuser is True
@@ -67,7 +67,7 @@ def test_check_if_user_is_superuser():
def test_check_if_user_is_superuser_normal_user(): def test_check_if_user_is_superuser_normal_user():
username = random_lower_string() username = random_lower_string()
password = random_lower_string() password = random_lower_string()
user_in = UserInCreate(email=username, password=password) user_in = UserCreate(email=username, password=password)
user = crud.user.create(db_session, user_in=user_in) user = crud.user.create(db_session, user_in=user_in)
is_superuser = crud.user.is_superuser(user) is_superuser = crud.user.is_superuser(user)
assert is_superuser is False assert is_superuser is False
@@ -76,7 +76,7 @@ def test_check_if_user_is_superuser_normal_user():
def test_get_user(): def test_get_user():
password = random_lower_string() password = random_lower_string()
username = random_lower_string() username = random_lower_string()
user_in = UserInCreate(email=username, password=password, is_superuser=True) user_in = UserCreate(email=username, password=password, is_superuser=True)
user = crud.user.create(db_session, user_in=user_in) user = crud.user.create(db_session, user_in=user_in)
user_2 = crud.user.get(db_session, user_id=user.id) user_2 = crud.user.get(db_session, user_id=user.id)
assert user.email == user_2.email assert user.email == user_2.email

View File

@@ -0,0 +1,17 @@
from app import crud
from app.db.session import db_session
from app.models.item import ItemCreate
from app.tests.utils.user import create_random_user
from app.tests.utils.utils import random_lower_string
def create_random_item(owner_id: int = None):
if owner_id is None:
user = create_random_user()
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(
db_session=db_session, item_in=item_in, owner_id=owner_id
)

View File

@@ -1,6 +1,10 @@
import requests import requests
from app import crud
from app.core import config from app.core import config
from app.db.session import db_session
from app.models.user import UserCreate
from app.tests.utils.utils import random_lower_string
def user_authentication_headers(server_api, email, password): def user_authentication_headers(server_api, email, password):
@@ -11,3 +15,11 @@ def user_authentication_headers(server_api, email, password):
auth_token = response["access_token"] auth_token = response["access_token"]
headers = {"Authorization": f"Bearer {auth_token}"} headers = {"Authorization": f"Bearer {auth_token}"}
return headers return headers
def create_random_user():
email = random_lower_string()
password = random_lower_string()
user_in = UserCreate(username=email, email=email, password=password)
user = crud.user.create(db_session=db_session, user_in=user_in)
return user

View File

@@ -3,7 +3,7 @@ import logging
from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed 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 db_session
from app.tests.api.api_v1.test_token import test_get_access_token from app.tests.api.api_v1.test_login import test_get_access_token
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -5,4 +5,4 @@ set -x
autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place app --exclude=__init__.py 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 isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --apply app
black app black app
vulture app vulture app --min-confidence 70

View File

@@ -3,4 +3,4 @@ set -e
python /app/app/tests_pre_start.py python /app/app/tests_pre_start.py
pytest /app/app/tests/ pytest $* /app/app/tests/

View File

@@ -1,12 +1,12 @@
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.6 FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7
RUN pip install celery==4.2.1 passlib[bcrypt] tenacity requests emails "fastapi>=0.7.1" uvicorn gunicorn pyjwt python-multipart email_validator jinja2 psycopg2-binary alembic SQLAlchemy RUN pip install celery~=4.3 passlib[bcrypt] tenacity requests emails "fastapi>=0.16.0" uvicorn gunicorn pyjwt python-multipart email_validator jinja2 psycopg2-binary alembic SQLAlchemy
# For development, Jupyter remote kernel, Hydrogen # For development, Jupyter remote kernel, Hydrogen
# Using inside the container: # Using inside the container:
# jupyter notebook --ip=0.0.0.0 --allow-root # jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888
ARG env=prod ARG env=prod
RUN bash -c "if [ $env == 'dev' ] ; then pip install jupyter ; fi" RUN bash -c "if [ $env == 'dev' ] ; then pip install jupyterlab ; fi"
EXPOSE 8888 EXPOSE 8888
COPY ./app /app COPY ./app /app

View File

@@ -1,12 +1,12 @@
FROM python:3.6 FROM python:3.7
RUN pip install raven celery==4.2.1 passlib[bcrypt] tenacity requests "fastapi>=0.7.1" emails pyjwt email_validator jinja2 psycopg2-binary alembic SQLAlchemy RUN pip install raven celery~=4.3 passlib[bcrypt] tenacity requests "fastapi>=0.16.0" emails pyjwt email_validator jinja2 psycopg2-binary alembic SQLAlchemy
# For development, Jupyter remote kernel, Hydrogen # For development, Jupyter remote kernel, Hydrogen
# Using inside the container: # Using inside the container:
# jupyter notebook --ip=0.0.0.0 --allow-root # jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888
ARG env=prod ARG env=prod
RUN bash -c "if [ $env == 'dev' ] ; then pip install jupyter ; fi" RUN bash -c "if [ $env == 'dev' ] ; then pip install jupyterlab ; fi"
EXPOSE 8888 EXPOSE 8888
ENV C_FORCE_ROOT=1 ENV C_FORCE_ROOT=1

View File

@@ -1,12 +1,12 @@
FROM python:3.6 FROM python:3.7
RUN pip install requests pytest tenacity passlib[bcrypt] "fastapi>=0.7.1" psycopg2-binary SQLAlchemy RUN pip install requests pytest tenacity passlib[bcrypt] "fastapi>=0.16.0" psycopg2-binary SQLAlchemy
# For development, Jupyter remote kernel, Hydrogen # For development, Jupyter remote kernel, Hydrogen
# Using inside the container: # Using inside the container:
# jupyter notebook --ip=0.0.0.0 --allow-root # jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888
ARG env=prod ARG env=prod
RUN bash -c "if [ $env == 'dev' ] ; then pip install jupyter ; fi" RUN bash -c "if [ $env == 'dev' ] ; then pip install jupyterlab ; fi"
EXPOSE 8888 EXPOSE 8888
COPY ./app /app COPY ./app /app

View File

@@ -2,13 +2,13 @@ version: '3.3'
services: services:
backend: backend:
environment: environment:
- 'JUPYTER=jupyter notebook --ip=0.0.0.0 --allow-root' - JUPYTER=jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888
- SERVER_HOST=http://${DOMAIN} - SERVER_HOST=http://${DOMAIN}
celeryworker: celeryworker:
environment: environment:
- RUN=celery worker -A app.worker -l info -Q main-queue -c 1 - RUN=celery worker -A app.worker -l info -Q main-queue -c 1
- JUPYTER=jupyter notebook --ip=0.0.0.0 --allow-root - JUPYTER=jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888
- SERVER_HOST=http://${DOMAIN} - SERVER_HOST=http://${DOMAIN}
backend-tests: backend-tests:
environment: environment:
- JUPYTER=jupyter notebook --ip=0.0.0.0 --allow-root - JUPYTER=jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888