feat: Update spec, fix bugs, improve UI/UX, and clean up code

This commit is contained in:
2025-12-25 08:33:01 -08:00
parent 8fe375a966
commit df9dcb2f79
21 changed files with 1741 additions and 1055 deletions

View File

@@ -1,9 +1,22 @@
from fastapi import APIRouter, Query, Response
from fastapi import APIRouter, Query, Response, HTTPException, Depends
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
from sqlalchemy import func
from ..models.activity import Activity
import logging
from ..services.postgresql_manager import PostgreSQLManager
from sqlalchemy.orm import Session
from ..utils.config import config
router = APIRouter()
logger = logging.getLogger(__name__)
def get_db():
db_manager = PostgreSQLManager(config.DATABASE_URL)
with db_manager.get_db_session() as session:
yield session
class ActivityResponse(BaseModel):
id: Optional[int] = None
garmin_activity_id: Optional[str] = None
@@ -19,26 +32,143 @@ class ActivityResponse(BaseModel):
@router.get("/activities/list", response_model=List[ActivityResponse])
async def list_activities(
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0)
offset: int = Query(0, ge=0),
db: Session = Depends(get_db)
):
# This would return metadata for all downloaded/available activities
# Implementation will connect with the services layer
return []
"""
Return metadata for all downloaded/available activities.
"""
try:
logger.info(f"Listing activities with limit={limit}, offset={offset}")
# Query the database for activities
activities = db.query(Activity).offset(offset).limit(limit).all()
# Convert SQLAlchemy objects to Pydantic models
activity_responses = []
for activity in activities:
activity_responses.append(
ActivityResponse(
id=activity.id,
garmin_activity_id=activity.garmin_activity_id,
activity_name=activity.activity_name,
activity_type=activity.activity_type,
start_time=activity.start_time.isoformat() if activity.start_time else None,
duration=activity.duration,
file_type=activity.file_type,
download_status=activity.download_status,
downloaded_at=activity.downloaded_at.isoformat() if activity.downloaded_at else None
)
)
logger.info(f"Returning {len(activity_responses)} activities")
return activity_responses
except Exception as e:
logger.error(f"Error in list_activities: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error listing activities: {str(e)}")
@router.get("/activities/query", response_model=List[ActivityResponse])
async def query_activities(
activity_type: Optional[str] = Query(None),
start_date: Optional[str] = Query(None),
end_date: Optional[str] = Query(None),
download_status: Optional[str] = Query(None)
download_status: Optional[str] = Query(None),
db: Session = Depends(get_db)
):
# This would allow advanced filtering of activities
# Implementation will connect with the services layer
return []
"""
Allow advanced filtering of activities.
"""
try:
logger.info(f"Querying activities - type: {activity_type}, start: {start_date}, end: {end_date}, status: {download_status}")
# Start building the query
query = db.query(Activity)
# Apply filters based on parameters
if activity_type:
query = query.filter(Activity.activity_type == activity_type)
if start_date:
from datetime import datetime
start_dt = datetime.fromisoformat(start_date)
query = query.filter(Activity.start_time >= start_dt)
if end_date:
from datetime import datetime
end_dt = datetime.fromisoformat(end_date)
query = query.filter(Activity.start_time <= end_dt)
if download_status:
query = query.filter(Activity.download_status == download_status)
# Execute the query
activities = query.all()
# Convert SQLAlchemy objects to Pydantic models
activity_responses = []
for activity in activities:
activity_responses.append(
ActivityResponse(
id=activity.id,
garmin_activity_id=activity.garmin_activity_id,
activity_name=activity.activity_name,
activity_type=activity.activity_type,
start_time=activity.start_time.isoformat() if activity.start_time else None,
duration=activity.duration,
file_type=activity.file_type,
download_status=activity.download_status,
downloaded_at=activity.downloaded_at.isoformat() if activity.downloaded_at else None
)
)
logger.info(f"Returning {len(activity_responses)} filtered activities")
return activity_responses
except Exception as e:
logger.error(f"Error in query_activities: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error querying activities: {str(e)}")
@router.get("/activities/download/{activity_id}")
async def download_activity(activity_id: str):
# This would serve the stored activity file from the database
# Implementation will connect with the services layer
# It should return the file content with appropriate content-type
return Response(content=b"sample_content", media_type="application/octet-stream", headers={"Content-Disposition": f"attachment; filename=activity_{activity_id}.tcx"})
async def download_activity(activity_id: str, db: Session = Depends(get_db)):
"""
Serve the stored activity file from the database.
"""
try:
logger.info(f"Downloading activity with ID: {activity_id}")
# Find the activity in the database
activity = db.query(Activity).filter(Activity.garmin_activity_id == activity_id).first()
if not activity:
raise HTTPException(status_code=404, detail=f"Activity with ID {activity_id} not found")
if not activity.file_content:
raise HTTPException(status_code=404, detail=f"No file content available for activity {activity_id}")
if activity.download_status != 'downloaded':
raise HTTPException(status_code=400, detail=f"File for activity {activity_id} is not ready for download (status: {activity.download_status})")
# Determine the appropriate content type based on the file type
content_type_map = {
'tcx': 'application/vnd.garmin.tcx+xml',
'gpx': 'application/gpx+xml',
'fit': 'application/octet-stream' # FIT files are binary
}
content_type = content_type_map.get(activity.file_type, 'application/octet-stream')
filename = f"activity_{activity_id}.{activity.file_type}"
logger.info(f"Returning file for activity {activity_id} with content type {content_type}")
return Response(
content=activity.file_content,
media_type=content_type,
headers={
"Content-Disposition": f"attachment; filename={filename}",
"Content-Length": str(len(activity.file_content))
}
)
except HTTPException:
# Re-raise HTTP exceptions as-is
raise
except Exception as e:
logger.error(f"Error in download_activity for ID {activity_id}: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error downloading activity: {str(e)}")