Skip to content

Commit

Permalink
Merge pull request #21 from qitpy/update/deprecated-beanie-config-and…
Browse files Browse the repository at this point in the history
…-add-tests

Update deprecated beanie config and add UnitTests
  • Loading branch information
Youngestdev authored Jun 25, 2023
2 parents e19685c + ae6843f commit fbab3ca
Show file tree
Hide file tree
Showing 22 changed files with 214 additions and 74 deletions.
2 changes: 1 addition & 1 deletion .env.sample
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
DATABASE_URL=mongodb://mongodb:27017/lms
DATABASE_URL=mongodb://localhost:27017/lms
secret_key=guiyfgc837tgf3iw87-012389764
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,7 @@ celerybeat.pid
*.sage.py

# Environments
.env
.env.*
.env.dev
.venv
env/
venv/
Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ $ python3 -m venv venv
```console
(venv)$ pip3 install -r requirements.txt
```
3. You also need to start your mongodb instance either locally or on Docker as well as create a `.env.dev` file. See the `.env.sample` for configurations.
3. You also need to start your mongodb instance either locally or on Docker as well as create a `.env.dev` file. See the `.env.sample` for configurations.

Example for running locally MongoDB at port 27017:
```console
cp .env.sample .env.dev
```

4. Start the application:

Expand All @@ -37,6 +42,18 @@ The starter listens on port 8000 on address [0.0.0.0](0.0.0.0:8080).

![FastAPI-MongoDB starter](https://user-images.githubusercontent.com/31009679/165318867-4a0504d5-1fd0-4adc-8df9-db2ff3c0c3b9.png)


## Testing

To run the tests, run the following command:

```console
(venv)$ pytest
```

You can also write your own tests in the `tests` directory.
The test follow by the official support [FastAPI testing guide](https://fastapi.tiangolo.com/tutorial/testing/), [pytest](https://docs.pytest.org/en/stable/), [anyio](https://anyio.readthedocs.io/en/stable/) for async testing application.

## Deployment

This application can be deployed on any PaaS such as [Heroku](https://heroku.com) or [Okteto](https://okteto) and any other cloud service provider.
Expand Down
7 changes: 6 additions & 1 deletion app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,9 @@ async def read_root():


app.include_router(AdminRouter, tags=["Administrator"], prefix="/admin")
app.include_router(StudentRouter, tags=["Students"], prefix="/student", dependencies=[Depends(token_listener)])
app.include_router(
StudentRouter,
tags=["Students"],
prefix="/student",
dependencies=[Depends(token_listener)],
)
9 changes: 4 additions & 5 deletions auth/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,13 @@
async def validate_login(credentials: HTTPBasicCredentials = Depends(security)):
admin = admin_collection.find_one({"email": credentials.username})
if admin:
password = hash_helper.verify(credentials.password, admin['password'])
password = hash_helper.verify(credentials.password, admin["password"])
if not password:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password"
detail="Incorrect email or password",
)
return True
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password"
)
status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password"
)
13 changes: 9 additions & 4 deletions auth/jwt_bearer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,24 @@ def verify_jwt(jwtoken: str) -> bool:


class JWTBearer(HTTPBearer):

def __init__(self, auto_error: bool = True):
super(JWTBearer, self).__init__(auto_error=auto_error)

async def __call__(self, request: Request):
credentials: HTTPAuthorizationCredentials = await super(JWTBearer, self).__call__(request)
credentials: HTTPAuthorizationCredentials = await super(
JWTBearer, self
).__call__(request)
print("Credentials :", credentials)
if credentials:
if not credentials.scheme == "Bearer":
raise HTTPException(status_code=403, detail="Invalid authentication token")
raise HTTPException(
status_code=403, detail="Invalid authentication token"
)

if not verify_jwt(credentials.credentials):
raise HTTPException(status_code=403, detail="Invalid token or expired token")
raise HTTPException(
status_code=403, detail="Invalid token or expired token"
)

return credentials.credentials
else:
Expand Down
11 changes: 3 additions & 8 deletions auth/jwt_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,18 @@


def token_response(token: str):
return {
"access_token": token
}
return {"access_token": token}


secret_key = Settings().secret_key


def sign_jwt(user_id: str) -> Dict[str, str]:
# Set the expiry time.
payload = {
'user_id': user_id,
'expires': time.time() + 2400
}
payload = {"user_id": user_id, "expires": time.time() + 2400}
return token_response(jwt.encode(payload, secret_key, algorithm="HS256"))


def decode_jwt(token: str) -> dict:
decoded_token = jwt.decode(token.encode(), secret_key, algorithms=["HS256"])
return decoded_token if decoded_token['expires'] >= time.time() else {}
return decoded_token if decoded_token["expires"] >= time.time() else {}
10 changes: 5 additions & 5 deletions config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@
from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseSettings

from models.admin import Admin
from models.student import Student
import models as models


class Settings(BaseSettings):
# database configurations
DATABASE_URL: Optional[str] = None

# JWT
secret_key: str
secret_key: str = "secret"
algorithm: str = "HS256"

class Config:
Expand All @@ -23,5 +22,6 @@ class Config:

async def initiate_database():
client = AsyncIOMotorClient(Settings().DATABASE_URL)
await init_beanie(database=client.get_default_database(),
document_models=[Admin, Student])
await init_beanie(
database=client.get_default_database(), document_models=models.__all__
)
4 changes: 1 addition & 3 deletions database/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ async def delete_student(id: PydanticObjectId) -> bool:

async def update_student_data(id: PydanticObjectId, data: dict) -> Union[bool, Student]:
des_body = {k: v for k, v in data.items() if v is not None}
update_query = {"$set": {
field: value for field, value in des_body.items()
}}
update_query = {"$set": {field: value for field, value in des_body.items()}}
student = await student_collection.get(id)
if student:
await student.update(update_query)
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ services:
ports:
- "8080:8080"
env_file:
- .env
- .env.docker-compose

mongodb:
image: bitnami/mongodb:latest
Expand Down
4 changes: 2 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import uvicorn

if __name__ == '__main__':
uvicorn.run('app:app', host="0.0.0.0", port=8080, reload=True)
if __name__ == "__main__":
uvicorn.run("app:app", host="0.0.0.0", port=8080, reload=True)
4 changes: 4 additions & 0 deletions models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from models.admin import Admin
from models.student import Student

__all__ = [Student, Admin]
13 changes: 5 additions & 8 deletions models/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,23 @@ class Admin(Document):
email: EmailStr
password: str

class Collection:
name = "admin"

class Config:
schema_extra = {
"example": {
"fullname": "Abdulazeez Abdulazeez Adeshina",
"email": "[email protected]",
"password": "3xt3m#"
"password": "3xt3m#",
}
}

class Settings:
name = "admin"


class AdminSignIn(HTTPBasicCredentials):
class Config:
schema_extra = {
"example": {
"username": "[email protected]",
"password": "3xt3m#"
}
"example": {"username": "[email protected]", "password": "3xt3m#"}
}


Expand Down
9 changes: 6 additions & 3 deletions models/student.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ class Config:
"email": "[email protected]",
"course_of_study": "Water resources engineering",
"year": 4,
"gpa": "3.76"
"gpa": "3.76",
}
}

class Settings:
name = "student"


class UpdateStudentModel(BaseModel):
fullname: Optional[str]
Expand All @@ -40,7 +43,7 @@ class Config:
"email": "[email protected]",
"course_of_study": "Water resources and environmental engineering",
"year": 4,
"gpa": "5.0"
"gpa": "5.0",
}
}

Expand All @@ -57,6 +60,6 @@ class Config:
"status_code": 200,
"response_type": "success",
"description": "Operation successful",
"data": "Sample data"
"data": "Sample data",
}
}
22 changes: 13 additions & 9 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
motor~=2.5.1
PyJWT~=2.4.0
bcrypt
uvicorn~=0.17.6
pymongo~=3.12.3
fastapi~=0.78.0
pydantic[email,dotenv]~=1.9.0
passlib~=1.7.4
motor #3.1.1
PyJWT
bcrypt # 4.0.1
uvicorn # 0.21.1
pymongo # 4.3.3
fastapi # 0.95.0
pydantic[email,dotenv] #1.10.7
passlib # 1.7.4

beanie~=1.10.8
beanie # 1.18.0
pytest # 7.3.1
httpx # 0.24.0
mongomock_motor # 0.0.19
pytest-mock # 3.10.0
16 changes: 4 additions & 12 deletions routes/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,21 @@
async def admin_login(admin_credentials: AdminSignIn = Body(...)):
admin_exists = await Admin.find_one(Admin.email == admin_credentials.username)
if admin_exists:
password = hash_helper.verify(
admin_credentials.password, admin_exists.password)
password = hash_helper.verify(admin_credentials.password, admin_exists.password)
if password:
return sign_jwt(admin_credentials.username)

raise HTTPException(
status_code=403,
detail="Incorrect email or password"
)
raise HTTPException(status_code=403, detail="Incorrect email or password")

raise HTTPException(
status_code=403,
detail="Incorrect email or password"
)
raise HTTPException(status_code=403, detail="Incorrect email or password")


@router.post("/new", response_model=AdminData)
async def admin_signup(admin: Admin = Body(...)):
admin_exists = await Admin.find_one(Admin.email == admin.email)
if admin_exists:
raise HTTPException(
status_code=409,
detail="Admin with email supplied already exists"
status_code=409, detail="Admin with email supplied already exists"
)

admin.password = hash_helper.encrypt(admin.password)
Expand Down
24 changes: 15 additions & 9 deletions routes/student.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,21 @@ async def get_students():
"status_code": 200,
"response_type": "success",
"description": "Students data retrieved successfully",
"data": students
"data": students,
}


@router.get("/{id}", response_description="Student data retrieved", response_model=Response)
@router.get(
"/{id}", response_description="Student data retrieved", response_model=Response
)
async def get_student_data(id: PydanticObjectId):
student = await retrieve_student(id)
if student:
return {
"status_code": 200,
"response_type": "success",
"description": "Student data retrieved successfully",
"data": student
"data": student,
}
return {
"status_code": 404,
Expand All @@ -34,14 +36,18 @@ async def get_student_data(id: PydanticObjectId):
}


@router.post("/", response_description="Student data added into the database", response_model=Response)
@router.post(
"/",
response_description="Student data added into the database",
response_model=Response,
)
async def add_student_data(student: Student = Body(...)):
new_student = await add_student(student)
return {
"status_code": 200,
"response_type": "success",
"description": "Student created successfully",
"data": new_student
"data": new_student,
}


Expand All @@ -53,13 +59,13 @@ async def delete_student_data(id: PydanticObjectId):
"status_code": 200,
"response_type": "success",
"description": "Student with ID: {} removed".format(id),
"data": deleted_student
"data": deleted_student,
}
return {
"status_code": 404,
"response_type": "error",
"description": "Student with id {0} doesn't exist".format(id),
"data": False
"data": False,
}


Expand All @@ -71,11 +77,11 @@ async def update_student(id: PydanticObjectId, req: UpdateStudentModel = Body(..
"status_code": 200,
"response_type": "success",
"description": "Student with ID: {} updated".format(id),
"data": updated_student
"data": updated_student,
}
return {
"status_code": 404,
"response_type": "error",
"description": "An error occurred. Student with ID: {} not found".format(id),
"data": False
"data": False,
}
Empty file added tests/__init__.py
Empty file.
Loading

0 comments on commit fbab3ca

Please sign in to comment.