refactor: improve code structure and add proxy support in APISession and SPAPI

This commit is contained in:
deesiigneer
2026-01-17 18:59:58 +00:00
parent 6e77bac3ba
commit 4fc530caeb
5 changed files with 100 additions and 68 deletions

View File

@@ -1,9 +1,13 @@
from .api import * """pyspapi - API wrapper for SP servers written in Python
from .spworlds import * TODO: заполнить описание"""
from .types import *
__author__ = 'deesiigneer' import importlib.metadata
__url__ = 'https://github.com/deesiigneer/pyspapi' from .spworlds import SPAPI
__description__ = 'API wrapper for SP servers written in Python.'
__license__ = 'MIT' __all__ = [SPAPI]
__version__ = "3.2.0"
__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")

View File

@@ -1,2 +1,3 @@
from .api import APISession from .api import APISession
__all__ = [APISession]

View File

@@ -8,17 +8,20 @@ import aiohttp
from ..exceptions import ValidationError, SPAPIError from ..exceptions import ValidationError, SPAPIError
log = getLogger('pyspapi') log = getLogger("pyspapi")
class APISession(object): class APISession(object):
def __init__(
def __init__(self, card_id: str, self,
card_id: str,
token: str, token: str,
timeout: int = 5, timeout: int = 5,
sleep_time: float = 0.2, sleep_time: float = 0.2,
retries: int = 0, retries: int = 0,
raise_exception: bool = False): raise_exception: bool = False,
proxy: str = None,
):
self.__url = "https://spworlds.ru/api/public/" self.__url = "https://spworlds.ru/api/public/"
self.__id = card_id self.__id = card_id
self.__token = token self.__token = token
@@ -26,23 +29,29 @@ class APISession(object):
self.__retries = retries self.__retries = retries
self.__timeout = timeout self.__timeout = timeout
self.__raise_exception = raise_exception self.__raise_exception = raise_exception
self.__proxy = proxy
self.session: Optional[aiohttp.ClientSession] = None self.session: Optional[aiohttp.ClientSession] = None
async def __aenter__(self): async def __aenter__(self):
print("proxy=", self.__proxy)
self.session = aiohttp.ClientSession( self.session = aiohttp.ClientSession(
json_serialize=json.dumps, json_serialize=json.dumps,
timeout=aiohttp.ClientTimeout(total=self.__timeout)) timeout=aiohttp.ClientTimeout(total=self.__timeout),
proxy=self.__proxy,
)
return self return self
async def __aexit__(self, *err): async def __aexit__(self, *err):
await self.session.close() await self.session.close()
self.session = None 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 url = self.__url + endpoint
headers = { headers = {
'Authorization': f"Bearer {str(b64encode(str(f'{self.__id}:{self.__token}').encode('utf-8')), 'utf-8')}", "Authorization": f"Bearer {str(b64encode(str(f'{self.__id}:{self.__token}').encode('utf-8')), 'utf-8')}",
'User-Agent': 'https://github.com/deesiigneer/pyspapi', "User-Agent": "https://github.com/deesiigneer/pyspapi",
"Content-Type": "application/json", "Content-Type": "application/json",
} }
@@ -50,9 +59,13 @@ class APISession(object):
while True: while True:
attempt += 1 attempt += 1
if 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: 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: if resp.status == 422:
errors = await resp.json() errors = await resp.json()
log.error(f"[pyspapi] Validation error: {errors}") log.error(f"[pyspapi] Validation error: {errors}")
@@ -69,7 +82,7 @@ class APISession(object):
return await resp.json() return await resp.json()
except (aiohttp.ClientError, asyncio.TimeoutError) as e: 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: if attempt > self.__retries:
return None return None
await asyncio.sleep(self.__sleep_timeout) await asyncio.sleep(self.__sleep_timeout)

View File

@@ -4,11 +4,11 @@ from hmac import new, compare_digest
from typing import Optional from typing import Optional
from .api import APISession from .api import APISession
from .types import User from pyspapi.types import User
from .types.me import Account from pyspapi.types.me import Account
from .types.payment import Item from pyspapi.types.payment import Item
__all__ = ['SPAPI'] __all__ = ["SPAPI"]
class SPAPI(APISession): class SPAPI(APISession):
@@ -19,12 +19,16 @@ class SPAPI(APISession):
информацией о пользователе, транзакциями и платежами, а также верификацией вебхуков. информацией о пользователе, транзакциями и платежами, а также верификацией вебхуков.
""" """
def __init__(self, card_id: str, def __init__(
self,
card_id: str,
token: str, token: str,
timeout: int = 5, timeout: int = 5,
sleep_time: float = 0.2, sleep_time: float = 0.2,
retries: int = 0, retries: int = 0,
raise_exception: bool = False): raise_exception: bool = False,
proxy: str = None,
):
""" """
Инициализирует объект SPAPI. Инициализирует объект SPAPI.
@@ -40,8 +44,12 @@ class SPAPI(APISession):
:type retries: int :type retries: int
:param raise_exception: Поднимать исключения при ошибке, если True. :param raise_exception: Поднимать исключения при ошибке, если True.
:type raise_exception: bool :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.__card_id = card_id
self.__token = token self.__token = token
@@ -59,7 +67,7 @@ class SPAPI(APISession):
:return: Текущий баланс карты. :return: Текущий баланс карты.
:rtype: int :rtype: int
""" """
return int((await super().get('card'))['balance']) return int((await super().get("card"))["balance"])
@property @property
async def webhook(self) -> Optional[str]: async def webhook(self) -> Optional[str]:
@@ -69,7 +77,7 @@ class SPAPI(APISession):
:return: URL вебхука. :return: URL вебхука.
:rtype: str :rtype: str
""" """
return str((await super().get('card'))['webhook']) return str((await super().get("card"))["webhook"])
@property @property
async def me(self) -> Optional[Account]: async def me(self) -> Optional[Account]:
@@ -79,16 +87,17 @@ class SPAPI(APISession):
:return: Объект Account, представляющий аккаунт текущего пользователя. :return: Объект Account, представляющий аккаунт текущего пользователя.
:rtype: :class:`Account` :rtype: :class:`Account`
""" """
me = await super().get('accounts/me') me = await super().get("accounts/me")
return Account( return Account(
account_id=me['id'], account_id=me["id"],
username=me['username'], username=me["username"],
minecraftuuid=me['minecraftUUID'], minecraftuuid=me["minecraftUUID"],
status=me['status'], status=me["status"],
roles=me['roles'], roles=me["roles"],
cities=me['cities'], cities=me["cities"],
cards=me['cards'], cards=me["cards"],
created_at=me['createdAt']) created_at=me["createdAt"],
)
async def get_user(self, discord_id: int) -> Optional[User]: async def get_user(self, discord_id: int) -> Optional[User]:
""" """
@@ -100,11 +109,16 @@ class SPAPI(APISession):
:return: Объект User, представляющий пользователя. :return: Объект User, представляющий пользователя.
:rtype: :class:`User` :rtype: :class:`User`
""" """
user = await super().get(f'users/{discord_id}') user = await super().get(f"users/{discord_id}")
if user:
cards = await super().get(f"accounts/{user['username']}/cards") cards = await super().get(f"accounts/{user['username']}/cards")
return User(user['username'], user['uuid'], 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: Баланс после транзакции. :return: Баланс после транзакции.
:rtype: int :rtype: int
""" """
data = { data = {"receiver": receiver, "amount": amount, "comment": comment}
'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 :rtype: str
""" """
data = { data = {
'items': items, "items": items,
'redirectUrl': redirect_url, "redirectUrl": redirect_url,
'webhookUrl': webhook_url, "webhookUrl": webhook_url,
'data': data "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]: async def update_webhook(self, url: str) -> Optional[dict]:
""" """
@@ -157,9 +169,9 @@ class SPAPI(APISession):
:param url: Новый URL вебхука. :param url: Новый URL вебхука.
:return: Ответ API в виде словаря или None при ошибке. :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: def webhook_verify(self, data: str, header: str) -> bool:
""" """
@@ -172,8 +184,8 @@ class SPAPI(APISession):
:return: True, если заголовок из вебхука достоверен, иначе False. :return: True, если заголовок из вебхука достоверен, иначе False.
:rtype: bool :rtype: bool
""" """
hmac_data = b64encode(new(self.__token.encode('utf-8'), data, sha256).digest()) hmac_data = b64encode(new(self.__token.encode("utf-8"), data, sha256).digest())
return compare_digest(hmac_data, header.encode('utf-8')) return compare_digest(hmac_data, header.encode("utf-8"))
def to_dict(self) -> dict: def to_dict(self) -> dict:
""" """

View File

@@ -1,3 +1,5 @@
from .payment import Item
from .users import User
from .me import Account from .me import Account
from .payment import Item
from .users import Cards, User
__all__ = [Account, Item, Cards, User]