from __future__ import annotations import uuid from datetime import datetime from typing import Literal from pydantic import BaseModel, ConfigDict, Field, model_validator 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 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 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): 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: if bool(self.exercise_id) == bool(self.equipment_id): raise ValueError("Provide exactly one of exercise_id or equipment_id") return self class WorkoutItemRead(WorkoutItemCreate): id: uuid.UUID workout_id: uuid.UUID source_kind: Literal["exercise", "equipment"] title_snapshot: str image_s3_url_snapshot: str | None 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]]