mirror of
https://github.com/fastapi-users/fastapi-users.git
synced 2025-11-02 12:21:53 +08:00
Implement working SQLAlchemy DB adapter
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -48,6 +48,7 @@ coverage.xml
|
||||
.pytest_cache/
|
||||
junit/
|
||||
junit.xml
|
||||
test.db
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
4
Pipfile
4
Pipfile
@ -8,11 +8,15 @@ flake8 = "*"
|
||||
pytest = "*"
|
||||
requests = "*"
|
||||
isort = "*"
|
||||
databases = {extras = ["sqlite"],version = "*"}
|
||||
pytest-asyncio = "*"
|
||||
|
||||
[packages]
|
||||
fastapi = "*"
|
||||
passlib = {extras = ["bcrypt"],version = "*"}
|
||||
email-validator = "*"
|
||||
sqlalchemy = "*"
|
||||
databases = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
|
||||
44
Pipfile.lock
generated
44
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "4879f37c108087df2ecbcee271a3d58de6bfd9d58013d3f9bf0127ef7e7acf92"
|
||||
"sha256": "a2653ab0f39cfc4780097259fdeef1f152ec4472b5b3b5cc80cf08997d7a1e81"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -70,6 +70,13 @@
|
||||
],
|
||||
"version": "==1.12.3"
|
||||
},
|
||||
"databases": {
|
||||
"hashes": [
|
||||
"sha256:1e3b21a237d8b8a8774da1237fa75e951e23bf8e943516df8fe2443f1968287f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.2.5"
|
||||
},
|
||||
"dnspython": {
|
||||
"hashes": [
|
||||
"sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01",
|
||||
@ -134,6 +141,13 @@
|
||||
],
|
||||
"version": "==1.12.0"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:272a835758908412e75e87f75dd0179a51422715c125ce42109632910526b1fd"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.9"
|
||||
},
|
||||
"starlette": {
|
||||
"hashes": [
|
||||
"sha256:f600bf9d0beeeeebcb143e6d0c4f8858c2b05067d5a4feb446ba7400ba5e5dc5"
|
||||
@ -142,6 +156,12 @@
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"aiosqlite": {
|
||||
"hashes": [
|
||||
"sha256:ad84fbd7516ca7065d799504fc41d6845c938e5306d1b7dd960caaeda12e22a9"
|
||||
],
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"atomicwrites": {
|
||||
"hashes": [
|
||||
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
|
||||
@ -170,6 +190,13 @@
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"databases": {
|
||||
"hashes": [
|
||||
"sha256:1e3b21a237d8b8a8774da1237fa75e951e23bf8e943516df8fe2443f1968287f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.2.5"
|
||||
},
|
||||
"entrypoints": {
|
||||
"hashes": [
|
||||
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
|
||||
@ -272,6 +299,14 @@
|
||||
"index": "pypi",
|
||||
"version": "==5.2.0"
|
||||
},
|
||||
"pytest-asyncio": {
|
||||
"hashes": [
|
||||
"sha256:9fac5100fd716cbecf6ef89233e8590a4ad61d729d1732e0a96b84182df1daaf",
|
||||
"sha256:d734718e25cfc32d2bf78d346e99d33724deeba774cc4afdf491530c6184b63b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
|
||||
@ -287,6 +322,13 @@
|
||||
],
|
||||
"version": "==1.12.0"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:272a835758908412e75e87f75dd0179a51422715c125ce42109632910526b1fd"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.9"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from typing import List
|
||||
|
||||
from ..models import UserDB
|
||||
from fastapi_users.models import UserDB
|
||||
|
||||
|
||||
class UserDBInterface:
|
||||
|
||||
44
fastapi_users/db/sqlalchemy.py
Normal file
44
fastapi_users/db/sqlalchemy.py
Normal file
@ -0,0 +1,44 @@
|
||||
from typing import List
|
||||
|
||||
from databases import Database
|
||||
from sqlalchemy import Boolean, Column, String
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
from fastapi_users.db import UserDBInterface
|
||||
from fastapi_users.models import UserDB
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = 'user'
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
email = Column(String, unique=True, index=True)
|
||||
hashed_password = Column(String)
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_superuser = Column(Boolean, default=False)
|
||||
|
||||
|
||||
users = User.__table__
|
||||
|
||||
|
||||
class SQLAlchemyUserDB(UserDBInterface):
|
||||
|
||||
database: Database
|
||||
|
||||
def __init__(self, database):
|
||||
self.database = database
|
||||
|
||||
async def list(self) -> List[UserDB]:
|
||||
query = users.select()
|
||||
return await self.database.fetch_all(query)
|
||||
|
||||
async def get_by_email(self, email: str) -> UserDB:
|
||||
query = users.select().where(User.email == email)
|
||||
return await self.database.fetch_one(query)
|
||||
|
||||
async def create(self, user: UserDB) -> UserDB:
|
||||
query = users.insert().values(**user.dict())
|
||||
await self.database.execute(query)
|
||||
return user
|
||||
@ -1,17 +1,20 @@
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
import pydantic
|
||||
from pydantic import BaseModel
|
||||
from pydantic.types import EmailStr
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
id: str = uuid.uuid4
|
||||
id: str = None
|
||||
email: Optional[EmailStr] = None
|
||||
is_active: Optional[bool] = True
|
||||
is_superuser: Optional[bool] = False
|
||||
first_name: Optional[str] = None
|
||||
last_name: Optional[str] = None
|
||||
|
||||
@pydantic.validator('id', pre=True, always=True)
|
||||
def default_id(cls, v):
|
||||
return v or str(uuid.uuid4())
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .db import UserDBInterface
|
||||
from .models import UserCreate, UserDB
|
||||
from .password import get_password_hash
|
||||
from fastapi_users.db import UserDBInterface
|
||||
from fastapi_users.models import UserCreate, UserDB
|
||||
from fastapi_users.password import get_password_hash
|
||||
|
||||
|
||||
class UserRouter:
|
||||
|
||||
56
tests/test_db_sqlalchemy.py
Normal file
56
tests/test_db_sqlalchemy.py
Normal file
@ -0,0 +1,56 @@
|
||||
from databases import Database
|
||||
import pytest
|
||||
import sqlalchemy
|
||||
import sqlite3
|
||||
|
||||
from fastapi_users.db.sqlalchemy import Base, SQLAlchemyUserDB
|
||||
from fastapi_users.models import UserDB
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def sqlalchemy_user_db() -> SQLAlchemyUserDB:
|
||||
DATABASE_URL = 'sqlite:///./test.db'
|
||||
database = Database(DATABASE_URL)
|
||||
|
||||
engine = sqlalchemy.create_engine(
|
||||
DATABASE_URL, connect_args={'check_same_thread': False}
|
||||
)
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
await database.connect()
|
||||
|
||||
yield SQLAlchemyUserDB(database)
|
||||
|
||||
Base.metadata.drop_all(engine)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user() -> UserDB:
|
||||
return UserDB(
|
||||
email='king.arthur@camelot.bt',
|
||||
hashed_password='abc',
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_queries(user, sqlalchemy_user_db):
|
||||
# Create
|
||||
user_db = await sqlalchemy_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
|
||||
|
||||
# List
|
||||
users = await sqlalchemy_user_db.list()
|
||||
assert len(users) == 1
|
||||
first_user = users[0]
|
||||
assert first_user.id == user_db.id
|
||||
|
||||
# Get by email
|
||||
email_user = await sqlalchemy_user_db.get_by_email(user.email)
|
||||
assert email_user.id == user_db.id
|
||||
|
||||
# Exception on existing email
|
||||
with pytest.raises(sqlite3.IntegrityError):
|
||||
await sqlalchemy_user_db.create(user)
|
||||
@ -25,20 +25,20 @@ def test_app_client() -> TestClient:
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def test_register_empty_body(test_app_client: TestClient):
|
||||
class TestRegister:
|
||||
|
||||
def test_empty_body(self, test_app_client: TestClient):
|
||||
response = test_app_client.post('/register', json={})
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
|
||||
def test_register_missing_password(test_app_client: TestClient):
|
||||
def test_missing_password(self, test_app_client: TestClient):
|
||||
json = {
|
||||
'email': 'king.arthur@camelot.bt',
|
||||
}
|
||||
response = test_app_client.post('/register', json=json)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
|
||||
def test_register_wrong_email(test_app_client: TestClient):
|
||||
def test_wrong_email(self, test_app_client: TestClient):
|
||||
json = {
|
||||
'email': 'king.arthur',
|
||||
'password': 'guinevere',
|
||||
@ -46,8 +46,7 @@ def test_register_wrong_email(test_app_client: TestClient):
|
||||
response = test_app_client.post('/register', json=json)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
|
||||
def test_register_valid_body(test_app_client: TestClient):
|
||||
def test_valid_body(self, test_app_client: TestClient):
|
||||
json = {
|
||||
'email': 'king.arthur@camelot.bt',
|
||||
'password': 'guinevere',
|
||||
|
||||
Reference in New Issue
Block a user