added garmin functional tests

This commit is contained in:
2025-09-28 07:09:36 -07:00
parent 88fb6a601a
commit dcc7f4e6fa
5 changed files with 4931 additions and 109 deletions

View File

@@ -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."""