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))
|