BMI logic + fixes

[aux] added auxiliary scripts
[backend] added error logic to error printing
[backend] rollbacked changes from Datetime to Timestamp
[backend] added body composition sync from Garmin
[backend] added additional validations for empty .gpx files
[backend] add calculate BMI logic
[backend] add BMI calculation in migration
[backend] added Timezone logic to Strava activity import
[frontend] removed block that prevent deleting Strava or GC activity
[frontend] added new generic modal for single number input
[frontend] replaced modal that retrieves Strava and GC activities for number of days with new generic modal
[frontend] added shoes gear in add gear to walk and hike activities
[frontend] minor bug fixes
This commit is contained in:
João Vitória Silva
2024-12-10 17:35:56 +00:00
parent e1848effdc
commit 746cf47da6
41 changed files with 928 additions and 436 deletions

3
.gitignore vendored
View File

@@ -1,6 +1,9 @@
# General
docker-compose.yml
# aux folder
aux_scripts/.garminconnect
# Python
backend/app/__pycache__/
backend/app/*/__pycache__/

View File

@@ -0,0 +1,99 @@
from garminconnect import (
Garmin,
GarminConnectAuthenticationError,
)
import requests
import datetime
from garth.exc import GarthHTTPError
import os
from getpass import getpass
email = os.getenv("EMAIL")
password = os.getenv("PASSWORD")
tokenstore = os.getenv("GARMINTOKENS") or ".garminconnect"
api = None
today = datetime.date.today()
def get_date():
"""Get date from user input."""
return input("Enter date (dd-mm-yyyy): ")
def get_credentials():
"""Get user credentials."""
email = input("Login e-mail: ")
password = getpass("Enter password: ")
return email, password
def get_mfa():
"""Get MFA."""
return input("MFA one-time code: ")
def init_api(email, password):
"""Initialize Garmin API with your credentials."""
try:
# Using Oauth1 and OAuth2 token files from directory
print(
f"Trying to login to Garmin Connect using token data from directory '{tokenstore}'...\n"
)
garmin = Garmin()
garmin.login(tokenstore)
except (FileNotFoundError, GarthHTTPError, GarminConnectAuthenticationError):
# Session is expired. You'll need to log in again
print(
"Login tokens not present, login with your Garmin Connect credentials to generate them.\n"
f"They will be stored in '{tokenstore}' for future use.\n"
)
try:
# Ask for credentials if not set as environment variables
if not email or not password:
email, password = get_credentials()
garmin = Garmin(
email=email, password=password, is_cn=False, prompt_mfa=get_mfa
)
garmin.login()
# Save Oauth1 and Oauth2 token files to directory for next login
garmin.garth.dump(tokenstore)
print(
f"Oauth tokens stored in '{tokenstore}' directory for future use. (first method)\n"
)
except (
FileNotFoundError,
GarthHTTPError,
GarminConnectAuthenticationError,
requests.exceptions.HTTPError,
) as err:
print(err)
return None
return garmin
date = get_date()
date_object = datetime.datetime.strptime(date, "%d-%m-%Y")
if not api:
api = init_api(email, password)
if api:
garmin_bc = api.get_body_composition(date_object, date_object)
if garmin_bc is None:
# Log an informational event if no body composition were found
print(
f"User: No new Garmin Connect body composition found after {today}: garmin_bc is None"
)
# Return the number of body compositions processed
print(garmin_bc)

View File

@@ -30,7 +30,9 @@ def get_all_activities(db: Session):
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_all_activities: {err}", "error")
core_logger.print_to_log(
f"Error in get_all_activities: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -63,7 +65,9 @@ def get_user_activities(
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_user_activities: {err}", "error")
core_logger.print_to_log(
f"Error in get_user_activities: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -537,7 +541,9 @@ def get_activities_if_contains_name(name: str, user_id: int, db: Session):
) from err
def create_activity(activity: activities_schema.Activity, db: Session):
def create_activity(
activity: activities_schema.Activity, db: Session
) -> activities_schema.Activity:
try:
# Create a new activity
db_activity = activities_models.Activity(
@@ -592,7 +598,7 @@ def create_activity(activity: activities_schema.Activity, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in create_activity: {err}", "error")
core_logger.print_to_log(f"Error in create_activity: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -638,7 +644,7 @@ def edit_activity(user_id: int, activity: activities_schema.Activity, db: Sessio
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in edit_activity: {err}", "error")
core_logger.print_to_log(f"Error in edit_activity: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -664,7 +670,9 @@ def add_gear_to_activity(activity_id: int, gear_id: int, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in add_gear_to_activity: {err}", "error")
core_logger.print_to_log(
f"Error in add_gear_to_activity: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -725,7 +733,7 @@ def delete_activity(activity_id: int, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in delete_activity: {err}", "error")
core_logger.print_to_log(f"Error in delete_activity: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(

View File

@@ -2,7 +2,7 @@ from sqlalchemy import (
Column,
Integer,
String,
TIMESTAMP,
DATETIME,
ForeignKey,
DECIMAL,
BigInteger,
@@ -38,11 +38,13 @@ class Activity(Base):
comment="Gear type (1 - mountain bike, 2 - gravel bike, ...)",
)
start_time = Column(
TIMESTAMP, nullable=False, comment="Activity start date (TIMESTAMP)"
DATETIME, nullable=False, comment="Activity start date (DATETIME)"
)
end_time = Column(TIMESTAMP, nullable=False, comment="Activity end date (TIMESTAMP)")
end_time = Column(DATETIME, nullable=False, comment="Activity end date (DATETIME)")
timezone = Column(
String(length=250), nullable=True, comment="Activity timezone (May include spaces)"
String(length=250),
nullable=True,
comment="Activity timezone (May include spaces)",
)
total_elapsed_time = Column(
DECIMAL(precision=20, scale=10),
@@ -66,7 +68,7 @@ class Activity(Base):
comment="Activity country (May include spaces)",
)
created_at = Column(
TIMESTAMP, nullable=False, comment="Activity creation date (TIMESTAMP)"
DATETIME, nullable=False, comment="Activity creation date (DATETIME)"
)
elevation_gain = Column(Integer, nullable=True, comment="Elevation gain in meters")
elevation_loss = Column(Integer, nullable=True, comment="Elevation loss in meters")
@@ -138,4 +140,4 @@ class Activity(Base):
"ActivityStreams",
back_populates="activity",
cascade="all, delete-orphan",
)
)

View File

@@ -646,10 +646,10 @@ async def delete_activity(
os.remove(file)
except FileNotFoundError as err:
# Log the exception
core_logger.print_to_log(f"File not found {file}: {err}", "error")
core_logger.print_to_log(f"File not found {file}: {err}", "error", exc=err)
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error deleting file {file}: {err}", "error")
core_logger.print_to_log(f"Error deleting file {file}: {err}", "error", exc=err)
# Return success message
return {"detail": f"Activity {activity_id} deleted successfully"}

View File

@@ -234,7 +234,7 @@ def move_file(new_dir: str, new_filename: str, file_path: str):
shutil.move(file_path, new_file_path)
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in move_file - {str(err)}", "error")
core_logger.print_to_log(f"Error in move_file - {str(err)}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -267,7 +267,7 @@ def parse_file(token_user_id: int, file_extension: str, filename: str) -> dict:
raise http_err
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in parse_file - {str(err)}", "error")
core_logger.print_to_log(f"Error in parse_file - {str(err)}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,

View File

@@ -28,7 +28,7 @@ def get_activity_streams(activity_id: int, db: Session):
return activity_streams
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_activity_streams: {err}", "error")
core_logger.print_to_log(f"Error in get_activity_streams: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -56,7 +56,7 @@ def get_activity_stream_by_type(activity_id: int, stream_type: int, db: Session)
return activity_stream
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_activity_stream_by_type: {err}", "error")
core_logger.print_to_log(f"Error in get_activity_stream_by_type: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -92,7 +92,7 @@ def create_activity_streams(
db.rollback()
# Log the exception
core_logger(f"Error in create_activity_streams: {err}", "error")
core_logger(f"Error in create_activity_streams: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,

View File

@@ -1,18 +1,18 @@
"""v0.6.5 migration
Revision ID: 7ba9b71fe203
Revision ID: 542605083c0c
Revises: 65a0f1d72997
Create Date: 2024-11-25 13:07:30.142799
Create Date: 2024-12-09 16:02:43.332696
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = '7ba9b71fe203'
revision: str = '542605083c0c'
down_revision: Union[str, None] = '65a0f1d72997'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
@@ -20,56 +20,52 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('gear', sa.Column('initial_kms', sa.DECIMAL(precision=11, scale=3), nullable=False, comment='Initial kilometers of the gear'))
op.add_column('activities', sa.Column('timezone', sa.String(length=250), nullable=True, comment='Activity timezone (May include spaces)'))
op.alter_column('activities', 'start_time',
existing_type=sa.DATETIME(),
type_=sa.TIMESTAMP(),
comment='Activity start date (TIMESTAMP)',
existing_type=mysql.DATETIME(),
comment='Activity start date (DATETIME)',
existing_comment='Activity start date (datetime)',
existing_nullable=False)
op.alter_column('activities', 'end_time',
existing_type=sa.DATETIME(),
type_=sa.TIMESTAMP(),
comment='Activity end date (TIMESTAMP)',
existing_type=mysql.DATETIME(),
comment='Activity end date (DATETIME)',
existing_comment='Activity end date (datetime)',
existing_nullable=False)
op.alter_column('activities', 'total_elapsed_time',
existing_type=sa.DECIMAL(precision=20, scale=10),
existing_type=mysql.DECIMAL(precision=20, scale=10),
comment='Activity total elapsed time (s)',
existing_comment='Activity total elapsed time (datetime)',
existing_nullable=False)
op.alter_column('activities', 'total_timer_time',
existing_type=sa.DECIMAL(precision=20, scale=10),
existing_type=mysql.DECIMAL(precision=20, scale=10),
comment='Activity total timer time (s)',
existing_comment='Activity total timer time (datetime)',
existing_nullable=False)
op.alter_column('activities', 'created_at',
existing_type=sa.DATETIME(),
type_=sa.TIMESTAMP(),
comment='Activity creation date (TIMESTAMP)',
existing_type=mysql.DATETIME(),
comment='Activity creation date (DATETIME)',
existing_comment='Activity creation date (datetime)',
existing_nullable=False)
op.add_column('gear', sa.Column('initial_kms', sa.DECIMAL(precision=11, scale=3), nullable=False, comment='Initial kilometers of the gear'))
op.alter_column('gear', 'created_at',
existing_type=sa.DATETIME(),
type_=sa.TIMESTAMP(),
comment='Gear creation date (TIMESTAMP)',
existing_type=mysql.DATETIME(),
comment='Gear creation date (DATETIME)',
existing_comment='Gear creation date (date)',
existing_nullable=False)
op.add_column('health_data', sa.Column('date', sa.Date(), nullable=True, comment='Health data creation date (date)'))
# Copy data from `created_at` to `date`
op.execute("""
UPDATE health_data
SET date = DATE(created_at)
""")
# Make `date` column non-nullable
op.alter_column('health_data', 'date', nullable=False, existing_type=sa.Date())
op.add_column('health_data', sa.Column('bmi', sa.DECIMAL(precision=10, scale=2), nullable=True, comment='Body mass index (BMI)'))
op.add_column('health_data', sa.Column('body_fat', sa.DECIMAL(precision=10, scale=2), nullable=True, comment='Body fat percentage'))
op.add_column('health_data', sa.Column('body_water', sa.DECIMAL(precision=10, scale=2), nullable=True, comment='Body hydration percentage'))
op.add_column('health_data', sa.Column('bone_mass', sa.DECIMAL(precision=10, scale=2), nullable=True, comment='Bone mass percentage'))
op.add_column('health_data', sa.Column('muscle_mass', sa.DECIMAL(precision=10, scale=2), nullable=True, comment='Muscle mass percentage'))
op.add_column('health_data', sa.Column('physique_rating', sa.DECIMAL(precision=10, scale=2), nullable=True, comment='Physique rating'))
op.add_column('health_data', sa.Column('visceral_fat', sa.DECIMAL(precision=10, scale=2), nullable=True, comment='Visceral fat rating'))
op.add_column('health_data', sa.Column('metabolic_age', sa.DECIMAL(precision=10, scale=2), nullable=True, comment='Metabolic age'))
op.add_column('health_data', sa.Column('garminconnect_body_composition_id', sa.String(length=45), nullable=True, comment='Garmin Connect body composition ID'))
op.alter_column('health_data', 'created_at',
existing_type=sa.DATE(),
comment='Health data creation date (date)',
existing_comment='Health data creation date (datetime)',
existing_nullable=False)
op.add_column('activities', sa.Column('timezone', sa.String(length=250), nullable=True, comment='Activity timezone (May include spaces)'))
op.drop_index('created_at', table_name='health_data')
op.create_unique_constraint(None, 'health_data', ['date'])
op.drop_column('health_data', 'created_at')
# Add the new entry to the migrations table
op.execute("""
INSERT INTO migrations (id, name, description, executed) VALUES
(2, 'v0.6.5', 'Process timezone for existing activities', false);
@@ -79,56 +75,52 @@ def upgrade() -> None:
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('gear', 'initial_kms')
op.alter_column('health_data', 'created_at',
existing_type=sa.DATE(),
comment='Health data creation date (datetime)',
existing_comment='Health data creation date (date)',
existing_nullable=False)
op.add_column('health_data', sa.Column('created_at', sa.DATE(), nullable=True, comment='Health data creation date (datetime)'))
# Copy data back from `date` to `created_at`
op.execute("""
UPDATE health_data
SET created_at = date
""")
# Make `created_at` column non-nullable
op.alter_column('health_data', 'created_at', nullable=False, existing_type=sa.Date())
op.drop_constraint(None, 'health_data', type_='unique')
op.create_index('created_at', 'health_data', ['created_at'], unique=True)
op.drop_column('health_data', 'garminconnect_body_composition_id')
op.drop_column('health_data', 'metabolic_age')
op.drop_column('health_data', 'visceral_fat')
op.drop_column('health_data', 'physique_rating')
op.drop_column('health_data', 'muscle_mass')
op.drop_column('health_data', 'bone_mass')
op.drop_column('health_data', 'body_water')
op.drop_column('health_data', 'body_fat')
op.drop_column('health_data', 'bmi')
op.drop_column('health_data', 'date')
op.alter_column('gear', 'created_at',
existing_type=sa.TIMESTAMP(),
type_=sa.DATETIME(),
existing_type=mysql.DATETIME(),
comment='Gear creation date (date)',
existing_comment='Gear creation date (TIMESTAMP)',
existing_comment='Gear creation date (DATETIME)',
existing_nullable=False)
op.drop_column('gear', 'initial_kms')
op.alter_column('activities', 'created_at',
existing_type=sa.TIMESTAMP(),
type_=sa.DATETIME(),
existing_type=mysql.DATETIME(),
comment='Activity creation date (datetime)',
existing_comment='Activity creation date (TIMESTAMP)',
existing_comment='Activity creation date (DATETIME)',
existing_nullable=False)
op.alter_column('activities', 'total_timer_time',
existing_type=sa.DECIMAL(precision=20, scale=10),
existing_type=mysql.DECIMAL(precision=20, scale=10),
comment='Activity total timer time (datetime)',
existing_comment='Activity total timer time (s)',
existing_nullable=False)
op.alter_column('activities', 'total_elapsed_time',
existing_type=sa.DECIMAL(precision=20, scale=10),
existing_type=mysql.DECIMAL(precision=20, scale=10),
comment='Activity total elapsed time (datetime)',
existing_comment='Activity total elapsed time (s)',
existing_nullable=False)
op.alter_column('activities', 'end_time',
existing_type=sa.TIMESTAMP(),
type_=sa.DATETIME(),
existing_type=mysql.DATETIME(),
comment='Activity end date (datetime)',
existing_comment='Activity end date (TIMESTAMP)',
existing_comment='Activity end date (DATETIME)',
existing_nullable=False)
op.alter_column('activities', 'start_time',
existing_type=sa.TIMESTAMP(),
type_=sa.DATETIME(),
existing_type=mysql.DATETIME(),
comment='Activity start date (datetime)',
existing_comment='Activity start date (TIMESTAMP)',
existing_comment='Activity start date (DATETIME)',
existing_nullable=False)
op.drop_column('activities', 'timezone')
# Remove the entry from the migrations table
op.execute("""
DELETE FROM migrations
WHERE id = 2;

View File

@@ -32,12 +32,12 @@ def get_main_logger():
return logging.getLogger("main_logger")
def print_to_log(message: str, type: str = "info"):
def print_to_log(message: str, type: str = "info", exc: Exception = None):
main_logger = get_main_logger()
if type == "info":
main_logger.info(message)
elif type == "error":
main_logger.error(message)
main_logger.error(message, exc_info=exc is not None)
elif type == "warning":
main_logger.warning(message)
elif type == "debug":

View File

@@ -1,14 +1,15 @@
#from apscheduler.schedulers.background import BackgroundScheduler
# from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.schedulers.asyncio import AsyncIOScheduler
import strava.utils as strava_utils
import strava.activity_utils as strava_activity_utils
import garmin.activity_utils as garmin_activity_utils
import garmin.health_utils as garmin_health_utils
import core.logger as core_logger
#scheduler = BackgroundScheduler()
# scheduler = BackgroundScheduler()
scheduler = AsyncIOScheduler()
@@ -38,7 +39,7 @@ def start_scheduler():
core_logger.print_to_log(
"Added scheduler job to retrieve last day Garmin Connect users activities every 60 minutes"
)
# Add scheduler jobs to retrieve last day activities from Garmin Connect
# Add scheduler job to retrieve last day activities from Garmin Connect
scheduler.add_job(
garmin_activity_utils.retrieve_garminconnect_users_activities_for_days,
"interval",
@@ -46,6 +47,18 @@ def start_scheduler():
args=[1],
)
# Log the addition of the job to retrieve last day Garmin Connect users body composition
core_logger.print_to_log(
"Added scheduler job to retrieve last day Garmin Connect users body composition every 4 hours (240 minutes)"
)
# Add scheduler job to retrieve last day body composition from Garmin Connect
scheduler.add_job(
garmin_health_utils.retrieve_garminconnect_users_bc_for_days,
"interval",
minutes=240,
args=[1],
)
def stop_scheduler():
scheduler.shutdown()

View File

@@ -58,7 +58,7 @@ def create_activity_objects(
pace = 0
if session_record["session"]["activity_type"]:
activity_type=activities_utils.define_activity_type(
activity_type = activities_utils.define_activity_type(
session_record["session"]["activity_type"]
)
@@ -82,7 +82,8 @@ def create_activity_objects(
else:
if session_record["time_offset"]:
timezone = find_timezone_name(
session_record["time_offset"], session_record["session"]["first_waypoint_time"]
session_record["time_offset"],
session_record["session"]["first_waypoint_time"],
)
parsed_activity = {
@@ -611,7 +612,7 @@ def parse_fit_file(file: str) -> dict:
raise http_err
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in parse_fit_file: {err}", "error")
core_logger.print_to_log(f"Error in parse_fit_file: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -822,9 +823,9 @@ def find_timezone_name(offset_seconds, reference_date):
tz = ZoneInfo(tz_name)
if reference_date.utcoffset() is None: # Skip invalid timezones
continue
# Get the UTC offset for the reference date
utc_offset = reference_date.astimezone(tz).utcoffset()
if utc_offset.total_seconds() == offset_seconds:
return tz_name
return tz_name

View File

@@ -25,7 +25,7 @@ def get_all_followers_by_user_id(user_id: int, db: Session):
return followers
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_all_followers_by_user_id: {err}", "error")
core_logger.print_to_log(f"Error in get_all_followers_by_user_id: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -53,7 +53,7 @@ def get_accepted_followers_by_user_id(user_id: int, db: Session):
return followers
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_accepted_followers_by_user_id: {err}", "error")
core_logger.print_to_log(f"Error in get_accepted_followers_by_user_id: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -78,7 +78,7 @@ def get_all_following_by_user_id(user_id: int, db: Session):
return followings
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_all_following_by_user_id: {err}", "error")
core_logger.print_to_log(f"Error in get_all_following_by_user_id: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -106,7 +106,7 @@ def get_accepted_following_by_user_id(user_id: int, db: Session):
return followings
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_accepted_following_by_user_id: {err}", "error")
core_logger.print_to_log(f"Error in get_accepted_following_by_user_id: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -136,7 +136,7 @@ def get_follower_for_user_id_and_target_user_id(
return follower
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_follower_for_user_id_and_target_user_id: {err}", "error")
core_logger.print_to_log(f"Error in get_follower_for_user_id_and_target_user_id: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -162,7 +162,7 @@ def create_follower(user_id: int, target_user_id: int, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in create_follower: {err}", "error")
core_logger.print_to_log(f"Error in create_follower: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -201,7 +201,7 @@ def accept_follower(user_id: int, target_user_id: int, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in accept_follower: {err}", "error")
core_logger.print_to_log(f"Error in accept_follower: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -236,7 +236,7 @@ def delete_follower(user_id: int, target_user_id: int, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in delete_follower: {err}", "error")
core_logger.print_to_log(f"Error in delete_follower: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(

View File

@@ -53,9 +53,7 @@ def fetch_and_process_activities(
)
continue
garmin_logger.print_to_log(
f"User {user_id}: Processing activity {activity_id}"
)
garmin_logger.print_to_log(f"User {user_id}: Processing activity {activity_id}")
# Get activity gear
activity_gear = garminconnect_client.get_activity_gear(activity_id)

View File

@@ -0,0 +1,133 @@
import os
import zipfile
from datetime import datetime, timedelta, date
import garminconnect
from sqlalchemy.orm import Session
import garmin.utils as garmin_utils
import garmin.logger as garmin_logger
import activities.utils as activities_utils
import activities.crud as activities_crud
import health_data.crud as health_data_crud
import health_data.schema as health_data_schema
import users.crud as users_crud
from core.database import SessionLocal
def fetch_and_process_bc(
garminconnect_client: garminconnect.Garmin,
start_date: datetime,
user_id: int,
db: Session,
) -> int:
# Fetch Garmin Connect body composition after the specified start date
garmin_bc = garminconnect_client.get_body_composition(start_date, date.today())
if garmin_bc is None:
# Log an informational event if no body composition were found
garmin_logger.print_to_log_and_console(
f"User {user_id}: No new Garmin Connect body composition found after {start_date}: garmin_bc is None"
)
# Return 0 to indicate no body composition were processed
return 0
# Process body composition
for bc in garmin_bc["dateWeightList"]:
health_data = health_data_schema.HealthData(
user_id=user_id,
date=bc["calendarDate"],
weight=bc["weight"] / 1000,
bmi=bc["bmi"],
#body_fat=bc["bodyFat"],
#body_water=bc["bodyWater"],
#bone_mass=bc["boneMass"],
#muscle_mass=bc["muscleMass"],
#physique_rating=bc["physiqueRating"],
#visceral_fat=bc["visceralFat"],
#metabolic_age=bc["metabolicAge"],
garminconnect_body_composition_id=str(bc["samplePk"]),
)
# Check if the body composition is already stored in the database
health_data_db = health_data_crud.get_health_data_by_date(
user_id, health_data.date, db
)
if health_data_db:
health_data.id = health_data_db.id
health_data_crud.edit_health_data(user_id, health_data, db)
garmin_logger.print_to_log(
f"User {user_id}: Body composition edited for date {health_data.date}"
)
else:
health_data_crud.create_health_data(health_data, user_id, db)
garmin_logger.print_to_log(
f"User {user_id}: Body composition created for date {health_data.date}"
)
# Return the number of body compositions processed
return len(garmin_bc)
def retrieve_garminconnect_users_bc_for_days(days: int):
# Create a new database session
db = SessionLocal()
try:
# Get all users
users = users_crud.get_all_users(db)
finally:
# Ensure the session is closed after use
db.close()
# Process the body composition for each user
for user in users:
get_user_garminconnect_bc_by_days(
(datetime.utcnow() - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%S"),
user.id,
)
def get_user_garminconnect_bc_by_days(start_date: datetime, user_id: int):
# Create a new database session
db = SessionLocal()
try:
# Get the user integrations by user ID
user_integrations = garmin_utils.fetch_user_integrations_and_validate_token(
user_id, db
)
if user_integrations is None:
garmin_logger.print_to_log(f"User {user_id}: Garmin Connect not linked")
return None
# Log the start of the body composition processing
garmin_logger.print_to_log(
f"User {user_id}: Started Garmin Connect body composition processing"
)
# Create a Garmin Connect client with the user's access token
garminconnect_client = garmin_utils.login_garminconnect_using_tokens(
user_integrations.garminconnect_oauth1,
user_integrations.garminconnect_oauth2,
)
# Fetch Garmin Connect body composition after the specified start date
num_garminconnect_bc_processed = fetch_and_process_bc(
garminconnect_client, start_date, user_id, db
)
# Log an informational event for tracing
garmin_logger.print_to_log(
f"User {user_id}: {num_garminconnect_bc_processed} Garmin Connect body composition processed"
)
finally:
# Ensure the session is closed after use
db.close()

View File

@@ -22,12 +22,12 @@ def get_garminconnect_logger():
return logging.getLogger("garminconnect_logger")
def print_to_log(message: str, type: str = "info"):
def print_to_log(message: str, type: str = "info", exc: Exception = None):
garminconnect_logger = get_garminconnect_logger()
if type == "info":
garminconnect_logger.info(message)
elif type == "error":
garminconnect_logger.error(message)
garminconnect_logger.error(message, exc_info=exc is not None)
elif type == "warning":
garminconnect_logger.warning(message)
elif type == "debug":

View File

@@ -26,7 +26,7 @@ def get_gear_user_by_id(gear_id: int, db: Session) -> gears_schema.Gear | None:
return gear
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_gear_user_by_id: {err}", "error")
core_logger.print_to_log(f"Error in get_gear_user_by_id: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -61,7 +61,7 @@ def get_gear_users_with_pagination(
except Exception as err:
# Log the exception
core_logger.print_to_log(
f"Error in get_gear_users_with_pagination: {err}", "error"
f"Error in get_gear_users_with_pagination: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -87,7 +87,7 @@ def get_gear_user(user_id: int, db: Session) -> list[gears_schema.Gear] | None:
return gears
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_gear_user: {err}", "error")
core_logger.print_to_log(f"Error in get_gear_user: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -124,7 +124,7 @@ def get_gear_user_by_nickname(
return gears
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_gear_user_by_nickname: {err}", "error")
core_logger.print_to_log(f"Error in get_gear_user_by_nickname: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -155,7 +155,7 @@ def get_gear_by_type_and_user(gear_type: int, user_id: int, db: Session):
return gear
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_gear_by_type_and_user: {err}", "error")
core_logger.print_to_log(f"Error in get_gear_by_type_and_user: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -189,7 +189,7 @@ def get_gear_by_strava_id_from_user_id(
except Exception as err:
# Log the exception
core_logger.print_to_log(
f"Error in get_gear_by_strava_id_from_user_id: {err}", "error"
f"Error in get_gear_by_strava_id_from_user_id: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -224,7 +224,7 @@ def get_gear_by_garminconnect_id_from_user_id(
except Exception as err:
# Log the exception
core_logger.print_to_log(
f"Error in get_gear_by_garminconnect_id_from_user_id: {err}", "error"
f"Error in get_gear_by_garminconnect_id_from_user_id: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -263,7 +263,7 @@ def create_multiple_gears(gears: list[gears_schema.Gear], user_id: int, db: Sess
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in create_multiple_gears: {err}", "error")
core_logger.print_to_log(f"Error in create_multiple_gears: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -298,7 +298,7 @@ def create_gear(gear: gears_schema.Gear, user_id: int, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in create_gear: {err}", "error")
core_logger.print_to_log(f"Error in create_gear: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -339,7 +339,7 @@ def edit_gear(gear_id: int, gear: gears_schema.Gear, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in edit_gear: {err}", "error")
core_logger.print_to_log(f"Error in edit_gear: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -367,7 +367,7 @@ def delete_gear(gear_id: int, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in delete_gear: {err}", "error")
core_logger.print_to_log(f"Error in delete_gear: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -397,7 +397,7 @@ def delete_all_strava_gear_for_user(user_id: int, db: Session):
# Log the exception
core_logger.print_to_log(
f"Error in delete_all_strava_gear_for_user: {err}", "error"
f"Error in delete_all_strava_gear_for_user: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
@@ -429,7 +429,7 @@ def delete_all_garminconnect_gear_for_user(user_id: int, db: Session):
# Log the exception
core_logger.print_to_log(
f"Error in delete_all_garminconnect_gear_for_user: {err}", "error"
f"Error in delete_all_garminconnect_gear_for_user: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code

View File

@@ -2,7 +2,7 @@ from sqlalchemy import (
Column,
Integer,
String,
TIMESTAMP,
DATETIME,
ForeignKey,
DECIMAL,
)
@@ -37,7 +37,7 @@ class Gear(Base):
nullable=False,
comment="User ID that the gear belongs to",
)
created_at = Column(TIMESTAMP, nullable=False, comment="Gear creation date (TIMESTAMP)")
created_at = Column(DATETIME, nullable=False, comment="Gear creation date (DATETIME)")
is_active = Column(
Integer, nullable=False, comment="Is gear active (0 - not active, 1 - active)"
)

View File

@@ -68,137 +68,150 @@ def parse_gpx_file(file: str, user_id: int) -> dict:
with open(file, "r") as gpx_file:
gpx = gpxpy.parse(gpx_file)
# Iterate over tracks in the GPX file
for track in gpx.tracks:
# Set activity name and type if available
activity_name = track.name if track.name else "Workout"
activity_type = track.type if track.type else "Workout"
if gpx.tracks:
# Iterate over tracks in the GPX file
for track in gpx.tracks:
# Set activity name and type if available
activity_name = track.name if track.name else "Workout"
activity_type = track.type if track.type else "Workout"
# Iterate over segments in each track
for segment in track.segments:
# Iterate over points in each segment
for point in segment.points:
# Extract latitude and longitude from the point
latitude, longitude = point.latitude, point.longitude
if track.segments:
# Iterate over segments in each track
for segment in track.segments:
# Iterate over points in each segment
for point in segment.points:
# Extract latitude and longitude from the point
latitude, longitude = point.latitude, point.longitude
# Calculate distance between waypoints
if prev_latitude is not None and prev_longitude is not None:
distance += geodesic(
(prev_latitude, prev_longitude), (latitude, longitude)
).meters
# Calculate distance between waypoints
if prev_latitude is not None and prev_longitude is not None:
distance += geodesic(
(prev_latitude, prev_longitude),
(latitude, longitude),
).meters
# Extract elevation, time, and location details
elevation, time = point.elevation, point.time
# Extract elevation, time, and location details
elevation, time = point.elevation, point.time
if elevation != 0:
is_elevation_set = True
if elevation != 0:
is_elevation_set = True
if first_waypoint_time is None:
first_waypoint_time = point.time
if first_waypoint_time is None:
first_waypoint_time = point.time
if process_one_time_fields == 0:
# Use geocoding API to get city, town, and country based on coordinates
location_data = (
activities_utils.location_based_on_coordinates(
latitude, longitude
if process_one_time_fields == 0:
# Use geocoding API to get city, town, and country based on coordinates
location_data = (
activities_utils.location_based_on_coordinates(
latitude, longitude
)
)
# Extract city, town, and country from location data
if location_data:
city = location_data["city"]
town = location_data["town"]
country = location_data["country"]
process_one_time_fields = 1
# Extract heart rate, cadence, and power data from point extensions
heart_rate, cadence, power = 0, 0, 0
if point.extensions:
# Iterate through each extension element
for extension in point.extensions:
if extension.tag.endswith("TrackPointExtension"):
hr_element = extension.find(
".//{http://www.garmin.com/xmlschemas/TrackPointExtension/v1}hr"
)
if hr_element is not None:
heart_rate = hr_element.text
cad_element = extension.find(
".//{http://www.garmin.com/xmlschemas/TrackPointExtension/v1}cad"
)
if cad_element is not None:
cadence = cad_element.text
elif extension.tag.endswith("power"):
# Extract 'power' value
power = extension.text
# Check if heart rate, cadence, power are set
if heart_rate != 0:
is_heart_rate_set = True
if cadence != 0:
is_cadence_set = True
if power != 0:
is_power_set = True
else:
power = None
# Calculate instant speed, pace, and update waypoint arrays
instant_speed = activities_utils.calculate_instant_speed(
last_waypoint_time,
time,
latitude,
longitude,
prev_latitude,
prev_longitude,
)
)
# Extract city, town, and country from location data
if location_data:
city = location_data["city"]
town = location_data["town"]
country = location_data["country"]
# Calculate instance pace
instant_pace = 0
if instant_speed > 0:
instant_pace = 1 / instant_speed
is_velocity_set = True
process_one_time_fields = 1
timestamp = time.strftime("%Y-%m-%dT%H:%M:%S")
# Extract heart rate, cadence, and power data from point extensions
heart_rate, cadence, power = 0, 0, 0
if point.extensions:
# Iterate through each extension element
for extension in point.extensions:
if extension.tag.endswith("TrackPointExtension"):
hr_element = extension.find(
".//{http://www.garmin.com/xmlschemas/TrackPointExtension/v1}hr"
# Append waypoint data to respective arrays
if latitude is not None and longitude is not None:
lat_lon_waypoints.append(
{
"time": timestamp,
"lat": latitude,
"lon": longitude,
}
)
if hr_element is not None:
heart_rate = hr_element.text
cad_element = extension.find(
".//{http://www.garmin.com/xmlschemas/TrackPointExtension/v1}cad"
)
if cad_element is not None:
cadence = cad_element.text
elif extension.tag.endswith("power"):
# Extract 'power' value
power = extension.text
is_lat_lon_set = True
# Check if heart rate, cadence, power are set
if heart_rate != 0:
is_heart_rate_set = True
activities_utils.append_if_not_none(
ele_waypoints, timestamp, elevation, "ele"
)
activities_utils.append_if_not_none(
hr_waypoints, timestamp, heart_rate, "hr"
)
activities_utils.append_if_not_none(
cad_waypoints, timestamp, cadence, "cad"
)
activities_utils.append_if_not_none(
power_waypoints, timestamp, power, "power"
)
activities_utils.append_if_not_none(
vel_waypoints, timestamp, instant_speed, "vel"
)
activities_utils.append_if_not_none(
pace_waypoints, timestamp, instant_pace, "pace"
)
if cadence != 0:
is_cadence_set = True
if power != 0:
is_power_set = True
else:
power = None
# Calculate instant speed, pace, and update waypoint arrays
instant_speed = activities_utils.calculate_instant_speed(
last_waypoint_time,
time,
latitude,
longitude,
prev_latitude,
prev_longitude,
)
# Calculate instance pace
instant_pace = 0
if instant_speed > 0:
instant_pace = 1 / instant_speed
is_velocity_set = True
timestamp = time.strftime("%Y-%m-%dT%H:%M:%S")
# Append waypoint data to respective arrays
if latitude is not None and longitude is not None:
lat_lon_waypoints.append(
{
"time": timestamp,
"lat": latitude,
"lon": longitude,
}
)
is_lat_lon_set = True
activities_utils.append_if_not_none(
ele_waypoints, timestamp, elevation, "ele"
)
activities_utils.append_if_not_none(
hr_waypoints, timestamp, heart_rate, "hr"
)
activities_utils.append_if_not_none(
cad_waypoints, timestamp, cadence, "cad"
)
activities_utils.append_if_not_none(
power_waypoints, timestamp, power, "power"
)
activities_utils.append_if_not_none(
vel_waypoints, timestamp, instant_speed, "vel"
)
activities_utils.append_if_not_none(
pace_waypoints, timestamp, instant_pace, "pace"
)
# Update previous latitude, longitude, and last waypoint time
prev_latitude, prev_longitude, last_waypoint_time = (
latitude,
longitude,
time,
# Update previous latitude, longitude, and last waypoint time
prev_latitude, prev_longitude, last_waypoint_time = (
latitude,
longitude,
time,
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid GPX file - no segments found in the GPX file",
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid GPX file - no tracks found in the GPX file",
)
# Calculate elevation gain/loss, pace, average speed, and average power
if ele_waypoints:
@@ -244,9 +257,9 @@ def parse_gpx_file(file: str, user_id: int) -> dict:
if activity_type != 3 and activity_type != 7:
if is_lat_lon_set:
timezone = tf.timezone_at(
lat=lat_lon_waypoints[0]["lat"],
lng=lat_lon_waypoints[0]["lon"],
)
lat=lat_lon_waypoints[0]["lat"],
lng=lat_lon_waypoints[0]["lon"],
)
# Create an Activity object with parsed data
activity = activities_schema.Activity(
@@ -268,12 +281,12 @@ def parse_gpx_file(file: str, user_id: int) -> dict:
average_speed=avg_speed,
max_speed=max_speed,
average_power=round(avg_power) if avg_power else None,
max_power=max_power,
max_power=round(max_power) if max_power else None,
normalized_power=round(np) if np else None,
average_hr=round(avg_hr) if avg_hr else None,
max_hr=max_hr,
max_hr=round(max_hr) if max_hr else None,
average_cad=round(avg_cadence) if avg_cadence else None,
max_cad=max_cadence,
max_cad=round(max_cadence) if max_cadence else None,
calories=calories,
visibility=visibility,
strava_gear_id=None,
@@ -302,7 +315,9 @@ def parse_gpx_file(file: str, user_id: int) -> dict:
raise http_err
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in parse_gpx_file - {str(err)}", "error")
core_logger.print_to_log(
f"Error in parse_gpx_file - {str(err)}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,

View File

@@ -7,10 +7,38 @@ import users.crud as users_crud
import health_data.schema as health_data_schema
import health_data.models as health_data_models
import health_data.utils as health_data_utils
import core.logger as core_logger
def get_all_health_data(db: Session):
try:
# Get the health_data from the database
health_data = (
db.query(health_data_models.HealthData)
.order_by(desc(health_data_models.HealthData.date))
.all()
)
# Check if there are health_data if not return None
if not health_data:
return None
# Return the health_data
return health_data
except Exception as err:
# Log the exception
core_logger.print_to_log(
f"Error in get_all_health_data: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal Server Error",
) from err
def get_health_data_number(user_id: int, db: Session):
try:
# Get the number of health_data from the database
@@ -21,7 +49,9 @@ def get_health_data_number(user_id: int, db: Session):
)
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_health_data_number: {err}", "error")
core_logger.print_to_log(
f"Error in get_health_data_number: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -35,7 +65,7 @@ def get_health_data(user_id: int, db: Session):
health_data = (
db.query(health_data_models.HealthData)
.filter(health_data_models.HealthData.user_id == user_id)
.order_by(desc(health_data_models.HealthData.created_at))
.order_by(desc(health_data_models.HealthData.date))
.all()
)
@@ -47,7 +77,7 @@ def get_health_data(user_id: int, db: Session):
return health_data
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_health_data: {err}", "error")
core_logger.print_to_log(f"Error in get_health_data: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -63,7 +93,7 @@ def get_health_data_with_pagination(
health_data = (
db.query(health_data_models.HealthData)
.filter(health_data_models.HealthData.user_id == user_id)
.order_by(desc(health_data_models.HealthData.created_at))
.order_by(desc(health_data_models.HealthData.date))
.offset((page_number - 1) * num_records)
.limit(num_records)
.all()
@@ -78,7 +108,7 @@ def get_health_data_with_pagination(
except Exception as err:
# Log the exception
core_logger.print_to_log(
f"Error in get_health_data_with_pagination: {err}", "error"
f"Error in get_health_data_with_pagination: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -87,13 +117,13 @@ def get_health_data_with_pagination(
) from err
def get_health_data_by_created_at(user_id: int, created_at: str, db: Session):
def get_health_data_by_date(user_id: int, date: str, db: Session):
try:
# Get the health_data from the database
health_data = (
db.query(health_data_models.HealthData)
.filter(
health_data_models.HealthData.created_at == created_at,
health_data_models.HealthData.date == date,
health_data_models.HealthData.user_id == user_id,
)
.first()
@@ -108,7 +138,7 @@ def get_health_data_by_created_at(user_id: int, created_at: str, db: Session):
except Exception as err:
# Log the exception
core_logger.print_to_log(
f"Error in get_health_data_by_created_at: {err}", "error"
f"Error in get_health_data_by_date: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -121,29 +151,29 @@ def create_health_data(
health_data: health_data_schema.HealthData, user_id: int, db: Session
):
try:
# Check if date is None
if health_data.date is None:
# Set the date to the current date
health_data.date = func.now()
# Check if bmi is None
if health_data.bmi is None:
# Get the user from the database
user = users_crud.get_user_by_id(user_id, db)
# Check if user is not None
if user is not None:
# Calculate the bmi
health_data.bmi = health_data.weight / ((user.height / 100) ** 2)
health_data = health_data_utils.calculate_bmi(health_data, user_id, db)
# Create a new health_data
db_health_data = health_data_models.HealthData(
user_id=user_id,
created_at=func.now(),
date=health_data.date,
weight=health_data.weight,
bmi=health_data.bmi,
body_fat=health_data.body_fat,
body_water=health_data.body_water,
bone_mass=health_data.bone_mass,
muscle_mass=health_data.muscle_mass,
physique_rating=health_data.physique_rating,
visceral_fat=health_data.visceral_fat,
metabolic_age=health_data.metabolic_age,
# body_fat=health_data.body_fat,
# body_water=health_data.body_water,
# bone_mass=health_data.bone_mass,
# muscle_mass=health_data.muscle_mass,
# physique_rating=health_data.physique_rating,
# visceral_fat=health_data.visceral_fat,
# metabolic_age=health_data.metabolic_age,
garminconnect_body_composition_id=health_data.garminconnect_body_composition_id,
)
# Add the health_data to the database
@@ -170,7 +200,63 @@ def create_health_data(
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in create_health_data: {err}", "error")
core_logger.print_to_log(
f"Error in create_health_data: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal Server Error",
) from err
def edit_health_data(user_id, health_data: health_data_schema.HealthData, db: Session):
try:
# Get the health_data from the database
db_health_data = (
db.query(health_data_models.HealthData)
.filter(
health_data_models.HealthData.id == health_data.id,
health_data_models.HealthData.user_id == user_id,
)
.first()
)
if db_health_data is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Health data not found",
headers={"WWW-Authenticate": "Bearer"},
)
# Check if bmi is None
if health_data.bmi is None:
health_data = health_data_utils.calculate_bmi(health_data, user_id, db)
# Dictionary of the fields to update if they are not None
health_data_data = health_data.dict(exclude_unset=True)
# Iterate over the fields and update the db_health_data dynamically
for key, value in health_data_data.items():
setattr(db_health_data, key, value)
# Commit the transaction
db.commit()
except IntegrityError as integrity_error:
# Rollback the transaction
db.rollback()
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Duplicate entry error. Check if date selected is not already added",
) from integrity_error
except Exception as err:
# Rollback the transaction
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in edit_health_data: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -185,7 +271,7 @@ def create_health_weight_data(
# Create a new health_data
db_health_data = health_data_models.HealthData(
user_id=user_id,
created_at=health_data.created_at,
date=health_data.date,
weight=health_data.weight,
)
@@ -212,7 +298,9 @@ def create_health_weight_data(
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in create_health_weight_data: {err}", "error")
core_logger.print_to_log(
f"Error in create_health_weight_data: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -236,9 +324,9 @@ def edit_health_weight_data(health_data: health_data_schema.HealthData, db: Sess
headers={"WWW-Authenticate": "Bearer"},
)
# Update the user
if health_data.created_at is not None:
db_health_data.created_at = health_data.created_at
# Update the health_data
if health_data.date is not None:
db_health_data.date = health_data.date
if health_data.weight is not None:
db_health_data.weight = health_data.weight
@@ -258,7 +346,9 @@ def edit_health_weight_data(health_data: health_data_schema.HealthData, db: Sess
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in edit_health_weight_data: {err}", "error")
core_logger.print_to_log(
f"Error in edit_health_weight_data: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -279,7 +369,7 @@ def delete_health_weight_data(health_data_id: int, user_id: int, db: Session):
.delete()
)
# Check if the gear was found and deleted
# Check if the health_data was found and deleted
if num_deleted == 0:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -293,7 +383,9 @@ def delete_health_weight_data(health_data_id: int, user_id: int, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in delete_health_weight_data: {err}", "error")
core_logger.print_to_log(
f"Error in delete_health_weight_data: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(

View File

@@ -21,7 +21,7 @@ class HealthData(Base):
index=True,
comment="User ID that the health_data belongs",
)
created_at = Column(
date = Column(
Date,
nullable=False,
unique=True,
@@ -37,41 +37,41 @@ class HealthData(Base):
nullable=True,
comment="Body mass index (BMI)",
)
body_fat = Column(
DECIMAL(precision=10, scale=2),
nullable=True,
comment="Body fat percentage",
)
body_water = Column(
DECIMAL(precision=10, scale=2),
nullable=True,
comment="Body hydration percentage",
)
bone_mass = Column(
DECIMAL(precision=10, scale=2),
nullable=True,
comment="Bone mass percentage",
)
muscle_mass = Column(
DECIMAL(precision=10, scale=2),
nullable=True,
comment="Muscle mass percentage",
)
physique_rating = Column(
DECIMAL(precision=10, scale=2),
nullable=True,
comment="Physique rating",
)
visceral_fat = Column(
DECIMAL(precision=10, scale=2),
nullable=True,
comment="Visceral fat rating",
)
metabolic_age = Column(
DECIMAL(precision=10, scale=2),
nullable=True,
comment="Metabolic age",
)
# body_fat = Column(
# DECIMAL(precision=10, scale=2),
# nullable=True,
# comment="Body fat percentage",
# )
# body_water = Column(
# DECIMAL(precision=10, scale=2),
# nullable=True,
# comment="Body hydration percentage",
# )
# bone_mass = Column(
# DECIMAL(precision=10, scale=2),
# nullable=True,
# comment="Bone mass percentage",
# )
# muscle_mass = Column(
# DECIMAL(precision=10, scale=2),
# nullable=True,
# comment="Muscle mass percentage",
# )
# physique_rating = Column(
# DECIMAL(precision=10, scale=2),
# nullable=True,
# comment="Physique rating",
# )
# visceral_fat = Column(
# DECIMAL(precision=10, scale=2),
# nullable=True,
# comment="Visceral fat rating",
# )
# metabolic_age = Column(
# DECIMAL(precision=10, scale=2),
# nullable=True,
# comment="Metabolic age",
# )
garminconnect_body_composition_id = Column(
String(length=45), nullable=True, comment="Garmin Connect body composition ID"
)

View File

@@ -121,8 +121,8 @@ async def create_health_weight_data(
Depends(core_database.get_db),
],
):
health_for_date = health_data_crud.get_health_data_by_created_at(
token_user_id, health_data.created_at, db
health_for_date = health_data_crud.get_health_data_by_date(
token_user_id, health_data.date, db
)
if health_for_date:
if health_for_date.weight is None:

View File

@@ -1,19 +1,20 @@
from pydantic import BaseModel
from datetime import date
from datetime import date as datetime_date
class HealthData(BaseModel):
id: int | None = None
user_id: int | None = None
created_at: date | None = None
date: datetime_date | None = None
weight: float | None = None
bmi: float | None = None
body_fat: float | None = None
body_water: float | None = None
bone_mass: float | None = None
muscle_mass: float | None = None
physique_rating: float | None = None
visceral_fat: float | None = None
metabolic_age: float | None = None
#body_fat: float | None = None
#body_water: float | None = None
#bone_mass: float | None = None
#muscle_mass: float | None = None
#physique_rating: float | None = None
#visceral_fat: float | None = None
#metabolic_age: float | None = None
garminconnect_body_composition_id: str | None = None
class Config:
orm_mode = True

View File

@@ -0,0 +1,17 @@
from sqlalchemy.orm import Session
import users.crud as users_crud
import health_data.schema as health_data_schema
def calculate_bmi(health_data: health_data_schema.HealthData, user_id: int, db: Session):
# Get the user from the database
user = users_crud.get_user_by_id(user_id, db)
# Check if user is not None and user height is not None
if user is not None and user.height is not None:
# Calculate the bmi
health_data.bmi = health_data.weight / ((user.height / 100) ** 2)
# return the health data
return health_data

View File

@@ -26,7 +26,7 @@ def get_user_health_targets(user_id: int, db: Session):
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_user_health_targets: {err}", "error")
core_logger.print_to_log(f"Error in get_user_health_targets: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -68,7 +68,7 @@ def create_health_targets(user_id: int, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in create_health_targets: {err}", "error")
core_logger.print_to_log(f"Error in create_health_targets: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,

View File

@@ -15,12 +15,13 @@ import core.migrations as core_migrations
import garmin.logger as garmin_logger
import garmin.activity_utils as garmin_activity_utils
import garmin.health_utils as garmin_health_utils
import strava.activity_utils as strava_activity_utils
import strava.logger as strava_logger
import migrations.logger as migrations_logger
import strava.logger as strava_logger
from core.routes import router as api_router
@@ -41,13 +42,19 @@ def startup_event():
# Create a scheduler to run background jobs
core_scheduler.start_scheduler()
# Retrieve last day activities from Garmin Connect
# Retrieve last day activities from Garmin Connect and Strava
core_logger.print_to_log_and_console(
"Retrieving last day activities from Garmin Connect and Strava on startup"
)
garmin_activity_utils.retrieve_garminconnect_users_activities_for_days(1)
strava_activity_utils.retrieve_strava_users_activities_for_days(1)
# Retrieve last day body composition from Garmin Connect
core_logger.print_to_log_and_console(
"Retrieving last day body composition from Garmin Connect on startup"
)
garmin_health_utils.retrieve_garminconnect_users_bc_for_days(1)
def shutdown_event():
# Log the shutdown event

View File

@@ -23,7 +23,7 @@ def get_migrations_not_executed(db: Session):
except Exception as err:
# Log the exception
core_logger.print_to_log_and_console(f"Error in get_migrations_not_executed. See migrations log for more information", "error")
migrations_logger.print_to_log(f"Error in get_migrations_not_executed: {err}", "error")
migrations_logger.print_to_log(f"Error in get_migrations_not_executed: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -58,7 +58,7 @@ def set_migration_as_executed(migration_id: int, db: Session):
# Log the exception
core_logger.print_to_log_and_console(f"Error in set_migration_as_executed. See migrations log for more information", "error")
migrations_logger.print_to_log(f"Error in set_migration_as_executed: {err}", "error")
migrations_logger.print_to_log(f"Error in set_migration_as_executed: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(

View File

@@ -15,6 +15,9 @@ import activity_streams.crud as activity_streams_crud
import migrations.crud as migrations_crud
import migrations.logger as migrations_logger
import health_data.crud as health_data_crud
import health_data.utils as health_data_utils
import core.logger as core_logger
@@ -56,7 +59,7 @@ def process_migration_1(db: Session):
activities = activities_crud.get_all_activities(db)
except Exception as err:
migrations_logger.print_to_log(
f"Migration 1 - Error fetching activities: {err}", "error"
f"Migration 1 - Error fetching activities: {err}", "error", exc=err
)
return
@@ -95,6 +98,7 @@ def process_migration_1(db: Session):
migrations_logger.print_to_log(
f"Migration 1 - Failed to fetch streams for activity {activity.id}: {err}",
"warning",
exc=err,
)
activities_processed_with_no_errors = False
continue
@@ -160,7 +164,9 @@ def process_migration_1(db: Session):
)
migrations_logger.print_to_log(
f"Migration 1 - Failed to process activity {activity.id}: {err}", "error"
f"Migration 1 - Failed to process activity {activity.id}: {err}",
"error",
exc=err,
)
# Mark migration as executed
@@ -174,7 +180,9 @@ def process_migration_1(db: Session):
)
migrations_logger.print_to_log(
f"Migration 1 - Failed to set migration as executed: {err}", "error"
f"Migration 1 - Failed to set migration as executed: {err}",
"error",
exc=err,
)
return
else:
@@ -192,19 +200,27 @@ def process_migration_2(db: Session):
# Create an instance of TimezoneFinder
tf = TimezoneFinder()
# Initialize flag to track if all activities and health_data were processed without errors
activities_processed_with_no_errors = True
health_data_processed_with_no_errors = True
# Fetch all activities and health_data
try:
activities = activities_crud.get_all_activities(db)
health_data = health_data_crud.get_all_health_data(db)
except Exception as err:
migrations_logger.print_to_log(
f"Migration 2 - Error fetching activities: {err}", "error"
f"Migration 2 - Error fetching activities and/or health_data: {err}",
"error",
exc=err,
)
return
if activities:
# Process each activity and add timezone
for activity in activities:
try:
# Skip if activity already has timezone
if activity.timezone:
migrations_logger.print_to_log(
f"Migration 2 - {activity.id} already has timezone defined. Skipping.",
@@ -225,6 +241,7 @@ def process_migration_2(db: Session):
migrations_logger.print_to_log(
f"Migration 2 - Failed to fetch streams for activity {activity.id}: {err}",
"warning",
exc=err,
)
activities_processed_with_no_errors = False
continue
@@ -239,7 +256,7 @@ def process_migration_2(db: Session):
# Update the activity in the database
activities_crud.edit_activity(activity.user_id, activity, db)
migrations_logger.print_to_log(
f"Migration 2 - Processed activity: {activity.id} - {activity.name}"
)
@@ -252,11 +269,45 @@ def process_migration_2(db: Session):
)
migrations_logger.print_to_log(
f"Migration 2 - Failed to process activity {activity.id}: {err}", "error"
f"Migration 2 - Failed to process activity {activity.id}: {err}",
"error",
exc=err,
)
if health_data:
# Process each weight and add timezone
for data in health_data:
try:
# Skip if weight already has timezone
if data.bmi:
migrations_logger.print_to_log(
f"Migration 2 - {data.id} already has BMI defined. Skipping.",
"info",
)
continue
# Update the weight in the database
health_data_crud.edit_health_data(data.user_id, data, db)
migrations_logger.print_to_log(
f"Migration 2 - Processed BMI: {data.id}"
)
except Exception as err:
health_data_processed_with_no_errors = False
core_logger.print_to_log_and_console(
f"Migration 2 - Failed to process BMI {data.id}. Please check migrations log for more details.",
"error",
)
migrations_logger.print_to_log(
f"Migration 2 - Failed to process BMI {data.id}: {err}",
"error",
exc=err,
)
# Mark migration as executed
if activities_processed_with_no_errors:
if activities_processed_with_no_errors and health_data_processed_with_no_errors:
try:
migrations_crud.set_migration_as_executed(2, db)
except Exception as err:
@@ -266,7 +317,9 @@ def process_migration_2(db: Session):
)
migrations_logger.print_to_log(
f"Migration 2 - Failed to set migration as executed: {err}", "error"
f"Migration 2 - Failed to set migration as executed: {err}",
"error",
exc=err,
)
return
else:

View File

@@ -1,6 +1,9 @@
import os
from datetime import datetime, timedelta
from sqlalchemy.orm import Session
from stravalib.client import Client
from timezonefinder import TimezoneFinder
import activities.schema as activities_schema
import activities.crud as activities_crud
@@ -55,6 +58,10 @@ def parse_activity(
user_integrations: user_integrations_schema.UserIntegrations,
db: Session,
) -> dict:
# Create an instance of TimezoneFinder
tf = TimezoneFinder()
timezone = os.environ.get("TZ")
# Get the detailed activity
detailedActivity = strava_client.get_activity(activity.id)
@@ -102,6 +109,7 @@ def parse_activity(
# Extract data from streams
lat_lon = streams["latlng"].data if "latlng" in streams else []
lat_lon_waypoints = []
is_lat_lon_set = False
ele = streams["altitude"].data if "altitude" in streams else []
ele_waypoints = []
is_elevation_set = False
@@ -128,6 +136,7 @@ def parse_activity(
"lon": lat_lon[i][1],
}
)
is_lat_lon_set = True
for i in range(len(ele)):
ele_waypoints.append({"time": time[i], "ele": ele[i]})
@@ -235,17 +244,26 @@ def parse_activity(
if gear is not None:
gear_id = gear.id
# Activity type
activity_type = activities_utils.define_activity_type(detailedActivity.sport_type.root)
if activity_type != 3 and activity_type != 7:
if is_lat_lon_set:
timezone = tf.timezone_at(
lat=lat_lon_waypoints[0]["lat"],
lng=lat_lon_waypoints[0]["lon"],
)
# Create the activity object
activity_to_store = activities_schema.Activity(
user_id=user_id,
name=detailedActivity.name,
distance=round(detailedActivity.distance) if detailedActivity.distance else 0,
description=detailedActivity.description,
activity_type=activities_utils.define_activity_type(
detailedActivity.sport_type.root
),
activity_type=activity_type,
start_time=start_date_parsed.strftime("%Y-%m-%dT%H:%M:%S"),
end_time=end_date_parsed.strftime("%Y-%m-%dT%H:%M:%S"),
timezone=timezone,
total_elapsed_time=total_elapsed_time,
total_timer_time=total_timer_time,
city=city,

View File

@@ -22,12 +22,12 @@ def get_strava_logger():
return logging.getLogger("strava_logger")
def print_to_log(message: str, type: str = "info"):
def print_to_log(message: str, type: str = "info", exc: Exception = None):
garminconnect_logger = get_strava_logger()
if type == "info":
garminconnect_logger.info(message)
elif type == "error":
garminconnect_logger.error(message)
garminconnect_logger.error(message, exc_info=exc is not None)
elif type == "warning":
garminconnect_logger.warning(message)
elif type == "debug":

View File

@@ -91,7 +91,7 @@ async def strava_link(
core_logger.print_to_log(
f"Error in strava_link. For more information check Strava log.", "error"
)
strava_logger.print_to_log(f"Error in strava_link: {err}", "error")
strava_logger.print_to_log(f"Error in strava_link: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(

View File

@@ -30,7 +30,7 @@ def get_user_integrations_by_user_id(user_id: int, db: Session):
except Exception as err:
# Log the exception
core_logger.print_to_log(
f"Error in get_user_integrations_by_user_id: {err}", "error"
f"Error in get_user_integrations_by_user_id: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -43,7 +43,9 @@ def get_user_integrations_by_strava_state(strava_state: str, db: Session):
try:
user_integrations = (
db.query(user_integrations_models.UserIntegrations)
.filter(user_integrations_models.UserIntegrations.strava_state == strava_state)
.filter(
user_integrations_models.UserIntegrations.strava_state == strava_state
)
.first()
)
@@ -56,7 +58,7 @@ def get_user_integrations_by_strava_state(strava_state: str, db: Session):
except Exception as err:
# Log the exception
core_logger.print_to_log(
f"Error in get_user_integrations_by_user_id: {err}", "error"
f"Error in get_user_integrations_by_user_id: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -86,7 +88,9 @@ def create_user_integrations(user_id: int, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in create_user_integrations: {err}", "error")
core_logger.print_to_log(
f"Error in create_user_integrations: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -118,7 +122,9 @@ def link_strava_account(
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in link_strava_account: {err}", "error")
core_logger.print_to_log(
f"Error in link_strava_account: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -153,7 +159,9 @@ def unlink_strava_account(user_id: int, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in unlink_strava_account: {err}", "error")
core_logger.print_to_log(
f"Error in unlink_strava_account: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -185,7 +193,9 @@ def set_user_strava_state(user_id: int, state: str, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in set_user_strava_state: {err}", "error")
core_logger.print_to_log(
f"Error in set_user_strava_state: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -209,7 +219,9 @@ def set_user_strava_sync_gear(user_id: int, strava_sync_gear: bool, db: Session)
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in set_user_strava_state: {err}", "error")
core_logger.print_to_log(
f"Error in set_user_strava_state: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -247,7 +259,9 @@ def link_garminconnect_account(
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in link_garminconnect_account: {err}", "error")
core_logger.print_to_log(
f"Error in link_garminconnect_account: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -274,7 +288,7 @@ def set_user_garminconnect_sync_gear(
# Log the exception
core_logger.print_to_log(
f"Error in set_user_garminconnect_sync_gear: {err}", "error"
f"Error in set_user_garminconnect_sync_gear: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code
@@ -310,7 +324,7 @@ def unlink_garminconnect_account(user_id: int, db: Session):
# Log the exception
core_logger.print_to_log(
f"Error in unlink_garminconnect_account: {err}", "error"
f"Error in unlink_garminconnect_account: {err}", "error", exc=err
)
# Raise an HTTPException with a 500 Internal Server Error status code

View File

@@ -25,7 +25,7 @@ def authenticate_user(username: str, db: Session):
return user
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in authenticate_user: {err}", "error")
core_logger.print_to_log(f"Error in authenticate_user: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -39,7 +39,7 @@ def get_all_users(db: Session):
return db.query(users_models.User).all()
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_all_number: {err}", "error")
core_logger.print_to_log(f"Error in get_all_number: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -55,7 +55,7 @@ def get_users_number(db: Session):
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_users_number: {err}", "error")
core_logger.print_to_log(f"Error in get_users_number: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -84,7 +84,7 @@ def get_users_with_pagination(db: Session, page_number: int = 1, num_records: in
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_users_with_pagination: {err}", "error")
core_logger.print_to_log(f"Error in get_users_with_pagination: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -115,7 +115,7 @@ def get_user_if_contains_username(username: str, db: Session):
return users
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_user_if_contains_username: {err}", "error")
core_logger.print_to_log(f"Error in get_user_if_contains_username: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -139,7 +139,7 @@ def get_user_by_username(username: str, db: Session):
return user
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_user_by_username: {err}", "error")
core_logger.print_to_log(f"Error in get_user_by_username: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -163,7 +163,7 @@ def get_user_by_id(user_id: int, db: Session):
return user
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_user_by_id: {err}", "error")
core_logger.print_to_log(f"Error in get_user_by_id: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -188,7 +188,7 @@ def get_user_id_by_username(username: str, db: Session):
return user_id
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_user_id_by_username: {err}", "error")
core_logger.print_to_log(f"Error in get_user_id_by_username: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -211,7 +211,7 @@ def get_user_photo_path_by_id(user_id: int, db: Session):
return user_db.photo_path
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_user_photo_path_by_id: {err}", "error")
core_logger.print_to_log(f"Error in get_user_photo_path_by_id: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -257,7 +257,7 @@ def create_user(user: users_schema.UserCreate, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in create_user: {err}", "error")
core_logger.print_to_log(f"Error in create_user: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -304,7 +304,7 @@ def edit_user(user_id: int, user: users_schema.User, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in edit_user: {err}", "error")
core_logger.print_to_log(f"Error in edit_user: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -328,7 +328,7 @@ def edit_user_password(user_id: int, password: str, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in edit_user_password: {err}", "error")
core_logger.print_to_log(f"Error in edit_user_password: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -355,7 +355,7 @@ def edit_user_photo_path(user_id: int, photo_path: str, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in edit_user_photo_path: {err}", "error")
core_logger.print_to_log(f"Error in edit_user_photo_path: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -382,7 +382,7 @@ def delete_user_photo(user_id: int, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in delete_user_photo: {err}", "error")
core_logger.print_to_log(f"Error in delete_user_photo: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
@@ -413,7 +413,7 @@ def delete_user(user_id: int, db: Session):
db.rollback()
# Log the exception
core_logger.print_to_log(f"Error in delete_user: {err}", "error")
core_logger.print_to_log(f"Error in delete_user: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(

View File

@@ -49,7 +49,7 @@ async def save_user_image(user_id: int, file: UploadFile, db: Session):
return users_crud.edit_user_photo_path(user_id, file_path_to_save, db)
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in save_user_image: {err}", "error")
core_logger.print_to_log(f"Error in save_user_image: {err}", "error", exc=err)
# Remove the file after processing
if os.path.exists(file_path_to_save):

View File

@@ -19,6 +19,7 @@ services:
container_name: backend
image: ghcr.io/joaovitoriasilva/endurain/backend:latest
environment:
- TZ=Europe/Lisbon
- DB_PASSWORD=changeme
- SECRET_KEY=changeme # openssl rand -hex 32
- STRAVA_CLIENT_ID=changeme

View File

@@ -1,8 +1,13 @@
<script setup>
import { RouterView } from 'vue-router'
import NavbarComponent from './components/Navbar/NavbarComponent.vue'
import FooterComponent from './components/FooterComponent.vue'
import { Notivue, Notification, NotificationProgress, pastelTheme } from 'notivue'
import { RouterView } from "vue-router";
import NavbarComponent from "./components/Navbar/NavbarComponent.vue";
import FooterComponent from "./components/FooterComponent.vue";
import {
Notivue,
Notification,
NotificationProgress,
pastelTheme,
} from "notivue";
</script>
<template>

View File

@@ -93,7 +93,7 @@
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" :class="{ disabled: activity.strava_activity_id || activity.garminconnect_activity_id }" href="#" data-bs-toggle="modal" data-bs-target="#deleteActivityModal">
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#deleteActivityModal">
{{ $t("activitySummary.buttonDeleteActivity") }}
</a>
</li>

View File

@@ -6,7 +6,8 @@
<div class="fw-bold">
{{ data.weight }}
</div>
{{ formatDate(data.created_at) }}
{{ data.date }}
{{ formatDate(data.date) }}
</div>
</div>
<div>

View File

@@ -0,0 +1,63 @@
<template>
<div class="modal fade" :id="`${modalId}`" tabindex="-1" :aria-labelledby="`${modalId}`" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" :id="`${modalId}`">{{ title }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- number field -->
<label for="numberToEmit"><b>* {{ numberFieldLabel }}</b></label>
<input class="form-control" type="number" name="numberToEmit" :placeholder="`${numberFieldLabel}`" v-model="numberToEmit" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t("generalItems.buttonClose") }}</button>
<a type="button" @click="submitAction()" class="btn" :class="{ 'btn-success': actionButtonType === 'success', 'btn-danger': actionButtonType === 'danger', 'btn-warning': actionButtonType === 'warning', 'btn-primary': actionButtonType === 'loading' }" data-bs-dismiss="modal">{{ actionButtonText }}</a>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref} from 'vue';
export default {
props: {
modalId: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
numberFieldLabel: {
type: String,
required: true,
},
actionButtonType: {
type: String,
required: true,
},
actionButtonText: {
type: String,
required: true,
},
},
emits: ['numberToEmitAction'],
setup(props, { emit }) {
const numberToEmit = ref(7);
function submitAction() {
emit('numberToEmitAction', numberToEmit.value);
}
return {
numberToEmit,
submitAction,
};
},
};
</script>

View File

@@ -95,57 +95,19 @@
</li>
</ul>
<!-- modal retrieve Strava activities by days -->
<ModalComponentNumberInput modalId="retrieveStravaActivitiesByDaysModal" :title="t('settingsIntegrationsZone.modalRetrieveActivitiesByDaysTitle')" :numberFieldLabel="`${t('settingsIntegrationsZone.modalRetrieveActivitiesByDaysLabel')}`" :actionButtonType="`success`" :actionButtonText="t('settingsIntegrationsZone.modalRetrieveButton')" @numberToEmitAction="submitRetrieveStravaActivities"/>
<!-- modal unlink Strava -->
<ModalComponent modalId="unlinkStravaModal" :title="t('settingsIntegrationsZone.modalUnlinkStravaTitle')" :body="`${t('settingsIntegrationsZone.modalUnlinkStravaBody')}`" :actionButtonType="`danger`" :actionButtonText="t('settingsIntegrationsZone.modalUnlinkStravaTitle')" @submitAction="buttonStravaUnlink"/>
<!-- modal garmin connect auth -->
<GarminConnectLoginModalComponent />
<!-- modal retrieve strava activities by days -->
<div class="modal fade" id="retrieveStravaActivitiesByDaysModal" tabindex="-1" aria-labelledby="retrieveStravaActivitiesByDaysModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="retrieveStravaActivitiesByDaysModalLabel">{{ $t("settingsIntegrationsZone.modalRetrieveActivitiesByDaysTitle") }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form @submit.prevent="submitRetrieveStravaActivities">
<div class="modal-body">
<!-- number of days fields -->
<label for="daysToRetrieve"><b>* {{ $t("settingsIntegrationsZone.modalRetrieveActivitiesByDaysLabel") }}</b></label>
<input class="form-control" type="number" name="daysToRetrieve" :placeholder='$t("settingsIntegrationsZone.modalRetrieveActivitiesByDaysPlaceholder")' v-model="daysToRetrieveStrava" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" name="retrieveStravaActivities" data-bs-dismiss="modal">{{ $t("generalItems.buttonClose") }}</button>
<button type="submit" class="btn btn-success" data-bs-dismiss="modal">{{ $t("settingsIntegrationsZone.modalRetrieveButton") }}</button>
</div>
</form>
</div>
</div>
</div>
<!-- modal retrieve garmin connect activities by days -->
<div class="modal fade" id="retrieveGarminConnectActivitiesByDaysModal" tabindex="-1" aria-labelledby="retrieveGarminConnectActivitiesByDaysModal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="retrieveGarminConnectActivitiesByDaysModal">{{ $t("settingsIntegrationsZone.modalRetrieveActivitiesByDaysTitle") }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form @submit.prevent="submitRetrieveGarminConnectActivities">
<div class="modal-body">
<!-- number of days fields -->
<label for="daysToRetrieve"><b>* {{ $t("settingsIntegrationsZone.modalRetrieveActivitiesByDaysLabel") }}</b></label>
<input class="form-control" type="number" name="daysToRetrieve" :placeholder='$t("settingsIntegrationsZone.modalRetrieveActivitiesByDaysPlaceholder")' v-model="daysToRetrieveGarmin" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" name="retrieveStravaActivities" data-bs-dismiss="modal">{{ $t("generalItems.buttonClose") }}</button>
<button type="submit" class="btn btn-success" data-bs-dismiss="modal">{{ $t("settingsIntegrationsZone.modalRetrieveButton") }}</button>
</div>
</form>
</div>
</div>
</div>
<ModalComponent modalId="unlinkStravaModal" :title="t('settingsIntegrationsZone.modalUnlinkStravaTitle')" :body="`${t('settingsIntegrationsZone.modalUnlinkStravaBody')}`" :actionButtonType="`danger`" :actionButtonText="t('settingsIntegrationsZone.modalUnlinkStravaTitle')" @submitAction="buttonStravaUnlink"/>
<!-- modal retrieve Garmin Connect activities by days -->
<ModalComponentNumberInput modalId="retrieveGarminConnectActivitiesByDaysModal" :title="t('settingsIntegrationsZone.modalRetrieveActivitiesByDaysTitle')" :numberFieldLabel="`${t('settingsIntegrationsZone.modalRetrieveActivitiesByDaysLabel')}`" :actionButtonType="`success`" :actionButtonText="t('settingsIntegrationsZone.modalRetrieveButton')" @numberToEmitAction="submitRetrieveGarminConnectActivities"/>
<!-- modal unlink Garmin Connect -->
<ModalComponent modalId="unlinkGarminConnectModal" :title="t('settingsIntegrationsZone.modalUnlinkGarminConnectTitle')" :body="`${t('settingsIntegrationsZone.modalUnlinkGarminConnectBody')}`" :actionButtonType="`danger`" :actionButtonText="t('settingsIntegrationsZone.modalUnlinkGarminConnectTitle')" @submitAction="buttonGarminConnectUnlink"/>
</div>
</template>
@@ -163,6 +125,7 @@ import { activities } from "@/services/activitiesService";
import { garminConnect } from "@/services/garminConnectService";
// Import the components
import ModalComponent from "@/components/Modals/ModalComponent.vue";
import ModalComponentNumberInput from "@/components/Modals/ModalComponentNumberInput.vue";
import GarminConnectLoginModalComponent from "./SettingsIntegrations/GarminConnectLoginModalComponent.vue";
//import Modal from 'bootstrap/js/dist/modal';
@@ -170,15 +133,12 @@ import GarminConnectLoginModalComponent from "./SettingsIntegrations/GarminConne
export default {
components: {
ModalComponent,
ModalComponentNumberInput,
GarminConnectLoginModalComponent,
},
setup() {
const authStore = useAuthStore();
const { locale, t } = useI18n();
const daysToRetrieveStrava = ref(7);
const daysToRetrieveGarmin = ref(7);
const garminConnectUsername = ref("");
const garminConnectPassword = ref("");
async function submitConnectStrava() {
const array = new Uint8Array(16);
@@ -199,9 +159,9 @@ export default {
}
}
async function submitRetrieveStravaActivities() {
async function submitRetrieveStravaActivities(daysToRetrieveStrava) {
try {
await strava.getStravaActivitiesLastDays(daysToRetrieveStrava.value);
await strava.getStravaActivitiesLastDays(daysToRetrieveStrava);
// Show the loading alert.
push.info(
@@ -268,9 +228,9 @@ export default {
}
}
async function submitRetrieveGarminConnectActivities() {
async function submitRetrieveGarminConnectActivities(daysToRetrieveGarmin) {
try {
await garminConnect.getGarminConnectActivitiesLastDays(daysToRetrieveGarmin.value);
await garminConnect.getGarminConnectActivitiesLastDays(daysToRetrieveGarmin);
// Show the loading alert.
push.info(
@@ -328,14 +288,10 @@ export default {
t,
submitConnectStrava,
submitRetrieveStravaActivities,
daysToRetrieveStrava,
submitRetrieveStravaGear,
buttonStravaUnlink,
submitBulkImport,
garminConnectUsername,
garminConnectPassword,
submitRetrieveGarminConnectActivities,
daysToRetrieveGarmin,
submitRetrieveGarminConnectGear,
buttonGarminConnectUnlink,
};

View File

@@ -278,7 +278,7 @@ export default {
gearId.value = activity.value.gear_id;
}
if (activity.value.activity_type === 1 || activity.value.activity_type === 2 || activity.value.activity_type === 3) {
if (activity.value.activity_type === 1 || activity.value.activity_type === 2 || activity.value.activity_type === 3 || activity.value.activity_type === 11 || activity.value.activity_type === 12) {
gearsByType.value = await gears.getGearFromType(2);
} else {
if (activity.value.activity_type === 4 || activity.value.activity_type === 5 || activity.value.activity_type === 6 || activity.value.activity_type === 7) {

View File

@@ -232,7 +232,7 @@ export default {
userNumberOfActivities.value ++;
} catch (error) {
// Set the error message
notification.reject(`${t('generalItems.errorFetchingInfo')} - ${error}`)
notification.reject(`${error}`)
}
}
};