yading@7: """ yading@7: This module contains tha base classes for the Musixmatch API generated content: yading@7: yading@7: * :py:class:`musixmatch.artist.Artist` yading@7: * :py:class:`musixmatch.artist.Album` yading@7: * :py:class:`musixmatch.track.Track` yading@7: * :py:class:`musixmatch.lyrics.Lyrics` yading@7: * :py:class:`musixmatch.subtitle.Subtitle` yading@7: """ yading@7: import musixmatch yading@7: __license__ = musixmatch.__license__ yading@7: __author__ = musixmatch.__author__ yading@7: yading@7: from musixmatch import api yading@7: import pprint yading@7: yading@7: class Base(object): yading@7: """ yading@7: The very base (abstract) class of the musixmatch package. I want all yading@7: classes to implement :py:meth:`__str__` and :py:meth:`__repr__`. yading@7: yading@7: Casting an :py:class:`Base` into a :py:class:`str` returns a pretty printed yading@7: (maybe using :py:mod:`pprint`) string representing the object. yading@7: yading@7: Using :py:func:`repr` on an :py:class:`Base` returns an evaluable string yading@7: representing the instance, whenever it is reasonable. yading@7: yading@7: :py:class:`Base` instances are hashable. yading@7: """ yading@7: yading@7: @classmethod yading@7: def label(cls): yading@7: """ yading@7: Returns the label that should be used as keyword in the yading@7: :py:class:`musixmatch.api.JsonResponseMessage` body. yading@7: """ yading@7: return getattr(cls, '__label__', cls.__name__.lower()) yading@7: yading@7: @classmethod yading@7: def apiMethod(cls): yading@7: """ yading@7: Returns the :py:class:`musixmatch.api.Method` that should be used to yading@7: build the object. Defaults to *label.get* where *label* is the result yading@7: from :py:meth:`label` yading@7: """ yading@7: api_method = getattr(cls, '__api_method__', '%s.%s' % (cls.label(),'get')) yading@7: if not isinstance(api_method, api.Method): yading@7: api_method = api.Method(str(api_method)) yading@7: return api_method yading@7: yading@7: def __str__(self): yading@7: raise NotImplementedError yading@7: yading@7: def __repr__(self): yading@7: raise NotImplementedError yading@7: yading@7: class Item(Base, dict): yading@7: """ yading@7: This is the base class for any entity in musixmatch package. Even if yading@7: response messages may have XML format, the JSON representation will be the yading@7: main data format, so the :py:class:`dict` sounds like the best base class. yading@7: It fetches the item data by guessing the :py:class:`musixmatch.api.Method` yading@7: and building the query based on a given keyword argument. Positional yading@7: argument is meant to be used by collection classes. Use only keyword yading@7: arguments. yading@7: """ yading@7: __api_method__ = None yading@7: yading@7: def __init__(self, dictionary=None, **keywords): yading@7: if dictionary: yading@7: dict.update(self, dictionary) yading@7: elif keywords: yading@7: message = self.apiMethod()(**keywords) yading@7: dict.update(self, self.fromResponseMessage(message)) yading@7: yading@7: def __str__(self): yading@7: return pprint.pformat(dict(self),4,1) yading@7: yading@7: def __repr__(self): yading@7: return '%s(%r)' % (type(self).__name__, dict(self)) yading@7: yading@7: def __hash__(self): yading@7: return int(self['%s_id' % self.label()]) yading@7: yading@7: @classmethod yading@7: def fromResponseMessage(cls, message): yading@7: """ yading@7: Returns an object instance, built from a yading@7: :py:class:`musixmatch.api.ResponseMessage` yading@7: """ yading@7: if not message.status_code: yading@7: raise api.Error(str(message.status_code)) yading@7: return cls.fromDictionary(message['body'][cls.label()]) yading@7: yading@7: @classmethod yading@7: def fromDictionary(cls, dictionary, **keywords): yading@7: """ yading@7: Returns an object instance, built from a :py:class:`dict` yading@7: """ yading@7: item = cls() yading@7: dict.update(item, dictionary, **keywords) yading@7: return item yading@7: yading@7: class ItemsCollection(Base, list): yading@7: """ yading@7: This is the base class for collections of items, like search results, or yading@7: charts. It behaves like :py:class:`list`, but enforce new items to be yading@7: instance of appropriate class checking against :py:meth:`allowedin`. yading@7: """ yading@7: yading@7: __allowedin__ = Item yading@7: yading@7: def __init__(self, *items): yading@7: self.extend(items) yading@7: yading@7: def __repr__(self): yading@7: items = [ repr(i) for i in self ] yading@7: return '%s(%s)' % (type(self).__name__, ', '.join(items)) yading@7: yading@7: def __str__(self): yading@7: return list.__str__(self) yading@7: yading@7: def __add__(self, iterable): yading@7: collection = self.copy() yading@7: collection.extend(iterable) yading@7: return collection yading@7: yading@7: def __iadd__(self, iterable): yading@7: raise NotImplementedError yading@7: yading@7: def __mul__(self, by): yading@7: raise NotImplementedError yading@7: yading@7: def __imul__(self, by): yading@7: raise NotImplementedError yading@7: yading@7: def __setitem__(self, key, item): yading@7: raise NotImplementedError yading@7: yading@7: def __setslice__(self, *indices): yading@7: raise NotImplementedError yading@7: yading@7: def __getslice__(self, i=0, j=-1): yading@7: return self.__getitem__(slice(i,j)) yading@7: yading@7: def __getitem__(self, i): yading@7: if type(i) is int: yading@7: return list.__getitem__(self, i) yading@7: elif type(i) is slice: yading@7: collection = type(self)() yading@7: list.extend(collection, list.__getitem__(self, i)) yading@7: return collection yading@7: else: yading@7: raise TypeError, i yading@7: yading@7: def append(self, item): yading@7: self.insert(len(self), item) yading@7: yading@7: def extend(self, iterable): yading@7: for item in iterable: yading@7: self.append(item) yading@7: yading@7: def count(self, item): yading@7: return int(item in self) yading@7: yading@7: def copy(self): yading@7: """Returns a shallow copy of the collection.""" yading@7: collection = type(self)() yading@7: list.extend(collection, self) yading@7: return collection yading@7: yading@7: def index(self, item, *indices): yading@7: return list.index(self, item, *indices[:2]) yading@7: yading@7: def insert(self, key, item): yading@7: allowed = self.allowedin() yading@7: if not isinstance(item, allowed): yading@7: item = allowed.fromDictionary(item) yading@7: if not item in self: yading@7: list.insert(self, key, item) yading@7: yading@7: def paged(self, page_size=3): yading@7: """ yading@7: Returns self, paged by **page_size**. That is, a list of yading@7: sub-collections which contain "at most" **page_size** items. yading@7: """ yading@7: return [ self.page(i,page_size) yading@7: for i in range(self.pages(page_size)) ] yading@7: yading@7: def page(self, page_index, page_size=3): yading@7: """ yading@7: Returns a specific page, considering pages that contain "at most" yading@7: **page_size** items. yading@7: """ yading@7: page = type(self)() yading@7: i = page_index * page_size yading@7: list.extend(page, self[i:i+page_size]) yading@7: return page yading@7: yading@7: def pager(self, page_size=3): yading@7: """ yading@7: A generator of pages, considering pages that contain "at most" yading@7: **page_size** items. yading@7: """ yading@7: for i in xrange(self.pages(page_size)): yading@7: yield self.page(i, page_size) yading@7: yading@7: def pages(self,page_size=3): yading@7: """ yading@7: Returns the number of pages, considering pages that contain "at most" yading@7: **page_size** items. yading@7: """ yading@7: pages, more = divmod(len(self), page_size) yading@7: return more and pages + 1 or pages yading@7: yading@7: @classmethod yading@7: def fromResponseMessage(cls, message): yading@7: """ yading@7: Returns an object instance, built on a yading@7: :py:class:`musixmatch.api.ResponseMessage` yading@7: """ yading@7: if not message.status_code: yading@7: raise api.Error(str(message.status_code)) yading@7: list_label = cls.label() yading@7: item_label = cls.allowedin().label() yading@7: items = [ i[item_label] for i in message['body'][list_label] ] yading@7: return cls(*items) yading@7: yading@7: @classmethod yading@7: def allowedin(cls): yading@7: """ yading@7: Returns the allowed content class. Defaults to :py:class:`Item` yading@7: """ yading@7: return cls.__allowedin__ yading@7: yading@7: @classmethod yading@7: def label(cls): yading@7: item_name = cls.allowedin().label() yading@7: return '%s_list' % item_name yading@7: