mirror of
https://github.com/fastapi-users/fastapi-users.git
synced 2025-11-02 12:21:53 +08:00
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:
committed by
François Voron
parent
358150bbff
commit
b5b0bbbb01
2
.gitignore
vendored
2
.gitignore
vendored
@ -48,7 +48,7 @@ coverage.xml
|
||||
.pytest_cache/
|
||||
junit/
|
||||
junit.xml
|
||||
test.db
|
||||
test.db*
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
1
Pipfile
1
Pipfile
@ -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
35
Pipfile.lock
generated
@ -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": {
|
||||
|
||||
53
docs/configuration/databases/tortoise.md
Normal file
53
docs/configuration/databases/tortoise.md
Normal 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).
|
||||
@ -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**.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
16
docs/src/db_tortoise.py
Normal 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
36
docs/src/full_tortoise.py
Normal 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}")
|
||||
58
fastapi_users/db/tortoise.py
Normal file
58
fastapi_users/db/tortoise.py
Normal 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()
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
82
tests/test_db_tortoise.py
Normal 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
|
||||
Reference in New Issue
Block a user