├── app ├── __init__.py ├── main.py └── server │ ├── app.py │ ├── models │ └── student.py │ ├── database.py │ └── routes │ └── student.py ├── .gitignore ├── Procfile ├── requirements.txt ├── README.md └── LICENSE /app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | venv/ 3 | __pycache__ 4 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: uvicorn app.server.app:app --host 0.0.0.0 --port=$PORT 2 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | if __name__ == "__main__": 4 | uvicorn.run("server.app:app", host="0.0.0.0", port=8000, reload=True) 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | motor==3.2.0 2 | uvicorn==0.22.0 3 | fastapi==0.100.0 4 | pydantic[email]==2.0.3 5 | python-decouple==3.8 6 | 7 | # Tested on Python 3.11.3 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # async-fastapi-mongo 2 | 3 | Repository housing code for the Testdriven article. 4 | 5 | [https://testdriven.io/blog/fastapi-mongo/](https://testdriven.io/blog/fastapi-mongo/) 6 | -------------------------------------------------------------------------------- /app/server/app.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from server.routes.student import router as StudentRouter 4 | 5 | app = FastAPI() 6 | 7 | app.include_router(StudentRouter, tags=["Student"], prefix="/student") 8 | 9 | 10 | @app.get("/", tags=["Root"]) 11 | async def read_root(): 12 | return {"message": "Welcome to this fantastic app!"} 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Abdulazeez Abdulazeez Adeshina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/server/models/student.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel, EmailStr, Field 4 | 5 | 6 | class StudentSchema(BaseModel): 7 | fullname: str = Field(...) 8 | email: EmailStr = Field(...) 9 | course_of_study: str = Field(...) 10 | year: int = Field(..., gt=0, lt=9) 11 | gpa: float = Field(..., le=4.0) 12 | 13 | class Config: 14 | json_schema_extra = { 15 | "example": { 16 | "fullname": "John Doe", 17 | "email": "jdoe@x.edu.ng", 18 | "course_of_study": "Water resources engineering", 19 | "year": 2, 20 | "gpa": "3.0", 21 | } 22 | } 23 | 24 | 25 | class UpdateStudentModel(BaseModel): 26 | fullname: Optional[str] 27 | email: Optional[EmailStr] 28 | course_of_study: Optional[str] 29 | year: Optional[int] 30 | gpa: Optional[float] 31 | 32 | class Config: 33 | json_schema_extra = { 34 | "example": { 35 | "fullname": "John Doe", 36 | "email": "jdoe@x.edu.ng", 37 | "course_of_study": "Water resources and environmental engineering", 38 | "year": 4, 39 | "gpa": "4.0", 40 | } 41 | } 42 | 43 | 44 | def ResponseModel(data, message): 45 | return { 46 | "data": [data], 47 | "code": 200, 48 | "message": message, 49 | } 50 | 51 | 52 | def ErrorResponseModel(error, code, message): 53 | return {"error": error, "code": code, "message": message} 54 | -------------------------------------------------------------------------------- /app/server/database.py: -------------------------------------------------------------------------------- 1 | import motor.motor_asyncio 2 | from bson.objectid import ObjectId 3 | from decouple import config 4 | 5 | MONGO_DETAILS = config("MONGO_DETAILS") # read environment variable 6 | 7 | client = motor.motor_asyncio.AsyncIOMotorClient(MONGO_DETAILS) 8 | 9 | database = client.students 10 | 11 | student_collection = database.get_collection("students_collection") 12 | 13 | 14 | # helpers 15 | 16 | 17 | def student_helper(student) -> dict: 18 | return { 19 | "id": str(student["_id"]), 20 | "fullname": student["fullname"], 21 | "email": student["email"], 22 | "course_of_study": student["course_of_study"], 23 | "year": student["year"], 24 | "GPA": student["gpa"], 25 | } 26 | 27 | 28 | # crud operations 29 | 30 | 31 | # Retrieve all students present in the database 32 | async def retrieve_students(): 33 | students = [] 34 | async for student in student_collection.find(): 35 | students.append(student_helper(student)) 36 | return students 37 | 38 | 39 | # Add a new student into to the database 40 | async def add_student(student_data: dict) -> dict: 41 | student = await student_collection.insert_one(student_data) 42 | new_student = await student_collection.find_one({"_id": student.inserted_id}) 43 | return student_helper(new_student) 44 | 45 | 46 | # Retrieve a student with a matching ID 47 | async def retrieve_student(id: str) -> dict: 48 | student = await student_collection.find_one({"_id": ObjectId(id)}) 49 | if student: 50 | return student_helper(student) 51 | 52 | 53 | # Update a student with a matching ID 54 | async def update_student(id: str, data: dict): 55 | # Return false if an empty request body is sent. 56 | if len(data) < 1: 57 | return False 58 | student = await student_collection.find_one({"_id": ObjectId(id)}) 59 | if student: 60 | updated_student = await student_collection.update_one( 61 | {"_id": ObjectId(id)}, {"$set": data} 62 | ) 63 | if updated_student: 64 | return True 65 | return False 66 | 67 | 68 | # Delete a student from the database 69 | async def delete_student(id: str): 70 | student = await student_collection.find_one({"_id": ObjectId(id)}) 71 | if student: 72 | await student_collection.delete_one({"_id": ObjectId(id)}) 73 | return True 74 | -------------------------------------------------------------------------------- /app/server/routes/student.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Body 2 | from fastapi.encoders import jsonable_encoder 3 | 4 | from server.database import ( 5 | add_student, 6 | delete_student, 7 | retrieve_student, 8 | retrieve_students, 9 | update_student, 10 | ) 11 | from server.models.student import ( 12 | ErrorResponseModel, 13 | ResponseModel, 14 | StudentSchema, 15 | UpdateStudentModel, 16 | ) 17 | 18 | router = APIRouter() 19 | 20 | 21 | @router.post("/", response_description="Student data added into the database") 22 | async def add_student_data(student: StudentSchema = Body(...)): 23 | student = jsonable_encoder(student) 24 | new_student = await add_student(student) 25 | return ResponseModel(new_student, "Student added successfully.") 26 | 27 | 28 | @router.get("/", response_description="Students retrieved") 29 | async def get_students(): 30 | students = await retrieve_students() 31 | if students: 32 | return ResponseModel(students, "Students data retrieved successfully") 33 | return ResponseModel(students, "Empty list returned") 34 | 35 | 36 | @router.get("/{id}", response_description="Student data retrieved") 37 | async def get_student_data(id): 38 | student = await retrieve_student(id) 39 | if student: 40 | return ResponseModel(student, "Student data retrieved successfully") 41 | return ErrorResponseModel("An error occurred.", 404, "Student doesn't exist.") 42 | 43 | 44 | @router.put("/{id}") 45 | async def update_student_data(id: str, req: UpdateStudentModel = Body(...)): 46 | req = {k: v for k, v in req.dict().items() if v is not None} 47 | updated_student = await update_student(id, req) 48 | if updated_student: 49 | return ResponseModel( 50 | "Student with ID: {} name update is successful".format(id), 51 | "Student name updated successfully", 52 | ) 53 | return ErrorResponseModel( 54 | "An error occurred", 55 | 404, 56 | "There was an error updating the student data.", 57 | ) 58 | 59 | 60 | @router.delete("/{id}", response_description="Student data deleted from the database") 61 | async def delete_student_data(id: str): 62 | deleted_student = await delete_student(id) 63 | if deleted_student: 64 | return ResponseModel( 65 | "Student with ID: {} removed".format(id), "Student deleted successfully" 66 | ) 67 | return ErrorResponseModel( 68 | "An error occurred", 404, "Student with id {0} doesn't exist".format(id) 69 | ) 70 | --------------------------------------------------------------------------------