All checks were successful
continuous-integration/drone/push Build is passing
361 lines
13 KiB
Plaintext
361 lines
13 KiB
Plaintext
"""
|
||
Служба календаря для приложения женской безопасности.
|
||
Предоставляет API для работы с записями календаря здоровья.
|
||
|
||
Поддерживаются следующие форматы запросов:
|
||
1. Стандартный формат: /api/v1/calendar/entries
|
||
2. Мобильный формат: /api/v1/calendar/entry
|
||
3. Запросы без префикса /calendar: /api/v1/entries и /api/v1/entry
|
||
"""
|
||
|
||
from fastapi import FastAPI, Depends, HTTPException, Header, Body, Query, Path
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.responses import JSONResponse
|
||
import json
|
||
from typing import List, Optional, Dict, Any, Union
|
||
from datetime import date, datetime, timedelta
|
||
from pydantic import BaseModel, Field
|
||
from enum import Enum
|
||
import os
|
||
import sys
|
||
import uuid
|
||
import logging
|
||
from typing import Dict, List, Optional
|
||
import time
|
||
import asyncio
|
||
|
||
# Добавляем путь к общим модулям
|
||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
|
||
|
||
# Импортируем общие модули
|
||
from shared.auth import get_current_user
|
||
from shared.database import get_db
|
||
from shared.config import settings
|
||
from services.calendar_service.models import CalendarEntryInDB, EntryType, FlowIntensity, MoodType
|
||
|
||
# Схемы данных для календарных записей
|
||
class CalendarEntryBase(BaseModel):
|
||
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[float] = None
|
||
symptoms: Optional[str] = None
|
||
medications: Optional[str] = None
|
||
notes: Optional[str] = None
|
||
|
||
class CalendarEntryCreate(CalendarEntryBase):
|
||
pass
|
||
|
||
class CalendarEntryResponse(CalendarEntryBase):
|
||
id: int
|
||
created_at: datetime
|
||
updated_at: Optional[datetime] = None
|
||
user_id: int
|
||
|
||
class Config:
|
||
orm_mode = True
|
||
|
||
# Мобильные типы записей и данных
|
||
class MobileEntryType(str, Enum):
|
||
MENSTRUATION = "MENSTRUATION"
|
||
OVULATION = "OVULATION"
|
||
SPOTTING = "SPOTTING"
|
||
DISCHARGE = "DISCHARGE"
|
||
PAIN = "PAIN"
|
||
MOOD = "MOOD"
|
||
|
||
class MobileMood(str, Enum):
|
||
HAPPY = "HAPPY"
|
||
SAD = "SAD"
|
||
ANXIOUS = "ANXIOUS"
|
||
IRRITABLE = "IRRITABLE"
|
||
ENERGETIC = "ENERGETIC"
|
||
TIRED = "TIRED"
|
||
NORMAL = "NORMAL"
|
||
|
||
class MobileSymptom(str, Enum):
|
||
CRAMPS = "CRAMPS"
|
||
HEADACHE = "HEADACHE"
|
||
BLOATING = "BLOATING"
|
||
FATIGUE = "FATIGUE"
|
||
NAUSEA = "NAUSEA"
|
||
BREAST_TENDERNESS = "BREAST_TENDERNESS"
|
||
ACNE = "ACNE"
|
||
BACKACHE = "BACKACHE"
|
||
|
||
# Схемы для мобильного API
|
||
class MobileCalendarEntry(BaseModel):
|
||
date: date
|
||
type: MobileEntryType
|
||
flow_intensity: Optional[int] = None # 1-5 scale
|
||
mood: Optional[MobileMood] = None
|
||
symptoms: Optional[List[str]] = None
|
||
notes: Optional[str] = None
|
||
|
||
# Инициализация приложения
|
||
app = FastAPI(title="Calendar Service API",
|
||
description="API для работы с календарём здоровья",
|
||
version="1.0.0")
|
||
|
||
# CORS
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=["*"],
|
||
allow_credentials=True,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
# Настройка логирования
|
||
logging.basicConfig(level=logging.INFO)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Конвертеры форматов
|
||
def mobile_to_standard_entry(mobile_entry: MobileCalendarEntry) -> CalendarEntryCreate:
|
||
"""Конвертирует запись из мобильного формата в стандартный"""
|
||
|
||
# Отображение типов записей
|
||
entry_type_mapping = {
|
||
MobileEntryType.MENSTRUATION: "period",
|
||
MobileEntryType.OVULATION: "ovulation",
|
||
MobileEntryType.SPOTTING: "spotting",
|
||
MobileEntryType.DISCHARGE: "discharge",
|
||
MobileEntryType.PAIN: "pain",
|
||
MobileEntryType.MOOD: "mood"
|
||
}
|
||
|
||
# Отображение интенсивности потока
|
||
flow_intensity_mapping = {
|
||
1: "very_light",
|
||
2: "light",
|
||
3: "medium",
|
||
4: "heavy",
|
||
5: "very_heavy"
|
||
}
|
||
|
||
# Отображение настроения
|
||
mood_mapping = {
|
||
MobileMood.HAPPY: "happy",
|
||
MobileMood.SAD: "sad",
|
||
MobileMood.ANXIOUS: "anxious",
|
||
MobileMood.IRRITABLE: "irritable",
|
||
MobileMood.ENERGETIC: "energetic",
|
||
MobileMood.TIRED: "tired",
|
||
MobileMood.NORMAL: "normal"
|
||
}
|
||
|
||
# Сопоставляем симптомы
|
||
symptoms_str = ""
|
||
if mobile_entry.symptoms:
|
||
symptoms_str = ", ".join(symptom.lower() for symptom in mobile_entry.symptoms)
|
||
|
||
# Определяем тип записи
|
||
entry_type = entry_type_mapping.get(mobile_entry.type, "other")
|
||
|
||
# Преобразуем интенсивность потока
|
||
flow_intensity = None
|
||
if mobile_entry.flow_intensity and mobile_entry.type == MobileEntryType.MENSTRUATION:
|
||
flow_intensity = flow_intensity_mapping.get(mobile_entry.flow_intensity)
|
||
|
||
# Преобразуем настроение
|
||
mood = None
|
||
if mobile_entry.mood:
|
||
mood = mood_mapping.get(mobile_entry.mood)
|
||
|
||
# Создаем стандартную запись
|
||
standard_entry = CalendarEntryCreate(
|
||
entry_date=mobile_entry.date,
|
||
entry_type=entry_type,
|
||
flow_intensity=flow_intensity,
|
||
period_symptoms="" if entry_type != "period" else symptoms_str,
|
||
mood=mood,
|
||
symptoms=symptoms_str if entry_type != "period" else "",
|
||
notes=mobile_entry.notes,
|
||
energy_level=None,
|
||
sleep_hours=None,
|
||
medications=""
|
||
)
|
||
|
||
return standard_entry
|
||
|
||
def standard_to_mobile_entry(entry: CalendarEntryResponse) -> Dict[str, Any]:
|
||
"""Конвертирует запись из стандартного формата в мобильный"""
|
||
|
||
# Обратное отображение типов записей
|
||
entry_type_mapping = {
|
||
"period": MobileEntryType.MENSTRUATION,
|
||
"ovulation": MobileEntryType.OVULATION,
|
||
"spotting": MobileEntryType.SPOTTING,
|
||
"discharge": MobileEntryType.DISCHARGE,
|
||
"pain": MobileEntryType.PAIN,
|
||
"mood": MobileEntryType.MOOD
|
||
}
|
||
|
||
# Обратное отображение интенсивности потока
|
||
flow_intensity_mapping = {
|
||
"very_light": 1,
|
||
"light": 2,
|
||
"medium": 3,
|
||
"heavy": 4,
|
||
"very_heavy": 5
|
||
}
|
||
|
||
# Обратное отображение настроения
|
||
mood_mapping = {
|
||
"happy": MobileMood.HAPPY,
|
||
"sad": MobileMood.SAD,
|
||
"anxious": MobileMood.ANXIOUS,
|
||
"irritable": MobileMood.IRRITABLE,
|
||
"energetic": MobileMood.ENERGETIC,
|
||
"tired": MobileMood.TIRED,
|
||
"normal": MobileMood.NORMAL
|
||
}
|
||
|
||
# Определяем тип записи
|
||
mobile_type = entry_type_mapping.get(entry.entry_type, MobileEntryType.MOOD)
|
||
|
||
# Интенсивность потока
|
||
flow_intensity = None
|
||
if entry.flow_intensity:
|
||
flow_intensity = flow_intensity_mapping.get(entry.flow_intensity)
|
||
|
||
# Настроение
|
||
mood = None
|
||
if entry.mood:
|
||
mood = mood_mapping.get(entry.mood, MobileMood.NORMAL)
|
||
|
||
# Симптомы
|
||
symptoms = []
|
||
if entry.entry_type == "period" and entry.period_symptoms:
|
||
symptoms = [s.strip().upper() for s in entry.period_symptoms.split(',') if s.strip()]
|
||
elif entry.symptoms:
|
||
symptoms = [s.strip().upper() for s in entry.symptoms.split(',') if s.strip()]
|
||
|
||
# Создаем мобильную запись
|
||
mobile_entry = {
|
||
"id": entry.id,
|
||
"date": entry.entry_date.isoformat(),
|
||
"type": mobile_type,
|
||
"flow_intensity": flow_intensity,
|
||
"mood": mood,
|
||
"symptoms": symptoms,
|
||
"notes": entry.notes,
|
||
"created_at": entry.created_at.isoformat(),
|
||
"updated_at": entry.updated_at.isoformat() if entry.updated_at else None
|
||
}
|
||
|
||
return mobile_entry
|
||
|
||
# Эндпоинт для проверки здоровья сервиса
|
||
@app.get("/health")
|
||
def health_check():
|
||
return {"status": "ok", "service": "calendar"}
|
||
|
||
# API для работы с календарем
|
||
# 1. Стандартный эндпоинт для получения записей календаря
|
||
@app.get("/api/v1/calendar/entries", response_model=List[CalendarEntryResponse])
|
||
async def get_calendar_entries(
|
||
skip: int = 0,
|
||
limit: int = 100,
|
||
current_user: dict = Depends(get_current_user),
|
||
db = Depends(get_db)
|
||
):
|
||
"""Получение списка записей календаря"""
|
||
entries = db.query(CalendarEntryInDB).filter(
|
||
CalendarEntryInDB.user_id == current_user["id"]
|
||
).offset(skip).limit(limit).all()
|
||
|
||
return entries
|
||
|
||
# 2. Стандартный эндпоинт для создания записи календаря
|
||
@app.post("/api/v1/calendar/entries", response_model=CalendarEntryResponse, status_code=201)
|
||
async def create_calendar_entry(
|
||
entry: CalendarEntryCreate,
|
||
current_user: dict = Depends(get_current_user),
|
||
db = Depends(get_db)
|
||
):
|
||
"""Создание новой записи в календаре"""
|
||
db_entry = CalendarEntryInDB(**entry.dict(), user_id=current_user["id"])
|
||
db.add(db_entry)
|
||
db.commit()
|
||
db.refresh(db_entry)
|
||
return db_entry
|
||
|
||
# 3. Поддержка legacy эндпоинта /api/v1/entries (без префикса /calendar)
|
||
@app.get("/api/v1/entries", response_model=List[CalendarEntryResponse])
|
||
async def get_entries_legacy(
|
||
skip: int = 0,
|
||
limit: int = 100,
|
||
current_user: dict = Depends(get_current_user),
|
||
db = Depends(get_db)
|
||
):
|
||
"""Legacy эндпоинт для получения записей календаря"""
|
||
return await get_calendar_entries(skip, limit, current_user, db)
|
||
|
||
@app.post("/api/v1/entries", response_model=CalendarEntryResponse, status_code=201)
|
||
async def create_entry_legacy(
|
||
entry: CalendarEntryCreate,
|
||
current_user: dict = Depends(get_current_user),
|
||
db = Depends(get_db)
|
||
):
|
||
"""Legacy эндпоинт для создания записи в календаре"""
|
||
return await create_calendar_entry(entry, current_user, db)
|
||
|
||
# 4. Поддержка эндпоинта /api/v1/entry (ед. число, без префикса /calendar)
|
||
@app.post("/api/v1/entry", response_model=CalendarEntryResponse, status_code=201)
|
||
async def create_entry_singular(
|
||
entry: CalendarEntryCreate,
|
||
current_user: dict = Depends(get_current_user),
|
||
db = Depends(get_db)
|
||
):
|
||
"""Эндпоинт для создания записи в календаре (ед. число)"""
|
||
return await create_calendar_entry(entry, current_user, db)
|
||
|
||
# 5. Поддержка мобильного API с эндпоинтом /api/v1/calendar/entry
|
||
@app.post("/api/v1/calendar/entry", status_code=201)
|
||
async def create_mobile_calendar_entry(
|
||
entry: MobileCalendarEntry,
|
||
current_user: dict = Depends(get_current_user),
|
||
db = Depends(get_db)
|
||
):
|
||
"""Создание записи в календаре из мобильного приложения"""
|
||
# Преобразуем мобильную запись в стандартную
|
||
standard_entry = mobile_to_standard_entry(entry)
|
||
|
||
# Создаем запись в БД
|
||
db_entry = CalendarEntryInDB(**standard_entry.dict(), user_id=current_user["id"])
|
||
db.add(db_entry)
|
||
db.commit()
|
||
db.refresh(db_entry)
|
||
|
||
# Преобразуем ответ обратно в мобильный формат
|
||
mobile_response = standard_to_mobile_entry(db_entry)
|
||
|
||
return mobile_response
|
||
|
||
@app.get("/api/v1/calendar/entry/{entry_id}", status_code=200)
|
||
async def get_mobile_calendar_entry(
|
||
entry_id: int,
|
||
current_user: dict = Depends(get_current_user),
|
||
db = Depends(get_db)
|
||
):
|
||
"""Получение записи календаря в мобильном формате"""
|
||
db_entry = db.query(CalendarEntryInDB).filter(
|
||
CalendarEntryInDB.id == entry_id,
|
||
CalendarEntryInDB.user_id == current_user["id"]
|
||
).first()
|
||
|
||
if not db_entry:
|
||
raise HTTPException(status_code=404, detail="Запись не найдена")
|
||
|
||
# Преобразуем ответ в мобильный формат
|
||
mobile_response = standard_to_mobile_entry(db_entry)
|
||
|
||
return mobile_response
|
||
|
||
if __name__ == "__main__":
|
||
import uvicorn
|
||
uvicorn.run(app, host="0.0.0.0", port=8004) |