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]]