from __future__ import annotations from datetime import date, datetime from typing import Any, Literal, Optional from pydantic import BaseModel, Field, field_validator Weekday = Literal[0, 1, 2, 3, 4, 5, 6] # Mon=0 .. Sun=6 class SetupPasswordRequest(BaseModel): password: str = Field(min_length=4, max_length=128) class LoginRequest(BaseModel): password: str class TokenResponse(BaseModel): access_token: str token_type: str = "bearer" needs_setup: bool = False onboarded: bool = False class StatusResponse(BaseModel): has_password: bool onboarded: bool phase: str = "active" class OnboardingRequest(BaseModel): one_rm_kg: float = Field(gt=0) training_weekdays: list[int] = Field(min_length=3, max_length=3) start_date: date @field_validator("training_weekdays") @classmethod def validate_days(cls, v: list[int]) -> list[int]: if len(set(v)) != 3: raise ValueError("Нужно выбрать 3 разных дня") for d in v: if d < 0 or d > 6: raise ValueError("День недели должен быть 0–6") sorted_days = sorted(v) # check circular gap of at least 1 day between consecutive selected days for i in range(3): a = sorted_days[i] b = sorted_days[(i + 1) % 3] if i < 2: gap = b - a - 1 else: gap = (a + 7) - b - 1 if gap < 1: raise ValueError("Между выбранными днями нужен минимум 1 день перерыва") return sorted(v) class ProfileResponse(BaseModel): one_rm_kg: Optional[float] training_weekdays: Optional[list[int]] start_date: Optional[date] onboarded: bool phase: str rest_until: Optional[datetime] probe_available_at: Optional[datetime] cycle_number: int rest_countdown_seconds: Optional[int] = None can_probe: bool = False class SetResponse(BaseModel): id: int set_index: int target_percent: Optional[float] target_reps: Optional[int] target_weight_kg: Optional[float] is_failure: bool completed: bool actual_reps: Optional[int] actual_weight_kg: Optional[float] class ExerciseResponse(BaseModel): id: int order_index: int name: str kind: str prescribed_weight_kg: Optional[float] weight_locked: bool needs_weight_input: bool pending_prompt: Optional[str] weight_step_kg: float = 1.25 sets: list[SetResponse] class WorkoutSummary(BaseModel): id: int cycle_number: int week_number: int day_index: int scheduled_date: date status: str exercise_count: int = 0 class WorkoutDetail(BaseModel): id: int cycle_number: int week_number: int day_index: int scheduled_date: date status: str started_at: Optional[datetime] paused_at: Optional[datetime] completed_at: Optional[datetime] current_exercise_index: int current_set_index: int rest_started_at: Optional[datetime] rest_elapsed_seconds: int = 0 can_start: bool = False can_resume: bool = False pause_expires_at: Optional[datetime] = None exercises: list[ExerciseResponse] class CalendarResponse(BaseModel): year: int month: int workouts: list[WorkoutSummary] profile: ProfileResponse class CompleteSetRequest(BaseModel): actual_reps: Optional[int] = None actual_weight_kg: Optional[float] = None class SetWeightRequest(BaseModel): weight_kg: float = Field(gt=0) class AdjustSetRequest(BaseModel): actual_weight_kg: float = Field(gt=0) actual_reps: int = Field(ge=0) class PromptChoiceRequest(BaseModel): choice: Literal["easy_yes", "easy_no"] weight_kg: Optional[float] = None increase_kg: Optional[float] = None decrease_kg: Optional[float] = None class StartRestRequest(BaseModel): rest_days: int = Field(ge=3, le=5) class ProbeRequest(BaseModel): one_rm_kg: float = Field(gt=0) start_date: date training_weekdays: Optional[list[int]] = None class ExportResponse(BaseModel): version: int = 1 exported_at: datetime data: dict[str, Any] class ImportRequest(BaseModel): version: int data: dict[str, Any] class MessageResponse(BaseModel): message: str workout: Optional[WorkoutDetail] = None profile: Optional[ProfileResponse] = None class MoveWorkoutRequest(BaseModel): scheduled_date: date class TrainingDaysRequest(BaseModel): training_weekdays: list[int] = Field(min_length=3, max_length=3) @field_validator("training_weekdays") @classmethod def validate_days(cls, v: list[int]) -> list[int]: return OnboardingRequest.validate_days(v)