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/ .pytest_cache/
junit/ junit/
junit.xml junit.xml
test.db
# Translations # Translations
*.mo *.mo

View File

@ -8,11 +8,15 @@ flake8 = "*"
pytest = "*" pytest = "*"
requests = "*" requests = "*"
isort = "*" isort = "*"
databases = {extras = ["sqlite"],version = "*"}
pytest-asyncio = "*"
[packages] [packages]
fastapi = "*" fastapi = "*"
passlib = {extras = ["bcrypt"],version = "*"} passlib = {extras = ["bcrypt"],version = "*"}
email-validator = "*" email-validator = "*"
sqlalchemy = "*"
databases = "*"
[requires] [requires]
python_version = "3.7" python_version = "3.7"

44
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "4879f37c108087df2ecbcee271a3d58de6bfd9d58013d3f9bf0127ef7e7acf92" "sha256": "a2653ab0f39cfc4780097259fdeef1f152ec4472b5b3b5cc80cf08997d7a1e81"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -70,6 +70,13 @@
], ],
"version": "==1.12.3" "version": "==1.12.3"
}, },
"databases": {
"hashes": [
"sha256:1e3b21a237d8b8a8774da1237fa75e951e23bf8e943516df8fe2443f1968287f"
],
"index": "pypi",
"version": "==0.2.5"
},
"dnspython": { "dnspython": {
"hashes": [ "hashes": [
"sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01", "sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01",
@ -134,6 +141,13 @@
], ],
"version": "==1.12.0" "version": "==1.12.0"
}, },
"sqlalchemy": {
"hashes": [
"sha256:272a835758908412e75e87f75dd0179a51422715c125ce42109632910526b1fd"
],
"index": "pypi",
"version": "==1.3.9"
},
"starlette": { "starlette": {
"hashes": [ "hashes": [
"sha256:f600bf9d0beeeeebcb143e6d0c4f8858c2b05067d5a4feb446ba7400ba5e5dc5" "sha256:f600bf9d0beeeeebcb143e6d0c4f8858c2b05067d5a4feb446ba7400ba5e5dc5"
@ -142,6 +156,12 @@
} }
}, },
"develop": { "develop": {
"aiosqlite": {
"hashes": [
"sha256:ad84fbd7516ca7065d799504fc41d6845c938e5306d1b7dd960caaeda12e22a9"
],
"version": "==0.10.0"
},
"atomicwrites": { "atomicwrites": {
"hashes": [ "hashes": [
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
@ -170,6 +190,13 @@
], ],
"version": "==3.0.4" "version": "==3.0.4"
}, },
"databases": {
"hashes": [
"sha256:1e3b21a237d8b8a8774da1237fa75e951e23bf8e943516df8fe2443f1968287f"
],
"index": "pypi",
"version": "==0.2.5"
},
"entrypoints": { "entrypoints": {
"hashes": [ "hashes": [
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
@ -272,6 +299,14 @@
"index": "pypi", "index": "pypi",
"version": "==5.2.0" "version": "==5.2.0"
}, },
"pytest-asyncio": {
"hashes": [
"sha256:9fac5100fd716cbecf6ef89233e8590a4ad61d729d1732e0a96b84182df1daaf",
"sha256:d734718e25cfc32d2bf78d346e99d33724deeba774cc4afdf491530c6184b63b"
],
"index": "pypi",
"version": "==0.10.0"
},
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
@ -287,6 +322,13 @@
], ],
"version": "==1.12.0" "version": "==1.12.0"
}, },
"sqlalchemy": {
"hashes": [
"sha256:272a835758908412e75e87f75dd0179a51422715c125ce42109632910526b1fd"
],
"index": "pypi",
"version": "==1.3.9"
},
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",

View File

@ -1,6 +1,6 @@
from typing import List from typing import List
from ..models import UserDB from fastapi_users.models import UserDB
class UserDBInterface: 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 import uuid
from typing import Optional from typing import Optional
import pydantic
from pydantic import BaseModel from pydantic import BaseModel
from pydantic.types import EmailStr from pydantic.types import EmailStr
class UserBase(BaseModel): class UserBase(BaseModel):
id: str = uuid.uuid4 id: str = None
email: Optional[EmailStr] = None email: Optional[EmailStr] = None
is_active: Optional[bool] = True is_active: Optional[bool] = True
is_superuser: Optional[bool] = False 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): class UserCreate(UserBase):

View File

@ -1,8 +1,8 @@
from fastapi import APIRouter from fastapi import APIRouter
from .db import UserDBInterface from fastapi_users.db import UserDBInterface
from .models import UserCreate, UserDB from fastapi_users.models import UserCreate, UserDB
from .password import get_password_hash from fastapi_users.password import get_password_hash
class UserRouter: 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) return TestClient(app)
def test_register_empty_body(test_app_client: TestClient): class TestRegister:
response = test_app_client.post('/register', json={})
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
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 = { json = {
'email': 'king.arthur@camelot.bt', 'email': 'king.arthur@camelot.bt',
} }
response = test_app_client.post('/register', json=json) response = test_app_client.post('/register', json=json)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY 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): def test_valid_body(self, test_app_client: TestClient):
json = { json = {
'email': 'king.arthur', 'email': 'king.arthur@camelot.bt',
'password': 'guinevere', 'password': 'guinevere',
} }
response = test_app_client.post('/register', json=json) response = test_app_client.post('/register', json=json)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert response.status_code == status.HTTP_200_OK
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