feat: Implement active workout flow with status management

- Added `status`, `total_sets`, and `total_volume` fields to the Workout model.
- Introduced `source_kind`, `title_snapshot`, and `image_s3_url_snapshot` fields to the WorkoutItem model.
- Created endpoints for managing active workouts, including finishing and discarding workouts.
- Updated workout creation to ensure only one active workout exists per user.
- Implemented batch addition of workout sets and updates to workout set details.
- Enhanced database schema with Alembic migrations to support new fields and constraints.
- Added validation to ensure at least one field is provided for workout set updates.
- Updated calorie estimation logic to reflect new workout set structure.
This commit is contained in:
Artem Kashaev
2026-05-29 10:09:56 +05:00
parent d7b0c7754f
commit 7b34ce1a98
30 changed files with 2081 additions and 846 deletions
+34 -1
View File
@@ -1,7 +1,7 @@
from typing import Annotated, Any
import httpx
from fastapi import Depends, FastAPI, File, HTTPException, Query, UploadFile, status
from fastapi import Body, Depends, FastAPI, File, HTTPException, Query, UploadFile, status
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
@@ -152,6 +152,11 @@ async def create_workout(payload: dict[str, Any], user: CurrentUser) -> Any:
return await logic_request("POST", "/internal/workouts", user, json=payload)
@app.get("/workouts/active")
async def get_active_workout(user: CurrentUser) -> Any:
return await logic_request("GET", "/internal/workouts/active", user)
@app.get("/workouts/{workout_id}")
async def get_workout(workout_id: str, user: CurrentUser) -> Any:
return await logic_request("GET", f"/internal/workouts/{workout_id}", user)
@@ -162,6 +167,22 @@ async def update_workout(workout_id: str, payload: dict[str, Any], user: Current
return await logic_request("PATCH", f"/internal/workouts/{workout_id}", user, json=payload)
@app.post("/workouts/{workout_id}/finish")
async def finish_workout(
workout_id: str,
user: CurrentUser,
payload: Annotated[dict[str, Any] | None, Body()] = None,
) -> Any:
return await logic_request(
"POST", f"/internal/workouts/{workout_id}/finish", user, json=payload
)
@app.post("/workouts/{workout_id}/discard")
async def discard_workout(workout_id: str, user: CurrentUser) -> Any:
return await logic_request("POST", f"/internal/workouts/{workout_id}/discard", user)
@app.post("/workouts/{workout_id}/items", status_code=status.HTTP_201_CREATED)
async def add_workout_item(workout_id: str, payload: dict[str, Any], user: CurrentUser) -> Any:
return await logic_request("POST", f"/internal/workouts/{workout_id}/items", user, json=payload)
@@ -174,6 +195,18 @@ async def add_workout_set(item_id: str, payload: dict[str, Any], user: CurrentUs
)
@app.post("/workout-items/{item_id}/sets/batch", status_code=status.HTTP_201_CREATED)
async def add_workout_sets_batch(item_id: str, payload: dict[str, Any], user: CurrentUser) -> Any:
return await logic_request(
"POST", f"/internal/workout-items/{item_id}/sets/batch", user, json=payload
)
@app.patch("/workout-sets/{set_id}")
async def update_workout_set(set_id: str, payload: dict[str, Any], user: CurrentUser) -> Any:
return await logic_request("PATCH", f"/internal/workout-sets/{set_id}", user, json=payload)
@app.delete("/workout-items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def remove_workout_item(item_id: str, user: CurrentUser) -> None:
await logic_request("DELETE", f"/internal/workout-items/{item_id}", user)