diff --git a/Dockerfile b/Dockerfile index 142e2d8..2ed7f6f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,13 +15,18 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ # Copy and install Python dependencies COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt # Copy application code -COPY . . +COPY garminsync/ ./garminsync/ + +# Create data directory +RUN mkdir -p /app/data # Set environment variables from .env file ENV ENV_FILE=/app/.env +ENV DATA_DIR=/app/data # Set the entrypoint to run the CLI -ENTRYPOINT ["python", "-m", "garminsync.cli"] +ENTRYPOINT ["python", "-m", "garminsync.cli"] \ No newline at end of file diff --git a/garminsync/cli.py b/garminsync/cli.py index bcb87b2..a518bdf 100644 --- a/garminsync/cli.py +++ b/garminsync/cli.py @@ -1,66 +1,72 @@ +import os import typer +from typing_extensions import Annotated from .config import load_config # Initialize environment variables load_config() -app = typer.Typer() +app = typer.Typer(help="GarminSync - Download Garmin Connect activities", rich_markup_mode=None) -@app.command(name="list") +@app.command("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") + all_activities: Annotated[bool, typer.Option("--all", help="List all activities")] = False, + missing: Annotated[bool, typer.Option("--missing", help="List missing activities")] = False, + downloaded: Annotated[bool, typer.Option("--downloaded", help="List downloaded activities")] = False ): - """ - List activities based on specified filters - """ + """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]): + if not any([all_activities, 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}") + try: + 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_activities: + 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}") + + except Exception as e: + typer.echo(f"Error: {str(e)}") + raise typer.Exit(code=1) + finally: + if 'session' in locals(): + session.close() -@app.command() +@app.command("download") def download( - missing: bool = typer.Option(False, "--missing", help="Download missing activities") + missing: Annotated[bool, typer.Option("--missing", help="Download missing activities")] = False ): - """ - Download activities based on specified filters - """ + """Download activities based on specified filters""" from tqdm import tqdm from pathlib import Path from .database import get_session, Activity @@ -71,50 +77,61 @@ def download( 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") + try: + 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") + + except Exception as e: + typer.echo(f"Error: {str(e)}") + raise typer.Exit(code=1) + finally: + if 'session' in locals(): + session.close() + +def main(): + app() if __name__ == "__main__": - app() + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7903913..dbae3f1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ -typer>=0.12.3,<0.13.0 +typer==0.9.0 +click==8.1.7 python-dotenv==1.0.0 garminconnect==0.2.28 sqlalchemy==2.0.23 -tqdm==4.66.1 +tqdm==4.66.1 \ No newline at end of file