diff --git a/myanimelist/__init__.py b/myanimelist/__init__.py index dc1c898..d8e35a8 100644 --- a/myanimelist/__init__.py +++ b/myanimelist/__init__.py @@ -1 +1 @@ -from myanimelist import * \ No newline at end of file +from myanimelist import * diff --git a/myanimelist/anime.py b/myanimelist/anime.py index 3b5e510..b59be37 100644 --- a/myanimelist/anime.py +++ b/myanimelist/anime.py @@ -7,289 +7,299 @@ import media from base import loadable + class MalformedAnimePageError(media.MalformedMediaPageError): - """Indicates that an anime-related page on MAL has irreparably broken markup in some way. - """ - pass + """Indicates that an anime-related page on MAL has irreparably broken markup in some way. + """ + pass + class InvalidAnimeError(media.InvalidMediaError): - """Indicates that the anime requested does not exist on MAL. - """ - pass + """Indicates that the anime requested does not exist on MAL. + """ + pass -class Anime(media.Media): - """Primary interface to anime resources on MAL. - """ - _status_terms = [ - u'Unknown', - u'Currently Airing', - u'Finished Airing', - u'Not yet aired' - ] - _consuming_verb = "watch" - - def __init__(self, session, anime_id): - """Creates a new instance of Anime. - - :type session: :class:`myanimelist.session.Session` - :param session: A valid MAL session - :type anime_id: int - :param anime_id: The desired anime's ID on MAL - - :raises: :class:`.InvalidAnimeError` +class Anime(media.Media): + """Primary interface to anime resources on MAL. """ - if not isinstance(anime_id, int) or int(anime_id) < 1: - raise InvalidAnimeError(anime_id) - super(Anime, self).__init__(session, anime_id) - self._episodes = None - self._aired = None - self._producers = None - self._duration = None - self._rating = None - self._voice_actors = None - self._staff = None - - def parse_sidebar(self, anime_page): - """Parses the DOM and returns anime attributes in the sidebar. - - :type anime_page: :class:`bs4.BeautifulSoup` - :param anime_page: MAL anime page's DOM - - :rtype: dict - :return: anime attributes - - :raises: :class:`.InvalidAnimeError`, :class:`.MalformedAnimePageError` - """ - # if MAL says the series doesn't exist, raise an InvalidAnimeError. - error_tag = anime_page.find(u'div', {'class': 'badresult'}) - if error_tag: - raise InvalidAnimeError(self.id) - - title_tag = anime_page.find(u'div', {'id': 'contentWrapper'}).find(u'h1') - if not title_tag.find(u'div'): - # otherwise, raise a MalformedAnimePageError. - raise MalformedAnimePageError(self.id, anime_page, message="Could not find title div") - - anime_info = super(Anime, self).parse_sidebar(anime_page) - info_panel_first = anime_page.find(u'div', {'id': 'content'}).find(u'table').find(u'td') - - try: - episode_tag = info_panel_first.find(text=u'Episodes:').parent.parent - utilities.extract_tags(episode_tag.find_all(u'span', {'class': 'dark_text'})) - anime_info[u'episodes'] = int(episode_tag.text.strip()) if episode_tag.text.strip() != 'Unknown' else 0 - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - aired_tag = info_panel_first.find(text=u'Aired:').parent.parent - utilities.extract_tags(aired_tag.find_all(u'span', {'class': 'dark_text'})) - aired_parts = aired_tag.text.strip().split(u' to ') - if len(aired_parts) == 1: - # this aired once. - try: - aired_date = utilities.parse_profile_date(aired_parts[0], suppress=self.session.suppress_parse_exceptions) - except ValueError: - raise MalformedAnimePageError(self.id, aired_parts[0], message="Could not parse single air date") - anime_info[u'aired'] = (aired_date,) - else: - # two airing dates. - try: - air_start = utilities.parse_profile_date(aired_parts[0], suppress=self.session.suppress_parse_exceptions) - except ValueError: - raise MalformedAnimePageError(self.id, aired_parts[0], message="Could not parse first of two air dates") + _status_terms = [ + u'Unknown', + u'Currently Airing', + u'Finished Airing', + u'Not yet aired' + ] + _consuming_verb = "watch" + + def __init__(self, session, anime_id): + """Creates a new instance of Anime. + + :type session: :class:`myanimelist.session.Session` + :param session: A valid MAL session + :type anime_id: int + :param anime_id: The desired anime's ID on MAL + + :raises: :class:`.InvalidAnimeError` + + """ + if not isinstance(anime_id, int) or int(anime_id) < 1: + raise InvalidAnimeError(anime_id) + super(Anime, self).__init__(session, anime_id) + self._episodes = None + self._aired = None + self._producers = None + self._duration = None + self._rating = None + self._voice_actors = None + self._staff = None + + def parse_sidebar(self, anime_page): + """Parses the DOM and returns anime attributes in the sidebar. + + :type anime_page: :class:`bs4.BeautifulSoup` + :param anime_page: MAL anime page's DOM + + :rtype: dict + :return: anime attributes + + :raises: :class:`.InvalidAnimeError`, :class:`.MalformedAnimePageError` + """ + # if MAL says the series doesn't exist, raise an InvalidAnimeError. + error_tag = anime_page.find(u'div', {'class': 'badresult'}) + if error_tag: + raise InvalidAnimeError(self.id) + + title_tag = anime_page.find(u'div', {'id': 'contentWrapper'}).find(u'h1') + if not title_tag.find(u'div'): + # otherwise, raise a MalformedAnimePageError. + raise MalformedAnimePageError(self.id, anime_page, message="Could not find title div") + + anime_info = super(Anime, self).parse_sidebar(anime_page) + info_panel_first = anime_page.find(u'div', {'id': 'content'}).find(u'table').find(u'td') + try: - air_end = utilities.parse_profile_date(aired_parts[1], suppress=self.session.suppress_parse_exceptions) - except ValueError: - raise MalformedAnimePageError(self.id, aired_parts[1], message="Could not parse second of two air dates") - anime_info[u'aired'] = (air_start, air_end) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - producers_tag = info_panel_first.find(text=u'Producers:').parent.parent - utilities.extract_tags(producers_tag.find_all(u'span', {'class': 'dark_text'})) - utilities.extract_tags(producers_tag.find_all(u'sup')) - anime_info[u'producers'] = [] - for producer_link in producers_tag.find_all('a'): - if producer_link.text == u'add some': - # MAL is saying "None found, add some". - break - link_parts = producer_link.get('href').split('p=') - # of the form: /anime.php?p=14 - if len(link_parts) > 1: - anime_info[u'producers'].append(self.session.producer(int(link_parts[1])).set({'name': producer_link.text})) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - duration_tag = info_panel_first.find(text=u'Duration:').parent.parent - utilities.extract_tags(duration_tag.find_all(u'span', {'class': 'dark_text'})) - anime_info[u'duration'] = duration_tag.text.strip() - duration_parts = [part.strip() for part in anime_info[u'duration'].split(u'.')] - duration_mins = 0 - for part in duration_parts: - part_match = re.match(u'(?P[0-9]+)', part) - if not part_match: - continue - part_volume = int(part_match.group(u'num')) - if part.endswith(u'hr'): - duration_mins += part_volume * 60 - elif part.endswith(u'min'): - duration_mins += part_volume - anime_info[u'duration'] = datetime.timedelta(minutes=duration_mins) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - rating_tag = info_panel_first.find(text=u'Rating:').parent.parent - utilities.extract_tags(rating_tag.find_all(u'span', {'class': 'dark_text'})) - anime_info[u'rating'] = rating_tag.text.strip() - except: - if not self.session.suppress_parse_exceptions: - raise - - return anime_info - - def parse_characters(self, character_page): - """Parses the DOM and returns anime character attributes in the sidebar. - - :type character_page: :class:`bs4.BeautifulSoup` - :param character_page: MAL anime character page's DOM - - :rtype: dict - :return: anime character attributes - - :raises: :class:`.InvalidAnimeError`, :class:`.MalformedAnimePageError` + episode_tag = info_panel_first.find(text=u'Episodes:').parent.parent + utilities.extract_tags(episode_tag.find_all(u'span', {'class': 'dark_text'})) + anime_info[u'episodes'] = int(episode_tag.text.strip()) if episode_tag.text.strip() != 'Unknown' else 0 + except: + if not self.session.suppress_parse_exceptions: + raise - """ - anime_info = self.parse_sidebar(character_page) - - try: - character_title = filter(lambda x: 'Characters & Voice Actors' in x.text, character_page.find_all(u'h2')) - anime_info[u'characters'] = {} - anime_info[u'voice_actors'] = {} - if character_title: - character_title = character_title[0] - curr_elt = character_title.nextSibling - while True: - if curr_elt.name != u'table': - break - curr_row = curr_elt.find(u'tr') - # character in second col, VAs in third. - (_, character_col, va_col) = curr_row.find_all(u'td', recursive=False) - - character_link = character_col.find(u'a') - character_name = ' '.join(reversed(character_link.text.split(u', '))) - link_parts = character_link.get(u'href').split(u'/') - # of the form /character/7373/Holo - character = self.session.character(int(link_parts[2])).set({'name': character_name}) - role = character_col.find(u'small').text - character_entry = {'role': role, 'voice_actors': {}} - - va_table = va_col.find(u'table') - if va_table: - for row in va_table.find_all(u'tr'): - va_info_cols = row.find_all(u'td') - if not va_info_cols: - # don't ask me why MAL has an extra blank table row i don't know!!! - continue - va_info_col = va_info_cols[0] - va_link = va_info_col.find(u'a') - if va_link: - va_name = ' '.join(reversed(va_link.text.split(u', '))) - link_parts = va_link.get(u'href').split(u'/') - # of the form /people/70/Ami_Koshimizu - person = self.session.person(int(link_parts[2])).set({'name': va_name}) - language = va_info_col.find(u'small').text - anime_info[u'voice_actors'][person] = {'role': role, 'character': character, 'language': language} - character_entry[u'voice_actors'][person] = language - anime_info[u'characters'][character] = character_entry - curr_elt = curr_elt.nextSibling - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - staff_title = filter(lambda x: 'Staff' in x.text, character_page.find_all(u'h2')) - anime_info[u'staff'] = {} - if staff_title: - staff_title = staff_title[0] - staff_table = staff_title.nextSibling.nextSibling - for row in staff_table.find_all(u'tr'): - # staff info in second col. - info = row.find_all(u'td')[1] - staff_link = info.find(u'a') - staff_name = ' '.join(reversed(staff_link.text.split(u', '))) - link_parts = staff_link.get(u'href').split(u'/') - # of the form /people/1870/Miyazaki_Hayao - person = self.session.person(int(link_parts[2])).set({'name': staff_name}) - # staff role(s). - anime_info[u'staff'][person] = set(info.find(u'small').text.split(u', ')) - except: - if not self.session.suppress_parse_exceptions: - raise - - return anime_info - - @property - @loadable(u'load') - def episodes(self): - """The number of episodes in this anime. If undetermined, is None, otherwise > 0. - """ - return self._episodes + try: + aired_tag = info_panel_first.find(text=u'Aired:').parent.parent + utilities.extract_tags(aired_tag.find_all(u'span', {'class': 'dark_text'})) + aired_parts = aired_tag.text.strip().split(u' to ') + if len(aired_parts) == 1: + # this aired once. + try: + aired_date = utilities.parse_profile_date(aired_parts[0], + suppress=self.session.suppress_parse_exceptions) + except ValueError: + raise MalformedAnimePageError(self.id, aired_parts[0], message="Could not parse single air date") + anime_info[u'aired'] = (aired_date,) + else: + # two airing dates. + try: + air_start = utilities.parse_profile_date(aired_parts[0], + suppress=self.session.suppress_parse_exceptions) + except ValueError: + raise MalformedAnimePageError(self.id, aired_parts[0], + message="Could not parse first of two air dates") + try: + air_end = utilities.parse_profile_date(aired_parts[1], + suppress=self.session.suppress_parse_exceptions) + except ValueError: + raise MalformedAnimePageError(self.id, aired_parts[1], + message="Could not parse second of two air dates") + anime_info[u'aired'] = (air_start, air_end) + except: + if not self.session.suppress_parse_exceptions: + raise - @property - @loadable(u'load') - def aired(self): - """A tuple(2) containing up to two :class:`datetime.date` objects representing the start and end dates of this anime's airing. + try: + producers_tag = info_panel_first.find(text=u'Producers:').parent.parent + utilities.extract_tags(producers_tag.find_all(u'span', {'class': 'dark_text'})) + utilities.extract_tags(producers_tag.find_all(u'sup')) + anime_info[u'producers'] = [] + for producer_link in producers_tag.find_all('a'): + if producer_link.text == u'add some': + # MAL is saying "None found, add some". + break + link_parts = producer_link.get('href').split('p=') + # of the form: /anime.php?p=14 + if len(link_parts) > 1: + anime_info[u'producers'].append( + self.session.producer(int(link_parts[1])).set({'name': producer_link.text})) + except: + if not self.session.suppress_parse_exceptions: + raise - Potential configurations: + try: + duration_tag = info_panel_first.find(text=u'Duration:').parent.parent + utilities.extract_tags(duration_tag.find_all(u'span', {'class': 'dark_text'})) + anime_info[u'duration'] = duration_tag.text.strip() + duration_parts = [part.strip() for part in anime_info[u'duration'].split(u'.')] + duration_mins = 0 + for part in duration_parts: + part_match = re.match(u'(?P[0-9]+)', part) + if not part_match: + continue + part_volume = int(part_match.group(u'num')) + if part.endswith(u'hr'): + duration_mins += part_volume * 60 + elif part.endswith(u'min'): + duration_mins += part_volume + anime_info[u'duration'] = datetime.timedelta(minutes=duration_mins) + except: + if not self.session.suppress_parse_exceptions: + raise - None -- Completely-unknown airing dates. + try: + rating_tag = info_panel_first.find(text=u'Rating:').parent.parent + utilities.extract_tags(rating_tag.find_all(u'span', {'class': 'dark_text'})) + anime_info[u'rating'] = rating_tag.text.strip() + except: + if not self.session.suppress_parse_exceptions: + raise - (:class:`datetime.date`, None) -- Anime start date is known, end date is unknown. + return anime_info - (:class:`datetime.date`, :class:`datetime.date`) -- Anime start and end dates are known. + def parse_characters(self, character_page): + """Parses the DOM and returns anime character attributes in the sidebar. - """ - return self._aired + :type character_page: :class:`bs4.BeautifulSoup` + :param character_page: MAL anime character page's DOM - @property - @loadable(u'load') - def producers(self): - """A list of :class:`myanimelist.producer.Producer` objects involved in this anime. - """ - return self._producers + :rtype: dict + :return: anime character attributes - @property - @loadable(u'load') - def duration(self): - """The duration of an episode of this anime as a :class:`datetime.timedelta`. - """ - return self._duration + :raises: :class:`.InvalidAnimeError`, :class:`.MalformedAnimePageError` - @property - @loadable(u'load') - def rating(self): - """The MPAA rating given to this anime. - """ - return self._rating + """ + anime_info = self.parse_sidebar(character_page) - @property - @loadable(u'load_characters') - def voice_actors(self): - """A voice actors dict with :class:`myanimelist.person.Person` objects of the voice actors as keys, and dicts containing info about the roles played, e.g. {'role': 'Main', 'character': myanimelist.character.Character(1)} as values. - """ - return self._voice_actors + try: + character_title = filter(lambda x: 'Characters & Voice Actors' in x.text, character_page.find_all(u'h2')) + anime_info[u'characters'] = {} + anime_info[u'voice_actors'] = {} + if character_title: + character_title = character_title[0] + curr_elt = character_title.nextSibling + while True: + if curr_elt.name != u'table': + break + curr_row = curr_elt.find(u'tr') + # character in second col, VAs in third. + (_, character_col, va_col) = curr_row.find_all(u'td', recursive=False) + + character_link = character_col.find(u'a') + character_name = ' '.join(reversed(character_link.text.split(u', '))) + link_parts = character_link.get(u'href').split(u'/') + # of the form /character/7373/Holo + character = self.session.character(int(link_parts[2])).set({'name': character_name}) + role = character_col.find(u'small').text + character_entry = {'role': role, 'voice_actors': {}} + + va_table = va_col.find(u'table') + if va_table: + for row in va_table.find_all(u'tr'): + va_info_cols = row.find_all(u'td') + if not va_info_cols: + # don't ask me why MAL has an extra blank table row i don't know!!! + continue + va_info_col = va_info_cols[0] + va_link = va_info_col.find(u'a') + if va_link: + va_name = ' '.join(reversed(va_link.text.split(u', '))) + link_parts = va_link.get(u'href').split(u'/') + # of the form /people/70/Ami_Koshimizu + person = self.session.person(int(link_parts[2])).set({'name': va_name}) + language = va_info_col.find(u'small').text + anime_info[u'voice_actors'][person] = {'role': role, 'character': character, + 'language': language} + character_entry[u'voice_actors'][person] = language + anime_info[u'characters'][character] = character_entry + curr_elt = curr_elt.nextSibling + except: + if not self.session.suppress_parse_exceptions: + raise - @property - @loadable(u'load_characters') - def staff(self): - """A staff dict with :class:`myanimelist.person.Person` objects of the staff members as keys, and lists containing the various duties performed by staff members as values. - """ - return self._staff \ No newline at end of file + try: + staff_title = filter(lambda x: 'Staff' in x.text, character_page.find_all(u'h2')) + anime_info[u'staff'] = {} + if staff_title: + staff_title = staff_title[0] + staff_table = staff_title.nextSibling.nextSibling + for row in staff_table.find_all(u'tr'): + # staff info in second col. + info = row.find_all(u'td')[1] + staff_link = info.find(u'a') + staff_name = ' '.join(reversed(staff_link.text.split(u', '))) + link_parts = staff_link.get(u'href').split(u'/') + # of the form /people/1870/Miyazaki_Hayao + person = self.session.person(int(link_parts[2])).set({'name': staff_name}) + # staff role(s). + anime_info[u'staff'][person] = set(info.find(u'small').text.split(u', ')) + except: + if not self.session.suppress_parse_exceptions: + raise + + return anime_info + + @property + @loadable(u'load') + def episodes(self): + """The number of episodes in this anime. If undetermined, is None, otherwise > 0. + """ + return self._episodes + + @property + @loadable(u'load') + def aired(self): + """A tuple(2) containing up to two :class:`datetime.date` objects representing the start and end dates of this anime's airing. + + Potential configurations: + + None -- Completely-unknown airing dates. + + (:class:`datetime.date`, None) -- Anime start date is known, end date is unknown. + + (:class:`datetime.date`, :class:`datetime.date`) -- Anime start and end dates are known. + + """ + return self._aired + + @property + @loadable(u'load') + def producers(self): + """A list of :class:`myanimelist.producer.Producer` objects involved in this anime. + """ + return self._producers + + @property + @loadable(u'load') + def duration(self): + """The duration of an episode of this anime as a :class:`datetime.timedelta`. + """ + return self._duration + + @property + @loadable(u'load') + def rating(self): + """The MPAA rating given to this anime. + """ + return self._rating + + @property + @loadable(u'load_characters') + def voice_actors(self): + """A voice actors dict with :class:`myanimelist.person.Person` objects of the voice actors as keys, and dicts containing info about the roles played, e.g. {'role': 'Main', 'character': myanimelist.character.Character(1)} as values. + """ + return self._voice_actors + + @property + @loadable(u'load_characters') + def staff(self): + """A staff dict with :class:`myanimelist.person.Person` objects of the staff members as keys, and lists containing the various duties performed by staff members as values. + """ + return self._staff diff --git a/myanimelist/anime_list.py b/myanimelist/anime_list.py index 6fa266f..6263eb2 100644 --- a/myanimelist/anime_list.py +++ b/myanimelist/anime_list.py @@ -5,72 +5,74 @@ from base import Base, Error, loadable import media_list + class AnimeList(media_list.MediaList): - __id_attribute = "username" - def __init__(self, session, user_name): - super(AnimeList, self).__init__(session, user_name) + __id_attribute = "username" + + def __init__(self, session, user_name): + super(AnimeList, self).__init__(session, user_name) + + @property + def type(self): + return "anime" - @property - def type(self): - return "anime" + @property + def verb(self): + return "watch" - @property - def verb(self): - return "watch" + def parse_entry_media_attributes(self, soup): + attributes = super(AnimeList, self).parse_entry_media_attributes(soup) - def parse_entry_media_attributes(self, soup): - attributes = super(AnimeList, self).parse_entry_media_attributes(soup) + try: + attributes['episodes'] = int(soup.find('series_episodes').text) + except ValueError: + attributes['episodes'] = None + except: + if not self.session.suppress_parse_exceptions: + raise - try: - attributes['episodes'] = int(soup.find('series_episodes').text) - except ValueError: - attributes['episodes'] = None - except: - if not self.session.suppress_parse_exceptions: - raise - - return attributes + return attributes - def parse_entry(self, soup): - anime,entry_info = super(AnimeList, self).parse_entry(soup) + def parse_entry(self, soup): + anime, entry_info = super(AnimeList, self).parse_entry(soup) - try: - entry_info[u'episodes_watched'] = int(soup.find('my_watched_episodes').text) - except ValueError: - entry_info[u'episodes_watched'] = 0 - except: - if not self.session.suppress_parse_exceptions: - raise + try: + entry_info[u'episodes_watched'] = int(soup.find('my_watched_episodes').text) + except ValueError: + entry_info[u'episodes_watched'] = 0 + except: + if not self.session.suppress_parse_exceptions: + raise - try: - entry_info[u'rewatching'] = bool(soup.find('my_rewatching').text) - except ValueError: - entry_info[u'rewatching'] = False - except: - if not self.session.suppress_parse_exceptions: - raise + try: + entry_info[u'rewatching'] = bool(soup.find('my_rewatching').text) + except ValueError: + entry_info[u'rewatching'] = False + except: + if not self.session.suppress_parse_exceptions: + raise - try: - entry_info[u'episodes_rewatched'] = int(soup.find('my_rewatching_ep').text) - except ValueError: - entry_info[u'episodes_rewatched'] = 0 - except: - if not self.session.suppress_parse_exceptions: - raise + try: + entry_info[u'episodes_rewatched'] = int(soup.find('my_rewatching_ep').text) + except ValueError: + entry_info[u'episodes_rewatched'] = 0 + except: + if not self.session.suppress_parse_exceptions: + raise - return anime,entry_info + return anime, entry_info - def parse_section_columns(self, columns): - column_names = super(AnimeList, self).parse_section_columns(columns) - for i,column in enumerate(columns): - if u'Type' in column.text: - column_names[u'type'] = i - elif u'Progress' in column.text: - column_names[u'progress'] = i - elif u'Tags' in column.text: - column_names[u'tags'] = i - elif u'Started' in column.text: - column_names[u'started'] = i - elif u'Finished' in column.text: - column_names[u'finished'] = i - return column_names \ No newline at end of file + def parse_section_columns(self, columns): + column_names = super(AnimeList, self).parse_section_columns(columns) + for i, column in enumerate(columns): + if u'Type' in column.text: + column_names[u'type'] = i + elif u'Progress' in column.text: + column_names[u'progress'] = i + elif u'Tags' in column.text: + column_names[u'tags'] = i + elif u'Started' in column.text: + column_names[u'started'] = i + elif u'Finished' in column.text: + column_names[u'finished'] = i + return column_names diff --git a/myanimelist/base.py b/myanimelist/base.py index bce8b5c..f12d2ad 100644 --- a/myanimelist/base.py +++ b/myanimelist/base.py @@ -6,131 +6,147 @@ import utilities -class Error(Exception): - """Base exception class that takes a message to display upon raising. - """ - def __init__(self, message=None): - """Creates an instance of Error. - - :type message: str - :param message: A message to display when raising the exception. +class Error(Exception): + """Base exception class that takes a message to display upon raising. """ - super(Error, self).__init__() - self.message = message - def __str__(self): - return unicode(self.message) if self.message is not None else u"" -class MalformedPageError(Error): - """Indicates that a page on MAL has broken markup in some way. - """ - def __init__(self, id, html, message=None): - super(MalformedPageError, self).__init__(message=message) - if isinstance(id, unicode): - self.id = id - else: - self.id = str(id).decode(u'utf-8') - if isinstance(html, unicode): - self.html = html - else: - self.html = str(html).decode(u'utf-8') - def __str__(self): - return "\n".join([ - super(MalformedPageError, self).__str__(), - "ID: " + self.id, - "HTML: " + self.html - ]).encode(u'utf-8') + def __init__(self, message=None): + """Creates an instance of Error. -class InvalidBaseError(Error): - """Indicates that the particular resource instance requested does not exist on MAL. - """ - def __init__(self, id, message=None): - super(InvalidBaseError, self).__init__(message=message) - self.id = id - def __str__(self): - return "\n".join([ - super(InvalidBaseError, self).__str__(), - "ID: " + unicode(self.id) - ]) + :type message: str + :param message: A message to display when raising the exception. -def loadable(func_name): - """Decorator for getters that require a load() upon first access. + """ + super(Error, self).__init__() + self.message = message - :type func_name: function - :param func_name: class method that requires that load() be called if the class's _attribute value is None + def __str__(self): + return unicode(self.message) if self.message is not None else u"" - :rtype: function - :return: the decorated class method. - """ - def inner(func): - cached_name = '_' + func.__name__ - @functools.wraps(func) - def _decorator(self, *args, **kwargs): - if getattr(self, cached_name) is None: - getattr(self, func_name)() - return func(self, *args, **kwargs) - return _decorator - return inner +class MalformedPageError(Error): + """Indicates that a page on MAL has broken markup in some way. + """ -class Base(object): - """Abstract base class for MAL resources. Provides autoloading, auto-setting functionality for other MAL objects. - """ - __metaclass__ = abc.ABCMeta + def __init__(self, id, html, message=None): + super(MalformedPageError, self).__init__(message=message) + if isinstance(id, unicode): + self.id = id + else: + self.id = str(id).decode(u'utf-8') + if isinstance(html, unicode): + self.html = html + else: + self.html = str(html).decode(u'utf-8') + + def __str__(self): + return "\n".join([ + super(MalformedPageError, self).__str__(), + "ID: " + self.id, + "HTML: " + self.html + ]).encode(u'utf-8') - """Attribute name for primary reference key to this object. - When an attribute by the name given by _id_attribute is passed into set(), set() doesn't prepend an underscore for load()ing. - """ - _id_attribute = "id" - def __repr__(self): - return u"".join([ - "<", - self.__class__.__name__, - " ", - self._id_attribute, - ": ", - unicode(getattr(self, self._id_attribute)), - ">" - ]) +class InvalidBaseError(Error): + """Indicates that the particular resource instance requested does not exist on MAL. + """ - def __hash__(self): - return hash('-'.join([self.__class__.__name__, unicode(getattr(self, self._id_attribute))])) + def __init__(self, id, message=None): + super(InvalidBaseError, self).__init__(message=message) + self.id = id - def __eq__(self, other): - return isinstance(other, self.__class__) and getattr(self, self._id_attribute) == getattr(other, other._id_attribute) + def __str__(self): + return "\n".join([ + super(InvalidBaseError, self).__str__(), + "ID: " + unicode(self.id) + ]) - def __ne__(self, other): - return not self.__eq__(other) - def __init__(self, session): - """Create an instance of Base. +def loadable(func_name): + """Decorator for getters that require a load() upon first access. - :type session: :class:`myanimelist.session.Session` - :param session: A valid MAL session. + :type func_name: function + :param func_name: class method that requires that load() be called if the class's _attribute value is None - """ - self.session = session + :rtype: function + :return: the decorated class method. - @abc.abstractmethod - def load(self): - """A callback to run before any @loadable attributes are returned. """ - pass - def set(self, attr_dict): - """Sets attributes of this user object. + def inner(func): + cached_name = '_' + func.__name__ + + @functools.wraps(func) + def _decorator(self, *args, **kwargs): + if getattr(self, cached_name) is None: + getattr(self, func_name)() + return func(self, *args, **kwargs) - :type attr_dict: dict - :param attr_dict: Parameters to set, with attribute keys. + return _decorator - :rtype: :class:`.Base` - :return: The current object. + return inner + + +class Base(object): + """Abstract base class for MAL resources. Provides autoloading, auto-setting functionality for other MAL objects. + """ + __metaclass__ = abc.ABCMeta + """Attribute name for primary reference key to this object. + When an attribute by the name given by _id_attribute is passed into set(), set() doesn't prepend an underscore for load()ing. """ - for key in attr_dict: - if key == self._id_attribute: - setattr(self, self._id_attribute, attr_dict[key]) - else: - setattr(self, u"_" + key, attr_dict[key]) - return self \ No newline at end of file + _id_attribute = "id" + + def __repr__(self): + return u"".join([ + "<", + self.__class__.__name__, + " ", + self._id_attribute, + ": ", + unicode(getattr(self, self._id_attribute)), + ">" + ]) + + def __hash__(self): + return hash('-'.join([self.__class__.__name__, unicode(getattr(self, self._id_attribute))])) + + def __eq__(self, other): + return isinstance(other, self.__class__) and getattr(self, self._id_attribute) == getattr(other, + other._id_attribute) + + def __ne__(self, other): + return not self.__eq__(other) + + def __init__(self, session): + """Create an instance of Base. + + :type session: :class:`myanimelist.session.Session` + :param session: A valid MAL session. + + """ + self.session = session + + @abc.abstractmethod + def load(self): + """A callback to run before any @loadable attributes are returned. + """ + pass + + def set(self, attr_dict): + """Sets attributes of this user object. + + :type attr_dict: dict + :param attr_dict: Parameters to set, with attribute keys. + + :rtype: :class:`.Base` + :return: The current object. + + """ + for key in attr_dict: + if key == self._id_attribute: + setattr(self, self._id_attribute, attr_dict[key]) + else: + setattr(self, u"_" + key, attr_dict[key]) + return self diff --git a/myanimelist/character.py b/myanimelist/character.py index fe0c145..2c76543 100644 --- a/myanimelist/character.py +++ b/myanimelist/character.py @@ -7,401 +7,418 @@ import utilities from base import Base, MalformedPageError, InvalidBaseError, loadable -class MalformedCharacterPageError(MalformedPageError): - """Indicates that a character-related page on MAL has irreparably broken markup in some way. - """ - pass - -class InvalidCharacterError(InvalidBaseError): - """Indicates that the character requested does not exist on MAL. - """ - pass - -class Character(Base): - """Primary interface to character resources on MAL. - """ - def __init__(self, session, character_id): - """Creates a new instance of Character. - - :type session: :class:`myanimelist.session.Session` - :param session: A valid MAL session - :type character_id: int - :param character_id: The desired character's ID on MAL - - :raises: :class:`.InvalidCharacterError` - - """ - super(Character, self).__init__(session) - self.id = character_id - if not isinstance(self.id, int) or int(self.id) < 1: - raise InvalidCharacterError(self.id) - self._name = None - self._full_name = None - self._name_jpn = None - self._description = None - self._voice_actors = None - self._animeography = None - self._mangaography = None - self._num_favorites = None - self._favorites = None - self._picture = None - self._pictures = None - self._clubs = None - - def parse_sidebar(self, character_page): - """Parses the DOM and returns character attributes in the sidebar. - - :type character_page: :class:`bs4.BeautifulSoup` - :param character_page: MAL character page's DOM - - :rtype: dict - :return: Character attributes - - :raises: :class:`.InvalidCharacterError`, :class:`.MalformedCharacterPageError` - """ - character_info = {} - - error_tag = character_page.find(u'div', {'class': 'badresult'}) - if error_tag: - # MAL says the character does not exist. - raise InvalidCharacterError(self.id) - - try: - full_name_tag = character_page.find(u'div', {'id': 'contentWrapper'}).find(u'h1') - if not full_name_tag: - # Page is malformed. - raise MalformedCharacterPageError(self.id, html, message="Could not find title div") - character_info[u'full_name'] = full_name_tag.text.strip() - except: - if not self.session.suppress_parse_exceptions: - raise - - info_panel_first = character_page.find(u'div', {'id': 'content'}).find(u'table').find(u'td') - - try: - picture_tag = info_panel_first.find(u'img') - character_info[u'picture'] = picture_tag.get(u'src').decode('utf-8') - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - # assemble animeography for this character. - character_info[u'animeography'] = {} - animeography_header = info_panel_first.find(u'div', text=u'Animeography') - if animeography_header: - animeography_table = animeography_header.find_next_sibling(u'table') - for row in animeography_table.find_all(u'tr'): - # second column has anime info. - info_col = row.find_all(u'td')[1] - anime_link = info_col.find(u'a') - link_parts = anime_link.get(u'href').split(u'/') - # of the form: /anime/1/Cowboy_Bebop - anime = self.session.anime(int(link_parts[2])).set({'title': anime_link.text}) - role = info_col.find(u'small').text - character_info[u'animeography'][anime] = role - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - # assemble mangaography for this character. - character_info[u'mangaography'] = {} - mangaography_header = info_panel_first.find(u'div', text=u'Mangaography') - if mangaography_header: - mangaography_table = mangaography_header.find_next_sibling(u'table') - for row in mangaography_table.find_all(u'tr'): - # second column has manga info. - info_col = row.find_all(u'td')[1] - manga_link = info_col.find(u'a') - link_parts = manga_link.get(u'href').split(u'/') - # of the form: /manga/1/Cowboy_Bebop - manga = self.session.manga(int(link_parts[2])).set({'title': manga_link.text}) - role = info_col.find(u'small').text - character_info[u'mangaography'][manga] = role - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - num_favorites_node = info_panel_first.find(text=re.compile(u'Member Favorites: ')) - character_info[u'num_favorites'] = int(num_favorites_node.strip().split(u': ')[1]) - except: - if not self.session.suppress_parse_exceptions: - raise - - return character_info - - def parse(self, character_page): - """Parses the DOM and returns character attributes in the main-content area. - - :type character_page: :class:`bs4.BeautifulSoup` - :param character_page: MAL character page's DOM - - :rtype: dict - :return: Character attributes. - - """ - character_info = self.parse_sidebar(character_page) - - second_col = character_page.find(u'div', {'id': 'content'}).find(u'table').find(u'tr').find_all(u'td', recursive=False)[1] - name_elt = second_col.find(u'div', {'class': 'normal_header'}) - - try: - name_jpn_node = name_elt.find(u'small') - if name_jpn_node: - character_info[u'name_jpn'] = name_jpn_node.text[1:-1] - else: - character_info[u'name_jpn'] = None - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - name_elt.find(u'span').extract() - character_info[u'name'] = name_elt.text.rstrip() - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - description_elts = [] - curr_elt = name_elt.nextSibling - while True: - if curr_elt.name not in [None, u'br']: - break - description_elts.append(unicode(curr_elt)) - curr_elt = curr_elt.nextSibling - character_info[u'description'] = ''.join(description_elts) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - character_info[u'voice_actors'] = {} - voice_actors_header = second_col.find(u'div', text=u'Voice Actors') - if voice_actors_header: - voice_actors_table = voice_actors_header.find_next_sibling(u'table') - for row in voice_actors_table.find_all(u'tr'): - # second column has va info. - info_col = row.find_all(u'td')[1] - voice_actor_link = info_col.find(u'a') - name = ' '.join(reversed(voice_actor_link.text.split(u', '))) - link_parts = voice_actor_link.get(u'href').split(u'/') - # of the form: /people/82/Romi_Park - person = self.session.person(int(link_parts[2])).set({'name': name}) - language = info_col.find(u'small').text - character_info[u'voice_actors'][person] = language - except: - if not self.session.suppress_parse_exceptions: - raise - - return character_info - - def parse_favorites(self, favorites_page): - """Parses the DOM and returns character favorites attributes. - - :type favorites_page: :class:`bs4.BeautifulSoup` - :param favorites_page: MAL character favorites page's DOM - - :rtype: dict - :return: Character favorites attributes. - - """ - character_info = self.parse_sidebar(favorites_page) - second_col = favorites_page.find(u'div', {'id': 'content'}).find(u'table').find(u'tr').find_all(u'td', recursive=False)[1] - - try: - character_info[u'favorites'] = [] - favorite_links = second_col.find_all('a', recursive=False) - for link in favorite_links: - # of the form /profile/shaldengeki - character_info[u'favorites'].append(self.session.user(username=link.text)) - except: - if not self.session.suppress_parse_exceptions: - raise - - return character_info - - def parse_pictures(self, picture_page): - """Parses the DOM and returns character pictures attributes. - - :type picture_page: :class:`bs4.BeautifulSoup` - :param picture_page: MAL character pictures page's DOM - - :rtype: dict - :return: character pictures attributes. - - """ - character_info = self.parse_sidebar(picture_page) - second_col = picture_page.find(u'div', {'id': 'content'}).find(u'table').find(u'tr').find_all(u'td', recursive=False)[1] - - try: - picture_table = second_col.find(u'table', recursive=False) - character_info[u'pictures'] = [] - if picture_table: - character_info[u'pictures'] = map(lambda img: img.get(u'src').decode('utf-8'), picture_table.find_all(u'img')) - except: - if not self.session.suppress_parse_exceptions: - raise - - return character_info - - def parse_clubs(self, clubs_page): - """Parses the DOM and returns character clubs attributes. - - :type clubs_page: :class:`bs4.BeautifulSoup` - :param clubs_page: MAL character clubs page's DOM - - :rtype: dict - :return: character clubs attributes. - - """ - character_info = self.parse_sidebar(clubs_page) - second_col = clubs_page.find(u'div', {'id': 'content'}).find(u'table').find(u'tr').find_all(u'td', recursive=False)[1] - - try: - clubs_header = second_col.find(u'div', text=u'Related Clubs') - character_info[u'clubs'] = [] - if clubs_header: - curr_elt = clubs_header.nextSibling - while curr_elt is not None: - if curr_elt.name == u'div': - link = curr_elt.find(u'a') - club_id = int(re.match(r'/clubs\.php\?cid=(?P[0-9]+)', link.get(u'href')).group(u'id')) - num_members = int(re.match(r'(?P[0-9]+) members', curr_elt.find(u'small').text).group(u'num')) - character_info[u'clubs'].append(self.session.club(club_id).set({'name': link.text, 'num_members': num_members})) - curr_elt = curr_elt.nextSibling - except: - if not self.session.suppress_parse_exceptions: - raise - - return character_info - - def load(self): - """Fetches the MAL character page and sets the current character's attributes. - - :rtype: :class:`.Character` - :return: Current character object. - - """ - character = self.session.session.get(u'http://myanimelist.net/character/' + str(self.id)).text - self.set(self.parse(utilities.get_clean_dom(character))) - return self - def load_favorites(self): - """Fetches the MAL character favorites page and sets the current character's favorites attributes. - - :rtype: :class:`.Character` - :return: Current character object. - - """ - character = self.session.session.get(u'http://myanimelist.net/character/' + str(self.id) + u'/' + utilities.urlencode(self.name) + u'/favorites').text - self.set(self.parse_favorites(utilities.get_clean_dom(character))) - return self - - def load_pictures(self): - """Fetches the MAL character pictures page and sets the current character's pictures attributes. - - :rtype: :class:`.Character` - :return: Current character object. - - """ - character = self.session.session.get(u'http://myanimelist.net/character/' + str(self.id) + u'/' + utilities.urlencode(self.name) + u'/pictures').text - self.set(self.parse_pictures(utilities.get_clean_dom(character))) - return self - - def load_clubs(self): - """Fetches the MAL character clubs page and sets the current character's clubs attributes. - - :rtype: :class:`.Character` - :return: Current character object. - - """ - character = self.session.session.get(u'http://myanimelist.net/character/' + str(self.id) + u'/' + utilities.urlencode(self.name) + u'/clubs').text - self.set(self.parse_clubs(utilities.get_clean_dom(character))) - return self - - @property - @loadable(u'load') - def name(self): - """Character name. - """ - return self._name - - @property - @loadable(u'load') - def full_name(self): - """Character's full name. - """ - return self._full_name - - @property - @loadable(u'load') - def name_jpn(self): - """Character's Japanese name. - """ - return self._name_jpn - - @property - @loadable(u'load') - def description(self): - """Character's description. - """ - return self._description - - @property - @loadable(u'load') - def voice_actors(self): - """Voice actor dict for this character, with :class:`myanimelist.person.Person` objects as keys and the language as values. - """ - return self._voice_actors - - @property - @loadable(u'load') - def animeography(self): - """Anime appearance dict for this character, with :class:`myanimelist.anime.Anime` objects as keys and the type of role as values, e.g. 'Main' - """ - return self._animeography - - @property - @loadable(u'load') - def mangaography(self): - """Manga appearance dict for this character, with :class:`myanimelist.manga.Manga` objects as keys and the type of role as values, e.g. 'Main' +class MalformedCharacterPageError(MalformedPageError): + """Indicates that a character-related page on MAL has irreparably broken markup in some way. """ - return self._mangaography + pass - @property - @loadable(u'load') - def num_favorites(self): - """Number of users who have favourited this character. - """ - return self._num_favorites - @property - @loadable(u'load_favorites') - def favorites(self): - """List of users who have favourited this character. +class InvalidCharacterError(InvalidBaseError): + """Indicates that the character requested does not exist on MAL. """ - return self._favorites + pass - @property - @loadable(u'load') - def picture(self): - """URL of primary picture for this character. - """ - return self._picture - @property - @loadable(u'load_pictures') - def pictures(self): - """List of picture URLs for this character. +class Character(Base): + """Primary interface to character resources on MAL. """ - return self._pictures - @property - @loadable(u'load_clubs') - def clubs(self): - """List of clubs relevant to this character. - """ - return self._clubs + def __init__(self, session, character_id): + """Creates a new instance of Character. + + :type session: :class:`myanimelist.session.Session` + :param session: A valid MAL session + :type character_id: int + :param character_id: The desired character's ID on MAL + + :raises: :class:`.InvalidCharacterError` + + """ + super(Character, self).__init__(session) + self.id = character_id + if not isinstance(self.id, int) or int(self.id) < 1: + raise InvalidCharacterError(self.id) + self._name = None + self._full_name = None + self._name_jpn = None + self._description = None + self._voice_actors = None + self._animeography = None + self._mangaography = None + self._num_favorites = None + self._favorites = None + self._picture = None + self._pictures = None + self._clubs = None + + def parse_sidebar(self, character_page): + """Parses the DOM and returns character attributes in the sidebar. + + :type character_page: :class:`bs4.BeautifulSoup` + :param character_page: MAL character page's DOM + + :rtype: dict + :return: Character attributes + + :raises: :class:`.InvalidCharacterError`, :class:`.MalformedCharacterPageError` + """ + character_info = {} + + error_tag = character_page.find(u'div', {'class': 'badresult'}) + if error_tag: + # MAL says the character does not exist. + raise InvalidCharacterError(self.id) + + try: + full_name_tag = character_page.find(u'div', {'id': 'contentWrapper'}).find(u'h1') + if not full_name_tag: + # Page is malformed. + raise MalformedCharacterPageError(self.id, html, message="Could not find title div") + character_info[u'full_name'] = full_name_tag.text.strip() + except: + if not self.session.suppress_parse_exceptions: + raise + + info_panel_first = character_page.find(u'div', {'id': 'content'}).find(u'table').find(u'td') + + try: + picture_tag = info_panel_first.find(u'img') + character_info[u'picture'] = picture_tag.get(u'src').decode('utf-8') + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + # assemble animeography for this character. + character_info[u'animeography'] = {} + animeography_header = info_panel_first.find(u'div', text=u'Animeography') + if animeography_header: + animeography_table = animeography_header.find_next_sibling(u'table') + for row in animeography_table.find_all(u'tr'): + # second column has anime info. + info_col = row.find_all(u'td')[1] + anime_link = info_col.find(u'a') + link_parts = anime_link.get(u'href').split(u'/') + # of the form: /anime/1/Cowboy_Bebop + anime = self.session.anime(int(link_parts[2])).set({'title': anime_link.text}) + role = info_col.find(u'small').text + character_info[u'animeography'][anime] = role + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + # assemble mangaography for this character. + character_info[u'mangaography'] = {} + mangaography_header = info_panel_first.find(u'div', text=u'Mangaography') + if mangaography_header: + mangaography_table = mangaography_header.find_next_sibling(u'table') + for row in mangaography_table.find_all(u'tr'): + # second column has manga info. + info_col = row.find_all(u'td')[1] + manga_link = info_col.find(u'a') + link_parts = manga_link.get(u'href').split(u'/') + # of the form: /manga/1/Cowboy_Bebop + manga = self.session.manga(int(link_parts[2])).set({'title': manga_link.text}) + role = info_col.find(u'small').text + character_info[u'mangaography'][manga] = role + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + num_favorites_node = info_panel_first.find(text=re.compile(u'Member Favorites: ')) + character_info[u'num_favorites'] = int(num_favorites_node.strip().split(u': ')[1]) + except: + if not self.session.suppress_parse_exceptions: + raise + + return character_info + + def parse(self, character_page): + """Parses the DOM and returns character attributes in the main-content area. + + :type character_page: :class:`bs4.BeautifulSoup` + :param character_page: MAL character page's DOM + + :rtype: dict + :return: Character attributes. + + """ + character_info = self.parse_sidebar(character_page) + + second_col = \ + character_page.find(u'div', {'id': 'content'}).find(u'table').find(u'tr').find_all(u'td', recursive=False)[1] + name_elt = second_col.find(u'div', {'class': 'normal_header'}) + + try: + name_jpn_node = name_elt.find(u'small') + if name_jpn_node: + character_info[u'name_jpn'] = name_jpn_node.text[1:-1] + else: + character_info[u'name_jpn'] = None + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + name_elt.find(u'span').extract() + character_info[u'name'] = name_elt.text.rstrip() + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + description_elts = [] + curr_elt = name_elt.nextSibling + while True: + if curr_elt.name not in [None, u'br']: + break + description_elts.append(unicode(curr_elt)) + curr_elt = curr_elt.nextSibling + character_info[u'description'] = ''.join(description_elts) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + character_info[u'voice_actors'] = {} + voice_actors_header = second_col.find(u'div', text=u'Voice Actors') + if voice_actors_header: + voice_actors_table = voice_actors_header.find_next_sibling(u'table') + for row in voice_actors_table.find_all(u'tr'): + # second column has va info. + info_col = row.find_all(u'td')[1] + voice_actor_link = info_col.find(u'a') + name = ' '.join(reversed(voice_actor_link.text.split(u', '))) + link_parts = voice_actor_link.get(u'href').split(u'/') + # of the form: /people/82/Romi_Park + person = self.session.person(int(link_parts[2])).set({'name': name}) + language = info_col.find(u'small').text + character_info[u'voice_actors'][person] = language + except: + if not self.session.suppress_parse_exceptions: + raise + + return character_info + + def parse_favorites(self, favorites_page): + """Parses the DOM and returns character favorites attributes. + + :type favorites_page: :class:`bs4.BeautifulSoup` + :param favorites_page: MAL character favorites page's DOM + + :rtype: dict + :return: Character favorites attributes. + + """ + character_info = self.parse_sidebar(favorites_page) + second_col = \ + favorites_page.find(u'div', {'id': 'content'}).find(u'table').find(u'tr').find_all(u'td', recursive=False)[1] + + try: + character_info[u'favorites'] = [] + favorite_links = second_col.find_all('a', recursive=False) + for link in favorite_links: + # of the form /profile/shaldengeki + character_info[u'favorites'].append(self.session.user(username=link.text)) + except: + if not self.session.suppress_parse_exceptions: + raise + + return character_info + + def parse_pictures(self, picture_page): + """Parses the DOM and returns character pictures attributes. + + :type picture_page: :class:`bs4.BeautifulSoup` + :param picture_page: MAL character pictures page's DOM + + :rtype: dict + :return: character pictures attributes. + + """ + character_info = self.parse_sidebar(picture_page) + second_col = \ + picture_page.find(u'div', {'id': 'content'}).find(u'table').find(u'tr').find_all(u'td', recursive=False)[1] + + try: + picture_table = second_col.find(u'table', recursive=False) + character_info[u'pictures'] = [] + if picture_table: + character_info[u'pictures'] = map(lambda img: img.get(u'src').decode('utf-8'), + picture_table.find_all(u'img')) + except: + if not self.session.suppress_parse_exceptions: + raise + + return character_info + + def parse_clubs(self, clubs_page): + """Parses the DOM and returns character clubs attributes. + + :type clubs_page: :class:`bs4.BeautifulSoup` + :param clubs_page: MAL character clubs page's DOM + + :rtype: dict + :return: character clubs attributes. + + """ + character_info = self.parse_sidebar(clubs_page) + second_col = \ + clubs_page.find(u'div', {'id': 'content'}).find(u'table').find(u'tr').find_all(u'td', recursive=False)[1] + + try: + clubs_header = second_col.find(u'div', text=u'Related Clubs') + character_info[u'clubs'] = [] + if clubs_header: + curr_elt = clubs_header.nextSibling + while curr_elt is not None: + if curr_elt.name == u'div': + link = curr_elt.find(u'a') + club_id = int(re.match(r'/clubs\.php\?cid=(?P[0-9]+)', link.get(u'href')).group(u'id')) + num_members = int( + re.match(r'(?P[0-9]+) members', curr_elt.find(u'small').text).group(u'num')) + character_info[u'clubs'].append( + self.session.club(club_id).set({'name': link.text, 'num_members': num_members})) + curr_elt = curr_elt.nextSibling + except: + if not self.session.suppress_parse_exceptions: + raise + + return character_info + + def load(self): + """Fetches the MAL character page and sets the current character's attributes. + + :rtype: :class:`.Character` + :return: Current character object. + + """ + character = self.session.session.get(u'http://myanimelist.net/character/' + str(self.id)).text + self.set(self.parse(utilities.get_clean_dom(character))) + return self + + def load_favorites(self): + """Fetches the MAL character favorites page and sets the current character's favorites attributes. + + :rtype: :class:`.Character` + :return: Current character object. + + """ + character = self.session.session.get( + u'http://myanimelist.net/character/' + str(self.id) + u'/' + utilities.urlencode( + self.name) + u'/favorites').text + self.set(self.parse_favorites(utilities.get_clean_dom(character))) + return self + + def load_pictures(self): + """Fetches the MAL character pictures page and sets the current character's pictures attributes. + + :rtype: :class:`.Character` + :return: Current character object. + + """ + character = self.session.session.get( + u'http://myanimelist.net/character/' + str(self.id) + u'/' + utilities.urlencode( + self.name) + u'/pictures').text + self.set(self.parse_pictures(utilities.get_clean_dom(character))) + return self + + def load_clubs(self): + """Fetches the MAL character clubs page and sets the current character's clubs attributes. + + :rtype: :class:`.Character` + :return: Current character object. + + """ + character = self.session.session.get( + u'http://myanimelist.net/character/' + str(self.id) + u'/' + utilities.urlencode( + self.name) + u'/clubs').text + self.set(self.parse_clubs(utilities.get_clean_dom(character))) + return self + + @property + @loadable(u'load') + def name(self): + """Character name. + """ + return self._name + + @property + @loadable(u'load') + def full_name(self): + """Character's full name. + """ + return self._full_name + + @property + @loadable(u'load') + def name_jpn(self): + """Character's Japanese name. + """ + return self._name_jpn + + @property + @loadable(u'load') + def description(self): + """Character's description. + """ + return self._description + + @property + @loadable(u'load') + def voice_actors(self): + """Voice actor dict for this character, with :class:`myanimelist.person.Person` objects as keys and the language as values. + """ + return self._voice_actors + + @property + @loadable(u'load') + def animeography(self): + """Anime appearance dict for this character, with :class:`myanimelist.anime.Anime` objects as keys and the type of role as values, e.g. 'Main' + """ + return self._animeography + + @property + @loadable(u'load') + def mangaography(self): + """Manga appearance dict for this character, with :class:`myanimelist.manga.Manga` objects as keys and the type of role as values, e.g. 'Main' + """ + return self._mangaography + + @property + @loadable(u'load') + def num_favorites(self): + """Number of users who have favourited this character. + """ + return self._num_favorites + + @property + @loadable(u'load_favorites') + def favorites(self): + """List of users who have favourited this character. + """ + return self._favorites + + @property + @loadable(u'load') + def picture(self): + """URL of primary picture for this character. + """ + return self._picture + + @property + @loadable(u'load_pictures') + def pictures(self): + """List of picture URLs for this character. + """ + return self._pictures + + @property + @loadable(u'load_clubs') + def clubs(self): + """List of clubs relevant to this character. + """ + return self._clubs diff --git a/myanimelist/club.py b/myanimelist/club.py index a53a4cc..04deb58 100644 --- a/myanimelist/club.py +++ b/myanimelist/club.py @@ -7,31 +7,34 @@ import utilities from base import Base, MalformedPageError, InvalidBaseError, loadable + class MalformedClubPageError(MalformedPageError): - pass + pass -class InvalidClubError(InvalidBaseError): - pass -class Club(Base): - def __init__(self, session, club_id): - super(Club, self).__init__(session) - self.id = club_id - if not isinstance(self.id, int) or int(self.id) < 1: - raise InvalidClubError(self.id) - self._name = None - self._num_members = None - - def load(self): - # TODO +class InvalidClubError(InvalidBaseError): pass - @property - @loadable(u'load') - def name(self): - return self._name - @property - @loadable(u'load') - def num_members(self): - return self._num_members +class Club(Base): + def __init__(self, session, club_id): + super(Club, self).__init__(session) + self.id = club_id + if not isinstance(self.id, int) or int(self.id) < 1: + raise InvalidClubError(self.id) + self._name = None + self._num_members = None + + def load(self): + # TODO + pass + + @property + @loadable(u'load') + def name(self): + return self._name + + @property + @loadable(u'load') + def num_members(self): + return self._num_members diff --git a/myanimelist/genre.py b/myanimelist/genre.py index cacfb99..ceb550f 100644 --- a/myanimelist/genre.py +++ b/myanimelist/genre.py @@ -7,25 +7,28 @@ import utilities from base import Base, MalformedPageError, InvalidBaseError, loadable + class MalformedGenrePageError(MalformedPageError): - pass + pass + class InvalidGenreError(InvalidBaseError): - pass + pass + class Genre(Base): - def __init__(self, session, genre_id): - super(Genre, self).__init__(session) - self.id = genre_id - if not isinstance(self.id, int) or int(self.id) < 1: - raise InvalidGenreError(self.id) - self._name = None - - def load(self): - # TODO - pass + def __init__(self, session, genre_id): + super(Genre, self).__init__(session) + self.id = genre_id + if not isinstance(self.id, int) or int(self.id) < 1: + raise InvalidGenreError(self.id) + self._name = None + + def load(self): + # TODO + pass - @property - @loadable(u'load') - def name(self): - return self._name + @property + @loadable(u'load') + def name(self): + return self._name diff --git a/myanimelist/manga.py b/myanimelist/manga.py index c85f1d8..26d0e1d 100644 --- a/myanimelist/manga.py +++ b/myanimelist/manga.py @@ -4,190 +4,197 @@ from base import Base, Error, loadable import media -class MalformedMangaPageError(media.MalformedMediaPageError): - """Indicates that a manga-related page on MAL has irreparably broken markup in some way. - """ - pass - -class InvalidMangaError(media.InvalidMediaError): - """Indicates that the manga requested does not exist on MAL. - """ - pass - -class Manga(media.Media): - """Primary interface to manga resources on MAL. - """ - _status_terms = [ - u'Unknown', - u'Publishing', - u'Finished', - u'Not yet published' - ] - _consuming_verb = "read" - - def __init__(self, session, manga_id): - """Creates a new instance of Manga. - - :type session: :class:`myanimelist.session.Session` - :param session: A valid MAL session - :type manga_id: int - :param manga_id: The desired manga's ID on MAL - - :raises: :class:`.InvalidMangaError` +class MalformedMangaPageError(media.MalformedMediaPageError): + """Indicates that a manga-related page on MAL has irreparably broken markup in some way. """ - if not isinstance(manga_id, int) or int(manga_id) < 1: - raise InvalidMangaError(manga_id) - super(Manga, self).__init__(session, manga_id) - self._volumes = None - self._chapters = None - self._published = None - self._authors = None - self._serialization = None + pass - def parse_sidebar(self, manga_page): - """Parses the DOM and returns manga attributes in the sidebar. - :type manga_page: :class:`bs4.BeautifulSoup` - :param manga_page: MAL manga page's DOM +class InvalidMangaError(media.InvalidMediaError): + """Indicates that the manga requested does not exist on MAL. + """ + pass - :rtype: dict - :return: manga attributes - :raises: :class:`.InvalidMangaError`, :class:`.MalformedMangaPageError` - """ - # if MAL says the series doesn't exist, raise an InvalidMangaError. - error_tag = manga_page.find(u'div', {'class': 'badresult'}) - if error_tag: - raise InvalidMangaError(self.id) - - try: - title_tag = manga_page.find(u'div', {'id': 'contentWrapper'}).find(u'h1') - if not title_tag.find(u'div'): - # otherwise, raise a MalformedMangaPageError. - raise MalformedMangaPageError(self.id, manga_page, message="Could not find title div") - except: - if not self.session.suppress_parse_exceptions: - raise - - # otherwise, begin parsing. - manga_info = super(Manga, self).parse_sidebar(manga_page) - - info_panel_first = manga_page.find(u'div', {'id': 'content'}).find(u'table').find(u'td') - - try: - volumes_tag = info_panel_first.find(text=u'Volumes:').parent.parent - utilities.extract_tags(volumes_tag.find_all(u'span', {'class': 'dark_text'})) - manga_info[u'volumes'] = int(volumes_tag.text.strip()) if volumes_tag.text.strip() != 'Unknown' else None - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - chapters_tag = info_panel_first.find(text=u'Chapters:').parent.parent - utilities.extract_tags(chapters_tag.find_all(u'span', {'class': 'dark_text'})) - manga_info[u'chapters'] = int(chapters_tag.text.strip()) if chapters_tag.text.strip() != 'Unknown' else None - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - published_tag = info_panel_first.find(text=u'Published:').parent.parent - utilities.extract_tags(published_tag.find_all(u'span', {'class': 'dark_text'})) - published_parts = published_tag.text.strip().split(u' to ') - if len(published_parts) == 1: - # this published once. - try: - published_date = utilities.parse_profile_date(published_parts[0]) - except ValueError: - raise MalformedMangaPageError(self.id, published_parts[0], message="Could not parse single publish date") - manga_info[u'published'] = (published_date,) - else: - # two publishing dates. - try: - publish_start = utilities.parse_profile_date(published_parts[0]) - except ValueError: - raise MalformedMangaPageError(self.id, published_parts[0], message="Could not parse first of two publish dates") - if published_parts == u'?': - # this is still publishing. - publish_end = None - else: - try: - publish_end = utilities.parse_profile_date(published_parts[1]) - except ValueError: - raise MalformedMangaPageError(self.id, published_parts[1], message="Could not parse second of two publish dates") - manga_info[u'published'] = (publish_start, publish_end) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - authors_tag = info_panel_first.find(text=u'Authors:').parent.parent - utilities.extract_tags(authors_tag.find_all(u'span', {'class': 'dark_text'})) - manga_info[u'authors'] = {} - for author_link in authors_tag.find_all('a'): - link_parts = author_link.get('href').split('/') - # of the form /people/1867/Naoki_Urasawa - person = self.session.person(int(link_parts[2])).set({'name': author_link.text}) - role = author_link.nextSibling.replace(' (', '').replace(')', '') - manga_info[u'authors'][person] = role - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - serialization_tag = info_panel_first.find(text=u'Serialization:').parent.parent - publication_link = serialization_tag.find('a') - manga_info[u'serialization'] = None - if publication_link: - link_parts = publication_link.get('href').split('mid=') - # of the form /manga.php?mid=1 - manga_info[u'serialization'] = self.session.publication(int(link_parts[1])).set({'name': publication_link.text}) - except: - if not self.session.suppress_parse_exceptions: - raise - - return manga_info - - @property - @loadable(u'load') - def volumes(self): - """The number of volumes in this manga. +class Manga(media.Media): + """Primary interface to manga resources on MAL. """ - return self._volumes + _status_terms = [ + u'Unknown', + u'Publishing', + u'Finished', + u'Not yet published' + ] + _consuming_verb = "read" + + def __init__(self, session, manga_id): + """Creates a new instance of Manga. + + :type session: :class:`myanimelist.session.Session` + :param session: A valid MAL session + :type manga_id: int + :param manga_id: The desired manga's ID on MAL + + :raises: :class:`.InvalidMangaError` + + """ + if not isinstance(manga_id, int) or int(manga_id) < 1: + raise InvalidMangaError(manga_id) + super(Manga, self).__init__(session, manga_id) + self._volumes = None + self._chapters = None + self._published = None + self._authors = None + self._serialization = None + + def parse_sidebar(self, manga_page): + """Parses the DOM and returns manga attributes in the sidebar. + + :type manga_page: :class:`bs4.BeautifulSoup` + :param manga_page: MAL manga page's DOM + + :rtype: dict + :return: manga attributes + + :raises: :class:`.InvalidMangaError`, :class:`.MalformedMangaPageError` + """ + # if MAL says the series doesn't exist, raise an InvalidMangaError. + error_tag = manga_page.find(u'div', {'class': 'badresult'}) + if error_tag: + raise InvalidMangaError(self.id) - @property - @loadable(u'load') - def chapters(self): - """The number of chapters in this manga. - """ - return self._chapters + try: + title_tag = manga_page.find(u'div', {'id': 'contentWrapper'}).find(u'h1') + if not title_tag.find(u'div'): + # otherwise, raise a MalformedMangaPageError. + raise MalformedMangaPageError(self.id, manga_page, message="Could not find title div") + except: + if not self.session.suppress_parse_exceptions: + raise - @property - @loadable(u'load') - def published(self): - """A tuple(2) containing up to two :class:`datetime.date` objects representing the start and end dates of this manga's publishing. + # otherwise, begin parsing. + manga_info = super(Manga, self).parse_sidebar(manga_page) - Potential configurations: + info_panel_first = manga_page.find(u'div', {'id': 'content'}).find(u'table').find(u'td') - None -- Completely-unknown publishing dates. + try: + volumes_tag = info_panel_first.find(text=u'Volumes:').parent.parent + utilities.extract_tags(volumes_tag.find_all(u'span', {'class': 'dark_text'})) + manga_info[u'volumes'] = int(volumes_tag.text.strip()) if volumes_tag.text.strip() != 'Unknown' else None + except: + if not self.session.suppress_parse_exceptions: + raise - (:class:`datetime.date`, None) -- Manga start date is known, end date is unknown. + try: + chapters_tag = info_panel_first.find(text=u'Chapters:').parent.parent + utilities.extract_tags(chapters_tag.find_all(u'span', {'class': 'dark_text'})) + manga_info[u'chapters'] = int(chapters_tag.text.strip()) if chapters_tag.text.strip() != 'Unknown' else None + except: + if not self.session.suppress_parse_exceptions: + raise - (:class:`datetime.date`, :class:`datetime.date`) -- Manga start and end dates are known. - """ - return self._published + try: + published_tag = info_panel_first.find(text=u'Published:').parent.parent + utilities.extract_tags(published_tag.find_all(u'span', {'class': 'dark_text'})) + published_parts = published_tag.text.strip().split(u' to ') + if len(published_parts) == 1: + # this published once. + try: + published_date = utilities.parse_profile_date(published_parts[0]) + except ValueError: + raise MalformedMangaPageError(self.id, published_parts[0], + message="Could not parse single publish date") + manga_info[u'published'] = (published_date,) + else: + # two publishing dates. + try: + publish_start = utilities.parse_profile_date(published_parts[0]) + except ValueError: + raise MalformedMangaPageError(self.id, published_parts[0], + message="Could not parse first of two publish dates") + if published_parts == u'?': + # this is still publishing. + publish_end = None + else: + try: + publish_end = utilities.parse_profile_date(published_parts[1]) + except ValueError: + raise MalformedMangaPageError(self.id, published_parts[1], + message="Could not parse second of two publish dates") + manga_info[u'published'] = (publish_start, publish_end) + except: + if not self.session.suppress_parse_exceptions: + raise - @property - @loadable(u'load') - def authors(self): - """An author dict with :class:`myanimelist.person.Person` objects of the authors as keys, and strings describing the duties of these authors as values. - """ - return self._authors + try: + authors_tag = info_panel_first.find(text=u'Authors:').parent.parent + utilities.extract_tags(authors_tag.find_all(u'span', {'class': 'dark_text'})) + manga_info[u'authors'] = {} + for author_link in authors_tag.find_all('a'): + link_parts = author_link.get('href').split('/') + # of the form /people/1867/Naoki_Urasawa + person = self.session.person(int(link_parts[2])).set({'name': author_link.text}) + role = author_link.nextSibling.replace(' (', '').replace(')', '') + manga_info[u'authors'][person] = role + except: + if not self.session.suppress_parse_exceptions: + raise - @property - @loadable(u'load') - def serialization(self): - """The :class:`myanimelist.publication.Publication` involved in the first serialization of this manga. - """ - return self._serialization \ No newline at end of file + try: + serialization_tag = info_panel_first.find(text=u'Serialization:').parent.parent + publication_link = serialization_tag.find('a') + manga_info[u'serialization'] = None + if publication_link: + link_parts = publication_link.get('href').split('mid=') + # of the form /manga.php?mid=1 + manga_info[u'serialization'] = self.session.publication(int(link_parts[1])).set( + {'name': publication_link.text}) + except: + if not self.session.suppress_parse_exceptions: + raise + + return manga_info + + @property + @loadable(u'load') + def volumes(self): + """The number of volumes in this manga. + """ + return self._volumes + + @property + @loadable(u'load') + def chapters(self): + """The number of chapters in this manga. + """ + return self._chapters + + @property + @loadable(u'load') + def published(self): + """A tuple(2) containing up to two :class:`datetime.date` objects representing the start and end dates of this manga's publishing. + + Potential configurations: + + None -- Completely-unknown publishing dates. + + (:class:`datetime.date`, None) -- Manga start date is known, end date is unknown. + + (:class:`datetime.date`, :class:`datetime.date`) -- Manga start and end dates are known. + """ + return self._published + + @property + @loadable(u'load') + def authors(self): + """An author dict with :class:`myanimelist.person.Person` objects of the authors as keys, and strings describing the duties of these authors as values. + """ + return self._authors + + @property + @loadable(u'load') + def serialization(self): + """The :class:`myanimelist.publication.Publication` involved in the first serialization of this manga. + """ + return self._serialization diff --git a/myanimelist/manga_list.py b/myanimelist/manga_list.py index 764bc3a..6245c51 100644 --- a/myanimelist/manga_list.py +++ b/myanimelist/manga_list.py @@ -5,73 +5,75 @@ from base import Base, Error, loadable import media_list + class MangaList(media_list.MediaList): - __id_attribute = "username" - def __init__(self, session, user_name): - super(MangaList, self).__init__(session, user_name) - - @property - def type(self): - return "manga" - - @property - def verb(self): - return "read" - - def parse_entry_media_attributes(self, soup): - attributes = super(MangaList, self).parse_entry_media_attributes(soup) - - try: - attributes['chapters'] = int(soup.find('series_chapters').text) - except ValueError: - attributes['chapters'] = None - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - attributes['volumes'] = int(soup.find('series_volumes').text) - except ValueError: - attributes['volumes'] = None - except: - if not self.session.suppress_parse_exceptions: - raise - - return attributes - - def parse_entry(self, soup): - manga,entry_info = super(MangaList, self).parse_entry(soup) - - try: - entry_info[u'chapters_read'] = int(soup.find('my_read_chapters').text) - except ValueError: - entry_info[u'chapters_read'] = 0 - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - entry_info[u'volumes_read'] = int(soup.find('my_read_volumes').text) - except ValueError: - entry_info[u'volumes_read'] = 0 - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - entry_info[u'rereading'] = bool(soup.find('my_rereadingg').text) - except ValueError: - entry_info[u'rereading'] = False - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - entry_info[u'chapters_reread'] = int(soup.find('my_rereading_chap').text) - except ValueError: - entry_info[u'chapters_reread'] = 0 - except: - if not self.session.suppress_parse_exceptions: - raise - - return manga,entry_info + __id_attribute = "username" + + def __init__(self, session, user_name): + super(MangaList, self).__init__(session, user_name) + + @property + def type(self): + return "manga" + + @property + def verb(self): + return "read" + + def parse_entry_media_attributes(self, soup): + attributes = super(MangaList, self).parse_entry_media_attributes(soup) + + try: + attributes['chapters'] = int(soup.find('series_chapters').text) + except ValueError: + attributes['chapters'] = None + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + attributes['volumes'] = int(soup.find('series_volumes').text) + except ValueError: + attributes['volumes'] = None + except: + if not self.session.suppress_parse_exceptions: + raise + + return attributes + + def parse_entry(self, soup): + manga, entry_info = super(MangaList, self).parse_entry(soup) + + try: + entry_info[u'chapters_read'] = int(soup.find('my_read_chapters').text) + except ValueError: + entry_info[u'chapters_read'] = 0 + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + entry_info[u'volumes_read'] = int(soup.find('my_read_volumes').text) + except ValueError: + entry_info[u'volumes_read'] = 0 + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + entry_info[u'rereading'] = bool(soup.find('my_rereadingg').text) + except ValueError: + entry_info[u'rereading'] = False + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + entry_info[u'chapters_reread'] = int(soup.find('my_rereading_chap').text) + except ValueError: + entry_info[u'chapters_reread'] = 0 + except: + if not self.session.suppress_parse_exceptions: + raise + + return manga, entry_info diff --git a/myanimelist/media.py b/myanimelist/media.py index be39114..e577707 100644 --- a/myanimelist/media.py +++ b/myanimelist/media.py @@ -8,592 +8,603 @@ import utilities from base import Base, MalformedPageError, InvalidBaseError, loadable -class MalformedMediaPageError(MalformedPageError): - """Indicates that a media-related page on MAL has broken markup in some way. - """ - pass - -class InvalidMediaError(InvalidBaseError): - """Indicates that the media requested does not exist on MAL. - """ - pass - -class Media(Base): - """Abstract base class for all media resources on MAL. - - To subclass, create a class that inherits from Media, implementing status_terms and consuming_verb at the bare minimum. - """ - __metaclass__ = abc.ABCMeta - - @abc.abstractproperty - def _status_terms(self): - """ - :rtype: dict - A status dict with:: - - keys -- int statuses - values -- string statuses e.g. "Airing" - """ - pass - @abc.abstractproperty - def _consuming_verb(self): - """ - :rtype: str - :return: the verb used to consume this media, e.g. "read" +class MalformedMediaPageError(MalformedPageError): + """Indicates that a media-related page on MAL has broken markup in some way. """ pass - @classmethod - def newest(cls, session): - """Fetches the latest media added to MAL. - - :type session: :class:`myanimelist.session.Session` - :param session: A valid MAL session - - :rtype: :class:`.Media` - :return: the newest media on MAL - - :raises: :class:`.MalformedMediaPageError` - - """ - media_type = cls.__name__.lower() - p = session.session.get(u'http://myanimelist.net/' + media_type + '.php?o=9&c[]=a&c[]=d&cv=2&w=1').text - soup = utilities.get_clean_dom(p) - latest_entry = soup.find(u"div", {u"class": u"hoverinfo"}) - if not latest_entry: - raise MalformedMediaPageError(0, p, u"No media entries found on recently-added page") - latest_id = int(latest_entry[u'rel'][1:]) - return getattr(session, media_type)(latest_id) - - def __init__(self, session, id): - """Creates an instance of Media. - - :type session: :class:`myanimelist.session.Session` - :param session: A valid MAL session. - - :type id: int - :param id: The media's ID. - - :raises: :class:`.InvalidMediaError` - - """ - super(Media, self).__init__(session) - self.id = id - if not isinstance(self.id, int) or int(self.id) < 1: - raise InvalidMediaError(self.id) - self._title = None - self._picture = None - self._alternative_titles = None - self._type = None - self._status = None - self._genres = None - self._score = None - self._rank = None - self._popularity = None - self._members = None - self._favorites = None - self._popular_tags = None - self._synopsis = None - self._related = None - self._characters = None - self._score_stats = None - self._status_stats = None - - def parse_sidebar(self, media_page): - """Parses the DOM and returns media attributes in the sidebar. - - :type media_page: :class:`bs4.BeautifulSoup` - :param media_page: MAL media page's DOM - - :rtype: dict - :return: media attributes. - - :raises: InvalidMediaError, MalformedMediaPageError - - """ - media_info = {} - - # if MAL says the series doesn't exist, raise an InvalidMediaError. - error_tag = media_page.find(u'div', {'class': 'badresult'}) - if error_tag: - raise InvalidMediaError(self.id) - - try: - title_tag = media_page.find(u'div', {'id': 'contentWrapper'}).find(u'h1') - if not title_tag.find(u'div'): - # otherwise, raise a MalformedMediaPageError. - raise MalformedMediaPageError(self.id, media_page, message="Could not find title div") - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - utilities.extract_tags(title_tag.find_all()) - media_info[u'title'] = title_tag.text.strip() - except: - if not self.session.suppress_parse_exceptions: - raise - - info_panel_first = media_page.find(u'div', {'id': 'content'}).find(u'table').find(u'td') - - try: - picture_tag = info_panel_first.find(u'img') - media_info[u'picture'] = picture_tag.get(u'src').decode('utf-8') - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - # assemble alternative titles for this series. - media_info[u'alternative_titles'] = {} - alt_titles_header = info_panel_first.find(u'h2', text=u'Alternative Titles') - if alt_titles_header: - next_tag = alt_titles_header.find_next_sibling(u'div', {'class': 'spaceit_pad'}) - while True: - if next_tag is None or not next_tag.find(u'span', {'class': 'dark_text'}): - # not a language node, break. - break - # get language and remove the node. - language = next_tag.find(u'span').text[:-1] - utilities.extract_tags(next_tag.find_all(u'span', {'class': 'dark_text'})) - names = next_tag.text.strip().split(u', ') - media_info[u'alternative_titles'][language] = names - next_tag = next_tag.find_next_sibling(u'div', {'class': 'spaceit_pad'}) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - type_tag = info_panel_first.find(text=u'Type:').parent.parent - utilities.extract_tags(type_tag.find_all(u'span', {'class': 'dark_text'})) - media_info[u'type'] = type_tag.text.strip() - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - status_tag = info_panel_first.find(text=u'Status:').parent.parent - utilities.extract_tags(status_tag.find_all(u'span', {'class': 'dark_text'})) - media_info[u'status'] = status_tag.text.strip() - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - genres_tag = info_panel_first.find(text=u'Genres:').parent.parent - utilities.extract_tags(genres_tag.find_all(u'span', {'class': 'dark_text'})) - media_info[u'genres'] = [] - for genre_link in genres_tag.find_all('a'): - link_parts = genre_link.get('href').split('[]=') - # of the form /anime|manga.php?genre[]=1 - genre = self.session.genre(int(link_parts[1])).set({'name': genre_link.text}) - media_info[u'genres'].append(genre) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - # grab statistics for this media. - score_tag = info_panel_first.find(text=u'Score:').parent.parent - # get score and number of users. - users_node = [x for x in score_tag.find_all(u'small') if u'scored by' in x.text][0] - num_users = int(users_node.text.split(u'scored by ')[-1].split(u' users')[0]) - utilities.extract_tags(score_tag.find_all()) - media_info[u'score'] = (decimal.Decimal(score_tag.text.strip()), num_users) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - rank_tag = info_panel_first.find(text=u'Ranked:').parent.parent - utilities.extract_tags(rank_tag.find_all()) - media_info[u'rank'] = int(rank_tag.text.strip()[1:].replace(u',', '')) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - popularity_tag = info_panel_first.find(text=u'Popularity:').parent.parent - utilities.extract_tags(popularity_tag.find_all()) - media_info[u'popularity'] = int(popularity_tag.text.strip()[1:].replace(u',', '')) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - members_tag = info_panel_first.find(text=u'Members:').parent.parent - utilities.extract_tags(members_tag.find_all()) - media_info[u'members'] = int(members_tag.text.strip().replace(u',', '')) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - favorites_tag = info_panel_first.find(text=u'Favorites:').parent.parent - utilities.extract_tags(favorites_tag.find_all()) - media_info[u'favorites'] = int(favorites_tag.text.strip().replace(u',', '')) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - # get popular tags. - tags_header = media_page.find(u'h2', text=u'Popular Tags') - tags_tag = tags_header.find_next_sibling(u'span') - media_info[u'popular_tags'] = {} - for tag_link in tags_tag.find_all('a'): - tag = self.session.tag(tag_link.text) - num_people = int(re.match(r'(?P[0-9]+) people', tag_link.get('title')).group('people')) - media_info[u'popular_tags'][tag] = num_people - except: - if not self.session.suppress_parse_exceptions: - raise - - return media_info - - def parse(self, media_page): - """Parses the DOM and returns media attributes in the main-content area. - - :type media_page: :class:`bs4.BeautifulSoup` - :param media_page: MAL media page's DOM - - :rtype: dict - :return: media attributes. - - """ - media_info = self.parse_sidebar(media_page) - - try: - synopsis_elt = media_page.find(u'h2', text=u'Synopsis').parent - utilities.extract_tags(synopsis_elt.find_all(u'h2')) - media_info[u'synopsis'] = synopsis_elt.text.strip() - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - related_title = media_page.find(u'h2', text=u'Related ' + self.__class__.__name__) - if related_title: - related_elt = related_title.parent - utilities.extract_tags(related_elt.find_all(u'h2')) - related = {} - for link in related_elt.find_all(u'a'): - href = link.get(u'href').replace(u'http://myanimelist.net', '') - if not re.match(r'/(anime|manga)', href): - break - curr_elt = link.previous_sibling - if curr_elt is None: - # we've reached the end of the list. - break - related_type = None - while True: - if not curr_elt: - raise MalformedAnimePageError(self.id, related_elt, message="Prematurely reached end of related anime listing") - if isinstance(curr_elt, bs4.NavigableString): - type_match = re.match(u'(?P[a-zA-Z\ \-]+):', curr_elt) - if type_match: - related_type = type_match.group(u'type') - break - curr_elt = curr_elt.previous_sibling - title = link.text - # parse link: may be manga or anime. - href_parts = href.split(u'/') - # sometimes links on MAL are broken, of the form /anime// - if href_parts[2] == '': - continue - # of the form: /(anime|manga)/1/Cowboy_Bebop - obj_id = int(href_parts[2]) - new_obj = getattr(self.session, href_parts[1])(obj_id).set({'title': title}) - if related_type not in related: - related[related_type] = [new_obj] - else: - related[related_type].append(new_obj) - media_info[u'related'] = related - else: - media_info[u'related'] = None - except: - if not self.session.suppress_parse_exceptions: - raise - - return media_info - - def parse_stats(self, media_page): - """Parses the DOM and returns media statistics attributes. - - :type media_page: :class:`bs4.BeautifulSoup` - :param media_page: MAL media stats page's DOM - - :rtype: dict - :return: media stats attributes. - - """ - media_info = self.parse_sidebar(media_page) - verb_progressive = self.consuming_verb + u'ing' - status_stats = { - verb_progressive: 0, - 'completed': 0, - 'on_hold': 0, - 'dropped': 0, - 'plan_to_' + self.consuming_verb: 0 - } - try: - consuming_elt = media_page.find(u'span', {'class': 'dark_text'}, text=verb_progressive.capitalize()) - if consuming_elt: - status_stats[verb_progressive] = int(consuming_elt.nextSibling.strip().replace(u',', '')) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - completed_elt = media_page.find(u'span', {'class': 'dark_text'}, text="Completed:") - if completed_elt: - status_stats[u'completed'] = int(completed_elt.nextSibling.strip().replace(u',', '')) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - on_hold_elt = media_page.find(u'span', {'class': 'dark_text'}, text="On-Hold:") - if on_hold_elt: - status_stats[u'on_hold'] = int(on_hold_elt.nextSibling.strip().replace(u',', '')) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - dropped_elt = media_page.find(u'span', {'class': 'dark_text'}, text="Dropped:") - if dropped_elt: - status_stats[u'dropped'] = int(dropped_elt.nextSibling.strip().replace(u',', '')) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - planning_elt = media_page.find(u'span', {'class': 'dark_text'}, text="Plan to " + self.consuming_verb.capitalize() + ":") - if planning_elt: - status_stats[u'plan_to_' + self.consuming_verb] = int(planning_elt.nextSibling.strip().replace(u',', '')) - except: - if not self.session.suppress_parse_exceptions: - raise - - media_info[u'status_stats'] = status_stats - - score_stats = { - 1: 0, - 2: 0, - 3: 0, - 4: 0, - 5: 0, - 6: 0, - 7: 0, - 8: 0, - 9: 0, - 10: 0 - } - try: - score_stats_header = media_page.find(u'h2', text='Score Stats') - if score_stats_header: - score_stats_table = score_stats_header.find_next_sibling(u'table') - if score_stats_table: - score_stats = {} - score_rows = score_stats_table.find_all(u'tr') - for i in xrange(len(score_rows)): - score_value = int(score_rows[i].find(u'td').text) - score_stats[score_value] = int(score_rows[i].find(u'small').text.replace(u'(u', '').replace(u' votes)', '')) - except: - if not self.session.suppress_parse_exceptions: - raise - - media_info[u'score_stats'] = score_stats - - return media_info - - def parse_characters(self, character_page): - """Parses the DOM and returns media character attributes in the sidebar. - - :type character_page: :class:`bs4.BeautifulSoup` - :param character_page: MAL character page's DOM - - :rtype: dict - :return: character attributes. - - """ - media_info = self.parse_sidebar(character_page) - - try: - character_title = filter(lambda x: u'Characters' in x.text, character_page.find_all(u'h2')) - media_info[u'characters'] = {} - if character_title: - character_title = character_title[0] - curr_elt = character_title.find_next_sibling(u'table') - while curr_elt: - curr_row = curr_elt.find(u'tr') - # character in second col. - character_col = curr_row.find_all(u'td', recursive=False)[1] - character_link = character_col.find(u'a') - character_name = ' '.join(reversed(character_link.text.split(u', '))) - link_parts = character_link.get(u'href').split(u'/') - # of the form /character/7373/Holo - character = self.session.character(int(link_parts[2])).set({'name': character_name}) - role = character_col.find(u'small').text - media_info[u'characters'][character] = {'role': role} - curr_elt = curr_elt.find_next_sibling(u'table') - except: - if not self.session.suppress_parse_exceptions: - raise - - return media_info - - def load(self): - """Fetches the MAL media page and sets the current media's attributes. - - :rtype: :class:`.Media` - :return: current media object. - - """ - media_page = self.session.session.get(u'http://myanimelist.net/' + self.__class__.__name__.lower() + u'/' + str(self.id)).text - self.set(self.parse(utilities.get_clean_dom(media_page))) - return self - - def load_stats(self): - """Fetches the MAL media statistics page and sets the current media's statistics attributes. - - :rtype: :class:`.Media` - :return: current media object. +class InvalidMediaError(InvalidBaseError): + """Indicates that the media requested does not exist on MAL. """ - stats_page = self.session.session.get(u'http://myanimelist.net/' + self.__class__.__name__.lower() + u'/' + str(self.id) + u'/' + utilities.urlencode(self.title) + u'/stats').text - self.set(self.parse_stats(utilities.get_clean_dom(stats_page))) - return self - - def load_characters(self): - """Fetches the MAL media characters page and sets the current media's character attributes. - - :rtype: :class:`.Media` - :return: current media object. - - """ - characters_page = self.session.session.get(u'http://myanimelist.net/' + self.__class__.__name__.lower() + u'/' + str(self.id) + u'/' + utilities.urlencode(self.title) + u'/characters').text - self.set(self.parse_characters(utilities.get_clean_dom(characters_page))) - return self - - @property - @loadable(u'load') - def title(self): - """Media's title. - """ - return self._title - - @property - @loadable(u'load') - def picture(self): - """URL of media's primary pictures. - """ - return self._picture - - @property - @loadable(u'load') - def alternative_titles(self): - """Alternative titles dict, with types of titles, e.g. 'Japanese', 'English', or 'Synonyms' as keys, and lists of said alternative titles as values. - """ - return self._alternative_titles - - @property - @loadable(u'load') - def type(self): - """Type of this media, e.g. 'TV' or 'Manga' or 'Movie' - """ - return self._type - - @property - @loadable(u'load') - def status(self): - """Publication status, e.g. 'Finished Airing' - """ - return self._status - - @property - @loadable(u'load') - def genres(self): - """A list of :class:`myanimelist.genre.Genre` objects associated with this media. - """ - return self._genres - - @property - @loadable(u'load') - def score(self): - """A tuple(2) containing an instance of decimal.Decimal storing the aggregate score, weighted or non-weighted, and an int storing the number of ratings - - """ - return self._score - - @property - @loadable(u'load') - def rank(self): - """Score rank. - """ - return self._rank - - @property - @loadable(u'load') - def popularity(self): - """Popularity rank. - """ - return self._popularity - - @property - @loadable(u'load') - def members(self): - """Number of members. - """ - return self._members - - @property - @loadable(u'load') - def favorites(self): - """Number of users who favourited this media. - """ - return self._favorites - - @property - @loadable(u'load') - def popular_tags(self): - """Tags dict with :class:`myanimelist.tag.Tag` objects as keys, and the number of tags as values. - """ - return self._popular_tags - - @property - @loadable(u'load') - def synopsis(self): - """Media synopsis. - """ - return self._synopsis - - @property - @loadable(u'load') - def related(self): - """Related media dict, with strings of relation types, e.g. 'Sequel' as keys, and lists containing instances of :class:`.Media` subclasses as values. - """ - return self._related + pass - @property - @loadable(u'load_characters') - def characters(self): - """Character dict, with :class:`myanimelist.character.Character` objects as keys, and a dict with attributes of this role, e.g. 'role': 'Main' as values. - """ - return self._characters - @property - @loadable(u'load_stats') - def status_stats(self): - """Status statistics dict, with strings of statuses, e.g. 'on_hold' as keys, and an int number of users as values. - """ - return self._status_stats +class Media(Base): + """Abstract base class for all media resources on MAL. - @property - @loadable(u'load_stats') - def score_stats(self): - """Score statistics dict, with int scores from 1-10 as keys, and an int number of users as values. + To subclass, create a class that inherits from Media, implementing status_terms and consuming_verb at the bare minimum. """ - return self._score_stats \ No newline at end of file + __metaclass__ = abc.ABCMeta + + @abc.abstractproperty + def _status_terms(self): + """ + :rtype: dict + A status dict with:: + + keys -- int statuses + values -- string statuses e.g. "Airing" + """ + pass + + @abc.abstractproperty + def _consuming_verb(self): + """ + :rtype: str + :return: the verb used to consume this media, e.g. "read" + """ + pass + + @classmethod + def newest(cls, session): + """Fetches the latest media added to MAL. + + :type session: :class:`myanimelist.session.Session` + :param session: A valid MAL session + + :rtype: :class:`.Media` + :return: the newest media on MAL + + :raises: :class:`.MalformedMediaPageError` + + """ + media_type = cls.__name__.lower() + p = session.session.get(u'http://myanimelist.net/' + media_type + '.php?o=9&c[]=a&c[]=d&cv=2&w=1').text + soup = utilities.get_clean_dom(p) + latest_entry = soup.find(u"div", {u"class": u"hoverinfo"}) + if not latest_entry: + raise MalformedMediaPageError(0, p, u"No media entries found on recently-added page") + latest_id = int(latest_entry[u'rel'][1:]) + return getattr(session, media_type)(latest_id) + + def __init__(self, session, id): + """Creates an instance of Media. + + :type session: :class:`myanimelist.session.Session` + :param session: A valid MAL session. + + :type id: int + :param id: The media's ID. + + :raises: :class:`.InvalidMediaError` + + """ + super(Media, self).__init__(session) + self.id = id + if not isinstance(self.id, int) or int(self.id) < 1: + raise InvalidMediaError(self.id) + self._title = None + self._picture = None + self._alternative_titles = None + self._type = None + self._status = None + self._genres = None + self._score = None + self._rank = None + self._popularity = None + self._members = None + self._favorites = None + self._popular_tags = None + self._synopsis = None + self._related = None + self._characters = None + self._score_stats = None + self._status_stats = None + + def parse_sidebar(self, media_page): + """Parses the DOM and returns media attributes in the sidebar. + + :type media_page: :class:`bs4.BeautifulSoup` + :param media_page: MAL media page's DOM + + :rtype: dict + :return: media attributes. + + :raises: InvalidMediaError, MalformedMediaPageError + + """ + media_info = {} + + # if MAL says the series doesn't exist, raise an InvalidMediaError. + error_tag = media_page.find(u'div', {'class': 'badresult'}) + if error_tag: + raise InvalidMediaError(self.id) + + try: + title_tag = media_page.find(u'div', {'id': 'contentWrapper'}).find(u'h1') + if not title_tag.find(u'div'): + # otherwise, raise a MalformedMediaPageError. + raise MalformedMediaPageError(self.id, media_page, message="Could not find title div") + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + utilities.extract_tags(title_tag.find_all()) + media_info[u'title'] = title_tag.text.strip() + except: + if not self.session.suppress_parse_exceptions: + raise + + info_panel_first = media_page.find(u'div', {'id': 'content'}).find(u'table').find(u'td') + + try: + picture_tag = info_panel_first.find(u'img') + media_info[u'picture'] = picture_tag.get(u'src').decode('utf-8') + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + # assemble alternative titles for this series. + media_info[u'alternative_titles'] = {} + alt_titles_header = info_panel_first.find(u'h2', text=u'Alternative Titles') + if alt_titles_header: + next_tag = alt_titles_header.find_next_sibling(u'div', {'class': 'spaceit_pad'}) + while True: + if next_tag is None or not next_tag.find(u'span', {'class': 'dark_text'}): + # not a language node, break. + break + # get language and remove the node. + language = next_tag.find(u'span').text[:-1] + utilities.extract_tags(next_tag.find_all(u'span', {'class': 'dark_text'})) + names = next_tag.text.strip().split(u', ') + media_info[u'alternative_titles'][language] = names + next_tag = next_tag.find_next_sibling(u'div', {'class': 'spaceit_pad'}) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + type_tag = info_panel_first.find(text=u'Type:').parent.parent + utilities.extract_tags(type_tag.find_all(u'span', {'class': 'dark_text'})) + media_info[u'type'] = type_tag.text.strip() + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + status_tag = info_panel_first.find(text=u'Status:').parent.parent + utilities.extract_tags(status_tag.find_all(u'span', {'class': 'dark_text'})) + media_info[u'status'] = status_tag.text.strip() + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + genres_tag = info_panel_first.find(text=u'Genres:').parent.parent + utilities.extract_tags(genres_tag.find_all(u'span', {'class': 'dark_text'})) + media_info[u'genres'] = [] + for genre_link in genres_tag.find_all('a'): + link_parts = genre_link.get('href').split('[]=') + # of the form /anime|manga.php?genre[]=1 + genre = self.session.genre(int(link_parts[1])).set({'name': genre_link.text}) + media_info[u'genres'].append(genre) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + # grab statistics for this media. + score_tag = info_panel_first.find(text=u'Score:').parent.parent + # get score and number of users. + users_node = [x for x in score_tag.find_all(u'small') if u'scored by' in x.text][0] + num_users = int(users_node.text.split(u'scored by ')[-1].split(u' users')[0]) + utilities.extract_tags(score_tag.find_all()) + media_info[u'score'] = (decimal.Decimal(score_tag.text.strip()), num_users) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + rank_tag = info_panel_first.find(text=u'Ranked:').parent.parent + utilities.extract_tags(rank_tag.find_all()) + media_info[u'rank'] = int(rank_tag.text.strip()[1:].replace(u',', '')) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + popularity_tag = info_panel_first.find(text=u'Popularity:').parent.parent + utilities.extract_tags(popularity_tag.find_all()) + media_info[u'popularity'] = int(popularity_tag.text.strip()[1:].replace(u',', '')) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + members_tag = info_panel_first.find(text=u'Members:').parent.parent + utilities.extract_tags(members_tag.find_all()) + media_info[u'members'] = int(members_tag.text.strip().replace(u',', '')) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + favorites_tag = info_panel_first.find(text=u'Favorites:').parent.parent + utilities.extract_tags(favorites_tag.find_all()) + media_info[u'favorites'] = int(favorites_tag.text.strip().replace(u',', '')) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + # get popular tags. + tags_header = media_page.find(u'h2', text=u'Popular Tags') + tags_tag = tags_header.find_next_sibling(u'span') + media_info[u'popular_tags'] = {} + for tag_link in tags_tag.find_all('a'): + tag = self.session.tag(tag_link.text) + num_people = int(re.match(r'(?P[0-9]+) people', tag_link.get('title')).group('people')) + media_info[u'popular_tags'][tag] = num_people + except: + if not self.session.suppress_parse_exceptions: + raise + + return media_info + + def parse(self, media_page): + """Parses the DOM and returns media attributes in the main-content area. + + :type media_page: :class:`bs4.BeautifulSoup` + :param media_page: MAL media page's DOM + + :rtype: dict + :return: media attributes. + + """ + media_info = self.parse_sidebar(media_page) + + try: + synopsis_elt = media_page.find(u'h2', text=u'Synopsis').parent + utilities.extract_tags(synopsis_elt.find_all(u'h2')) + media_info[u'synopsis'] = synopsis_elt.text.strip() + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + related_title = media_page.find(u'h2', text=u'Related ' + self.__class__.__name__) + if related_title: + related_elt = related_title.parent + utilities.extract_tags(related_elt.find_all(u'h2')) + related = {} + for link in related_elt.find_all(u'a'): + href = link.get(u'href').replace(u'http://myanimelist.net', '') + if not re.match(r'/(anime|manga)', href): + break + curr_elt = link.previous_sibling + if curr_elt is None: + # we've reached the end of the list. + break + related_type = None + while True: + if not curr_elt: + raise MalformedAnimePageError(self.id, related_elt, + message="Prematurely reached end of related anime listing") + if isinstance(curr_elt, bs4.NavigableString): + type_match = re.match(u'(?P[a-zA-Z\ \-]+):', curr_elt) + if type_match: + related_type = type_match.group(u'type') + break + curr_elt = curr_elt.previous_sibling + title = link.text + # parse link: may be manga or anime. + href_parts = href.split(u'/') + # sometimes links on MAL are broken, of the form /anime// + if href_parts[2] == '': + continue + # of the form: /(anime|manga)/1/Cowboy_Bebop + obj_id = int(href_parts[2]) + new_obj = getattr(self.session, href_parts[1])(obj_id).set({'title': title}) + if related_type not in related: + related[related_type] = [new_obj] + else: + related[related_type].append(new_obj) + media_info[u'related'] = related + else: + media_info[u'related'] = None + except: + if not self.session.suppress_parse_exceptions: + raise + + return media_info + + def parse_stats(self, media_page): + """Parses the DOM and returns media statistics attributes. + + :type media_page: :class:`bs4.BeautifulSoup` + :param media_page: MAL media stats page's DOM + + :rtype: dict + :return: media stats attributes. + + """ + media_info = self.parse_sidebar(media_page) + verb_progressive = self.consuming_verb + u'ing' + status_stats = { + verb_progressive: 0, + 'completed': 0, + 'on_hold': 0, + 'dropped': 0, + 'plan_to_' + self.consuming_verb: 0 + } + try: + consuming_elt = media_page.find(u'span', {'class': 'dark_text'}, text=verb_progressive.capitalize()) + if consuming_elt: + status_stats[verb_progressive] = int(consuming_elt.nextSibling.strip().replace(u',', '')) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + completed_elt = media_page.find(u'span', {'class': 'dark_text'}, text="Completed:") + if completed_elt: + status_stats[u'completed'] = int(completed_elt.nextSibling.strip().replace(u',', '')) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + on_hold_elt = media_page.find(u'span', {'class': 'dark_text'}, text="On-Hold:") + if on_hold_elt: + status_stats[u'on_hold'] = int(on_hold_elt.nextSibling.strip().replace(u',', '')) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + dropped_elt = media_page.find(u'span', {'class': 'dark_text'}, text="Dropped:") + if dropped_elt: + status_stats[u'dropped'] = int(dropped_elt.nextSibling.strip().replace(u',', '')) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + planning_elt = media_page.find(u'span', {'class': 'dark_text'}, + text="Plan to " + self.consuming_verb.capitalize() + ":") + if planning_elt: + status_stats[u'plan_to_' + self.consuming_verb] = int( + planning_elt.nextSibling.strip().replace(u',', '')) + except: + if not self.session.suppress_parse_exceptions: + raise + + media_info[u'status_stats'] = status_stats + + score_stats = { + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + 6: 0, + 7: 0, + 8: 0, + 9: 0, + 10: 0 + } + try: + score_stats_header = media_page.find(u'h2', text='Score Stats') + if score_stats_header: + score_stats_table = score_stats_header.find_next_sibling(u'table') + if score_stats_table: + score_stats = {} + score_rows = score_stats_table.find_all(u'tr') + for i in xrange(len(score_rows)): + score_value = int(score_rows[i].find(u'td').text) + score_stats[score_value] = int( + score_rows[i].find(u'small').text.replace(u'(u', '').replace(u' votes)', '')) + except: + if not self.session.suppress_parse_exceptions: + raise + + media_info[u'score_stats'] = score_stats + + return media_info + + def parse_characters(self, character_page): + """Parses the DOM and returns media character attributes in the sidebar. + + :type character_page: :class:`bs4.BeautifulSoup` + :param character_page: MAL character page's DOM + + :rtype: dict + :return: character attributes. + + """ + media_info = self.parse_sidebar(character_page) + + try: + character_title = filter(lambda x: u'Characters' in x.text, character_page.find_all(u'h2')) + media_info[u'characters'] = {} + if character_title: + character_title = character_title[0] + curr_elt = character_title.find_next_sibling(u'table') + while curr_elt: + curr_row = curr_elt.find(u'tr') + # character in second col. + character_col = curr_row.find_all(u'td', recursive=False)[1] + character_link = character_col.find(u'a') + character_name = ' '.join(reversed(character_link.text.split(u', '))) + link_parts = character_link.get(u'href').split(u'/') + # of the form /character/7373/Holo + character = self.session.character(int(link_parts[2])).set({'name': character_name}) + role = character_col.find(u'small').text + media_info[u'characters'][character] = {'role': role} + curr_elt = curr_elt.find_next_sibling(u'table') + except: + if not self.session.suppress_parse_exceptions: + raise + + return media_info + + def load(self): + """Fetches the MAL media page and sets the current media's attributes. + + :rtype: :class:`.Media` + :return: current media object. + + """ + media_page = self.session.session.get( + u'http://myanimelist.net/' + self.__class__.__name__.lower() + u'/' + str(self.id)).text + self.set(self.parse(utilities.get_clean_dom(media_page))) + return self + + def load_stats(self): + """Fetches the MAL media statistics page and sets the current media's statistics attributes. + + :rtype: :class:`.Media` + :return: current media object. + + """ + stats_page = self.session.session.get(u'http://myanimelist.net/' + self.__class__.__name__.lower() + u'/' + str( + self.id) + u'/' + utilities.urlencode(self.title) + u'/stats').text + self.set(self.parse_stats(utilities.get_clean_dom(stats_page))) + return self + + def load_characters(self): + """Fetches the MAL media characters page and sets the current media's character attributes. + + :rtype: :class:`.Media` + :return: current media object. + + """ + characters_page = self.session.session.get( + u'http://myanimelist.net/' + self.__class__.__name__.lower() + u'/' + str( + self.id) + u'/' + utilities.urlencode(self.title) + u'/characters').text + self.set(self.parse_characters(utilities.get_clean_dom(characters_page))) + return self + + @property + @loadable(u'load') + def title(self): + """Media's title. + """ + return self._title + + @property + @loadable(u'load') + def picture(self): + """URL of media's primary pictures. + """ + return self._picture + + @property + @loadable(u'load') + def alternative_titles(self): + """Alternative titles dict, with types of titles, e.g. 'Japanese', 'English', or 'Synonyms' as keys, and lists of said alternative titles as values. + """ + return self._alternative_titles + + @property + @loadable(u'load') + def type(self): + """Type of this media, e.g. 'TV' or 'Manga' or 'Movie' + """ + return self._type + + @property + @loadable(u'load') + def status(self): + """Publication status, e.g. 'Finished Airing' + """ + return self._status + + @property + @loadable(u'load') + def genres(self): + """A list of :class:`myanimelist.genre.Genre` objects associated with this media. + """ + return self._genres + + @property + @loadable(u'load') + def score(self): + """A tuple(2) containing an instance of decimal.Decimal storing the aggregate score, weighted or non-weighted, and an int storing the number of ratings + + """ + return self._score + + @property + @loadable(u'load') + def rank(self): + """Score rank. + """ + return self._rank + + @property + @loadable(u'load') + def popularity(self): + """Popularity rank. + """ + return self._popularity + + @property + @loadable(u'load') + def members(self): + """Number of members. + """ + return self._members + + @property + @loadable(u'load') + def favorites(self): + """Number of users who favourited this media. + """ + return self._favorites + + @property + @loadable(u'load') + def popular_tags(self): + """Tags dict with :class:`myanimelist.tag.Tag` objects as keys, and the number of tags as values. + """ + return self._popular_tags + + @property + @loadable(u'load') + def synopsis(self): + """Media synopsis. + """ + return self._synopsis + + @property + @loadable(u'load') + def related(self): + """Related media dict, with strings of relation types, e.g. 'Sequel' as keys, and lists containing instances of :class:`.Media` subclasses as values. + """ + return self._related + + @property + @loadable(u'load_characters') + def characters(self): + """Character dict, with :class:`myanimelist.character.Character` objects as keys, and a dict with attributes of this role, e.g. 'role': 'Main' as values. + """ + return self._characters + + @property + @loadable(u'load_stats') + def status_stats(self): + """Status statistics dict, with strings of statuses, e.g. 'on_hold' as keys, and an int number of users as values. + """ + return self._status_stats + + @property + @loadable(u'load_stats') + def score_stats(self): + """Score statistics dict, with int scores from 1-10 as keys, and an int number of users as values. + """ + return self._score_stats diff --git a/myanimelist/media_list.py b/myanimelist/media_list.py index 3e19160..e096b95 100644 --- a/myanimelist/media_list.py +++ b/myanimelist/media_list.py @@ -11,258 +11,264 @@ import utilities from base import Base, MalformedPageError, InvalidBaseError, loadable -class MalformedMediaListPageError(MalformedPageError): - pass - -class InvalidMediaListError(InvalidBaseError): - pass - -class MediaList(Base, collections.Mapping): - __metaclass__ = abc.ABCMeta - - __id_attribute = "username" - - def __getitem__(self, media): - return self.list[media] - def __contains__(self, media): - return media in self.list - - def __len__(self): - return len(self.list) - - def __iter__(self): - for media in self.list: - yield media +class MalformedMediaListPageError(MalformedPageError): + pass - def __init__(self, session, user_name): - super(MediaList, self).__init__(session) - self.username = user_name - if not isinstance(self.username, unicode) or len(self.username) < 1: - raise InvalidMediaListError(self.username) - self._list = None - self._stats = None - # subclasses must define a list type, ala "anime" or "manga" - @abc.abstractproperty - def type(self): +class InvalidMediaListError(InvalidBaseError): pass - # a list verb ala "watch", "read", etc - @abc.abstractproperty - def verb(self): - pass - # a list with status ints as indices and status texts as values. - @property - def user_status_terms(self): - statuses = collections.defaultdict(lambda: u'Unknown') - statuses[1] = self.verb.capitalize() + u'ing' - statuses[2] = u'Completed' - statuses[3] = u'On-Hold' - statuses[4] = u'Dropped' - statuses[6] = u'Plan to ' + self.verb.capitalize() - return statuses - - def parse_entry_media_attributes(self, soup): - """ - Args: - soup: a bs4 element containing a row from the current media list - - Return a dict of attributes of the media the row is about. - """ - row_info = {} - - try: - start = utilities.parse_profile_date(soup.find('series_start').text) - except ValueError: - start = None - except: - if not self.session.suppress_parse_exceptions: - raise - - if start is not None: - try: - row_info['aired'] = (start, utilities.parse_profile_date(soup.find('series_end').text)) - except ValueError: - row_info['aired'] = (start, None) - except: - if not self.session.suppress_parse_exceptions: - raise - - # look up the given media type's status terms. - status_terms = getattr(self.session, self.type)(1)._status_terms - - try: - row_info['id'] = int(soup.find('series_' + self.type + 'db_id').text) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - row_info['title'] = soup.find('series_title').text - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - row_info['status'] = status_terms[int(soup.find('series_status').text)] - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - row_info['picture'] = soup.find('series_image').text - except: - if not self.session.suppress_parse_exceptions: - raise - - return row_info - - def parse_entry(self, soup): - """ - Given: - soup: a bs4 element containing a row from the current media list - - Return a tuple: - (media object, dict of this row's parseable attributes) - """ - # parse the media object first. - media_attrs = self.parse_entry_media_attributes(soup) - media_id = media_attrs[u'id'] - del media_attrs[u'id'] - media = getattr(self.session, self.type)(media_id).set(media_attrs) - - entry_info = {} - try: - entry_info[u'started'] = utilities.parse_profile_date(soup.find(u'my_start_date').text) - except ValueError: - entry_info[u'started'] = None - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - entry_info[u'finished'] = utilities.parse_profile_date(soup.find(u'my_finish_date').text) - except ValueError: - entry_info[u'finished'] = None - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - entry_info[u'status'] = self.user_status_terms[int(soup.find(u'my_status').text)] - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - entry_info[u'score'] = int(soup.find(u'my_score').text) - # if user hasn't set a score, set it to None to indicate as such. - if entry_info[u'score'] == 0: - entry_info[u'score'] = None - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - entry_info[u'last_updated'] = datetime.datetime.fromtimestamp(int(soup.find(u'my_last_updated').text)) - except: - if not self.session.suppress_parse_exceptions: - raise - - return media,entry_info - - def parse_stats(self, soup): - """ - Given: - soup: a bs4 element containing the current media list's stats - - Return a dict of this media list's stats. - """ - stats = {} - for row in soup.children: - try: - key = row.name.replace(u'user_', u'') - if key == u'id': - stats[key] = int(row.text) - elif key == u'name': - stats[key] = row.text - elif key == self.verb + u'ing': - try: - stats[key] = int(row.text) - except ValueError: - stats[key] = 0 - elif key == u'completed': - try: - stats[key] = int(row.text) - except ValueError: - stats[key] = 0 - elif key == u'onhold': - try: - stats['on_hold'] = int(row.text) - except ValueError: - stats[key] = 0 - elif key == u'dropped': - try: - stats[key] = int(row.text) - except ValueError: - stats[key] = 0 - elif key == u'planto' + self.verb: - try: - stats[u'plan_to_' + self.verb] = int(row.text) - except ValueError: - stats[key] = 0 - # for some reason, MAL doesn't substitute 'read' in for manga for the verb here - elif key == u'days_spent_watching': - try: - stats[u'days_spent'] = decimal.Decimal(row.text) - except decimal.InvalidOperation: - stats[key] = decimal.Decimal(0) - except: - if not self.session.suppress_parse_exceptions: - raise - return stats - - def parse(self, xml): - list_info = {} - list_page = bs4.BeautifulSoup(xml, "xml") - - primary_elt = list_page.find('myanimelist') - if not primary_elt: - raise MalformedMediaListPageError(self.username, xml, message="Could not find root XML element in " + self.type + " list") - - bad_username_elt = list_page.find('error') - if bad_username_elt: - raise InvalidMediaListError(self.username, message=u"Invalid username when fetching " + self.type + " list") - - stats_elt = list_page.find('myinfo') - if not stats_elt: - raise MalformedMediaListPageError(self.username, html, message="Could not find stats element in " + self.type + " list") - - list_info[u'stats'] = self.parse_stats(stats_elt) - - list_info[u'list'] = {} - for row in list_page.find_all(self.type): - (media, entry) = self.parse_entry(row) - list_info[u'list'][media] = entry - - return list_info - - def load(self): - media_list = self.session.session.get(u'http://myanimelist.net/malappinfo.php?' + urllib.urlencode({'u': self.username, 'status': 'all', 'type': self.type})).text - self.set(self.parse(media_list)) - return self - - @property - @loadable(u'load') - def list(self): - return self._list - - @property - @loadable(u'load') - def stats(self): - return self._stats - - def section(self, status): - return {k: self.list[k] for k in self.list if self.list[k][u'status'] == status} \ No newline at end of file +class MediaList(Base, collections.Mapping): + __metaclass__ = abc.ABCMeta + + __id_attribute = "username" + + def __getitem__(self, media): + return self.list[media] + + def __contains__(self, media): + return media in self.list + + def __len__(self): + return len(self.list) + + def __iter__(self): + for media in self.list: + yield media + + def __init__(self, session, user_name): + super(MediaList, self).__init__(session) + self.username = user_name + if not isinstance(self.username, unicode) or len(self.username) < 1: + raise InvalidMediaListError(self.username) + self._list = None + self._stats = None + + # subclasses must define a list type, ala "anime" or "manga" + @abc.abstractproperty + def type(self): + pass + + # a list verb ala "watch", "read", etc + @abc.abstractproperty + def verb(self): + pass + + # a list with status ints as indices and status texts as values. + @property + def user_status_terms(self): + statuses = collections.defaultdict(lambda: u'Unknown') + statuses[1] = self.verb.capitalize() + u'ing' + statuses[2] = u'Completed' + statuses[3] = u'On-Hold' + statuses[4] = u'Dropped' + statuses[6] = u'Plan to ' + self.verb.capitalize() + return statuses + + def parse_entry_media_attributes(self, soup): + """ + Args: + soup: a bs4 element containing a row from the current media list + + Return a dict of attributes of the media the row is about. + """ + row_info = {} + + try: + start = utilities.parse_profile_date(soup.find('series_start').text) + except ValueError: + start = None + except: + if not self.session.suppress_parse_exceptions: + raise + + if start is not None: + try: + row_info['aired'] = (start, utilities.parse_profile_date(soup.find('series_end').text)) + except ValueError: + row_info['aired'] = (start, None) + except: + if not self.session.suppress_parse_exceptions: + raise + + # look up the given media type's status terms. + status_terms = getattr(self.session, self.type)(1)._status_terms + + try: + row_info['id'] = int(soup.find('series_' + self.type + 'db_id').text) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + row_info['title'] = soup.find('series_title').text + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + row_info['status'] = status_terms[int(soup.find('series_status').text)] + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + row_info['picture'] = soup.find('series_image').text + except: + if not self.session.suppress_parse_exceptions: + raise + + return row_info + + def parse_entry(self, soup): + """ + Given: + soup: a bs4 element containing a row from the current media list + + Return a tuple: + (media object, dict of this row's parseable attributes) + """ + # parse the media object first. + media_attrs = self.parse_entry_media_attributes(soup) + media_id = media_attrs[u'id'] + del media_attrs[u'id'] + media = getattr(self.session, self.type)(media_id).set(media_attrs) + + entry_info = {} + try: + entry_info[u'started'] = utilities.parse_profile_date(soup.find(u'my_start_date').text) + except ValueError: + entry_info[u'started'] = None + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + entry_info[u'finished'] = utilities.parse_profile_date(soup.find(u'my_finish_date').text) + except ValueError: + entry_info[u'finished'] = None + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + entry_info[u'status'] = self.user_status_terms[int(soup.find(u'my_status').text)] + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + entry_info[u'score'] = int(soup.find(u'my_score').text) + # if user hasn't set a score, set it to None to indicate as such. + if entry_info[u'score'] == 0: + entry_info[u'score'] = None + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + entry_info[u'last_updated'] = datetime.datetime.fromtimestamp(int(soup.find(u'my_last_updated').text)) + except: + if not self.session.suppress_parse_exceptions: + raise + + return media, entry_info + + def parse_stats(self, soup): + """ + Given: + soup: a bs4 element containing the current media list's stats + + Return a dict of this media list's stats. + """ + stats = {} + for row in soup.children: + try: + key = row.name.replace(u'user_', u'') + if key == u'id': + stats[key] = int(row.text) + elif key == u'name': + stats[key] = row.text + elif key == self.verb + u'ing': + try: + stats[key] = int(row.text) + except ValueError: + stats[key] = 0 + elif key == u'completed': + try: + stats[key] = int(row.text) + except ValueError: + stats[key] = 0 + elif key == u'onhold': + try: + stats['on_hold'] = int(row.text) + except ValueError: + stats[key] = 0 + elif key == u'dropped': + try: + stats[key] = int(row.text) + except ValueError: + stats[key] = 0 + elif key == u'planto' + self.verb: + try: + stats[u'plan_to_' + self.verb] = int(row.text) + except ValueError: + stats[key] = 0 + # for some reason, MAL doesn't substitute 'read' in for manga for the verb here + elif key == u'days_spent_watching': + try: + stats[u'days_spent'] = decimal.Decimal(row.text) + except decimal.InvalidOperation: + stats[key] = decimal.Decimal(0) + except: + if not self.session.suppress_parse_exceptions: + raise + return stats + + def parse(self, xml): + list_info = {} + list_page = bs4.BeautifulSoup(xml, "xml") + + primary_elt = list_page.find('myanimelist') + if not primary_elt: + raise MalformedMediaListPageError(self.username, xml, + message="Could not find root XML element in " + self.type + " list") + + bad_username_elt = list_page.find('error') + if bad_username_elt: + raise InvalidMediaListError(self.username, message=u"Invalid username when fetching " + self.type + " list") + + stats_elt = list_page.find('myinfo') + if not stats_elt: + raise MalformedMediaListPageError(self.username, html, + message="Could not find stats element in " + self.type + " list") + + list_info[u'stats'] = self.parse_stats(stats_elt) + + list_info[u'list'] = {} + for row in list_page.find_all(self.type): + (media, entry) = self.parse_entry(row) + list_info[u'list'][media] = entry + + return list_info + + def load(self): + media_list = self.session.session.get(u'http://myanimelist.net/malappinfo.php?' + urllib.urlencode( + {'u': self.username, 'status': 'all', 'type': self.type})).text + self.set(self.parse(media_list)) + return self + + @property + @loadable(u'load') + def list(self): + return self._list + + @property + @loadable(u'load') + def stats(self): + return self._stats + + def section(self, status): + return {k: self.list[k] for k in self.list if self.list[k][u'status'] == status} diff --git a/myanimelist/myanimelist.py b/myanimelist/myanimelist.py index 0c4dcd7..81db38a 100644 --- a/myanimelist/myanimelist.py +++ b/myanimelist/myanimelist.py @@ -5,10 +5,13 @@ # causes httplib to return the partial response from a server in case the read fails to be complete. def patch_http_response_read(func): - def inner(*args): - try: - return func(*args) - except httplib.IncompleteRead, e: - return e.partial - return inner -httplib.HTTPResponse.read = patch_http_response_read(httplib.HTTPResponse.read) \ No newline at end of file + def inner(*args): + try: + return func(*args) + except httplib.IncompleteRead, e: + return e.partial + + return inner + + +httplib.HTTPResponse.read = patch_http_response_read(httplib.HTTPResponse.read) diff --git a/myanimelist/person.py b/myanimelist/person.py index faf0059..01a5e2a 100644 --- a/myanimelist/person.py +++ b/myanimelist/person.py @@ -7,25 +7,28 @@ import utilities from base import Base, MalformedPageError, InvalidBaseError, loadable + class MalformedPersonPageError(MalformedPageError): - pass + pass + class InvalidPersonError(InvalidBaseError): - pass + pass + class Person(Base): - def __init__(self, session, person_id): - super(Person, self).__init__(session) - self.id = person_id - if not isinstance(self.id, int) or int(self.id) < 1: - raise InvalidPersonError(self.id) - self._name = None - - def load(self): - # TODO - pass + def __init__(self, session, person_id): + super(Person, self).__init__(session) + self.id = person_id + if not isinstance(self.id, int) or int(self.id) < 1: + raise InvalidPersonError(self.id) + self._name = None + + def load(self): + # TODO + pass - @property - @loadable(u'load') - def name(self): - return self._name \ No newline at end of file + @property + @loadable(u'load') + def name(self): + return self._name diff --git a/myanimelist/producer.py b/myanimelist/producer.py index 3369885..d66ba62 100644 --- a/myanimelist/producer.py +++ b/myanimelist/producer.py @@ -7,25 +7,28 @@ import utilities from base import Base, MalformedPageError, InvalidBaseError, loadable + class MalformedProducerPageError(MalformedPageError): - pass + pass + class InvalidProducerError(InvalidBaseError): - pass + pass + class Producer(Base): - def __init__(self, session, producer_id): - super(Producer, self).__init__(session) - self.id = producer_id - if not isinstance(self.id, int) or int(self.id) < 1: - raise InvalidProducerError(self.id) - self._name = None - - def load(self): - # TODO - pass + def __init__(self, session, producer_id): + super(Producer, self).__init__(session) + self.id = producer_id + if not isinstance(self.id, int) or int(self.id) < 1: + raise InvalidProducerError(self.id) + self._name = None + + def load(self): + # TODO + pass - @property - @loadable(u'load') - def name(self): - return self._name \ No newline at end of file + @property + @loadable(u'load') + def name(self): + return self._name diff --git a/myanimelist/publication.py b/myanimelist/publication.py index 7b71f8b..642bb73 100644 --- a/myanimelist/publication.py +++ b/myanimelist/publication.py @@ -7,25 +7,28 @@ import utilities from base import Base, MalformedPageError, InvalidBaseError, loadable + class MalformedPublicationPageError(MalformedPageError): - pass + pass + class InvalidPublicationError(InvalidBaseError): - pass + pass + class Publication(Base): - def __init__(self, session, publication_id): - super(Publication, self).__init__(session) - self.id = publication_id - if not isinstance(self.id, int) or int(self.id) < 1: - raise InvalidPublicationError(self.id) - self._name = None - - def load(self): - # TODO - pass + def __init__(self, session, publication_id): + super(Publication, self).__init__(session) + self.id = publication_id + if not isinstance(self.id, int) or int(self.id) < 1: + raise InvalidPublicationError(self.id) + self._name = None + + def load(self): + # TODO + pass - @property - @loadable(u'load') - def name(self): - return self._name + @property + @loadable(u'load') + def name(self): + return self._name diff --git a/myanimelist/session.py b/myanimelist/session.py index 31a99cf..95c9876 100644 --- a/myanimelist/session.py +++ b/myanimelist/session.py @@ -22,249 +22,254 @@ from base import Error + class UnauthorizedError(Error): - """ - Indicates that the current session is unauthorized to make the given request. - """ - def __init__(self, session, url, result): - """Creates a new instance of UnauthorizedError. + """ + Indicates that the current session is unauthorized to make the given request. + """ - :type session: :class:`.Session` - :param session: A valid MAL session. + def __init__(self, session, url, result): + """Creates a new instance of UnauthorizedError. - :type url: str - :param url: The requested URL. + :type session: :class:`.Session` + :param session: A valid MAL session. - :type result: str - :param result: The result of the request. + :type url: str + :param url: The requested URL. - :rtype: :class:`.UnauthorizedError` - :return: The desired error. + :type result: str + :param result: The result of the request. + + :rtype: :class:`.UnauthorizedError` + :return: The desired error. + + """ + super(UnauthorizedError, self).__init__() + self.session = session + self.url = url + self.result = result + + def __str__(self): + return "\n".join([ + super(UnauthorizedError, self).__str__(), + "URL: " + self.url, + "Result: " + self.result + ]) - """ - super(UnauthorizedError, self).__init__() - self.session = session - self.url = url - self.result = result - - def __str__(self): - return "\n".join([ - super(UnauthorizedError, self).__str__(), - "URL: " + self.url, - "Result: " + self.result - ]) class Session(object): - """Class to handle requests to MAL. Handles login, setting HTTP headers, etc. - """ - def __init__(self, username=None, password=None, user_agent="iMAL-iOS"): - """Creates a new instance of Session. + """Class to handle requests to MAL. Handles login, setting HTTP headers, etc. + """ - :type username: str - :param username: A MAL username. May be omitted. + def __init__(self, username=None, password=None, user_agent="iMAL-iOS"): + """Creates a new instance of Session. - :type password: str - :param username: A MAL password. May be omitted. + :type username: str + :param username: A MAL username. May be omitted. - :type user_agent: str - :param user_agent: A user-agent to send to MAL in requests. If you have a user-agent assigned to you by Incapsula, pass it in here. + :type password: str + :param username: A MAL password. May be omitted. - :rtype: :class:`.Session` - :return: The desired session. + :type user_agent: str + :param user_agent: A user-agent to send to MAL in requests. If you have a user-agent assigned to you by Incapsula, pass it in here. - """ - self.username = username - self.password = password - self.session = requests.Session() - self.session.headers.update({ - 'User-Agent': user_agent - }) + :rtype: :class:`.Session` + :return: The desired session. - """Suppresses any Malformed*PageError exceptions raised during parsing. + """ + self.username = username + self.password = password + self.session = requests.Session() + self.session.headers.update({ + 'User-Agent': user_agent + }) - Attributes which raise these exceptions will be set to None. - """ - self.suppress_parse_exceptions = False + """Suppresses any Malformed*PageError exceptions raised during parsing. - def logged_in(self): - """Checks the logged-in status of the current session. - Expensive (requests a page), so use sparingly! Best practice is to try a request and catch an UnauthorizedError. + Attributes which raise these exceptions will be set to None. + """ + self.suppress_parse_exceptions = False - :rtype: bool - :return: Whether or not the current session is logged-in. + def logged_in(self): + """Checks the logged-in status of the current session. + Expensive (requests a page), so use sparingly! Best practice is to try a request and catch an UnauthorizedError. - """ - if self.session is None: - return False + :rtype: bool + :return: Whether or not the current session is logged-in. - panel_url = u'http://myanimelist.net/panel.php' - panel = self.session.get(panel_url) + """ + if self.session is None: + return False - if 'Logout' in panel.content: - return True + panel_url = u'http://myanimelist.net/panel.php' + panel = self.session.get(panel_url) - return False + if 'Logout' in panel.content: + return True - def login(self): - """Logs into MAL and sets cookies appropriately. + return False - :rtype: :class:`.Session` - :return: The current session. + def login(self): + """Logs into MAL and sets cookies appropriately. - """ - # POSTS a login to mal. - mal_headers = { - 'Host': 'myanimelist.net', - } - mal_payload = { - 'username': self.username, - 'password': self.password, - 'cookie': 1, - 'sublogin': 'Login' - } - self.session.headers.update(mal_headers) - r = self.session.post(u'http://myanimelist.net/login.php', data=mal_payload) - return self - - def anime(self, anime_id): - """Creates an instance of myanimelist.Anime with the given ID. - - :type anime_id: int - :param anime_id: The desired anime's ID. - - :rtype: :class:`myanimelist.anime.Anime` - :return: A new Anime instance with the given ID. + :rtype: :class:`.Session` + :return: The current session. - """ - return anime.Anime(self, anime_id) + """ + # POSTS a login to mal. + mal_headers = { + 'Host': 'myanimelist.net', + } + mal_payload = { + 'username': self.username, + 'password': self.password, + 'cookie': 1, + 'sublogin': 'Login' + } + self.session.headers.update(mal_headers) + r = self.session.post(u'http://myanimelist.net/login.php', data=mal_payload) + return self - def anime_list(self, username): - """Creates an instance of myanimelist.AnimeList belonging to the given username. + def anime(self, anime_id): + """Creates an instance of myanimelist.Anime with the given ID. - :type username: str - :param username: The username to whom the desired anime list belongs. + :type anime_id: int + :param anime_id: The desired anime's ID. - :rtype: :class:`myanimelist.anime_list.AnimeList` - :return: A new AnimeList instance belonging to the given username. + :rtype: :class:`myanimelist.anime.Anime` + :return: A new Anime instance with the given ID. - """ - return anime_list.AnimeList(self, username) + """ + return anime.Anime(self, anime_id) - def character(self, character_id): - """Creates an instance of myanimelist.Character with the given ID. + def anime_list(self, username): + """Creates an instance of myanimelist.AnimeList belonging to the given username. - :type character_id: int - :param character_id: The desired character's ID. + :type username: str + :param username: The username to whom the desired anime list belongs. - :rtype: :class:`myanimelist.character.Character` - :return: A new Character instance with the given ID. + :rtype: :class:`myanimelist.anime_list.AnimeList` + :return: A new AnimeList instance belonging to the given username. - """ - return character.Character(self, character_id) + """ + return anime_list.AnimeList(self, username) - def club(self, club_id): - """Creates an instance of myanimelist.Club with the given ID. + def character(self, character_id): + """Creates an instance of myanimelist.Character with the given ID. - :type club_id: int - :param club_id: The desired club's ID. + :type character_id: int + :param character_id: The desired character's ID. - :rtype: :class:`myanimelist.club.Club` - :return: A new Club instance with the given ID. + :rtype: :class:`myanimelist.character.Character` + :return: A new Character instance with the given ID. - """ - return club.Club(self, club_id) + """ + return character.Character(self, character_id) - def genre(self, genre_id): - """Creates an instance of myanimelist.Genre with the given ID. + def club(self, club_id): + """Creates an instance of myanimelist.Club with the given ID. - :type genre_id: int - :param genre_id: The desired genre's ID. + :type club_id: int + :param club_id: The desired club's ID. - :rtype: :class:`myanimelist.genre.Genre` - :return: A new Genre instance with the given ID. + :rtype: :class:`myanimelist.club.Club` + :return: A new Club instance with the given ID. - """ - return genre.Genre(self, genre_id) + """ + return club.Club(self, club_id) - def manga(self, manga_id): - """Creates an instance of myanimelist.Manga with the given ID. + def genre(self, genre_id): + """Creates an instance of myanimelist.Genre with the given ID. - :type manga_id: int - :param manga_id: The desired manga's ID. + :type genre_id: int + :param genre_id: The desired genre's ID. - :rtype: :class:`myanimelist.manga.Manga` - :return: A new Manga instance with the given ID. + :rtype: :class:`myanimelist.genre.Genre` + :return: A new Genre instance with the given ID. - """ - return manga.Manga(self, manga_id) + """ + return genre.Genre(self, genre_id) - def manga_list(self, username): - """Creates an instance of myanimelist.MangaList belonging to the given username. + def manga(self, manga_id): + """Creates an instance of myanimelist.Manga with the given ID. - :type username: str - :param username: The username to whom the desired manga list belongs. + :type manga_id: int + :param manga_id: The desired manga's ID. - :rtype: :class:`myanimelist.manga_list.MangaList` - :return: A new MangaList instance belonging to the given username. + :rtype: :class:`myanimelist.manga.Manga` + :return: A new Manga instance with the given ID. - """ - return manga_list.MangaList(self, username) + """ + return manga.Manga(self, manga_id) - def person(self, person_id): - """Creates an instance of myanimelist.Person with the given ID. + def manga_list(self, username): + """Creates an instance of myanimelist.MangaList belonging to the given username. - :type person_id: int - :param person_id: The desired person's ID. + :type username: str + :param username: The username to whom the desired manga list belongs. - :rtype: :class:`myanimelist.person.Person` - :return: A new Person instance with the given ID. + :rtype: :class:`myanimelist.manga_list.MangaList` + :return: A new MangaList instance belonging to the given username. - """ - return person.Person(self, person_id) - def producer(self, producer_id): - """Creates an instance of myanimelist.Producer with the given ID. + """ + return manga_list.MangaList(self, username) - :type producer_id: int - :param producer_id: The desired producer's ID. + def person(self, person_id): + """Creates an instance of myanimelist.Person with the given ID. - :rtype: :class:`myanimelist.producer.Producer` - :return: A new Producer instance with the given ID. + :type person_id: int + :param person_id: The desired person's ID. - """ - return producer.Producer(self, producer_id) - - def publication(self, publication_id): - """Creates an instance of myanimelist.Publication with the given ID. + :rtype: :class:`myanimelist.person.Person` + :return: A new Person instance with the given ID. - :type publication_id: int - :param publication_id: The desired publication's ID. + """ + return person.Person(self, person_id) - :rtype: :class:`myanimelist.publication.Publication` - :return: A new Publication instance with the given ID. + def producer(self, producer_id): + """Creates an instance of myanimelist.Producer with the given ID. - """ - return publication.Publication(self, publication_id) + :type producer_id: int + :param producer_id: The desired producer's ID. - def tag(self, tag_id): - """Creates an instance of myanimelist.Tag with the given ID. + :rtype: :class:`myanimelist.producer.Producer` + :return: A new Producer instance with the given ID. - :type tag_id: int - :param tag_id: The desired tag's ID. + """ + return producer.Producer(self, producer_id) - :rtype: :class:`myanimelist.tag.Tag` - :return: A new Tag instance with the given ID. + def publication(self, publication_id): + """Creates an instance of myanimelist.Publication with the given ID. - """ - return tag.Tag(self, tag_id) + :type publication_id: int + :param publication_id: The desired publication's ID. - def user(self, username): - """Creates an instance of myanimelist.User with the given username + :rtype: :class:`myanimelist.publication.Publication` + :return: A new Publication instance with the given ID. - :type username: str - :param username: The desired user's username. + """ + return publication.Publication(self, publication_id) - :rtype: :class:`myanimelist.user.User` - :return: A new User instance with the given username. + def tag(self, tag_id): + """Creates an instance of myanimelist.Tag with the given ID. - """ - return user.User(self, username) \ No newline at end of file + :type tag_id: int + :param tag_id: The desired tag's ID. + + :rtype: :class:`myanimelist.tag.Tag` + :return: A new Tag instance with the given ID. + + """ + return tag.Tag(self, tag_id) + + def user(self, username): + """Creates an instance of myanimelist.User with the given username + + :type username: str + :param username: The desired user's username. + + :rtype: :class:`myanimelist.user.User` + :return: A new User instance with the given username. + + """ + return user.User(self, username) diff --git a/myanimelist/tag.py b/myanimelist/tag.py index e882bb1..bf2d2f1 100644 --- a/myanimelist/tag.py +++ b/myanimelist/tag.py @@ -7,20 +7,24 @@ import utilities from base import Base, MalformedPageError, InvalidBaseError, loadable + class MalformedTagPageError(MalformedPageError): - pass + pass + class InvalidTagError(InvalidBaseError): - pass + pass + class Tag(Base): - _id_attribute = "name" - def __init__(self, session, name): - super(Tag, self).__init__(session) - self.name = name - if not isinstance(self.name, unicode) or len(self.name) < 1: - raise InvalidTagError(self.name) - - def load(self): - # TODO - pass \ No newline at end of file + _id_attribute = "name" + + def __init__(self, session, name): + super(Tag, self).__init__(session) + self.name = name + if not isinstance(self.name, unicode) or len(self.name) < 1: + raise InvalidTagError(self.name) + + def load(self): + # TODO + pass diff --git a/myanimelist/user.py b/myanimelist/user.py index 1aee3c4..cc2508f 100644 --- a/myanimelist/user.py +++ b/myanimelist/user.py @@ -8,863 +8,891 @@ import utilities from base import Base, MalformedPageError, InvalidBaseError, loadable -class MalformedUserPageError(MalformedPageError): - """Indicates that a user-related page on MAL has irreparably broken markup in some way. - """ - pass - -class InvalidUserError(InvalidBaseError): - """Indicates that the user requested does not exist on MAL. - """ - pass - -class User(Base): - """Primary interface to user resources on MAL. - """ - _id_attribute = "username" - - @staticmethod - def find_username_from_user_id(session, user_id): - """Look up a MAL username's user ID. - - :type session: :class:`myanimelist.session.Session` - :param session: A valid MAL session. - :type user_id: int - :param user_id: The user ID for which we want to look up a username. - - :raises: :class:`.InvalidUserError` - - :rtype: str - :return: The given user's username. +class MalformedUserPageError(MalformedPageError): + """Indicates that a user-related page on MAL has irreparably broken markup in some way. """ - comments_page = session.session.get(u'http://myanimelist.net/comments.php?' + urllib.urlencode({'id': int(user_id)})).text - comments_page = bs4.BeautifulSoup(comments_page) - username_elt = comments_page.find('h1') - if "'s Comments" not in username_elt.text: - raise InvalidUserError(user_id, message="Invalid user ID given when looking up username") - return username_elt.text.replace("'s Comments", "") - - def __init__(self, session, username): - """Creates a new instance of User. - - :type session: :class:`myanimelist.session.Session` - :param session: A valid MAL session - :type username: str - :param username: The desired user's username on MAL - - :raises: :class:`.InvalidUserError` + pass - """ - super(User, self).__init__(session) - self.username = username - if not isinstance(self.username, unicode) or len(self.username) < 1: - raise InvalidUserError(self.username) - self._picture = None - self._favorite_anime = None - self._favorite_manga = None - self._favorite_characters = None - self._favorite_people = None - self._last_online = None - self._gender = None - self._birthday = None - self._location = None - self._website = None - self._join_date = None - self._access_rank = None - self._anime_list_views = None - self._manga_list_views = None - self._num_comments = None - self._num_forum_posts = None - self._last_list_updates = None - self._about = None - self._anime_stats = None - self._manga_stats = None - self._reviews = None - self._recommendations = None - self._clubs = None - self._friends = None - - def parse_sidebar(self, user_page): - """Parses the DOM and returns user attributes in the sidebar. - - :type user_page: :class:`bs4.BeautifulSoup` - :param user_page: MAL user page's DOM - - :rtype: dict - :return: User attributes - - :raises: :class:`.InvalidUserError`, :class:`.MalformedUserPageError` - """ - user_info = {} - # if MAL says the series doesn't exist, raise an InvalidUserError. - error_tag = user_page.find(u'div', {u'class': u'badresult'}) - if error_tag: - raise InvalidUserError(self.username) - - try: - username_tag = user_page.find(u'div', {u'id': u'contentWrapper'}).find(u'h1') - if not username_tag.find(u'div'): - # otherwise, raise a MalformedUserPageError. - raise MalformedUserPageError(self.username, user_page, message=u"Could not find title div") - except: - if not self.session.suppress_parse_exceptions: - raise - - info_panel_first = user_page.find(u'div', {u'id': u'content'}).find(u'table').find(u'td') - - try: - picture_tag = info_panel_first.find(u'img') - user_info[u'picture'] = picture_tag.get(u'src').decode('utf-8') - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - # the user ID is always present in the blogfeed link. - all_comments_link = info_panel_first.find(u'a', text=u'Blog Feed') - user_info[u'id'] = int(all_comments_link.get(u'href').split(u'&id=')[1]) - except: - if not self.session.suppress_parse_exceptions: - raise - - infobar_headers = info_panel_first.find_all(u'div', {u'class': u'normal_header'}) - if infobar_headers: - try: - favorite_anime_header = infobar_headers[0] - if u'Favorite Anime' in favorite_anime_header.text: - user_info[u'favorite_anime'] = [] - favorite_anime_table = favorite_anime_header.nextSibling.nextSibling - if favorite_anime_table.name == u'table': - for row in favorite_anime_table.find_all(u'tr'): - cols = row.find_all(u'td') - anime_link = cols[1].find(u'a') - link_parts = anime_link.get(u'href').split(u'/') - # of the form /anime/467/Ghost_in_the_Shell:_Stand_Alone_Complex - user_info[u'favorite_anime'].append(self.session.anime(int(link_parts[2])).set({u'title': anime_link.text})) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - favorite_manga_header = infobar_headers[1] - if u'Favorite Manga' in favorite_manga_header.text: - user_info[u'favorite_manga'] = [] - favorite_manga_table = favorite_manga_header.nextSibling.nextSibling - if favorite_manga_table.name == u'table': - for row in favorite_manga_table.find_all(u'tr'): - cols = row.find_all(u'td') - manga_link = cols[1].find(u'a') - link_parts = manga_link.get(u'href').split(u'/') - # of the form /manga/467/Ghost_in_the_Shell:_Stand_Alone_Complex - user_info[u'favorite_manga'].append(self.session.manga(int(link_parts[2])).set({u'title': manga_link.text})) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - favorite_character_header = infobar_headers[2] - if u'Favorite Characters' in favorite_character_header.text: - user_info[u'favorite_characters'] = {} - favorite_character_table = favorite_character_header.nextSibling.nextSibling - if favorite_character_table.name == u'table': - for row in favorite_character_table.find_all(u'tr'): - cols = row.find_all(u'td') - character_link = cols[1].find(u'a') - link_parts = character_link.get(u'href').split(u'/') - # of the form /character/467/Ghost_in_the_Shell:_Stand_Alone_Complex - character = self.session.character(int(link_parts[2])).set({u'title': character_link.text}) - - media_link = cols[1].find(u'div').find(u'a') - link_parts = media_link.get(u'href').split(u'/') - # of the form /anime|manga/467 - anime = getattr(self.session, link_parts[1])(int(link_parts[2])).set({u'title': media_link.text}) - - user_info[u'favorite_characters'][character] = anime - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - favorite_people_header = infobar_headers[3] - if u'Favorite People' in favorite_people_header.text: - user_info[u'favorite_people'] = [] - favorite_person_table = favorite_people_header.nextSibling.nextSibling - if favorite_person_table.name == u'table': - for row in favorite_person_table.find_all(u'tr'): - cols = row.find_all(u'td') - person_link = cols[1].find(u'a') - link_parts = person_link.get(u'href').split(u'/') - # of the form /person/467/Ghost_in_the_Shell:_Stand_Alone_Complex - user_info[u'favorite_people'].append(self.session.person(int(link_parts[2])).set({u'title': person_link.text})) - except: - if not self.session.suppress_parse_exceptions: - raise - return user_info - - def parse(self, user_page): - """Parses the DOM and returns user attributes in the main-content area. - - :type user_page: :class:`bs4.BeautifulSoup` - :param user_page: MAL user page's DOM - - :rtype: dict - :return: User attributes. +class InvalidUserError(InvalidBaseError): + """Indicates that the user requested does not exist on MAL. """ - user_info = self.parse_sidebar(user_page) - - section_headings = user_page.find_all(u'div', {u'class': u'normal_header'}) + pass - # parse general details. - # we have to work from the bottom up, since there's broken HTML after every header. - last_online_elt = user_page.find(u'td', text=u'Last Online') - if last_online_elt: - try: - general_table = last_online_elt.parent.parent - except: - if not self.session.suppress_parse_exceptions: - raise - - if general_table and general_table.name == u'table': - try: - last_online_elt = general_table.find(u'td', text=u'Last Online') - if last_online_elt: - user_info[u'last_online'] = utilities.parse_profile_date(last_online_elt.findNext(u'td').text) - except: - if not self.session.suppress_parse_exceptions: - raise - try: - gender = general_table.find(u'td', text=u'Gender') - if gender: - user_info[u'gender'] = gender.findNext(u'td').text - except: - if not self.session.suppress_parse_exceptions: - raise +class User(Base): + """Primary interface to user resources on MAL. + """ + _id_attribute = "username" + + @staticmethod + def find_username_from_user_id(session, user_id): + """Look up a MAL username's user ID. + + :type session: :class:`myanimelist.session.Session` + :param session: A valid MAL session. + + :type user_id: int + :param user_id: The user ID for which we want to look up a username. + + :raises: :class:`.InvalidUserError` + + :rtype: str + :return: The given user's username. + """ + comments_page = session.session.get( + u'http://myanimelist.net/comments.php?' + urllib.urlencode({'id': int(user_id)})).text + comments_page = bs4.BeautifulSoup(comments_page) + username_elt = comments_page.find('h1') + if "'s Comments" not in username_elt.text: + raise InvalidUserError(user_id, message="Invalid user ID given when looking up username") + return username_elt.text.replace("'s Comments", "") + + def __init__(self, session, username): + """Creates a new instance of User. + + :type session: :class:`myanimelist.session.Session` + :param session: A valid MAL session + :type username: str + :param username: The desired user's username on MAL + + :raises: :class:`.InvalidUserError` + + """ + super(User, self).__init__(session) + self.username = username + if not isinstance(self.username, unicode) or len(self.username) < 1: + raise InvalidUserError(self.username) + self._picture = None + self._favorite_anime = None + self._favorite_manga = None + self._favorite_characters = None + self._favorite_people = None + self._last_online = None + self._gender = None + self._birthday = None + self._location = None + self._website = None + self._join_date = None + self._access_rank = None + self._anime_list_views = None + self._manga_list_views = None + self._num_comments = None + self._num_forum_posts = None + self._last_list_updates = None + self._about = None + self._anime_stats = None + self._manga_stats = None + self._reviews = None + self._recommendations = None + self._clubs = None + self._friends = None + + def parse_sidebar(self, user_page): + """Parses the DOM and returns user attributes in the sidebar. + + :type user_page: :class:`bs4.BeautifulSoup` + :param user_page: MAL user page's DOM + + :rtype: dict + :return: User attributes + + :raises: :class:`.InvalidUserError`, :class:`.MalformedUserPageError` + """ + user_info = {} + # if MAL says the series doesn't exist, raise an InvalidUserError. + error_tag = user_page.find(u'div', {u'class': u'badresult'}) + if error_tag: + raise InvalidUserError(self.username) try: - birthday = general_table.find(u'td', text=u'Birthday') - if birthday: - user_info[u'birthday'] = utilities.parse_profile_date(birthday.findNext(u'td').text) + username_tag = user_page.find(u'div', {u'id': u'contentWrapper'}).find(u'h1') + if not username_tag.find(u'div'): + # otherwise, raise a MalformedUserPageError. + raise MalformedUserPageError(self.username, user_page, message=u"Could not find title div") except: - if not self.session.suppress_parse_exceptions: - raise + if not self.session.suppress_parse_exceptions: + raise - try: - location = general_table.find(u'td', text=u'Location') - if location: - user_info[u'location'] = location.findNext(u'td').text - except: - if not self.session.suppress_parse_exceptions: - raise + info_panel_first = user_page.find(u'div', {u'id': u'content'}).find(u'table').find(u'td') try: - website = general_table.find(u'td', text=u'Website') - if website: - user_info[u'website'] = website.findNext(u'td').text + picture_tag = info_panel_first.find(u'img') + user_info[u'picture'] = picture_tag.get(u'src').decode('utf-8') except: - if not self.session.suppress_parse_exceptions: - raise + if not self.session.suppress_parse_exceptions: + raise try: - join_date = general_table.find(u'td', text=u'Join Date') - if join_date: - user_info[u'join_date'] = utilities.parse_profile_date(join_date.findNext(u'td').text) + # the user ID is always present in the blogfeed link. + all_comments_link = info_panel_first.find(u'a', text=u'Blog Feed') + user_info[u'id'] = int(all_comments_link.get(u'href').split(u'&id=')[1]) except: - if not self.session.suppress_parse_exceptions: - raise + if not self.session.suppress_parse_exceptions: + raise + + infobar_headers = info_panel_first.find_all(u'div', {u'class': u'normal_header'}) + if infobar_headers: + try: + favorite_anime_header = infobar_headers[0] + if u'Favorite Anime' in favorite_anime_header.text: + user_info[u'favorite_anime'] = [] + favorite_anime_table = favorite_anime_header.nextSibling.nextSibling + if favorite_anime_table.name == u'table': + for row in favorite_anime_table.find_all(u'tr'): + cols = row.find_all(u'td') + anime_link = cols[1].find(u'a') + link_parts = anime_link.get(u'href').split(u'/') + # of the form /anime/467/Ghost_in_the_Shell:_Stand_Alone_Complex + user_info[u'favorite_anime'].append( + self.session.anime(int(link_parts[2])).set({u'title': anime_link.text})) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + favorite_manga_header = infobar_headers[1] + if u'Favorite Manga' in favorite_manga_header.text: + user_info[u'favorite_manga'] = [] + favorite_manga_table = favorite_manga_header.nextSibling.nextSibling + if favorite_manga_table.name == u'table': + for row in favorite_manga_table.find_all(u'tr'): + cols = row.find_all(u'td') + manga_link = cols[1].find(u'a') + link_parts = manga_link.get(u'href').split(u'/') + # of the form /manga/467/Ghost_in_the_Shell:_Stand_Alone_Complex + user_info[u'favorite_manga'].append( + self.session.manga(int(link_parts[2])).set({u'title': manga_link.text})) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + favorite_character_header = infobar_headers[2] + if u'Favorite Characters' in favorite_character_header.text: + user_info[u'favorite_characters'] = {} + favorite_character_table = favorite_character_header.nextSibling.nextSibling + if favorite_character_table.name == u'table': + for row in favorite_character_table.find_all(u'tr'): + cols = row.find_all(u'td') + character_link = cols[1].find(u'a') + link_parts = character_link.get(u'href').split(u'/') + # of the form /character/467/Ghost_in_the_Shell:_Stand_Alone_Complex + character = self.session.character(int(link_parts[2])).set({u'title': character_link.text}) + + media_link = cols[1].find(u'div').find(u'a') + link_parts = media_link.get(u'href').split(u'/') + # of the form /anime|manga/467 + anime = getattr(self.session, link_parts[1])(int(link_parts[2])).set( + {u'title': media_link.text}) + + user_info[u'favorite_characters'][character] = anime + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + favorite_people_header = infobar_headers[3] + if u'Favorite People' in favorite_people_header.text: + user_info[u'favorite_people'] = [] + favorite_person_table = favorite_people_header.nextSibling.nextSibling + if favorite_person_table.name == u'table': + for row in favorite_person_table.find_all(u'tr'): + cols = row.find_all(u'td') + person_link = cols[1].find(u'a') + link_parts = person_link.get(u'href').split(u'/') + # of the form /person/467/Ghost_in_the_Shell:_Stand_Alone_Complex + user_info[u'favorite_people'].append( + self.session.person(int(link_parts[2])).set({u'title': person_link.text})) + except: + if not self.session.suppress_parse_exceptions: + raise + return user_info + + def parse(self, user_page): + """Parses the DOM and returns user attributes in the main-content area. + + :type user_page: :class:`bs4.BeautifulSoup` + :param user_page: MAL user page's DOM + + :rtype: dict + :return: User attributes. + + """ + user_info = self.parse_sidebar(user_page) + + section_headings = user_page.find_all(u'div', {u'class': u'normal_header'}) + + # parse general details. + # we have to work from the bottom up, since there's broken HTML after every header. + last_online_elt = user_page.find(u'td', text=u'Last Online') + if last_online_elt: + try: + general_table = last_online_elt.parent.parent + except: + if not self.session.suppress_parse_exceptions: + raise + + if general_table and general_table.name == u'table': + try: + last_online_elt = general_table.find(u'td', text=u'Last Online') + if last_online_elt: + user_info[u'last_online'] = utilities.parse_profile_date(last_online_elt.findNext(u'td').text) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + gender = general_table.find(u'td', text=u'Gender') + if gender: + user_info[u'gender'] = gender.findNext(u'td').text + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + birthday = general_table.find(u'td', text=u'Birthday') + if birthday: + user_info[u'birthday'] = utilities.parse_profile_date(birthday.findNext(u'td').text) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + location = general_table.find(u'td', text=u'Location') + if location: + user_info[u'location'] = location.findNext(u'td').text + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + website = general_table.find(u'td', text=u'Website') + if website: + user_info[u'website'] = website.findNext(u'td').text + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + join_date = general_table.find(u'td', text=u'Join Date') + if join_date: + user_info[u'join_date'] = utilities.parse_profile_date(join_date.findNext(u'td').text) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + access_rank = general_table.find(u'td', text=u'Access Rank') + if access_rank: + user_info[u'access_rank'] = access_rank.findNext(u'td').text + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + anime_list_views = general_table.find(u'td', text=u'Anime List Views') + if anime_list_views: + user_info[u'anime_list_views'] = int(anime_list_views.findNext(u'td').text.replace(',', '')) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + manga_list_views = general_table.find(u'td', text=u'Manga List Views') + if manga_list_views: + user_info[u'manga_list_views'] = int(manga_list_views.findNext(u'td').text.replace(',', '')) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + num_comments = general_table.find(u'td', text=u'Comments') + if num_comments: + user_info[u'num_comments'] = int(num_comments.findNext(u'td').text.replace(',', '')) + except: + if not self.session.suppress_parse_exceptions: + raise + + try: + num_forum_posts = general_table.find(u'td', text=u'Forum Posts') + if num_forum_posts: + user_info[u'num_forum_posts'] = int( + num_forum_posts.findNext(u'td').text.replace(" (Find All)", "").replace(',', '')) + except: + if not self.session.suppress_parse_exceptions: + raise try: - access_rank = general_table.find(u'td', text=u'Access Rank') - if access_rank: - user_info[u'access_rank'] = access_rank.findNext(u'td').text + # last list updates. + list_updates_header = filter(lambda x: u'Last List Updates' in x.text, section_headings) + if list_updates_header: + list_updates_header = list_updates_header[0] + list_updates_table = list_updates_header.findNext(u'table') + if list_updates_table: + user_info[u'last_list_updates'] = {} + for row in list_updates_table.find_all(u'tr'): + cols = row.find_all(u'td') + info_col = cols[1] + media_link = info_col.find(u'a') + link_parts = media_link.get(u'href').split(u'/') + # of the form /(anime|manga)/10087/Fate/Zero + if link_parts[1] == u'anime': + media = self.session.anime(int(link_parts[2])).set({u'title': media_link.text}) + else: + media = self.session.manga(int(link_parts[2])).set({u'title': media_link.text}) + list_update = {} + progress_div = info_col.find(u'div', {u'class': u'spaceit_pad'}) + if progress_div: + progress_match = re.match( + r'(?P[A-Za-z]+)( at (?P[0-9]+) of (?P[0-9]+))?', + progress_div.text).groupdict() + list_update[u'status'] = progress_match[u'status'] + if progress_match[u'episodes'] is None: + list_update[u'episodes'] = None + else: + list_update[u'episodes'] = int(progress_match[u'episodes']) + if progress_match[u'total_episodes'] is None: + list_update[u'total_episodes'] = None + else: + list_update[u'total_episodes'] = int(progress_match[u'total_episodes']) + time_div = info_col.find(u'div', {u'class': u'lightLink'}) + if time_div: + list_update[u'time'] = utilities.parse_profile_date(time_div.text) + user_info[u'last_list_updates'][media] = list_update except: - if not self.session.suppress_parse_exceptions: - raise + if not self.session.suppress_parse_exceptions: + raise + lower_section_headings = user_page.find_all(u'h2') + # anime stats. try: - anime_list_views = general_table.find(u'td', text=u'Anime List Views') - if anime_list_views: - user_info[u'anime_list_views'] = int(anime_list_views.findNext(u'td').text.replace(',', '')) + anime_stats_header = filter(lambda x: u'Anime Stats' in x.text, lower_section_headings) + if anime_stats_header: + anime_stats_header = anime_stats_header[0] + anime_stats_table = anime_stats_header.findNext(u'table') + if anime_stats_table: + user_info[u'anime_stats'] = {} + for row in anime_stats_table.find_all(u'tr'): + cols = row.find_all(u'td') + value = cols[1].text + if cols[1].find(u'span', {u'title': u'Days'}): + value = round(float(value), 1) + else: + value = int(value) + user_info[u'anime_stats'][cols[0].text] = value except: - if not self.session.suppress_parse_exceptions: - raise + if not self.session.suppress_parse_exceptions: + raise try: - manga_list_views = general_table.find(u'td', text=u'Manga List Views') - if manga_list_views: - user_info[u'manga_list_views'] = int(manga_list_views.findNext(u'td').text.replace(',', '')) + # manga stats. + manga_stats_header = filter(lambda x: u'Manga Stats' in x.text, lower_section_headings) + if manga_stats_header: + manga_stats_header = manga_stats_header[0] + manga_stats_table = manga_stats_header.findNext(u'table') + if manga_stats_table: + user_info[u'manga_stats'] = {} + for row in manga_stats_table.find_all(u'tr'): + cols = row.find_all(u'td') + value = cols[1].text + if cols[1].find(u'span', {u'title': u'Days'}): + value = round(float(value), 1) + else: + value = int(value) + user_info[u'manga_stats'][cols[0].text] = value except: - if not self.session.suppress_parse_exceptions: - raise + if not self.session.suppress_parse_exceptions: + raise try: - num_comments = general_table.find(u'td', text=u'Comments') - if num_comments: - user_info[u'num_comments'] = int(num_comments.findNext(u'td').text.replace(',', '')) + about_header = filter(lambda x: u'About' in x.text, section_headings) + if about_header: + about_header = about_header[0] + user_info[u'about'] = about_header.findNext(u'div').text.strip() except: - if not self.session.suppress_parse_exceptions: - raise - - try: - num_forum_posts = general_table.find(u'td', text=u'Forum Posts') - if num_forum_posts: - user_info[u'num_forum_posts'] = int(num_forum_posts.findNext(u'td').text.replace(" (Find All)", "").replace(',', '')) - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - # last list updates. - list_updates_header = filter(lambda x: u'Last List Updates' in x.text, section_headings) - if list_updates_header: - list_updates_header = list_updates_header[0] - list_updates_table = list_updates_header.findNext(u'table') - if list_updates_table: - user_info[u'last_list_updates'] = {} - for row in list_updates_table.find_all(u'tr'): - cols = row.find_all(u'td') - info_col = cols[1] - media_link = info_col.find(u'a') - link_parts = media_link.get(u'href').split(u'/') - # of the form /(anime|manga)/10087/Fate/Zero - if link_parts[1] == u'anime': - media = self.session.anime(int(link_parts[2])).set({u'title': media_link.text}) - else: - media = self.session.manga(int(link_parts[2])).set({u'title': media_link.text}) - list_update = {} - progress_div = info_col.find(u'div', {u'class': u'spaceit_pad'}) - if progress_div: - progress_match = re.match(r'(?P[A-Za-z]+)( at (?P[0-9]+) of (?P[0-9]+))?', progress_div.text).groupdict() - list_update[u'status'] = progress_match[u'status'] - if progress_match[u'episodes'] is None: - list_update[u'episodes'] = None - else: - list_update[u'episodes'] = int(progress_match[u'episodes']) - if progress_match[u'total_episodes'] is None: - list_update[u'total_episodes'] = None - else: - list_update[u'total_episodes'] = int(progress_match[u'total_episodes']) - time_div = info_col.find(u'div', {u'class': u'lightLink'}) - if time_div: - list_update[u'time'] = utilities.parse_profile_date(time_div.text) - user_info[u'last_list_updates'][media] = list_update - except: - if not self.session.suppress_parse_exceptions: - raise - - lower_section_headings = user_page.find_all(u'h2') - # anime stats. - try: - anime_stats_header = filter(lambda x: u'Anime Stats' in x.text, lower_section_headings) - if anime_stats_header: - anime_stats_header = anime_stats_header[0] - anime_stats_table = anime_stats_header.findNext(u'table') - if anime_stats_table: - user_info[u'anime_stats'] = {} - for row in anime_stats_table.find_all(u'tr'): - cols = row.find_all(u'td') - value = cols[1].text - if cols[1].find(u'span', {u'title': u'Days'}): - value = round(float(value), 1) - else: - value = int(value) - user_info[u'anime_stats'][cols[0].text] = value - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - # manga stats. - manga_stats_header = filter(lambda x: u'Manga Stats' in x.text, lower_section_headings) - if manga_stats_header: - manga_stats_header = manga_stats_header[0] - manga_stats_table = manga_stats_header.findNext(u'table') - if manga_stats_table: - user_info[u'manga_stats'] = {} - for row in manga_stats_table.find_all(u'tr'): - cols = row.find_all(u'td') - value = cols[1].text - if cols[1].find(u'span', {u'title': u'Days'}): - value = round(float(value), 1) - else: - value = int(value) - user_info[u'manga_stats'][cols[0].text] = value - except: - if not self.session.suppress_parse_exceptions: - raise - - try: - about_header = filter(lambda x: u'About' in x.text, section_headings) - if about_header: - about_header = about_header[0] - user_info[u'about'] = about_header.findNext(u'div').text.strip() - except: - if not self.session.suppress_parse_exceptions: - raise - - return user_info - - def parse_reviews(self, reviews_page): - """Parses the DOM and returns user reviews attributes. - - :type reviews_page: :class:`bs4.BeautifulSoup` - :param reviews_page: MAL user reviews page's DOM - - :rtype: dict - :return: User reviews attributes. - - """ - user_info = self.parse_sidebar(reviews_page) - second_col = reviews_page.find(u'div', {u'id': u'content'}).find(u'table').find(u'tr').find_all(u'td', recursive=False)[1] - - try: - user_info[u'reviews'] = {} - reviews = second_col.find_all(u'div', {u'class': u'borderDark'}, recursive=False) - if reviews: - for row in reviews: - review_info = {} - try: - (meta_elt, review_elt) = row.find_all(u'div', recursive=False)[0:2] - except ValueError: - raise - meta_rows = meta_elt.find_all(u'div', recursive=False) - review_info[u'date'] = utilities.parse_profile_date(meta_rows[0].find(u'div').text) - media_link = meta_rows[0].find(u'a') - link_parts = media_link.get(u'href').split(u'/') - # of the form /(anime|manga)/9760/Hoshi_wo_Ou_Kodomo - media = getattr(self.session, link_parts[1])(int(link_parts[2])).set({u'title': media_link.text}) - - helpfuls = meta_rows[1].find(u'span', recursive=False) - helpful_match = re.match(r'(?P[0-9]+) of (?P[0-9]+)', helpfuls.text).groupdict() - review_info[u'people_helped'] = int(helpful_match[u'people_helped']) - review_info[u'people_total'] = int(helpful_match[u'people_total']) - - consumption_match = re.match(r'(?P[0-9]+) of (?P[0-9?]+)', meta_rows[2].text).groupdict() - review_info[u'media_consumed'] = int(consumption_match[u'media_consumed']) - if consumption_match[u'media_total'] == u'?': - review_info[u'media_total'] = None - else: - review_info[u'media_total'] = int(consumption_match[u'media_total']) - - review_info[u'rating'] = int(meta_rows[3].find(u'div').text.replace(u'Overall Rating: ', '')) - - for x in review_elt.find_all([u'div', 'a']): - x.extract() - review_info[u'text'] = review_elt.text.strip() - user_info[u'reviews'][media] = review_info - except: - if not self.session.suppress_parse_exceptions: - raise - - return user_info - - def parse_recommendations(self, recommendations_page): - """Parses the DOM and returns user recommendations attributes. - - :type recommendations_page: :class:`bs4.BeautifulSoup` - :param recommendations_page: MAL user recommendations page's DOM - - :rtype: dict - :return: User recommendations attributes. - - """ - user_info = self.parse_sidebar(recommendations_page) - second_col = recommendations_page.find(u'div', {u'id': u'content'}).find(u'table').find(u'tr').find_all(u'td', recursive=False)[1] - - try: - recommendations = second_col.find_all(u"div", {u"class": u"spaceit borderClass"}) - if recommendations: - user_info[u'recommendations'] = {} - for row in recommendations[1:]: - anime_table = row.find(u'table') - animes = anime_table.find_all(u'td') - liked_media_link = animes[0].find(u'a', recursive=False) - link_parts = liked_media_link.get(u'href').split(u'/') - # of the form /anime|manga/64/Rozen_Maiden - liked_media = getattr(self.session, link_parts[1])(int(link_parts[2])).set({u'title': liked_media_link.text}) - - recommended_media_link = animes[1].find(u'a', recursive=False) - link_parts = recommended_media_link.get(u'href').split(u'/') - # of the form /anime|manga/64/Rozen_Maiden - recommended_media = getattr(self.session, link_parts[1])(int(link_parts[2])).set({u'title': recommended_media_link.text}) - - recommendation_text = row.find(u'p').text - - recommendation_menu = row.find(u'div', recursive=False) - utilities.extract_tags(recommendation_menu) - recommendation_date = utilities.parse_profile_date(recommendation_menu.text.split(u' - ')[1]) - - user_info[u'recommendations'][liked_media] = {link_parts[1]: recommended_media, 'text': recommendation_text, 'date': recommendation_date} - except: - if not self.session.suppress_parse_exceptions: - raise - - return user_info - - def parse_clubs(self, clubs_page): - """Parses the DOM and returns user clubs attributes. - - :type clubs_page: :class:`bs4.BeautifulSoup` - :param clubs_page: MAL user clubs page's DOM - - :rtype: dict - :return: User clubs attributes. - - """ - user_info = self.parse_sidebar(clubs_page) - second_col = clubs_page.find(u'div', {u'id': u'content'}).find(u'table').find(u'tr').find_all(u'td', recursive=False)[1] - - try: - user_info[u'clubs'] = [] - - club_list = second_col.find(u'ol') - if club_list: - clubs = club_list.find_all(u'li') - for row in clubs: - club_link = row.find(u'a') - link_parts = club_link.get(u'href').split(u'?cid=') - # of the form /clubs.php?cid=10178 - user_info[u'clubs'].append(self.session.club(int(link_parts[1])).set({u'name': club_link.text})) - except: - if not self.session.suppress_parse_exceptions: - raise - return user_info - - def parse_friends(self, friends_page): - """Parses the DOM and returns user friends attributes. - - :type friends_page: :class:`bs4.BeautifulSoup` - :param friends_page: MAL user friends page's DOM - - :rtype: dict - :return: User friends attributes. - - """ - user_info = self.parse_sidebar(friends_page) - second_col = friends_page.find(u'div', {u'id': u'content'}).find(u'table').find(u'tr').find_all(u'td', recursive=False)[1] - - try: - user_info[u'friends'] = {} - - friends = second_col.find_all(u'div', {u'class': u'friendHolder'}) - if friends: - for row in friends: - block = row.find(u'div', {u'class': u'friendBlock'}) - cols = block.find_all(u'div') - - friend_link = cols[1].find(u'a') - friend = self.session.user(friend_link.text) - - friend_info = {} - if len(cols) > 2 and cols[2].text != u'': - friend_info[u'last_active'] = utilities.parse_profile_date(cols[2].text.strip()) - - if len(cols) > 3 and cols[3].text != u'': - friend_info[u'since'] = utilities.parse_profile_date(cols[3].text.replace(u'Friends since', '').strip()) - user_info[u'friends'][friend] = friend_info - except: - if not self.session.suppress_parse_exceptions: - raise - - return user_info + if not self.session.suppress_parse_exceptions: + raise - def load(self): - """Fetches the MAL user page and sets the current user's attributes. + return user_info - :rtype: :class:`.User` - :return: Current user object. + def parse_reviews(self, reviews_page): + """Parses the DOM and returns user reviews attributes. - """ - user_profile = self.session.session.get(u'http://myanimelist.net/profile/' + utilities.urlencode(self.username)).text - self.set(self.parse(utilities.get_clean_dom(user_profile))) - return self - - def load_reviews(self): - """Fetches the MAL user reviews page and sets the current user's reviews attributes. - - :rtype: :class:`.User` - :return: Current user object. - - """ - page = 0 - # collect all reviews over all pages. - review_collection = [] - while True: - user_reviews = self.session.session.get(u'http://myanimelist.net/profile/' + utilities.urlencode(self.username) + u'/reviews&' + urllib.urlencode({u'p': page})).text - parse_result = self.parse_reviews(utilities.get_clean_dom(user_reviews)) - if page == 0: - # only set attributes once the first time around. - self.set(parse_result) - if len(parse_result[u'reviews']) == 0: - break - review_collection.append(parse_result[u'reviews']) - page += 1 - - # merge the review collections into one review dict, and set it. - self.set({ - 'reviews': {k: v for d in review_collection for k,v in d.iteritems()} - }) - return self - - def load_recommendations(self): - """Fetches the MAL user recommendations page and sets the current user's recommendations attributes. - - :rtype: :class:`.User` - :return: Current user object. - - """ - user_recommendations = self.session.session.get(u'http://myanimelist.net/profile/' + utilities.urlencode(self.username) + u'/recommendations').text - self.set(self.parse_recommendations(utilities.get_clean_dom(user_recommendations))) - return self - - def load_clubs(self): - """Fetches the MAL user clubs page and sets the current user's clubs attributes. - - :rtype: :class:`.User` - :return: Current user object. - - """ - user_clubs = self.session.session.get(u'http://myanimelist.net/profile/' + utilities.urlencode(self.username) + u'/clubs').text - self.set(self.parse_clubs(utilities.get_clean_dom(user_clubs))) - return self - - def load_friends(self): - """Fetches the MAL user friends page and sets the current user's friends attributes. - - :rtype: :class:`.User` - :return: Current user object. + :type reviews_page: :class:`bs4.BeautifulSoup` + :param reviews_page: MAL user reviews page's DOM - """ - user_friends = self.session.session.get(u'http://myanimelist.net/profile/' + utilities.urlencode(self.username) + u'/friends').text - self.set(self.parse_friends(utilities.get_clean_dom(user_friends))) - return self - - @property - @loadable(u'load') - def id(self): - """User ID. - """ - return self._id - - @property - @loadable(u'load') - def picture(self): - """User's picture. - """ - return self._picture + :rtype: dict + :return: User reviews attributes. - @property - @loadable(u'load') - def favorite_anime(self): - """A list of :class:`myanimelist.anime.Anime` objects containing this user's favorite anime. - """ - return self._favorite_anime - - @property - @loadable(u'load') - def favorite_manga(self): - """A list of :class:`myanimelist.manga.Manga` objects containing this user's favorite manga. - """ - return self._favorite_manga - - @property - @loadable(u'load') - def favorite_characters(self): - """A dict with :class:`myanimelist.character.Character` objects as keys and :class:`myanimelist.media.Media` as values. - """ - return self._favorite_characters - - @property - @loadable(u'load') - def favorite_people(self): - """A list of :class:`myanimelist.person.Person` objects containing this user's favorite people. - """ - return self._favorite_people - - @property - @loadable(u'load') - def last_online(self): - """A :class:`datetime.datetime` object marking when this user was active on MAL. - """ - return self._last_online - - @property - @loadable(u'load') - def gender(self): - """This user's gender. - """ - return self._gender - - @property - @loadable(u'load') - def birthday(self): - """A :class:`datetime.datetime` object marking this user's birthday. - """ - return self._birthday - - @property - @loadable(u'load') - def location(self): - """This user's location. - """ - return self._location + """ + user_info = self.parse_sidebar(reviews_page) + second_col = \ + reviews_page.find(u'div', {u'id': u'content'}).find(u'table').find(u'tr').find_all(u'td', recursive=False)[1] - @property - @loadable(u'load') - def website(self): - """This user's website. - """ - return self._website - - @property - @loadable(u'load') - def join_date(self): - """A :class:`datetime.datetime` object marking when this user joined MAL. - """ - return self._join_date - - @property - @loadable(u'load') - def access_rank(self): - """This user's access rank on MAL. - """ - return self._access_rank + try: + user_info[u'reviews'] = {} + reviews = second_col.find_all(u'div', {u'class': u'borderDark'}, recursive=False) + if reviews: + for row in reviews: + review_info = {} + try: + (meta_elt, review_elt) = row.find_all(u'div', recursive=False)[0:2] + except ValueError: + raise + meta_rows = meta_elt.find_all(u'div', recursive=False) + review_info[u'date'] = utilities.parse_profile_date(meta_rows[0].find(u'div').text) + media_link = meta_rows[0].find(u'a') + link_parts = media_link.get(u'href').split(u'/') + # of the form /(anime|manga)/9760/Hoshi_wo_Ou_Kodomo + media = getattr(self.session, link_parts[1])(int(link_parts[2])).set({u'title': media_link.text}) + + helpfuls = meta_rows[1].find(u'span', recursive=False) + helpful_match = re.match(r'(?P[0-9]+) of (?P[0-9]+)', + helpfuls.text).groupdict() + review_info[u'people_helped'] = int(helpful_match[u'people_helped']) + review_info[u'people_total'] = int(helpful_match[u'people_total']) + + consumption_match = re.match(r'(?P[0-9]+) of (?P[0-9?]+)', + meta_rows[2].text).groupdict() + review_info[u'media_consumed'] = int(consumption_match[u'media_consumed']) + if consumption_match[u'media_total'] == u'?': + review_info[u'media_total'] = None + else: + review_info[u'media_total'] = int(consumption_match[u'media_total']) + + review_info[u'rating'] = int(meta_rows[3].find(u'div').text.replace(u'Overall Rating: ', '')) + + for x in review_elt.find_all([u'div', 'a']): + x.extract() + review_info[u'text'] = review_elt.text.strip() + user_info[u'reviews'][media] = review_info + except: + if not self.session.suppress_parse_exceptions: + raise - @property - @loadable(u'load') - def anime_list_views(self): - """The number of times this user's anime list has been viewed. - """ - return self._anime_list_views + return user_info - @property - @loadable(u'load') - def manga_list_views(self): - """The number of times this user's manga list has been viewed. - """ - return self._manga_list_views + def parse_recommendations(self, recommendations_page): + """Parses the DOM and returns user recommendations attributes. - @property - @loadable(u'load') - def num_comments(self): - """The number of comments this user has made. - """ - return self._num_comments + :type recommendations_page: :class:`bs4.BeautifulSoup` + :param recommendations_page: MAL user recommendations page's DOM - @property - @loadable(u'load') - def num_forum_posts(self): - """The number of forum posts this user has made. - """ - return self._num_forum_posts - - @property - @loadable(u'load') - def last_list_updates(self): - """A dict of this user's last list updates, with keys as :class:`myanimelist.media.Media` objects, and values as dicts of attributes, e.g. {'status': str, 'episodes': int, 'total_episodes': int, 'time': :class:`datetime.datetime`} - """ - return self._last_list_updates + :rtype: dict + :return: User recommendations attributes. - @property - @loadable(u'load') - def about(self): - """This user's self-bio. - """ - return self._about + """ + user_info = self.parse_sidebar(recommendations_page) + second_col = recommendations_page.find(u'div', {u'id': u'content'}).find(u'table').find(u'tr').find_all(u'td', + recursive=False)[ + 1] - @property - @loadable(u'load') - def anime_stats(self): - """A dict of this user's anime stats, with keys as strings, and values as numerics. - """ - return self._anime_stats + try: + recommendations = second_col.find_all(u"div", {u"class": u"spaceit borderClass"}) + if recommendations: + user_info[u'recommendations'] = {} + for row in recommendations[1:]: + anime_table = row.find(u'table') + animes = anime_table.find_all(u'td') + liked_media_link = animes[0].find(u'a', recursive=False) + link_parts = liked_media_link.get(u'href').split(u'/') + # of the form /anime|manga/64/Rozen_Maiden + liked_media = getattr(self.session, link_parts[1])(int(link_parts[2])).set( + {u'title': liked_media_link.text}) + + recommended_media_link = animes[1].find(u'a', recursive=False) + link_parts = recommended_media_link.get(u'href').split(u'/') + # of the form /anime|manga/64/Rozen_Maiden + recommended_media = getattr(self.session, link_parts[1])(int(link_parts[2])).set( + {u'title': recommended_media_link.text}) + + recommendation_text = row.find(u'p').text + + recommendation_menu = row.find(u'div', recursive=False) + utilities.extract_tags(recommendation_menu) + recommendation_date = utilities.parse_profile_date(recommendation_menu.text.split(u' - ')[1]) + + user_info[u'recommendations'][liked_media] = {link_parts[1]: recommended_media, + 'text': recommendation_text, + 'date': recommendation_date} + except: + if not self.session.suppress_parse_exceptions: + raise - @property - @loadable(u'load') - def manga_stats(self): - """A dict of this user's manga stats, with keys as strings, and values as numerics. - """ - return self._manga_stats + return user_info - @property - @loadable(u'load_reviews') - def reviews(self): - """A dict of this user's reviews, with keys as :class:`myanimelist.media.Media` objects, and values as dicts of attributes, e.g. + def parse_clubs(self, clubs_page): + """Parses the DOM and returns user clubs attributes. - { + :type clubs_page: :class:`bs4.BeautifulSoup` + :param clubs_page: MAL user clubs page's DOM - 'people_helped': int, + :rtype: dict + :return: User clubs attributes. - 'people_total': int, + """ + user_info = self.parse_sidebar(clubs_page) + second_col = \ + clubs_page.find(u'div', {u'id': u'content'}).find(u'table').find(u'tr').find_all(u'td', recursive=False)[1] - 'media_consumed': int, + try: + user_info[u'clubs'] = [] + + club_list = second_col.find(u'ol') + if club_list: + clubs = club_list.find_all(u'li') + for row in clubs: + club_link = row.find(u'a') + link_parts = club_link.get(u'href').split(u'?cid=') + # of the form /clubs.php?cid=10178 + user_info[u'clubs'].append(self.session.club(int(link_parts[1])).set({u'name': club_link.text})) + except: + if not self.session.suppress_parse_exceptions: + raise + return user_info - 'media_total': int, + def parse_friends(self, friends_page): + """Parses the DOM and returns user friends attributes. - 'rating': int, + :type friends_page: :class:`bs4.BeautifulSoup` + :param friends_page: MAL user friends page's DOM - 'text': str, + :rtype: dict + :return: User friends attributes. - 'date': :class:`datetime.datetime` + """ + user_info = self.parse_sidebar(friends_page) + second_col = \ + friends_page.find(u'div', {u'id': u'content'}).find(u'table').find(u'tr').find_all(u'td', recursive=False)[1] - } + try: + user_info[u'friends'] = {} - """ - return self._reviews + friends = second_col.find_all(u'div', {u'class': u'friendHolder'}) + if friends: + for row in friends: + block = row.find(u'div', {u'class': u'friendBlock'}) + cols = block.find_all(u'div') - @property - @loadable(u'load_recommendations') - def recommendations(self): - """A dict of this user's recommendations, with keys as :class:`myanimelist.media.Media` objects, and values as dicts of attributes, e.g. + friend_link = cols[1].find(u'a') + friend = self.session.user(friend_link.text) - { + friend_info = {} + if len(cols) > 2 and cols[2].text != u'': + friend_info[u'last_active'] = utilities.parse_profile_date(cols[2].text.strip()) - 'anime|media': :class:`myanimelist.media.Media`, + if len(cols) > 3 and cols[3].text != u'': + friend_info[u'since'] = utilities.parse_profile_date( + cols[3].text.replace(u'Friends since', '').strip()) + user_info[u'friends'][friend] = friend_info + except: + if not self.session.suppress_parse_exceptions: + raise + + return user_info + + def load(self): + """Fetches the MAL user page and sets the current user's attributes. + + :rtype: :class:`.User` + :return: Current user object. + + """ + user_profile = self.session.session.get( + u'http://myanimelist.net/profile/' + utilities.urlencode(self.username)).text + self.set(self.parse(utilities.get_clean_dom(user_profile))) + return self + + def load_reviews(self): + """Fetches the MAL user reviews page and sets the current user's reviews attributes. + + :rtype: :class:`.User` + :return: Current user object. + + """ + page = 0 + # collect all reviews over all pages. + review_collection = [] + while True: + user_reviews = self.session.session.get(u'http://myanimelist.net/profile/' + utilities.urlencode( + self.username) + u'/reviews&' + urllib.urlencode({u'p': page})).text + parse_result = self.parse_reviews(utilities.get_clean_dom(user_reviews)) + if page == 0: + # only set attributes once the first time around. + self.set(parse_result) + if len(parse_result[u'reviews']) == 0: + break + review_collection.append(parse_result[u'reviews']) + page += 1 + + # merge the review collections into one review dict, and set it. + self.set({ + 'reviews': {k: v for d in review_collection for k, v in d.iteritems()} + }) + return self + + def load_recommendations(self): + """Fetches the MAL user recommendations page and sets the current user's recommendations attributes. + + :rtype: :class:`.User` + :return: Current user object. + + """ + user_recommendations = self.session.session.get( + u'http://myanimelist.net/profile/' + utilities.urlencode(self.username) + u'/recommendations').text + self.set(self.parse_recommendations(utilities.get_clean_dom(user_recommendations))) + return self + + def load_clubs(self): + """Fetches the MAL user clubs page and sets the current user's clubs attributes. + + :rtype: :class:`.User` + :return: Current user object. + + """ + user_clubs = self.session.session.get( + u'http://myanimelist.net/profile/' + utilities.urlencode(self.username) + u'/clubs').text + self.set(self.parse_clubs(utilities.get_clean_dom(user_clubs))) + return self + + def load_friends(self): + """Fetches the MAL user friends page and sets the current user's friends attributes. + + :rtype: :class:`.User` + :return: Current user object. + + """ + user_friends = self.session.session.get( + u'http://myanimelist.net/profile/' + utilities.urlencode(self.username) + u'/friends').text + self.set(self.parse_friends(utilities.get_clean_dom(user_friends))) + return self + + @property + @loadable(u'load') + def id(self): + """User ID. + """ + return self._id + + @property + @loadable(u'load') + def picture(self): + """User's picture. + """ + return self._picture + + @property + @loadable(u'load') + def favorite_anime(self): + """A list of :class:`myanimelist.anime.Anime` objects containing this user's favorite anime. + """ + return self._favorite_anime + + @property + @loadable(u'load') + def favorite_manga(self): + """A list of :class:`myanimelist.manga.Manga` objects containing this user's favorite manga. + """ + return self._favorite_manga + + @property + @loadable(u'load') + def favorite_characters(self): + """A dict with :class:`myanimelist.character.Character` objects as keys and :class:`myanimelist.media.Media` as values. + """ + return self._favorite_characters + + @property + @loadable(u'load') + def favorite_people(self): + """A list of :class:`myanimelist.person.Person` objects containing this user's favorite people. + """ + return self._favorite_people + + @property + @loadable(u'load') + def last_online(self): + """A :class:`datetime.datetime` object marking when this user was active on MAL. + """ + return self._last_online + + @property + @loadable(u'load') + def gender(self): + """This user's gender. + """ + return self._gender + + @property + @loadable(u'load') + def birthday(self): + """A :class:`datetime.datetime` object marking this user's birthday. + """ + return self._birthday + + @property + @loadable(u'load') + def location(self): + """This user's location. + """ + return self._location + + @property + @loadable(u'load') + def website(self): + """This user's website. + """ + return self._website + + @property + @loadable(u'load') + def join_date(self): + """A :class:`datetime.datetime` object marking when this user joined MAL. + """ + return self._join_date + + @property + @loadable(u'load') + def access_rank(self): + """This user's access rank on MAL. + """ + return self._access_rank + + @property + @loadable(u'load') + def anime_list_views(self): + """The number of times this user's anime list has been viewed. + """ + return self._anime_list_views + + @property + @loadable(u'load') + def manga_list_views(self): + """The number of times this user's manga list has been viewed. + """ + return self._manga_list_views + + @property + @loadable(u'load') + def num_comments(self): + """The number of comments this user has made. + """ + return self._num_comments + + @property + @loadable(u'load') + def num_forum_posts(self): + """The number of forum posts this user has made. + """ + return self._num_forum_posts + + @property + @loadable(u'load') + def last_list_updates(self): + """A dict of this user's last list updates, with keys as :class:`myanimelist.media.Media` objects, and values as dicts of attributes, e.g. {'status': str, 'episodes': int, 'total_episodes': int, 'time': :class:`datetime.datetime`} + """ + return self._last_list_updates + + @property + @loadable(u'load') + def about(self): + """This user's self-bio. + """ + return self._about + + @property + @loadable(u'load') + def anime_stats(self): + """A dict of this user's anime stats, with keys as strings, and values as numerics. + """ + return self._anime_stats + + @property + @loadable(u'load') + def manga_stats(self): + """A dict of this user's manga stats, with keys as strings, and values as numerics. + """ + return self._manga_stats + + @property + @loadable(u'load_reviews') + def reviews(self): + """A dict of this user's reviews, with keys as :class:`myanimelist.media.Media` objects, and values as dicts of attributes, e.g. + + { + + 'people_helped': int, + + 'people_total': int, + + 'media_consumed': int, + + 'media_total': int, + + 'rating': int, + + 'text': str, + + 'date': :class:`datetime.datetime` + + } + + """ + return self._reviews + + @property + @loadable(u'load_recommendations') + def recommendations(self): + """A dict of this user's recommendations, with keys as :class:`myanimelist.media.Media` objects, and values as dicts of attributes, e.g. + + { + + 'anime|media': :class:`myanimelist.media.Media`, - 'text': str, + 'text': str, - 'date': :class:`datetime.datetime` + 'date': :class:`datetime.datetime` - } - """ - return self._recommendations + } + """ + return self._recommendations - @property - @loadable(u'load_clubs') - def clubs(self): - """A list of :class:`myanimelist.club.Club` objects containing this user's club memberships. - """ - return self._clubs + @property + @loadable(u'load_clubs') + def clubs(self): + """A list of :class:`myanimelist.club.Club` objects containing this user's club memberships. + """ + return self._clubs - @property - @loadable(u'load_friends') - def friends(self): - """A dict of this user's friends, with keys as :class:`myanimelist.user.User` objects, and values as dicts of attributes, e.g. + @property + @loadable(u'load_friends') + def friends(self): + """A dict of this user's friends, with keys as :class:`myanimelist.user.User` objects, and values as dicts of attributes, e.g. - { + { - 'last_active': :class:`datetime.datetime`, + 'last_active': :class:`datetime.datetime`, - 'since': :class:`datetime.datetime` + 'since': :class:`datetime.datetime` - } - """ - return self._friends + } + """ + return self._friends - def anime_list(self): - """This user's anime list. + def anime_list(self): + """This user's anime list. - :rtype: :class:`myanimelist.anime_list.AnimeList` - :return: The desired anime list. - """ - return self.session.anime_list(self.username) + :rtype: :class:`myanimelist.anime_list.AnimeList` + :return: The desired anime list. + """ + return self.session.anime_list(self.username) - def manga_list(self): - """This user's manga list. + def manga_list(self): + """This user's manga list. - :rtype: :class:`myanimelist.manga_list.MangaList` - :return: The desired manga list. - """ - return self.session.manga_list(self.username) \ No newline at end of file + :rtype: :class:`myanimelist.manga_list.MangaList` + :return: The desired manga list. + """ + return self.session.manga_list(self.username) diff --git a/myanimelist/utilities.py b/myanimelist/utilities.py index c1db33e..8027812 100644 --- a/myanimelist/utilities.py +++ b/myanimelist/utilities.py @@ -5,131 +5,149 @@ import re import urllib + def fix_bad_html(html): - """ - Fixes for various DOM errors that MAL commits. - Yes, I know this is a cardinal sin, but there's really no elegant way to fix this. - """ - # on anime list pages, sometimes tds won't be properly opened. - html = re.sub(r'[\s]td class=', "L represents licensing company', 'L represents licensing company') - - # on manga character pages, sometimes the character info column will have an extra . - def manga_character_double_closed_div_picture(match): - return "\n\t\t\t
\n\t\t\t" - html = re.sub(r"""[^>]+)>\n\t\t\t\n\t\t\t\n\t\t\t""", manga_character_double_closed_div_picture, html) - - def manga_character_double_closed_div_character(match): - return """""" + match.group(u'char_name') + """\n\t\t\t
""" + match.group(u'role') + """
""" - html = re.sub(r"""(?P[^<]+)\n\t\t\t
(?P[A-Za-z ]+)
\n\t\t\t""", manga_character_double_closed_div_character, html) - return html + """ + Fixes for various DOM errors that MAL commits. + Yes, I know this is a cardinal sin, but there's really no elegant way to fix this. + """ + # on anime list pages, sometimes tds won't be properly opened. + html = re.sub(r'[\s]td class=', "L represents licensing company', + 'L represents licensing company') + + # on manga character pages, sometimes the character info column will have an extra . + def manga_character_double_closed_div_picture(match): + return "\n\t\t\t
\n\t\t\t" + + html = re.sub( + r"""[^>]+)>\n\t\t\t\n\t\t\t\n\t\t\t""", + manga_character_double_closed_div_picture, html) + + def manga_character_double_closed_div_character(match): + return """""" + match.group( + u'char_name') + """\n\t\t\t
""" + match.group( + u'role') + """
""" + + html = re.sub( + r"""(?P[^<]+)\n\t\t\t
(?P[A-Za-z ]+)
\n\t\t\t""", + manga_character_double_closed_div_character, html) + return html + def get_clean_dom(html): - """ - Given raw HTML from a MAL page, return a BeautifulSoup object with cleaned HTML. - """ - return bs4.BeautifulSoup(fix_bad_html(html), "html.parser") + """ + Given raw HTML from a MAL page, return a BeautifulSoup object with cleaned HTML. + """ + return bs4.BeautifulSoup(fix_bad_html(html), "html.parser") + def urlencode(url): - """ - Given a string, return a string that can be used safely in a MAL url. - """ - return urllib.urlencode({'': url.encode(u'utf-8').replace(' ', '_')})[1:].replace('%2F', '/') + """ + Given a string, return a string that can be used safely in a MAL url. + """ + return urllib.urlencode({'': url.encode(u'utf-8').replace(' ', '_')})[1:].replace('%2F', '/') + def extract_tags(tags): - map(lambda x: x.extract(), tags) + map(lambda x: x.extract(), tags) -def parse_profile_date(text, suppress=False): - """ - Parses a MAL date on a profile page. - May raise ValueError if a malformed date is found. - If text is "Unknown" or "?" or "Not available" then returns None. - Otherwise, returns a datetime.date object. - """ - try: - if text == u"Unknown" or text == u"?" or text == u"Not available": - return None - if text == u"Now": - return datetime.datetime.now() - - seconds_match = re.match(r'(?P[0-9]+) second(s)? ago', text) - if seconds_match: - return datetime.datetime.now() - datetime.timedelta(seconds=int(seconds_match.group(u'seconds'))) - - minutes_match = re.match(r'(?P[0-9]+) minute(s)? ago', text) - if minutes_match: - return datetime.datetime.now() - datetime.timedelta(minutes=int(minutes_match.group(u'minutes'))) - - hours_match = re.match(r'(?P[0-9]+) hour(s)? ago', text) - if hours_match: - return datetime.datetime.now() - datetime.timedelta(hours=int(hours_match.group(u'hours'))) - - today_match = re.match(r'Today, (?P[0-9]+):(?P[0-9]+) (?P[APM]+)', text) - if today_match: - hour = int(today_match.group(u'hour')) - minute = int(today_match.group(u'minute')) - am = today_match.group(u'am') - if am == u'PM' and hour < 12: - hour += 12 - today = datetime.date.today() - return datetime.datetime(year=today.year, month=today.month, day=today.day, hour=hour, minute=minute, second=0) - - yesterday_match = re.match(r'Yesterday, (?P[0-9]+):(?P[0-9]+) (?P[APM]+)', text) - if yesterday_match: - hour = int(yesterday_match.group(u'hour')) - minute = int(yesterday_match.group(u'minute')) - am = yesterday_match.group(u'am') - if am == u'PM' and hour < 12: - hour += 12 - yesterday = datetime.date.today() - datetime.timedelta(days=1) - return datetime.datetime(year=yesterday.year, month=yesterday.month, day=yesterday.day, hour=hour, minute=minute, second=0) +def parse_profile_date(text, suppress=False): + """ + Parses a MAL date on a profile page. + May raise ValueError if a malformed date is found. + If text is "Unknown" or "?" or "Not available" then returns None. + Otherwise, returns a datetime.date object. + """ try: - return datetime.datetime.strptime(text, '%m-%d-%y, %I:%M %p') - except ValueError: - pass - # see if it's a date. - try: - return datetime.datetime.strptime(text, '%m-%d-%y').date() - except ValueError: - pass - try: - return datetime.datetime.strptime(text, '%Y-%m-%d').date() - except ValueError: - pass - try: - return datetime.datetime.strptime(text, '%Y-%m-00').date() - except ValueError: - pass - try: - return datetime.datetime.strptime(text, '%Y-00-00').date() - except ValueError: - pass - try: - return datetime.datetime.strptime(text, '%B %d, %Y').date() - except ValueError: - pass - try: - return datetime.datetime.strptime(text, '%b %d, %Y').date() - except ValueError: - pass - try: - return datetime.datetime.strptime(text, '%Y').date() - except ValueError: - pass - try: - return datetime.datetime.strptime(text, '%b %d, %Y').date() - except ValueError: - pass - # see if it's a month/year pairing. - return datetime.datetime.strptime(text, '%b %Y').date() - except: - if suppress: - return None - raise \ No newline at end of file + if text == u"Unknown" or text == u"?" or text == u"Not available": + return None + if text == u"Now": + return datetime.datetime.now() + + seconds_match = re.match(r'(?P[0-9]+) second(s)? ago', text) + if seconds_match: + return datetime.datetime.now() - datetime.timedelta(seconds=int(seconds_match.group(u'seconds'))) + + minutes_match = re.match(r'(?P[0-9]+) minute(s)? ago', text) + if minutes_match: + return datetime.datetime.now() - datetime.timedelta(minutes=int(minutes_match.group(u'minutes'))) + + hours_match = re.match(r'(?P[0-9]+) hour(s)? ago', text) + if hours_match: + return datetime.datetime.now() - datetime.timedelta(hours=int(hours_match.group(u'hours'))) + + today_match = re.match(r'Today, (?P[0-9]+):(?P[0-9]+) (?P[APM]+)', text) + if today_match: + hour = int(today_match.group(u'hour')) + minute = int(today_match.group(u'minute')) + am = today_match.group(u'am') + if am == u'PM' and hour < 12: + hour += 12 + today = datetime.date.today() + return datetime.datetime(year=today.year, month=today.month, day=today.day, hour=hour, minute=minute, + second=0) + + yesterday_match = re.match(r'Yesterday, (?P[0-9]+):(?P[0-9]+) (?P[APM]+)', text) + if yesterday_match: + hour = int(yesterday_match.group(u'hour')) + minute = int(yesterday_match.group(u'minute')) + am = yesterday_match.group(u'am') + if am == u'PM' and hour < 12: + hour += 12 + yesterday = datetime.date.today() - datetime.timedelta(days=1) + return datetime.datetime(year=yesterday.year, month=yesterday.month, day=yesterday.day, hour=hour, + minute=minute, second=0) + + try: + return datetime.datetime.strptime(text, '%m-%d-%y, %I:%M %p') + except ValueError: + pass + # see if it's a date. + try: + return datetime.datetime.strptime(text, '%m-%d-%y').date() + except ValueError: + pass + try: + return datetime.datetime.strptime(text, '%Y-%m-%d').date() + except ValueError: + pass + try: + return datetime.datetime.strptime(text, '%Y-%m-00').date() + except ValueError: + pass + try: + return datetime.datetime.strptime(text, '%Y-00-00').date() + except ValueError: + pass + try: + return datetime.datetime.strptime(text, '%B %d, %Y').date() + except ValueError: + pass + try: + return datetime.datetime.strptime(text, '%b %d, %Y').date() + except ValueError: + pass + try: + return datetime.datetime.strptime(text, '%Y').date() + except ValueError: + pass + try: + return datetime.datetime.strptime(text, '%b %d, %Y').date() + except ValueError: + pass + # see if it's a month/year pairing. + return datetime.datetime.strptime(text, '%b %Y').date() + except: + if suppress: + return None + raise diff --git a/setup.py b/setup.py index 1cd2813..95cafc5 100644 --- a/setup.py +++ b/setup.py @@ -1,20 +1,20 @@ try: - from setuptools import setup + from setuptools import setup except ImportError: - from distutils.core import setup + from distutils.core import setup config = { - 'name': 'python-mal', - 'description': 'Provides programmatic access to MyAnimeList resources.', - 'author': 'Shal Dengeki', - 'license': 'LICENSE.txt', - 'url': 'https://github.com/shaldengeki/python-mal', - 'download_url': 'https://github.com/shaldengeki/python-mal/archive/master.zip', - 'author_email': 'shaldengeki@gmail.com', - 'version': '0.1.7', - 'install_requires': ['beautifulsoup4', 'requests', 'pytz', 'lxml'], - 'tests_require': ['nose'], - 'packages': ['myanimelist'] + 'name': 'python-mal', + 'description': 'Provides programmatic access to MyAnimeList resources.', + 'author': 'Shal Dengeki', + 'license': 'LICENSE.txt', + 'url': 'https://github.com/shaldengeki/python-mal', + 'download_url': 'https://github.com/shaldengeki/python-mal/archive/master.zip', + 'author_email': 'shaldengeki@gmail.com', + 'version': '0.1.7', + 'install_requires': ['beautifulsoup4', 'requests', 'pytz', 'lxml'], + 'tests_require': ['nose'], + 'packages': ['myanimelist'] } -setup(**config) \ No newline at end of file +setup(**config) diff --git a/tests/anime_list_tests.py b/tests/anime_list_tests.py index 571e64f..2b7db35 100644 --- a/tests/anime_list_tests.py +++ b/tests/anime_list_tests.py @@ -1,95 +1,182 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +from unittest import TestCase -from nose.tools import * import datetime import myanimelist.session import myanimelist.media_list import myanimelist.anime_list -class testAnimeListClass(object): - @classmethod - def setUpClass(self): - self.session = myanimelist.session.Session() - - self.shal = self.session.anime_list(u'shaldengeki') - self.fz = self.session.anime(10087) - self.trigun = self.session.anime(6) - self.clannad = self.session.anime(2167) - - self.pl = self.session.anime_list(u'PaperLuigi') - self.baccano = self.session.anime(2251) - self.pokemon = self.session.anime(20159) - self.dmc = self.session.anime(3702) - - self.mona = self.session.anime_list(u'monausicaa') - self.zombie = self.session.anime(3354) - self.lollipop = self.session.anime(1509) - self.musume = self.session.anime(5246) - - self.threger = self.session.anime_list(u'threger') - - @raises(TypeError) - def testNoUsernameInvalidAnimeList(self): - self.session.anime_list() - - @raises(myanimelist.media_list.InvalidMediaListError) - def testNonexistentUsernameInvalidAnimeList(self): - self.session.anime_list(u'aspdoifpjsadoifjapodsijfp').load() - - def testUserValid(self): - assert isinstance(self.shal, myanimelist.anime_list.AnimeList) - - def testUsername(self): - assert self.shal.username == u'shaldengeki' - assert self.mona.username == u'monausicaa' - - def testType(self): - assert self.shal.type == u'anime' - - def testList(self): - assert isinstance(self.shal.list, dict) and len(self.shal) == 146 - assert self.fz in self.shal and self.clannad in self.shal and self.trigun in self.shal - assert self.shal[self.fz][u'status'] == u'Watching' and self.shal[self.clannad][u'status'] == u'Completed' and self.shal[self.trigun][u'status'] == u'Plan to Watch' - assert self.shal[self.fz][u'score'] == None and self.shal[self.clannad][u'score'] == 9 and self.shal[self.trigun][u'score'] == None - assert self.shal[self.fz][u'episodes_watched'] == 6 and self.shal[self.clannad][u'episodes_watched'] == 23 and self.shal[self.trigun][u'episodes_watched'] == 6 - assert self.shal[self.fz][u'started'] == None and self.shal[self.clannad][u'started'] == None and self.shal[self.trigun][u'started'] == None - assert self.shal[self.fz][u'finished'] == None and self.shal[self.clannad][u'finished'] == None and self.shal[self.trigun][u'finished'] == None - - assert isinstance(self.pl.list, dict) and len(self.pl) >= 795 - assert self.baccano in self.pl and self.pokemon in self.pl and self.dmc in self.pl - assert self.pl[self.baccano][u'status'] == u'Completed' and self.pl[self.pokemon][u'status'] == u'On-Hold' and self.pl[self.dmc][u'status'] == u'Dropped' - assert self.pl[self.baccano][u'score'] == 10 and self.pl[self.pokemon][u'score'] == None and self.pl[self.dmc][u'score'] == 2 - assert self.pl[self.baccano][u'episodes_watched'] == 13 and self.pl[self.pokemon][u'episodes_watched'] == 2 and self.pl[self.dmc][u'episodes_watched'] == 1 - assert self.pl[self.baccano][u'started'] == datetime.date(year=2009, month=7, day=27) and self.pl[self.pokemon][u'started'] == datetime.date(year=2013, month=10, day=5) and self.pl[self.dmc][u'started'] == datetime.date(year=2010, month=9, day=27) - assert self.pl[self.baccano][u'finished'] == datetime.date(year=2009, month=7, day=28) and self.pl[self.pokemon][u'finished'] == None and self.pl[self.dmc][u'finished'] == None - - assert isinstance(self.mona.list, dict) and len(self.mona) >= 1822 - assert self.zombie in self.mona and self.lollipop in self.mona and self.musume in self.mona - assert self.mona[self.zombie][u'status'] == u'Completed' and self.mona[self.lollipop][u'status'] == u'On-Hold' and self.mona[self.musume][u'status'] == u'Plan to Watch' - assert self.mona[self.zombie][u'score'] == 7 and self.mona[self.lollipop][u'score'] == None and self.mona[self.musume][u'score'] == None - assert self.mona[self.zombie][u'episodes_watched'] == 2 and self.mona[self.lollipop][u'episodes_watched'] == 12 and self.mona[self.musume][u'episodes_watched'] == 0 - assert self.mona[self.zombie][u'started'] == None and self.mona[self.lollipop][u'started'] == datetime.date(year=2013, month=4, day=14) and self.mona[self.musume][u'started'] == None - assert self.mona[self.zombie][u'finished'] == None and self.mona[self.lollipop][u'finished'] == None and self.mona[self.musume][u'finished'] == None - - assert isinstance(self.threger.list, dict) and len(self.threger) == 0 - - def testStats(self): - assert isinstance(self.shal.stats, dict) and len(self.shal.stats) > 0 - assert self.shal.stats[u'watching'] == 10 and self.shal.stats[u'completed'] == 102 and self.shal.stats[u'on_hold'] == 1 and self.shal.stats[u'dropped'] == 5 and self.shal.stats[u'plan_to_watch'] == 28 and float(self.shal.stats[u'days_spent']) == 38.88 - - assert isinstance(self.pl.stats, dict) and len(self.pl.stats) > 0 - assert self.pl.stats[u'watching'] >= 0 and self.pl.stats[u'completed'] >= 355 and self.pl.stats[u'on_hold'] >= 0 and self.pl.stats[u'dropped'] >= 385 and self.pl.stats[u'plan_to_watch'] >= 0 and float(self.pl.stats[u'days_spent']) >= 125.91 - - assert isinstance(self.mona.stats, dict) and len(self.mona.stats) > 0 - assert self.mona.stats[u'watching'] >= 0 and self.mona.stats[u'completed'] >= 1721 and self.mona.stats[u'on_hold'] >= 0 and self.mona.stats[u'dropped'] >= 0 and self.mona.stats[u'plan_to_watch'] >= 0 and float(self.mona.stats[u'days_spent']) >= 470.30 - - assert isinstance(self.threger.stats, dict) and len(self.threger.stats) > 0 - assert self.threger.stats[u'watching'] == 0 and self.threger.stats[u'completed'] == 0 and self.threger.stats[u'on_hold'] == 0 and self.threger.stats[u'dropped'] == 0 and self.threger.stats[u'plan_to_watch'] == 0 and float(self.threger.stats[u'days_spent']) == 0.00 - - def testSection(self): - assert isinstance(self.shal.section(u'Watching'), dict) and self.fz in self.shal.section(u'Watching') - assert isinstance(self.pl.section(u'Completed'), dict) and self.baccano in self.pl.section(u'Completed') - assert isinstance(self.mona.section(u'Plan to Watch'), dict) and self.musume in self.mona.section(u'Plan to Watch') - assert isinstance(self.threger.section(u'Watching'), dict) and len(self.threger.section(u'Watching')) == 0 + +class testAnimeListClass(TestCase): + @classmethod + def setUpClass(self): + self.session = myanimelist.session.Session() + + self.shal = self.session.anime_list(u'shaldengeki') + self.fz = self.session.anime(10087) + self.trigun = self.session.anime(6) + self.clannad = self.session.anime(2167) + + self.pl = self.session.anime_list(u'PaperLuigi') + self.baccano = self.session.anime(2251) + self.pokemon = self.session.anime(20159) + self.dmc = self.session.anime(3702) + + self.mona = self.session.anime_list(u'monausicaa') + self.zombie = self.session.anime(3354) + self.lollipop = self.session.anime(1509) + self.musume = self.session.anime(5246) + + self.threger = self.session.anime_list(u'threger') + + def testNoUsernameInvalidAnimeList(self): + with self.assertRaises(TypeError): + self.session.anime_list() + + def testNonexistentUsernameInvalidAnimeList(self): + with self.assertRaises(myanimelist.media_list.InvalidMediaListError): + self.session.anime_list(u'aspdoifpjsadoifjapodsijfp').load() + + def testUserValid(self): + self.assertIsInstance(self.shal, myanimelist.anime_list.AnimeList) + + def testUsername(self): + self.assertEqual(self.shal.username, u'shaldengeki') + self.assertEqual(self.mona.username, u'monausicaa') + + def testType(self): + self.assertEqual(self.shal.type, u'anime') + + def testList(self): + self.assertIsInstance(self.shal.list, dict) + self.assertEqual(len(self.shal), 146) + + self.assertIn(self.fz, self.shal) + self.assertIn(self.clannad, self.shal) + self.assertIn(self.trigun, self.shal) + + self.assertEqual(self.shal[self.fz][u'status'], u'Watching') + self.assertEqual(self.shal[self.clannad][u'status'], u'Completed') + self.assertEqual(self.shal[self.trigun][u'status'], u'Plan to Watch') + + self.assertIsNone(self.shal[self.fz][u'score']) + self.assertEqual(self.shal[self.clannad][u'score'], 9) + self.assertIsNone(self.shal[self.trigun][u'score']) + + self.assertEqual(self.shal[self.fz][u'episodes_watched'], 6) + self.assertEqual(self.shal[self.clannad][u'episodes_watched'], 23) + self.assertEqual(self.shal[self.trigun][u'episodes_watched'], 6) + + self.assertIsNone(self.shal[self.fz][u'started']) + self.assertIsNone(self.shal[self.clannad][u'started']) + self.assertIsNone(self.shal[self.trigun][u'started']) + + self.assertIsNone(self.shal[self.fz][u'finished']) + self.assertIsNone(self.shal[self.clannad][u'finished']) + self.assertIsNone(self.shal[self.trigun][u'finished']) + + self.assertIsInstance(self.pl.list, dict) + self.assertGreaterEqual(len(self.pl), 795) + + self.assertIn(self.baccano, self.pl) + self.assertIn(self.pokemon, self.pl) + self.assertIn(self.dmc, self.pl) + + self.assertEqual(self.pl[self.baccano][u'status'], u'Completed') + self.assertEqual(self.pl[self.pokemon][u'status'], u'On-Hold') + self.assertEqual(self.pl[self.dmc][u'status'], u'Dropped') + + self.assertEqual(self.pl[self.baccano][u'score'], 10) + self.assertIsNone(self.pl[self.pokemon][u'score']) + self.assertEqual(self.pl[self.dmc][u'score'], 2) + + self.assertEqual(self.pl[self.baccano][u'episodes_watched'], 13) + self.assertEqual(self.pl[self.pokemon][u'episodes_watched'], 2) + self.assertEqual(self.pl[self.dmc][u'episodes_watched'], 1) + + self.assertEqual(self.pl[self.baccano][u'started'], datetime.date(year=2009, month=7, day=27)) + self.assertEqual(self.pl[self.pokemon][u'started'], datetime.date(year=2013, month=10, day=5)) + self.assertEqual(self.pl[self.dmc][u'started'], datetime.date(year=2010, month=9, day=27)) + + self.assertEqual(self.pl[self.baccano][u'finished'], datetime.date(year=2009, month=7, day=28)) + self.assertIsNone(self.pl[self.pokemon][u'finished']) + self.assertIsNone(self.pl[self.dmc][u'finished']) + + self.assertIsInstance(self.mona.list, dict) + self.assertGreaterEqual(len(self.mona), 1822) + + self.assertIn(self.zombie, self.mona) + self.assertIn(self.lollipop, self.mona) + self.assertIn(self.musume, self.mona) + + self.assertEqual(self.mona[self.zombie][u'status'], u'Completed') + self.assertEqual(self.mona[self.lollipop][u'status'], u'On-Hold') + self.assertEqual(self.mona[self.musume][u'status'], u'Plan to Watch') + + self.assertEqual(self.mona[self.zombie][u'score'], 7) + self.assertIsNone(self.mona[self.lollipop][u'score']) + self.assertIsNone(self.mona[self.musume][u'score']) + + self.assertEqual(self.mona[self.zombie][u'episodes_watched'], 2) + self.assertEqual(self.mona[self.lollipop][u'episodes_watched'], 12) + self.assertEqual(self.mona[self.musume][u'episodes_watched'], 0) + + self.assertIsNone(self.mona[self.zombie][u'started']) + self.assertEqual(self.mona[self.lollipop][u'started'], datetime.date(year=2013, month=4, day=14)) + self.assertIsNone(self.mona[self.musume][u'started']) + + self.assertIsNone(self.mona[self.zombie][u'finished']) + self.assertIsNone(self.mona[self.lollipop][u'finished']) + self.assertIsNone(self.mona[self.musume][u'finished']) + + self.assertIsInstance(self.threger.list, dict) + self.assertEqual(len(self.threger), 0) + + def testStats(self): + self.assertIsInstance(self.shal.stats, dict) + self.assertGreater(len(self.shal.stats), 0) + self.assertEqual(self.shal.stats[u'watching'], 10) + self.assertEqual(self.shal.stats[u'completed'], 102) + self.assertEqual(self.shal.stats[u'on_hold'], 1) + self.assertEqual(self.shal.stats[u'dropped'], 5) + self.assertEqual(self.shal.stats[u'plan_to_watch'], 28) + self.assertEqual(float(self.shal.stats[u'days_spent']), 38.88) + + self.assertIsInstance(self.pl.stats, dict) + self.assertGreater(len(self.pl.stats), 0) + self.assertGreaterEqual(self.pl.stats[u'watching'], 0) + self.assertGreaterEqual(self.pl.stats[u'completed'], 355) + self.assertGreaterEqual(self.pl.stats[u'on_hold'], 0) + self.assertGreaterEqual(self.pl.stats[u'dropped'], 385) + self.assertGreaterEqual(self.pl.stats[u'plan_to_watch'], 0) + self.assertGreaterEqual(float(self.pl.stats[u'days_spent']), 125.91) + + self.assertIsInstance(self.mona.stats, dict) + self.assertGreater(len(self.mona.stats), 0) + self.assertGreaterEqual(self.mona.stats[u'watching'], 0) + self.assertGreaterEqual(self.mona.stats[u'completed'], 1721) + self.assertGreaterEqual(self.mona.stats[u'on_hold'], 0) + self.assertGreaterEqual(self.mona.stats[u'dropped'], 0) + self.assertGreaterEqual(self.mona.stats[u'plan_to_watch'], 0) + self.assertGreaterEqual(float(self.mona.stats[u'days_spent']), 470.30) + + self.assertIsInstance(self.threger.stats, dict) + self.assertGreaterEqual(len(self.threger.stats), 0) + self.assertEqual(self.threger.stats[u'watching'], 0) + self.assertEqual(self.threger.stats[u'completed'], 0) + self.assertEqual(self.threger.stats[u'on_hold'], 0) + self.assertEqual(self.threger.stats[u'dropped'], 0) + self.assertEqual(self.threger.stats[u'plan_to_watch'], 0) + self.assertEqual(float(self.threger.stats[u'days_spent']), 0.00) + + def testSection(self): + self.assertIsInstance(self.shal.section(u'Watching'), dict) + self.assertIn(self.fz, self.shal.section(u'Watching')) + self.assertIsInstance(self.pl.section(u'Completed'), dict) + self.assertIn(self.baccano, self.pl.section(u'Completed')) + self.assertIsInstance(self.mona.section(u'Plan to Watch'), dict) + self.assertIn(self.musume, self.mona.section(u'Plan to Watch')) + self.assertIsInstance(self.threger.section(u'Watching'), dict) + self.assertEqual(len(self.threger.section(u'Watching')), 0) diff --git a/tests/anime_tests.py b/tests/anime_tests.py index 6e104a7..c17e06c 100644 --- a/tests/anime_tests.py +++ b/tests/anime_tests.py @@ -1,249 +1,377 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -from nose.tools import * +from unittest import TestCase + import datetime import myanimelist.session import myanimelist.anime -class testAnimeClass(object): - @classmethod - def setUpClass(self): - self.session = myanimelist.session.Session() - self.bebop = self.session.anime(1) - self.sunrise = self.session.producer(14) - self.action = self.session.genre(1) - self.hex = self.session.character(94717) - self.hex_va = self.session.person(5766) - self.bebop_side_story = self.session.anime(5) - self.space_tag = self.session.tag(u'space') - - self.spicy_wolf = self.session.anime(2966) - self.kadokawa = self.session.producer(352) - self.romance = self.session.genre(22) - self.holo = self.session.character(7373) - self.holo_va = self.session.person(70) - self.spicy_wolf_sequel = self.session.anime(6007) - self.adventure_tag = self.session.tag(u'adventure') - - self.space_dandy = self.session.anime(20057) - self.funi = self.session.producer(102) - self.scifi = self.session.genre(24) - self.toaster = self.session.character(110427) - self.toaster_va = self.session.person(611) - - self.totoro = self.session.anime(523) - self.gkids = self.session.producer(783) - self.supernatural = self.session.genre(37) - self.satsuki = self.session.character(267) - self.satsuki_va = self.session.person(1104) - - self.prisma = self.session.anime(18851) - self.silver_link = self.session.producer(300) - self.fantasy = self.session.genre(10) - self.ilya = self.session.character(503) - self.ilya_va = self.session.person(117) - - self.invalid_anime = self.session.anime(457384754) - self.latest_anime = myanimelist.anime.Anime.newest(self.session) - - self.non_tagged_anime = self.session.anime(10448) - - @raises(TypeError) - def testNoIDInvalidAnime(self): - self.session.anime() - - @raises(TypeError) - def testNoSessionInvalidLatestAnime(self): - myanimelist.anime.Anime.newest() - - @raises(myanimelist.anime.InvalidAnimeError) - def testNegativeInvalidAnime(self): - self.session.anime(-1) - - @raises(myanimelist.anime.InvalidAnimeError) - def testFloatInvalidAnime(self): - self.session.anime(1.5) - - @raises(myanimelist.anime.InvalidAnimeError) - def testNonExistentAnime(self): - self.invalid_anime.load() - - def testLatestAnime(self): - assert isinstance(self.latest_anime, myanimelist.anime.Anime) - assert self.latest_anime.id > 20000 - - def testAnimeValid(self): - assert isinstance(self.bebop, myanimelist.anime.Anime) - - def testTitle(self): - assert self.bebop.title == u'Cowboy Bebop' - assert self.spicy_wolf.title == u'Ookami to Koushinryou' - assert self.space_dandy.title == u'Space☆Dandy' - assert self.prisma.title == u'Fate/kaleid liner Prisma☆Illya: Undoukai de Dance!' - - def testPicture(self): - assert isinstance(self.spicy_wolf.picture, unicode) - assert isinstance(self.space_dandy.picture, unicode) - assert isinstance(self.bebop.picture, unicode) - assert isinstance(self.totoro.picture, unicode) - assert isinstance(self.prisma.picture, unicode) - - def testAlternativeTitles(self): - assert u'Japanese' in self.bebop.alternative_titles and isinstance(self.bebop.alternative_titles[u'Japanese'], list) and u'カウボーイビバップ' in self.bebop.alternative_titles[u'Japanese'] - assert u'English' in self.spicy_wolf.alternative_titles and isinstance(self.spicy_wolf.alternative_titles[u'English'], list) and u'Spice and Wolf' in self.spicy_wolf.alternative_titles[u'English'] - assert u'Japanese' in self.space_dandy.alternative_titles and isinstance(self.space_dandy.alternative_titles[u'Japanese'], list) and u'スペース☆ダンディ' in self.space_dandy.alternative_titles[u'Japanese'] - assert u'Japanese' in self.prisma.alternative_titles and isinstance(self.prisma.alternative_titles[u'Japanese'], list) and u'Fate/kaleid liner プリズマ☆イリヤ 運動会 DE ダンス!' in self.prisma.alternative_titles[u'Japanese'] - - def testTypes(self): - assert self.bebop.type == u'TV' - assert self.totoro.type == u'Movie' - assert self.prisma.type == u'OVA' - - def testEpisodes(self): - assert self.spicy_wolf.episodes == 13 - assert self.bebop.episodes == 26 - assert self.totoro.episodes == 1 - assert self.space_dandy.episodes == 13 - assert self.prisma.episodes == 1 - - def testStatus(self): - assert self.spicy_wolf.status == u'Finished Airing' - assert self.totoro.status == u'Finished Airing' - assert self.bebop.status == u'Finished Airing' - assert self.space_dandy.status == u'Finished Airing' - assert self.prisma.status == u'Finished Airing' - - def testAired(self): - assert self.spicy_wolf.aired == (datetime.date(month=1, day=8, year=2008), datetime.date(month=5, day=30, year=2008)) - assert self.bebop.aired == (datetime.date(month=4, day=3, year=1998), datetime.date(month=4, day=24, year=1999)) - assert self.space_dandy.aired == (datetime.date(month=1, day=5, year=2014),datetime.date(month=3,day=27,year=2014)) - assert self.totoro.aired == (datetime.date(month=4, day=16, year=1988),) - assert self.prisma.aired == (datetime.date(month=3, day=10, year=2014),) - - def testProducers(self): - assert isinstance(self.bebop.producers, list) and len(self.bebop.producers) > 0 - assert self.sunrise in self.bebop.producers - assert isinstance(self.spicy_wolf.producers, list) and len(self.spicy_wolf.producers) > 0 - assert self.kadokawa in self.spicy_wolf.producers - assert isinstance(self.space_dandy.producers, list) and len(self.space_dandy.producers) > 0 - assert self.funi in self.space_dandy.producers - assert isinstance(self.totoro.producers, list) and len(self.totoro.producers) > 0 - assert self.gkids in self.totoro.producers - assert isinstance(self.prisma.producers, list) and len(self.prisma.producers) > 0 - assert self.silver_link in self.prisma.producers - - def testGenres(self): - assert isinstance(self.bebop.genres, list) and len(self.bebop.genres) > 0 - assert self.action in self.bebop.genres - assert isinstance(self.spicy_wolf.genres, list) and len(self.spicy_wolf.genres) > 0 - assert self.romance in self.spicy_wolf.genres - assert isinstance(self.space_dandy.genres, list) and len(self.space_dandy.genres) > 0 - assert self.scifi in self.space_dandy.genres - assert isinstance(self.totoro.genres, list) and len(self.totoro.genres) > 0 - assert self.supernatural in self.totoro.genres - assert isinstance(self.prisma.genres, list) and len(self.prisma.genres) > 0 - assert self.fantasy in self.prisma.genres - - def testDuration(self): - assert self.spicy_wolf.duration.total_seconds() == 1440 - assert self.totoro.duration.total_seconds() == 5160 - assert self.space_dandy.duration.total_seconds() == 1440 - assert self.bebop.duration.total_seconds() == 1440 - assert self.prisma.duration.total_seconds() == 1500 - - def testScore(self): - assert isinstance(self.spicy_wolf.score, tuple) - assert self.spicy_wolf.score[0] > 0 and self.spicy_wolf.score[0] < 10 - assert isinstance(self.spicy_wolf.score[1], int) and self.spicy_wolf.score[1] >= 0 - assert isinstance(self.bebop.score, tuple) - assert self.bebop.score[0] > 0 and self.bebop.score[0] < 10 - assert isinstance(self.bebop.score[1], int) and self.bebop.score[1] >= 0 - assert isinstance(self.space_dandy.score, tuple) - assert self.space_dandy.score[0] > 0 and self.space_dandy.score[0] < 10 - assert isinstance(self.space_dandy.score[1], int) and self.space_dandy.score[1] >= 0 - assert isinstance(self.totoro.score, tuple) - assert self.totoro.score[0] > 0 and self.totoro.score[0] < 10 - assert isinstance(self.totoro.score[1], int) and self.totoro.score[1] >= 0 - assert self.prisma.score[0] > 0 and self.prisma.score[0] < 10 - assert isinstance(self.prisma.score[1], int) and self.prisma.score[1] >= 0 - - def testRank(self): - assert isinstance(self.spicy_wolf.rank, int) and self.spicy_wolf.rank > 0 - assert isinstance(self.bebop.rank, int) and self.bebop.rank > 0 - assert isinstance(self.space_dandy.rank, int) and self.space_dandy.rank > 0 - assert isinstance(self.totoro.rank, int) and self.totoro.rank > 0 - assert isinstance(self.prisma.rank, int) and self.prisma.rank > 0 - - def testPopularity(self): - assert isinstance(self.spicy_wolf.popularity, int) and self.spicy_wolf.popularity > 0 - assert isinstance(self.bebop.popularity, int) and self.bebop.popularity > 0 - assert isinstance(self.space_dandy.popularity, int) and self.space_dandy.popularity > 0 - assert isinstance(self.totoro.popularity, int) and self.totoro.popularity > 0 - assert isinstance(self.prisma.popularity, int) and self.prisma.popularity > 0 - - def testMembers(self): - assert isinstance(self.spicy_wolf.members, int) and self.spicy_wolf.members > 0 - assert isinstance(self.bebop.members, int) and self.bebop.members > 0 - assert isinstance(self.space_dandy.members, int) and self.space_dandy.members > 0 - assert isinstance(self.totoro.members, int) and self.totoro.members > 0 - assert isinstance(self.prisma.members, int) and self.prisma.members > 0 - - def testFavorites(self): - assert isinstance(self.spicy_wolf.favorites, int) and self.spicy_wolf.favorites > 0 - assert isinstance(self.bebop.favorites, int) and self.bebop.favorites > 0 - assert isinstance(self.space_dandy.favorites, int) and self.space_dandy.favorites > 0 - assert isinstance(self.totoro.favorites, int) and self.totoro.favorites > 0 - assert isinstance(self.prisma.favorites, int) and self.prisma.favorites > 0 - - def testSynopsis(self): - assert isinstance(self.spicy_wolf.synopsis, unicode) and len(self.spicy_wolf.synopsis) > 0 and u'Holo' in self.spicy_wolf.synopsis - assert isinstance(self.bebop.synopsis, unicode) and len(self.bebop.synopsis) > 0 and u'Spike' in self.bebop.synopsis - assert isinstance(self.space_dandy.synopsis, unicode) and len(self.space_dandy.synopsis) > 0 and u'dandy' in self.space_dandy.synopsis - assert isinstance(self.totoro.synopsis, unicode) and len(self.totoro.synopsis) > 0 and u'Satsuki' in self.totoro.synopsis - assert isinstance(self.prisma.synopsis, unicode) and len(self.prisma.synopsis) > 0 and u'Einzbern' in self.prisma.synopsis - - def testRelated(self): - assert isinstance(self.spicy_wolf.related, dict) and 'Sequel' in self.spicy_wolf.related and self.spicy_wolf_sequel in self.spicy_wolf.related[u'Sequel'] - assert isinstance(self.bebop.related, dict) and 'Side story' in self.bebop.related and self.bebop_side_story in self.bebop.related[u'Side story'] - - def testCharacters(self): - assert isinstance(self.spicy_wolf.characters, dict) and len(self.spicy_wolf.characters) > 0 - assert self.holo in self.spicy_wolf.characters and self.spicy_wolf.characters[self.holo][u'role'] == 'Main' and self.holo_va in self.spicy_wolf.characters[self.holo][u'voice_actors'] - assert isinstance(self.bebop.characters, dict) and len(self.bebop.characters) > 0 - assert self.hex in self.bebop.characters and self.bebop.characters[self.hex][u'role'] == 'Supporting' and self.hex_va in self.bebop.characters[self.hex][u'voice_actors'] - assert isinstance(self.space_dandy.characters, dict) and len(self.space_dandy.characters) > 0 - assert self.toaster in self.space_dandy.characters and self.space_dandy.characters[self.toaster][u'role'] == 'Supporting' and self.toaster_va in self.space_dandy.characters[self.toaster][u'voice_actors'] - assert isinstance(self.totoro.characters, dict) and len(self.totoro.characters) > 0 - assert self.satsuki in self.totoro.characters and self.totoro.characters[self.satsuki][u'role'] == 'Main' and self.satsuki_va in self.totoro.characters[self.satsuki][u'voice_actors'] - assert isinstance(self.prisma.characters, dict) and len(self.prisma.characters) > 0 - assert self.ilya in self.prisma.characters and self.prisma.characters[self.ilya][u'role'] == 'Main' and self.ilya_va in self.prisma.characters[self.ilya][u'voice_actors'] - - def testVoiceActors(self): - assert isinstance(self.spicy_wolf.voice_actors, dict) and len(self.spicy_wolf.voice_actors) > 0 - assert self.holo_va in self.spicy_wolf.voice_actors and self.spicy_wolf.voice_actors[self.holo_va][u'role'] == 'Main' and self.spicy_wolf.voice_actors[self.holo_va][u'character'] == self.holo - assert isinstance(self.bebop.voice_actors, dict) and len(self.bebop.voice_actors) > 0 - assert self.hex_va in self.bebop.voice_actors and self.bebop.voice_actors[self.hex_va][u'role'] == 'Supporting' and self.bebop.voice_actors[self.hex_va][u'character'] == self.hex - assert isinstance(self.space_dandy.voice_actors, dict) and len(self.space_dandy.voice_actors) > 0 - assert self.toaster_va in self.space_dandy.voice_actors and self.space_dandy.voice_actors[self.toaster_va][u'role'] == 'Supporting' and self.space_dandy.voice_actors[self.toaster_va][u'character'] == self.toaster - assert isinstance(self.totoro.voice_actors, dict) and len(self.totoro.voice_actors) > 0 - assert self.satsuki_va in self.totoro.voice_actors and self.totoro.voice_actors[self.satsuki_va][u'role'] == 'Main' and self.totoro.voice_actors[self.satsuki_va][u'character'] == self.satsuki - assert isinstance(self.prisma.voice_actors, dict) and len(self.prisma.voice_actors) > 0 - assert self.ilya_va in self.prisma.voice_actors and self.prisma.voice_actors[self.ilya_va][u'role'] == 'Main' and self.prisma.voice_actors[self.ilya_va][u'character'] == self.ilya - - def testStaff(self): - assert isinstance(self.spicy_wolf.staff, dict) and len(self.spicy_wolf.staff) > 0 - assert self.session.person(472) in self.spicy_wolf.staff and u'Producer' in self.spicy_wolf.staff[self.session.person(472)] - assert isinstance(self.bebop.staff, dict) and len(self.bebop.staff) > 0 - assert self.session.person(12221) in self.bebop.staff and u'Inserted Song Performance' in self.bebop.staff[self.session.person(12221)] - assert isinstance(self.space_dandy.staff, dict) and len(self.space_dandy.staff) > 0 - assert self.session.person(10127) in self.space_dandy.staff and all(x in self.space_dandy.staff[self.session.person(10127)] for x in [u'Theme Song Composition', u'Theme Song Lyrics', u'Theme Song Performance']) - assert isinstance(self.totoro.staff, dict) and len(self.totoro.staff) > 0 - assert self.session.person(1870) in self.totoro.staff and all(x in self.totoro.staff[self.session.person(1870)] for x in [u'Director', u'Script', u'Storyboard']) - assert isinstance(self.prisma.staff, dict) and len(self.prisma.staff) > 0 - assert self.session.person(10617) in self.prisma.staff and u'ADR Director' in self.prisma.staff[self.session.person(10617)] - - def testPopularTags(self): - assert len(self.bebop.popular_tags) > 0 and self.space_tag in self.bebop.popular_tags - assert len(self.spicy_wolf.popular_tags) > 0 and self.adventure_tag in self.spicy_wolf.popular_tags - assert len(self.non_tagged_anime.popular_tags) == 0 \ No newline at end of file + +class testAnimeClass(TestCase): + @classmethod + def setUpClass(self): + self.session = myanimelist.session.Session() + self.bebop = self.session.anime(1) + self.sunrise = self.session.producer(14) + self.action = self.session.genre(1) + self.hex = self.session.character(94717) + self.hex_va = self.session.person(5766) + self.bebop_side_story = self.session.anime(5) + self.space_tag = self.session.tag(u'space') + + self.spicy_wolf = self.session.anime(2966) + self.kadokawa = self.session.producer(352) + self.romance = self.session.genre(22) + self.holo = self.session.character(7373) + self.holo_va = self.session.person(70) + self.spicy_wolf_sequel = self.session.anime(6007) + self.adventure_tag = self.session.tag(u'adventure') + + self.space_dandy = self.session.anime(20057) + self.funi = self.session.producer(102) + self.scifi = self.session.genre(24) + self.toaster = self.session.character(110427) + self.toaster_va = self.session.person(611) + + self.totoro = self.session.anime(523) + self.gkids = self.session.producer(783) + self.supernatural = self.session.genre(37) + self.satsuki = self.session.character(267) + self.satsuki_va = self.session.person(1104) + + self.prisma = self.session.anime(18851) + self.silver_link = self.session.producer(300) + self.fantasy = self.session.genre(10) + self.ilya = self.session.character(503) + self.ilya_va = self.session.person(117) + + self.invalid_anime = self.session.anime(457384754) + self.latest_anime = myanimelist.anime.Anime.newest(self.session) + + self.non_tagged_anime = self.session.anime(10448) + + def testNoIDInvalidAnime(self): + with self.assertRaises(TypeError): + self.session.anime() + + def testNoSessionInvalidLatestAnime(self): + with self.assertRaises(TypeError): + myanimelist.anime.Anime.newest() + + def testNegativeInvalidAnime(self): + with self.assertRaises(myanimelist.anime.InvalidAnimeError): + self.session.anime(-1) + + def testFloatInvalidAnime(self): + with self.assertRaises(myanimelist.anime.InvalidAnimeError): + self.session.anime(1.5) + + def testNonExistentAnime(self): + with self.assertRaises(myanimelist.anime.InvalidAnimeError): + self.invalid_anime.load() + + def testLatestAnime(self): + self.assertIsInstance(self.latest_anime, myanimelist.anime.Anime) + self.assertGreater(self.latest_anime.id, 20000) + + def testAnimeValid(self): + self.assertIsInstance(self.bebop, myanimelist.anime.Anime) + + def testTitle(self): + self.assertEqual(self.bebop.title, u'Cowboy Bebop') + self.assertEqual(self.spicy_wolf.title, u'Ookami to Koushinryou') + self.assertEqual(self.space_dandy.title, u'Space☆Dandy') + self.assertEqual(self.prisma.title, u'Fate/kaleid liner Prisma☆Illya: Undoukai de Dance!') + + def testPicture(self): + self.assertIsInstance(self.spicy_wolf.picture, unicode) + self.assertIsInstance(self.space_dandy.picture, unicode) + self.assertIsInstance(self.bebop.picture, unicode) + self.assertIsInstance(self.totoro.picture, unicode) + self.assertIsInstance(self.prisma.picture, unicode) + + def testAlternativeTitles(self): + self.assertIn(u'Japanese', self.bebop.alternative_titles) + self.assertIsInstance(self.bebop.alternative_titles[u'Japanese'], list) + self.assertIn(u'カウボーイビバップ', self.bebop.alternative_titles[u'Japanese']) + + self.assertIn(u'English', self.spicy_wolf.alternative_titles) + self.assertIsInstance(self.spicy_wolf.alternative_titles[u'English'], list) + self.assertIn(u'Spice Wolf', self.spicy_wolf.alternative_titles[u'English']) + + self.assertIn(u'Japanese', self.space_dandy.alternative_titles) + self.assertIsInstance(self.space_dandy.alternative_titles[u'Japanese'], list) + self.assertIn(u'スペース☆ダンディ', self.space_dandy.alternative_titles[u'Japanese']) + + self.assertIn(u'Japanese', self.prisma.alternative_titles) + self.assertIsInstance(self.prisma.alternative_titles[u'Japanese'], list) + self.assertIn(u'Fate/kaleid liner プリズマ☆イリヤ 運動会 DE ダンス!', + self.prisma.alternative_titles[u'Japanese']) + + def testTypes(self): + self.assertEqual(self.bebop.type, u'TV') + self.assertEqual(self.totoro.type, u'Movie') + self.assertEqual(self.prisma.type, u'OVA') + + def testEpisodes(self): + self.assertEqual(self.spicy_wolf.episodes, 13) + self.assertEqual(self.bebop.episodes, 26) + self.assertEqual(self.totoro.episodes, 1) + self.assertEqual(self.space_dandy.episodes, 13) + self.assertEqual(self.prisma.episodes, 1) + + def testStatus(self): + self.assertEqual(self.spicy_wolf.status, u'Finished Airing') + self.assertEqual(self.totoro.status, u'Finished Airing') + self.assertEqual(self.bebop.status, u'Finished Airing') + self.assertEqual(self.space_dandy.status, u'Finished Airing') + self.assertEqual(self.prisma.status, u'Finished Airing') + + def testAired(self): + self.assertEqual(self.spicy_wolf.aired == (datetime.date(month=1, day=8, year=2008), datetime.date(month=5, day=30, year=2008))) + + self.assertEqual(self.bebop.aired, (datetime.date(month=4, day=3, year=1998), datetime.date(month=4, day=24, year=1999))) + self.assertEqual(self.space_dandy.aired, (datetime.date(month=1, day=5, year=2014), datetime.date(month=3, day=27, year=2014))) + self.assertEqual(self.totoro.aired, (datetime.date(month=4, day=16, year=1988),)) + self.assertEqual(self.prisma.aired, (datetime.date(month=3, day=10, year=2014),)) + + def testProducers(self): + self.assertIsInstance(self.bebop.producers, list) + self.assertGreater(len(self.bebop.producers), 0) + + self.assertIn(self.sunrise, self.bebop.producers) + + self.assertIsInstance(self.spicy_wolf.producers, list) + self.assertGreater(len(self.spicy_wolf.producers), 0) + + self.assertIn(self.kadokawa, self.spicy_wolf.producers) + + self.assertIsInstance(self.space_dandy.producers, list) + self.assertGreater(len(self.space_dandy.producers), 0) + + self.assertIn(self.funi, self.space_dandy.producers) + + self.assertIsInstance(self.totoro.producers, list) + self.assertGreater(len(self.totoro.producers), 0) + + self.assertIn(self.gkids, self.totoro.producers) + + self.assertIsInstance(self.prisma.producers, list) + self.assertGreater(len(self.prisma.producers), 0) + + self.assertIn(self.silver_link, self.prisma.producers) + + def testGenres(self): + self.assertIsInstance(self.bebop.genres, list) + self.assertGreater(len(self.bebop.genres), 0) + self.assertIn(self.action, self.bebop.genres) + self.assertIsInstance(self.spicy_wolf.genres, list) + self.assertGreater(len(self.spicy_wolf.genres), 0) + + self.assertIn(self.romance, self.spicy_wolf.genres) + self.assertIsInstance(self.space_dandy.genres, list) + self.assertGreater(len(self.space_dandy.genres), 0) + + self.assertIn(self.scifi, self.space_dandy.genres) + self.assertIsInstance(self.totoro.genres, list) + self.assertGreater(len(self.totoro.genres), 0) + + self.assertIn(self.supernatural, self.totoro.genres) + self.assertIsInstance(self.prisma.genres, list) + self.assertGreater(len(self.prisma.genres), 0) + self.assertIn(self.fantasy, self.prisma.genres) + + def testDuration(self): + self.assertEquals(self.spicy_wolf.duration.total_seconds(), 1440) + self.assertEquals(self.totoro.duration.total_seconds(), 5160) + self.assertEquals(self.space_dandy.duration.total_seconds(), 1440) + self.assertEquals(self.bebop.duration.total_seconds(), 1440) + self.assertEquals(self.prisma.duration.total_seconds(), 1500) + + def testScore(self): + self.assertIsInstance(self.spicy_wolf.score, tuple) + self.assertGreater(self.spicy_wolf.score[0], 0) + self.assertLess(self.spicy_wolf.score[0], 10) + self.assertIsInstance(self.spicy_wolf.score[1], int) + self.assertGreaterEqual(self.spicy_wolf.score[1], 0) + self.assertIsInstance(self.bebop.score, tuple) + self.assertGreater(self.bebop.score[0], 0) + self.assertLess(self.bebop.score[0], 10) + self.assertIsInstance(self.bebop.score[1], int) + self.assertGreaterEqual(self.bebop.score[1], 0) + self.assertIsInstance(self.space_dandy.score, tuple) + self.assertGreater(self.space_dandy.score[0], 0) + self.assertLess(self.space_dandy.score[0], 10) + self.assertIsInstance(self.space_dandy.score[1], int) + self.assertGreaterEqual(self.space_dandy.score[1], 0) + self.assertIsInstance(self.totoro.score, tuple) + self.assertGreater(self.totoro.score[0], 0) + self.assertLess(self.totoro.score[0], 10) + self.assertIsInstance(self.totoro.score[1], int) + self.assertGreaterEqual(self.totoro.score[1], 0) + self.assertGreater(self.prisma.score[0], 0) + self.assertLess(self.prisma.score[0], 10) + self.assertIsInstance(self.prisma.score[1], int) + self.assertGreaterEqual(self.prisma.score[1], 0) + + def testRank(self): + self.assertIsInstance(self.spicy_wolf.rank, int) + self.assertGreater(self.spicy_wolf.rank, 0) + self.assertIsInstance(self.bebop.rank, int) + self.assertGreater(self.bebop.rank, 0) + self.assertIsInstance(self.space_dandy.rank, int) + self.assertGreater(self.space_dandy.rank, 0) + self.assertIsInstance(self.totoro.rank, int) + self.assertGreater(self.totoro.rank, 0) + self.assertIsInstance(self.prisma.rank, int) + self.assertGreater(self.prisma.rank, 0) + + def testPopularity(self): + self.assertIsInstance(self.spicy_wolf.popularity, int) + self.assertGreater(self.spicy_wolf.popularity, 0) + self.assertIsInstance(self.bebop.popularity, int) + self.assertGreater(self.bebop.popularity, 0) + self.assertIsInstance(self.space_dandy.popularity, int) + self.assertGreater(self.space_dandy.popularity, 0) + self.assertIsInstance(self.totoro.popularity, int) + self.assertGreater(self.totoro.popularity, 0) + self.assertIsInstance(self.prisma.popularity, int) + self.assertGreater(self.prisma.popularity, 0) + + def testMembers(self): + self.assertIsInstance(self.spicy_wolf.members, int) + self.assertGreater(self.spicy_wolf.members, 0) + self.assertIsInstance(self.bebop.members, int) + self.assertGreater(self.bebop.members, 0) + self.assertIsInstance(self.space_dandy.members, int) + self.assertGreater(self.space_dandy.members, 0) + self.assertIsInstance(self.totoro.members, int) + self.assertGreater(self.totoro.members, 0) + self.assertIsInstance(self.prisma.members, int) + self.assertGreater(self.prisma.members, 0) + + def testFavorites(self): + self.assertIsInstance(self.spicy_wolf.favorites, int) + self.assertGreater(self.spicy_wolf.favorites, 0) + self.assertIsInstance(self.bebop.favorites, int) + self.assertGreater(self.bebop.favorites, 0) + self.assertIsInstance(self.space_dandy.favorites, int) + self.assertGreater(self.space_dandy.favorites, 0) + self.assertIsInstance(self.totoro.favorites, int) + self.assertGreater(self.totoro.favorites, 0) + self.assertIsInstance(self.prisma.favorites, int) + self.assertGreater(self.prisma.favorites, 0) + + def testSynopsis(self): + self.assertIsInstance(self.spicy_wolf.synopsis, unicode) + self.assertGreater(len(self.spicy_wolf.synopsis), 0) + self.assertIn(u'Holo', self.spicy_wolf.synopsis) + self.assertIsInstance(self.bebop.synopsis, unicode) + self.assertGreater(len(self.bebop.synopsis), 0) + self.assertIn(u'Spike', self.bebop.synopsis) + self.assertIsInstance(self.space_dandy.synopsis, unicode) + self.assertGreater(len(self.space_dandy.synopsis), 0) + self.assertIn(u'dandy', self.space_dandy.synopsis) + self.assertIsInstance(self.totoro.synopsis, unicode) + self.assertGreater(len(self.totoro.synopsis), 0) + self.assertIn(u'Satsuki', self.totoro.synopsis) + self.assertIsInstance(self.prisma.synopsis, unicode) + self.assertGreater(len(self.prisma.synopsis), 0) + self.assertIn(u'Einzbern', self.prisma.synopsis) + + def testRelated(self): + self.assertIsInstance(self.spicy_wolf.related, dict) + self.assertIn('Sequel', self.spicy_wolf.related) + self.assertIn(self.spicy_wolf_sequel, self.spicy_wolf.related[u'Sequel']) + self.assertIsInstance(self.bebop.related, dict) + self.assertIn('Side story', self.bebop.related) + self.assertIn(self.bebop_side_story, self.bebop.related[u'Side story']) + + def testCharacters(self): + self.assertIsInstance(self.spicy_wolf.characters, dict) + self.assertGreater(len(self.spicy_wolf.characters), 0) + self.assertIn(self.holo, self.spicy_wolf.characters) + self.assertEqual(self.spicy_wolf.characters[self.holo][u'role'], 'Main') + self.assertIn(self.holo_va, self.spicy_wolf.characters[self.holo][u'voice_actors']) + + self.assertIsInstance(self.bebop.characters, dict) + self.assertGreater(len(self.bebop.characters), 0) + + self.assertIn(self.hex, self.bebop.characters) + self.assertEqual(self.bebop.characters[self.hex][u'role'], 'Supporting') + self.assertIn(self.hex_va, self.bebop.characters[self.hex][u'voice_actors']) + self.assertIsInstance(self.space_dandy.characters, dict) + self.assertGreater(len(self.space_dandy.characters), 0) + self.assertIn(self.toaster, self.space_dandy.characters) + self.assertEqual(self.space_dandy.characters[self.toaster][u'role'], 'Supporting') + self.assertIn(self.toaster_va, self.space_dandy.characters[self.toaster][u'voice_actors']) + self.assertIsInstance(self.totoro.characters, dict) + self.assertGreater(len(self.totoro.characters), 0) + self.assertIn(self.satsuki, self.totoro.characters) + self.assertEqual(self.totoro.characters[self.satsuki][u'role'], 'Main') + self.assertIn(self.satsuki_va, self.totoro.characters[self.satsuki][u'voice_actors']) + self.assertIsInstance(self.prisma.characters, dict) + self.assertGreater(len(self.prisma.characters), 0) + self.assertIn(self.ilya, self.prisma.characters) + self.assertEqual(self.prisma.characters[self.ilya][u'role'], 'Main') + self.assertIn(self.ilya_va, self.prisma.characters[self.ilya][u'voice_actors']) + + def testVoiceActors(self): + self.assertIsInstance(self.spicy_wolf.voice_actors, dict) + self.assertGreater(len(self.spicy_wolf.voice_actors), 0) + self.assertIn(self.holo_va, self.spicy_wolf.voice_actors) + self.assertEqual(self.spicy_wolf.voice_actors[self.holo_va][u'role'], 'Main') + self.assertEqual(self.spicy_wolf.voice_actors[self.holo_va][u'character'], self.holo) + self.assertIsInstance(self.bebop.voice_actors, dict) + self.assertGreater(len(self.bebop.voice_actors), 0) + self.assertIn(self.hex_va, self.bebop.voice_actors) + self.assertEqual(self.bebop.voice_actors[self.hex_va][u'role'], 'Supporting') + self.assertEqual(self.bebop.voice_actors[self.hex_va][u'character'], self.hex) + self.assertIsInstance(self.space_dandy.voice_actors, dict) + self.assertGreater(len(self.space_dandy.voice_actors), 0) + self.assertIn(self.toaster_va, self.space_dandy.voice_actors) + self.assertEqual(self.space_dandy.voice_actors[self.toaster_va][u'role'], 'Supporting') + self.assertEqual(self.space_dandy.voice_actors[self.toaster_va][u'character'], self.toaster) + self.assertIsInstance(self.totoro.voice_actors, dict) + self.assertGreater(len(self.totoro.voice_actors), 0) + self.assertIn(self.satsuki_va, self.totoro.voice_actors) + self.assertEqual(self.totoro.voice_actors[self.satsuki_va][u'role'], 'Main') + self.assertEqual(self.totoro.voice_actors[self.satsuki_va][u'character'], self.satsuki) + self.assertIsInstance(self.prisma.voice_actors, dict) + self.assertGreater(len(self.prisma.voice_actors), 0) + self.assertIn(self.ilya_va, self.prisma.voice_actors) + self.assertEqual(self.prisma.voice_actors[self.ilya_va][u'role'], 'Main') + self.assertEqual(self.prisma.voice_actors[self.ilya_va][u'character'], self.ilya) + + def testStaff(self): + self.assertIsInstance(self.spicy_wolf.staff, dict) + self.assertGreater(len(self.spicy_wolf.staff), 0) + self.assertIn(self.session.person(472), self.spicy_wolf.staff) + self.assertIn(u'Producer', self.spicy_wolf.staff[self.session.person(472)]) + self.assertIsInstance(self.bebop.staff, dict) + self.assertGreater(len(self.bebop.staff), 0) + self.assertIn(self.session.person(12221), self.bebop.staff) + self.assertIn(u'Inserted Song Performance', self.bebop.staff[self.session.person(12221)]) + self.assertIsInstance(self.space_dandy.staff, dict) + self.assertGreater(len(self.space_dandy.staff), 0) + self.assertIn(self.session.person(10127), self.space_dandy.staff) + + for x in [u'Theme Song Composition', u'Theme Song Lyrics', u'Theme Song Performance']: + self.assertIn(x, self.space_dandy.staff[self.session.person(10127)]) + self.assertIsInstance(self.totoro.staff, dict) + self.assertGreater(len(self.totoro.staff), 0) + self.assertIn(self.session.person(1870), self.totoro.staff) + for x in [u'Director', u'Script', u'Storyboard']: + self.assertIn(x, self.totoro.staff[self.session.person(1870)]) + self.assertIsInstance(self.prisma.staff, dict) + self.assertGreater(len(self.prisma.staff), 0) + self.assertIn(self.session.person(10617), self.prisma.staff) + self.assertIn(u'ADR Director', self.prisma.staff[self.session.person(10617)]) + + def testPopularTags(self): + self.assertGreater(len(self.bebop.popular_tags), 0) + self.assertIn(self.space_tag, self.bebop.popular_tags) + self.assertGreater(len(self.spicy_wolf.popular_tags), 0) + self.assertIn(self.adventure_tag, self.spicy_wolf.popular_tags) + self.assertEquals(len(self.non_tagged_anime.popular_tags), 0) diff --git a/tests/character_tests.py b/tests/character_tests.py index 5f31a8b..00b9f9c 100644 --- a/tests/character_tests.py +++ b/tests/character_tests.py @@ -1,91 +1,132 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -from nose.tools import * +from unittest import TestCase import myanimelist.session import myanimelist.character import myanimelist.user -class testCharacterClass(object): - @classmethod - def setUpClass(self): - self.session = myanimelist.session.Session() - self.spike = self.session.character(1) - self.ed = self.session.character(11) - self.maria = self.session.character(112693) - self.invalid_character = self.session.character(457384754) - - @raises(TypeError) - def testNoIDInvalidCharacter(self): - self.session.character() - - @raises(myanimelist.character.InvalidCharacterError) - def testNegativeInvalidCharacter(self): - self.session.character(-1) - - @raises(myanimelist.character.InvalidCharacterError) - def testFloatInvalidCharacter(self): - self.session.character(1.5) - - @raises(myanimelist.character.InvalidCharacterError) - def testNonExistentCharacter(self): - self.invalid_character.load() - - def testCharacterValid(self): - assert isinstance(self.spike, myanimelist.character.Character) - assert isinstance(self.maria, myanimelist.character.Character) - - def testName(self): - assert self.spike.name == u'Spike Spiegel' - assert self.ed.name == u'Edward Elric' - assert self.maria.name == u'Maria' - - def testFullName(self): - assert self.spike.full_name == u'Spike Spiegel' - assert self.ed.full_name == u'Edward "Ed, Fullmetal Alchemist, Hagane no shounen, Chibi, Pipsqueak" Elric' - assert self.maria.full_name == u'Maria' - - def testJapaneseName(self): - assert self.spike.name_jpn == u'スパイク・スピーゲル' - assert self.ed.name_jpn == u'エドワード・エルリック' - assert self.maria.name_jpn == u'マリア' - - def testDescription(self): - assert isinstance(self.spike.description, unicode) and len(self.spike.description) > 0 - assert isinstance(self.ed.description, unicode) and len(self.ed.description) > 0 - assert isinstance(self.maria.description, unicode) and len(self.maria.description) > 0 - - def testPicture(self): - assert isinstance(self.spike.picture, unicode) and len(self.spike.picture) > 0 - assert isinstance(self.ed.picture, unicode) and len(self.ed.picture) > 0 - assert isinstance(self.maria.picture, unicode) and len(self.maria.picture) > 0 - - def testPictures(self): - assert isinstance(self.spike.pictures, list) and len(self.spike.pictures) > 0 and all(map(lambda p: isinstance(p, unicode) and p.startswith(u'http://'), self.spike.pictures)) - assert isinstance(self.ed.pictures, list) and len(self.ed.pictures) > 0 and all(map(lambda p: isinstance(p, unicode) and p.startswith(u'http://'), self.ed.pictures)) - assert isinstance(self.maria.pictures, list) - - def testAnimeography(self): - assert isinstance(self.spike.animeography, dict) and len(self.spike.animeography) > 0 and self.session.anime(1) in self.spike.animeography - assert isinstance(self.ed.animeography, dict) and len(self.ed.animeography) > 0 and self.session.anime(5114) in self.ed.animeography - assert isinstance(self.maria.animeography, dict) and len(self.maria.animeography) > 0 and self.session.anime(26441) in self.maria.animeography - - def testMangaography(self): - assert isinstance(self.spike.mangaography, dict) and len(self.spike.mangaography) > 0 and self.session.manga(173) in self.spike.mangaography - assert isinstance(self.ed.mangaography, dict) and len(self.ed.mangaography) > 0 and self.session.manga(4658) in self.ed.mangaography - assert isinstance(self.maria.mangaography, dict) and len(self.maria.mangaography) > 0 and self.session.manga(12336) in self.maria.mangaography - - def testNumFavorites(self): - assert isinstance(self.spike.num_favorites, int) and self.spike.num_favorites > 12000 - assert isinstance(self.ed.num_favorites, int) and self.ed.num_favorites > 19000 - assert isinstance(self.maria.num_favorites, int) - - def testFavorites(self): - assert isinstance(self.spike.favorites, list) and len(self.spike.favorites) > 12000 and all(map(lambda u: isinstance(u, myanimelist.user.User), self.spike.favorites)) - assert isinstance(self.ed.favorites, list) and len(self.ed.favorites) > 19000 and all(map(lambda u: isinstance(u, myanimelist.user.User), self.ed.favorites)) - assert isinstance(self.maria.favorites, list) - - def testClubs(self): - assert isinstance(self.spike.clubs, list) and len(self.spike.clubs) > 50 and all(map(lambda u: isinstance(u, myanimelist.club.Club), self.spike.clubs)) - assert isinstance(self.ed.clubs, list) and len(self.ed.clubs) > 200 and all(map(lambda u: isinstance(u, myanimelist.club.Club), self.ed.clubs)) - assert isinstance(self.maria.clubs, list) \ No newline at end of file + +class testCharacterClass(TestCase): + @classmethod + def setUpClass(self): + self.session = myanimelist.session.Session() + self.spike = self.session.character(1) + self.ed = self.session.character(11) + self.maria = self.session.character(112693) + self.invalid_character = self.session.character(457384754) + + def testNoIDInvalidCharacter(self): + with self.assertRaises(TypeError): + self.session.character() + + def testNegativeInvalidCharacter(self): + with self.assertRaises(myanimelist.character.InvalidCharacterError): + self.session.character(-1) + + def testFloatInvalidCharacter(self): + with self.assertRaises(myanimelist.character.InvalidCharacterError): + self.session.character(1.5) + + def testNonExistentCharacter(self): + with self.assertRaises(myanimelist.character.InvalidCharacterError): + self.invalid_character.load() + + def testCharacterValid(self): + self.assertIsInstance(self.spike, myanimelist.character.Character) + self.assertIsInstance(self.maria, myanimelist.character.Character) + + def testName(self): + self.assertEqual(self.spike.name, u'Spike Spiegel') + self.assertEqual(self.ed.name, u'Edward Elric') + self.assertEqual(self.maria.name, u'Maria') + + def testFullName(self): + self.assertEqual(self.spike.full_name, u'Spike Spiegel') + self.assertEqual(self.ed.full_name, u'Edward "Ed, Fullmetal Alchemist, Hagane no shounen, Chibi, Pipsqueak" Elric') + self.assertEqual(self.maria.full_name, u'Maria') + + def testJapaneseName(self): + self.assertEqual(self.spike.name_jpn, u'スパイク・スピーゲル') + self.assertEqual(self.ed.name_jpn, u'エドワード・エルリック') + self.assertEqual(self.maria.name_jpn, u'マリア') + + def testDescription(self): + self.assertIsInstance(self.spike.description, unicode) + self.assertGreater(len(self.spike.description), 0) + self.assertIsInstance(self.ed.description, unicode) + self.assertGreater(len(self.ed.description), 0) + self.assertIsInstance(self.maria.description, unicode) + self.assertGreater(len(self.maria.description), 0) + + def testPicture(self): + self.assertIsInstance(self.spike.picture, unicode) + self.assertGreater(len(self.spike.picture), 0) + self.assertIsInstance(self.ed.picture, unicode) + self.assertGreater(len(self.ed.picture), 0) + self.assertIsInstance(self.maria.picture, unicode) + self.assertGreater(len(self.maria.picture), 0) + + def testPictures(self): + self.assertIsInstance(self.spike.pictures, list) + self.assertGreater(len(self.spike.pictures), 0) + for p in self.spike.pictures: + self.assertIsInstance(p, unicode) + self.assertTrue(p.startswith(u'http://')) + self.assertIsInstance(self.ed.pictures, list) + self.assertGreater(len(self.ed.pictures), 0) + for p in self.spike.pictures: + self.assertIsInstance(p, unicode) + self.assertTrue(p.startswith(u'http://')) + self.assertIsInstance(self.maria.pictures, list) + + def testAnimeography(self): + self.assertIsInstance(self.spike.animeography, dict) + self.assertGreater(len(self.spike.animeography), 0) + self.assertIn(self.session.anime(1), self.spike.animeography) + self.assertIsInstance(self.ed.animeography, dict) + self.assertGreater(len(self.ed.animeography), 0) + self.assertIn(self.session.anime(5114), self.ed.animeography) + self.assertIsInstance(self.maria.animeography, dict) + self.assertGreater(len(self.maria.animeography), 0) + self.assertIn(self.session.anime(26441), self.maria.animeography) + + def testMangaography(self): + self.assertIsInstance(self.spike.mangaography, dict) + self.assertGreater(len(self.spike.mangaography), 0) + self.assertIn(self.session.manga(173), self.spike.mangaography) + self.assertIsInstance(self.ed.mangaography, dict) + self.assertGreater(len(self.ed.mangaography), 0) + self.assertIn(self.session.manga(4658), self.ed.mangaography) + self.assertIsInstance(self.maria.mangaography, dict) + self.assertGreater(len(self.maria.mangaography), 0) + self.assertIn(self.session.manga(12336), self.maria.mangaography) + + def testNumFavorites(self): + self.assertIsInstance(self.spike.num_favorites, int) + self.assertGreater(self.spike.num_favorites, 12000) + self.assertIsInstance(self.ed.num_favorites, int) + self.assertGreater(self.ed.num_favorites, 19000) + self.assertIsInstance(self.maria.num_favorites, int) + + def testFavorites(self): + self.assertIsInstance(self.spike.favorites, list) + self.assertGreater(len(self.spike.favorites), 12000) + for u in self.spike.favorites: + self.assertIsInstance(u, myanimelist.user.User) + self.assertIsInstance(self.ed.favorites, list) + self.assertGreater(len(self.ed.favorites), 19000) + for u in self.ed.favorites: + self.assertIsInstance(u, myanimelist.user.User) + self.assertIsInstance(self.maria.favorites, list) + + def testClubs(self): + self.assertIsInstance(self.spike.clubs, list) + self.assertGreater(len(self.spike.clubs), 50) + for u in self.spike.clubs: + self.assertIsInstance(u, myanimelist.club.Club) + self.assertIsInstance(self.ed.clubs, list) + self.assertGreater(len(self.ed.clubs), 200) + for u in self.spike.clubs: + self.assertIsInstance(u, myanimelist.club.Club) + self.assertIsInstance(self.maria.clubs, list) diff --git a/tests/manga_list_tests.py b/tests/manga_list_tests.py index 5e80c51..3752175 100644 --- a/tests/manga_list_tests.py +++ b/tests/manga_list_tests.py @@ -1,97 +1,168 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -from nose.tools import * +from unittest import TestCase import datetime import myanimelist.session import myanimelist.media_list import myanimelist.manga_list -class testMangaListClass(object): - @classmethod - def setUpClass(self): - self.session = myanimelist.session.Session() - - self.shal = self.session.manga_list(u'shaldengeki') - self.tomoyo_after = self.session.manga(3941) - self.fma = self.session.manga(25) - - self.pl = self.session.manga_list(u'PaperLuigi') - self.to_love_ru = self.session.manga(671) - self.amnesia = self.session.manga(15805) - self.sao = self.session.manga(21479) - - self.josh = self.session.manga_list(u'angryaria') - self.juicy = self.session.manga(13250) - self.tsubasa = self.session.manga(1147) - self.jojo = self.session.manga(1706) - - self.threger = self.session.manga_list(u'threger') - - @raises(TypeError) - def testNoUsernameInvalidMangaList(self): - self.session.manga_list() - - @raises(myanimelist.media_list.InvalidMediaListError) - def testNonexistentUsernameInvalidMangaList(self): - self.session.manga_list(u'aspdoifpjsadoifjapodsijfp').load() - - def testUserValid(self): - assert isinstance(self.shal, myanimelist.manga_list.MangaList) - - def testUsername(self): - assert self.shal.username == u'shaldengeki' - assert self.josh.username == u'angryaria' - - def testType(self): - assert self.shal.type == u'manga' - - def testList(self): - assert isinstance(self.shal.list, dict) and len(self.shal) == 2 - assert self.tomoyo_after in self.shal and self.fma in self.shal - assert self.shal[self.tomoyo_after][u'status'] == u'Completed' and self.shal[self.fma][u'status'] == u'Dropped' - assert self.shal[self.tomoyo_after][u'score'] == 9 and self.shal[self.fma][u'score'] == 6 - assert self.shal[self.tomoyo_after][u'chapters_read'] == 4 and self.shal[self.fma][u'chapters_read'] == 73 - assert self.shal[self.tomoyo_after][u'volumes_read'] == 1 and self.shal[self.fma][u'volumes_read'] == 18 - assert self.shal[self.tomoyo_after][u'started'] == None and self.shal[self.fma][u'started'] == None - assert self.shal[self.tomoyo_after][u'finished'] == None and self.shal[self.fma][u'finished'] == None - - assert isinstance(self.pl.list, dict) and len(self.pl) >= 45 - assert self.to_love_ru in self.pl and self.amnesia in self.pl and self.sao in self.pl - assert self.pl[self.to_love_ru][u'status'] == u'Completed' and self.pl[self.amnesia][u'status'] == u'On-Hold' and self.pl[self.sao][u'status'] == u'Plan to Read' - assert self.pl[self.to_love_ru][u'score'] == 6 and self.pl[self.amnesia][u'score'] == None and self.pl[self.sao][u'score'] == None - assert self.pl[self.to_love_ru][u'chapters_read'] == 162 and self.pl[self.amnesia][u'chapters_read'] == 9 and self.pl[self.sao][u'chapters_read'] == 0 - assert self.pl[self.to_love_ru][u'volumes_read'] == 18 and self.pl[self.amnesia][u'volumes_read'] == 0 and self.pl[self.sao][u'volumes_read'] == 0 - assert self.pl[self.to_love_ru][u'started'] == datetime.date(year=2011, month=9, day=8) and self.pl[self.amnesia][u'started'] == datetime.date(year=2010, month=6, day=27) and self.pl[self.sao][u'started'] == datetime.date(year=2012, month=9, day=24) - assert self.pl[self.to_love_ru][u'finished'] == datetime.date(year=2011, month=9, day=16) and self.pl[self.amnesia][u'finished'] == None and self.pl[self.sao][u'finished'] == None - - assert isinstance(self.josh.list, dict) and len(self.josh) >= 151 - assert self.juicy in self.josh and self.tsubasa in self.josh and self.jojo in self.josh - assert self.josh[self.juicy][u'status'] == u'Completed' and self.josh[self.tsubasa][u'status'] == u'Dropped' and self.josh[self.jojo][u'status'] == u'Plan to Read' - assert self.josh[self.juicy][u'score'] == 6 and self.josh[self.tsubasa][u'score'] == 6 and self.josh[self.jojo][u'score'] == None - assert self.josh[self.juicy][u'chapters_read'] == 33 and self.josh[self.tsubasa][u'chapters_read'] == 27 and self.josh[self.jojo][u'chapters_read'] == 0 - assert self.josh[self.juicy][u'volumes_read'] == 2 and self.josh[self.tsubasa][u'volumes_read'] == 0 and self.josh[self.jojo][u'volumes_read'] == 0 - assert self.josh[self.juicy][u'started'] == None and self.josh[self.tsubasa][u'started'] == None and self.josh[self.jojo][u'started'] == datetime.date(year=2010, month=9, day=16) - assert self.josh[self.juicy][u'finished'] == None and self.josh[self.tsubasa][u'finished'] == None and self.josh[self.jojo][u'finished'] == None - - assert isinstance(self.threger.list, dict) and len(self.threger) == 0 - - def testStats(self): - assert isinstance(self.shal.stats, dict) and len(self.shal.stats) > 0 - assert self.shal.stats[u'reading'] == 0 and self.shal.stats[u'completed'] == 1 and self.shal.stats[u'on_hold'] == 0 and self.shal.stats[u'dropped'] == 1 and self.shal.stats[u'plan_to_read'] == 0 and float(self.shal.stats[u'days_spent']) == 0.95 - - assert isinstance(self.pl.stats, dict) and len(self.pl.stats) > 0 - assert self.pl.stats[u'reading'] >= 0 and self.pl.stats[u'completed'] >= 16 and self.pl.stats[u'on_hold'] >= 0 and self.pl.stats[u'dropped'] >= 0 and self.pl.stats[u'plan_to_read'] >= 0 and float(self.pl.stats[u'days_spent']) >= 10.28 - - assert isinstance(self.josh.stats, dict) and len(self.josh.stats) > 0 - assert self.josh.stats[u'reading'] >= 0 and self.josh.stats[u'completed'] >= 53 and self.josh.stats[u'on_hold'] >= 0 and self.josh.stats[u'dropped'] >= 0 and self.josh.stats[u'plan_to_read'] >= 0 and float(self.josh.stats[u'days_spent']) >= 25.41 - - assert isinstance(self.threger.stats, dict) and len(self.threger.stats) > 0 - assert self.threger.stats[u'reading'] == 0 and self.threger.stats[u'completed'] == 0 and self.threger.stats[u'on_hold'] == 0 and self.threger.stats[u'dropped'] == 0 and self.threger.stats[u'plan_to_read'] == 0 and float(self.threger.stats[u'days_spent']) == 0.00 - - def testSection(self): - assert isinstance(self.shal.section(u'Completed'), dict) and self.tomoyo_after in self.shal.section(u'Completed') - assert isinstance(self.pl.section(u'On-Hold'), dict) and self.amnesia in self.pl.section(u'On-Hold') - assert isinstance(self.josh.section(u'Plan to Read'), dict) and self.jojo in self.josh.section(u'Plan to Read') - assert isinstance(self.threger.section(u'Reading'), dict) and len(self.threger.section(u'Reading')) == 0 + +class testMangaListClass(TestCase): + @classmethod + def setUpClass(self): + self.session = myanimelist.session.Session() + + self.shal = self.session.manga_list(u'shaldengeki') + self.tomoyo_after = self.session.manga(3941) + self.fma = self.session.manga(25) + + self.pl = self.session.manga_list(u'PaperLuigi') + self.to_love_ru = self.session.manga(671) + self.amnesia = self.session.manga(15805) + self.sao = self.session.manga(21479) + + self.josh = self.session.manga_list(u'angryaria') + self.juicy = self.session.manga(13250) + self.tsubasa = self.session.manga(1147) + self.jojo = self.session.manga(1706) + + self.threger = self.session.manga_list(u'threger') + + def testNoUsernameInvalidMangaList(self): + with self.assertRaises(TypeError): + self.session.manga_list() + + def testNonexistentUsernameInvalidMangaList(self): + with self.assertRaises(myanimelist.media_list.InvalidMediaListError): + self.session.manga_list(u'aspdoifpjsadoifjapodsijfp').load() + + def testUserValid(self): + assert isinstance(self.shal, myanimelist.manga_list.MangaList) + + def testUsername(self): + self.assertEqual(self.shal.username, u'shaldengeki') + self.assertEqual(self.josh.username, u'angryaria') + + def testType(self): + self.assertEqual(self.shal.type, u'manga') + + def testList(self): + assert isinstance(self.shal.list, dict) + self.assertEqual(len(self.shal), 2) + self.assertIn(self.tomoyo_after, self.shal) + self.assertIn(self.fma, self.shal) + self.assertEqual(self.shal[self.tomoyo_after][u'status'], u'Completed') + self.assertEqual(self.shal[self.fma][u'status'], u'Dropped') + self.assertEqual(self.shal[self.tomoyo_after][u'score'], 9) + self.assertEqual(self.shal[self.fma][u'score'], 6) + self.assertEqual(self.shal[self.tomoyo_after][u'chapters_read'], 4) + self.assertEqual(self.shal[self.fma][u'chapters_read'], 73) + self.assertEqual(self.shal[self.tomoyo_after][u'volumes_read'], 1) + self.assertEqual(self.shal[self.fma][u'volumes_read'], 18) + self.assertIsNone(self.shal[self.tomoyo_after][u'started']) + self.assertIsNone(self.shal[self.fma][u'started']) + self.assertIsNone(self.shal[self.tomoyo_after][u'finished']) + self.assertIsNone(self.shal[self.fma][u'finished']) + + assert isinstance(self.pl.list, dict) + self.assertGreaterEqual(len(self.pl), 45) + self.assertIn(self.to_love_ru, self.pl) + self.assertIn(self.amnesia, self.pl) + self.assertIn(self.sao, self.pl) + self.assertEqual(self.pl[self.to_love_ru][u'status'], u'Completed') + assert self.pl[self.amnesia][ + u'status'] == u'On-Hold' + self.assertEqual(self.pl[self.sao][u'status'], u'Plan to Read') + self.assertEqual(self.pl[self.to_love_ru][u'score'], 6) + self.assertIsNone(self.pl[self.amnesia][u'score']) + self.assertIsNone(self.pl[self.sao][u'score']) + self.assertEqual(self.pl[self.to_love_ru][u'chapters_read'], 162) + self.assertEqual(self.pl[self.amnesia][u'chapters_read'], 9) + self.assertEqual(self.pl[self.sao][u'chapters_read'], 0) + self.assertEqual(self.pl[self.to_love_ru][u'volumes_read'], 18) + self.assertEqual(self.pl[self.amnesia][u'volumes_read'], 0) + self.assertEqual(self.pl[self.sao][u'volumes_read'], 0) + self.assertEqual(self.pl[self.to_love_ru][u'started'], datetime.date(year=2011, month=9, day=8)) + self.assertEqual(self.pl[self.amnesia][u'started'], datetime.date(year=2010, month=6, day=27)) + self.assertEqual(self.pl[self.sao][u'started'], datetime.date(year=2012, month=9, day=24)) + self.assertEqual(self.pl[self.to_love_ru][u'finished'], datetime.date(year=2011, month=9, day=16)) + self.assertIsNone(self.pl[self.amnesia][u'finished']) + self.assertIsNone(self.pl[self.sao][u'finished']) + + self.assertIsInstance(self.josh.list, dict) + self.assertGreaterEqual(len(self.josh), 151) + self.assertIn(self.juicy, self.josh) + self.assertIn(self.tsubasa, self.josh) + self.assertIn(self.jojo, self.josh) + self.assertEqual(self.josh[self.juicy][u'status'], u'Completed') + self.assertEqual(self.josh[self.tsubasa][u'status'], u'Dropped') + self.assertEqual(self.josh[self.jojo][u'status'], u'Plan to Read') + self.assertEqual(self.josh[self.juicy][u'score'], 6) + self.assertEqual(self.josh[self.tsubasa][u'score'], 6) + self.assertIsNone(self.josh[self.jojo][u'score']) + self.assertEqual(self.josh[self.juicy][u'chapters_read'], 33) + self.assertEqual(self.josh[self.tsubasa][u'chapters_read'], 27) + self.assertEqual(self.josh[self.jojo][u'chapters_read'], 0) + self.assertEqual(self.josh[self.juicy][u'volumes_read'], 2) + self.assertEqual(self.josh[self.tsubasa][u'volumes_read'], 0) + self.assertEqual(self.josh[self.jojo][u'volumes_read'], 0) + self.assertIsNone(self.josh[self.juicy][u'started']) + self.assertIsNone(self.josh[self.tsubasa][u'started']) + self.assertEqual(self.josh[self.jojo][u'started'], datetime.date(year=2010, month=9, day=16)) + self.assertIsNone(self.josh[self.juicy][u'finished']) + self.assertIsNone(self.josh[self.tsubasa][u'finished']) + self.assertIsNone(self.josh[self.jojo][u'finished']) + + self.assertIsInstance(self.threger.list, dict) + self.assertEqual(len(self.threger), 0) + + def testStats(self): + self.assertIsInstance(self.shal.stats, dict) + self.assertGreater(len(self.shal.stats), 0) + self.assertEqual(self.shal.stats[u'reading'], 0) + self.assertEqual(self.shal.stats[u'completed'], 1) + assert self.shal.stats[ + u'on_hold'] == 0 + self.assertEqual(self.shal.stats[u'dropped'], 1) + self.assertEqual(self.shal.stats[u'plan_to_read'], 0) + self.assertEqual(float(self.shal.stats[u'days_spent']), 0.95) + + self.assertIsInstance(self.pl.stats, dict) + self.assertGreater(len(self.pl.stats), 0) + self.assertGreaterEqual(self.pl.stats[u'reading'], 0) + self.assertGreaterEqual(self.pl.stats[u'completed'], 16) + assert self.pl.stats[ + u'on_hold'] >= 0 + self.assertGreaterEqual(self.pl.stats[u'dropped'], 0) + self.assertGreaterEqual(self.pl.stats[u'plan_to_read'], 0) + self.assertGreaterEqual(float(self.pl.stats[u'days_spent']), 10.28) + + self.assertIsInstance(self.josh.stats, dict) + self.assertGreater(len(self.josh.stats), 0) + self.assertGreaterEqual(self.josh.stats[u'reading'], 0) + self.assertGreaterEqual(self.josh.stats[u'completed'], 53) + self.assertGreaterEqual(self.josh.stats[u'on_hold'], 0) + self.assertGreaterEqual(self.josh.stats[u'dropped'], 0) + self.assertGreaterEqual(self.josh.stats[u'plan_to_read'], 0) + self.assertGreaterEqual(float(self.josh.stats[u'days_spent']), 25.41) + + self.assertIsInstance(self.threger.stats, dict) + self.assertGreater(len(self.threger.stats), 0) + self.assertEqual(self.threger.stats[u'reading'], 0) + self.assertEqual(self.threger.stats[u'completed'], 0) + self.assertEqual(self.threger.stats[u'on_hold'], 0) + self.assertEqual(self.threger.stats[u'dropped'], 0) + self.assertEqual(self.threger.stats[u'plan_to_read'], 0) + self.assertEqual(float(self.threger.stats[u'days_spent']), 0.00) + + def testSection(self): + self.assertIsInstance(self.shal.section(u'Completed'), dict) + self.assertIn(self.tomoyo_after, self.shal.section(u'Completed')) + self.assertIsInstance(self.pl.section(u'On-Hold'), dict) + self.assertIn(self.amnesia, self.pl.section(u'On-Hold')) + self.assertIsInstance(self.josh.section(u'Plan to Read'), dict) + self.assertIn(self.jojo, self.josh.section(u'Plan to Read')) + self.assertIsInstance(self.threger.section(u'Reading'), dict) + self.assertEqual(len(self.threger.section(u'Reading')), 0) diff --git a/tests/manga_tests.py b/tests/manga_tests.py index 7f179ee..a0dac79 100644 --- a/tests/manga_tests.py +++ b/tests/manga_tests.py @@ -1,224 +1,332 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -from nose.tools import * +from unittest import TestCase import datetime import myanimelist.session import myanimelist.manga -class testMangaClass(object): - @classmethod - def setUpClass(self): - self.session = myanimelist.session.Session() - - self.monster = self.session.manga(1) - self.mystery = self.session.genre(7) - self.mystery_tag = self.session.tag(u'mystery') - self.urasawa = self.session.person(1867) - self.original = self.session.publication(1) - self.heinemann = self.session.character(6123) - self.monster_side_story = self.session.manga(10968) - - self.holic = self.session.manga(10) - self.supernatural = self.session.genre(37) - self.supernatural_tag = self.session.tag(u'supernatural') - self.clamp = self.session.person(1877) - self.bessatsu = self.session.publication(450) - self.doumeki = self.session.character(567) - self.holic_sequel = self.session.manga(46010) - - self.naruto = self.session.manga(11) - self.shounen = self.session.genre(27) - self.action_tag = self.session.tag(u'action') - self.kishimoto = self.session.person(1879) - self.shonen_jump_weekly = self.session.publication(83) - self.ebizou = self.session.character(31825) - - self.tomoyo_after = self.session.manga(3941) - self.drama = self.session.genre(8) - self.romance_tag = self.session.tag(u'romance') - self.sumiyoshi = self.session.person(3830) - self.dragon_age = self.session.publication(98) - self.kanako = self.session.character(21227) - - self.judos = self.session.manga(79819) - self.action = self.session.genre(1) - self.kondou = self.session.person(18765) - - self.invalid_anime = self.session.manga(457384754) - self.latest_manga = myanimelist.manga.Manga.newest(self.session) - - @raises(TypeError) - def testNoIDInvalidManga(self): - self.session.manga() - - @raises(TypeError) - def testNoSessionInvalidLatestManga(self): - myanimelist.manga.Manga.newest() - - @raises(myanimelist.manga.InvalidMangaError) - def testNegativeInvalidManga(self): - self.session.manga(-1) - - @raises(myanimelist.manga.InvalidMangaError) - def testFloatInvalidManga(self): - self.session.manga(1.5) - - @raises(myanimelist.manga.InvalidMangaError) - def testNonExistentManga(self): - self.invalid_anime.load() - - def testLatestManga(self): - assert isinstance(self.latest_manga, myanimelist.manga.Manga) - assert self.latest_manga.id > 79818 - - def testMangaValid(self): - assert isinstance(self.monster, myanimelist.manga.Manga) - - def testTitle(self): - assert self.monster.title == u'Monster' - assert self.holic.title == u'xxxHOLiC' - assert self.naruto.title == u'Naruto' - assert self.tomoyo_after.title == u'Clannad: Tomoyo After' - assert self.judos.title == u'Judos' - - def testPicture(self): - assert isinstance(self.holic.picture, unicode) - assert isinstance(self.naruto.picture, unicode) - assert isinstance(self.monster.picture, unicode) - assert isinstance(self.tomoyo_after.picture, unicode) - assert isinstance(self.judos.picture, unicode) - - def testAlternativeTitles(self): - assert u'Japanese' in self.monster.alternative_titles and isinstance(self.monster.alternative_titles[u'Japanese'], list) and u'MONSTER モンスター' in self.monster.alternative_titles[u'Japanese'] - assert u'Synonyms' in self.holic.alternative_titles and isinstance(self.holic.alternative_titles[u'Synonyms'], list) and u'xxxHolic Cage' in self.holic.alternative_titles[u'Synonyms'] - assert u'Japanese' in self.naruto.alternative_titles and isinstance(self.naruto.alternative_titles[u'Japanese'], list) and u'NARUTO -ナルト-' in self.naruto.alternative_titles[u'Japanese'] - assert u'English' in self.tomoyo_after.alternative_titles and isinstance(self.tomoyo_after.alternative_titles[u'English'], list) and u'Tomoyo After ~Dear Shining Memories~' in self.tomoyo_after.alternative_titles[u'English'] - assert u'Synonyms' in self.judos.alternative_titles and isinstance(self.judos.alternative_titles[u'Synonyms'], list) and u'Juudouzu' in self.judos.alternative_titles[u'Synonyms'] - - def testTypes(self): - assert self.monster.type == u'Manga' - assert self.tomoyo_after.type == u'Manga' - assert self.judos.type == u'Manga' - - def testVolumes(self): - assert self.holic.volumes == 19 - assert self.monster.volumes == 18 - assert self.tomoyo_after.volumes == 1 - assert self.naruto.volumes == 72 - assert self.judos.volumes == 3 - - def testChapters(self): - assert self.holic.chapters == 213 - assert self.monster.chapters == 162 - assert self.tomoyo_after.chapters == 4 - assert self.naruto.chapters == 700 - assert self.judos.chapters == None - - def testStatus(self): - assert self.holic.status == u'Finished' - assert self.tomoyo_after.status == u'Finished' - assert self.monster.status == u'Finished' - assert self.naruto.status == u'Finished' - - def testPublished(self): - assert self.holic.published == (datetime.date(month=2, day=24, year=2003), datetime.date(month=2, day=9, year=2011)) - assert self.monster.published == (datetime.date(month=12, day=5, year=1994), datetime.date(month=12, day=20, year=2001)) - assert self.naruto.published == (datetime.date(month=9, day=21, year=1999),datetime.date(month=11, day=10, year=2014)) - assert self.tomoyo_after.published == (datetime.date(month=4, day=20, year=2007), datetime.date(month=10, day=20, year=2007)) - - def testGenres(self): - assert isinstance(self.holic.genres, list) and len(self.holic.genres) > 0 and self.mystery in self.holic.genres and self.supernatural in self.holic.genres - assert isinstance(self.tomoyo_after.genres, list) and len(self.tomoyo_after.genres) > 0 and self.drama in self.tomoyo_after.genres - assert isinstance(self.naruto.genres, list) and len(self.naruto.genres) > 0 and self.shounen in self.naruto.genres - assert isinstance(self.monster.genres, list) and len(self.monster.genres) > 0 and self.mystery in self.monster.genres - assert isinstance(self.judos.genres, list) and len(self.judos.genres) > 0 and self.shounen in self.judos.genres and self.action in self.judos.genres - - def testAuthors(self): - assert isinstance(self.holic.authors, dict) and len(self.holic.authors) > 0 and self.clamp in self.holic.authors and self.holic.authors[self.clamp] == u'Story & Art' - assert isinstance(self.tomoyo_after.authors, dict) and len(self.tomoyo_after.authors) > 0 and self.sumiyoshi in self.tomoyo_after.authors and self.tomoyo_after.authors[self.sumiyoshi] == u'Art' - assert isinstance(self.naruto.authors, dict) and len(self.naruto.authors) > 0 and self.kishimoto in self.naruto.authors and self.naruto.authors[self.kishimoto] == u'Story & Art' - assert isinstance(self.monster.authors, dict) and len(self.monster.authors) > 0 and self.urasawa in self.monster.authors and self.monster.authors[self.urasawa] == u'Story & Art' - assert isinstance(self.judos.authors, dict) and len(self.judos.authors) > 0 and self.kondou in self.judos.authors and self.judos.authors[self.kondou] == u'Story & Art' - - def testSerialization(self): - assert isinstance(self.holic.serialization, myanimelist.publication.Publication) and self.bessatsu == self.holic.serialization - assert isinstance(self.tomoyo_after.serialization, myanimelist.publication.Publication) and self.dragon_age == self.tomoyo_after.serialization - assert isinstance(self.naruto.serialization, myanimelist.publication.Publication) and self.shonen_jump_weekly == self.naruto.serialization - assert isinstance(self.monster.serialization, myanimelist.publication.Publication) and self.original == self.monster.serialization - assert isinstance(self.judos.serialization, myanimelist.publication.Publication) and self.shonen_jump_weekly == self.judos.serialization - - def testScore(self): - assert isinstance(self.holic.score, tuple) - assert self.holic.score[0] > 0 and self.holic.score[0] < 10 - assert isinstance(self.holic.score[1], int) and self.holic.score[1] >= 0 - assert isinstance(self.monster.score, tuple) - assert self.monster.score[0] > 0 and self.monster.score[0] < 10 - assert isinstance(self.monster.score[1], int) and self.monster.score[1] >= 0 - assert isinstance(self.naruto.score, tuple) - assert self.naruto.score[0] > 0 and self.naruto.score[0] < 10 - assert isinstance(self.naruto.score[1], int) and self.naruto.score[1] >= 0 - assert isinstance(self.tomoyo_after.score, tuple) - assert self.tomoyo_after.score[0] > 0 and self.tomoyo_after.score[0] < 10 - assert isinstance(self.tomoyo_after.score[1], int) and self.tomoyo_after.score[1] >= 0 - assert self.judos.score[0] >= 0 and self.judos.score[0] <= 10 - assert isinstance(self.judos.score[1], int) and self.judos.score[1] >= 0 - - def testRank(self): - assert isinstance(self.holic.rank, int) and self.holic.rank > 0 - assert isinstance(self.monster.rank, int) and self.monster.rank > 0 - assert isinstance(self.naruto.rank, int) and self.naruto.rank > 0 - assert isinstance(self.tomoyo_after.rank, int) and self.tomoyo_after.rank > 0 - assert isinstance(self.judos.rank, int) and self.judos.rank > 0 - - def testPopularity(self): - assert isinstance(self.holic.popularity, int) and self.holic.popularity > 0 - assert isinstance(self.monster.popularity, int) and self.monster.popularity > 0 - assert isinstance(self.naruto.popularity, int) and self.naruto.popularity > 0 - assert isinstance(self.tomoyo_after.popularity, int) and self.tomoyo_after.popularity > 0 - assert isinstance(self.judos.popularity, int) and self.judos.popularity > 0 - - def testMembers(self): - assert isinstance(self.holic.members, int) and self.holic.members > 0 - assert isinstance(self.monster.members, int) and self.monster.members > 0 - assert isinstance(self.naruto.members, int) and self.naruto.members > 0 - assert isinstance(self.tomoyo_after.members, int) and self.tomoyo_after.members > 0 - assert isinstance(self.judos.members, int) and self.judos.members > 0 - - def testFavorites(self): - assert isinstance(self.holic.favorites, int) and self.holic.favorites > 0 - assert isinstance(self.monster.favorites, int) and self.monster.favorites > 0 - assert isinstance(self.naruto.favorites, int) and self.naruto.favorites > 0 - assert isinstance(self.tomoyo_after.favorites, int) and self.tomoyo_after.favorites > 0 - assert isinstance(self.judos.favorites, int) and self.judos.favorites >= 0 - - def testPopularTags(self): - assert isinstance(self.holic.popular_tags, dict) and len(self.holic.popular_tags) > 0 and self.supernatural_tag in self.holic.popular_tags and self.holic.popular_tags[self.supernatural_tag] >= 269 - assert isinstance(self.tomoyo_after.popular_tags, dict) and len(self.tomoyo_after.popular_tags) > 0 and self.romance_tag in self.tomoyo_after.popular_tags and self.tomoyo_after.popular_tags[self.romance_tag] >= 57 - assert isinstance(self.naruto.popular_tags, dict) and len(self.naruto.popular_tags) > 0 and self.action_tag in self.naruto.popular_tags and self.naruto.popular_tags[self.action_tag] >= 561 - assert isinstance(self.monster.popular_tags, dict) and len(self.monster.popular_tags) > 0 and self.mystery_tag in self.monster.popular_tags and self.monster.popular_tags[self.mystery_tag] >= 105 - assert isinstance(self.judos.popular_tags, dict) and len(self.judos.popular_tags) == 0 - - def testSynopsis(self): - assert isinstance(self.holic.synopsis, unicode) and len(self.holic.synopsis) > 0 and u'Watanuki' in self.holic.synopsis - assert isinstance(self.monster.synopsis, unicode) and len(self.monster.synopsis) > 0 and u'Tenma' in self.monster.synopsis - assert isinstance(self.naruto.synopsis, unicode) and len(self.naruto.synopsis) > 0 and u'Hokage' in self.naruto.synopsis - assert isinstance(self.tomoyo_after.synopsis, unicode) and len(self.tomoyo_after.synopsis) > 0 and u'Clannad' in self.tomoyo_after.synopsis - assert isinstance(self.judos.synopsis, unicode) and len(self.judos.synopsis) > 0 and u'hardcore' in self.judos.synopsis - - def testRelated(self): - assert isinstance(self.holic.related, dict) and 'Sequel' in self.holic.related and self.holic_sequel in self.holic.related[u'Sequel'] - assert isinstance(self.monster.related, dict) and 'Side story' in self.monster.related and self.monster_side_story in self.monster.related[u'Side story'] - - def testCharacters(self): - assert isinstance(self.holic.characters, dict) and len(self.holic.characters) > 0 - assert self.doumeki in self.holic.characters and self.holic.characters[self.doumeki]['role'] == 'Main' - - assert isinstance(self.monster.characters, dict) and len(self.monster.characters) > 0 - assert self.heinemann in self.monster.characters and self.monster.characters[self.heinemann]['role'] == 'Main' - - assert isinstance(self.naruto.characters, dict) and len(self.naruto.characters) > 0 - assert self.ebizou in self.naruto.characters and self.naruto.characters[self.ebizou]['role'] == 'Supporting' - - assert isinstance(self.tomoyo_after.characters, dict) and len(self.tomoyo_after.characters) > 0 - assert self.kanako in self.tomoyo_after.characters and self.tomoyo_after.characters[self.kanako]['role'] == 'Supporting' \ No newline at end of file + +class testMangaClass(TestCase): + @classmethod + def setUpClass(self): + self.session = myanimelist.session.Session() + + self.monster = self.session.manga(1) + self.mystery = self.session.genre(7) + self.mystery_tag = self.session.tag(u'mystery') + self.urasawa = self.session.person(1867) + self.original = self.session.publication(1) + self.heinemann = self.session.character(6123) + self.monster_side_story = self.session.manga(10968) + + self.holic = self.session.manga(10) + self.supernatural = self.session.genre(37) + self.supernatural_tag = self.session.tag(u'supernatural') + self.clamp = self.session.person(1877) + self.bessatsu = self.session.publication(450) + self.doumeki = self.session.character(567) + self.holic_sequel = self.session.manga(46010) + + self.naruto = self.session.manga(11) + self.shounen = self.session.genre(27) + self.action_tag = self.session.tag(u'action') + self.kishimoto = self.session.person(1879) + self.shonen_jump_weekly = self.session.publication(83) + self.ebizou = self.session.character(31825) + + self.tomoyo_after = self.session.manga(3941) + self.drama = self.session.genre(8) + self.romance_tag = self.session.tag(u'romance') + self.sumiyoshi = self.session.person(3830) + self.dragon_age = self.session.publication(98) + self.kanako = self.session.character(21227) + + self.judos = self.session.manga(79819) + self.action = self.session.genre(1) + self.kondou = self.session.person(18765) + + self.invalid_anime = self.session.manga(457384754) + self.latest_manga = myanimelist.manga.Manga.newest(self.session) + + def testNoIDInvalidManga(self): + with self.assertRaises(TypeError): + self.session.manga() + + def testNoSessionInvalidLatestManga(self): + with self.assertRaises(TypeError): + myanimelist.manga.Manga.newest() + + def testNegativeInvalidManga(self): + with self.assertRaises(myanimelist.manga.InvalidMangaError): + self.session.manga(-1) + + def testFloatInvalidManga(self): + with self.assertRaises(myanimelist.manga.InvalidMangaError): + self.session.manga(1.5) + + def testNonExistentManga(self): + with self.assertRaises(myanimelist.manga.InvalidMangaError): + self.invalid_anime.load() + + def testLatestManga(self): + self.assertIsInstance(self.latest_manga, myanimelist.manga.Manga) + self.assertGreater(self.latest_manga.id, 79818) + + def testMangaValid(self): + self.assertIsInstance(self.monster, myanimelist.manga.Manga) + + def testTitle(self): + self.assertEqual(self.monster.title, u'Monster') + self.assertEqual(self.holic.title, u'xxxHOLiC') + self.assertEqual(self.naruto.title, u'Naruto') + self.assertEqual(self.tomoyo_after.title, u'Clannad: Tomoyo After') + self.assertEqual(self.judos.title, u'Judos') + + def testPicture(self): + self.assertIsInstance(self.holic.picture, unicode) + self.assertIsInstance(self.naruto.picture, unicode) + self.assertIsInstance(self.monster.picture, unicode) + self.assertIsInstance(self.tomoyo_after.picture, unicode) + self.assertIsInstance(self.judos.picture, unicode) + + def testAlternativeTitGreater(self): + self.assertIn(u'Japanese', self.monster.alternative_titles) + self.assertIsInstance(self.monster.alternative_titles[u'Japanese'], list) + self.assertIn(u'MONSTER モンスター', self.monster.alternative_titles[u'Japanese']) + self.assertIn(u'Synonyms', self.holic.alternative_titles) + self.assertIsInstance(self.holic.alternative_titles[u'Synonyms'], list) + self.assertIn(u'xxxHolic Cage', self.holic.alternative_titles[u'Synonyms']) + self.assertIn(u'Japanese', self.naruto.alternative_titles) + self.assertIsInstance(self.naruto.alternative_titles[u'Japanese'], list) + self.assertIn(u'NARUTO -ナルト-', self.naruto.alternative_titles[u'Japanese']) + self.assertIn(u'English', self.tomoyo_after.alternative_titles) + self.assertIsInstance(self.tomoyo_after.alternative_titles[u'English'], list) + self.assertIn(u'Tomoyo After ~Dear Shining Memories~', self.tomoyo_after.alternative_titles[u'English']) + self.assertIn(u'Synonyms', self.judos.alternative_titles) + self.assertIsInstance(self.judos.alternative_titles[u'Synonyms'], list) + self.assertIn(u'Juudouzu', self.judos.alternative_titles[u'Synonyms']) + + def testTypes(self): + self.assertEqual(self.monster.type, u'Manga') + self.assertEqual(self.tomoyo_after.type, u'Manga') + self.assertEqual(self.judos.type, u'Manga') + + def testVolumes(self): + self.assertEqual(self.holic.volumes, 19) + self.assertEqual(self.monster.volumes, 18) + self.assertEqual(self.tomoyo_after.volumes, 1) + self.assertEqual(self.naruto.volumes, 72) + self.assertEqual(self.judos.volumes, 3) + + def testChapters(self): + self.assertEqual(self.holic.chapters, 213) + self.assertEqual(self.monster.chapters, 162) + self.assertEqual(self.tomoyo_after.chapters, 4) + self.assertEqual(self.naruto.chapters, 700) + self.assertEqual(self.judos.chapters, None) + + def testStatus(self): + self.assertEqual(self.holic.status, u'Finished') + self.assertEqual(self.tomoyo_after.status, u'Finished') + self.assertEqual(self.monster.status, u'Finished') + self.assertEqual(self.naruto.status, u'Finished') + + def testPublished(self): + self.assertEqual(self.holic.published, datetime.date(month=2, day=24, year=2003), datetime.date(month=2, day=9, year=2011)) + self.assertEqual(self.monster.published, datetime.date(month=12, day=5, year=1994), datetime.date(month=12, day=20, year=2001)) + self.assertEqual(self.naruto.published, datetime.date(month=9, day=21, year=1999), datetime.date(month=11, day=10, year=2014)) + self.assertEqual(self.tomoyo_after.published, datetime.date(month=4, day=20, year=2007), datetime.date(month=10, day=20, year=2007)) + + def testGenres(self): + self.assertIsInstance(self.holic.genres, list) + self.assertGreater(self.holic.genres, 0) + self.assertIn(self.mystery, self.holic.genres) + self.assertIn(self.supernatural, self.holic.genres) + self.assertIsInstance(self.tomoyo_after.genres, list) + self.assertGreater(self.tomoyo_after.genres, 0) + self.assertIn(self.drama, self.tomoyo_after.genres) + self.assertIsInstance(self.naruto.genres, list) + self.assertGreater(self.naruto.genres, 0) + self.assertIn(self.shounen, self.naruto.genres) + self.assertIsInstance(self.monster.genres, list) + self.assertGreater(self.monster.genres, 0) + self.assertIn(self.mystery, self.monster.genres) + self.assertIsInstance(self.judos.genres, list) + self.assertGreater(self.judos.genres, 0) + self.assertIn(self.shounen, self.judos.genres) + self.assertIn(self.action, self.judos.genres) + + def testAuthors(self): + self.assertIsInstance(self.holic.authors, dict) + self.assertGreater(self.holic.authors, 0) + self.assertIn(self.clamp, self.holic.authors) + self.assertEqual(self.holic.authors[self.clamp], u'Story & Art') + self.assertIsInstance(self.tomoyo_after.authors, dict) + self.assertGreater(self.tomoyo_after.authors, 0) + self.assertIn(self.sumiyoshi, self.tomoyo_after.authors) + self.assertEqual(self.tomoyo_after.authors[self.sumiyoshi], u'Art') + self.assertIsInstance(self.naruto.authors, dict) + self.assertGreater(self.naruto.authors, 0) + self.assertIn(self.kishimoto, self.naruto.authors) + self.assertEqual(self.naruto.authors[self.kishimoto], u'Story & Art') + self.assertIsInstance(self.monster.authors, dict) + self.assertGreater(self.monster.authors, 0) + self.assertIn(self.urasawa, self.monster.authors) + self.assertEqual(self.monster.authors[self.urasawa], u'Story & Art') + self.assertIsInstance(self.judos.authors, dict) + self.assertGreater(self.judos.authors, 0) + self.assertIn(self.kondou, self.judos.authors) + self.assertEqual(self.judos.authors[self.kondou], u'Story & Art') + + def testSerialization(self): + self.assertIsInstance(self.holic.serialization, myanimelist.publication.Publication) + self.assertEqual(self.bessatsu, self.holic.serialization) + self.assertIsInstance(self.tomoyo_after.serialization, myanimelist.publication.Publication) + self.assertEqual(self.dragon_age, self.tomoyo_after.serialization) + self.assertIsInstance(self.naruto.serialization, myanimelist.publication.Publication) + self.assertEqual(self.shonen_jump_weekly, self.naruto.serialization) + self.assertIsInstance(self.monster.serialization, myanimelist.publication.Publication) + self.assertEqual(self.original, self.monster.serialization) + self.assertIsInstance(self.judos.serialization, myanimelist.publication.Publication) + self.assertEqual(self.shonen_jump_weekly, self.judos.serialization) + + def testScore(self): + self.assertIsInstance(self.holic.score, tuple) + self.assertGreater(self.holic.score[0], 0) + self.assertGreater(self.holic.score[0], 10) + self.assertIsInstance(self.holic.score[1], int) + self.assertGreater(self.holic.score[1], 0) + self.assertIsInstance(self.monster.score, tuple) + self.assertGreater(self.monster.score[0], 0) + self.assertGreater(self.monster.score[0], 10) + self.assertIsInstance(self.monster.score[1], int) + self.assertGreater(self.monster.score[1], 0) + self.assertIsInstance(self.naruto.score, tuple) + self.assertGreater(self.naruto.score[0], 0) + self.assertGreater(self.naruto.score[0], 10) + self.assertIsInstance(self.naruto.score[1], int) + self.assertGreater(self.naruto.score[1], 0) + self.assertIsInstance(self.tomoyo_after.score, tuple) + self.assertGreater(self.tomoyo_after.score[0], 0) + self.assertGreater(self.tomoyo_after.score[0], 10) + self.assertIsInstance(self.tomoyo_after.score[1], int) + self.assertGreater(self.tomoyo_after.score[1], 0) + self.assertGreater(self.judos.score[0], 0) + assert self.judos.score[0] <= 10 + self.assertIsInstance(self.judos.score[1], int) + self.assertGreater(self.judos.score[1], 0) + + def testRank(self): + self.assertIsInstance(self.holic.rank, int) + self.assertGreater(self.holic.rank, 0) + self.assertIsInstance(self.monster.rank, int) + self.assertGreater(self.monster.rank, 0) + self.assertIsInstance(self.naruto.rank, int) + self.assertGreater(self.naruto.rank, 0) + self.assertIsInstance(self.tomoyo_after.rank, int) + self.assertGreater(self.tomoyo_after.rank, 0) + self.assertIsInstance(self.judos.rank, int) + self.assertGreater(self.judos.rank, 0) + + def testPopularity(self): + self.assertIsInstance(self.holic.popularity, int) + self.assertGreater(self.holic.popularity, 0) + self.assertIsInstance(self.monster.popularity, int) + self.assertGreater(self.monster.popularity, 0) + self.assertIsInstance(self.naruto.popularity, int) + self.assertGreater(self.naruto.popularity, 0) + self.assertIsInstance(self.tomoyo_after.popularity, int) + self.assertGreater(self.tomoyo_after.popularity, 0) + self.assertIsInstance(self.judos.popularity, int) + self.assertGreater(self.judos.popularity, 0) + + def testMembers(self): + self.assertIsInstance(self.holic.members, int) + self.assertGreater(self.holic.members, 0) + self.assertIsInstance(self.monster.members, int) + self.assertGreater(self.monster.members, 0) + self.assertIsInstance(self.naruto.members, int) + self.assertGreater(self.naruto.members, 0) + self.assertIsInstance(self.tomoyo_after.members, int) + self.assertGreater(self.tomoyo_after.members, 0) + self.assertIsInstance(self.judos.members, int) + self.assertGreater(self.judos.members, 0) + + def testFavorites(self): + self.assertIsInstance(self.holic.favorites, int) + self.assertGreater(self.holic.favorites, 0) + self.assertIsInstance(self.monster.favorites, int) + self.assertGreater(self.monster.favorites, 0) + self.assertIsInstance(self.naruto.favorites, int) + self.assertGreater(self.naruto.favorites, 0) + self.assertIsInstance(self.tomoyo_after.favorites, int) + self.assertGreater(self.tomoyo_after.favorites, 0) + self.assertIsInstance(self.judos.favorites, int) + self.assertGreater(self.judos.favorites, 0) + + def testPopularTags(self): + self.assertIsInstance(self.holic.popular_tags, dict) + self.assertGreater(self.holic.popular_tags, 0) + self.assertIn(self.supernatural_tag, self.holic.popular_tags) + self.assertGreater(self.holic.popular_tags[self.supernatural_tag], 269) + self.assertIsInstance(self.tomoyo_after.popular_tags, dict) + self.assertGreater(self.tomoyo_after.popular_tags, 0) + self.assertIn(self.romance_tag, self.tomoyo_after.popular_tags) + self.assertGreater(self.tomoyo_after.popular_tags[self.romance_tag], 57) + self.assertIsInstance(self.naruto.popular_tags, dict) + self.assertGreater(self.naruto.popular_tags, 0) + self.assertIn(self.action_tag, self.naruto.popular_tags) + self.assertGreater(self.naruto.popular_tags[self.action_tag], 561) + self.assertIsInstance(self.monster.popular_tags, dict) + self.assertGreater(self.monster.popular_tags, 0) + self.assertIn(self.mystery_tag, self.monster.popular_tags) + self.assertGreater(self.monster.popular_tags[self.mystery_tag], 105) + self.assertIsInstance(self.judos.popular_tags, dict) + self.assertEqual(len(self.judos.popular_tags), 0) + + def testSynopsis(self): + self.assertIsInstance(self.holic.synopsis, unicode) + self.assertGreater(self.holic.synopsis, 0) + self.assertIn(u'Watanuki', self.holic.synopsis) + self.assertIsInstance(self.monster.synopsis, unicode) + self.assertGreater(self.monster.synopsis, 0) + self.assertIn(u'Tenma', self.monster.synopsis) + self.assertIsInstance(self.naruto.synopsis, unicode) + self.assertGreater(self.naruto.synopsis, 0) + self.assertIn(u'Hokage', self.naruto.synopsis) + self.assertIsInstance(self.tomoyo_after.synopsis, unicode) + self.assertGreater(self.tomoyo_after.synopsis, 0) + self.assertIn(u'Clannad', self.tomoyo_after.synopsis) + self.assertIsInstance(self.judos.synopsis, unicode) + self.assertGreater(self.judos.synopsis, 0) + self.assertIn(u'hardcore', self.judos.synopsis) + + def testRelated(self): + self.assertIsInstance(self.holic.related, dict) + self.assertIn('Sequel', self.holic.related) + self.assertIn(self.holic_sequel, self.holic.related[u'Sequel']) + self.assertIsInstance(self.monster.related, dict) + self.assertIn('Side story', self.monster.related) + self.assertIn(self.monster_side_story, self.monster.related[u'Side story']) + + def testCharacters(self): + self.assertIsInstance(self.holic.characters, dict) + self.assertGreater(self.holic.characters, 0) + self.assertIn(self.doumeki, self.holic.characters) + self.assertEqual(self.holic.characters[self.doumeki]['role'], 'Main') + + self.assertIsInstance(self.monster.characters, dict) + self.assertGreater(self.monster.characters, 0) + self.assertIn(self.heinemann, self.monster.characters) + self.assertEqual(self.monster.characters[self.heinemann]['role'], 'Main') + + self.assertIsInstance(self.naruto.characters, dict) + self.assertGreater(self.naruto.characters, 0) + self.assertIn(self.ebizou, self.naruto.characters) + self.assertEqual(self.naruto.characters[self.ebizou]['role'], 'Supporting') + + self.assertIsInstance(self.tomoyo_after.characters, dict) + self.assertGreater(self.tomoyo_after.characters, 0) + self.assertIn(self.kanako, self.tomoyo_after.characters) + self.assertEqual(self.tomoyo_after.characters[self.kanako]['role'], 'Supporting') diff --git a/tests/media_list_tests.py b/tests/media_list_tests.py index ce47e2c..d3797c2 100644 --- a/tests/media_list_tests.py +++ b/tests/media_list_tests.py @@ -1,15 +1,16 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -from nose.tools import * +from unittest import TestCase import myanimelist.session import myanimelist.media_list -class testMediaListClass(object): - @classmethod - def setUpClass(self): - self.session = myanimelist.session.Session() - @raises(TypeError) - def testCannotInstantiateMediaList(self): - myanimelist.media_list.MediaList(self.session, "test_username") \ No newline at end of file +class testMediaListClass(TestCase): + @classmethod + def setUpClass(self): + self.session = myanimelist.session.Session() + + def testCannotInstantiateMediaList(self): + with self.assertRaises(TypeError): + myanimelist.media_list.MediaList(self.session, "test_username") diff --git a/tests/session_tests.py b/tests/session_tests.py index b3b240b..08b00c7 100644 --- a/tests/session_tests.py +++ b/tests/session_tests.py @@ -1,36 +1,40 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -from nose.tools import * +from unittest import TestCase import myanimelist.session import myanimelist.anime import os -class testSessionClass(object): - @classmethod - def setUpClass(self): - # see if our environment has credentials. - if 'MAL_USERNAME' and 'MAL_PASSWORD' in os.environ: - self.username = os.environ[u'MAL_USERNAME'] - self.password = os.environ[u'MAL_PASSWORD'] - else: - # rely on a flat textfile in project root. - with open(u'credentials.txt', 'r') as cred_file: - line = cred_file.read().strip().split(u'\n')[0] - self.username, self.password = line.strip().split(u',') - self.session = myanimelist.session.Session(self.username, self.password) - self.logged_in_session = myanimelist.session.Session(self.username, self.password).login() - self.fake_session = myanimelist.session.Session(u'no-username', 'no-password') - def testLoggedIn(self): - assert not self.fake_session.logged_in() - self.fake_session.login() - assert not self.fake_session.logged_in() - assert not self.session.logged_in() - assert self.logged_in_session.logged_in() - def testLogin(self): - assert not self.session.logged_in() - self.session.login() - assert self.session.logged_in() - def testAnime(self): - assert isinstance(self.session.anime(1), myanimelist.anime.Anime) \ No newline at end of file +class testSessionClass(TestCase): + @classmethod + def setUpClass(self): + # see if our environment has credentials. + if 'MAL_USERNAME' and 'MAL_PASSWORD' in os.environ: + self.username = os.environ[u'MAL_USERNAME'] + self.password = os.environ[u'MAL_PASSWORD'] + else: + # rely on a flat textfile in project root. + with open(u'credentials.txt', 'r') as cred_file: + line = cred_file.read().strip().split(u'\n')[0] + self.username, self.password = line.strip().split(u',') + + self.session = myanimelist.session.Session(self.username, self.password) + self.logged_in_session = myanimelist.session.Session(self.username, self.password).login() + self.fake_session = myanimelist.session.Session(u'no-username', 'no-password') + + def testLoggedIn(self): + self.assertFalse(self.fake_session.logged_in()) + self.fake_session.login() + self.assertFalse(self.fake_session.logged_in()) + self.assertFalse(self.session.logged_in()) + self.assertTrue(self.logged_in_session.logged_in()) + + def testLogin(self): + self.assertFalse(self.session.logged_in()) + self.session.login() + self.assertTrue(self.session.logged_in()) + + def testAnime(self): + self.assertIsInstance(self.session.anime(1), myanimelist.anime.Anime) diff --git a/tests/user_tests.py b/tests/user_tests.py index e92fc03..253f8bf 100644 --- a/tests/user_tests.py +++ b/tests/user_tests.py @@ -1,203 +1,276 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -from nose.tools import * +from unittest import TestCase import datetime import myanimelist.session import myanimelist.user -class testUserClass(object): - @classmethod - def setUpClass(self): - self.session = myanimelist.session.Session() - self.shal = self.session.user(u'shaldengeki') - self.gits = self.session.anime(467) - self.clannad_as = self.session.anime(4181) - self.tohsaka = self.session.character(498) - self.fsn = self.session.anime(356) - self.fujibayashi = self.session.character(4605) - self.clannad_movie = self.session.anime(1723) - self.fate_zero = self.session.anime(10087) - self.bebop = self.session.anime(1) - self.kanon = self.session.anime(1530) - self.fang_tan_club = self.session.club(9560) - self.satsuki_club = self.session.club(6246) - - self.ziron = self.session.user(u'Ziron') - self.seraph = self.session.user(u'seraphzero') - - self.mona = self.session.user(u'monausicaa') - self.megami = self.session.manga(446) - self.chobits = self.session.manga(107) - self.kugimiya = self.session.person(8) - self.kayano = self.session.person(10765) - - self.naruleach = self.session.user(u'Naruleach') - self.mal_rewrite_club = self.session.club(6498) - self.fantasy_anime_club = self.session.club(379) - - self.smooched = self.session.user(u'Smooched') - self.sao = self.session.anime(11757) - self.threger = self.session.user(u'threger') - - @raises(TypeError) - def testNoIDInvalidUser(self): - self.session.user() - - @raises(myanimelist.user.InvalidUserError) - def testNegativeInvalidUser(self): - self.session.user(-1) - - @raises(myanimelist.user.InvalidUserError) - def testFloatInvalidUser(self): - self.session.user(1.5) - - @raises(myanimelist.user.InvalidUserError) - def testNonExistentUser(self): - self.session.user(457384754).load() - - def testUserValid(self): - assert isinstance(self.shal, myanimelist.user.User) - - def testId(self): - assert self.shal.id == 64611 - assert self.mona.id == 244263 - - def testUsername(self): - assert self.shal.username == u'shaldengeki' - assert self.mona.username == u'monausicaa' - - def testPicture(self): - assert isinstance(self.shal.picture, unicode) and self.shal.picture == u'http://cdn.myanimelist.net/images/userimages/64611.jpg' - assert isinstance(self.mona.picture, unicode) - - def testFavoriteAnime(self): - assert isinstance(self.shal.favorite_anime, list) and len(self.shal.favorite_anime) > 0 - assert self.gits in self.shal.favorite_anime and self.clannad_as in self.shal.favorite_anime - assert isinstance(self.mona.favorite_anime, list) and len(self.mona.favorite_anime) > 0 - - def testFavoriteManga(self): - assert isinstance(self.shal.favorite_manga, list) and len(self.shal.favorite_manga) == 0 - assert isinstance(self.mona.favorite_manga, list) and len(self.mona.favorite_manga) > 0 - assert self.megami in self.mona.favorite_manga and self.chobits in self.mona.favorite_manga - - def testFavoriteCharacters(self): - assert isinstance(self.shal.favorite_characters, dict) and len(self.shal.favorite_characters) > 0 - assert self.tohsaka in self.shal.favorite_characters and self.fujibayashi in self.shal.favorite_characters - assert self.shal.favorite_characters[self.tohsaka] == self.fsn and self.shal.favorite_characters[self.fujibayashi] == self.clannad_movie - assert isinstance(self.mona.favorite_characters, dict) and len(self.mona.favorite_characters) > 0 - - def testFavoritePeople(self): - assert isinstance(self.shal.favorite_people, list) and len(self.shal.favorite_people) == 0 - assert isinstance(self.mona.favorite_people, list) and len(self.mona.favorite_people) > 0 - assert self.kugimiya in self.mona.favorite_people and self.kayano in self.mona.favorite_people - - def testLastOnline(self): - assert isinstance(self.shal.last_online, datetime.datetime) - assert isinstance(self.mona.last_online, datetime.datetime) - - def testGender(self): - assert self.shal.gender == u"Not specified" - assert self.mona.gender == u"Male" - - def testBirthday(self): - assert isinstance(self.shal.birthday, datetime.date) and self.shal.birthday == datetime.date(year=1989, month=11, day=5) - assert isinstance(self.mona.birthday, datetime.date) and self.mona.birthday == datetime.date(year=1991, month=8, day=11) - - def testLocation(self): - assert self.shal.location == u'Chicago, IL' - assert isinstance(self.mona.location, unicode) - - def testWebsite(self): - assert self.shal.website == u'llanim.us' - assert self.mona.website == None - - def testJoinDate(self): - assert isinstance(self.shal.join_date, datetime.date) and self.shal.join_date == datetime.date(year=2008, month=5, day=30) - assert isinstance(self.mona.join_date, datetime.date) and self.mona.join_date == datetime.date(year=2009, month=10, day=9) - - def testAccessRank(self): - assert self.shal.access_rank == u'Member' - assert self.mona.access_rank == u'Member' - assert self.naruleach.access_rank == u'Anime DB Moderator' - - def testAnimeListViews(self): - assert isinstance(self.shal.anime_list_views, int) and self.shal.anime_list_views >= 1767 - assert isinstance(self.mona.anime_list_views, int) and self.mona.anime_list_views >= 1969 - - def testMangaListViews(self): - assert isinstance(self.shal.manga_list_views, int) and self.shal.manga_list_views >= 1037 - assert isinstance(self.mona.manga_list_views, int) and self.mona.manga_list_views >= 548 - - def testNumComments(self): - assert isinstance(self.shal.num_comments, int) and self.shal.num_comments >= 93 - assert isinstance(self.mona.num_comments, int) and self.mona.num_comments >= 30 - - def testNumForumPosts(self): - assert isinstance(self.shal.num_forum_posts, int) and self.shal.num_forum_posts >= 5 - assert isinstance(self.mona.num_forum_posts, int) and self.mona.num_forum_posts >= 1 - - def testLastListUpdates(self): - assert isinstance(self.shal.last_list_updates, dict) and len(self.shal.last_list_updates) > 0 - assert self.fate_zero in self.shal.last_list_updates and self.bebop in self.shal.last_list_updates - assert self.shal.last_list_updates[self.fate_zero][u'status'] == u'Watching' and self.shal.last_list_updates[self.fate_zero][u'episodes'] == 6 and self.shal.last_list_updates[self.fate_zero][u'total_episodes'] == 13 - assert isinstance(self.shal.last_list_updates[self.fate_zero][u'time'], datetime.datetime) and self.shal.last_list_updates[self.fate_zero][u'time'] == datetime.datetime(year=2014, month=9, day=5, hour=14, minute=1, second=0) - assert self.bebop in self.shal.last_list_updates and self.bebop in self.shal.last_list_updates - assert self.shal.last_list_updates[self.bebop][u'status'] == u'Completed' and self.shal.last_list_updates[self.bebop][u'episodes'] == 26 and self.shal.last_list_updates[self.bebop][u'total_episodes'] == 26 - assert isinstance(self.shal.last_list_updates[self.bebop][u'time'], datetime.datetime) and self.shal.last_list_updates[self.bebop][u'time'] == datetime.datetime(year=2012, month=8, day=20, hour=11, minute=56, second=0) - assert isinstance(self.mona.last_list_updates, dict) and len(self.mona.last_list_updates) > 0 - - def testAnimeStats(self): - assert isinstance(self.shal.anime_stats, dict) and len(self.shal.anime_stats) > 0 - assert self.shal.anime_stats[u'Time (Days)'] == 38.9 and self.shal.anime_stats[u'Total Entries'] == 146 - assert isinstance(self.mona.anime_stats, dict) and len(self.mona.anime_stats) > 0 - assert self.mona.anime_stats[u'Time (Days)'] >= 470 and self.mona.anime_stats[u'Total Entries'] >= 1822 - - def testMangaStats(self): - assert isinstance(self.shal.manga_stats, dict) and len(self.shal.manga_stats) > 0 - assert self.shal.manga_stats[u'Time (Days)'] == 1.0 and self.shal.manga_stats[u'Total Entries'] == 2 - assert isinstance(self.mona.manga_stats, dict) and len(self.mona.manga_stats) > 0 - assert self.mona.manga_stats[u'Time (Days)'] >= 69.4 and self.mona.manga_stats[u'Total Entries'] >= 186 - - def testAbout(self): - assert isinstance(self.shal.about, unicode) and len(self.shal.about) > 0 - assert u'retiree' in self.shal.about - assert isinstance(self.mona.about, unicode) and len(self.mona.about) > 0 - assert self.mona.about == u'Nothing yet' - - def testReviews(self): - assert isinstance(self.shal.reviews, dict) and len(self.shal.reviews) == 0 - - assert isinstance(self.smooched.reviews, dict) and len(self.smooched.reviews) >= 9 - assert self.sao in self.smooched.reviews - assert isinstance(self.smooched.reviews[self.sao][u'date'], datetime.date) and self.smooched.reviews[self.sao][u'date'] == datetime.date(year=2012, month=7, day=24) - assert self.smooched.reviews[self.sao][u'people_helped'] >= 259 and self.smooched.reviews[self.sao][u'people_total'] >= 644 - assert self.smooched.reviews[self.sao][u'media_consumed'] == 13 and self.smooched.reviews[self.sao][u'media_total'] == 25 - assert self.smooched.reviews[self.sao][u'rating'] == 6 - assert isinstance(self.smooched.reviews[self.sao][u'text'], unicode) and len(self.smooched.reviews[self.sao][u'text']) > 0 - - assert isinstance(self.threger.reviews, dict) and len(self.threger.reviews) == 0 - - def testRecommendations(self): - assert isinstance(self.shal.recommendations, dict) and len(self.shal.recommendations) > 0 - assert self.kanon in self.shal.recommendations and self.shal.recommendations[self.kanon][u'anime'] == self.clannad_as - assert isinstance(self.shal.recommendations[self.kanon][u'date'], datetime.date) and self.shal.recommendations[self.kanon][u'date'] == datetime.date(year=2009, month=3, day=13) - assert isinstance(self.shal.recommendations[self.kanon][u'text'], unicode) and len(self.shal.recommendations[self.kanon][u'text']) > 0 - assert isinstance(self.mona.recommendations, dict) and len(self.mona.recommendations) >= 0 - assert isinstance(self.naruleach.recommendations, dict) and len(self.naruleach.recommendations) >= 0 - assert isinstance(self.threger.recommendations, dict) and len(self.threger.recommendations) == 0 - - def testClubs(self): - assert isinstance(self.shal.clubs, list) and len(self.shal.clubs) == 7 - assert self.fang_tan_club in self.shal.clubs and self.satsuki_club in self.shal.clubs - assert isinstance(self.naruleach.clubs, list) and len(self.naruleach.clubs) >= 15 - assert self.mal_rewrite_club in self.naruleach.clubs and self.fantasy_anime_club in self.naruleach.clubs - assert isinstance(self.threger.clubs, list) and len(self.threger.clubs) == 0 - - def testFriends(self): - assert isinstance(self.shal.friends, dict) and len(self.shal.friends) >= 31 - assert self.ziron in self.shal.friends and isinstance(self.shal.friends[self.ziron][u'last_active'], datetime.datetime) - assert self.ziron in self.shal.friends and isinstance(self.shal.friends[self.ziron][u'last_active'], datetime.datetime) - assert self.seraph in self.shal.friends and isinstance(self.shal.friends[self.seraph][u'last_active'], datetime.datetime) and self.shal.friends[self.seraph][u'since'] == datetime.datetime(year=2012, month=10, day=13, hour=19, minute=31, second=0) - assert isinstance(self.mona.friends, dict) and len(self.mona.friends) >= 0 - assert isinstance(self.threger.friends, dict) and len(self.threger.friends) == 0 \ No newline at end of file + +class testUserClass(TestCase): + @classmethod + def setUpClass(self): + self.session = myanimelist.session.Session() + self.shal = self.session.user(u'shaldengeki') + self.gits = self.session.anime(467) + self.clannad_as = self.session.anime(4181) + self.tohsaka = self.session.character(498) + self.fsn = self.session.anime(356) + self.fujibayashi = self.session.character(4605) + self.clannad_movie = self.session.anime(1723) + self.fate_zero = self.session.anime(10087) + self.bebop = self.session.anime(1) + self.kanon = self.session.anime(1530) + self.fang_tan_club = self.session.club(9560) + self.satsuki_club = self.session.club(6246) + + self.ziron = self.session.user(u'Ziron') + self.seraph = self.session.user(u'seraphzero') + + self.mona = self.session.user(u'monausicaa') + self.megami = self.session.manga(446) + self.chobits = self.session.manga(107) + self.kugimiya = self.session.person(8) + self.kayano = self.session.person(10765) + + self.naruleach = self.session.user(u'Naruleach') + self.mal_rewrite_club = self.session.club(6498) + self.fantasy_anime_club = self.session.club(379) + + self.smooched = self.session.user(u'Smooched') + self.sao = self.session.anime(11757) + self.threger = self.session.user(u'threger') + + def testNoIDInvalidUser(self): + with self.assertRaises(TypeError): + self.session.user() + + def testNegativeInvalidUser(self): + with self.assertRaises(myanimelist.user.InvalidUserError): + self.session.user(-1) + + def testFloatInvalidUser(self): + with self.assertRaises(myanimelist.user.InvalidUserError): + self.session.user(1.5) + + def testNonExistentUser(self): + with self.assertRaises(myanimelist.user.InvalidUserError): + self.session.user(457384754).load() + + def testUserValid(self): + self.assertIsInstance(self.shal, myanimelist.user.User) + + def testId(self): + self.assertEqual(self.shal.id, 64611) + self.assertEqual(self.mona.id, 244263) + + def testUsername(self): + self.assertEqual(self.shal.username, u'shaldengeki') + self.assertEqual(self.mona.username, u'monausicaa') + + def testPicture(self): + self.assertIsInstance(self.shal.picture, unicode) + self.assertEqual(self.shal.picture, u'http://cdn.myanimelist.net/images/userimages/64611.jpg') + self.assertIsInstance(self.mona.picture, unicode) + + def testFavoriteAnime(self): + self.assertIsInstance(self.shal.favorite_anime, list) + self.assertGreater(len(self.shal.favorite_anime), 0) + self.assertIn(self.gits, self.shal.favorite_anime) + self.assertIn(self.clannad_as, self.shal.favorite_anime) + self.assertIsInstance(self.mona.favorite_anime, list) + self.assertGreater(len(self.mona.favorite_anime), 0) + + def testFavoriteManga(self): + self.assertIsInstance(self.shal.favorite_manga, list) + self.assertEqual(len(self.shal.favorite_manga), 0) + self.assertIsInstance(self.mona.favorite_manga, list) + self.assertGreater(len(self.mona.favorite_manga), 0) + self.assertIn(self.megami, self.mona.favorite_manga) + self.assertIn(self.chobits, self.mona.favorite_manga) + + def testFavoriteCharacters(self): + self.assertIsInstance(self.shal.favorite_characters, dict) + self.assertGreater(len(self.shal.favorite_characters), 0) + self.assertIn(self.tohsaka, self.shal.favorite_characters) + self.assertIn(self.fujibayashi, self.shal.favorite_characters) + self.assertEqual(self.shal.favorite_characters[self.tohsaka], self.fsn) + self.assertEqual(self.shal.favorite_characters[self.fujibayashi], self.clannad_movie) + self.assertIsInstance(self.mona.favorite_characters, dict) + self.assertGreater(len(self.mona.favorite_characters), 0) + + def testFavoritePeople(self): + self.assertIsInstance(self.shal.favorite_people, list) + self.assertEqual(len(self.shal.favorite_people), 0) + self.assertIsInstance(self.mona.favorite_people, list) + self.assertGreater(len(self.mona.favorite_people), 0) + self.assertIn(self.kugimiya, self.mona.favorite_people) + self.assertIn(self.kayano, self.mona.favorite_people) + + def testLastOnline(self): + self.assertIsInstance(self.shal.last_online, datetime.datetime) + self.assertIsInstance(self.mona.last_online, datetime.datetime) + + def testGender(self): + self.assertEqual(self.shal.gender, u"Not specified") + self.assertEqual(self.mona.gender, u"Male") + + def testBirthday(self): + self.assertIsInstance(self.shal.birthday, datetime.date) + self.assertEqual(self.shal.birthday, datetime.date(year=1989, month=11, day=5)) + self.assertIsInstance(self.mona.birthday, datetime.date) + self.assertEqual(self.mona.birthday, datetime.date(year=1991, month=8, day=11)) + + def testLocation(self): + self.assertEqual(self.shal.location, u'Chicago, IL') + self.assertIsInstance(self.mona.location, unicode) + + def testWebsite(self): + self.assertEqual(self.shal.website, u'llanim.us') + self.assertIsNone(self.mona.website) + + def testJoinDate(self): + self.assertIsInstance(self.shal.join_date, datetime.date) + self.assertEqual(self.shal.join_date, datetime.date(year=2008, month=5, day=30)) + self.assertIsInstance(self.mona.join_date, datetime.date) + self.assertEqual(self.mona.join_date, datetime.date(year=2009, month=10, day=9)) + + def testAccessRank(self): + self.assertEqual(self.shal.access_rank, u'Member') + self.assertEqual(self.mona.access_rank, u'Member') + self.assertEqual(self.naruleach.access_rank, u'Anime DB Moderator') + + def testAnimeListViews(self): + self.assertIsInstance(self.shal.anime_list_views, int) + self.assertGreaterEqual(self.shal.anime_list_views, 1767) + self.assertIsInstance(self.mona.anime_list_views, int) + self.assertGreaterEqual(self.mona.anime_list_views, 1969) + + def testMangaListViews(self): + self.assertIsInstance(self.shal.manga_list_views, int) + self.assertGreaterEqual(self.shal.manga_list_views, 1037) + self.assertIsInstance(self.mona.manga_list_views, int) + self.assertGreaterEqual(self.mona.manga_list_views, 548) + + def testNumComments(self): + self.assertIsInstance(self.shal.num_comments, int) + self.assertGreaterEqual(self.shal.num_comments, 93) + self.assertIsInstance(self.mona.num_comments, int) + self.assertGreaterEqual(self.mona.num_comments, 30) + + def testNumForumPosts(self): + self.assertIsInstance(self.shal.num_forum_posts, int) + self.assertGreaterEqual(self.shal.num_forum_posts, 5) + self.assertIsInstance(self.mona.num_forum_posts, int) + self.assertGreaterEqual(self.mona.num_forum_posts, 1) + + def testLastListUpdates(self): + self.assertIsInstance(self.shal.last_list_updates, dict) + self.assertGreater(len(self.shal.last_list_updates), 0) + self.assertIn(self.fate_zero, self.shal.last_list_updates) + self.assertIn(self.bebop, self.shal.last_list_updates) + self.assertEqual(self.shal.last_list_updates[self.fate_zero][u'status'], u'Watching') + self.assertEqual(self.shal.last_list_updates[self.fate_zero][u'episodes'], 6) + self.assertEqual(self.shal.last_list_updates[self.fate_zero][u'total_episodes'], 13) + self.assertIsInstance(self.shal.last_list_updates[self.fate_zero][u'time'], datetime.datetime) + self.assertEqual(self.shal.last_list_updates[self.fate_zero][u'time'], datetime.datetime(year=2014, month=9, day=5, hour=14, minute=1, second=0)) + self.assertIn(self.bebop, self.shal.last_list_updates) + self.assertIn(self.bebop, self.shal.last_list_updates) + self.assertEqual(self.shal.last_list_updates[self.bebop][u'status'], u'Completed') + self.assertEqual(self.shal.last_list_updates[self.bebop][u'episodes'], 26) + self.assertEqual(self.shal.last_list_updates[self.bebop][u'total_episodes'], 26) + self.assertIsInstance(self.shal.last_list_updates[self.bebop][u'time'], datetime.datetime) + self.assertEqual(self.shal.last_list_updates[self.bebop][u'time'], datetime.datetime(year=2012, month=8, day=20, hour=11, minute=56, second=0)) + self.assertIsInstance(self.mona.last_list_updates, dict) + self.assertGreater(len(self.mona.last_list_updates), 0) + + def testAnimeStats(self): + self.assertIsInstance(self.shal.anime_stats, dict) + self.assertGreater(len(self.shal.anime_stats), 0) + self.assertEqual(self.shal.anime_stats[u'Time (Days)'], 38.9) + self.assertEqual(self.shal.anime_stats[u'Total Entries'], 146) + self.assertIsInstance(self.mona.anime_stats, dict) + self.assertGreater(len(self.mona.anime_stats), 0) + self.assertGreaterEqual(self.mona.anime_stats[u'Time (Days)'], 470) + self.assertGreaterEqual(self.mona.anime_stats[u'Total Entries'], 1822) + + def testMangaStats(self): + self.assertIsInstance(self.shal.manga_stats, dict) + self.assertGreater(len(self.shal.manga_stats), 0) + self.assertEqual(self.shal.manga_stats[u'Time (Days)'], 1.0) + self.assertEqual(self.shal.manga_stats[u'Total Entries'], 2) + self.assertIsInstance(self.mona.manga_stats, dict) + self.assertGreater(len(self.mona.manga_stats), 0) + self.assertGreaterEqual(self.mona.manga_stats[u'Time (Days)'], 69.4) + self.assertGreaterEqual(self.mona.manga_stats[u'Total Entries'], 186) + + def testAbout(self): + self.assertIsInstance(self.shal.about, unicode) + self.assertGreater(len(self.shal.about), 0) + self.assertIn(u'retiree', self.shal.about) + self.assertIsInstance(self.mona.about, unicode) + self.assertGreater(len(self.mona.about), 0) + self.assertEqual(self.mona.about, u'Nothing yet') + + def testReviews(self): + self.assertIsInstance(self.shal.reviews, dict) + self.assertEqual(len(self.shal.reviews), 0) + + self.assertIsInstance(self.smooched.reviews, dict) + self.assertGreaterEqual(len(self.smooched.reviews), 9) + self.assertIn(self.sao, self.smooched.reviews) + self.assertIsInstance(self.smooched.reviews[self.sao][u'date'], datetime.date) + self.assertEqual(self.smooched.reviews[self.sao][u'date'], datetime.date(year=2012, month=7, day=24)) + self.assertGreaterEqual(self.smooched.reviews[self.sao][u'people_helped'], 259) + self.assertGreaterEqual(self.smooched.reviews[self.sao][u'people_total'], 644) + self.assertEqual(self.smooched.reviews[self.sao][u'media_consumed'], 13) + self.assertEqual(self.smooched.reviews[self.sao][u'media_total'], 25) + self.assertEqual(self.smooched.reviews[self.sao][u'rating'], 6) + self.assertIsInstance(self.smooched.reviews[self.sao][u'text'], unicode) + self.assertGreater(len(self.smooched.reviews[self.sao][u'text']), 0) + + self.assertIsInstance(self.threger.reviews, dict) + self.assertEqual(len(self.threger.reviews), 0) + + def testRecommendations(self): + self.assertIsInstance(self.shal.recommendations, dict) + self.assertGreater(len(self.shal.recommendations), 0) + self.assertIn(self.kanon, self.shal.recommendations) + self.assertEqual(self.shal.recommendations[self.kanon][u'anime'], self.clannad_as) + self.assertIsInstance(self.shal.recommendations[self.kanon][u'date'], datetime.date) + self.assertEqual(self.shal.recommendations[self.kanon][u'date'], datetime.date(year=2009, month=3, day=13)) + self.assertIsInstance(self.shal.recommendations[self.kanon][u'text'], unicode) + self.assertGreater(len(self.shal.recommendations[self.kanon][u'text']), 0) + self.assertIsInstance(self.mona.recommendations, dict) + self.assertGreaterEqual(len(self.mona.recommendations), 0) + self.assertIsInstance(self.naruleach.recommendations, dict) + self.assertGreaterEqual(len(self.naruleach.recommendations), 0) + self.assertIsInstance(self.threger.recommendations, dict) + self.assertEqual(len(self.threger.recommendations), 0) + + def testClubs(self): + self.assertIsInstance(self.shal.clubs, list) + self.assertEqual(len(self.shal.clubs), 7) + self.assertIn(self.fang_tan_club, self.shal.clubs) + self.assertIn(self.satsuki_club, self.shal.clubs) + self.assertIsInstance(self.naruleach.clubs, list) + self.assertGreaterEqual(len(self.naruleach.clubs), 15) + self.assertIn(self.mal_rewrite_club, self.naruleach.clubs) + self.assertIn(self.fantasy_anime_club, self.naruleach.clubs) + self.assertIsInstance(self.threger.clubs, list) + self.assertEqual(len(self.threger.clubs), 0) + + def testFriends(self): + self.assertIsInstance(self.shal.friends, dict) + self.assertGreaterEqual(len(self.shal.friends), 31) + self.assertIn(self.ziron, self.shal.friends) + self.assertIsInstance(self.shal.friends[self.ziron][u'last_active'], datetime.datetime) + self.assertIn(self.ziron, self.shal.friends) + self.assertIsInstance(self.shal.friends[self.ziron][u'last_active'], datetime.datetime) + self.assertIn(self.seraph, self.shal.friends) + self.assertIsInstance(self.shal.friends[self.seraph][u'last_active'],datetime.datetime) + self.assertEqual(self.shal.friends[self.seraph][u'since'], datetime.datetime(year=2012, month=10, day=13, hour=19, minute=31, second=0)) + self.assertIsInstance(self.mona.friends, dict) + self.assertGreaterEqual(len(self.mona.friends), 0) + self.assertIsInstance(self.threger.friends, dict) + self.assertEqual(len(self.threger.friends), 0)