Skip to content

Commit c5dff8f

Browse files
committed
readme and better conftest, moved fixture there
1 parent 4eb1b47 commit c5dff8f

File tree

3 files changed

+178
-25
lines changed

3 files changed

+178
-25
lines changed

README.md

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## Small FastAPI template, all boring and tedious things covered
1+
## Minimal async FastAPI + postgresql template
22

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

@@ -57,3 +57,159 @@ This project is heavily base on official template https://github.com/tiangolo/fu
5757
## Step by step example
5858

5959
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+
```

{{cookiecutter.project_name}}/app/tests/conftest.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import asyncio
2-
from typing import AsyncGenerator
2+
from typing import AsyncGenerator, Optional
33

44
import pytest
55
from httpx import AsyncClient
6+
from sqlalchemy import select
67
from sqlalchemy.ext.asyncio import AsyncSession
78

89
from app.core.config import settings
10+
from app.core.security import get_password_hash
911
from app.main import app
10-
from app.models import Base
12+
from app.models import Base, User
1113
from app.session import async_engine, async_session
1214

1315

@@ -42,3 +44,20 @@ async def test_db_setup_sessionmaker():
4244
async def session(test_db_setup_sessionmaker) -> AsyncGenerator[AsyncSession, None]:
4345
async with test_db_setup_sessionmaker() as session:
4446
yield session
47+
48+
49+
@pytest.fixture
50+
async def default_user(session: AsyncSession):
51+
result = await session.execute(select(User).where(User.email == "user@email.com"))
52+
user: Optional[User] = result.scalars().first()
53+
if user is None:
54+
new_user = User(
55+
email="user@email.com",
56+
hashed_password=get_password_hash("password"),
57+
full_name="fullname",
58+
)
59+
session.add(new_user)
60+
await session.commit()
61+
await session.refresh(new_user)
62+
return new_user
63+
return user

{{cookiecutter.project_name}}/app/tests/test_auth.py

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,12 @@
1-
from typing import Optional
2-
31
import pytest
42
from httpx import AsyncClient
5-
from sqlalchemy import select
6-
from sqlalchemy.ext.asyncio import AsyncSession
73

8-
from app.core.security import get_password_hash
94
from app.models import User
105

116
# All test coroutines in file will be treated as marked (async allowed).
127
pytestmark = pytest.mark.asyncio
138

149

15-
@pytest.fixture
16-
async def default_user(session: AsyncSession):
17-
result = await session.execute(select(User).where(User.email == "user@email.com"))
18-
user: Optional[User] = result.scalars().first()
19-
if user is None:
20-
new_user = User(
21-
email="user@email.com",
22-
hashed_password=get_password_hash("password"),
23-
full_name="fullname",
24-
)
25-
session.add(new_user)
26-
await session.commit()
27-
await session.refresh(new_user)
28-
return new_user
29-
return user
30-
31-
3210
async def test_login_endpoints(client: AsyncClient, default_user: User):
3311

3412
# access-token endpoint

0 commit comments

Comments
 (0)