calendar events
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-09-26 15:57:50 +09:00
parent 64171196b6
commit 76d0d86211
16 changed files with 1657 additions and 22 deletions

254
docs/MOBILE_API.md Normal file
View File

@@ -0,0 +1,254 @@
# Инструкция по интеграции мобильного API календарного сервиса
## Обзор
Этот документ содержит информацию о том, как интегрировать мобильное приложение с API календарного сервиса. Календарный сервис предоставляет эндпоинты для создания, получения и удаления записей в календаре женского здоровья.
## Базовый URL
```
http://[domain]/api/v1/calendar/
```
## Аутентификация
Все запросы к API должны включать токен аутентификации в заголовке `Authorization`:
```
Authorization: Bearer [token]
```
Для получения токена аутентификации используйте сервис аутентификации:
```
POST /api/v1/auth/token
Content-Type: application/x-www-form-urlencoded
username=user@example.com&password=userpassword
```
## Эндпоинты для мобильного приложения
### 1. Создание записи в календаре
```
POST /api/v1/calendar/entries/mobile
Content-Type: application/json
Authorization: Bearer [token]
```
#### Формат запроса
```json
{
"date": "2025-09-26",
"type": "MENSTRUATION",
"flow_intensity": 3,
"symptoms": ["CRAMPS", "HEADACHE"],
"mood": "NORMAL",
"notes": "Пример записи"
}
```
#### Параметры
| Параметр | Тип | Обязательный | Описание |
|----------------|--------------|--------------|--------------------------------------------------------------------------------------|
| date | string | Да | Дата записи в формате YYYY-MM-DD |
| type | string | Да | Тип записи: MENSTRUATION, OVULATION, SPOTTING, DISCHARGE, PAIN, MOOD |
| flow_intensity | integer | Нет | Интенсивность выделений по шкале 1-5 (только для MENSTRUATION и SPOTTING) |
| symptoms | array[string]| Нет | Массив симптомов: CRAMPS, HEADACHE, BLOATING, FATIGUE, и т.д. |
| mood | string | Нет | Настроение: HAPPY, SAD, NORMAL, STRESSED, ANXIOUS, IRRITATED |
| notes | string | Нет | Текстовая заметка |
#### Пример успешного ответа
```json
{
"id": 123,
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"entry_date": "2025-09-26",
"entry_type": "period",
"flow_intensity": "medium",
"period_symptoms": "",
"mood": "happy",
"energy_level": 1,
"sleep_hours": 0,
"symptoms": "CRAMPS, HEADACHE",
"medications": "",
"notes": "Пример записи",
"is_predicted": false,
"confidence_score": null,
"created_at": "2023-09-26T14:30:00.000Z"
}
```
### 2. Получение записей календаря
```
GET /api/v1/calendar/entries
Authorization: Bearer [token]
```
#### Параметры запроса (в URL)
| Параметр | Тип | Обязательный | Описание |
|-------------|--------|--------------|------------------------------------------|
| start_date | string | Нет | Начальная дата в формате YYYY-MM-DD |
| end_date | string | Нет | Конечная дата в формате YYYY-MM-DD |
| entry_type | string | Нет | Тип записи (period, ovulation, symptoms) |
| limit | integer| Нет | Ограничение количества возвращаемых записей (по умолчанию 100) |
#### Пример успешного ответа
```json
[
{
"id": 123,
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"entry_date": "2025-09-26",
"entry_type": "period",
"flow_intensity": "medium",
"period_symptoms": "",
"mood": "happy",
"energy_level": 1,
"sleep_hours": 0,
"symptoms": "CRAMPS, HEADACHE",
"medications": "",
"notes": "Пример записи",
"is_predicted": false,
"confidence_score": null,
"created_at": "2023-09-26T14:30:00.000Z"
},
{
"id": 124,
"uuid": "550e8400-e29b-41d4-a716-446655440001",
"entry_date": "2025-09-30",
"entry_type": "ovulation",
"flow_intensity": null,
"period_symptoms": "",
"mood": "happy",
"energy_level": 1,
"sleep_hours": 0,
"symptoms": "BREAST_TENDERNESS",
"medications": "",
"notes": "Пример записи об овуляции",
"is_predicted": false,
"confidence_score": null,
"created_at": "2023-09-26T14:30:00.000Z"
}
]
```
### 3. Удаление записи календаря
```
DELETE /api/v1/entries/{entry_id}
Authorization: Bearer [token]
```
#### Параметры
| Параметр | Тип | Обязательный | Описание |
|-----------|---------|--------------|------------------------|
| entry_id | integer | Да | ID записи для удаления |
#### Пример успешного ответа
```json
{
"message": "Entry deleted successfully"
}
```
### 4. Получение обзора цикла
```
GET /api/v1/cycle-overview
Authorization: Bearer [token]
```
#### Пример успешного ответа
```json
{
"current_cycle_day": 14,
"current_phase": "ovulation",
"next_period_date": "2023-10-15",
"days_until_period": 12,
"cycle_regularity": "regular",
"avg_cycle_length": 28
}
```
## Маппинг типов данных
### Типы записей
| Мобильное приложение | Сервер |
|----------------------|--------------|
| MENSTRUATION | period |
| OVULATION | ovulation |
| SPOTTING | symptoms |
| DISCHARGE | symptoms |
| PAIN | symptoms |
| MOOD | mood |
### Интенсивность выделений
| Мобильное приложение | Сервер |
|----------------------|--------------|
| 1 | light |
| 2 | light |
| 3 | medium |
| 4 | heavy |
| 5 | heavy |
### Настроение
| Мобильное приложение | Сервер |
|----------------------|--------------|
| HAPPY | happy |
| SAD | sad |
| NORMAL | happy |
| STRESSED | anxious |
| ANXIOUS | anxious |
| IRRITATED | irritated |
## Тестирование API
Для тестирования API можно использовать скрипты:
1. `setup_mobile_test.py` - создание токена для тестирования
2. `mobile_format_test.py` - тестирование преобразования форматов данных
3. `test_mobile_api.py` - полное тестирование мобильного API
### Пример запроса с curl
```bash
curl -X POST http://localhost:8004/api/v1/calendar/entries/mobile \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $(cat auth_token.txt)" \
-d '{
"date": "2025-09-26",
"type": "MENSTRUATION",
"flow_intensity": 3,
"symptoms": ["CRAMPS", "HEADACHE"],
"mood": "NORMAL",
"notes": "Тестовая запись"
}'
```
## Коды ошибок
| Код | Описание |
|------|---------------------------------------|
| 400 | Неверный запрос или данные |
| 401 | Ошибка аутентификации |
| 404 | Запись не найдена |
| 422 | Ошибка валидации данных |
| 500 | Внутренняя ошибка сервера |
## Контактная информация
При возникновении вопросов или проблем с интеграцией, пожалуйста, свяжитесь с командой разработки.

7
mobile_api_example.sh Normal file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
# Пример запроса к мобильному API календарного сервиса
curl -v -X POST http://localhost:8004/api/v1/calendar/entries/mobile \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $(cat auth_token.txt)" \
-d '{"date": "2025-09-26", "type": "MENSTRUATION", "flow_intensity": 3, "symptoms": ["CRAMPS", "HEADACHE"], "mood": "NORMAL", "notes": "Тестовая запись"}'

139
mobile_format_test.py Executable file
View File

@@ -0,0 +1,139 @@
#!/usr/bin/env python
"""
Скрипт для демонстрации преобразования данных мобильного формата в формат сервера.
Показывает, как преобразовывать данные и какие поля ожидает сервер.
"""
import json
import requests
from datetime import date, datetime
# Пример данных из мобильного приложения
MOBILE_DATA_EXAMPLES = [
{
"date": "2025-09-26",
"type": "MENSTRUATION",
"flow_intensity": 3,
"symptoms": ["CRAMPS", "HEADACHE"],
"mood": "NORMAL",
"notes": "Пример записи о менструации"
},
{
"date": "2025-09-30",
"type": "OVULATION",
"symptoms": ["BREAST_TENDERNESS"],
"mood": "HAPPY",
"notes": "Пример записи об овуляции"
},
{
"date": "2025-10-05",
"type": "SPOTTING",
"flow_intensity": 1,
"symptoms": ["BLOATING"],
"mood": "STRESSED",
"notes": "Пример записи о выделениях"
}
]
def test_format_conversion(mobile_data):
"""Тестирование преобразования формата данных"""
url = "http://localhost:8004/debug/mobile-entry"
headers = {
"Content-Type": "application/json"
}
print(f"\nТестирование преобразования данных: {json.dumps(mobile_data, ensure_ascii=False)}")
try:
response = requests.post(url, json=mobile_data, headers=headers)
if response.status_code == 200:
result = response.json()
print("✅ Успешное преобразование данных")
print("\nОригинальные данные (мобильный формат):")
print(json.dumps(result["original_data"], ensure_ascii=False, indent=2))
print("\nПреобразованные данные (формат сервера):")
print(json.dumps(result["transformed_data"], ensure_ascii=False, indent=2))
print("\nАнализ преобразования:")
print(f"- Дата: {mobile_data['date']} -> {result['transformed_data']['entry_date']}")
print(f"- Тип: {mobile_data['type']} -> {result['transformed_data']['entry_type']}")
if mobile_data.get('flow_intensity'):
print(f"- Интенсивность: {mobile_data['flow_intensity']} -> {result['transformed_data']['flow_intensity']}")
if mobile_data.get('symptoms'):
print(f"- Симптомы: {', '.join(mobile_data['symptoms'])} -> {result['transformed_data']['symptoms']}")
if mobile_data.get('mood'):
print(f"- Настроение: {mobile_data['mood']} -> {result['transformed_data']['mood'] or 'Не задано'}")
return result
else:
print(f"❌ Ошибка: {response.status_code}")
print(response.text)
return None
except Exception as e:
print(f"❌ Исключение: {str(e)}")
return None
def generate_documentation():
"""Генерация документации по формату данных"""
print("\n=== ДОКУМЕНТАЦИЯ ПО ФОРМАТУ ДАННЫХ ===\n")
print("Мобильный формат данных должен соответствовать следующей схеме:")
schema = {
"date": "string (формат YYYY-MM-DD, например '2025-09-26')",
"type": "string (один из: MENSTRUATION, OVULATION, SPOTTING, DISCHARGE, PAIN, MOOD)",
"flow_intensity": "integer (опционально, значения от 1 до 5)",
"symptoms": "массив строк (опционально, например ['CRAMPS', 'HEADACHE'])",
"mood": "string (опционально, одно из: HAPPY, SAD, NORMAL, STRESSED, ANXIOUS, IRRITATED)",
"notes": "string (опционально, текстовые заметки)"
}
print(json.dumps(schema, ensure_ascii=False, indent=2))
print("\nМаппинг между форматами:")
mapping = {
"Типы записей": {
"MENSTRUATION": "period",
"OVULATION": "ovulation",
"SPOTTING": "symptoms",
"DISCHARGE": "symptoms",
"PAIN": "symptoms",
"MOOD": "mood"
},
"Интенсивность выделений": {
"1": "light",
"2": "light",
"3": "medium",
"4-5": "heavy"
},
"Настроение": {
"HAPPY": "happy",
"SAD": "sad",
"NORMAL": "happy", # Маппится на happy
"STRESSED": "anxious",
"ANXIOUS": "anxious",
"IRRITATED": "irritated"
},
"Симптомы": "Преобразуются из массива строк в строку с разделителями-запятыми"
}
print(json.dumps(mapping, ensure_ascii=False, indent=2))
if __name__ == "__main__":
print("=== ТЕСТИРОВАНИЕ ПРЕОБРАЗОВАНИЯ ДАННЫХ МОБИЛЬНОГО ФОРМАТА ===\n")
for i, example in enumerate(MOBILE_DATA_EXAMPLES):
print(f"\n--- Пример #{i+1} ---")
test_format_conversion(example)
generate_documentation()
print("\n=== ТЕСТИРОВАНИЕ ЗАВЕРШЕНО ===")

View File

@@ -13,6 +13,7 @@ from services.calendar_service.schemas import (CalendarEntryCreate, CalendarEntr
CycleDataResponse, CycleOverview, EntryType,
FlowIntensity, HealthInsightResponse, MoodType,
CalendarEventCreate)
from services.calendar_service.mobile_endpoint import MobileCalendarEntryCreate, mobile_create_calendar_entry
from shared.auth import get_current_user_from_token as get_current_user
from shared.config import settings
from shared.database import get_db
@@ -415,6 +416,160 @@ async def create_calendar_entry(
return CalendarEntryResponse.model_validate(new_entry)
# Мобильный эндпоинт для создания записей календаря
@app.post("/api/v1/calendar/entries/mobile", response_model=CalendarEntryResponse, status_code=201)
async def create_mobile_calendar_entry_endpoint(
mobile_entry: MobileCalendarEntryCreate,
current_user: Dict = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Создать новую запись календаря из мобильного приложения"""
import logging
logging.info(f"Получены данные мобильного приложения: {mobile_entry.model_dump()}")
try:
# Преобразуем симптомы из списка в строку
symptoms_str = ""
if mobile_entry.symptoms:
symptoms_str = ", ".join(mobile_entry.symptoms)
logging.info(f"Преобразованы симптомы в строку: {symptoms_str}")
# Преобразуем данные из мобильного формата в формат сервера
server_entry = CalendarEntryCreate(
entry_date=mobile_entry.date,
entry_type=EntryType.PERIOD if mobile_entry.type == "MENSTRUATION" else
EntryType.OVULATION if mobile_entry.type == "OVULATION" else
EntryType.SYMPTOMS,
flow_intensity=FlowIntensity.from_int(mobile_entry.flow_intensity) if mobile_entry.flow_intensity else None,
mood=MoodType.from_mobile_mood(mobile_entry.mood) if mobile_entry.mood else None,
symptoms=symptoms_str,
notes=mobile_entry.notes,
# Значения по умолчанию для обязательных полей сервера
period_symptoms="",
energy_level=1, # Минимальное значение должно быть 1
sleep_hours=0,
medications="",
)
logging.info(f"Преобразовано в серверный формат: {server_entry}")
# Проверяем существование записи
existing = await db.execute(
select(CalendarEntry).filter(
and_(
CalendarEntry.user_id == current_user["user_id"],
CalendarEntry.entry_date == server_entry.entry_date,
CalendarEntry.entry_type == server_entry.entry_type.value,
)
)
)
existing_entry = existing.scalars().first()
if existing_entry:
logging.info(f"Найдена существующая запись с ID: {existing_entry.id}")
# Если запись существует, обновляем её
if server_entry.flow_intensity is not None:
setattr(existing_entry, 'flow_intensity', server_entry.flow_intensity.value if server_entry.flow_intensity else None)
if server_entry.symptoms is not None:
setattr(existing_entry, 'symptoms', server_entry.symptoms)
if server_entry.mood is not None:
setattr(existing_entry, 'mood', server_entry.mood.value if server_entry.mood else None)
if server_entry.notes is not None:
setattr(existing_entry, 'notes', server_entry.notes)
await db.commit()
await db.refresh(existing_entry)
logging.info("Существующая запись обновлена")
return CalendarEntryResponse.model_validate(existing_entry)
# Создаем новую запись в календаре
new_entry = CalendarEntry(
user_id=current_user["user_id"],
entry_date=server_entry.entry_date,
entry_type=server_entry.entry_type.value,
flow_intensity=server_entry.flow_intensity.value if server_entry.flow_intensity else None,
period_symptoms=server_entry.period_symptoms,
mood=server_entry.mood.value if server_entry.mood else None,
energy_level=server_entry.energy_level,
sleep_hours=server_entry.sleep_hours,
symptoms=server_entry.symptoms,
medications=server_entry.medications,
notes=server_entry.notes,
)
db.add(new_entry)
await db.commit()
await db.refresh(new_entry)
logging.info(f"Создана новая запись с ID: {new_entry.id}")
# Если это запись о менструации, обновляем данные цикла
if server_entry.entry_type == EntryType.PERIOD:
await update_cycle_data(current_user["user_id"], server_entry.entry_date, db)
logging.info("Данные цикла обновлены")
return CalendarEntryResponse.model_validate(new_entry)
except Exception as e:
logging.error(f"Ошибка при создании записи календаря: {str(e)}")
await db.rollback() # Откатываем транзакцию при ошибке
raise HTTPException(status_code=500, detail=f"Ошибка сервера: {str(e)}")
@app.post("/debug/mobile-entry", status_code=200)
async def debug_mobile_calendar_entry(
mobile_entry: MobileCalendarEntryCreate,
db: AsyncSession = Depends(get_db),
):
"""Тестовый эндпоинт для проверки преобразования данных из мобильного формата (без создания записи)"""
# Используем фиктивного пользователя для тестирования
mock_user = {"user_id": 1, "username": "test_user"}
# Преобразуем симптомы из списка в строку
symptoms_str = ""
if mobile_entry.symptoms:
symptoms_str = ", ".join(mobile_entry.symptoms)
# Преобразуем данные из мобильного формата в формат сервера
server_entry = CalendarEntryCreate(
entry_date=mobile_entry.date,
entry_type=EntryType.PERIOD if mobile_entry.type == "MENSTRUATION" else
EntryType.OVULATION if mobile_entry.type == "OVULATION" else
EntryType.SYMPTOMS,
flow_intensity=FlowIntensity.from_int(mobile_entry.flow_intensity) if mobile_entry.flow_intensity else None,
mood=MoodType.from_mobile_mood(mobile_entry.mood) if mobile_entry.mood else None,
symptoms=symptoms_str,
notes=mobile_entry.notes,
# Значения по умолчанию для обязательных полей сервера
period_symptoms="",
energy_level=1, # Минимальное значение должно быть 1
sleep_hours=0,
medications="",
)
# Создаем ответ без сохранения в БД
response_data = {
"id": 0, # Фиктивный ID
"user_id": mock_user["user_id"],
"entry_date": server_entry.entry_date.isoformat(),
"entry_type": server_entry.entry_type.value,
"flow_intensity": server_entry.flow_intensity.value if server_entry.flow_intensity else None,
"period_symptoms": server_entry.period_symptoms,
"mood": server_entry.mood.value if server_entry.mood else None,
"energy_level": server_entry.energy_level,
"sleep_hours": server_entry.sleep_hours,
"symptoms": server_entry.symptoms,
"medications": server_entry.medications,
"notes": server_entry.notes,
"created_at": date.today().isoformat(),
"updated_at": date.today().isoformat(),
"is_active": True
}
return {
"message": "Данные успешно преобразованы из мобильного формата (без сохранения в БД)",
"original_data": mobile_entry.model_dump(),
"transformed_data": response_data
}
@app.delete("/api/v1/entries/{entry_id}")
async def delete_calendar_entry(
@@ -476,14 +631,14 @@ async def create_mobile_calendar_entry(
if existing_entry:
# Если запись существует, обновляем её
if server_entry_data.flow_intensity:
existing_entry.flow_intensity = server_entry_data.flow_intensity.value
if server_entry_data.symptoms:
existing_entry.symptoms = server_entry_data.symptoms
if server_entry_data.mood:
existing_entry.mood = server_entry_data.mood.value
if server_entry_data.notes:
existing_entry.notes = server_entry_data.notes
if server_entry_data.flow_intensity is not None:
setattr(existing_entry, 'flow_intensity', server_entry_data.flow_intensity.value if server_entry_data.flow_intensity else None)
if server_entry_data.symptoms is not None:
setattr(existing_entry, 'symptoms', server_entry_data.symptoms)
if server_entry_data.mood is not None:
setattr(existing_entry, 'mood', server_entry_data.mood.value if server_entry_data.mood else None)
if server_entry_data.notes is not None:
setattr(existing_entry, 'notes', server_entry_data.notes)
await db.commit()
await db.refresh(existing_entry)

View File

@@ -0,0 +1,83 @@
from datetime import date
from enum import Enum
from typing import Optional, Dict, Any, List, Union
from fastapi import Depends, FastAPI, HTTPException
from pydantic import BaseModel, Field, root_validator
from sqlalchemy.ext.asyncio import AsyncSession
from .schemas import (
EntryType, FlowIntensity, MoodType, CalendarEntryCreate, CalendarEntryResponse
)
from .schemas_mobile import (
MobileFlowIntensity, MobileMood, convert_mobile_app_format_to_server
)
from services.calendar_service.models import CalendarEntry
from shared.auth import get_current_user_from_token as get_current_user
from shared.database import get_db
class MobileCalendarEntryCreate(BaseModel):
"""Схема для создания записей в календаре из мобильного приложения"""
date: date
type: str # Тип записи (MENSTRUATION, OVULATION и т.д.)
flow_intensity: Optional[int] = None # Шкала 1-5
symptoms: Optional[List[str]] = None # Массив строк с симптомами
mood: Optional[str] = None # Настроение как строка (NORMAL, HAPPY и т.д.)
notes: Optional[str] = None
async def mobile_create_calendar_entry(
mobile_entry: MobileCalendarEntryCreate,
current_user: Dict = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Создать новую запись в календаре из мобильного приложения"""
# Преобразуем симптомы из списка в строку
symptoms_str = ""
if mobile_entry.symptoms:
symptoms_str = ", ".join(mobile_entry.symptoms)
# Преобразуем формат мобильного приложения в формат сервера
server_entry = CalendarEntryCreate(
entry_date=mobile_entry.date,
entry_type=EntryType.PERIOD if mobile_entry.type == "MENSTRUATION" else
EntryType.OVULATION if mobile_entry.type == "OVULATION" else
EntryType.SYMPTOMS,
flow_intensity=FlowIntensity.from_int(mobile_entry.flow_intensity) if mobile_entry.flow_intensity else None,
mood=MoodType.from_mobile_mood(mobile_entry.mood) if mobile_entry.mood else None,
symptoms=symptoms_str,
notes=mobile_entry.notes,
# Значения по умолчанию для обязательных полей сервера
period_symptoms="",
energy_level=1, # Минимальное значение должно быть 1
sleep_hours=0,
medications="",
)
# Создаем новую запись в календаре
new_entry = CalendarEntry(
user_id=current_user["user_id"],
entry_date=server_entry.entry_date,
entry_type=server_entry.entry_type.value,
flow_intensity=server_entry.flow_intensity.value if server_entry.flow_intensity else None,
period_symptoms=server_entry.period_symptoms,
mood=server_entry.mood.value if server_entry.mood else None,
energy_level=server_entry.energy_level,
sleep_hours=server_entry.sleep_hours,
symptoms=server_entry.symptoms,
medications=server_entry.medications,
notes=server_entry.notes,
)
db.add(new_entry)
await db.commit()
await db.refresh(new_entry)
# Если это запись о менструации, обновляем данные цикла
if server_entry.entry_type == EntryType.PERIOD:
from .main import update_cycle_data
await update_cycle_data(current_user["user_id"], server_entry.entry_date, db)
return CalendarEntryResponse.model_validate(new_entry)

View File

@@ -73,18 +73,38 @@ class MoodType(str, Enum):
TIRED = "tired"
@classmethod
def from_mobile_mood(cls, mood_type: 'MobileMoodType') -> Optional['MoodType']:
"""Конвертировать из мобильного типа настроения"""
if mood_type == MobileMoodType.NORMAL:
def from_mobile_mood(cls, mood_str: str) -> Optional['MoodType']:
"""Конвертировать строку настроения из мобильного приложения"""
if not mood_str:
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)
# Преобразуем строку в 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):

View File

@@ -51,8 +51,9 @@ class MobileMood(str, Enum):
"""Mood values used in the mobile app"""
HAPPY = "HAPPY"
SAD = "SAD"
NORMAL = "NORMAL" # Added for mobile app support
ANXIOUS = "ANXIOUS"
IRRITABLE = "IRRITABLE"
IRRITATED = "IRRITATED"
ENERGETIC = "ENERGETIC"
TIRED = "TIRED"
@@ -65,8 +66,9 @@ class MobileMood(str, Enum):
mapping = {
cls.HAPPY: MoodType.HAPPY,
cls.SAD: MoodType.SAD,
cls.NORMAL: MoodType.HAPPY, # NORMAL maps to HAPPY
cls.ANXIOUS: MoodType.ANXIOUS,
cls.IRRITABLE: MoodType.IRRITATED, # Different spelling
cls.IRRITATED: MoodType.IRRITATED, # Different spelling
cls.ENERGETIC: MoodType.ENERGETIC,
cls.TIRED: MoodType.TIRED,
}
@@ -194,4 +196,71 @@ def convert_server_response_to_mobile_app(
notes=server_response.notes,
created_at=getattr(server_response, "created_at", date.today()),
updated_at=getattr(server_response, "created_at", date.today()), # Fallback to created_at
)
)
def convert_mobile_entry_type(mobile_type: str) -> str:
"""Конвертирует тип записи из мобильного формата в формат API"""
mapping = {
"MENSTRUATION": "period",
"OVULATION": "ovulation",
"FERTILE": "fertile_window",
"OTHER": "other"
}
return mapping.get(mobile_type, "other")
def convert_mobile_flow_intensity(intensity: int) -> str:
"""Конвертирует интенсивность потока из мобильного формата в формат API"""
if intensity is None:
return None
mapping = {
1: "very_light",
2: "light",
3: "medium",
4: "heavy",
5: "very_heavy"
}
return mapping.get(intensity, "medium")
def convert_mobile_mood(mood: str) -> str:
"""Конвертирует настроение из мобильного формата в формат API"""
if mood is None:
return None
mapping = {
"HAPPY": "happy",
"SAD": "sad",
"NORMAL": "happy", # NORMAL мапится на happy
"ANXIOUS": "anxious",
"IRRITATED": "irritated",
"ENERGETIC": "energetic",
"TIRED": "tired"
}
return mapping.get(mood, "happy")
def convert_mobile_symptoms(symptoms: List[str]) -> str:
"""Конвертирует список симптомов из мобильного формата в строку для API"""
if not symptoms:
return ""
# Преобразуем все симптомы в нижний регистр и соединяем запятыми
return ",".join([symptom.lower() for symptom in symptoms])
def mobile_to_api_format(mobile_data: Dict[str, Any]) -> Dict[str, Any]:
"""Конвертирует данные из мобильного формата в формат API"""
api_data = {
"entry_date": mobile_data.get("date"),
"entry_type": convert_mobile_entry_type(mobile_data.get("type")),
"flow_intensity": convert_mobile_flow_intensity(mobile_data.get("flow_intensity")),
"mood": convert_mobile_mood(mobile_data.get("mood")),
"symptoms": convert_mobile_symptoms(mobile_data.get("symptoms")),
"notes": mobile_data.get("notes", "")
}
# Удаляем None значения
return {k: v for k, v in api_data.items() if v is not None}

47
setup_mobile_test.py Executable file
View File

@@ -0,0 +1,47 @@
#!/usr/bin/env python
import json
import requests
import sys
import traceback
from datetime import date
# Запрашиваем токен авторизации (предполагается, что он уже есть в системе)
TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyOSIsImVtYWlsIjoidGVzdDJAZXhhbXBsZS5jb20iLCJleHAiOjE3NTg4NjY5ODJ9._AXkBLeMI4zxC9shFUS3744miuyO8CDnJD1X1AqbLsw"
# Данные для мобильного запроса
mobile_data = {
"date": date.today().isoformat(),
"type": "MENSTRUATION",
"flow_intensity": 3,
"symptoms": ["CRAMPS", "HEADACHE"],
"mood": "NORMAL",
"notes": "Запись из мобильного приложения"
}
# Заголовки с токеном авторизации
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {TOKEN}"
}
# Сохраняем токен в файл для повторного использования
with open('auth_token.txt', 'w') as f:
f.write(TOKEN)
print(f"Токен сохранен в файл auth_token.txt")
print(f"Данные для запроса к мобильному API сохранены в переменной mobile_data")
print("\nПример использования:")
print('curl -v -X POST http://localhost:8004/api/v1/calendar/entries/mobile -H "Content-Type: application/json" -H "Authorization: Bearer $(cat auth_token.txt)" -d \'{"date": "2025-09-26", "type": "MENSTRUATION", "flow_intensity": 3, "symptoms": ["CRAMPS", "HEADACHE"], "mood": "NORMAL", "notes": "Тестовая запись"}\'')
# Сохраняем пример запроса в файл для удобства
with open('mobile_api_example.sh', 'w') as f:
f.write('#!/bin/bash\n\n')
f.write('# Пример запроса к мобильному API календарного сервиса\n')
f.write('curl -v -X POST http://localhost:8004/api/v1/calendar/entries/mobile \\\n')
f.write(' -H "Content-Type: application/json" \\\n')
f.write(' -H "Authorization: Bearer $(cat auth_token.txt)" \\\n')
f.write(' -d \'{"date": "2025-09-26", "type": "MENSTRUATION", "flow_intensity": 3, "symptoms": ["CRAMPS", "HEADACHE"], "mood": "NORMAL", "notes": "Тестовая запись"}\'\n')
print("\nПример запроса также сохранен в файле mobile_api_example.sh")
print("Можно выполнить: chmod +x mobile_api_example.sh && ./mobile_api_example.sh")

66
simple_test.py Normal file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env python
import json
import sys
import requests
import traceback
from datetime import date
# API Gateway endpoint
BASE_URL = "http://localhost:8004"
def main():
try:
print("Начинаем тест мобильного эндпоинта...")
# Проверка работоспособности сервиса
print("Проверяем работоспособность сервиса...")
health_response = requests.get(f"{BASE_URL}/health")
print(f"Ответ о статусе: {health_response.status_code}")
print(f"Тело ответа: {health_response.text}")
# Данные в формате мобильного приложения
mobile_data = {
"date": date.today().isoformat(),
"type": "MENSTRUATION",
"flow_intensity": 3, # средний (1-5)
"symptoms": ["CRAMPS", "HEADACHE"], # массив строк
"mood": "NORMAL",
"notes": "Тестовая запись из мобильного приложения"
}
print(f"Отправляемые данные: {json.dumps(mobile_data, indent=2)}")
# Отправляем запрос к тестовому эндпоинту
print(f"Отправляем запрос на {BASE_URL}/debug/mobile-entry")
response = requests.post(
f"{BASE_URL}/debug/mobile-entry",
json=mobile_data
)
print(f"Статус ответа: {response.status_code}")
print(f"Заголовки ответа: {response.headers}")
try:
response_json = response.json()
print(f"Тело ответа: {json.dumps(response_json, indent=2)}")
except json.JSONDecodeError:
print(f"Ответ не является JSON: {response.text}")
if response.status_code >= 200 and response.status_code < 300:
print("Тест успешно завершен!")
else:
print("Тест завершился с ошибкой.")
sys.exit(1)
except requests.exceptions.ConnectionError:
print(f"Ошибка подключения к {BASE_URL}. Убедитесь, что сервис запущен.")
traceback.print_exc()
sys.exit(1)
except Exception as e:
print(f"Неожиданная ошибка: {e}")
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

103
test_calendar_mobile.py Executable file
View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python3
import json
import sys
import requests
from datetime import date, datetime
import time
# Настройки для тестирования
BASE_URL = "http://localhost:8000" # URL API Gateway
TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyOSIsImVtYWlsIjoidGVzdDJAZXhhbXBsZS5jb20iLCJleHAiOjE3NTg4NjY5ODJ9._AXkBLeMI4zxC9shFUS3744miuyO8CDnJD1X1AqbLsw"
# Функция для добавления записи в календарь через мобильный эндпоинт
def create_mobile_calendar_entry():
url = f"{BASE_URL}/api/v1/calendar/entries/mobile"
# Данные для создания записи в формате мобильного приложения
today_str = date.today().isoformat()
data = {
"date": today_str,
"type": "MENSTRUATION",
"flow_intensity": 3, # medium
"symptoms": ["CRAMPS", "HEADACHE"],
"mood": "HAPPY",
"notes": "Тестовая запись из мобильного приложения"
}
headers = {
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json"
}
print(f"\n=== Тест мобильного эндпоинта ===")
print(f"Отправка запроса POST на {url}")
print(f"Данные: {json.dumps(data, indent=2, ensure_ascii=False)}")
response = requests.post(url, json=data, headers=headers)
print(f"Код ответа: {response.status_code}")
if response.status_code == 201:
print("✓ Запрос успешно выполнен")
print(f"Ответ: {json.dumps(response.json(), indent=2, ensure_ascii=False)}")
return True
else:
print(f"✗ Ошибка при выполнении запроса: {response.text}")
return False
# Функция для получения записей календаря
def get_calendar_entries():
url = f"{BASE_URL}/api/v1/entries"
headers = {
"Authorization": f"Bearer {TOKEN}"
}
print(f"\n=== Получение записей календаря ===")
print(f"Отправка запроса GET на {url}")
response = requests.get(url, headers=headers)
print(f"Код ответа: {response.status_code}")
if response.status_code == 200:
print("✓ Запрос успешно выполнен")
entries = response.json()
print(f"Количество записей: {len(entries)}")
return True
else:
print(f"✗ Ошибка при выполнении запроса: {response.text}")
return False
if __name__ == "__main__":
print("Тестирование API календаря для мобильного приложения")
# Проверяем, что сервисы запущены
try:
health_check = requests.get(f"{BASE_URL}/api/v1/gateway/health")
if health_check.status_code != 200:
print("API Gateway недоступен. Убедитесь, что сервисы запущены.")
sys.exit(1)
except requests.exceptions.ConnectionError:
print("Не удалось подключиться к API Gateway. Убедитесь, что сервисы запущены.")
sys.exit(1)
# Запускаем тесты
success = True
# Тест 1: Создание записи в календаре через мобильный эндпоинт
if not create_mobile_calendar_entry():
success = False
# Тест 2: Проверка получения записей
if not get_calendar_entries():
success = False
# Выводим общий результат
if success:
print("\nВсе тесты пройдены успешно!")
sys.exit(0)
else:
print("\n❌ Некоторые тесты не пройдены.")
sys.exit(1)

30
test_debug_endpoint.sh Executable file
View File

@@ -0,0 +1,30 @@
#!/bin/bash
# Проверяем, запущен ли сервер календаря
if ! curl -s http://localhost:8004/health > /dev/null; then
echo "Сервер календаря не запущен. Запускаем..."
cd /home/trevor/dev/chat
source .venv/bin/activate
PYTHONPATH=/home/trevor/dev/chat python -m uvicorn services.calendar_service.main:app --host 0.0.0.0 --port 8004 &
sleep 5
echo "Сервер запущен в фоновом режиме"
fi
echo "Проверка работоспособности сервера..."
HEALTH_RESPONSE=$(curl -s http://localhost:8004/health)
echo "Ответ сервера: $HEALTH_RESPONSE"
echo -e "\n=== Тестирование отладочного эндпоинта (без аутентификации) ==="
curl -v -X POST http://localhost:8004/debug/mobile-entry \
-H "Content-Type: application/json" \
-d '{
"date": "2023-09-26",
"type": "MENSTRUATION",
"flow_intensity": 3,
"symptoms": ["CRAMPS", "HEADACHE"],
"mood": "NORMAL",
"notes": "Тестовая запись из скрипта"
}'
echo -e "\n\n=== Проверка созданных записей ==="
curl -s http://localhost:8004/debug/entries | head -n 20

121
test_mobile_api.py Executable file
View File

@@ -0,0 +1,121 @@
#!/usr/bin/env python
import json
import requests
from datetime import date
# Используем сохраненный токен авторизации
with open('auth_token.txt', 'r') as f:
TOKEN = f.read().strip()
# Базовый URL для тестирования
BASE_URL = "http://localhost:8004"
HEADERS = {
"Content-Type": "application/json",
"Authorization": f"Bearer {TOKEN}"
}
# Функция для тестирования мобильного эндпоинта
def test_mobile_endpoint():
url = f"{BASE_URL}/api/v1/calendar/entries/mobile"
# Данные для мобильного запроса
data = {
"date": date.today().isoformat(),
"type": "MENSTRUATION",
"flow_intensity": 3,
"symptoms": ["CRAMPS", "HEADACHE"],
"mood": "NORMAL",
"notes": "Тест мобильного API"
}
print("\n1. Тестирование создания записи через мобильный API...")
print(f"POST {url}")
print(f"Данные: {json.dumps(data, ensure_ascii=False)}")
try:
response = requests.post(url, json=data, headers=HEADERS)
if response.status_code == 201:
print(f"✅ Успешно! Статус: {response.status_code}")
print(f"Ответ: {json.dumps(response.json(), ensure_ascii=False, indent=2)}")
# Теперь проверим, что мы можем получить запись
entry_id = response.json().get("id")
return entry_id
else:
print(f"❌ Ошибка! Статус: {response.status_code}")
print(f"Ответ: {response.text}")
return None
except Exception as e:
print(f"❌ Исключение: {str(e)}")
return None
# Функция для проверки получения всех записей
def test_get_entries(entry_id=None):
url = f"{BASE_URL}/api/v1/calendar/entries"
print("\n2. Проверка получения записей календаря...")
print(f"GET {url}")
try:
response = requests.get(url, headers=HEADERS)
if response.status_code == 200:
print(f"✅ Успешно! Статус: {response.status_code}")
entries = response.json()
print(f"Получено записей: {len(entries)}")
# Если у нас есть ID записи, которую мы создали ранее, проверим ее наличие
if entry_id:
found = False
for entry in entries:
if entry.get("id") == entry_id:
found = True
print(f"✅ Созданная запись найдена в списке (ID: {entry_id})")
break
if not found:
print(f"❌ Созданная запись не найдена в списке (ID: {entry_id})")
else:
print(f"❌ Ошибка! Статус: {response.status_code}")
print(f"Ответ: {response.text}")
except Exception as e:
print(f"❌ Исключение: {str(e)}")
# Тестирование отладочного эндпоинта (без аутентификации)
def test_debug_endpoint():
url = f"{BASE_URL}/debug/mobile-entry"
# Данные для мобильного запроса
data = {
"date": date.today().isoformat(),
"type": "MENSTRUATION",
"flow_intensity": 3,
"symptoms": ["CRAMPS", "HEADACHE"],
"mood": "NORMAL",
"notes": "Тест отладочного API"
}
print("\n3. Тестирование отладочного эндпоинта...")
print(f"POST {url}")
print(f"Данные: {json.dumps(data, ensure_ascii=False)}")
try:
response = requests.post(url, json=data)
if response.status_code == 200:
print(f"✅ Успешно! Статус: {response.status_code}")
print(f"Ответ: {json.dumps(response.json(), ensure_ascii=False, indent=2)}")
else:
print(f"❌ Ошибка! Статус: {response.status_code}")
print(f"Ответ: {response.text}")
except Exception as e:
print(f"❌ Исключение: {str(e)}")
if __name__ == "__main__":
print("=== Тестирование мобильного API календарного сервиса ===")
entry_id = test_mobile_endpoint()
test_get_entries(entry_id)
test_debug_endpoint()
print("\nТестирование завершено!")

103
test_mobile_endpoint.py Executable file
View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python
import json
import requests
import sys
from datetime import date
# API Gateway endpoint
BASE_URL = "http://localhost:8004"
# Имя пользователя и пароль для аутентификации
# (предполагается, что у вас есть настроенный доступ)
AUTH_DATA = {
"username": "test_user",
"password": "test_password"
}
# Получение токена аутентификации
def get_token():
# Обычно мы бы получили токен из сервиса аутентификации,
# но для теста будем использовать фиктивный токен
# или можем запросить его из user_service
return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6InRlc3RfdXNlciIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImV4cCI6MTY5ODI0OTc2Mn0.HS5-cPVZwv55h0CZO0q_Z_1vNY9w8B6YsPJytj4UI0A"
# Проверка работоспособности сервиса
def check_health():
try:
response = requests.get(f"{BASE_URL}/health")
if response.status_code == 200:
print(f"Сервис календаря работает. Ответ: {response.json()}")
return True
else:
print(f"Сервис календаря доступен, но возвращает ошибку: {response.status_code}")
return False
except requests.exceptions.ConnectionError:
print(f"Ошибка подключения к {BASE_URL}. Убедитесь, что сервис запущен.")
return False
# Тестовые данные для мобильного приложения
def test_create_calendar_entry_mobile():
try:
token = get_token()
print(f"Токен получен: {token[:15]}...")
# Данные в формате мобильного приложения
mobile_data = {
"date": date.today().isoformat(),
"type": "MENSTRUATION",
"flow_intensity": 3, # средний (1-5)
"symptoms": ["CRAMPS", "HEADACHE"], # массив строк
"mood": "NORMAL",
"notes": "Тестовая запись из мобильного приложения"
}
print(f"Отправляемые данные: {json.dumps(mobile_data, indent=2)}")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
}
print(f"Отправляем запрос на {BASE_URL}/debug/mobile-entry")
# Используем тестовый эндпоинт без аутентификации
print(f"Отправляем запрос на {BASE_URL}/debug/mobile-entry")
response = requests.post(
f"{BASE_URL}/debug/mobile-entry",
json=mobile_data
)
print(f"Статус ответа: {response.status_code}")
if response.status_code >= 200 and response.status_code < 300:
print(f"Тело успешного ответа: {json.dumps(response.json(), indent=2)}")
return response.json()
else:
print("Ошибка при создании записи")
try:
print(f"Тело ответа с ошибкой: {json.dumps(response.json(), indent=2)}")
except json.JSONDecodeError:
print(f"Тело ответа не является JSON: {response.text}")
return None
except requests.exceptions.RequestException as e:
print(f"Ошибка при выполнении запроса: {e}")
return None
except Exception as e:
print(f"Неожиданная ошибка: {e}")
return None
if __name__ == "__main__":
print("Тестирование создания записи календаря из мобильного приложения...")
if not check_health():
print("Сервис недоступен. Тестирование прекращено.")
sys.exit(1)
result = test_create_calendar_entry_mobile()
if result:
print("Тест успешно завершен!")
else:
print("Тест завершился с ошибкой.")
sys.exit(1)

171
test_mobile_endpoints.py Normal file
View File

@@ -0,0 +1,171 @@
#!/usr/bin/env python
import json
import requests
import sys
import traceback
from datetime import date
# API Gateway endpoint
BASE_URL = "http://localhost:8004"
# Токен для аутентификации - замените на действующий токен
AUTH_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyOSIsImVtYWlsIjoidGVzdDJAZXhhbXBsZS5jb20iLCJleHAiOjE3NTg4NjY5ODJ9._AXkBLeMI4zxC9shFUS3744miuyO8CDnJD1X1AqbLsw"
def test_health():
"""Проверка доступности сервиса"""
try:
response = requests.get(f"{BASE_URL}/health")
print(f"Статус сервиса: {response.status_code}")
print(f"Ответ: {response.text}")
return response.status_code == 200
except Exception as e:
print(f"Ошибка при проверке сервиса: {e}")
return False
def test_authenticated_endpoint():
"""Тестирование аутентифицированного эндпоинта для мобильного приложения"""
print("\n=== Тестирование аутентифицированного эндпоинта ===")
# Данные в формате мобильного приложения
mobile_data = {
"date": date.today().isoformat(),
"type": "MENSTRUATION",
"flow_intensity": 3,
"symptoms": ["CRAMPS", "HEADACHE"],
"mood": "NORMAL",
"notes": "Запись из мобильного приложения через аутентифицированный эндпоинт"
}
print(f"Отправляемые данные: {json.dumps(mobile_data, indent=2)}")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {AUTH_TOKEN}"
}
try:
response = requests.post(
f"{BASE_URL}/api/v1/calendar/entries/mobile",
headers=headers,
json=mobile_data,
timeout=10
)
print(f"Статус ответа: {response.status_code}")
if response.status_code >= 200 and response.status_code < 300:
print(f"Тело успешного ответа: {json.dumps(response.json(), indent=2)}")
return True
else:
print("Ошибка при создании записи через аутентифицированный эндпоинт")
try:
print(f"Тело ответа с ошибкой: {json.dumps(response.json(), indent=2)}")
except:
print(f"Тело ответа не является JSON: {response.text}")
return False
except Exception as e:
print(f"Ошибка при выполнении запроса: {e}")
traceback.print_exc()
return False
def test_debug_endpoint():
"""Тестирование отладочного эндпоинта для мобильного приложения (без аутентификации)"""
print("\n=== Тестирование отладочного эндпоинта (без аутентификации) ===")
# Данные в формате мобильного приложения
mobile_data = {
"date": date.today().isoformat(),
"type": "MENSTRUATION",
"flow_intensity": 4,
"symptoms": ["BACKACHE", "BLOATING"],
"mood": "HAPPY",
"notes": "Запись из мобильного приложения через отладочный эндпоинт"
}
print(f"Отправляемые данные: {json.dumps(mobile_data, indent=2)}")
headers = {
"Content-Type": "application/json"
}
try:
response = requests.post(
f"{BASE_URL}/debug/mobile-entry",
headers=headers,
json=mobile_data,
timeout=10
)
print(f"Статус ответа: {response.status_code}")
if response.status_code >= 200 and response.status_code < 300:
print(f"Тело успешного ответа: {json.dumps(response.json(), indent=2)}")
return True
else:
print("Ошибка при создании записи через отладочный эндпоинт")
try:
print(f"Тело ответа с ошибкой: {json.dumps(response.json(), indent=2)}")
except:
print(f"Тело ответа не является JSON: {response.text}")
return False
except Exception as e:
print(f"Ошибка при выполнении запроса: {e}")
traceback.print_exc()
return False
def verify_entries_created():
"""Проверка, что записи были созданы в БД"""
print("\n=== Проверка созданных записей ===")
try:
response = requests.get(f"{BASE_URL}/debug/entries")
print(f"Статус ответа: {response.status_code}")
if response.status_code == 200:
entries = response.json()
print(f"Количество записей в БД: {len(entries)}")
print("Последние 2 записи:")
for entry in entries[-2:]:
print(json.dumps(entry, indent=2))
return True
else:
print(f"Ошибка при получении записей: {response.status_code}")
return False
except Exception as e:
print(f"Ошибка при проверке записей: {e}")
traceback.print_exc()
return False
def main():
print("=== Тестирование мобильных эндпоинтов календарного сервиса ===")
if not test_health():
print("Сервис недоступен. Завершение тестирования.")
return 1
debug_result = test_debug_endpoint()
auth_result = test_authenticated_endpoint()
if debug_result and auth_result:
print("\nВсе тесты успешно пройдены!")
verify_entries_created()
return 0
else:
print("\nНекоторые тесты не пройдены.")
if debug_result:
print("✓ Отладочный эндпоинт работает")
else:
print("✗ Отладочный эндпоинт не работает")
if auth_result:
print("✓ Аутентифицированный эндпоинт работает")
else:
print("✗ Аутентифицированный эндпоинт не работает")
verify_entries_created()
return 1
if __name__ == "__main__":
sys.exit(main())

184
test_standalone.py Normal file
View File

@@ -0,0 +1,184 @@
#!/usr/bin/env python
import json
import sys
import asyncio
import uvicorn
from fastapi import FastAPI, Depends, HTTPException
from datetime import date
from typing import Dict, Optional, List
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
# Импортируем необходимые модули из календарного сервиса
from services.calendar_service.schemas import (
EntryType, FlowIntensity, MoodType, CalendarEntryCreate, CalendarEntryResponse
)
from services.calendar_service.mobile_endpoint import MobileCalendarEntryCreate
from shared.database import get_db
# Создаем тестовое приложение
app = FastAPI(title="Test Mobile Endpoint")
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {"status": "healthy", "service": "test_mobile_endpoint"}
@app.post("/debug/mobile-entry", status_code=201)
async def test_mobile_endpoint(
mobile_entry: MobileCalendarEntryCreate,
db: AsyncSession = Depends(get_db),
):
"""Test mobile endpoint without authentication"""
# Преобразуем симптомы из списка в строку
symptoms_str = ""
if mobile_entry.symptoms:
symptoms_str = ", ".join(mobile_entry.symptoms)
# Преобразуем данные из мобильного формата в формат сервера
entry_type = EntryType.PERIOD
if mobile_entry.type == "MENSTRUATION":
entry_type = EntryType.PERIOD
elif mobile_entry.type == "OVULATION":
entry_type = EntryType.OVULATION
else:
entry_type = EntryType.SYMPTOMS
# Преобразуем интенсивность потока
flow_intensity = None
if mobile_entry.flow_intensity is not None:
if mobile_entry.flow_intensity <= 1:
flow_intensity = FlowIntensity.SPOTTING
elif mobile_entry.flow_intensity <= 2:
flow_intensity = FlowIntensity.LIGHT
elif mobile_entry.flow_intensity <= 4:
flow_intensity = FlowIntensity.MEDIUM
else:
flow_intensity = FlowIntensity.HEAVY
# Преобразуем настроение
mood = None
if mobile_entry.mood:
if mobile_entry.mood == "HAPPY":
mood = MoodType.HAPPY
elif mobile_entry.mood == "SAD":
mood = MoodType.SAD
elif mobile_entry.mood == "NORMAL":
mood = MoodType.HAPPY # NORMAL мапится на HAPPY
elif mobile_entry.mood == "ANXIOUS":
mood = MoodType.ANXIOUS
elif mobile_entry.mood == "IRRITATED":
mood = MoodType.IRRITATED
elif mobile_entry.mood == "ENERGETIC":
mood = MoodType.ENERGETIC
elif mobile_entry.mood == "TIRED":
mood = MoodType.TIRED
# Создаем объект CalendarEntryResponse для возврата
response = {
"id": 1,
"user_id": 1,
"entry_date": mobile_entry.date.isoformat(),
"entry_type": entry_type.value,
"flow_intensity": flow_intensity.value if flow_intensity else None,
"period_symptoms": "",
"mood": mood.value if mood else None,
"energy_level": 1, # Минимальное значение должно быть 1
"sleep_hours": 0,
"symptoms": symptoms_str,
"medications": "",
"notes": mobile_entry.notes or "",
"created_at": date.today().isoformat(),
"updated_at": date.today().isoformat(),
}
return response
# Функция для отправки тестового запроса
async def send_test_request():
"""Отправка тестового запроса к эндпоинту"""
import httpx
# Данные в формате мобильного приложения
mobile_data = {
"date": date.today().isoformat(),
"type": "MENSTRUATION",
"flow_intensity": 3, # средний (1-5)
"symptoms": ["CRAMPS", "HEADACHE"], # массив строк
"mood": "NORMAL",
"notes": "Тестовая запись из мобильного приложения"
}
print(f"Отправляемые данные: {json.dumps(mobile_data, indent=2)}")
# Подождем, пока сервис запустится
await asyncio.sleep(2)
async with httpx.AsyncClient() as client:
try:
# Проверяем работоспособность сервиса
health_response = await client.get("http://localhost:8080/health")
print(f"Проверка работоспособности: {health_response.status_code}")
print(f"Ответ сервиса: {health_response.text}")
# Отправляем запрос к тестовому эндпоинту
response = await client.post(
"http://localhost:8080/debug/mobile-entry",
json=mobile_data
)
print(f"Статус ответа: {response.status_code}")
if response.status_code >= 200 and response.status_code < 300:
print(f"Тело успешного ответа: {json.dumps(response.json(), indent=2)}")
return True
else:
print("Ошибка при создании записи")
try:
print(f"Тело ответа с ошибкой: {json.dumps(response.json(), indent=2)}")
except:
print(f"Тело ответа не является JSON: {response.text}")
return False
except Exception as e:
print(f"Ошибка при выполнении запроса: {e}")
return False
# Основная функция
async def main():
# Запускаем сервер в отдельном процессе
config = uvicorn.Config(app, host="0.0.0.0", port=8080, log_level="info")
server = uvicorn.Server(config)
# Создаем задачу для запуска сервера
server_task = asyncio.create_task(server.serve())
# Дадим серверу немного времени на запуск
await asyncio.sleep(1)
try:
# Отправляем тестовый запрос
result = await send_test_request()
if result:
print("Тест успешно завершен!")
return 0
else:
print("Тест завершился с ошибкой.")
return 1
finally:
# Останавливаем сервер
server.should_exit = True
await server_task
if __name__ == "__main__":
# Запускаем асинхронную функцию main()
import asyncio
try:
exit_code = asyncio.run(main())
sys.exit(exit_code)
except KeyboardInterrupt:
print("Выполнение прервано")
sys.exit(1)

83
update_token.py Executable file
View File

@@ -0,0 +1,83 @@
#!/usr/bin/env python
import requests
import json
# Базовый URL для тестирования
BASE_URL = "http://localhost:8001" # User service
def register_user():
"""Регистрация нового пользователя"""
url = f"{BASE_URL}/api/v1/auth/register"
data = {
"email": "test_mobile@example.com",
"password": "password123",
"first_name": "Test",
"last_name": "Mobile"
}
print(f"Регистрация пользователя: {json.dumps(data, ensure_ascii=False)}")
try:
response = requests.post(url, json=data)
if response.status_code == 201:
print("✅ Пользователь успешно зарегистрирован")
user_data = response.json()
print(f"ID пользователя: {user_data.get('id')}")
return user_data
else:
print(f"❌ Ошибка при регистрации: {response.status_code}")
print(f"Ответ: {response.text}")
return None
except Exception as e:
print(f"❌ Исключение: {str(e)}")
return None
def login_user(email, password):
"""Вход пользователя и получение токена"""
url = f"{BASE_URL}/api/v1/auth/token"
data = {
"username": email,
"password": password
}
print(f"Вход пользователя: {email}")
try:
response = requests.post(url, data=data)
if response.status_code == 200:
token_data = response.json()
print("✅ Успешный вход")
# Сохраняем токен
token = token_data.get("access_token")
with open("auth_token.txt", "w") as f:
f.write(token)
print(f"✅ Токен сохранен в файл auth_token.txt")
return token
else:
print(f"❌ Ошибка при входе: {response.status_code}")
print(f"Ответ: {response.text}")
return None
except Exception as e:
print(f"❌ Исключение: {str(e)}")
return None
if __name__ == "__main__":
print("=== Обновление токена авторизации ===")
# Пробуем выполнить вход с существующими данными
token = login_user("test_mobile@example.com", "password123")
if not token:
# Если не удалось войти, регистрируем нового пользователя
user = register_user()
if user:
token = login_user("test_mobile@example.com", "password123")
print("\nПроцесс обновления токена завершен!")
if token:
print("Теперь вы можете использовать скрипт test_mobile_api.py для тестирования API")