From 4fc530caeb7dddd3c7e2bac00e6febc80b78cd30 Mon Sep 17 00:00:00 2001 From: deesiigneer Date: Sat, 17 Jan 2026 18:59:58 +0000 Subject: [PATCH] refactor: improve code structure and add proxy support in APISession and SPAPI --- pyspapi/__init__.py | 20 ++++---- pyspapi/api/__init__.py | 1 + pyspapi/api/api.py | 43 +++++++++++------ pyspapi/spworlds.py | 98 ++++++++++++++++++++++----------------- pyspapi/types/__init__.py | 6 ++- 5 files changed, 100 insertions(+), 68 deletions(-) diff --git a/pyspapi/__init__.py b/pyspapi/__init__.py index bd97613..952cd4f 100644 --- a/pyspapi/__init__.py +++ b/pyspapi/__init__.py @@ -1,9 +1,13 @@ -from .api import * -from .spworlds import * -from .types import * +"""pyspapi - API wrapper for SP servers written in Python +TODO: заполнить описание""" -__author__ = 'deesiigneer' -__url__ = 'https://github.com/deesiigneer/pyspapi' -__description__ = 'API wrapper for SP servers written in Python.' -__license__ = 'MIT' -__version__ = "3.2.0" +import importlib.metadata +from .spworlds import SPAPI + +__all__ = [SPAPI] + +__author__: str = "deesiigneer" +__url__: str = "https://github.com/deesiigneer/pyspapi" +__description__: str = "API wrapper for SP servers written in Python." +__license__: str = "MIT" +__version__: str = importlib.metadata.version("pyspapi") diff --git a/pyspapi/api/__init__.py b/pyspapi/api/__init__.py index 9c4afe7..91e48bb 100644 --- a/pyspapi/api/__init__.py +++ b/pyspapi/api/__init__.py @@ -1,2 +1,3 @@ from .api import APISession +__all__ = [APISession] diff --git a/pyspapi/api/api.py b/pyspapi/api/api.py index a4311e0..9224c6e 100644 --- a/pyspapi/api/api.py +++ b/pyspapi/api/api.py @@ -8,17 +8,20 @@ import aiohttp from ..exceptions import ValidationError, SPAPIError -log = getLogger('pyspapi') +log = getLogger("pyspapi") class APISession(object): - - def __init__(self, card_id: str, - token: str, - timeout: int = 5, - sleep_time: float = 0.2, - retries: int = 0, - raise_exception: bool = False): + def __init__( + self, + card_id: str, + token: str, + timeout: int = 5, + sleep_time: float = 0.2, + retries: int = 0, + raise_exception: bool = False, + proxy: str = None, + ): self.__url = "https://spworlds.ru/api/public/" self.__id = card_id self.__token = token @@ -26,23 +29,29 @@ class APISession(object): self.__retries = retries self.__timeout = timeout self.__raise_exception = raise_exception + self.__proxy = proxy self.session: Optional[aiohttp.ClientSession] = None async def __aenter__(self): + print("proxy=", self.__proxy) self.session = aiohttp.ClientSession( json_serialize=json.dumps, - timeout=aiohttp.ClientTimeout(total=self.__timeout)) + timeout=aiohttp.ClientTimeout(total=self.__timeout), + proxy=self.__proxy, + ) return self async def __aexit__(self, *err): await self.session.close() self.session = None - async def request(self, method: str, endpoint: str, data: Optional[Dict] = None) -> Any: + 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', + "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", } @@ -50,9 +59,13 @@ class APISession(object): while True: attempt += 1 if attempt > 1: - log.warning(f'[pyspapi] Repeat attempt {attempt}: {method.upper()} {url}') + log.warning( + f"[pyspapi] Repeat attempt {attempt}: {method.upper()} {url}" + ) try: - async with self.session.request(method, url, json=data, headers=headers) as resp: + async with self.session.request( + method, url, json=data, headers=headers + ) as resp: if resp.status == 422: errors = await resp.json() log.error(f"[pyspapi] Validation error: {errors}") @@ -69,7 +82,7 @@ class APISession(object): return await resp.json() except (aiohttp.ClientError, asyncio.TimeoutError) as e: - log.exception(f"[pyspapi] Connection error: {e}") + log.exception(f"[pyspapi] Connection error: {e} \n attempt {attempt}") if attempt > self.__retries: return None await asyncio.sleep(self.__sleep_timeout) diff --git a/pyspapi/spworlds.py b/pyspapi/spworlds.py index 81ca7c5..a0c4674 100644 --- a/pyspapi/spworlds.py +++ b/pyspapi/spworlds.py @@ -4,27 +4,31 @@ from hmac import new, compare_digest from typing import Optional from .api import APISession -from .types import User -from .types.me import Account -from .types.payment import Item +from pyspapi.types import User +from pyspapi.types.me import Account +from pyspapi.types.payment import Item -__all__ = ['SPAPI'] +__all__ = ["SPAPI"] class SPAPI(APISession): """ - pyspapi — высокоуровневый клиент для взаимодействия с SPWorldsAPI. + pyspapi — высокоуровневый клиент для взаимодействия с SPWorlds API. Предоставляет удобные методы для работы с балансом карты, вебхуками, информацией о пользователе, транзакциями и платежами, а также верификацией вебхуков. """ - def __init__(self, card_id: str, - token: str, - timeout: int = 5, - sleep_time: float = 0.2, - retries: int = 0, - raise_exception: bool = False): + def __init__( + self, + card_id: str, + token: str, + timeout: int = 5, + sleep_time: float = 0.2, + retries: int = 0, + raise_exception: bool = False, + proxy: str = None, + ): """ Инициализирует объект SPAPI. @@ -40,8 +44,12 @@ class SPAPI(APISession): :type retries: int :param raise_exception: Поднимать исключения при ошибке, если True. :type raise_exception: bool + :param proxy: Прокся! + :type proxy: str """ - super().__init__(card_id, token, timeout, sleep_time, retries, raise_exception) + super().__init__( + card_id, token, timeout, sleep_time, retries, raise_exception, proxy + ) self.__card_id = card_id self.__token = token @@ -59,7 +67,7 @@ class SPAPI(APISession): :return: Текущий баланс карты. :rtype: int """ - return int((await super().get('card'))['balance']) + return int((await super().get("card"))["balance"]) @property async def webhook(self) -> Optional[str]: @@ -69,7 +77,7 @@ class SPAPI(APISession): :return: URL вебхука. :rtype: str """ - return str((await super().get('card'))['webhook']) + return str((await super().get("card"))["webhook"]) @property async def me(self) -> Optional[Account]: @@ -79,16 +87,17 @@ class SPAPI(APISession): :return: Объект Account, представляющий аккаунт текущего пользователя. :rtype: :class:`Account` """ - me = await super().get('accounts/me') + 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']) + 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"], + ) async def get_user(self, discord_id: int) -> Optional[User]: """ @@ -100,11 +109,16 @@ class SPAPI(APISession): :return: Объект User, представляющий пользователя. :rtype: :class:`User` """ - user = await super().get(f'users/{discord_id}') - cards = await super().get(f"accounts/{user['username']}/cards") - return User(user['username'], user['uuid'], cards) + user = await super().get(f"users/{discord_id}") + if user: + cards = await super().get(f"accounts/{user['username']}/cards") + return User(user["username"], user["uuid"], cards) + else: + return None - async def create_transaction(self, receiver: str, amount: int, comment: str) -> Optional[int]: + async def create_transaction( + self, receiver: str, amount: int, comment: str + ) -> Optional[int]: """ Создает транзакцию. @@ -118,15 +132,13 @@ class SPAPI(APISession): :return: Баланс после транзакции. :rtype: int """ - data = { - 'receiver': receiver, - 'amount': amount, - 'comment': comment - } + data = {"receiver": receiver, "amount": amount, "comment": comment} - return int((await super().post('transactions', data))['balance']) + return int((await super().post("transactions", data))["balance"]) - async def create_payment(self, webhook_url: str, redirect_url: str, data: str, items: list[Item]) -> Optional[str]: + async def create_payment( + self, webhook_url: str, redirect_url: str, data: str, items: list[Item] + ) -> Optional[str]: """ Создает платеж. @@ -142,13 +154,13 @@ class SPAPI(APISession): :rtype: str """ data = { - 'items': items, - 'redirectUrl': redirect_url, - 'webhookUrl': webhook_url, - 'data': data + "items": items, + "redirectUrl": redirect_url, + "webhookUrl": webhook_url, + "data": data, } - return str((await super().post('payments', data))['url']) + return str((await super().post("payments", data))["url"]) async def update_webhook(self, url: str) -> Optional[dict]: """ @@ -157,9 +169,9 @@ class SPAPI(APISession): :param url: Новый URL вебхука. :return: Ответ API в виде словаря или None при ошибке. """ - data = {'url': url} + data = {"url": url} - return await super().put('card/webhook', data) + return await super().put("card/webhook", data) def webhook_verify(self, data: str, header: str) -> bool: """ @@ -172,8 +184,8 @@ class SPAPI(APISession): :return: True, если заголовок из вебхука достоверен, иначе False. :rtype: bool """ - hmac_data = b64encode(new(self.__token.encode('utf-8'), data, sha256).digest()) - return compare_digest(hmac_data, header.encode('utf-8')) + hmac_data = b64encode(new(self.__token.encode("utf-8"), data, sha256).digest()) + return compare_digest(hmac_data, header.encode("utf-8")) def to_dict(self) -> dict: """ diff --git a/pyspapi/types/__init__.py b/pyspapi/types/__init__.py index ddc58fe..70ed719 100644 --- a/pyspapi/types/__init__.py +++ b/pyspapi/types/__init__.py @@ -1,3 +1,5 @@ -from .payment import Item -from .users import User from .me import Account +from .payment import Item +from .users import Cards, User + +__all__ = [Account, Item, Cards, User]