Skip to content

Commit c9e2aa2

Browse files
authored
Merge pull request #1 from rafsaf/new_version
New version
2 parents 76da97f + c5dff8f commit c9e2aa2

31 files changed

+585
-354
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.vscode

.vscode/settings.json

Lines changed: 0 additions & 4 deletions
This file was deleted.

README.md

Lines changed: 181 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
1-
# Full featured FastAPI template, all boring and tedious things are covered
1+
## Minimal async FastAPI + postgresql template
2+
3+
![OpenAPIexample](./docs/OpenAPI_example.png)
24

35
- SQLAlchemy using new 2.0 API + async queries
46
- Postgresql database under `asyncpg`
57
- Alembic migrations
68
- Very minimal project structure yet ready for quick start building new api
79
- Refresh token endpoint (not only access like in official template)
8-
- Two databases in docker-compose.yml (second for tests)
10+
- Two databases in docker-compose.yml (second one for tests)
911
- poetry
1012
- `pre-push.sh` script with poetry export, autoflake, black, isort and flake8
11-
- Setup for tests, one big test for token flow and very extensible `conftest.py`
13+
- Setup for async tests, one func test for token flow and very extensible `conftest.py`
14+
15+
## What this repo is
16+
17+
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.
18+
19+
## What this repo is not
1220

13-
# Quickstart
21+
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).
22+
23+
## Quickstart
1424

1525
```bash
16-
# You can install it globally
26+
# Install cookiecutter globally
1727
pip install cookiecutter
1828

1929
# And cookiecutter this project :)
@@ -22,24 +32,184 @@ cookiecutter https://github.com/rafsaf/minimal-fastapi-postgres-template
2232
cd project_name
2333
# Poetry install (and activate environment!)
2434
poetry install
25-
# Databases
35+
# Setup two databases
2636
docker-compose up -d
2737
# Alembic migrations upgrade and initial_data.py script
2838
bash init.sh
2939
# And this is it:
30-
uvicorn app.main:app
40+
uvicorn app.main:app --reload
3141
```
3242

3343
tests:
3444

3545
```bash
46+
# Note, it will use second database declared in docker-compose.yml, not default one
3647
pytest
37-
# Note, it will use second database declared in docker-compose.yml, not default one like
38-
# in official template
48+
3949
```
4050

41-
# About
51+
## About
4252

4353
This project is heavily base on official template https://github.com/tiangolo/full-stack-fastapi-postgresql (and on my previous work: [link1](https://github.com/rafsaf/fastapi-plan), [link2](https://github.com/rafsaf/docker-fastapi-projects)), but as it is now not too much up-to-date, it is much easier to create new one than change official. I didn't like some of conventions over there also (`crud` and `db` folders for example).
4454

45-
`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, 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)
55+
`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).
56+
57+
## Step by step example
58+
59+
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.
60+
61+
### 1. Add `HappyDog` model
62+
63+
```python
64+
# /app/models.py
65+
(...)
66+
67+
class HappyDog(Base):
68+
__tablename__ = "happy_dog"
69+
id = Column(Integer, primary_key=True, index=True)
70+
puppy_name = Column(String(500))
71+
puppy_age = Column(Integer)
72+
```
73+
74+
### 2. Create and apply alembic migrations
75+
76+
```bash
77+
# Run
78+
alembic revision --autogenerate -m "add_happy_dog"
79+
80+
# Somethig like `YYYY-MM-DD-....py` will appear in `/alembic/versions` folder
81+
82+
alembic upgrade head
83+
84+
# (...)
85+
# INFO [alembic.runtime.migration] Running upgrade cefce371682e -> 038f530b0e9b, add_happy_dog
86+
```
87+
88+
PS. Note, alembic is configured in a way that it work with async setup and also detects specific column changes.
89+
90+
### 3. Create schemas
91+
92+
```python
93+
# /app/schemas/happy_dog.py
94+
95+
from typing import Optional
96+
97+
from pydantic import BaseModel
98+
99+
100+
class BaseHappyDog(BaseModel):
101+
puppy_name: str
102+
puppy_age: Optional[int]
103+
104+
105+
class CreateHappyDog(BaseHappyDog):
106+
pass
107+
108+
109+
class HappyDog(BaseHappyDog):
110+
id: int
111+
112+
```
113+
114+
Then add it to schemas `__init__.py`
115+
116+
```python
117+
# /app/schemas/__init__.py
118+
119+
from .token import Token, TokenPayload, TokenRefresh
120+
from .user import User, UserCreate, UserUpdate
121+
from .happy_dog import HappyDog, CreateHappyDog
122+
```
123+
124+
### 4. Create endpoint
125+
126+
```python
127+
# /app/api/endpoints/dogs.py
128+
129+
from typing import Any
130+
from fastapi import APIRouter, Depends
131+
from sqlalchemy.ext.asyncio import AsyncSession
132+
133+
from app import models, schemas
134+
from app.api import deps
135+
136+
router = APIRouter()
137+
138+
139+
@router.post("/", response_model=schemas.HappyDog, status_code=201)
140+
async def create_happy_dog(
141+
dog_create: schemas.CreateHappyDog,
142+
session: AsyncSession = Depends(deps.get_session),
143+
current_user: models.User = Depends(deps.get_current_active_user),
144+
) -> Any:
145+
"""
146+
Creates new happy dog. Only for logged users.
147+
"""
148+
new_dog = models.HappyDog(
149+
puppy_name=dog_create.puppy_name, puppy_age=dog_create.puppy_age
150+
)
151+
152+
session.add(new_dog)
153+
await session.commit()
154+
await session.refresh(new_dog)
155+
156+
return new_dog
157+
158+
```
159+
160+
Also, add it to router
161+
162+
```python
163+
# /app/api/api.py
164+
165+
from fastapi import APIRouter
166+
167+
from app.api.endpoints import auth, users, dogs
168+
169+
api_router = APIRouter()
170+
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
171+
api_router.include_router(users.router, prefix="/users", tags=["users"])
172+
# new content below
173+
api_router.include_router(dogs.router, prefix="/dogs", tags=["dogs"])
174+
175+
```
176+
177+
### 5. Test it simply
178+
179+
```python
180+
# /app/tests/test_dogs.py
181+
182+
import pytest
183+
from httpx import AsyncClient
184+
from app.models import User
185+
186+
pytestmark = pytest.mark.asyncio
187+
188+
189+
async def test_dog_endpoint(client: AsyncClient, default_user: User):
190+
# better to create fixture auth_client or similar than repeat code with access_token
191+
access_token = await client.post(
192+
"/auth/access-token",
193+
data={
194+
"username": "user@email.com",
195+
"password": "password",
196+
},
197+
headers={"Content-Type": "application/x-www-form-urlencoded"},
198+
)
199+
assert access_token.status_code == 200
200+
access_token = access_token.json()["access_token"]
201+
202+
puppy_name = "Sonia"
203+
puppy_age = 6
204+
205+
create_dog = await client.post(
206+
"/dogs/",
207+
json={"puppy_name": puppy_name, "puppy_age": puppy_age},
208+
headers={"Authorization": f"Bearer {access_token}"},
209+
)
210+
assert create_dog.status_code == 201
211+
create_dog_json = create_dog.json()
212+
assert create_dog_json["puppy_name"] == puppy_name
213+
assert create_dog_json["puppy_age"] == puppy_age
214+
215+
```

docs/OpenAPI_example.png

55.2 KB
Loading
Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1-
DEBUG=true
2-
SECRET_KEY=string
3-
ACCESS_TOKEN_EXPIRE_MINUTES=11520
1+
SECRET_KEY="DVnFmhwvjEhJZpuhndxjhlezxQPJmBIIkMDEmFREWQADPcUnrG"
2+
ENVIRONMENT="DEV"
3+
ACCESS_TOKEN_EXPIRE_MINUTES="11520"
4+
REFRESH_TOKEN_EXPIRE_MINUTES="40320"
5+
BACKEND_CORS_ORIGINS="http://localhost:3000,http://localhost:8001"
46

7+
DEFAULT_DATABASE_HOSTNAME="localhost"
8+
DEFAULT_DATABASE_USER="rDGJeEDqAz"
9+
DEFAULT_DATABASE_PASSWORD="XsPQhCoEfOQZueDjsILetLDUvbvSxAMnrVtgVZpmdcSssUgbvs"
10+
DEFAULT_DATABASE_PORT="5387"
11+
DEFAULT_DATABASE_DB="default_db"
512

6-
VERSION="0.1.0"
7-
DESCRIPTION=string
8-
PROJECT_NAME=string
9-
API_STR=
10-
BACKEND_CORS_ORIGINS=http://localhost:3000,http://localhost:8001
13+
TEST_DATABASE_HOSTNAME="localhost"
14+
TEST_DATABASE_USER="test"
15+
TEST_DATABASE_PASSWORD="ywRCUjJijmQoBmWxIfLldOoITPzajPSNvTvHyugQoSqGwNcvQE"
16+
TEST_DATABASE_PORT="37270"
17+
TEST_DATABASE_DB="test_db"
1118

12-
POSTGRES_USER=postgres
13-
POSTGRES_PASSWORD=strong_password
14-
POSTGRES_SERVER=db
15-
POSTGRES_DB=db
16-
POSTGRES_PORT=4999
17-
18-
FIRST_SUPERUSER_EMAIL=example@example.com
19-
FIRST_SUPERUSER_PASSWORD=string_password
19+
FIRST_SUPERUSER_EMAIL="example@example.com"
20+
FIRST_SUPERUSER_PASSWORD="OdLknKQJMUwuhpAVHvRC"
Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1-
DEBUG=true
21
SECRET_KEY="{{ random_ascii_string(50) }}"
3-
ACCESS_TOKEN_EXPIRE_MINUTES=11520
2+
ENVIRONMENT="DEV"
3+
ACCESS_TOKEN_EXPIRE_MINUTES="11520"
4+
REFRESH_TOKEN_EXPIRE_MINUTES="40320"
5+
BACKEND_CORS_ORIGINS="http://localhost:3000,http://localhost:8001"
46

7+
DEFAULT_DATABASE_HOSTNAME="localhost"
8+
DEFAULT_DATABASE_USER="{{ random_ascii_string(10) }}"
9+
DEFAULT_DATABASE_PASSWORD="{{ random_ascii_string(50) }}"
10+
DEFAULT_DATABASE_PORT="{{ range(4000, 7000) | random }}"
11+
DEFAULT_DATABASE_DB="default_db"
512

6-
VERSION="0.1.0"
7-
DESCRIPTION="{{ cookiecutter.project_name }}"
8-
PROJECT_NAME="{{ cookiecutter.project_name }}"
9-
API_STR=
10-
BACKEND_CORS_ORIGINS=http://localhost:3000,http://localhost:8001
13+
TEST_DATABASE_HOSTNAME="localhost"
14+
TEST_DATABASE_USER="test"
15+
TEST_DATABASE_PASSWORD="{{ random_ascii_string(50) }}"
16+
TEST_DATABASE_PORT="{{ range(30000, 40000) | random }}"
17+
TEST_DATABASE_DB="test_db"
1118

12-
POSTGRES_USER=postgres
13-
POSTGRES_PASSWORD="{{ random_ascii_string(50) }}"
14-
POSTGRES_SERVER=db
15-
POSTGRES_DB=db
16-
POSTGRES_PORT=4999
17-
18-
FIRST_SUPERUSER_EMAIL=example@example.com
19-
FIRST_SUPERUSER_PASSWORD="{{ random_ascii_string(20) }}"
19+
FIRST_SUPERUSER_EMAIL="example@example.com"
20+
FIRST_SUPERUSER_PASSWORD="{{ random_ascii_string(20) }}"

{{cookiecutter.project_name}}/.flake8

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ ignore = E203, E501, W503
55
exclude =
66
__init__.py,
77
.venv,
8+
venv,
89
__pycache__,
910
.github,
1011
.vscode,
11-
config,
12-
locale,
13-
webhook
14-
app/tests/conftest.py

{{cookiecutter.project_name}}/.gitignore

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
# vscode
2-
.vscode
31

42
# postgresql
53
data
6-
data-debug
7-
tests_data
4+
default_database_data
5+
test_database_data
86

97
# Byte-compiled / optimized / DLL files
108
__pycache__/
@@ -14,7 +12,6 @@ __pycache__/
1412

1513
# C extensions
1614
*.so
17-
.env
1815

1916
# Distribution / packaging
2017
.Python

{{cookiecutter.project_name}}/Dockerfile

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
1+
# See https://unit.nginx.org/installation/#docker-images
2+
13
FROM nginx/unit:1.25.0-python3.9
24

3-
# Our Debian with Python and Nginx for python apps.
4-
# See https://hub.docker.com/r/nginx/unit/
55
ENV PYTHONUNBUFFERED 1
66

7-
COPY ./app/initial.sh /docker-entrypoint.d/initial.sh
8-
COPY ./config/config.json /docker-entrypoint.d/config.json
7+
# Nginx unit config and init.sh will be consumed at container startup.
8+
COPY ./app/init.sh /docker-entrypoint.d/init.sh
9+
COPY ./nginx-unit-config.json /docker-entrypoint.d/config.json
10+
RUN chmod +x /docker-entrypoint.d/init.sh
911

12+
# Build folder for our app, only stuff that matters copied.
1013
RUN mkdir build
11-
12-
# We create folder named build for our app.
1314
WORKDIR /build
1415

1516
COPY ./app ./app
17+
COPY ./alembic ./alembic
1618
COPY ./alembic.ini .
1719
COPY ./requirements.txt .
1820

19-
# We copy our app folder to the /build
20-
21+
# Update, install requirements and then cleanup.
2122
RUN apt update && apt install -y python3-pip \
2223
&& pip3 install -r requirements.txt \
2324
&& apt remove -y python3-pip \

{{cookiecutter.project_name}}/alembic.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
script_location = alembic
66

77
# template used to generate migration files
8-
file_template = %%(year)d%%(month).2d%%(day).2d%%(hour).2d%%(minute).2d%%(second).2d_%%(rev)s_%%(slug)s
8+
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d_%%(slug)s__%%(rev)s
99

1010
# sys.path path, will be prepended to sys.path if present.
1111
# defaults to the current working directory.

0 commit comments

Comments
 (0)