mirror of
https://github.com/sstent/AICyclingCoach.git
synced 2026-01-26 17:12:30 +00:00
added garmin functional tests
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
import garth
|
||||
from garth.exc import GarthException
|
||||
import asyncio
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime, timedelta
|
||||
@@ -17,71 +18,61 @@ class GarminService:
|
||||
self.db = db
|
||||
self.username = os.getenv("GARMIN_USERNAME")
|
||||
self.password = os.getenv("GARMIN_PASSWORD")
|
||||
logger.debug(f"GarminService initialized with username: {self.username is not None}, password: {self.password is not None}")
|
||||
self.client: Optional[garth.Client] = None
|
||||
self.session_dir = Path("data/sessions")
|
||||
|
||||
# Ensure session directory exists
|
||||
self.session_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
async def authenticate(self) -> bool:
|
||||
"""Authenticate with Garmin Connect and persist session."""
|
||||
if not self.client:
|
||||
self.client = garth.Client()
|
||||
|
||||
try:
|
||||
# Try to load existing session
|
||||
await asyncio.to_thread(self.client.load, self.session_dir)
|
||||
await asyncio.to_thread(garth.resume, self.session_dir)
|
||||
logger.info("Loaded existing Garmin session")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load existing Garmin session: {e}. Attempting fresh authentication.")
|
||||
# Fresh authentication required
|
||||
except (FileNotFoundError, GarthException):
|
||||
logger.warning("No existing session found. Attempting fresh authentication.")
|
||||
if not self.username or not self.password:
|
||||
logger.error("Garmin username or password not set in environment variables.")
|
||||
raise GarminAuthError("Garmin username or password not configured.")
|
||||
try:
|
||||
await asyncio.to_thread(self.client.login, self.username, self.password)
|
||||
await asyncio.to_thread(self.client.save, self.session_dir)
|
||||
await asyncio.to_thread(garth.login, self.username, self.password)
|
||||
await asyncio.to_thread(garth.save, self.session_dir)
|
||||
logger.info("Successfully authenticated with Garmin Connect")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Garmin authentication failed: {str(e)}")
|
||||
raise GarminAuthError(f"Authentication failed: {str(e)}")
|
||||
return True
|
||||
|
||||
async def get_activities(self, limit: int = 10, start_date: datetime = None) -> List[Dict[str, Any]]:
|
||||
"""Fetch recent activities from Garmin Connect."""
|
||||
if not self.client:
|
||||
await self.authenticate()
|
||||
await self.authenticate()
|
||||
|
||||
if not start_date:
|
||||
start_date = datetime.now() - timedelta(days=7)
|
||||
|
||||
try:
|
||||
activities = await asyncio.to_thread(self.client.get_activities, limit=limit, start=start_date)
|
||||
activities = await asyncio.to_thread(
|
||||
garth.connectapi,
|
||||
"/activity-service/activity/activities",
|
||||
params={"limit": limit, "start": start_date.strftime("%Y-%m-%d")},
|
||||
)
|
||||
logger.info(f"Fetched {len(activities)} activities from Garmin")
|
||||
return activities
|
||||
return activities or []
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch activities: {str(e)}")
|
||||
raise GarminAPIError(f"Failed to fetch activities: {str(e)}")
|
||||
|
||||
async def get_activity_details(self, activity_id: str) -> Dict[str, Any]:
|
||||
"""Get detailed activity data including metrics."""
|
||||
if not self.client:
|
||||
await self.authenticate()
|
||||
await self.authenticate()
|
||||
|
||||
try:
|
||||
details = await asyncio.to_thread(self.client.get_activity, activity_id)
|
||||
details = await asyncio.to_thread(
|
||||
garth.connectapi, f"/activity-service/activity/{activity_id}"
|
||||
)
|
||||
logger.info(f"Fetched details for activity {activity_id}")
|
||||
return details
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch activity details for {activity_id}: {str(e)}")
|
||||
raise GarminAPIError(f"Failed to fetch activity details: {str(e)}")
|
||||
|
||||
def is_authenticated(self) -> bool:
|
||||
"""Check if we have a valid authenticated session."""
|
||||
return self.client is not None
|
||||
|
||||
|
||||
class GarminAuthError(Exception):
|
||||
"""Raised when Garmin authentication fails."""
|
||||
|
||||
@@ -2,8 +2,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, desc
|
||||
from backend.app.services.garmin import GarminService, GarminAPIError, GarminAuthError
|
||||
from backend.app.models.workout import Workout
|
||||
from backend.app.models.garmin_sync_log import GarminSyncLog
|
||||
from backend.app.models.garmin_sync_log import GarminSyncLog
|
||||
from backend.app.models.garmin_sync_log import GarminSyncLog, GarminSyncStatus
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
@@ -63,7 +62,7 @@ class WorkoutSyncService:
|
||||
synced_count += 1
|
||||
|
||||
# Update sync log
|
||||
sync_log.status = "success"
|
||||
sync_log.status = GarminSyncStatus.COMPLETED
|
||||
sync_log.activities_synced = synced_count
|
||||
sync_log.last_sync_time = datetime.now()
|
||||
|
||||
@@ -72,19 +71,19 @@ class WorkoutSyncService:
|
||||
return synced_count
|
||||
|
||||
except GarminAuthError as e:
|
||||
sync_log.status = "auth_error"
|
||||
sync_log.status = GarminSyncStatus.AUTH_FAILED
|
||||
sync_log.error_message = str(e)
|
||||
await self.db.commit()
|
||||
logger.error(f"Garmin authentication failed: {str(e)}")
|
||||
raise
|
||||
except GarminAPIError as e:
|
||||
sync_log.status = "api_error"
|
||||
sync_log.status = GarminSyncStatus.FAILED
|
||||
sync_log.error_message = str(e)
|
||||
await self.db.commit()
|
||||
logger.error(f"Garmin API error during sync: {str(e)}")
|
||||
raise
|
||||
except Exception as e:
|
||||
sync_log.status = "error"
|
||||
sync_log.status = GarminSyncStatus.FAILED
|
||||
sync_log.error_message = str(e)
|
||||
await self.db.commit()
|
||||
logger.error(f"Unexpected error during sync: {str(e)}")
|
||||
@@ -104,7 +103,7 @@ class WorkoutSyncService:
|
||||
result = await self.db.execute(
|
||||
select(Workout).where(Workout.garmin_activity_id == garmin_activity_id)
|
||||
)
|
||||
return (await result.scalar_one_or_none()) is not None
|
||||
return result.scalar_one_or_none() is not None # Remove the await here
|
||||
|
||||
async def parse_activity_data(self, activity: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Parse Garmin activity data into workout model format."""
|
||||
|
||||
Reference in New Issue
Block a user