13 Commits

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
Aleksey
b738db3252 Merge pull request #15 from deesiigneer/v3-(asyncio)
new version
2024-04-18 21:49:27 +05:00
18c1ff1daf Update conf.py 2024-04-18 18:34:34 +05:00
20 changed files with 2210 additions and 350 deletions

View File

@@ -17,22 +17,28 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [ 3.9 ] python-version: [3.12]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 - name: Set up Python ${{ matrix.python-version }}
with: uses: actions/setup-python@v2
python-version: ${{ matrix.python-version }} with:
- name: Install dependencies python-version: ${{ matrix.python-version }}
run: |
python -m pip install --upgrade pip - name: Install Poetry
pip install twine run: |
- name: Compile package curl -sSL https://install.python-poetry.org | python3 -
run: | echo "$HOME/.local/bin" >> $GITHUB_PATH
python3 setup.py sdist
- name: Publish package - name: Install dependencies
uses: pypa/gh-action-pypi-publish@release/v1 run: poetry install --no-interaction
with:
user: __token__ - name: Build package
password: ${{ secrets.PYPI_API_TOKEN }} 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 version: 2
formats: []
build: build:
os: ubuntu-lts-latest os: ubuntu-lts-latest
tools: tools:
python: '3.8' python: '3.12'
sphinx: sphinx:
configuration: docs/conf.py configuration: docs/conf.py
fail_on_warning: false
builder: html builder: html
python: python:
@@ -18,4 +16,3 @@ python:
path: . path: .
extra_requirements: extra_requirements:
- docs - 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. # from the environment for the first two.
SPHINXOPTS ?= SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build SPHINXBUILD ?= sphinx-build
SOURCEDIR = source SOURCEDIR = .
BUILDDIR = build BUILDDIR = _build
# Put it first so that "make" without argument is like "make help". # Put it first so that "make" without argument is like "make help".
help: help:

View File

@@ -1,25 +1,25 @@
.. currentmodule:: pyspapi .. 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 .. 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 Как и в :obj:`py:sys.version_info`, допустимые значения для ``releaselevel`` это
'alpha', 'beta', 'candidate' and 'final'. 'alpha', 'beta', 'candidate' и 'final'.
.. data:: __version__ .. data:: __version__
A string representation of the version. Строковое представление версии.
``pyspapi`` ``pyspapi``
----------- -----------

View File

@@ -1,59 +1,44 @@
from re import search, MULTILINE from importlib.metadata import version as pkg_version
import os import os
import sys
project = "pyspapi"
author = "deesiigneer"
copyright = "2022, deesiigneer"
project = 'pyspapi' version = pkg_version("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.
release = version release = version
# -- General configuration
sys.path.insert(0, os.path.abspath(".."))
extensions = [ extensions = [
'sphinx.ext.duration', "sphinx.ext.duration",
'sphinx.ext.doctest', "sphinx.ext.doctest",
'sphinx.ext.autodoc', "sphinx.ext.autodoc",
'sphinx.ext.autosummary', "sphinx.ext.autosummary",
'sphinx.ext.intersphinx', "sphinx.ext.intersphinx",
] ]
autodoc_member_order = "bysource" autosummary_generate = True
autodoc_typehinta = "none"
intersphinx_mapping = {
'python': ('https://docs.python.org/3/', None),
'sphinx': ('https://www.sphinx-doc.org/en/master/', None),
}
version_match = os.environ.get("READTHEDOCS_VERSION") version_match = os.environ.get("READTHEDOCS_VERSION")
json_url = f"https://pyspapi.readthedocs.io/ru/{version_match}/_static/switcher.json" json_url = f"https://pyspapi.readthedocs.io/ru/{version_match}/_static/switcher.json"
intersphinx_disabled_domains = ['std']
language = 'en' language = "ru"
locale_dirs = ["locale/"]
exclude_patterns = [] exclude_patterns = []
html_static_path = ["_static"] html_static_path = ["_static"]
html_theme = "pydata_sphinx_theme" html_theme = "pydata_sphinx_theme"
html_logo = "./images/logo.png" html_logo = "./images/logo.png"
html_favicon = "./images/logo.ico" html_favicon = "./images/logo.ico"
html_theme_options = { html_theme_options = {
"external_links": [ "external_links": [
{ {
"url": "https://github.com/deesiigneer/pyspapi/releases", "url": "https://github.com/deesiigneer/pyspapi/releases",
"name": "Changelog", "name": "Changelog",
},
{
"url": "https://github.com/sp-worlds/api-docs/wiki",
"name": "SPWorlds API Docs",
} }
], ],
"icon_links": [ "icon_links": [
@@ -61,24 +46,27 @@ html_theme_options = {
"name": "GitHub", "name": "GitHub",
"url": "https://github.com/deesiigneer/pyspapi", "url": "https://github.com/deesiigneer/pyspapi",
"icon": "fab fa-brands fa-github", "icon": "fab fa-brands fa-github",
"type": "fontawesome" "type": "fontawesome",
}, },
{ {
"name": "Discord", "name": "Discord",
"url": "https://discord.gg/VbyHaKRAaN", "url": "https://discord.gg/VbyHaKRAaN",
"icon": "fab fa-brands fa-discord", "icon": "fab fa-brands fa-discord",
"type": "fontawesome" "type": "fontawesome",
}, },
{ {
"name": "PyPi", "name": "PyPI",
"url": "https://pypi.org/project/pyspapi/", "url": "https://pypi.org/project/pyspapi/",
"icon": "fab fa-brands fa-python", "icon": "fab fa-brands fa-python",
"type": "fontawesome" "type": "fontawesome",
} },
], ],
"header_links_before_dropdown": 4, "header_links_before_dropdown": 4,
"show_toc_level": 1, "show_toc_level": 1,
"navbar_start": ["navbar-logo"], "navbar_start": ["navbar-logo"],
"navigation_with_keys": True, "navigation_with_keys": True,
"switcher": {
"json_url": json_url,
"version_match": version_match,
},
} }
html_css_files = ["custom.css"]

View File

@@ -1,32 +1,31 @@
:theme_html_remove_secondary_sidebar: :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>` - **Первые шаги:** :ref:`Быстрый старт <quickstart>`
- **Examples:** Many examples are available in the `examples directory <https://github.com/deesiigneer/pyspapi/tree/main/examples/>`_. - **Примеры:** Много примеров доступно в `папке примеров <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. - Задавайте вопросы на сервере `Discord <https://discord.gg/VbyHaKRAaN>`_.
- If you're looking for something specific, try the :ref:`searching <search>`. - Если вы ищете что-то конкретное, попробуйте :ref:`поиск <search>`.
- Report bugs in the `issue tracker <https://github.com/deesiigneer/pyspapi/issues>`_. - Сообщайте об ошибках в `трекер проблем <https://github.com/deesiigneer/pyspapi/issues>`_.
- Ask in `GitHub discussions page <https://github.com/deesiigneer/pyspapi/discussions>`_.
Manuals Руководства
------- -----------
These pages go into great detail about everything the API can do. Эти страницы подробно описывают все, что может сделать API.
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1

View File

@@ -4,17 +4,17 @@
.. currentmodule:: pyspapi .. 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 .. code-block:: python
@@ -30,7 +30,6 @@ It looks something like this:
loop = get_event_loop() loop = get_event_loop()
loop.run_until_complete(main()) loop.run_until_complete(main())
Make sure not to name it ``pyspapi`` as that'll conflict with the library. Убедитесь, что вы не называете его ``pyspapi``, так как это вызовет конфликт с библиотекой.
Вы можете найти больше примеров в `папке примеров <https://github.com/deesiigneer/pyspapi/tree/main/examples/>`_ на GitHub.
You can find more examples in the `examples directory <https://github.com/deesiigneer/pyspapi/tree/main/examples/>`_ on GitHub.

View File

@@ -1,16 +1,15 @@
from pyspapi import SPAPI
from asyncio import get_event_loop from asyncio import get_event_loop
from pyspapi import SPAPI
spapi = SPAPI(card_id='CARD_ID', token='TOKEN') spapi = SPAPI(card_id='CARD_ID', token='TOKEN')
async def main(): async def main():
new_balance = await spapi.create_transaction(receiver='77552', new_balance = await spapi.create_transaction(receiver='77552',
amount=1, amount=1,
comment="test" comment='test')
)
print(new_balance) print(new_balance)
loop = get_event_loop() loop = get_event_loop()
loop.run_until_complete(main()) 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' __all__ = [SPAPI]
__url__ = 'https://github.com/deesiigneer/pyspapi'
__description__ = 'API wrapper for SP servers written in Python.' __author__: str = "deesiigneer"
__license__ = 'MIT' __url__: str = "https://github.com/deesiigneer/pyspapi"
__version__ = "3.1.0" __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

@@ -1,90 +1,100 @@
import asyncio
import json
from base64 import b64encode from base64 import b64encode
from logging import getLogger from logging import getLogger
from typing import Optional, Any, Dict
import aiohttp import aiohttp
import json
from ..exceptions import ValidationError, SPAPIError
log = getLogger('pyspapi') log = getLogger("pyspapi")
class APISession(object): class APISession(object):
""" Holds aiohttp session for its lifetime and wraps different types of request """ def __init__(
self,
def __init__(self, card_id: str, token: str, timeout=5, sleep_time=0.2, retries=0): card_id: str,
self.__url = "https://spworlds.ru/" 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.__id = card_id
self.__token = token self.__token = token
self.__sleep_timeout = sleep_time self.__sleep_timeout = sleep_time
self.__retries = retries self.__retries = retries
self.__timeout = timeout self.__timeout = timeout
self.session = None self.__raise_exception = raise_exception
self.__proxy = proxy
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
def __get_url(self, endpoint: str) -> str: async def request(
""" Get URL for requests """ self, method: str, endpoint: str, data: Optional[Dict] = None
url = self.__url ) -> Any:
api = "api/public" url = self.__url + endpoint
return f"{url}{api}/{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): attempt = 0
url = self.__get_url(endpoint) while True:
response = await self.session.request( attempt += 1
method=method, if attempt > 1:
url=url, log.warning(
json=data, f"[pyspapi] Repeat attempt {attempt}: {method.upper()} {url}"
headers={'Authorization': f"Bearer {str(b64encode(str(f'{self.__id}:{self.__token}').encode('utf-8')), 'utf-8')}", )
'User-Agent': 'https://github.com/deesiigneer/pyspapi'}, try:
ssl=True async with self.session.request(
) method, url, json=data, headers=headers
if response.status not in [200, 201]: ) as resp:
message = await response.json() if resp.status == 422:
raise aiohttp.ClientResponseError( errors = await resp.json()
code=response.status, log.error(f"[pyspapi] Validation error: {errors}")
message=message['message'], if self.__raise_exception:
headers=response.headers, raise ValidationError(errors)
history=response.history, return None
request_info=response.request_info
)
return response
async def get(self, endpoint, **kwargs): if resp.status >= 400:
""" GET requests """ content = await resp.text()
try: log.error(f"[pyspapi] API error {resp.status}: {content}")
return await self.__request("GET", endpoint, None, **kwargs) if self.__raise_exception:
except aiohttp.ClientResponseError as e: raise SPAPIError(resp.status, content)
log.error(f"GET request to {endpoint} failed with status {e.status}: {e.message}") return None
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}")
async def post(self, endpoint, data, **kwargs): return await resp.json()
""" POST requests """ except (aiohttp.ClientError, asyncio.TimeoutError) as e:
try: log.exception(f"[pyspapi] Connection error: {e} \n attempt {attempt}")
return await self.__request("POST", endpoint, data, **kwargs) if attempt > self.__retries:
except aiohttp.ClientResponseError as e: return None
log.error(f"POST request to {endpoint} failed with status {e.status}: {e.message}") await asyncio.sleep(self.__sleep_timeout)
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}")
async def put(self, endpoint, data, **kwargs): async def get(self, endpoint: str) -> Any:
""" PUT requests """ async with self:
try: return await self.request("GET", endpoint)
return await self.__request("PUT", endpoint, data, **kwargs)
except aiohttp.ClientResponseError as e: async def post(self, endpoint: str, data: Optional[Dict] = None) -> Any:
log.error(f"PUT request to {endpoint} failed with status {e.status}: {e.message}") async with self:
except aiohttp.ClientError as e: return await self.request("POST", endpoint, data)
log.error(f"PUT request to {endpoint} failed: {e}")
except Exception as e: async def put(self, endpoint: str, data: Optional[Dict] = None) -> Any:
log.error(f"PUT request to {endpoint} failed: {e}") 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 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): 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. Инициализирует объект SPAPI.
@@ -24,13 +37,19 @@ class SPAPI(APISession):
:param token: Токен API. :param token: Токен API.
:type token: str :type token: str
:param timeout: Таймаут для запросов API в секундах. По умолчанию 5. :param timeout: Таймаут для запросов API в секундах. По умолчанию 5.
:type timeout: int, optional :type timeout: int
:param sleep_time: Время ожидания между повторными запросами в секундах. По умолчанию 0.2. :param sleep_time: Время ожидания между повторными запросами в секундах. По умолчанию 0.2.
:type sleep_time: float, optional :type sleep_time: float
:param retries: Количество повторных попыток для неудачных запросов. По умолчанию 0. :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.__card_id = card_id
self.__token = token self.__token = token
@@ -38,67 +57,49 @@ class SPAPI(APISession):
""" """
Возвращает строковое представление объекта SPAPI. Возвращает строковое представление объекта SPAPI.
""" """
return "%s(%s)" % ( return f"{self.__class__.__name__}({vars(self)})"
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
@property @property
async def balance(self): async def balance(self) -> Optional[int]:
""" """
Получает текущий баланс карты. Получает текущий баланс карты.
:return: Текущий баланс карты. :return: Текущий баланс карты.
:rtype: int :rtype: int
""" """
card = await self.__get('card') return int((await super().get("card"))["balance"])
return card['balance']
@property @property
async def webhook(self) -> str: async def webhook(self) -> Optional[str]:
""" """
Получает URL вебхука, связанного с картой. Получает URL вебхука, связанного с картой.
:return: URL вебхука. :return: URL вебхука.
:rtype: str :rtype: str
""" """
card = await self.__get('card') return str((await super().get("card"))["webhook"])
return card['webhook']
@property @property
async def me(self): async def me(self) -> Optional[Account]:
""" """
Получает информацию об аккаунте текущего пользователя. Получает информацию об аккаунте текущего пользователя.
:return: Объект Account, представляющий аккаунт текущего пользователя. :return: Объект Account, представляющий аккаунт текущего пользователя.
:rtype: Account :rtype: :class:`Account`
""" """
me = await self.__get('account/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"],
status=me['status'], minecraftuuid=me["minecraftUUID"],
roles=me['roles'], status=me["status"],
city=me['city'], roles=me["roles"],
cards=me['cards'], cities=me["cities"],
created_at=me['createdAt']) 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. Получает информацию о пользователе по его ID в Discord.
@@ -106,13 +107,18 @@ class SPAPI(APISession):
:type discord_id: int :type discord_id: int
:return: Объект User, представляющий пользователя. :return: Объект User, представляющий пользователя.
:rtype: User :rtype: :class:`User`
""" """
user = await self.__get(f'users/{discord_id}') user = await super().get(f"users/{discord_id}")
cards = await self.__get(f"accounts/{user['username']}/cards") if user:
return User(user['username'], user['uuid'], cards) 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: Баланс после транзакции. :return: Баланс после транзакции.
:rtype: int :rtype: int
""" """
async with APISession(self.__card_id, self.__token) as session: data = {"receiver": receiver, "amount": amount, "comment": comment}
data = {
'receiver': receiver,
'amount': amount,
'comment': comment
}
res = await session.post('transactions', data)
res = await res.json()
return res['balance']
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 :type data: str
:param items: Элементы, включаемые в платеж. :param items: Элементы, включаемые в платеж.
:return: URL для платежа. :return: URL для платежа или None при ошибке.
:rtype: str :rtype: str
""" """
async with APISession(self.__card_id, self.__token) as session: data = {
data = { "items": items,
'items': items, "redirectUrl": redirect_url,
'redirectUrl': redirect_url, "webhookUrl": webhook_url,
'webhookUrl': webhook_url, "data": data,
'data': data }
}
res = await session.post('payments',data)
res = await res.json()
return res['url']
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 вебхука, связанного с картой. Обновляет URL вебхука, связанного с картой.
:param url: Новый URL вебхука. :param url: Новый URL вебхука.
:type url: str :return: Ответ API в виде словаря или None при ошибке.
:return: JSON-ответ от API.
:rtype: dict
""" """
async with APISession(self.__card_id, self.__token) as session: data = {"url": url}
data = {
'url': url
}
res = await session.put(endpoint='card/webhook', data=data)
if res:
res = await res.json()
return res
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. :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]

View File

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