Implement working SQLAlchemy DB adapter

This commit is contained in:
François Voron
2019-10-06 11:00:24 +02:00
parent 552f313d76
commit bcc88a8b14
9 changed files with 182 additions and 33 deletions

1
.gitignore vendored
View File

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

View File

@ -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
View File

@ -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",

View File

@ -1,6 +1,6 @@
from typing import List
from ..models import UserDB
from fastapi_users.models import UserDB
class UserDBInterface:

View 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

View File

@ -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):

View File

@ -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:

View 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)

View File

@ -25,32 +25,31 @@ def test_app_client() -> TestClient:
return TestClient(app)
def test_register_empty_body(test_app_client: TestClient):
response = test_app_client.post('/register', json={})
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
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):
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_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_wrong_email(self, test_app_client: TestClient):
json = {
'email': 'king.arthur',
'password': 'guinevere',
}
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):
json = {
'email': 'king.arthur',
'password': 'guinevere',
}
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):
json = {
'email': 'king.arthur@camelot.bt',
'password': 'guinevere',
}
response = test_app_client.post('/register', json=json)
assert response.status_code == status.HTTP_200_OK
def test_valid_body(self, test_app_client: TestClient):
json = {
'email': 'king.arthur@camelot.bt',
'password': 'guinevere',
}
response = test_app_client.post('/register', json=json)
assert response.status_code == status.HTTP_200_OK