annotate musixmatch-master/build/lib/musixmatch/api.py @ 13:844d341cf643 tip

Back up before ISMIR
author Yading Song <yading.song@eecs.qmul.ac.uk>
date Thu, 31 Oct 2013 13:17:06 +0000
parents 8c29444cb5fd
children
rev   line source
yading@7 1 """ This module define the base API classes.
yading@7 2 """
yading@7 3 import musixmatch
yading@7 4 from urllib import urlencode, urlopen
yading@7 5 from contextlib import contextmanager
yading@7 6 import os
yading@7 7 try:
yading@7 8 import json
yading@7 9 except ImportError:
yading@7 10 import simplejson as json
yading@7 11 try:
yading@7 12 from lxml import etree
yading@7 13 except ImportError:
yading@7 14 try:
yading@7 15 import xml.etree.cElementTree as etree
yading@7 16 except ImportError:
yading@7 17 try:
yading@7 18 import xml.etree.ElementTree as etree
yading@7 19 except ImportError:
yading@7 20 try:
yading@7 21 import cElementTree as etree
yading@7 22 except ImportError:
yading@7 23 import elementtree.ElementTree as etree
yading@7 24
yading@7 25 __license__ = musixmatch.__license__
yading@7 26 __author__ = musixmatch.__author__
yading@7 27
yading@7 28 class Error(Exception):
yading@7 29 """Base musiXmatch API error.
yading@7 30
yading@7 31 >>> import musixmatch
yading@7 32 >>> raise musixmatch.api.Error('Error message')
yading@7 33 Traceback (most recent call last):
yading@7 34 ...
yading@7 35 Error: Error message
yading@7 36 """
yading@7 37 def __str__(self):
yading@7 38 return ': '.join(map(str, self.args))
yading@7 39
yading@7 40 def __repr__(self):
yading@7 41 name = self.__class__.__name__
yading@7 42 return '%s%r' % (name, self.args)
yading@7 43
yading@7 44 class ResponseMessageError(Error):
yading@7 45 """Represents errors occurred while parsing the response messages."""
yading@7 46
yading@7 47 class ResponseStatusCode(int):
yading@7 48 """
yading@7 49 Represents response message status code. Casting a
yading@7 50 :py:class:`ResponseStatusCode` to :py:class:`str` returns the message
yading@7 51 associated with the status code:
yading@7 52
yading@7 53 >>> from musixmatch.api import ResponseStatusCode
yading@7 54 >>> str(ResponseStatusCode(200))
yading@7 55 'The request was successful.'
yading@7 56 >>> str(ResponseStatusCode(401))
yading@7 57 'Authentication failed, probably because of a bad API key.'
yading@7 58
yading@7 59 The status code to description mapping is:
yading@7 60
yading@7 61 +------+-----------------------------------------------------------+
yading@7 62 | Code | Description |
yading@7 63 +======+===========================================================+
yading@7 64 | 200 | The request was successful. |
yading@7 65 +------+-----------------------------------------------------------+
yading@7 66 | 400 | The request had bad syntax or was inherently |
yading@7 67 | | impossible to be satisfied. |
yading@7 68 +------+-----------------------------------------------------------+
yading@7 69 | 401 | Authentication failed, probably because of a bad API key. |
yading@7 70 +------+-----------------------------------------------------------+
yading@7 71 | 402 | A limit was reached, either you exceeded per hour |
yading@7 72 | | requests limits or your balance is insufficient. |
yading@7 73 +------+-----------------------------------------------------------+
yading@7 74 | 403 | You are not authorized to perform this operation or |
yading@7 75 | | the api version you're trying to use has been shut down. |
yading@7 76 +------+-----------------------------------------------------------+
yading@7 77 | 404 | Requested resource was not found. |
yading@7 78 +------+-----------------------------------------------------------+
yading@7 79 | 405 | Requested method was not found. |
yading@7 80 +------+-----------------------------------------------------------+
yading@7 81
yading@7 82 Any other status code will produce a default message:
yading@7 83
yading@7 84 >>> from musixmatch.api import ResponseStatusCode
yading@7 85 >>> str(ResponseStatusCode(666))
yading@7 86 'Unknown status code 666!'
yading@7 87
yading@7 88 Casting a :py:class:`ResponseStatusCode` to :py:class:`bool` returns True if
yading@7 89 status code is 200, False otherwise:
yading@7 90
yading@7 91 >>> from musixmatch.api import ResponseStatusCode
yading@7 92 >>> bool(ResponseStatusCode(200))
yading@7 93 True
yading@7 94 >>> bool(ResponseStatusCode(400))
yading@7 95 False
yading@7 96 >>> bool(ResponseStatusCode(666))
yading@7 97 False
yading@7 98
yading@7 99 """
yading@7 100 __status__ = {
yading@7 101 200: "The request was successful.",
yading@7 102 400: "The request had bad syntax or was inherently " + \
yading@7 103 "impossible to be satisfied.",
yading@7 104 401: "Authentication failed, probably because of a bad API key.",
yading@7 105 402: "A limit was reached, either you exceeded per hour " + \
yading@7 106 "requests limits or your balance is insufficient.",
yading@7 107 403: "You are not authorized to perform this operation or " + \
yading@7 108 "the api version you're trying to use has been shut down.",
yading@7 109 404: "Requested resource was not found.",
yading@7 110 405: "Requested method was not found.",
yading@7 111 }
yading@7 112
yading@7 113 def __str__(self):
yading@7 114 return self.__status__.get(self, 'Unknown status code %i!' % self)
yading@7 115
yading@7 116 def __repr__(self):
yading@7 117 return 'ResponseStatusCode(%i)' % self
yading@7 118
yading@7 119 def __nonzero__(self):
yading@7 120 return self == 200
yading@7 121
yading@7 122 class ResponseMessage(dict):
yading@7 123 """
yading@7 124 Abstract class which provides a base class for formatted response.
yading@7 125 """
yading@7 126 def __init__(self, response):
yading@7 127 raise NotImplementedError
yading@7 128
yading@7 129 @property
yading@7 130 def status_code(self):
yading@7 131 """
yading@7 132 Is the :py:class:`ResponseStatusCode` object representing the
yading@7 133 message status code.
yading@7 134
yading@7 135 :raises: :py:exc:`ValueError` if not set.
yading@7 136 """
yading@7 137 raise NotImplementedError
yading@7 138
yading@7 139 def __repr__(self):
yading@7 140 return "%s('...')" % type(self).__name__
yading@7 141
yading@7 142 class JsonResponseMessage(ResponseMessage, dict):
yading@7 143 """
yading@7 144 A :py:class:`ResponseMessage` subclass which behaves like a
yading@7 145 :py:class:`dict` to expose the Json structure contained in the response
yading@7 146 message. Parses the Json response message and build a proper python
yading@7 147 :py:class:`dict` containing all the information. Also, setup a
yading@7 148 :py:class:`ResponseStatusCode` by querying the :py:class:`dict` for the
yading@7 149 *['header']['status_code']* item.
yading@7 150 """
yading@7 151 def __init__(self, response):
yading@7 152 try:
yading@7 153 parsed = json.load(response)
yading@7 154 except Exception, e:
yading@7 155 raise ResponseMessageError(u'Invalid Json response message', e)
yading@7 156 self.update(parsed['message'])
yading@7 157
yading@7 158 def __str__(self):
yading@7 159 s = json.dumps({ 'message': self }, sort_keys=True, indent=4)
yading@7 160 return '\n'.join([l.rstrip() for l in s.splitlines()])
yading@7 161
yading@7 162 @property
yading@7 163 def status_code(self):
yading@7 164 """Overload :py:meth:`ResponseMessage.status_code`"""
yading@7 165 return ResponseStatusCode(self['header']['status_code'])
yading@7 166
yading@7 167 class XMLResponseMessage(ResponseMessage, etree.ElementTree):
yading@7 168 """
yading@7 169 A :py:class:`ResponseMessage` subclass which exposes
yading@7 170 :py:class:`ElementTree` methods to handle XML response
yading@7 171 messages. Parses the XML response message and build a
yading@7 172 :py:class:`ElementTree` instance. Also setup the a
yading@7 173 :py:class:`ResponseStatusCode` by querying the *status_code* tag content.
yading@7 174
yading@7 175 Casting a :py:class:`XMLResponseMessage` returns (actually re-builds) a
yading@7 176 pretty printed string representing the XML API response message.
yading@7 177 """
yading@7 178
yading@7 179 def __init__(self, response):
yading@7 180 etree.ElementTree.__init__(self, None, response)
yading@7 181
yading@7 182 def __str__(self):
yading@7 183 s = StringIO()
yading@7 184 self.wite(s)
yading@7 185 return s.getvalue()
yading@7 186
yading@7 187 @property
yading@7 188 def status_code(self):
yading@7 189 """Overload :py:meth:`ResponseMessage.status_code`"""
yading@7 190 return ResponseStatusCode(self.findtext('header/status_code'))
yading@7 191
yading@7 192 class QueryString(dict):
yading@7 193 """
yading@7 194 A class representing the keyword arguments to be used in HTTP requests as
yading@7 195 query string. Takes a :py:class:`dict` of keywords, and encode values
yading@7 196 using utf-8. Also, the query string is sorted by keyword name, so that its
yading@7 197 string representation is always the same, thus can be used in hashes.
yading@7 198
yading@7 199 Casting a :py:class:`QueryString` to :py:class:`str` returns the urlencoded
yading@7 200 query string:
yading@7 201
yading@7 202 >>> from musixmatch.api import QueryString
yading@7 203 >>> str(QueryString({ 'country': 'it', 'page': 1, 'page_size': 3 }))
yading@7 204 'country=it&page=1&page_size=3'
yading@7 205
yading@7 206 Using :py:func:`repr` on :py:class:`QueryString` returns an evaluable
yading@7 207 representation of the current instance, excluding apikey value:
yading@7 208
yading@7 209 >>> from musixmatch.api import QueryString
yading@7 210 >>> repr(QueryString({ 'country': 'it', 'page': 1, 'apikey': 'whatever'}))
yading@7 211 "QueryString({'country': 'it', 'page': '1'})"
yading@7 212 """
yading@7 213 def __init__(self, items=(), **keywords):
yading@7 214 dict.__init__(self, items, **keywords)
yading@7 215 for k in self:
yading@7 216 self[k] = str(self[k]).encode('utf-8')
yading@7 217
yading@7 218 def __str__(self):
yading@7 219 return urlencode(self)
yading@7 220
yading@7 221 def __repr__(self):
yading@7 222 query = self.copy()
yading@7 223 if 'apikey' in query:
yading@7 224 del query['apikey']
yading@7 225 return 'QueryString(%r)' % query
yading@7 226
yading@7 227 def __iter__(self):
yading@7 228 """
yading@7 229 Returns an iterator method which will yield keys sorted by name.
yading@7 230 Sorting allow the query strings to be used (reasonably) as caching key.
yading@7 231 """
yading@7 232 keys = dict.keys(self)
yading@7 233 keys.sort()
yading@7 234 for key in keys:
yading@7 235 yield key
yading@7 236
yading@7 237 def values(self):
yading@7 238 """Overloads :py:meth:`dict.values` using :py:meth:`__iter__`."""
yading@7 239 return tuple(self[k] for k in self)
yading@7 240
yading@7 241 def keys(self):
yading@7 242 """Overloads :py:meth:`dict.keys` using :py:meth:`__iter__`."""
yading@7 243 return tuple(k for k in self)
yading@7 244
yading@7 245 def items(self):
yading@7 246 """Overloads :py:meth:`dict.item` using :py:meth:`__iter__`."""
yading@7 247 return tuple((k, self[k]) for k in self)
yading@7 248
yading@7 249 def __hash__(self):
yading@7 250 return hash(str(self))
yading@7 251
yading@7 252 def __cmp__(self, other):
yading@7 253 return cmp(hash(self), hash(other))
yading@7 254
yading@7 255 class Method(str):
yading@7 256 """
yading@7 257 Utility class to build API methods name and call them as functions.
yading@7 258
yading@7 259 :py:class:`Method` has custom attribute access to build method names like
yading@7 260 those specified in the API. Each attribute access builds a new Method with
yading@7 261 a new name.
yading@7 262
yading@7 263 Calling a :py:class:`Method` as a function with keyword arguments,
yading@7 264 builds a :py:class:`Request`, runs it and returns the result. If **apikey**
yading@7 265 is undefined, environment variable **musixmatch_apikey** will be used. If
yading@7 266 **format** is undefined, environment variable **musixmatch_format** will be
yading@7 267 used. If **musixmatch_format** is undefined, jason format will be used.
yading@7 268
yading@7 269 >>> import musixmatch
yading@7 270 >>> artist = musixmatch.api.Method('artist')
yading@7 271 >>>
yading@7 272 >>> try:
yading@7 273 ... chart = artist.chart.get(country='it', page=1, page_size=3)
yading@7 274 ... except musixmatch.api.Error, e:
yading@7 275 ... pass
yading@7 276 """
yading@7 277 __separator__ = '.'
yading@7 278
yading@7 279 def __getattribute__(self, name):
yading@7 280 if name.startswith('_'):
yading@7 281 return super(Method, self).__getattribute__(name)
yading@7 282 else:
yading@7 283 return Method(self.__separator__.join([self, name]))
yading@7 284
yading@7 285 def __call__ (self, apikey=None, format=None, **query):
yading@7 286 query['apikey'] = apikey or musixmatch.apikey
yading@7 287 query['format'] = format or musixmatch.format
yading@7 288 return Request(self, query).response
yading@7 289
yading@7 290 def __repr__(self):
yading@7 291 return "Method('%s')" % self
yading@7 292
yading@7 293 class Request(object):
yading@7 294 """
yading@7 295 This is the main API class. Given a :py:class:`Method` or a method name, a
yading@7 296 :py:class:`QueryString` or a :py:class:`dict`, it can build the API query
yading@7 297 URL, run the request and return the response either as a string or as a
yading@7 298 :py:class:`ResponseMessage` subclass. Assuming the default web services
yading@7 299 location, this class try to build a proper request:
yading@7 300
yading@7 301 >>> from musixmatch.api import Request, Method, QueryString
yading@7 302 >>> method_name = 'artist.chart.get'
yading@7 303 >>> method = Method(method_name)
yading@7 304 >>> keywords = { 'country': 'it', 'page': 1, 'page_size': 3 }
yading@7 305 >>> query_string = QueryString(keywords)
yading@7 306 >>>
yading@7 307 >>> r1 = Request(method_name, keywords)
yading@7 308 >>> r2 = Request(method_name, **keywords)
yading@7 309 >>> r3 = Request(method_name, query_string)
yading@7 310 >>> r4 = Request(method, keywords)
yading@7 311 >>> r5 = Request(method, **keywords)
yading@7 312 >>> r6 = Request(method, query_string)
yading@7 313
yading@7 314 If **method** is string, try to cast it into a :py:class:`Method`. If
yading@7 315 **query_string** is a :py:class:`dict`, try to cast it into a
yading@7 316 :py:class:`QueryString`. If **query_string** is not specified, try to
yading@7 317 use **keywords** arguments as a :py:class:`dict` and cast it into a
yading@7 318 :py:class:`QueryString`.
yading@7 319
yading@7 320 Turning the :py:class:`Request` into a :py:class:`str` returns the URL
yading@7 321 representing the API request:
yading@7 322
yading@7 323 >>> str(Request('artist.chart.get', { 'country': 'it', 'page': 1 }))
yading@7 324 'http://api.musixmatch.com/ws/1.1/artist.chart.get?country=it&page=1'
yading@7 325 """
yading@7 326 def __init__ (self, api_method, query=(), **keywords):
yading@7 327 self.__api_method = isinstance(api_method, Method) and \
yading@7 328 api_method or Method(api_method)
yading@7 329 self.__query_string = isinstance(query, QueryString) and \
yading@7 330 query or QueryString(query)
yading@7 331 self.__query_string.update(keywords)
yading@7 332 self.__response = None
yading@7 333
yading@7 334 @property
yading@7 335 def api_method(self):
yading@7 336 """The :py:class:`Method` instance."""
yading@7 337 return self.__api_method
yading@7 338
yading@7 339 @property
yading@7 340 def query_string(self):
yading@7 341 """The :py:class:`QueryString` instance."""
yading@7 342 return self.__query_string
yading@7 343
yading@7 344 @contextmanager
yading@7 345 def _received(self):
yading@7 346 """A context manager to handle url opening"""
yading@7 347 try:
yading@7 348 response = urlopen(str(self))
yading@7 349 yield response
yading@7 350 finally:
yading@7 351 response.close()
yading@7 352
yading@7 353 @property
yading@7 354 def response(self):
yading@7 355 """
yading@7 356 The :py:class:`ResponseMessage` based on the **format** key in the
yading@7 357 :py:class:`QueryString`.
yading@7 358 """
yading@7 359 if self.__response is None:
yading@7 360
yading@7 361 format = self.query_string.get('format')
yading@7 362 ResponseMessageClass = {
yading@7 363 'json': JsonResponseMessage,
yading@7 364 'xml': XMLResponseMessage,
yading@7 365 }.get(format, None)
yading@7 366
yading@7 367 if not ResponseMessageClass:
yading@7 368 raise ResponseMessageError("Unsupported format `%s'" % format)
yading@7 369
yading@7 370 with self._received() as response:
yading@7 371 self.__response = ResponseMessageClass(response)
yading@7 372
yading@7 373 return self.__response
yading@7 374
yading@7 375 def __repr__(self):
yading@7 376 return 'Request(%r, %r)' % (self.api_method, self.query_string)
yading@7 377
yading@7 378 def __str__(self):
yading@7 379 return '%(ws_location)s/%(api_method)s?%(query_string)s' % {
yading@7 380 'ws_location': musixmatch.ws.location,
yading@7 381 'api_method': self.api_method,
yading@7 382 'query_string': self.query_string
yading@7 383 }
yading@7 384
yading@7 385 def __hash__(self):
yading@7 386 return hash(str(self))
yading@7 387
yading@7 388 def __cmp__(self, other):
yading@7 389 return cmp(hash(self), hash(other))