Skip to content

Commit

Permalink
Merge pull request #5 from rafsaf/fastapi-users-support
Browse files Browse the repository at this point in the history
Fastapi users support
  • Loading branch information
rafsaf authored Jan 30, 2022
2 parents 949dd71 + bd25047 commit 2cfd225
Show file tree
Hide file tree
Showing 88 changed files with 3,104 additions and 666 deletions.
63 changes: 47 additions & 16 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
python-version: [3.7, 3.8, 3.9]
python-version: ["3.9", "3.10"]

services:
postgres:
Expand All @@ -32,33 +32,64 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Load cached venv
id: cached-poetry-dependencies
- name: Load cached venv1
id: cached-poetry-dependencies1
uses: actions/cache@v2
with:
path: .venv
key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}
path: .venv1
key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('{{cookiecutter.project_name}}/template_minimal/poetry.lock') }}
- name: Install dependencies and actiavte virtualenv
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
if: steps.cached-poetry-dependencies1.outputs.cache-hit != 'true'
run: |
python -m venv .venv
source .venv/bin/activate
pip install -r {{cookiecutter.project_name}}/requirements-dev.txt
python -m venv .venv1
source .venv1/bin/activate
pip install -r {{cookiecutter.project_name}}/template_minimal/requirements-dev.txt
pip install cookiecutter
- name: Lint with flake8
- name: Load cached venv2
id: cached-poetry-dependencies2
uses: actions/cache@v2
with:
path: .venv2
key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('{{cookiecutter.project_name}}/template_fastapi_users/poetry.lock') }}
- name: Install dependencies and actiavte virtualenv
if: steps.cached-poetry-dependencies2.outputs.cache-hit != 'true'
run: |
python -m venv .venv2
source .venv2/bin/activate
pip install -r {{cookiecutter.project_name}}/template_fastapi_users/requirements-dev.txt
pip install cookiecutter
- name: Lint with flake8 minimal project
run: |
source .venv1/bin/activate
# stop the build if there are Python syntax errors or undefined names
cd \{\{cookiecutter.project_name\}\}/template_minimal
flake8 app --count --exit-zero --statistics
- name: Lint with flake8 fastapi_users project
run: |
source .venv/bin/activate
source .venv2/bin/activate
# stop the build if there are Python syntax errors or undefined names
cd \{\{cookiecutter.project_name\}\}/
cd \{\{cookiecutter.project_name\}\}/template_fastapi_users
flake8 app --count --exit-zero --statistics
- name: Test new cookiecuttered project is passing pytest test
- name: Test minimal project is passing pytest test
run: |
source .venv1/bin/activate
python tests/create_minimal_project.py
export TEST_DATABASE_HOSTNAME=localhost
export TEST_DATABASE_USER=test
export TEST_DATABASE_PASSWORD=test
export TEST_DATABASE_PORT=30000
export TEST_DATABASE_DB=test
pytest minimal_project
- name: Test fastapi_users project is passing pytest test
run: |
source .venv/bin/activate
python tests/create_test_project.py
source .venv2/bin/activate
python tests/create_fastapi_users_project.py
export TEST_DATABASE_HOSTNAME=localhost
export TEST_DATABASE_USER=test
export TEST_DATABASE_PASSWORD=test
export TEST_DATABASE_PORT=30000
export TEST_DATABASE_DB=test
pytest generated_project_for_tests
pytest fastapi_users_project
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
.vscode
.vscode
.venv
venv
.venv1
.venv2
43 changes: 34 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@
<img src="https://github.com/rafsaf/minimal-fastapi-postgres-template/workflows/tests/badge.svg" alt="Test">
</a>

<a href="https://github.com/rafsaf/respo/blob/main/LICENSE" target="_blank">
<img src="https://img.shields.io/github/license/rafsaf/respo" alt="License">
<a href="https://github.com/rafsaf/minimal-fastapi-postgres-template/blob/main/LICENSE" target="_blank">
<img src="https://img.shields.io/github/license/rafsaf/minimal-fastapi-postgres-template" alt="License">
</a>

## Minimal FastAPI Postgres Template

There are two templates to choose from, first **minimal template** is a minimal template for FastAPI backend + postgresql db as of 2021.11, async style for database sessions, endpoints and tests. It provides basic codebase that almost every application has, but nothing more.

Seconds one, **fastapi-users template** is similar to minimal in structure, but based on popular library [FastAPI Users](https://fastapi-users.github.io/fastapi-users/) and very powerful (yet it require using it for user accounts management).

:bangbang: | This repository contains two different templates to choose from.
:---: | :---

## Minimal async FastAPI + postgresql template

![OpenAPIexample](./docs/OpenAPI_example.png)


- SQLAlchemy using new 2.0 API + async queries
- Postgresql database under `asyncpg`
Expand All @@ -20,13 +29,24 @@
- `pre-push.sh` script with poetry export, autoflake, black, isort and flake8
- Setup for async tests, one func test for token flow and very extensible `conftest.py`

## What this repo is
![template-fastapi-minimal-openapi-example](./docs/template-minimal-openapi-example.png)

## Async FastAPI + postgresql template based on [fastapi-users](https://fastapi-users.github.io/fastapi-users/)

This is a minimal template for FastAPI backend + postgresql db as of 2021.11, `async` style for database sessions, endpoints and tests. It provides basic codebase that almost every application has, but nothing more.
- Extensible base user model
- Ready-to-use register, login, reset password and verify e-mail routes
- Ready-to-use social OAuth2 login flow
- SQLAlchemy using new 2.0 API + async queries
- Postgresql database under `asyncpg`
- Alembic migrations
- Very minimal project structure yet ready for quick start building new api
- Two databases in docker-compose.yml (second one for tests)
- poetry
- `pre-push.sh` script with poetry export, autoflake, black, isort and flake8
- Setup for async tests, one func test for token flow and very extensible `conftest.py`

## What this repo is not
![template-fastapi-users-openapi-example](./docs/template-fastapi-users-openapi-example.png)

It is not complex, full featured solutions for all human kind problems. It doesn't include any third party that isn't necessary for most of apps (dashboards, queues) or implementation differs so much in every project that it's pointless (complex User model, emails, RBAC, permissions).

## Quickstart

Expand All @@ -37,6 +57,9 @@ pip install cookiecutter
# And cookiecutter this project :)
cookiecutter https://github.com/rafsaf/minimal-fastapi-postgres-template

# decide if u want fastapi-users based template by
# changing "use_fastapi_users" in cookiecutter option to True, by default it is minimal template.

cd project_name
# Poetry install (and activate environment!)
poetry install
Expand All @@ -62,9 +85,11 @@ This project is heavily base on official template https://github.com/tiangolo/fu

`2.0` style SQLAlchemy API is good enough so there is no need to write everything in `crud` and waste our time... The `core` folder was also rewritten. There is great base for writting tests in `tests`, but I didn't want to write hundreds of them, I noticed that usually after changes in the structure of the project, auto tests are useless and you have to write them from scratch anyway (delete old ones...), hence less than more. Similarly with the `User` model, it is very modest, because it will be adapted to the project anyway (and there are no tests for these endpoints, you would remove them probably).

## Step by step example
On January 30 I added another template to this repository, that is based on [FastAPI Users](https://fastapi-users.github.io/fastapi-users/) project. The main assumptions have not changed and project structure is very similar. What it gives is not buggy, extensible, tested Users accounts managing system. This is a lot, if your project needs to manage accounts, this is definitely better than minimal template.

## Step by step example for minimal template

I always enjoy to to have some kind of example in templates (even if I don't like it much, _some_ parts may be useful and save my time...), so let's create `POST` endpoint for creating dogs.
I always enjoy to to have some kind of example in templates (even if I don't like it much, _some_ parts may be useful and save my time...), so let's create `POST` endpoint for creating dogs. For second template, there may be some differences (imports etc.)

### 1. Add `HappyDog` model

Expand Down
5 changes: 3 additions & 2 deletions cookiecutter.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"project_name": "Base Project"
}
"project_name": "Base Project",
"use_fastapi_users": false
}
Binary file removed docs/OpenAPI_example.png
Binary file not shown.
Binary file added docs/template-fastapi-users-openapi-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/template-minimal-openapi-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 38 additions & 6 deletions hooks/post_gen_project.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,44 @@
"""
Copy template choosen from TEMPLATES to main folder.
Copy content from env template (autogenerated passwords etc)
to .env which is gitignored, then remove template env file as it is redundant
to .env which is gitignored, then remove template env file as it is redundant.
"""

from pathlib import Path
from shutil import copytree, rmtree

PROJECT_NAME = "{{ cookiecutter.project_name }}"
USE_FASTAPI_USERS = "{{ cookiecutter.use_fastapi_users }}"
TEMPLATES = ["template_fastapi_users", "template_minimal"]


def copy_choosen_template_to_main_dir(used_template: str):
if used_template not in TEMPLATES:
raise ValueError(f"{used_template} not in {TEMPLATES}")

copytree(used_template, "./", dirs_exist_ok=True)

for template in TEMPLATES:
rmtree(Path(template))


def create_env_file_and_remove_env_template():
env_template_file = Path(".env.template")
env_file = Path(".env")

env_file.write_text(env_template_file.read_text())
env_template_file.unlink()


env_template_file = Path("./.env.template")
env_file = Path("./.env")
if __name__ == "__main__":
truthy = ["T", "t", "true", "True", 1]
falsy = ["F", "f", "false", "False", 0]
if USE_FASTAPI_USERS in truthy:
used_template = "template_fastapi_users"
elif USE_FASTAPI_USERS in falsy:
used_template = "template_minimal"
else:
raise ValueError(f"use_fastapi_users param must be in {truthy + falsy}")

env_file.write_text(env_template_file.read_text())
env_template_file.unlink()
copy_choosen_template_to_main_dir(used_template=used_template)
create_env_file_and_remove_env_template()
24 changes: 24 additions & 0 deletions tests/create_fastapi_users_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
Creates fastapi_users template project in root folder with default values
"""

from pathlib import Path

from cookiecutter.main import cookiecutter

ROOT_FOLDER = Path(__file__).parent.parent


def main():
cookiecutter(
template=str(ROOT_FOLDER),
no_input=True,
extra_context={
"project_name": "fastapi_users_project",
"use_fastapi_users": True,
},
)


if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Creates template project in root folder with default values
Creates minimal template project in root folder with default values
"""

from pathlib import Path
Expand All @@ -14,7 +14,8 @@ def main():
template=str(ROOT_FOLDER),
no_input=True,
extra_context={
"project_name": "generated_project_for_tests",
"project_name": "minimal_project",
"use_fastapi_users": False,
},
)

Expand Down
15 changes: 0 additions & 15 deletions {{cookiecutter.project_name}}/nginx-unit-config.json

This file was deleted.

19 changes: 19 additions & 0 deletions {{cookiecutter.project_name}}/template_fastapi_users/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
SECRET_KEY="DVnFmhwvjEhJZpuhndxjhlezxQPJmBIIkMDEmFREWQADPcUnrG"
ENVIRONMENT="DEV"
ACCESS_TOKEN_EXPIRE_MINUTES="11520"
BACKEND_CORS_ORIGINS="http://localhost:3000,http://localhost:8001"

DEFAULT_DATABASE_HOSTNAME="localhost"
DEFAULT_DATABASE_USER="rDGJeEDqAz"
DEFAULT_DATABASE_PASSWORD="XsPQhCoEfOQZueDjsILetLDUvbvSxAMnrVtgVZpmdcSssUgbvs"
DEFAULT_DATABASE_PORT="5387"
DEFAULT_DATABASE_DB="default_db"

TEST_DATABASE_HOSTNAME="localhost"
TEST_DATABASE_USER="test"
TEST_DATABASE_PASSWORD="ywRCUjJijmQoBmWxIfLldOoITPzajPSNvTvHyugQoSqGwNcvQE"
TEST_DATABASE_PORT="37270"
TEST_DATABASE_DB="test_db"

FIRST_SUPERUSER_EMAIL="[email protected]"
FIRST_SUPERUSER_PASSWORD="OdLknKQJMUwuhpAVHvRC"
19 changes: 19 additions & 0 deletions {{cookiecutter.project_name}}/template_fastapi_users/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
SECRET_KEY="{{ random_ascii_string(50) }}"
ENVIRONMENT="DEV"
ACCESS_TOKEN_EXPIRE_MINUTES="11520"
BACKEND_CORS_ORIGINS="http://localhost:3000,http://localhost:8001"

DEFAULT_DATABASE_HOSTNAME="localhost"
DEFAULT_DATABASE_USER="{{ random_ascii_string(10) }}"
DEFAULT_DATABASE_PASSWORD="{{ random_ascii_string(50) }}"
DEFAULT_DATABASE_PORT="{{ range(4000, 7000) | random }}"
DEFAULT_DATABASE_DB="default_db"

TEST_DATABASE_HOSTNAME="localhost"
TEST_DATABASE_USER="test"
TEST_DATABASE_PASSWORD="{{ random_ascii_string(50) }}"
TEST_DATABASE_PORT="{{ range(30000, 40000) | random }}"
TEST_DATABASE_DB="test_db"

FIRST_SUPERUSER_EMAIL="[email protected]"
FIRST_SUPERUSER_PASSWORD="{{ random_ascii_string(20) }}"
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
# See https://unit.nginx.org/installation/#docker-images

FROM nginx/unit:1.25.0-python3.9
FROM nginx/unit:1.26.1-python3.9

ENV PYTHONUNBUFFERED 1

# Nginx unit config and init.sh will be consumed at container startup.
COPY ./app/init.sh /docker-entrypoint.d/init.sh
COPY ./nginx-unit-config.json /docker-entrypoint.d/config.json
RUN chmod +x /docker-entrypoint.d/init.sh
RUN apt update && apt install -y python3-pip

# Build folder for our app, only stuff that matters copied.
RUN mkdir build
WORKDIR /build

COPY ./app ./app
COPY ./alembic ./alembic
COPY ./alembic.ini .
# Update, install requirements and then cleanup.
COPY ./requirements.txt .

# Update, install requirements and then cleanup.
RUN apt update && apt install -y python3-pip \
&& pip3 install -r requirements.txt \
RUN pip3 install -r requirements.txt \
&& apt remove -y python3-pip \
&& apt autoremove --purge -y \
&& rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/*.list
&& rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/*.list

# Copy the rest of app
COPY ./app ./app
COPY ./alembic ./alembic
COPY ./alembic.ini .

# Nginx unit config and init.sh will be consumed at container startup.
COPY ./app/init.sh /docker-entrypoint.d/init.sh
COPY ./nginx-unit-config.json /docker-entrypoint.d/config.json
RUN chmod a+x /docker-entrypoint.d/init.sh
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from sqlalchemy.ext.asyncio import AsyncEngine
from asyncio import get_event_loop

from app.core.config import settings
from app.core import config as app_config
from alembic import context

# this is the Alembic Config object, which provides
Expand All @@ -31,7 +31,7 @@


def get_database_uri():
return settings.DEFAULT_SQLALCHEMY_DATABASE_URI
return app_config.settings.DEFAULT_SQLALCHEMY_DATABASE_URI


def run_migrations_offline():
Expand Down
Loading

0 comments on commit 2cfd225

Please sign in to comment.