Skip to content

Commit c6659cd

Browse files
authored
Merge pull request #4 from mongodb-labs/PYTHON-4089
PYTHON-4089: Switch to using ODMantic
2 parents d35fcc4 + 7ca6177 commit c6659cd

File tree

19 files changed

+69
-182
lines changed

19 files changed

+69
-182
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Accelerate your next web development project with this FastAPI/React/MongoDB bas
44

55
This project is for developers looking to build and maintain full-feature progressive web applications using Python on the backend / Typescript on the frontend, and want the complex-but-routine aspects of auth 'n auth, and component and deployment configuration, taken care of, including interactive API documentation.
66

7-
This is an **experimental** fork of [Sebastián Ramírez's](https://github.com/tiangolo) [Full Stack FastAPI and PostgreSQL Base Project Generator](https://github.com/tiangolo/full-stack-fastapi-postgresql) and [Whythawk's](https://github.com/whythawk) [Full Stack FastAPI and PostgreSQL Base Project Generator](https://github.com/whythawk/full-stack-fastapi-postgresql). FastAPI is updated to version 0.103.2, MongoDB Motor 3.4, Beanie ODM 1.23, and the frontend to React.
7+
This is an **experimental** fork of [Sebastián Ramírez's](https://github.com/tiangolo) [Full Stack FastAPI and PostgreSQL Base Project Generator](https://github.com/tiangolo/full-stack-fastapi-postgresql) and [Whythawk's](https://github.com/whythawk) [Full Stack FastAPI and PostgreSQL Base Project Generator](https://github.com/whythawk/full-stack-fastapi-postgresql). FastAPI is updated to version 0.103.2, MongoDB Motor 3.4, ODMantic ODM 1.0.0, and the frontend to React.
88

99

1010
- [Screenshots](#screenshots)
@@ -50,7 +50,7 @@ This FastAPI, React, MongoDB repo will generate a complete web application stack
5050
- **Authentication** user management schemas, models, crud and apis already built, with OAuth2 JWT token support & default hashing. Offers _magic link_ authentication, with password fallback, with cookie management, including `access` and `refresh` tokens.
5151
- [**FastAPI**](https://github.com/tiangolo/fastapi) backend with [Inboard](https://inboard.bws.bio/) one-repo Docker images:
5252
- **MongoDB Motor** https://motor.readthedocs.io/en/stable/
53-
- **MongoDB Beanie** for handling ODM creation https://beanie-odm.dev/
53+
- **MongoDB ODMantic** for handling ODM creation https://art049.github.io/odmantic/
5454
- **Common CRUD** support via generic inheritance.
5555
- **Standards-based**: Based on (and fully compatible with) the open standards for APIs: [OpenAPI](https://github.com/OAI/OpenAPI-Specification) and [JSON Schema](http://json-schema.org/).
5656
- [**Many other features**]("https://fastapi.tiangolo.com/features/"): including automatic validation, serialization, interactive documentation, etc.
@@ -90,6 +90,9 @@ This stack is in an experimental state, so there is no guarantee for bugs or iss
9090

9191
See notes:
9292

93+
## CalVer 2023.12.XX
94+
- Replaced Beanie usage with ODMantic
95+
9396
## CalVer 2023.11.10
9497

9598
- Replaced Next/Vue.js frontend framework with entirely React/Redux

docs/getting-started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ This FastAPI, React, MongoDB repo will generate a complete web application stack
2525
- **Authentication** user management schemas, models, crud and apis already built, with OAuth2 JWT token support & default hashing. Offers _magic link_ authentication, with password fallback, with cookie management, including `access` and `refresh` tokens.
2626
- [**FastAPI**](https://github.com/tiangolo/fastapi) backend with [Inboard](https://inboard.bws.bio/) one-repo Docker images:
2727
- **Mongo Motor** https://motor.readthedocs.io/en/stable/
28-
- **Mongo Beanie** for handling ODM creation https://beanie-odm.dev/
28+
- **MongoDB ODMantic** for handling ODM creation https://art049.github.io/odmantic/
2929
- **Common CRUD** support via generic inheritance.
3030
- **Standards-based**: Based on (and fully compatible with) the open standards for APIs: [OpenAPI](https://github.com/OAI/OpenAPI-Specification) and [JSON Schema](http://json-schema.org/).
3131
- [**Many other features**]("https://fastapi.tiangolo.com/features/"): including automatic validation, serialization, interactive documentation, etc.
Binary file not shown.

{{cookiecutter.project_slug}}/backend/app/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ Next, open your editor at `./backend/app/` (instead of the project root: `./`),
9292
$ code .
9393
```
9494

95-
Modify or add beanie models in `./backend/app/app/models/` (make sure to include them in `MODELS` within `.backend/app/app/models/__init__.py`), Pydantic schemas in `./backend/app/app/schemas/`, API endpoints in `./backend/app/app/api/`, CRUD (Create, Read, Update, Delete) utils in `./backend/app/app/crud/`. The easiest might be to copy the ones for Items (models, endpoints, and CRUD utils) and update them to your needs.
95+
Modify or add odmantic models in `./backend/app/app/models/` (make sure to include them in `MODELS` within `.backend/app/app/models/__init__.py`), Pydantic schemas in `./backend/app/app/schemas/`, API endpoints in `./backend/app/app/api/`, CRUD (Create, Read, Update, Delete) utils in `./backend/app/app/crud/`. The easiest might be to copy the ones for Items (models, endpoints, and CRUD utils) and update them to your needs.
9696

9797
Add and modify tasks to the Celery worker in `./backend/app/app/worker.py`.
9898

{{cookiecutter.project_slug}}/backend/app/app/crud/base.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
from fastapi.encoders import jsonable_encoder
44
from pydantic import BaseModel
55
from motor.core import AgnosticDatabase
6+
from odmantic import AIOEngine
67

78
from app.db.base_class import Base
89
from app.core.config import settings
10+
from app.db.session import get_engine
911

1012
ModelType = TypeVar("ModelType", bound=Base)
1113
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
@@ -23,18 +25,19 @@ def __init__(self, model: Type[ModelType]):
2325
* `schema`: A Pydantic model (schema) class
2426
"""
2527
self.model = model
28+
self.engine: AIOEngine = get_engine()
2629

2730
async def get(self, db: AgnosticDatabase, id: Any) -> Optional[ModelType]:
28-
return await self.model.get(id)
31+
return await self.engine.find_one(self.model, self.model.id == id)
2932

3033
async def get_multi(self, db: AgnosticDatabase, *, page: int = 0, page_break: bool = False) -> list[ModelType]:
3134
offset = {"skip": page * settings.MULTI_MAX, "limit": settings.MULTI_MAX} if page_break else {}
32-
return await self.model.find_all(**offset).to_list()
35+
return await self.engine.find(self.model, **offset)
3336

3437
async def create(self, db: AgnosticDatabase, *, obj_in: CreateSchemaType) -> ModelType:
3538
obj_in_data = jsonable_encoder(obj_in)
3639
db_obj = self.model(**obj_in_data) # type: ignore
37-
return await db_obj.create()
40+
return await self.engine.save(db_obj)
3841

3942
async def update(
4043
self, db: AgnosticDatabase, *, db_obj: ModelType, obj_in: Union[UpdateSchemaType, Dict[str, Any]]
@@ -43,16 +46,16 @@ async def update(
4346
if isinstance(obj_in, dict):
4447
update_data = obj_in
4548
else:
46-
update_data = obj_in.dict(exclude_unset=True)
49+
update_data = obj_in.model_dump(exclude_unset=True)
4750
for field in obj_data:
4851
if field in update_data:
4952
setattr(db_obj, field, update_data[field])
5053
# TODO: Check if this saves changes with the setattr calls
51-
await db_obj.save()
54+
await self.engine.save(db_obj)
5255
return db_obj
5356

5457
async def remove(self, db: AgnosticDatabase, *, id: int) -> ModelType:
5558
obj = await self.model.get(id)
5659
if obj:
57-
await obj.delete()
60+
await self.engine.delete(obj)
5861
return obj
Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
from __future__ import annotations
22
from motor.core import AgnosticDatabase
3-
from beanie import WriteRules
4-
from beanie.operators import And, In, PullAll
53

64
from app.crud.base import CRUDBase
75
from app.models import User, Token
@@ -12,27 +10,31 @@
1210
class CRUDToken(CRUDBase[Token, RefreshTokenCreate, RefreshTokenUpdate]):
1311
# Everything is user-dependent
1412
async def create(self, db: AgnosticDatabase, *, obj_in: str, user_obj: User) -> Token:
15-
db_obj = await self.model.find_one(self.model.token == obj_in)
13+
db_obj = await self.engine.find_one(self.model, self.model.token == obj_in)
1614
if db_obj:
1715
if db_obj.authenticates_id != user_obj.id:
1816
raise ValueError("Token mismatch between key and user.")
1917
return db_obj
2018
else:
21-
new_token = self.model(token=obj_in, authenticates_id=user_obj.id)
22-
user_obj.refresh_tokens.append(new_token)
23-
await user_obj.save(link_rule=WriteRules.WRITE)
19+
new_token = self.model(token=obj_in, authenticates_id=user_obj)
20+
user_obj.refresh_tokens.append(new_token.id)
21+
await self.engine.save_all([new_token, user_obj])
2422
return new_token
2523

2624
async def get(self, *, user: User, token: str) -> Token:
27-
return await user.find_one(And(User.id == user.id, User.refresh_tokens.token == token), fetch_links=True)
25+
return await self.engine.find_one(User, ((User.id == user.id) & (User.refresh_tokens.token == token)))
2826

2927
async def get_multi(self, *, user: User, page: int = 0, page_break: bool = False) -> list[Token]:
3028
offset = {"skip": page * settings.MULTI_MAX, "limit": settings.MULTI_MAX} if page_break else {}
31-
return await User.find(In(User.refresh_tokens, user.refresh_tokens), **offset).to_list()
29+
return await self.engine.find(User, (User.refresh_tokens.in_([user.refresh_tokens])), **offset)
3230

3331
async def remove(self, db: AgnosticDatabase, *, db_obj: Token) -> None:
34-
await User.update_all(PullAll({User.refresh_tokens: [db_obj.to_ref()]}))
35-
await db_obj.delete()
32+
users = []
33+
async for user in self.engine.find(User, User.refresh_tokens.in_([db_obj.id])):
34+
user.refresh_tokens.remove(db_obj.id)
35+
users.append(user)
36+
await self.engine.save(users)
37+
await self.engine.delete(db_obj)
3638

3739

3840
token = CRUDToken(Token)

{{cookiecutter.project_slug}}/backend/app/app/crud/crud_user.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# ODM, Schema, Schema
1313
class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
1414
async def get_by_email(self, db: AgnosticDatabase, *, email: str) -> Optional[User]:
15-
return await User.find_one(User.email == email)
15+
return await self.engine.find_one(User, User.email == email)
1616

1717
async def create(self, db: AgnosticDatabase, *, obj_in: UserCreate) -> User:
1818
# TODO: Figure out what happens when you have a unique key like 'email'
@@ -24,7 +24,7 @@ async def create(self, db: AgnosticDatabase, *, obj_in: UserCreate) -> User:
2424
"is_superuser": obj_in.is_superuser,
2525
}
2626

27-
return await User(**user).create()
27+
return await self.engine.save(User(**user))
2828

2929
async def update(self, db: AgnosticDatabase, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]]) -> User:
3030
if isinstance(obj_in, dict):
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
from beanie import Document
1+
from odmantic import Model
22

3-
Base = Document
3+
Base = Model

{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
from pymongo.database import Database
2-
from beanie import init_beanie
32

43
from app import crud, schemas
54
from app.core.config import settings
6-
from app.models import MODELS
75

86

97
async def init_db(db: Database) -> None:
10-
await init_beanie(db, document_models=MODELS)
118
user = await crud.user.get_by_email(db, email=settings.FIRST_SUPERUSER)
129
if not user:
1310
# Create user auth

{{cookiecutter.project_slug}}/backend/app/app/db/session.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,34 @@
11
from app.core.config import settings
22
from app.__version__ import __version__
33
from motor import motor_asyncio, core
4+
from odmantic import AIOEngine
45
from pymongo.driver_info import DriverInfo
56

6-
DRIVER_INFO=DriverInfo(name="full-stack-fastapi-mongodb", version=__version__)
7+
DRIVER_INFO = DriverInfo(name="full-stack-fastapi-mongodb", version=__version__)
78

89

910
class _MongoClientSingleton:
1011
mongo_client: motor_asyncio.AsyncIOMotorClient | None
12+
engine: AIOEngine
1113

1214
def __new__(cls):
1315
if not hasattr(cls, "instance"):
1416
cls.instance = super(_MongoClientSingleton, cls).__new__(cls)
1517
cls.instance.mongo_client = motor_asyncio.AsyncIOMotorClient(
16-
settings.MONGO_DATABASE_URI,
17-
driver=DRIVER_INFO
18+
settings.MONGO_DATABASE_URI, driver=DRIVER_INFO
19+
)
20+
cls.instance.engine = AIOEngine(
21+
client=cls.instance.mongo_client,
22+
database=settings.MONGO_DATABASE
1823
)
1924
return cls.instance
2025

2126

2227
def MongoDatabase() -> core.AgnosticDatabase:
2328
return _MongoClientSingleton().mongo_client[settings.MONGO_DATABASE]
2429

30+
def get_engine() -> AIOEngine:
31+
return _MongoClientSingleton().engine
2532

2633
async def ping():
2734
await MongoDatabase().command("ping")

0 commit comments

Comments
 (0)