Backend optimizations

- Added constants file
- Added api version to metadata on api responses
- Fixed bug that some routes raised an exception when date and datetime
columns where set in the database and not properly parsed before JSON
Response
This commit is contained in:
João Vitória Silva
2024-01-08 00:13:21 +00:00
parent 51ec61c3e1
commit d197c6a475
6 changed files with 184 additions and 96 deletions

6
backend/constants.py Normal file
View File

@@ -0,0 +1,6 @@
# Constant related to version
API_VERSION="v0.1.2"
# Constants related to user access types
ADMIN_ACCESS = 2
REGULAR_ACCESS = 1

View File

@@ -59,6 +59,7 @@ import calendar
from dependencies import get_db_session, create_error_response, get_current_user
from sqlalchemy.orm import Session
from fastapi.responses import JSONResponse
from constants import API_VERSION
# Define the API router
router = APIRouter()
@@ -220,7 +221,7 @@ async def read_activities_all(
]
# Include metadata in the response
metadata = {"total_records": len(activity_records)}
metadata = {"total_records": len(activity_records), "api_version": API_VERSION}
# Return the queried values using JSONResponse
return JSONResponse(
@@ -272,7 +273,7 @@ async def read_activities_useractivities(
]
# Include metadata in the response
metadata = {"total_records": len(activity_records)}
metadata = {"total_records": len(activity_records), "api_version": API_VERSION}
# Return the queried values using JSONResponse
return JSONResponse(
@@ -347,7 +348,12 @@ async def read_activities_useractivities_thisweek_number(
]
# Include metadata in the response
metadata = {"total_records": len(activity_records)}
metadata = {
"total_records": len(activity_records),
"user_id": user_id,
"week_number": week_number,
"api_version": API_VERSION,
}
# Return the queried values using JSONResponse
return JSONResponse(
@@ -416,15 +422,13 @@ async def read_activities_useractivities_thisweek_distances(
distances = calculate_activity_distances(activity_records)
# Return the queried values using JSONResponse
#return JSONResponse(content=distances)
# return JSONResponse(content=distances)
# Include metadata in the response
metadata = {"total_records": 1}
metadata = {"total_records": 1, "user_id": user_id, "api_version": API_VERSION}
# Return the queried values using JSONResponse
return JSONResponse(
content={"metadata": metadata, "content": distances}
)
return JSONResponse(content={"metadata": metadata, "content": distances})
except JWTError:
# Return an error response if the user is not authenticated
return create_error_response("UNAUTHORIZED", "Unauthorized", 401)
@@ -487,15 +491,13 @@ async def read_activities_useractivities_thismonth_distances(
distances = calculate_activity_distances(activity_records)
# Return the queried values using JSONResponse
#return JSONResponse(content=distances)
# return JSONResponse(content=distances)
# Include metadata in the response
metadata = {"total_records": 1}
metadata = {"total_records": 1, "user_id": user_id, "api_version": API_VERSION}
# Return the queried values using JSONResponse
return JSONResponse(
content={"metadata": metadata, "content": distances}
)
return JSONResponse(content={"metadata": metadata, "content": distances})
except JWTError:
# Return an error response if the user is not authenticated
return create_error_response("UNAUTHORIZED", "Unauthorized", 401)
@@ -554,12 +556,10 @@ async def read_activities_useractivities_thismonth_number(
)
# Include metadata in the response
metadata = {"total_records": 1}
metadata = {"total_records": 1, "user_id": user_id, "api_version": API_VERSION}
# Return the queried values using JSONResponse
return JSONResponse(
content={"metadata": metadata, "content": activity_count}
)
return JSONResponse(content={"metadata": metadata, "content": activity_count})
except JWTError:
# Return an error response if the user is not authenticated
@@ -575,9 +575,9 @@ async def read_activities_useractivities_thismonth_number(
)
@router.get("/activities/gear/{gearID}", response_model=List[dict])
@router.get("/activities/gear/{gear_id}", response_model=List[dict])
async def read_activities_gearactivities(
gearID=int,
gear_id=int,
user_id: int = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
@@ -600,7 +600,7 @@ async def read_activities_gearactivities(
# Query the activities records using SQLAlchemy
activity_records = (
db_session.query(Activity)
.filter(Activity.user_id == user_id, Activity.gear_id == gearID)
.filter(Activity.user_id == user_id, Activity.gear_id == gear_id)
.order_by(desc(Activity.start_time))
.all()
)
@@ -611,7 +611,11 @@ async def read_activities_gearactivities(
]
# Include metadata in the response
metadata = {"total_records": len(activity_records)}
metadata = {
"total_records": len(activity_records),
"gear_id": gear_id,
"api_version": API_VERSION,
}
# Return the queried values using JSONResponse
return JSONResponse(
@@ -655,12 +659,10 @@ async def read_activities_all_number(
activity_count = db_session.query(func.count(Activity.id)).scalar()
# Include metadata in the response
metadata = {"total_records": 1}
metadata = {"total_records": 1, "api_version": API_VERSION}
# Return the queried values using JSONResponse
return JSONResponse(
content={"metadata": metadata, "content": activity_count}
)
return JSONResponse(content={"metadata": metadata, "content": activity_count})
except JWTError:
# Return an error response if the user is not authenticated
@@ -701,12 +703,10 @@ async def read_activities_useractivities_number(
)
# Include metadata in the response
metadata = {"total_records": 1}
metadata = {"total_records": 1, "api_version": API_VERSION}
# Return the queried values using JSONResponse
return JSONResponse(
content={"metadata": metadata, "content": activity_count}
)
return JSONResponse(content={"metadata": metadata, "content": activity_count})
except JWTError:
# Return an error response if the user is not authenticated
@@ -756,12 +756,10 @@ async def read_activities_followed_useractivities_number(
)
# Include metadata in the response
metadata = {"total_records": 1}
metadata = {"total_records": 1, "api_version": API_VERSION}
# Return the queried values using JSONResponse
return JSONResponse(
content={"metadata": metadata, "content": activity_count}
)
return JSONResponse(content={"metadata": metadata, "content": activity_count})
except JWTError:
# Return an error response if the user is not authenticated
@@ -826,6 +824,7 @@ async def read_activities_all_pagination(
"total_records": len(activity_records),
"page_number": pageNumber,
"num_records": numRecords,
"api_version": API_VERSION,
}
# Return the queried values using JSONResponse
@@ -891,6 +890,7 @@ async def read_activities_useractivities_pagination(
"total_records": len(activity_records),
"page_number": pageNumber,
"num_records": numRecords,
"api_version": API_VERSION,
}
# Return the queried values using JSONResponse
@@ -966,6 +966,7 @@ async def read_activities_followed_user_activities_pagination(
"total_records": len(activity_records),
"page_number": pageNumber,
"num_records": numRecords,
"api_version": API_VERSION,
}
# Return the queried values using JSONResponse
@@ -1032,7 +1033,11 @@ async def read_activities_activityFromId(
]
# Include metadata in the response
metadata = {"total_records": len(activity_records)}
metadata = {
"total_records": len(activity_records),
"id": id,
"api_version": API_VERSION,
}
# Return the queried values using JSONResponse
return JSONResponse(

View File

@@ -46,6 +46,7 @@ from db.db import (
)
from dependencies import get_db_session, create_error_response
from sqlalchemy.orm import Session
from constants import API_VERSION
# Define the API router
router = APIRouter()
@@ -97,7 +98,12 @@ async def read_followers_user_specific_user(
if follower:
# Include metadata in the response
metadata = {"total_records": 1}
metadata = {
"total_records": 1,
"user_id": user_id,
"target_user_id": target_user_id,
"api_version": API_VERSION,
}
# User follows target_user_id or vice versa
response_data = {
@@ -112,14 +118,18 @@ async def read_followers_user_specific_user(
)
# Users are not following each other
return create_error_response("NOT_FOUND", "Users are not following each other.", 404)
return create_error_response(
"NOT_FOUND", "Users are not following each other.", 404
)
except JWTError:
# Return an error response if the user is not authenticated
return create_error_response("UNAUTHORIZED", "Unauthorized", 401)
except Exception as err:
# Log the error and return an error response
logger.error(f"Error in read_followers_user_specific_user: {err}", exc_info=True)
logger.error(
f"Error in read_followers_user_specific_user: {err}", exc_info=True
)
return create_error_response(
"INTERNAL_SERVER_ERROR", "Internal Server Error", 500
)
@@ -156,12 +166,10 @@ async def get_user_follower_count_all(
)
# Include metadata in the response
metadata = {"total_records": 1}
metadata = {"total_records": 1, "user_id": user_id, "api_version": API_VERSION}
# Return the queried values using JSONResponse
return JSONResponse(
content={"metadata": metadata, "content": follower_count}
)
return JSONResponse(content={"metadata": metadata, "content": follower_count})
except JWTError:
# Return an error response if the user is not authenticated
@@ -174,7 +182,6 @@ async def get_user_follower_count_all(
)
@router.get("/followers/user/{user_id}/followers/count")
async def get_user_follower_count(
user_id: int,
@@ -208,12 +215,10 @@ async def get_user_follower_count(
)
# Include metadata in the response
metadata = {"total_records": 1}
metadata = {"total_records": 1, "user_id": user_id, "api_version": API_VERSION}
# Return the queried values using JSONResponse
return JSONResponse(
content={"metadata": metadata, "content": follower_count}
)
return JSONResponse(content={"metadata": metadata, "content": follower_count})
except JWTError:
# Return an error response if the user is not authenticated
@@ -263,12 +268,14 @@ async def get_user_follower_all(
]
# Include metadata in the response
metadata = {"total_records": len(followers_list)}
metadata = {
"total_records": len(followers_list),
"user_id": user_id,
"api_version": API_VERSION,
}
# Return the queried values using JSONResponse
return JSONResponse(
content={"metadata": metadata, "content": followers_list}
)
return JSONResponse(content={"metadata": metadata, "content": followers_list})
except JWTError:
# Return an error response if the user is not authenticated
@@ -312,12 +319,10 @@ async def get_user_following_count_all(
)
# Include metadata in the response
metadata = {"total_records": 1}
metadata = {"total_records": 1, "user_id": user_id, "api_version": API_VERSION}
# Return the queried values using JSONResponse
return JSONResponse(
content={"metadata": metadata, "content": following_count}
)
return JSONResponse(content={"metadata": metadata, "content": following_count})
except JWTError:
# Return an error response if the user is not authenticated
@@ -363,12 +368,10 @@ async def get_user_following_count(
)
# Include metadata in the response
metadata = {"total_records": 1}
metadata = {"total_records": 1, "user_id": user_id, "api_version": API_VERSION}
# Return the queried values using JSONResponse
return JSONResponse(
content={"metadata": metadata, "content": following_count}
)
return JSONResponse(content={"metadata": metadata, "content": following_count})
except JWTError:
# Return an error response if the user is not authenticated
@@ -421,12 +424,14 @@ async def get_user_following_all(
]
# Include metadata in the response
metadata = {"total_records": len(following_list)}
metadata = {
"total_records": len(following_list),
"user_id": user_id,
"api_version": API_VERSION,
}
# Return the queried values using JSONResponse
return JSONResponse(
content={"metadata": metadata, "content": following_list}
)
return JSONResponse(content={"metadata": metadata, "content": following_list})
except JWTError:
# Return an error response if the user is not authenticated
@@ -543,7 +548,9 @@ async def create_follow(
if existing_follow:
# Follow relationship already exists
return create_error_response("BAD_REQUEST", "Follow relationship already exists.", 400)
return create_error_response(
"BAD_REQUEST", "Follow relationship already exists.", 400
)
# Create a new follow relationship
new_follow = Follower(

View File

@@ -47,6 +47,7 @@ from urllib.parse import unquote
from pydantic import BaseModel
from dependencies import get_db_session, create_error_response, get_current_user
from fastapi.responses import JSONResponse
from constants import API_VERSION
# Define the API router
router = APIRouter()
@@ -162,7 +163,7 @@ async def read_gear_all(
gear_records_dict = [gear_record_to_dict(record) for record in gear_records]
# Include metadata in the response
metadata = {"total_records": len(gear_records)}
metadata = {"total_records": len(gear_records), "api_version": API_VERSION}
# Return the queried values using JSONResponse
return JSONResponse(
@@ -211,7 +212,11 @@ async def read_gear_all_by_type(
# Validate the gear type (example validation)
if not (1 <= gear_type <= 3):
# Return an error response if the gear type in invalid
return create_error_response("UNPROCESSABLE COMTENT", "Invalid gear type. Must be between 1 and 3", 422)
return create_error_response(
"UNPROCESSABLE COMTENT",
"Invalid gear type. Must be between 1 and 3",
422,
)
# Query the gear records using SQLAlchemy and filter by gear type and user ID
gear_records = (
@@ -228,6 +233,7 @@ async def read_gear_all_by_type(
metadata = {
"total_records": len(gear_records),
"gear_type": gear_type,
"api_version": API_VERSION,
}
# Return the queried values using JSONResponse
@@ -278,7 +284,7 @@ async def read_gear_number(
)
# Include metadata in the response
metadata = {"total_records": 1}
metadata = {"total_records": 1, "api_version": API_VERSION}
# Return the queried values using JSONResponse
return JSONResponse(content={"metadata": metadata, "content": gear_count})
@@ -297,7 +303,7 @@ async def read_gear_number(
@router.get(
"/gear/all/pagenumber/{pageNumber}/numRecords/{numRecords}",
response_model=List[dict],
#tags=["Pagination"],
# tags=["Pagination"],
)
async def read_gear_all_pagination(
pageNumber: int,
@@ -344,6 +350,7 @@ async def read_gear_all_pagination(
"total_records": len(gear_records),
"page_number": pageNumber,
"num_records": numRecords,
"api_version": API_VERSION,
}
# Return the queried values using JSONResponse
@@ -407,6 +414,7 @@ async def read_gear_nickname(
metadata = {
"total_records": len(gear_records),
"nickname": nickname,
"api_version": API_VERSION,
}
# Return the queried values using JSONResponse
@@ -464,6 +472,7 @@ async def read_gear_id(
metadata = {
"total_records": len(gear_records),
"id": id,
"api_version": API_VERSION,
}
# Return the queried values using JSONResponse
@@ -594,7 +603,11 @@ async def edit_gear(
)
else:
# Return an error response if the gear record does not belong to the user
return create_error_response("NOT_FOUND", f"Gear does not belong to user {user_id}. Will not delete", 404)
return create_error_response(
"NOT_FOUND",
f"Gear does not belong to user {user_id}. Will not delete",
404,
)
else:
# Return an error response if the gear record is not found
return create_error_response("NOT_FOUND", "Gear not found", 404)
@@ -657,7 +670,11 @@ async def delete_gear(
)
else:
# Return an error response if the gear record does not belong to the user
return create_error_response("NOT_FOUND", f"Gear does not belong to user {user_id}. Will not delete", 404)
return create_error_response(
"NOT_FOUND",
f"Gear does not belong to user {user_id}. Will not delete",
404,
)
else:
# Return an error response if the gear record is not found
return create_error_response("NOT_FOUND", "Gear not found", 404)

View File

@@ -65,6 +65,8 @@ from pydantic import BaseModel
# Custom dependencies for dependency injection in FastAPI
from dependencies import get_db_session, create_error_response
from constants import ADMIN_ACCESS
# Define the API router
router = APIRouter()
@@ -434,7 +436,7 @@ def get_user_data(db_session: Session, token: str = Depends(oauth2_scheme)):
"username": user.username,
"email": user.email,
"city": user.city,
"birthdate": user.birthdate,
"birthdate": user.birthdate.strftime('%Y-%m-%d') if user.birthdate else None,
"preferred_language": user.preferred_language,
"gender": user.gender,
"access_type": user.access_type,
@@ -487,6 +489,8 @@ def validate_token(db_session: Session, token: str):
)
if not access_token or datetime.utcnow() > datetime.fromtimestamp(exp):
logger.info("Token expired, will force remove_expired_tokens to run")
remove_expired_tokens(db_session=Session)
raise JWTError("Token expired")
else:
return {"message": "Token is valid"}
@@ -517,7 +521,7 @@ def validate_admin_access(token: str):
"""
try:
user_access_type = get_access_type_from_token(token)
if user_access_type != 2:
if user_access_type != ADMIN_ACCESS:
return create_error_response("UNAUTHORIZED", "Unauthorized", 401)
except JWTError:
raise JWTError("Invalid token")

View File

@@ -53,6 +53,7 @@ from db.db import (
)
from urllib.parse import unquote
from dependencies import get_db_session, create_error_response
from constants import API_VERSION
# Define the API router
router = APIRouter()
@@ -94,19 +95,22 @@ class UserBase(BaseModel):
photo_path_aux: Optional[str]
is_active: int
class UserCreateRequest(UserBase):
"""
Pydantic model for creating user records.
Inherits from UserBase, which defines the base attributes for user.
This class extends the UserBase Pydantic model and is designed for creating
This class extends the UserBase Pydantic model and is designed for creating
new user records. Includes an additional attribute 'password'
to idefine user password.
"""
password: str
class UserEditRequest(UserBase):
"""
Pydantic model for editing user records.
@@ -116,8 +120,10 @@ class UserEditRequest(UserBase):
This class extends the UserBase Pydantic model and is specifically tailored for
editing existing user records.
"""
pass
class UserResponse(UserBase):
"""
Pydantic model for representing user responses.
@@ -135,6 +141,7 @@ class UserResponse(UserBase):
id: int
is_strava_linked: Optional[int]
# Define a function to convert User SQLAlchemy objects to dictionaries
def user_record_to_dict(record: User) -> dict:
"""
@@ -154,7 +161,9 @@ def user_record_to_dict(record: User) -> dict:
"username": record.username,
"email": record.email,
"city": record.city,
"birthdate": record.birthdate,
"birthdate": record.birthdate.strftime("%Y-%m-%d")
if record.birthdate
else None,
"preferred_language": record.preferred_language,
"gender": record.gender,
"access_type": record.access_type,
@@ -164,7 +173,11 @@ def user_record_to_dict(record: User) -> dict:
"strava_state": record.strava_state,
"strava_token": record.strava_token,
"strava_refresh_token": record.strava_refresh_token,
"strava_token_expires_at": record.strava_token_expires_at,
"strava_token_expires_at": record.strava_token_expires_at.strftime(
"%Y-%m-%dT%H:%M:%S"
)
if record.strava_token_expires_at
else None,
}
@@ -201,7 +214,7 @@ async def read_users_all(
user_records_dict = [user_record_to_dict(record) for record in user_records]
# Include metadata in the response
metadata = {"total_records": len(user_records)}
metadata = {"total_records": len(user_records), "api_version": API_VERSION}
# Return the queried values using JSONResponse
return JSONResponse(
@@ -249,7 +262,7 @@ async def read_users_number(
user_count = db_session.query(User).count()
# Include metadata in the response
metadata = {"total_records": 1}
metadata = {"total_records": 1, "api_version": API_VERSION}
# Return the queried values using JSONResponse
return JSONResponse(content={"metadata": metadata, "content": user_count})
@@ -312,7 +325,12 @@ async def read_users_all_pagination(
user_records_dict = [user_record_to_dict(record) for record in user_records]
# Include metadata in the response
metadata = {"total_records": len(user_records)}
metadata = {
"total_records": len(user_records),
"pageNumber": pageNumber,
"numRecords": numRecords,
"api_version": API_VERSION,
}
# Return the queried values using JSONResponse
return JSONResponse(
@@ -373,7 +391,11 @@ async def read_users_username(
user_records_dict = [user_record_to_dict(record) for record in user_records]
# Include metadata in the response
metadata = {"total_records": len(user_records)}
metadata = {
"total_records": len(user_records),
"username": username,
"api_version": API_VERSION,
}
# Return the queried values using JSONResponse
return JSONResponse(
@@ -424,7 +446,11 @@ async def read_users_id(
user_records_dict = [user_record_to_dict(record) for record in user_records]
# Include metadata in the response
metadata = {"total_records": len(user_records)}
metadata = {
"total_records": len(user_records),
"user_id": user_id,
"api_version": API_VERSION,
}
# Return the queried values using JSONResponse
return JSONResponse(
@@ -479,12 +505,14 @@ async def read_users_username_id(
)
# Include metadata in the response
metadata = {"total_records": 1}
metadata = {
"total_records": 1,
"username": username,
"api_version": API_VERSION,
}
# Return the queried values using JSONResponse
return JSONResponse(
content={"metadata": metadata, "content": {"id": user_id}}
)
return JSONResponse(content={"metadata": metadata, "content": {"id": user_id}})
except JWTError:
# Return an error response if the user is not authenticated
@@ -531,15 +559,24 @@ async def read_users_id_photo_path(
if user:
# Include metadata in the response
metadata = {"total_records": 1}
metadata = {
"total_records": 1,
"user_id": user_id,
"api_version": API_VERSION,
}
# Return the queried values using JSONResponse
return JSONResponse(
content={"metadata": metadata, "content": {"photo_path": user.photo_path}}
content={
"metadata": metadata,
"content": {"photo_path": user.photo_path},
}
)
else:
# Handle the case where the user was not found or doesn't have a photo path
return create_error_response("NOT_FOUND", "User not found or no photo path available", 404)
return create_error_response(
"NOT_FOUND", "User not found or no photo path available", 404
)
except JWTError:
# Return an error response if the user is not authenticated
@@ -586,15 +623,24 @@ async def read_users_id_photo_path_aux(
if user:
# Include metadata in the response
metadata = {"total_records": 1}
metadata = {
"total_records": 1,
"user_id": user_id,
"api_version": API_VERSION,
}
# Return the queried values using JSONResponse
return JSONResponse(
content={"metadata": metadata, "content": {"photo_path_aux": user.photo_path_aux}}
content={
"metadata": metadata,
"content": {"photo_path_aux": user.photo_path_aux},
}
)
else:
# Handle the case where the user was not found or doesn't have a photo path aux
return create_error_response("NOT_FOUND", "User not found or no photo path aux available", 404)
return create_error_response(
"NOT_FOUND", "User not found or no photo path aux available", 404
)
except JWTError:
# Return an error response if the user is not authenticated
@@ -742,7 +788,7 @@ async def edit_user(
return JSONResponse(
content={"message": "User edited successfully"}, status_code=200
)
except JWTError:
# Return an error response if the user is not authenticated
return create_error_response("UNAUTHORIZED", "Unauthorized", 401)
@@ -801,9 +847,10 @@ async def delete_user_photo(
# Return a JSONResponse indicating the success of the user edit
return JSONResponse(
content={"message": f"Photo for user {user_id} has been deleted"}, status_code=200
content={"message": f"Photo for user {user_id} has been deleted"},
status_code=200,
)
except JWTError:
# Return an error response if the user is not authenticated
return create_error_response("UNAUTHORIZED", "Unauthorized", 401)
@@ -856,7 +903,9 @@ async def delete_user(
count_gear = db_session.query(Gear).filter(Gear.user_id == user_id).count()
if count_gear > 0:
# Return an error response if the user has gear created
return create_error_response("CONFLIT", "Cannot delete user due to existing dependencies", 409)
return create_error_response(
"CONFLIT", "Cannot delete user due to existing dependencies", 409
)
# Delete the user from the database
db_session.delete(user)
db_session.commit()
@@ -865,7 +914,7 @@ async def delete_user(
return JSONResponse(
content={"message": f"User {user_id} has been deleted"}, status_code=200
)
except JWTError:
# Return an error response if the user is not authenticated
return create_error_response("UNAUTHORIZED", "Unauthorized", 401)