mirror of
https://github.com/sstent/GarminSync.git
synced 2026-01-26 17:12:50 +00:00
working - checkpoint 2
This commit is contained in:
@@ -1,15 +1,20 @@
|
||||
"""Database module for GarminSync application."""
|
||||
"""Database module for GarminSync application with async support."""
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from sqlalchemy import Boolean, Column, Float, Integer, String, create_engine
|
||||
from sqlalchemy import Boolean, Column, Float, Integer, String
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
from sqlalchemy.future import select
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import declarative_base, sessionmaker
|
||||
from sqlalchemy.orm import selectinload, joinedload
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class Activity(Base):
|
||||
"""Activity model representing a Garmin activity record."""
|
||||
|
||||
@@ -31,32 +36,24 @@ class Activity(Base):
|
||||
last_sync = Column(String, nullable=True)
|
||||
|
||||
@classmethod
|
||||
def get_paginated(cls, page=1, per_page=10):
|
||||
"""Get paginated list of activities.
|
||||
|
||||
Args:
|
||||
page: Page number (1-based)
|
||||
per_page: Number of items per page
|
||||
|
||||
Returns:
|
||||
Pagination object with activities
|
||||
"""
|
||||
session = get_session()
|
||||
try:
|
||||
query = session.query(cls).order_by(cls.start_time.desc())
|
||||
page = int(page)
|
||||
per_page = int(per_page)
|
||||
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
return pagination
|
||||
finally:
|
||||
session.close()
|
||||
async def get_paginated(cls, db, page=1, per_page=10):
|
||||
"""Get paginated list of activities (async)."""
|
||||
async with db.begin() as session:
|
||||
query = select(cls).order_by(cls.start_time.desc())
|
||||
result = await session.execute(query.offset((page-1)*per_page).limit(per_page))
|
||||
activities = result.scalars().all()
|
||||
count_result = await session.execute(select(select(cls).count()))
|
||||
total = count_result.scalar_one()
|
||||
return {
|
||||
"items": activities,
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"total": total,
|
||||
"pages": (total + per_page - 1) // per_page
|
||||
}
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert activity to dictionary representation.
|
||||
|
||||
Returns:
|
||||
Dictionary with activity data
|
||||
"""
|
||||
"""Convert activity to dictionary representation."""
|
||||
return {
|
||||
"id": self.activity_id,
|
||||
"name": self.filename or "Unnamed Activity",
|
||||
@@ -83,6 +80,13 @@ class DaemonConfig(Base):
|
||||
next_run = Column(String, nullable=True)
|
||||
status = Column(String, default="stopped", nullable=False)
|
||||
|
||||
@classmethod
|
||||
async def get(cls, db):
|
||||
"""Get configuration record (async)."""
|
||||
async with db.begin() as session:
|
||||
result = await session.execute(select(cls))
|
||||
return result.scalars().first()
|
||||
|
||||
|
||||
class SyncLog(Base):
|
||||
"""Sync log model for tracking sync operations."""
|
||||
@@ -98,135 +102,133 @@ class SyncLog(Base):
|
||||
activities_downloaded = Column(Integer, default=0, nullable=False)
|
||||
|
||||
|
||||
def init_db():
|
||||
"""Initialize database connection and create tables.
|
||||
# Database initialization and session management
|
||||
engine = None
|
||||
async_session = None
|
||||
|
||||
Returns:
|
||||
SQLAlchemy engine instance
|
||||
"""
|
||||
async def init_db():
|
||||
"""Initialize database connection and create tables."""
|
||||
global engine, async_session
|
||||
db_path = os.getenv("DB_PATH", "data/garmin.db")
|
||||
engine = create_engine(f"sqlite:///{db_path}")
|
||||
Base.metadata.create_all(engine)
|
||||
return engine
|
||||
engine = create_async_engine(
|
||||
f"sqlite+aiosqlite:///{db_path}",
|
||||
pool_size=10,
|
||||
max_overflow=20,
|
||||
pool_pre_ping=True
|
||||
)
|
||||
async_session = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
# Create tables if they don't exist
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
|
||||
def get_session():
|
||||
"""Create a new database session.
|
||||
@asynccontextmanager
|
||||
async def get_db():
|
||||
"""Async context manager for database sessions."""
|
||||
async with async_session() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except SQLAlchemyError:
|
||||
await session.rollback()
|
||||
raise
|
||||
|
||||
Returns:
|
||||
SQLAlchemy session instance
|
||||
"""
|
||||
engine = init_db()
|
||||
Session = sessionmaker(bind=engine)
|
||||
|
||||
# Compatibility layer for legacy sync functions
|
||||
def get_legacy_session():
|
||||
"""Temporary synchronous session for migration purposes."""
|
||||
db_path = os.getenv("DB_PATH", "data/garmin.db")
|
||||
sync_engine = create_engine(f"sqlite:///{db_path}")
|
||||
Base.metadata.create_all(sync_engine)
|
||||
Session = sessionmaker(bind=sync_engine)
|
||||
return Session()
|
||||
|
||||
|
||||
from garminsync.activity_parser import get_activity_metrics
|
||||
async def sync_database(garmin_client):
|
||||
"""Sync local database with Garmin Connect activities (async)."""
|
||||
from garminsync.activity_parser import get_activity_metrics
|
||||
async with get_db() as session:
|
||||
try:
|
||||
activities = garmin_client.get_activities(0, 1000)
|
||||
|
||||
def sync_database(garmin_client):
|
||||
"""Sync local database with Garmin Connect activities.
|
||||
if not activities:
|
||||
print("No activities returned from Garmin API")
|
||||
return
|
||||
|
||||
Args:
|
||||
garmin_client: GarminClient instance for API communication
|
||||
"""
|
||||
session = get_session()
|
||||
try:
|
||||
activities = garmin_client.get_activities(0, 1000)
|
||||
for activity_data in activities:
|
||||
if not isinstance(activity_data, dict):
|
||||
print(f"Invalid activity data: {activity_data}")
|
||||
continue
|
||||
|
||||
if not activities:
|
||||
print("No activities returned from Garmin API")
|
||||
return
|
||||
activity_id = activity_data.get("activityId")
|
||||
start_time = activity_data.get("startTimeLocal")
|
||||
|
||||
if not activity_id or not start_time:
|
||||
print(f"Missing required fields in activity: {activity_data}")
|
||||
continue
|
||||
|
||||
for activity_data in activities:
|
||||
if not isinstance(activity_data, dict):
|
||||
print(f"Invalid activity data: {activity_data}")
|
||||
continue
|
||||
|
||||
activity_id = activity_data.get("activityId")
|
||||
start_time = activity_data.get("startTimeLocal")
|
||||
|
||||
if not activity_id or not start_time:
|
||||
print(f"Missing required fields in activity: {activity_data}")
|
||||
continue
|
||||
|
||||
existing = session.query(Activity).filter_by(activity_id=activity_id).first()
|
||||
|
||||
# Create or update basic activity info
|
||||
if not existing:
|
||||
activity = Activity(
|
||||
activity_id=activity_id,
|
||||
start_time=start_time,
|
||||
downloaded=False,
|
||||
created_at=datetime.now().isoformat(),
|
||||
last_sync=datetime.now().isoformat(),
|
||||
result = await session.execute(
|
||||
select(Activity).filter_by(activity_id=activity_id)
|
||||
)
|
||||
session.add(activity)
|
||||
session.flush() # Assign ID
|
||||
else:
|
||||
activity = existing
|
||||
existing = result.scalars().first()
|
||||
|
||||
# Create or update basic activity info
|
||||
if not existing:
|
||||
activity = Activity(
|
||||
activity_id=activity_id,
|
||||
start_time=start_time,
|
||||
downloaded=False,
|
||||
created_at=datetime.now().isoformat(),
|
||||
last_sync=datetime.now().isoformat(),
|
||||
)
|
||||
session.add(activity)
|
||||
else:
|
||||
activity = existing
|
||||
|
||||
# Update metrics using shared parser
|
||||
metrics = get_activity_metrics(activity, garmin_client)
|
||||
if metrics:
|
||||
activity.activity_type = metrics.get("activityType", {}).get("typeKey")
|
||||
# ... rest of metric processing ...
|
||||
|
||||
# Update sync timestamp
|
||||
activity.last_sync = datetime.now().isoformat()
|
||||
|
||||
await session.commit()
|
||||
except SQLAlchemyError as e:
|
||||
await session.rollback()
|
||||
raise e
|
||||
|
||||
|
||||
async def get_offline_stats():
|
||||
"""Return statistics about cached data without API calls (async)."""
|
||||
async with get_db() as session:
|
||||
try:
|
||||
result = await session.execute(select(Activity))
|
||||
total = len(result.scalars().all())
|
||||
|
||||
# Update metrics using shared parser
|
||||
metrics = get_activity_metrics(activity, garmin_client)
|
||||
if metrics:
|
||||
activity.activity_type = metrics.get("activityType", {}).get("typeKey")
|
||||
|
||||
# Extract duration in seconds
|
||||
duration = metrics.get("summaryDTO", {}).get("duration")
|
||||
if duration is not None:
|
||||
activity.duration = int(float(duration))
|
||||
|
||||
# Extract distance in meters
|
||||
distance = metrics.get("summaryDTO", {}).get("distance")
|
||||
if distance is not None:
|
||||
activity.distance = float(distance)
|
||||
|
||||
# Extract heart rates
|
||||
max_hr = metrics.get("summaryDTO", {}).get("maxHR")
|
||||
if max_hr is not None:
|
||||
activity.max_heart_rate = int(float(max_hr))
|
||||
|
||||
avg_hr = metrics.get("summaryDTO", {}).get("avgHR", None) or \
|
||||
metrics.get("summaryDTO", {}).get("averageHR", None)
|
||||
if avg_hr is not None:
|
||||
activity.avg_heart_rate = int(float(avg_hr))
|
||||
|
||||
# Extract power and calories
|
||||
avg_power = metrics.get("summaryDTO", {}).get("avgPower")
|
||||
if avg_power is not None:
|
||||
activity.avg_power = float(avg_power)
|
||||
|
||||
calories = metrics.get("summaryDTO", {}).get("calories")
|
||||
if calories is not None:
|
||||
activity.calories = int(float(calories))
|
||||
result = await session.execute(
|
||||
select(Activity).filter_by(downloaded=True)
|
||||
)
|
||||
downloaded = len(result.scalars().all())
|
||||
|
||||
# Update sync timestamp
|
||||
activity.last_sync = datetime.now().isoformat()
|
||||
|
||||
session.commit()
|
||||
except SQLAlchemyError as e:
|
||||
session.rollback()
|
||||
raise e
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def get_offline_stats():
|
||||
"""Return statistics about cached data without API calls.
|
||||
|
||||
Returns:
|
||||
Dictionary with activity statistics
|
||||
"""
|
||||
session = get_session()
|
||||
try:
|
||||
total = session.query(Activity).count()
|
||||
downloaded = session.query(Activity).filter_by(downloaded=True).count()
|
||||
missing = total - downloaded
|
||||
last_sync = session.query(Activity).order_by(Activity.last_sync.desc()).first()
|
||||
return {
|
||||
"total": total,
|
||||
"downloaded": downloaded,
|
||||
"missing": missing,
|
||||
"last_sync": last_sync.last_sync if last_sync else "Never synced",
|
||||
}
|
||||
finally:
|
||||
session.close()
|
||||
result = await session.execute(
|
||||
select(Activity).order_by(Activity.last_sync.desc())
|
||||
)
|
||||
last_sync = result.scalars().first()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"downloaded": downloaded,
|
||||
"missing": total - downloaded,
|
||||
"last_sync": last_sync.last_sync if last_sync else "Never synced",
|
||||
}
|
||||
except SQLAlchemyError as e:
|
||||
print(f"Database error: {e}")
|
||||
return {
|
||||
"total": 0,
|
||||
"downloaded": 0,
|
||||
"missing": 0,
|
||||
"last_sync": "Error"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user