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 # General
docker-compose.yml docker-compose.yml
# aux folder
aux_scripts/.garminconnect
# Python # Python
backend/app/__pycache__/ backend/app/__pycache__/
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: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -63,7 +65,9 @@ def get_user_activities(
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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 ) from err
def create_activity(activity: activities_schema.Activity, db: Session): def create_activity(
activity: activities_schema.Activity, db: Session
) -> activities_schema.Activity:
try: try:
# Create a new activity # Create a new activity
db_activity = activities_models.Activity( db_activity = activities_models.Activity(
@@ -592,7 +598,7 @@ def create_activity(activity: activities_schema.Activity, db: Session):
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -664,7 +670,9 @@ def add_gear_to_activity(activity_id: int, gear_id: int, db: Session):
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -725,7 +733,7 @@ def delete_activity(activity_id: int, db: Session):
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(

View File

@@ -2,7 +2,7 @@ from sqlalchemy import (
Column, Column,
Integer, Integer,
String, String,
TIMESTAMP, DATETIME,
ForeignKey, ForeignKey,
DECIMAL, DECIMAL,
BigInteger, BigInteger,
@@ -38,11 +38,13 @@ class Activity(Base):
comment="Gear type (1 - mountain bike, 2 - gravel bike, ...)", comment="Gear type (1 - mountain bike, 2 - gravel bike, ...)",
) )
start_time = Column( 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( 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( total_elapsed_time = Column(
DECIMAL(precision=20, scale=10), DECIMAL(precision=20, scale=10),
@@ -66,7 +68,7 @@ class Activity(Base):
comment="Activity country (May include spaces)", comment="Activity country (May include spaces)",
) )
created_at = Column( 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_gain = Column(Integer, nullable=True, comment="Elevation gain in meters")
elevation_loss = Column(Integer, nullable=True, comment="Elevation loss in meters") elevation_loss = Column(Integer, nullable=True, comment="Elevation loss in meters")
@@ -138,4 +140,4 @@ class Activity(Base):
"ActivityStreams", "ActivityStreams",
back_populates="activity", back_populates="activity",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )

View File

@@ -646,10 +646,10 @@ async def delete_activity(
os.remove(file) os.remove(file)
except FileNotFoundError as err: except FileNotFoundError as err:
# Log the exception # 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: except Exception as err:
# Log the exception # 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 success message
return {"detail": f"Activity {activity_id} deleted successfully"} 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) shutil.move(file_path, new_file_path)
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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 raise http_err
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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 return activity_streams
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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 return activity_stream
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -92,7 +92,7 @@ def create_activity_streams(
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,

View File

@@ -1,18 +1,18 @@
"""v0.6.5 migration """v0.6.5 migration
Revision ID: 7ba9b71fe203 Revision ID: 542605083c0c
Revises: 65a0f1d72997 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 typing import Sequence, Union
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = '7ba9b71fe203' revision: str = '542605083c0c'
down_revision: Union[str, None] = '65a0f1d72997' down_revision: Union[str, None] = '65a0f1d72997'
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: 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: def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### 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', op.alter_column('activities', 'start_time',
existing_type=sa.DATETIME(), existing_type=mysql.DATETIME(),
type_=sa.TIMESTAMP(), comment='Activity start date (DATETIME)',
comment='Activity start date (TIMESTAMP)',
existing_comment='Activity start date (datetime)', existing_comment='Activity start date (datetime)',
existing_nullable=False) existing_nullable=False)
op.alter_column('activities', 'end_time', op.alter_column('activities', 'end_time',
existing_type=sa.DATETIME(), existing_type=mysql.DATETIME(),
type_=sa.TIMESTAMP(), comment='Activity end date (DATETIME)',
comment='Activity end date (TIMESTAMP)',
existing_comment='Activity end date (datetime)', existing_comment='Activity end date (datetime)',
existing_nullable=False) existing_nullable=False)
op.alter_column('activities', 'total_elapsed_time', 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)', comment='Activity total elapsed time (s)',
existing_comment='Activity total elapsed time (datetime)', existing_comment='Activity total elapsed time (datetime)',
existing_nullable=False) existing_nullable=False)
op.alter_column('activities', 'total_timer_time', 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)', comment='Activity total timer time (s)',
existing_comment='Activity total timer time (datetime)', existing_comment='Activity total timer time (datetime)',
existing_nullable=False) existing_nullable=False)
op.alter_column('activities', 'created_at', op.alter_column('activities', 'created_at',
existing_type=sa.DATETIME(), existing_type=mysql.DATETIME(),
type_=sa.TIMESTAMP(), comment='Activity creation date (DATETIME)',
comment='Activity creation date (TIMESTAMP)',
existing_comment='Activity creation date (datetime)', existing_comment='Activity creation date (datetime)',
existing_nullable=False) 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', op.alter_column('gear', 'created_at',
existing_type=sa.DATETIME(), existing_type=mysql.DATETIME(),
type_=sa.TIMESTAMP(), comment='Gear creation date (DATETIME)',
comment='Gear creation date (TIMESTAMP)',
existing_comment='Gear creation date (date)', existing_comment='Gear creation date (date)',
existing_nullable=False) 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('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.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', op.drop_index('created_at', table_name='health_data')
existing_type=sa.DATE(), op.create_unique_constraint(None, 'health_data', ['date'])
comment='Health data creation date (date)', op.drop_column('health_data', 'created_at')
existing_comment='Health data creation date (datetime)', # Add the new entry to the migrations table
existing_nullable=False)
op.add_column('activities', sa.Column('timezone', sa.String(length=250), nullable=True, comment='Activity timezone (May include spaces)'))
op.execute(""" op.execute("""
INSERT INTO migrations (id, name, description, executed) VALUES INSERT INTO migrations (id, name, description, executed) VALUES
(2, 'v0.6.5', 'Process timezone for existing activities', false); (2, 'v0.6.5', 'Process timezone for existing activities', false);
@@ -79,56 +75,52 @@ def upgrade() -> None:
def downgrade() -> None: def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.drop_column('gear', 'initial_kms') op.add_column('health_data', sa.Column('created_at', sa.DATE(), nullable=True, comment='Health data creation date (datetime)'))
op.alter_column('health_data', 'created_at', # Copy data back from `date` to `created_at`
existing_type=sa.DATE(), op.execute("""
comment='Health data creation date (datetime)', UPDATE health_data
existing_comment='Health data creation date (date)', SET created_at = date
existing_nullable=False) """)
# 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', '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', 'bmi')
op.drop_column('health_data', 'date')
op.alter_column('gear', 'created_at', op.alter_column('gear', 'created_at',
existing_type=sa.TIMESTAMP(), existing_type=mysql.DATETIME(),
type_=sa.DATETIME(),
comment='Gear creation date (date)', comment='Gear creation date (date)',
existing_comment='Gear creation date (TIMESTAMP)', existing_comment='Gear creation date (DATETIME)',
existing_nullable=False) existing_nullable=False)
op.drop_column('gear', 'initial_kms')
op.alter_column('activities', 'created_at', op.alter_column('activities', 'created_at',
existing_type=sa.TIMESTAMP(), existing_type=mysql.DATETIME(),
type_=sa.DATETIME(),
comment='Activity creation date (datetime)', comment='Activity creation date (datetime)',
existing_comment='Activity creation date (TIMESTAMP)', existing_comment='Activity creation date (DATETIME)',
existing_nullable=False) existing_nullable=False)
op.alter_column('activities', 'total_timer_time', 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)', comment='Activity total timer time (datetime)',
existing_comment='Activity total timer time (s)', existing_comment='Activity total timer time (s)',
existing_nullable=False) existing_nullable=False)
op.alter_column('activities', 'total_elapsed_time', 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)', comment='Activity total elapsed time (datetime)',
existing_comment='Activity total elapsed time (s)', existing_comment='Activity total elapsed time (s)',
existing_nullable=False) existing_nullable=False)
op.alter_column('activities', 'end_time', op.alter_column('activities', 'end_time',
existing_type=sa.TIMESTAMP(), existing_type=mysql.DATETIME(),
type_=sa.DATETIME(),
comment='Activity end date (datetime)', comment='Activity end date (datetime)',
existing_comment='Activity end date (TIMESTAMP)', existing_comment='Activity end date (DATETIME)',
existing_nullable=False) existing_nullable=False)
op.alter_column('activities', 'start_time', op.alter_column('activities', 'start_time',
existing_type=sa.TIMESTAMP(), existing_type=mysql.DATETIME(),
type_=sa.DATETIME(),
comment='Activity start date (datetime)', comment='Activity start date (datetime)',
existing_comment='Activity start date (TIMESTAMP)', existing_comment='Activity start date (DATETIME)',
existing_nullable=False) existing_nullable=False)
op.drop_column('activities', 'timezone') op.drop_column('activities', 'timezone')
# Remove the entry from the migrations table
op.execute(""" op.execute("""
DELETE FROM migrations DELETE FROM migrations
WHERE id = 2; WHERE id = 2;

View File

@@ -32,12 +32,12 @@ def get_main_logger():
return logging.getLogger("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() main_logger = get_main_logger()
if type == "info": if type == "info":
main_logger.info(message) main_logger.info(message)
elif type == "error": elif type == "error":
main_logger.error(message) main_logger.error(message, exc_info=exc is not None)
elif type == "warning": elif type == "warning":
main_logger.warning(message) main_logger.warning(message)
elif type == "debug": 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 from apscheduler.schedulers.asyncio import AsyncIOScheduler
import strava.utils as strava_utils import strava.utils as strava_utils
import strava.activity_utils as strava_activity_utils import strava.activity_utils as strava_activity_utils
import garmin.activity_utils as garmin_activity_utils import garmin.activity_utils as garmin_activity_utils
import garmin.health_utils as garmin_health_utils
import core.logger as core_logger import core.logger as core_logger
#scheduler = BackgroundScheduler() # scheduler = BackgroundScheduler()
scheduler = AsyncIOScheduler() scheduler = AsyncIOScheduler()
@@ -38,7 +39,7 @@ def start_scheduler():
core_logger.print_to_log( core_logger.print_to_log(
"Added scheduler job to retrieve last day Garmin Connect users activities every 60 minutes" "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( scheduler.add_job(
garmin_activity_utils.retrieve_garminconnect_users_activities_for_days, garmin_activity_utils.retrieve_garminconnect_users_activities_for_days,
"interval", "interval",
@@ -46,6 +47,18 @@ def start_scheduler():
args=[1], 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(): def stop_scheduler():
scheduler.shutdown() scheduler.shutdown()

View File

@@ -58,7 +58,7 @@ def create_activity_objects(
pace = 0 pace = 0
if session_record["session"]["activity_type"]: 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"] session_record["session"]["activity_type"]
) )
@@ -82,7 +82,8 @@ def create_activity_objects(
else: else:
if session_record["time_offset"]: if session_record["time_offset"]:
timezone = find_timezone_name( 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 = { parsed_activity = {
@@ -611,7 +612,7 @@ def parse_fit_file(file: str) -> dict:
raise http_err raise http_err
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -822,9 +823,9 @@ def find_timezone_name(offset_seconds, reference_date):
tz = ZoneInfo(tz_name) tz = ZoneInfo(tz_name)
if reference_date.utcoffset() is None: # Skip invalid timezones if reference_date.utcoffset() is None: # Skip invalid timezones
continue continue
# Get the UTC offset for the reference date # Get the UTC offset for the reference date
utc_offset = reference_date.astimezone(tz).utcoffset() utc_offset = reference_date.astimezone(tz).utcoffset()
if utc_offset.total_seconds() == offset_seconds: 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 return followers
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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 return followers
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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 return followings
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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 return followings
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -136,7 +136,7 @@ def get_follower_for_user_id_and_target_user_id(
return follower return follower
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -201,7 +201,7 @@ def accept_follower(user_id: int, target_user_id: int, db: Session):
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -236,7 +236,7 @@ def delete_follower(user_id: int, target_user_id: int, db: Session):
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(

View File

@@ -53,9 +53,7 @@ def fetch_and_process_activities(
) )
continue continue
garmin_logger.print_to_log( garmin_logger.print_to_log(f"User {user_id}: Processing activity {activity_id}")
f"User {user_id}: Processing activity {activity_id}"
)
# Get activity gear # Get activity gear
activity_gear = garminconnect_client.get_activity_gear(activity_id) 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") 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() garminconnect_logger = get_garminconnect_logger()
if type == "info": if type == "info":
garminconnect_logger.info(message) garminconnect_logger.info(message)
elif type == "error": elif type == "error":
garminconnect_logger.error(message) garminconnect_logger.error(message, exc_info=exc is not None)
elif type == "warning": elif type == "warning":
garminconnect_logger.warning(message) garminconnect_logger.warning(message)
elif type == "debug": 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 return gear
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -61,7 +61,7 @@ def get_gear_users_with_pagination(
except Exception as err: except Exception as err:
# Log the exception # Log the exception
core_logger.print_to_log( 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -87,7 +87,7 @@ def get_gear_user(user_id: int, db: Session) -> list[gears_schema.Gear] | None:
return gears return gears
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -124,7 +124,7 @@ def get_gear_user_by_nickname(
return gears return gears
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -155,7 +155,7 @@ def get_gear_by_type_and_user(gear_type: int, user_id: int, db: Session):
return gear return gear
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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: except Exception as err:
# Log the exception # Log the exception
core_logger.print_to_log( 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -224,7 +224,7 @@ def get_gear_by_garminconnect_id_from_user_id(
except Exception as err: except Exception as err:
# Log the exception # Log the exception
core_logger.print_to_log( 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -263,7 +263,7 @@ def create_multiple_gears(gears: list[gears_schema.Gear], user_id: int, db: Sess
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -298,7 +298,7 @@ def create_gear(gear: gears_schema.Gear, user_id: int, db: Session):
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -339,7 +339,7 @@ def edit_gear(gear_id: int, gear: gears_schema.Gear, db: Session):
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -367,7 +367,7 @@ def delete_gear(gear_id: int, db: Session):
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -397,7 +397,7 @@ def delete_all_strava_gear_for_user(user_id: int, db: Session):
# Log the exception # Log the exception
core_logger.print_to_log( 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 # 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 # Log the exception
core_logger.print_to_log( 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 # Raise an HTTPException with a 500 Internal Server Error status code

View File

@@ -2,7 +2,7 @@ from sqlalchemy import (
Column, Column,
Integer, Integer,
String, String,
TIMESTAMP, DATETIME,
ForeignKey, ForeignKey,
DECIMAL, DECIMAL,
) )
@@ -37,7 +37,7 @@ class Gear(Base):
nullable=False, nullable=False,
comment="User ID that the gear belongs to", 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( is_active = Column(
Integer, nullable=False, comment="Is gear active (0 - not active, 1 - active)" 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: with open(file, "r") as gpx_file:
gpx = gpxpy.parse(gpx_file) gpx = gpxpy.parse(gpx_file)
# Iterate over tracks in the GPX file if gpx.tracks:
for track in gpx.tracks: # Iterate over tracks in the GPX file
# Set activity name and type if available for track in gpx.tracks:
activity_name = track.name if track.name else "Workout" # Set activity name and type if available
activity_type = track.type if track.type else "Workout" activity_name = track.name if track.name else "Workout"
activity_type = track.type if track.type else "Workout"
# Iterate over segments in each track if track.segments:
for segment in track.segments: # Iterate over segments in each track
# Iterate over points in each segment for segment in track.segments:
for point in segment.points: # Iterate over points in each segment
# Extract latitude and longitude from the point for point in segment.points:
latitude, longitude = point.latitude, point.longitude # Extract latitude and longitude from the point
latitude, longitude = point.latitude, point.longitude
# Calculate distance between waypoints # Calculate distance between waypoints
if prev_latitude is not None and prev_longitude is not None: if prev_latitude is not None and prev_longitude is not None:
distance += geodesic( distance += geodesic(
(prev_latitude, prev_longitude), (latitude, longitude) (prev_latitude, prev_longitude),
).meters (latitude, longitude),
).meters
# Extract elevation, time, and location details # Extract elevation, time, and location details
elevation, time = point.elevation, point.time elevation, time = point.elevation, point.time
if elevation != 0: if elevation != 0:
is_elevation_set = True is_elevation_set = True
if first_waypoint_time is None: if first_waypoint_time is None:
first_waypoint_time = point.time first_waypoint_time = point.time
if process_one_time_fields == 0: if process_one_time_fields == 0:
# Use geocoding API to get city, town, and country based on coordinates # Use geocoding API to get city, town, and country based on coordinates
location_data = ( location_data = (
activities_utils.location_based_on_coordinates( activities_utils.location_based_on_coordinates(
latitude, longitude 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 # Calculate instance pace
if location_data: instant_pace = 0
city = location_data["city"] if instant_speed > 0:
town = location_data["town"] instant_pace = 1 / instant_speed
country = location_data["country"] 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 # Append waypoint data to respective arrays
heart_rate, cadence, power = 0, 0, 0 if latitude is not None and longitude is not None:
lat_lon_waypoints.append(
if point.extensions: {
# Iterate through each extension element "time": timestamp,
for extension in point.extensions: "lat": latitude,
if extension.tag.endswith("TrackPointExtension"): "lon": longitude,
hr_element = extension.find( }
".//{http://www.garmin.com/xmlschemas/TrackPointExtension/v1}hr"
) )
if hr_element is not None: is_lat_lon_set = True
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 activities_utils.append_if_not_none(
if heart_rate != 0: ele_waypoints, timestamp, elevation, "ele"
is_heart_rate_set = True )
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: # Update previous latitude, longitude, and last waypoint time
is_cadence_set = True prev_latitude, prev_longitude, last_waypoint_time = (
latitude,
if power != 0: longitude,
is_power_set = True time,
else: )
power = None else:
raise HTTPException(
# Calculate instant speed, pace, and update waypoint arrays status_code=status.HTTP_400_BAD_REQUEST,
instant_speed = activities_utils.calculate_instant_speed( detail="Invalid GPX file - no segments found in the GPX file",
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,
) )
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 # Calculate elevation gain/loss, pace, average speed, and average power
if ele_waypoints: 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 activity_type != 3 and activity_type != 7:
if is_lat_lon_set: if is_lat_lon_set:
timezone = tf.timezone_at( timezone = tf.timezone_at(
lat=lat_lon_waypoints[0]["lat"], lat=lat_lon_waypoints[0]["lat"],
lng=lat_lon_waypoints[0]["lon"], lng=lat_lon_waypoints[0]["lon"],
) )
# Create an Activity object with parsed data # Create an Activity object with parsed data
activity = activities_schema.Activity( activity = activities_schema.Activity(
@@ -268,12 +281,12 @@ def parse_gpx_file(file: str, user_id: int) -> dict:
average_speed=avg_speed, average_speed=avg_speed,
max_speed=max_speed, max_speed=max_speed,
average_power=round(avg_power) if avg_power else None, 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, normalized_power=round(np) if np else None,
average_hr=round(avg_hr) if avg_hr 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, 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, calories=calories,
visibility=visibility, visibility=visibility,
strava_gear_id=None, strava_gear_id=None,
@@ -302,7 +315,9 @@ def parse_gpx_file(file: str, user_id: int) -> dict:
raise http_err raise http_err
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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.schema as health_data_schema
import health_data.models as health_data_models import health_data.models as health_data_models
import health_data.utils as health_data_utils
import core.logger as core_logger 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): def get_health_data_number(user_id: int, db: Session):
try: try:
# Get the number of health_data from the database # 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: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -35,7 +65,7 @@ def get_health_data(user_id: int, db: Session):
health_data = ( health_data = (
db.query(health_data_models.HealthData) db.query(health_data_models.HealthData)
.filter(health_data_models.HealthData.user_id == user_id) .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() .all()
) )
@@ -47,7 +77,7 @@ def get_health_data(user_id: int, db: Session):
return health_data return health_data
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -63,7 +93,7 @@ def get_health_data_with_pagination(
health_data = ( health_data = (
db.query(health_data_models.HealthData) db.query(health_data_models.HealthData)
.filter(health_data_models.HealthData.user_id == user_id) .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) .offset((page_number - 1) * num_records)
.limit(num_records) .limit(num_records)
.all() .all()
@@ -78,7 +108,7 @@ def get_health_data_with_pagination(
except Exception as err: except Exception as err:
# Log the exception # Log the exception
core_logger.print_to_log( 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -87,13 +117,13 @@ def get_health_data_with_pagination(
) from err ) 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: try:
# Get the health_data from the database # Get the health_data from the database
health_data = ( health_data = (
db.query(health_data_models.HealthData) db.query(health_data_models.HealthData)
.filter( .filter(
health_data_models.HealthData.created_at == created_at, health_data_models.HealthData.date == date,
health_data_models.HealthData.user_id == user_id, health_data_models.HealthData.user_id == user_id,
) )
.first() .first()
@@ -108,7 +138,7 @@ def get_health_data_by_created_at(user_id: int, created_at: str, db: Session):
except Exception as err: except Exception as err:
# Log the exception # Log the exception
core_logger.print_to_log( 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -121,29 +151,29 @@ def create_health_data(
health_data: health_data_schema.HealthData, user_id: int, db: Session health_data: health_data_schema.HealthData, user_id: int, db: Session
): ):
try: 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 # Check if bmi is None
if health_data.bmi is None: if health_data.bmi is None:
# Get the user from the database health_data = health_data_utils.calculate_bmi(health_data, user_id, db)
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)
# Create a new health_data # Create a new health_data
db_health_data = health_data_models.HealthData( db_health_data = health_data_models.HealthData(
user_id=user_id, user_id=user_id,
created_at=func.now(), date=health_data.date,
weight=health_data.weight, weight=health_data.weight,
bmi=health_data.bmi, bmi=health_data.bmi,
body_fat=health_data.body_fat, # body_fat=health_data.body_fat,
body_water=health_data.body_water, # body_water=health_data.body_water,
bone_mass=health_data.bone_mass, # bone_mass=health_data.bone_mass,
muscle_mass=health_data.muscle_mass, # muscle_mass=health_data.muscle_mass,
physique_rating=health_data.physique_rating, # physique_rating=health_data.physique_rating,
visceral_fat=health_data.visceral_fat, # visceral_fat=health_data.visceral_fat,
metabolic_age=health_data.metabolic_age, # metabolic_age=health_data.metabolic_age,
garminconnect_body_composition_id=health_data.garminconnect_body_composition_id,
) )
# Add the health_data to the database # Add the health_data to the database
@@ -170,7 +200,63 @@ def create_health_data(
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -185,7 +271,7 @@ def create_health_weight_data(
# Create a new health_data # Create a new health_data
db_health_data = health_data_models.HealthData( db_health_data = health_data_models.HealthData(
user_id=user_id, user_id=user_id,
created_at=health_data.created_at, date=health_data.date,
weight=health_data.weight, weight=health_data.weight,
) )
@@ -212,7 +298,9 @@ def create_health_weight_data(
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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"}, headers={"WWW-Authenticate": "Bearer"},
) )
# Update the user # Update the health_data
if health_data.created_at is not None: if health_data.date is not None:
db_health_data.created_at = health_data.created_at db_health_data.date = health_data.date
if health_data.weight is not None: if health_data.weight is not None:
db_health_data.weight = health_data.weight 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() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -279,7 +369,7 @@ def delete_health_weight_data(health_data_id: int, user_id: int, db: Session):
.delete() .delete()
) )
# Check if the gear was found and deleted # Check if the health_data was found and deleted
if num_deleted == 0: if num_deleted == 0:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, 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() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(

View File

@@ -21,7 +21,7 @@ class HealthData(Base):
index=True, index=True,
comment="User ID that the health_data belongs", comment="User ID that the health_data belongs",
) )
created_at = Column( date = Column(
Date, Date,
nullable=False, nullable=False,
unique=True, unique=True,
@@ -37,41 +37,41 @@ class HealthData(Base):
nullable=True, nullable=True,
comment="Body mass index (BMI)", comment="Body mass index (BMI)",
) )
body_fat = Column( # body_fat = Column(
DECIMAL(precision=10, scale=2), # DECIMAL(precision=10, scale=2),
nullable=True, # nullable=True,
comment="Body fat percentage", # comment="Body fat percentage",
) # )
body_water = Column( # body_water = Column(
DECIMAL(precision=10, scale=2), # DECIMAL(precision=10, scale=2),
nullable=True, # nullable=True,
comment="Body hydration percentage", # comment="Body hydration percentage",
) # )
bone_mass = Column( # bone_mass = Column(
DECIMAL(precision=10, scale=2), # DECIMAL(precision=10, scale=2),
nullable=True, # nullable=True,
comment="Bone mass percentage", # comment="Bone mass percentage",
) # )
muscle_mass = Column( # muscle_mass = Column(
DECIMAL(precision=10, scale=2), # DECIMAL(precision=10, scale=2),
nullable=True, # nullable=True,
comment="Muscle mass percentage", # comment="Muscle mass percentage",
) # )
physique_rating = Column( # physique_rating = Column(
DECIMAL(precision=10, scale=2), # DECIMAL(precision=10, scale=2),
nullable=True, # nullable=True,
comment="Physique rating", # comment="Physique rating",
) # )
visceral_fat = Column( # visceral_fat = Column(
DECIMAL(precision=10, scale=2), # DECIMAL(precision=10, scale=2),
nullable=True, # nullable=True,
comment="Visceral fat rating", # comment="Visceral fat rating",
) # )
metabolic_age = Column( # metabolic_age = Column(
DECIMAL(precision=10, scale=2), # DECIMAL(precision=10, scale=2),
nullable=True, # nullable=True,
comment="Metabolic age", # comment="Metabolic age",
) # )
garminconnect_body_composition_id = Column( garminconnect_body_composition_id = Column(
String(length=45), nullable=True, comment="Garmin Connect body composition ID" 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), Depends(core_database.get_db),
], ],
): ):
health_for_date = health_data_crud.get_health_data_by_created_at( health_for_date = health_data_crud.get_health_data_by_date(
token_user_id, health_data.created_at, db token_user_id, health_data.date, db
) )
if health_for_date: if health_for_date:
if health_for_date.weight is None: if health_for_date.weight is None:

View File

@@ -1,19 +1,20 @@
from pydantic import BaseModel from pydantic import BaseModel
from datetime import date from datetime import date as datetime_date
class HealthData(BaseModel): class HealthData(BaseModel):
id: int | None = None id: int | None = None
user_id: int | None = None user_id: int | None = None
created_at: date | None = None date: datetime_date | None = None
weight: float | None = None weight: float | None = None
bmi: float | None = None bmi: float | None = None
body_fat: float | None = None #body_fat: float | None = None
body_water: float | None = None #body_water: float | None = None
bone_mass: float | None = None #bone_mass: float | None = None
muscle_mass: float | None = None #muscle_mass: float | None = None
physique_rating: float | None = None #physique_rating: float | None = None
visceral_fat: float | None = None #visceral_fat: float | None = None
metabolic_age: float | None = None #metabolic_age: float | None = None
garminconnect_body_composition_id: str | None = None
class Config: class Config:
orm_mode = True 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: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -68,7 +68,7 @@ def create_health_targets(user_id: int, db: Session):
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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.logger as garmin_logger
import garmin.activity_utils as garmin_activity_utils 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.activity_utils as strava_activity_utils
import strava.logger as strava_logger
import migrations.logger as migrations_logger import migrations.logger as migrations_logger
import strava.logger as strava_logger
from core.routes import router as api_router from core.routes import router as api_router
@@ -41,13 +42,19 @@ def startup_event():
# Create a scheduler to run background jobs # Create a scheduler to run background jobs
core_scheduler.start_scheduler() 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( core_logger.print_to_log_and_console(
"Retrieving last day activities from Garmin Connect and Strava on startup" "Retrieving last day activities from Garmin Connect and Strava on startup"
) )
garmin_activity_utils.retrieve_garminconnect_users_activities_for_days(1) garmin_activity_utils.retrieve_garminconnect_users_activities_for_days(1)
strava_activity_utils.retrieve_strava_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(): def shutdown_event():
# Log the shutdown event # Log the shutdown event

View File

@@ -23,7 +23,7 @@ def get_migrations_not_executed(db: Session):
except Exception as err: except Exception as err:
# Log the exception # Log the exception
core_logger.print_to_log_and_console(f"Error in get_migrations_not_executed. See migrations log for more information", "error") 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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 # Log the exception
core_logger.print_to_log_and_console(f"Error in set_migration_as_executed. See migrations log for more information", "error") 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(

View File

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

View File

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

View File

@@ -22,12 +22,12 @@ def get_strava_logger():
return logging.getLogger("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() garminconnect_logger = get_strava_logger()
if type == "info": if type == "info":
garminconnect_logger.info(message) garminconnect_logger.info(message)
elif type == "error": elif type == "error":
garminconnect_logger.error(message) garminconnect_logger.error(message, exc_info=exc is not None)
elif type == "warning": elif type == "warning":
garminconnect_logger.warning(message) garminconnect_logger.warning(message)
elif type == "debug": elif type == "debug":

View File

@@ -91,7 +91,7 @@ async def strava_link(
core_logger.print_to_log( core_logger.print_to_log(
f"Error in strava_link. For more information check Strava log.", "error" 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(

View File

@@ -30,7 +30,7 @@ def get_user_integrations_by_user_id(user_id: int, db: Session):
except Exception as err: except Exception as err:
# Log the exception # Log the exception
core_logger.print_to_log( 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -43,7 +43,9 @@ def get_user_integrations_by_strava_state(strava_state: str, db: Session):
try: try:
user_integrations = ( user_integrations = (
db.query(user_integrations_models.UserIntegrations) 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() .first()
) )
@@ -56,7 +58,7 @@ def get_user_integrations_by_strava_state(strava_state: str, db: Session):
except Exception as err: except Exception as err:
# Log the exception # Log the exception
core_logger.print_to_log( 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -86,7 +88,9 @@ def create_user_integrations(user_id: int, db: Session):
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -118,7 +122,9 @@ def link_strava_account(
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -153,7 +159,9 @@ def unlink_strava_account(user_id: int, db: Session):
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -185,7 +193,9 @@ def set_user_strava_state(user_id: int, state: str, db: Session):
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -209,7 +219,9 @@ def set_user_strava_sync_gear(user_id: int, strava_sync_gear: bool, db: Session)
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -247,7 +259,9 @@ def link_garminconnect_account(
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -274,7 +288,7 @@ def set_user_garminconnect_sync_gear(
# Log the exception # Log the exception
core_logger.print_to_log( 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 # 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 # Log the exception
core_logger.print_to_log( 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 # 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 return user
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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() return db.query(users_models.User).all()
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -55,7 +55,7 @@ def get_users_number(db: Session):
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -115,7 +115,7 @@ def get_user_if_contains_username(username: str, db: Session):
return users return users
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -139,7 +139,7 @@ def get_user_by_username(username: str, db: Session):
return user return user
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -163,7 +163,7 @@ def get_user_by_id(user_id: int, db: Session):
return user return user
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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 return user_id
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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 return user_db.photo_path
except Exception as err: except Exception as err:
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -257,7 +257,7 @@ def create_user(user: users_schema.UserCreate, db: Session):
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -304,7 +304,7 @@ def edit_user(user_id: int, user: users_schema.User, db: Session):
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -328,7 +328,7 @@ def edit_user_password(user_id: int, password: str, db: Session):
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -355,7 +355,7 @@ def edit_user_photo_path(user_id: int, photo_path: str, db: Session):
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -382,7 +382,7 @@ def delete_user_photo(user_id: int, db: Session):
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( raise HTTPException(
@@ -413,7 +413,7 @@ def delete_user(user_id: int, db: Session):
db.rollback() db.rollback()
# Log the exception # 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 an HTTPException with a 500 Internal Server Error status code
raise HTTPException( 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) return users_crud.edit_user_photo_path(user_id, file_path_to_save, db)
except Exception as err: except Exception as err:
# Log the exception # 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 # Remove the file after processing
if os.path.exists(file_path_to_save): if os.path.exists(file_path_to_save):

View File

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

View File

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

View File

@@ -93,7 +93,7 @@
</li> </li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<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") }} {{ $t("activitySummary.buttonDeleteActivity") }}
</a> </a>
</li> </li>

View File

@@ -6,7 +6,8 @@
<div class="fw-bold"> <div class="fw-bold">
{{ data.weight }} {{ data.weight }}
</div> </div>
{{ formatDate(data.created_at) }} {{ data.date }}
{{ formatDate(data.date) }}
</div> </div>
</div> </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> </li>
</ul> </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 --> <!-- modal garmin connect auth -->
<GarminConnectLoginModalComponent /> <GarminConnectLoginModalComponent />
<!-- modal retrieve strava activities by days --> <!-- modal retrieve Garmin Connect activities by days -->
<div class="modal fade" id="retrieveStravaActivitiesByDaysModal" tabindex="-1" aria-labelledby="retrieveStravaActivitiesByDaysModalLabel" aria-hidden="true"> <ModalComponentNumberInput modalId="retrieveGarminConnectActivitiesByDaysModal" :title="t('settingsIntegrationsZone.modalRetrieveActivitiesByDaysTitle')" :numberFieldLabel="`${t('settingsIntegrationsZone.modalRetrieveActivitiesByDaysLabel')}`" :actionButtonType="`success`" :actionButtonText="t('settingsIntegrationsZone.modalRetrieveButton')" @numberToEmitAction="submitRetrieveGarminConnectActivities"/>
<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 unlink Garmin Connect -->
<ModalComponent modalId="unlinkGarminConnectModal" :title="t('settingsIntegrationsZone.modalUnlinkGarminConnectTitle')" :body="`${t('settingsIntegrationsZone.modalUnlinkGarminConnectBody')}`" :actionButtonType="`danger`" :actionButtonText="t('settingsIntegrationsZone.modalUnlinkGarminConnectTitle')" @submitAction="buttonGarminConnectUnlink"/> <ModalComponent modalId="unlinkGarminConnectModal" :title="t('settingsIntegrationsZone.modalUnlinkGarminConnectTitle')" :body="`${t('settingsIntegrationsZone.modalUnlinkGarminConnectBody')}`" :actionButtonType="`danger`" :actionButtonText="t('settingsIntegrationsZone.modalUnlinkGarminConnectTitle')" @submitAction="buttonGarminConnectUnlink"/>
</div> </div>
</template> </template>
@@ -163,6 +125,7 @@ import { activities } from "@/services/activitiesService";
import { garminConnect } from "@/services/garminConnectService"; import { garminConnect } from "@/services/garminConnectService";
// Import the components // Import the components
import ModalComponent from "@/components/Modals/ModalComponent.vue"; import ModalComponent from "@/components/Modals/ModalComponent.vue";
import ModalComponentNumberInput from "@/components/Modals/ModalComponentNumberInput.vue";
import GarminConnectLoginModalComponent from "./SettingsIntegrations/GarminConnectLoginModalComponent.vue"; import GarminConnectLoginModalComponent from "./SettingsIntegrations/GarminConnectLoginModalComponent.vue";
//import Modal from 'bootstrap/js/dist/modal'; //import Modal from 'bootstrap/js/dist/modal';
@@ -170,15 +133,12 @@ import GarminConnectLoginModalComponent from "./SettingsIntegrations/GarminConne
export default { export default {
components: { components: {
ModalComponent, ModalComponent,
ModalComponentNumberInput,
GarminConnectLoginModalComponent, GarminConnectLoginModalComponent,
}, },
setup() { setup() {
const authStore = useAuthStore(); const authStore = useAuthStore();
const { locale, t } = useI18n(); const { locale, t } = useI18n();
const daysToRetrieveStrava = ref(7);
const daysToRetrieveGarmin = ref(7);
const garminConnectUsername = ref("");
const garminConnectPassword = ref("");
async function submitConnectStrava() { async function submitConnectStrava() {
const array = new Uint8Array(16); const array = new Uint8Array(16);
@@ -199,9 +159,9 @@ export default {
} }
} }
async function submitRetrieveStravaActivities() { async function submitRetrieveStravaActivities(daysToRetrieveStrava) {
try { try {
await strava.getStravaActivitiesLastDays(daysToRetrieveStrava.value); await strava.getStravaActivitiesLastDays(daysToRetrieveStrava);
// Show the loading alert. // Show the loading alert.
push.info( push.info(
@@ -268,9 +228,9 @@ export default {
} }
} }
async function submitRetrieveGarminConnectActivities() { async function submitRetrieveGarminConnectActivities(daysToRetrieveGarmin) {
try { try {
await garminConnect.getGarminConnectActivitiesLastDays(daysToRetrieveGarmin.value); await garminConnect.getGarminConnectActivitiesLastDays(daysToRetrieveGarmin);
// Show the loading alert. // Show the loading alert.
push.info( push.info(
@@ -328,14 +288,10 @@ export default {
t, t,
submitConnectStrava, submitConnectStrava,
submitRetrieveStravaActivities, submitRetrieveStravaActivities,
daysToRetrieveStrava,
submitRetrieveStravaGear, submitRetrieveStravaGear,
buttonStravaUnlink, buttonStravaUnlink,
submitBulkImport, submitBulkImport,
garminConnectUsername,
garminConnectPassword,
submitRetrieveGarminConnectActivities, submitRetrieveGarminConnectActivities,
daysToRetrieveGarmin,
submitRetrieveGarminConnectGear, submitRetrieveGarminConnectGear,
buttonGarminConnectUnlink, buttonGarminConnectUnlink,
}; };

View File

@@ -278,7 +278,7 @@ export default {
gearId.value = activity.value.gear_id; 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); gearsByType.value = await gears.getGearFromType(2);
} else { } else {
if (activity.value.activity_type === 4 || activity.value.activity_type === 5 || activity.value.activity_type === 6 || activity.value.activity_type === 7) { 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 ++; userNumberOfActivities.value ++;
} catch (error) { } catch (error) {
// Set the error message // Set the error message
notification.reject(`${t('generalItems.errorFetchingInfo')} - ${error}`) notification.reject(`${error}`)
} }
} }
}; };