All checks were successful
continuous-integration/drone/push Build is passing
278 lines
9.0 KiB
Python
278 lines
9.0 KiB
Python
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 |