Files
Airwallex/airwallex-sdk/airwallex/exceptions.py
zqq61 4f53889a8e feat: Airwallex 发卡管理后台完整实现
- 后端: FastAPI + SQLAlchemy + SQLite, JWT认证, 代理支持的AirwallexClient
- 前端: React 18 + Vite + Ant Design 5, 中文界面
- 功能: 卡片管理, 持卡人管理, 交易记录, API令牌, 系统设置, 审计日志
- 第三方API: X-API-Key认证, 权限控制
- Docker部署: docker-compose编排前后端
2026-03-15 23:05:08 +08:00

223 lines
6.3 KiB
Python

"""
Exceptions for the Airwallex API client.
"""
from typing import Any, Dict, Optional, Type, ClassVar, Mapping
import httpx
class AirwallexAPIError(Exception):
"""Base exception for Airwallex API errors."""
def __init__(
self,
*,
status_code: int,
response: httpx.Response,
method: str,
url: str,
kwargs: Dict[str, Any],
message: Optional[str] = None
):
self.status_code = status_code
self.response = response
self.method = method
self.url = url
self.kwargs = kwargs
# Try to parse error details from the response
try:
error_data = response.json()
self.error_code = error_data.get("code", "unknown")
self.error_message = error_data.get("message", "Unknown error")
self.error_source = error_data.get("source", None)
self.request_id = error_data.get("request_id", None)
except Exception:
self.error_code = "unknown"
self.error_message = message or f"HTTP {status_code} error"
self.error_source = None
self.request_id = None
super().__init__(self.__str__())
def __str__(self) -> str:
source_info = f" (source: {self.error_source})" if self.error_source else ""
return (
f"Airwallex API Error (HTTP {self.status_code}): [{self.error_code}] {self.error_message}{source_info} "
f"for {self.method} {self.url}"
)
class AuthenticationError(AirwallexAPIError):
"""Raised when there's an authentication issue."""
pass
class RateLimitError(AirwallexAPIError):
"""Raised when the API rate limit has been exceeded."""
pass
class ResourceNotFoundError(AirwallexAPIError):
"""Raised when a requested resource is not found."""
pass
class ValidationError(AirwallexAPIError):
"""Raised when the request data is invalid."""
pass
class ServerError(AirwallexAPIError):
"""Raised when the server returns a 5xx error."""
pass
class ResourceExistsError(ValidationError):
"""Raised when trying to create a resource that already exists."""
pass
class AmountLimitError(ValidationError):
"""Raised when a transaction amount exceeds or falls below the allowed limits."""
pass
class EditForbiddenError(ValidationError):
"""Raised when trying to edit a resource that can't be modified."""
pass
class CurrencyError(ValidationError):
"""Raised for currency-related errors like invalid pairs or unsupported currencies."""
pass
class DateError(ValidationError):
"""Raised for date-related validation errors."""
pass
class TransferMethodError(ValidationError):
"""Raised when the transfer method is not supported."""
pass
class ConversionError(AirwallexAPIError):
"""Raised for conversion-related errors."""
pass
class ServiceUnavailableError(ServerError):
"""Raised when a service is temporarily unavailable."""
pass
# Mapping of error codes to exception classes
ERROR_CODE_MAP: Dict[str, Type[AirwallexAPIError]] = {
# Authentication errors
"credentials_expired": AuthenticationError,
"credentials_invalid": AuthenticationError,
# Rate limiting
"too_many_requests": RateLimitError,
# Resource exists
"already_exists": ResourceExistsError,
# Amount limit errors
"amount_above_limit": AmountLimitError,
"amount_below_limit": AmountLimitError,
"amount_above_transfer_method_limit": AmountLimitError,
# Edit forbidden
"can_not_be_edited": EditForbiddenError,
# Conversion errors
"conversion_create_failed": ConversionError,
# Validation errors
"field_required": ValidationError,
"invalid_argument": ValidationError,
"term_agreement_is_required": ValidationError,
# Currency errors
"invalid_currency_pair": CurrencyError,
"unsupported_currency": CurrencyError,
# Date errors
"invalid_transfer_date": DateError,
"invalid_conversion_date": DateError,
# Transfer method errors
"unsupported_country_code": TransferMethodError,
"unsupported_transfer_method": TransferMethodError,
# Service unavailable
"service_unavailable": ServiceUnavailableError,
}
def create_exception_from_response(
*,
response: httpx.Response,
method: str,
url: str,
kwargs: Dict[str, Any],
message: Optional[str] = None
) -> AirwallexAPIError:
"""
Create the appropriate exception based on the API response.
This function first checks for specific error codes in the response body.
If no specific error code is found or it's not recognized, it falls back
to using the HTTP status code to determine the exception type.
Args:
response: The HTTP response
method: HTTP method used for the request
url: URL of the request
kwargs: Additional keyword arguments passed to the request
message: Optional custom error message
Returns:
An instance of the appropriate AirwallexAPIError subclass
"""
status_code = response.status_code
try:
error_data = response.json()
error_code = error_data.get("code")
if error_code and error_code in ERROR_CODE_MAP:
exception_class = ERROR_CODE_MAP[error_code]
else:
# Fall back to status code-based exception
exception_class = exception_for_status(status_code)
except Exception:
# If we can't parse the response JSON, fall back to status code
exception_class = exception_for_status(status_code)
return exception_class(
status_code=status_code,
response=response,
method=method,
url=url,
kwargs=kwargs,
message=message
)
def exception_for_status(status_code: int) -> Type[AirwallexAPIError]:
"""Return the appropriate exception class for a given HTTP status code."""
if status_code == 401:
return AuthenticationError
elif status_code == 429:
return RateLimitError
elif status_code == 404:
return ResourceNotFoundError
elif 400 <= status_code < 500:
return ValidationError
elif 500 <= status_code < 600:
return ServerError
return AirwallexAPIError # Default to the base exception for other status codes