Merge branch 'v3' into v3

This commit is contained in:
Mad Cat 2023-06-07 14:29:46 +03:00 committed by GitHub
commit ad652e749f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 422 additions and 24 deletions

View file

@ -1,4 +1,4 @@
Copyright 2021 - 2023 Viktor K Copyright 2021 - 2023 Viktor K (Marakarka)
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: 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:

View file

@ -9,9 +9,8 @@ A basic wrapper for the BotiCord API.
__title__ = "boticordpy" __title__ = "boticordpy"
__author__ = "Marakarka" __author__ = "Marakarka"
__license__ = "MIT" __license__ = "MIT"
__copyright__ = "Copyright 2023 Marakarka" __copyright__ = "Copyright 2021 - 2023 Marakarka"
__version__ = "2.2.2" __version__ = "3.0.0a"
from .client import BoticordClient from .client import BoticordClient
from .types import * from .types import *

View file

@ -3,6 +3,7 @@ import typing
from . import types as boticord_types from . import types as boticord_types
from .http import HttpClient from .http import HttpClient
from .autopost import AutoPost from .autopost import AutoPost
from .exceptions import MeilisearchException
class BoticordClient: class BoticordClient:
@ -18,12 +19,13 @@ class BoticordClient:
BotiCord API version (Default: 3) BotiCord API version (Default: 3)
""" """
__slots__ = ("http", "_autopost", "_token") __slots__ = ("http", "_autopost", "_token", "_meilisearch_api_key")
http: HttpClient http: HttpClient
def __init__(self, token: str = None, version: int = 3): def __init__(self, token: str = None, version: int = 3):
self._token = token self._token = token
self._meilisearch_api_key = None
self._autopost: typing.Optional[AutoPost] = None self._autopost: typing.Optional[AutoPost] = None
self.http = HttpClient(token, version) self.http = HttpClient(token, version)
@ -104,6 +106,79 @@ class BoticordClient:
response = await self.http.get_user_info(user_id) response = await self.http.get_user_info(user_id)
return boticord_types.UserProfile.from_dict(response) return boticord_types.UserProfile.from_dict(response)
async def __search_for(self, index, data):
"""Search for smth on BotiCord"""
if self._meilisearch_api_key is None:
token_response = await self.http.get_search_key()
self._meilisearch_api_key = token_response["key"]
try:
response = await self.http.search_for(
index, self._meilisearch_api_key, data
)
except MeilisearchException:
token_response = await self.http.get_search_key()
self._meilisearch_api_key = token_response["key"]
response = await self.http.search_for(
index, self._meilisearch_api_key, data
)
return response["hits"]
async def search_for_bots(
self, **kwargs
) -> typing.List[boticord_types.MeiliIndexedBot]:
"""Search for bots on BotiCord.
Note:
You can find every keyword argument `here <https://www.meilisearch.com/docs/reference/api/search#search-parameters>`_.
Returns:
List[:obj:`~.types.MeiliIndexedBot`]:
List of found bots
"""
response = await self.__search_for("bots", kwargs)
return [boticord_types.MeiliIndexedBot.from_dict(bot) for bot in response]
async def search_for_servers(
self, **kwargs
) -> typing.List[boticord_types.MeiliIndexedServer]:
"""Search for servers on BotiCord.
Note:
You can find every keyword argument `here <https://www.meilisearch.com/docs/reference/api/search#search-parameters>`_.
Returns:
List[:obj:`~.types.MeiliIndexedServer`]:
List of found servers
"""
response = await self.__search_for("servers", kwargs)
return [
boticord_types.MeiliIndexedServer.from_dict(server) for server in response
]
async def search_for_comments(
self, **kwargs
) -> typing.List[boticord_types.MeiliIndexedComment]:
"""Search for comments on BotiCord.
Note:
You can find every keyword argument `here <https://www.meilisearch.com/docs/reference/api/search#search-parameters>`_.
Returns:
List[:obj:`~.types.MeiliIndexedComment`]:
List of found comments
"""
response = await self.__search_for("comments", kwargs)
return [
boticord_types.MeiliIndexedComment.from_dict(comment)
for comment in response
]
def autopost(self) -> AutoPost: def autopost(self) -> AutoPost:
"""Returns a helper instance for auto-posting. """Returns a helper instance for auto-posting.

View file

@ -21,7 +21,7 @@ class InternalException(BoticordException):
class HTTPException(BoticordException): class HTTPException(BoticordException):
"""Exception that's thrown when an HTTP request operation fails. """Exception that's thrown when request to BotiCord API operation fails.
Attributes Attributes
---------- ----------
@ -37,6 +37,23 @@ class HTTPException(BoticordException):
super().__init__(fmt) super().__init__(fmt)
class MeilisearchException(BoticordException):
"""Exception that's thrown when request to Meilisearch API operation fails.
Attributes
----------
response:
The response of the failed HTTP request.
"""
def __init__(self, response):
self.response = response
fmt = f"{self.response['code']} ({self.response['message']})"
super().__init__(fmt)
class StatusCodes(IntEnum): class StatusCodes(IntEnum):
"""Status codes of response""" """Status codes of response"""

View file

@ -1,10 +1,10 @@
from urllib.parse import urlparse
import asyncio import asyncio
import typing import typing
import aiohttp import aiohttp
from . import exceptions from . import exceptions
from .types import LinkDomain
class HttpClient: class HttpClient:
@ -29,13 +29,17 @@ class HttpClient:
self.session = kwargs.get("session") or aiohttp.ClientSession(loop=loop) self.session = kwargs.get("session") or aiohttp.ClientSession(loop=loop)
async def make_request(self, method: str, endpoint: str, **kwargs) -> dict: async def make_request(
self, method: str, endpoint: str, *, meilisearch_token: str = None, **kwargs
) -> dict:
"""Send requests to the API""" """Send requests to the API"""
kwargs["headers"] = {"Content-Type": "application/json"} kwargs["headers"] = {"Content-Type": "application/json"}
if self.token is not None: if self.token is not None:
kwargs["headers"]["Authorization"] = self.token kwargs["headers"]["Authorization"] = self.token
if meilisearch_token is not None:
kwargs["headers"]["Authorization"] = f"Bearer {meilisearch_token}"
url = f"{self.API_URL}{endpoint}" url = f"{self.API_URL}{endpoint}"
@ -43,11 +47,14 @@ class HttpClient:
data = await response.json() data = await response.json()
if (200, 201).__contains__(response.status): if (200, 201).__contains__(response.status):
return data["result"] return data["result"] if not meilisearch_token else data
else: else:
if not meilisearch_token:
raise exceptions.HTTPException( raise exceptions.HTTPException(
{"status": response.status, "error": data["errors"][0]["code"]} {"status": response.status, "error": data["errors"][0]["code"]}
) )
else:
raise exceptions.MeilisearchException(data)
def get_bot_info(self, bot_id: typing.Union[str, int]): def get_bot_info(self, bot_id: typing.Union[str, int]):
"""Get information about the specified bot""" """Get information about the specified bot"""
@ -64,3 +71,16 @@ class HttpClient:
def get_user_info(self, user_id: typing.Union[str, int]): def get_user_info(self, user_id: typing.Union[str, int]):
"""Get information about specified user""" """Get information about specified user"""
return self.make_request("GET", f"users/{user_id}") return self.make_request("GET", f"users/{user_id}")
def get_search_key(self):
"""Get API key for Meilisearch"""
return self.make_request("GET", f"search-key")
def search_for(self, index: str, api_key: str, data: dict):
"""Search for something on BotiCord."""
return self.make_request(
"POST",
f"search/indexes/{index}/search",
meilisearch_token=api_key,
json=data,
)

View file

@ -548,7 +548,7 @@ class PartialUser(APIObjectBase):
self: cls = super().__new__(cls) self: cls = super().__new__(cls)
self.username = data["username"] self.username = data["username"]
self.discriminator = data["discriminator"] self.discriminator = data.get("discriminator")
self.avatar = data.get("avatar") self.avatar = data.get("avatar")
self.id = data["id"] self.id = data["id"]
self.socials = UserLinks.from_dict(data.get("socials", {})) self.socials = UserLinks.from_dict(data.get("socials", {}))
@ -562,7 +562,12 @@ class PartialUser(APIObjectBase):
@dataclass(repr=False) @dataclass(repr=False)
class ResourceServer(APIObjectBase): class ResourceServer(APIObjectBase):
"""Information about server from BotiCord.""" """Information about server from BotiCord.
.. warning::
The result of the reverse conversion (`.to_dict()`) may not match the actual data.
"""
id: str id: str
"""Server's ID""" """Server's ID"""
@ -703,6 +708,9 @@ class ResourceBot(APIObjectBase):
short_link: Optional[str] short_link: Optional[str]
"""Short link to the bot's page""" """Short link to the bot's page"""
standart_banner_id: int
"""Server's standart banner ID"""
invite_link: str invite_link: str
"""Invite link""" """Invite link"""
@ -791,6 +799,7 @@ class ResourceBot(APIObjectBase):
self.support_server_invite_link = data.get("support_server_invite") self.support_server_invite_link = data.get("support_server_invite")
self.website = data.get("website") self.website = data.get("website")
self.up_count = data.get("upCount") self.up_count = data.get("upCount")
self.standart_banner_id = data.get("standartBannerID")
self.premium_active = data["premium"].get("active") self.premium_active = data["premium"].get("active")
self.premium_splash_url = data["premium"].get("splashURL") self.premium_splash_url = data["premium"].get("splashURL")
@ -853,5 +862,231 @@ class UserProfile(PartialUser):
return self return self
class LinkDomain: @dataclass(repr=False)
pass class MeiliIndexedBot(APIObjectBase):
"""Bot found on BotiCord
.. warning::
The result of the reverse conversion (`.to_dict()`) may not match the actual data.
"""
id: str
"""ID of the bot"""
name: str
"""Name of the bot"""
short_description: str
"""Short description of the bot"""
description: str
"""Description of the bot"""
avatar: Optional[str]
"""Avatar of the bot"""
invite: str
"""Invite link"""
premium_active: bool
"""Is premium status active? (True/False)"""
premium_banner: Optional[str]
"""Premium banner URL"""
banner: int
"""Standart banner"""
rating: int
"""Bot's rating"""
discriminator: str
"""Bot's discriminator"""
library: Optional[BotLibrary]
"""The library that the bot is based on"""
guilds: Optional[int]
"""Number of guilds"""
shards: Optional[int]
"""Number of shards"""
members: Optional[int]
"""Number of members"""
tags: List[BotTag]
"""List of bot tags"""
ups: int
"""List of bot's ups"""
@classmethod
def from_dict(cls, data: dict):
"""Generate a MeiliIndexedBot from the given data.
Parameters
----------
data: :class:`dict`
The dictionary to convert into a MeiliIndexedBot.
"""
self: MeiliIndexedBot = super().__new__(cls)
self.id = data.get("id")
self.name = data.get("name")
self.short_description = data.get("shortDescription")
self.description = data.get("description")
self.avatar = data.get("avatar")
self.invite = data.get("invite")
self.discriminator = data.get("discriminator")
self.ups = data.get("ups")
self.rating = data.get("rating")
self.banner = data.get("banner")
self.premium_active = data.get("premiumActive")
self.premium_banner = data.get("premiumBanner")
self.library = (
BotLibrary(data["library"]) if data.get("library") is not None else None
)
self.tags = [BotTag(tag) for tag in data.get("tags", [])]
self.guilds = data.get("guilds")
self.shards = data.get("shards")
self.members = data.get("members")
return self
@dataclass(repr=False)
class MeiliIndexedServer(APIObjectBase):
"""Server found on BotiCord
.. warning::
The result of the reverse conversion (`.to_dict()`) may not match the actual data.
"""
id: str
"""ID of the server"""
name: str
"""Name of the server"""
short_description: str
"""Short description of the server"""
description: str
"""Description of the server"""
avatar: Optional[str]
"""Avatar of the server"""
invite: str
"""Invite link"""
premium_active: bool
"""Is premium status active? (True/False)"""
premium_banner: Optional[str]
"""Premium banner URL"""
banner: int
"""Standart banner"""
discord_banner: Optional[str]
"""Discord banner URL"""
rating: int
"""Server's rating"""
members: Optional[int]
"""Number of members"""
tags: List[ServerTag]
"""List of server tags"""
ups: int
"""List of server's ups"""
@classmethod
def from_dict(cls, data: dict):
"""Generate a MeiliIndexedServer from the given data.
Parameters
----------
data: :class:`dict`
The dictionary to convert into a MeiliIndexedServer.
"""
self: MeiliIndexedServer = super().__new__(cls)
self.id = data.get("id")
self.name = data.get("name")
self.short_description = data.get("shortDescription")
self.description = data.get("description")
self.avatar = data.get("avatar")
self.invite = data.get("invite")
self.ups = data.get("ups")
self.rating = data.get("rating")
self.banner = data.get("banner")
self.premium_active = data.get("premiumActive")
self.premium_banner = data.get("premiumBanner")
self.discord_banner = data.get("discordBanner")
self.tags = [ServerTag(tag) for tag in data.get("tags", [])]
self.members = data.get("members")
return self
@dataclass(repr=False)
class MeiliIndexedComment(APIObjectBase):
"""Comment found on BotiCord"""
id: str
"""ID of the comment"""
author: str
"""Id of the author of the comment"""
rating: int
"""Comment's rating"""
content: str
"""Content of the comment"""
resource: str
"""Id of the resource"""
created: datetime
"""When the comment was created"""
mod_reply: Optional[str]
"""Reply to the comment"""
@classmethod
def from_dict(cls, data: dict):
"""Generate a MeiliIndexedComment from the given data.
Parameters
----------
data: :class:`dict`
The dictionary to convert into a MeiliIndexedComment.
"""
self: MeiliIndexedComment = super().__new__(cls)
self.id = data.get("id")
self.rating = data.get("rating")
self.author = data.get("author")
self.content = data.get("content")
self.resource = data.get("resource")
self.mod_reply = data.get("modReply")
self.created = datetime.utcfromtimestamp(data.get("created") / 1000)
return self

View file

@ -13,6 +13,9 @@ Exceptions API Reference
.. autoclass:: HTTPException .. autoclass:: HTTPException
:members: :members:
.. autoclass:: MeilisearchException
:members:
.. autoclass:: StatusCodes .. autoclass:: StatusCodes
:members: :members:

View file

@ -56,3 +56,16 @@ Users
:members: :members:
:exclude-members: to_dict :exclude-members: to_dict
:inherited-members: :inherited-members:
MeiliSearch
------------
.. autoclass:: MeiliIndexedBot
:members:
.. autoclass:: MeiliIndexedServer
:members:
.. autoclass:: MeiliIndexedComment
:members:

View file

@ -15,15 +15,17 @@ async def get_stats():
# Function that will be called if stats are posted successfully. # Function that will be called if stats are posted successfully.
async def on_success_posting(): async def on_success_posting():
print("stats posting successfully") print("wow stats posting works")
boticord_client = BoticordClient("Bot your_api_token", version=2) boticord_client = BoticordClient(
"your_boticord_api_token", version=3
) # <--- BotiCord API token
autopost = ( autopost = (
boticord_client.autopost() boticord_client.autopost()
.init_stats(get_stats) .init_stats(get_stats)
.on_success(on_success_posting) .on_success(on_success_posting)
.start() .start("id_of_your_bot") # <--- ID of your bot
) )
bot.run("bot token") bot.run("bot token") # <--- Discord bot's token

View file

@ -44,10 +44,11 @@ setup(
}, },
packages=find_packages(), packages=find_packages(),
version=version, version=version,
python_requires=">= 3.6", python_requires=">= 3.8",
description="A Python wrapper for BotiCord API", description="A Python wrapper for BotiCord API",
long_description=README, long_description=README,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
include_package_data=True,
url="https://github.com/boticord/boticordpy", url="https://github.com/boticord/boticordpy",
author="Marakarka", author="Marakarka",
author_email="support@kerdoku.top", author_email="support@kerdoku.top",
@ -55,7 +56,9 @@ setup(
classifiers=[ classifiers=[
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
], ],
install_requires=["aiohttp"], install_requires=["aiohttp", "typing_extensions"],
) )

View file

@ -10,6 +10,20 @@ resource_bot_dict = {
"createdDate": "2023-05-22T22:29:23.264Z", "createdDate": "2023-05-22T22:29:23.264Z",
"premium": {}, "premium": {},
} }
resource_server_dict = {
"id": "722424773233213460",
"name": "BotiCord.top",
"tags": [134, 132],
"status": 1,
"createdDate": "2023-05-23T15:16:45.387Z",
"premium": {},
}
user_profile_dict = {
"id": "585766846268047370",
"username": "Marakarka",
"bots": [resource_bot_dict],
"shortDescription": None,
}
def test_resource_up_convertation(): def test_resource_up_convertation():
@ -41,3 +55,20 @@ def test_resource_bot_convertation():
assert int(model_from_dict.created_date.timestamp()) == 1684794563 assert int(model_from_dict.created_date.timestamp()) == 1684794563
assert model_from_dict.status.name == "PUBLIC" assert model_from_dict.status.name == "PUBLIC"
def test_resource_server_convertation():
model_from_dict = types.ResourceServer.from_dict(resource_server_dict)
assert int(model_from_dict.created_date.timestamp()) == 1684855005
assert model_from_dict.name == "BotiCord.top"
assert model_from_dict.tags[1].name == "GAMES"
def test_user_profile_convertation():
model_from_dict = types.UserProfile.from_dict(user_profile_dict)
assert model_from_dict.id == "585766846268047370"
assert model_from_dict.username == "Marakarka"
assert model_from_dict.short_description == None
assert model_from_dict.bots[0].id == "947141336451153931"