800dee31b2
- Added boto3 as a dependency in pyproject.toml and uv.lock. - Introduced multiple new exercise images in various formats (jpg, webp, avif, png). - Added new machine images to enhance the workout assets library.
218 lines
5.5 KiB
Python
218 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
|
|
ActivityKind = Literal["exercise", "machine"]
|
|
ActivityCategory = Literal[
|
|
"chest",
|
|
"back",
|
|
"legs",
|
|
"shoulders",
|
|
"biceps",
|
|
"triceps",
|
|
"core",
|
|
"cardio",
|
|
"full_body",
|
|
"other",
|
|
]
|
|
ActivityEquipment = Literal[
|
|
"barbell",
|
|
"dumbbell",
|
|
"machine",
|
|
"cable",
|
|
"bodyweight",
|
|
"kettlebell",
|
|
"cardio_machine",
|
|
"other",
|
|
]
|
|
MeasurementType = Literal[
|
|
"weight_reps",
|
|
"reps_only",
|
|
"duration",
|
|
"distance_duration",
|
|
"duration_calories",
|
|
]
|
|
Difficulty = Literal["beginner", "intermediate", "advanced"]
|
|
|
|
|
|
class ActivitySourceCreate(BaseModel):
|
|
slug: str | None = Field(default=None, min_length=1, max_length=180)
|
|
kind: ActivityKind
|
|
title: str = Field(min_length=1, max_length=160)
|
|
description: str | None = None
|
|
category: ActivityCategory = "other"
|
|
equipment: ActivityEquipment = "other"
|
|
measurement_type: MeasurementType = "weight_reps"
|
|
difficulty: Difficulty = "intermediate"
|
|
image_s3_url: str | None = None
|
|
image_s3_key: str | None = None
|
|
default_calories_per_minute: float | None = None
|
|
|
|
|
|
class ActivitySourceRead(ActivitySourceCreate):
|
|
id: uuid.UUID
|
|
slug: str
|
|
owner_user_id: uuid.UUID | None
|
|
is_builtin: bool
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class EquipmentCreate(BaseModel):
|
|
name: str = Field(min_length=1, max_length=160)
|
|
description: str | None = None
|
|
image_s3_url: str | None = None
|
|
image_s3_key: str | None = None
|
|
|
|
|
|
class EquipmentRead(EquipmentCreate):
|
|
id: uuid.UUID
|
|
owner_user_id: uuid.UUID | None
|
|
is_builtin: bool
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class ExerciseCreate(BaseModel):
|
|
name: str = Field(min_length=1, max_length=160)
|
|
description: str | None = None
|
|
equipment_id: uuid.UUID | None = None
|
|
image_s3_url: str | None = None
|
|
image_s3_key: str | None = None
|
|
default_calories_per_minute: float | None = None
|
|
|
|
|
|
class ExerciseRead(ExerciseCreate):
|
|
id: uuid.UUID
|
|
owner_user_id: uuid.UUID | None
|
|
is_builtin: bool
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class WorkoutCreate(BaseModel):
|
|
started_at: datetime | None = None
|
|
notes: str | None = None
|
|
|
|
|
|
class WorkoutUpdate(BaseModel):
|
|
finished_at: datetime | None = None
|
|
notes: str | None = None
|
|
|
|
|
|
class WorkoutFinishRequest(BaseModel):
|
|
notes: str | None = None
|
|
|
|
|
|
class WorkoutSetCreate(BaseModel):
|
|
weight: float = Field(default=0, ge=0)
|
|
reps: int = Field(default=0, ge=0)
|
|
duration_seconds: int | None = None
|
|
distance_km: float | None = Field(default=None, ge=0)
|
|
calories: float | None = None
|
|
completed_at: datetime | None = None
|
|
|
|
|
|
class WorkoutSetBatchCreate(BaseModel):
|
|
sets: list[WorkoutSetCreate] = Field(min_length=1)
|
|
|
|
|
|
class WorkoutSetUpdate(BaseModel):
|
|
weight: float | None = Field(default=None, ge=0)
|
|
reps: int | None = Field(default=None, ge=0)
|
|
duration_seconds: int | None = None
|
|
distance_km: float | None = Field(default=None, ge=0)
|
|
calories: float | None = None
|
|
completed_at: datetime | None = None
|
|
|
|
@model_validator(mode="after")
|
|
def at_least_one_field(self) -> WorkoutSetUpdate:
|
|
if not self.model_fields_set:
|
|
raise ValueError("Provide at least one field to update")
|
|
return self
|
|
|
|
|
|
class WorkoutSetRead(WorkoutSetCreate):
|
|
id: uuid.UUID
|
|
workout_item_id: uuid.UUID
|
|
set_index: int
|
|
completed_at: datetime
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class WorkoutItemCreate(BaseModel):
|
|
activity_source_id: uuid.UUID | None = None
|
|
exercise_id: uuid.UUID | None = None
|
|
equipment_id: uuid.UUID | None = None
|
|
order_index: int | None = None
|
|
planned_working_weight: float | None = None
|
|
|
|
@model_validator(mode="after")
|
|
def exactly_one_entity(self) -> WorkoutItemCreate:
|
|
provided = [self.activity_source_id, self.exercise_id, self.equipment_id]
|
|
if sum(value is not None for value in provided) != 1:
|
|
raise ValueError("Provide exactly one activity source, exercise, or equipment id")
|
|
return self
|
|
|
|
|
|
class WorkoutItemRead(WorkoutItemCreate):
|
|
id: uuid.UUID
|
|
workout_id: uuid.UUID
|
|
source_kind: Literal["exercise", "machine", "equipment"]
|
|
title_snapshot: str
|
|
image_s3_url_snapshot: str | None
|
|
measurement_type_snapshot: MeasurementType
|
|
category_snapshot: ActivityCategory
|
|
equipment_snapshot: ActivityEquipment
|
|
order_index: int
|
|
created_at: datetime
|
|
sets: list[WorkoutSetRead] = []
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class WorkoutRead(BaseModel):
|
|
id: uuid.UUID
|
|
user_id: uuid.UUID
|
|
status: Literal["active", "finished", "discarded"]
|
|
started_at: datetime
|
|
finished_at: datetime | None
|
|
notes: str | None
|
|
total_sets: int
|
|
total_volume: float
|
|
estimated_calories: float
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
items: list[WorkoutItemRead] = []
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class ProgressionPoint(BaseModel):
|
|
date: str
|
|
max_weight: float
|
|
volume: float
|
|
|
|
|
|
class ProgressionRead(BaseModel):
|
|
last_weight: float | None
|
|
max_weight: float | None
|
|
previous_delta: float | None
|
|
points: list[ProgressionPoint]
|
|
|
|
|
|
class CaloriesRead(BaseModel):
|
|
total_calories: float
|
|
workouts: list[dict[str, str | float]]
|