From f1012e94651f734fb35549277c2bcc7456b08e9b Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Mon, 5 Jun 2023 12:51:48 +0300 Subject: [PATCH 1/6] test: ResourceServer convertation --- tests/test_convertation.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_convertation.py b/tests/test_convertation.py index 81d39df..a9d38b2 100644 --- a/tests/test_convertation.py +++ b/tests/test_convertation.py @@ -10,6 +10,17 @@ resource_bot_dict = { "createdDate": "2023-05-22T22:29:23.264Z", "premium": {}, } +resource_server_dict = { + "id": "722424773233213460", + "name": "BotiCord.top", + "tags": [ + 134, + 132 + ], + "status": 1, + "createdDate": "2023-05-23T15:16:45.387Z", + "premium": {}, +} def test_resource_up_convertation(): @@ -41,3 +52,10 @@ def test_resource_bot_convertation(): assert int(model_from_dict.created_date.timestamp()) == 1684794563 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" From d546175131f627b8b4ca2ced6526dddbf13e8014 Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Mon, 5 Jun 2023 13:00:28 +0300 Subject: [PATCH 2/6] test: UserProfile convertation --- boticordpy/client.py | 2 +- boticordpy/types.py | 2 +- tests/test_convertation.py | 21 +++++++++++++++++---- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/boticordpy/client.py b/boticordpy/client.py index c621cb9..6816833 100644 --- a/boticordpy/client.py +++ b/boticordpy/client.py @@ -87,7 +87,7 @@ class BoticordClient: """ response = await self.http.get_server_info(server_id) return boticord_types.ResourceServer.from_dict(response) - + async def get_user_info( self, user_id: typing.Union[str, int] ) -> boticord_types.UserProfile: diff --git a/boticordpy/types.py b/boticordpy/types.py index 918f088..917317d 100644 --- a/boticordpy/types.py +++ b/boticordpy/types.py @@ -545,7 +545,7 @@ class PartialUser(APIObjectBase): self: cls = super().__new__(cls) self.username = data["username"] - self.discriminator = data["discriminator"] + self.discriminator = data.get("discriminator") self.avatar = data.get("avatar") self.id = data["id"] self.socials = UserLinks.from_dict(data.get("socials", {})) diff --git a/tests/test_convertation.py b/tests/test_convertation.py index a9d38b2..2a5e55a 100644 --- a/tests/test_convertation.py +++ b/tests/test_convertation.py @@ -13,14 +13,17 @@ resource_bot_dict = { resource_server_dict = { "id": "722424773233213460", "name": "BotiCord.top", - "tags": [ - 134, - 132 - ], + "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(): @@ -53,9 +56,19 @@ def test_resource_bot_convertation(): assert int(model_from_dict.created_date.timestamp()) == 1684794563 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" From bde481924ef30a34bf6b2ffa19a0c8bd11712c4e Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Mon, 5 Jun 2023 13:06:50 +0300 Subject: [PATCH 3/6] updated setup.py --- LICENSE.txt | 2 +- setup.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index df4f53d..0463ab7 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -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: diff --git a/setup.py b/setup.py index b0b7a5b..9da7f02 100644 --- a/setup.py +++ b/setup.py @@ -44,10 +44,11 @@ setup( }, packages=find_packages(), version=version, - python_requires=">= 3.6", + python_requires=">= 3.8", description="A Python wrapper for BotiCord API", long_description=README, long_description_content_type="text/markdown", + include_package_data=True, url="https://github.com/boticord/boticordpy", author="Marakarka", author_email="support@kerdoku.top", @@ -55,7 +56,9 @@ setup( classifiers=[ "License :: OSI Approved :: MIT License", "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"], ) From e26366da74f8f87c3e75a254e3a7e086e15be288 Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Mon, 5 Jun 2023 13:11:25 +0300 Subject: [PATCH 4/6] updated examples/autopost.py --- examples/autopost.py | 10 ++++++---- setup.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/autopost.py b/examples/autopost.py index ac370f2..6d41e6f 100644 --- a/examples/autopost.py +++ b/examples/autopost.py @@ -15,15 +15,17 @@ async def get_stats(): # Function that will be called if stats are posted successfully. 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 = ( boticord_client.autopost() .init_stats(get_stats) .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 diff --git a/setup.py b/setup.py index 9da7f02..63e80aa 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ setup( description="A Python wrapper for BotiCord API", long_description=README, long_description_content_type="text/markdown", - include_package_data=True, + include_package_data=True, url="https://github.com/boticord/boticordpy", author="Marakarka", author_email="support@kerdoku.top", From daa2e317328c6044c47e3e35c78074087d01304e Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Wed, 7 Jun 2023 12:31:52 +0300 Subject: [PATCH 5/6] search --- boticordpy/__init__.py | 7 +- boticordpy/client.py | 77 +++++++++++- boticordpy/exceptions.py | 19 ++- boticordpy/http.py | 32 ++++- boticordpy/types.py | 241 +++++++++++++++++++++++++++++++++++++- docs/source/api/types.rst | 13 ++ 6 files changed, 374 insertions(+), 15 deletions(-) diff --git a/boticordpy/__init__.py b/boticordpy/__init__.py index 9e868ea..037ce87 100644 --- a/boticordpy/__init__.py +++ b/boticordpy/__init__.py @@ -2,16 +2,15 @@ Boticord API Wrapper ~~~~~~~~~~~~~~~~~~~ A basic wrapper for the BotiCord API. -:copyright: (c) 2022 Marakarka +:copyright: (c) 2023 Marakarka :license: MIT, see LICENSE for more details. """ __title__ = "boticordpy" __author__ = "Marakarka" __license__ = "MIT" -__copyright__ = "Copyright 2022 Marakarka" -__version__ = "2.2.2" +__copyright__ = "Copyright 2021 - 2023 Marakarka" +__version__ = "3.0.0a" from .client import BoticordClient - from .types import * diff --git a/boticordpy/client.py b/boticordpy/client.py index 6816833..4220d10 100644 --- a/boticordpy/client.py +++ b/boticordpy/client.py @@ -3,6 +3,7 @@ import typing from . import types as boticord_types from .http import HttpClient from .autopost import AutoPost +from .exceptions import MeilisearchException class BoticordClient: @@ -18,12 +19,13 @@ class BoticordClient: BotiCord API version (Default: 3) """ - __slots__ = ("http", "_autopost", "_token") + __slots__ = ("http", "_autopost", "_token", "_meilisearch_api_key") http: HttpClient def __init__(self, token: str = None, version: int = 3): self._token = token + self._meilisearch_api_key = None self._autopost: typing.Optional[AutoPost] = None self.http = HttpClient(token, version) @@ -104,6 +106,79 @@ class BoticordClient: response = await self.http.get_user_info(user_id) 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 `_. + + 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 `_. + + 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 `_. + + 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: """Returns a helper instance for auto-posting. diff --git a/boticordpy/exceptions.py b/boticordpy/exceptions.py index 0f43862..e1401c5 100644 --- a/boticordpy/exceptions.py +++ b/boticordpy/exceptions.py @@ -21,7 +21,7 @@ class InternalException(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 ---------- @@ -37,6 +37,23 @@ class HTTPException(BoticordException): 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): """Status codes of response""" diff --git a/boticordpy/http.py b/boticordpy/http.py index 954a102..10689b8 100644 --- a/boticordpy/http.py +++ b/boticordpy/http.py @@ -1,10 +1,10 @@ +from urllib.parse import urlparse import asyncio import typing import aiohttp from . import exceptions -from .types import LinkDomain class HttpClient: @@ -29,13 +29,17 @@ class HttpClient: 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""" kwargs["headers"] = {"Content-Type": "application/json"} if self.token is not None: kwargs["headers"]["Authorization"] = self.token + if meilisearch_token is not None: + kwargs["headers"]["Authorization"] = f"Bearer {meilisearch_token}" url = f"{self.API_URL}{endpoint}" @@ -43,11 +47,14 @@ class HttpClient: data = await response.json() if (200, 201).__contains__(response.status): - return data["result"] + return data["result"] if not meilisearch_token else data else: - raise exceptions.HTTPException( - {"status": response.status, "error": data["errors"][0]["code"]} - ) + if not meilisearch_token: + raise exceptions.HTTPException( + {"status": response.status, "error": data["errors"][0]["code"]} + ) + else: + raise exceptions.MeilisearchException(data) def get_bot_info(self, bot_id: typing.Union[str, int]): """Get information about the specified bot""" @@ -64,3 +71,16 @@ class HttpClient: def get_user_info(self, user_id: typing.Union[str, int]): """Get information about specified user""" 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, + ) diff --git a/boticordpy/types.py b/boticordpy/types.py index 917317d..4852bcf 100644 --- a/boticordpy/types.py +++ b/boticordpy/types.py @@ -559,7 +559,12 @@ class PartialUser(APIObjectBase): @dataclass(repr=False) 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 """Server's ID""" @@ -700,6 +705,9 @@ class ResourceBot(APIObjectBase): short_link: Optional[str] """Short link to the bot's page""" + standart_banner_id: int + """Server's standart banner ID""" + invite_link: str """Invite link""" @@ -788,6 +796,7 @@ class ResourceBot(APIObjectBase): self.support_server_invite_link = data.get("support_server_invite") self.website = data.get("website") self.up_count = data.get("upCount") + self.standart_banner_id = data.get("standartBannerID") self.premium_active = data["premium"].get("active") self.premium_splash_url = data["premium"].get("splashURL") @@ -850,5 +859,231 @@ class UserProfile(PartialUser): return self -class LinkDomain: - pass +@dataclass(repr=False) +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 diff --git a/docs/source/api/types.rst b/docs/source/api/types.rst index 675d748..959cb54 100644 --- a/docs/source/api/types.rst +++ b/docs/source/api/types.rst @@ -56,3 +56,16 @@ Users :members: :exclude-members: to_dict :inherited-members: + + +MeiliSearch +------------ + +.. autoclass:: MeiliIndexedBot + :members: + +.. autoclass:: MeiliIndexedServer + :members: + +.. autoclass:: MeiliIndexedComment + :members: From 3c1a002e815c0cb4047ec9197547d00fcf1bdc11 Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Wed, 7 Jun 2023 12:35:35 +0300 Subject: [PATCH 6/6] update docs --- docs/source/api/exceptions.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/api/exceptions.rst b/docs/source/api/exceptions.rst index 1767b4a..ac2cea0 100644 --- a/docs/source/api/exceptions.rst +++ b/docs/source/api/exceptions.rst @@ -13,6 +13,9 @@ Exceptions API Reference .. autoclass:: HTTPException :members: +.. autoclass:: MeilisearchException + :members: + .. autoclass:: StatusCodes :members: