commit bf8eba17af366ad5cfdd60608574fb91e1d1fdd7 Author: grey-cat-1908 Date: Tue Aug 24 21:20:10 2021 +0300 fix setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0793985 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.idea +venv +.git +.DS_Store +__pycache__ +.egg-info +dist +docs/_build diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..7ab55d1 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,7 @@ +Copyright 2021 Victor Kotlin + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..259b1c6 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +

Boticordpy

+ +

Модуль для работы с Boticord API

+ +

+ + +

+ +--- +* [Документация](https://boticordpy.readthedocs.io/) +* [Исходный код](https://github.com/grey-cat-1908/boticordpy) +--- + +### Примеры + +Публикуем статистику нашего бота в Boticord. + +```Python +from discord.ext import commands + +from boticordpy import BoticordClient + +bot = commands.Bot(command_prefix="!") +boticord = BoticordClient(bot, "your-boticord-token") + + +@bot.event +async def on_connect(): + stats = {"servers": len(bot.guilds), "shards": bot.shard_count, "users": len(bot.users)} + await boticord.Bots.postStats(stats) + + +bot.run("your-bot-token") +``` diff --git a/boticordpy.egg-info/PKG-INFO b/boticordpy.egg-info/PKG-INFO new file mode 100644 index 0000000..39d263f --- /dev/null +++ b/boticordpy.egg-info/PKG-INFO @@ -0,0 +1,49 @@ +Metadata-Version: 2.1 +Name: boticordpy +Version: 1.3.5 +Summary: Simple Python Module for boticord api +Home-page: https://github.com/grey-cat-1908/boticordpy +Author: KerdokuCat +Author-email: support@kerdoku.top +License: MIT +Description:

Boticordpy

+ +

Модуль для работы с Boticord API

+ +

+ + +

+ + --- + * [Документация](https://boticordpy.readthedocs.io/) + * [Исходный код](https://github.com/grey-cat-1908/boticordpy) + --- + + ### Примеры + + Публикуем статистику нашего бота в Boticord. + + ```Python + from discord.ext import commands + + from boticordpy import BoticordClient + + bot = commands.Bot(command_prefix="!") + boticord = BoticordClient(bot, "your-boticord-token") + + + @bot.event + async def on_connect(): + stats = {"servers": len(bot.guilds), "shards": bot.shard_count, "users": len(bot.users)} + await boticord.Bots.postStats(stats) + + + bot.run("your-bot-token") + ``` + +Platform: UNKNOWN +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.7 +Description-Content-Type: text/markdown diff --git a/boticordpy.egg-info/SOURCES.txt b/boticordpy.egg-info/SOURCES.txt new file mode 100644 index 0000000..e1e4006 --- /dev/null +++ b/boticordpy.egg-info/SOURCES.txt @@ -0,0 +1,12 @@ +LICENSE.txt +README.md +setup.py +boticordpy/__init__.py +boticordpy/client.py +boticordpy/config.py +boticordpy/exceptions.py +boticordpy.egg-info/PKG-INFO +boticordpy.egg-info/SOURCES.txt +boticordpy.egg-info/dependency_links.txt +boticordpy.egg-info/requires.txt +boticordpy.egg-info/top_level.txt \ No newline at end of file diff --git a/boticordpy.egg-info/dependency_links.txt b/boticordpy.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/boticordpy.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/boticordpy.egg-info/requires.txt b/boticordpy.egg-info/requires.txt new file mode 100644 index 0000000..b9465ab --- /dev/null +++ b/boticordpy.egg-info/requires.txt @@ -0,0 +1,2 @@ +discord.py +aiohttp diff --git a/boticordpy.egg-info/top_level.txt b/boticordpy.egg-info/top_level.txt new file mode 100644 index 0000000..6140ae2 --- /dev/null +++ b/boticordpy.egg-info/top_level.txt @@ -0,0 +1 @@ +boticordpy diff --git a/boticordpy/__init__.py b/boticordpy/__init__.py new file mode 100644 index 0000000..8da7c7a --- /dev/null +++ b/boticordpy/__init__.py @@ -0,0 +1 @@ +from .client import BoticordClient diff --git a/boticordpy/client.py b/boticordpy/client.py new file mode 100644 index 0000000..0dee32b --- /dev/null +++ b/boticordpy/client.py @@ -0,0 +1,44 @@ +from discord.ext import commands +from typing import Union +import asyncio +import aiohttp + +from .modules import Bots, Servers, Users + + +class BoticordClient: + + """ + This class is used to make it much easier to use the Boticord API. + + Parameters + ---------- + bot : :class:`commands.Bot` | :class:`commands.AutoShardedBot` + The discord.py Bot instance + token : :class:`str` + boticord api key + + Attributes + ---------- + Bots : :class:`modules.bots.Bots` + :class:`modules.bots.Bots` with all arguments filled. + Servers : :class:`modules.servers.Servers` + :class:`modules.servers.Servers` with all arguments filled. + Users : :class:`modules.users.Users` + :class:`modules.users.Users` with all arguments filled. + """ + + __slots__ = ( + "Bots", + "Servers", + "Users" + ) + + bot: Union[commands.Bot, commands.AutoShardedBot] + + def __init__(self, bot, token=None, **kwargs): + loop = kwargs.get('loop') or asyncio.get_event_loop() + session = kwargs.get('session') or aiohttp.ClientSession(loop=loop) + self.Bots = Bots(bot, token=token, loop=loop, session=session) + self.Servers = Servers(bot, token=token, loop=loop, session=session) + self.Users = Users(token=token, loop=loop, session=session) diff --git a/boticordpy/config.py b/boticordpy/config.py new file mode 100644 index 0000000..244e85c --- /dev/null +++ b/boticordpy/config.py @@ -0,0 +1,12 @@ +from . import exceptions + + +class Config: + local_api = "https://boticord.top/api" + general_api = "https://api.boticord.top/v1" + http_exceptions = {401: exceptions.Unauthorized, + 403: exceptions.Forbidden, + 404: exceptions.NotFound, + 429: exceptions.ToManyRequests, + 500: exceptions.ServerError, + 503: exceptions.ServerError} diff --git a/boticordpy/exceptions.py b/boticordpy/exceptions.py new file mode 100644 index 0000000..4b7bdc4 --- /dev/null +++ b/boticordpy/exceptions.py @@ -0,0 +1,50 @@ +class BoticordException(Exception): + """Base exception class for boticordpy. + This could be caught to handle any exceptions thrown from this library. + """ + + +class HTTPException(BoticordException): + """Exception that's thrown when an HTTP request operation fails. + + Attributes + ---------- + response: + The response of the failed HTTP request. + message: + The text of the error. Could be an empty string. + """ + + def __init__(self, response, message): + self.response = response + if isinstance(message, dict): + self.text = message.get('message', '') + self.code = message.get('code', 0) + else: + self.text = message + + fmt = f"{self.response.reason} (Status code: {self.response.status})" + if self.text: + fmt = f"{fmt}: {self.text}" + + super().__init__(fmt) + + +class Unauthorized(HTTPException): + """Exception that's thrown when status code 401 occurs.""" + + +class Forbidden(HTTPException): + """Exception that's thrown when status code 403 occurs.""" + + +class NotFound(HTTPException): + """Exception that's thrown when status code 404 occurs.""" + + +class ToManyRequests(HTTPException): + """Exception that's thrown when status code 429 occurs.""" + + +class ServerError(HTTPException): + """Exception that's thrown when status code 500 or 503 occurs.""" diff --git a/boticordpy/modules/__init__.py b/boticordpy/modules/__init__.py new file mode 100644 index 0000000..99e6f20 --- /dev/null +++ b/boticordpy/modules/__init__.py @@ -0,0 +1,3 @@ +from .bots import Bots +from .servers import Servers +from .users import Users \ No newline at end of file diff --git a/boticordpy/modules/bots.py b/boticordpy/modules/bots.py new file mode 100644 index 0000000..21d4149 --- /dev/null +++ b/boticordpy/modules/bots.py @@ -0,0 +1,86 @@ +import json + +from aiohttp import ClientResponse +from typing import Union +import aiohttp +import asyncio + +from ..config import Config + + +async def _json_or_text(response: ClientResponse) -> Union[dict, str]: + text = await response.text() + if response.headers['Content-Type'] == 'application/json; charset=utf-8': + return json.loads(text) + return text + + +class Bots: + + """ + Class with methods to work with Boticord API Bots. + + Parameters + ---------- + bot : :class:`commands.Bot` | :class:`commands.AutoShardedBot` + The discord.py Bot instance + """ + + def __init__(self, bot, **kwargs): + self.bot = bot + self.token = kwargs.get('token') + self.loop = kwargs.get('loop') or asyncio.get_event_loop() + self.session = kwargs.get('session') or aiohttp.ClientSession(loop=self.loop) + + async def getBotInfo(self, botID: int): + """ + Returns information about discord bot with the given ID. + + Parameters + ---------- + botID : :class:`int` + Discord Bot's ID + """ + headers = {} + async with self.session.get(f'{Config.general_api}/bot/{botID}', headers=headers) as resp: + data = await _json_or_text(resp) + status = Config.http_exceptions.get(resp.status) + if status is not None: + raise status + return data + + async def getBotComments(self, botID: int): + """ + Returns comments of the discord bot with the given ID. + + Parameters + ---------- + botID : :class:`int` + Discord Bot's ID + """ + headers = {} + async with self.session.get(f'{Config.general_api}/bot/{botID}/comments', headers=headers) as resp: + data = await _json_or_text(resp) + status = Config.http_exceptions.get(resp.status) + if status is not None: + raise status + return data + + async def postStats(self, stats: dict): + """ + Post stats to Boticord API. + + Parameters + ---------- + stats: :class:`dict` + A dictionary of {``guilds``: :class:`int`, ``shards``: :class:`int`, ``users``: :class:`int`} + """ + if not self.token: + return "Require Authentication" + headers = {"Authorization": self.token} + async with self.session.post(f'{Config.local_api}/stats', headers=headers, json=stats) as resp: + data = await _json_or_text(resp) + status = Config.http_exceptions.get(resp.status) + if status is not None: + raise status + return data diff --git a/boticordpy/modules/servers.py b/boticordpy/modules/servers.py new file mode 100644 index 0000000..1c07098 --- /dev/null +++ b/boticordpy/modules/servers.py @@ -0,0 +1,108 @@ +import json + +from aiohttp import ClientResponse +from typing import Union +import aiohttp +import asyncio +import discord + +from ..config import Config + + +async def _json_or_text(response: ClientResponse) -> Union[dict, str]: + text = await response.text() + if response.headers['Content-Type'] == 'application/json; charset=utf-8': + return json.loads(text) + return text + + +class Servers: + + """ + Class with methods to work with Boticord API Servers. + + Parameters + ---------- + bot : :class:`commands.Bot` | :class:`commands.AutoShardedBot` + The discord.py Bot instance + """ + + def __init__(self, bot, **kwargs): + self.bot = bot + self.token = kwargs.get('token') + self.loop = kwargs.get('loop') or asyncio.get_event_loop() + self.session = kwargs.get('session') or aiohttp.ClientSession(loop=self.loop) + + async def getServerInfo(self, serverID: int): + """ + Returns information about discord server with the given ID. + + Parameters + ---------- + serverID : :class:`int` + Discord Server's ID + """ + headers = {} + async with self.session.get(f'{Config.general_api}/server/{serverID}', headers=headers) as resp: + data = await _json_or_text(resp) + status = Config.http_exceptions.get(resp.status) + if status is not None: + raise status + return data + + async def getServerComments(self, serverID: int): + """ + Returns comments of the discord server with the given ID. + + Parameters + ---------- + serverID : :class:`int` + Discord Server's ID + """ + headers = {} + async with self.session.get(f'{Config.general_api}/server/{serverID}/comments', headers=headers) as resp: + data = await _json_or_text(resp) + status = Config.http_exceptions.get(resp.status) + if status is not None: + raise status + return data + + async def postServerStats(self, message: discord.Message, custom_stats: dict = None): + """ + Post server stats to Boticord API. + + Parameters + ---------- + message: :class:`discord.Message` + Message object of used command. + custom_stats: :class:`dict` + Dict with custom server stats. (Optional) + """ + if not self.token: + return "Require Authentication" + + if custom_stats is None: + guild = message.guild + guild_owner = guild.owner + + stats = { + "serverID": str(guild.id), + "up": 1, + "status": 1, + "serverName": guild.name, + "serverAvatar": str(guild.icon_url), + "serverMembersAllCount": guild.member_count, + "serverMembersOnlineCount": 0, + "serverOwnerTag": guild_owner.name + "#" + guild_owner.discriminator, + "serverOwnerID": str(guild_owner.id) + } + else: + stats = custom_stats + + headers = {"Authorization": self.token} + async with self.session.post(f'{Config.general_api}/server', headers=headers, json=stats) as resp: + data = await _json_or_text(resp) + status = Config.http_exceptions.get(resp.status) + if status is not None: + raise status + return data diff --git a/boticordpy/modules/users.py b/boticordpy/modules/users.py new file mode 100644 index 0000000..0eb3a60 --- /dev/null +++ b/boticordpy/modules/users.py @@ -0,0 +1,78 @@ +import json + +from aiohttp import ClientResponse +from typing import Union +import aiohttp +import asyncio + +from ..config import Config + + +async def _json_or_text(response: ClientResponse) -> Union[dict, str]: + text = await response.text() + if response.headers['Content-Type'] == 'application/json; charset=utf-8': + return json.loads(text) + return text + + +class Users: + + """ + Class with methods to work with Boticord API Users. + """ + + def __init__(self, **kwargs): + self.token = kwargs.get('token') + self.loop = kwargs.get('loop') or asyncio.get_event_loop() + self.session = kwargs.get('session') or aiohttp.ClientSession(loop=self.loop) + + async def getUserInfo(self, userID: int): + """ + Returns information about discord user with the given ID. + + Parameters + ---------- + userID : :class:`int` + Discord User's ID + """ + headers = {} + async with self.session.get(f'{Config.general_api}/profile/{userID}', headers=headers) as resp: + data = await _json_or_text(resp) + status = Config.http_exceptions.get(resp.status) + if status is not None: + raise status + return data + + async def getUserComments(self, userID: int): + """ + Returns comments of discord user with the given ID. + + Parameters + ---------- + userID : :class:`int` + Discord User's ID + """ + headers = {} + async with self.session.get(f'{Config.general_api}/profile/{userID}/comments', headers=headers) as resp: + data = await _json_or_text(resp) + status = Config.http_exceptions.get(resp.status) + if status is not None: + raise status + return data + + async def getUserBots(self, userID: int): + """ + Returns bots of discord user with the given ID. + + Parameters + ---------- + userID : :class:`int` + Discord User's ID + """ + headers = {} + async with self.session.get(f'{Config.general_api}/bots/{userID}', headers=headers) as resp: + data = await _json_or_text(resp) + status = Config.http_exceptions.get(resp.status) + if status is not None: + raise status + return data diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -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 = . +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) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..01f6523 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,59 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + + +# -- Project information ----------------------------------------------------- + +project = 'BoticordPY' +copyright = '2021, Grey Cat' +author = 'Grey Cat' + +# The full version, including alpha/beta/rc tags +release = '1.3.2' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.napoleon', + 'sphinx.ext.ifconfig', + 'sphinx_rtd_theme' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" +html_show_sourcelink = False + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] diff --git a/docs/exceptions.rst b/docs/exceptions.rst new file mode 100644 index 0000000..4fe3279 --- /dev/null +++ b/docs/exceptions.rst @@ -0,0 +1,12 @@ +.. currentmodule:: boticordpy + +.. exceptions: + +Exceptions +============== + +This page describes all the exceptions of boticordpy module. + +.. automodule:: boticordpy.exceptions + :members: + diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..afbb042 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,29 @@ +.. BoticordPy documentation master file, created by + sphinx-quickstart on Sat Aug 7 21:14:56 2021. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to BoticordPy's documentation! +====================================== + +This is a documentation for simple python module to work with the boticord api. + +====================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + quickstart + main + modules + exceptions + +Links +===== + +* :ref:`search` +* `PyPi `_ +* `GitHub `_ +* `Boticord `_ + diff --git a/docs/main.rst b/docs/main.rst new file mode 100644 index 0000000..eed81c8 --- /dev/null +++ b/docs/main.rst @@ -0,0 +1,13 @@ +.. currentmodule:: boticordpy + +.. main: + +Main +=================== + +This class is used to make it much easier to use the Boticord API. + +BoticordClient +--------------- +.. autoclass:: boticordpy.BoticordClient + :members: \ No newline at end of file diff --git a/docs/modules.rst b/docs/modules.rst new file mode 100644 index 0000000..6fc064e --- /dev/null +++ b/docs/modules.rst @@ -0,0 +1,23 @@ +.. currentmodule:: boticordpy + +.. modules: + +Modules +============== + +This page describes all the modules of boticordpy. + +Bots +------------ +.. autoclass:: boticordpy.modules.Bots + :members: + +Servers +------------ +.. autoclass:: boticordpy.modules.Servers + :members: + +Users +------------ +.. autoclass:: boticordpy.modules.Users + :members: diff --git a/docs/quickstart.rst b/docs/quickstart.rst new file mode 100644 index 0000000..5bde73e --- /dev/null +++ b/docs/quickstart.rst @@ -0,0 +1,50 @@ +.. currentmodule:: boticordpy + +.. quickstart: + +Quickstart +========== + +Installation +------------ + +Enter one of these commands to install the library: + +:: + + pip install boticordpy + + +:: + + python3 -m pip install boticordpy + + +Or just clone the repo: https://github.com/grey-cat-1908/boticordpy + + + +Post Bot Stats +------------------------- + +Let's post our bot's stats to Boticord. + +:: + + from discord.ext import commands + + from boticordpy import BoticordClient + + bot = commands.Bot(command_prefix="!") + boticord = BoticordClient(bot, "your-boticord-token") + + + @bot.event + async def on_connect(): + stats = {"servers": len(bot.guilds), "shards": bot.shard_count, "users": len(bot.users)} + await boticord.Bots.postStats(stats) + + + bot.run("your-bot-token") + +.. \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..a8a7a9f --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +boticordpy \ No newline at end of file diff --git a/examples/postdata.py b/examples/postdata.py new file mode 100644 index 0000000..aeab8d3 --- /dev/null +++ b/examples/postdata.py @@ -0,0 +1,15 @@ +from discord.ext import commands + +from boticordpy import BoticordClient + +bot = commands.Bot(command_prefix="!") +boticord = BoticordClient(bot, "your-boticord-token") + + +@bot.event +async def on_connect(): + stats = {"servers": len(bot.guilds), "shards": bot.shard_count, "users": len(bot.users)} + await boticord.Bots.postStats(stats) + + +bot.run("your-bot-token") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b9465ab --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +discord.py +aiohttp diff --git a/setuo.cfg b/setuo.cfg new file mode 100644 index 0000000..224a779 --- /dev/null +++ b/setuo.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..dbc7fb9 --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +import pathlib + +from setuptools import setup + +# The directory containing this file +HERE = pathlib.Path(__file__).parent + +# The text of the README file +README = (HERE / "README.md").read_text(encoding="utf8") + +# This call to setup() does all the work +setup( + name="boticordpy", + packages = ['boticordpy'], + version="1.3.5", + description="Simple Python Module for boticord api", + long_description=README, + long_description_content_type="text/markdown", + url="https://github.com/grey-cat-1908/boticordpy", + author="KerdokuCat", + author_email="support@kerdoku.top", + license="MIT", + classifiers=[ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + ], + install_requires=["discord.py", "aiohttp"], +)