brief-rags-bench/app/interfaces/base.py

308 lines
10 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.

"""
TgBackendInterface base class implementation.
Базовый интерфейс для всех HTTP API клиентов с использованием httpx.
Предоставляет автоматическую сериализацию/десериализацию Pydantic моделей,
error handling и логирование.
"""
import logging
from typing import Optional, Type, TypeVar, Any, Dict
from pydantic import BaseModel, ValidationError
import httpx
T = TypeVar('T', bound=BaseModel)
logger = logging.getLogger(__name__)
class TgBackendInterface:
"""
Базовый интерфейс для HTTP API клиентов.
Возможности:
- httpx.AsyncClient для асинхронных HTTP запросов
- Автоматическая сериализация Pydantic моделей в JSON
- Автоматическая десериализация JSON в Pydantic модели
- Error handling с детальными логами
- Настраиваемые timeout и retry параметры
"""
def __init__(
self,
api_prefix: str,
timeout: float = 30.0,
max_retries: int = 3,
**kwargs
):
"""
Инициализация клиента.
Args:
api_prefix: Базовый URL API (например, http://db-api:8080/api/v1)
timeout: Таймаут запроса в секундах (default: 30)
max_retries: Максимальное количество повторных попыток (default: 3)
**kwargs: Дополнительные параметры для httpx.AsyncClient
"""
self.api_prefix = api_prefix.rstrip('/')
self.client = httpx.AsyncClient(
timeout=httpx.Timeout(timeout),
transport=httpx.AsyncHTTPTransport(retries=max_retries),
follow_redirects=True,
**kwargs
)
logger.info(f"TgBackendInterface initialized with api_prefix: {self.api_prefix}")
async def close(self):
"""Закрыть HTTP клиент."""
await self.client.aclose()
logger.info("TgBackendInterface client closed")
async def __aenter__(self):
"""Async context manager entry."""
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Async context manager exit."""
await self.close()
def _build_url(self, path: str) -> str:
"""
Построить полный URL из api_prefix и path.
Args:
path: Путь эндпоинта (может начинаться с / или без)
Returns:
Полный URL
"""
if not path.startswith('/'):
path = '/' + path
return f"{self.api_prefix}{path}"
def _serialize_body(self, body: Optional[BaseModel]) -> Optional[Dict]:
"""
Сериализовать Pydantic модель в dict для JSON.
Args:
body: Pydantic модель
Returns:
Dict с данными или None
"""
if body is None:
return None
return body.model_dump(mode='json', exclude_none=False)
def _deserialize_response(
self,
data: Any,
response_model: Optional[Type[T]]
) -> Any:
"""
Десериализовать JSON ответ в Pydantic модель.
Args:
data: Данные из JSON ответа
response_model: Pydantic модель для валидации
Returns:
Экземпляр Pydantic модели или исходные данные
"""
if response_model is None:
return data
try:
return response_model(**data) if isinstance(data, dict) else response_model(data)
except ValidationError as e:
logger.error(f"Validation error for {response_model.__name__}: {e}")
raise
async def _handle_response(
self,
response: httpx.Response,
response_model: Optional[Type[T]] = None
) -> Any:
"""
Обработать HTTP ответ.
Args:
response: HTTP ответ от httpx
response_model: Pydantic модель для валидации
Returns:
Десериализованные данные
Raises:
httpx.HTTPStatusError: При HTTP ошибках
"""
try:
response.raise_for_status()
except httpx.HTTPStatusError as e:
logger.error(
f"HTTP error {e.response.status_code} for {e.request.url}: "
f"{e.response.text}"
)
raise
if response.status_code == 204 or len(response.content) == 0:
return {}
try:
data = response.json()
except Exception as e:
logger.error(f"Failed to parse JSON response: {e}")
logger.debug(f"Response content: {response.text}")
raise
return self._deserialize_response(data, response_model)
async def get(
self,
path: str,
params: Optional[dict] = None,
response_model: Optional[Type[T]] = None,
**kwargs
) -> Any:
"""
HTTP GET запрос к {api_prefix}{path}.
Args:
path: Путь эндпоинта
params: Query параметры
response_model: Pydantic модель для валидации ответа
**kwargs: Дополнительные параметры для httpx
Returns:
Десериализованный ответ (Pydantic модель или dict)
Raises:
httpx.HTTPStatusError: При HTTP ошибках
ValidationError: При ошибках валидации Pydantic
"""
url = self._build_url(path)
logger.debug(f"GET {url} with params={params}")
response = await self.client.get(url, params=params, **kwargs)
return await self._handle_response(response, response_model)
async def post(
self,
path: str,
body: Optional[BaseModel] = None,
response_model: Optional[Type[T]] = None,
**kwargs
) -> Any:
"""
HTTP POST запрос к {api_prefix}{path}.
Args:
path: Путь эндпоинта
body: Pydantic модель для тела запроса
response_model: Pydantic модель для валидации ответа
**kwargs: Дополнительные параметры для httpx
Returns:
Десериализованный ответ (Pydantic модель или dict)
Raises:
httpx.HTTPStatusError: При HTTP ошибках
ValidationError: При ошибках валидации Pydantic
"""
url = self._build_url(path)
json_body = self._serialize_body(body)
logger.debug(f"POST {url} with body={json_body}")
response = await self.client.post(url, json=json_body, **kwargs)
return await self._handle_response(response, response_model)
async def put(
self,
path: str,
body: Optional[BaseModel] = None,
response_model: Optional[Type[T]] = None,
**kwargs
) -> Any:
"""
HTTP PUT запрос к {api_prefix}{path}.
Args:
path: Путь эндпоинта
body: Pydantic модель для тела запроса
response_model: Pydantic модель для валидации ответа
**kwargs: Дополнительные параметры для httpx
Returns:
Десериализованный ответ (Pydantic модель или dict)
Raises:
httpx.HTTPStatusError: При HTTP ошибках
ValidationError: При ошибках валидации Pydantic
"""
url = self._build_url(path)
json_body = self._serialize_body(body)
logger.debug(f"PUT {url} with body={json_body}")
response = await self.client.put(url, json=json_body, **kwargs)
return await self._handle_response(response, response_model)
async def patch(
self,
path: str,
body: Optional[BaseModel] = None,
response_model: Optional[Type[T]] = None,
**kwargs
) -> Any:
"""
HTTP PATCH запрос к {api_prefix}{path}.
Args:
path: Путь эндпоинта
body: Pydantic модель для тела запроса
response_model: Pydantic модель для валидации ответа
**kwargs: Дополнительные параметры для httpx
Returns:
Десериализованный ответ (Pydantic модель или dict)
Raises:
httpx.HTTPStatusError: При HTTP ошибках
ValidationError: При ошибках валидации Pydantic
"""
url = self._build_url(path)
json_body = self._serialize_body(body)
logger.debug(f"PATCH {url} with body={json_body}")
response = await self.client.patch(url, json=json_body, **kwargs)
return await self._handle_response(response, response_model)
async def delete(
self,
path: str,
**kwargs
) -> Any:
"""
HTTP DELETE запрос к {api_prefix}{path}.
Args:
path: Путь эндпоинта
**kwargs: Дополнительные параметры для httpx
Returns:
Ответ от сервера (обычно пустой dict для 204)
Raises:
httpx.HTTPStatusError: При HTTP ошибках
"""
url = self._build_url(path)
logger.debug(f"DELETE {url}")
response = await self.client.delete(url, **kwargs)
return await self._handle_response(response)