mirror of
https://github.com/fastapi-users/fastapi-users.git
synced 2025-08-14 18:58:10 +08:00
Implement MongoDB database adapter (#29)
* Implement MongoDB adapter using motor * Add mongo container to build pipeline * Tidy up dependencies * Update documentation for MongoDB * Export MongoDB adapter from db package * Pass black format * Update README
This commit is contained in:
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@ -10,6 +10,12 @@ jobs:
|
||||
matrix:
|
||||
python_version: [3.7]
|
||||
|
||||
services:
|
||||
mongo:
|
||||
image: mvertes/alpine-mongo
|
||||
ports:
|
||||
- 27017:27017
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python 3.7
|
||||
|
3
Makefile
3
Makefile
@ -1,4 +1,5 @@
|
||||
PIPENV_RUN := pipenv run
|
||||
MONGODB_CONTAINER_NAME := fastapi-users-test-mongo
|
||||
|
||||
isort-src:
|
||||
$(PIPENV_RUN) isort -rc ./fastapi_users
|
||||
@ -10,7 +11,9 @@ format: isort-src isort-docs
|
||||
$(PIPENV_RUN) black .
|
||||
|
||||
test:
|
||||
docker run -d --rm --name $(MONGODB_CONTAINER_NAME) -p 27017:27017 mvertes/alpine-mongo
|
||||
$(PIPENV_RUN) pytest --cov=fastapi_users/
|
||||
docker stop $(MONGODB_CONTAINER_NAME)
|
||||
|
||||
docs-serve:
|
||||
$(PIPENV_RUN) mkdocs serve
|
||||
|
1
Pipfile
1
Pipfile
@ -33,6 +33,7 @@ sqlalchemy = "==1.3.10"
|
||||
databases = "==0.2.5"
|
||||
pyjwt = "==1.7.1"
|
||||
python-multipart = "==0.0.5"
|
||||
motor = "==2.0.0"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
|
45
Pipfile.lock
generated
45
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "91f160a7c296aed283da008704715c542e10164b461da5e0c6a930d131382046"
|
||||
"sha256": "4d68606cc0933e1ea598e1b935ae02465814e98d344e52e94c02bc234219d7d0"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -62,10 +62,12 @@
|
||||
"sha256:7cfcfda59ef1f95b9f729c56fe8a4041899f96b72685d36ef16a3440a0f85da8",
|
||||
"sha256:819f8d5197c2684524637f940445c06e003c4a541f9983fd30d6deaa2a5487d8",
|
||||
"sha256:825ecffd9574557590e3225560a8a9d751f6ffe4a49e3c40918c9969b93395fa",
|
||||
"sha256:8a2bcae2258d00fcfc96a9bde4a6177bc4274fe033f79311c5dd3d3148c26518",
|
||||
"sha256:9009e917d8f5ef780c2626e29b6bc126f4cb2a4d43ca67aa2b40f2a5d6385e78",
|
||||
"sha256:9c77564a51d4d914ed5af096cd9843d90c45b784b511723bd46a8a9d09cf16fc",
|
||||
"sha256:a19089fa74ed19c4fe96502a291cfdb89223a9705b1d73b3005df4256976142e",
|
||||
"sha256:a40ed527bffa2b7ebe07acc5a3f782da072e262ca994b4f2085100b5a444bbb2",
|
||||
"sha256:b8f09f21544b9899defb09afbdaeb200e6a87a2b8e604892940044cf94444644",
|
||||
"sha256:bb75ba21d5716abc41af16eac1145ab2e471deedde1f22c6f99bd9f995504df0",
|
||||
"sha256:e22a00c0c81ffcecaf07c2bfb3672fa372c50e2bd1024ffee0da191c1b27fc71",
|
||||
"sha256:e55b5a746fb77f10c83e8af081979351722f6ea48facea79d470b3731c7b2891",
|
||||
@ -110,6 +112,14 @@
|
||||
],
|
||||
"version": "==2.8"
|
||||
},
|
||||
"motor": {
|
||||
"hashes": [
|
||||
"sha256:462fbb824f4289481c158227a2579d6adaf1ec7c70cf7ebe60ed6ceb321e5869",
|
||||
"sha256:d035c09ab422bc50bf3efb134f7405694cae76268545bd21e14fb22e2638f84e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"passlib": {
|
||||
"extras": [
|
||||
"bcrypt"
|
||||
@ -146,6 +156,39 @@
|
||||
"index": "pypi",
|
||||
"version": "==1.7.1"
|
||||
},
|
||||
"pymongo": {
|
||||
"hashes": [
|
||||
"sha256:09f8196e1cb081713aa3face08d1806dc0a5dd64cb9f67fefc568519253a7ff2",
|
||||
"sha256:1be549c0ce2ba8242c149156ae2064b12a5d4704448d49f630b4910606efd474",
|
||||
"sha256:1f9fe869e289210250cba4ea20fbd169905b1793e1cd2737f423e107061afa98",
|
||||
"sha256:3653cea82d1e35edd0a2355150daf8a27ebf12cf55182d5ad1046bfa288f5140",
|
||||
"sha256:4249c6ba45587b959292a727532826c5032d59171f923f7f823788f413c2a5a3",
|
||||
"sha256:4ff8f5e7c0a78983c1ee07894fff1b21c0e0ad3a122d9786cc3745fd60e4a2ce",
|
||||
"sha256:56b29c638ab924716b48a3e94e3d7ac00b04acec1daa8190c36d61fc714c3629",
|
||||
"sha256:56ec9358bbfe5ae3b25e785f8a14619d6799c855a44734c9098bb457174019bf",
|
||||
"sha256:5dca250cbf1183c3e7b7b18c882c2b2199bfb20c74c4c68dbf11596808a296da",
|
||||
"sha256:61101d1cc92881fac1f9ac7e99b033062f4c210178dc33193c8f5567feecb069",
|
||||
"sha256:86624c0205a403fb4fbfedef79c5b4ab27e21fd018fdb6a27cf03b3c32a9e2b9",
|
||||
"sha256:88ac09e1b197c3b4531e43054d49c022a3ea1281431b2f4980abafa35d2a5ce2",
|
||||
"sha256:8b0339809b12ea292d468524dd1777f1a9637d9bdc0353a9261b88f82537d606",
|
||||
"sha256:93dbf7388f6bf9af48dbb32f265b75b3dbc743a7a2ce98e44c88c049c58d85d3",
|
||||
"sha256:9b705daec636c560dd2d63935f428a6b3cddfe903fffc0f349e0e91007c893d6",
|
||||
"sha256:a090a819fe6fefadc2901d3911c07c76c0935ec5c790a50e9f3c3c47bacd5978",
|
||||
"sha256:a102b346f1921237eaa9a31ee89eda57ad3c3973d79be3a456d92524e7df8fec",
|
||||
"sha256:a13363869f2f36291d6367069c65d51d7b8d1b2fb410266b0b6b1f3c90d6deb0",
|
||||
"sha256:a409a43c76da50881b70cc9ee70a1744f882848e8e93a68fb434254379777fa3",
|
||||
"sha256:a76475834a978058425b0163f1bad35a5f70e45929a543075633c3fc1df564c5",
|
||||
"sha256:ad474e93525baa6c58d75d63a73143af24c9f93c8e26e8d382f32c4da637901a",
|
||||
"sha256:b268c7fa03ac77a8662fab3b2ab0be4beecb82f60f4c24b584e69565691a107f",
|
||||
"sha256:cca4e1ab5ba0cd7877d3938167ee8ae9c2986cc0e10d3dcc3243d664d3a83fec",
|
||||
"sha256:cef61de3f0f4441ec40266ff2ab42e5c16eaba1dc1fc6e1036f274621c52adc1",
|
||||
"sha256:e28153b5d5ca33d4ba0c3bbc0e1ff161b9016e5e5f3f8ca10d6fa49106eb9e04",
|
||||
"sha256:f30d7b37804daf0bab1143abc71666c630d7e270f5c14c5a7c300a6699c21108",
|
||||
"sha256:f70f0133301cccf9bfd68fd20f67184ef991be578b646e78441106f9e27cc44d",
|
||||
"sha256:fa75c21c1d82f20cce62f6fc4a68c2b0f33572ab406df1b17cd77a947d0b2993"
|
||||
],
|
||||
"version": "==3.9.0"
|
||||
},
|
||||
"python-multipart": {
|
||||
"hashes": [
|
||||
"sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"
|
||||
|
@ -27,9 +27,10 @@ Add quickly a registration and authentication system to your [FastAPI](https://f
|
||||
|
||||
* [X] Extensible base user model
|
||||
* [X] Ready-to-use register, login, forgot and reset password routes.
|
||||
* [X] Dependency callables to inject current user in route.
|
||||
* [X] Customizable database backend
|
||||
* [X] SQLAlchemy backend included
|
||||
* [ ] MongoDB backend included ([#4](https://github.com/frankie567/fastapi-users/issues/4))
|
||||
* [X] SQLAlchemy async backend included thanks to [encode/databases](https://www.encode.io/databases/)
|
||||
* [X] MongoDB async backend included thanks to [mongodb/motor](https://github.com/mongodb/motor)
|
||||
* [X] Customizable authentication backend
|
||||
* [X] JWT authentication backend included
|
||||
|
||||
|
@ -1,6 +1,30 @@
|
||||
# MongoDB
|
||||
|
||||
**Coming soon**. Track the progress of this feature in [ticket #4](https://github.com/frankie567/fastapi-users/issues/4).
|
||||
**FastAPI Users** provides the necessary tools to work with MongoDB databases thanks to [mongodb/motor](https://github.com/mongodb/motor) package for full async support.
|
||||
|
||||
## Setup database connection and collection
|
||||
|
||||
Let's create a MongoDB connection and instantiate a collection.
|
||||
|
||||
```py hl_lines="6 7 8 9"
|
||||
{!./src/db_mongodb.py!}
|
||||
```
|
||||
|
||||
You can choose any name for the database and the collection.
|
||||
|
||||
## 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="15"
|
||||
{!./src/db_mongodb.py!}
|
||||
```
|
||||
|
||||
!!! info
|
||||
The database adapter will automatically create a [unique index](https://docs.mongodb.com/manual/core/index-unique/) on `id` and `email`.
|
||||
|
||||
!!! warning
|
||||
**FastAPI Users** will use its defined [`id` UUID-string](../model.md) as unique identifier for the user, rather than the builtin MongoDB `_id`.
|
||||
|
||||
## Next steps
|
||||
|
||||
|
@ -7,7 +7,7 @@ Here is a full working example with JWT authentication to help get you started.
|
||||
```
|
||||
|
||||
```py tab="MongoDB"
|
||||
# Coming soon
|
||||
{!./src/full_mongodb.py!}
|
||||
```
|
||||
|
||||
## What now?
|
||||
|
@ -2,14 +2,16 @@
|
||||
|
||||
You can add **FastAPI Users** to your FastAPI project in a few easy steps. First of all, install the dependency:
|
||||
|
||||
## With SQLAlchemy support
|
||||
|
||||
```sh
|
||||
pip install fastapi-users
|
||||
pip install fastapi-users[sqlalchemy]
|
||||
```
|
||||
|
||||
...or if you're already in the future:
|
||||
## With MongoDB support
|
||||
|
||||
```sh
|
||||
pipenv install fastapi-users
|
||||
pip install fastapi-users[mongodb]
|
||||
```
|
||||
|
||||
---
|
||||
|
14
docs/src/db_mongodb.py
Normal file
14
docs/src/db_mongodb.py
Normal file
@ -0,0 +1,14 @@
|
||||
import motor.motor_asyncio
|
||||
from fastapi import FastAPI
|
||||
from fastapi_users.db import MongoDBUserDatabase
|
||||
|
||||
DATABASE_URL = "mongodb://localhost:27017"
|
||||
client = motor.motor_asyncio.AsyncIOMotorClient(DATABASE_URL)
|
||||
db = client["database_name"]
|
||||
collection = db["users"]
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
user_db = MongoDBUserDatabase(collection)
|
37
docs/src/full_mongodb.py
Normal file
37
docs/src/full_mongodb.py
Normal file
@ -0,0 +1,37 @@
|
||||
import motor.motor_asyncio
|
||||
from fastapi import FastAPI
|
||||
from fastapi_users import BaseUser, FastAPIUsers
|
||||
from fastapi_users.authentication import JWTAuthentication
|
||||
from fastapi_users.db import MongoDBUserDatabase
|
||||
|
||||
DATABASE_URL = "mongodb://localhost:27017"
|
||||
SECRET = "SECRET"
|
||||
|
||||
|
||||
client = motor.motor_asyncio.AsyncIOMotorClient(DATABASE_URL)
|
||||
db = client["database_name"]
|
||||
collection = db["users"]
|
||||
|
||||
|
||||
user_db = MongoDBUserDatabase(collection)
|
||||
|
||||
|
||||
class User(BaseUser):
|
||||
pass
|
||||
|
||||
|
||||
auth = JWTAuthentication(secret=SECRET, lifetime_seconds=3600)
|
||||
|
||||
app = FastAPI()
|
||||
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}")
|
@ -1,4 +1,5 @@
|
||||
from fastapi_users.db.base import BaseUserDatabase # noqa: F401
|
||||
from fastapi_users.db.mongodb import MongoDBUserDatabase # noqa: F401
|
||||
from fastapi_users.db.sqlalchemy import ( # noqa: F401
|
||||
SQLAlchemyBaseUserTable,
|
||||
SQLAlchemyUserDatabase,
|
||||
|
40
fastapi_users/db/mongodb.py
Normal file
40
fastapi_users/db/mongodb.py
Normal file
@ -0,0 +1,40 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from motor.motor_asyncio import AsyncIOMotorCollection
|
||||
|
||||
from fastapi_users.db.base import BaseUserDatabase
|
||||
from fastapi_users.models import BaseUserDB
|
||||
|
||||
|
||||
class MongoDBUserDatabase(BaseUserDatabase):
|
||||
"""
|
||||
Database adapter for MongoDB.
|
||||
|
||||
:param collection: Collection instance from `motor`.
|
||||
"""
|
||||
|
||||
collection: AsyncIOMotorCollection
|
||||
|
||||
def __init__(self, collection: AsyncIOMotorCollection):
|
||||
self.collection = collection
|
||||
self.collection.create_index("id", unique=True)
|
||||
self.collection.create_index("email", unique=True)
|
||||
|
||||
async def list(self) -> List[BaseUserDB]:
|
||||
return [BaseUserDB(**user) async for user in self.collection.find()]
|
||||
|
||||
async def get(self, id: str) -> Optional[BaseUserDB]:
|
||||
user = await self.collection.find_one({"id": id})
|
||||
return BaseUserDB(**user) if user else None
|
||||
|
||||
async def get_by_email(self, email: str) -> Optional[BaseUserDB]:
|
||||
user = await self.collection.find_one({"email": email})
|
||||
return BaseUserDB(**user) if user else None
|
||||
|
||||
async def create(self, user: BaseUserDB) -> BaseUserDB:
|
||||
await self.collection.insert_one(user.dict())
|
||||
return user
|
||||
|
||||
async def update(self, user: BaseUserDB) -> BaseUserDB:
|
||||
await self.collection.replace_one({"id": user.id}, user.dict())
|
||||
return user
|
@ -1,4 +1,4 @@
|
||||
from typing import List, cast
|
||||
from typing import List, Optional, cast
|
||||
|
||||
from databases import Database
|
||||
from sqlalchemy import Boolean, Column, String, Table
|
||||
@ -38,11 +38,11 @@ class SQLAlchemyUserDatabase(BaseUserDatabase):
|
||||
query = self.users.select()
|
||||
return cast(List[BaseUserDB], await self.database.fetch_all(query))
|
||||
|
||||
async def get(self, id: str) -> BaseUserDB:
|
||||
async def get(self, id: str) -> Optional[BaseUserDB]:
|
||||
query = self.users.select().where(self.users.c.id == id)
|
||||
return cast(BaseUserDB, await self.database.fetch_one(query))
|
||||
|
||||
async def get_by_email(self, email: str) -> BaseUserDB:
|
||||
async def get_by_email(self, email: str) -> Optional[BaseUserDB]:
|
||||
query = self.users.select().where(self.users.c.email == email)
|
||||
return cast(BaseUserDB, await self.database.fetch_one(query))
|
||||
|
||||
|
@ -13,7 +13,6 @@ classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Framework :: AsyncIO",
|
||||
"Intended Audience :: Developers",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
@ -25,11 +24,18 @@ requires = [
|
||||
"fastapi ==0.42.0",
|
||||
"passlib[bcrypt] ==1.7.1",
|
||||
"email-validator ==1.0.5",
|
||||
"sqlalchemy ==1.3.10",
|
||||
"databases ==0.2.5",
|
||||
"pyjwt ==1.7.1",
|
||||
"python-multipart ==0.0.5",
|
||||
]
|
||||
|
||||
[tool.flit.metadata.requires-extra]
|
||||
sqlalchemy = [
|
||||
"sqlalchemy ==1.3.10",
|
||||
"databases ==0.2.5",
|
||||
]
|
||||
mongodb = [
|
||||
"motor ==2.0.0",
|
||||
]
|
||||
|
||||
[tool.flit.metadata.urls]
|
||||
Documentation = "https://frankie567.github.io/fastapi-users/"
|
||||
|
63
tests/test_db_mongodb.py
Normal file
63
tests/test_db_mongodb.py
Normal file
@ -0,0 +1,63 @@
|
||||
from typing import AsyncGenerator
|
||||
|
||||
import motor.motor_asyncio
|
||||
import pytest
|
||||
import pymongo.errors
|
||||
|
||||
from fastapi_users.db.mongodb import MongoDBUserDatabase
|
||||
from fastapi_users.models import BaseUserDB
|
||||
from fastapi_users.password import get_password_hash
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mongodb_user_db() -> AsyncGenerator[MongoDBUserDatabase, None]:
|
||||
client = motor.motor_asyncio.AsyncIOMotorClient("mongodb://localhost:27017")
|
||||
db = client["test_database"]
|
||||
collection = db["users"]
|
||||
|
||||
yield MongoDBUserDatabase(collection)
|
||||
|
||||
await collection.drop()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_queries(mongodb_user_db):
|
||||
user = BaseUserDB(
|
||||
id="111",
|
||||
email="lancelot@camelot.bt",
|
||||
hashed_password=get_password_hash("guinevere"),
|
||||
)
|
||||
|
||||
# Create
|
||||
user_db = await mongodb_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 mongodb_user_db.update(user_db)
|
||||
|
||||
# Get by id
|
||||
id_user = await mongodb_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 mongodb_user_db.get_by_email(user.email)
|
||||
assert email_user.id == user_db.id
|
||||
|
||||
# List
|
||||
users = await mongodb_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(pymongo.errors.DuplicateKeyError):
|
||||
await mongodb_user_db.create(user)
|
||||
|
||||
# Unknown user
|
||||
unknown_user = await mongodb_user_db.get_by_email("galahad@camelot.bt")
|
||||
assert unknown_user is None
|
Reference in New Issue
Block a user