from datetime import date, datetime from enum import Enum from typing import List, Optional, Union, Any import uuid from uuid import UUID from pydantic import BaseModel, Field, field_serializer, ConfigDict class EntryType(str, Enum): PERIOD = "period" OVULATION = "ovulation" SYMPTOMS = "symptoms" MEDICATION = "medication" MOOD = "mood" EXERCISE = "exercise" APPOINTMENT = "appointment" # Мобильное приложение использует другие названия типов class MobileEntryType(str, Enum): MENSTRUATION = "MENSTRUATION" OVULATION = "OVULATION" SPOTTING = "SPOTTING" DISCHARGE = "DISCHARGE" PAIN = "PAIN" MOOD = "MOOD" def to_server_type(self) -> EntryType: """Конвертировать тип из мобильного приложения в тип сервера""" mapping = { self.MENSTRUATION: EntryType.PERIOD, self.OVULATION: EntryType.OVULATION, self.SPOTTING: EntryType.SYMPTOMS, self.DISCHARGE: EntryType.SYMPTOMS, self.PAIN: EntryType.SYMPTOMS, self.MOOD: EntryType.MOOD, } return mapping.get(self, EntryType.SYMPTOMS) class FlowIntensity(str, Enum): LIGHT = "light" MEDIUM = "medium" HEAVY = "heavy" SPOTTING = "spotting" @classmethod def from_int(cls, value: int) -> 'FlowIntensity': """Конвертировать числовое значение в enum""" if value <= 1: return cls.LIGHT elif value <= 3: return cls.MEDIUM else: return cls.HEAVY class MobileMoodType(str, Enum): """Типы настроения из мобильного приложения""" HAPPY = "HAPPY" SAD = "SAD" NORMAL = "NORMAL" STRESSED = "STRESSED" ANXIOUS = "ANXIOUS" IRRITATED = "IRRITATED" class MoodType(str, Enum): HAPPY = "happy" SAD = "sad" ANXIOUS = "anxious" IRRITATED = "irritated" ENERGETIC = "energetic" TIRED = "tired" @classmethod def from_mobile_mood(cls, mood_str: str) -> Optional['MoodType']: """Конвертировать строку настроения из мобильного приложения""" if not mood_str: return None # Преобразуем строку в enum или используем исходную строку, если не удалось try: mood_type = MobileMoodType(mood_str) if mood_type == MobileMoodType.NORMAL: return None mapping = { MobileMoodType.HAPPY: cls.HAPPY, MobileMoodType.SAD: cls.SAD, MobileMoodType.ANXIOUS: cls.ANXIOUS, MobileMoodType.IRRITATED: cls.IRRITATED, MobileMoodType.STRESSED: cls.ANXIOUS, } return mapping.get(mood_type) except ValueError: # Если строка не является допустимым MobileMoodType # Пробуем сопоставить с сервером напрямую mood_map = { "HAPPY": cls.HAPPY, "SAD": cls.SAD, "ANXIOUS": cls.ANXIOUS, "IRRITATED": cls.IRRITATED, "ENERGETIC": cls.ENERGETIC, "TIRED": cls.TIRED, "NORMAL": cls.HAPPY, # NORMAL мапится на HAPPY } return mood_map.get(mood_str) class CalendarEntryBase(BaseModel): entry_date: date entry_type: EntryType flow_intensity: Optional[FlowIntensity] = None period_symptoms: Optional[str] = Field(None, max_length=500) mood: Optional[MoodType] = None energy_level: Optional[int] = Field(None, ge=1, le=5) sleep_hours: Optional[int] = Field(None, ge=0, le=24) symptoms: Optional[str] = Field(None, max_length=1000) medications: Optional[str] = Field(None, max_length=500) notes: Optional[str] = Field(None, max_length=1000) class MobileSymptom(str, Enum): """Симптомы, используемые в мобильном приложении""" CRAMPS = "CRAMPS" HEADACHE = "HEADACHE" BLOATING = "BLOATING" FATIGUE = "FATIGUE" NAUSEA = "NAUSEA" BREAST_TENDERNESS = "BREAST_TENDERNESS" ACNE = "ACNE" BACKACHE = "BACKACHE" class CalendarEventCreate(BaseModel): """Модель создания события календаря из мобильного приложения""" date: date type: MobileEntryType flow_intensity: Optional[int] = Field(None, ge=1, le=5) mood: Optional[MobileMoodType] = None symptoms: Optional[List[MobileSymptom]] = None notes: Optional[str] = Field(None, max_length=1000) def to_server_format(self) -> 'CalendarEntryCreate': """Преобразовать в серверный формат""" symptoms_str = None if self.symptoms: symptoms_str = ", ".join([s.value for s in self.symptoms]) flow = None if self.flow_intensity is not None: flow = FlowIntensity.from_int(self.flow_intensity) mood_val = None if self.mood: mood_val = MoodType.from_mobile_mood(self.mood) return CalendarEntryCreate( entry_date=self.date, entry_type=self.type.to_server_type(), flow_intensity=flow, mood=mood_val, symptoms=symptoms_str, notes=self.notes, period_symptoms=None, energy_level=None, sleep_hours=None, medications=None ) class CalendarEntryCreate(CalendarEntryBase): pass class CalendarEntryResponse(BaseModel): id: int uuid: UUID # Используем UUID из uuid модуля entry_date: date entry_type: str flow_intensity: Optional[str] = None period_symptoms: Optional[str] = None mood: Optional[str] = None energy_level: Optional[int] = None sleep_hours: Optional[int] = None symptoms: Optional[str] = None medications: Optional[str] = None notes: Optional[str] = None is_predicted: bool confidence_score: Optional[int] = None created_at: datetime updated_at: Optional[datetime] = None is_active: Optional[bool] = None user_id: Optional[int] = None model_config = ConfigDict(from_attributes=True) @field_serializer('uuid') def serialize_uuid(self, uuid_val: UUID) -> str: """Преобразование UUID в строку для JSON-сериализации""" return str(uuid_val) # Модель ответа для мобильного приложения class CalendarEvent(BaseModel): id: int uuid: UUID # Используем тип UUID из модуля uuid date: date type: str flow_intensity: Optional[int] = None mood: Optional[str] = None symptoms: Optional[List[str]] = None notes: Optional[str] = None created_at: datetime model_config = ConfigDict(from_attributes=True) @classmethod def from_server_response(cls, entry: CalendarEntryResponse) -> 'CalendarEvent': """Преобразовать из серверной модели в модель для мобильного приложения""" # Преобразование flow_intensity из строки в число flow_int = None if entry.flow_intensity: if entry.flow_intensity == "light": flow_int = 1 elif entry.flow_intensity == "medium": flow_int = 3 elif entry.flow_intensity == "heavy": flow_int = 5 # Преобразование symptoms из строки в список symptoms_list = None if entry.symptoms: symptoms_list = [s.strip() for s in entry.symptoms.split(",")] return cls( id=entry.id, uuid=entry.uuid, date=entry.entry_date, type=entry.entry_type.upper(), flow_intensity=flow_int, mood=entry.mood.upper() if entry.mood else None, symptoms=symptoms_list, notes=entry.notes, created_at=entry.created_at ) class CycleDataResponse(BaseModel): id: int cycle_start_date: date cycle_length: Optional[int] period_length: Optional[int] ovulation_date: Optional[date] class CycleOverview(BaseModel): current_cycle_day: Optional[int] current_phase: str # menstrual, follicular, ovulation, luteal next_period_date: Optional[date] days_until_period: Optional[int] cycle_regularity: str # very_regular, regular, irregular, very_irregular avg_cycle_length: Optional[int] class HealthInsightResponse(BaseModel): id: int insight_type: str title: str description: str recommendation: Optional[str] confidence_level: str created_at: datetime class Config: from_attributes = True