mirror of
https://github.com/kevin-DL/full-stack-fastapi-postgresql.git
synced 2026-01-14 11:04:41 +00:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44d8a4358b | ||
|
|
9b4108fdae | ||
|
|
b4fa418e65 | ||
|
|
a612765b83 | ||
|
|
de7140f1e7 | ||
|
|
546dc8bdcb | ||
|
|
eae33cda72 | ||
|
|
1d30172e7a | ||
|
|
6fc9a37eb5 | ||
|
|
170231783a | ||
|
|
5216fcfd77 | ||
|
|
8bf3607d2b | ||
|
|
8ce745b7ef | ||
|
|
6bbd58c76f | ||
|
|
1aeb3208bf | ||
|
|
45317e54c7 | ||
|
|
47e0fe56e3 | ||
|
|
42ee0fe0ba | ||
|
|
92b757fc96 | ||
|
|
bece399368 | ||
|
|
5dd83c6350 | ||
|
|
f365a4d026 | ||
|
|
ecd634e497 | ||
|
|
1fe4908b0a | ||
|
|
cd86803daa | ||
|
|
cf5516cda6 | ||
|
|
151f7ed79b | ||
|
|
9c23d69f36 | ||
|
|
f947ba8749 | ||
|
|
5cb50a9986 |
51
README.md
51
README.md
@@ -146,7 +146,56 @@ After using this generator, your new project (the directory created) will contai
|
|||||||
|
|
||||||
## Release Notes
|
## Release Notes
|
||||||
|
|
||||||
### Next
|
### 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
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
* Update generated README.
|
||||||
|
|
||||||
### 0.2.0
|
### 0.2.0
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|
||||||
@@ -69,7 +71,13 @@ The changes to those files only affect the local development environment, not th
|
|||||||
|
|
||||||
For example, the directory with the backend code is mounted as a Docker "host volume" (in the file `docker-compose.dev.volumes.yml`), mapping the code you change live to the directory inside the container. That allows you to test your changes right away, without having to build the Docker image again. It should only be done during development, for production, you should build the Docker image with a recent version of the backend code. But during development, it allows you to iterate very fast.
|
For example, the directory with the backend code is mounted as a Docker "host volume" (in the file `docker-compose.dev.volumes.yml`), mapping the code you change live to the directory inside the container. That allows you to test your changes right away, without having to build the Docker image again. It should only be done during development, for production, you should build the Docker image with a recent version of the backend code. But during development, it allows you to iterate very fast.
|
||||||
|
|
||||||
There is also a commented out `command` override (in the file `docker-compose.dev.command.yml`), if you want to enable it, uncomment it. It makes the backend container run a process that does "nothing", but keeps the process running. That allows you to get inside your living container and run commands inside, for example a Python interpreter to test installed dependencies, or start the development server that reloads when it detects changes.
|
There is a command override in the file `docker-compose.dev.command.yml` that runs `/start-reload.sh` (included in the base image) instead of the default `/start.sh` (also included in the base image). It starts a single server process (instead of multiple, as would be for production) and reloads the process whenever the code changes. As it is in `docker-compose.dev.command.yml`, it only applies to local development. Have in mind that if you have a syntax error and save the Python file, it will break and exit, and the container will stop. After that, you can restart the container by fixing the error and running again:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
There is also a commented out `command` override (in the file `docker-compose.dev.command.yml`), you can uncomment it and comment the default one. It makes the backend container run a process that does "nothing", but keeps the process running. That allows you to get inside your living container and run commands inside, for example a Python interpreter to test installed dependencies, or start the development server that reloads when it detects changes, or start a Jupyter Notebook session.
|
||||||
|
|
||||||
To get inside the container with a `bash` session you can start the stack with:
|
To get inside the container with a `bash` session you can start the stack with:
|
||||||
|
|
||||||
@@ -91,7 +99,7 @@ root@7f2607af31c3:/app#
|
|||||||
|
|
||||||
that means that you are in a `bash` session inside your container, as a `root` user, under the `/app` directory.
|
that means that you are in a `bash` session inside your container, as a `root` user, under the `/app` directory.
|
||||||
|
|
||||||
There is also a script `/start-reload.sh` to run the debug live reloading server. You can run that script from inside the container with:
|
There you use the script `/start-reload.sh` to run the debug live reloading server. You can run that script from inside the container with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash /start-reload.sh
|
bash /start-reload.sh
|
||||||
@@ -103,11 +111,11 @@ bash /start-reload.sh
|
|||||||
root@7f2607af31c3:/app# bash /start-reload.sh
|
root@7f2607af31c3:/app# bash /start-reload.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
and then hit enter. That runs the debugging server that auto reloads when it detects code changes.
|
and then hit enter. That runs the live reloading server that auto reloads when it detects code changes.
|
||||||
|
|
||||||
Nevertheless, if it doesn't detect a change but a syntax error, it will just stop with an error. But as the container is still alive and you are in a Bash session, you can quickly restart it after fixing the error, running the same command ("up arrow" and "Enter").
|
Nevertheless, if it doesn't detect a change but a syntax error, it will just stop with an error. But as the container is still alive and you are in a Bash session, you can quickly restart it after fixing the error, running the same command ("up arrow" and "Enter").
|
||||||
|
|
||||||
...this previous detail is what makes it useful to have the container alive doing nothing and then, in a Bash session, make it run the debugging server.
|
...this previous detail is what makes it useful to have the container alive doing nothing and then, in a Bash session, make it run the live reload server.
|
||||||
|
|
||||||
|
|
||||||
### Backend tests
|
### Backend tests
|
||||||
@@ -115,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/`.
|
||||||
|
|
||||||
@@ -126,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.
|
||||||
@@ -376,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:
|
||||||
@@ -393,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'
|
||||||
@@ -482,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**
|
||||||
@@ -497,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**
|
||||||
@@ -510,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
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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 = "*"
|
||||||
|
|||||||
1022
{{cookiecutter.project_slug}}/backend/app/Pipfile.lock
generated
1022
{{cookiecutter.project_slug}}/backend/app/Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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')
|
||||||
@@ -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"])
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
"""
|
"""
|
||||||
@@ -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 username (email)
|
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,
|
||||||
@@ -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"}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
from . import user
|
from . import item, user
|
||||||
|
|||||||
55
{{cookiecutter.project_slug}}/backend/app/app/crud/item.py
Normal file
55
{{cookiecutter.project_slug}}/backend/app/app/crud/item.py
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|||||||
12
{{cookiecutter.project_slug}}/backend/app/app/db_models/item.py
Executable file
12
{{cookiecutter.project_slug}}/backend/app/app/db_models/item.py
Executable 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")
|
||||||
@@ -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")
|
||||||
|
|||||||
34
{{cookiecutter.project_slug}}/backend/app/app/models/item.py
Normal file
34
{{cookiecutter.project_slug}}/backend/app/app/models/item.py
Normal 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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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(
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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__)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -10,3 +10,5 @@ services:
|
|||||||
--web
|
--web
|
||||||
# backend:
|
# backend:
|
||||||
# command: bash -c "while true; do sleep 1; done" # Infinite loop to keep container live doing nothing
|
# command: bash -c "while true; do sleep 1; done" # Infinite loop to keep container live doing nothing
|
||||||
|
backend:
|
||||||
|
command: /start-reload.sh
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ FROM nginx:1.15
|
|||||||
COPY --from=build-stage /app/dist/ /usr/share/nginx/html
|
COPY --from=build-stage /app/dist/ /usr/share/nginx/html
|
||||||
|
|
||||||
COPY --from=build-stage /nginx.conf /etc/nginx/conf.d/default.conf
|
COPY --from=build-stage /nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY ./nginx-backend-not-found.conf /etc/nginx/extra-conf.d/backend-not-found.conf
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
location /api {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
location /docs {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
location /redoc {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user