Initial Commit
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, desc
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models.user import User, Activity, ActivityDataPoint, ActivityLap
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ActivitySummary(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
sport_type: str
|
||||
start_time: datetime
|
||||
distance_m: Optional[float]
|
||||
duration_s: Optional[float]
|
||||
elevation_gain_m: Optional[float]
|
||||
avg_heart_rate: Optional[float]
|
||||
avg_cadence: Optional[float]
|
||||
avg_speed_ms: Optional[float]
|
||||
calories: Optional[float]
|
||||
polyline: Optional[str]
|
||||
bounding_box: Optional[dict]
|
||||
hr_zones: Optional[dict]
|
||||
named_route_id: Optional[int]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ActivityDetail(ActivitySummary):
|
||||
end_time: Optional[datetime]
|
||||
elevation_loss_m: Optional[float]
|
||||
max_heart_rate: Optional[float]
|
||||
avg_power: Optional[float]
|
||||
normalized_power: Optional[float]
|
||||
max_speed_ms: Optional[float]
|
||||
avg_temperature_c: Optional[float]
|
||||
training_stress_score: Optional[float]
|
||||
vo2max_estimate: Optional[float]
|
||||
|
||||
|
||||
class DataPointOut(BaseModel):
|
||||
timestamp: Optional[datetime]
|
||||
latitude: Optional[float]
|
||||
longitude: Optional[float]
|
||||
altitude_m: Optional[float]
|
||||
heart_rate: Optional[float]
|
||||
cadence: Optional[float]
|
||||
speed_ms: Optional[float]
|
||||
power: Optional[float]
|
||||
temperature_c: Optional[float]
|
||||
distance_m: Optional[float]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class LapOut(BaseModel):
|
||||
lap_number: int
|
||||
start_time: Optional[datetime]
|
||||
duration_s: Optional[float]
|
||||
distance_m: Optional[float]
|
||||
avg_heart_rate: Optional[float]
|
||||
avg_cadence: Optional[float]
|
||||
avg_speed_ms: Optional[float]
|
||||
avg_power: Optional[float]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ActivitySummary])
|
||||
async def list_activities(
|
||||
page: int = Query(1, ge=1),
|
||||
per_page: int = Query(20, ge=1, le=100),
|
||||
sport_type: Optional[str] = None,
|
||||
from_date: Optional[datetime] = None,
|
||||
to_date: Optional[datetime] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
q = select(Activity).where(Activity.user_id == current_user.id)
|
||||
|
||||
if sport_type:
|
||||
q = q.where(Activity.sport_type == sport_type)
|
||||
if from_date:
|
||||
q = q.where(Activity.start_time >= from_date)
|
||||
if to_date:
|
||||
q = q.where(Activity.start_time <= to_date)
|
||||
|
||||
q = q.order_by(desc(Activity.start_time))
|
||||
q = q.offset((page - 1) * per_page).limit(per_page)
|
||||
|
||||
result = await db.execute(q)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.get("/{activity_id}", response_model=ActivityDetail)
|
||||
async def get_activity(
|
||||
activity_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(Activity).where(
|
||||
Activity.id == activity_id,
|
||||
Activity.user_id == current_user.id,
|
||||
)
|
||||
)
|
||||
activity = result.scalar_one_or_none()
|
||||
if not activity:
|
||||
raise HTTPException(status_code=404, detail="Activity not found")
|
||||
return activity
|
||||
|
||||
|
||||
@router.get("/{activity_id}/data-points", response_model=List[DataPointOut])
|
||||
async def get_data_points(
|
||||
activity_id: int,
|
||||
downsample: int = Query(0, ge=0, description="Return every Nth point; 0 = all"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
# Verify ownership
|
||||
act = await db.execute(
|
||||
select(Activity).where(
|
||||
Activity.id == activity_id,
|
||||
Activity.user_id == current_user.id,
|
||||
)
|
||||
)
|
||||
if not act.scalar_one_or_none():
|
||||
raise HTTPException(status_code=404, detail="Activity not found")
|
||||
|
||||
q = select(ActivityDataPoint).where(
|
||||
ActivityDataPoint.activity_id == activity_id
|
||||
).order_by(ActivityDataPoint.timestamp)
|
||||
|
||||
result = await db.execute(q)
|
||||
points = result.scalars().all()
|
||||
|
||||
if downsample > 1:
|
||||
points = points[::downsample]
|
||||
|
||||
return points
|
||||
|
||||
|
||||
@router.get("/{activity_id}/laps", response_model=List[LapOut])
|
||||
async def get_laps(
|
||||
activity_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
act = await db.execute(
|
||||
select(Activity).where(
|
||||
Activity.id == activity_id,
|
||||
Activity.user_id == current_user.id,
|
||||
)
|
||||
)
|
||||
if not act.scalar_one_or_none():
|
||||
raise HTTPException(status_code=404, detail="Activity not found")
|
||||
|
||||
result = await db.execute(
|
||||
select(ActivityLap)
|
||||
.where(ActivityLap.activity_id == activity_id)
|
||||
.order_by(ActivityLap.lap_number)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.patch("/{activity_id}/name")
|
||||
async def rename_activity(
|
||||
activity_id: int,
|
||||
body: dict,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(Activity).where(
|
||||
Activity.id == activity_id,
|
||||
Activity.user_id == current_user.id,
|
||||
)
|
||||
)
|
||||
activity = result.scalar_one_or_none()
|
||||
if not activity:
|
||||
raise HTTPException(status_code=404, detail="Activity not found")
|
||||
|
||||
activity.name = body.get("name", activity.name)
|
||||
await db.commit()
|
||||
return {"id": activity_id, "name": activity.name}
|
||||
|
||||
|
||||
@router.delete("/{activity_id}", status_code=204)
|
||||
async def delete_activity(
|
||||
activity_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(Activity).where(
|
||||
Activity.id == activity_id,
|
||||
Activity.user_id == current_user.id,
|
||||
)
|
||||
)
|
||||
activity = result.scalar_one_or_none()
|
||||
if not activity:
|
||||
raise HTTPException(status_code=404, detail="Activity not found")
|
||||
await db.delete(activity)
|
||||
await db.commit()
|
||||
Reference in New Issue
Block a user