Adding latest changes

This commit is contained in:
Moritz Graf
2026-01-02 17:58:42 +01:00
parent 408cfa87d1
commit 3b836d945e
12 changed files with 245 additions and 91 deletions
@@ -2,7 +2,7 @@
import json
import os
import sys
from typing import Dict, Any
from typing import Any, Dict
# Ensure we can import from src
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
@@ -10,6 +10,7 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from common.env_manager import EnvManager
from garmin.workout_manager import WorkoutManager
def load_sample_workout() -> Dict[str, Any]:
"""Create a minimal valid workout for testing."""
return {
+38 -1
View File
@@ -53,6 +53,26 @@ class GarminSync:
pass
return activities
def get_last_sync_date(self) -> Any:
"""Get the date of the latest stored activity."""
activities = self.load_local_activities()
if not activities:
return None
latest_date = None
for act in activities:
start_str = act.get("startTimeLocal")
if start_str:
try:
# Parse "YYYY-MM-DD HH:MM:SS" -> date
d = datetime.strptime(start_str.split(" ")[0], "%Y-%m-%d").date()
if latest_date is None or d > latest_date:
latest_date = d
except ValueError:
continue
return latest_date
def sync_smart(self) -> int:
"""Sync only new activities since the last local one."""
try:
@@ -271,6 +291,22 @@ class GarminSync:
label = k.replace("_", " ").title()
breakdown_list.append({"label": label, "count": v})
# VO2 Max Logic
sorted_acts = sorted(
activities,
key=lambda x: x.get("startTimeLocal", ""),
reverse=True
)
vo2_max = None
for act in sorted_acts:
if "vo2MaxValue" in act:
vo2_max = act["vo2MaxValue"]
break
if "vo2MaxCyclingValue" in act:
vo2_max = act["vo2MaxCyclingValue"]
break
return {
"summary": {
"total_hours": round(current_period["hours"], 1),
@@ -278,5 +314,6 @@ class GarminSync:
"period_label": "Last 7 Days"
},
"breakdown": breakdown_list,
"strength_sessions": strength_count
"strength_sessions": strength_count,
"vo2_max": vo2_max
}
+7 -1
View File
@@ -107,10 +107,16 @@ async def get_settings_status():
env.load_service_env("withings")
env.load_service_env("gemini")
# Get last sync date
sync = GarminSync(None, storage_dir=get_storage_path("garmin"))
last_date = sync.get_last_sync_date()
return {
"garmin": {
"configured": bool(os.getenv("GARMIN_EMAIL") and os.getenv("GARMIN_PASSWORD")),
"email": os.getenv("GARMIN_EMAIL")
"email": os.getenv("GARMIN_EMAIL"),
"last_synced_workout": last_date.strftime("%d.%m.%Y") if last_date else None
},
"withings": {
"configured": bool(os.getenv("WITHINGS_CLIENT_ID") and os.getenv("WITHINGS_CLIENT_SECRET"))
+10 -1
View File
@@ -72,10 +72,19 @@ def test_settings_status():
assert response.status_code == 200
assert "garmin" in response.json()
def test_settings_status_v2():
def test_settings_status_v2(mock_sync):
mock_sync.return_value.get_last_sync_date.return_value = None
response = client.get("/settings/status")
assert response.status_code == 200
assert "garmin" in response.json()
assert "last_synced_workout" in response.json()["garmin"]
def test_settings_status_with_date(mock_sync):
from datetime import date
mock_sync.return_value.get_last_sync_date.return_value = date(2023, 12, 12)
response = client.get("/settings/status")
assert response.status_code == 200
assert response.json()["garmin"]["last_synced_workout"] == "12.12.2023"
def test_update_settings_garmin():
with patch("main.env") as mock_env:
+75
View File
@@ -0,0 +1,75 @@
import json
import os
from datetime import date
from unittest.mock import MagicMock, patch
import pytest
from garmin.sync import GarminSync
@pytest.fixture
def mock_client():
return MagicMock()
@pytest.fixture
def temp_storage(tmp_path):
return str(tmp_path / "data")
def test_get_last_sync_date(mock_client, temp_storage):
os.makedirs(temp_storage, exist_ok=True)
sync = GarminSync(mock_client, storage_dir=temp_storage)
# 1. No files
assert sync.get_last_sync_date() is None
# 2. Add files
today = date(2025, 12, 12)
with open(os.path.join(temp_storage, "activity_1.json"), "w") as f:
json.dump({"activityId": 1, "startTimeLocal": "2025-12-10 10:00:00"}, f)
with open(os.path.join(temp_storage, "activity_2.json"), "w") as f:
json.dump({"activityId": 2, "startTimeLocal": "2025-12-12 10:00:00"}, f)
# 3. Invalid Date
with open(os.path.join(temp_storage, "activity_bad.json"), "w") as f:
json.dump({"activityId": 3, "startTimeLocal": "invalid-date"}, f)
assert sync.get_last_sync_date() == today
def test_sync_smart_no_local(mock_client, temp_storage):
os.makedirs(temp_storage, exist_ok=True)
sync = GarminSync(mock_client, storage_dir=temp_storage)
with patch.object(sync, 'sync_activities', return_value=10) as mock_sync_act:
count = sync.sync_smart()
assert count == 10
mock_sync_act.assert_called_with(days=365)
def test_sync_smart_incremental(mock_client, temp_storage):
os.makedirs(temp_storage, exist_ok=True)
sync = GarminSync(mock_client, storage_dir=temp_storage)
# Latest is 2 days ago
mock_today = date(2025, 1, 5)
last_sync = date(2025, 1, 3)
with open(os.path.join(temp_storage, "activity_1.json"), "w") as f:
json.dump({"activityId": 1, "startTimeLocal": f"{last_sync} 10:00:00"}, f)
with patch("garmin.sync.date") as mock_date_cls:
mock_date_cls.today.return_value = mock_today
# Need to allow date constructor usage inside sync.py if used
# But Sync.py imports date, datetime separately.
# When we patch 'garmin.sync.date', we replace the class 'date' imported in that module
# So date.today() is mocked.
# But side effect: date(2025,1,5) might fail if we mocked the constructor?
# Usually today is a classmethod.
# Simpler: sync_smart calculates days = (today - latest_date).days
# If today=5th, latest=3rd. diff = 2.
with patch.object(sync, 'sync_activities', return_value=5) as mock_sync_act:
sync.sync_smart()
mock_sync_act.assert_called_with(days=2)