Fix #600: revamp Tortoise/Pydantic interaction (#612)

This commit is contained in:
François Voron
2021-04-20 14:46:36 +02:00
committed by GitHub
parent 61a99755e8
commit 3ac67377cb
6 changed files with 76 additions and 44 deletions

View File

@ -24,17 +24,27 @@ For the sake of this tutorial from now on, we'll use a simple SQLite databse.
Let's declare our User ORM model.
```py hl_lines="26 27"
```py hl_lines="8 9"
{!./src/db_tortoise.py!}
```
As you can see, **FastAPI Users** provides an abstract model that will include base fields for our User table. You can of course add you own fields there to fit to your needs!
## Tweak `UserDB` model
In order to make the Pydantic model and the Tortoise ORM model working well together, you'll have to add a mixin and some configuration options to your `UserDB` model. Tortoise ORM provides [utilities to ease the integration with Pydantic](https://tortoise-orm.readthedocs.io/en/latest/contrib/pydantic.html) and we'll use them here.
```py hl_lines="5 24 25 26 27"
{!./src/db_tortoise.py!}
```
The `PydanticModel` mixin adds methods used internally by Tortoise ORM to the Pydantic model so that it can easily transform it back to an ORM model. It expects then that you provide the property `orig_model` which should point to the **User ORM model we defined just above**.
## 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="30"
```py hl_lines="32"
{!./src/db_tortoise.py!}
```
@ -46,7 +56,7 @@ For using Tortoise ORM we must register our models and database.
Tortoise ORM supports integration with FastAPI out-of-the-box. It will automatically bind startup and shutdown events.
```py hl_lines="33 34 35 36 37 38"
```py hl_lines="35 36 37 38 39 40"
{!./src/db_tortoise.py!}
```

View File

@ -2,6 +2,11 @@ from fastapi import FastAPI
from fastapi_users import models
from fastapi_users.db import TortoiseBaseUserModel, TortoiseUserDatabase
from tortoise.contrib.fastapi import register_tortoise
from tortoise.contrib.pydantic import PydanticModel
class UserModel(TortoiseBaseUserModel):
pass
class User(models.BaseUser):
@ -16,17 +21,14 @@ class UserUpdate(User, models.BaseUserUpdate):
pass
class UserDB(User, models.BaseUserDB):
pass
class UserDB(User, models.BaseUserDB, PydanticModel):
class Config:
orm_mode = True
orig_model = UserModel
DATABASE_URL = "sqlite://./test.db"
class UserModel(TortoiseBaseUserModel):
pass
user_db = TortoiseUserDatabase(UserDB, UserModel)
app = FastAPI()

View File

@ -3,11 +3,16 @@ from fastapi_users import FastAPIUsers, models
from fastapi_users.authentication import JWTAuthentication
from fastapi_users.db import TortoiseBaseUserModel, TortoiseUserDatabase
from tortoise.contrib.fastapi import register_tortoise
from tortoise.contrib.pydantic import PydanticModel
DATABASE_URL = "sqlite://./test.db"
SECRET = "SECRET"
class UserModel(TortoiseBaseUserModel):
pass
class User(models.BaseUser):
pass
@ -20,12 +25,10 @@ class UserUpdate(User, models.BaseUserUpdate):
pass
class UserDB(User, models.BaseUserDB):
pass
class UserModel(TortoiseBaseUserModel):
pass
class UserDB(User, models.BaseUserDB, PydanticModel):
class Config:
orm_mode = True
orig_model = UserModel
user_db = TortoiseUserDatabase(UserDB, UserModel)
@ -33,7 +36,7 @@ app = FastAPI()
register_tortoise(
app,
db_url=DATABASE_URL,
modules={"models": ["path_to_your_package"]},
modules={"models": ["full_tortoise"]},
generate_schemas=True,
)

View File

@ -9,6 +9,7 @@ from fastapi_users.db import (
from httpx_oauth.clients.google import GoogleOAuth2
from tortoise import fields
from tortoise.contrib.fastapi import register_tortoise
from tortoise.contrib.pydantic import PydanticModel
DATABASE_URL = "sqlite://./test.db"
SECRET = "SECRET"
@ -17,6 +18,14 @@ SECRET = "SECRET"
google_oauth_client = GoogleOAuth2("CLIENT_ID", "CLIENT_SECRET")
class UserModel(TortoiseBaseUserModel):
pass
class OAuthAccountModel(TortoiseBaseOAuthAccountModel):
user = fields.ForeignKeyField("models.UserModel", related_name="oauth_accounts")
class User(models.BaseUser, models.BaseOAuthAccountMixin):
pass
@ -29,16 +38,10 @@ class UserUpdate(User, models.BaseUserUpdate):
pass
class UserDB(User, models.BaseUserDB):
pass
class UserModel(TortoiseBaseUserModel):
pass
class OAuthAccountModel(TortoiseBaseOAuthAccountModel):
user = fields.ForeignKeyField("models.UserModel", related_name="oauth_accounts")
class UserDB(User, models.BaseUserDB, PydanticModel):
class Config:
orm_mode = True
orig_model = UserModel
user_db = TortoiseUserDatabase(UserDB, UserModel, OAuthAccountModel)
@ -46,7 +49,7 @@ app = FastAPI()
register_tortoise(
app,
db_url=DATABASE_URL,
modules={"models": ["path_to_your_package"]},
modules={"models": ["oauth_full_tortoise"]},
generate_schemas=True,
)

View File

@ -1,7 +1,8 @@
from typing import Optional, Type
from typing import Optional, Type, cast
from pydantic import UUID4
from tortoise import fields, models
from tortoise.contrib.pydantic import PydanticModel
from tortoise.exceptions import DoesNotExist
from tortoise.queryset import QuerySetSingle
@ -17,14 +18,6 @@ class TortoiseBaseUserModel(models.Model):
is_superuser = fields.BooleanField(default=False, null=False)
is_verified = fields.BooleanField(default=False, null=False)
async def to_dict(self):
d = {}
for field in self._meta.db_fields:
d[field] = getattr(self, field)
for field in self._meta.backward_fk_fields:
d[field] = await getattr(self, field).all().values()
return d
class Meta:
abstract = True
@ -72,9 +65,11 @@ class TortoiseUserDatabase(BaseUserDatabase[UD]):
query = query.prefetch_related("oauth_accounts")
user = await query
user_dict = await user.to_dict()
pydantic_user = await cast(
PydanticModel, self.user_db_model
).from_tortoise_orm(user)
return self.user_db_model(**user_dict)
return cast(UD, pydantic_user)
except DoesNotExist:
return None
@ -89,8 +84,11 @@ class TortoiseUserDatabase(BaseUserDatabase[UD]):
if user is None:
return None
user_dict = await user.to_dict()
return self.user_db_model(**user_dict)
pydantic_user = await cast(PydanticModel, self.user_db_model).from_tortoise_orm(
user
)
return cast(UD, pydantic_user)
async def get_by_oauth_account(self, oauth: str, account_id: str) -> Optional[UD]:
try:
@ -99,9 +97,11 @@ class TortoiseUserDatabase(BaseUserDatabase[UD]):
).prefetch_related("oauth_accounts")
user = await query
user_dict = await user.to_dict()
pydantic_user = await cast(
PydanticModel, self.user_db_model
).from_tortoise_orm(user)
return self.user_db_model(**user_dict)
return cast(UD, pydantic_user)
except DoesNotExist:
return None

View File

@ -2,6 +2,7 @@ from typing import AsyncGenerator
import pytest
from tortoise import Tortoise, fields
from tortoise.contrib.pydantic import PydanticModel
from tortoise.exceptions import IntegrityError
from fastapi_users.db.tortoise import (
@ -10,17 +11,30 @@ from fastapi_users.db.tortoise import (
TortoiseUserDatabase,
)
from fastapi_users.password import get_password_hash
from tests.conftest import UserDB, UserDBOAuth
from tests.conftest import UserDB as BaseUserDB
from tests.conftest import UserDBOAuth as BaseUserDBOAuth
class User(TortoiseBaseUserModel):
first_name = fields.CharField(null=True, max_length=255)
class UserDB(BaseUserDB, PydanticModel):
class Config:
orm_mode = True
orig_model = User
class OAuthAccount(TortoiseBaseOAuthAccountModel):
user = fields.ForeignKeyField("models.User", related_name="oauth_accounts")
class UserDBOAuth(BaseUserDBOAuth, PydanticModel):
class Config:
orm_mode = True
orig_model = OAuthAccount
@pytest.fixture
async def tortoise_user_db() -> AsyncGenerator[TortoiseUserDatabase, None]:
DATABASE_URL = "sqlite://./test-tortoise-user.db"