feat: Update spec, fix bugs, improve UI/UX, and clean up code
This commit is contained in:
@@ -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)}")
|
||||
Reference in New Issue
Block a user