Files
bot-telegram/app.bot/api_client.py
2025-09-22 12:35:41 +05:00

307 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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