diff --git a/.gitignore b/.gitignore index 6a2a66b43..6acf1faa6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # General docker-compose.yml +# aux folder +aux_scripts/.garminconnect + # Python backend/app/__pycache__/ backend/app/*/__pycache__/ diff --git a/aux_scripts/aux_gc_getbcstats.py b/aux_scripts/aux_gc_getbcstats.py new file mode 100644 index 000000000..6f274b5bf --- /dev/null +++ b/aux_scripts/aux_gc_getbcstats.py @@ -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) diff --git a/backend/app/activities/crud.py b/backend/app/activities/crud.py index 75f4b5d34..2ac96a134 100644 --- a/backend/app/activities/crud.py +++ b/backend/app/activities/crud.py @@ -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( diff --git a/backend/app/activities/models.py b/backend/app/activities/models.py index 1fd28acb3..cec621baa 100644 --- a/backend/app/activities/models.py +++ b/backend/app/activities/models.py @@ -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", - ) \ No newline at end of file + ) diff --git a/backend/app/activities/router.py b/backend/app/activities/router.py index afd3f66a6..5703dfe1c 100644 --- a/backend/app/activities/router.py +++ b/backend/app/activities/router.py @@ -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"} diff --git a/backend/app/activities/utils.py b/backend/app/activities/utils.py index df0b09383..fd9ead348 100644 --- a/backend/app/activities/utils.py +++ b/backend/app/activities/utils.py @@ -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, diff --git a/backend/app/activity_streams/crud.py b/backend/app/activity_streams/crud.py index 997cf3914..9efa6a04a 100644 --- a/backend/app/activity_streams/crud.py +++ b/backend/app/activity_streams/crud.py @@ -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, diff --git a/backend/app/alembic/versions/7ba9b71fe203_v0_6_5_migration.py b/backend/app/alembic/versions/542605083c0c_v0_6_5_migration.py similarity index 52% rename from backend/app/alembic/versions/7ba9b71fe203_v0_6_5_migration.py rename to backend/app/alembic/versions/542605083c0c_v0_6_5_migration.py index 79879f4a2..bc2bf04ee 100644 --- a/backend/app/alembic/versions/7ba9b71fe203_v0_6_5_migration.py +++ b/backend/app/alembic/versions/542605083c0c_v0_6_5_migration.py @@ -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; diff --git a/backend/app/core/logger.py b/backend/app/core/logger.py index d57f3d03b..231831917 100644 --- a/backend/app/core/logger.py +++ b/backend/app/core/logger.py @@ -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": diff --git a/backend/app/core/scheduler.py b/backend/app/core/scheduler.py index b06a07d76..b62873bdd 100644 --- a/backend/app/core/scheduler.py +++ b/backend/app/core/scheduler.py @@ -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() diff --git a/backend/app/fit/utils.py b/backend/app/fit/utils.py index 0e409115f..bad43c31c 100644 --- a/backend/app/fit/utils.py +++ b/backend/app/fit/utils.py @@ -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 \ No newline at end of file + return tz_name diff --git a/backend/app/followers/crud.py b/backend/app/followers/crud.py index d922b08f1..a87c582ff 100644 --- a/backend/app/followers/crud.py +++ b/backend/app/followers/crud.py @@ -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( diff --git a/backend/app/garmin/activity_utils.py b/backend/app/garmin/activity_utils.py index cbec770b2..fea1fff62 100644 --- a/backend/app/garmin/activity_utils.py +++ b/backend/app/garmin/activity_utils.py @@ -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) diff --git a/backend/app/garmin/health_utils.py b/backend/app/garmin/health_utils.py new file mode 100644 index 000000000..9331bd4b1 --- /dev/null +++ b/backend/app/garmin/health_utils.py @@ -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() diff --git a/backend/app/garmin/logger.py b/backend/app/garmin/logger.py index 8db1a8d51..f31a21bf5 100644 --- a/backend/app/garmin/logger.py +++ b/backend/app/garmin/logger.py @@ -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": diff --git a/backend/app/gears/crud.py b/backend/app/gears/crud.py index 4a6c8ffe5..dfd4586fc 100644 --- a/backend/app/gears/crud.py +++ b/backend/app/gears/crud.py @@ -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 diff --git a/backend/app/gears/models.py b/backend/app/gears/models.py index 91971822d..b782cb8d4 100644 --- a/backend/app/gears/models.py +++ b/backend/app/gears/models.py @@ -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)" ) diff --git a/backend/app/gpx/utils.py b/backend/app/gpx/utils.py index 21c703942..002704199 100644 --- a/backend/app/gpx/utils.py +++ b/backend/app/gpx/utils.py @@ -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, diff --git a/backend/app/health_data/crud.py b/backend/app/health_data/crud.py index 57dcec285..e101fb773 100644 --- a/backend/app/health_data/crud.py +++ b/backend/app/health_data/crud.py @@ -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( diff --git a/backend/app/health_data/models.py b/backend/app/health_data/models.py index fb3b037a3..fa4d5f2ee 100644 --- a/backend/app/health_data/models.py +++ b/backend/app/health_data/models.py @@ -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" ) diff --git a/backend/app/health_data/router.py b/backend/app/health_data/router.py index ef1dbed88..733a26c7c 100644 --- a/backend/app/health_data/router.py +++ b/backend/app/health_data/router.py @@ -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: diff --git a/backend/app/health_data/schema.py b/backend/app/health_data/schema.py index 09a52a158..a39d28852 100644 --- a/backend/app/health_data/schema.py +++ b/backend/app/health_data/schema.py @@ -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 \ No newline at end of file diff --git a/backend/app/health_data/utils.py b/backend/app/health_data/utils.py new file mode 100644 index 000000000..5b257b4dc --- /dev/null +++ b/backend/app/health_data/utils.py @@ -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 \ No newline at end of file diff --git a/backend/app/health_targets/crud.py b/backend/app/health_targets/crud.py index f8354f90e..45dc3ebf1 100644 --- a/backend/app/health_targets/crud.py +++ b/backend/app/health_targets/crud.py @@ -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, diff --git a/backend/app/main.py b/backend/app/main.py index 1b0f525e6..9d6359403 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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 diff --git a/backend/app/migrations/crud.py b/backend/app/migrations/crud.py index 1367f2226..1310f0fe5 100644 --- a/backend/app/migrations/crud.py +++ b/backend/app/migrations/crud.py @@ -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( diff --git a/backend/app/migrations/utils.py b/backend/app/migrations/utils.py index b4c4de0c7..6897da99e 100644 --- a/backend/app/migrations/utils.py +++ b/backend/app/migrations/utils.py @@ -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: diff --git a/backend/app/strava/activity_utils.py b/backend/app/strava/activity_utils.py index c1dbea7e7..3e6e1b758 100644 --- a/backend/app/strava/activity_utils.py +++ b/backend/app/strava/activity_utils.py @@ -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, diff --git a/backend/app/strava/logger.py b/backend/app/strava/logger.py index f692e5da2..0ce2c7823 100644 --- a/backend/app/strava/logger.py +++ b/backend/app/strava/logger.py @@ -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": diff --git a/backend/app/strava/router.py b/backend/app/strava/router.py index 0920fb43c..f76a255ab 100644 --- a/backend/app/strava/router.py +++ b/backend/app/strava/router.py @@ -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( diff --git a/backend/app/user_integrations/crud.py b/backend/app/user_integrations/crud.py index 6dafbb9f0..ddc717453 100644 --- a/backend/app/user_integrations/crud.py +++ b/backend/app/user_integrations/crud.py @@ -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 diff --git a/backend/app/users/crud.py b/backend/app/users/crud.py index 719a0f959..55840de1b 100644 --- a/backend/app/users/crud.py +++ b/backend/app/users/crud.py @@ -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( diff --git a/backend/app/users/utils.py b/backend/app/users/utils.py index 5d45edbf8..67c3b225b 100644 --- a/backend/app/users/utils.py +++ b/backend/app/users/utils.py @@ -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): diff --git a/docker-compose.yml.example b/docker-compose.yml.example index a6ad03e55..030cebaeb 100644 --- a/docker-compose.yml.example +++ b/docker-compose.yml.example @@ -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 diff --git a/frontend/app/src/App.vue b/frontend/app/src/App.vue index 223741643..8f0376d43 100644 --- a/frontend/app/src/App.vue +++ b/frontend/app/src/App.vue @@ -1,8 +1,13 @@ @@ -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, }; diff --git a/frontend/app/src/views/ActivityView.vue b/frontend/app/src/views/ActivityView.vue index d99528af2..10fecc325 100644 --- a/frontend/app/src/views/ActivityView.vue +++ b/frontend/app/src/views/ActivityView.vue @@ -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) { diff --git a/frontend/app/src/views/HomeView.vue b/frontend/app/src/views/HomeView.vue index 52cba7d8c..453590f9b 100644 --- a/frontend/app/src/views/HomeView.vue +++ b/frontend/app/src/views/HomeView.vue @@ -232,7 +232,7 @@ export default { userNumberOfActivities.value ++; } catch (error) { // Set the error message - notification.reject(`${t('generalItems.errorFetchingInfo')} - ${error}`) + notification.reject(`${error}`) } } };