fitness-web/app/routers/agent_api.py

319 lines
9.0 KiB
Python

from datetime import datetime, timezone
import json
from fastapi import APIRouter, Header, HTTPException, Depends
from pydantic import BaseModel
from sqlalchemy import select
from app.config import AGENT_API_KEY
from app.models.base import async_session
from app.models.user import User
from app.models.workout import Phase, Workout, WorkoutSet, WorkoutSnapshot
from app.models.checkin import Checkin
router = APIRouter(prefix="/api/agent", tags=["agent"])
async def verify_agent(x_api_key: str = Header("")):
if AGENT_API_KEY and x_api_key != AGENT_API_KEY:
raise HTTPException(status_code=403, detail="invalid api key")
async def snapshot_sets(workout_id: int, changed_by: str, reason: str):
async with async_session() as session:
result = await session.execute(
select(WorkoutSet)
.where(WorkoutSet.workout_id == workout_id)
.order_by(WorkoutSet.exercise, WorkoutSet.set_number)
)
current_sets = result.scalars().all()
if not current_sets:
return
snapshot_data = [
{
"id": s.id,
"exercise": s.exercise,
"set_number": s.set_number,
"reps": s.reps,
"weight": s.weight,
"rpe": s.rpe,
"notes": s.notes,
}
for s in current_sets
]
snap = WorkoutSnapshot(
workout_id=workout_id,
changed_by=changed_by,
reason=reason,
sets_snapshot=json.dumps(snapshot_data),
created_at=datetime.now(timezone.utc).isoformat(),
)
session.add(snap)
await session.commit()
class CreatePhaseRequest(BaseModel):
name: str
description: str = ""
start_date: str | None = None
end_date: str | None = None
notes: str = ""
class CreateWorkoutRequest(BaseModel):
username: str
name: str
date: str
phase_id: int | None = None
notes: str = ""
class CreateSetRequest(BaseModel):
exercise: str
set_number: int
reps: int | None = None
weight: float | None = None
rpe: float | None = None
notes: str = ""
reason: str = ""
class UpdateSetRequest(BaseModel):
exercise: str | None = None
set_number: int | None = None
reps: int | None = None
weight: float | None = None
rpe: float | None = None
notes: str | None = None
reason: str = ""
class CreateCheckinRequest(BaseModel):
username: str
date: str
feeling: str | None = None
weight_lb: float | None = None
calories: int | None = None
steps: int | None = None
sleep_hours: float | None = None
notes: str = ""
@router.post("/workouts")
async def agent_create_workout(
body: CreateWorkoutRequest,
_=Depends(verify_agent),
):
async with async_session() as session:
result = await session.execute(
select(User).where(User.username == body.username)
)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="user not found")
workout = Workout(
user_id=user.id,
phase_id=body.phase_id,
name=body.name,
date=body.date,
notes=body.notes,
status="plan",
)
session.add(workout)
await session.commit()
await session.refresh(workout)
return {"id": workout.id, "name": workout.name, "date": workout.date, "status": workout.status}
@router.post("/workouts/{workout_id}/sets")
async def agent_add_set(
workout_id: int,
body: CreateSetRequest,
_=Depends(verify_agent),
):
if body.reason:
await snapshot_sets(workout_id, "agent", body.reason)
async with async_session() as session:
ws = WorkoutSet(
workout_id=workout_id,
exercise=body.exercise,
set_number=body.set_number,
reps=body.reps,
weight=body.weight,
rpe=body.rpe,
notes=body.notes,
)
session.add(ws)
await session.commit()
await session.refresh(ws)
return {"id": ws.id, "exercise": ws.exercise, "set_number": ws.set_number}
@router.put("/workouts/{workout_id}/sets/{set_id}")
async def agent_update_set(
workout_id: int,
set_id: int,
body: UpdateSetRequest,
_=Depends(verify_agent),
):
if body.reason:
await snapshot_sets(workout_id, "agent", body.reason)
async with async_session() as session:
result = await session.execute(
select(WorkoutSet).where(
WorkoutSet.id == set_id,
WorkoutSet.workout_id == workout_id,
)
)
ws = result.scalar_one_or_none()
if not ws:
raise HTTPException(status_code=404, detail="set not found")
if body.exercise is not None:
ws.exercise = body.exercise
if body.set_number is not None:
ws.set_number = body.set_number
if body.reps is not None:
ws.reps = body.reps
if body.weight is not None:
ws.weight = body.weight
if body.rpe is not None:
ws.rpe = body.rpe
if body.notes is not None:
ws.notes = body.notes
await session.commit()
return {"id": ws.id, "exercise": ws.exercise, "set_number": ws.set_number}
@router.delete("/workouts/{workout_id}/sets/{set_id}")
async def agent_delete_set(
workout_id: int,
set_id: int,
reason: str = "",
_=Depends(verify_agent),
):
if reason:
await snapshot_sets(workout_id, "agent", reason)
async with async_session() as session:
result = await session.execute(
select(WorkoutSet).where(
WorkoutSet.id == set_id,
WorkoutSet.workout_id == workout_id,
)
)
ws = result.scalar_one_or_none()
if not ws:
raise HTTPException(status_code=404, detail="set not found")
await session.delete(ws)
await session.commit()
return {"deleted": set_id}
@router.put("/workouts/{workout_id}/complete")
async def agent_complete_workout(
workout_id: int,
_=Depends(verify_agent),
):
async with async_session() as session:
result = await session.execute(
select(Workout).where(Workout.id == workout_id)
)
workout = result.scalar_one_or_none()
if not workout:
raise HTTPException(status_code=404, detail="workout not found")
workout.status = "complete"
await session.commit()
return {"id": workout.id, "status": "complete"}
@router.post("/checkins")
async def agent_create_checkin(
body: CreateCheckinRequest,
_=Depends(verify_agent),
):
async with async_session() as session:
result = await session.execute(
select(User).where(User.username == body.username)
)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="user not found")
checkin = Checkin(
user_id=user.id,
date=body.date,
feeling=body.feeling,
weight_lb=body.weight_lb,
calories=body.calories,
steps=body.steps,
sleep_hours=body.sleep_hours,
notes=body.notes,
)
session.add(checkin)
await session.commit()
await session.refresh(checkin)
return {"id": checkin.id, "date": checkin.date}
@router.get("/phases")
async def agent_list_phases(_=Depends(verify_agent)):
async with async_session() as session:
result = await session.execute(select(Phase).order_by(Phase.start_date.nulls_last()))
phases = result.scalars().all()
return [
{"id": p.id, "name": p.name, "description": p.description,
"start_date": p.start_date, "end_date": p.end_date, "notes": p.notes}
for p in phases
]
@router.post("/phases")
async def agent_create_phase(
body: CreatePhaseRequest,
_=Depends(verify_agent),
):
async with async_session() as session:
phase = Phase(
name=body.name,
description=body.description,
start_date=body.start_date,
end_date=body.end_date,
notes=body.notes,
)
session.add(phase)
await session.commit()
await session.refresh(phase)
return {"id": phase.id, "name": phase.name}
@router.put("/phases/{phase_id}")
async def agent_update_phase(
phase_id: int,
body: CreatePhaseRequest,
_=Depends(verify_agent),
):
async with async_session() as session:
result = await session.execute(select(Phase).where(Phase.id == phase_id))
phase = result.scalar_one_or_none()
if not phase:
raise HTTPException(status_code=404, detail="phase not found")
phase.name = body.name
phase.description = body.description
phase.start_date = body.start_date
phase.end_date = body.end_date
phase.notes = body.notes
await session.commit()
return {"id": phase.id, "name": phase.name}