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 *
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")

View File

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

View File

@@ -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)

View File

@@ -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:
"""

View File

@@ -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]