Package: fastapi-motor-oil
FastAPI-motor-oilThis project is deprecated and replaced bymotorhead, adding Pydantic v2 support along with a couple of smaller improvements. Please create an issue if you need help with the migration.FastAPI-motor-oilis a collection of async utilities for working with MongoDB and conveniently creating performant APIs with async web frameworks such aFastAPI.Key features:Databasemodeldesign withPydantic.Relationship support and validation using asyncvalidators and delete ruleswith a declarative, decorator-based syntax.Declarativeindexspecification.Typedutilitiesfor convenient model and API creation.Ready to use, customizableasync service layerwithtransaction supportthat integrates all the above to keep your API and business logic clean, flexible, and easy to understand.By providing a convenient, declarative middle layer between MongoDB and your API,FastAPI-motor-oilis halfway between an object document mapper (based onPydantic) and a database driver (by wrapping the official, asyncmotordriver).See thefull documentation here.InstallationThe library is available on PyPI and can be installed with:$pipinstallfastapi-motor-oilExamplePrerequisites:MongoDB (e.g. the Community Edition) installed and running locally;fastapiwith all its dependencies (pip install fastapi[all]);This library (pip install fastapi-motor-oil).In this example we will create:a simpleTreeNodedocument model with anameand an optional reference to aparentnode and some delete rules;the services that are necessary to create, read, update, and delete documents;afastapiAPIRouterfactory that can be included infastapiapplications;and thefastapiapplication itself.The project layout under your root directory will be as follows:/tree_app__init__.pyapi.pymain.pymodel.pyservice.pyModel definitions (inmodel.py):fromfastapi_motor_oilimportDocumentModel,StrObjectId,UTCDatetimefrompydanticimportBaseModelclassTreeNode(DocumentModel):"""Tree node document model."""name:strparent:StrObjectId|Nonecreated_at:UTCDatetimeclassTreeNodeCreate(BaseModel):"""Tree node creation model."""name:strparent:StrObjectId|NoneclassTreeNodeUpdate(BaseModel):"""Tree node update model."""name:str|Noneparent:StrObjectId|NoneService implementation (inservice.py):fromtypingimportAnyfromcollections.abcimportSequencefromdatetimeimportdatetime,timezonefrombsonimportObjectIdfromfastapi_motor_oilimport(CollectionOptions,MongoQuery,MongoService,delete_rule,validator,)frommotor.coreimportAgnosticClientSessionfrom.modelimportTreeNodeCreate,TreeNodeUpdateclassTreeNodeService(MongoService[TreeNodeCreate,TreeNodeUpdate]):"""Tree node database services."""__slots__=()collection_name:str="tree_nodes"collection_options:CollectionOptions|None=None@delete_rule("pre")# Delete rule that remove the subtrees of deleted nodes.asyncdefdr_delete_subtree(self,session:AgnosticClientSession,ids:Sequence[ObjectId])->None:child_ids=awaitself.find_ids({"parent":{"$in":ids}},session=session)iflen(child_ids)>0:# Recursionawaitself.delete_many({"_id":{"$in":child_ids}},options={"session":session})@delete_rule("deny")# Delete rule that prevents the removal of root nodes.asyncdefdr_deny_if_root(self,session:AgnosticClientSession,ids:Sequence[ObjectId])->None:root_cnt=awaitself.count_documents({"$and":[{"_id":{"$in":ids}},{"parent":None}]},options={"session":session},)ifroot_cnt>0:raiseValueError("Can not delete root nodes.")@validator("insert-update")asyncdefv_parent_valid(self,query:MongoQuery|None,data:TreeNodeCreate|TreeNodeUpdate)->None:ifdata.parentisNone:# No parent node is always finereturnifnotawaitself.exists(data.parent):# Parent must exist.raiseValueError("Parent does not exist.")ifisinstance(data,TreeNodeCreate):# No more checks during creation.returnmatched_ids=((awaitself.find_ids(query))ifisinstance(data,TreeNodeUpdate)else[])ifdata.parentinmatched_ids:# Self reference is forbidden.raiseValueError("Self-reference.")asyncdef_convert_for_insert(self,data:TreeNodeCreate)->dict[str,Any]:return{**(awaitsuper()._convert_for_insert(data)),"created_at":datetime.now(timezone.utc),}Routing implementation (inapi.py):fromtypingimportAnyfromfastapiimportAPIRouter,Depends,HTTPException,statusfromfastapi_motor_oilimport(AgnosticDatabase,DatabaseProvider,DeleteError,DeleteResultModel,StrObjectId,)from.modelimportTreeNode,TreeNodeCreate,TreeNodeUpdatefrom.serviceimportTreeNodeServicedefmake_api(*,get_database:DatabaseProvider,prefix:str="/tree-node",)->APIRouter:"""Tree node `APIRouter` factory.Arguments:get_database: FastAPI dependency that returns the `AgnosticDatabase`database instance for the API.prefix: The prefix for the created `APIRouter`.Returns:The created `APIRouter` instance."""api=APIRouter(prefix=prefix)@api.get("/",response_model=list[TreeNode])asyncdefget_all(database:AgnosticDatabase=Depends(get_database),)->list[dict[str,Any]]:svc=TreeNodeService(database)return[dasyncfordinsvc.find()]@api.post("/",response_model=TreeNode)asyncdefcreate(data:TreeNodeCreate,database:AgnosticDatabase=Depends(get_database),)->dict[str,Any]:svc=TreeNodeService(database)try:result=awaitsvc.insert_one(data)exceptException:raiseHTTPException(status_code=status.HTTP_400_BAD_REQUEST,detail="Creation failed.")if(created:=awaitsvc.get_by_id(result.inserted_id))isnotNone:returncreatedraiseHTTPException(status.HTTP_409_CONFLICT)@api.get("/{id}",response_model=TreeNode)asyncdefget_by_id(id:StrObjectId,database:AgnosticDatabase=Depends(get_database),)->dict[str,Any]:svc=TreeNodeService(database)if(result:=awaitsvc.get_by_id(id))isnotNone:returnresultraiseHTTPException(status_code=status.HTTP_404_NOT_FOUND,detail=str(id))@api.put("/{id}",response_model=TreeNode)asyncdefupdate_by_id(id:StrObjectId,data:TreeNodeUpdate,database:AgnosticDatabase=Depends(get_database),)->dict[str,Any]:svc=TreeNodeService(database)try:result=awaitsvc.update_by_id(id,data)exceptException:raiseHTTPException(status_code=status.HTTP_400_BAD_REQUEST,detail=str(id))ifresult.matched_count==0:raiseHTTPException(status.HTTP_404_NOT_FOUND,detail=str(id))if(updated:=awaitsvc.get_by_id(id))isnotNone:returnupdatedraiseHTTPException(status.HTTP_404_NOT_FOUND,detail=str(id))@api.delete("/{id}",response_model=DeleteResultModel)asyncdefdelete_by_id(id:StrObjectId,database:AgnosticDatabase=Depends(get_database),)->DeleteResultModel:svc=TreeNodeService(database)try:result=awaitsvc.delete_by_id(id)exceptDeleteError:raiseHTTPException(status.HTTP_400_BAD_REQUEST,detail=str(id))ifresult.deleted_count==0:raiseHTTPException(status.HTTP_404_NOT_FOUND,detail=str(id))returnDeleteResultModel(delete_count=result.deleted_count)returnapiApplication (inmain.py):fromfunctoolsimportlru_cachefromfastapiimportFastAPIfrommotor.motor_asyncioimportAsyncIOMotorClient,AsyncIOMotorDatabase@lru_cache(maxsize=1)defget_database()->AsyncIOMotorDatabase:"""Database provider dependency for the created API."""mongo_connection_string="mongodb://127.0.0.1:27017"database_name="tree-db"client=AsyncIOMotorClient(mongo_connection_string)returnclient[database_name]defregister_routes(app:FastAPI)->None:"""Registers all routes of the application."""from.apiimportmake_apiasmake_tree_node_apiapi_prefix="/api/v1"app.include_router(make_tree_node_api(get_database=get_database),prefix=api_prefix,)defcreate_app()->FastAPI:app=FastAPI()register_routes(app)returnappWith everything in place, you can serve the application by executinguvicorn tree_app.main:create_app --reload --factoryin your root directory. Go tohttp://127.0.0.1:8000/docsin the browser to see and try the created REST API.RequirementsThe project depends onmotor(the official asyncio MongoDB driver, which is built on top ofpymongoandbson) andpydantic.fastapiis not an actual dependency, but the code was written withfastapiapplications with a REST API in mind.DevelopmentUseblackfor code formatting andmypyfor static code analysis.ContributingContributions are welcome.License - MITThe library is open-sourced under the conditions of theMIT license.