11 Commits
3.1.0 ... 3.3.0

Author SHA1 Message Date
deesiigneer
931b1a8621 update poetry 2026-01-17 19:39:52 +00:00
deesiigneer
e9765e8b6a refactor: update Python publish workflow to use Poetry for dependency management 2026-01-17 19:30:51 +00:00
deesiigneer
047dbb38d0 chore: update Python version to 3.12 in GitHub Actions workflow 2026-01-17 19:02:52 +00:00
deesiigneer
6da906e0d1 feat(docs): localize documentation to Russian and update Makefile for Sphinx 2026-01-17 19:02:11 +00:00
deesiigneer
4fc530caeb refactor: improve code structure and add proxy support in APISession and SPAPI 2026-01-17 18:59:58 +00:00
deesiigneer
6e77bac3ba feat: migrate to poetry for dependency management and project configuration
- Added pyproject.toml for project metadata and dependencies.
- Removed requirements.txt as dependencies are now managed by poetry.
- Deleted setup.py as it is no longer needed with poetry.
2026-01-17 18:59:20 +00:00
deesiigneer
d36ecfca36 feat(api): добавлены новые исключения и параметр raise_exception для управления ошибками
- Добавлены классы исключений SPAPIError и ValidationError для улучшенной обработки ошибок API
- В APISession добавлен параметр raise_exception, который позволяет выбрасывать исключения при ошибках API
- Обновлены методы request, get, post, put для поддержки raise_exception
- Расширена модель SPAPI с передачей параметра raise_exception

refactor(api, models): улучшена структура кода и модели данных
- Упрощена и улучшена реализация APISession, исправлены устаревшие методы и типы
- Модель City переработана: добавлены новые поля (nether_x, nether_z, lane, role, created_at), улучшены свойства и __repr__
- Исправлена модель Card (исправлено имя класса с Cards на Card)
- В модели Account добавлено поле minecraftuuid, заменено поле city на cities с поддержкой списка объектов City
- Исправлены типы возвращаемых значений и добавлены аннотации типов в ключевых местах
- Устранены дублирования и улучшена читаемость кода
- Комментарии и докстринги уточнены и унифицированы

fix(api): исправлены ошибки и опечатки в коде
- Исправлено использование устаревших методов для запросов к API
- Удалены лишние пустые строки и форматирование под PEP8

Fixes #16

Signed-off-by: deesiigneer <goldenrump@gmail.com>
2025-07-14 21:35:57 +05:00
deesiigneer
c086954c25 fix endpoint 2024-04-18 23:09:40 +05:00
deesiigneer
8d60472b9a update packages in setup.py 2024-04-18 22:56:23 +05:00
deesiigneer
14166cf519 version update 2024-04-18 22:38:23 +05:00
deesiigneer
ab60b4f104 module load fix 2024-04-18 22:36:16 +05:00
20 changed files with 2210 additions and 350 deletions

View File

@@ -17,22 +17,28 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ 3.9 ]
python-version: [3.12]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install twine
- name: Compile package
run: |
python3 setup.py sdist
- name: Publish package
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
run: |
curl -sSL https://install.python-poetry.org | python3 -
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: poetry install --no-interaction
- name: Build package
run: poetry build
- name: Publish package
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}

View File

@@ -1,15 +1,13 @@
version: 2
formats: []
build:
os: ubuntu-lts-latest
tools:
python: '3.8'
python: '3.12'
sphinx:
configuration: docs/conf.py
fail_on_warning: false
builder: html
python:
@@ -18,4 +16,3 @@ python:
path: .
extra_requirements:
- docs
- requirements: docs/requirements.txt

View File

@@ -1,3 +0,0 @@
include README.rst
include LICENSE
include requirements.txt

View File

@@ -5,8 +5,8 @@
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@@ -17,4 +17,4 @@ help:
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@@ -1,25 +1,25 @@
.. currentmodule:: pyspapi
API Reference
Справочник API
===============
The following section outlines the API of pyspapi.
В следующем разделе описывается API pyspapi.
Version Info
Информация о версии
---------------------
There are two main ways to query version information.
Существует два основных способа запроса информации о версии.
.. data:: version_info
A named tuple that is similar to :obj:`py:sys.version_info`.
Именованный кортеж, аналогичный :obj:`py:sys.version_info`.
Just like :obj:`py:sys.version_info` the valid values for ``releaselevel`` are
'alpha', 'beta', 'candidate' and 'final'.
Как и в :obj:`py:sys.version_info`, допустимые значения для ``releaselevel`` это
'alpha', 'beta', 'candidate' и 'final'.
.. data:: __version__
A string representation of the version.
Строковое представление версии.
``pyspapi``
-----------

View File

@@ -1,59 +1,44 @@
from re import search, MULTILINE
from importlib.metadata import version as pkg_version
import os
import sys
project = "pyspapi"
author = "deesiigneer"
copyright = "2022, deesiigneer"
project = 'pyspapi'
copyright = '2022, deesiigneer'
author = 'deesiigneer'
with open("../pyspapi/__init__.py") as f:
match = search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), MULTILINE)
if not match or match.group(1) is None:
raise RuntimeError("The version could not be resolved")
version = match.group(1)
# The full version, including alpha/beta/rc tags.
version = pkg_version("pyspapi")
release = version
# -- General configuration
sys.path.insert(0, os.path.abspath(".."))
extensions = [
'sphinx.ext.duration',
'sphinx.ext.doctest',
'sphinx.ext.autodoc',
'sphinx.ext.autosummary',
'sphinx.ext.intersphinx',
"sphinx.ext.duration",
"sphinx.ext.doctest",
"sphinx.ext.autodoc",
"sphinx.ext.autosummary",
"sphinx.ext.intersphinx",
]
autodoc_member_order = "bysource"
autodoc_typehinta = "none"
intersphinx_mapping = {
'python': ('https://docs.python.org/3/', None),
'sphinx': ('https://www.sphinx-doc.org/en/master/', None),
}
autosummary_generate = True
version_match = os.environ.get("READTHEDOCS_VERSION")
json_url = f"https://pyspapi.readthedocs.io/ru/{version_match}/_static/switcher.json"
intersphinx_disabled_domains = ['std']
language = 'en'
locale_dirs = ["locale/"]
language = "ru"
exclude_patterns = []
html_static_path = ["_static"]
html_theme = "pydata_sphinx_theme"
html_logo = "./images/logo.png"
html_favicon = "./images/logo.ico"
html_theme_options = {
"external_links": [
{
"url": "https://github.com/deesiigneer/pyspapi/releases",
"name": "Changelog",
},
{
"url": "https://github.com/sp-worlds/api-docs/wiki",
"name": "SPWorlds API Docs",
}
],
"icon_links": [
@@ -61,24 +46,27 @@ html_theme_options = {
"name": "GitHub",
"url": "https://github.com/deesiigneer/pyspapi",
"icon": "fab fa-brands fa-github",
"type": "fontawesome"
"type": "fontawesome",
},
{
"name": "Discord",
"url": "https://discord.gg/VbyHaKRAaN",
"icon": "fab fa-brands fa-discord",
"type": "fontawesome"
"type": "fontawesome",
},
{
"name": "PyPi",
"name": "PyPI",
"url": "https://pypi.org/project/pyspapi/",
"icon": "fab fa-brands fa-python",
"type": "fontawesome"
}
"type": "fontawesome",
},
],
"header_links_before_dropdown": 4,
"show_toc_level": 1,
"navbar_start": ["navbar-logo"],
"navigation_with_keys": True,
"switcher": {
"json_url": json_url,
"version_match": version_match,
},
}
html_css_files = ["custom.css"]

View File

@@ -1,35 +1,34 @@
:theme_html_remove_secondary_sidebar:
Welcome to pyspapi
===================
Добро пожаловать в pyspapi
===========================
API wrapper for SP servers written in Python.
Обертка API для SPWorlds серверов, написанная на Python.
Getting started
Начало работы
---------------
Is this your first time using the library? This is the place to get started!
Вы впервые используете библиотеку? Это место, с которого нужно начать!
- **First steps:** :ref:`Quickstart <quickstart>`
- **Examples:** Many examples are available in the `examples directory <https://github.com/deesiigneer/pyspapi/tree/main/examples/>`_.
- **Первые шаги:** :ref:`Быстрый старт <quickstart>`
- **Примеры:** Много примеров доступно в `папке примеров <https://github.com/deesiigneer/pyspapi/tree/main/examples/>`_.
Getting help
------------
Получение помощи
------------------
If you're having trouble with something, these resources might help.
Если у вас есть проблемы с чем-то, эти ресурсы могут помочь.
- Ask questions in `Discord <https://discord.gg/VbyHaKRAaN>`_ server.
- If you're looking for something specific, try the :ref:`searching <search>`.
- Report bugs in the `issue tracker <https://github.com/deesiigneer/pyspapi/issues>`_.
- Ask in `GitHub discussions page <https://github.com/deesiigneer/pyspapi/discussions>`_.
- Задавайте вопросы на сервере `Discord <https://discord.gg/VbyHaKRAaN>`_.
- Если вы ищете что-то конкретное, попробуйте :ref:`поиск <search>`.
- Сообщайте об ошибках в `трекер проблем <https://github.com/deesiigneer/pyspapi/issues>`_.
Manuals
-------
Руководства
-----------
These pages go into great detail about everything the API can do.
Эти страницы подробно описывают все, что может сделать API.
.. toctree::
:maxdepth: 1
api
quickstart
quickstart

View File

@@ -4,17 +4,17 @@
.. currentmodule:: pyspapi
Quickstart
==========
Быстрый старт
==============
This page gives a brief introduction to the library.
На этой странице дается краткое введение в библиотеку.
Checking balance
----------------
Проверка баланса
-----------------
Let's output the amount of money remaining in the card account to the console.
Выведем количество денег, оставшихся на счету карты, на консоль.
It looks something like this:
Это выглядит примерно так:
.. code-block:: python
@@ -30,7 +30,6 @@ It looks something like this:
loop = get_event_loop()
loop.run_until_complete(main())
Make sure not to name it ``pyspapi`` as that'll conflict with the library.
Убедитесь, что вы не называете его ``pyspapi``, так как это вызовет конфликт с библиотекой.
You can find more examples in the `examples directory <https://github.com/deesiigneer/pyspapi/tree/main/examples/>`_ on GitHub.
Вы можете найти больше примеров в `папке примеров <https://github.com/deesiigneer/pyspapi/tree/main/examples/>`_ на GitHub.

View File

@@ -1,16 +1,15 @@
from pyspapi import SPAPI
from asyncio import get_event_loop
from pyspapi import SPAPI
spapi = SPAPI(card_id='CARD_ID', token='TOKEN')
async def main():
new_balance = await spapi.create_transaction(receiver='77552',
amount=1,
comment="test"
)
comment='test')
print(new_balance)
loop = get_event_loop()
loop.run_until_complete(main())

1826
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

29
pyproject.toml Normal file
View File

@@ -0,0 +1,29 @@
[project]
name = "pyspapi"
version = "3.3.0"
description = "API wrapper for SP servers written in Python."
readme = "README.rst"
license = { text = "MIT" }
authors = [{ name = "deesiigneer", email = "xdeesiigneerx@gmail.com" }]
requires-python = ">=3.12,<3.15"
dependencies = ["aiohttp>=3.9,<4.0"]
[project.urls]
Homepage = "https://pyspapi.deesiigneer.ru"
Documentation = "https://pyspapi.readthedocs.org"
Repository = "https://github.com/deesiigneer/pyspapi"
"Issue Tracker" = "https://github.com/deesiigneer/pyspapi/issues"
Discord = "https://discord.com/invite/VbyHaKRAaN"
[project.optional-dependencies]
docs = [
"sphinx>=7,<9",
"sphinx-autobuild>=2025.8.25,<2026.0.0",
"pydata-sphinx-theme>=0.16.1,<0.17.0",
]
dev = ["ruff>=0.14,<0.15", "toml-sort>=0.24,<0.25"]
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"

View File

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

@@ -1,90 +1,100 @@
import asyncio
import json
from base64 import b64encode
from logging import getLogger
from typing import Optional, Any, Dict
import aiohttp
import json
from ..exceptions import ValidationError, SPAPIError
log = getLogger('pyspapi')
log = getLogger("pyspapi")
class APISession(object):
""" Holds aiohttp session for its lifetime and wraps different types of request """
def __init__(self, card_id: str, token: str, timeout=5, sleep_time=0.2, retries=0):
self.__url = "https://spworlds.ru/"
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
self.__sleep_timeout = sleep_time
self.__retries = retries
self.__timeout = timeout
self.session = None
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
def __get_url(self, endpoint: str) -> str:
""" Get URL for requests """
url = self.__url
api = "api/public"
return f"{url}{api}/{endpoint}"
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",
}
async def __request(self, method: str, endpoint: str, data=None):
url = self.__get_url(endpoint)
response = await self.session.request(
method=method,
url=url,
json=data,
headers={'Authorization': f"Bearer {str(b64encode(str(f'{self.__id}:{self.__token}').encode('utf-8')), 'utf-8')}",
'User-Agent': 'https://github.com/deesiigneer/pyspapi'},
ssl=True
)
if response.status not in [200, 201]:
message = await response.json()
raise aiohttp.ClientResponseError(
code=response.status,
message=message['message'],
headers=response.headers,
history=response.history,
request_info=response.request_info
)
return response
attempt = 0
while True:
attempt += 1
if attempt > 1:
log.warning(
f"[pyspapi] Repeat attempt {attempt}: {method.upper()} {url}"
)
try:
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}")
if self.__raise_exception:
raise ValidationError(errors)
return None
async def get(self, endpoint, **kwargs):
""" GET requests """
try:
return await self.__request("GET", endpoint, None, **kwargs)
except aiohttp.ClientResponseError as e:
log.error(f"GET request to {endpoint} failed with status {e.status}: {e.message}")
except aiohttp.ClientError as e:
log.error(f"GET request to {endpoint} failed: {e}")
except Exception as e:
log.error(f"GET request to {endpoint} failed: {e}")
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)
return None
async def post(self, endpoint, data, **kwargs):
""" POST requests """
try:
return await self.__request("POST", endpoint, data, **kwargs)
except aiohttp.ClientResponseError as e:
log.error(f"POST request to {endpoint} failed with status {e.status}: {e.message}")
except aiohttp.ClientError as e:
log.error(f"POST request to {endpoint} failed: {e}")
except Exception as e:
log.error(f"POST request to {endpoint} failed: {e}")
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)
async def put(self, endpoint, data, **kwargs):
""" PUT requests """
try:
return await self.__request("PUT", endpoint, data, **kwargs)
except aiohttp.ClientResponseError as e:
log.error(f"PUT request to {endpoint} failed with status {e.status}: {e.message}")
except aiohttp.ClientError as e:
log.error(f"PUT request to {endpoint} failed: {e}")
except Exception as e:
log.error(f"PUT request to {endpoint} failed: {e}")
async def get(self, endpoint: str) -> Any:
async with self:
return await self.request("GET", endpoint)
async def post(self, endpoint: str, data: Optional[Dict] = None) -> Any:
async with self:
return await self.request("POST", endpoint, data)
async def put(self, endpoint: str, data: Optional[Dict] = None) -> Any:
async with self:
return await self.request("PUT", endpoint, data)

View File

@@ -0,0 +1,25 @@
class SPAPIError(Exception):
"""
Базовая ошибка для всех исключений, связанных с API SPWorlds.
"""
def __init__(self, status_code: int, message: str):
self.status_code = status_code
self.message = message
super().__init__(f"[{status_code}] {message}")
def __str__(self):
return f"SPAPIError: [{self.status_code}] {self.message}"
class ValidationError(SPAPIError):
"""
Ошибка валидации.
"""
def __init__(self, errors):
self.errors = errors
super().__init__(422, f"Validation failed: {errors}")
def __str__(self):
return f"ValidationError: {self.errors}"

View File

@@ -1,21 +1,34 @@
from .api import APISession
from .types import User
from .types.me import Account
from .types.payment import Item
from hmac import new, compare_digest
from hashlib import sha256
from base64 import b64encode
import aiohttp
from hashlib import sha256
from hmac import new, compare_digest
from typing import Optional
__all__ = ['SPAPI']
from .api import APISession
from pyspapi.types import User
from pyspapi.types.me import Account
from pyspapi.types.payment import Item
__all__ = ["SPAPI"]
class SPAPI(APISession):
"""
Представляет собой клиент API для взаимодействия с конкретным сервисом.
pyspapi — высокоуровневый клиент для взаимодействия с SPWorlds API.
Предоставляет удобные методы для работы с балансом карты, вебхуками,
информацией о пользователе, транзакциями и платежами, а также верификацией вебхуков.
"""
def __init__(self, card_id=None, token=None, timeout=5, sleep_time=0.2, retries=0):
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.
@@ -24,13 +37,19 @@ class SPAPI(APISession):
:param token: Токен API.
:type token: str
:param timeout: Таймаут для запросов API в секундах. По умолчанию 5.
:type timeout: int, optional
:type timeout: int
:param sleep_time: Время ожидания между повторными запросами в секундах. По умолчанию 0.2.
:type sleep_time: float, optional
:type sleep_time: float
:param retries: Количество повторных попыток для неудачных запросов. По умолчанию 0.
:type retries: int, optional
: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)
super().__init__(
card_id, token, timeout, sleep_time, retries, raise_exception, proxy
)
self.__card_id = card_id
self.__token = token
@@ -38,67 +57,49 @@ class SPAPI(APISession):
"""
Возвращает строковое представление объекта SPAPI.
"""
return "%s(%s)" % (
self.__class__.__name__,
self.__dict__
)
async def __get(self, method):
"""
Выполняет GET-запрос к API.
:param method: Метод API для вызова.
:type method: str
:return: JSON-ответ от API.
:rtype: dict
"""
async with APISession(self.__card_id, self.__token) as session:
response = await session.get(method)
response = await response.json()
return response
return f"{self.__class__.__name__}({vars(self)})"
@property
async def balance(self):
async def balance(self) -> Optional[int]:
"""
Получает текущий баланс карты.
:return: Текущий баланс карты.
:rtype: int
"""
card = await self.__get('card')
return card['balance']
return int((await super().get("card"))["balance"])
@property
async def webhook(self) -> str:
async def webhook(self) -> Optional[str]:
"""
Получает URL вебхука, связанного с картой.
:return: URL вебхука.
:rtype: str
"""
card = await self.__get('card')
return card['webhook']
return str((await super().get("card"))["webhook"])
@property
async def me(self):
async def me(self) -> Optional[Account]:
"""
Получает информацию об аккаунте текущего пользователя.
:return: Объект Account, представляющий аккаунт текущего пользователя.
:rtype: Account
:rtype: :class:`Account`
"""
me = await self.__get('account/me')
me = await super().get("accounts/me")
return Account(
account_id=me['id'],
username=me['username'],
status=me['status'],
roles=me['roles'],
city=me['city'],
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) -> User:
async def get_user(self, discord_id: int) -> Optional[User]:
"""
Получает информацию о пользователе по его ID в Discord.
@@ -106,13 +107,18 @@ class SPAPI(APISession):
:type discord_id: int
:return: Объект User, представляющий пользователя.
:rtype: User
:rtype: :class:`User`
"""
user = await self.__get(f'users/{discord_id}')
cards = await self.__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):
async def create_transaction(
self, receiver: str, amount: int, comment: str
) -> Optional[int]:
"""
Создает транзакцию.
@@ -126,17 +132,13 @@ class SPAPI(APISession):
:return: Баланс после транзакции.
:rtype: int
"""
async with APISession(self.__card_id, self.__token) as session:
data = {
'receiver': receiver,
'amount': amount,
'comment': comment
}
res = await session.post('transactions', data)
res = await res.json()
return res['balance']
data = {"receiver": receiver, "amount": amount, "comment": comment}
async def create_payment(self, webhook_url: str, redirect_url: str, data: str, items) -> str:
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]:
"""
Создает платеж.
@@ -148,40 +150,30 @@ class SPAPI(APISession):
:type data: str
:param items: Элементы, включаемые в платеж.
:return: URL для платежа.
:return: URL для платежа или None при ошибке.
:rtype: str
"""
async with APISession(self.__card_id, self.__token) as session:
data = {
'items': items,
'redirectUrl': redirect_url,
'webhookUrl': webhook_url,
'data': data
}
res = await session.post('payments',data)
res = await res.json()
return res['url']
data = {
"items": items,
"redirectUrl": redirect_url,
"webhookUrl": webhook_url,
"data": data,
}
async def update_webhook(self, url: str):
return str((await super().post("payments", data))["url"])
async def update_webhook(self, url: str) -> Optional[dict]:
"""
Обновляет URL вебхука, связанного с картой.
:param url: Новый URL вебхука.
:type url: str
:return: JSON-ответ от API.
:rtype: dict
:return: Ответ API в виде словаря или None при ошибке.
"""
async with APISession(self.__card_id, self.__token) as session:
data = {
'url': url
}
res = await session.put(endpoint='card/webhook', data=data)
if res:
res = await res.json()
return res
data = {"url": url}
def webhook_verify(self, data: str, header) -> bool:
return await super().put("card/webhook", data)
def webhook_verify(self, data: str, header: str) -> bool:
"""
Проверяет достоверность вебхука.
@@ -192,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]

View File

@@ -1,49 +1,61 @@
class City:
def __init__(
self,
city_id=None,
name=None,
description=None,
x_cord=None,
z_cord=None,
is_mayor=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._description = description
self._x_cord = x_cord
self._z_cord = z_cord
self._isMayor = is_mayor
self._x = x
self._z = z
self._nether_x = nether_x
self._nether_z = nether_z
self._lane = lane
self._role = role
self._created_at = created_at
@property
def id(self):
return self._id
@property
def description(self):
return self._description
@property
def name(self):
return self._name
@property
def x_cord(self):
return self._x_cord
def x(self):
return self._x
@property
def z_cord(self):
return self._z_cord
def z(self):
return self._z
@property
def mayor(self):
return self._isMayor
def nether_x(self):
return self._nether_x
@property
def nether_z(self):
return self._nether_z
@property
def lane(self):
return self._lane
@property
def role(self):
return self._role
@property
def created_at(self):
return self._created_at
def __repr__(self):
return f"City(id={self.id}, name={self.name}, description={self.description}, x={self.x_cord}, z={self.z_cord}, is_mayor={self.mayor})"
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})"
)
class Cards:
class Card:
def __init__(self, card_id=None, name=None, number=None, color=None):
self._id = card_id
self._name = name
@@ -67,18 +79,32 @@ class Cards:
return self._color
def __repr__(self):
return f"Card(id={self.id}, name={self.name}, number={self.number}, color={self.color})"
return f"Card(id={self._id}, name={self._name}, number={self._number}, color={self._color})"
class Account:
def __init__(self, account_id, username, status, roles, created_at, cards, city):
def __init__(self, account_id, username, minecraftuuid, status, roles, created_at, cards, cities):
self._id = account_id
self._username = username
self._minecraftuuid = minecraftuuid
self._status = status
self._roles = roles
self._city = City(**city) if city else None
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'],
)
for city in cities
]
self._cards = [
Cards(
Card(
card_id=card["id"],
name=card["name"],
number=card["number"],
@@ -96,6 +122,10 @@ class Account:
def username(self):
return self._username
@property
def minecraftuuid(self):
return self._minecraftuuid
@property
def status(self):
return self._status
@@ -105,8 +135,8 @@ class Account:
return self._roles
@property
def city(self):
return self._city
def cities(self):
return self._cities
@property
def cards(self):
@@ -117,4 +147,6 @@ class Account:
return self._created_at
def __repr__(self):
return f"Account(id={self.id}, username={self.username}, status={self.status}, roles={self.roles}, city={self.city}, cards={self.cards}, created_at={self.created_at})"
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})")

View File

@@ -1 +0,0 @@
aiohttp>=3.8.0

View File

@@ -1,46 +0,0 @@
import re
from setuptools import setup
requirements = []
with open("requirements.txt") as f:
requirements = f.read().splitlines()
version = ""
with open("pyspapi/__init__.py") as f:
match = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE)
if match is None or match.group(1) is None:
raise RuntimeError('Version is not set')
version = match.group(1)
if not version:
raise RuntimeError("Version is not set")
readme = ""
with open("README.rst") as f:
readme = f.read()
packages = [
"pyspapi"
]
setup(
name='pyspapi',
license='MIT',
author='deesiigneer',
version=version,
url='https://github.com/deesiigneer/pyspapi',
project_urls={
"Documentation": "https://pyspapi.readthedocs.io/ru/latest/",
"GitHub": "https://github.com/deesiigneer/pyspapi",
"Discord": "https://discord.com/invite/VbyHaKRAaN"
},
description='API wrapper for SP servers written in Python',
long_description=readme,
long_description_content_type='text/x-rst',
packages=packages,
include_package_data=True,
install_requires=requirements,
python_requires='>=3.8.0',
)