mirror of
https://github.com/deesiigneer/pyspapi.git
synced 2026-04-20 04:25:25 +00:00
refactor: enhance error handling and logging in API interactions, improve exception classes
This commit is contained in:
@@ -1,13 +1,53 @@
|
||||
"""pyspapi - API wrapper for SP servers written in Python
|
||||
TODO: заполнить описание"""
|
||||
"""
|
||||
SPWorlds API Wrapper
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
High-level client for interacting with the SPWorlds API.
|
||||
|
||||
:copyright: (c) 2022-present deesiigneer
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
import importlib.metadata
|
||||
from .spworlds import SPAPI
|
||||
|
||||
__all__ = [SPAPI]
|
||||
from pyspapi.exceptions import (
|
||||
BadRequestError,
|
||||
ClientError,
|
||||
ForbiddenError,
|
||||
HTTPError,
|
||||
InsufficientBalanceError,
|
||||
NetworkError,
|
||||
NotFoundError,
|
||||
RateLimitError,
|
||||
ServerError,
|
||||
SPAPIError,
|
||||
TimeoutError,
|
||||
UnauthorizedError,
|
||||
ValidationError,
|
||||
)
|
||||
from pyspapi.spworlds import SPAPI
|
||||
|
||||
__all__ = [
|
||||
"SPAPI",
|
||||
"BadRequestError",
|
||||
"ClientError",
|
||||
"ForbiddenError",
|
||||
"HTTPError",
|
||||
"InsufficientBalanceError",
|
||||
"NetworkError",
|
||||
"NotFoundError",
|
||||
"RateLimitError",
|
||||
"ServerError",
|
||||
"SPAPIError",
|
||||
"TimeoutError",
|
||||
"UnauthorizedError",
|
||||
"ValidationError",
|
||||
]
|
||||
|
||||
__title__: str = "pyspapi"
|
||||
__author__: str = "deesiigneer"
|
||||
__url__: str = "https://github.com/deesiigneer/pyspapi"
|
||||
__description__: str = "API wrapper for SP servers written in Python."
|
||||
__license__: str = "MIT"
|
||||
__url__: str = "https://github.com/deesiigneer/pyspapi"
|
||||
__copyright__: str = "2022-present deesiigneer"
|
||||
__version__: str = importlib.metadata.version("pyspapi")
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from .api import APISession
|
||||
|
||||
__all__ = [APISession]
|
||||
__all__ = ["APISession"]
|
||||
|
||||
@@ -1,14 +1,31 @@
|
||||
import asyncio
|
||||
import json
|
||||
from base64 import b64encode
|
||||
from logging import getLogger
|
||||
from typing import Optional, Any, Dict
|
||||
from logging import NullHandler, getLogger
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import aiohttp
|
||||
|
||||
from ..exceptions import ValidationError, SPAPIError
|
||||
from pyspapi.exceptions import (
|
||||
BadRequestError,
|
||||
ClientError,
|
||||
ForbiddenError,
|
||||
HTTPError,
|
||||
InsufficientBalanceError,
|
||||
NetworkError,
|
||||
NotFoundError,
|
||||
RateLimitError,
|
||||
ServerError,
|
||||
SPAPIError,
|
||||
UnauthorizedError,
|
||||
ValidationError,
|
||||
)
|
||||
from pyspapi.exceptions import (
|
||||
TimeoutError as APITimeoutError,
|
||||
)
|
||||
|
||||
log = getLogger("pyspapi")
|
||||
log.addHandler(NullHandler())
|
||||
|
||||
|
||||
class APISession(object):
|
||||
@@ -22,6 +39,8 @@ class APISession(object):
|
||||
raise_exception: bool = False,
|
||||
proxy: str = None,
|
||||
):
|
||||
self._validate_credentials(card_id, token)
|
||||
|
||||
self.__url = "https://spworlds.ru/api/public/"
|
||||
self.__id = card_id
|
||||
self.__token = token
|
||||
@@ -31,61 +50,294 @@ class APISession(object):
|
||||
self.__raise_exception = raise_exception
|
||||
self.__proxy = proxy
|
||||
self.session: Optional[aiohttp.ClientSession] = None
|
||||
self._session_owner = False
|
||||
|
||||
@staticmethod
|
||||
def _validate_credentials(card_id: str, token: str) -> None:
|
||||
if not card_id or not isinstance(card_id, str):
|
||||
raise ValueError("card_id must be a non-empty string")
|
||||
if not token or not isinstance(token, str):
|
||||
raise ValueError("token must be a non-empty string")
|
||||
|
||||
async def __aenter__(self):
|
||||
print("proxy=", self.__proxy)
|
||||
self.session = aiohttp.ClientSession(
|
||||
json_serialize=json.dumps,
|
||||
timeout=aiohttp.ClientTimeout(total=self.__timeout),
|
||||
proxy=self.__proxy,
|
||||
)
|
||||
try:
|
||||
if not self.session:
|
||||
self.session = aiohttp.ClientSession(
|
||||
json_serialize=json.dumps,
|
||||
timeout=aiohttp.ClientTimeout(total=self.__timeout),
|
||||
proxy=self.__proxy,
|
||||
)
|
||||
self._session_owner = True
|
||||
log.debug(f"[pyspapi] Session created with timeout={self.__timeout}s")
|
||||
else:
|
||||
self._session_owner = False
|
||||
except Exception as e:
|
||||
log.error(f"[pyspapi] Failed to create session: {e}")
|
||||
raise
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *err):
|
||||
await self.session.close()
|
||||
self.session = None
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
if self._session_owner and self.session:
|
||||
try:
|
||||
await self.session.close()
|
||||
log.debug("[pyspapi] Session closed")
|
||||
except Exception as e:
|
||||
log.error(f"[pyspapi] Error closing session: {e}")
|
||||
self.session = None
|
||||
|
||||
return False
|
||||
|
||||
def _get_auth_header(self) -> str:
|
||||
credentials = f"{self.__id}:{self.__token}"
|
||||
encoded = b64encode(credentials.encode("utf-8")).decode("utf-8")
|
||||
return f"Bearer {encoded}"
|
||||
|
||||
def _get_headers(self) -> Dict[str, str]:
|
||||
return {
|
||||
"Authorization": self._get_auth_header(),
|
||||
"User-Agent": "https://github.com/deesiigneer/pyspapi",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
def _parse_error_response(self, content: str) -> Dict[str, Any]:
|
||||
try:
|
||||
return json.loads(content)
|
||||
except json.JSONDecodeError:
|
||||
return {"raw_response": content}
|
||||
|
||||
def _format_error_message(
|
||||
self, error_data: Dict[str, Any], status_code: int
|
||||
) -> str:
|
||||
message = (
|
||||
error_data.get("message")
|
||||
or error_data.get("detail")
|
||||
or error_data.get("msg")
|
||||
or f"HTTP {status_code}"
|
||||
)
|
||||
|
||||
if "error" in error_data:
|
||||
message = f"{message} (error: {error_data['error']})"
|
||||
|
||||
return message
|
||||
|
||||
def _log_error_with_details(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
status_code: int,
|
||||
error_data: Dict[str, Any],
|
||||
content: str,
|
||||
) -> None:
|
||||
message = self._format_error_message(error_data, status_code)
|
||||
|
||||
log.error(
|
||||
f"[pyspapi] HTTP {status_code}: {method.upper()} {endpoint} | {message}"
|
||||
)
|
||||
|
||||
def _should_retry(self, status_code: int, attempt: int) -> bool:
|
||||
if attempt > self.__retries:
|
||||
return False
|
||||
return status_code in {408, 429, 500, 502, 503, 504}
|
||||
|
||||
async def _handle_http_error(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
status_code: int,
|
||||
content: str,
|
||||
) -> None:
|
||||
error_data = self._parse_error_response(content)
|
||||
|
||||
self._log_error_with_details(method, endpoint, status_code, error_data, content)
|
||||
|
||||
if not self.__raise_exception:
|
||||
return
|
||||
|
||||
error_message = self._format_error_message(error_data, status_code)
|
||||
|
||||
if status_code == 400:
|
||||
error_code = error_data.get("error", "")
|
||||
if "notEnoughBalance" in error_code:
|
||||
raise InsufficientBalanceError(details=error_data)
|
||||
raise BadRequestError(details=error_data)
|
||||
elif status_code == 401:
|
||||
raise UnauthorizedError(details=error_data)
|
||||
elif status_code == 403:
|
||||
raise ForbiddenError(details=error_data)
|
||||
elif status_code == 404:
|
||||
raise NotFoundError(resource=endpoint, details=error_data)
|
||||
elif status_code == 422:
|
||||
raise ValidationError(error_data)
|
||||
elif status_code == 429:
|
||||
retry_after = error_data.get("retry_after")
|
||||
raise RateLimitError(retry_after=retry_after, details=error_data)
|
||||
elif 400 <= status_code < 500:
|
||||
raise ClientError(
|
||||
status_code=status_code,
|
||||
message=error_message,
|
||||
response_body=content,
|
||||
details=error_data,
|
||||
)
|
||||
elif 500 <= status_code < 600:
|
||||
raise ServerError(
|
||||
status_code=status_code,
|
||||
message=error_message,
|
||||
response_body=content,
|
||||
details=error_data,
|
||||
)
|
||||
else:
|
||||
raise HTTPError(
|
||||
status_code=status_code,
|
||||
message=error_message,
|
||||
response_body=content,
|
||||
details=error_data,
|
||||
)
|
||||
|
||||
async def request(
|
||||
self, method: str, endpoint: str, data: Optional[Dict] = None
|
||||
) -> Any:
|
||||
url = self.__url + endpoint
|
||||
headers = {
|
||||
"Authorization": f"Bearer {str(b64encode(str(f'{self.__id}:{self.__token}').encode('utf-8')), 'utf-8')}",
|
||||
"User-Agent": "https://github.com/deesiigneer/pyspapi",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
headers = self._get_headers()
|
||||
|
||||
attempt = 0
|
||||
|
||||
while True:
|
||||
attempt += 1
|
||||
if attempt > 1:
|
||||
log.warning(
|
||||
f"[pyspapi] Repeat attempt {attempt}: {method.upper()} {url}"
|
||||
f"[pyspapi] Retry attempt {attempt}/{self.__retries + 1}: {method.upper()} {endpoint}"
|
||||
)
|
||||
|
||||
try:
|
||||
async with self.session.request(
|
||||
method, url, json=data, headers=headers
|
||||
) as resp:
|
||||
response_text = await resp.text()
|
||||
|
||||
if resp.status == 422:
|
||||
errors = await resp.json()
|
||||
log.error(f"[pyspapi] Validation error: {errors}")
|
||||
try:
|
||||
errors = json.loads(response_text)
|
||||
except json.JSONDecodeError:
|
||||
errors = {"raw_response": response_text}
|
||||
|
||||
error_msg = self._format_error_message(errors, 422)
|
||||
log.error(
|
||||
f"[pyspapi] Validation error (422): {method.upper()} {endpoint} | {error_msg}"
|
||||
)
|
||||
if self.__raise_exception:
|
||||
raise ValidationError(errors)
|
||||
return None
|
||||
|
||||
if resp.status >= 400:
|
||||
content = await resp.text()
|
||||
log.error(f"[pyspapi] API error {resp.status}: {content}")
|
||||
if self.__raise_exception:
|
||||
raise SPAPIError(resp.status, content)
|
||||
await self._handle_http_error(
|
||||
method, endpoint, resp.status, response_text
|
||||
)
|
||||
|
||||
if self._should_retry(resp.status, attempt):
|
||||
await asyncio.sleep(self.__sleep_timeout * attempt)
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
return await resp.json()
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
||||
log.exception(f"[pyspapi] Connection error: {e} \n attempt {attempt}")
|
||||
if attempt > self.__retries:
|
||||
return None
|
||||
await asyncio.sleep(self.__sleep_timeout)
|
||||
try:
|
||||
return await resp.json()
|
||||
except json.JSONDecodeError as e:
|
||||
log.error(
|
||||
f"[pyspapi] Failed to parse JSON response: {e} | Status: {resp.status}"
|
||||
)
|
||||
if self.__raise_exception:
|
||||
raise SPAPIError(
|
||||
status_code=resp.status,
|
||||
message="Invalid JSON in response",
|
||||
details={
|
||||
"error": str(e),
|
||||
"response": response_text[:500],
|
||||
},
|
||||
)
|
||||
return None
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
log.warning(
|
||||
f"[pyspapi] Request timeout ({self.__timeout}s): {method.upper()} {endpoint} | Attempt {attempt}/{self.__retries + 1}"
|
||||
)
|
||||
|
||||
if self._should_retry(408, attempt):
|
||||
await asyncio.sleep(self.__sleep_timeout * attempt)
|
||||
continue
|
||||
|
||||
log.error("[pyspapi] Max retries reached for timeout")
|
||||
if self.__raise_exception:
|
||||
raise APITimeoutError(
|
||||
timeout=self.__timeout,
|
||||
endpoint=endpoint,
|
||||
details={"method": method, "attempt": attempt},
|
||||
)
|
||||
return None
|
||||
|
||||
except aiohttp.ClientSSLError as e:
|
||||
log.error(f"[pyspapi] SSL error: {e} | {method.upper()} {endpoint}")
|
||||
if self.__raise_exception:
|
||||
raise NetworkError(
|
||||
message=f"SSL error: {str(e)}",
|
||||
details={
|
||||
"method": method,
|
||||
"endpoint": endpoint,
|
||||
"error": str(e),
|
||||
},
|
||||
)
|
||||
return None
|
||||
|
||||
except (aiohttp.ClientConnectorError, aiohttp.ClientOSError) as e:
|
||||
log.warning(
|
||||
f"[pyspapi] Connection error: {e} | {method.upper()} {endpoint} | Attempt {attempt}/{self.__retries + 1}"
|
||||
)
|
||||
|
||||
if self._should_retry(0, attempt):
|
||||
await asyncio.sleep(self.__sleep_timeout * attempt)
|
||||
continue
|
||||
|
||||
log.error("[pyspapi] Max retries reached for connection error")
|
||||
if self.__raise_exception:
|
||||
raise NetworkError(
|
||||
message=f"Connection error: {str(e)}",
|
||||
details={
|
||||
"method": method,
|
||||
"endpoint": endpoint,
|
||||
"error": str(e),
|
||||
"attempt": attempt,
|
||||
},
|
||||
)
|
||||
return None
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
log.error(f"[pyspapi] Client error: {e} | {method.upper()} {endpoint}")
|
||||
if self.__raise_exception:
|
||||
raise NetworkError(
|
||||
message=f"HTTP client error: {str(e)}",
|
||||
details={
|
||||
"method": method,
|
||||
"endpoint": endpoint,
|
||||
"error": str(e),
|
||||
},
|
||||
)
|
||||
return None
|
||||
|
||||
except SPAPIError:
|
||||
raise
|
||||
except Exception as e:
|
||||
log.exception(
|
||||
f"[pyspapi] Unexpected error: {e} | {method.upper()} {endpoint}"
|
||||
)
|
||||
if self.__raise_exception:
|
||||
raise SPAPIError(
|
||||
message=f"Unexpected error: {str(e)}",
|
||||
details={
|
||||
"error": str(e),
|
||||
"method": method,
|
||||
"endpoint": endpoint,
|
||||
},
|
||||
)
|
||||
return None
|
||||
|
||||
async def get(self, endpoint: str) -> Any:
|
||||
async with self:
|
||||
|
||||
@@ -1,25 +1,197 @@
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
class SPAPIError(Exception):
|
||||
"""
|
||||
Базовая ошибка для всех исключений, связанных с API SPWorlds.
|
||||
"""
|
||||
|
||||
def __init__(self, status_code: int, message: str):
|
||||
def __init__(
|
||||
self,
|
||||
status_code: Optional[int] = None,
|
||||
message: str = "",
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
self.status_code = status_code
|
||||
self.message = message
|
||||
super().__init__(f"[{status_code}] {message}")
|
||||
self.details = details or {}
|
||||
error_msg = f"[{status_code}] {message}" if status_code else message
|
||||
super().__init__(error_msg)
|
||||
|
||||
def __str__(self):
|
||||
return f"SPAPIError: [{self.status_code}] {self.message}"
|
||||
if self.status_code:
|
||||
return f"{self.__class__.__name__}: [{self.status_code}] {self.message}"
|
||||
return f"{self.__class__.__name__}: {self.message}"
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}(status_code={self.status_code}, message={self.message!r}, details={self.details!r})"
|
||||
|
||||
|
||||
class ValidationError(SPAPIError):
|
||||
"""
|
||||
Ошибка валидации.
|
||||
Ошибка валидации (HTTP 422).
|
||||
"""
|
||||
|
||||
def __init__(self, errors):
|
||||
def __init__(self, errors: Dict[str, Any]):
|
||||
self.errors = errors
|
||||
super().__init__(422, f"Validation failed: {errors}")
|
||||
super().__init__(
|
||||
status_code=422,
|
||||
message="Validation failed",
|
||||
details={"validation_errors": errors},
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"ValidationError: {self.errors}"
|
||||
return f"{self.__class__.__name__}: {self.errors}"
|
||||
|
||||
|
||||
class NetworkError(SPAPIError):
|
||||
"""
|
||||
Ошибка сетевого соединения.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
||||
super().__init__(message=message, details=details)
|
||||
|
||||
|
||||
class TimeoutError(SPAPIError):
|
||||
"""
|
||||
Ошибка истечения времени ожидания.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
timeout: float,
|
||||
endpoint: str = "",
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
msg = f"Request timeout after {timeout}s"
|
||||
if endpoint:
|
||||
msg += f" for endpoint: {endpoint}"
|
||||
super().__init__(
|
||||
message=msg, details=details or {"timeout": timeout, "endpoint": endpoint}
|
||||
)
|
||||
|
||||
|
||||
class HTTPError(SPAPIError):
|
||||
"""
|
||||
Ошибка HTTP (4xx, 5xx).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
status_code: int,
|
||||
message: str = "",
|
||||
response_body: str = "",
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
self.response_body = response_body
|
||||
super().__init__(
|
||||
status_code=status_code,
|
||||
message=message or f"HTTP {status_code}",
|
||||
details=details or {"response_body": response_body},
|
||||
)
|
||||
|
||||
|
||||
class ClientError(HTTPError):
|
||||
"""
|
||||
Ошибка клиента (4xx).
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ServerError(HTTPError):
|
||||
"""
|
||||
Ошибка сервера (5xx).
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RateLimitError(ClientError):
|
||||
"""
|
||||
Превышен лимит запросов (HTTP 429).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
retry_after: Optional[int] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
self.retry_after = retry_after
|
||||
msg = "Rate limit exceeded"
|
||||
if retry_after:
|
||||
msg += f". Retry after {retry_after}s"
|
||||
super().__init__(
|
||||
status_code=429,
|
||||
message=msg,
|
||||
details=details or {"retry_after": retry_after},
|
||||
)
|
||||
|
||||
|
||||
class UnauthorizedError(ClientError):
|
||||
"""
|
||||
Ошибка аутентификации (HTTP 401).
|
||||
"""
|
||||
|
||||
def __init__(self, details: Optional[Dict[str, Any]] = None):
|
||||
super().__init__(
|
||||
status_code=401,
|
||||
message="Unauthorized. Invalid or missing credentials.",
|
||||
details=details,
|
||||
)
|
||||
|
||||
|
||||
class ForbiddenError(ClientError):
|
||||
"""
|
||||
Ошибка доступа (HTTP 403).
|
||||
"""
|
||||
|
||||
def __init__(self, details: Optional[Dict[str, Any]] = None):
|
||||
super().__init__(
|
||||
status_code=403,
|
||||
message="Forbidden. Access denied.",
|
||||
details=details,
|
||||
)
|
||||
|
||||
|
||||
class NotFoundError(ClientError):
|
||||
"""
|
||||
Ресурс не найден (HTTP 404).
|
||||
"""
|
||||
|
||||
def __init__(self, resource: str = "", details: Optional[Dict[str, Any]] = None):
|
||||
msg = "Resource not found"
|
||||
if resource:
|
||||
msg += f": {resource}"
|
||||
super().__init__(
|
||||
status_code=404,
|
||||
message=msg,
|
||||
details=details or {"resource": resource},
|
||||
)
|
||||
|
||||
|
||||
class BadRequestError(ClientError):
|
||||
"""
|
||||
Некорректный запрос (HTTP 400).
|
||||
"""
|
||||
|
||||
def __init__(self, details: Optional[Dict[str, Any]] = None):
|
||||
super().__init__(
|
||||
status_code=400,
|
||||
message="Bad request. Invalid request parameters.",
|
||||
details=details,
|
||||
)
|
||||
|
||||
|
||||
class InsufficientBalanceError(ClientError):
|
||||
"""
|
||||
Недостаточно средств на счете.
|
||||
"""
|
||||
|
||||
def __init__(self, details: Optional[Dict[str, Any]] = None):
|
||||
super().__init__(
|
||||
status_code=400,
|
||||
message="Insufficient balance. Not enough funds to complete the transaction.",
|
||||
details=details or {"error": "error.public.transactions.notEnoughBalance"},
|
||||
)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from base64 import b64encode
|
||||
from hashlib import sha256
|
||||
from hmac import new, compare_digest
|
||||
from hmac import compare_digest, new
|
||||
from typing import Optional
|
||||
|
||||
from .api import APISession
|
||||
from pyspapi.api import APISession
|
||||
from pyspapi.exceptions import InsufficientBalanceError
|
||||
from pyspapi.types import User
|
||||
from pyspapi.types.me import Account
|
||||
from pyspapi.types.payment import Item
|
||||
@@ -44,7 +45,7 @@ class SPAPI(APISession):
|
||||
:type retries: int
|
||||
:param raise_exception: Поднимать исключения при ошибке, если True.
|
||||
:type raise_exception: bool
|
||||
:param proxy: Прокся!
|
||||
:param proxy: Прокси для подключения к API. По умолчанию None.
|
||||
:type proxy: str
|
||||
"""
|
||||
super().__init__(
|
||||
@@ -67,7 +68,15 @@ class SPAPI(APISession):
|
||||
:return: Текущий баланс карты.
|
||||
:rtype: int
|
||||
"""
|
||||
return int((await super().get("card"))["balance"])
|
||||
try:
|
||||
response = await super().get("card")
|
||||
if response is None:
|
||||
return None
|
||||
return int(response.get("balance", 0))
|
||||
except (KeyError, ValueError, TypeError) as e:
|
||||
log = __import__("logging").getLogger("pyspapi")
|
||||
log.error(f"Failed to parse balance response: {e}")
|
||||
return None
|
||||
|
||||
@property
|
||||
async def webhook(self) -> Optional[str]:
|
||||
@@ -77,7 +86,15 @@ class SPAPI(APISession):
|
||||
:return: URL вебхука.
|
||||
:rtype: str
|
||||
"""
|
||||
return str((await super().get("card"))["webhook"])
|
||||
try:
|
||||
response = await super().get("card")
|
||||
if response is None:
|
||||
return None
|
||||
return str(response.get("webhook", ""))
|
||||
except (KeyError, ValueError, TypeError) as e:
|
||||
log = __import__("logging").getLogger("pyspapi")
|
||||
log.error(f"Failed to parse webhook response: {e}")
|
||||
return None
|
||||
|
||||
@property
|
||||
async def me(self) -> Optional[Account]:
|
||||
@@ -87,17 +104,25 @@ class SPAPI(APISession):
|
||||
:return: Объект Account, представляющий аккаунт текущего пользователя.
|
||||
:rtype: :class:`Account`
|
||||
"""
|
||||
me = await super().get("accounts/me")
|
||||
return Account(
|
||||
account_id=me["id"],
|
||||
username=me["username"],
|
||||
minecraftuuid=me["minecraftUUID"],
|
||||
status=me["status"],
|
||||
roles=me["roles"],
|
||||
cities=me["cities"],
|
||||
cards=me["cards"],
|
||||
created_at=me["createdAt"],
|
||||
)
|
||||
try:
|
||||
me = await super().get("accounts/me")
|
||||
if me is None:
|
||||
return None
|
||||
|
||||
return Account(
|
||||
account_id=me.get("id"),
|
||||
username=me.get("username"),
|
||||
minecraftuuid=me.get("minecraftUUID"),
|
||||
status=me.get("status"),
|
||||
roles=me.get("roles", []),
|
||||
cities=me.get("cities", []),
|
||||
cards=me.get("cards", []),
|
||||
created_at=me.get("createdAt"),
|
||||
)
|
||||
except (KeyError, TypeError) as e:
|
||||
log = __import__("logging").getLogger("pyspapi")
|
||||
log.error(f"Failed to parse account response: {e}")
|
||||
return None
|
||||
|
||||
async def get_user(self, discord_id: int) -> Optional[User]:
|
||||
"""
|
||||
@@ -109,11 +134,22 @@ class SPAPI(APISession):
|
||||
:return: Объект User, представляющий пользователя.
|
||||
:rtype: :class:`User`
|
||||
"""
|
||||
user = await super().get(f"users/{discord_id}")
|
||||
if user:
|
||||
if not discord_id:
|
||||
raise ValueError("discord_id must be a non-empty integer")
|
||||
|
||||
try:
|
||||
user = await super().get(f"users/{discord_id}")
|
||||
if user is None:
|
||||
return None
|
||||
|
||||
cards = await super().get(f"accounts/{user['username']}/cards")
|
||||
if cards is None:
|
||||
cards = []
|
||||
|
||||
return User(user["username"], user["uuid"], cards)
|
||||
else:
|
||||
except (KeyError, TypeError) as e:
|
||||
log = __import__("logging").getLogger("pyspapi")
|
||||
log.error(f"Failed to parse user response: {e}")
|
||||
return None
|
||||
|
||||
async def create_transaction(
|
||||
@@ -132,9 +168,27 @@ class SPAPI(APISession):
|
||||
:return: Баланс после транзакции.
|
||||
:rtype: int
|
||||
"""
|
||||
data = {"receiver": receiver, "amount": amount, "comment": comment}
|
||||
if not receiver:
|
||||
raise ValueError("receiver must be a non-empty string")
|
||||
if not isinstance(amount, int) or amount <= 0:
|
||||
raise ValueError("amount must be a positive integer")
|
||||
|
||||
return int((await super().post("transactions", data))["balance"])
|
||||
try:
|
||||
data = {"receiver": receiver, "amount": amount, "comment": comment}
|
||||
response = await super().post("transactions", data)
|
||||
|
||||
if response is None:
|
||||
return None
|
||||
|
||||
return int(response.get("balance", 0))
|
||||
except (KeyError, ValueError, TypeError) as e:
|
||||
log = __import__("logging").getLogger("pyspapi")
|
||||
log.error(f"Failed to create transaction: {e}")
|
||||
return None
|
||||
except InsufficientBalanceError as ibe:
|
||||
log = __import__("logging").getLogger("pyspapi")
|
||||
log.error(f"Insufficient balance for transaction: {ibe}")
|
||||
return None
|
||||
|
||||
async def create_payment(
|
||||
self, webhook_url: str, redirect_url: str, data: str, items: list[Item]
|
||||
@@ -153,14 +207,29 @@ class SPAPI(APISession):
|
||||
:return: URL для платежа или None при ошибке.
|
||||
:rtype: str
|
||||
"""
|
||||
data = {
|
||||
"items": items,
|
||||
"redirectUrl": redirect_url,
|
||||
"webhookUrl": webhook_url,
|
||||
"data": data,
|
||||
}
|
||||
if not webhook_url or not redirect_url:
|
||||
raise ValueError("webhook_url and redirect_url must be non-empty strings")
|
||||
if not items or len(items) == 0:
|
||||
raise ValueError("items must contain at least one item")
|
||||
|
||||
return str((await super().post("payments", data))["url"])
|
||||
try:
|
||||
payload = {
|
||||
"items": items,
|
||||
"redirectUrl": redirect_url,
|
||||
"webhookUrl": webhook_url,
|
||||
"data": data,
|
||||
}
|
||||
|
||||
response = await super().post("payments", payload)
|
||||
|
||||
if response is None:
|
||||
return None
|
||||
|
||||
return str(response.get("url", ""))
|
||||
except (KeyError, ValueError, TypeError) as e:
|
||||
log = __import__("logging").getLogger("pyspapi")
|
||||
log.error(f"Failed to create payment: {e}")
|
||||
return None
|
||||
|
||||
async def update_webhook(self, url: str) -> Optional[dict]:
|
||||
"""
|
||||
@@ -169,9 +238,21 @@ class SPAPI(APISession):
|
||||
:param url: Новый URL вебхука.
|
||||
:return: Ответ API в виде словаря или None при ошибке.
|
||||
"""
|
||||
data = {"url": url}
|
||||
if not url:
|
||||
raise ValueError("url must be a non-empty string")
|
||||
|
||||
return await super().put("card/webhook", data)
|
||||
try:
|
||||
data = {"url": url}
|
||||
response = await super().put("card/webhook", data)
|
||||
|
||||
if response is None:
|
||||
return None
|
||||
|
||||
return response
|
||||
except (KeyError, TypeError) as e:
|
||||
log = __import__("logging").getLogger("pyspapi")
|
||||
log.error(f"Failed to update webhook: {e}")
|
||||
return None
|
||||
|
||||
def webhook_verify(self, data: str, header: str) -> bool:
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from .me import Account
|
||||
from .payment import Item
|
||||
from .users import Cards, User
|
||||
from pyspapi.types.me import Account
|
||||
from pyspapi.types.payment import Item
|
||||
from pyspapi.types.users import Cards, User
|
||||
|
||||
__all__ = [Account, Item, Cards, User]
|
||||
__all__ = ["Account", "Item", "Cards", "User"]
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
class City:
|
||||
def __init__(self, city_id=None, name=None, x=None, z=None, nether_x=None, nether_z=None, lane=None, role=None,
|
||||
created_at=None):
|
||||
def __init__(
|
||||
self,
|
||||
city_id=None,
|
||||
name=None,
|
||||
x=None,
|
||||
z=None,
|
||||
nether_x=None,
|
||||
nether_z=None,
|
||||
lane=None,
|
||||
role=None,
|
||||
created_at=None,
|
||||
):
|
||||
self._id = city_id
|
||||
self._name = name
|
||||
self._x = x
|
||||
@@ -48,11 +58,7 @@ class City:
|
||||
return self._created_at
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"City(id={self._id}, name={self._name}, x={self._x}, z={self._z}, "
|
||||
f"nether_x={self._nether_x}, nether_z={self._nether_z}, lane={self._lane}, role={self._role}, "
|
||||
f"created_at={self._created_at})"
|
||||
)
|
||||
return f"<{self.__class__.__name__}(id={self._id!r}, name={self._name!r}, lane={self._lane!r}, role={self._role!r})>"
|
||||
|
||||
|
||||
class Card:
|
||||
@@ -79,11 +85,21 @@ class Card:
|
||||
return self._color
|
||||
|
||||
def __repr__(self):
|
||||
return f"Card(id={self._id}, name={self._name}, number={self._number}, color={self._color})"
|
||||
return f"<{self.__class__.__name__}(id={self._id!r}, name={self._name!r}, number={self._number!r})>"
|
||||
|
||||
|
||||
class Account:
|
||||
def __init__(self, account_id, username, minecraftuuid, status, roles, created_at, cards, cities):
|
||||
def __init__(
|
||||
self,
|
||||
account_id,
|
||||
username,
|
||||
minecraftuuid,
|
||||
status,
|
||||
roles,
|
||||
created_at,
|
||||
cards,
|
||||
cities,
|
||||
):
|
||||
self._id = account_id
|
||||
self._username = username
|
||||
self._minecraftuuid = minecraftuuid
|
||||
@@ -91,15 +107,15 @@ class Account:
|
||||
self._roles = roles
|
||||
self._cities = [
|
||||
City(
|
||||
city_id=city['city_id'],
|
||||
name=city['name'],
|
||||
x=city['x'],
|
||||
z=city['z'],
|
||||
nether_x=city['nether_x'],
|
||||
nether_z=city['nether_z'],
|
||||
lane=city['lane'],
|
||||
role=city['role'],
|
||||
created_at=city['created_at'],
|
||||
city_id=city["city"]["id"],
|
||||
name=city["city"]["name"],
|
||||
x=city["city"]["x"],
|
||||
z=city["city"]["z"],
|
||||
nether_x=city["city"]["netherX"],
|
||||
nether_z=city["city"]["netherZ"],
|
||||
lane=city["city"]["lane"],
|
||||
role=city["role"],
|
||||
created_at=city["createdAt"],
|
||||
)
|
||||
for city in cities
|
||||
]
|
||||
@@ -147,6 +163,7 @@ class Account:
|
||||
return self._created_at
|
||||
|
||||
def __repr__(self):
|
||||
return (f"Account(id={self._id}, username={self._username}, minecraftUUID={self._minecraftuuid}, "
|
||||
f"status={self._status}, roles={self._roles}, cities={self._cities}, cards={self._cards}, "
|
||||
f"created_at={self._created_at})")
|
||||
return (
|
||||
f"<{self.__class__.__name__}(id={self._id!r}, username={self._username!r}, status={self._status!r}, "
|
||||
f"roles={self._roles}, cities={self._cities}, cards={self._cards})>"
|
||||
)
|
||||
|
||||
@@ -9,10 +9,13 @@ class Item:
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}(name={self._name!r}, count={self._count!r}, price={self._price!r}, comment={self._comment!r})>"
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
"name": self._name,
|
||||
"count": self._count,
|
||||
"price": self._price,
|
||||
"comment": self._comment
|
||||
"comment": self._comment,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
class Cards:
|
||||
|
||||
class UserCards:
|
||||
def __init__(self, name, number):
|
||||
self._name: str = name
|
||||
self._number: str = number
|
||||
@@ -12,20 +11,16 @@ class Cards:
|
||||
def number(self):
|
||||
return self._number
|
||||
|
||||
# def __repr__(self):
|
||||
# return "%s(%s)" % (
|
||||
# self.__class__.__name__,
|
||||
# self.__dict__
|
||||
# )
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}(id={self._id!r}, name={self._name!r}, number={self._number!r})>"
|
||||
|
||||
|
||||
class User:
|
||||
|
||||
def __init__(self, username, uuid, cards):
|
||||
self._username: str = username
|
||||
self._uuid: str = uuid
|
||||
self._cards = [
|
||||
Cards(
|
||||
UserCards(
|
||||
name=card["name"],
|
||||
number=card["number"],
|
||||
)
|
||||
@@ -45,7 +40,4 @@ class User:
|
||||
return self._cards
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (
|
||||
self.__class__.__name__,
|
||||
self.__dict__
|
||||
)
|
||||
return "%s(%s)" % (self.__class__.__name__, self.__dict__)
|
||||
|
||||
Reference in New Issue
Block a user