before claude fix #1

This commit is contained in:
2025-12-23 06:09:34 -08:00
parent c505fb69a6
commit a23fa1b30d
83 changed files with 5682 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
import fitbit
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
import logging
from ..utils.helpers import setup_logger
logger = setup_logger(__name__)
class FitbitClient:
def __init__(self, client_id: str, client_secret: str, access_token: str = None, refresh_token: str = None):
self.client_id = client_id
self.client_secret = client_secret
self.access_token = access_token
self.refresh_token = refresh_token
self.fitbit_client = None
if access_token and refresh_token:
self.fitbit_client = fitbit.Fitbit(
client_id=client_id,
client_secret=client_secret,
access_token=access_token,
refresh_token=refresh_token,
# Callback for token refresh if needed
)
def get_authorization_url(self, redirect_uri: str) -> str:
"""Generate authorization URL for Fitbit OAuth flow."""
# This would generate the Fitbit authorization URL
auth_url = f"https://www.fitbit.com/oauth2/authorize?response_type=code&client_id={self.client_id}&redirect_uri={redirect_uri}&scope=weight"
logger.info(f"Generated Fitbit authorization URL: {auth_url}")
return auth_url
def exchange_code_for_token(self, code: str, redirect_uri: str) -> Dict[str, str]:
"""Exchange authorization code for access and refresh tokens."""
# This would exchange the authorization code for tokens
# Implementation would use the Fitbit library to exchange the code
logger.info(f"Exchanging authorization code for tokens")
# Return mock response for now
return {
"access_token": "mock_access_token",
"refresh_token": "mock_refresh_token",
"expires_at": (datetime.now() + timedelta(hours=1)).isoformat()
}
def get_weight_logs(self, start_date: str, end_date: str = None) -> List[Dict[str, Any]]:
"""Fetch weight logs from Fitbit API."""
if not self.fitbit_client:
raise Exception("Fitbit client not authenticated")
if not end_date:
end_date = datetime.now().strftime('%Y-%m-%d')
try:
# Get weight logs from Fitbit
weight_logs = self.fitbit_client.get_bodyweight(
base_date=start_date,
end_date=end_date
)
logger.info(f"Fetched {len(weight_logs.get('weight', []))} weight entries from Fitbit")
return weight_logs.get('weight', [])
except Exception as e:
logger.error(f"Error fetching weight logs from Fitbit: {str(e)}")
raise e
def refresh_access_token(self) -> Dict[str, str]:
"""Refresh the Fitbit access token."""
# Implementation for token refresh
logger.info("Refreshing Fitbit access token")
# Return mock response for now
return {
"access_token": "new_mock_access_token",
"expires_at": (datetime.now() + timedelta(hours=1)).isoformat()
}

View File

@@ -0,0 +1,217 @@
import garth
import garminconnect
from datetime import datetime, timedelta
from uuid import uuid4
import json
import traceback
from src.utils.helpers import setup_logger
from src.models.api_token import APIToken
from src.services.postgresql_manager import PostgreSQLManager
from src.utils.config import config
logger = setup_logger(__name__)
class AuthMixin:
def login(self):
"""Login to Garmin Connect with proper token handling."""
logger.info(f"Starting login process for Garmin user: {self.username}")
try:
logger.debug(f"Attempting garth login for user: {self.username}")
garth.login(self.username, self.password, return_on_mfa=True)
logger.debug(f"Successfully completed garth authentication for: {self.username}")
logger.debug(f"Creating Garmin Connect client for user: {self.username}")
self.garmin_client = garminconnect.Garmin(self.username, self.password)
self.garmin_client.garth = garth.client
logger.debug(f"Successfully created Garmin Connect client for user: {self.username}")
self.is_connected = True
logger.info(f"Setting is_connected to True for user: {self.username}")
self.save_tokens()
logger.info(f"Successfully logged in to Garmin Connect as {self.username}")
except Exception as e:
logger.error(f"Error logging in to Garmin Connect: {str(e)}")
logger.error(f"Exception type: {type(e).__name__}")
error_str = str(e).lower()
if "mfa" in error_str or "2fa" in error_str or "unauthorized" in error_str:
logger.warning(f"Multi-factor authentication likely required for {self.username}")
logger.debug(f"Detected MFA indicator in error message: {error_str}")
raise Exception("MFA Required: Please provide verification code")
logger.error(f"Full traceback: {traceback.format_exc()}")
raise e
def save_tokens(self):
"""Save garth tokens to be used later."""
logger.info(f"Starting token saving process for user: {self.username}")
try:
db_manager = PostgreSQLManager(config.DATABASE_URL)
with db_manager.get_db_session() as session:
token_record = session.query(APIToken).filter(APIToken.token_type == 'garmin').first()
if not token_record:
token_record = APIToken(token_type='garmin')
session.add(token_record)
oauth1_token = getattr(garth.client, 'oauth1_token', None)
oauth2_token = getattr(garth.client, 'oauth2_token', None)
if oauth1_token:
try:
token_dict = oauth1_token.__dict__ if hasattr(oauth1_token, '__dict__') else str(oauth1_token)
token_record.garth_oauth1_token = json.dumps(token_dict, default=str)
except Exception as e:
logger.warning(f"Could not serialize OAuth1 token for user {self.username}: {e}")
if oauth2_token:
try:
token_dict = oauth2_token.__dict__ if hasattr(oauth2_token, '__dict__') else str(oauth2_token)
token_record.garth_oauth2_token = json.dumps(token_dict, default=str)
except Exception as e:
logger.warning(f"Could not serialize OAuth2 token for user {self.username}: {e}")
session.commit()
logger.info(f"Garmin tokens saved successfully for user: {self.username}")
except Exception as e:
logger.error(f"Error saving garth tokens for user {self.username}: {str(e)}")
raise e
def load_tokens(self):
"""Load garth tokens to resume a session."""
logger.info(f"Starting token loading process for user: {self.username}")
try:
db_manager = PostgreSQLManager(config.DATABASE_URL)
with db_manager.get_db_session() as session:
try:
token_record = session.query(APIToken).filter(APIToken.token_type == 'garmin').first()
except Exception as db_error:
logger.info(f"No existing Garmin tokens found for user {self.username} or table doesn't exist: {db_error}")
return False
if not token_record or (not token_record.garth_oauth1_token and not token_record.garth_oauth2_token):
logger.info(f"No Garmin token record found in database for user: {self.username}")
return False
if token_record.garth_oauth1_token:
try:
oauth1_data = json.loads(token_record.garth_oauth1_token)
setattr(garth.client, 'oauth1_token', oauth1_data)
logger.info(f"Successfully restored OAuth1 token for user: {self.username}")
except Exception as e:
logger.warning(f"Could not restore OAuth1 token for user {self.username}: {e}")
if token_record.garth_oauth2_token:
try:
oauth2_data = json.loads(token_record.garth_oauth2_token)
setattr(garth.client, 'oauth2_token', oauth2_data)
logger.info(f"Successfully restored OAuth2 token for user: {self.username}")
self.garmin_client = garminconnect.Garmin(self.username, self.password)
self.garmin_client.garth = garth.client
self.is_connected = True
logger.debug(f"Successfully created Garmin Connect client for user {self.username} with restored session")
return True
except Exception as e:
logger.warning(f"Could not restore OAuth2 token for user {self.username}: {e}")
return True
except Exception as e:
logger.error(f"Error loading garth tokens for user {self.username}: {str(e)}")
return False
def initiate_mfa(self, username: str = None):
"""Initiate the MFA process and return session data."""
user_identifier = username if username else self.username
logger.info(f"Initiating MFA process for Garmin user: {user_identifier}")
mfa_session_id = str(uuid4())
db_manager = PostgreSQLManager(config.DATABASE_URL)
with db_manager.get_db_session() as session:
token_record = session.query(APIToken).filter(APIToken.token_type == 'garmin').first()
if not token_record:
token_record = APIToken(token_type='garmin')
session.add(token_record)
token_record.mfa_session_id = mfa_session_id
resume_data = {
'username': user_identifier,
'password': self.password,
'is_china': self.is_china
}
token_record.mfa_resume_data = json.dumps(resume_data)
token_record.mfa_expires_at = datetime.now() + timedelta(minutes=10)
session.commit()
logger.info(f"MFA session initiated for user: {user_identifier}, session ID: {mfa_session_id}")
return mfa_session_id
def handle_mfa(self, verification_code: str, session_id: str = None):
"""Handle the MFA process by completing authentication with the verification code."""
logger.info(f"Starting MFA completion process with session ID: {session_id}")
db_manager = PostgreSQLManager(config.DATABASE_URL)
with db_manager.get_db_session() as session:
token_record = session.query(APIToken).filter(
APIToken.token_type == 'garmin',
APIToken.mfa_session_id == session_id
).first()
if not token_record:
raise Exception("No pending MFA authentication for this session.")
if token_record.mfa_expires_at and datetime.now() > token_record.mfa_expires_at:
self.cleanup_mfa_session(token_record, session)
raise Exception("MFA verification code has expired.")
try:
resume_data = json.loads(token_record.mfa_resume_data)
self.username = resume_data.get('username')
self.password = resume_data.get('password')
if resume_data.get('is_china', False):
garth.configure(domain="garmin.cn")
try:
garth.client.mfa_submit(verification_code)
except AttributeError:
garth.login(self.username, self.password, verification_code)
self.garmin_client = garminconnect.Garmin(self.username, self.password)
self.garmin_client.garth = garth.client
try:
profile = self.garmin_client.get_full_name()
logger.info(f"Verified authentication for user: {profile}")
except Exception as verify_error:
logger.warning(f"Could not verify authentication for user {self.username}: {verify_error}")
self.is_connected = True
self.save_tokens()
self.cleanup_mfa_session(token_record, session)
logger.info(f"Successfully completed MFA authentication for {self.username}")
return True
except Exception as e:
logger.error(f"Error during MFA completion for user {self.username}: {e}")
self.cleanup_mfa_session(token_record, session)
raise e
def cleanup_mfa_session(self, token_record, session):
"""Clear out MFA session data from the token record."""
token_record.mfa_session_id = None
token_record.mfa_resume_data = None
token_record.mfa_expires_at = None
session.commit()
logger.debug("MFA session data cleaned up.")

View File

@@ -0,0 +1,39 @@
import garth
from src.utils.helpers import setup_logger
from .auth import AuthMixin
from .data import DataMixin
logger = setup_logger(__name__)
class GarminClient(AuthMixin, DataMixin):
def __init__(self, username: str = None, password: str = None, is_china: bool = False):
self.username = username
self.password = password
self.is_china = is_china
self.garmin_client = None
self.is_connected = False
logger.debug(f"Initializing GarminClient for user: {username}, is_china: {is_china}")
if is_china:
logger.debug("Configuring garth for China domain")
garth.configure(domain="garmin.cn")
if username and password:
logger.info(f"Attempting to authenticate Garmin user: {username}")
if not self.load_tokens():
logger.info("No valid tokens found, attempting fresh login")
self.login()
else:
logger.info("Successfully loaded existing tokens, skipping fresh login")
else:
logger.debug("No username/password provided during initialization")
def check_connection(self) -> bool:
"""Check if the connection to Garmin is still valid."""
try:
profile = self.garmin_client.get_full_name() if self.garmin_client else None
return profile is not None
except:
self.is_connected = False
return False

View File

@@ -0,0 +1,139 @@
from datetime import datetime
from typing import List, Dict, Any, Optional
from src.utils.helpers import setup_logger
logger = setup_logger(__name__)
class DataMixin:
def upload_weight(self, weight: float, unit: str = 'kg', timestamp: datetime = None) -> bool:
"""Upload weight entry to Garmin Connect."""
if not self.is_connected:
raise Exception("Not connected to Garmin Connect")
try:
if not timestamp:
timestamp = datetime.now()
try:
result = self.garmin_client.add_body_composition(
timestamp=timestamp,
weight=weight
)
except Exception:
try:
result = self.garmin_client.add_body_composition(
timestamp=timestamp.isoformat(),
weight=weight
)
except Exception:
result = self.garmin_client.add_body_composition(
timestamp=timestamp.strftime('%Y-%m-%d'),
weight=weight
)
logger.info(f"Successfully uploaded weight: {weight} {unit} at {timestamp}")
return result is not None
except Exception as e:
logger.error(f"Error uploading weight to Garmin: {str(e)}")
if "401" in str(e) or "unauthorized" in str(e).lower():
logger.error("Authentication failed - need to re-authenticate")
raise Exception("Authentication expired, needs re-authentication")
raise e
def get_activities(self, start_date: str, end_date: str = None, limit: int = 100) -> List[Dict[str, Any]]:
"""Fetch activity list from Garmin Connect."""
if not self.is_connected:
raise Exception("Not connected to Garmin Connect")
try:
if not end_date:
end_date = datetime.now().strftime('%Y-%m-%d')
activities = self.garmin_client.get_activities(start_date, end_date)
logger.info(f"Fetched {len(activities)} activities from Garmin")
return activities
except Exception as e:
logger.error(f"Error fetching activities from Garmin: {str(e)}")
raise e
def download_activity(self, activity_id: str, file_type: str = 'tcx') -> Optional[bytes]:
"""Download activity file from Garmin Connect and return its content."""
if not self.is_connected:
raise Exception("Not connected to Garmin Connect")
try:
file_content = self.garmin_client.get_activity_details(activity_id)
logger.info(f"Downloaded activity {activity_id} as {file_type} format")
return file_content if file_content else b""
except Exception as e:
logger.error(f"Error downloading activity {activity_id} from Garmin: {str(e)}")
raise e
def get_heart_rates(self, start_date: str, end_date: str = None) -> Dict[str, Any]:
"""Fetch heart rate data from Garmin Connect."""
if not self.is_connected:
raise Exception("Not connected to Garmin Connect")
try:
if not end_date:
end_date = datetime.now().strftime('%Y-%m-%d')
heart_rates = self.garmin_client.get_heart_rates(start_date, end_date)
logger.info(f"Fetched heart rate data from Garmin for {start_date} to {end_date}")
return heart_rates
except Exception as e:
logger.error(f"Error fetching heart rate data from Garmin: {str(e)}")
raise e
def get_sleep_data(self, start_date: str, end_date: str = None) -> Dict[str, Any]:
"""Fetch sleep data from Garmin Connect."""
if not self.is_connected:
raise Exception("Not connected to Garmin Connect")
try:
if not end_date:
end_date = datetime.now().strftime('%Y-%m-%d')
sleep_data = self.garmin_client.get_sleep_data(start_date, end_date)
logger.info(f"Fetched sleep data from Garmin for {start_date} to {end_date}")
return sleep_data
except Exception as e:
logger.error(f"Error fetching sleep data from Garmin: {str(e)}")
raise e
def get_steps_data(self, start_date: str, end_date: str = None) -> Dict[str, Any]:
"""Fetch steps data from Garmin Connect."""
if not self.is_connected:
raise Exception("Not connected to Garmin Connect")
try:
if not end_date:
end_date = datetime.now().strftime('%Y-%m-%d')
steps_data = self.garmin_client.get_steps_data(start_date, end_date)
logger.info(f"Fetched steps data from Garmin for {start_date} to {end_date}")
return steps_data
except Exception as e:
logger.error(f"Error fetching steps data from Garmin: {str(e)}")
raise e
def get_all_metrics(self, start_date: str, end_date: str = None) -> Dict[str, Any]:
"""Fetch all available metrics from Garmin Connect."""
if not self.is_connected:
raise Exception("Not connected to Garmin Connect")
try:
if not end_date:
end_date = datetime.now().strftime('%Y-%m-%d')
metrics = {
'heart_rates': self.get_heart_rates(start_date, end_date),
'sleep_data': self.get_sleep_data(start_date, end_date),
'steps_data': self.get_steps_data(start_date, end_date),
}
logger.info(f"Fetched all metrics from Garmin for {start_date} to {end_date}")
return metrics
except Exception as e:
logger.error(f"Error fetching all metrics from Garmin: {str(e)}")
raise e

View File

@@ -0,0 +1,47 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import QueuePool
import os
from contextlib import contextmanager
# Create a base class for declarative models
Base = declarative_base()
class PostgreSQLManager:
def __init__(self, database_url: str = None):
if database_url is None:
database_url = os.getenv("DATABASE_URL", "postgresql://postgres:password@localhost:5432/fitbit_garmin_sync")
self.engine = create_engine(
database_url,
poolclass=QueuePool,
pool_size=10,
max_overflow=20,
pool_pre_ping=True,
pool_recycle=300,
)
self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)
def init_db(self):
"""Initialize the database by creating all tables."""
# Import all models to ensure they're registered with the Base
from ..models.config import Configuration
from ..models.api_token import APIToken
from ..models.auth_status import AuthStatus
from ..models.weight_record import WeightRecord
from ..models.activity import Activity
from ..models.health_metric import HealthMetric
from ..models.sync_log import SyncLog
# Create all tables
Base.metadata.create_all(bind=self.engine)
@contextmanager
def get_db_session(self):
"""Provide a database session."""
db = self.SessionLocal()
try:
yield db
finally:
db.close()

View File

@@ -0,0 +1,322 @@
from ..models.weight_record import WeightRecord
from ..models.sync_log import SyncLog
from ..services.fitbit_client import FitbitClient
from ..services.garmin.client import GarminClient
from sqlalchemy.orm import Session
from datetime import datetime, timedelta
from typing import Dict
import logging
from ..utils.helpers import setup_logger
logger = setup_logger(__name__)
class SyncApp:
def __init__(self, db_session: Session, fitbit_client: FitbitClient, garmin_client: GarminClient):
self.db_session = db_session
self.fitbit_client = fitbit_client
self.garmin_client = garmin_client
def sync_weight_data(self, start_date: str = None, end_date: str = None) -> Dict[str, int]:
"""Sync weight data from Fitbit to Garmin."""
if not start_date:
# Default to 1 year back
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y-%m-%d')
if not end_date:
end_date = datetime.now().strftime('%Y-%m-%d')
# Create a sync log entry
sync_log = SyncLog(
operation="weight_sync",
status="started",
start_time=datetime.now(),
records_processed=0,
records_failed=0
)
self.db_session.add(sync_log)
self.db_session.commit()
try:
# Fetch unsynced weight records from Fitbit
fitbit_weights = self.fitbit_client.get_weight_logs(start_date, end_date)
# Track processing results
processed_count = 0
failed_count = 0
for weight_entry in fitbit_weights:
try:
# Check if this weight entry already exists in our DB (prevents duplicates)
fitbit_id = weight_entry.get('logId', str(weight_entry.get('date', '') + str(weight_entry.get('weight', 0))))
existing_record = self.db_session.query(WeightRecord).filter(
WeightRecord.fitbit_id == fitbit_id
).first()
if existing_record and existing_record.sync_status == 'synced':
# Skip if already synced
continue
# Create or update weight record
if not existing_record:
weight_record = WeightRecord(
fitbit_id=fitbit_id,
weight=weight_entry.get('weight'),
unit=weight_entry.get('unit', 'kg'),
date=datetime.fromisoformat(weight_entry.get('date')) if isinstance(weight_entry.get('date'), str) else weight_entry.get('date'),
timestamp=datetime.fromisoformat(weight_entry.get('date')) if isinstance(weight_entry.get('date'), str) else weight_entry.get('date'),
sync_status='unsynced'
)
self.db_session.add(weight_record)
self.db_session.flush() # Get the ID
else:
weight_record = existing_record
# Upload to Garmin if not already synced
if weight_record.sync_status != 'synced':
# Upload weight to Garmin
success = self.garmin_client.upload_weight(
weight=weight_record.weight,
unit=weight_record.unit,
timestamp=weight_record.timestamp
)
if success:
weight_record.sync_status = 'synced'
weight_record.garmin_id = "garmin_" + fitbit_id # Placeholder for Garmin ID
else:
weight_record.sync_status = 'failed'
failed_count += 1
processed_count += 1
except Exception as e:
logger.error(f"Error processing weight entry: {str(e)}")
failed_count += 1
# Update sync log with results
sync_log.status = "completed" if failed_count == 0 else "completed_with_errors"
sync_log.end_time = datetime.now()
sync_log.records_processed = processed_count
sync_log.records_failed = failed_count
self.db_session.commit()
logger.info(f"Weight sync completed: {processed_count} processed, {failed_count} failed")
return {
"processed": processed_count,
"failed": failed_count
}
except Exception as e:
logger.error(f"Error during weight sync: {str(e)}")
# Update sync log with error status
sync_log.status = "failed"
sync_log.end_time = datetime.now()
sync_log.message = str(e)
self.db_session.commit()
raise e
def sync_activities(self, days_back: int = 30) -> Dict[str, int]:
"""Sync activity data from Garmin to local storage."""
start_date = (datetime.now() - timedelta(days=days_back)).strftime('%Y-%m-%d')
end_date = datetime.now().strftime('%Y-%m-%d')
# Create a sync log entry
sync_log = SyncLog(
operation="activity_archive",
status="started",
start_time=datetime.now(),
records_processed=0,
records_failed=0
)
self.db_session.add(sync_log)
self.db_session.commit()
try:
# Fetch activities from Garmin
garmin_activities = self.garmin_client.get_activities(start_date, end_date)
processed_count = 0
failed_count = 0
from ..models.activity import Activity
for activity in garmin_activities:
try:
activity_id = str(activity.get('activityId', ''))
existing_activity = self.db_session.query(Activity).filter(
Activity.garmin_activity_id == activity_id
).first()
if existing_activity and existing_activity.download_status == 'downloaded':
# Skip if already downloaded
continue
# Create or update activity record
if not existing_activity:
activity_record = Activity(
garmin_activity_id=activity_id,
activity_name=activity.get('activityName', ''),
activity_type=activity.get('activityType', ''),
start_time=datetime.fromisoformat(activity.get('startTimeLocal', '')) if activity.get('startTimeLocal') else None,
duration=activity.get('duration', 0),
download_status='pending'
)
self.db_session.add(activity_record)
self.db_session.flush()
else:
activity_record = existing_activity
# Download activity file if not already downloaded
if activity_record.download_status != 'downloaded':
# Download in various formats
file_formats = ['tcx', 'gpx', 'fit']
downloaded_successfully = False
for fmt in file_formats:
try:
# Get file content from Garmin client
file_content = self.garmin_client.download_activity(activity_id, file_type=fmt)
if file_content:
# Store file content directly in the database
activity_record.file_content = file_content
activity_record.file_type = fmt
activity_record.download_status = 'downloaded'
activity_record.downloaded_at = datetime.now()
downloaded_successfully = True
break
except Exception as e:
logger.warning(f"Could not download activity {activity_id} in {fmt} format: {str(e)}")
continue
if not downloaded_successfully:
activity_record.download_status = 'failed'
failed_count += 1
processed_count += 1
except Exception as e:
logger.error(f"Error processing activity {activity.get('activityId', '')}: {str(e)}")
failed_count += 1
# Update sync log with results
sync_log.status = "completed" if failed_count == 0 else "completed_with_errors"
sync_log.end_time = datetime.now()
sync_log.records_processed = processed_count
sync_log.records_failed = failed_count
self.db_session.commit()
logger.info(f"Activity sync completed: {processed_count} processed, {failed_count} failed")
return {
"processed": processed_count,
"failed": failed_count
}
except Exception as e:
logger.error(f"Error during activity sync: {str(e)}")
# Update sync log with error status
sync_log.status = "failed"
sync_log.end_time = datetime.now()
sync_log.message = str(e)
self.db_session.commit()
raise e
def sync_health_metrics(self, start_date: str = None, end_date: str = None) -> Dict[str, int]:
"""Sync health metrics from Garmin to local database."""
if not start_date:
# Default to 1 year back
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y-%m-%d')
if not end_date:
end_date = datetime.now().strftime('%Y-%m-%d')
# Create a sync log entry
sync_log = SyncLog(
operation="metrics_download",
status="started",
start_time=datetime.now(),
records_processed=0,
records_failed=0
)
self.db_session.add(sync_log)
self.db_session.commit()
try:
# Fetch all metrics from Garmin
all_metrics = self.garmin_client.get_all_metrics(start_date, end_date)
processed_count = 0
failed_count = 0
from ..models.health_metric import HealthMetric
# Process heart rate data
heart_rates = all_metrics.get('heart_rates', {})
if 'heartRateValues' in heart_rates:
for hr_data in heart_rates['heartRateValues']:
try:
timestamp = datetime.fromisoformat(hr_data[0]) if isinstance(hr_data[0], str) else datetime.fromtimestamp(hr_data[0]/1000)
metric = HealthMetric(
metric_type='heart_rate',
metric_value=hr_data[1],
unit='bpm',
timestamp=timestamp,
date=timestamp.date(),
source='garmin',
detailed_data=None
)
self.db_session.add(metric)
processed_count += 1
except Exception as e:
logger.error(f"Error processing heart rate data: {str(e)}")
failed_count += 1
# Process other metrics similarly...
# For brevity, I'll show just one more example
sleep_data = all_metrics.get('sleep_data', {})
sleep_levels = sleep_data.get('sleep', [])
for sleep_entry in sleep_levels:
try:
metric = HealthMetric(
metric_type='sleep',
metric_value=sleep_entry.get('duration', 0),
unit='minutes',
timestamp=datetime.now(), # Actual timestamp would come from data
date=datetime.now().date(), # Actual date would come from data
source='garmin',
detailed_data=sleep_entry
)
self.db_session.add(metric)
processed_count += 1
except Exception as e:
logger.error(f"Error processing sleep data: {str(e)}")
failed_count += 1
# Update sync log with results
sync_log.status = "completed" if failed_count == 0 else "completed_with_errors"
sync_log.end_time = datetime.now()
sync_log.records_processed = processed_count
sync_log.records_failed = failed_count
self.db_session.commit()
logger.info(f"Health metrics sync completed: {processed_count} processed, {failed_count} failed")
return {
"processed": processed_count,
"failed": failed_count
}
except Exception as e:
logger.error(f"Error during health metrics sync: {str(e)}")
# Update sync log with error status
sync_log.status = "failed"
sync_log.end_time = datetime.now()
sync_log.message = str(e)
self.db_session.commit()
raise e