From 9ed2f3720d309c607a689ee5dfb23baa13f8c8f0 Mon Sep 17 00:00:00 2001 From: sstent Date: Fri, 8 Aug 2025 14:12:39 -0700 Subject: [PATCH] python v2 - added feartures 1 and 2 - no errors --- garminsync/utils.py | 85 +++++++++++++++++++ plan.md | 203 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 garminsync/utils.py create mode 100644 plan.md diff --git a/garminsync/utils.py b/garminsync/utils.py new file mode 100644 index 0000000..553c848 --- /dev/null +++ b/garminsync/utils.py @@ -0,0 +1,85 @@ +import logging +import sys +from datetime import datetime + +# Configure logging +def setup_logger(name="garminsync", level=logging.INFO): + """Setup logger with consistent formatting""" + logger = logging.getLogger(name) + + # Prevent duplicate handlers + if logger.handlers: + return logger + + logger.setLevel(level) + + # Create console handler + handler = logging.StreamHandler(sys.stdout) + handler.setLevel(level) + + # Create formatter + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + handler.setFormatter(formatter) + + # Add handler to logger + logger.addHandler(handler) + + return logger + +# Create default logger instance +logger = setup_logger() + +def format_timestamp(timestamp_str=None): + """Format timestamp string for display""" + if not timestamp_str: + return "Never" + + try: + # Parse ISO format timestamp + dt = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00')) + return dt.strftime("%Y-%m-%d %H:%M:%S") + except (ValueError, AttributeError): + return timestamp_str + +def safe_filename(filename): + """Make filename safe for filesystem""" + import re + # Replace problematic characters + safe_name = re.sub(r'[<>:"/\\|?*]', '_', filename) + # Replace spaces and colons commonly found in timestamps + safe_name = safe_name.replace(':', '-').replace(' ', '_') + return safe_name + +def bytes_to_human_readable(bytes_count): + """Convert bytes to human readable format""" + if bytes_count == 0: + return "0 B" + + for unit in ['B', 'KB', 'MB', 'GB']: + if bytes_count < 1024.0: + return f"{bytes_count:.1f} {unit}" + bytes_count /= 1024.0 + return f"{bytes_count:.1f} TB" + +def validate_cron_expression(cron_expr): + """Basic validation of cron expression""" + try: + from apscheduler.triggers.cron import CronTrigger + # Try to create a CronTrigger with the expression + CronTrigger.from_crontab(cron_expr) + return True + except (ValueError, TypeError): + return False + +# Utility function for error handling +def handle_db_error(func): + """Decorator for database operations with error handling""" + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as e: + logger.error(f"Database operation failed in {func.__name__}: {e}") + raise + return wrapper \ No newline at end of file diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..1d49607 --- /dev/null +++ b/plan.md @@ -0,0 +1,203 @@ +# GarminSync Fixes and Updated Requirements + +## Primary Issue: Dependency Conflicts + +The main error you're encountering is a dependency conflict between `pydantic` and `garth` (a dependency of `garminconnect`). Here's the solution: + +### Updated requirements.txt +``` +typer==0.9.0 +click==8.1.7 +python-dotenv==1.0.0 +garminconnect==0.2.29 +sqlalchemy==2.0.23 +tqdm==4.66.1 +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +apscheduler==3.10.4 +pydantic>=2.0.0,<2.5.0 +jinja2==3.1.2 +python-multipart==0.0.6 +aiofiles==23.2.1 +``` + +**Key Change**: Changed `pydantic==2.5.0` to `pydantic>=2.0.0,<2.5.0` to avoid the compatibility issue with `garth`. + +## Code Issues Found and Fixes + +### 1. Missing utils.py File +Your `daemon.py` imports `from .utils import logger` but this file doesn't exist. + +**Fix**: Create `garminsync/utils.py`: +```python +import logging + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) + +logger = logging.getLogger('garminsync') +``` + +### 2. Daemon.py Import Issues +The `daemon.py` file has several import and method call issues. + +**Fix for garminsync/daemon.py** (line 56-75): +```python +def sync_and_download(self): + """Scheduled job function""" + try: + self.log_operation("sync", "started") + + # Import here to avoid circular imports + from .garmin import GarminClient + from .database import sync_database + + # Perform sync and download + client = GarminClient() + + # Sync database first + sync_database(client) + + # Download missing activities + downloaded_count = 0 + session = get_session() + missing_activities = session.query(Activity).filter_by(downloaded=False).all() + + for activity in missing_activities: + try: + # Use the correct method name + fit_data = client.download_activity_fit(activity.activity_id) + + # Save the file + import os + from pathlib import Path + data_dir = Path(os.getenv("DATA_DIR", "data")) + data_dir.mkdir(parents=True, exist_ok=True) + + timestamp = activity.start_time.replace(":", "-").replace(" ", "_") + filename = f"activity_{activity.activity_id}_{timestamp}.fit" + filepath = data_dir / filename + + with open(filepath, "wb") as f: + f.write(fit_data) + + activity.filename = str(filepath) + activity.downloaded = True + activity.last_sync = datetime.now().isoformat() + downloaded_count += 1 + session.commit() + + except Exception as e: + logger.error(f"Failed to download activity {activity.activity_id}: {e}") + session.rollback() + + session.close() + self.log_operation("sync", "success", + f"Downloaded {downloaded_count} new activities") + + except Exception as e: + logger.error(f"Sync failed: {e}") + self.log_operation("sync", "error", str(e)) +``` + +### 3. Missing created_at Field in Database Sync +The `sync_database` function in `database.py` doesn't set the `created_at` field. + +**Fix for garminsync/database.py** (line 64-75): +```python +def sync_database(garmin_client): + """Sync local database with Garmin Connect activities""" + from datetime import datetime + 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, + created_at=datetime.now().isoformat(), # Add this line + last_sync=datetime.now().isoformat() + ) + session.add(new_activity) + + session.commit() + except SQLAlchemyError as e: + session.rollback() + raise e + finally: + session.close() +``` + +### 4. Add Missing created_at Field to Database Model +The `Activity` model is missing the `created_at` field that's used in the daemon. + +**Fix for garminsync/database.py** (line 12): +```python +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) + created_at = Column(String, nullable=False) # Add this line + last_sync = Column(String, nullable=True) # ISO timestamp of last sync +``` + +### 5. JavaScript Function Missing in Dashboard +The dashboard template calls `toggleDaemon()` but this function doesn't exist in the JavaScript. + +**Fix for garminsync/web/static/app.js** (add this function): +```javascript +async function toggleDaemon() { + // TODO: Implement daemon toggle functionality + alert('Daemon toggle functionality not yet implemented'); +} +``` + +## Testing the Fixes + +After applying these fixes: + +1. **Rebuild the Docker image**: + ```bash + docker build -t garminsync . + ``` + +2. **Test the daemon mode**: + ```bash + docker run -d --env-file .env -v $(pwd)/data:/app/data -p 8080:8080 garminsync daemon --start + ``` + +3. **Check the logs**: + ```bash + docker logs + ``` + +4. **Access the web UI**: + Open http://localhost:8080 in your browser + +## Additional Recommendations + +1. **Add error handling for missing directories**: The daemon should create the data directory if it doesn't exist. + +2. **Improve logging**: Add more detailed logging throughout the application. + +3. **Add health checks**: Implement health check endpoints for the daemon. + +4. **Database migrations**: Consider adding database migration support for schema changes. + +The primary fix for your immediate issue is updating the `pydantic` version constraint in `requirements.txt`. The other fixes address various code quality and functionality issues I found during the review. \ No newline at end of file