From c1a172d4be3abdb1d243295921f341d8a7cf3a2a Mon Sep 17 00:00:00 2001 From: Teleport <77507478+teleport2@users.noreply.github.com> Date: Tue, 9 May 2023 02:28:12 +0300 Subject: [PATCH] Add more pre-checkers, docstrings - add SpwCardNotFound error to `send_transaction()` - Add balance checker to `send_transactions()` - Made working docstrings - Correction errors in Parameters --- docs/source/conf.py | 2 +- pyspw/Parameters.py | 58 ++++++++++++++--- pyspw/User.py | 17 +++++ pyspw/api.py | 154 ++++++++++++++++++++++++++++++++------------ pyspw/errors.py | 25 +++++++ 5 files changed, 205 insertions(+), 51 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 329b3a6..36e6091 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -68,5 +68,5 @@ html_theme_options = { ], "source_repository": "https://github.com/teleportx/Py-SPW/", "source_branch": "main", - "source_directory": "docs/", + "source_directory": "docs/source/", } diff --git a/pyspw/Parameters.py b/pyspw/Parameters.py index b1acd54..93fb4c1 100644 --- a/pyspw/Parameters.py +++ b/pyspw/Parameters.py @@ -1,45 +1,83 @@ from pydantic import BaseModel, validator import validators +from . import errors as err + class Payment(BaseModel): + """ + Класс параметров оплаты. + + :param amount: Сумма которую должен оплатить пользователь. + :type amount: int + + :param redirectUrl: Ссылка на которую перенаправит пользователя после успешной оплаты. + :type redirectUrl: str + + :param webhookUrl: Ссылка вебхука, туда придет сообщение о успешной оплате. + :type webhookUrl: str + + :param data: Полезные данные, которые вы хотите получить в будущем вместе с вебхуком. + :type data: str + + :raises BigAmountError: Запрашиваемая сумма слишком большая *(макс. 1728)* + :raises LengthError: Строка data слишком длинная *(макс. 100)* + :raises IsNotURLError: Параметр не является URL + """ + amount: int redirectUrl: str webhookUrl: str data: str @validator('amount') - def max_amount(cls, value: int): + def _max_amount(cls, value: int): if value > 1728: - raise ValueError('amount must be <= 1728') + raise err.BigAmountError() return value @validator('data') - def data_size(cls, value): + def _data_size(cls, value): if len(value) > 100: - raise ValueError('data length must be <=100.') + raise err.LengthError(100) return value @validator('redirectUrl', 'webhookUrl') - def verify_url(cls, value: str): + def _verify_url(cls, value: str): if validators.url(value): return value - raise ValueError('is not url') + raise err.IsNotURLError() class Transaction(BaseModel): + """ + Класс параметров транзакции. + + :param receiver: Карта получателя транзакции. + :type receiver: str + + :param amount: Сумма которую должен оплатить пользователь. + :type amount: int + + :param comment: Комментарий к транзакции. + :type comment: str + + :raises LengthError: Комментарий к транзакции comment слишком длинный *(макс. 32)* + :raises IsNotCardError: Неверно указана карта получателя + """ + receiver: str amount: int comment: str @validator('comment') - def comment_size(cls, value: str): + def _comment_size(cls, value: str): if len(value) > 32: - raise ValueError('comment length must be <=32.') + raise err.LengthError(32) return value @validator('receiver') - def receiver_type(cls, value: str): + def _receiver_type(cls, value: str): if len(value) != 5 or not value.isnumeric(): - raise ValueError(f'Receiver card (`{value}`) number not valid') + raise err.IsNotCardError(value) return value diff --git a/pyspw/User.py b/pyspw/User.py index 7f361bf..7c5c5f0 100644 --- a/pyspw/User.py +++ b/pyspw/User.py @@ -12,6 +12,9 @@ mapi = MAPI() class SkinVariant(Enum): + """ + Варианты скинов. + """ SLIM = 'slim' CLASSIC = 'classic' @@ -27,9 +30,20 @@ class _SkinPart: return self.get_image() def get_url(self) -> str: + """ + Получения ссылки на изображение части скина. + + :return: Ссылка на изображение части скина. + """ return self.__skin_part_url def get_image(self) -> bytes: + """ + Получения изображения части скина. + + :return: Изображения части скина. + """ + try: visage_surgeplay_response = rq.get(self.__skin_part_url) if visage_surgeplay_response.status_code != 200: @@ -99,4 +113,7 @@ class User: return self._profile def get_skin(self) -> Skin: + """ + Получения объекта скина пользователя. + """ return Skin(self._profile) diff --git a/pyspw/api.py b/pyspw/api.py index e2d771e..8538948 100644 --- a/pyspw/api.py +++ b/pyspw/api.py @@ -29,6 +29,16 @@ class _Card: class SpApi: + """ + API класс для работы с spworlds api + + :param card_id: Индефикатор карты + :type card_id: str + + :param card_token: Секретный ключ доступа карты + :type card_token: str + """ + _spworlds_api_url = 'https://spworlds.ru/api/public' def __init__(self, card_id: str, card_token: str): @@ -65,8 +75,9 @@ class SpApi: def ping(self) -> bool: """ - Проверка работоспособности API - :return: Bool работает или нет + Проверка работоспособности API. + + :return: Состояние API. """ try: self.get_balance() @@ -77,9 +88,14 @@ class SpApi: def get_user(self, discord_id: str) -> User: """ - Получение пользователя + Получение пользователя. + :param discord_id: ID пользователя дискорда. - :return: Class pyspw.User.User + :type discord_id: bool + + :return: Объект пользователя. + + :raises SpwUserNotFound: Пользователь не был найден. """ response = self._request(_RequestTypes.GET, f'/users/{discord_id}', ignore_codes=[404]) @@ -90,51 +106,75 @@ class SpApi: def check_access(self, discord_id: str) -> bool: """ - Получение статуса проходки + Получение статуса проходки. + :param discord_id: ID пользователя дискорда. - :return: Bool True если у пользователя есть проходка, иначе False + :type discord_id: bool + + :return: Состояние проходки пользователя. """ response = self._request(_RequestTypes.GET, f'/users/{discord_id}', ignore_codes=[404]) return response.status_code != 404 def check_webhook(self, webhook_data: str, X_Body_Hash: str) -> bool: """ - Валидирует webhook + Валидирует webhook. + :param webhook_data: Тело webhook'а. + :type webhook_data: str + :param X_Body_Hash: Хэдер X-Body-Hash из webhook. - :return: Bool True если вебхук пришел от верифицированного сервера, иначе False + :type X_Body_Hash: str + + :return: Верефецирован или нет вебхук. """ hmac_data = hmac.new(self._card.token.encode('utf-8'), webhook_data.encode('utf-8'), sha256).digest() base64_data = b64encode(hmac_data) return hmac.compare_digest(base64_data, X_Body_Hash.encode('utf-8')) - def create_payment(self, params: Payment) -> str: + def create_payment(self, payment: Payment) -> str: """ - Создание ссылки на оплату - :param params: class PaymentParams параметров оплаты - :return: Str ссылка на страницу оплаты, на которую стоит перенаправить пользователя. - """ - return self._request(_RequestTypes.POST, '/payment', params.dict()).json()['url'] + Создание ссылки на оплату. - def send_transaction(self, params: Transaction) -> None: + :param payment: Параметры оплаты. + :type payment: Payment + + :return: Ссылку на страницу оплаты, на которую стоит перенаправить пользователя. """ - Отправка транзакции - :param params: class TransactionParameters параметры транзакции - :return: None. + return self._request(_RequestTypes.POST, '/payment', payment.dict()).json()['url'] + + def send_transaction(self, transaction: Transaction) -> None: """ - response = self._request(_RequestTypes.POST, '/transactions', params.dict(), ignore_codes=[400]) - if response.status_code == 400 and response.json()["message"] == 'Недостаточно средств на карте': - raise err.SpwInsufficientFunds() + Отправка транзакции. + + :param transaction: Параметры транзакции. + :type transaction: Transaction + + :raises SpwInsufficientFunds: Недостаточно средств на карте. + :raises SpwCardNotFound: Карта получателя не найдена. + """ + response = self._request(_RequestTypes.POST, '/transactions', transaction.dict(), ignore_codes=[400]) + if response.status_code == 400: + msg = response.json()["message"] + if msg == 'Недостаточно средств на карте': + raise err.SpwInsufficientFunds() + + elif msg == 'Карты не существует': + raise err.SpwCardNotFound() def get_balance(self) -> int: """ - Получение баланса - :return: Int со значением баланса + Получение баланса. + + :return: Значения баланса карты. """ return self._request(_RequestTypes.GET, '/card').json()['balance'] - # Manys + # --------------------------------- + # ------------- Manys ------------- + # --------------------------------- + def _many_req(self, iterable: List, method: Callable, delay: float) -> List: users = [] @@ -149,36 +189,70 @@ class SpApi: def get_users(self, discord_ids: List[str], delay: float = 0.3) -> List[User]: """ - Получение пользователей - :param delay: Значение задержки между запросами, указывается в секундах - :param discord_ids: List с IDs пользователей дискорда. - :return: List содержащий Classes pyspw.User.User + Получение пользователей. + + :param delay: Значение задержки между запросами, указывается в секундах. + :type delay: float + + :param discord_ids: Список discord id пользователей, которых вы бы хотели получить. + :type discord_ids: List[str] + + :return: Список с пользователями. + + :raises SpwUserNotFound: Пользователь не был найден. """ return self._many_req(discord_ids, self.get_user, delay) def check_accesses(self, discord_ids: List[str], delay: float = 0.3) -> List[bool]: """ - Получение статуса проходок - :param delay: Значение задержки между запросами, указывается в секундах - :param discord_ids: List с IDs пользователей дискорда. - :return: List содержащий bool со значением статуса проходки + Получение статуса проходок. + + :param delay: Значение задержки между запросами, указывается в секундах. + :type delay: float + + :param discord_ids: Список discord id пользователей статусы проходок, которых вы бы хотели получить. + :type discord_ids: List[str] + + :return: Список со статусами проходок. """ return self._many_req(discord_ids, self.check_access, delay) def create_payments(self, payments: List[Payment], delay: float = 0.5) -> List[str]: """ - Создание ссылок на оплату - :param payments: Список содержащий classes PaymentParams - :param delay: Значение задержки между запросами, указывается в секундах - :return: List со ссылками на страницы оплаты, в том порядке, в котором они были в кортеже payments + Создание ссылок на оплату. + + :param delay: Значение задержки между запросами, указывается в секундах. + :type delay: float + + :param payments: Список параметров оплаты. + :type payments: List[Payment] + + :return: Список ссылок на оплату. """ return self._many_req(payments, self.create_payment, delay) def send_transactions(self, transactions: List[Transaction], delay: float = 0.5) -> None: """ - Отправка транзакций - :param delay: Значение задержки между запросами, указывается в секундах - :param transactions: Список содержащий classes TransactionParameters - :return: List со ссылками на страницы оплаты, в том порядке, в котором они были в кортеже payments + Отправка транзакций. + + .. warning:: + **Важно: Перед множетсвенной отправки транзаций проводится дополнительная проверка на количество средств на карте. + В случае если во время совершения транзакций кто-либо еще спишет с этой карты сумму, после которой + остаток на карте не будет достаточен для проведения транзакции, то выполнение транзакций прервется, + а предыдущие транзации не откатятся.** + + :param delay: Значение задержки между запросами, указывается в секундах. + :type delay: float + + :param transactions: Список параметров транзакций + :type transactions: List[Transaction] + + :raises SpwInsufficientFunds: Недостаточно средств на карте. + :raises SpwCardNotFound: Карта получателя не найдена. """ + + # Additional balance verify + if self.get_balance() < sum([tr.amount for tr in transactions]): + raise err.SpwInsufficientFunds() + self._many_req(transactions, self.send_transaction, delay) diff --git a/pyspw/errors.py b/pyspw/errors.py index f11e43b..b832e3a 100644 --- a/pyspw/errors.py +++ b/pyspw/errors.py @@ -35,6 +35,11 @@ class SpwInsufficientFunds(SpwApiError): super().__init__("Insufficient funds on the card") +class SpwCardNotFound(SpwApiError): + def __init__(self): + super().__init__("Receiver card not found") + + class MojangApiError(_ApiError): pass @@ -51,3 +56,23 @@ class MojangAccountNotFound(MojangApiError): class SurgeplayApiError(_ApiError): pass + + +class LengthError(ValueError): + def __init__(self, max_length: int): + super().__init__(f"length must be <= {max_length}.") + + +class BigAmountError(ValueError): + def __init__(self): + super().__init__(f"amount must be <= 1728.") + + +class IsNotURLError(ValueError): + def __init__(self): + super().__init__(f"is not url.") + + +class IsNotCardError(ValueError): + def __init__(self, card: str): + super().__init__(f"Receiver card (`{card}`) number not valid")