python v1

This commit is contained in:
2025-08-08 12:34:33 -07:00
parent 0c13b92e5a
commit 760868c98c
6 changed files with 243 additions and 0 deletions

0
garminsync/__init__.py Normal file
View File

120
garminsync/cli.py Normal file
View File

@@ -0,0 +1,120 @@
import typer
from .config import load_config
# Initialize environment variables
load_config()
app = typer.Typer()
@app.command(name="list")
def list_activities(
all: bool = typer.Option(False, "--all", help="List all activities"),
missing: bool = typer.Option(False, "--missing", help="List missing activities"),
downloaded: bool = typer.Option(False, "--downloaded", help="List downloaded activities")
):
"""
List activities based on specified filters
"""
from tqdm import tqdm
from .database import get_session, Activity
from .garmin import GarminClient
# Validate input
if not any([all, missing, downloaded]):
typer.echo("Error: Please specify at least one filter option (--all, --missing, --downloaded)")
raise typer.Exit(code=1)
client = GarminClient()
session = get_session()
# Sync database with latest activities
typer.echo("Syncing activities from Garmin Connect...")
from .database import sync_database
sync_database(client)
# Build query based on filters
query = session.query(Activity)
if all:
pass # Return all activities
elif missing:
query = query.filter_by(downloaded=False)
elif downloaded:
query = query.filter_by(downloaded=True)
# Execute query and display results
activities = query.all()
if not activities:
typer.echo("No activities found matching your criteria")
return
# Display results with progress bar
typer.echo(f"Found {len(activities)} activities:")
for activity in tqdm(activities, desc="Listing activities"):
status = "Downloaded" if activity.downloaded else "Missing"
typer.echo(f"- ID: {activity.activity_id}, Start: {activity.start_time}, Status: {status}")
@app.command()
def download(
missing: bool = typer.Option(False, "--missing", help="Download missing activities")
):
"""
Download activities based on specified filters
"""
from tqdm import tqdm
from pathlib import Path
from .database import get_session, Activity
from .garmin import GarminClient
# Validate input
if not missing:
typer.echo("Error: Currently only --missing downloads are supported")
raise typer.Exit(code=1)
client = GarminClient()
session = get_session()
# Sync database with latest activities
typer.echo("Syncing activities from Garmin Connect...")
from .database import sync_database
sync_database(client)
# Get missing activities
activities = session.query(Activity).filter_by(downloaded=False).all()
if not activities:
typer.echo("No missing activities found")
return
# Create data directory if it doesn't exist
data_dir = Path(os.getenv("DATA_DIR", "data"))
data_dir.mkdir(parents=True, exist_ok=True)
# Download activities with progress bar
typer.echo(f"Downloading {len(activities)} missing activities...")
for activity in tqdm(activities, desc="Downloading"):
try:
# Download FIT data
fit_data = client.download_activity_fit(activity.activity_id)
# Create filename-safe timestamp
timestamp = activity.start_time.replace(":", "-").replace(" ", "_")
filename = f"activity_{activity.activity_id}_{timestamp}.fit"
filepath = data_dir / filename
# Save file
with open(filepath, "wb") as f:
f.write(fit_data)
# Update database
activity.filename = str(filepath)
activity.downloaded = True
session.commit()
except Exception as e:
typer.echo(f"Error downloading activity {activity.activity_id}: {str(e)}")
session.rollback()
typer.echo("Download completed successfully")
if __name__ == "__main__":
app()

15
garminsync/config.py Normal file
View File

@@ -0,0 +1,15 @@
from dotenv import load_dotenv
import os
def load_config():
"""Load environment variables from .env file"""
load_dotenv()
class Config:
GARMIN_EMAIL = os.getenv("GARMIN_EMAIL")
GARMIN_PASSWORD = os.getenv("GARMIN_PASSWORD")
@classmethod
def validate(cls):
if not cls.GARMIN_EMAIL or not cls.GARMIN_PASSWORD:
raise ValueError("Missing GARMIN_EMAIL or GARMIN_PASSWORD in environment")

61
garminsync/database.py Normal file
View File

@@ -0,0 +1,61 @@
import os
from sqlalchemy import create_engine, Column, Integer, String, Boolean
from sqlalchemy.orm import declarative_base, sessionmaker
from sqlalchemy.exc import SQLAlchemyError
Base = declarative_base()
class Activity(Base):
__tablename__ = 'activities'
activity_id = Column(Integer, primary_key=True)
start_time = Column(String, nullable=False)
filename = Column(String, unique=True, nullable=True)
downloaded = Column(Boolean, default=False, nullable=False)
def init_db():
"""Initialize database connection and create tables"""
db_path = os.path.join(os.getenv("DATA_DIR", "data"), "garmin.db")
engine = create_engine(f"sqlite:///{db_path}")
Base.metadata.create_all(engine)
return engine
def get_session():
"""Create a new database session"""
engine = init_db()
Session = sessionmaker(bind=engine)
return Session()
def sync_database(garmin_client):
"""Sync local database with Garmin Connect activities"""
session = get_session()
try:
# Fetch activities from Garmin Connect
activities = garmin_client.get_activities(0, 1000)
# Process activities and update database
for activity in activities:
activity_id = activity["activityId"]
start_time = activity["startTimeLocal"]
# Check if activity exists in database
existing = session.query(Activity).filter_by(activity_id=activity_id).first()
if not existing:
new_activity = Activity(
activity_id=activity_id,
start_time=start_time,
downloaded=False
)
session.add(new_activity)
session.commit()
except SQLAlchemyError as e:
session.rollback()
raise e
finally:
session.close()
# Example usage:
# from .garmin import GarminClient
# client = GarminClient()
# sync_database(client)

42
garminsync/garmin.py Normal file
View File

@@ -0,0 +1,42 @@
import os
import time
from garminconnect import Garmin
class GarminClient:
def __init__(self):
self.client = None
def authenticate(self):
"""Authenticate using credentials from environment variables"""
email = os.getenv("GARMIN_EMAIL")
password = os.getenv("GARMIN_PASSWORD")
if not email or not password:
raise ValueError("Garmin credentials not found in environment variables")
self.client = Garmin(email, password)
self.client.login()
return self.client
def get_activities(self, start=0, limit=10):
"""Get list of activities with rate limiting"""
if not self.client:
self.authenticate()
activities = self.client.get_activities(start, limit)
time.sleep(2) # Rate limiting
return activities
def download_activity_fit(self, activity_id):
"""Download .fit file for a specific activity"""
if not self.client:
self.authenticate()
fit_data = self.client.download_activity(activity_id, dl_fmt='fit')
time.sleep(2) # Rate limiting
return fit_data
# Example usage:
# client = GarminClient()
# activities = client.get_activities(0, 10)
# fit_data = client.download_activity_fit(12345)

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
typer>=0.12.3,<0.13.0
python-dotenv==1.0.0
garminconnect==0.2.28
sqlalchemy==2.0.23
tqdm==4.66.1