yading@7: """ This module define the base API classes. yading@7: """ yading@7: import musixmatch yading@7: from urllib import urlencode, urlopen yading@7: from contextlib import contextmanager yading@7: import os yading@7: try: yading@7: import json yading@7: except ImportError: yading@7: import simplejson as json yading@7: try: yading@7: from lxml import etree yading@7: except ImportError: yading@7: try: yading@7: import xml.etree.cElementTree as etree yading@7: except ImportError: yading@7: try: yading@7: import xml.etree.ElementTree as etree yading@7: except ImportError: yading@7: try: yading@7: import cElementTree as etree yading@7: except ImportError: yading@7: import elementtree.ElementTree as etree yading@7: yading@7: __license__ = musixmatch.__license__ yading@7: __author__ = musixmatch.__author__ yading@7: yading@7: class Error(Exception): yading@7: """Base musiXmatch API error. yading@7: yading@7: >>> import musixmatch yading@7: >>> raise musixmatch.api.Error('Error message') yading@7: Traceback (most recent call last): yading@7: ... yading@7: Error: Error message yading@7: """ yading@7: def __str__(self): yading@7: return ': '.join(map(str, self.args)) yading@7: yading@7: def __repr__(self): yading@7: name = self.__class__.__name__ yading@7: return '%s%r' % (name, self.args) yading@7: yading@7: class ResponseMessageError(Error): yading@7: """Represents errors occurred while parsing the response messages.""" yading@7: yading@7: class ResponseStatusCode(int): yading@7: """ yading@7: Represents response message status code. Casting a yading@7: :py:class:`ResponseStatusCode` to :py:class:`str` returns the message yading@7: associated with the status code: yading@7: yading@7: >>> from musixmatch.api import ResponseStatusCode yading@7: >>> str(ResponseStatusCode(200)) yading@7: 'The request was successful.' yading@7: >>> str(ResponseStatusCode(401)) yading@7: 'Authentication failed, probably because of a bad API key.' yading@7: yading@7: The status code to description mapping is: yading@7: yading@7: +------+-----------------------------------------------------------+ yading@7: | Code | Description | yading@7: +======+===========================================================+ yading@7: | 200 | The request was successful. | yading@7: +------+-----------------------------------------------------------+ yading@7: | 400 | The request had bad syntax or was inherently | yading@7: | | impossible to be satisfied. | yading@7: +------+-----------------------------------------------------------+ yading@7: | 401 | Authentication failed, probably because of a bad API key. | yading@7: +------+-----------------------------------------------------------+ yading@7: | 402 | A limit was reached, either you exceeded per hour | yading@7: | | requests limits or your balance is insufficient. | yading@7: +------+-----------------------------------------------------------+ yading@7: | 403 | You are not authorized to perform this operation or | yading@7: | | the api version you're trying to use has been shut down. | yading@7: +------+-----------------------------------------------------------+ yading@7: | 404 | Requested resource was not found. | yading@7: +------+-----------------------------------------------------------+ yading@7: | 405 | Requested method was not found. | yading@7: +------+-----------------------------------------------------------+ yading@7: yading@7: Any other status code will produce a default message: yading@7: yading@7: >>> from musixmatch.api import ResponseStatusCode yading@7: >>> str(ResponseStatusCode(666)) yading@7: 'Unknown status code 666!' yading@7: yading@7: Casting a :py:class:`ResponseStatusCode` to :py:class:`bool` returns True if yading@7: status code is 200, False otherwise: yading@7: yading@7: >>> from musixmatch.api import ResponseStatusCode yading@7: >>> bool(ResponseStatusCode(200)) yading@7: True yading@7: >>> bool(ResponseStatusCode(400)) yading@7: False yading@7: >>> bool(ResponseStatusCode(666)) yading@7: False yading@7: yading@7: """ yading@7: __status__ = { yading@7: 200: "The request was successful.", yading@7: 400: "The request had bad syntax or was inherently " + \ yading@7: "impossible to be satisfied.", yading@7: 401: "Authentication failed, probably because of a bad API key.", yading@7: 402: "A limit was reached, either you exceeded per hour " + \ yading@7: "requests limits or your balance is insufficient.", yading@7: 403: "You are not authorized to perform this operation or " + \ yading@7: "the api version you're trying to use has been shut down.", yading@7: 404: "Requested resource was not found.", yading@7: 405: "Requested method was not found.", yading@7: } yading@7: yading@7: def __str__(self): yading@7: return self.__status__.get(self, 'Unknown status code %i!' % self) yading@7: yading@7: def __repr__(self): yading@7: return 'ResponseStatusCode(%i)' % self yading@7: yading@7: def __nonzero__(self): yading@7: return self == 200 yading@7: yading@7: class ResponseMessage(dict): yading@7: """ yading@7: Abstract class which provides a base class for formatted response. yading@7: """ yading@7: def __init__(self, response): yading@7: raise NotImplementedError yading@7: yading@7: @property yading@7: def status_code(self): yading@7: """ yading@7: Is the :py:class:`ResponseStatusCode` object representing the yading@7: message status code. yading@7: yading@7: :raises: :py:exc:`ValueError` if not set. yading@7: """ yading@7: raise NotImplementedError yading@7: yading@7: def __repr__(self): yading@7: return "%s('...')" % type(self).__name__ yading@7: yading@7: class JsonResponseMessage(ResponseMessage, dict): yading@7: """ yading@7: A :py:class:`ResponseMessage` subclass which behaves like a yading@7: :py:class:`dict` to expose the Json structure contained in the response yading@7: message. Parses the Json response message and build a proper python yading@7: :py:class:`dict` containing all the information. Also, setup a yading@7: :py:class:`ResponseStatusCode` by querying the :py:class:`dict` for the yading@7: *['header']['status_code']* item. yading@7: """ yading@7: def __init__(self, response): yading@7: try: yading@7: parsed = json.load(response) yading@7: except Exception, e: yading@7: raise ResponseMessageError(u'Invalid Json response message', e) yading@7: self.update(parsed['message']) yading@7: yading@7: def __str__(self): yading@7: s = json.dumps({ 'message': self }, sort_keys=True, indent=4) yading@7: return '\n'.join([l.rstrip() for l in s.splitlines()]) yading@7: yading@7: @property yading@7: def status_code(self): yading@7: """Overload :py:meth:`ResponseMessage.status_code`""" yading@7: return ResponseStatusCode(self['header']['status_code']) yading@7: yading@7: class XMLResponseMessage(ResponseMessage, etree.ElementTree): yading@7: """ yading@7: A :py:class:`ResponseMessage` subclass which exposes yading@7: :py:class:`ElementTree` methods to handle XML response yading@7: messages. Parses the XML response message and build a yading@7: :py:class:`ElementTree` instance. Also setup the a yading@7: :py:class:`ResponseStatusCode` by querying the *status_code* tag content. yading@7: yading@7: Casting a :py:class:`XMLResponseMessage` returns (actually re-builds) a yading@7: pretty printed string representing the XML API response message. yading@7: """ yading@7: yading@7: def __init__(self, response): yading@7: etree.ElementTree.__init__(self, None, response) yading@7: yading@7: def __str__(self): yading@7: s = StringIO() yading@7: self.wite(s) yading@7: return s.getvalue() yading@7: yading@7: @property yading@7: def status_code(self): yading@7: """Overload :py:meth:`ResponseMessage.status_code`""" yading@7: return ResponseStatusCode(self.findtext('header/status_code')) yading@7: yading@7: class QueryString(dict): yading@7: """ yading@7: A class representing the keyword arguments to be used in HTTP requests as yading@7: query string. Takes a :py:class:`dict` of keywords, and encode values yading@7: using utf-8. Also, the query string is sorted by keyword name, so that its yading@7: string representation is always the same, thus can be used in hashes. yading@7: yading@7: Casting a :py:class:`QueryString` to :py:class:`str` returns the urlencoded yading@7: query string: yading@7: yading@7: >>> from musixmatch.api import QueryString yading@7: >>> str(QueryString({ 'country': 'it', 'page': 1, 'page_size': 3 })) yading@7: 'country=it&page=1&page_size=3' yading@7: yading@7: Using :py:func:`repr` on :py:class:`QueryString` returns an evaluable yading@7: representation of the current instance, excluding apikey value: yading@7: yading@7: >>> from musixmatch.api import QueryString yading@7: >>> repr(QueryString({ 'country': 'it', 'page': 1, 'apikey': 'whatever'})) yading@7: "QueryString({'country': 'it', 'page': '1'})" yading@7: """ yading@7: def __init__(self, items=(), **keywords): yading@7: dict.__init__(self, items, **keywords) yading@7: for k in self: yading@7: self[k] = str(self[k]).encode('utf-8') yading@7: yading@7: def __str__(self): yading@7: return urlencode(self) yading@7: yading@7: def __repr__(self): yading@7: query = self.copy() yading@7: if 'apikey' in query: yading@7: del query['apikey'] yading@7: return 'QueryString(%r)' % query yading@7: yading@7: def __iter__(self): yading@7: """ yading@7: Returns an iterator method which will yield keys sorted by name. yading@7: Sorting allow the query strings to be used (reasonably) as caching key. yading@7: """ yading@7: keys = dict.keys(self) yading@7: keys.sort() yading@7: for key in keys: yading@7: yield key yading@7: yading@7: def values(self): yading@7: """Overloads :py:meth:`dict.values` using :py:meth:`__iter__`.""" yading@7: return tuple(self[k] for k in self) yading@7: yading@7: def keys(self): yading@7: """Overloads :py:meth:`dict.keys` using :py:meth:`__iter__`.""" yading@7: return tuple(k for k in self) yading@7: yading@7: def items(self): yading@7: """Overloads :py:meth:`dict.item` using :py:meth:`__iter__`.""" yading@7: return tuple((k, self[k]) for k in self) yading@7: yading@7: def __hash__(self): yading@7: return hash(str(self)) yading@7: yading@7: def __cmp__(self, other): yading@7: return cmp(hash(self), hash(other)) yading@7: yading@7: class Method(str): yading@7: """ yading@7: Utility class to build API methods name and call them as functions. yading@7: yading@7: :py:class:`Method` has custom attribute access to build method names like yading@7: those specified in the API. Each attribute access builds a new Method with yading@7: a new name. yading@7: yading@7: Calling a :py:class:`Method` as a function with keyword arguments, yading@7: builds a :py:class:`Request`, runs it and returns the result. If **apikey** yading@7: is undefined, environment variable **musixmatch_apikey** will be used. If yading@7: **format** is undefined, environment variable **musixmatch_format** will be yading@7: used. If **musixmatch_format** is undefined, jason format will be used. yading@7: yading@7: >>> import musixmatch yading@7: >>> artist = musixmatch.api.Method('artist') yading@7: >>> yading@7: >>> try: yading@7: ... chart = artist.chart.get(country='it', page=1, page_size=3) yading@7: ... except musixmatch.api.Error, e: yading@7: ... pass yading@7: """ yading@7: __separator__ = '.' yading@7: yading@7: def __getattribute__(self, name): yading@7: if name.startswith('_'): yading@7: return super(Method, self).__getattribute__(name) yading@7: else: yading@7: return Method(self.__separator__.join([self, name])) yading@7: yading@7: def __call__ (self, apikey=None, format=None, **query): yading@7: query['apikey'] = apikey or musixmatch.apikey yading@7: query['format'] = format or musixmatch.format yading@7: return Request(self, query).response yading@7: yading@7: def __repr__(self): yading@7: return "Method('%s')" % self yading@7: yading@7: class Request(object): yading@7: """ yading@7: This is the main API class. Given a :py:class:`Method` or a method name, a yading@7: :py:class:`QueryString` or a :py:class:`dict`, it can build the API query yading@7: URL, run the request and return the response either as a string or as a yading@7: :py:class:`ResponseMessage` subclass. Assuming the default web services yading@7: location, this class try to build a proper request: yading@7: yading@7: >>> from musixmatch.api import Request, Method, QueryString yading@7: >>> method_name = 'artist.chart.get' yading@7: >>> method = Method(method_name) yading@7: >>> keywords = { 'country': 'it', 'page': 1, 'page_size': 3 } yading@7: >>> query_string = QueryString(keywords) yading@7: >>> yading@7: >>> r1 = Request(method_name, keywords) yading@7: >>> r2 = Request(method_name, **keywords) yading@7: >>> r3 = Request(method_name, query_string) yading@7: >>> r4 = Request(method, keywords) yading@7: >>> r5 = Request(method, **keywords) yading@7: >>> r6 = Request(method, query_string) yading@7: yading@7: If **method** is string, try to cast it into a :py:class:`Method`. If yading@7: **query_string** is a :py:class:`dict`, try to cast it into a yading@7: :py:class:`QueryString`. If **query_string** is not specified, try to yading@7: use **keywords** arguments as a :py:class:`dict` and cast it into a yading@7: :py:class:`QueryString`. yading@7: yading@7: Turning the :py:class:`Request` into a :py:class:`str` returns the URL yading@7: representing the API request: yading@7: yading@7: >>> str(Request('artist.chart.get', { 'country': 'it', 'page': 1 })) yading@7: 'http://api.musixmatch.com/ws/1.1/artist.chart.get?country=it&page=1' yading@7: """ yading@7: def __init__ (self, api_method, query=(), **keywords): yading@7: self.__api_method = isinstance(api_method, Method) and \ yading@7: api_method or Method(api_method) yading@7: self.__query_string = isinstance(query, QueryString) and \ yading@7: query or QueryString(query) yading@7: self.__query_string.update(keywords) yading@7: self.__response = None yading@7: yading@7: @property yading@7: def api_method(self): yading@7: """The :py:class:`Method` instance.""" yading@7: return self.__api_method yading@7: yading@7: @property yading@7: def query_string(self): yading@7: """The :py:class:`QueryString` instance.""" yading@7: return self.__query_string yading@7: yading@7: @contextmanager yading@7: def _received(self): yading@7: """A context manager to handle url opening""" yading@7: try: yading@7: response = urlopen(str(self)) yading@7: yield response yading@7: finally: yading@7: response.close() yading@7: yading@7: @property yading@7: def response(self): yading@7: """ yading@7: The :py:class:`ResponseMessage` based on the **format** key in the yading@7: :py:class:`QueryString`. yading@7: """ yading@7: if self.__response is None: yading@7: yading@7: format = self.query_string.get('format') yading@7: ResponseMessageClass = { yading@7: 'json': JsonResponseMessage, yading@7: 'xml': XMLResponseMessage, yading@7: }.get(format, None) yading@7: yading@7: if not ResponseMessageClass: yading@7: raise ResponseMessageError("Unsupported format `%s'" % format) yading@7: yading@7: with self._received() as response: yading@7: self.__response = ResponseMessageClass(response) yading@7: yading@7: return self.__response yading@7: yading@7: def __repr__(self): yading@7: return 'Request(%r, %r)' % (self.api_method, self.query_string) yading@7: yading@7: def __str__(self): yading@7: return '%(ws_location)s/%(api_method)s?%(query_string)s' % { yading@7: 'ws_location': musixmatch.ws.location, yading@7: 'api_method': self.api_method, yading@7: 'query_string': self.query_string yading@7: } yading@7: yading@7: def __hash__(self): yading@7: return hash(str(self)) yading@7: yading@7: def __cmp__(self, other): yading@7: return cmp(hash(self), hash(other))