mirror of
https://github.com/sstent/AICyclingCoach.git
synced 2026-02-14 19:32:25 +00:00
sync - still working on the TUI
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from fastapi import FastAPI, Depends, Request, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from .database import get_db
|
||||
@@ -47,8 +48,11 @@ logger.addHandler(console_handler)
|
||||
|
||||
# Configure rotating file handler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
# Create logs directory relative to the project root
|
||||
log_dir = Path(__file__).parent.parent.parent / "logs"
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
file_handler = RotatingFileHandler(
|
||||
filename="/app/logs/app.log",
|
||||
filename=log_dir / "backend.log",
|
||||
maxBytes=10*1024*1024, # 10 MB
|
||||
backupCount=5,
|
||||
encoding='utf-8'
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
from sqlalchemy import Column, Integer, DateTime, String, Text
|
||||
from sqlalchemy import Column, Integer, DateTime, String, Text, Enum
|
||||
from .base import BaseModel
|
||||
import enum
|
||||
|
||||
|
||||
class GarminSyncStatus(str, enum.Enum):
|
||||
PENDING = "pending"
|
||||
IN_PROGRESS = "in_progress"
|
||||
COMPLETED = "completed"
|
||||
AUTH_FAILED = "auth_failed"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class GarminSyncLog(BaseModel):
|
||||
@@ -8,5 +17,5 @@ class GarminSyncLog(BaseModel):
|
||||
|
||||
last_sync_time = Column(DateTime)
|
||||
activities_synced = Column(Integer, default=0)
|
||||
status = Column(String(20)) # success, error, in_progress
|
||||
status = Column(Enum(GarminSyncStatus), default=GarminSyncStatus.PENDING)
|
||||
error_message = Column(Text)
|
||||
@@ -162,7 +162,7 @@ class AIService:
|
||||
timeout=30.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
data = await response.json()
|
||||
return data["choices"][0]["message"]["content"]
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
import garth
|
||||
import asyncio
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -10,14 +13,16 @@ logger = logging.getLogger(__name__)
|
||||
class GarminService:
|
||||
"""Service for interacting with Garmin Connect API."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, db: Optional[AsyncSession] = None):
|
||||
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 = "/app/data/sessions"
|
||||
self.session_dir = Path("data/sessions")
|
||||
|
||||
# Ensure session directory exists
|
||||
os.makedirs(self.session_dir, exist_ok=True)
|
||||
self.session_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
async def authenticate(self) -> bool:
|
||||
"""Authenticate with Garmin Connect and persist session."""
|
||||
@@ -26,14 +31,18 @@ class GarminService:
|
||||
|
||||
try:
|
||||
# Try to load existing session
|
||||
self.client.load(self.session_dir)
|
||||
await asyncio.to_thread(self.client.load, self.session_dir)
|
||||
logger.info("Loaded existing Garmin session")
|
||||
return True
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load existing Garmin session: {e}. Attempting fresh authentication.")
|
||||
# Fresh authentication required
|
||||
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 self.client.login(self.username, self.password)
|
||||
self.client.save(self.session_dir)
|
||||
await asyncio.to_thread(self.client.login, self.username, self.password)
|
||||
await asyncio.to_thread(self.client.save, self.session_dir)
|
||||
logger.info("Successfully authenticated with Garmin Connect")
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -49,7 +58,7 @@ class GarminService:
|
||||
start_date = datetime.now() - timedelta(days=7)
|
||||
|
||||
try:
|
||||
activities = self.client.get_activities(limit=limit, start=start_date)
|
||||
activities = await asyncio.to_thread(self.client.get_activities, limit=limit, start=start_date)
|
||||
logger.info(f"Fetched {len(activities)} activities from Garmin")
|
||||
return activities
|
||||
except Exception as e:
|
||||
@@ -62,7 +71,7 @@ class GarminService:
|
||||
await self.authenticate()
|
||||
|
||||
try:
|
||||
details = self.client.get_activity(activity_id)
|
||||
details = await asyncio.to_thread(self.client.get_activity, activity_id)
|
||||
logger.info(f"Fetched details for activity {activity_id}")
|
||||
return details
|
||||
except Exception as e:
|
||||
|
||||
@@ -62,7 +62,7 @@ class PlanEvolutionService:
|
||||
)
|
||||
.order_by(Plan.version)
|
||||
)
|
||||
return result.scalars().all()
|
||||
return (await result.scalars()).all()
|
||||
|
||||
async def get_current_active_plan(self) -> Plan:
|
||||
"""Get the most recent active plan."""
|
||||
|
||||
@@ -22,7 +22,7 @@ class PromptManager:
|
||||
query = query.where(Prompt.model == model)
|
||||
|
||||
result = await self.db.execute(query.order_by(Prompt.version.desc()))
|
||||
prompt = result.scalar_one_or_none()
|
||||
prompt = await result.scalar_one_or_none()
|
||||
return prompt.prompt_text if prompt else None
|
||||
|
||||
async def create_prompt_version(
|
||||
|
||||
@@ -97,14 +97,14 @@ class WorkoutSyncService:
|
||||
.order_by(desc(GarminSyncLog.created_at))
|
||||
.limit(1)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
return await result.scalar_one_or_none()
|
||||
|
||||
async def activity_exists(self, garmin_activity_id: str) -> bool:
|
||||
"""Check if activity already exists in database."""
|
||||
result = await self.db.execute(
|
||||
select(Workout).where(Workout.garmin_activity_id == garmin_activity_id)
|
||||
)
|
||||
return result.scalar_one_or_none() is not None
|
||||
return (await result.scalar_one_or_none()) is not None
|
||||
|
||||
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