Tortoise ORM support (#59)

* add tortoise to dependencies

* add tortoise as optional dependency in pyproject.toml

* add tortoise support (tests needed)

* Add tortoise support (also defined orm_mode in pydantic model

* tests for tortoise support

* format by black

* docs for tortoise

* delete type annotations

* delete underscore

* do it in 1 line

* add 1 line before yield

* fix in docs

* fix bug and add annotation for test

* Tweak documentation and fix Tortoise error about id update

* Improve Tortoise coverage by using get instead of filter

* Fix Pipfile.lock
This commit is contained in:
prostomarkeloff
2019-12-03 23:24:27 +03:00
committed by François Voron
parent 358150bbff
commit b5b0bbbb01
14 changed files with 300 additions and 2 deletions

2
.gitignore vendored
View File

@ -48,7 +48,7 @@ coverage.xml
.pytest_cache/
junit/
junit.xml
test.db
test.db*
# Translations
*.mo

View File

@ -34,6 +34,7 @@ databases = "==0.2.6"
pyjwt = "==1.7.1"
python-multipart = "==0.0.5"
motor = "==2.0.0"
tortoise-orm = "==0.15.1"
[requires]
python_version = "3.7"

35
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "4d00b916bd84a62b9047900bbec33a5eecf31c43baa93058122fb4b73bceedc4"
"sha256": "78d0c10e48c0662beb7525d9173388d78804f2a9ba7e4d993b50c63e9d57059b"
},
"pipfile-spec": 6,
"requires": {
@ -16,6 +16,12 @@
]
},
"default": {
"aiosqlite": {
"hashes": [
"sha256:ad84fbd7516ca7065d799504fc41d6845c938e5306d1b7dd960caaeda12e22a9"
],
"version": "==0.10.0"
},
"bcrypt": {
"hashes": [
"sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89",
@ -77,6 +83,12 @@
],
"version": "==1.13.2"
},
"ciso8601": {
"hashes": [
"sha256:307342e8bb362ae41a3f3a089c11b374116823bce6fbe5d784e2a2dc37f2c753"
],
"version": "==2.1.2"
},
"databases": {
"hashes": [
"sha256:a04db1d158a91db7bd49db16e14266e8e6c7336f06f88c700147690683c769a3"
@ -198,6 +210,12 @@
],
"version": "==3.9.0"
},
"pypika": {
"hashes": [
"sha256:2d23365f7d30e313d6d3f9a1670f2ac9ddb72b391a21ad4737644ace797c6ae1"
],
"version": "==0.35.16"
},
"python-multipart": {
"hashes": [
"sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"
@ -224,6 +242,21 @@
"sha256:c2ac9a42e0e0328ad20fe444115ac5e3760c1ee2ac1ff8cdb5ec915c4a453411"
],
"version": "==0.12.9"
},
"tortoise-orm": {
"hashes": [
"sha256:590a036f224cc3627fbcd950b7492ec98b8411df3491b725eb49c8b5edeaf54c"
],
"index": "pypi",
"version": "==0.15.1"
},
"typing-extensions": {
"hashes": [
"sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2",
"sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d",
"sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575"
],
"version": "==3.7.4.1"
}
},
"develop": {

View File

@ -0,0 +1,53 @@
# Tortoise ORM
**FastAPI Users** provides the necessary tools to work with Tortoise ORM.
## Installation
Install the database driver that corresponds to your DBMS:
```sh
pip install asyncpg
```
```sh
pip install aiomysql
```
```sh
pip install aiosqlite
```
For the sake of this tutorial from now on, we'll use a simple SQLite databse.
## Setup User table
Let's declare our User model.
```py hl_lines="9 10"
{!./src/db_tortoise.py!}
```
As you can see, **FastAPI Users** provides a mixin that will include base fields for our User table. You can of course add you own fields there to fit to your needs!
## Create the database adapter
The database adapter of **FastAPI Users** makes the link between your database configuration and the users logic. Create it like this.
```py hl_lines="13"
{!./src/db_tortoise.py!}
```
## Register Tortoise
For using Tortoise ORM we must register our models and database.
Tortoise ORM supports integration with Starlette/FastAPI out-of-the-box. It will automatically bind startup and shutdown events.
```py hl_lines="16"
{!./src/db_tortoise.py!}
```
## Next steps
We will now configure an [authentication method](../authentication/index.md).

View File

@ -10,6 +10,10 @@ Here is a full working example with JWT authentication to help get you started.
{!./src/full_mongodb.py!}
```
```py tab="Tortoise ORM"
{!./src/full_tortoise.py!}
```
## What now?
You're ready to go! Be sure to check the [Usage](../usage/routes.md) section to understand how yo work with **FastAPI Users**.

View File

@ -28,3 +28,5 @@ Depending on your database backend, database configuration will differ a bit.
[I'm using SQLAlchemy](databases/sqlalchemy.md)
[I'm using MongoDB](databases/mongodb.md)
[I'm using Tortoise ORM](databases/tortoise.md)

View File

@ -14,6 +14,12 @@ pip install fastapi-users[sqlalchemy]
pip install fastapi-users[mongodb]
```
## With Tortoise ORM support
```sh
pip install fastapi-users[tortoise-orm]
```
---
That's it! Now, let's have a look at our [User model](./configuration/model.md).

16
docs/src/db_tortoise.py Normal file
View File

@ -0,0 +1,16 @@
from fastapi import FastAPI
from fastapi_users.db.tortoise import BaseUserModel, TortoiseUserDatabase
from tortoise import Model
from tortoise.contrib.starlette import register_tortoise
DATABASE_URL = "sqlite://./test.db"
class UserModel(BaseUserModel, Model):
pass
user_db = TortoiseUserDatabase(UserModel)
app = FastAPI()
register_tortoise(app, modules={"models": ["path_to_your_package"]})

36
docs/src/full_tortoise.py Normal file
View File

@ -0,0 +1,36 @@
from fastapi import FastAPI
from fastapi_users import BaseUser, FastAPIUsers
from fastapi_users.authentication import JWTAuthentication
from fastapi_users.db.tortoise import BaseUserModel, TortoiseUserDatabase
from tortoise import Model
from tortoise.contrib.starlette import register_tortoise
DATABASE_URL = "sqlite://./test.db"
SECRET = "SECRET"
class UserModel(BaseUserModel, Model):
pass
class User(BaseUser):
pass
auth = JWTAuthentication(secret=SECRET, lifetime_seconds=3600)
user_db = TortoiseUserDatabase(UserModel)
app = FastAPI()
register_tortoise(app, db_url=DATABASE_URL, modules={"models": ["test"]})
fastapi_users = FastAPIUsers(user_db, auth, User, SECRET)
app.include_router(fastapi_users.router, prefix="/users", tags=["users"])
@fastapi_users.on_after_register()
def on_after_register(user: User):
print(f"User {user.id} has registered.")
@fastapi_users.on_after_forgot_password()
def on_after_forgot_password(user: User, token: str):
print(f"User {user.id} has forgot their password. Reset token: {token}")

View File

@ -0,0 +1,58 @@
from typing import List, Optional, Type
from tortoise import Model, fields
from tortoise.exceptions import DoesNotExist
from fastapi_users.db import BaseUserDatabase
from fastapi_users.models import BaseUserDB
class BaseUserModel:
id = fields.TextField(pk=True, generated=False)
email = fields.CharField(index=True, unique=True, null=False, max_length=255)
hashed_password = fields.CharField(null=False, max_length=255)
is_active = fields.BooleanField(default=True, null=False)
is_superuser = fields.BooleanField(default=False, null=False)
class Meta:
table = "user"
class TortoiseUserDatabase(BaseUserDatabase):
model: Type[Model]
def __init__(self, model: Type[Model]):
self.model = model
async def list(self) -> List[BaseUserDB]:
users = await self.model.all()
return [BaseUserDB.from_orm(user) for user in users]
async def get(self, id: str) -> Optional[BaseUserDB]:
try:
user = await self.model.get(id=id)
return BaseUserDB.from_orm(user)
except DoesNotExist:
return None
async def get_by_email(self, email: str) -> Optional[BaseUserDB]:
try:
user = await self.model.get(email=email)
return BaseUserDB.from_orm(user)
except DoesNotExist:
return None
async def create(self, user: BaseUserDB) -> BaseUserDB:
model = self.model(**user.dict())
await model.save()
return user
async def update(self, user: BaseUserDB) -> BaseUserDB:
user_dict = user.dict()
user_dict.pop("id") # Tortoise complains if we pass the PK again
await self.model.filter(id=user.id).update(**user_dict)
return user
async def delete(self, user: BaseUserDB) -> None:
await self.model.filter(id=user.id).delete()

View File

@ -25,6 +25,9 @@ class BaseUser(BaseModel):
def create_update_dict_superuser(self):
return self.dict(exclude_unset=True, exclude={"id"})
class Config:
orm_mode = True
class BaseUserCreate(BaseUser):
email: EmailStr

View File

@ -32,6 +32,7 @@ nav:
- Databases:
- configuration/databases/sqlalchemy.md
- configuration/databases/mongodb.md
- configuration/databases/tortoise.md
- Authentication:
- configuration/authentication/index.md
- configuration/authentication/jwt.md

View File

@ -36,6 +36,9 @@ sqlalchemy = [
mongodb = [
"motor ==2.0.0",
]
tortoise-orm = [
"tortoise-orm ==0.15.1"
]
[tool.flit.metadata.urls]
Documentation = "https://frankie567.github.io/fastapi-users/"

82
tests/test_db_tortoise.py Normal file
View File

@ -0,0 +1,82 @@
from typing import AsyncGenerator
import pytest
from tortoise import Model
from tortoise.exceptions import IntegrityError
from tortoise import Tortoise
from fastapi_users.db.tortoise import TortoiseUserDatabase, BaseUserModel
from fastapi_users.models import BaseUserDB
from fastapi_users.password import get_password_hash
class User(BaseUserModel, Model):
pass
@pytest.fixture
async def tortoise_user_db() -> AsyncGenerator[TortoiseUserDatabase, None]:
DATABASE_URL = "sqlite://./test.db"
await Tortoise.init(
db_url=DATABASE_URL, modules={"models": ["tests.test_db_tortoise"]}
)
await Tortoise.generate_schemas()
yield TortoiseUserDatabase(User)
await User.all().delete()
await Tortoise.close_connections()
@pytest.mark.asyncio
@pytest.mark.db
async def test_queries(tortoise_user_db: TortoiseUserDatabase):
user = BaseUserDB(
id="111",
email="lancelot@camelot.bt",
hashed_password=get_password_hash("guinevere"),
)
# Create
user_db = await tortoise_user_db.create(user)
assert user_db.id is not None
assert user_db.is_active is True
assert user_db.is_superuser is False
assert user_db.email == user.email
# Update
user_db.is_superuser = True
await tortoise_user_db.update(user_db)
# Get by id
id_user = await tortoise_user_db.get(user.id)
assert id_user.id == user_db.id
assert id_user.is_superuser is True
# Get by email
email_user = await tortoise_user_db.get_by_email(user.email)
assert email_user.id == user_db.id
# List
users = await tortoise_user_db.list()
assert len(users) == 1
first_user = users[0]
assert first_user.id == user_db.id
# Exception when inserting existing email
with pytest.raises(IntegrityError):
await tortoise_user_db.create(user)
# Exception when inserting non-nullable fields
with pytest.raises(ValueError):
wrong_user = BaseUserDB(id="222", hashed_password="aaa")
await tortoise_user_db.create(wrong_user)
# Unknown user
unknown_user = await tortoise_user_db.get_by_email("galahad@camelot.bt")
assert unknown_user is None
# Delete user
await tortoise_user_db.delete(user)
deleted_user = await tortoise_user_db.get(user.id)
assert deleted_user is None