""" HTTP клиент для взаимодействия с API """ import aiohttp import asyncio import logging from typing import Optional, List, Dict, Any, Union from dataclasses import dataclass import json from bot_config import config logger = logging.getLogger(__name__) @dataclass class APIResponse: """Стандартный ответ API""" success: bool data: Any = None error: Optional[str] = None status_code: int = 200 class APIClient: """HTTP клиент для работы с FastAPI""" def __init__(self, base_url: str = None, timeout: int = 30): self.base_url = base_url or config.API_BASE_URL self.timeout = timeout self.session: Optional[aiohttp.ClientSession] = None async def __aenter__(self): """Асинхронный контекстный менеджер""" await self.create_session() return self async def __aexit__(self, exc_type, exc_val, exc_tb): """Закрытие сессии""" await self.close_session() async def create_session(self): """Создание HTTP сессии""" if not self.session or self.session.closed: timeout = aiohttp.ClientTimeout(total=self.timeout) self.session = aiohttp.ClientSession( timeout=timeout, headers={'Content-Type': 'application/json'} ) async def close_session(self): """Закрытие HTTP сессии""" if self.session and not self.session.closed: await self.session.close() async def _make_request( self, method: str, endpoint: str, data: Optional[Dict] = None, params: Optional[Dict] = None, files: Optional[Dict] = None ) -> APIResponse: """Базовый метод для HTTP запросов""" await self.create_session() url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}" try: # Если есть файлы, не устанавливаем Content-Type headers = {} if not files: headers['Content-Type'] = 'application/json' kwargs = { 'params': params, 'headers': headers } # Данные для запроса if files: # Для файлов используем FormData kwargs['data'] = aiohttp.FormData() if data: for key, value in data.items(): kwargs['data'].add_field(key, str(value)) for key, file_data in files.items(): kwargs['data'].add_field(key, file_data) elif data: kwargs['data'] = json.dumps(data) async with self.session.request(method, url, **kwargs) as response: status_code = response.status # Обработка ответа if response.content_type == 'application/json': response_data = await response.json() else: response_data = await response.text() if 200 <= status_code < 300: return APIResponse( success=True, data=response_data, status_code=status_code ) else: error_msg = response_data.get('detail', f'HTTP {status_code}') if isinstance(response_data, dict) else str(response_data) logger.error(f"API Error {status_code}: {error_msg}") return APIResponse( success=False, error=error_msg, status_code=status_code ) except aiohttp.ClientError as e: logger.error(f"HTTP Client Error: {e}") return APIResponse( success=False, error=f"Connection error: {str(e)}", status_code=500 ) except Exception as e: logger.error(f"Unexpected error: {e}") return APIResponse( success=False, error=f"Unexpected error: {str(e)}", status_code=500 ) # ===== ПОЛЬЗОВАТЕЛИ ===== async def get_user_by_telegram_id(self, telegram_id: int) -> APIResponse: """Получение пользователя по Telegram ID""" return await self._make_request('GET', f'/users/{telegram_id}') async def create_user(self, user_data: Dict) -> APIResponse: """Создание нового пользователя""" return await self._make_request('POST', '/users/', data=user_data) async def update_user(self, telegram_id: int, user_data: Dict) -> APIResponse: """Обновление данных пользователя""" return await self._make_request('PUT', f'/users/{telegram_id}', data=user_data) async def get_all_users(self, limit: int = 100, offset: int = 0, is_active: Optional[bool] = None) -> APIResponse: """Получение списка пользователей""" params = {'limit': limit, 'offset': offset} if is_active is not None: params['is_active'] = is_active return await self._make_request('GET', '/users/', params=params) # ===== ТЕСТЫ ===== async def get_tests(self, is_active: Optional[bool] = None, category_id: Optional[int] = None) -> APIResponse: """Получение списка тестов""" params = {} if is_active is not None: params['is_active'] = is_active if category_id is not None: params['category_id'] = category_id return await self._make_request('GET', '/tests/', params=params) async def get_test_by_id(self, test_id: int) -> APIResponse: """Получение теста по ID""" return await self._make_request('GET', f'/tests/{test_id}') async def create_test(self, test_data: Dict) -> APIResponse: """Создание нового теста""" return await self._make_request('POST', '/tests/', data=test_data) # ===== ВОПРОСЫ ===== async def get_questions_by_test(self, test_id: int) -> APIResponse: """Получение всех вопросов для теста""" return await self._make_request('GET', f'/questions/test/{test_id}') async def get_question_by_id(self, question_id: int) -> APIResponse: """Получение вопроса по ID""" return await self._make_request('GET', f'/questions/{question_id}') async def create_question(self, question_data: Dict) -> APIResponse: """Создание нового вопроса""" return await self._make_request('POST', '/questions/', data=question_data) # ===== ВАРИАНТЫ ОТВЕТОВ ===== async def get_answer_options_by_question(self, question_id: int) -> APIResponse: """Получение вариантов ответов для вопроса""" return await self._make_request('GET', f'/answer-options/question/{question_id}') async def create_answer_option(self, option_data: Dict) -> APIResponse: """Создание варианта ответа""" return await self._make_request('POST', '/answer-options/', data=option_data) # ===== ИЗОБРАЖЕНИЯ ===== async def upload_image(self, file_content: bytes, filename: str, content_type: str, alt_text: Optional[str] = None) -> APIResponse: """Загрузка изображения""" files = {'file': (filename, file_content, content_type)} data = {} if alt_text: data['alt_text'] = alt_text return await self._make_request('POST', '/images/upload', data=data, files=files) async def get_image(self, image_id: int) -> APIResponse: """Получение изображения по ID""" return await self._make_request('GET', f'/images/{image_id}') # ===== НАСТРОЙКИ ===== async def get_all_settings(self) -> APIResponse: """Получение всех настроек""" return await self._make_request('GET', '/settings/') async def get_setting(self, key: str) -> APIResponse: """Получение настройки по ключу""" return await self._make_request('GET', f'/settings/{key}') async def create_setting(self, setting_data: Dict) -> APIResponse: """Создание новой настройки""" return await self._make_request('POST', '/settings/', data=setting_data) async def update_setting(self, key: str, setting_data: Dict) -> APIResponse: """Обновление настройки""" return await self._make_request('PUT', f'/settings/{key}', data=setting_data) # ===== СООБЩЕНИЯ ===== async def create_message(self, message_data: Dict) -> APIResponse: """Создание записи о сообщении""" return await self._make_request('POST', '/messages/', data=message_data) async def get_messages(self, user_id: Optional[int] = None, message_type: Optional[str] = None, limit: int = 100) -> APIResponse: """Получение списка сообщений""" params = {'limit': limit} if user_id: params['user_id'] = user_id if message_type: params['message_type'] = message_type return await self._make_request('GET', '/messages/', params=params) # ===== УТИЛИТЫ ===== async def health_check(self) -> bool: """Проверка доступности API""" try: response = await self._make_request('GET', '/health') return response.success except Exception as e: logger.error(f"Health check failed: {e}") return False # Глобальный экземпляр клиента api_client = APIClient() # Вспомогательные функции для удобства использования async def ensure_user_exists(telegram_id: int, user_data: Dict) -> Optional[Dict]: """Убедиться, что пользователь существует, создать если нет""" async with api_client: # Попробуем получить пользователя response = await api_client.get_user_by_telegram_id(telegram_id) if response.success: return response.data # Если пользователь не найден, создаем нового if response.status_code == 404: create_response = await api_client.create_user(user_data) if create_response.success: logger.info(f"Создан новый пользователь: {telegram_id}") return create_response.data else: logger.error(f"Ошибка создания пользователя: {create_response.error}") return None else: logger.error(f"Ошибка получения пользователя: {response.error}") return None async def get_available_tests() -> List[Dict]: """Получить доступные тесты""" async with api_client: response = await api_client.get_tests(is_active=True) if response.success: return response.data else: logger.error(f"Ошибка получения тестов: {response.error}") return [] async def get_test_questions_with_options(test_id: int) -> List[Dict]: """Получить вопросы теста с вариантами ответов""" async with api_client: # Получаем вопросы questions_response = await api_client.get_questions_by_test(test_id) if not questions_response.success: logger.error(f"Ошибка получения вопросов: {questions_response.error}") return [] questions = questions_response.data # Для каждого вопроса получаем варианты ответов for question in questions: options_response = await api_client.get_answer_options_by_question(question['id']) if options_response.success: question['options'] = options_response.data else: question['options'] = [] logger.error(f"Ошибка получения вариантов для вопроса {question['id']}: {options_response.error}") return questions