""" 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 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)