11 Commits

Author SHA1 Message Date
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
deesiigneer
b3d56a6059 new version 2024-04-18 21:45:13 +05:00
18c1ff1daf Update conf.py 2024-04-18 18:34:34 +05:00
Aleksey
eff14052fd Merge pull request #11 from stepan-zubkov/fix-readme
Fix Linux/Mac OS installation in README
2023-01-09 12:24:16 +03:00
Степан Зубков
3f935a060b Update README.rst 2023-01-09 10:48:49 +03:00
Aleksey
3ecf1fff8a add v3-asyncio 2022-08-16 08:38:27 +03:00
deesiigneer
74a46277f8 version dropdown fix 2022-08-16 00:12:04 +03:00
34 changed files with 622 additions and 485 deletions

View File

@@ -2,7 +2,10 @@ version: 2
formats: []
build:
image: latest
os: ubuntu-lts-latest
tools:
python: '3.8'
sphinx:
configuration: docs/conf.py
@@ -10,7 +13,6 @@ sphinx:
builder: html
python:
version: "3.8"
install:
- method: pip
path: .

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 Aleksey
Copyright (c) 2022 deesiigneer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -37,7 +37,7 @@ Installation
.. code:: sh
sudo apt pip3 install pyspapi
pip3 install pyspapi
Quick example
--------------
@@ -46,9 +46,17 @@ Checking the balance
~~~~~~~~~~~~~~~~~~~~~
.. code:: py
import pyspapi
from pyspapi import SPAPI
from asyncio import get_event_loop
print(await pyspapi.API(card_id='card_id', token='token').balance)
spapi = SPAPI(card_id='CARD_ID', token='TOKEN')
async def main():
print(await spapi.balance)
loop = get_event_loop()
loop.run_until_complete(main())
More examples can be found in the `examples <https://github.com/deesiigneer/pyspapi/tree/main/examples>`_
@@ -57,6 +65,5 @@ Links
- `Discord server <https://discord.gg/VbyHaKRAaN>`_
- `pyspapi documentation <https://pyspapi.readthedocs.io/>`_
- `API documentation <https://spworlds.readthedocs.io>`_
- `PyPi <https://pypi.org/project/pyspapi/>`_
- `API documentation for SP sites <https://github.com/sp-worlds/api-docs>`_

View File

@@ -1,13 +1,13 @@
/* Background of stable should be green */
#version_switcher a[data-version-name*="stable"] {
.version-switcher__container a[data-version-name*="stable"] {
position: relative;
}
#version_switcher a[data-version-name*="stable"] span {
.version-switcher__container a[data-version-name*="stable"] span {
color: var(--pst-color-success);
}
#version_switcher a[data-version-name*="stable"] span:before {
.version-switcher__container a[data-version-name*="stable"] span:before {
content: "";
width: 100%;
height: 100%;
@@ -16,4 +16,4 @@
top: 0;
background-color: var(--pst-color-success);
opacity: 0.1;
}
}

View File

@@ -2,11 +2,16 @@
{
"name": "latest",
"version": "latest",
"url": "https://pyspapi.readthedocs.io/en/latest/"
"url": "https://pyspapi.readthedocs.io/ru/latest/"
},
{
"name": "stable",
"version": "stable",
"url": "https://pyspapi.readthedocs.io/en/stable/"
"url": "https://pyspapi.readthedocs.io/ru/stable/"
},
{
"name": "v3-asyncio",
"version": "v3-asyncio",
"url": "https://pyspapi.readthedocs.io/ru/v3-asyncio/"
}
]

View File

@@ -1,4 +1,4 @@
.. py:currentmodule:: pyspapi
.. currentmodule:: pyspapi
API Reference
===============
@@ -25,50 +25,6 @@ There are two main ways to query version information.
-----------
``SPAPI``
~~~~~
~~~~~~~~~
.. autoclass:: SPAPI
:members:
.. automethod:: SPAPI.event()
:decorator:
.. automethod:: SPAPI.check_user_access
:decorator:
.. automethod:: SPAPI.get_user
:decorator:
.. automethod:: SPAPI.get_users
:decorator:
.. automethod:: SPAPI.payment
:decorator:
.. automethod:: SPAPI.transaction
:decorator:
.. automethod:: SPAPI.webhook_verify
:decorator:
MojangAPI
~~~~~
.. autoclass:: MojangAPI
:members:
.. automethod:: SPAPI.event()
:decorator:
.. automethod:: SPAPI.get_name_history
:decorator:
.. automethod:: SPAPI.get_profile
:decorator:
.. automethod:: SPAPI.get_username
:decorator:
.. automethod:: SPAPI.get_uuid
:decorator:
.. automethod:: SPAPI.get_uuids
:decorator:

View File

@@ -1,4 +1,7 @@
from re import search, MULTILINE
import os
import sys
project = 'pyspapi'
copyright = '2022, deesiigneer'
@@ -16,6 +19,9 @@ release = version
# -- General configuration
sys.path.insert(0, os.path.abspath(".."))
extensions = [
'sphinx.ext.duration',
'sphinx.ext.doctest',
@@ -24,12 +30,19 @@ extensions = [
'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),
}
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 = None
language = 'en'
locale_dirs = ["locale/"]
exclude_patterns = []
html_static_path = ["_static"]
@@ -65,12 +78,7 @@ html_theme_options = {
],
"header_links_before_dropdown": 4,
"show_toc_level": 1,
"navbar_start": ["navbar-logo", "version-switcher"],
"switcher": {
"json_url": "https://pyspapi.readthedocs.io/en/latest/_static/switcher.json",
"version_match": "latest"
},
"navbar_start": ["navbar-logo"],
"navigation_with_keys": True,
}
html_css_files = ["custom.css"]

View File

@@ -10,7 +10,7 @@ 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.
@@ -18,9 +18,17 @@ It looks something like this:
.. code-block:: python
import pyspapi
from pyspapi import SPAPI
from asyncio import get_event_loop
print(pyspapi.SPAPI(card_id='card_id', token='token').balance)
spapi = SPAPI(card_id='CARD_ID', token='TOKEN')
async def main():
print(await spapi.balance)
loop = get_event_loop()
loop.run_until_complete(main())
Make sure not to name it ``pyspapi`` as that'll conflict with the library.

View File

@@ -1,12 +0,0 @@
import pyspapi
import asyncio
api = pyspapi.API(card_id='card_id', token='token')
async def main():
print(await api.get_name_history(uuid='63ed47877aa3470fbfc46c5356c3d797'))
loop = asyncio.get_event_loop()
loop.run_until_complete(main())

View File

@@ -1,15 +0,0 @@
import pyspapi
import asyncio
api = pyspapi.API(card_id='card_id', token='token')
async def main():
mojang_profile = await api.get_profile(uuid='63ed47877aa3470fbfc46c5356c3d797')
print(mojang_profile)
print(mojang_profile.id, mojang_profile.timestamp)
print(mojang_profile.skin, mojang_profile.skin.model, mojang_profile.skin.cape_url, mojang_profile.skin.url)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())

View File

@@ -1,21 +0,0 @@
import pyspapi
import asyncio
api = pyspapi.API(card_id='card_id', token='token')
async def main():
user = await api.get_user(264329096920563714)
print(user)
print(user.access)
# У API есть лимиты, каждый user = 1 запрос, учитывайте это при использовании get_users
# https://spworlds.readthedocs.io/ru/latest/index.html#id3
users = await api.get_users([262632724928397312, 264329096920563714])
for user in users:
print(user)
if user is not None:
print(user.access)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())

15
examples/get_user.py Normal file
View File

@@ -0,0 +1,15 @@
from pyspapi import SPAPI
from asyncio import get_event_loop
spapi = SPAPI(card_id='CARD_ID', token='TOKEN')
async def main():
user = await spapi.get_user(262632724928397312)
print(user.username, user.uuid)
for card in user.cards:
print(card.name, card.number)
loop = get_event_loop()
loop.run_until_complete(main())

View File

@@ -1,17 +0,0 @@
import pyspapi
import pyspapi
import asyncio
api = pyspapi.API(card_id='card_id', token='token')
async def main():
uuid = await pyspapi.API(card_id='card_id', token='token').get_uuid(username='deesiigneer')
print(uuid)
print(uuid.id, uuid.name)
print(await pyspapi.API(card_id='card_id', token='token').get_uuids(['deesiigneer', '5opka', 'OsterMiner']))
loop = asyncio.get_event_loop()
loop.run_until_complete(main())

12
examples/me.py Normal file
View File

@@ -0,0 +1,12 @@
from pyspapi import SPAPI
from asyncio import get_event_loop
spapi = SPAPI(card_id='CARD_ID', token='TOKEN')
async def main():
me = await spapi.me
print(me)
loop = get_event_loop()
loop.run_until_complete(main())

View File

@@ -1,16 +1,21 @@
import pyspapi
import asyncio
from pyspapi import SPAPI
from pyspapi.types import Item
from asyncio import get_event_loop
api = pyspapi.API(card_id='card_id', token='token')
spapi = SPAPI(card_id='CARD_ID', token='TOKEN')
items = [Item('first item', 1, 2, 'first item comment').to_json(),
Item('second item', 3, 4, 'second item comment').to_json()]
async def main():
print(await api.payment(amount=1,
redirect_url='https://www.google.com/',
webhook_url='https://www.google.com/',
data='some-data'
)
print(await spapi.create_payment(items=items,
redirect_url='https://www.google.com/',
webhook_url='https://www.google.com/',
data='some-data'
)
)
loop = asyncio.get_event_loop()
loop = get_event_loop()
loop.run_until_complete(main())

View File

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

13
examples/webhook.py Normal file
View File

@@ -0,0 +1,13 @@
from pyspapi import SPAPI
from asyncio import get_event_loop
spapi = SPAPI(card_id='CARD_ID', token='TOKEN')
# print(spapi.webhook_verify(data='webhook_data', header='webhook_header'))
async def main():
print(await spapi.update_webhook(url='https://example.com/webhook'))
loop = get_event_loop()
loop.run_until_complete(main())

View File

@@ -1,5 +0,0 @@
import pyspapi
api = pyspapi.API(card_id='your_card_id', token='your_token')
api.listener(host='myhost.com', port=80, webhook_path='/webhook/')

View File

@@ -1,10 +0,0 @@
import pyspapi
import asyncio
api = pyspapi.API(card_id='your_card_id', token='your_token')
async def main():
print(await api.webhook_verify(data='webhook_data', header='webhook_header'))
loop = asyncio.get_event_loop()
loop.run_until_complete(main())

View File

@@ -1,4 +1,10 @@
from .api import API
from .types import SPUser, MojangProfile, Skin, UsernameToUUID
from .spworlds import *
from .types import *
from .api import *
__version__ = "3.0.0a0"
__author__ = 'deesiigneer'
__url__ = 'https://github.com/deesiigneer/pyspapi'
__description__ = 'API wrapper for SP servers written in Python.'
__license__ = 'MIT'
__version__ = "3.1.2"

View File

@@ -1,182 +0,0 @@
import ast
import warnings
import asyncio
from aiohttp import web
from sys import version_info
from base64 import b64encode, b64decode
from hmac import new, compare_digest
from hashlib import sha256
from logging import getLogger
from typing import Any, Dict, List, Optional, Union
from .types import SPUser, MojangProfile, UsernameToUUID
from .request import Request
from .errors import Error
log = getLogger('pyspapi')
class _BaseAPI:
_SPWORLDS = "https://spworlds.ru/api/public"
_API_MOJANG = "https://api.mojang.com"
_SESSIONSERVER_MOJANG = "https://sessionserver.mojang.com"
def __init__(self, card_id: str, token: str):
"""
:param card_id:
:param token:
"""
self._id = card_id
self._token = token
self._HEADER = {
'Authorization': f"Bearer {str(b64encode(str(f'{self._id}:{self._token}').encode('utf-8')), 'utf-8')}",
'User-Agent': f'pyspapi (https://github.com/deesiigneer/pyspapi) '
f'Python {version_info.major}.{version_info.minor}.{version_info.micro}'
}
async def webhook_verify(self, data: str, header) -> bool:
"""
Проверяет достоверность webhook'а. \n
:param data: data из webhook.
:param header: header X-Body-Hash из webhook.
:return: True если header из webhook'а достоверен, иначе False
"""
print()
hmac_data = b64encode(new(self._token.encode('utf-8'), data, sha256).digest())
return compare_digest(hmac_data, header.encode('utf-8'))
def listener(self, host: str = '127.0.0.1', port: int = 80, webhook_path: str = '/webhook/'):
app = web.Application()
async def handle(request):
request_data = await request.read()
header = request.headers.get('X-Body-Hash')
if header is not None:
if await self.webhook_verify(data=request_data, header=header) is True:
web.json_response(status=202)
return True
else:
web.json_response(status=400)
else:
return web.json_response(status=404)
app.add_routes([web.get(webhook_path, handle)])
web.run_app(app, port=port, host=host)
class API(_BaseAPI):
"""
class API
"""
async def get_user(self, user_id: int) -> Optional[SPUser]:
"""
Получение информации об игроке SP \n
:param user_id: ID пользователя в Discord.
:return: Class User.
"""
sp_user = await Request.get(f'{self._SPWORLDS}/users/{str(user_id)}', self._HEADER)
if sp_user is not None:
return SPUser(await Request.get(f'{self._SPWORLDS}/users/{str(user_id)}', self._HEADER))
else:
return None
async def get_users(self, user_ids: List[int]) -> Union[SPUser, Any]:
"""
Получение никнеймов игроков в майнкрафте. **Максимально можно указать 60 user_ids, не используйте эту функцию
чаще 1 раза в минуту если указали больше 60 user_ids**\n
https://spworlds.readthedocs.io/ru/latest/index.html#id3\n
:param user_ids: List[int] ID пользователей в Discord.
:return: List[str] который содержит майнкрафт никнеймы игроков в том же порядке, который был задан, None если
пользователь не найден или нет проходки.
"""
if len(user_ids) > 60:
user_ids = user_ids[:60]
warnings.warn('user_ids больше чем 60. Уменьшено до 60.')
tasks = []
for user_id in user_ids:
tasks.append(self.get_user(user_id))
return await asyncio.gather(*tasks, return_exceptions=True)
async def get_uuid(self, username: str) -> Optional[UsernameToUUID]:
"""
Получить UUID игрока Minecraft.\n
:param username: str никнейм игрока Minecraft.
:return: Optional[str] UUID игрока Minecraft.
"""
response = await Request.get(f'{self._API_MOJANG}/users/profiles/minecraft/{username}')
return UsernameToUUID(await Request.get(f'{self._API_MOJANG}/users/profiles/minecraft/{username}'))
async def get_uuids(self, usernames: list[str]) -> Dict[str, str]:
"""
Получить UUID's игроков Minecraft. **Не больше 10**\n
:param usernames: List[str] Список с никнеймами игроков Minecraft.
:return: Dict[str, str] UUID игроков Minecraft.
"""
if len(usernames) > 10:
usernames = usernames[:10]
warnings.warn('usernames больше чем 10. Уменьшено до 10.')
return await Request.post(f'{self._API_MOJANG}/profiles/minecraft', payload=usernames)
async def get_name_history(self, uuid: str) -> List[Dict[str, Any]]:
"""
История никнеймов в Minecraft.\n
:param uuid: UUID игрока Minecraft.
:return: List[Dict[str, Any]] который содержит name и changed_to_at
"""
requests = await Request.get(f"{self._API_MOJANG}/user/profiles/{uuid}/names")
name_data = []
for data in requests:
name_data_dict = {"name": data["name"]}
if data.get("changedToAt"):
name_data_dict["changed_to_at"] = data["changedToAt"]
else:
name_data_dict["changed_to_at"] = 0
name_data.append(name_data_dict)
return name_data
async def get_profile(self, uuid: str) -> MojangProfile:
response = await Request.get(f'{self._SESSIONSERVER_MOJANG}/session/minecraft/profile/{uuid}')
return MojangProfile(ast.literal_eval(b64decode(response["properties"][0]["value"]).decode()))
async def payment(self, amount: int, redirect_url: str, webhook_url: str, data: str) -> Optional[str]:
"""
Создание ссылки для оплаты.\n
:param amount: Стоимость покупки в АРах.
:param redirect_url: URL страницы, на которую попадет пользователь после оплаты.
:param webhook_url: URL, куда наш сервер направит запрос, чтобы оповестить ваш сервер об успешной оплате.
:param data: Строка до 100 символов, сюда можно поместить любые полезные данных.
:return: Ссылку на страницу оплаты, на которую стоит перенаправить пользователя.
"""
if len(data) > 100:
raise Error('В data больше 100 символов')
return await Request.post(f'{self._SPWORLDS}/payment',
payload={
'amount': amount,
'redirectUrl': redirect_url,
'webhookUrl': webhook_url,
'data': data},
headers=self._HEADER)
async def transaction(self, receiver: int, amount: int, comment: str) -> Optional[str]:
"""
Перевод АР на карту. \n
:param receiver: Номер карты получателя.
:param amount: Количество АР для перевода.
:param comment: Комментарий для перевода.
:return: True если перевод успешен, иначе False.
"""
return 'Удачно' if await Request.post(f'{self._SPWORLDS}/transactions',
payload={'receiver': receiver,
'amount': amount,
'comment': comment},
headers=self._HEADER) else 'Что-то пошло не так...'
@property
async def balance(self) -> Optional[int]:
"""
Проверка баланса карты \n
:return: Количество АР на карте.
"""
balance = await Request.get(f'{self._SPWORLDS}/card', headers=self._HEADER)
return balance['balance']

2
pyspapi/api/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from .api import APISession

90
pyspapi/api/api.py Normal file
View File

@@ -0,0 +1,90 @@
from base64 import b64encode
from logging import getLogger
import aiohttp
import json
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/"
self.__id = card_id
self.__token = token
self.__sleep_timeout = sleep_time
self.__retries = retries
self.__timeout = timeout
self.session = None
async def __aenter__(self):
self.session = aiohttp.ClientSession(
json_serialize=json.dumps,
timeout=aiohttp.ClientTimeout(total=self.__timeout))
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=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
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}")
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}")
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}")

View File

@@ -1,29 +0,0 @@
class Error(Exception):
pass
class Unauthorized(Exception):
pass
class NotFound(Exception):
pass
class TooManyRequests(Exception):
pass
class UserNotFound(Exception):
pass
def handle(response):
if response['error'] == 'Unauthorized':
raise Unauthorized(response['message'])
elif response['error'] == 'Not Found':
raise NotFound(response['message'])
elif response['error'] == 'Too Many Requests':
raise TooManyRequests(response['message'])
else:
raise Exception(response)

0
pyspapi/exceptions.py Normal file
View File

View File

@@ -1,31 +0,0 @@
import aiohttp
from .errors import handle
from typing import Coroutine, Any, TypeVar
T = TypeVar("T")
Response = Coroutine[Any, Any, T]
class Request:
async def get(url: str, headers=None):
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as session_response:
try:
response = await session_response.json()
except:
response = await session_response.text()
if session_response.status == 404:
return None
if not session_response.ok:
handle(response)
return response
async def post(url: str, payload, headers=None):
async with aiohttp.ClientSession() as session:
session_response = await session.post(url, json=payload, headers=headers)
try:
response = await session_response.json()
except:
response = await session_response.text()
if not session_response.ok:
handle(response)
return response

205
pyspapi/spworlds.py Normal file
View File

@@ -0,0 +1,205 @@
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
__all__ = ['SPAPI']
class SPAPI(APISession):
"""
Представляет собой клиент API для взаимодействия с конкретным сервисом.
"""
def __init__(self, card_id=None, token=None, timeout=5, sleep_time=0.2, retries=0):
"""
Инициализирует объект SPAPI.
:param card_id: Идентификатор карты.
:type card_id: str
:param token: Токен API.
:type token: str
:param timeout: Таймаут для запросов API в секундах. По умолчанию 5.
:type timeout: int, optional
:param sleep_time: Время ожидания между повторными запросами в секундах. По умолчанию 0.2.
:type sleep_time: float, optional
:param retries: Количество повторных попыток для неудачных запросов. По умолчанию 0.
:type retries: int, optional
"""
super().__init__(card_id, token, timeout, sleep_time, retries)
self.__card_id = card_id
self.__token = token
def __repr__(self):
"""
Возвращает строковое представление объекта 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
@property
async def balance(self):
"""
Получает текущий баланс карты.
:return: Текущий баланс карты.
:rtype: int
"""
card = await self.__get('card')
return card['balance']
@property
async def webhook(self) -> str:
"""
Получает URL вебхука, связанного с картой.
:return: URL вебхука.
:rtype: str
"""
card = await self.__get('card')
return card['webhook']
@property
async def me(self):
"""
Получает информацию об аккаунте текущего пользователя.
:return: Объект Account, представляющий аккаунт текущего пользователя.
:rtype: Account
"""
me = await self.__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'])
async def get_user(self, discord_id: int) -> User:
"""
Получает информацию о пользователе по его ID в Discord.
:param discord_id: ID пользователя в Discord.
:type discord_id: int
:return: Объект User, представляющий пользователя.
:rtype: 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)
async def create_transaction(self, receiver: str, amount: int, comment: str):
"""
Создает транзакцию.
:param receiver: Получатель транзакции.
:type receiver: str
:param amount: Сумма транзакции.
:type amount: int
:param comment: Комментарий к транзакции.
:type comment: str
: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']
async def create_payment(self, webhook_url: str, redirect_url: str, data: str, items) -> str:
"""
Создает платеж.
:param webhook_url: URL вебхука для платежа.
:type webhook_url: str
:param redirect_url: URL для перенаправления после платежа.
:type redirect_url: str
:param data: Дополнительные данные для платежа.
:type data: str
:param items: Элементы, включаемые в платеж.
:return: URL для платежа.
: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']
async def update_webhook(self, url: str):
"""
Обновляет URL вебхука, связанного с картой.
:param url: Новый URL вебхука.
:type url: str
:return: JSON-ответ от API.
:rtype: dict
"""
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
def webhook_verify(self, data: str, header) -> bool:
"""
Проверяет достоверность вебхука.
:param data: Данные из вебхука.
:type data: str
:param header: Заголовок X-Body-Hash из вебхука.
: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'))
def to_dict(self) -> dict:
"""
Преобразует объект SPAPI в словарь.
:return: Словарное представление объекта SPAPI.
:rtype: dict
"""
return self.__dict__.copy()

View File

@@ -1,63 +0,0 @@
class SPUser:
def __init__(self, data: dict):
self.__data = data
@property
def access(self) -> bool:
return True if self.__data['username'] is not None else False
def __repr__(self):
return self.__data['username']
class MojangProfile:
def __init__(self, data: dict):
self.__data = data
self.skin = Skin(data)
@property
def id(self) -> str:
return self.__data['profileId']
@property
def timestamp(self):
return self.__data['timestamp']
def __repr__(self):
return self.__data['profileName']
class Skin:
def __init__(self, data: dict):
self.__data = data
@property
def url(self) -> str:
return self.__data['textures']['SKIN']['url']
@property
def cape_url(self) -> str:
return self.__data['textures']['CAPE']['url']
@property
def model(self) -> str:
return 'classic' if self.__data['textures']['SKIN'].get('metadata') is None else 'slim'
def __repr__(self):
return str(self.__data['textures']['SKIN'])
class UsernameToUUID:
def __init__(self, data: dict):
self.__data = data
@property
def id(self):
return self.__data['id']
@property
def name(self):
return self.__data['name']
def __repr__(self):
return str(self.__data['id'])

View File

@@ -0,0 +1,3 @@
from .payment import Item
from .users import User
from .me import Account

120
pyspapi/types/me.py Normal file
View File

@@ -0,0 +1,120 @@
class City:
def __init__(
self,
city_id=None,
name=None,
description=None,
x_cord=None,
z_cord=None,
is_mayor=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
@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
@property
def z_cord(self):
return self._z_cord
@property
def mayor(self):
return self._isMayor
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})"
class Cards:
def __init__(self, card_id=None, name=None, number=None, color=None):
self._id = card_id
self._name = name
self._number = number
self._color = color
@property
def id(self):
return self._id
@property
def name(self):
return self._name
@property
def number(self):
return self._number
@property
def color(self):
return self._color
def __repr__(self):
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):
self._id = account_id
self._username = username
self._status = status
self._roles = roles
self._city = City(**city) if city else None
self._cards = [
Cards(
card_id=card["id"],
name=card["name"],
number=card["number"],
color=card["color"],
)
for card in cards
]
self._created_at = created_at
@property
def id(self):
return self._id
@property
def username(self):
return self._username
@property
def status(self):
return self._status
@property
def roles(self):
return self._roles
@property
def city(self):
return self._city
@property
def cards(self):
return self._cards
@property
def created_at(self):
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})"

18
pyspapi/types/payment.py Normal file
View File

@@ -0,0 +1,18 @@
class Item:
def __init__(self, name: str, count: int, price: int, comment: str):
self._name = name
self._count = count
self._price = price
self._comment = comment
@property
def name(self):
return self._name
def to_json(self):
return {
"name": self._name,
"count": self._count,
"price": self._price,
"comment": self._comment
}

51
pyspapi/types/users.py Normal file
View File

@@ -0,0 +1,51 @@
class Cards:
def __init__(self, name, number):
self._name: str = name
self._number: str = number
@property
def name(self):
return self._name
@property
def number(self):
return self._number
# def __repr__(self):
# return "%s(%s)" % (
# self.__class__.__name__,
# self.__dict__
# )
class User:
def __init__(self, username, uuid, cards):
self._username: str = username
self._uuid: str = uuid
self._cards = [
Cards(
name=card["name"],
number=card["number"],
)
for card in cards
]
@property
def username(self):
return self._username
@property
def uuid(self):
return self._uuid
@property
def cards(self):
return self._cards
def __repr__(self):
return "%s(%s)" % (
self.__class__.__name__,
self.__dict__
)

View File

@@ -1,2 +1 @@
requests==2.28.1
aiohttp>=3.8.0,<4.0.0
aiohttp>=3.8.0

View File

@@ -1,6 +1,6 @@
import re
from setuptools import setup
from setuptools import setup, find_packages
requirements = []
with open("requirements.txt") as f:
@@ -21,10 +21,6 @@ readme = ""
with open("README.rst") as f:
readme = f.read()
packages = [
"pyspapi"
]
setup(
name='pyspapi',
license='MIT',
@@ -32,15 +28,15 @@ setup(
version=version,
url='https://github.com/deesiigneer/pyspapi',
project_urls={
"pyspapi documentation": "https://pyspapi.readthedocs.io/",
"api documentation": "https://spworlds.readthedocs.io/",
"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,
packages=find_packages(),
package_data={'pyspapi': ['types/*', 'api/*']}, # Включаем дополнительные файлы и папки
include_package_data=True,
install_requires=requirements,
python_requires='>=3.8.0',