from __future__ import annotations
from typing import Any, Dict, Optional, Type
import httpx
[документация]
class TBankError(Exception):
"""Базовое исключение SDK."""
[документация]
class TBankNetworkError(TBankError):
"""Сетевой сбой (соединение/таймаут) после исчерпания ретраев."""
[документация]
class TBankTimeoutError(TBankNetworkError):
"""Истёк таймаут запроса."""
[документация]
class MutualTLSRequiredError(TBankError):
"""Вызван secured-метод, но клиент создан без mTLS-сертификата."""
[документация]
class TBankAPIError(TBankError):
"""Логическая ошибка API (Success=false / HTTP 4xx-5xx)."""
def __init__(
self,
*,
code: str,
message: str,
details: Optional[str] = None,
http_status: Optional[int] = None,
status: Optional[str] = None,
error_id: Optional[str] = None,
) -> None:
self.code = code
self.message = message
self.details = details
self.http_status = http_status
self.status = status
self.error_id = error_id
text = f"[{code}] {message}"
if details:
text += f" ({details})"
super().__init__(text)
[документация]
class AuthenticationError(TBankAPIError):
"""Ошибка аутентификации/подписи."""
[документация]
class InvalidRequestError(TBankAPIError):
"""Некорректный запрос/параметры."""
[документация]
class InsufficientFundsError(TBankAPIError):
"""Недостаточно средств на карте."""
[документация]
class ThreeDSError(TBankAPIError):
"""Не пройдена аутентификация 3DS."""
[документация]
class TerminalBlockedError(TBankAPIError):
"""Операция заблокирована для терминала."""
[документация]
class ForbiddenError(TBankAPIError):
"""Доступ запрещён (неизвестный IP / нехватка скоупов / нет прав). HTTP 403."""
[документация]
class ValidationError(TBankAPIError):
"""Некорректные данные запроса. HTTP 400/422."""
[документация]
class RateLimitError(TBankAPIError):
"""Превышен лимит запросов. HTTP 429."""
[документация]
class ServerError(TBankAPIError):
"""Ошибка на стороне сервера. HTTP 5xx."""
# EACQ ErrorCode -> класс исключения (сверять с mapi_errors_list.pdf при расширении).
ERROR_REGISTRY: Dict[str, Type[TBankAPIError]] = {
"10": TerminalBlockedError,
"101": ThreeDSError,
"1051": InsufficientFundsError,
}
[документация]
def build_api_error(
*,
code: str,
message: str,
details: Optional[str] = None,
http_status: Optional[int] = None,
status: Optional[str] = None,
) -> TBankAPIError:
"""Собрать типизированное исключение по коду ошибки."""
cls = ERROR_REGISTRY.get(code, TBankAPIError)
return cls(
code=code,
message=message,
details=details,
http_status=http_status,
status=status,
)
# HTTP-статус -> класс исключения (общий формат ошибок открытого банка T-API).
STATUS_ERROR_MAP: Dict[int, Type[TBankAPIError]] = {
400: ValidationError,
401: AuthenticationError,
403: ForbiddenError,
404: InvalidRequestError,
422: ValidationError,
429: RateLimitError,
500: ServerError,
502: ServerError,
503: ServerError,
504: ServerError,
}
[документация]
def error_from_tapi_response(
response: httpx.Response, *, fallback: str = "request failed"
) -> TBankAPIError:
"""Исключение из T-API error-ответа ({errorId, errorMessage, errorCode,
errorDetails})."""
status = response.status_code
try:
raw = response.json()
except ValueError:
raw = None
data: Dict[str, Any] = raw if isinstance(raw, dict) else {}
code = str(data.get("errorCode") or status)
message = data.get("errorMessage") or response.text or fallback
details = data.get("errorDetails")
cls = STATUS_ERROR_MAP.get(status, TBankAPIError)
return cls(
code=code,
message=message,
http_status=status,
error_id=data.get("errorId"),
details=str(details) if details is not None else None,
)