mirror of
https://github.com/sstent/GarminSync.git
synced 2026-01-25 16:42:20 +00:00
python v2 - added feartures 1 and 2 - no errors
This commit is contained in:
85
garminsync/utils.py
Normal file
85
garminsync/utils.py
Normal file
@@ -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
|
||||||
203
plan.md
Normal file
203
plan.md
Normal file
@@ -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 <container_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
Reference in New Issue
Block a user