46 Commits

Author SHA1 Message Date
deesiigneer
b3d56a6059 new version 2024-04-18 21:45:13 +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
Aleksey
63ee509067 update logo 2022-07-23 20:05:52 +03:00
deesiigneer
4ecafef192 upload repo banner 2022-07-23 20:04:52 +03:00
deesiigneer
d44199c4cc upload repo banner 2022-07-23 20:00:37 +03:00
deesiigneer
056ebce615 logo fix (incorrect gradient placement, lol) 2022-07-23 19:35:22 +03:00
deesiigneer
b00b2b76cd docs upload 2022-07-23 19:33:28 +03:00
deesiigneer
da86771e3c version bump 2022-07-20 22:21:33 +03:00
deesiigneer
479a02b95f transactions fix #9 2022-07-20 22:20:22 +03:00
deesiigneer
5c905ea097 python version downgrade for /docs 2022-07-20 21:12:47 +03:00
deesiigneer
83d9663da3 version bump 2022-07-20 21:12:17 +03:00
deesiigneer
93760d2d87 add MANIFEST file for pip 2022-07-20 21:02:57 +03:00
deesiigneer
108d30ebac typo fix 2022-07-19 21:52:59 +03:00
deesiigneer
b4ddd1fd93 update example 2022-07-18 22:27:58 +03:00
deesiigneer
5449659566 update example 2022-07-18 22:15:43 +03:00
deesiigneer
85ef993011 update example and add get_uuids() example 2022-07-18 22:15:05 +03:00
deesiigneer
8e6664bc51 new feature get_uuids() 2022-07-18 22:14:06 +03:00
deesiigneer
0d46f0a126 version bump 2022-07-18 22:12:48 +03:00
Aleksey
f5484d3900 version fix 2022-07-18 05:59:29 +03:00
Aleksey
1c35daf142 Delete 68747470733a2f2f692e696d6775722e636f6d2f6d656c685768552e706e67.png 2022-07-18 05:47:29 +03:00
deesiigneer
91ed017ac6 Merge remote-tracking branch 'origin/main' 2022-07-18 05:47:10 +03:00
deesiigneer
1806ba7929 logo asset 2022-07-18 05:47:00 +03:00
deesiigneer
f4623e9204 typo pypi fix 2022-07-18 05:45:05 +03:00
Aleksey
906060a4b7 Add files via upload 2022-07-18 04:58:01 +03:00
Aleksey
6bf342a9f1 update
translate to English
2022-07-18 04:49:47 +03:00
Aleksey
a6cbf2adcc fix 2022-07-18 04:25:31 +03:00
Aleksey
59d43736c0 Update python-publish.yml 2022-07-18 03:44:58 +03:00
Aleksey
9429c443c4 Merge pull request #5 from deesiigneer/v2.0.x
v2.0.0!
2022-07-18 03:17:19 +03:00
Aleksey
152b5272c1 Update README.rst 2022-07-18 03:15:38 +03:00
deesiigneer
81d8b1de35 v2.0.0 2022-07-18 03:12:27 +03:00
Aleksey
0667922c8c Update and rename README.md to README.rst 2022-07-08 11:26:54 +03:00
Aleksey
9a61797253 Update README.md 2022-06-22 11:59:41 +03:00
Aleksey
c9c1bc8e81 Update README.md 2022-06-09 14:46:41 +03:00
Aleksey
f551a860bc Update setup.py 2022-06-08 10:53:05 +03:00
Aleksey
1065b5748b Merge pull request #3 from deesiigneer/feature/webhook_verify
Добавление webhook_verify
2022-06-08 10:50:20 +03:00
Aleksey
1d6af2b8ec Update README.md
Немного исправлено описание библиотеки и добавлено описание функции webhook_veify()
2022-06-08 10:24:55 +03:00
deesiigneer
f244e3f4db new feature webhook_verify 2022-06-08 10:00:38 +03:00
deesiigneer
76dc15a332 Merge remote-tracking branch 'origin/main' 2022-06-06 14:52:00 +03:00
deesiigneer
a2a4ac9fab fix #1 2022-06-06 14:51:38 +03:00
Aleksey
ef40d83a9c Update python-publish.yml 2022-06-05 17:25:41 +03:00
Aleksey
4a611396a1 Create python-publish.yml 2022-06-05 17:23:55 +03:00
Aleksey
7cc708ee03 fix: typo 2022-06-04 11:43:56 +03:00
Aleksey
8cba25a754 fix: Разметка 2022-06-03 21:25:42 +03:00
39 changed files with 1022 additions and 180 deletions

38
.github/workflows/python-publish.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Upload to PYPI
on:
release:
types: [published]
jobs:
regular:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ 3.9 ]
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 }}

21
.readthedocs.yml Normal file
View File

@@ -0,0 +1,21 @@
version: 2
formats: []
build:
os: ubuntu-lts-latest
tools:
python: '3.8'
sphinx:
configuration: docs/conf.py
fail_on_warning: false
builder: html
python:
install:
- method: pip
path: .
extra_requirements:
- docs
- requirements: docs/requirements.txt

View File

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

3
MANIFEST.in Normal file
View File

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

View File

@@ -1,87 +0,0 @@
# pyspapi
Фреймворк [API](https://github.com/sp-worlds/api-docs) для серверов СП
## Установка
**Требуется *Python 3.7* или выше**
*Windows*
```commandline
> pip install pyspapi
```
*Linux*
```commandline
$ sudo apt pip3 install pyspapi
```
## Примеры
### [Оплата](https://github.com/sp-worlds/api-docs/blob/main/PAYMENTS.md)
```Python
import spapi
api = spapi.Api(card_id='CARD_ID',
token='TOKEN')
print(api.payment(amount=1,
redirecturl='https://www.google.com/',
webhookurl='https://www.yourwebhook.com/',
data='Какие-то данные'
)
)
```
- `amount` - Стоимость покупки в АРах
- `redirectUrl` - URL страницы, на которую попадет пользователь после оплаты
- `webhookUrl` - URL, куда наш сервер направит запрос, чтобы оповестить ваш сервер об успешной оплате
- `data` - Строка до 100 символов, сюда можно пометить любые полезные данных.
#### Получение данных об успешной оплате
После успешной оплаты на URL указанный в `webhookUrl` придет POST запрос.
*Тело запроса будет в формате JSON:*
- `payer` - Ник игрока, который совершил оплату
- `amount` - Стоимость покупки
- `data` - Данные, которые вы отдали при создании запроса на оплату
### [Переводы](https://github.com/sp-worlds/api-docs/blob/main/TRANSACTIONS.md)
```Python
import spapi
api = spapi.Api(card_id='CARD_ID',
token='TOKEN')
print(api.transaction(receiver='12345',
amount=1,
comment="test"
)
)
```
- `receiver` - Номер карты получателя
- `amount` - Количество АР для перевода
- `comment` - Комментарий к переводу
-
### [Проверка наличия проходки](https://github.com/sp-worlds/api-docs/blob/main/USERS.md)
```Python
import spapi
api = spapi.Api(card_id='CARD_ID',
token='TOKEN')
print(api.check_user(discord_user_id=123456789012345678)
)
```
- `discord_user_id` - ID пользователя в Discord.
*В ответ вы получите JSON:*
- `username` - Ник пользователя или null, если у пользователя нет входа на сервер.
## Ссылки
- [Discord сервер разработчика](https://discord.gg/sJYtYnhN)
- [Документация API сайтов СП](https://github.com/sp-worlds/api-docs)

69
README.rst Normal file
View File

@@ -0,0 +1,69 @@
.. image:: https://raw.githubusercontent.com/deesiigneer/pyspapi/main/assets/repo-banner.png
:alt: pyspapi
.. image:: https://img.shields.io/discord/850091193190973472?color=5865F2&label=discord
:target: https://discord.gg/VbyHaKRAaN
:alt: Discord server invite
.. image:: https://img.shields.io/github/v/release/deesiigneer/pyspapi?include_prereleases&label=github%20release
:target: https://github.com/deesiigneer/pyspapi/
:alt: GitHub release (latest by date including pre-releases)
.. image:: https://img.shields.io/pypi/v/pyspapi.svg
:target: https://pypi.org/project/pyspapi/
:alt: PyPI downloads info
.. image:: https://img.shields.io/pypi/dm/pyspapi?color=informational&label=pypi%20downloads
:target: https://pypi.org/project/pyspapi/
:alt: PyPI version info
.. image:: https://img.shields.io/readthedocs/pyspapi
:target: https://pyspapi.readthedocs.io/
:alt: pyspapi documentation
pyspapi
========
`API <https://github.com/sp-worlds/api-docs>`_ wrapper for SP servers written in Python.
Installation
-------------
**Requires Python 3.8 or higher**
*Windows*
.. code:: sh
pip install pyspapi
*Linux/macOS*
.. code:: sh
pip3 install pyspapi
Quick example
--------------
Checking the balance
~~~~~~~~~~~~~~~~~~~~~
.. code:: py
from pyspapi import SPAPI
from asyncio import get_event_loop
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>`_
Links
------
- `Discord server <https://discord.gg/VbyHaKRAaN>`_
- `pyspapi documentation <https://pyspapi.readthedocs.io/>`_
- `PyPi <https://pypi.org/project/pyspapi/>`_
- `API documentation for SP sites <https://github.com/sp-worlds/api-docs>`_

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
assets/repo-banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

20
docs/Makefile Normal file
View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# 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)

BIN
docs/_static/404.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

19
docs/_static/custom.css vendored Normal file
View File

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

17
docs/_static/switcher.json vendored Normal file
View File

@@ -0,0 +1,17 @@
[
{
"name": "latest",
"version": "latest",
"url": "https://pyspapi.readthedocs.io/ru/latest/"
},
{
"name": "stable",
"version": "stable",
"url": "https://pyspapi.readthedocs.io/ru/stable/"
},
{
"name": "v3-asyncio",
"version": "v3-asyncio",
"url": "https://pyspapi.readthedocs.io/ru/v3-asyncio/"
}
]

30
docs/api.rst Normal file
View File

@@ -0,0 +1,30 @@
.. currentmodule:: pyspapi
API Reference
===============
The following section outlines the API of 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`.
Just like :obj:`py:sys.version_info` the valid values for ``releaselevel`` are
'alpha', 'beta', 'candidate' and 'final'.
.. data:: __version__
A string representation of the version.
``pyspapi``
-----------
``SPAPI``
~~~~~~~~~
.. autoclass:: SPAPI
:members:

84
docs/conf.py Normal file
View File

@@ -0,0 +1,84 @@
from re import search, MULTILINE
import os
import sys
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.
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',
]
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 = 'en'
locale_dirs = ["locale/"]
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",
}
],
"icon_links": [
{
"name": "GitHub",
"url": "https://github.com/deesiigneer/pyspapi",
"icon": "fab fa-brands fa-github",
"type": "fontawesome"
},
{
"name": "Discord",
"url": "https://discord.gg/VbyHaKRAaN",
"icon": "fab fa-brands fa-discord",
"type": "fontawesome"
},
{
"name": "PyPi",
"url": "https://pypi.org/project/pyspapi/",
"icon": "fab fa-brands fa-python",
"type": "fontawesome"
}
],
"header_links_before_dropdown": 4,
"show_toc_level": 1,
"navbar_start": ["navbar-logo"],
"navigation_with_keys": True,
}
html_css_files = ["custom.css"]

BIN
docs/images/logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
docs/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

35
docs/index.rst Normal file
View File

@@ -0,0 +1,35 @@
:theme_html_remove_secondary_sidebar:
Welcome to pyspapi
===================
API wrapper for SP servers written in 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/>`_.
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>`_.
Manuals
-------
These pages go into great detail about everything the API can do.
.. toctree::
:maxdepth: 1
api
quickstart

35
docs/make.bat Normal file
View File

@@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

36
docs/quickstart.rst Normal file
View File

@@ -0,0 +1,36 @@
:orphan:
.. _quickstart:
.. 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
from pyspapi import SPAPI
from asyncio import get_event_loop
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.
You can find more examples in the `examples directory <https://github.com/deesiigneer/pyspapi/tree/main/examples/>`_ on GitHub.

1
docs/requirements.txt Normal file
View File

@@ -0,0 +1 @@
pydata_sphinx_theme

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

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

21
examples/payments.py Normal file
View File

@@ -0,0 +1,21 @@
from pyspapi import SPAPI
from pyspapi.types import Item
from asyncio import get_event_loop
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 spapi.create_payment(items=items,
redirect_url='https://www.google.com/',
webhook_url='https://www.google.com/',
data='some-data'
)
)
loop = get_event_loop()
loop.run_until_complete(main())

16
examples/transaction.py Normal file
View File

@@ -0,0 +1,16 @@
from pyspapi import SPAPI
from asyncio import get_event_loop
spapi = SPAPI(card_id='CARD_ID', token='TOKEN')
async def main():
new_balance = await spapi.create_transaction(receiver='77552',
amount=1,
comment="test"
)
print(new_balance)
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())

8
pyspapi/__init__.py Normal file
View File

@@ -0,0 +1,8 @@
from .spworlds import *
__author__ = 'deesiigneer'
__url__ = 'https://github.com/deesiigneer/pyspapi'
__description__ = 'API wrapper for SP servers written in Python.'
__license__ = 'MIT'
__version__ = "3.1.0"

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

0
pyspapi/exceptions.py Normal file
View File

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('account/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

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

1
requirements.txt Normal file
View File

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

View File

@@ -1,4 +0,0 @@
[egg_info]
tag_build =
tag_date = 0

View File

@@ -1,20 +1,46 @@
import re
from setuptools import setup from setuptools import setup
from os import path requirements = []
this_directory = path.abspath(path.dirname(__file__)) with open("requirements.txt") as f:
with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: requirements = f.read().splitlines()
description = f.read()
requires = ['requests==2.25.1'] 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( setup(
name='pyspapi', name='pyspapi',
version='1.0.0a', license='MIT',
description='Framework for SP API',
long_description=description,
long_description_content_type='text/markdown',
author='deesiigneer', author='deesiigneer',
author_email='xdeesiigneerx@gmail.com', version=version,
packages=['spapi'], url='https://github.com/deesiigneer/pyspapi',
install_requires=requires, 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',
) )

View File

@@ -1,76 +0,0 @@
# pyspapi by deesiigneer
#
import base64
from requests import post, get
class Error(Exception):
pass
class ApiError(Error):
pass
class Api:
def __init__(self, card_id: str, token: str):
self.id = card_id
self.token = token
self.header = {
'Authorization': f"Bearer {str(base64.b64encode(str(f'{self.id}:{self.token}').encode('utf-8')), 'utf-8')}",
}
def _fetch(self, path, data):
if path is None:
result = get(url=f'https://spworlds.ru/api/public/users/{data}',
headers=self.header)
else:
result = post(
url=f'https://spworlds.ru/api/public/{path}',
headers=self.header,
json=data
)
if result.status_code == [200, 400]:
ApiError(f'Ошибка при запросе к API {result.status_code}')
return result.json()
def payment(self, amount, redirecturl, webhookurl, data):
"""
Создание запроса на оплату.
:param amount: Стоимость покупки в АРах
:param redirecturl: URL страницы, на которую попадет пользователь после оплаты
:param webhookurl: URL, куда наш сервер направит запрос, чтобы оповестить ваш сервер об успешной оплате
:param data: Строка до 100 символов, сюда можно поместить любые полезные данных.
:return: url - Ссылка на страницу оплаты, на которую стоит перенаправить пользователя.
"""
return self._fetch('payment', data={'amount': amount,
'redirectUrl': redirecturl,
'webhookUrl': webhookurl,
'data': data})
def transaction(self, receiver, amount, comment):
"""
Перевод АР на карту.
:param receiver : Номер карты получателя
:param amount: Количество АР для перевода
:param comment: Комментарий для перевода
"""
return self._fetch('transactions', data={'receiver': receiver,
'amount': amount,
'comment': comment})
def check_user(self, discord_user_id):
"""
Проверка на наличие проходки
:param discord_user_id: Стоимость покупки в АРах
:return: username - Ник пользователя или null, если у пользователя нет проходки на сервер.
"""
return self._fetch(None, data=discord_user_id)