319 lines
9.0 KiB
Python
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}
|