/
Ru5Kasper
/
Trackly
Обзор
Документация
Войти
/
Ru5Kasper
/
Trackly
Код
Запросы
1
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
backend/app/schemas.py
271 строка
8 KB
DORANDKOR
feat(backend): offline first
19 май 2026, 12:21
19 май 2026, 12:21
d70f9df
Код
Авторство
О чём код?
from datetime import datetime from typing import Literal from pydantic import ( BaseModel, ConfigDict, EmailStr, Field, field_validator, model_validator, ) from typing_extensions import Self class HabitSchedule(BaseModel): """Schedule configuration for habit recurrence patterns.""" type: Literal["daily", "weekly", "interval"] times: list[str] = Field(min_length=1) days_of_week: list[int] | None = None interval_days: int | None = None start_date: str | None = None end_date: str | None = None enabled: bool = True @field_validator("times") @classmethod def validate_times(cls, v: list[str]) -> list[str]: """Validate time format (HH:MM), dedupe, and sort.""" import re time_pattern = re.compile(r"^\d{2}:\d{2}$") for time_str in v: if not time_pattern.match(time_str): raise ValueError(f"Time '{time_str}' must match format HH:MM") # Validate it's a valid 24h time parts = time_str.split(":") hour, minute = int(parts[0]), int(parts[1]) if hour < 0 or hour > 23: raise ValueError(f"Hour in '{time_str}' must be 0-23") if minute < 0 or minute > 59: raise ValueError(f"Minute in '{time_str}' must be 0-59") # Dedupe and sort return sorted(list(set(v))) @field_validator("days_of_week") @classmethod def validate_days_of_week(cls, v: list[int] | None) -> list[int] | None: """Validate days are 0-6 (Mon-Sun), dedupe, and sort.""" if v is None: return None for day in v: if day < 0 or day > 6: raise ValueError(f"Day {day} must be 0-6 (Mon=0, Sun=6)") # Dedupe and sort return sorted(list(set(v))) @field_validator("interval_days") @classmethod def validate_interval_days(cls, v: int | None) -> int | None: """Validate interval_days is >= 1.""" if v is not None and v < 1: raise ValueError("interval_days must be >= 1") return v @model_validator(mode="after") def validate_schedule(self) -> Self: """Validate type-specific required fields and date logic.""" # Validate required fields based on type if self.type == "weekly": if self.days_of_week is None or len(self.days_of_week) == 0: raise ValueError( "days_of_week is required and must be non-empty for type='weekly'" ) elif self.type == "interval": if self.interval_days is None: raise ValueError("interval_days is required for type='interval'") # Validate date logic if self.start_date and self.end_date: if self.end_date < self.start_date: raise ValueError("end_date must be >= start_date") return self class UserBase(BaseModel): name: str = Field(min_length=1, max_length=50) email: EmailStr = Field(max_length=120) class UserCreate(UserBase): password: str = Field(min_length=8) class User(UserBase): id: int has_avatar: bool = False bool_theme: bool = False disable_notifications: bool = False avatar_url: str | None = None updated_at: datetime model_config = ConfigDict(from_attributes=True) class UserUpdate(BaseModel): name: str | None = Field(default=None, min_length=1, max_length=50) email: EmailStr | None = Field(default=None, max_length=120) old_password: str | None = Field(default=None, min_length=8) password: str | None = Field(default=None, min_length=8) class Token(BaseModel): access_token: str refresh_token: str token_type: str class RefreshTokenRequest(BaseModel): refresh_token: str class HabitSchemaBase(BaseModel): model_config = ConfigDict(from_attributes=True) class HabitCreate(BaseModel): title: str = Field(min_length=1, max_length=120) description: str = Field(default="", max_length=1000) color: str = Field(pattern=r"^#[0-9A-Fa-f]{6}$", default="#FF0000") target_count: int = Field(ge=1, default=1) is_active: bool = True schedule: HabitSchedule | None = None client_ref: str | None = Field(default=None, max_length=64) created_at: datetime | None = None updated_at: datetime | None = None class HabitUpdate(BaseModel): title: str = Field(min_length=1, max_length=120) description: str = Field(default="", max_length=1000) color: str = Field(pattern=r"^#[0-9A-Fa-f]{6}$") target_count: int = Field(ge=1) is_active: bool schedule: HabitSchedule | None = None client_ref: str | None = Field(default=None, max_length=64) updated_at: datetime | None = None class HabitShort(HabitSchemaBase): id: int client_ref: str | None = None title: str color: str = Field(pattern=r"^#[0-9A-Fa-f]{6}$") frequency: int target_count: int is_active: bool created_at: datetime updated_at: datetime deleted_at: datetime | None = None schedule: HabitSchedule | None = None class Habit(HabitShort): user_id: int description: str class HabitLogOut(BaseModel): id: int client_ref: str | None = None habit_id: int completed_at: datetime updated_at: datetime deleted_at: datetime | None = None model_config = ConfigDict(from_attributes=True) class SyncUserPayload(BaseModel): name: str | None = Field(default=None, min_length=1, max_length=50) email: EmailStr | None = Field(default=None, max_length=120) theme: Literal["light", "dark"] | None = None disable_notifications: bool | None = None avatar_url: str | None = Field(default=None, max_length=500) updated_at: datetime class SyncHabitPayload(BaseModel): id: int | None = None client_ref: str | None = Field(default=None, max_length=64) title: str | None = Field(default=None, min_length=1, max_length=120) description: str | None = Field(default=None, max_length=1000) color: str | None = Field(default=None, pattern=r"^#[0-9A-Fa-f]{6}$") frequency: int | None = Field(default=None, ge=0) target_count: int | None = Field(default=None, ge=1) is_active: bool | None = None schedule: HabitSchedule | None = None created_at: datetime | None = None updated_at: datetime deleted_at: datetime | None = None @model_validator(mode="after") def validate_sync_payload(self) -> Self: if self.id is None and self.client_ref is None: raise ValueError("Either id or client_ref must be provided") return self class SyncHabitLogPayload(BaseModel): id: int | None = None client_ref: str | None = Field(default=None, max_length=64) habit_id: int | None = None habit_client_ref: str | None = Field(default=None, max_length=64) completed_at: datetime | None = None updated_at: datetime deleted_at: datetime | None = None @model_validator(mode="after") def validate_sync_payload(self) -> Self: if self.id is None and self.client_ref is None: raise ValueError("Either id or client_ref must be provided") if self.habit_id is None and self.habit_client_ref is None: raise ValueError("Either habit_id or habit_client_ref must be provided") return self class SyncRequest(BaseModel): last_synced_at: datetime | None = None user: SyncUserPayload | None = None habits: list[SyncHabitPayload] = Field(default_factory=list) habit_logs: list[SyncHabitLogPayload] = Field(default_factory=list) class SyncIdMapping(BaseModel): client_ref: str id: int class SyncResponse(BaseModel): server_time: datetime user: User | None = None habits: list[Habit] = Field(default_factory=list) habit_logs: list[HabitLogOut] = Field(default_factory=list) habit_mappings: list[SyncIdMapping] = Field(default_factory=list) habit_log_mappings: list[SyncIdMapping] = Field(default_factory=list) class StreakInfo(BaseModel): current_streak: int best_streak: int class ChartPoint(BaseModel): date: str count: int class CompletionRate(BaseModel): timespan: str completions: int total_days: int percentage: float