Files

216 lines
8.4 KiB
Python

"""Health check and metric endpoints for API v1."""
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import FileResponse
import os
from sqlalchemy.orm import Session
from sqlalchemy import text
from typing import List, Optional
import datetime
from backend.database import get_db
from backend.models.daily_health_metrics import DailyHealthMetrics
from backend.models.user import User
from backend.models.workout_session import WorkoutSession
from backend.schemas.daily_health_metrics import DailyHealthMetricsResponse
from backend import auth
router = APIRouter(prefix="/api/health", tags=["health"])
@router.get("", status_code=status.HTTP_200_OK)
def health_check(db: Session = Depends(get_db)):
"""Check if the application and database are healthy."""
try:
# Check database connection
db.execute(text("SELECT 1"))
return {
"status": "healthy",
"database": "connected"
}
except Exception as e:
raise HTTPException(status_code=503, detail=f"Unhealthy: {str(e)}")
@router.get("/stats", response_model=List[DailyHealthMetricsResponse])
def get_health_stats(
period: str = "week",
ref_date: Optional[datetime.date] = None,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""Retrieve health statistics for various periods."""
if ref_date is None:
ref_date = datetime.date.today()
if period == "day":
return db.query(DailyHealthMetrics).filter(
DailyHealthMetrics.user_id == current_user.id,
DailyHealthMetrics.date == ref_date
).all()
elif period == "week":
start_date = ref_date - datetime.timedelta(days=6)
return db.query(DailyHealthMetrics).filter(
DailyHealthMetrics.user_id == current_user.id,
DailyHealthMetrics.date.between(start_date, ref_date)
).order_by(DailyHealthMetrics.date.asc()).all()
elif period == "month":
start_date = ref_date - datetime.timedelta(days=29)
raw_data = db.query(DailyHealthMetrics).filter(
DailyHealthMetrics.user_id == current_user.id,
DailyHealthMetrics.date.between(start_date, ref_date)
).order_by(DailyHealthMetrics.date.asc()).all()
aggregated = []
import itertools
key_func = lambda x: x.date.isocalendar()[1]
for key, group in itertools.groupby(raw_data, key_func):
g = list(group)
first = g[0]
agg = {
"id": key,
"user_id": current_user.id,
"date": first.date,
"step_count": sum(x.step_count for x in g) // len(g),
"step_count_total": sum(x.step_count for x in g),
"calories_burned": sum(x.calories_burned for x in g) / len(g),
"distance_meters": sum(x.distance_meters for x in g) / len(g),
"sleep_duration_minutes": sum(x.sleep_duration_minutes for x in g) // len(g),
"deep_sleep_minutes": sum(x.deep_sleep_minutes for x in g) // len(g),
"light_sleep_minutes": sum(getattr(x, 'light_sleep_minutes', 0) for x in g) // len(g),
"rem_sleep_minutes": sum(getattr(x, 'rem_sleep_minutes', 0) for x in g) // len(g),
"awake_duration_minutes": sum(x.awake_duration_minutes for x in g) // len(g),
"pai_score": sum(x.pai_score for x in g) / len(g),
"avg_heart_rate": sum(x.avg_heart_rate or 0 for x in g) // len(g),
"avg_spo2": sum(x.avg_spo2 or 0 for x in g) / len(g),
"min_heart_rate": min((x.min_heart_rate for x in g if x.min_heart_rate), default=0),
"max_heart_rate": max((x.max_heart_rate for x in g if x.max_heart_rate), default=0)
}
aggregated.append(agg)
return aggregated
elif period == "year":
start_date = datetime.date.today() - datetime.timedelta(days=365)
raw_data = db.query(DailyHealthMetrics).filter(
DailyHealthMetrics.user_id == current_user.id,
DailyHealthMetrics.date >= start_date
).order_by(DailyHealthMetrics.date.asc()).all()
aggregated = []
import itertools
key_func = lambda x: x.date.month
for key, group in itertools.groupby(raw_data, key_func):
g = list(group)
first = g[0]
agg = {
"id": key,
"user_id": current_user.id,
"date": first.date.replace(day=1),
"step_count": sum(x.step_count for x in g) // len(g),
"step_count_total": sum(x.step_count for x in g),
"calories_burned": sum(x.calories_burned for x in g) / len(g),
"distance_meters": sum(x.distance_meters for x in g) / len(g),
"sleep_duration_minutes": sum(x.sleep_duration_minutes for x in g) // len(g),
"deep_sleep_minutes": sum(x.deep_sleep_minutes for x in g) // len(g),
"light_sleep_minutes": sum(getattr(x, 'light_sleep_minutes', 0) for x in g) // len(g),
"rem_sleep_minutes": sum(getattr(x, 'rem_sleep_minutes', 0) for x in g) // len(g),
"awake_duration_minutes": sum(x.awake_duration_minutes for x in g) // len(g),
"pai_score": sum(x.pai_score for x in g) / len(g),
"avg_heart_rate": sum(x.avg_heart_rate or 0 for x in g) // len(g),
"avg_spo2": sum(x.avg_spo2 or 0 for x in g) / len(g),
"min_heart_rate": min((x.min_heart_rate for x in g if x.min_heart_rate), default=0),
"max_heart_rate": max((x.max_heart_rate for x in g if x.max_heart_rate), default=0)
}
aggregated.append(agg)
return aggregated
return []
@router.get("/workouts")
def get_workouts(
limit: int = 50,
offset: int = 0,
activity_type: Optional[str] = None,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""Retrieve workout sessions for the current user."""
query = db.query(WorkoutSession).filter(
WorkoutSession.user_id == current_user.id
)
if activity_type:
query = query.filter(WorkoutSession.activity_type == activity_type)
workouts = query.order_by(
WorkoutSession.start_time.desc()
).offset(offset).limit(limit).all()
# Manually serialize to avoid Pydantic issues
return [
{
"id": w.id,
"user_id": w.user_id,
"start_time": w.start_time.isoformat() if w.start_time else None,
"end_time": w.end_time.isoformat() if w.end_time else None,
"activity_type": w.activity_type,
"activity_type_id": w.activity_type_id,
"duration_seconds": w.duration_seconds or 0,
"distance_meters": w.distance_meters or 0.0,
"calories": w.calories or 0.0,
"avg_pace": w.avg_pace,
"max_pace": w.max_pace,
"min_pace": w.min_pace,
"avg_hr": w.avg_hr,
"max_hr": w.max_hr,
"min_hr": w.min_hr,
"notes": w.notes
}
for w in workouts
]
@router.get("/workouts/summary")
def get_workout_summary(
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""Get summary statistics for workouts."""
from sqlalchemy import func
# Total counts
total = db.query(func.count(WorkoutSession.id)).filter(
WorkoutSession.user_id == current_user.id
).scalar()
# By activity type
by_type = db.query(
WorkoutSession.activity_type,
func.count(WorkoutSession.id).label('count'),
func.sum(WorkoutSession.duration_seconds).label('total_duration'),
func.sum(WorkoutSession.calories).label('total_calories'),
func.sum(WorkoutSession.distance_meters).label('total_distance')
).filter(
WorkoutSession.user_id == current_user.id
).group_by(WorkoutSession.activity_type).all()
return {
"total_workouts": total,
"by_activity_type": [
{
"activity_type": row[0],
"count": row[1],
"total_duration_seconds": row[2] or 0,
"total_calories": row[3] or 0,
"total_distance_meters": row[4] or 0
}
for row in by_type
]
}