194 lines
4.7 KiB
Python
194 lines
4.7 KiB
Python
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)
|