diff --git a/docs/MOBILE_API.md b/docs/MOBILE_API.md new file mode 100644 index 0000000..1eda981 --- /dev/null +++ b/docs/MOBILE_API.md @@ -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 | Внутренняя ошибка сервера | + +## Контактная информация + +При возникновении вопросов или проблем с интеграцией, пожалуйста, свяжитесь с командой разработки. \ No newline at end of file diff --git a/mobile_api_example.sh b/mobile_api_example.sh new file mode 100644 index 0000000..30fefa4 --- /dev/null +++ b/mobile_api_example.sh @@ -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": "Тестовая запись"}' diff --git a/mobile_format_test.py b/mobile_format_test.py new file mode 100755 index 0000000..dd18ec5 --- /dev/null +++ b/mobile_format_test.py @@ -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=== ТЕСТИРОВАНИЕ ЗАВЕРШЕНО ===") \ No newline at end of file diff --git a/services/calendar_service/main.py b/services/calendar_service/main.py index ab67558..9d939b8 100644 --- a/services/calendar_service/main.py +++ b/services/calendar_service/main.py @@ -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) diff --git a/services/calendar_service/mobile_endpoint.py b/services/calendar_service/mobile_endpoint.py new file mode 100644 index 0000000..cd2ddad --- /dev/null +++ b/services/calendar_service/mobile_endpoint.py @@ -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) \ No newline at end of file diff --git a/services/calendar_service/schemas.py b/services/calendar_service/schemas.py index 6e7d52b..dbde2c8 100644 --- a/services/calendar_service/schemas.py +++ b/services/calendar_service/schemas.py @@ -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): diff --git a/services/calendar_service/schemas_mobile.py b/services/calendar_service/schemas_mobile.py index 4ab7814..bab961a 100644 --- a/services/calendar_service/schemas_mobile.py +++ b/services/calendar_service/schemas_mobile.py @@ -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 - ) \ No newline at end of file + ) + + +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} diff --git a/setup_mobile_test.py b/setup_mobile_test.py new file mode 100755 index 0000000..a6a1a01 --- /dev/null +++ b/setup_mobile_test.py @@ -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") \ No newline at end of file diff --git a/simple_test.py b/simple_test.py new file mode 100644 index 0000000..fc6adcb --- /dev/null +++ b/simple_test.py @@ -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() \ No newline at end of file diff --git a/test_calendar_mobile.py b/test_calendar_mobile.py new file mode 100755 index 0000000..40c3be1 --- /dev/null +++ b/test_calendar_mobile.py @@ -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) \ No newline at end of file diff --git a/test_debug_endpoint.sh b/test_debug_endpoint.sh new file mode 100755 index 0000000..2a804b9 --- /dev/null +++ b/test_debug_endpoint.sh @@ -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 \ No newline at end of file diff --git a/test_mobile_api.py b/test_mobile_api.py new file mode 100755 index 0000000..3319914 --- /dev/null +++ b/test_mobile_api.py @@ -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Тестирование завершено!") \ No newline at end of file diff --git a/test_mobile_endpoint.py b/test_mobile_endpoint.py new file mode 100755 index 0000000..2de87bf --- /dev/null +++ b/test_mobile_endpoint.py @@ -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) \ No newline at end of file diff --git a/test_mobile_endpoints.py b/test_mobile_endpoints.py new file mode 100644 index 0000000..5c61199 --- /dev/null +++ b/test_mobile_endpoints.py @@ -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()) \ No newline at end of file diff --git a/test_standalone.py b/test_standalone.py new file mode 100644 index 0000000..4fcc564 --- /dev/null +++ b/test_standalone.py @@ -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) \ No newline at end of file diff --git a/update_token.py b/update_token.py new file mode 100755 index 0000000..66ba56a --- /dev/null +++ b/update_token.py @@ -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") \ No newline at end of file