From 1d595fe4f8ef294e9f46b859b3c3507f2e5de6f7 Mon Sep 17 00:00:00 2001 From: SecondThundeR Date: Sat, 4 Jun 2022 14:26:49 +0300 Subject: [PATCH 01/15] add tokens refresh on config init --- shikithon/api.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shikithon/api.py b/shikithon/api.py index 8ed77d23..cb4ef8b8 100644 --- a/shikithon/api.py +++ b/shikithon/api.py @@ -272,6 +272,7 @@ def _init_config(self, config: Union[str, Dict[str, str]]): - Validation of config and variables - Customizing the session header user agent - Getting access/refresh tokens if they are missing + - Refresh current tokens if they are not valid Otherwise, if only app name is provided, setting it @@ -289,6 +290,10 @@ def _init_config(self, config: Union[str, Dict[str, str]]): tokens_data: Tuple[str, str] = self._get_access_token() self._update_tokens(tokens_data) + if self.token_expired(): + logger.debug('Token has expired. Refreshing...') + self.refresh_tokens() + @logger.catch(onerror=lambda _: sys.exit(1)) def _validate_config(self, config: Union[str, Dict[str, str]]): """ From 4a28d744f73c288c654d5e65f29293529e08fc3b Mon Sep 17 00:00:00 2001 From: SecondThundeR Date: Sat, 4 Jun 2022 14:27:38 +0300 Subject: [PATCH 02/15] rename LinkedType of favorite.py to prevent collision --- shikithon/endpoints.py | 11 ++++++----- shikithon/enums/favorite.py | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/shikithon/endpoints.py b/shikithon/endpoints.py index 20ee3cc2..79d31918 100644 --- a/shikithon/endpoints.py +++ b/shikithon/endpoints.py @@ -6,7 +6,7 @@ """ from typing import Union -from shikithon.enums.favorite import LinkedType +from shikithon.enums.favorite import FavoriteLinkedType from shikithon.enums.person import PersonKind from shikithon.utils import Utils @@ -634,14 +634,14 @@ def episode_notifications(self) -> str: """ return f'{self.base_url_v2}/episode_notifications' - def favorites_create(self, linked_type: LinkedType, linked_id: int, + def favorites_create(self, linked_type: FavoriteLinkedType, linked_id: int, kind: PersonKind) -> str: """ Returns endpoint for creating some type of object as favorite :param linked_type: Type of object - :type linked_type: LinkedType + :type linked_type: FavoriteLinkedType :param linked_id: ID of linked object :type linked_id: int @@ -656,13 +656,14 @@ def favorites_create(self, linked_type: LinkedType, linked_id: int, return f'{self.base_url}/favorites/' \ f'{linked_type.value}/{linked_id}/{kind.value}' - def favorites_destroy(self, linked_type: LinkedType, linked_id: int) -> str: + def favorites_destroy(self, linked_type: FavoriteLinkedType, + linked_id: int) -> str: """ Returns endpoint for destroying some type of object from favorites :param linked_type: Type of object - :type linked_type: LinkedType + :type linked_type: FavoriteLinkedType :param linked_id: ID of linked object :type linked_id: int diff --git a/shikithon/enums/favorite.py b/shikithon/enums/favorite.py index d898c7db..e5066f42 100644 --- a/shikithon/enums/favorite.py +++ b/shikithon/enums/favorite.py @@ -2,7 +2,7 @@ from enum import Enum -class LinkedType(Enum): +class FavoriteLinkedType(Enum): """Contains constants related for favorite linked type.""" ANIME = 'Anime' MANGA = 'Manga' From 26c41869b22714b292200016665a66e6256449ac Mon Sep 17 00:00:00 2001 From: SecondThundeR Date: Sat, 4 Jun 2022 14:28:10 +0300 Subject: [PATCH 03/15] add /api/topics enums --- shikithon/enums/topic.py | 67 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 shikithon/enums/topic.py diff --git a/shikithon/enums/topic.py b/shikithon/enums/topic.py new file mode 100644 index 00000000..e2250cf4 --- /dev/null +++ b/shikithon/enums/topic.py @@ -0,0 +1,67 @@ +"""Enums for /api/topics.""" +from enum import Enum + + +class TopicsType(Enum): + """Contains constants related for getting certain type of topic.""" + REGULAR_TOPIC = 'Topic' + CLUB_USER_TOPIC = 'Topics::ClubUserTopic' + ENTRY_TOPIC = 'Topics::EntryTopic' + NEWS_TOPIC = 'Topics::NewsTopic' + + +class EntryTopics(Enum): + """Contains constants related for getting certain type of topic.""" + ANIME_TOPIC = 'Topics::EntryTopics::AnimeTopic' + ARTICLE_TOPIC = 'Topics::EntryTopics::ArticleTopic' + CHARACTER_TOPIC = 'Topics::EntryTopics::CharacterTopic' + CLUB_PAGE_TOPIC = 'Topics::EntryTopics::ClubPageTopic' + CLUB_TOPIC = 'Topics::EntryTopics::ClubTopic' + COLLECTION_TOPIC = 'Topics::EntryTopics::CollectionTopic' + CONTEST_TOPIC = 'Topics::EntryTopics::ContestTopic' + COSPLAY_GALLERY_TOPIC = 'Topics::EntryTopics::CosplayGalleryTopic' + MANGA_TOPIC = 'Topics::EntryTopics::MangaTopic' + PERSON_TOPIC = 'Topics::EntryTopics::PersonTopic' + RANOBE_TOPIC = 'Topics::EntryTopics::RanobeTopic' + CRITIQUE_TOPIC = 'Topics::EntryTopics::CritiqueTopic' + REVIEW_TOPIC = 'Topics::EntryTopics::ReviewTopic' + + +class NewsTopics(Enum): + """Contains constants related for getting certain type of news topic.""" + CONTEST_STATUS_TOPIC = 'Topics::NewsTopics::ContestStatusTopic' + + +class ForumType(Enum): + """Contains constants related for getting certain type of forum.""" + ALL = 'all' + ANIMANGA = 'animanga' + SITE = 'site' + GAMES = 'games' + VN = 'vn' + CONTESTS = 'contests' + OFFTOPIC = 'offtopic' + CLUBS = 'clubs' + MY_CLUBS = 'my_clubs' + CRITIQUES = 'critiques' + NEWS = 'news' + COLLECTIONS = 'collections' + ARTICLES = 'articles' + COSPLAY = 'cosplay' + + +class TopicLinkedType(Enum): + """Contains constants related for getting certain linked type of topic.""" + ANIME = 'Anime' + MANGA = 'Manga' + RANOBE = 'Ranobe' + CHARACTER = 'Character' + PERSON = 'Person' + CLUB = 'Club' + CLUB_PAGE = 'ClubPage' + CRITIQUE = 'Critique' + REVIEW = 'Review' + CONTEST = 'Contest' + COSPLAY_GALERY = 'CosplayGallery' + COLLECTION = 'Collection' + ARTICLE = 'Article' From b0db0ae4e7af291676a2a3233052b06afed305bd Mon Sep 17 00:00:00 2001 From: SecondThundeR Date: Sat, 4 Jun 2022 15:01:26 +0300 Subject: [PATCH 04/15] fix topic model fields --- shikithon/models/topic.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/shikithon/models/topic.py b/shikithon/models/topic.py index 14dd6c5d..59436194 100644 --- a/shikithon/models/topic.py +++ b/shikithon/models/topic.py @@ -13,19 +13,19 @@ class Topic(BaseModel): """Represents topic entity.""" id: int - topic_title: str - body: str - html_body: str - html_footer: str + topic_title: Optional[str] + body: Optional[str] + html_body: Optional[str] + html_footer: Optional[str] created_at: datetime - comments_count: int - forum: Forum - user: User - type: str - linked_id: int - linked_type: str + comments_count: Optional[int] + forum: Optional[Forum] + user: Optional[User] + type: Optional[str] + linked_id: Optional[int] + linked_type: Optional[str] linked: Union[Anime, Manga] - viewed: bool + viewed: Optional[bool] last_comment_viewed: Optional[bool] event: Optional[str] episode: Optional[int] From d33ad9e7cd5a8616a9a9af9ee73a385c7979bf59 Mon Sep 17 00:00:00 2001 From: SecondThundeR Date: Sat, 4 Jun 2022 15:12:03 +0300 Subject: [PATCH 05/15] add unified validator for response data --- shikithon/utils.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/shikithon/utils.py b/shikithon/utils.py index e5488dcb..2d4ab377 100644 --- a/shikithon/utils.py +++ b/shikithon/utils.py @@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional, Union from loguru import logger +from pydantic.main import ModelMetaclass LOWER_LIMIT_NUMBER = 1 @@ -241,3 +242,32 @@ def query_numbers_validator(**query_numbers: List[Optional[int]] validated_numbers[name] = (Utils.validate_query_number( data[0], data[1])) return validated_numbers + + @staticmethod + def validate_return_data( + response_data: Union[List[Dict[str, Any]], + Dict[str, Any]], data_model: ModelMetaclass + ) -> Optional[Union[ModelMetaclass, List[ModelMetaclass]]]: + """ + Validates passed response data and returns + parsed models. + + :param response_data: Response data + :type response_data: Union[List[Dict[str, Any]] + + :param data_model: Model to convert into passed response data + :type data_model: ModelMetaclass + + :return: Parsed response data + :rtype: Optional[ModelMetaclass, List[ModelMetaclass]] + """ + if not response_data: + return None + + if 'errors' in response_data or 'code' in response_data: + return None + + if isinstance(response_data, list): + return [data_model(**item) for item in response_data] + + return data_model(**response_data) From 1631f12def3f311a43eb281db3374b839cde2bab Mon Sep 17 00:00:00 2001 From: SecondThundeR Date: Sat, 4 Jun 2022 15:12:29 +0300 Subject: [PATCH 06/15] update /api/topics/updates property name --- shikithon/endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shikithon/endpoints.py b/shikithon/endpoints.py index 79d31918..e5e3a126 100644 --- a/shikithon/endpoints.py +++ b/shikithon/endpoints.py @@ -1071,7 +1071,7 @@ def topic(self, topic_id: int) -> str: return f'{self.topics}/{topic_id}' @property - def topics_updates(self) -> str: + def updates_topics(self) -> str: """ Returns endpoint of the topics updates list. From 8ac854aec6acc8498ccc1cf657536da402e43acb Mon Sep 17 00:00:00 2001 From: SecondThundeR Date: Sat, 4 Jun 2022 19:29:25 +0300 Subject: [PATCH 07/15] update validator logic --- shikithon/utils.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/shikithon/utils.py b/shikithon/utils.py index 2d4ab377..8d9c104b 100644 --- a/shikithon/utils.py +++ b/shikithon/utils.py @@ -12,6 +12,8 @@ from loguru import logger from pydantic.main import ModelMetaclass +from shikithon.enums.response import ResponseCode + LOWER_LIMIT_NUMBER = 1 @@ -245,29 +247,34 @@ def query_numbers_validator(**query_numbers: List[Optional[int]] @staticmethod def validate_return_data( - response_data: Union[List[Dict[str, Any]], - Dict[str, Any]], data_model: ModelMetaclass - ) -> Optional[Union[ModelMetaclass, List[ModelMetaclass]]]: + response_data: Union[List[Dict[str, Any]], Dict[str, Any], List[Any]], + data_model: Optional[ModelMetaclass] = None + ) -> Optional[Union[ModelMetaclass, List[ModelMetaclass], List[Any], bool]]: """ Validates passed response data and returns parsed models. :param response_data: Response data - :type response_data: Union[List[Dict[str, Any]] + :type response_data: Union[List[Dict[str, Any], + Dict[str, Any], List[Any]]] :param data_model: Model to convert into passed response data - :type data_model: ModelMetaclass + :type data_model: Optional[ModelMetaclass] :return: Parsed response data - :rtype: Optional[ModelMetaclass, List[ModelMetaclass]] + :rtype: Optional[Union[ModelMetaclass, List[ModelMetaclass], bool]] """ if not response_data: return None + if isinstance(response_data, int): + return response_data == ResponseCode.SUCCESS.value + if 'errors' in response_data or 'code' in response_data: return None - if isinstance(response_data, list): - return [data_model(**item) for item in response_data] + if data_model is None: + return response_data - return data_model(**response_data) + return [data_model(**item) for item in response_data] if isinstance( + response_data, list) else data_model(**response_data) From 055e18709746a700bbf552e7569945d052fe0388 Mon Sep 17 00:00:00 2001 From: SecondThundeR Date: Sat, 4 Jun 2022 19:42:02 +0300 Subject: [PATCH 08/15] update validator fix import for ModelMetaclass add new parameter for response code add logging support --- shikithon/utils.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/shikithon/utils.py b/shikithon/utils.py index 8d9c104b..781fe02e 100644 --- a/shikithon/utils.py +++ b/shikithon/utils.py @@ -9,8 +9,8 @@ from time import time from typing import Any, Dict, List, Optional, Union +import pydantic.main from loguru import logger -from pydantic.main import ModelMetaclass from shikithon.enums.response import ResponseCode @@ -248,8 +248,10 @@ def query_numbers_validator(**query_numbers: List[Optional[int]] @staticmethod def validate_return_data( response_data: Union[List[Dict[str, Any]], Dict[str, Any], List[Any]], - data_model: Optional[ModelMetaclass] = None - ) -> Optional[Union[ModelMetaclass, List[ModelMetaclass], List[Any], bool]]: + data_model: Optional[pydantic.main.ModelMetaclass] = None, + response_code: Optional[ResponseCode] = None + ) -> Optional[Union[pydantic.main.ModelMetaclass, + List[pydantic.main.ModelMetaclass], List[Any], bool]]: """ Validates passed response data and returns parsed models. @@ -259,22 +261,35 @@ def validate_return_data( Dict[str, Any], List[Any]]] :param data_model: Model to convert into passed response data - :type data_model: Optional[ModelMetaclass] + :type data_model: Optional[pydantic.main.ModelMetaclass] + + :param response_code: Code of response + (Used only when response_data is int) + :type response_code: Optional[ResponseCode] :return: Parsed response data - :rtype: Optional[Union[ModelMetaclass, List[ModelMetaclass], bool]] + :rtype: Optional[Union[pydantic.main.ModelMetaclass, + List[pydantic.main.ModelMetaclass], bool]] """ + logger.debug(f'Validating return data: {response_data=}, ' + f'{data_model=}, {response_code=}') if not response_data: + logger.debug('Response data is empty. Returning None') return None if isinstance(response_data, int): - return response_data == ResponseCode.SUCCESS.value + logger.debug('Response data is int. Returning value ' + 'of response code comparison') + return response_data == response_code.value if 'errors' in response_data or 'code' in response_data: + logger.debug('Response data contains errors info. Returning None') return None if data_model is None: + logger.debug("Data model isn't passed. Returning response data") return response_data + logger.debug('Data model is passed. Returning parsed data') return [data_model(**item) for item in response_data] if isinstance( response_data, list) else data_model(**response_data) From e140f7d558a16f4203757634eb9b663739134521 Mon Sep 17 00:00:00 2001 From: SecondThundeR Date: Sat, 4 Jun 2022 19:48:46 +0300 Subject: [PATCH 09/15] update validator (again) add support for success info in response --- shikithon/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shikithon/utils.py b/shikithon/utils.py index 781fe02e..94450a5d 100644 --- a/shikithon/utils.py +++ b/shikithon/utils.py @@ -286,6 +286,10 @@ def validate_return_data( logger.debug('Response data contains errors info. Returning None') return None + if 'notice' in response_data or 'success' in response_data: + logger.debug('Response data contains success info. Returning True') + return True + if data_model is None: logger.debug("Data model isn't passed. Returning response data") return response_data From a3b94d6d036ed43b8f27d7b4d507958e48b9a4b9 Mon Sep 17 00:00:00 2001 From: SecondThundeR Date: Sat, 4 Jun 2022 21:08:12 +0300 Subject: [PATCH 10/15] remove check for True for bool type of data --- shikithon/utils.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/shikithon/utils.py b/shikithon/utils.py index 94450a5d..57c480e4 100644 --- a/shikithon/utils.py +++ b/shikithon/utils.py @@ -96,10 +96,7 @@ def generate_query_dict( if data is None: continue if isinstance(data, bool): - if data is True: - query_dict[key] = str(int(data)) - else: - continue + query_dict[key] = str(int(data)) elif isinstance(data, int): query_dict[key] = str(data) elif isinstance(data, Enum): @@ -247,7 +244,8 @@ def query_numbers_validator(**query_numbers: List[Optional[int]] @staticmethod def validate_return_data( - response_data: Union[List[Dict[str, Any]], Dict[str, Any], List[Any]], + response_data: Union[List[Dict[str, Any]], Dict[str, Any], List[Any], + int], data_model: Optional[pydantic.main.ModelMetaclass] = None, response_code: Optional[ResponseCode] = None ) -> Optional[Union[pydantic.main.ModelMetaclass, From cdc13dc0dba3d9a6f3d4a144c578b0f98c62a7dc Mon Sep 17 00:00:00 2001 From: SecondThundeR Date: Sat, 4 Jun 2022 21:51:03 +0300 Subject: [PATCH 11/15] update api add /api/topics methods move to unified response data validation rename favorite enum import make all non-optional return types to optional (only for endpoint methods) remove check and note for create/delete friend by nickname remove note about None return for empty lists fixes and improvements --- shikithon/api.py | 747 ++++++++++++++++++++++++++--------------------- 1 file changed, 422 insertions(+), 325 deletions(-) diff --git a/shikithon/api.py b/shikithon/api.py index cb4ef8b8..e0cf4b24 100644 --- a/shikithon/api.py +++ b/shikithon/api.py @@ -21,7 +21,7 @@ from shikithon.enums.club import (CommentPolicy, ImageUploadPolicy, JoinPolicy, PagePolicy, TopicPolicy) from shikithon.enums.comment import CommentableType -from shikithon.enums.favorite import LinkedType +from shikithon.enums.favorite import FavoriteLinkedType from shikithon.enums.history import TargetType from shikithon.enums.manga import (MangaCensorship, MangaKind, MangaList, MangaOrder, MangaStatus) @@ -32,6 +32,8 @@ from shikithon.enums.request import RequestType from shikithon.enums.response import ResponseCode from shikithon.enums.style import OwnerType +from shikithon.enums.topic import (EntryTopics, ForumType, NewsTopics, + TopicLinkedType, TopicsType) from shikithon.exceptions import (AccessTokenException, MissingAppName, MissingAuthCode, MissingClientID, MissingClientSecret, MissingConfigData, @@ -119,7 +121,6 @@ def __init__(self, config: Union[str, Dict[str, str]]): 'compression': 'zip' }, ]) - logger.info('Initializing API object') self._endpoints: Endpoints = Endpoints(SHIKIMORI_API_URL, @@ -625,16 +626,14 @@ def achievements(self, user_id: int) -> Optional[List[Achievement]]: :param user_id: User ID for getting achievements :type user_id: int - :return: List of achievements or None if list is empty + :return: List of achievements :rtype: Optional[List[Achievement]] """ logger.debug('Executing "/api/achievements/ method') response: List[Dict[str, Any]] = self._request( self._endpoints.achievements, query=Utils.generate_query_dict(user_id=user_id)) - if not response: - return None - return [Achievement(**achievement) for achievement in response] + return Utils.validate_return_data(response, data_model=Achievement) def animes(self, page: Optional[int] = None, @@ -711,7 +710,7 @@ def animes(self, :param search: Search phrase to filter animes by name :type search: Optional[str] - :return: Animes list or None if list is empty + :return: Animes list :rtype: Optional[List[Anime]] """ logger.debug('Executing "/api/animes" method') @@ -744,11 +743,9 @@ def animes(self, ids=ids, exclude_ids=exclude_ids, search=search)) - if not response: - return None - return [Anime(**anime) for anime in response] + return Utils.validate_return_data(response, data_model=Anime) - def anime(self, anime_id: int) -> Anime: + def anime(self, anime_id: int) -> Optional[Anime]: """ Returns info about certain anime. @@ -756,12 +753,12 @@ def anime(self, anime_id: int) -> Anime: :type anime_id: int :return: Anime info - :rtype: Anime + :rtype: Optional[Anime] """ logger.debug('Executing "/api/animes/:id" method') response: Dict[str, Any] = self._request(self._endpoints.anime(anime_id)) - return Anime(**response) + return Utils.validate_return_data(response, data_model=Anime) def anime_creators(self, anime_id: int) -> Optional[List[Creator]]: """ @@ -770,15 +767,13 @@ def anime_creators(self, anime_id: int) -> Optional[List[Creator]]: :param anime_id: Anime ID to get creators :type anime_id: int - :return: List of anime creators or None if list is empty + :return: List of anime creators :rtype: Optional[List[Creator]] """ logger.debug('Executing "/api/animes/:id/roles" method') response: List[Dict[str, Any]] = self._request( self._endpoints.anime_roles(anime_id)) - if not response: - return None - return [Creator(**creator) for creator in response] + return Utils.validate_return_data(response, data_model=Creator) def similar_animes(self, anime_id: int) -> Optional[List[Anime]]: """ @@ -787,15 +782,13 @@ def similar_animes(self, anime_id: int) -> Optional[List[Anime]]: :param anime_id: Anime ID to get similar animes :type anime_id: int - :return: List of similar animes or None if list is empty + :return: List of similar animes :rtype: Optional[List[Anime]] """ logger.debug('Executing "/api/animes/:id/similar" method') response: List[Dict[str, Any]] = self._request( self._endpoints.similar_animes(anime_id)) - if not response: - return None - return [Anime(**anime) for anime in response] + return Utils.validate_return_data(response, data_model=Anime) def anime_related_content(self, anime_id: int) -> Optional[List[Relation]]: """ @@ -804,15 +797,13 @@ def anime_related_content(self, anime_id: int) -> Optional[List[Relation]]: :param anime_id: Anime ID to get related content :type anime_id: int - :return: List of relations or None if list is empty + :return: List of relations :rtype: Optional[List[Relation]] """ logger.debug('Executing "/api/animes/:id/related" method') response: List[Dict[str, Any]] = self._request( self._endpoints.anime_related_content(anime_id)) - if not response: - return None - return [Relation(**relation) for relation in response] + return Utils.validate_return_data(response, data_model=Relation) def anime_screenshots(self, anime_id: int) -> Optional[List[Screenshot]]: """ @@ -821,17 +812,15 @@ def anime_screenshots(self, anime_id: int) -> Optional[List[Screenshot]]: :param anime_id: Anime ID to get screenshot links :type anime_id: int - :return: List of screenshot links or None if list is empty + :return: List of screenshot links :rtype: Optional[List[Screenshot]] """ logger.debug('Executing "/api/animes/:id/screenshots" method') response: List[Dict[str, Any]] = self._request( self._endpoints.anime_screenshots(anime_id)) - if not response: - return None - return [Screenshot(**screenshot) for screenshot in response] + return Utils.validate_return_data(response, data_model=Screenshot) - def anime_franchise_tree(self, anime_id: int) -> FranchiseTree: + def anime_franchise_tree(self, anime_id: int) -> Optional[FranchiseTree]: """ Returns franchise tree of certain anime. @@ -839,12 +828,12 @@ def anime_franchise_tree(self, anime_id: int) -> FranchiseTree: :type anime_id: int :return: Franchise tree of certain anime - :rtype: FranchiseTree + :rtype: Optional[FranchiseTree] """ logger.debug('Executing "/api/animes/:id/franchise" method') response: Dict[str, Any] = self._request( self._endpoints.anime_franchise_tree(anime_id)) - return FranchiseTree(**response) + return Utils.validate_return_data(response, data_model=FranchiseTree) def anime_external_links(self, anime_id: int) -> Optional[List[Link]]: """ @@ -853,15 +842,13 @@ def anime_external_links(self, anime_id: int) -> Optional[List[Link]]: :param anime_id: Anime ID to get external links :type anime_id: int - :return: List of external links or None if list is empty + :return: List of external links :rtype: Optional[List[Link]] """ logger.debug('Executing "/api/animes/:id/external_links" method') response: List[Dict[str, Any]] = self._request( self._endpoints.anime_external_links(anime_id)) - if not response: - return None - return [Link(**link) for link in response] + return Utils.validate_return_data(response, data_model=Link) def anime_topics(self, anime_id: int, @@ -889,7 +876,7 @@ def anime_topics(self, :param episode: Number of anime episode :type episode: Optional[int] - :return: List of topics or None if list is empty + :return: List of topics :rtype: Optional[List[Topic]] """ logger.debug('Executing "/api/animes/:id/topics" method') @@ -902,9 +889,7 @@ def anime_topics(self, limit=validated_numbers['limit'], kind=kind, episode=episode)) - if not response: - return None - return [Topic(**topic) for topic in response] + return Utils.validate_return_data(response, data_model=Topic) @protected_method() def appears(self, comment_ids: List[str]) -> bool: @@ -921,12 +906,13 @@ def appears(self, comment_ids: List[str]) -> bool: :rtype: bool """ logger.debug('Executing "/api/appears" method') - response_code: int = self._request( + response: Union[Dict[str, Any], int] = self._request( self._endpoints.appears, headers=self._authorization_header, data=Utils.generate_query_dict(ids=comment_ids), request_type=RequestType.POST) - return response_code == ResponseCode.SUCCESS.value + return Utils.validate_return_data(response, + response_code=ResponseCode.SUCCESS) def bans(self, page: Optional[int] = None, @@ -940,7 +926,7 @@ def bans(self, :param limit: Number of results limit :type limit: Optional[int] - :return: List of recent bans or None if list is empty + :return: List of recent bans :rtype: Optional[List[Ban]] """ logger.debug('Executing "/api/bans" method') @@ -954,9 +940,7 @@ def bans(self, self._endpoints.bans_list, query=Utils.generate_query_dict(page=validated_numbers['page'], limit=validated_numbers['limit'])) - if not response: - return None - return [Ban(**ban) for ban in response] + return Utils.validate_return_data(response, data_model=Ban) def calendar( self, @@ -968,18 +952,16 @@ def calendar( :param censored: Status of censorship for events :type censored: Optional[AnimeCensorship] - :return: List of calendar events or None if list is empty + :return: List of calendar events :rtype: Optional[List[CalendarEvent]] """ logger.debug('Executing "api/calendar" method') response: List[Dict[str, Any]] = self._request( self._endpoints.calendar, query=Utils.generate_query_dict(censored=censored)) - if not response: - return None - return [CalendarEvent(**calendar_event) for calendar_event in response] + return Utils.validate_return_data(response, data_model=CalendarEvent) - def character(self, character_id: int) -> Character: + def character(self, character_id: int) -> Optional[Character]: """ Returns character info by ID. @@ -987,12 +969,12 @@ def character(self, character_id: int) -> Character: :type character_id: int :return: Character info - :rtype: Character + :rtype: Optional[Character] """ logger.debug('Executing "/api/characters/:id" method') response: Dict[str, Any] = self._request( self._endpoints.character(character_id)) - return Character(**response) + return Utils.validate_return_data(response, data_model=Character) def character_search(self, search: Optional[str] = None @@ -1003,16 +985,14 @@ def character_search(self, :param search: Search query for characters :type search: Optional[str] - :return: List of found characters or None if list is empty + :return: List of found characters :rtype: Optional[List[Character]] """ logger.debug('Executing "/api/characters/search" method') response: List[Dict[str, Any]] = self._request( self._endpoints.character_search, query=Utils.generate_query_dict(search=search)) - if not response: - return None - return [Character(**character) for character in response] + return Utils.validate_return_data(response, data_model=Character) def clubs(self, page: Optional[int] = None, @@ -1030,7 +1010,7 @@ def clubs(self, :param search: Search phrase to filter clubs by name :type search: Optional[str] - :return: Clubs list or None if list is empty + :return: Clubs list :rtype: Optional[List[Club]] """ logger.debug('Executing "/api/clubs" method') @@ -1044,11 +1024,9 @@ def clubs(self, query=Utils.generate_query_dict(page=validated_numbers['page'], limit=validated_numbers['limit'], search=search)) - if not response: - return None - return [Club(**club) for club in response] + return Utils.validate_return_data(response, data_model=Club) - def club(self, club_id: int) -> Club: + def club(self, club_id: int) -> Optional[Club]: """ Returns info about club. @@ -1056,11 +1034,11 @@ def club(self, club_id: int) -> Club: :type club_id: int :return: Info about club - :rtype: Club + :rtype: Optional[Club] """ logger.debug('Executing "/api/clubs/:id" method') response: Dict[str, Any] = self._request(self._endpoints.club(club_id)) - return Club(**response) + return Utils.validate_return_data(response, data_model=Club) @protected_method(scope='clubs') def club_update( @@ -1140,7 +1118,7 @@ def club_update( :param banned_user_ids: New banned user ids of club :type banned_user_ids: Optional[List[int]] - :return: Updated club info or None if an error occurred + :return: Updated club info :rtype: Optional[Club] """ logger.debug('Executing "/api/clubs/:id" method') @@ -1167,7 +1145,7 @@ def club_update( collection_ids=collection_ids, banned_user_ids=banned_user_ids), request_type=RequestType.PATCH) - return Club(**response) if 'errors' not in response else None + return Utils.validate_return_data(response, data_model=Club) def club_animes(self, club_id: int) -> Optional[List[Anime]]: """ @@ -1176,15 +1154,13 @@ def club_animes(self, club_id: int) -> Optional[List[Anime]]: :param club_id: Club ID to get anime list :type club_id: int - :return: Club anime list or None if list is empty + :return: Club anime list :rtype: Optional[List[Anime]] """ logger.debug('Executing "/api/clubs/:id/animes" method') response: List[Dict[str, Any]] = self._request( self._endpoints.club_animes(club_id)) - if not response: - return None - return [Anime(**anime) for anime in response] + return Utils.validate_return_data(response, data_model=Anime) def club_mangas(self, club_id: int) -> Optional[List[Manga]]: """ @@ -1193,15 +1169,13 @@ def club_mangas(self, club_id: int) -> Optional[List[Manga]]: :param club_id: Club ID to get manga list :type club_id: int - :return: Club manga list or None if list is empty + :return: Club manga list :rtype: Optional[List[Manga]] """ logger.debug('Executing "/api/clubs/:id/mangas" method') response: List[Dict[str, Any]] = self._request( self._endpoints.club_mangas(club_id)) - if not response: - return None - return [Manga(**manga) for manga in response] + return Utils.validate_return_data(response, data_model=Manga) def club_ranobe(self, club_id: int) -> Optional[List[Ranobe]]: """ @@ -1210,15 +1184,13 @@ def club_ranobe(self, club_id: int) -> Optional[List[Ranobe]]: :param club_id: Club ID to get ranobe list :type club_id: int - :return: Club ranobe list or None if list is empty + :return: Club ranobe list :rtype: Optional[List[Ranobe]] """ logger.debug('Executing "/api/clubs/:id/ranobe" method') response: List[Dict[str, Any]] = self._request( self._endpoints.club_ranobe(club_id)) - if not response: - return None - return [Ranobe(**ranobe) for ranobe in response] + return Utils.validate_return_data(response, data_model=Ranobe) def club_characters(self, club_id: int) -> Optional[List[Character]]: """ @@ -1227,15 +1199,13 @@ def club_characters(self, club_id: int) -> Optional[List[Character]]: :param club_id: Club ID to get character list :type club_id: int - :return: Club character list or None if list is empty + :return: Club character list :rtype: Optional[List[Character]] """ logger.debug('Executing "/api/clubs/:id/characters" method') response: List[Dict[str, Any]] = self._request( self._endpoints.club_characters(club_id)) - if not response: - return None - return [Character(**character) for character in response] + return Utils.validate_return_data(response, data_model=Character) def club_members(self, club_id: int) -> Optional[List[User]]: """ @@ -1244,15 +1214,13 @@ def club_members(self, club_id: int) -> Optional[List[User]]: :param club_id: Club ID to get member list :type club_id: int - :return: Club member list or None if list is empty + :return: Club member list :rtype: Optional[List[User]] """ logger.debug('Executing "/api/clubs/:id/members" method') response: List[Dict[str, Any]] = self._request( self._endpoints.club_members(club_id)) - if not response: - return None - return [User(**user) for user in response] + return Utils.validate_return_data(response, data_model=User) def club_images(self, club_id: int) -> Optional[List[ClubImage]]: """ @@ -1261,15 +1229,13 @@ def club_images(self, club_id: int) -> Optional[List[ClubImage]]: :param club_id: Club ID to get images :type club_id: int - :return: Club's images or None if list is empty + :return: Club's images :rtype: Optional[List[ClubImage]] """ logger.debug('Executing "/api/clubs/:id/images" method') response: List[Dict[str, Any]] = self._request( self._endpoints.club_images(club_id)) - if not response: - return None - return [ClubImage(**club_image) for club_image in response] + return Utils.validate_return_data(response, data_model=ClubImage) @protected_method(scope='clubs') def club_join(self, club_id: int): @@ -1287,7 +1253,7 @@ def club_join(self, club_id: int): int] = self._request(self._endpoints.club_join(club_id), headers=self._authorization_header, request_type=RequestType.POST) - return response == ResponseCode.SUCCESS.value + return Utils.validate_return_data(response) @protected_method(scope='clubs') def club_leave(self, club_id: int) -> bool: @@ -1305,7 +1271,7 @@ def club_leave(self, club_id: int) -> bool: self._endpoints.club_leave(club_id), headers=self._authorization_header, request_type=RequestType.POST) - return response == ResponseCode.SUCCESS.value + return Utils.validate_return_data(response) def comments(self, commentable_id: int, @@ -1331,7 +1297,7 @@ def comments(self, :param desc: Status of description in request. Can be 1 or 0 :type desc: Optional[int] - :return: List of comments or None if list is empty + :return: List of comments :rtype: Optional[List[Comment]] """ logger.debug('Executing "/api/comments" method') @@ -1347,11 +1313,9 @@ def comments(self, commentable_id=commentable_id, commentable_type=commentable_type, desc=desc)) - if not response: - return None - return [Comment(**comment) for comment in response] + return Utils.validate_return_data(response, data_model=Comment) - def comment(self, comment_id: int) -> Comment: + def comment(self, comment_id: int) -> Optional[Comment]: """ Returns comment info. @@ -1359,12 +1323,12 @@ def comment(self, comment_id: int) -> Comment: :type comment_id: int :return: Comment info - :rtype: Comment + :rtype: Optional[Comment] """ logger.debug('Executing "/api/comments/:id" method') response: Dict[str, Any] = self._request(self._endpoints.comment(comment_id)) - return Comment(**response) + return Utils.validate_return_data(response, data_model=Comment) @protected_method(scope='comments') def create_comment(self, @@ -1394,7 +1358,7 @@ def create_comment(self, :param broadcast: Broadcast comment in club’s topic status :type broadcast: Optional[bool] - :return: Created comment info or None if an error occurred + :return: Created comment info :rtype: Optional[Comment] """ logger.debug('Executing "/api/comments" method') @@ -1414,7 +1378,7 @@ def create_comment(self, headers=self._authorization_header, data=data_dict, request_type=RequestType.POST) - return Comment(**response) if 'errors' not in response else None + return Utils.validate_return_data(response, data_model=Comment) @protected_method(scope='comments') def update_comment(self, comment_id: int, body: str) -> Optional[Comment]: @@ -1427,7 +1391,7 @@ def update_comment(self, comment_id: int, body: str) -> Optional[Comment]: :param body: New body of comment :type body: str - :return: Updated comment info or None if an error occurred + :return: Updated comment info :rtype: Optional[Comment] """ logger.debug('Executing "/api/comments/:id" method') @@ -1436,7 +1400,7 @@ def update_comment(self, comment_id: int, body: str) -> Optional[Comment]: headers=self._authorization_header, data=Utils.generate_data_dict(dict_name='comment', body=body), request_type=RequestType.PATCH) - return Comment(**response) if 'errors' not in response else None + return Utils.validate_return_data(response, data_model=Comment) @protected_method(scope='comments') def delete_comment(self, comment_id: int) -> bool: @@ -1454,83 +1418,80 @@ def delete_comment(self, comment_id: int) -> bool: Any] = self._request(self._endpoints.comment(comment_id), headers=self._authorization_header, request_type=RequestType.DELETE) - return 'notice' in response + return Utils.validate_return_data(response) - def anime_constants(self) -> AnimeConstants: + def anime_constants(self) -> Optional[AnimeConstants]: """ Returns anime constants values. :return: Anime constants values - :rtype: AnimeConstants + :rtype: Optional[AnimeConstants] """ logger.debug('Executing "/api/constants/anime" method') response: Dict[str, Any] = self._request(self._endpoints.anime_constants) - return AnimeConstants(**response) + return Utils.validate_return_data(response, data_model=AnimeConstants) - def manga_constants(self) -> MangaConstants: + def manga_constants(self) -> Optional[MangaConstants]: """ Returns manga constants values. :return: Manga constants values - :rtype: MangaConstants + :rtype: Optional[MangaConstants] """ logger.debug('Executing "/api/constants/manga" method') response: Dict[str, Any] = self._request(self._endpoints.manga_constants) - return MangaConstants(**response) + return Utils.validate_return_data(response, data_model=MangaConstants) - def user_rate_constants(self) -> UserRateConstants: + def user_rate_constants(self) -> Optional[UserRateConstants]: """ Returns user rate constants values. :return: User rate constants values - :rtype: UserRateConstants + :rtype: Optional[UserRateConstants] """ logger.debug('Executing "/api/constants/user_rate" method') response: Dict[str, Any] = self._request(self._endpoints.user_rate_constants) - return UserRateConstants(**response) + return Utils.validate_return_data(response, + data_model=UserRateConstants) - def club_constants(self) -> ClubConstants: + def club_constants(self) -> Optional[ClubConstants]: """ Returns club constants values. :return: Club constants values - :rtype: ClubConstants + :rtype: Optional[ClubConstants] """ logger.debug('Executing "/api/constants/club" method') response: Dict[str, Any] = self._request(self._endpoints.club_constants) - return ClubConstants(**response) + return Utils.validate_return_data(response, data_model=ClubConstants) def smileys_constants(self) -> Optional[List[SmileyConstants]]: """ Returns list of smileys constants values. - :return: List of smileys constants values or None if list is empty + :return: List of smileys constants values :rtype: Optional[List[SmileyConstants]] """ logger.debug('Executing "/api/constants/smileys" method') response: List[Dict[str, Any]] = self._request( self._endpoints.smileys_constants) - if not response: - return None - return [SmileyConstants(**smiley) for smiley in response] + return Utils.validate_return_data(response, data_model=SmileyConstants) @protected_method(scope='messages') def dialogs(self) -> Optional[List[Dialog]]: """ Returns list of current user's dialogs. - :return: List of dialogs or None if list is empty + :return: List of dialogs :rtype: Optional[List[Dialog]] """ logger.debug('Executing "/api/dialogs" method') response: List[Dict[str, Any]] = self._request( self._endpoints.dialogs, headers=self._authorization_header) - if not response: - return None - return [Dialog(**dialog) for dialog in response] + return Utils.validate_return_data(response, data_model=Dialog) @protected_method(scope='messages') def dialog(self, user_id: Union[int, str]) -> Optional[List[Message]]: @@ -1540,15 +1501,13 @@ def dialog(self, user_id: Union[int, str]) -> Optional[List[Message]]: :param user_id: ID/Nickname of the user to get dialog :type user_id: Union[int, str] - :return: List of messages or None if list is empty + :return: List of messages :rtype: Optional[List[Message]] """ logger.debug('Executing "/api/dialogs/:id" method') response: List[Dict[str, Any]] = self._request( self._endpoints.dialog(user_id), headers=self._authorization_header) - if not response: - return None - return [Message(**message) for message in response] + return Utils.validate_return_data(response, data_model=Message) @protected_method(scope='messages') def delete_dialog(self, user_id: Union[int, str]) -> bool: @@ -1566,18 +1525,18 @@ def delete_dialog(self, user_id: Union[int, str]) -> bool: self._endpoints.dialog(user_id), headers=self._authorization_header, request_type=RequestType.DELETE) - return 'notice' in response + return Utils.validate_return_data(response) @protected_method() def create_favorite(self, - linked_type: LinkedType, + linked_type: FavoriteLinkedType, linked_id: int, kind: PersonKind = PersonKind.NONE) -> bool: """ Creates a favorite. :param linked_type: Type of object for making favorite - :type linked_type: LinkedType + :type linked_type: FavoriteLinkedType :param linked_id: ID of linked type :type linked_id: int @@ -1597,15 +1556,16 @@ def create_favorite(self, linked_type, linked_id, kind), headers=self._authorization_header, request_type=RequestType.POST) - return 'success' in response + return Utils.validate_return_data(response) @protected_method() - def destroy_favorite(self, linked_type: LinkedType, linked_id: int) -> bool: + def destroy_favorite(self, linked_type: FavoriteLinkedType, + linked_id: int) -> bool: """ Destroys a favorite. :param linked_type: Type of object for destroying from favorite - :type linked_type: LinkedType + :type linked_type: FavoriteLinkedType :param linked_id: ID of linked type :type linked_id: int @@ -1621,7 +1581,7 @@ def destroy_favorite(self, linked_type: LinkedType, linked_id: int) -> bool: linked_type, linked_id), headers=self._authorization_header, request_type=RequestType.DELETE) - return 'success' in response + return Utils.validate_return_data(response) @protected_method() def reorder_favorite(self, @@ -1645,32 +1605,25 @@ def reorder_favorite(self, headers=self._authorization_header, query=Utils.generate_query_dict(new_index=new_index), request_type=RequestType.POST) - if isinstance(response, int): - return response == ResponseCode.SUCCESS.value - return False + return Utils.validate_return_data(response, + response_code=ResponseCode.SUCCESS) def forums(self) -> Optional[List[Forum]]: """ Returns list of forums. - :returns: List of forums or None if list is empty + :returns: List of forums :rtype: Optional[List[Forum]] """ logger.debug('Executing "/api/forums" method') response: List[Dict[str, Any]] = self._request(self._endpoints.forums) - if not response: - return None - return [Forum(**forum) for forum in response] + return Utils.validate_return_data(response, data_model=Forum) @protected_method(scope='friends') def create_friend(self, friend_id: int): """ Creates (adds) new friend by ID. - Although it is possible to use this method - with user nicknames, I recommend to pass user ID - (In this case there is an additional check for string) - :param friend_id: ID of a friend to create (add) :type friend_id: int @@ -1678,27 +1631,17 @@ def create_friend(self, friend_id: int): :rtype: bool """ logger.debug('Executing "/api/friends/:id" method') - - if isinstance(friend_id, str): - logger.debug( - 'Adding to friends was canceled because a string was passed') - return False - response: Union[Dict[str, Any], int] = self._request(self._endpoints.friend(friend_id), headers=self._authorization_header, request_type=RequestType.POST) - return 'notice' in response + return Utils.validate_return_data(response) @protected_method(scope='friends') def destroy_friend(self, friend_id: int): """ Destroys (removes) current friend by ID. - Although it is possible to use this method - with user nicknames, I recommend to pass user ID - (In this case there is an additional check for string) - :param friend_id: ID of a friend to destroy (remove) :type friend_id: int @@ -1706,30 +1649,22 @@ def destroy_friend(self, friend_id: int): :rtype: bool """ logger.debug('Executing "/api/friends/:id" method') - - if isinstance(friend_id, str): - logger.debug( - 'Deletion from friends canceled because a string was passed') - return False - response: Union[Dict[str, Any], int] = self._request(self._endpoints.friend(friend_id), headers=self._authorization_header, request_type=RequestType.DELETE) - return 'notice' in response + return Utils.validate_return_data(response) def genres(self) -> Optional[List[Genre]]: """ Returns list of genres. - :return: List of genres or None if list is empty + :return: List of genres :rtype: Optional[List[Genre]] """ logger.debug('Executing "/api/genres" method') response: List[Dict[str, Any]] = self._request(self._endpoints.genres) - if not response: - return None - return [Genre(**genre) for genre in response] + return Utils.validate_return_data(response, data_model=Genre) def mangas(self, page: Optional[int] = None, @@ -1797,7 +1732,7 @@ def mangas(self, :param search: Search phrase to filter mangas by name :type search: Optional[str] - :return: List of Mangas or None if list is empty + :return: List of Mangas :rtype: Optional[List[Manga]] """ logger.debug('Executing "/api/mangas" method') @@ -1828,11 +1763,9 @@ def mangas(self, ids=ids, exclude_ids=exclude_ids, search=search)) - if not response: - return None - return [Manga(**manga) for manga in response] + return Utils.validate_return_data(response, data_model=Manga) - def manga(self, manga_id: int) -> Manga: + def manga(self, manga_id: int) -> Optional[Manga]: """ Returns info about certain manga. @@ -1840,12 +1773,12 @@ def manga(self, manga_id: int) -> Manga: :type manga_id: int :return: Manga info - :rtype: Manga + :rtype: Optional[Manga] """ logger.debug('Executing "/api/mangas/:id" method') response: Dict[str, Any] = self._request(self._endpoints.manga(manga_id)) - return Manga(**response) + return Utils.validate_return_data(response, data_model=Manga) def manga_creators(self, manga_id: int) -> Optional[List[Creator]]: """ @@ -1854,15 +1787,13 @@ def manga_creators(self, manga_id: int) -> Optional[List[Creator]]: :param manga_id: Manga ID to get creators :type manga_id: int - :return: List of manga creators or None if list is empty + :return: List of manga creators :rtype: Optional[List[Creator]] """ logger.debug('Executing "/api/mangas/:id/roles" method') response: List[Dict[str, Any]] = self._request( self._endpoints.manga_roles(manga_id)) - if not response: - return None - return [Creator(**creator) for creator in response] + return Utils.validate_return_data(response, data_model=Creator) def similar_mangas(self, manga_id: int) -> Optional[List[Manga]]: """ @@ -1871,15 +1802,13 @@ def similar_mangas(self, manga_id: int) -> Optional[List[Manga]]: :param manga_id: Manga ID to get similar mangas :type manga_id: int - :return: List of similar mangas or None if list is empty + :return: List of similar mangas :rtype: Optional[List[Manga]] """ logger.debug('Executing "/api/mangas/:id/similar" method') response: List[Dict[str, Any]] = self._request( self._endpoints.similar_mangas(manga_id)) - if not response: - return None - return [Manga(**manga) for manga in response] + return Utils.validate_return_data(response, data_model=Manga) def manga_related_content(self, manga_id: int) -> Optional[List[Relation]]: """ @@ -1888,17 +1817,15 @@ def manga_related_content(self, manga_id: int) -> Optional[List[Relation]]: :param manga_id: Manga ID to get related content :type manga_id: int - :return: List of relations or None if list is empty + :return: List of relations :rtype: Optional[List[Relation]] """ logger.debug('Executing "/api/mangas/:id/related" method') response: List[Dict[str, Any]] = self._request( self._endpoints.manga_related_content(manga_id)) - if not response: - return None - return [Relation(**relation) for relation in response] + return Utils.validate_return_data(response, data_model=Relation) - def manga_franchise_tree(self, manga_id: int) -> FranchiseTree: + def manga_franchise_tree(self, manga_id: int) -> Optional[FranchiseTree]: """ Returns franchise tree of certain manga. @@ -1906,12 +1833,12 @@ def manga_franchise_tree(self, manga_id: int) -> FranchiseTree: :type manga_id: int :return: Franchise tree of certain manga - :rtype: FranchiseTree + :rtype: Optional[FranchiseTree] """ logger.debug('Executing "/api/mangas/:id/franchise" method') response: Dict[str, Any] = self._request( self._endpoints.manga_franchise_tree(manga_id)) - return FranchiseTree(**response) + return Utils.validate_return_data(response, data_model=FranchiseTree) def manga_external_links(self, manga_id: int) -> Optional[List[Link]]: """ @@ -1920,15 +1847,13 @@ def manga_external_links(self, manga_id: int) -> Optional[List[Link]]: :param manga_id: Manga ID to get external links :type manga_id: int - :return: List of external links or None if list is empty + :return: List of external links :rtype: Optional[List[Link]] """ logger.debug('Executing "/api/mangas/:id/external_links" method') response: List[Dict[str, Any]] = self._request( self._endpoints.manga_external_links(manga_id)) - if not response: - return None - return [Link(**link) for link in response] + return Utils.validate_return_data(response, data_model=Link) def manga_topics(self, manga_id: int, @@ -1948,7 +1873,7 @@ def manga_topics(self, :param limit: Number of results limit :type limit: Optional[int] - :return: List of topics or None if list is empty + :return: List of topics :rtype: Optional[List[Topic]] """ logger.debug('Executing "/api/mangas/:id/topics" method') @@ -1961,12 +1886,10 @@ def manga_topics(self, self._endpoints.manga_topics(manga_id), query=Utils.generate_query_dict(page=validated_numbers['page'], limit=validated_numbers['limit'])) - if not response: - return None - return [Topic(**topic) for topic in response] + return Utils.validate_return_data(response, data_model=Topic) @protected_method(scope='messages') - def message(self, message_id) -> Message: + def message(self, message_id) -> Optional[Message]: """ Returns message info. @@ -1974,13 +1897,13 @@ def message(self, message_id) -> Message: :type message_id: int :return: Message info - :rtype: Message + :rtype: Optional[Message] """ logger.debug('Executing "/api/messages/:id" method') response: Dict[str, Any] = self._request(self._endpoints.message(message_id), headers=self._authorization_header) - return Message(**response) + return Utils.validate_return_data(response, data_model=Message) @protected_method(scope='messages') def create_message(self, body: str, from_id: int, @@ -1997,7 +1920,7 @@ def create_message(self, body: str, from_id: int, :param to_id: Reciver ID :type to_id: int - :return: Created message info or None if an error occurred + :return: Created message info :rtype: Optional[Message] """ logger.debug('Executing "/api/messages" method') @@ -2010,7 +1933,7 @@ def create_message(self, body: str, from_id: int, kind='Private', to_id=to_id), request_type=RequestType.POST) - return Message(**response) if 'errors' not in response else None + return Utils.validate_return_data(response, data_model=Message) @protected_method(scope='messages') def update_message(self, message_id: int, body: str) -> Optional[Message]: @@ -2023,7 +1946,7 @@ def update_message(self, message_id: int, body: str) -> Optional[Message]: :param body: New body of message :type body: str - :return: Updated message info or None if an error occurred + :return: Updated message info :rtype: Optional[Message] """ logger.debug('Executing "/api/messages/:id" method') @@ -2032,7 +1955,7 @@ def update_message(self, message_id: int, body: str) -> Optional[Message]: headers=self._authorization_header, data=Utils.generate_data_dict(dict_name='message', body=body), request_type=RequestType.PATCH) - return Message(**response) if 'errors' not in response else None + return Utils.validate_return_data(response, data_model=Message) @protected_method(scope='messages') def delete_message(self, message_id: int) -> bool: @@ -2050,9 +1973,8 @@ def delete_message(self, message_id: int) -> bool: self._endpoints.message(message_id), headers=self._authorization_header, request_type=RequestType.DELETE) - if isinstance(response, int): - return response == ResponseCode.NO_CONTENT.value - return False + return Utils.validate_return_data(response, + response_code=ResponseCode.NO_CONTENT) @protected_method(scope='messages') def mark_messages_read(self, @@ -2079,9 +2001,8 @@ def mark_messages_read(self, headers=self._authorization_header, data=Utils.generate_query_dict(ids=message_ids, is_read=is_read), request_type=RequestType.POST) - if isinstance(response, int): - return response == ResponseCode.SUCCESS.value - return False + return Utils.validate_return_data(response, + response_code=ResponseCode.SUCCESS) @protected_method(scope='messages') def read_all_messages(self, message_type: MessageType) -> bool: @@ -2106,9 +2027,8 @@ def read_all_messages(self, message_type: MessageType) -> bool: headers=self._authorization_header, data=Utils.generate_query_dict(type=message_type), request_type=RequestType.POST) - if isinstance(response, int): - return response == ResponseCode.SUCCESS.value - return False + return Utils.validate_return_data(response, + response_code=ResponseCode.SUCCESS) @protected_method(scope='messages') def delete_all_messages(self, message_type: MessageType) -> bool: @@ -2133,11 +2053,10 @@ def delete_all_messages(self, message_type: MessageType) -> bool: headers=self._authorization_header, data=Utils.generate_query_dict(type=message_type), request_type=RequestType.POST) - if isinstance(response, int): - return response == ResponseCode.SUCCESS.value - return False + return Utils.validate_return_data(response, + response_code=ResponseCode.SUCCESS) - def people(self, people_id: int) -> People: + def people(self, people_id: int) -> Optional[People]: """ Returns info about a person. @@ -2145,12 +2064,12 @@ def people(self, people_id: int) -> People: :type people_id: int :return: Info about a person - :rtype: People + :rtype: Optional[People] """ logger.debug('Executing "/api/people/:id" method') response: Dict[str, Any] = self._request(self._endpoints.people(people_id)) - return People(**response) + return Utils.validate_return_data(response, data_model=People) def people_search( self, @@ -2168,30 +2087,26 @@ def people_search( :param people_kind: Kind of person for searching :type people_kind: Optional[PersonKind] - :return: List of found persons or None if list is empty + :return: List of found persons :rtype: Optional[List[People]] """ logger.debug('Executing "/api/people/search" method') response: List[Dict[str, Any]] = self._request( self._endpoints.people_search, query=Utils.generate_query_dict(search=search, kind=people_kind)) - if not response: - return None - return [People(**people) for people in response] + return Utils.validate_return_data(response, data_model=People) def publishers(self) -> Optional[List[Publisher]]: """ Returns list of publishers. - :return: List of publishers or None if list is empty + :return: List of publishers :rtype: Optional[List[Publisher]] """ logger.debug('Executing "/api/publishers" method') response: List[Dict[str, Any]] = self._request(self._endpoints.publishers) - if not response: - return None - return [Publisher(**publisher) for publisher in response] + return Utils.validate_return_data(response, data_model=Publisher) def ranobes(self, page: Optional[int] = None, @@ -2256,7 +2171,7 @@ def ranobes(self, :param search: Search phrase to filter ranobe by name :type search: Optional[str] - :return: List of Ranobe or None if list is empty + :return: List of Ranobe :rtype: Optional[List[Ranobe]] """ logger.debug('Executing "/api/ranobe" method') @@ -2286,11 +2201,9 @@ def ranobes(self, ids=ids, exclude_ids=exclude_ids, search=search)) - if not response: - return None - return [Ranobe(**ranobe) for ranobe in response] + return Utils.validate_return_data(response, data_model=Ranobe) - def ranobe(self, ranobe_id: int) -> Ranobe: + def ranobe(self, ranobe_id: int) -> Optional[Ranobe]: """ Returns info about certain ranobe. @@ -2298,12 +2211,12 @@ def ranobe(self, ranobe_id: int) -> Ranobe: :type ranobe_id: int :return: Ranobe info - :rtype: Ranobe + :rtype: Optional[Ranobe] """ logger.debug('Executing "/api/ranobe/:id" method') response: Dict[str, Any] = self._request(self._endpoints.ranobe(ranobe_id)) - return Ranobe(**response) + return Utils.validate_return_data(response, data_model=Ranobe) def ranobe_creators(self, ranobe_id: int) -> Optional[List[Creator]]: """ @@ -2312,15 +2225,13 @@ def ranobe_creators(self, ranobe_id: int) -> Optional[List[Creator]]: :param ranobe_id: Ranobe ID to get creators :type ranobe_id: int - :return: List of ranobe creators or None if list is empty + :return: List of ranobe creators :rtype: Optional[List[Creator]] """ logger.debug('Executing "/api/ranobe/:id/roles" method') response: List[Dict[str, Any]] = self._request( self._endpoints.ranobe_roles(ranobe_id)) - if not response: - return None - return [Creator(**creator) for creator in response] + return Utils.validate_return_data(response, data_model=Creator) def similar_ranobes(self, ranobe_id: int) -> Optional[List[Ranobe]]: """ @@ -2329,15 +2240,13 @@ def similar_ranobes(self, ranobe_id: int) -> Optional[List[Ranobe]]: :param ranobe_id: Ranobe ID to get similar ranobes :type ranobe_id: int - :return: List of similar ranobes or None if list is empty + :return: List of similar ranobes :rtype: Optional[List[Ranobe]] """ logger.debug('Executing "/api/ranobe/:id/similar" method') response: List[Dict[str, Any]] = self._request( self._endpoints.similar_ranobes(ranobe_id)) - if not response: - return None - return [Ranobe(**ranobe) for ranobe in response] + return Utils.validate_return_data(response, data_model=Ranobe) def ranobe_related_content(self, ranobe_id: int) -> Optional[List[Relation]]: @@ -2347,17 +2256,15 @@ def ranobe_related_content(self, :param ranobe_id: Ranobe ID to get related content :type ranobe_id: int - :return: List of relations or None if list is empty + :return: List of relations :rtype: Optional[List[Relation]] """ logger.debug('Executing "/api/ranobe/:id/related" method') response: List[Dict[str, Any]] = self._request( self._endpoints.ranobe_related_content(ranobe_id)) - if not response: - return None - return [Relation(**relation) for relation in response] + return Utils.validate_return_data(response, data_model=Relation) - def ranobe_franchise_tree(self, ranobe_id: int) -> FranchiseTree: + def ranobe_franchise_tree(self, ranobe_id: int) -> Optional[FranchiseTree]: """ Returns franchise tree of certain ranobe. @@ -2365,12 +2272,12 @@ def ranobe_franchise_tree(self, ranobe_id: int) -> FranchiseTree: :type ranobe_id: int :return: Franchise tree of certain ranobe - :rtype: FranchiseTree + :rtype: Optional[FranchiseTree] """ logger.debug('Executing "/api/ranobe/:id/franchise" method') response: Dict[str, Any] = self._request( self._endpoints.ranobe_franchise_tree(ranobe_id)) - return FranchiseTree(**response) + return Utils.validate_return_data(response, data_model=FranchiseTree) def ranobe_external_links(self, ranobe_id: int) -> Optional[List[Link]]: """ @@ -2379,15 +2286,13 @@ def ranobe_external_links(self, ranobe_id: int) -> Optional[List[Link]]: :param ranobe_id: Ranobe ID to get external links :type ranobe_id: int - :return: List of external links or None if list is empty + :return: List of external links :rtype: Optional[List[Link]] """ logger.debug('Executing "/api/ranobe/:id/external_links" method') response: List[Dict[str, Any]] = self._request( self._endpoints.ranobe_external_links(ranobe_id)) - if not response: - return None - return [Link(**link) for link in response] + return Utils.validate_return_data(response, data_model=Link) def ranobe_topics(self, ranobe_id: int, @@ -2407,7 +2312,7 @@ def ranobe_topics(self, :param limit: Number of results limit :type limit: Optional[int] - :return: List of topics or None if list is empty + :return: List of topics :rtype: Optional[List[Topic]] """ logger.debug('Executing "/api/ranobe/:id/topics" method') @@ -2420,35 +2325,29 @@ def ranobe_topics(self, self._endpoints.ranobe_topics(ranobe_id), query=Utils.generate_query_dict(page=validated_numbers['page'], limit=validated_numbers['limit'])) - if not response: - return None - return [Topic(**topic) for topic in response] + return Utils.validate_return_data(response, data_model=Topic) def active_users(self) -> Optional[List[int]]: """ Returns list of IDs of active users. - :return: List of IDs of active users or None if list is empty + :return: List of IDs of active users :rtype: Optional[List[int]] """ logger.debug('Executing "/api/stats/active_users" method') response: List[int] = self._request(self._endpoints.active_users) - if not response: - return None - return response + return Utils.validate_return_data(response) def studios(self) -> Optional[List[Studio]]: """ Returns list of studios. - :return: List of studios or None if list is empty + :return: List of studios :rtype: Optional[List[Studio]] """ logger.debug('Executing "/api/studios" method') response: List[Dict[str, Any]] = self._request(self._endpoints.studios) - if not response: - return None - return [Studio(**studio) for studio in response] + return Utils.validate_return_data(response, data_model=Studio) def style(self, style_id: int) -> Optional[Style]: """ @@ -2457,15 +2356,13 @@ def style(self, style_id: int) -> Optional[Style]: :param style_id: Style ID to get info :type style_id: int - :return: Info about style or None if there is an error + :return: Info about style :rtype: Optional[Style] """ logger.debug('Executing "/api/styles/:id" method') response: Dict[str, Any] = self._request(self._endpoints.style(style_id)) - if not response or 'code' in response: - return None - return Style(**response) + return Utils.validate_return_data(response, data_model=Style) @protected_method() def preview_style(self, css: str) -> Optional[Style]: @@ -2475,7 +2372,7 @@ def preview_style(self, css: str) -> Optional[Style]: :param css: CSS code to preview :type css: str - :return: Info about previewed style or None if there is an error + :return: Info about previewed style :rtype: Optional[Style] """ logger.debug('Executing "/api/styles/preview" method') @@ -2484,7 +2381,7 @@ def preview_style(self, css: str) -> Optional[Style]: headers=self._authorization_header, data=Utils.generate_data_dict(dict_name='style', css=css), request_type=RequestType.POST) - return Style(**response) if 'errors' not in response else None + return Utils.validate_return_data(response, data_model=Style) @protected_method() def create_style(self, css: str, name: str, owner_id: int, @@ -2504,7 +2401,7 @@ def create_style(self, css: str, name: str, owner_id: int, :param owner_type: Type of owner (User/Club) :type owner_type: OwnerType - :return: Info about previewed style or None if there is an error + :return: Info about previewed style :rtype: Optional[Style] """ logger.debug('Executing "/api/styles" method') @@ -2517,7 +2414,7 @@ def create_style(self, css: str, name: str, owner_id: int, owner_id=owner_id, owner_type=owner_type), request_type=RequestType.POST) - return Style(**response) if 'errors' not in response else None + return Utils.validate_return_data(response, data_model=Style) @protected_method() def update_style(self, style_id: int, css: Optional[str], @@ -2534,7 +2431,7 @@ def update_style(self, style_id: int, css: Optional[str], :param name: New style name :type name: Optional[str] - :return: Info about updated style or None if there is an error + :return: Info about updated style :rtype: Optional[Style] """ logger.debug('Executing "/api/styles/:id" method') @@ -2544,7 +2441,222 @@ def update_style(self, style_id: int, css: Optional[str], data=Utils.generate_data_dict(dict_name='style', css=css, name=name), request_type=RequestType.PATCH) - return Style(**response) if 'errors' not in response else None + return Utils.validate_return_data(response, data_model=Style) + + def topics( + self, + page: Optional[int] = None, + limit: Optional[int] = None, + forum: Optional[ForumType] = None, + linked_id: Optional[int] = None, + linked_type: Optional[TopicLinkedType] = None, + topic_type: Optional[Union[TopicsType, EntryTopics, NewsTopics]] = None + ) -> Optional[List[Topic]]: + """ + Returns list of topics. + + :param page: Number of page + :type page: Optional[int] + + :param limit: Number of results limit + :type limit: Optional[int] + + :param forum: Number of results limit + :type forum: Optional[ForumType] + + :param linked_id: ID of linked topic (Used together with linked_type) + :type linked_id: Optional[int] + + :param linked_type: Type of linked topic (Used together with linked_id) + :type linked_type: Optional[TopicLinkedType] + + :param topic_type: Optional[Union[TopicsType, EntryTopics, NewsTopics]] + :type topic_type: Optional[int] + + :return: List of topics + :rtype: Optional[List[Topic]] + """ + logger.debug('Executing "/api/topics" method') + validated_numbers = Utils.query_numbers_validator( + page=[page, 100000], + limit=[limit, 30], + ) + response: List[Dict[str, Any]] = self._request( + self._endpoints.topics, + query=Utils.generate_query_dict(page=validated_numbers['page'], + limit=validated_numbers['limit'], + forum=forum, + linked_id=linked_id, + linked_type=linked_type, + type=topic_type)) + return Utils.validate_return_data(response, data_model=Topic) + + def updates_topics(self, + page: Optional[int] = None, + limit: Optional[int] = None) -> Optional[List[Topic]]: + """ + Returns list of NewsTopics about database updates. + + :param page: Number of page + :type page: Optional[int] + + :param limit: Number of results limit + :type limit: Optional[int] + + :return: List of topics + :rtype: Optional[List[Topic]] + """ + logger.debug('Executing "/api/topics/updates" method') + validated_numbers = Utils.query_numbers_validator( + page=[page, 100000], + limit=[limit, 30], + ) + response: List[Dict[str, Any]] = self._request( + self._endpoints.updates_topics, + query=Utils.generate_query_dict( + page=validated_numbers['page'], + limit=validated_numbers['limit'], + )) + return Utils.validate_return_data(response, data_model=Topic) + + def hot_topics(self, limit: Optional[int] = None) -> Optional[List[Topic]]: + """ + Returns list of hot topics. + + :param limit: Number of results limit + :type limit: Optional[int] + + :return: List of topics + :rtype: Optional[List[Topic]] + """ + logger.debug('Executing "/api/topics/hot" method') + validated_numbers = Utils.query_numbers_validator(limit=[limit, 10],) + response: List[Dict[str, Any]] = self._request( + self._endpoints.hot_topics, + query=Utils.generate_query_dict(limit=validated_numbers['limit'],)) + return Utils.validate_return_data(response, data_model=Topic) + + def topic(self, topic_id: int) -> Optional[Topic]: + """ + Returns info about topic. + + :param topic_id: ID of topic to get + :type topic_id: int + + :return: Info about topic + :rtype: Optional[Topic] + """ + logger.debug('Executing "/api/topics/:id" method') + response: Dict[str, + Any] = self._request(self._endpoints.topic(topic_id)) + return Utils.validate_return_data(response, data_model=Topic) + + @protected_method(scope='topics') + def create_topic( + self, + body: str, + forum_id: int, + title: str, + user_id: int, + linked_id: Optional[int] = None, + linked_type: Optional[TopicLinkedType] = None) -> Optional[Topic]: + """ + Creates topic. + + :param body: Body of topic + :type body: str + + :param forum_id: ID of forum to post + :type forum_id: int + + :param title: Title of topic + :type title: str + + :param user_id: ID of topic creator + :type user_id: int + + :param linked_id: ID of linked topic (Used together with linked_type) + :type linked_type: Optional[int] + + :param linked_type: Type of linked topic (Used together with linked_id) + :type linked_type: Optional[TopicLinkedType] + + :return: Created topic info + :rtype: Optional[Topic] + """ + logger.debug('Executing "/api/topics" method') + response: Dict[str, Any] = self._request( + self._endpoints.topics, + headers=self._authorization_header, + data=Utils.generate_data_dict(dict_name='topic', + body=body, + forum_id=forum_id, + linked_id=linked_id, + linked_type=linked_type, + title=title, + type=TopicsType.REGULAR_TOPIC, + user_id=user_id), + request_type=RequestType.POST) + return Utils.validate_return_data(response, data_model=Topic) + + @protected_method(scope='topics') + def update_topic( + self, + topic_id: int, + body: str, + title: str, + linked_id: Optional[int] = None, + linked_type: Optional[TopicLinkedType] = None) -> Optional[Topic]: + """ + Updated topic. + + :param topic_id: ID of topic to update + :type topic_id: int + + :param body: Body of topic + :type body: str + + :param title: Title of topic + :type title: str + + :param linked_id: ID of linked topic (Used together with linked_type) + :type linked_type: Optional[int] + + :param linked_type: Type of linked topic (Used together with linked_id) + :type linked_type: Optional[TopicLinkedType] + + :return: Updated topic info + :rtype: Optional[Topic] + """ + logger.debug('Executing "/api/topics/:id" method') + response: Dict[str, Any] = self._request( + self._endpoints.topic(topic_id), + headers=self._authorization_header, + data=Utils.generate_data_dict(dict_name='topic', + body=body, + linked_id=linked_id, + linked_type=linked_type, + title=title), + request_type=RequestType.PATCH) + return Utils.validate_return_data(response, data_model=Topic) + + @protected_method(scope='topics') + def delete_topic(self, topic_id: int) -> Optional[bool]: + """ + Deletes topic. + + :param topic_id: ID of topic to delete + :type topic_id: int + + :return: Status of topic deletion + :rtype: bool + """ + logger.debug('Executing "/api/topics/:id" method') + response: Union[Dict[str, Any], + int] = self._request(self._endpoints.topic(topic_id), + headers=self._authorization_header, + request_type=RequestType.DELETE) + return Utils.validate_return_data(response) def users(self, page: Optional[int] = None, @@ -2558,7 +2670,7 @@ def users(self, :param limit: Number of results limit :type limit: Optional[int] - :return: List of users or None if list is empty + :return: List of users :rtype: Optional[List[User]] """ logger.debug('Executing "/api/users" method') @@ -2571,13 +2683,11 @@ def users(self, self._endpoints.users, query=Utils.generate_query_dict(page=validated_numbers['page'], limit=validated_numbers['limit'])) - if not response: - return None - return [User(**user) for user in response] + return Utils.validate_return_data(response, data_model=User) def user(self, user_id: Union[str, int], - is_nickname: Optional[bool] = None) -> User: + is_nickname: Optional[bool] = None) -> Optional[User]: """ Returns info about user. @@ -2588,17 +2698,17 @@ def user(self, :type is_nickname: Optional[bool] :return: Info about user - :rtype: User + :rtype: Optional[User] """ logger.debug('Executing "/api/users/:id" method') response: Dict[str, Any] = self._request( self._endpoints.user(user_id), query=Utils.generate_query_dict(is_nickname=is_nickname)) - return User(**response) + return Utils.validate_return_data(response, data_model=User) def user_info(self, user_id: Union[str, int], - is_nickname: Optional[bool] = None) -> User: + is_nickname: Optional[bool] = None) -> Optional[User]: """ Returns user's brief info. @@ -2609,29 +2719,29 @@ def user_info(self, :type is_nickname: Optional[bool] :return: User's brief info - :rtype: User + :rtype: Optional[User] """ logger.debug('Executing "/api/users"/:id/info method') response: Dict[str, Any] = self._request( self._endpoints.user_info(user_id), query=Utils.generate_query_dict(is_nickname=is_nickname)) - return User(**response) + return Utils.validate_return_data(response, data_model=User) @protected_method() - def current_user(self) -> User: + def current_user(self) -> Optional[User]: """ Returns brief info about current user. Current user evaluated depending on authorization code. :return: Current user brief info - :rtype: User + :rtype: Optional[User] """ logger.debug('Executing "/api/users/whoami" method') response: Dict[str, Any] = self._request(self._endpoints.whoami, headers=self._authorization_header) - return User(**response) + return Utils.validate_return_data(response, data_model=User) @protected_method() def user_sign_out(self): @@ -2653,16 +2763,14 @@ def user_friends( :param is_nickname: Specify if passed user_id is nickname :type is_nickname: Optional[bool] - :return: List of user's friends or None if list is empty + :return: List of user's friends :rtype: Optional[List[User]] """ logger.debug('Executing "/api/users/:id/friends" method') response: List[Dict[str, Any]] = self._request( self._endpoints.user_friends(user_id), query=Utils.generate_query_dict(is_nickname=is_nickname)) - if not response: - return None - return [User(**friend) for friend in response] + return Utils.validate_return_data(response, data_model=User) def user_clubs(self, user_id: Union[int, str], @@ -2676,16 +2784,14 @@ def user_clubs(self, :param is_nickname: Specify if passed user_id is nickname :type is_nickname: Optional[bool] - :return: List of user's clubs or None if list is empty + :return: List of user's clubs :rtype: Optional[List[Club]] """ logger.debug('Executing "/api/users/:id/clubs" method') response: List[Dict[str, Any]] = self._request( self._endpoints.user_clubs(user_id), query=Utils.generate_query_dict(is_nickname=is_nickname)) - if not response: - return None - return [Club(**club) for club in response] + return Utils.validate_return_data(response, data_model=Club) def user_anime_rates( self, @@ -2717,7 +2823,7 @@ def user_anime_rates( :param censored: Type of anime censorship :type censored: Optional[AnimeCensorship] - :return: User's anime list or None if list is empty + :return: User's anime list :rtype: Optional[List[UserList]] """ logger.debug('Executing "/api/users/:id/anime_rates" method') @@ -2733,9 +2839,7 @@ def user_anime_rates( limit=validated_numbers['limit'], status=status, censored=censored)) - if not response: - return None - return [UserList(**user_list) for user_list in response] + return Utils.validate_return_data(response, data_model=UserList) def user_manga_rates( self, @@ -2763,7 +2867,7 @@ def user_manga_rates( :param censored: Type of manga censorship :type censored: Optional[AnimeCensorship] - :return: User's manga list or None if list is empty + :return: User's manga list :rtype: Optional[List[UserList]] """ logger.debug('Executing "/api/users/:id/manga_rates" method') @@ -2778,13 +2882,12 @@ def user_manga_rates( page=validated_numbers['page'], limit=validated_numbers['limit'], censored=censored)) - if not response: - return None - return [UserList(**user_list) for user_list in response] + return Utils.validate_return_data(response, data_model=UserList) - def user_favourites(self, - user_id: Union[int, str], - is_nickname: Optional[bool] = None) -> Favourites: + def user_favourites( + self, + user_id: Union[int, str], + is_nickname: Optional[bool] = None) -> Optional[Favourites]: """ Returns user's favourites. @@ -2795,13 +2898,13 @@ def user_favourites(self, :type is_nickname: Optional[bool] :return: User's favourites - :rtype: Favourites + :rtype: Optional[Favourites] """ logger.debug('Executing "/api/users/:id/favourites" method') response: Dict[str, Any] = self._request( self._endpoints.user_favourites(user_id), query=Utils.generate_query_dict(is_nickname=is_nickname)) - return Favourites(**response) + return Utils.validate_return_data(response, data_model=Favourites) @protected_method(scope='messages') def current_user_messages( @@ -2830,7 +2933,7 @@ def current_user_messages( :param message_type: Type of message :type message_type: MessageType - :return: Current user's messages or None if list is empty + :return: Current user's messages :rtype: Optional[List[Message]] """ logger.debug('Executing "/api/users/:id/messages" method') @@ -2846,15 +2949,13 @@ def current_user_messages( page=validated_numbers['page'], limit=validated_numbers['limit'], type=message_type)) - if not response: - return None - return [Message(**message) for message in response] + return Utils.validate_return_data(response, data_model=Message) @protected_method(scope='messages') def current_user_unread_messages( self, user_id: Union[int, str], - is_nickname: Optional[bool] = None) -> UnreadMessages: + is_nickname: Optional[bool] = None) -> Optional[UnreadMessages]: """ Returns current user's unread messages counter. @@ -2865,14 +2966,14 @@ def current_user_unread_messages( :type is_nickname: Optional[bool] :return: Current user's unread messages counters - :rtype: UnreadMessages + :rtype: Optional[UnreadMessages] """ logger.debug('Executing "/api/users/:id/unread_messages" method') response: Dict[str, Any] = self._request( self._endpoints.user_unread_messages(user_id), headers=self._authorization_header, query=Utils.generate_query_dict(is_nickname=is_nickname)) - return UnreadMessages(**response) + return Utils.validate_return_data(response, data_model=UnreadMessages) def user_history( self, @@ -2904,7 +3005,7 @@ def user_history( :param target_type: Type of target (Anime/Manga) :type target_type: Optional[TargetType] - :return: User's history or None if list is empty + :return: User's history :rtype: Optional[List[History]] """ logger.debug('Executing "/api/users/:id/history" method') @@ -2920,9 +3021,7 @@ def user_history( limit=validated_numbers['limit'], target_id=target_id, target_type=target_type)) - if not response: - return None - return [History(**history) for history in response] + return Utils.validate_return_data(response, data_model=History) def user_bans(self, user_id: Union[int, str], @@ -2936,13 +3035,11 @@ def user_bans(self, :param is_nickname: Specify if passed user_id is nickname :type is_nickname: Optional[bool] - :return: User's bans or None if list is empty + :return: User's bans :rtype: Optional[List[Ban]] """ logger.debug('Executing "/api/users/:id/bans" method') response: List[Dict[str, Any]] = self._request( self._endpoints.user_bans(user_id), query=Utils.generate_query_dict(is_nickname=is_nickname)) - if not response: - return None - return [Ban(**ban) for ban in response] + return Utils.validate_return_data(response, data_model=Ban) From ad36ecd1013de8e4d9d9d72e7659639f8f75480a Mon Sep 17 00:00:00 2001 From: SecondThundeR Date: Sat, 4 Jun 2022 21:53:46 +0300 Subject: [PATCH 12/15] fix validator typing --- shikithon/utils.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/shikithon/utils.py b/shikithon/utils.py index 57c480e4..9c9b1e37 100644 --- a/shikithon/utils.py +++ b/shikithon/utils.py @@ -7,9 +7,8 @@ """ from enum import Enum from time import time -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Type, Union -import pydantic.main from loguru import logger from shikithon.enums.response import ResponseCode @@ -246,10 +245,9 @@ def query_numbers_validator(**query_numbers: List[Optional[int]] def validate_return_data( response_data: Union[List[Dict[str, Any]], Dict[str, Any], List[Any], int], - data_model: Optional[pydantic.main.ModelMetaclass] = None, + data_model: Optional[Type[Any]] = None, response_code: Optional[ResponseCode] = None - ) -> Optional[Union[pydantic.main.ModelMetaclass, - List[pydantic.main.ModelMetaclass], List[Any], bool]]: + ) -> Optional[Union[Type[Any], List[Type[Any]], List[Any], bool]]: """ Validates passed response data and returns parsed models. @@ -259,15 +257,14 @@ def validate_return_data( Dict[str, Any], List[Any]]] :param data_model: Model to convert into passed response data - :type data_model: Optional[pydantic.main.ModelMetaclass] + :type data_model: Optional[Type[Any]] :param response_code: Code of response (Used only when response_data is int) :type response_code: Optional[ResponseCode] :return: Parsed response data - :rtype: Optional[Union[pydantic.main.ModelMetaclass, - List[pydantic.main.ModelMetaclass], bool]] + :rtype: Optional[Union[Type[Any], List[Type[Any]], bool]] """ logger.debug(f'Validating return data: {response_data=}, ' f'{data_model=}, {response_code=}') From 7d38316642319f8ad46137293044628b5e9e5313 Mon Sep 17 00:00:00 2001 From: SecondThundeR Date: Sat, 4 Jun 2022 21:59:30 +0300 Subject: [PATCH 13/15] fix Python version in README.md also fix div and img tags --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3157bddb..88b11d93 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -
+
- + Shikithon Logo

Shikithon

Очередной враппер для Shikimori API, написанный на Python

@@ -27,12 +27,12 @@ Это позволяет не задумываться об обработке очередного ответа от сервера и сосредоточиться над реализацией своей идеи. -Также, данная библиотека поддерживает ранние версии Python, начиная с 3.8.10. +Также, данная библиотека поддерживает ранние версии Python, начиная с 3.8. > Поддержка Python 3.6.x не имеет смысла, так как она не является актуальной на момент разработки, а Python 3.7.x > не поддерживается на Apple Silicon _(Основная платформа, на которой разрабатывается данная библиотека)_. > -> Поэтому в качестве минимальной версии был выбран Python 3.8.10 +> Поэтому в качестве минимальной версии был выбран Python 3.8 ## Установка From 044d50ab3abd39bda758c2c380d2641bc2fd9921 Mon Sep 17 00:00:00 2001 From: SecondThundeR Date: Sat, 4 Jun 2022 22:03:24 +0300 Subject: [PATCH 14/15] Revert "fix Python version in README.md" This reverts commit 7d38316642319f8ad46137293044628b5e9e5313. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 88b11d93..3157bddb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -
+
- Shikithon Logo +

Shikithon

Очередной враппер для Shikimori API, написанный на Python

@@ -27,12 +27,12 @@ Это позволяет не задумываться об обработке очередного ответа от сервера и сосредоточиться над реализацией своей идеи. -Также, данная библиотека поддерживает ранние версии Python, начиная с 3.8. +Также, данная библиотека поддерживает ранние версии Python, начиная с 3.8.10. > Поддержка Python 3.6.x не имеет смысла, так как она не является актуальной на момент разработки, а Python 3.7.x > не поддерживается на Apple Silicon _(Основная платформа, на которой разрабатывается данная библиотека)_. > -> Поэтому в качестве минимальной версии был выбран Python 3.8 +> Поэтому в качестве минимальной версии был выбран Python 3.8.10 ## Установка From 23b01462a3ee142b73290003696c15eb8a9d8971 Mon Sep 17 00:00:00 2001 From: SecondThundeR Date: Sat, 4 Jun 2022 22:05:12 +0300 Subject: [PATCH 15/15] add alt parameter and align obsolete comment --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3157bddb..6d4d2868 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ +
- + Shikithon Logo

Shikithon

Очередной враппер для Shikimori API, написанный на Python