python v2 - added feartures 1 and 2 - no errors

This commit is contained in:
2025-08-08 14:12:39 -07:00
parent 0d3a974be4
commit 9ed2f3720d
2 changed files with 288 additions and 0 deletions

85
garminsync/utils.py Normal file
View 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
View 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.